diff --git a/klp-ui/package.json b/klp-ui/package.json index 730b8376..89204e7f 100644 --- a/klp-ui/package.json +++ b/klp-ui/package.json @@ -61,6 +61,7 @@ "konva": "^10.0.2", "mqtt": "^5.13.3", "nprogress": "0.2.0", + "pdf-lib": "^1.17.1", "pdfjs-dist": "^3.6.172", "print-js": "^1.6.0", "qrcode": "^1.5.4", diff --git a/klp-ui/src/views/wms/coil/panels/base.vue b/klp-ui/src/views/wms/coil/panels/base.vue index d8dcc571..f69a26e3 100644 --- a/klp-ui/src/views/wms/coil/panels/base.vue +++ b/klp-ui/src/views/wms/coil/panels/base.vue @@ -63,6 +63,9 @@ 导出 + + 批量打印标签 + @@ -229,6 +232,24 @@ + + + +
+ 已选择 {{ batchPrint.list.length }} 个钢卷。点击“生成PDF并打开”将每个标签作为一页(180mm × 100mm)。 +
+
+ 取消 + 生成PDF并打开 +
+ + + +
@@ -247,12 +268,16 @@ import RawMaterialInfo from "@/components/KLPService/Renderer/RawMaterialInfo"; // 引入封装的追溯结果组件 import CoilTraceResult from "./CoilTraceResult.vue"; import LabelRender from './LabelRender/index.vue' +import OuterTagPreview from './LabelRender/OuterTagPreview.vue' import MaterialSelect from "@/components/KLPService/MaterialSelect"; import ActualWarehouseSelect from "@/components/KLPService/ActualWarehouseSelect"; import { findItemWithBom } from "@/store/modules/category"; import CoilNo from "@/components/KLPService/Renderer/CoilNo.vue"; import MemoInput from "@/components/MemoInput"; import MutiSelect from "@/components/MutiSelect"; +import html2canvas from 'html2canvas'; +import { PDFDocument } from 'pdf-lib'; + export default { @@ -272,6 +297,7 @@ export default { CoilNo, MemoInput, MutiSelect, + OuterTagPreview, }, dicts: ['product_coil_status', 'coil_material', 'coil_itemname', 'coil_manufacturer'], props: { @@ -407,6 +433,12 @@ export default { data: {}, type: '2' }, + batchPrint: { + visible: false, + loading: false, + list: [], + }, + __printOldTitle: document.title, floatLayerConfig: { columns: [ { label: '入场钢卷号', prop: 'enterCoilNo' }, @@ -736,6 +768,136 @@ export default { ...this.queryParams }, `materialCoil_${new Date().getTime()}.xlsx`) }, + + /** 批量打印标签按钮 */ + handleBatchPrintLabel() { + if (!this.ids || this.ids.length === 0) { + this.$message.warning('请先勾选要打印标签的钢卷'); + return; + } + + // 取出选中行数据,并补齐标签渲染需要的字段 + const selectedData = this.materialCoilList + .filter(item => this.ids.includes(item.coilId)) + .map(row => { + const item = row.itemType === 'product' ? row.product : row.rawMaterial; + const itemName = row.itemType === 'product' ? item?.productName || '' : item?.rawMaterialName || ''; + + return { + ...row, + itemName, + // OuterTagPreview.vue 里字段名使用了 specification/material(而列表里是 itemSpecification/itemMaterial) + specification: row.itemSpecification || row.specification || '', + material: row.itemMaterial || row.material || '', + updateTime: row.updateTime?.split(' ')[0] || row.updateTime || '', + } + }); + + this.batchPrint.list = selectedData; + this.batchPrint.visible = true; + }, + + /** 批量导出标签PDF:每个标签一页(180mm × 100mm) */ + async handleBatchExportLabelPdf() { + if (!this.batchPrint.list || this.batchPrint.list.length === 0) { + this.$message.warning('没有可导出的数据'); + return; + } + + const container = this.$refs.batchPdfContainer; + if (!container) { + this.$message.error('PDF渲染容器未初始化'); + return; + } + + try { + this.batchPrint.loading = true; + + await this.$nextTick(); + await new Promise(resolve => setTimeout(resolve, 100)); + + const mmToPt = 72 / 25.4; + // 纸张尺寸 + const paperWidthMm = 180; + const paperHeightMm = 100; + // 留白:左右约 4mm;上下更小一些,避免底部空得太多 + const marginXmm = 4; + const marginTopMm = 2; + const marginBottomMm = 1; + + const pageWidthPt = paperWidthMm * mmToPt; + const pageHeightPt = paperHeightMm * mmToPt; + const marginXPt = marginXmm * mmToPt; + const marginTopPt = marginTopMm * mmToPt; + const marginBottomPt = marginBottomMm * mmToPt; + + const contentWidthPt = pageWidthPt - marginXPt * 2; + const contentHeightPt = pageHeightPt - marginTopPt - marginBottomPt; + + const pdfDoc = await PDFDocument.create(); + + // 关键:只截取标签自身(OuterTagPreview),不要把页面菜单/弹窗带进去 + const pageEls = container.querySelectorAll('.batch-pdf-page'); + + for (let i = 0; i < pageEls.length; i++) { + const el = pageEls[i]; + + // 强制用标签的mm尺寸作为截图基准,避免被外层布局影响 + const canvas = await html2canvas(el, { + backgroundColor: '#ffffff', + scale: 2, + useCORS: true, + // 让 html2canvas 为频繁读回优化 Canvas(浏览器会提示 willReadFrequently) + willReadFrequently: true, + // 确保按元素尺寸截图 + width: el.offsetWidth, + height: el.offsetHeight, + windowWidth: el.scrollWidth, + windowHeight: el.scrollHeight, + }); + + const imgDataUrl = canvas.toDataURL('image/png'); + const pngImage = await pdfDoc.embedPng(imgDataUrl); + + const page = pdfDoc.addPage([pageWidthPt, pageHeightPt]); + + // 图片铺满页面(保持比例,居中) + const imgW = pngImage.width; + const imgH = pngImage.height; + // 按“内容区域(去掉边距)”计算缩放,让四周留下约 4mm 空白 + const scale = Math.min(contentWidthPt / imgW, contentHeightPt / imgH); + const drawW = imgW * scale; + const drawH = imgH * scale; + + // 内容区域内居中 + 外层边距 + const x = marginXPt + (contentWidthPt - drawW) / 2; + const y = marginBottomPt + (contentHeightPt - drawH) / 2; + + page.drawImage(pngImage, { x, y, width: drawW, height: drawH }); + } + + const pdfBytes = await pdfDoc.save(); + const blob = new Blob([pdfBytes], { type: 'application/pdf' }); + const url = URL.createObjectURL(blob); + + // 尽量避免被浏览器拦截弹窗:优先在当前tab打开;如仍被策略限制,可改为下载 + const win = window.open(url, '_blank'); + if (!win) { + // fallback:触发下载 + const a = document.createElement('a'); + a.href = url; + a.download = `钢卷标签_${new Date().getTime()}.pdf`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } + } catch (e) { + console.error(e); + this.$message.error('生成PDF失败,请重试'); + } finally { + this.batchPrint.loading = false; + } + }, /** 导出选中数据操作 */ handleExport() { // 1. 判断是否有选中数据 @@ -792,4 +954,22 @@ export default { } } }; - \ No newline at end of file + + +