feat(标签打印): 优化标签打印功能并新增批量导出
- 新增html2canvas依赖以支持高清打印 - 重构标签打印逻辑,解决二维码丢失和文字模糊问题 - 优化外标签样式布局和公司名称显示 - 新增标签预览功能,可在物料列表中直接查看 - 实现批量导出功能,支持任务规划和进度展示 - 添加导出配置选项,可调整清晰度和单次导出数量
This commit is contained in:
@@ -52,15 +52,16 @@
|
|||||||
"flv.js": "^1.6.2",
|
"flv.js": "^1.6.2",
|
||||||
"fuse.js": "6.4.3",
|
"fuse.js": "6.4.3",
|
||||||
"highlight.js": "10.5.0",
|
"highlight.js": "10.5.0",
|
||||||
|
"html2canvas": "^1.4.1",
|
||||||
"js-beautify": "1.13.0",
|
"js-beautify": "1.13.0",
|
||||||
"js-cookie": "3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
"jsbarcode": "^3.12.1",
|
"jsbarcode": "^3.12.1",
|
||||||
"jsencrypt": "3.0.0-rc.1",
|
"jsencrypt": "3.0.0-rc.1",
|
||||||
"jspdf": "^2.5.2",
|
"jspdf": "^2.5.2",
|
||||||
"pdfjs-dist": "^3.6.172",
|
|
||||||
"konva": "^10.0.2",
|
"konva": "^10.0.2",
|
||||||
"mqtt": "^5.13.3",
|
"mqtt": "^5.13.3",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
|
"pdfjs-dist": "^3.6.172",
|
||||||
"print-js": "^1.6.0",
|
"print-js": "^1.6.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"quill": "1.3.7",
|
"quill": "1.3.7",
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
<div class="company-header">
|
<div class="company-header">
|
||||||
<img :src="logo" alt="Company Logo" class="company-logo" />
|
<img :src="logo" alt="Company Logo" class="company-logo" />
|
||||||
<div class="company-name">
|
<div class="company-name">
|
||||||
嘉祥科伦普重工有限公司<br />
|
科伦普<br />
|
||||||
<span class="english-name">Jiaxiang KLP Heavy Industry Co., Ltd.</span>
|
<span class="english-name">KE LUN PU</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="title">嘉祥科伦普重工有限公司</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 核心信息网格布局:替代原表格 -->
|
<!-- 核心信息网格布局:替代原表格 -->
|
||||||
@@ -102,7 +103,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="contact-timestamp">
|
<div class="contact-timestamp">
|
||||||
<QRCode :content="content.qrcodeRecordId || ''" :size="10" />
|
<QRCode :content="content.qrcodeRecordId || ''" :size="60" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,11 +162,11 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.label-container {
|
.label-container {
|
||||||
width: 45em;
|
width: 45em;
|
||||||
height: 25em;
|
height: 26em;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-family: 'Arial', sans-serif;
|
font-family: 'Arial', sans-serif;
|
||||||
border: 1px solid #000;
|
/* border: 1px solid #000; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.company-header {
|
.company-header {
|
||||||
@@ -175,17 +176,24 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.company-logo {
|
.company-logo {
|
||||||
width: 2.8em;
|
width: 4em;
|
||||||
height: 2.8em;
|
height: 4em;
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.company-name {
|
.company-name {
|
||||||
font-size: 0.95em;
|
font-size: 1em;
|
||||||
width: 100%;
|
font-weight: bold;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.english-name {
|
.english-name {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 0.6em;
|
font-size: 0.6em;
|
||||||
@@ -211,8 +219,8 @@ export default {
|
|||||||
|
|
||||||
.info-grid-item {
|
.info-grid-item {
|
||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
padding: 0.25em;
|
padding: 0.1em;
|
||||||
font-size: 0.78em;
|
font-size: 1em;
|
||||||
height: 2.1em;
|
height: 2.1em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
@@ -220,10 +228,11 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-cell {
|
.label-cell {
|
||||||
font-weight: bold;
|
/* font-weight: bold; */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 生产日期跨3列 */
|
/* 生产日期跨3列 */
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="label-cell" colspan="1">料卷号</td>
|
<td class="label-cell" colspan="1">料卷号</td>
|
||||||
<td class="value-cell" colspan="2">
|
<td class="value-cell" colspan="2">
|
||||||
<input type="text" class="nob" :value="content.enterCoilNo || ''" />
|
<input type="text" class="nob" style="line-height: 95px;" :value="content.enterCoilNo || ''" />
|
||||||
</td>
|
</td>
|
||||||
<td class="value-cell" colspan="1">
|
<td class="value-cell" colspan="1">
|
||||||
<QRCode :content="content.qrcodeRecordId || ' '" :size="5" />
|
<QRCode :content="content.qrcodeRecordId || ' '" :size="5" />
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<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="label-preview-container" ref="labelRef">
|
||||||
<ProductionTagPreview v-if="labelType === '2'" :content="content" />
|
<ProductionTagPreview v-if="labelType === '2'" :content="content" />
|
||||||
@@ -20,7 +18,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import domToImage from 'dom-to-image';
|
import domToImage from 'dom-to-image';
|
||||||
import printJS from 'print-js';
|
import printJS from 'print-js';
|
||||||
import { Message } from 'element-ui'; // 若使用Element UI,引入消息提示
|
import html2canvas from 'html2canvas'; // 新增:引入高清渲染库
|
||||||
|
import { Message } from 'element-ui';
|
||||||
|
|
||||||
import ProductionTagPreview from './ProductionTagPreview.vue';
|
import ProductionTagPreview from './ProductionTagPreview.vue';
|
||||||
import OuterTagPreview from './OuterTagPreview.vue';
|
import OuterTagPreview from './OuterTagPreview.vue';
|
||||||
@@ -48,9 +47,8 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// -------- 图片下载方法 --------
|
// -------- 图片下载方法(保留,可按需替换为html2canvas) --------
|
||||||
async downloadLabelAsImage() {
|
async downloadLabelAsImage() {
|
||||||
// 1. 获取要转换的DOM容器
|
|
||||||
const labelContainer = document.getElementById('label-preview-container');
|
const labelContainer = document.getElementById('label-preview-container');
|
||||||
if (!labelContainer) {
|
if (!labelContainer) {
|
||||||
Message.error('未找到标签容器,无法下载');
|
Message.error('未找到标签容器,无法下载');
|
||||||
@@ -58,24 +56,25 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 2. 用dom-to-image生成PNG图片的DataURL
|
// 可选:也替换为html2canvas提升下载清晰度
|
||||||
|
// const canvas = await html2canvas(labelContainer, { scale: 3, backgroundColor: '#ffffff', allowTaint: true, taintTest: false });
|
||||||
|
// const dataUrl = canvas.toDataURL('image/png', 1.0);
|
||||||
const dataUrl = await domToImage.toPng(labelContainer);
|
const dataUrl = await domToImage.toPng(labelContainer);
|
||||||
|
|
||||||
// 3. 创建临时a标签,触发下载
|
|
||||||
const downloadLink = document.createElement('a');
|
const downloadLink = document.createElement('a');
|
||||||
downloadLink.href = dataUrl;
|
downloadLink.href = dataUrl;
|
||||||
downloadLink.download = `标签_${new Date().getTime()}.png`; // 自定义文件名(带时间戳防重复)
|
downloadLink.download = `标签_${new Date().getTime()}.png`;
|
||||||
document.body.appendChild(downloadLink);
|
document.body.appendChild(downloadLink);
|
||||||
downloadLink.click();
|
downloadLink.click();
|
||||||
document.body.removeChild(downloadLink); // 下载后移除临时标签
|
document.body.removeChild(downloadLink);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('标签图片下载失败:', error);
|
console.error('标签图片下载失败:', error);
|
||||||
Message.error('标签图片下载失败,请重试');
|
Message.error('标签图片下载失败,请重试');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 打印方法
|
// -------- 重构后的打印方法(核心优化) --------
|
||||||
printLabel() {
|
async printLabel() {
|
||||||
// 1. 获取标签容器DOM
|
// 1. 获取标签容器DOM
|
||||||
const labelContainer = document.getElementById('label-preview-container');
|
const labelContainer = document.getElementById('label-preview-container');
|
||||||
if (!labelContainer) {
|
if (!labelContainer) {
|
||||||
@@ -83,21 +82,137 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 调用print-js打印
|
try {
|
||||||
printJS({
|
Message.info('正在准备打印内容,请稍等...');
|
||||||
printable: 'label-preview-container', // 要打印的元素ID
|
|
||||||
type: 'html', // 打印类型为HTML
|
// 2. 等待二维码/字体等资源加载完成(复用之前的等待逻辑)
|
||||||
header: null, // 不显示页眉
|
await this.waitForAllResources(labelContainer);
|
||||||
footer: null, // 不显示页脚
|
|
||||||
// style: printStyles, // 注入打印样式
|
// 3. 用html2canvas生成高清Canvas(解决文字模糊+二维码丢失)
|
||||||
scanStyles: true, // 禁用自动扫描页面样式(避免冲突)
|
const canvas = await html2canvas(labelContainer, {
|
||||||
targetStyles: ['*'], // 允许所有样式生效
|
scale: 3, // 3倍高清渲染(核心)
|
||||||
printContainer: true, // 打印指定容器
|
backgroundColor: '#ffffff', // 强制白色背景,避免打印时背景透明
|
||||||
onError: (error) => { // 错误处理
|
useCORS: true, // 支持跨域图片
|
||||||
console.error('打印失败:', error);
|
allowTaint: true, // 允许渲染canvas(二维码)
|
||||||
Message.error('打印失败,请重试');
|
taintTest: false, // 关闭canvas污染检测
|
||||||
}
|
windowWidth: labelContainer.offsetWidth * 3, // 适配缩放后的宽度
|
||||||
|
windowHeight: labelContainer.offsetHeight * 3, // 适配缩放后的高度
|
||||||
|
logging: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 创建临时打印容器(避免影响原DOM)
|
||||||
|
const printContainerId = 'temp-print-container-' + new Date().getTime();
|
||||||
|
const tempPrintContainer = document.createElement('div');
|
||||||
|
tempPrintContainer.id = printContainerId;
|
||||||
|
// 样式:让打印容器居中、适配纸张
|
||||||
|
tempPrintContainer.style.cssText = `
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #fff;
|
||||||
|
`;
|
||||||
|
// 将高清Canvas插入临时容器
|
||||||
|
tempPrintContainer.appendChild(canvas);
|
||||||
|
// document.body.appendChild(tempPrintContainer);
|
||||||
|
|
||||||
|
// 5. 调用printJS打印高清Canvas(而非原HTML)
|
||||||
|
printJS({
|
||||||
|
printable: tempPrintContainer, // 打印临时容器
|
||||||
|
type: 'html',
|
||||||
|
header: null,
|
||||||
|
footer: null,
|
||||||
|
scanStyles: false, // 禁用自动扫描样式,手动控制
|
||||||
|
// 自定义打印样式:确保Canvas适配纸张、无多余边距
|
||||||
|
style: `
|
||||||
|
#${printContainerId} {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
#${printContainerId} canvas {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
body * { visibility: hidden; }
|
||||||
|
#${printContainerId}, #${printContainerId} * { visibility: visible; }
|
||||||
|
#${printContainerId} { position: absolute; top: 0; left: 0; }
|
||||||
|
@page { margin: 10mm; } /* 打印纸张边距 */
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
printContainer: true,
|
||||||
|
onAfterPrint: () => {
|
||||||
|
// 6. 打印完成后清理临时容器
|
||||||
|
document.body.removeChild(tempPrintContainer);
|
||||||
|
Message.success('打印准备完成,请在打印预览中确认打印');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
// 异常时也清理临时容器
|
||||||
|
document.body.removeChild(tempPrintContainer);
|
||||||
|
console.error('打印失败:', error);
|
||||||
|
Message.error('打印失败,请重试');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// document.removeChild(tempPrintContainer);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打印准备失败:', error);
|
||||||
|
Message.error('打印内容准备失败,请重试');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// -------- 辅助方法:等待所有资源加载(图片+字体+二维码) --------
|
||||||
|
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));
|
||||||
|
},
|
||||||
|
|
||||||
|
// -------- 辅助方法:等待二维码Canvas渲染完成 --------
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -108,7 +223,6 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
/* 按钮与预览垂直排列 */
|
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
@@ -117,7 +231,6 @@ export default {
|
|||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
/* 按钮与预览区的间距 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-container {
|
.preview-container {
|
||||||
@@ -137,4 +250,18 @@ export default {
|
|||||||
box-shadow: 0 0 80px rgba(255, 255, 255, 0.3);
|
box-shadow: 0 0 80px rgba(255, 255, 255, 0.3);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 强制二维码Canvas可见,避免渲染丢失 */
|
||||||
|
:deep(canvas) {
|
||||||
|
display: block;
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 字体平滑,提升文字清晰度 */
|
||||||
|
:deep(.preview-container) {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
font-smooth: always;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<el-tag type="info" size="small" class="coil-no-tag">{{ item.currentCoilNo }}</el-tag>
|
<el-tag type="info" size="small" class="coil-no-tag">{{ item.currentCoilNo }}</el-tag>
|
||||||
<span class="material-type">{{ item.materialType || '原料' }}</span>
|
<!-- <span class="material-type">{{ item.materialType || '原料' }}</span> -->
|
||||||
<el-popover v-if="item.rawMaterial || item.product" placement="top" width="280" trigger="hover"
|
<el-popover v-if="item.rawMaterial || item.product" placement="top" width="280" trigger="hover"
|
||||||
popper-class="material-params-popover">
|
popper-class="material-params-popover">
|
||||||
<div class="material-params-content">
|
<div class="material-params-content">
|
||||||
@@ -136,6 +136,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<i slot="reference" class="el-icon-setting param-icon"></i>
|
<i slot="reference" class="el-icon-setting param-icon"></i>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
|
|
||||||
|
<i class="el-icon-view param-icon" @click="handlePreviewLabel(item)" title="查看标签"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -330,6 +332,11 @@
|
|||||||
<el-button @click="cancelException">取 消</el-button>
|
<el-button @click="cancelException">取 消</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 标签预览弹窗 -->
|
||||||
|
<el-dialog title="标签预览" :visible.sync="labelRender.visible" append-to-body>
|
||||||
|
<label-render :content="labelRender.data" :labelType="labelRender.type" />
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -340,6 +347,7 @@ import { parseTime } from '@/utils/klp'
|
|||||||
import ProductInfo from '@/components/KLPService/Renderer/ProductInfo'
|
import ProductInfo from '@/components/KLPService/Renderer/ProductInfo'
|
||||||
import RawMaterialInfo from '@/components/KLPService/Renderer/RawMaterialInfo'
|
import RawMaterialInfo from '@/components/KLPService/Renderer/RawMaterialInfo'
|
||||||
import { addCoilAbnormal } from '@/api/wms/coilAbnormal'
|
import { addCoilAbnormal } from '@/api/wms/coilAbnormal'
|
||||||
|
import LabelRender from './LabelRender/index.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DoPage',
|
name: 'DoPage',
|
||||||
@@ -356,7 +364,8 @@ export default {
|
|||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ProductInfo,
|
ProductInfo,
|
||||||
RawMaterialInfo
|
RawMaterialInfo,
|
||||||
|
LabelRender
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -372,6 +381,11 @@ export default {
|
|||||||
enterCoilNo: null,
|
enterCoilNo: null,
|
||||||
currentCoilNo: null
|
currentCoilNo: null
|
||||||
},
|
},
|
||||||
|
labelRender: {
|
||||||
|
visible: false,
|
||||||
|
data: {},
|
||||||
|
type: '2'
|
||||||
|
},
|
||||||
|
|
||||||
// 待操作列表相关
|
// 待操作列表相关
|
||||||
actionLoading: false,
|
actionLoading: false,
|
||||||
@@ -418,6 +432,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
tabs: {
|
tabs: {
|
||||||
handler(newVal) {
|
handler(newVal) {
|
||||||
if (newVal.length > 0) {
|
if (newVal.length > 0) {
|
||||||
@@ -462,6 +477,19 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
parseTime,
|
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() {
|
getMaterialCoil() {
|
||||||
|
|||||||
@@ -1,34 +1,167 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div style="display: flex; gap: 20px; padding: 20px; width: 100%; box-sizing: border-box;">
|
||||||
<button
|
<!-- 左侧:原有导出功能区域 -->
|
||||||
@click="handleExportAll"
|
<div style="flex: 1; max-width: 700px;">
|
||||||
:disabled="exportLoading"
|
<!-- 导出配置区域 -->
|
||||||
style="padding: 8px 16px; cursor: pointer; margin-bottom: 20px;"
|
<div class="config-container" style="margin-bottom: 20px; padding: 16px; border: 1px solid #e6e6e6; border-radius: 8px;">
|
||||||
>
|
<div style="font-size: 16px; font-weight: 600; margin-bottom: 12px;">导出配置</div>
|
||||||
{{ exportLoading ? '导出中...' : '导出所有外标签' }}
|
|
||||||
</button>
|
<!-- 清晰度配置 -->
|
||||||
|
<div class="config-item" style="margin-bottom: 10px; display: flex; align-items: center;">
|
||||||
|
<label style="width: 120px; font-size: 14px; color: #333;">导出清晰度:</label>
|
||||||
|
<input
|
||||||
|
v-model.number="config.renderScale"
|
||||||
|
type="number"
|
||||||
|
min="1.0"
|
||||||
|
max="4.0"
|
||||||
|
step="0.1"
|
||||||
|
style="width: 120px;"
|
||||||
|
placeholder="如:1.5/2.0/3.0"
|
||||||
|
@blur="validateRenderScale"
|
||||||
|
/>
|
||||||
|
<span style="margin-left: 8px; font-size: 12px; color: #666;">
|
||||||
|
范围:1.0-4.0(值越高越清晰,导出越慢)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 进度条容器:导出时显示 -->
|
<!-- 单次导出数量配置 -->
|
||||||
<div v-if="showProgress" class="progress-container" style="margin-bottom: 20px; width: 100%; max-width: 600px;">
|
<div class="config-item" style="display: flex; align-items: center;">
|
||||||
<!-- 进度文本:显示当前进度/总数量 -->
|
<label style="width: 120px; font-size: 14px; color: #333;">单次导出数量:</label>
|
||||||
<div class="progress-text" style="margin-bottom: 8px; font-size: 14px; color: #666;">
|
<input
|
||||||
导出进度:{{ currentIndex }}/{{ totalCount }} ({{ progress }}%)
|
v-model.number="config.pageSize"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="200"
|
||||||
|
style="width: 120px;"
|
||||||
|
placeholder="如:10/50/100"
|
||||||
|
@blur="validatePageSize"
|
||||||
|
@change="replanTask"
|
||||||
|
/>
|
||||||
|
<span style="margin-left: 8px; font-size: 12px; color: #666;">
|
||||||
|
范围:1-200(建议不超过100,避免卡顿)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 进度条背景 -->
|
|
||||||
<div class="progress-bar-bg" style="width: 100%; height: 8px; background: #f5f5f5; border-radius: 4px; overflow: hidden;">
|
<!-- 导出按钮:批量导出所有未完成任务 -->
|
||||||
<!-- 进度条进度 -->
|
<button
|
||||||
<div
|
@click="handleExportAll"
|
||||||
class="progress-bar"
|
:disabled="exportLoading || taskPlan.totalTask === 0 || hasRunningTask"
|
||||||
style="height: 100%; background: #409eff; transition: width 0.3s ease;"
|
style="padding: 8px 16px; cursor: pointer; margin-bottom: 20px;"
|
||||||
:style="{ width: `${progress}%` }"
|
>
|
||||||
></div>
|
{{ exportLoading ? '导出中...' : '批量导出所有未完成任务' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 单批导出进度条 -->
|
||||||
|
<div v-if="showProgress" class="progress-container" style="margin-bottom: 20px; width: 100%; max-width: 600px;">
|
||||||
|
<div class="progress-text" style="margin-bottom: 8px; font-size: 14px; color: #666;">
|
||||||
|
当前批次进度:{{ currentIndex }}/{{ totalCount }} ({{ progress }}%)
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-bg" style="width: 100%; height: 8px; background: #f5f5f5; border-radius: 4px; overflow: hidden;">
|
||||||
|
<div
|
||||||
|
class="progress-bar"
|
||||||
|
style="height: 100%; background: #409eff; transition: width 0.3s ease;"
|
||||||
|
:style="{ width: `${progress}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 预览组件 -->
|
||||||
|
<OuterTagPreview ref="outerTagPreview" :content="current" />
|
||||||
|
<!-- 临时渲染容器 -->
|
||||||
|
<div
|
||||||
|
ref="tempTagContainer"
|
||||||
|
style="position: fixed; top: -9999px; left: -9999px; z-index: -9999; width: 794px; padding: 10px;"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 预览组件:保留预览,同时用于复用渲染逻辑 -->
|
<!-- 右侧:任务规划+进度展示区域 -->
|
||||||
<OuterTagPreview ref="outerTagPreview" :content="current" />
|
<div style="flex: 0 0 400px; padding: 16px; border: 1px solid #e6e6e6; border-radius: 8px; height: fit-content;">
|
||||||
<!-- 临时渲染容器:用于逐个渲染标签并转换图片 -->
|
<!-- 任务概览 -->
|
||||||
<div ref="tempTagContainer" style="position: fixed; top: -9999px; left: -9999px; z-index: -9999;"></div>
|
<div style="font-size: 16px; font-weight: 600; margin-bottom: 16px;">
|
||||||
|
任务规划(总数量:{{ taskPlan.totalCount }})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务统计 -->
|
||||||
|
<div style="display: flex; gap: 16px; margin-bottom: 20px; font-size: 14px;">
|
||||||
|
<div>
|
||||||
|
<div style="color: #666; margin-bottom: 4px;">总任务数</div>
|
||||||
|
<div style="font-size: 20px; font-weight: 600; color: #333;">{{ taskPlan.totalTask }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="color: #666; margin-bottom: 4px;">已完成</div>
|
||||||
|
<div style="font-size: 20px; font-weight: 600; color: #67c23a;">{{ taskPlan.completedTask }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="color: #666; margin-bottom: 4px;">进行中</div>
|
||||||
|
<div style="font-size: 20px; font-weight: 600; color: #409eff;">{{ taskPlan.runningTask }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 总进度条 -->
|
||||||
|
<div style="margin-bottom: 20px;">
|
||||||
|
<div style="display: flex; justify-content: space-between; font-size: 12px; color: #666; margin-bottom: 8px;">
|
||||||
|
<span>整体导出进度</span>
|
||||||
|
<span>{{ taskPlan.totalProgress }}%</span>
|
||||||
|
</div>
|
||||||
|
<div style="width: 100%; height: 8px; background: #f5f5f5; border-radius: 4px; overflow: hidden;">
|
||||||
|
<div
|
||||||
|
style="height: 100%; background: #67c23a; transition: width 0.3s ease;"
|
||||||
|
:style="{ width: `${taskPlan.totalProgress}%` }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 当前任务详情 -->
|
||||||
|
<div style="margin-bottom: 20px; padding: 12px; background: #f0f9ff; border-radius: 6px;">
|
||||||
|
<div style="font-size: 14px; font-weight: 600; color: #333; margin-bottom: 8px;">当前任务详情</div>
|
||||||
|
<div v-if="currentTask.idx > 0" style="font-size: 13px; line-height: 1.6;">
|
||||||
|
<div>任务序号:{{ currentTask.idx }}/{{ taskPlan.totalTask }}</div>
|
||||||
|
<div>处理范围:{{ currentTask.startNum }} - {{ currentTask.endNum }} 条</div>
|
||||||
|
<div>当前进度:{{ currentTask.progress }}%</div>
|
||||||
|
<div>状态:<span :style="{ color: currentTask.statusColor }">{{ currentTask.statusText }}</span></div>
|
||||||
|
</div>
|
||||||
|
<div v-else style="font-size: 13px; color: #666;">暂无进行中的任务</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 任务列表 -->
|
||||||
|
<div style="max-height: 400px; overflow-y: auto;">
|
||||||
|
<div
|
||||||
|
v-for="(task, idx) in taskList"
|
||||||
|
:key="idx"
|
||||||
|
style="padding: 10px; border: 1px solid #e6e6e6; border-radius: 6px; margin-bottom: 8px;"
|
||||||
|
:style="{
|
||||||
|
borderColor: task.status === 'running' ? '#409eff' : task.status === 'completed' ? '#67c23a' : '#e6e6e6',
|
||||||
|
background: task.status === 'running' ? '#f0f9ff' : 'transparent'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
|
||||||
|
<span style="font-size: 14px; font-weight: 500;">任务 {{ idx + 1 }}</span>
|
||||||
|
<span :style="{ color: task.statusColor }">{{ task.statusText }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px; color: #666; margin-bottom: 6px;">
|
||||||
|
处理范围:{{ task.startNum }} - {{ task.endNum }} 条
|
||||||
|
</div>
|
||||||
|
<div style="width: 100%; height: 6px; background: #f5f5f5; border-radius: 3px; overflow: hidden; margin-bottom: 8px;">
|
||||||
|
<div
|
||||||
|
style="height: 100%; transition: width 0.3s ease;"
|
||||||
|
:style="{
|
||||||
|
width: `${task.progress}%`,
|
||||||
|
background: task.status === 'running' ? '#409eff' : task.status === 'completed' ? '#67c23a' : '#ccc'
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<!-- 单个任务执行按钮 -->
|
||||||
|
<button
|
||||||
|
@click="handleRunSingleTask(task.idx)"
|
||||||
|
:disabled="exportLoading || task.status === 'running' || task.status === 'completed'"
|
||||||
|
style="padding: 4px 8px; font-size: 12px; border: none; border-radius: 4px; cursor: pointer; background: #409eff; color: white;"
|
||||||
|
>
|
||||||
|
{{ task.status === 'failed' ? '重新执行' : '执行任务' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -36,155 +169,436 @@
|
|||||||
import OuterTagPreview from './LabelRender/OuterTagPreview.vue';
|
import OuterTagPreview from './LabelRender/OuterTagPreview.vue';
|
||||||
import { listMaterialCoil } from "@/api/wms/coil";
|
import { listMaterialCoil } from "@/api/wms/coil";
|
||||||
import jsPDF from 'jspdf';
|
import jsPDF from 'jspdf';
|
||||||
import domtoimage from 'dom-to-image';
|
import html2canvas from 'html2canvas';
|
||||||
|
import { Message } from 'element-ui';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ToolPanel',
|
name: 'ToolPanel',
|
||||||
components: {
|
components: {
|
||||||
OuterTagPreview,
|
OuterTagPreview
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
list: [],
|
list: [], // 当前批次数据
|
||||||
pos: 0,
|
current: {}, // 当前预览数据
|
||||||
current: {},
|
exportLoading: false, // 全局导出加载状态
|
||||||
exportLoading: false,
|
showProgress: false, // 单批进度条显示
|
||||||
// 进度条相关变量
|
progress: 0, // 单批进度百分比
|
||||||
showProgress: false, // 是否显示进度条
|
currentIndex: 0, // 单批当前处理索引
|
||||||
progress: 0, // 进度值(0-100)
|
totalCount: 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() {
|
mounted() {
|
||||||
this.fetchData();
|
this.fetchTotalData(); // 先获取总数量,规划任务
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 获取标签数据
|
// ========== 配置校验 ==========
|
||||||
fetchData() {
|
validateRenderScale() {
|
||||||
listMaterialCoil({
|
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,
|
status: 0,
|
||||||
dataType: 1,
|
dataType: 1,
|
||||||
materialType: '成品',
|
materialType: '成品',
|
||||||
itemType: 'product',
|
itemType: 'product',
|
||||||
selectType: 'raw_material',
|
selectType: 'raw_material',
|
||||||
pageSize: 100,
|
pageSize: this.config.pageSize,
|
||||||
pageNum: 10
|
pageNum
|
||||||
}).then(res => {
|
});
|
||||||
this.list = res.rows || [];
|
|
||||||
if (this.list.length > 0) {
|
|
||||||
this.current = this.list[0];
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('获取标签数据失败:', err);
|
|
||||||
this.$message.error('获取标签数据失败,请刷新重试');
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
// 核心:复用OuterTagPreview组件渲染标签,再转图片导出PDF
|
this.list = res.rows || [];
|
||||||
async handleExportAll() {
|
this.totalCount = this.list.length;
|
||||||
if (this.exportLoading) return;
|
if (this.totalCount === 0) {
|
||||||
if (this.list.length === 0) {
|
this.updateTaskStatus(taskIdx, 'completed', 100);
|
||||||
this.$message.warning('暂无外标签数据可导出');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// 处理当前批次的每个标签
|
||||||
// 初始化进度条
|
this.showProgress = true;
|
||||||
this.exportLoading = true;
|
this.currentIndex = 0;
|
||||||
this.showProgress = true; // 显示进度条
|
this.progress = 0;
|
||||||
this.totalCount = this.list.length; // 总数量
|
|
||||||
this.currentIndex = 0; // 重置当前索引
|
|
||||||
this.progress = 0; // 重置进度
|
|
||||||
|
|
||||||
const doc = new jsPDF({
|
for (let index = 0; index < this.list.length; index++) {
|
||||||
unit: 'mm',
|
this.current = this.list[index];
|
||||||
format: 'a4',
|
await this.waitForDOMRender();
|
||||||
orientation: 'portrait'
|
|
||||||
});
|
// 等待资源加载(图片+字体+二维码)
|
||||||
const tempContainer = this.$refs.tempTagContainer;
|
|
||||||
const previewComponent = this.$refs.outerTagPreview;
|
const previewComponent = this.$refs.outerTagPreview;
|
||||||
|
await this.waitForAllResources(previewComponent.$el);
|
||||||
|
|
||||||
// 循环处理每个标签
|
// 克隆DOM到临时容器
|
||||||
for (let index = 0; index < this.list.length; index++) {
|
const tempContainer = this.$refs.tempTagContainer;
|
||||||
const item = this.list[index];
|
const renderedDOM = previewComponent.$el.cloneNode(true);
|
||||||
this.current = item; // 更新预览组件的content,触发重新渲染
|
tempContainer.innerHTML = '';
|
||||||
|
tempContainer.appendChild(renderedDOM);
|
||||||
// 等待组件渲染完成
|
|
||||||
await this.waitForDOMRender();
|
|
||||||
|
|
||||||
// 复制预览组件的DOM到临时容器
|
await this.waitForDOMRender();
|
||||||
const renderedDOM = previewComponent.$el.cloneNode(true);
|
this.copyCanvasContent(previewComponent.$el, tempContainer);
|
||||||
tempContainer.innerHTML = '';
|
|
||||||
tempContainer.appendChild(renderedDOM);
|
|
||||||
|
|
||||||
// 再次等待临时容器DOM渲染
|
if (!tempContainer.firstChild) {
|
||||||
await this.waitForDOMRender();
|
console.warn(`任务${taskIdx}第${index+1}个标签DOM为空`, this.list[index]);
|
||||||
|
|
||||||
// 检查临时容器是否有内容
|
|
||||||
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);
|
|
||||||
|
|
||||||
// 更新进度条(关键:处理完一个标签就更新)
|
|
||||||
this.updateProgress(index + 1);
|
this.updateProgress(index + 1);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存PDF
|
// 生成高清Canvas
|
||||||
const fileName = `外标签导出_${new Date().toLocaleString().replace(/[/: ]/g, '-')}.pdf`;
|
const canvas = await html2canvas(tempContainer, {
|
||||||
doc.save(fileName);
|
scale: this.config.renderScale,
|
||||||
this.$message.success('PDF导出成功!');
|
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导出失败,详情请查看控制台');
|
// 1. jsPDF实例化时默认创建1个空白页
|
||||||
} finally {
|
// 2. 第一个任务的第一个标签:直接使用默认空白页,不新增
|
||||||
// 重置进度条和加载状态
|
// 3. 第一个任务的非第一个标签:新增页面
|
||||||
this.$refs.tempTagContainer.innerHTML = '';
|
// 4. 非第一个任务的所有标签:新增页面
|
||||||
this.exportLoading = false;
|
const needAddPage = index > 0 || taskIdx > 1;
|
||||||
this.showProgress = false; // 隐藏进度条
|
if (needAddPage) {
|
||||||
this.progress = 0;
|
this.pdfDoc.addPage();
|
||||||
this.currentIndex = 0;
|
}
|
||||||
this.totalCount = 0;
|
|
||||||
|
// 尺寸计算
|
||||||
|
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) {
|
updateProgress(current) {
|
||||||
this.currentIndex = current;
|
this.currentIndex = current;
|
||||||
// 计算进度百分比(保留整数)
|
|
||||||
this.progress = Math.round((current / this.totalCount) * 100);
|
this.progress = Math.round((current / this.totalCount) * 100);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 辅助方法:等待DOM渲染完成
|
// 等待DOM渲染
|
||||||
waitForDOMRender(time = 300) {
|
waitForDOMRender(time = 300) {
|
||||||
return new Promise(resolve => setTimeout(resolve, time));
|
return new Promise(resolve => setTimeout(resolve, time));
|
||||||
}
|
}
|
||||||
@@ -193,6 +607,35 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
:deep(.label-container) {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
font-smooth: always;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(canvas) {
|
||||||
|
display: block;
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 配置区域样式 */
|
||||||
|
.config-container {
|
||||||
|
input {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
input:focus {
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式优化 */
|
||||||
button {
|
button {
|
||||||
border: 1px solid #409eff;
|
border: 1px solid #409eff;
|
||||||
background: #409eff;
|
background: #409eff;
|
||||||
@@ -204,33 +647,4 @@ button:disabled {
|
|||||||
background: #a0cfff;
|
background: #a0cfff;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 进度条样式优化(可选:替换内联样式,统一管理) */
|
|
||||||
.progress-container {
|
|
||||||
:deep(.progress-text) {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
:deep(.progress-bar-bg) {
|
|
||||||
width: 100%;
|
|
||||||
height: 8px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
:deep(.progress-bar) {
|
|
||||||
height: 100%;
|
|
||||||
background: #409eff;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保预览组件的DOM能被正确复制 */
|
|
||||||
:deep(.label-container) {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user