Merge branch '0.8.X' of https://gitee.com/hdka/klp-oa into 0.8.X

This commit is contained in:
砂糖
2026-01-09 19:06:36 +08:00
4 changed files with 179 additions and 172 deletions

View File

@@ -103,7 +103,8 @@
</span> </span>
</div> </div>
<div class="contact-timestamp"> <div class="contact-timestamp">
<QRCode :content="content.qrcodeRecordId || ''" :size="60" /> <!-- 放大二维码占用更多下方空间弱化底部留白感 -->
<QRCode :content="content.qrcodeRecordId || ''" :size="90" />
</div> </div>
</div> </div>
</div> </div>
@@ -275,7 +276,8 @@ export default {
/* height: 377.953px; */ /* height: 377.953px; */
width: fit-content; width: fit-content;
height: fit-content; height: fit-content;
padding: 0.5em; /* 减少内边距避免生成PDF时上下留白不一致 */
padding: 0.1em;
font-size: 12px; font-size: 12px;
border: 1px solid #000; border: 1px solid #000;
box-sizing: border-box; box-sizing: border-box;
@@ -287,31 +289,36 @@ export default {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 0.3em; margin-bottom: 0.3em;
position: relative; /* 为绝对定位的 title 提供定位上下文 */
} }
.company-logo { .company-logo {
width: 4em; width: 5em;
height: 4em; height: 5em;
margin-right: 0.5em; margin-right: 0.5em;
} }
.company-name { .company-name {
font-size: 1em; font-size: 1.2em;
font-weight: bold; font-weight: bold;
line-height: 1.1; line-height: 1.1;
} }
.title { .title {
flex: 1; position: absolute; /* 绝对定位,脱离 flex 流 */
left: 50%; /* 从左边50%开始 */
transform: translateX(-50%); /* 向左移动自身宽度的50%,实现居中 */
font-size: 2em; font-size: 2em;
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
white-space: nowrap; /* 防止文字换行 */
} }
.english-name { .english-name {
width: 100%; width: 100%;
font-size: 0.6em; font-size: 1em;
opacity: 0.8; opacity: 0.9;
letter-spacing: 0.08em;
} }
.product-title { .product-title {
@@ -354,23 +361,27 @@ export default {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
font-size: 0.7em; font-size: 0.85em;
margin-top: 0.3em; margin-top: 0.2em;
} }
.address { .address {
line-height: 1.2; line-height: 1.25;
width: 65%; width: 65%;
font-size: 1.3em;
} }
.english-address { .english-address {
opacity: 0.8; opacity: 0.9;
font-size: 0.9em; font-size: 1.1em;
line-height: 1.15;
word-break: normal; /* 正常换行,只在单词边界(空格)处换行 */
overflow-wrap: normal; /* 不在单词内部断行,保持单词完整 */
} }
.contact-timestamp { .contact-timestamp {
text-align: right; text-align: right;
line-height: 1.4; line-height: 1.2;
} }
.nob { .nob {

View File

@@ -170,7 +170,7 @@ export default {
const container = this.$el; const container = this.$el;
if (!container) return; if (!container) return;
// 纸张尺寸180mm x 100mm // 纸张尺寸100mm x 80mm
const dpi = 96; // 标准 DPI const dpi = 96; // 标准 DPI
const mmToPx = dpi / 25.4; // 1mm = 3.779527559px const mmToPx = dpi / 25.4; // 1mm = 3.779527559px
@@ -231,6 +231,7 @@ export default {
flex-direction: column; /* 子元素垂直排列 */ flex-direction: column; /* 子元素垂直排列 */
font-family: "SimSun", serif; font-family: "SimSun", serif;
box-sizing: border-box; /* 确保内边距/边框不影响总尺寸 */ box-sizing: border-box; /* 确保内边距/边框不影响总尺寸 */
overflow: visible; /* 确保所有内容可见,不被裁剪 */
} }
.material-label-table { .material-label-table {
@@ -269,10 +270,10 @@ export default {
text-align: center; text-align: center;
} }
/* 打印样式 - 强制单页适配180mm x 100mm纸张保持原有样式不变 */ /* 打印样式 - 强制单页适配100mm x 80mm纸张保持原有样式不变 */
@media print { @media print {
@page { @page {
size: 180mm 100mm; size: 100mm 80mm;
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 !important;
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="label-render-container"> <div class="label-render-container">
<!-- 标签预览容器 --> <!-- 标签预览容器 -->
<div class="preview-container" id="label-preview-container" ref="labelRef"> <div class="preview-container" :id="hideActions ? undefined : 'label-preview-container'" ref="labelRef">
<ProductionTagPreview <ProductionTagPreview
v-if="labelType === '2'" v-if="labelType === '2'"
:content="content" :content="content"
@@ -18,7 +18,7 @@
<ForgeTagPreview v-if="labelType === '5'" :content="content" /> <ForgeTagPreview v-if="labelType === '5'" :content="content" />
<SaltSprayTagPreview v-if="labelType === '6'" :content="content" /> <SaltSprayTagPreview v-if="labelType === '6'" :content="content" />
</div> </div>
<div class="action-buttons"> <div class="action-buttons" v-if="!hideActions">
<el-button type="primary" @click="downloadLabelAsImage">下载标签图片</el-button> <el-button type="primary" @click="downloadLabelAsImage">下载标签图片</el-button>
<el-button type="primary" @click="printLabel" style="margin-left: 10px;">打印标签</el-button> <el-button type="primary" @click="printLabel" style="margin-left: 10px;">打印标签</el-button>
</div> </div>
@@ -27,9 +27,9 @@
<script> <script>
import domToImage from 'dom-to-image'; import domToImage from 'dom-to-image';
// import printJS from 'print-js'; // 改为自建 iframe 打印,避免多余空白页 import html2canvas from 'html2canvas'; // 高清渲染
import html2canvas from 'html2canvas'; // 新增:引入高清渲染库
import { Message } from 'element-ui'; import { Message } from 'element-ui';
import { PDFDocument } from 'pdf-lib';
import ProductionTagPreview from './ProductionTagPreview.vue'; import ProductionTagPreview from './ProductionTagPreview.vue';
import OuterTagPreview from './OuterTagPreview.vue'; import OuterTagPreview from './OuterTagPreview.vue';
@@ -55,11 +55,15 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
hideActions: {
type: Boolean,
default: false,
},
}, },
methods: { methods: {
// -------- 图片下载方法保留可按需替换为html2canvas -------- // -------- 图片下载方法保留可按需替换为html2canvas --------
async downloadLabelAsImage() { async downloadLabelAsImage() {
const labelContainer = document.getElementById('label-preview-container'); const labelContainer = this.$refs.labelRef;
if (!labelContainer) { if (!labelContainer) {
Message.error('未找到标签容器,无法下载'); Message.error('未找到标签容器,无法下载');
return; return;
@@ -83,11 +87,12 @@ export default {
} }
}, },
// -------- 重构后的打印方法(核心优化) -------- // -------- 单张打印:先生成 PDF再让浏览器打印 --------
async printLabel() { async printLabel() {
// 1. 获取标签容器DOM // 1. 获取标签容器DOM
const labelContainer = document.querySelector('.label-container') || // 只在当前组件作用域内查找,避免页面上存在多个标签时选错
document.querySelector('.material-label-container'); const labelContainer = this.$refs.labelRef?.querySelector('.label-container') ||
this.$refs.labelRef?.querySelector('.material-label-container');
if (!labelContainer) { if (!labelContainer) {
Message.error('未找到标签容器,无法打印'); Message.error('未找到标签容器,无法打印');
return; return;
@@ -96,137 +101,128 @@ export default {
try { try {
Message.info('正在准备打印内容,请稍等...'); Message.info('正在准备打印内容,请稍等...');
// 2. 等待二维码/字体等资源加载完成(复用之前的等待逻辑 // 2. 计算纸张尺寸(与批量导出保持一致
await this.waitForAllResources(labelContainer); // 根据 labelType 判断:'2' 是材料标签100x80'3' 是外标180x100
const isMaterial = this.labelType === '2';
// 3. 计算纸张尺寸和缩放比例
// 根据标签类型设置纸张尺寸:
// 产品码100mm x 180mm横向原先180x100
// 原料码80mm x 100mm竖向
const isMaterial = labelContainer.classList.contains('material-label-container');
const paperWidthMm = isMaterial ? 100 : 180; const paperWidthMm = isMaterial ? 100 : 180;
const paperHeightMm = isMaterial ? 80 : 100; const paperHeightMm = isMaterial ? 80 : 100;
const dpi = 96;
const mmToPx = dpi / 25.4;
const paperWidthPx = paperWidthMm * mmToPx;
const paperHeightPx = paperHeightMm * mmToPx;
// 获取标签容器的实际尺寸
const containerRect = labelContainer.getBoundingClientRect();
const containerWidth = containerRect.width;
const containerHeight = containerRect.height;
// 计算缩放比例确保内容适配到纸张留出2mm边距
const marginMm = 2;
const marginPx = marginMm * mmToPx;
const availableWidth = paperWidthPx - marginPx * 2;
const availableHeight = paperHeightPx - marginPx * 2;
const scaleX = containerWidth > 0 ? availableWidth / containerWidth : 1;
const scaleY = containerHeight > 0 ? availableHeight / containerHeight : 1;
const printScale = Math.min(scaleX, scaleY, 1); // 不超过1不放大
// 计算最终Canvas尺寸适配纸张
const finalCanvasWidth = containerWidth * printScale;
const finalCanvasHeight = containerHeight * printScale;
// 使用合适的scale值生成高清Canvas但不超过纸张尺寸 // 使用合适的scale值生成高清Canvas但不超过纸张尺寸
const canvasScale = Math.min(2, printScale * 2); // 适当提高清晰度 const canvasScale = 3; // 提高清晰度(单张打印)
// 4. 用html2canvas生成高清Canvas解决文字模糊+二维码丢失) // 3. 为单张打印临时创建一个与批量打印完全一致的隐藏容器
// 注意:不再强制指定 width/height避免裁掉最右/最下边框 // 完全模拟批量打印的 .batch-pdf-page 容器
const originalParent = labelContainer.parentNode;
const originalNext = labelContainer.nextSibling;
const wrapper = document.createElement('div');
wrapper.style.position = 'fixed';
wrapper.style.left = '-100000px'; // 与批量打印一致
wrapper.style.top = '0';
wrapper.style.width = `${paperWidthMm}mm`; // 固定尺寸,与批量打印的 .batch-pdf-page 一致
wrapper.style.height = `${paperHeightMm}mm`;
wrapper.style.boxSizing = 'border-box';
wrapper.style.backgroundColor = '#ffffff';
wrapper.style.overflow = 'hidden'; // 与批量打印的 .batch-pdf-page 一致
// 根据标签类型设置不同的布局方式
if (isMaterial) {
// 材料标签:填充 wrapper
wrapper.style.display = 'flex';
wrapper.style.alignItems = 'center';
wrapper.style.justifyContent = 'center';
labelContainer.style.width = '100%';
labelContainer.style.height = '100%';
} else {
// 成品标签:直接放在 wrapper 中,保持原始布局(与批量打印一致)
wrapper.style.display = 'block';
wrapper.style.padding = '0';
// 不修改 labelContainer 的样式,保持 fit-content与批量打印一致
}
wrapper.appendChild(labelContainer);
document.body.appendChild(wrapper);
// 4. 等待二维码/字体等资源加载完成(与批量打印一致)
await this.waitForAllResources(labelContainer);
// 等待布局稳定(与批量打印一致)
await new Promise(resolve => setTimeout(resolve, 100));
// 5. 用html2canvas生成高清Canvas与批量导出完全一致
// 关键:直接截图 labelContainer与批量打印完全一致
const canvas = await html2canvas(labelContainer, { const canvas = await html2canvas(labelContainer, {
scale: canvasScale, backgroundColor: '#ffffff',
backgroundColor: '#ffffff', // 强制白色背景,避免打印时背景透明 scale: 3,
useCORS: true, // 支持跨域图片 useCORS: true,
allowTaint: true, // 允许渲染canvas二维码 // 让 html2canvas 为频繁读回优化 Canvas与批量导出一致
taintTest: false, // 关闭canvas污染检测 willReadFrequently: true,
logging: false, // 确保按元素尺寸截图(与批量导出完全一致)
width: labelContainer.offsetWidth,
height: labelContainer.offsetHeight,
windowWidth: labelContainer.scrollWidth,
windowHeight: labelContainer.scrollHeight,
}); });
// 5. 如果Canvas尺寸超出纸张需要缩放Canvas // 5. 使用 pdf-lib 生成单页 PDF与批量导出完全一致
let finalCanvas = canvas; const mmToPt = 72 / 25.4;
if (canvas.width > paperWidthPx || canvas.height > paperHeightPx) { const pageWidthPt = paperWidthMm * mmToPt;
// 创建新的Canvas尺寸适配纸张 const pageHeightPt = paperHeightMm * mmToPt;
const scaledCanvas = document.createElement('canvas');
scaledCanvas.width = Math.min(canvas.width, paperWidthPx); // 边距:与批量导出保持一致
scaledCanvas.height = Math.min(canvas.height, paperHeightPx); // 材料标签100x80左右2mm上下0.5mm
const ctx = scaledCanvas.getContext('2d'); // 成品标签180x100左右4mm上下0.5mm
ctx.drawImage(canvas, 0, 0, scaledCanvas.width, scaledCanvas.height); const marginXmm = isMaterial ? 2 : 4;
finalCanvas = scaledCanvas; const marginYmm = 0.5; // 上下对称边距(不裁切前提下尽量贴边)
const marginXPt = marginXmm * mmToPt;
const marginYPt = marginYmm * mmToPt;
const contentWidthPt = pageWidthPt - marginXPt * 2;
const contentHeightPt = pageHeightPt - marginYPt * 2;
const pdfDoc = await PDFDocument.create();
const imgPng = await pdfDoc.embedPng(canvas.toDataURL('image/png'));
const page = pdfDoc.addPage([pageWidthPt, pageHeightPt]);
const imgW = imgPng.width;
const imgH = imgPng.height;
// 标准适配:确保完整显示且不变形(在内容区域内等比缩放,与批量导出完全一致)
const scale = Math.min(contentWidthPt / imgW, contentHeightPt / imgH);
const drawW = imgW * scale;
const drawH = imgH * scale;
// 内容区域内居中 + 外层边距(上下对称,与批量导出完全一致)
const x = marginXPt + (contentWidthPt - drawW) / 2;
const y = marginYPt + (contentHeightPt - drawH) / 2;
page.drawImage(imgPng, { x, y, width: drawW, height: drawH });
const pdfBytes = await pdfDoc.save();
const blob = new Blob([pdfBytes], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const win = window.open(url, '_blank');
if (!win) {
const a = document.createElement('a');
a.href = url;
a.download = `标签_${new Date().getTime()}.pdf`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
} }
// 6. 生成 DataURL使用 printJS 的 image 模式(单图),并减少额外样式以避免隐藏内容 // 还原 DOM 结构
const dataUrl = finalCanvas.toDataURL('image/png'); // 恢复 labelContainer 的原始样式(如果之前修改过)
if (isMaterial) {
// 6. 创建隐藏 iframe 进行打印,避免 printJS 的分页行为 labelContainer.style.width = '';
const iframe = document.createElement('iframe'); labelContainer.style.height = '';
iframe.style.position = 'fixed'; }
iframe.style.right = '0'; // 成品标签不需要恢复,因为没有修改样式
iframe.style.bottom = '0'; if (originalParent) {
iframe.style.width = '0'; if (originalNext) {
iframe.style.height = '0'; originalParent.insertBefore(labelContainer, originalNext);
iframe.style.border = '0'; } else {
document.body.appendChild(iframe); originalParent.appendChild(labelContainer);
}
const doc = iframe.contentWindow.document; }
doc.open(); document.body.removeChild(wrapper);
doc.write(`
<!doctype html>
<html>
<head>
<style>
@page {
size: ${paperWidthMm}mm ${paperHeightMm}mm;
margin: 2mm;
}
* {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
body {
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
}
img {
/* 稍微缩小一点,避免被打印机物理不可打印区域裁掉边框 */
width: 94%;
height: auto;
max-height: 94%;
object-fit: contain;
display: block;
page-break-after: avoid;
page-break-before: avoid;
page-break-inside: avoid;
}
</style>
</head>
<body>
<img src="${dataUrl}" />
</body>
</html>
`);
doc.close();
iframe.onload = () => {
setTimeout(() => {
iframe.contentWindow.focus();
iframe.contentWindow.print();
setTimeout(() => {
document.body.removeChild(iframe);
}, 1000);
}, 300);
};
} catch (error) { } catch (error) {
console.error('打印准备失败:', error); console.error('打印准备失败:', error);
Message.error('打印内容准备失败,请重试'); Message.error('打印内容准备失败,请重试');
@@ -285,6 +281,7 @@ export default {
</script> </script>
<style scoped> <style scoped>
.label-render-container { .label-render-container {
width: 100%; width: 100%;
display: flex; display: flex;

View File

@@ -63,7 +63,7 @@
<el-col :span="1.5"> <el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExportAll">导出</el-button> <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExportAll">导出</el-button>
</el-col> </el-col>
<el-col :span="2" v-if="canExportAll"> <el-col :span="2">
<el-button type="info" plain icon="el-icon-printer" size="mini" :disabled="multiple" @click="handleBatchPrintLabel">批量打印标签</el-button> <el-button type="info" plain icon="el-icon-printer" size="mini" :disabled="multiple" @click="handleBatchPrintLabel">批量打印标签</el-button>
</el-col> </el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar> <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
@@ -246,7 +246,7 @@
<!-- 渲染容器屏幕隐藏仅用于截图生成PDF --> <!-- 渲染容器屏幕隐藏仅用于截图生成PDF -->
<div ref="batchPdfContainer" class="batch-pdf-root" aria-hidden="true"> <div ref="batchPdfContainer" class="batch-pdf-root" aria-hidden="true">
<div v-for="(item, idx) in batchPrint.list" :key="item.coilId || idx" class="batch-pdf-page"> <div v-for="(item, idx) in batchPrint.list" :key="item.coilId || idx" class="batch-pdf-page">
<OuterTagPreview :content="item" :paperWidthMm="180" :paperHeightMm="100" /> <label-render :content="item" :labelType="labelType" :hideActions="true" />
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
@@ -343,10 +343,6 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
canExportAll: {
type: Boolean,
default: false,
}
}, },
data() { data() {
return { return {
@@ -822,40 +818,44 @@ export default {
// 纸张尺寸 // 纸张尺寸
const paperWidthMm = 180; const paperWidthMm = 180;
const paperHeightMm = 100; const paperHeightMm = 100;
// 留白:左右 4mm上下更小一些,避免底部空得太多 // 边距:左右 4mm上下对称 2mm确保垂直居中
const marginXmm = 4; const marginXmm = 4;
const marginTopMm = 0; const marginYmm = 0.5; // 上下对称边距(不裁切前提下尽量贴边)
const marginBottomMm = 0;
const pageWidthPt = paperWidthMm * mmToPt; const pageWidthPt = paperWidthMm * mmToPt;
const pageHeightPt = paperHeightMm * mmToPt; const pageHeightPt = paperHeightMm * mmToPt;
const marginXPt = marginXmm * mmToPt; const marginXPt = marginXmm * mmToPt;
const marginTopPt = marginTopMm * mmToPt; const marginYPt = marginYmm * mmToPt;
const marginBottomPt = marginBottomMm * mmToPt;
const contentWidthPt = pageWidthPt - marginXPt * 2; const contentWidthPt = pageWidthPt - marginXPt * 2;
const contentHeightPt = pageHeightPt - marginTopPt - marginBottomPt; const contentHeightPt = pageHeightPt - marginYPt * 2;
const pdfDoc = await PDFDocument.create(); const pdfDoc = await PDFDocument.create();
// 关键:只截取标签身(OuterTagPreview不要把页面菜单/弹窗带进去 // 关键:只截取标签身(.label-container不要把外层容器/按钮高度算进去
const pageEls = container.querySelectorAll('.batch-pdf-page'); const pageEls = container.querySelectorAll('.batch-pdf-page');
for (let i = 0; i < pageEls.length; i++) { for (let i = 0; i < pageEls.length; i++) {
const el = pageEls[i]; const el = pageEls[i];
// 强制用标签的mm尺寸作为截图基准避免被外层布局影响 // 在每一页内部优先查找标签根节点
const canvas = await html2canvas(el, { const labelEl =
el.querySelector('.label-container') ||
el.querySelector('.material-label-container') ||
el; // 兜底:找不到时退回整页
// 强制用标签的实际尺寸作为截图基准,避免被外层布局影响
const canvas = await html2canvas(labelEl, {
backgroundColor: '#ffffff', backgroundColor: '#ffffff',
scale: 2, scale: 3,
useCORS: true, useCORS: true,
// 让 html2canvas 为频繁读回优化 Canvas浏览器会提示 willReadFrequently // 让 html2canvas 为频繁读回优化 Canvas浏览器会提示 willReadFrequently
willReadFrequently: true, willReadFrequently: true,
// 确保按元素尺寸截图 // 确保按元素尺寸截图
width: el.offsetWidth, width: labelEl.offsetWidth,
height: el.offsetHeight, height: labelEl.offsetHeight,
windowWidth: el.scrollWidth, windowWidth: labelEl.scrollWidth,
windowHeight: el.scrollHeight, windowHeight: labelEl.scrollHeight,
}); });
const imgDataUrl = canvas.toDataURL('image/png'); const imgDataUrl = canvas.toDataURL('image/png');
@@ -866,18 +866,16 @@ export default {
// 图片铺满页面(保持比例,居中) // 图片铺满页面(保持比例,居中)
const imgW = pngImage.width; const imgW = pngImage.width;
const imgH = pngImage.height; const imgH = pngImage.height;
// 按“内容区域(去掉边距)”计算缩放,让四周留下约 4mm 空白 // 标准适配:确保完整显示且不变形(在内容区域内等比缩放)
const scale = Math.min(contentWidthPt / imgW, contentHeightPt / imgH); const scale = Math.min(contentWidthPt / imgW, contentHeightPt / imgH);
const drawW = imgW * scale; const drawW = imgW * scale;
const drawH = imgH * scale; const drawH = imgH * scale;
// 内容区域内居中 + 外层边距 // 内容区域内居中 + 外层边距(上下对称)
const x = marginXPt + (contentWidthPt - drawW) / 2; const x = marginXPt + (contentWidthPt - drawW) / 2;
const y = marginBottomPt + (contentHeightPt - drawH) / 2; const y = marginYPt + (contentHeightPt - drawH) / 2;
console.log(pngImage) page.drawImage(pngImage, { x, y, width: drawW, height: drawH });
page.drawImage(pngImage, { x, y: y - 20, width: drawW, height: drawH + 20 });
} }
const pdfBytes = await pdfDoc.save(); const pdfBytes = await pdfDoc.save();