chenlu
8 小时以前 33dbc6a161554f3a897f9e9273feb4f2c1b47381
north-glass-erp/northglass-erp/src/views/pp/glassOptimize/page/OptimizationRect.vue
@@ -1,16 +1,80 @@
<template>
  <div style="display: flex; height: 90vh;">
    <!-- Sidebar -->
    <div class="sidebar" style="width: 200px; background: #f4f4f4; padding: 10px;">
      <div
        v-for="(layout, layoutIndex) in layouts"
        :key="layoutIndex"
        class="sidebar-item"
        @click="selectLayout(layoutIndex)"
        :class="{ 'selected': selectedLayoutIndex === layoutIndex }"
      >
        {{ layout.width }} × {{ layout.height }} × {{ layout.quantity  }}
    <div class="sidebar" style="width: 200px; background: #f4f4f4; padding: 10px; height: 93%; overflow-y: auto; max-height: 90vh; border-radius: 8px;">
      <div class="folder">
        <div
          class="folder-header"          style="padding: 8px; background: #e0e0e0; margin-bottom: 5px; border-radius: 4px; user-select: none; display: flex; justify-content: space-between; align-items: center;"
        >
          <span @click="toggleFolder('pending')" style="flex: 1; cursor: pointer;">待切割原片</span>
          <button
            @click="toggleFolder('pending')"            style="background: none; border: none; cursor: pointer; font-size: 14px; padding: 2px 5px; border-radius: 3px;"
            :title="openFolders.pending ? '收起' : '展开'"
          >
            <el-icon v-if="openFolders.pending"><ArrowUp /></el-icon>
            <el-icon v-else><ArrowDown /></el-icon>
          </button>
        </div>
        <div v-show="openFolders.pending" class="folder-content" style="padding-left: 15px;">
          <div
            v-for="(layout, layoutIndex) in layouts"
            :key="layoutIndex"
            class="sidebar-item"
            @click="selectLayout(layoutIndex)"
            :class="{ 'selected': selectedLayoutIndex === layoutIndex }"            style="margin-bottom: 5px;"
          >
            {{ layout.realWidth }} × {{ layout.realHeight }} × {{ layout.quantity }}
          </div>
        </div>
      </div>
<!-- 待补片队列文件夹 -->
      <div class="folder">
        <div
          class="folder-header"          style="padding: 8px; background: #e0e0e0; margin-bottom: 5px; border-radius: 4px; user-select: none; display: flex; justify-content: space-between; align-items: center;"
        >
          <span @click="toggleFolder('patchQueue')" style="flex: 1; cursor: pointer;">待补片队列</span>
          <button
            @click="toggleFolder('patchQueue')"            style="background: none; border: none; cursor: pointer; font-size: 14px; padding: 2px 5px; border-radius: 3px;"
            :title="openFolders.patchQueue ? '收起' : '展开'"
          >
            <el-icon v-if="openFolders.patchQueue"><ArrowUp /></el-icon>
            <el-icon v-else><ArrowDown /></el-icon>
          </button>
        </div>
        <div v-show="openFolders.patchQueue" class="folder-content" style="padding-left: 15px;">
          <div style="padding: 10px; color: #666; font-style: italic;">
            暂无补片任务
          </div>
        </div>
      </div>
      <!-- 添加自定义尺寸文件夹 -->
      <div class="folder">
        <div
          class="folder-header"          style="padding: 8px; background: #e0e0e0; margin-bottom: 5px; border-radius: 4px; user-select: none; display: flex; justify-content: space-between; align-items: center;"
        >
          <span @click="toggleFolder('customSize')" style="flex: 1; cursor: pointer;">添加自定义尺寸</span>
          <button
            @click="toggleFolder('customSize')"            style="background: none; border: none; cursor: pointer; font-size: 14px; padding: 2px 5px; border-radius: 3px;"
            :title="openFolders.customSize ? '收起' : '展开'"
          >
            <el-icon v-if="openFolders.customSize"><ArrowUp /></el-icon>
            <el-icon v-else><ArrowDown /></el-icon>
          </button>
        </div>
        <div v-show="openFolders.customSize" class="folder-content" style="padding-left: 15px;">
          <div style="padding: 10px;">
            <button
              @click="showAddCustomSizeDialog"              style="width: 100%; padding: 8px; background: #409eff; color: white; border: none; border-radius: 4px; cursor: pointer;"
            >
              + 添加自定义尺寸
            </button>
          </div>
        </div>
      </div>
    </div>
    <!-- Main Layout Panel -->
@@ -27,6 +91,7 @@
        </div>
        <!-- Layout Container -->
        <div class="layout-container" :style="layoutContainerStyle1(layoutIndex)">
        <div class="layout-container" :style="layoutContainerStyle(layoutIndex)">
          <!-- 灰色矩形 -->
          <div
@@ -34,7 +99,7 @@
            :key="`gray-${rectIndex}`"
            :ref="(el) => { if (el) rectsElements[layoutIndex + '-' + rectIndex] = el }"
            class="layout-glassDetail"
            :style="rectStyle(glassDetail, layoutIndex)"
            :style="rectStyle1(glassDetail, layoutIndex)"
            @contextmenu.prevent="handleGrayRectRightClick(layoutIndex, rectIndex,glassDetail)"
          >
<!--            <div class="glassDetail-content">
@@ -52,25 +117,21 @@
            class="layout-glassDetail"
            :style="rectStyle(glassDetail, layoutIndex)"
            @contextmenu.prevent="handleRectRightClick(layoutIndex, rectIndex)"
            @mousedown="handleRectDragStart(layoutIndex, rectIndex)"
            @mousemove="handleRectDragging"
            @mouseup="handleRectDragEnd"
            @mouseleave="handleRectDragEnd"
            @click="handleRectClick(layoutIndex, rectIndex)"
          >
            <div class="glassDetail-content">
              <div class="size">{{ glassDetail.realWidth }}×{{ glassDetail.realHeight }}</div>
              <div>{{rectIndex }}</div>
              <div>{{glassDetail.polySort }}</div>
              <div v-if="showJiaHao" class="jia-hao">{{ glassDetail.rackNo }}</div>
              <div v-if="showProcessId" class="liuchengka">{{ glassDetail.processId }}</div>
            </div>
          </div>
        </div>
        </div>
      </div>
    </div>
    <!-- 提交按钮 -->
    <button @click="submitLayouts" style="position: fixed; bottom: 20px; right: 20px; padding: 10px; background: #4CAF50; color: white; border: none; border-radius: 5px; cursor: pointer;">
    <button @click="submitLayouts" style="position: fixed; top: 90px; right: 20px; padding: 10px; background: #409eff; color: white; border: none; border-radius: 5px; cursor: pointer;">
      保存调整
    </button>
  </div>
@@ -78,7 +139,10 @@
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue'
import request from "@/utils/request";
const router = useRouter();
import { useI18n } from "vue-i18n";
import { ElMessage, ElMessageBox } from "element-plus";
import useUserInfoStore from "@/stores/userInfo";
@@ -105,9 +169,9 @@
const rectClass = ref('layout-glassDetail');
const selectedLayoutIndex = ref(0);
const currentRect = ref(null);
const dragging = ref(false);
const dragStartPos = ref({ x: 0, y: 0 });
const dragRect = ref(null);
// const dragging = ref(false);
// const dragStartPos = ref({ x: 0, y: 0 });
// const dragRect = ref(null);
const showJiaHao = ref(false);
const showProcessId = ref(false);
const themeColor = ref(null);
@@ -116,15 +180,26 @@
const submitLayouts = async () => {
  layouts.value.forEach(layout => {
    layout.glassDetails.forEach(glassDetail => {
      glassDetail.x = Math.round(glassDetail.x);
      glassDetail.y = Math.round(glassDetail.y);
      glassDetail.width = Math.round(glassDetail.width);
      glassDetail.height = Math.round(glassDetail.height);
      glassDetail.x = parseFloat(glassDetail.x.toFixed(2));
      glassDetail.y = parseFloat(glassDetail.y.toFixed(2));
      glassDetail.width = parseFloat(glassDetail.width.toFixed(2));
      glassDetail.height = parseFloat(glassDetail.height.toFixed(2));
    });
  });
  const savedProjectNo = localStorage.getItem('projectNo');
  const processId = savedProjectNo;
  layoutsHead.value.Layouts=layouts.value
  // 构造与原始数据结构一致的对象
  const saveData = {
    projectNo: processId,
    layouts: layouts.value,
    // 复制原始数据中的其他必要字段
    ...layoutsHead.value
  };
  // 确保 Layouts 字段是序列化的字符串
  const requestData = {
    Layouts: JSON.stringify(saveData)
  };
  request.post(`/glassOptimize/updateOptimizeResult/${processId}`, layoutsHead.value, {
    headers: {
      'Content-Type': 'application/json'
@@ -132,10 +207,36 @@
  }).then((res) => {
    if (res.code == 200 && res.data === true) {
      ElMessage.success(t('basicData.msg.saveSuccess'));
      // // 保存成功后跳转到数控界面
      // router.push({ path: '/main/glassOptimize/OptimizeControl' });
    } else {
      ElMessage.warning(res.msg);
    }
  });
};
const openFolders = ref({
  pending: true,  // 默认展开"待切割原片"文件夹
   patchQueue: false,   // 默认收起"待补片队列"文件夹
  customSize: false   // 默认收起"添加自定义尺寸"文件夹
});
// 切换文件夹展开/收起状态
const toggleFolder = (folderName) => {
  openFolders.value[folderName] = !openFolders.value[folderName];
};
const showAddCustomSizeDialog = () => {
  // 检查是否选择了版图
  if (selectedLayoutIndex.value === null || layouts.value.length === 0) {
    ElMessage.warning('请先选择一个版图');
    return;
  }
  // 使用现有的添加成品逻辑,传入当前选中的版图索引
  showAddDialog(selectedLayoutIndex.value);
};
//查询设置的基础信息架号,矩形颜色,订单序号等
@@ -241,6 +342,12 @@
  return bestFit;
};
const isSelected = (layoutIndex, rectIndex) => {
  return focusIndex.value &&
         focusIndex.value.layoutIndex === layoutIndex &&
         focusIndex.value.rectIndex === rectIndex;
};
//版图内容样式加载
const layoutContainerStyle = (layoutIndex) => {
  const layout = layouts.value[layoutIndex];
@@ -248,12 +355,28 @@
  );
  return {
    position: 'absolute',
    left: `20px`,
    top: `140px`,
    left: `${layout.leftTrim * scale}px`,
    bottom: `${layout.upTrim * scale}px`,
    width: `${layout.width * scale}px`,
    height: `${layout.height * scale}px`,
    overflow: 'visible',
    border: '1px solid #ccc',
    //border: '1px solid #ccc',
    background: '#fff'
  };
};
const layoutContainerStyle1 = (layoutIndex) => {
  const layout = layouts.value[layoutIndex];
  const scale = Math.min(0.25
  );
  return {
    position: 'absolute',
    left: `20px`,
    top: `140px`,
    width: `${layout.realWidth * scale}px`,
    height: `${layout.realHeight * scale}px`,
    overflow: 'visible',
    //border: '1px solid #ccc',
    background: '#fff'
  };
};
@@ -276,12 +399,13 @@
//版图内容小片样式加载
const rectStyle = (glassDetail, layoutIndex) => {
  const layout = layouts.value[layoutIndex];
  const scale = Math.min(0.25
  );
  return {
  const scale = Math.min(0.25);
  const isSelectedRect = isSelected(layoutIndex, layout.glassDetails.indexOf(glassDetail));
  let style = {
    position: 'absolute',
    left: `${glassDetail.x * scale}px`,
    bottom: `${glassDetail.y * scale}px`,
    top: `${glassDetail.y * scale}px`,
    width: `${glassDetail.width * scale}px`,
    height: `${glassDetail.height * scale}px`,
    backgroundColor: glassDetail.isRemain ? '#f0f0f0' : themeColor.value,
@@ -290,10 +414,47 @@
    draggable: !glassDetail.isRemain,
    zIndex: glassDetail.isRemain ? 1 : 2
  };
  // 如果被选中,则添加内侧红框
  if (isSelectedRect) {
    style.boxShadow = 'inset 0 0 0 2px red';
  }
  return style;
};
const rectStyle1 = (glassDetail, layoutIndex) => {
  const layout = layouts.value[layoutIndex];
  const scale = Math.min(0.25);
  const isSelectedRect = isSelected(layoutIndex, layout.glassDetails.indexOf(glassDetail));
  let style = {
    position: 'absolute',
    left: `${glassDetail.x * scale}px`,
    top: `${glassDetail.y * scale}px`,
    width: `${glassDetail.width * scale}px`,
    height: `${glassDetail.height * scale}px`,
    backgroundColor: glassDetail.isRemain ? '#f0f0f0' : themeColor.value,
    border: '1px solid #000',
    cursor: 'pointer',
    draggable: !glassDetail.isRemain,
    zIndex: glassDetail.isRemain ? 1 : 2
  };
  // 如果被选中,则添加内侧红框
  if (isSelectedRect) {
    style.boxShadow = 'inset 0 0 0 2px red';
  }
  return style;
};
//点击小片
const handleRectClick = (layoutIndex, rectIndex) => {
  // if (dragging.value) {
  //   return;
  // }
  focusIndex.value = { layoutIndex, rectIndex };
  emit('rectClicked', layoutIndex, rectIndex);
};
@@ -302,12 +463,13 @@
const handleRectRightClick = (layoutIndex, rectIndex) => {
  const glassDetail = layouts.value[layoutIndex].glassDetails[rectIndex];
  if (glassDetail.isRemain) return;
  document.querySelectorAll('.context-menu').forEach(el => el.remove());
  const contextMenu = document.createElement('div');
  contextMenu.className = 'context-menu';
  contextMenu.style.position = 'absolute';
  contextMenu.style.left = `${event.clientX}px`;
  contextMenu.style.bottom = `${event.clientY}px`;
  contextMenu.style.top = `${event.clientY}px`;
  contextMenu.style.backgroundColor = '#fff';
  contextMenu.style.border = '1px solid #ccc';
  contextMenu.style.padding = '5px';
@@ -325,7 +487,7 @@
  moveUpAndRotateItem.textContent = '向上移动并旋转';
  moveUpAndRotateItem.style.cursor = 'pointer';
  moveUpAndRotateItem.addEventListener('click', () => {
    moveRectAndRotate(layoutIndex, rectIndex, 'up');
    moveRectAndRotate(layoutIndex, rectIndex, 'down');
    document.body.removeChild(contextMenu);
  });
@@ -333,7 +495,7 @@
  moveDownAndRotateItem.textContent = '向下移动并旋转';
  moveDownAndRotateItem.style.cursor = 'pointer';
  moveDownAndRotateItem.addEventListener('click', () => {
    moveRectAndRotate(layoutIndex, rectIndex, 'down');
    moveRectAndRotate(layoutIndex, rectIndex, 'up');
    document.body.removeChild(contextMenu);
  });
@@ -357,7 +519,7 @@
  moveUpItem.textContent = '向上移动';
  moveUpItem.style.cursor = 'pointer';
  moveUpItem.addEventListener('click', () => {
    moveRect(layoutIndex, rectIndex, 'up');
    moveRect(layoutIndex, rectIndex, 'down');
    document.body.removeChild(contextMenu);
  });
@@ -365,7 +527,7 @@
  moveDownItem.textContent = '向下移动';
  moveDownItem.style.cursor = 'pointer';
  moveDownItem.addEventListener('click', () => {
    moveRect(layoutIndex, rectIndex, 'down');
    moveRect(layoutIndex, rectIndex, 'up');
    document.body.removeChild(contextMenu);
  });
@@ -438,12 +600,13 @@
const handleGrayRectRightClick = (layoutIndex, rectIndex,glassDetails) => {
  //const glassDetail = glassDetails[rectIndex];
  if (!glassDetails.isRemain) return;
  document.querySelectorAll('.context-menu').forEach(el => el.remove());
  const contextMenu = document.createElement('div');
  contextMenu.className = 'context-menu';
  contextMenu.style.position = 'absolute';
  contextMenu.style.left = `${event.clientX}px`;
  contextMenu.style.bottom = `${event.clientY}px`;
  contextMenu.style.top = `${event.clientY}px`;
  contextMenu.style.backgroundColor = '#fff';
  contextMenu.style.border = '1px solid #ccc';
  contextMenu.style.padding = '5px';
@@ -481,85 +644,122 @@
};
//小片鼠标按下事件
const handleRectDragStart = (layoutIndex, rectIndex) => {
  const layout = layouts.value[layoutIndex];
  const glassDetail = layout.glassDetails[rectIndex];
  if (glassDetail.isRemain) return;
// const handleRectDragStart = (layoutIndex, rectIndex) => {
//   const layout = layouts.value[layoutIndex];
//   const glassDetail = layout.glassDetails[rectIndex];
//   if (glassDetail.isRemain) return;
//
//   dragRect.value = { layoutIndex, rectIndex };
//   dragStartPos.value = {
//     x: event.clientX,
//     y: event.clientY
//   };
// };
  dragging.value = true;
  dragRect.value = { layoutIndex, rectIndex };
  dragStartPos.value = {
    x: event.clientX,
    y: event.clientY
  };
};
// //小片鼠标移动事件
// const handleRectDragging = (event) => {
//   if (!dragRect.value) return;
//
//   // 如果还没确认是拖拽,则先判断是否达到拖拽阈值(例如5像素)
//   if (!dragging.value) {
//     const deltaX = event.clientX - dragStartPos.value.x;
//     const deltaY = event.clientY - dragStartPos.value.y;
//     const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
//
//     // 如果移动距离小于阈值,则认为是点击而非拖拽
//     if (distance < 5) {
//       return;
//     }
//
//     // 达到阈值,确认是拖拽操作
//     dragging.value = true;
//   }
//
//   const layoutIndex = dragRect.value.layoutIndex;
//   const rectIndex = dragRect.value.rectIndex;
//   const layout = layouts.value[layoutIndex];
//   const glassDetail = layout.glassDetails[rectIndex];
//
//   // 保存原始坐标用于计算偏移量
//   const originalX = glassDetail.x;
//   const originalY = glassDetail.y;
//
//   const scale = Math.min(
//     (props.gw - 100) / layout.width,
//     (props.gh - 100) / layout.height
//   );
//
//   const deltaX = event.clientX - dragStartPos.value.x;
//   const deltaY = event.clientY - dragStartPos.value.y;
//
//   const newRect = { ...glassDetail };
//   newRect.x += deltaX / scale;
//   newRect.y += deltaY / scale;
//
//   const otherRects = layout.glassDetails.filter(r => !r.isRemain && r !== glassDetail);
//   let isValidMove = true;
//
//   otherRects.forEach(otherRect => {
//     if (checkOverlap(newRect, otherRect)) {
//       isValidMove = false;
//     }
//   });
//
//   if (newRect.x < 0 || newRect.y < 0 ||
//       newRect.x + newRect.width > layout.width ||
//       newRect.y + newRect.height > layout.height) {
//     isValidMove = false;
//   }
//
//   if (isValidMove) {
//     glassDetail.x = newRect.x;
//     glassDetail.y = newRect.y;
//
//     // 更新glassPoint坐标
//     if (glassDetail.glassPoint && Array.isArray(glassDetail.glassPoint)) {
//       const offsetX = glassDetail.x - originalX;
//       const offsetY = glassDetail.y - originalY;
//
//       glassDetail.glassPoint.forEach(point => {
//         point.X += offsetX;
//         point.Y += offsetY;
//         // 添加精度控制
//         point.X = parseFloat(point.X.toFixed(2));
//         point.Y = parseFloat(point.Y.toFixed(2));
//       });
//     }
//
//     dragStartPos.value = {
//       x: event.clientX,
//       y: event.clientY
//     };
//     adjustGrayRectangles(layoutIndex);
//   }
// };
//小片鼠标移动事件
const handleRectDragging = (event) => {
  if (!dragging.value || !dragRect.value) return;
  const layoutIndex = dragRect.value.layoutIndex;
  const rectIndex = dragRect.value.rectIndex;
  const layout = layouts.value[layoutIndex];
  const glassDetail = layout.glassDetails[rectIndex];
  const scale = Math.min(
    (props.gw - 100) / layout.width,
    (props.gh - 100) / layout.height
  );
  const deltaX = event.clientX - dragStartPos.value.x;
  const deltaY = event.clientY - dragStartPos.value.y;
  const newRect = { ...glassDetail };
  newRect.x += deltaX / scale;
  newRect.y += deltaY / scale;
  const otherRects = layout.glassDetails.filter(r => !r.isRemain && r !== glassDetail);
  let isValidMove = true;
  otherRects.forEach(otherRect => {
    if (checkOverlap(newRect, otherRect)) {
      isValidMove = false;
    }
  });
  if (newRect.x < 0 || newRect.y < 0 ||
      newRect.x + newRect.width > layout.width ||
      newRect.y + newRect.height > layout.height) {
    isValidMove = false;
  }
  if (isValidMove) {
    glassDetail.x = newRect.x;
    glassDetail.y = newRect.y;
    dragStartPos.value = {
      x: event.clientX,
      y: event.clientY
    };
    adjustGrayRectangles(layoutIndex);
  }
};
//小片鼠标松开事件
const handleRectDragEnd = () => {
  if (dragRect.value) {
    const layoutIndex = dragRect.value.layoutIndex;
    const rectIndex = dragRect.value.rectIndex;
    const glassDetail = layouts.value[layoutIndex].glassDetails[rectIndex];
    const layout = layouts.value[layoutIndex];
    const scale = Math.min(
      (props.gw - 100) / layout.width,
      (props.gh - 100) / layout.height
    );
    glassDetail.x = Math.round(glassDetail.x);
    glassDetail.y = Math.round(glassDetail.y);
    adjustAlignmentPosition(layoutIndex, rectIndex);
  }
  dragging.value = false;
  dragRect.value = null;
};
// //小片鼠标松开事件
// const handleRectDragEnd = () => {
//   dragging.value = false;
//   dragRect.value = null;
//   dragStartPos.value = { x: 0, y: 0 };
//   if (dragRect.value) {
//     const layoutIndex = dragRect.value.layoutIndex;
//     const rectIndex = dragRect.value.rectIndex;
//     const glassDetail = layouts.value[layoutIndex].glassDetails[rectIndex];
//     const layout = layouts.value[layoutIndex];
//     const scale = Math.min(
//       (props.gw - 100) / layout.width,
//       (props.gh - 100) / layout.height
//     );
//
//     glassDetail.x = parseFloat(glassDetail.x.toFixed(2));
//     glassDetail.y = parseFloat(glassDetail.y.toFixed(2));
//     adjustAlignmentPosition(layoutIndex, rectIndex);
//   }
//
//   dragging.value = false;
//   dragRect.value = null;
// };
const adjustAlignmentPosition = (layoutIndex, rectIndex) => {
  const layout = layouts.value[layoutIndex];
@@ -581,7 +781,7 @@
    if (Math.abs(glassDetail.y - otherRect.y) < threshold) {
      glassDetail.y = Math.round((glassDetail.y + otherRect.y) / 2);
    }
    // 垂直对齐下边缘
    // 垂直对齐上边缘
    if (Math.abs((glassDetail.y + glassDetail.height) - (otherRect.y + otherRect.height)) < threshold) {
      glassDetail.y = Math.round((otherRect.y + otherRect.height - glassDetail.height));
    }
@@ -597,97 +797,152 @@
  adjustGrayRectangles(layoutIndex);
};
const mergeAdjacentGrayRects = (glassDetails,totalWidth,totalHeight) => {
const mergeAdjacentGrayRects = (glassDetails, totalWidth, totalHeight) => {
  const grayRects = glassDetails.filter(r => r.isRemain);
  const grayRects2 = glassDetails.filter(r => r.isRemain);
  let merged = [];
  const nonGrayRects = glassDetails.filter(r => !r.isRemain);
  // 按坐标排序,优先按y坐标,其次按x坐标(这样更符合从上到下、从左到右的阅读习惯)
  grayRects.sort((a, b) => {
    if (a.x !== b.x) return a.x - b.x;
    return a.y - b.y;
    if (a.y !== b.y) return a.y - b.y;
    return a.x - b.x;
  });
  if (grayRects.length === 0) return;
  merged.push({ ...grayRects[0] });
  const merged = [];
  let current = { ...grayRects[0] };
  // 遍历所有余料矩形进行合并
  for (let i = 1; i < grayRects.length; i++) {
      const last = merged[merged.length-1];
      const current = grayRects[i];
    const next = grayRects[i];
    if (current.x === last.x + last.width &&
        current.y === last.y &&
        current.height === last.height) {
      last.width += current.width;
      last.x = Math.round(last.x);
      last.y = Math.round(last.y);
      last.width = Math.round(last.width);
      last.height = Math.round(last.height);
    } else if (current.y === last.y + last.height &&
               current.x === last.x &&
               current.width === last.width) {
      last.height += current.height;
      last.x = Math.round(last.x);
      last.y = Math.round(last.y);
      last.width = Math.round(last.width);
      last.height = Math.round(last.height);
    } else {
    // 检查是否可以水平合并(同一行,高度相同,相邻)
    if (current.y === next.y &&
        current.height === next.height &&
        current.x + current.width === next.x) {
      // 水平合并
      current.width += next.width;
    }
    // 检查是否可以垂直合并(同一列,宽度相同,相邻)
    else if (current.x === next.x &&
        current.width === next.width &&
        current.y + current.height === next.y) {
      // 垂直合并
      current.height += next.height;
    }
    else {
      // 无法合并,保存当前矩形,开始新的合并
      merged.push({
        x: Math.round(current.x),
        y: Math.round(current.y),
        width: Math.round(current.width),
        height: Math.round(current.height),
        x: current.x,
        y: current.y,
        width: current.width,
        height: current.height,
        isRemain: true
      });
      current = { ...next };
    }
  }
  const nonGray = glassDetails.filter(r => !r.isRemain);
  //删除原数组拼接新的小片跟余料
  glassDetails.splice(0, glassDetails.length, ...nonGray, ...merged);
  // 添加最后一个矩形
  merged.push({
    x: current.x,
    y: current.y,
    width: current.width,
    height: current.height,
    isRemain: true
  });
  // 重新构建数组:非余料 + 合并后的余料
  glassDetails.splice(0, glassDetails.length, ...nonGrayRects, ...merged);
};
// 确保返回的区域不重叠
const calculateRemainingAreas = (totalWidth, totalHeight, obstacles) => {
  // 从整个原片开始
  let remaining = [{ x: 0, y: 0, width: totalWidth, height: totalHeight }];
  // 逐个处理障碍物(已放置的玻璃片)
  obstacles.forEach(obstacle => {
    remaining = cutRemainingAreas(remaining, obstacle, totalWidth, totalHeight);
  });
  // 对结果进行排序,确保一致性
  remaining.sort((a, b) => {
    if (a.x !== b.x) return a.x - b.x;
    return a.y - b.y;
  });
  return remaining;
};
// 调整后重新计算灰色余料
const adjustGrayRectangles = (layoutIndex) => {
  const layout = layouts.value[layoutIndex];
  const glassDetails = layout.glassDetails;
  //小片的数据
  // 1. 筛选出非余料的玻璃片(即实际要切割的玻璃片)
  const nonGrayRects = glassDetails.filter(glassDetail => !glassDetail.isRemain);
  //所有的小片余料坐标跟尺寸
  // 2. 计算剩余可用区域
  let remainingAreas = calculateRemainingAreas(layout.width, layout.height, nonGrayRects);
  const uniqueArr = Array.from(
      new Set(remainingAreas.map(item => JSON.stringify(item)))
  ).map(item => JSON.parse(item));
  //余料的数据
  // 3. 去重处理 - 更严格的去重逻辑
  const uniqueArr = removeDuplicateAreas(remainingAreas);
  // 4. 获取当前已存在的余料矩形(需要保留引用以便更新)
  const currentGrayRects = glassDetails.filter(r => r.isRemain);
  //循环余料数据跟全部的对比
  currentGrayRects.forEach((_, index) => {
    if (index >= remainingAreas.length) {
      glassDetails.splice(index, 1);
    }
  // 5. 清除所有现有的余料矩形
  // 先收集非余料矩形
  const nonRemainRects = glassDetails.filter(r => !r.isRemain);
  // 6. 重新构建玻璃详情数组
  // 保留非余料矩形
  const newGlassDetails = [...nonRemainRects];
  // 添加新的余料矩形
  uniqueArr.forEach((area) => {
    newGlassDetails.push({
      x: area.x,
      y: area.y,
      width: area.width,
      height: area.height,
      isRemain: true
    });
  });
  uniqueArr.forEach((area, index) => {
    if (index < currentGrayRects.length) {
      currentGrayRects[index].x = Math.round(area.x);
      currentGrayRects[index].y = Math.round(area.y);
      currentGrayRects[index].width = Math.round(area.width);
      currentGrayRects[index].height = Math.round(area.height);
    } else {
      glassDetails.push({
        x: Math.round(area.x),
        y: Math.round(area.y),
        width: Math.round(area.width),
        height: Math.round(area.height),
        isRemain: true
      });
    }
  });
  // 7. 更新布局的玻璃详情
  layout.glassDetails = newGlassDetails;
  mergeAdjacentGrayRects(glassDetails,layout.width, layout.height);
  // 8. 合并相邻的余料矩形
  mergeAdjacentGrayRects(layout.glassDetails, layout.width, layout.height);
};
const removeDuplicateAreas = (areas) => {
  const result = [];
  areas.forEach(area => {
    // 检查是否与已存在的区域重叠或相等
    const isDuplicate = result.some(existingArea => {
      return (
          existingArea.x === area.x &&
          existingArea.y === area.y &&
          existingArea.width === area.width &&
          existingArea.height === area.height
      );
    });
    if (!isDuplicate) {
      result.push(area);
    }
  });
  return result;
};
//旋转方法
const rotateRect = (layoutIndex, rectIndex) => {
@@ -713,6 +968,25 @@
  }
  if (isValidRotation) {
    // 更新glassPoint坐标(如果存在)
    if (glassDetail.glassPoint && Array.isArray(glassDetail.glassPoint)) {
      // 保存原始点坐标
      const originalPoints = JSON.parse(JSON.stringify(glassDetail.glassPoint));
      // 旋转点坐标(以矩形左上角为原点的旋转)
      glassDetail.glassPoint.forEach((point, index) => {
        // 计算相对于矩形左上角的坐标
        const relX = originalPoints[index].X - originalState.x;
        const relY = originalPoints[index].Y - originalState.y;
        // 旋转90度后的坐标(顺时针)
        point.X = originalState.x + relY;
        point.Y = originalState.y + (originalState.width - relX);
        // 添加精度控制
        point.X = parseFloat(point.X.toFixed(2));
        point.Y = parseFloat(point.Y.toFixed(2));
      });
    }
    adjustGrayRectangles(layoutIndex);
  } else {
    glassDetail.width = originalState.width;
@@ -776,6 +1050,11 @@
    return;
  }
  // 保存原始坐标
  const originalX = glassDetail.x;
  const originalY = glassDetail.y;
  switch (direction) {
    case 'up':
      glassDetail.y += maxStep;
@@ -807,6 +1086,19 @@
  }
  if (isValidMove) {
    // 更新glassPoint坐标
    if (glassDetail.glassPoint && Array.isArray(glassDetail.glassPoint)) {
      const offsetX = glassDetail.x - originalX;
      const offsetY = glassDetail.y - originalY;
      glassDetail.glassPoint.forEach(point => {
        point.X += offsetX;
        point.Y += offsetY;
        // 添加精度控制
        point.X = parseFloat(point.X.toFixed(2));
        point.Y = parseFloat(point.Y.toFixed(2));
      });
    }
    adjustGrayRectangles(layoutIndex);
  } else {
    glassDetail.x = originalState.x;
@@ -831,56 +1123,78 @@
};
//重新计算余料坐标以及尺寸1
const calculateRemainingAreas = (totalWidth, totalHeight, obstacles) => {
  let remaining = [{ x: 0, y: 0, width: totalWidth, height: totalHeight }];
  obstacles.forEach(glassDetail => {
    remaining = cutRemainingAreas(remaining, glassDetail,totalWidth,totalHeight);
  });
  return remaining;
};
// const calculateRemainingAreas = (totalWidth, totalHeight, obstacles) => {
//   let remaining = [{ x: 0, y: 0, width: totalWidth, height: totalHeight }];
//   obstacles.forEach(glassDetail => {
//     remaining = cutRemainingAreas(remaining, glassDetail,totalWidth,totalHeight);
//   });
//   return remaining;
// };
//重新计算余料坐标以及尺寸2
const cutRemainingAreas = (remainingAreas, obstacle,totalWidth,totalHeight) => {
const cutRemainingAreas = (remainingAreas, obstacle, totalWidth, totalHeight) => {
  const newRemaining = [];
  remainingAreas.forEach(area => {
    if (checkOverlap(area, obstacle)) {
      if (obstacle.x > area.x) {
        newRemaining.push({
          x: area.x,
          y: area.y,
          width: obstacle.x - area.x,
          height: area.height
        });
      }
      if (obstacle.x + obstacle.width < area.x + area.width) {
        newRemaining.push({
          x: obstacle.x + obstacle.width,
          y: area.y,
          width: area.width - (obstacle.x + obstacle.width - area.x),
          height: area.height
        });
      }
      if (obstacle.y > area.y) {
        newRemaining.push({
          x: area.x,
          y: area.y,
          width: area.width,
          height: obstacle.y - area.y
        });
      }
      if (obstacle.y + obstacle.height < area.y + area.height ) {
          newRemaining.push({
            x: area.x,
            y: obstacle.y + obstacle.height,
            width: area.width,
            height: area.height - (obstacle.y + obstacle.height - area.y)
          });
      }
    } else {
  remainingAreas.forEach(area => {
    // 如果障碍物与当前区域没有重叠,保留原区域
    if (!checkOverlap(area, obstacle)) {
      newRemaining.push(area);
      return;
    }
    // 计算重叠区域的边界
    const overlapLeft = Math.max(area.x, obstacle.x);
    const overlapRight = Math.min(area.x + area.width, obstacle.x + obstacle.width);
    const overlapTop = Math.max(area.y, obstacle.y);
    const overlapBottom = Math.min(area.y + area.height, obstacle.y + obstacle.height);
    // 生成四个可能的新区域(上、下、左、右)
    // 上方区域
    if (overlapTop > area.y) {
      newRemaining.push({
        x: area.x,
        y: area.y,
        width: area.width,
        height: overlapTop - area.y
      });
    }
    // 下方区域
    if (overlapBottom < area.y + area.height) {
      newRemaining.push({
        x: area.x,
        y: overlapBottom,
        width: area.width,
        height: area.y + area.height - overlapBottom
      });
    }
    // 左方区域(仅在重叠区域的垂直范围内)
    if (overlapLeft > area.x) {
      const regionTop = overlapTop;
      const regionBottom = overlapBottom;
      newRemaining.push({
        x: area.x,
        y: regionTop,
        width: overlapLeft - area.x,
        height: regionBottom - regionTop
      });
    }
    // 右方区域(仅在重叠区域的垂直范围内)
    if (overlapRight < area.x + area.width) {
      const regionTop = overlapTop;
      const regionBottom = overlapBottom;
      newRemaining.push({
        x: overlapRight,
        y: regionTop,
        width: area.x + area.width - overlapRight,
        height: regionBottom - regionTop
      });
    }
  });
  return newRemaining;
};
@@ -891,8 +1205,8 @@
  if (!glassDetail) return '';*/
  const totalRects = layouts.value.length;
  const currentRectIndex = layoutIndex + 1;
  const width = layout.width;
  const height = layout.height;
  const width = layout.realWidth;
  const height = layout.realHeight;
  const percentage = (layout.usageRate * 100).toFixed(2) + '%';
  return `${currentRectIndex}/${totalRects} ${width}×${height} ×1 ${percentage}`;
};
@@ -911,7 +1225,7 @@
//向上移动计算坐标
const getAvailableSpaceUp = (glassDetail, layout, obstacles) => {
  let maxSpace = layout.height - (glassDetail.y + glassDetail.height);
  let maxSpace = layout.height - glassDetail.y - glassDetail.height;
  obstacles.forEach(obstacle => {
    if (obstacle.y > glassDetail.y + glassDetail.height &&
        obstacle.x <= glassDetail.x + glassDetail.width &&
@@ -977,7 +1291,7 @@
      event.preventDefault();
      if (!moveInterval) {
        moveInterval = setInterval(() => {
          moveRect(layoutIndex, rectIndex, 'up');
          moveRect(layoutIndex, rectIndex, 'down');
        }, 50);
      }
      break;
@@ -985,7 +1299,7 @@
      event.preventDefault();
      if (!moveInterval) {
        moveInterval = setInterval(() => {
          moveRect(layoutIndex, rectIndex, 'down');
          moveRect(layoutIndex, rectIndex, 'up');
        }, 50);
      }
      break;
@@ -1033,6 +1347,15 @@
    // 更新矩形位置
    glassDetail.x = newX;
    glassDetail.y = newY;
    // 更新glassPoint坐标
    if (glassDetail.glassPoint && Array.isArray(glassDetail.glassPoint)) {
      glassDetail.glassPoint.forEach(point => {
        point.X = width - point.X;
        point.X = parseFloat(point.X.toFixed(2));
        point.Y = parseFloat(point.Y.toFixed(2));
      });
    }
  });
  // 更新布局
@@ -1053,6 +1376,16 @@
    // 更新矩形位置
    glassDetail.y = newY;
    // 更新glassPoint坐标
    if (glassDetail.glassPoint && Array.isArray(glassDetail.glassPoint)) {
      glassDetail.glassPoint.forEach(point => {
        point.Y = height - point.Y;
        // 添加精度控制
        point.X = parseFloat(point.X.toFixed(2));
        point.Y = parseFloat(point.Y.toFixed(2));
      });
    }
  });
  // 更新布局
@@ -1164,4 +1497,17 @@
.context-menu div:hover {
  background-color: #f0f0f0;
}
.folder-header {
  font-weight: bold;
}
.folder-header:hover {
  background-color: #d0d0d0 !important;
}
.folder-content {
  border-left: 2px solid #ccc;
  margin-left: 5px;
}
</style>