| | |
| | | <template>
|
| | | <div style="display: flex; height: 100vh;">
|
| | | <div style="display: flex; height: 90vh;">
|
| | | <!-- Sidebar -->
|
| | | <div class="sidebar" style="width: 200px; background: #f4f4f4; padding: 10px;">
|
| | | <div class="sidebar" style="width: 200px; background: #f4f4f4; padding: 10px; height: 93%; overflow-y: auto; max-height: 90vh; border-radius: 8px;">
|
| | | <div
|
| | | v-for="(layout, layoutIndex) in layouts"
|
| | | :key="layoutIndex"
|
| | | class="sidebar-item"
|
| | | @click="selectLayout(layoutIndex)"
|
| | | :class="{ 'selected': selectedLayoutIndex === layoutIndex }"
|
| | | v-for="(layout, layoutIndex) in layouts"
|
| | | :key="layoutIndex"
|
| | | class="sidebar-item"
|
| | | @click="selectLayout(layoutIndex)"
|
| | | :class="{ 'selected': selectedLayoutIndex === layoutIndex }" style="margin-bottom: 5px;"
|
| | | >
|
| | | {{ layout.width }} × {{ layout.height }} × {{ layout.SameCount }}
|
| | | {{ layout.realWidth }} × {{ layout.realHeight }} × {{ layout.quantity }}
|
| | | </div>
|
| | | </div>
|
| | |
|
| | |
| | | </div>
|
| | |
|
| | | <!-- Layout Container -->
|
| | | <div class="layout-container" :style="layoutContainerStyle1(layoutIndex)">
|
| | | <div class="layout-container" :style="layoutContainerStyle(layoutIndex)">
|
| | | <!-- 灰色矩形 -->
|
| | | <div
|
| | | v-for="(rect, rectIndex) in layout.rects.filter(r => r.isRemain)"
|
| | | v-for="(glassDetail, rectIndex) in layout.glassDetails.filter(r => r.isRemain)"
|
| | | :key="`gray-${rectIndex}`"
|
| | | :ref="(el) => { if (el) rectsElements[layoutIndex + '-' + rectIndex] = el }"
|
| | | class="layout-rect"
|
| | | :style="rectStyle(rect, layoutIndex)"
|
| | | @contextmenu.prevent="handleGrayRectRightClick(layoutIndex, rectIndex)"
|
| | | />
|
| | | class="layout-glassDetail"
|
| | | :style="rectStyle1(glassDetail, layoutIndex)"
|
| | | @contextmenu.prevent="handleGrayRectRightClick(layoutIndex, rectIndex,glassDetail)"
|
| | | >
|
| | | <!-- <div class="glassDetail-content">
|
| | | <div class="size">{{ glassDetail.width }}×{{ glassDetail.height }}</div>
|
| | | <div v-if="showJiaHao" class="jia-hao">{{ glassDetail.JiaHao }}</div>
|
| | | <div v-if="showProcessId" class="liuchengka">{{ glassDetail.liuchengka }}</div>
|
| | | </div>-->
|
| | | </div>
|
| | |
|
| | | <!-- 蓝色矩形 -->
|
| | | <div
|
| | | v-for="(rect, rectIndex) in layout.rects.filter(r => !r.isRemain)"
|
| | | v-for="(glassDetail, rectIndex) in layout.glassDetails.filter(r => !r.isRemain)"
|
| | | :key="`blue-${rectIndex}`"
|
| | | :ref="(el) => { if (el) rectsElements[layoutIndex + '-' + rectIndex] = el }"
|
| | | class="layout-rect"
|
| | | :style="rectStyle(rect, layoutIndex)"
|
| | | 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="rect-content">
|
| | | <div class="size">{{ rect.w }}×{{ rect.h }}</div>
|
| | | <div class="jia-hao">{{ rect.JiaHao }}</div>
|
| | | <div class="glassDetail-content">
|
| | | <div class="size">{{ glassDetail.realWidth }}×{{ glassDetail.realHeight }}</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>
|
| | |
| | |
|
| | | <script setup>
|
| | | import { ref, reactive, onMounted, onUnmounted } from 'vue';
|
| | | import { useRouter } from 'vue-router';
|
| | | import request from "@/utils/request";
|
| | | const router = useRouter();
|
| | | import { useI18n } from "vue-i18n";
|
| | | import { ElMessage, ElMessageBox } from "element-plus";
|
| | | import useUserInfoStore from "@/stores/userInfo";
|
| | |
|
| | | const { t } = useI18n();
|
| | | const userStore = useUserInfoStore()
|
| | | const username = userStore.user.userName
|
| | | let clickEventListener = null;
|
| | | const props = defineProps({
|
| | | layoutData: { type: Object, required: true },
|
| | | gw: { type: Number, default: 1000 },
|
| | |
| | | const rectsElements = ref({});
|
| | | const focusIndex = ref(null);
|
| | | const layouts = ref([]);
|
| | | const layoutsHead = ref();
|
| | | const panelClass = ref('');
|
| | | const panelStyle = ref(props.style);
|
| | | const rectClass = ref('layout-rect');
|
| | | 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 showJiaHao = ref(false);
|
| | | const showProcessId = ref(false);
|
| | | const themeColor = ref(null);
|
| | |
|
| | | // 提交布局数据到后端
|
| | | //保存调整
|
| | | const submitLayouts = async () => {
|
| | | layouts.value.forEach(layout => {
|
| | | layout.rects.forEach(rect => {
|
| | | rect.x = Math.round(rect.x);
|
| | | rect.y = Math.round(rect.y);
|
| | | rect.w = Math.round(rect.w);
|
| | | rect.h = Math.round(rect.h);
|
| | | layout.glassDetails.forEach(glassDetail => {
|
| | | 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;
|
| | | const Layouts = layouts.value;
|
| | | request.post(`/glassOptimize/updateOptimizeResult/${processId}`, JSON.stringify({ Layouts }), {
|
| | | // 构造与原始数据结构一致的对象
|
| | | 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'
|
| | | }
|
| | | }).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 fetchSettings = async (username) => {
|
| | | try {
|
| | | const response = await request.post(`/glassOptimize/selectOptimizeParms/${username}`);
|
| | | if (response.code == 200) {
|
| | | if (!response.data) {
|
| | | console.error('响应数据为空');
|
| | | return;
|
| | | }
|
| | | const parsedData = JSON.parse(response.data);
|
| | | if (parsedData.display && parsedData.frameNumber) {
|
| | | showJiaHao.value = parsedData.display.frameNumber;
|
| | | }
|
| | | if (parsedData.display && parsedData.orderNumber) {
|
| | | showProcessId.value = parsedData.display.orderNumber;
|
| | | }
|
| | | if (parsedData.display) {
|
| | | themeColor.value = parsedData.display.themeColor;
|
| | | }
|
| | | } else {
|
| | | console.error('请求失败,状态码:', response.code);
|
| | | }
|
| | | } catch (error) {
|
| | | console.error('请求发生错误:', error);
|
| | | }
|
| | | };
|
| | |
|
| | | //添加成品
|
| | | const showAddDialog = (layoutIndex, rectIndex) => {
|
| | | ElMessageBox.prompt('请输入成品的宽度和高度', '添加成品', {
|
| | | inputType: 'text',
|
| | |
| | | },
|
| | | inputErrorMessage: '输入格式不正确'
|
| | | })
|
| | | .then(({ value }) => {
|
| | | const values = value.split(',').map(v => parseFloat(v.trim()));
|
| | | const newRect = {
|
| | | x: 0,
|
| | | y: 0,
|
| | | w: values[0],
|
| | | h: values[1],
|
| | | isRemain: false,
|
| | | JiaHao: `新成品`
|
| | | };
|
| | | addNewRect(layoutIndex, newRect);
|
| | | })
|
| | | .catch(() => {
|
| | | // 用户取消
|
| | | });
|
| | | .then(({ value }) => {
|
| | | const values = value.split(',').map(v => parseFloat(v.trim()));
|
| | | const newRect = {
|
| | | x: 0,
|
| | | y: 0,
|
| | | width: values[0],
|
| | | height: values[1],
|
| | | isRemain: false
|
| | | };
|
| | | addNewRect(layoutIndex, newRect);
|
| | | })
|
| | | .catch(() => {
|
| | | // 用户取消
|
| | | });
|
| | | };
|
| | |
|
| | | //添加成品逻辑判断
|
| | | const addNewRect = (layoutIndex, newRect) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | layout.rects.push(newRect);
|
| | | adjustGrayRectangles(layoutIndex);
|
| | | const bestFitPosition = findBestFitPosition(layoutIndex, newRect);
|
| | | if (bestFitPosition) {
|
| | | newRect.x = bestFitPosition.x;
|
| | | newRect.y = bestFitPosition.y;
|
| | | layout.glassDetails.push(newRect);
|
| | | adjustGrayRectangles(layoutIndex);
|
| | | } else {
|
| | | ElMessage.warning('无法放置,没有足够的空间');
|
| | | }
|
| | | };
|
| | |
|
| | | const layoutContainerStyle = (layoutIndex) => {
|
| | | const containerWidth = (props.gw - 210) / 2;
|
| | | const containerHeight = (props.gh - 100) / 3;
|
| | | //添加成品判断是否可以放下
|
| | | const findBestFitPosition = (layoutIndex, newRect) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const scale = Math.min(
|
| | | (props.gw - 100) / layout.width,
|
| | | (props.gh - 100) / layout.height
|
| | | const obstacles = layout.glassDetails.filter(r => !r.isRemain);
|
| | | let bestFit = null;
|
| | | let minAreaDifference = Infinity;
|
| | |
|
| | | const remainingAreas = calculateRemainingAreas(layout.width, layout.height, obstacles);
|
| | | remainingAreas.forEach(area => {
|
| | | if (newRect.width <= area.width && newRect.height <= area.height) {
|
| | | const areaDifference = Math.abs(area.width * area.height - newRect.width * newRect.height);
|
| | | if (areaDifference < minAreaDifference) {
|
| | | minAreaDifference = areaDifference;
|
| | | bestFit = {
|
| | | x: area.x,
|
| | | y: area.y,
|
| | | width: newRect.width,
|
| | | height: newRect.height
|
| | | };
|
| | | }
|
| | | }
|
| | | });
|
| | |
|
| | | return bestFit;
|
| | | };
|
| | |
|
| | | //版图内容样式加载
|
| | | const layoutContainerStyle = (layoutIndex) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const scale = Math.min(0.25
|
| | | );
|
| | | return {
|
| | | position: 'absolute',
|
| | | left: `${(props.gw - layout.width * scale) / 2}px`,
|
| | | top: `${(props.gh - layout.height * scale) / 2}px`,
|
| | | 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 layoutInfoStyle = (layoutIndex) => {
|
| | | const layoutContainerStyle1 = (layoutIndex) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const scale = Math.min(
|
| | | (props.gw - 100) / layout.width,
|
| | | (props.gh - 100) / layout.height
|
| | | const scale = Math.min(0.25
|
| | | );
|
| | | return {
|
| | | position: 'absolute',
|
| | | left: `${(props.gw - layout.width * scale) / 2}px`,
|
| | | top: `${(props.gh - layout.height * scale) / 2 - 45}px`,
|
| | | left: `20px`,
|
| | | top: `140px`,
|
| | | width: `${layout.realWidth * scale}px`,
|
| | | height: `${layout.realHeight * scale}px`,
|
| | | overflow: 'visible',
|
| | | //border: '1px solid #ccc',
|
| | | background: '#fff'
|
| | | };
|
| | | };
|
| | |
|
| | | //版图内容头部样式加载
|
| | | const layoutInfoStyle = (layoutIndex) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const scale = Math.min(0.25
|
| | | );
|
| | | return {
|
| | | position: 'absolute',
|
| | | left: `20px`,
|
| | | top: `100px`,
|
| | | background: 'none',
|
| | | textAlign: 'center',
|
| | | zIndex: 1000
|
| | | };
|
| | | };
|
| | |
|
| | | const rectStyle = (rect, layoutIndex) => {
|
| | | //版图内容小片样式加载
|
| | | const rectStyle = (glassDetail, layoutIndex) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const scale = Math.min(
|
| | | (props.gw - 100) / layout.width,
|
| | | (props.gh - 100) / layout.height
|
| | | const scale = Math.min(0.25
|
| | | );
|
| | | return {
|
| | | position: 'absolute',
|
| | | left: `${rect.x * scale}px`,
|
| | | top: `${rect.y * scale}px`,
|
| | | width: `${rect.w * scale}px`,
|
| | | height: `${rect.h * scale}px`,
|
| | | backgroundColor: rect.isRemain ? '#f0f0f0' : '#a0d8ef',
|
| | | 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: !rect.isRemain,
|
| | | zIndex: rect.isRemain ? 1 : 2
|
| | | draggable: !glassDetail.isRemain,
|
| | | zIndex: glassDetail.isRemain ? 1 : 2
|
| | | };
|
| | | };
|
| | |
|
| | | const rectStyle1 = (glassDetail, layoutIndex) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const scale = Math.min(0.25
|
| | | );
|
| | | return {
|
| | | 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
|
| | | };
|
| | | };
|
| | |
|
| | | //点击小片
|
| | | const handleRectClick = (layoutIndex, rectIndex) => {
|
| | | focusIndex.value = { layoutIndex, rectIndex };
|
| | | emit('rectClicked', layoutIndex, rectIndex);
|
| | | };
|
| | |
|
| | | //小片右键菜单功能
|
| | | const handleRectRightClick = (layoutIndex, rectIndex) => {
|
| | | const rect = layouts.value[layoutIndex].rects[rectIndex];
|
| | | if (rect.isRemain) return;
|
| | | const glassDetail = layouts.value[layoutIndex].glassDetails[rectIndex];
|
| | | if (glassDetail.isRemain) return;
|
| | |
|
| | | const contextMenu = document.createElement('div');
|
| | | contextMenu.className = 'context-menu';
|
| | | contextMenu.style.position = 'absolute';
|
| | | contextMenu.style.left = `${event.clientX}px`;
|
| | | contextMenu.style.top = `${event.clientY}px`;
|
| | | contextMenu.style.bottom = `${event.clientY}px`;
|
| | | contextMenu.style.backgroundColor = '#fff';
|
| | | contextMenu.style.border = '1px solid #ccc';
|
| | | contextMenu.style.padding = '5px';
|
| | |
| | | moveUpAndRotateItem.textContent = '向上移动并旋转';
|
| | | moveUpAndRotateItem.style.cursor = 'pointer';
|
| | | moveUpAndRotateItem.addEventListener('click', () => {
|
| | | moveRectAndRotate(layoutIndex, rectIndex, 'up');
|
| | | moveRectAndRotate(layoutIndex, rectIndex, 'down');
|
| | | document.body.removeChild(contextMenu);
|
| | | });
|
| | |
|
| | |
| | | moveDownAndRotateItem.textContent = '向下移动并旋转';
|
| | | moveDownAndRotateItem.style.cursor = 'pointer';
|
| | | moveDownAndRotateItem.addEventListener('click', () => {
|
| | | moveRectAndRotate(layoutIndex, rectIndex, 'down');
|
| | | moveRectAndRotate(layoutIndex, rectIndex, 'up');
|
| | | document.body.removeChild(contextMenu);
|
| | | });
|
| | |
|
| | |
| | | moveUpItem.textContent = '向上移动';
|
| | | moveUpItem.style.cursor = 'pointer';
|
| | | moveUpItem.addEventListener('click', () => {
|
| | | moveRect(layoutIndex, rectIndex, 'up');
|
| | | moveRect(layoutIndex, rectIndex, 'down');
|
| | | document.body.removeChild(contextMenu);
|
| | | });
|
| | |
|
| | |
| | | moveDownItem.textContent = '向下移动';
|
| | | moveDownItem.style.cursor = 'pointer';
|
| | | moveDownItem.addEventListener('click', () => {
|
| | | moveRect(layoutIndex, rectIndex, 'down');
|
| | | moveRect(layoutIndex, rectIndex, 'up');
|
| | | document.body.removeChild(contextMenu);
|
| | | });
|
| | |
|
| | |
| | | document.body.removeChild(contextMenu);
|
| | | });
|
| | |
|
| | | const mirrorXItem = document.createElement('div');
|
| | | mirrorXItem.textContent = 'X镜像';
|
| | | mirrorXItem.style.cursor = 'pointer';
|
| | | mirrorXItem.addEventListener('click', () => {
|
| | | mirrorLayoutX(layoutIndex);
|
| | | document.body.removeChild(contextMenu);
|
| | | });
|
| | |
|
| | | const mirrorYItem = document.createElement('div');
|
| | | mirrorYItem.textContent = 'Y镜像';
|
| | | mirrorYItem.style.cursor = 'pointer';
|
| | | mirrorYItem.addEventListener('click', () => {
|
| | | mirrorLayoutY(layoutIndex);
|
| | | document.body.removeChild(contextMenu);
|
| | | });
|
| | |
|
| | | contextMenu.appendChild(rotateItem);
|
| | | contextMenu.appendChild(moveUpAndRotateItem);
|
| | | contextMenu.appendChild(moveDownAndRotateItem);
|
| | |
| | | contextMenu.appendChild(moveRightItem);
|
| | | contextMenu.appendChild(deleteItem);
|
| | | contextMenu.appendChild(addItem);
|
| | | contextMenu.appendChild(mirrorXItem);
|
| | | contextMenu.appendChild(mirrorYItem);
|
| | |
|
| | | document.body.appendChild(contextMenu);
|
| | | };
|
| | |
|
| | | const handleGrayRectRightClick = (layoutIndex, rectIndex) => {
|
| | | const rect = layouts.value[layoutIndex].rects[rectIndex];
|
| | | if (!rect.isRemain) return;
|
| | | //余料右键菜单功能
|
| | | const handleGrayRectRightClick = (layoutIndex, rectIndex,glassDetails) => {
|
| | | //const glassDetail = glassDetails[rectIndex];
|
| | | if (!glassDetails.isRemain) return;
|
| | |
|
| | | const contextMenu = document.createElement('div');
|
| | | contextMenu.className = 'context-menu';
|
| | | contextMenu.style.position = 'absolute';
|
| | | contextMenu.style.left = `${event.clientX}px`;
|
| | | contextMenu.style.top = `${event.clientY}px`;
|
| | | contextMenu.style.bottom = `${event.clientY}px`;
|
| | | contextMenu.style.backgroundColor = '#fff';
|
| | | contextMenu.style.border = '1px solid #ccc';
|
| | | contextMenu.style.padding = '5px';
|
| | | contextMenu.style.zIndex = 1001;
|
| | |
|
| | | const mergeLeftItem = document.createElement('div');
|
| | | mergeLeftItem.textContent = '向左合并';
|
| | | mergeLeftItem.style.cursor = 'pointer';
|
| | | mergeLeftItem.addEventListener('click', () => {
|
| | | mergeGrayRects(layoutIndex, rectIndex, 'left');
|
| | | document.body.removeChild(contextMenu);
|
| | | });
|
| | |
|
| | | const mergeRightItem = document.createElement('div');
|
| | | mergeRightItem.textContent = '向右合并';
|
| | | mergeRightItem.style.cursor = 'pointer';
|
| | | mergeRightItem.addEventListener('click', () => {
|
| | | mergeGrayRects(layoutIndex, rectIndex, 'right');
|
| | | document.body.removeChild(contextMenu);
|
| | | });
|
| | |
|
| | | const mergeUpItem = document.createElement('div');
|
| | | mergeUpItem.textContent = '向上合并';
|
| | | mergeUpItem.style.cursor = 'pointer';
|
| | | mergeUpItem.addEventListener('click', () => {
|
| | | mergeGrayRects(layoutIndex, rectIndex, 'up');
|
| | | document.body.removeChild(contextMenu);
|
| | | });
|
| | |
|
| | | const mergeDownItem = document.createElement('div');
|
| | | mergeDownItem.textContent = '向下合并';
|
| | | mergeDownItem.style.cursor = 'pointer';
|
| | | mergeDownItem.addEventListener('click', () => {
|
| | | mergeGrayRects(layoutIndex, rectIndex, 'down');
|
| | | document.body.removeChild(contextMenu);
|
| | | });
|
| | |
|
| | | const addItem = document.createElement('div');
|
| | | addItem.textContent = '添加成品';
|
| | |
| | | document.body.removeChild(contextMenu);
|
| | | });
|
| | |
|
| | | contextMenu.appendChild(mergeLeftItem);
|
| | | contextMenu.appendChild(mergeRightItem);
|
| | | contextMenu.appendChild(mergeUpItem);
|
| | | contextMenu.appendChild(mergeDownItem);
|
| | | const mirrorXItem = document.createElement('div');
|
| | | mirrorXItem.textContent = 'X镜像';
|
| | | mirrorXItem.style.cursor = 'pointer';
|
| | | mirrorXItem.addEventListener('click', () => {
|
| | | mirrorLayoutX(layoutIndex);
|
| | | document.body.removeChild(contextMenu);
|
| | | });
|
| | |
|
| | | const mirrorYItem = document.createElement('div');
|
| | | mirrorYItem.textContent = 'Y镜像';
|
| | | mirrorYItem.style.cursor = 'pointer';
|
| | | mirrorYItem.addEventListener('click', () => {
|
| | | mirrorLayoutY(layoutIndex);
|
| | | document.body.removeChild(contextMenu);
|
| | | });
|
| | |
|
| | | contextMenu.appendChild(addItem);
|
| | | contextMenu.appendChild(mirrorXItem);
|
| | | contextMenu.appendChild(mirrorYItem);
|
| | |
|
| | | document.body.appendChild(contextMenu);
|
| | | };
|
| | |
|
| | | //小片鼠标按下事件
|
| | | const handleRectDragStart = (layoutIndex, rectIndex) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const rect = layout.rects[rectIndex];
|
| | | if (rect.isRemain) return;
|
| | | const glassDetail = layout.glassDetails[rectIndex];
|
| | | if (glassDetail.isRemain) return;
|
| | |
|
| | | dragging.value = true;
|
| | | dragRect.value = { layoutIndex, rectIndex };
|
| | |
| | | };
|
| | | };
|
| | |
|
| | | //小片鼠标移动事件
|
| | | 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 rect = layout.rects[rectIndex];
|
| | | 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 = { ...rect };
|
| | | const newRect = { ...glassDetail };
|
| | | newRect.x += deltaX / scale;
|
| | | newRect.y += deltaY / scale;
|
| | |
|
| | | // 检查是否与其他蓝色矩形重叠
|
| | | const otherRects = layout.rects.filter(r => !r.isRemain && r !== rect);
|
| | | 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.w > layout.width ||
|
| | | newRect.y + newRect.h > layout.height) {
|
| | | newRect.x + newRect.width > layout.width ||
|
| | | newRect.y + newRect.height > layout.height) {
|
| | | isValidMove = false;
|
| | | }
|
| | |
|
| | | if (isValidMove) {
|
| | | rect.x = newRect.x;
|
| | | rect.y = newRect.y;
|
| | | 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
|
| | |
| | | }
|
| | | };
|
| | |
|
| | | //小片鼠标松开事件
|
| | | const handleRectDragEnd = () => {
|
| | | if (dragRect.value) {
|
| | | const layoutIndex = dragRect.value.layoutIndex;
|
| | | const rectIndex = dragRect.value.rectIndex;
|
| | | const rect = layouts.value[layoutIndex].rects[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
|
| | | );
|
| | |
|
| | | // 拖动结束后自动对齐到最近的整数位置
|
| | | rect.x = Math.round(rect.x);
|
| | | rect.y = Math.round(rect.y);
|
| | |
|
| | | // 只调整位置对齐,不调整大小
|
| | | //adjustAlignmentPosition(layoutIndex, rectIndex);
|
| | |
|
| | | // 调整灰色矩形
|
| | | adjustGrayRectangles(layoutIndex);
|
| | | glassDetail.x = parseFloat(glassDetail.x.toFixed(2));
|
| | | glassDetail.y = parseFloat(glassDetail.y.toFixed(2));
|
| | | adjustAlignmentPosition(layoutIndex, rectIndex);
|
| | | }
|
| | |
|
| | | dragging.value = false;
|
| | |
| | |
|
| | | const adjustAlignmentPosition = (layoutIndex, rectIndex) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const rect = layout.rects[rectIndex];
|
| | | const otherRects = layout.rects.filter((r, i) => i !== rectIndex);
|
| | | const glassDetail = layout.glassDetails[rectIndex];
|
| | | const otherRects = layout.glassDetails.filter((r, i) => i !== rectIndex);
|
| | |
|
| | | const threshold = Math.max(rect.w, rect.h) * 0.1;
|
| | | const threshold = Math.max(glassDetail.width, glassDetail.height) * 0.1;
|
| | |
|
| | | otherRects.forEach(otherRect => {
|
| | | if (Math.abs(rect.x - otherRect.x) < threshold) {
|
| | | rect.x = Math.round((rect.x + otherRect.x) / 2);
|
| | | // 水平对齐
|
| | | if (Math.abs(glassDetail.x - otherRect.x) < threshold) {
|
| | | glassDetail.x = Math.round((glassDetail.x + otherRect.x) / 2);
|
| | | }
|
| | | if (Math.abs((rect.x + rect.w) - (otherRect.x + otherRect.w)) < threshold) {
|
| | | // 不调整宽度
|
| | | // 水平对齐右侧边缘
|
| | | if (Math.abs((glassDetail.x + glassDetail.width) - (otherRect.x + otherRect.width)) < threshold) {
|
| | | glassDetail.x = Math.round((otherRect.x + otherRect.width - glassDetail.width));
|
| | | }
|
| | | if (Math.abs(rect.y - otherRect.y) < threshold) {
|
| | | rect.y = Math.round((rect.y + otherRect.y) / 2);
|
| | | // 垂直对齐
|
| | | if (Math.abs(glassDetail.y - otherRect.y) < threshold) {
|
| | | glassDetail.y = Math.round((glassDetail.y + otherRect.y) / 2);
|
| | | }
|
| | | if (Math.abs((rect.y + rect.h) - (otherRect.y + otherRect.h)) < threshold) {
|
| | | // 不调整高度
|
| | | // 垂直对齐上边缘
|
| | | if (Math.abs((glassDetail.y + glassDetail.height) - (otherRect.y + otherRect.height)) < threshold) {
|
| | | glassDetail.y = Math.round((otherRect.y + otherRect.height - glassDetail.height));
|
| | | }
|
| | | });
|
| | |
|
| | | // 确保矩形不会超出布局边界
|
| | | glassDetail.x = Math.max(0, glassDetail.x);
|
| | | glassDetail.y = Math.max(0, glassDetail.y);
|
| | | glassDetail.x = Math.min(glassDetail.x, layout.width - glassDetail.width);
|
| | | glassDetail.y = Math.min(glassDetail.y, layout.height - glassDetail.height);
|
| | |
|
| | | // 调整后重新计算灰色余料
|
| | | adjustGrayRectangles(layoutIndex);
|
| | | };
|
| | |
|
| | | const mergeGrayRects = (layoutIndex, rectIndex, direction) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const rect = layout.rects[rectIndex];
|
| | | const grayRects = layout.rects.filter(r => r.isRemain);
|
| | | const mergeAdjacentGrayRects = (glassDetails, totalWidth, totalHeight) => {
|
| | | const grayRects = glassDetails.filter(r => r.isRemain);
|
| | | const nonGrayRects = glassDetails.filter(r => !r.isRemain);
|
| | |
|
| | | let targetRect = null;
|
| | |
|
| | | switch (direction) {
|
| | | case 'left':
|
| | | targetRect = grayRects.find(r => r.x + r.w === rect.x && r.y === rect.y && r.h === rect.h);
|
| | | break;
|
| | | case 'right':
|
| | | targetRect = grayRects.find(r => r.x === rect.x + rect.w && r.y === rect.y && r.h === rect.h);
|
| | | break;
|
| | | case 'up':
|
| | | targetRect = grayRects.find(r => r.y + r.h === rect.y && r.x === rect.x && r.w === rect.w);
|
| | | break;
|
| | | case 'down':
|
| | | targetRect = grayRects.find(r => r.y === rect.y + rect.h && r.x === rect.x && r.w === rect.w);
|
| | | break;
|
| | | }
|
| | |
|
| | | if (targetRect) {
|
| | | const mergedRect = {
|
| | | x: Math.min(rect.x, targetRect.x),
|
| | | y: Math.min(rect.y, targetRect.y),
|
| | | w: Math.max(rect.x + rect.w, targetRect.x + targetRect.w) - Math.min(rect.x, targetRect.x),
|
| | | h: Math.max(rect.y + rect.h, targetRect.y + targetRect.h) - Math.min(rect.y, targetRect.y),
|
| | | isRemain: true
|
| | | };
|
| | |
|
| | | const index = layout.rects.indexOf(rect);
|
| | | layout.rects.splice(index, 1);
|
| | | const targetIndex = layout.rects.indexOf(targetRect);
|
| | | layout.rects.splice(targetIndex, 1);
|
| | | layout.rects.push(mergedRect);
|
| | |
|
| | | adjustGrayRectangles(layoutIndex);
|
| | | } else {
|
| | | ElMessage.warning('无法合并,没有相邻的灰色矩形');
|
| | | }
|
| | | };
|
| | |
|
| | | const mergeAdjacentGrayRects = (rects) => {
|
| | | const grayRects = rects.filter(r => r.isRemain);
|
| | | let merged = [];
|
| | | |
| | | // 按坐标排序,优先按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.w && |
| | | current.y === last.y && |
| | | current.h === last.h) {
|
| | | last.w += current.w;
|
| | | last.x = Math.round(last.x);
|
| | | last.y = Math.round(last.y);
|
| | | last.w = Math.round(last.w);
|
| | | last.h = Math.round(last.h);
|
| | | } else if (current.y === last.y + last.h && |
| | | current.x === last.x && |
| | | current.w === last.w) {
|
| | | last.h += current.h;
|
| | | last.x = Math.round(last);
|
| | | last.y = Math.round(last.y);
|
| | | last.w = Math.round(last.w);
|
| | | last.h = Math.round(last.h);
|
| | | } else {
|
| | | merged.push({ |
| | | x: Math.round(current.x),
|
| | | y: Math.round(current.y),
|
| | | w: Math.round(current.w),
|
| | | h: Math.round(current.h),
|
| | | // 检查是否可以水平合并(同一行,高度相同,相邻)
|
| | | 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: current.x,
|
| | | y: current.y,
|
| | | width: current.width,
|
| | | height: current.height,
|
| | | isRemain: true
|
| | | });
|
| | | current = { ...next };
|
| | | }
|
| | | }
|
| | |
|
| | | const nonGray = rects.filter(r => !r.isRemain);
|
| | | rects.splice(0, rects.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 rects = layout.rects;
|
| | | const nonGrayRects = rects.filter(rect => !rect.isRemain);
|
| | | const glassDetails = layout.glassDetails;
|
| | |
|
| | | const remainingAreas = calculateRemainingAreas(layout.width, layout.height, nonGrayRects);
|
| | | // 1. 筛选出非余料的玻璃片(即实际要切割的玻璃片)
|
| | | const nonGrayRects = glassDetails.filter(glassDetail => !glassDetail.isRemain);
|
| | |
|
| | | const currentGrayRects = rects.filter(r => r.isRemain);
|
| | | currentGrayRects.forEach((_, index) => {
|
| | | if (index >= remainingAreas.length) {
|
| | | rects.splice(index, 1);
|
| | | }
|
| | | // 2. 计算剩余可用区域
|
| | | let remainingAreas = calculateRemainingAreas(layout.width, layout.height, nonGrayRects);
|
| | |
|
| | | // 3. 去重处理 - 更严格的去重逻辑
|
| | | const uniqueArr = removeDuplicateAreas(remainingAreas);
|
| | |
|
| | | // 4. 获取当前已存在的余料矩形(需要保留引用以便更新)
|
| | | const currentGrayRects = glassDetails.filter(r => r.isRemain);
|
| | |
|
| | | // 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
|
| | | });
|
| | | });
|
| | |
|
| | | remainingAreas.forEach((area, index) => {
|
| | | if (index < currentGrayRects.length) {
|
| | | currentGrayRects[index].x = Math.round(area.x);
|
| | | currentGrayRects[index].y = Math.round(area.y);
|
| | | currentGrayRects[index].w = Math.round(area.w);
|
| | | currentGrayRects[index].h = Math.round(area.h);
|
| | | } else {
|
| | | rects.push({
|
| | | x: Math.round(area.x),
|
| | | y: Math.round(area.y),
|
| | | w: Math.round(area.w),
|
| | | h: Math.round(area.h),
|
| | | isRemain: true
|
| | | });
|
| | | }
|
| | | });
|
| | | // 7. 更新布局的玻璃详情
|
| | | layout.glassDetails = newGlassDetails;
|
| | |
|
| | | mergeAdjacentGrayRects(rects);
|
| | | // 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) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const rect = layout.rects[rectIndex];
|
| | | const originalState = { ...rect };
|
| | | const glassDetail = layout.glassDetails[rectIndex];
|
| | | const originalState = { ...glassDetail };
|
| | |
|
| | | // 旋转矩形
|
| | | const temp = rect.w;
|
| | | rect.w = rect.h;
|
| | | rect.h = temp;
|
| | | const temp = glassDetail.width;
|
| | | glassDetail.width = glassDetail.height;
|
| | | glassDetail.height = temp;
|
| | |
|
| | | // 检查旋转后是否与其他蓝色矩形重叠
|
| | | const otherRects = layout.rects.filter(r => !r.isRemain && r !== rect);
|
| | | const otherRects = layout.glassDetails.filter(r => !r.isRemain && r !== glassDetail);
|
| | | let isValidRotation = true;
|
| | |
|
| | | otherRects.forEach(otherRect => {
|
| | | if (checkOverlap(rect, otherRect)) {
|
| | | if (checkOverlap(glassDetail, otherRect)) {
|
| | | isValidRotation = false;
|
| | | }
|
| | | });
|
| | |
|
| | | // 检查是否超出布局边界
|
| | | if (rect.x + rect.w > layout.width || rect.y + rect.h > layout.height) {
|
| | | if (glassDetail.x + glassDetail.width > layout.width || glassDetail.y + glassDetail.height > layout.height) {
|
| | | isValidRotation = false;
|
| | | }
|
| | |
|
| | | 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 {
|
| | | // 恢复原状
|
| | | rect.w = originalState.w;
|
| | | rect.h = originalState.h;
|
| | | glassDetail.width = originalState.width;
|
| | | glassDetail.height = originalState.height;
|
| | | ElMessage.warning('无法旋转,存在重叠或超出边界');
|
| | | }
|
| | | };
|
| | |
|
| | | //移动旋转方法
|
| | | const moveRectAndRotate = (layoutIndex, rectIndex, direction) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const rect = layout.rects[rectIndex];
|
| | | const grayRects = layout.rects.filter(r => r.isRemain);
|
| | | const glassDetail = layout.glassDetails[rectIndex];
|
| | | const grayRects = layout.glassDetails.filter(r => r.isRemain);
|
| | |
|
| | | // 旋转矩形
|
| | | const temp = rect.w;
|
| | | rect.w = rect.h;
|
| | | rect.h = temp;
|
| | | const temp = glassDetail.width;
|
| | | glassDetail.width = glassDetail.height;
|
| | | glassDetail.height = temp;
|
| | |
|
| | | // 检查旋转后的矩形是否可以放置在某个灰色矩形的位置
|
| | | const canPlace = grayRects.some(grayRect => {
|
| | | return grayRect.w >= rect.w && grayRect.h >= rect.h;
|
| | | return grayRect.width >= glassDetail.width && grayRect.height >= glassDetail.height;
|
| | | });
|
| | |
|
| | | if (!canPlace) {
|
| | | // 如果不能放置,恢复原状
|
| | | const temp = rect.w;
|
| | | rect.w = rect.h;
|
| | | rect.h = temp;
|
| | | const temp = glassDetail.width;
|
| | | glassDetail.width = glassDetail.height;
|
| | | glassDetail.height = temp;
|
| | | ElMessage.warning('无法旋转,没有足够的空间');
|
| | | return;
|
| | | }
|
| | |
|
| | | // 调整灰色矩形
|
| | | adjustGrayRectangles(layoutIndex);
|
| | |
|
| | | // 移动矩形
|
| | | moveRect(layoutIndex, rectIndex, direction);
|
| | | };
|
| | |
|
| | | //移动方法
|
| | | const moveRect = (layoutIndex, rectIndex, direction) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const rect = layout.rects[rectIndex];
|
| | | const originalState = { ...rect };
|
| | | const glassDetail = layout.glassDetails[rectIndex];
|
| | | const originalState = { ...glassDetail };
|
| | |
|
| | | // 计算剩余空间
|
| | | const remainingAreas = calculateRemainingAreas(layout.width, layout.height, layout.rects.filter(r => !r.isRemain));
|
| | | |
| | | // 根据方向计算可移动的最大步长
|
| | | let maxStep = 0;
|
| | | const obstacles = layout.glassDetails.filter(r => r.isRemain || r !== glassDetail);
|
| | |
|
| | | switch (direction) {
|
| | | case 'up':
|
| | | maxStep = rect.y;
|
| | | maxStep = getAvailableSpaceUp(glassDetail, layout, obstacles);
|
| | | break;
|
| | | case 'down':
|
| | | maxStep = layout.height - (rect.y + rect.h);
|
| | | maxStep = getAvailableSpaceDown(glassDetail, layout, obstacles);
|
| | | break;
|
| | | case 'left':
|
| | | maxStep = rect.x;
|
| | | maxStep = getAvailableSpaceLeft(glassDetail, layout, obstacles);
|
| | | break;
|
| | | case 'right':
|
| | | maxStep = layout.width - (rect.x + rect.w);
|
| | | maxStep = getAvailableSpaceRight(glassDetail, layout, obstacles);
|
| | | break;
|
| | | }
|
| | |
|
| | | // 移动步长,根据剩余空间动态调整
|
| | | const stepSize = maxStep;
|
| | | const actualStep = Math.min(maxStep, stepSize);
|
| | | if (maxStep <= 0) {
|
| | | ElMessage.warning('无法移动,没有足够的空间');
|
| | | return;
|
| | | }
|
| | |
|
| | | // 移动矩形
|
| | |
|
| | | // 保存原始坐标
|
| | | const originalX = glassDetail.x;
|
| | | const originalY = glassDetail.y;
|
| | |
|
| | | switch (direction) {
|
| | | case 'up':
|
| | | rect.y -= actualStep;
|
| | | glassDetail.y += maxStep;
|
| | | break;
|
| | | case 'down':
|
| | | rect.y += actualStep;
|
| | | glassDetail.y -= maxStep;
|
| | | break;
|
| | | case 'left':
|
| | | rect.x -= actualStep;
|
| | | glassDetail.x -= maxStep;
|
| | | break;
|
| | | case 'right':
|
| | | rect.x += actualStep;
|
| | | glassDetail.x += maxStep;
|
| | | break;
|
| | | }
|
| | |
|
| | | // 检查是否与其他蓝色矩形重叠
|
| | | const otherRects = layout.rects.filter(r => !r.isRemain && r !== rect);
|
| | | const otherRects = layout.glassDetails.filter(r => !r.isRemain && r !== glassDetail);
|
| | | let isValidMove = true;
|
| | |
|
| | | otherRects.forEach(otherRect => {
|
| | | if (checkOverlap(rect, otherRect)) {
|
| | | if (checkOverlap(glassDetail, otherRect)) {
|
| | | isValidMove = false;
|
| | | }
|
| | | });
|
| | |
|
| | | // 检查是否超出布局边界
|
| | | if (rect.x < 0 || rect.y < 0 ||
|
| | | rect.x + rect.w > layout.width ||
|
| | | rect.y + rect.h > layout.height) {
|
| | | if (glassDetail.x < 0 || glassDetail.y < 0 ||
|
| | | glassDetail.x + glassDetail.width > layout.width ||
|
| | | glassDetail.y + glassDetail.height > layout.height) {
|
| | | isValidMove = false;
|
| | | }
|
| | |
|
| | | 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 {
|
| | | // 恢复原状
|
| | | rect.x = originalState.x;
|
| | | rect.y = originalState.y;
|
| | | glassDetail.x = originalState.x;
|
| | | glassDetail.y = originalState.y;
|
| | | ElMessage.warning('无法移动,存在重叠或超出边界');
|
| | | }
|
| | | };
|
| | |
|
| | | //删除小片
|
| | | const deleteRect = (layoutIndex, rectIndex) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | layout.rects.splice(rectIndex, 1);
|
| | | layout.glassDetails.splice(rectIndex, 1);
|
| | | adjustGrayRectangles(layoutIndex);
|
| | | };
|
| | |
|
| | | //判断原片跟小片移动是否超出
|
| | | const checkOverlap = (rect1, rect2) => {
|
| | | return !(rect1.x + rect1.w <= rect2.x ||
|
| | | rect1.x >= rect2.x + rect2.w ||
|
| | | rect1.y + rect1.h <= rect2.y ||
|
| | | rect1.y >= rect2.y + rect2.h);
|
| | | return !(rect1.x + rect1.width <= rect2.x ||
|
| | | rect1.x >= rect2.x + rect2.width ||
|
| | | rect1.y + rect1.height <= rect2.y ||
|
| | | rect1.y >= rect2.y + rect2.height);
|
| | | };
|
| | |
|
| | | const calculateRemainingAreas = (totalWidth, totalHeight, obstacles) => {
|
| | | let remaining = [{ x: 0, y: 0, w: totalWidth, h: totalHeight }];
|
| | | obstacles.forEach(rect => {
|
| | | remaining = cutRemainingAreas(remaining, rect);
|
| | | });
|
| | | return remaining;
|
| | | };
|
| | | //重新计算余料坐标以及尺寸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 cutRemainingAreas = (remainingAreas, obstacle) => {
|
| | | //重新计算余料坐标以及尺寸2
|
| | | 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,
|
| | | w: obstacle.x - area.x,
|
| | | h: area.h
|
| | | });
|
| | | }
|
| | | if (obstacle.x + obstacle.w < area.x + area.w) {
|
| | | newRemaining.push({
|
| | | x: obstacle.x + obstacle.w,
|
| | | y: area.y,
|
| | | w: area.w - (obstacle.x + obstacle.w - area.x),
|
| | | h: area.h
|
| | | });
|
| | | }
|
| | | if (obstacle.y > area.y) {
|
| | | newRemaining.push({
|
| | | x: area.x,
|
| | | y: area.y,
|
| | | w: area.w,
|
| | | h: obstacle.y - area.y
|
| | | });
|
| | | }
|
| | | if (obstacle.y + obstacle.h < area.y + area.h) {
|
| | | newRemaining.push({
|
| | | x: area.x,
|
| | | y: obstacle.y + obstacle.h,
|
| | | w: area.w,
|
| | | h: area.h - (obstacle.y + obstacle.h - area.y)
|
| | | });
|
| | | }
|
| | | } else {
|
| | | // 如果障碍物与当前区域没有重叠,保留原区域
|
| | | 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;
|
| | | };
|
| | |
|
| | | //定义每个版图的综合内容
|
| | | const getCurrentRectInfo = (layoutIndex) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const rect = layout.rects[focusIndex.value?.rectIndex || 0];
|
| | | if (!rect) return '';
|
| | | /* const glassDetail = layout.glassDetails[focusIndex.value?.rectIndex || 0];
|
| | | if (!glassDetail) return '';*/
|
| | | const totalRects = layouts.value.length;
|
| | | const currentRectIndex = layoutIndex + 1;
|
| | | const width = layout.width;
|
| | | const height = layout.height;
|
| | | const percentage = ((rect.w / layout.width) * 100).toFixed(1) + '%';
|
| | | const width = layout.realWidth;
|
| | | const height = layout.realHeight;
|
| | | const percentage = (layout.usageRate * 100).toFixed(2) + '%';
|
| | | return `${currentRectIndex}/${totalRects} ${width}×${height} ×1 ${percentage}`;
|
| | | };
|
| | |
|
| | | //点击左边版图切换
|
| | | const selectLayout = (layoutIndex) => {
|
| | | selectedLayoutIndex.value = layoutIndex;
|
| | | };
|
| | |
|
| | | //把传输的数据赋值
|
| | | const updateLayout = () => {
|
| | | if (!layoutPanel.value) return;
|
| | | layouts.value = props.layoutData.Layouts;
|
| | | layoutsHead.value = props.layoutData;
|
| | | layouts.value = props.layoutData.layouts;
|
| | | };
|
| | |
|
| | | //向上移动计算坐标
|
| | | const getAvailableSpaceUp = (glassDetail, layout, obstacles) => {
|
| | | let maxSpace = layout.height - glassDetail.y - glassDetail.height;
|
| | | obstacles.forEach(obstacle => {
|
| | | if (obstacle.y > glassDetail.y + glassDetail.height &&
|
| | | obstacle.x <= glassDetail.x + glassDetail.width &&
|
| | | obstacle.x + obstacle.width >= glassDetail.x) {
|
| | | maxSpace = Math.min(maxSpace, obstacle.y - (glassDetail.y + glassDetail.height));
|
| | | }
|
| | | });
|
| | | return maxSpace;
|
| | | };
|
| | |
|
| | | //向下移动计算坐标
|
| | | const getAvailableSpaceDown = (glassDetail, layout, obstacles) => {
|
| | | let maxSpace = glassDetail.y;
|
| | | obstacles.forEach(obstacle => {
|
| | | if (obstacle.y + obstacle.height < glassDetail.y &&
|
| | | obstacle.x <= glassDetail.x + glassDetail.width &&
|
| | | obstacle.x + obstacle.width >= glassDetail.x) {
|
| | | maxSpace = Math.min(maxSpace, glassDetail.y - (obstacle.y + obstacle.height));
|
| | | }
|
| | | });
|
| | | return maxSpace;
|
| | | };
|
| | |
|
| | | //向左移动计算坐标
|
| | | const getAvailableSpaceLeft = (glassDetail, layout, obstacles) => {
|
| | | let maxSpace = glassDetail.x;
|
| | | obstacles.forEach(obstacle => {
|
| | | if (obstacle.x + obstacle.width < glassDetail.x &&
|
| | | obstacle.y <= glassDetail.y + glassDetail.height &&
|
| | | obstacle.y + obstacle.height >= glassDetail.y) {
|
| | | maxSpace = Math.min(maxSpace, glassDetail.x - (obstacle.x + obstacle.width));
|
| | | }
|
| | | });
|
| | | return maxSpace;
|
| | | };
|
| | |
|
| | | //向右移动计算坐标
|
| | | const getAvailableSpaceRight = (glassDetail, layout, obstacles) => {
|
| | | let maxSpace = layout.width - (glassDetail.x + glassDetail.width);
|
| | | obstacles.forEach(obstacle => {
|
| | | if (obstacle.x > glassDetail.x + glassDetail.width &&
|
| | | obstacle.y <= glassDetail.y + glassDetail.height &&
|
| | | obstacle.y + obstacle.height >= glassDetail.y) {
|
| | | maxSpace = Math.min(maxSpace, obstacle.x - (glassDetail.x + glassDetail.width));
|
| | | }
|
| | | });
|
| | | return maxSpace;
|
| | | };
|
| | |
|
| | | let moveInterval = null;
|
| | |
|
| | | //按下按键上下左右
|
| | | const handleKeyDown = (event) => {
|
| | | if (!focusIndex.value) return;
|
| | |
|
| | | const { layoutIndex, rectIndex } = focusIndex.value;
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const glassDetail = layout.glassDetails[rectIndex];
|
| | | const obstacles = layout.glassDetails.filter(r => r.isRemain || r !== glassDetail);
|
| | |
|
| | | switch (event.key) {
|
| | | case 'ArrowUp':
|
| | | event.preventDefault();
|
| | | if (!moveInterval) {
|
| | | moveInterval = setInterval(() => {
|
| | | moveRect(layoutIndex, rectIndex, 'down');
|
| | | }, 50);
|
| | | }
|
| | | break;
|
| | | case 'ArrowDown':
|
| | | event.preventDefault();
|
| | | if (!moveInterval) {
|
| | | moveInterval = setInterval(() => {
|
| | | moveRect(layoutIndex, rectIndex, 'up');
|
| | | }, 50);
|
| | | }
|
| | | break;
|
| | | case 'ArrowLeft':
|
| | | event.preventDefault();
|
| | | if (!moveInterval) {
|
| | | moveInterval = setInterval(() => {
|
| | | moveRect(layoutIndex, rectIndex, 'left');
|
| | | }, 50);
|
| | | }
|
| | | break;
|
| | | case 'ArrowRight':
|
| | | event.preventDefault();
|
| | | if (!moveInterval) {
|
| | | moveInterval = setInterval(() => {
|
| | | moveRect(layoutIndex, rectIndex, 'right');
|
| | | }, 50);
|
| | | }
|
| | | break;
|
| | | }
|
| | | };
|
| | |
|
| | | //松开按键上下左右
|
| | | const handleKeyUp = (event) => {
|
| | | if (event.key === 'ArrowUp' || event.key === 'ArrowDown' ||
|
| | | event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
|
| | | if (moveInterval) {
|
| | | clearInterval(moveInterval);
|
| | | moveInterval = null;
|
| | | }
|
| | | }
|
| | | };
|
| | |
|
| | | //x镜像
|
| | | const mirrorLayoutX = (layoutIndex) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const width = layout.width;
|
| | | const glassDetails = [...layout.glassDetails]; // 创建副本避免直接修改
|
| | |
|
| | | glassDetails.forEach(glassDetail => {
|
| | | // 计算X镜像后的坐标
|
| | | const newX = width - glassDetail.x - glassDetail.width;
|
| | | const newY = glassDetail.y;
|
| | |
|
| | | // 更新矩形位置
|
| | | 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));
|
| | | });
|
| | | }
|
| | | });
|
| | |
|
| | | // 更新布局
|
| | | layout.glassDetails = glassDetails;
|
| | | adjustGrayRectangles(layoutIndex);
|
| | | };
|
| | |
|
| | | //y镜像
|
| | | const mirrorLayoutY = (layoutIndex) => {
|
| | | const layout = layouts.value[layoutIndex];
|
| | | const height = layout.height;
|
| | | const glassDetails = [...layout.glassDetails]; // 创建副本避免直接修改
|
| | |
|
| | | glassDetails.forEach(glassDetail => {
|
| | | // 计算Y镜像后的坐标
|
| | | const newX = glassDetail.x;
|
| | | const newY = height - glassDetail.y - glassDetail.height;
|
| | |
|
| | | // 更新矩形位置
|
| | | 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));
|
| | | });
|
| | | }
|
| | | });
|
| | |
|
| | | // 更新布局
|
| | | layout.glassDetails = glassDetails;
|
| | | adjustGrayRectangles(layoutIndex);
|
| | | };
|
| | |
|
| | | onMounted(() => {
|
| | | setTimeout(updateLayout, 1000);
|
| | | fetchSettings(username);
|
| | | updateLayout();
|
| | |
|
| | | selectedLayoutIndex.value = 0;
|
| | |
|
| | | clickEventListener = (event) => {
|
| | | const contextMenus = document.querySelectorAll('.context-menu');
|
| | | if (contextMenus.length > 0) {
|
| | | contextMenus.forEach(menu => menu.remove());
|
| | | }
|
| | | };
|
| | | document.addEventListener('click', clickEventListener);
|
| | |
|
| | | document.addEventListener('keydown', handleKeyDown);
|
| | | document.addEventListener('keyup', handleKeyUp);
|
| | | });
|
| | |
|
| | | onUnmounted(() => {
|
| | | rectsElements.value = {};
|
| | | if (clickEventListener) {
|
| | | document.removeEventListener('click', clickEventListener);
|
| | | clickEventListener = null;
|
| | | }
|
| | | document.removeEventListener('keydown', handleKeyDown);
|
| | | document.removeEventListener('keyup', handleKeyUp);
|
| | | });
|
| | | </script>
|
| | |
|
| | |
| | | .layout-wrapper {
|
| | | position: relative;
|
| | | margin-top: 50px;
|
| | | }
|
| | |
|
| | | .layout-glassDetail {
|
| | | user-select: none;
|
| | | }
|
| | |
|
| | | .layout-container {
|
| | |
| | | font-weight: bold;
|
| | | }
|
| | |
|
| | | .rect-content {
|
| | | display: grid;
|
| | | grid-template-columns: 1fr;
|
| | | grid-template-rows: 1fr;
|
| | | .glassDetail-content {
|
| | | display: flex;
|
| | | flex-direction: column;
|
| | | align-items: flex-start;
|
| | | padding: 5px;
|
| | | min-width: 60px;
|
| | | min-height: 20px;
|
| | | white-space: normal;
|
| | | overflow-wrap: break-word;
|
| | | }
|
| | |
|
| | | .size {
|
| | | grid-row: 1;
|
| | | grid-column: 1;
|
| | | color: #444;
|
| | | font-size: 12px;
|
| | | color: #444;
|
| | | white-space: normal;
|
| | | word-wrap: break-word;
|
| | | }
|
| | |
|
| | | .jia-hao {
|
| | | grid-row: 2;
|
| | | grid-column: 1;
|
| | | margin: auto;
|
| | | .jia-hao, .liuchengka {
|
| | | font-size: 14px;
|
| | | font-weight: bold;
|
| | | white-space: normal;
|
| | | word-wrap: break-word;
|
| | | }
|
| | |
|
| | | .sidebar-item {
|