north-glass-erp/northglass-erp/package-lock.json
@@ -42,25 +42,28 @@ } }, "node_modules/@babel/helper-string-parser": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "version": "7.25.9", "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "version": "7.25.9", "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "version": "7.27.0", "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.27.0.tgz", "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dependencies": { "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" }, @@ -85,13 +88,12 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/@babel/types": { "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "version": "7.27.0", "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.27.0.tgz", "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dependencies": { "@babel/helper-string-parser": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -7201,14 +7203,6 @@ "utrie": "^1.0.2" } }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "engines": { "node": ">=4" } }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7594,19 +7588,22 @@ }, "dependencies": { "@babel/helper-string-parser": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" "version": "7.25.9", "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==" }, "@babel/helper-validator-identifier": { "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" "version": "7.25.9", "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" }, "@babel/parser": { "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==" "version": "7.27.0", "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.27.0.tgz", "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "requires": { "@babel/types": "^7.27.0" } }, "@babel/runtime": { "version": "7.26.10", @@ -7624,13 +7621,12 @@ } }, "@babel/types": { "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "version": "7.27.0", "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.27.0.tgz", "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "requires": { "@babel/helper-string-parser": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "@claviska/jquery-minicolors": { @@ -8846,19 +8842,22 @@ }, "dependencies": { "@babel/helper-string-parser": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" "version": "7.25.9", "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==" }, "@babel/helper-validator-identifier": { "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" "version": "7.25.9", "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" }, "@babel/parser": { "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==" "version": "7.27.0", "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.27.0.tgz", "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "requires": { "@babel/types": "^7.27.0" } }, "@babel/runtime": { "version": "7.26.10", @@ -8876,13 +8875,12 @@ } }, "@babel/types": { "version": "7.23.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "version": "7.27.0", "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.27.0.tgz", "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "requires": { "@babel/helper-string-parser": "^7.22.5", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "@claviska/jquery-minicolors": { @@ -15413,11 +15411,6 @@ "utrie": "^1.0.2" } }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -21040,11 +21033,6 @@ "requires": { "utrie": "^1.0.2" } }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" }, "to-regex-range": { "version": "5.0.1", north-glass-erp/northglass-erp/src/views/pp/glassOptimize/Optimization.vue
@@ -1,6 +1,7 @@ <template> <div> <RectRenderer v-if="dataLoaded" :layoutData="layoutData" :gw="1150" :gh="850" @@ -10,7 +11,7 @@ </template> <script setup> import { ref,onMounted } from 'vue'; import { ref, onMounted } from 'vue'; import RectRenderer from './page/OptimizationRect.vue'; import mockLayoutData from '../../../components/pp/MockData'; import request from "@/utils/request"; @@ -18,42 +19,34 @@ import { ElMessage } from "element-plus"; const { t } = useI18n(); //const layoutData = ref(mockLayoutData); // const processId = "P25030309"; const savedProjectNo = localStorage.getItem('projectNo'); const processId = savedProjectNo; console.log(processId) const layoutData = ref(null); const dataLoaded = ref(false); const selectLayout = () => { request.post(`/glassOptimize/selectOptimizeResult/${processId}`) .then((res) => { if (res.code == 200) { try { // 将字符串数据转换为对象 // console.log("原始数据:", res.data.data[0].Layouts); const parsedData = JSON.parse(res.data.data[0].Layouts); layoutData.value = parsedData; // console.log("解析后的数据:", layoutData.value); // console.log("数据类型:", typeof parsedData); ElMessage.success("打开版图成功") } catch (error) { ElMessage.error("解析数据时出错:", error); } } else { } }) .catch((error) => { console.error("请求失败:", error); ElMessage.error(t('basicData.msg.requestFailed')); }); request.post(`/glassOptimize/selectOptimizeResult/${processId}`) .then((res) => { if (res.code == 200) { try { const parsedData = JSON.parse(res.data.data[0].Layouts); layoutData.value = parsedData; dataLoaded.value = true; ElMessage.success("打开版图成功"); } catch (error) { ElMessage.error("解析数据时出错:", error); } } else { ElMessage.error(t('basicData.msg.requestFailed')); } }) .catch((error) => { console.error("请求失败:", error); ElMessage.error(t('basicData.msg.requestFailed')); }); } onMounted(() => { selectLayout(); }); </script> north-glass-erp/northglass-erp/src/views/pp/glassOptimize/OptimizationRectPrint.vue
@@ -1,150 +1,103 @@ <template> <div> <el-button id="button" type="primary" @click="handlePrint">打印版图</el-button> <RectRenderer ref="rectRenderer" :layoutData="layoutData" :gw="1400" :gh="1100" style="width: 100%; height: 800px; position: relative;" /> <el-select v-model="printLayout" placeholder="选择打印布局" @change="handleLayoutChange" style="width: 150px; margin-bottom: 10px;"> <el-option label="四行两列" value="4rows-2cols"></el-option> <el-option label="三行两列" value="3rows-2cols"></el-option> <el-option label="三行一列" value="3rows-1col"></el-option> <el-option label="两行两列" value="2rows-2cols"></el-option> </el-select> <div ref="printContainer" style="position: relative;"> <RectRenderer ref="rectRenderer" :layoutData="layoutData" :gw="currentGw" :gh="currentGh" :printLayout="printLayout" :printWidth="currentPrintWidth" :printHeight="currentPrintHeight" style="position: absolute;" v-if="dataLoaded" /> </div> </div> </template> <script setup> import { ref,onMounted } from 'vue'; import { ref, onMounted, watch } from 'vue'; import RectRenderer from './page/RectRenderer.vue'; import mockLayoutData from '../../../components/pp/MockData'; import request from "@/utils/request"; import { useI18n } from "vue-i18n"; //const layoutData = ref(mockLayoutData); const rectRenderer = ref(null); const printLayout = ref('2rows-2cols'); const rectRenderer = ref(null); const savedProjectNo = localStorage.getItem('projectNo'); const processId = savedProjectNo; const layoutData = ref(null); const dataLoaded = ref(false); // 定义不同布局对应的尺寸 const layoutDimensions = { '4rows-2cols': { width: 1000, height: 1000 }, '3rows-2cols': { width: 1000, height: 1000 }, '3rows-1col': { width: 1000, height: 1000 }, '2rows-2cols': { width: 1200, height: 1200 } }; // 当前布局的尺寸 const currentGw = ref(layoutDimensions[printLayout.value].width); const currentGh = ref(layoutDimensions[printLayout.value].height); const currentPrintWidth = ref(layoutDimensions[printLayout.value].width); const currentPrintHeight = ref(layoutDimensions[printLayout.value].height); const selectLayout = () => { request.post(`/glassOptimize/selectOptimizeResult/${processId}`) .then((res) => { if (res.code == 200) { try { const parsedData = JSON.parse(res.data.data[0].Layouts); layoutData.value = parsedData; } catch (error) { } } else { } }) .catch((error) => { console.error("请求失败:", error); ElMessage.error(t('basicData.msg.requestFailed')); }); } request.post(`/glassOptimize/selectOptimizeResult/${processId}`) .then((res) => { if (res.code == 200) { try { const parsedData = JSON.parse(res.data.data[0].Layouts); layoutData.value = parsedData; dataLoaded.value = true; } catch (error) { console.error("解析布局数据失败:", error); } } else { console.error("请求失败,状态码:", res.code); } }) .catch((error) => { console.error("请求失败:", error); }); }; onMounted(() => { selectLayout(); }); const handlePrint = () => { // 创建一个隐藏的iframe const iframe = document.createElement('iframe'); iframe.style.position = 'fixed'; iframe.style.top = '-100vh'; iframe.style.left = '-100vw'; iframe.style.width = '200%'; iframe.style.height = '200%'; document.body.appendChild(iframe); // 将RectRenderer的内容加载到iframe中 const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; iframeDoc.open(); iframeDoc.write(` <html> <head> <title>Print Layout</title> <style> @page { size: A4 landscape; margin: 20mm; } body { margin: 0; padding: 0; } .layout-wrapper { display: flex; flex-direction: column; } .layout-container { page-break-inside: avoid; break-inside: avoid; page-break-after: auto; margin-bottom: 20mm; } .layout-item { page-break-inside: avoid; break-inside: avoid; } </style> </head> <body> <div class="layout-wrapper"> ${rectRenderer.value.$el.outerHTML} </div> </body> </html> `); iframeDoc.close(); // 设置打印样式 const printStyle = iframeDoc.createElement('style'); printStyle.type = 'text/css'; printStyle.innerHTML = ` @page { size: A4 landscape; margin: 20mm; } body { -webkit-print-color-adjust: exact; } .layout-wrapper { display: flex; flex-direction: column; } .layout-container { page-break-inside: avoid; break-inside: avoid; page-break-after: auto; margin-bottom: 20mm; } .layout-item { page-break-inside: avoid; break-inside: avoid; } `; iframeDoc.head.appendChild(printStyle); // 调整iframe大小以适应内容 const contentWidth = rectRenderer.value.$el.offsetWidth; const contentHeight = rectRenderer.value.$el.offsetHeight; iframe.width = contentWidth + 'px'; iframe.height = contentHeight + 'px'; // 执行打印 iframe.contentWindow.print(); // 清理 setTimeout(() => { document.body.removeChild(iframe); }, 100); if (rectRenderer.value) { rectRenderer.value.print(); } }; const handleLayoutChange = () => { // 更新布局尺寸 const dimensions = layoutDimensions[printLayout.value]; currentGw.value = dimensions.width; currentGh.value = dimensions.height; currentPrintWidth.value = dimensions.width; currentPrintHeight.value = dimensions.height; if (rectRenderer.value) { rectRenderer.value.updateLayout(); } }; // 监听布局变化 watch(printLayout, (newVal) => { handleLayoutChange(); }); </script> north-glass-erp/northglass-erp/src/views/pp/glassOptimize/OptimizeControl.vue
@@ -5,6 +5,7 @@ :gw="1400" :gh="1100" style="width: 1500px; height: 800px; position: relative;" v-if="dataLoaded" /> @@ -25,23 +26,10 @@ const { t } = useI18n(); // const layoutData = ref(mockLayoutData); const savedProjectNo = localStorage.getItem('projectNo'); const processId = savedProjectNo; const layoutData = ref(null); const dataLoaded = ref(false); const selectLayout = () => { @@ -51,6 +39,7 @@ try { const parsedData = JSON.parse(res.data.data[0].Layouts); layoutData.value = parsedData; dataLoaded.value = true; } catch (error) { north-glass-erp/northglass-erp/src/views/pp/glassOptimize/page/OptimizationRect.vue
@@ -50,6 +50,7 @@ @mousemove="handleRectDragging" @mouseup="handleRectDragEnd" @mouseleave="handleRectDragEnd" @click="handleRectClick(layoutIndex, rectIndex)" > <div class="rect-content"> <div class="size">{{ rect.w }}×{{ rect.h }}</div> @@ -78,8 +79,7 @@ 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 }, @@ -100,10 +100,10 @@ const dragging = ref(false); const dragStartPos = ref({ x: 0, y: 0 }); const dragRect = ref(null); const showJiaHao = ref(false); // 新增:控制jia-hao的显示状态 const showProcessId= ref(false); const themeColor=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 => { @@ -129,8 +129,6 @@ }); }; //获取优化设置 const fetchSettings = async (username) => { try { const response = await request.post(`/glassOptimize/selectOptimizeParms/${username}`); @@ -140,23 +138,15 @@ return; } const parsedData = JSON.parse(response.data); console.log(parsedData.display.frameNumber) if (parsedData.display && parsedData.display.frameNumber) { if (parsedData.display && parsedData.frameNumber) { showJiaHao.value = parsedData.display.frameNumber; } if (parsedData.display && parsedData.display.orderNumber) { if (parsedData.display && parsedData.orderNumber) { showProcessId.value = parsedData.display.orderNumber; } if (parsedData.display ) { if (parsedData.display) { themeColor.value = parsedData.display.themeColor; } console.log( parsedData); } else { console.error('请求失败,状态码:', response.code); } @@ -164,8 +154,6 @@ console.error('请求发生错误:', error); } }; const showAddDialog = (layoutIndex, rectIndex) => { ElMessageBox.prompt('请输入成品的宽度和高度', '添加成品', { @@ -186,32 +174,61 @@ }, 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 }; addNewRect(layoutIndex, newRect); }) .catch(() => { // 用户取消 }); .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 }; 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.rects.push(newRect); adjustGrayRectangles(layoutIndex); } else { ElMessage.warning('无法放置,没有足够的空间'); } }; const findBestFitPosition = (layoutIndex, newRect) => { const layout = layouts.value[layoutIndex]; const obstacles = layout.rects.filter(r => !r.isRemain); let bestFit = null; let minAreaDifference = Infinity; const remainingAreas = calculateRemainingAreas(layout.width, layout.height, obstacles); remainingAreas.forEach(area => { if (newRect.w <= area.w && newRect.h <= area.h) { const areaDifference = Math.abs(area.w * area.h - newRect.w * newRect.h); if (areaDifference < minAreaDifference) { minAreaDifference = areaDifference; bestFit = { x: area.x, y: area.y, w: newRect.w, h: newRect.h }; } } }); return bestFit; }; const layoutContainerStyle = (layoutIndex) => { const containerWidth = (props.gw - 210) / 2; const containerHeight = (props.gh - 100) / 3; const layout = layouts.value[layoutIndex]; const scale = Math.min( (props.gw - 100) / layout.width, @@ -372,6 +389,22 @@ 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); @@ -383,6 +416,8 @@ contextMenu.appendChild(moveRightItem); contextMenu.appendChild(deleteItem); contextMenu.appendChild(addItem); contextMenu.appendChild(mirrorXItem); contextMenu.appendChild(mirrorYItem); document.body.appendChild(contextMenu); }; @@ -409,7 +444,25 @@ 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); }; @@ -442,22 +495,19 @@ const deltaX = event.clientX - dragStartPos.value.x; const deltaY = event.clientY - dragStartPos.value.y; // 只调整位置,不调整大小 const newRect = { ...rect }; newRect.x += deltaX / scale; newRect.y += deltaY / scale; // 检查是否与其他蓝色矩形重叠 const otherRects = layout.rects.filter(r => !r.isRemain && r !== rect); 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) { @@ -486,14 +536,9 @@ (props.gh - 100) / layout.height ); // 拖动结束后自动对齐到最近的整数位置 rect.x = Math.round(rect.x); rect.y = Math.round(rect.y); // 只调整位置对齐,不调整大小 adjustAlignmentPosition(layoutIndex, rectIndex); // 调整灰色矩形 adjustGrayRectangles(layoutIndex); } @@ -509,25 +554,38 @@ const threshold = Math.max(rect.w, rect.h) * 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((rect.x + rect.w) - (otherRect.x + otherRect.w)) < threshold) { // 不调整宽度 rect.x = Math.round((otherRect.x + otherRect.w - rect.w)); } // 垂直对齐 if (Math.abs(rect.y - otherRect.y) < threshold) { rect.y = Math.round((rect.y + otherRect.y) / 2); } // 垂直对齐下边缘 if (Math.abs((rect.y + rect.h) - (otherRect.y + otherRect.h)) < threshold) { // 不调整高度 rect.y = Math.round((otherRect.y + otherRect.h - rect.h)); } }); // 确保矩形不会超出布局边界 rect.x = Math.max(0, rect.x); rect.y = Math.max(0, rect.y); rect.x = Math.min(rect.x, layout.width - rect.w); rect.y = Math.min(rect.y, layout.height - rect.h); // 调整后重新计算灰色余料 adjustGrayRectangles(layoutIndex); }; const mergeAdjacentGrayRects = (rects) => { const grayRects = rects.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; @@ -541,16 +599,16 @@ const last = merged[merged.length - 1]; const current = grayRects[i]; if (current.x === last.x + last.w && current.y === last.y && 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 && } 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.x); @@ -558,7 +616,7 @@ last.w = Math.round(last.w); last.h = Math.round(last.h); } else { merged.push({ merged.push({ x: Math.round(current.x), y: Math.round(current.y), w: Math.round(current.w), @@ -611,12 +669,10 @@ const rect = layout.rects[rectIndex]; const originalState = { ...rect }; // 旋转矩形 const temp = rect.w; rect.w = rect.h; rect.h = temp; // 检查旋转后是否与其他蓝色矩形重叠 const otherRects = layout.rects.filter(r => !r.isRemain && r !== rect); let isValidRotation = true; @@ -626,7 +682,6 @@ } }); // 检查是否超出布局边界 if (rect.x + rect.w > layout.width || rect.y + rect.h > layout.height) { isValidRotation = false; } @@ -634,7 +689,6 @@ if (isValidRotation) { adjustGrayRectangles(layoutIndex); } else { // 恢复原状 rect.w = originalState.w; rect.h = originalState.h; ElMessage.warning('无法旋转,存在重叠或超出边界'); @@ -646,18 +700,15 @@ const rect = layout.rects[rectIndex]; const grayRects = layout.rects.filter(r => r.isRemain); // 旋转矩形 const temp = rect.w; rect.w = rect.h; rect.h = temp; // 检查旋转后的矩形是否可以放置在某个灰色矩形的位置 const canPlace = grayRects.some(grayRect => { return grayRect.w >= rect.w && grayRect.h >= rect.h; }); if (!canPlace) { // 如果不能放置,恢复原状 const temp = rect.w; rect.w = rect.h; rect.h = temp; @@ -665,10 +716,7 @@ return; } // 调整灰色矩形 adjustGrayRectangles(layoutIndex); // 移动矩形 moveRect(layoutIndex, rectIndex, direction); }; @@ -677,47 +725,44 @@ const rect = layout.rects[rectIndex]; const originalState = { ...rect }; // 计算剩余空间 const remainingAreas = calculateRemainingAreas(layout.width, layout.height, layout.rects.filter(r => !r.isRemain)); // 根据方向计算可移动的最大步长 let maxStep = 0; const obstacles = layout.rects.filter(r => r.isRemain || r !== rect); switch (direction) { case 'up': maxStep = rect.y; maxStep = getAvailableSpaceUp(rect, layout, obstacles); break; case 'down': maxStep = layout.height - (rect.y + rect.h); maxStep = getAvailableSpaceDown(rect, layout, obstacles); break; case 'left': maxStep = rect.x; maxStep = getAvailableSpaceLeft(rect, layout, obstacles); break; case 'right': maxStep = layout.width - (rect.x + rect.w); maxStep = getAvailableSpaceRight(rect, layout, obstacles); break; } // 移动步长,根据剩余空间动态调整 const stepSize = maxStep; const actualStep = Math.min(maxStep, stepSize); if (maxStep <= 0) { ElMessage.warning('无法移动,没有足够的空间'); return; } // 移动矩形 switch (direction) { case 'up': rect.y -= actualStep; rect.y -= maxStep; break; case 'down': rect.y += actualStep; rect.y += maxStep; break; case 'left': rect.x -= actualStep; rect.x -= maxStep; break; case 'right': rect.x +=actualStep; rect.x += maxStep; break; } // 检查是否与其他蓝色矩形重叠 const otherRects = layout.rects.filter(r => !r.isRemain && r !== rect); let isValidMove = true; @@ -727,7 +772,6 @@ } }); // 检查是否超出布局边界 if (rect.x < 0 || rect.y < 0 || rect.x + rect.w > layout.width || rect.y + rect.h > layout.height) { @@ -737,7 +781,6 @@ if (isValidMove) { adjustGrayRectangles(layoutIndex); } else { // 恢复原状 rect.x = originalState.x; rect.y = originalState.y; ElMessage.warning('无法移动,存在重叠或超出边界'); @@ -829,33 +872,175 @@ layouts.value = props.layoutData.Layouts; }; const getAvailableSpaceUp = (rect, layout, obstacles) => { let maxSpace = rect.y; obstacles.forEach(obstacle => { if (obstacle.y + obstacle.h < rect.y && obstacle.x <= rect.x + rect.w && obstacle.x + obstacle.w >= rect.x) { maxSpace = Math.min(maxSpace, rect.y - (obstacle.y + obstacle.h)); } }); return maxSpace; }; let clickEventListener = null; const getAvailableSpaceDown = (rect, layout, obstacles) => { let maxSpace = layout.height - (rect.y + rect.h); obstacles.forEach(obstacle => { if (obstacle.y > rect.y + rect.h && obstacle.x <= rect.x + rect.w && obstacle.x + obstacle.w >= rect.x) { maxSpace = Math.min(maxSpace, obstacle.y - (rect.y + rect.h)); } }); return maxSpace; }; const getAvailableSpaceLeft = (rect, layout, obstacles) => { let maxSpace = rect.x; obstacles.forEach(obstacle => { if (obstacle.x + obstacle.w < rect.x && obstacle.y <= rect.y + rect.h && obstacle.y + obstacle.h >= rect.y) { maxSpace = Math.min(maxSpace, rect.x - (obstacle.x + obstacle.w)); } }); return maxSpace; }; const getAvailableSpaceRight = (rect, layout, obstacles) => { let maxSpace = layout.width - (rect.x + rect.w); obstacles.forEach(obstacle => { if (obstacle.x > rect.x + rect.w && obstacle.y <= rect.y + rect.h && obstacle.y + obstacle.h >= rect.y) { maxSpace = Math.min(maxSpace, obstacle.x - (rect.x + rect.w)); } }); return maxSpace; }; let moveInterval = null; const handleKeyDown = (event) => { if (!focusIndex.value) return; const { layoutIndex, rectIndex } = focusIndex.value; const layout = layouts.value[layoutIndex]; const rect = layout.rects[rectIndex]; const obstacles = layout.rects.filter(r => r.isRemain || r !== rect); 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; } } }; const mirrorLayoutX = (layoutIndex) => { const layout = layouts.value[layoutIndex]; const width = layout.width; const rects = [...layout.rects]; // 创建副本避免直接修改 rects.forEach(rect => { // 计算X镜像后的坐标 const newX = width - rect.x - rect.w; const newY = rect.y; // 更新矩形位置 rect.x = newX; rect.y = newY; }); // 更新布局 layout.rects = rects; adjustGrayRectangles(layoutIndex); }; const mirrorLayoutY = (layoutIndex) => { const layout = layouts.value[layoutIndex]; const height = layout.height; const rects = [...layout.rects]; // 创建副本避免直接修改 rects.forEach(rect => { // 计算Y镜像后的坐标 const newX = rect.x; const newY = height - rect.y - rect.h; // 更新矩形位置 rect.y = newY; }); // 更新布局 layout.rects = rects; adjustGrayRectangles(layoutIndex); }; onMounted(() => { fetchSettings(username); setTimeout(updateLayout, 500); updateLayout(); selectedLayoutIndex.value = 0; // 添加全局点击事件监听器 clickEventListener = (event) => { // 检查是否存在右键菜单 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) { if (clickEventListener) { document.removeEventListener('click', clickEventListener); clickEventListener = null; } document.removeEventListener('keydown', handleKeyDown); document.removeEventListener('keyup', handleKeyUp); }); </script> @@ -885,25 +1070,28 @@ } .rect-content { display: grid; grid-template-columns: 1fr; grid-template-rows: 1fr; 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 .liuchengka { 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 { north-glass-erp/northglass-erp/src/views/pp/glassOptimize/page/RectRenderer.vue
@@ -1,21 +1,26 @@ <template> <div ref="layoutPanel" :class="panelClass" :style="panelStyle"> <div v-for="(layout, layoutIndex) in layouts" :key="layoutIndex" class="layout-wrapper"> <!-- 布局信息标签 --> <div class="layout-info" :style="layoutInfoStyle(layoutIndex)"> {{ getCurrentRectInfo(layoutIndex) }} </div> <!-- 布局容器 --> <div class="layout-container" :style="layoutContainerStyle(layoutIndex)"> <div v-for="(rect, rectIndex) in layout.rects" :key="rectIndex" :ref="(el) => { if (el) rectsElements[layoutIndex + '-' + rectIndex] = el }" :class="rectClass" :style="rectStyle(rect, layoutIndex)" @click="handleRectClick(layoutIndex, rectIndex)"> <div v-if="!rect.isRemain" class="rect-content"> <div class="size">{{ rect.w }}×{{ rect.h }}</div> <div class="jia-hao">{{ rect.JiaHao }}</div> <div id="printFlowCard"> <div v-for="(layout, layoutIndex) in layouts" :key="layoutIndex" class="layout-wrapper"> <div class="header" :style="headerStyle(layoutIndex)"> 工程号{{ processId }} {{ getCurrentRectInfo(layoutIndex) }} </div> <div class="layout-container" :style="layoutContainerStyle(layoutIndex)"> <div class="grid-container" :class="`cols-${printColumns}`"> <div v-for="(rect, rectIndex) in layout.rects" :key="rectIndex" :ref="(el) => { if (el) rectsElements[layoutIndex + '-' + rectIndex] = el }" :class="rectClass" :style="rectStyle(rect, layoutIndex)" @click="handleRectClick(layoutIndex, rectIndex)" > <div v-if="!rect.isRemain" class="rect-content"> <div class="size">{{ rect.w }}×{{ rect.h }}</div> <div class="jia-hao">{{ rect.JiaHao }}</div> </div> </div> </div> </div> </div> @@ -24,13 +29,16 @@ </template> <script setup> import { ref, reactive, onMounted, onUnmounted } from 'vue'; import { ref, reactive, onMounted, onUnmounted, watch, nextTick } from 'vue'; import request from "@/utils/request"; 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' } gw: { type: Number, default: 1400 }, gh: { type: Number, default: 1100 }, style: { type: String, default: 'width:100%;height:800px;display:block;background:gray' }, printLayout: { type: String, default: '2rows-2cols' }, // 可选值:4rows-2cols, 3rows-2cols, 3rows-1col, 2rows-2cols fixedPageHeight: { type: Number, default: 1100 } // 固定页面高度 }); const emit = defineEmits(['rectClicked']); @@ -41,12 +49,29 @@ const panelClass = ref(''); const panelStyle = ref(props.style); const rectClass = ref('layout-rect'); const processId = localStorage.getItem('projectNo'); const printColumns = ref(2); // 初始化为2列 const layoutsPerPage = ref(4); // 默认每页显示4个布局(2行×2列) // 定义不同布局的放大比例 const layoutScales = { '4rows-2cols': 0.8, // 四行两列,较小的放大比例 '3rows-2cols': 0.9, // 三行两列,适中的放大比例 '3rows-1col': 1.0, // 三行一列,较大的放大比例 '2rows-2cols': 1 // 两行两列,较大的放大比例 }; // 监听printLayout变化 watch(() => props.printLayout, (newVal) => { adjustPrintLayout(); updateLayout(); }); const layoutContainerStyle = (layoutIndex) => { const containerWidth = (props.gw - 210) / 2; const containerHeight = (props.gh - 100) / 3; const x = (layoutIndex % 2) * (containerWidth + 50); const y = Math.floor(layoutIndex / 2) * (containerHeight + 50); const containerWidth = (props.gw - 20) / printColumns.value; // 减少边距 const containerHeight = (props.gh - 20) / Math.ceil(layoutsPerPage.value / printColumns.value); const x = (layoutIndex % printColumns.value) * containerWidth; const y = Math.floor(layoutIndex / printColumns.value) * containerHeight; return { position: 'absolute', left: `${x}px`, @@ -54,34 +79,45 @@ width: `${containerWidth}px`, height: `${containerHeight}px`, overflow: 'visible', border: '1px solid #ccc', background: '#fff' padding: '10px' // 添加内边距 }; }; const layoutInfoStyle = (layoutIndex) => { const containerWidth = (props.gw - 210) / 2; const containerHeight = (props.gh - 100) / 3; const x = (layoutIndex % 2) * (containerWidth + 50); const y = Math.floor(layoutIndex / 2) * (containerHeight + 50); const headerStyle = (layoutIndex) => { const containerWidth = (props.gw - 20) / printColumns.value; const containerHeight = (props.gh - 20) / Math.ceil(layoutsPerPage.value / printColumns.value); const x = (layoutIndex % printColumns.value) * containerWidth; const y = Math.floor(layoutIndex / printColumns.value) * containerHeight; const scale = Math.min( containerWidth, containerHeight ) * 1.2; // 放大1.2倍 return { position: 'absolute', left: `${x}px`, top: `${y - 45}px`, background: 'none', width: `${scale}px`, textAlign: 'center', zIndex: 1000 zIndex: 1000, background: '#ffffff', padding: '5px', fontSize: '12px' }; }; const rectStyle = (rect, layoutIndex) => { const layout = layouts.value[layoutIndex]; const containerWidth = (props.gw - 100) / 2; const containerHeight = (props.gh - 100) / 3; const containerWidth = (props.gw - 100) / printColumns.value; const containerHeight = (props.gh - 100) / Math.ceil(layoutsPerPage.value / printColumns.value); // 根据当前打印布局获取放大比例 const currentScale = layoutScales[props.printLayout] || 1.0; const scale = Math.min( containerWidth / layout.width, containerHeight / layout.height ); ) * currentScale; // 应用当前布局的放大比例 return { position: 'absolute', left: `${rect.x * scale}px`, @@ -104,48 +140,181 @@ const rect = layout.rects[focusIndex.value?.rectIndex || 0]; if (!rect) return ''; const totalRects = layouts.value.length; const currentRectIndex = layoutIndex+1; const currentRectIndex = layoutIndex + 1; const width = layout.width; const height = layout.height; const percentage = ((rect.w / layout.width) * 100).toFixed(1) + '%'; return `${currentRectIndex}/${totalRects} ${width}×${height} ×1 ${percentage}`; const sum = layout.rects.reduce((sum, r) => sum + (r.w * r.h), 0); const areaUtilization = ((sum / (width * height)) * 100).toFixed(2); return `${currentRectIndex}/${totalRects} ${height}X${width}X1 ${areaUtilization}%`; }; const adjustPrintLayout = () => { switch (props.printLayout) { case '4rows-2cols': printColumns.value = 2; layoutsPerPage.value = 8; // 4行×2列 break; case '3rows-2cols': printColumns.value = 2; layoutsPerPage.value = 6; // 3行×2列 break; case '3rows-1col': printColumns.value = 1; layoutsPerPage.value = 3; // 3行×1列 break; case '2rows-2cols': printColumns.value = 2; layoutsPerPage.value = 4; // 2行×2列 break; default: printColumns.value = 2; layoutsPerPage.value = 4; } }; const updateLayout = () => { if (!layoutPanel.value) return; layouts.value = props.layoutData.Layouts; adjustPrintLayout(); // 强制重新渲染 layoutPanel.value.offsetHeight; // 触发布局更新 }; onMounted(() => { setTimeout(updateLayout, 500); updateLayout(); }); onUnmounted(() => { rectsElements.value = {}; }); const print = () => { const el = document.getElementById('printFlowCard'); const doc = document; const body = doc.body || doc.getElementsByTagName("body")[0]; const printId = "print-" + Date.now(); // 创建一个克隆的节点 const content = document.createElement("div"); content.id = printId; content.appendChild(el.cloneNode(true)); // 克隆节点并保留所有属性和子节点 const style = document.createElement("style"); style.innerHTML = "body>#" + printId + "{display:none}@media print{" + "@page {" + " size: auto; " + " margin: 13mm 4mm 0mm 4mm; " + " }body>:not(#" + printId + "){display:none !important}body>#" + printId + "{display:block;padding-top:1px}}"; body.appendChild(style); body.appendChild(content); // 优化分页逻辑 const layoutWrappers = content.querySelectorAll('.layout-wrapper'); let currentPageHeight = 0; let currentWrapperIndex = 0; layoutWrappers.forEach((wrapper, index) => { const wrapperHeight = wrapper.offsetHeight; if (currentPageHeight + wrapperHeight > props.fixedPageHeight) { const pageBreak = document.createElement('div'); pageBreak.className = 'element-to-break-after'; layoutWrappers[currentWrapperIndex - 1].appendChild(pageBreak); currentPageHeight = wrapperHeight; } else { currentPageHeight += wrapperHeight; } currentWrapperIndex = index + 1; }); setTimeout(() => { window.print(); body.removeChild(content); body.removeChild(style); }, 200); }; defineExpose({ print, updateLayout }); </script> <style scoped> @media print { .layout-wrapper { page-break-inside: avoid; margin-bottom: 20px; } .element-to-break-after { page-break-after: always; } .header { position: static; width: 100%; } .layout-container { position: static; width: 100%; height: auto; } .grid-container { display: grid; gap: 10px; /* 减少打印时的网格间距 */ } .cols-1 { grid-template-columns: 1fr; } .cols-2 { grid-template-columns: repeat(2, 1fr); } .cols-3 { grid-template-columns: repeat(3, 1fr); } .cols-4 { grid-template-columns: repeat(4, 1fr); } } .element-to-break-after { page-break-after: always; } .layout-wrapper { position: relative; margin-top: 50px; } .header { position: absolute; top: -45px; left: 0; width: 100%; text-align: center; z-index: 1000; background-color: #ffffff; padding: 5px; font-size: 12px; } .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; } .rect-content {