<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>
|
</div>
|
|
<!-- Main Layout Panel -->
|
<div ref="layoutPanel" :class="panelClass" :style="panelStyle">
|
<div
|
v-for="(layout, layoutIndex) in layouts"
|
:key="layoutIndex"
|
class="layout-wrapper"
|
:style="{ display: selectedLayoutIndex === layoutIndex ? 'block' : 'none', top: '-150px' }"
|
>
|
<!-- Layout Info Label -->
|
<div class="layout-info" :style="layoutInfoStyle(layoutIndex)">
|
{{ getCurrentRectInfo(layoutIndex) }}
|
</div>
|
|
<!-- Layout Container -->
|
<div class="layout-container" :style="layoutContainerStyle(layoutIndex)">
|
<!-- 灰色矩形 -->
|
<div
|
v-for="(glassDetail, rectIndex) in layout.glassDetails.filter(r => r.isRemain)"
|
:key="`gray-${rectIndex}`"
|
:ref="(el) => { if (el) rectsElements[layoutIndex + '-' + rectIndex] = el }"
|
class="layout-glassDetail"
|
:style="rectStyle(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="(glassDetail, rectIndex) in layout.glassDetails.filter(r => !r.isRemain)"
|
:key="`blue-${rectIndex}`"
|
:ref="(el) => { if (el) rectsElements[layoutIndex + '-' + rectIndex] = el }"
|
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 v-if="showJiaHao" class="jia-hao">{{ glassDetail.rackNo }}</div>
|
<div v-if="showProcessId" class="liuchengka">{{ glassDetail.processId }}</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>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted, onUnmounted } from 'vue';
|
import request from "@/utils/request";
|
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 },
|
gh: { type: Number, default: 1000 },
|
style: { type: String, default: 'width:1000px;height:600px;display:block;background:gray' }
|
});
|
|
const emit = defineEmits(['rectClicked']);
|
const layoutPanel = ref(null);
|
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-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.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);
|
});
|
});
|
const savedProjectNo = localStorage.getItem('projectNo');
|
const processId = savedProjectNo;
|
layoutsHead.value.Layouts=layouts.value
|
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'));
|
} 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',
|
inputValidator: (value) => {
|
const values = value.split(',').map(v => v.trim());
|
if (values.length !== 2) {
|
return '请输入两个数字,用逗号分隔';
|
}
|
const [width, height] = values;
|
if (isNaN(width) || isNaN(height)) {
|
return '请输入有效的数字';
|
}
|
if (width <= 0 || height <= 0) {
|
return '宽度和高度必须大于0';
|
}
|
return true;
|
},
|
inputErrorMessage: '输入格式不正确'
|
})
|
.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];
|
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 findBestFitPosition = (layoutIndex, newRect) => {
|
const layout = layouts.value[layoutIndex];
|
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: `20px`,
|
top: `140px`,
|
width: `${layout.width * scale}px`,
|
height: `${layout.height * 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 = (glassDetail, layoutIndex) => {
|
const layout = layouts.value[layoutIndex];
|
const scale = Math.min(0.25
|
);
|
return {
|
position: 'absolute',
|
left: `${glassDetail.x * scale}px`,
|
bottom: `${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 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.bottom = `${event.clientY}px`;
|
contextMenu.style.backgroundColor = '#fff';
|
contextMenu.style.border = '1px solid #ccc';
|
contextMenu.style.padding = '5px';
|
contextMenu.style.zIndex = 1001;
|
|
const rotateItem = document.createElement('div');
|
rotateItem.textContent = '旋转';
|
rotateItem.style.cursor = 'pointer';
|
rotateItem.addEventListener('click', () => {
|
rotateRect(layoutIndex, rectIndex);
|
document.body.removeChild(contextMenu);
|
});
|
|
const moveUpAndRotateItem = document.createElement('div');
|
moveUpAndRotateItem.textContent = '向上移动并旋转';
|
moveUpAndRotateItem.style.cursor = 'pointer';
|
moveUpAndRotateItem.addEventListener('click', () => {
|
moveRectAndRotate(layoutIndex, rectIndex, 'up');
|
document.body.removeChild(contextMenu);
|
});
|
|
const moveDownAndRotateItem = document.createElement('div');
|
moveDownAndRotateItem.textContent = '向下移动并旋转';
|
moveDownAndRotateItem.style.cursor = 'pointer';
|
moveDownAndRotateItem.addEventListener('click', () => {
|
moveRectAndRotate(layoutIndex, rectIndex, 'down');
|
document.body.removeChild(contextMenu);
|
});
|
|
const moveLeftAndRotateItem = document.createElement('div');
|
moveLeftAndRotateItem.textContent = '向左移动并旋转';
|
moveLeftAndRotateItem.style.cursor = 'pointer';
|
moveLeftAndRotateItem.addEventListener('click', () => {
|
moveRectAndRotate(layoutIndex, rectIndex, 'left');
|
document.body.removeChild(contextMenu);
|
});
|
|
const moveRightAndRotateItem = document.createElement('div');
|
moveRightAndRotateItem.textContent = '向右移动并旋转';
|
moveRightAndRotateItem.style.cursor = 'pointer';
|
moveRightAndRotateItem.addEventListener('click', () => {
|
moveRectAndRotate(layoutIndex, rectIndex, 'right');
|
document.body.removeChild(contextMenu);
|
});
|
|
const moveUpItem = document.createElement('div');
|
moveUpItem.textContent = '向上移动';
|
moveUpItem.style.cursor = 'pointer';
|
moveUpItem.addEventListener('click', () => {
|
moveRect(layoutIndex, rectIndex, 'up');
|
document.body.removeChild(contextMenu);
|
});
|
|
const moveDownItem = document.createElement('div');
|
moveDownItem.textContent = '向下移动';
|
moveDownItem.style.cursor = 'pointer';
|
moveDownItem.addEventListener('click', () => {
|
moveRect(layoutIndex, rectIndex, 'down');
|
document.body.removeChild(contextMenu);
|
});
|
|
const moveLeftItem = document.createElement('div');
|
moveLeftItem.textContent = '向左移动';
|
moveLeftItem.style.cursor = 'pointer';
|
moveLeftItem.addEventListener('click', () => {
|
moveRect(layoutIndex, rectIndex, 'left');
|
document.body.removeChild(contextMenu);
|
});
|
|
const moveRightItem = document.createElement('div');
|
moveRightItem.textContent = '向右移动';
|
moveRightItem.style.cursor = 'pointer';
|
moveRightItem.addEventListener('click', () => {
|
moveRect(layoutIndex, rectIndex, 'right');
|
document.body.removeChild(contextMenu);
|
});
|
|
const deleteItem = document.createElement('div');
|
deleteItem.textContent = '删除';
|
deleteItem.style.cursor = 'pointer';
|
deleteItem.addEventListener('click', () => {
|
deleteRect(layoutIndex, rectIndex);
|
document.body.removeChild(contextMenu);
|
});
|
|
const addItem = document.createElement('div');
|
addItem.textContent = '添加成品';
|
addItem.style.cursor = 'pointer';
|
addItem.addEventListener('click', () => {
|
showAddDialog(layoutIndex, rectIndex);
|
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(moveLeftAndRotateItem);
|
contextMenu.appendChild(moveRightAndRotateItem);
|
contextMenu.appendChild(moveUpItem);
|
contextMenu.appendChild(moveDownItem);
|
contextMenu.appendChild(moveLeftItem);
|
contextMenu.appendChild(moveRightItem);
|
contextMenu.appendChild(deleteItem);
|
contextMenu.appendChild(addItem);
|
contextMenu.appendChild(mirrorXItem);
|
contextMenu.appendChild(mirrorYItem);
|
|
document.body.appendChild(contextMenu);
|
};
|
|
//余料右键菜单功能
|
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.bottom = `${event.clientY}px`;
|
contextMenu.style.backgroundColor = '#fff';
|
contextMenu.style.border = '1px solid #ccc';
|
contextMenu.style.padding = '5px';
|
contextMenu.style.zIndex = 1001;
|
|
const addItem = document.createElement('div');
|
addItem.textContent = '添加成品';
|
addItem.style.cursor = 'pointer';
|
addItem.addEventListener('click', () => {
|
showAddDialog(layoutIndex, rectIndex);
|
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(addItem);
|
contextMenu.appendChild(mirrorXItem);
|
contextMenu.appendChild(mirrorYItem);
|
|
document.body.appendChild(contextMenu);
|
};
|
|
//小片鼠标按下事件
|
const handleRectDragStart = (layoutIndex, rectIndex) => {
|
const layout = layouts.value[layoutIndex];
|
const glassDetail = layout.glassDetails[rectIndex];
|
if (glassDetail.isRemain) return;
|
|
dragging.value = true;
|
dragRect.value = { layoutIndex, rectIndex };
|
dragStartPos.value = {
|
x: event.clientX,
|
y: event.clientY
|
};
|
};
|
|
//小片鼠标移动事件
|
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 adjustAlignmentPosition = (layoutIndex, rectIndex) => {
|
const layout = layouts.value[layoutIndex];
|
const glassDetail = layout.glassDetails[rectIndex];
|
const otherRects = layout.glassDetails.filter((r, i) => i !== rectIndex);
|
|
const threshold = Math.max(glassDetail.width, glassDetail.height) * 0.1;
|
|
otherRects.forEach(otherRect => {
|
// 水平对齐
|
if (Math.abs(glassDetail.x - otherRect.x) < threshold) {
|
glassDetail.x = Math.round((glassDetail.x + otherRect.x) / 2);
|
}
|
// 水平对齐右侧边缘
|
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(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));
|
}
|
});
|
|
// 确保矩形不会超出布局边界
|
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 mergeAdjacentGrayRects = (glassDetails,totalWidth,totalHeight) => {
|
const grayRects = glassDetails.filter(r => r.isRemain);
|
const grayRects2 = glassDetails.filter(r => r.isRemain);
|
let merged = [];
|
|
grayRects.sort((a, b) => {
|
if (a.x !== b.x) return a.x - b.x;
|
return a.y - b.y;
|
});
|
|
if (grayRects.length === 0) return;
|
|
merged.push({ ...grayRects[0] });
|
|
for (let i = 1; i < grayRects.length; i++) {
|
const last = merged[merged.length-1];
|
const current = 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 {
|
merged.push({
|
x: Math.round(current.x),
|
y: Math.round(current.y),
|
width: Math.round(current.width),
|
height: Math.round(current.height),
|
isRemain: true
|
});
|
}
|
}
|
|
const nonGray = glassDetails.filter(r => !r.isRemain);
|
//删除原数组拼接新的小片跟余料
|
glassDetails.splice(0, glassDetails.length, ...nonGray, ...merged);
|
};
|
|
// 调整后重新计算灰色余料
|
const adjustGrayRectangles = (layoutIndex) => {
|
const layout = layouts.value[layoutIndex];
|
const glassDetails = layout.glassDetails;
|
//小片的数据
|
const nonGrayRects = glassDetails.filter(glassDetail => !glassDetail.isRemain);
|
|
//所有的小片余料坐标跟尺寸
|
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));
|
//余料的数据
|
const currentGrayRects = glassDetails.filter(r => r.isRemain);
|
//循环余料数据跟全部的对比
|
currentGrayRects.forEach((_, index) => {
|
if (index >= remainingAreas.length) {
|
glassDetails.splice(index, 1);
|
}
|
});
|
|
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
|
});
|
}
|
});
|
|
mergeAdjacentGrayRects(glassDetails,layout.width, layout.height);
|
};
|
|
//旋转方法
|
const rotateRect = (layoutIndex, rectIndex) => {
|
const layout = layouts.value[layoutIndex];
|
const glassDetail = layout.glassDetails[rectIndex];
|
const originalState = { ...glassDetail };
|
|
const temp = glassDetail.width;
|
glassDetail.width = glassDetail.height;
|
glassDetail.height = temp;
|
|
const otherRects = layout.glassDetails.filter(r => !r.isRemain && r !== glassDetail);
|
let isValidRotation = true;
|
|
otherRects.forEach(otherRect => {
|
if (checkOverlap(glassDetail, otherRect)) {
|
isValidRotation = false;
|
}
|
});
|
|
if (glassDetail.x + glassDetail.width > layout.width || glassDetail.y + glassDetail.height > layout.height) {
|
isValidRotation = false;
|
}
|
|
if (isValidRotation) {
|
adjustGrayRectangles(layoutIndex);
|
} else {
|
glassDetail.width = originalState.width;
|
glassDetail.height = originalState.height;
|
ElMessage.warning('无法旋转,存在重叠或超出边界');
|
}
|
};
|
|
//移动旋转方法
|
const moveRectAndRotate = (layoutIndex, rectIndex, direction) => {
|
const layout = layouts.value[layoutIndex];
|
const glassDetail = layout.glassDetails[rectIndex];
|
const grayRects = layout.glassDetails.filter(r => r.isRemain);
|
|
const temp = glassDetail.width;
|
glassDetail.width = glassDetail.height;
|
glassDetail.height = temp;
|
|
const canPlace = grayRects.some(grayRect => {
|
return grayRect.width >= glassDetail.width && grayRect.height >= glassDetail.height;
|
});
|
|
if (!canPlace) {
|
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 glassDetail = layout.glassDetails[rectIndex];
|
const originalState = { ...glassDetail };
|
|
let maxStep = 0;
|
const obstacles = layout.glassDetails.filter(r => r.isRemain || r !== glassDetail);
|
|
switch (direction) {
|
case 'up':
|
maxStep = getAvailableSpaceUp(glassDetail, layout, obstacles);
|
break;
|
case 'down':
|
maxStep = getAvailableSpaceDown(glassDetail, layout, obstacles);
|
break;
|
case 'left':
|
maxStep = getAvailableSpaceLeft(glassDetail, layout, obstacles);
|
break;
|
case 'right':
|
maxStep = getAvailableSpaceRight(glassDetail, layout, obstacles);
|
break;
|
}
|
|
if (maxStep <= 0) {
|
ElMessage.warning('无法移动,没有足够的空间');
|
return;
|
}
|
|
switch (direction) {
|
case 'up':
|
glassDetail.y += maxStep;
|
break;
|
case 'down':
|
glassDetail.y -= maxStep;
|
break;
|
case 'left':
|
glassDetail.x -= maxStep;
|
break;
|
case 'right':
|
glassDetail.x += maxStep;
|
break;
|
}
|
|
const otherRects = layout.glassDetails.filter(r => !r.isRemain && r !== glassDetail);
|
let isValidMove = true;
|
|
otherRects.forEach(otherRect => {
|
if (checkOverlap(glassDetail, otherRect)) {
|
isValidMove = false;
|
}
|
});
|
|
if (glassDetail.x < 0 || glassDetail.y < 0 ||
|
glassDetail.x + glassDetail.width > layout.width ||
|
glassDetail.y + glassDetail.height > layout.height) {
|
isValidMove = false;
|
}
|
|
if (isValidMove) {
|
adjustGrayRectangles(layoutIndex);
|
} else {
|
glassDetail.x = originalState.x;
|
glassDetail.y = originalState.y;
|
ElMessage.warning('无法移动,存在重叠或超出边界');
|
}
|
};
|
|
//删除小片
|
const deleteRect = (layoutIndex, rectIndex) => {
|
const layout = layouts.value[layoutIndex];
|
layout.glassDetails.splice(rectIndex, 1);
|
adjustGrayRectangles(layoutIndex);
|
};
|
|
//判断原片跟小片移动是否超出
|
const checkOverlap = (rect1, rect2) => {
|
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);
|
};
|
|
//重新计算余料坐标以及尺寸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;
|
};
|
|
//重新计算余料坐标以及尺寸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,
|
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 {
|
newRemaining.push(area);
|
}
|
});
|
return newRemaining;
|
};
|
|
//定义每个版图的综合内容
|
const getCurrentRectInfo = (layoutIndex) => {
|
const layout = layouts.value[layoutIndex];
|
/* 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 = (layout.usageRate * 100).toFixed(2) + '%';
|
return `${currentRectIndex}/${totalRects} ${width}×${height} ×1 ${percentage}`;
|
};
|
|
//点击左边版图切换
|
const selectLayout = (layoutIndex) => {
|
selectedLayoutIndex.value = layoutIndex;
|
};
|
|
//把传输的数据赋值
|
const updateLayout = () => {
|
if (!layoutPanel.value) return;
|
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, 'up');
|
}, 50);
|
}
|
break;
|
case 'ArrowDown':
|
event.preventDefault();
|
if (!moveInterval) {
|
moveInterval = setInterval(() => {
|
moveRect(layoutIndex, rectIndex, 'down');
|
}, 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;
|
});
|
|
// 更新布局
|
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;
|
});
|
|
// 更新布局
|
layout.glassDetails = glassDetails;
|
adjustGrayRectangles(layoutIndex);
|
};
|
|
onMounted(() => {
|
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>
|
|
<style scoped>
|
.layout-wrapper {
|
position: relative;
|
margin-top: 50px;
|
}
|
|
.layout-glassDetail {
|
user-select: none;
|
}
|
|
.layout-container {
|
position: relative;
|
overflow: visible;
|
}
|
|
.layout-info {
|
color: #444;
|
font-size: 12px;
|
background-color: #ffffff;
|
padding: 5px 10px;
|
border-radius: 3px;
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
font-weight: bold;
|
}
|
|
.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 {
|
font-size: 12px;
|
color: #444;
|
white-space: normal;
|
word-wrap: break-word;
|
}
|
|
.jia-hao, .liuchengka {
|
font-size: 14px;
|
font-weight: bold;
|
white-space: normal;
|
word-wrap: break-word;
|
}
|
|
.sidebar-item {
|
padding: 10px;
|
cursor: pointer;
|
}
|
|
.sidebar-item.selected {
|
background: #ddd;
|
}
|
|
.context-menu {
|
position: absolute;
|
background-color: #fff;
|
border: 1px solid #ccc;
|
padding: 5px;
|
z-index: 1001;
|
}
|
|
.context-menu div {
|
padding: 5px;
|
cursor: pointer;
|
}
|
|
.context-menu div:hover {
|
background-color: #f0f0f0;
|
}
|
</style>
|