From 0bf61a3dd61f6751e60314ab8931e7833f731e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A0=82=E7=B3=96?= Date: Thu, 8 Jan 2026 16:29:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=A0=87=E7=AD=BE=E6=89=93=E5=8D=B0):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=A0=87=E7=AD=BE=E6=89=93=E5=8D=B0=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E6=96=B0=E5=A2=9E=E6=89=B9=E9=87=8F=E5=AF=BC?= =?UTF-8?q?=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增html2canvas依赖以支持高清打印 - 重构标签打印逻辑,解决二维码丢失和文字模糊问题 - 优化外标签样式布局和公司名称显示 - 新增标签预览功能,可在物料列表中直接查看 - 实现批量导出功能,支持任务规划和进度展示 - 添加导出配置选项,可调整清晰度和单次导出数量 --- klp-ui/package.json | 3 +- .../panels/LabelRender/OuterTagPreview.vue | 33 +- .../LabelRender/ProductionTagPreview.vue | 2 +- .../wms/coil/panels/LabelRender/index.vue | 183 ++++- klp-ui/src/views/wms/coil/panels/do.vue | 32 +- klp-ui/src/views/wms/coil/panels/tool.vue | 744 ++++++++++++++---- 6 files changed, 788 insertions(+), 209 deletions(-) diff --git a/klp-ui/package.json b/klp-ui/package.json index a0ca0762..730b8376 100644 --- a/klp-ui/package.json +++ b/klp-ui/package.json @@ -52,15 +52,16 @@ "flv.js": "^1.6.2", "fuse.js": "6.4.3", "highlight.js": "10.5.0", + "html2canvas": "^1.4.1", "js-beautify": "1.13.0", "js-cookie": "3.0.1", "jsbarcode": "^3.12.1", "jsencrypt": "3.0.0-rc.1", "jspdf": "^2.5.2", - "pdfjs-dist": "^3.6.172", "konva": "^10.0.2", "mqtt": "^5.13.3", "nprogress": "0.2.0", + "pdfjs-dist": "^3.6.172", "print-js": "^1.6.0", "qrcode": "^1.5.4", "quill": "1.3.7", diff --git a/klp-ui/src/views/wms/coil/panels/LabelRender/OuterTagPreview.vue b/klp-ui/src/views/wms/coil/panels/LabelRender/OuterTagPreview.vue index ebb4ecd3..e6f22342 100644 --- a/klp-ui/src/views/wms/coil/panels/LabelRender/OuterTagPreview.vue +++ b/klp-ui/src/views/wms/coil/panels/LabelRender/OuterTagPreview.vue @@ -4,9 +4,10 @@
- 嘉祥科伦普重工有限公司
- Jiaxiang KLP Heavy Industry Co., Ltd. + 科伦普
+ KE LUN PU
+
嘉祥科伦普重工有限公司
@@ -102,7 +103,7 @@
- +
@@ -161,11 +162,11 @@ export default { \ No newline at end of file diff --git a/klp-ui/src/views/wms/coil/panels/do.vue b/klp-ui/src/views/wms/coil/panels/do.vue index 09ff9b21..b31d8bf2 100644 --- a/klp-ui/src/views/wms/coil/panels/do.vue +++ b/klp-ui/src/views/wms/coil/panels/do.vue @@ -42,7 +42,7 @@
{{ item.currentCoilNo }} - {{ item.materialType || '原料' }} +
@@ -136,6 +136,8 @@
+ +
@@ -330,6 +332,11 @@ 取 消 + + + + + @@ -340,6 +347,7 @@ import { parseTime } from '@/utils/klp' import ProductInfo from '@/components/KLPService/Renderer/ProductInfo' import RawMaterialInfo from '@/components/KLPService/Renderer/RawMaterialInfo' import { addCoilAbnormal } from '@/api/wms/coilAbnormal' +import LabelRender from './LabelRender/index.vue' export default { name: 'DoPage', @@ -356,7 +364,8 @@ export default { }, components: { ProductInfo, - RawMaterialInfo + RawMaterialInfo, + LabelRender }, data() { return { @@ -372,6 +381,11 @@ export default { enterCoilNo: null, currentCoilNo: null }, + labelRender: { + visible: false, + data: {}, + type: '2' + }, // 待操作列表相关 actionLoading: false, @@ -418,6 +432,7 @@ export default { } } }, + tabs: { handler(newVal) { if (newVal.length > 0) { @@ -462,6 +477,19 @@ export default { methods: { parseTime, + /** 预览标签 */ + handlePreviewLabel(row) { + this.labelRender.visible = true; + const item = row.itemType === 'product' ? row.product : row.rawMaterial; + const itemName = row.itemType === 'product' ? item?.productName || '' : item?.rawMaterialName || ''; + + this.labelRender.type = row.itemType === 'product' ? '3' : '2' + this.labelRender.data = { + ...row, + itemName: itemName, + updateTime: row.updateTime?.split(' ')[0] || '', + }; + }, // ========== 物料列表相关方法 ========== /** 查询物料列表 */ getMaterialCoil() { diff --git a/klp-ui/src/views/wms/coil/panels/tool.vue b/klp-ui/src/views/wms/coil/panels/tool.vue index 674f5859..ff74eabe 100644 --- a/klp-ui/src/views/wms/coil/panels/tool.vue +++ b/klp-ui/src/views/wms/coil/panels/tool.vue @@ -1,34 +1,167 @@ @@ -36,155 +169,436 @@ import OuterTagPreview from './LabelRender/OuterTagPreview.vue'; import { listMaterialCoil } from "@/api/wms/coil"; import jsPDF from 'jspdf'; -import domtoimage from 'dom-to-image'; +import html2canvas from 'html2canvas'; +import { Message } from 'element-ui'; export default { name: 'ToolPanel', components: { - OuterTagPreview, + OuterTagPreview }, data() { return { - list: [], - pos: 0, - current: {}, - exportLoading: false, - // 进度条相关变量 - showProgress: false, // 是否显示进度条 - progress: 0, // 进度值(0-100) - currentIndex: 0, // 当前处理的标签索引 - totalCount: 0, // 总标签数量 + list: [], // 当前批次数据 + current: {}, // 当前预览数据 + exportLoading: false, // 全局导出加载状态 + showProgress: false, // 单批进度条显示 + progress: 0, // 单批进度百分比 + currentIndex: 0, // 单批当前处理索引 + totalCount: 0, // 当前批次总数 + + // 导出配置 + config: { + renderScale: 1.2, // 清晰度 + pageSize: 30 // 单次导出数量 + }, + + // 任务规划数据 + taskPlan: { + totalCount: 0, // 总数据量(接口返回的res.total) + totalTask: 0, // 总任务数(Math.ceil(totalCount/pageSize)) + completedTask: 0, // 已完成任务数 + runningTask: 0, // 进行中任务数 + totalProgress: 0 // 整体进度百分比 + }, + taskList: [], // 任务列表(每个任务的详情) + currentTask: { // 当前任务详情 + idx: 0, // 任务序号 + startNum: 0, // 起始条数 + endNum: 0, // 结束条数 + progress: 0, // 当前任务进度 + status: 'pending',// 状态:pending/running/completed/failed + statusText: '未开始', + statusColor: '#909399' + }, + pdfDoc: null // 全局PDF实例,支持追加内容 + } + }, + computed: { + // 是否有任务正在执行 + hasRunningTask() { + return this.taskPlan.runningTask > 0; } }, mounted() { - this.fetchData(); + this.fetchTotalData(); // 先获取总数量,规划任务 }, methods: { - // 获取标签数据 - fetchData() { - listMaterialCoil({ + // ========== 配置校验 ========== + validateRenderScale() { + let scale = this.config.renderScale; + if (!scale || isNaN(scale)) { + this.config.renderScale = 1.5; + Message.warning('清晰度请输入1.0-4.0之间的数字,已重置为默认值1.5'); + return; + } + if (scale < 1.0) this.config.renderScale = 1.0; + else if (scale > 4.0) this.config.renderScale = 4.0; + }, + + validatePageSize() { + let size = this.config.pageSize; + if (!size || isNaN(size) || size < 1) { + this.config.pageSize = 10; + Message.warning('每页数量请输入1-200之间的正整数,已重置为默认值10'); + return; + } + if (size > 200) this.config.pageSize = 200; + }, + + // ========== 任务规划核心逻辑 ========== + // 1. 获取总数据量,初始化任务规划 + async fetchTotalData() { + try { + const res = await listMaterialCoil({ + status: 0, + dataType: 1, + materialType: '成品', + itemType: 'product', + selectType: 'raw_material', + pageSize: 1, // 仅获取总数量,pageSize设为1 + pageNum: 1 + }); + + // 初始化任务规划 + this.taskPlan.totalCount = res.total || 0; + this.taskPlan.totalTask = Math.ceil(this.taskPlan.totalCount / this.config.pageSize); + this.initTaskList(); // 初始化任务列表 + } catch (err) { + console.error('获取总数据量失败:', err); + Message.error('获取数据总量失败,请刷新重试'); + } + }, + + // 2. 初始化任务列表 + initTaskList() { + this.taskList = []; + for (let i = 0; i < this.taskPlan.totalTask; i++) { + const startNum = i * this.config.pageSize + 1; + const endNum = Math.min((i + 1) * this.config.pageSize, this.taskPlan.totalCount); + this.taskList.push({ + idx: i + 1, // 任务序号 + startNum, // 起始条数 + endNum, // 结束条数 + progress: 0, // 任务进度 + status: 'pending', // 状态:pending/running/completed/failed + statusText: '未开始', + statusColor: '#909399' + }); + } + // 重置任务统计 + this.taskPlan.completedTask = 0; + this.taskPlan.runningTask = 0; + this.taskPlan.totalProgress = 0; + // 重置当前任务 + this.currentTask = { idx: 0, startNum: 0, endNum: 0, progress: 0, status: 'pending', statusText: '未开始', statusColor: '#909399' }; + // 重置PDF实例 + this.pdfDoc = null; + }, + + // 3. 修改pageSize后重新规划任务 + replanTask() { + // 先校验参数 + this.validatePageSize(); + // 重新计算总任务数 + this.taskPlan.totalTask = Math.ceil(this.taskPlan.totalCount / this.config.pageSize); + // 重新初始化任务列表 + this.initTaskList(); + Message.success(`已重新规划任务!单次导出${this.config.pageSize}条,总计${this.taskPlan.totalTask}个任务`); + }, + + // 4. 更新任务状态 + updateTaskStatus(taskIdx, status, progress = 0) { + const task = this.taskList[taskIdx - 1]; + if (!task) return; + + // 更新任务状态 + task.status = status; + task.progress = progress; + switch (status) { + case 'running': + task.statusText = '进行中'; + task.statusColor = '#409eff'; + // 更新当前任务 + this.currentTask = { ...task }; + break; + case 'completed': + task.statusText = '已完成'; + task.statusColor = '#67c23a'; + break; + case 'failed': + task.statusText = '失败'; + task.statusColor = '#f56c6c'; + break; + default: + task.statusText = '未开始'; + task.statusColor = '#909399'; + } + + // 更新任务统计 + this.taskPlan.completedTask = this.taskList.filter(t => t.status === 'completed').length; + this.taskPlan.runningTask = this.taskList.filter(t => t.status === 'running').length; + // 计算整体进度 + const totalCompleted = this.taskList.reduce((sum, t) => sum + (t.progress / 100) * (t.endNum - t.startNum + 1), 0); + this.taskPlan.totalProgress = Math.round((totalCompleted / this.taskPlan.totalCount) * 100); + }, + + // ========== 核心导出逻辑 ========== + // 1. 批量导出所有未完成任务 + async handleExportAll() { + if (this.exportLoading || this.hasRunningTask) return; + if (this.taskPlan.totalCount === 0) { + Message.warning('暂无外标签数据可导出'); + return; + } + + try { + this.exportLoading = true; + // 初始化PDF文档(jsPDF默认创建1个空白页) + this.pdfDoc = new jsPDF({ unit: 'mm', format: 'a4', orientation: 'portrait' }); + + // 筛选出未完成的任务(pending/failed) + const uncompletedTasks = this.taskList.filter(t => t.status !== 'completed'); + if (uncompletedTasks.length === 0) { + Message.info('所有任务均已完成,无需导出'); + this.exportLoading = false; + return; + } + + // 循环处理每个未完成任务 + for (const task of uncompletedTasks) { + await this.runSingleTaskCore(task.idx); + } + + // 保存最终PDF + const fileName = `外标签批量导出_${new Date().toLocaleString().replace(/[/: ]/g, '-')}.pdf`; + this.pdfDoc.save(fileName); + Message.success(`PDF批量导出成功!共完成${uncompletedTasks.length}个任务,总计${this.taskPlan.totalCount}条数据`); + + } catch (err) { + console.error('批量导出失败:', err); + Message.error('批量导出失败,请查看控制台'); + } finally { + this.exportLoading = false; + this.showProgress = false; + this.progress = 0; + this.currentIndex = 0; + } + }, + + // 2. 单独执行单个任务 + async handleRunSingleTask(taskIdx) { + if (this.exportLoading || this.hasRunningTask) { + Message.warning('有任务正在执行中,请等待完成后再操作'); + return; + } + + try { + this.exportLoading = true; + // 初始化PDF文档(如果未初始化) + if (!this.pdfDoc) { + this.pdfDoc = new jsPDF({ unit: 'mm', format: 'a4', orientation: 'portrait' }); + } + + // 执行单个任务核心逻辑 + await this.runSingleTaskCore(taskIdx); + + // 保存单个任务的PDF + const fileName = `外标签任务${taskIdx}_导出_${new Date().toLocaleString().replace(/[/: ]/g, '-')}.pdf`; + this.pdfDoc.save(fileName); + Message.success(`任务${taskIdx}导出成功!处理范围:${this.taskList[taskIdx-1].startNum}-${this.taskList[taskIdx-1].endNum}条`); + + } catch (err) { + console.error(`任务${taskIdx}执行失败:`, err); + Message.error(`任务${taskIdx}执行失败,请查看控制台`); + this.updateTaskStatus(taskIdx, 'failed'); + } finally { + this.exportLoading = false; + this.showProgress = false; + this.progress = 0; + this.currentIndex = 0; + } + }, + + // 3. 单个任务执行核心逻辑(修复空白页关键) + async runSingleTaskCore(taskIdx) { + // 更新当前任务为“进行中” + this.updateTaskStatus(taskIdx, 'running', 0); + + // 获取当前任务的批次数据 + const pageNum = taskIdx; + const res = await listMaterialCoil({ status: 0, dataType: 1, materialType: '成品', itemType: 'product', selectType: 'raw_material', - pageSize: 100, - pageNum: 10 - }).then(res => { - this.list = res.rows || []; - if (this.list.length > 0) { - this.current = this.list[0]; - } - }).catch(err => { - console.error('获取标签数据失败:', err); - this.$message.error('获取标签数据失败,请刷新重试'); - }) - }, + pageSize: this.config.pageSize, + pageNum + }); - // 核心:复用OuterTagPreview组件渲染标签,再转图片导出PDF - async handleExportAll() { - if (this.exportLoading) return; - if (this.list.length === 0) { - this.$message.warning('暂无外标签数据可导出'); + this.list = res.rows || []; + this.totalCount = this.list.length; + if (this.totalCount === 0) { + this.updateTaskStatus(taskIdx, 'completed', 100); return; } - try { - // 初始化进度条 - this.exportLoading = true; - this.showProgress = true; // 显示进度条 - this.totalCount = this.list.length; // 总数量 - this.currentIndex = 0; // 重置当前索引 - this.progress = 0; // 重置进度 + // 处理当前批次的每个标签 + this.showProgress = true; + this.currentIndex = 0; + this.progress = 0; - const doc = new jsPDF({ - unit: 'mm', - format: 'a4', - orientation: 'portrait' - }); - const tempContainer = this.$refs.tempTagContainer; + for (let index = 0; index < this.list.length; index++) { + this.current = this.list[index]; + await this.waitForDOMRender(); + + // 等待资源加载(图片+字体+二维码) const previewComponent = this.$refs.outerTagPreview; + await this.waitForAllResources(previewComponent.$el); - // 循环处理每个标签 - for (let index = 0; index < this.list.length; index++) { - const item = this.list[index]; - this.current = item; // 更新预览组件的content,触发重新渲染 - - // 等待组件渲染完成 - await this.waitForDOMRender(); + // 克隆DOM到临时容器 + const tempContainer = this.$refs.tempTagContainer; + const renderedDOM = previewComponent.$el.cloneNode(true); + tempContainer.innerHTML = ''; + tempContainer.appendChild(renderedDOM); - // 复制预览组件的DOM到临时容器 - const renderedDOM = previewComponent.$el.cloneNode(true); - tempContainer.innerHTML = ''; - tempContainer.appendChild(renderedDOM); + await this.waitForDOMRender(); + this.copyCanvasContent(previewComponent.$el, tempContainer); - // 再次等待临时容器DOM渲染 - await this.waitForDOMRender(); - - // 检查临时容器是否有内容 - if (!tempContainer.firstChild) { - console.warn(`第${index+1}个标签DOM为空`, item); - // 即使当前标签为空,也更新进度 - this.updateProgress(index + 1); - continue; - } - - // 将临时容器的DOM转为图片 - const imgData = await domtoimage.toPng(previewComponent.$el); - - // 分页:非第一个标签新增页面 - if (index > 0) { - doc.addPage(); - } - - // 计算图片在PDF中的尺寸(适配A4,保持原比例) - const a4Width = 210; - const a4Height = 297; - const imgRatio = (previewComponent.$el.offsetWidth || 800) / (previewComponent.$el.offsetHeight || 1100); - const imgWidth = a4Width - 20; - const imgHeight = imgWidth / imgRatio; - - // 确保图片高度不超过A4高度 - const finalImgHeight = imgHeight > a4Height - 20 ? (a4Height - 20) : imgHeight; - const finalImgWidth = finalImgHeight * imgRatio; - // 居中显示 - const xPos = (a4Width - finalImgWidth) / 2; - const yPos = (a4Height - finalImgHeight) / 2; - - // 插入图片到PDF - doc.addImage(imgData, 'PNG', xPos, yPos, finalImgWidth, finalImgHeight); - - // 更新进度条(关键:处理完一个标签就更新) + if (!tempContainer.firstChild) { + console.warn(`任务${taskIdx}第${index+1}个标签DOM为空`, this.list[index]); this.updateProgress(index + 1); + continue; } - // 保存PDF - const fileName = `外标签导出_${new Date().toLocaleString().replace(/[/: ]/g, '-')}.pdf`; - doc.save(fileName); - this.$message.success('PDF导出成功!'); + // 生成高清Canvas + const canvas = await html2canvas(tempContainer, { + scale: this.config.renderScale, + backgroundColor: '#ffffff', + useCORS: true, + width: tempContainer.offsetWidth, + height: tempContainer.offsetHeight, + windowWidth: tempContainer.offsetWidth * this.config.renderScale, + windowHeight: tempContainer.offsetHeight * this.config.renderScale, + logging: false, + allowTaint: true, + taintTest: false, + }); - } catch (err) { - console.error('PDF导出失败详情:', err); - this.$message.error('PDF导出失败,详情请查看控制台'); - } finally { - // 重置进度条和加载状态 - this.$refs.tempTagContainer.innerHTML = ''; - this.exportLoading = false; - this.showProgress = false; // 隐藏进度条 - this.progress = 0; - this.currentIndex = 0; - this.totalCount = 0; + // ========== 修复空白页核心逻辑 ========== + // 关键规则: + // 1. jsPDF实例化时默认创建1个空白页 + // 2. 第一个任务的第一个标签:直接使用默认空白页,不新增 + // 3. 第一个任务的非第一个标签:新增页面 + // 4. 非第一个任务的所有标签:新增页面 + const needAddPage = index > 0 || taskIdx > 1; + if (needAddPage) { + this.pdfDoc.addPage(); + } + + // 尺寸计算 + const domWidth = tempContainer.offsetWidth; + const domHeight = tempContainer.offsetHeight; + const domWidthMM = domWidth * (25.4 / 96); + const domHeightMM = domHeight * (25.4 / 96); + + const a4Width = 210; + const a4Height = 297; + const margin = 5; + const availableWidth = a4Width - 2 * margin; + const availableHeight = a4Height - 2 * margin; + + const scaleRatio = Math.min(availableWidth / domWidthMM, availableHeight / domHeightMM); + const finalWidth = domWidthMM * scaleRatio; + const finalHeight = domHeightMM * scaleRatio; + + const x = (a4Width - finalWidth) / 2; + const y = (a4Height - finalHeight) / 2; + + // 插入图片到PDF + this.pdfDoc.addImage(canvas.toDataURL('image/png', 1.0), 'PNG', x, y, finalWidth, finalHeight); + + // 更新单批进度 + 任务进度 + this.updateProgress(index + 1); + this.updateTaskStatus(taskIdx, 'running', this.progress); } + + // 当前任务完成 + this.updateTaskStatus(taskIdx, 'completed', 100); }, - // 辅助方法:更新进度条 + // ========== 辅助方法 ========== + // 等待二维码/图片/字体加载 + async waitForAllResources(element) { + // 等待图片加载 + const images = element.querySelectorAll('img'); + const imgPromises = Array.from(images).map(img => + img.complete ? Promise.resolve() : new Promise(resolve => { + img.onload = resolve; + img.onerror = resolve; + }) + ); + await Promise.all(imgPromises); + + // 等待字体加载 + await document.fonts.ready; + + // 等待二维码Canvas渲染 + await this.waitForQRCodeRender(element); + + // 最终等待 + await new Promise(resolve => setTimeout(resolve, 300)); + }, + + // 等待二维码渲染 + async waitForQRCodeRender(element) { + const qrCanvasList = element.querySelectorAll('canvas'); + if (qrCanvasList.length === 0) return; + + const qrLoadPromises = Array.from(qrCanvasList).map(canvas => { + return new Promise(resolve => { + const checkInterval = setInterval(() => { + const ctx = canvas.getContext('2d'); + if (!ctx) { + clearInterval(checkInterval); + resolve(); + return; + } + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const hasContent = imageData.data.some(value => value !== 0); + if (hasContent || Date.now() - (ctx.startTime || 0) > 2000) { + clearInterval(checkInterval); + resolve(); + } + }, 100); + }); + }); + await Promise.all(qrLoadPromises); + }, + + // 复制Canvas内容 + copyCanvasContent(sourceEl, targetEl) { + const sourceCanvases = sourceEl.querySelectorAll('canvas'); + const targetCanvases = targetEl.querySelectorAll('canvas'); + + sourceCanvases.forEach((canvas, index) => { + const targetCanvas = targetCanvases[index]; + if (!targetCanvas) return; + targetCanvas.width = canvas.width; + targetCanvas.height = canvas.height; + const targetCtx = targetCanvas.getContext('2d'); + targetCtx.drawImage(canvas, 0, 0); + }); + }, + + // 更新单批进度 updateProgress(current) { this.currentIndex = current; - // 计算进度百分比(保留整数) this.progress = Math.round((current / this.totalCount) * 100); }, - // 辅助方法:等待DOM渲染完成 + // 等待DOM渲染 waitForDOMRender(time = 300) { return new Promise(resolve => setTimeout(resolve, time)); } @@ -193,6 +607,35 @@ export default { \ No newline at end of file