2026-01-08 10:02:59 +08:00
|
|
|
|
<template>
|
2026-01-08 16:29:47 +08:00
|
|
|
|
<div style="display: flex; gap: 20px; padding: 20px; width: 100%; box-sizing: border-box;">
|
|
|
|
|
|
<!-- 左侧:原有导出功能区域 -->
|
|
|
|
|
|
<div style="flex: 1; max-width: 700px;">
|
|
|
|
|
|
<!-- 导出配置区域 -->
|
2026-01-09 13:26:52 +08:00
|
|
|
|
<div class="config-container"
|
|
|
|
|
|
style="margin-bottom: 20px; padding: 16px; border: 1px solid #e6e6e6; border-radius: 8px;">
|
2026-01-08 16:29:47 +08:00
|
|
|
|
<div style="font-size: 16px; font-weight: 600; margin-bottom: 12px;">导出配置</div>
|
2026-01-09 13:26:52 +08:00
|
|
|
|
|
2026-01-08 16:29:47 +08:00
|
|
|
|
<!-- 清晰度配置 -->
|
|
|
|
|
|
<div class="config-item" style="margin-bottom: 10px; display: flex; align-items: center;">
|
|
|
|
|
|
<label style="width: 120px; font-size: 14px; color: #333;">导出清晰度:</label>
|
2026-01-09 13:26:52 +08:00
|
|
|
|
<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" />
|
2026-01-08 16:29:47 +08:00
|
|
|
|
<span style="margin-left: 8px; font-size: 12px; color: #666;">
|
|
|
|
|
|
范围:1.0-4.0(值越高越清晰,导出越慢)
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 单次导出数量配置 -->
|
|
|
|
|
|
<div class="config-item" style="display: flex; align-items: center;">
|
|
|
|
|
|
<label style="width: 120px; font-size: 14px; color: #333;">单次导出数量:</label>
|
2026-01-09 13:26:52 +08:00
|
|
|
|
<input v-model.number="config.pageSize" type="number" min="1" max="200" style="width: 120px;"
|
|
|
|
|
|
placeholder="如:10/50/100" @blur="validatePageSize" @change="replanTask" />
|
2026-01-08 16:29:47 +08:00
|
|
|
|
<span style="margin-left: 8px; font-size: 12px; color: #666;">
|
|
|
|
|
|
范围:1-200(建议不超过100,避免卡顿)
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-01-08 10:02:59 +08:00
|
|
|
|
</div>
|
2026-01-08 16:29:47 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 导出按钮:批量导出所有未完成任务 -->
|
2026-01-09 13:26:52 +08:00
|
|
|
|
<button @click="handleExportAll" :disabled="exportLoading || taskPlan.totalTask === 0 || hasRunningTask"
|
|
|
|
|
|
style="padding: 8px 16px; cursor: pointer; margin-bottom: 20px;">
|
2026-01-08 16:29:47 +08:00
|
|
|
|
{{ 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>
|
2026-01-09 13:26:52 +08:00
|
|
|
|
<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>
|
2026-01-08 16:29:47 +08:00
|
|
|
|
</div>
|
2026-01-08 10:02:59 +08:00
|
|
|
|
</div>
|
2026-01-08 16:29:47 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 预览组件 -->
|
|
|
|
|
|
<OuterTagPreview ref="outerTagPreview" :content="current" />
|
2026-01-09 13:26:52 +08:00
|
|
|
|
<!-- 临时渲染容器【已修复:解决留白核心样式】 -->
|
|
|
|
|
|
<div ref="tempTagContainer"
|
|
|
|
|
|
style="position: fixed; top: -9999px; left: -9999px; z-index: -9999; width: 794px; padding: 10px; box-sizing: border-box; overflow: hidden !important;">
|
|
|
|
|
|
</div>
|
2026-01-08 10:02:59 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-08 16:29:47 +08:00
|
|
|
|
<!-- 右侧:任务规划+进度展示区域 -->
|
|
|
|
|
|
<div style="flex: 0 0 400px; padding: 16px; border: 1px solid #e6e6e6; border-radius: 8px; height: fit-content;">
|
|
|
|
|
|
<!-- 任务概览 -->
|
|
|
|
|
|
<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;">
|
2026-01-09 13:26:52 +08:00
|
|
|
|
<div style="height: 100%; background: #67c23a; transition: width 0.3s ease;"
|
|
|
|
|
|
:style="{ width: `${taskPlan.totalProgress}%` }"></div>
|
2026-01-08 16:29:47 +08:00
|
|
|
|
</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;">
|
2026-01-09 13:26:52 +08:00
|
|
|
|
<div v-for="(task, idx) in taskList" :key="idx"
|
|
|
|
|
|
style="padding: 10px; border: 1px solid #e6e6e6; border-radius: 6px; margin-bottom: 8px;" :style="{
|
2026-01-08 16:29:47 +08:00
|
|
|
|
borderColor: task.status === 'running' ? '#409eff' : task.status === 'completed' ? '#67c23a' : '#e6e6e6',
|
|
|
|
|
|
background: task.status === 'running' ? '#f0f9ff' : 'transparent'
|
2026-01-09 13:26:52 +08:00
|
|
|
|
}">
|
2026-01-08 16:29:47 +08:00
|
|
|
|
<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>
|
2026-01-09 13:26:52 +08:00
|
|
|
|
<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>
|
2026-01-08 16:29:47 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 单个任务执行按钮 -->
|
2026-01-09 13:26:52 +08:00
|
|
|
|
<button @click="handleRunSingleTask(task.idx)"
|
2026-01-08 16:29:47 +08:00
|
|
|
|
:disabled="exportLoading || task.status === 'running' || task.status === 'completed'"
|
2026-01-09 13:26:52 +08:00
|
|
|
|
style="padding: 4px 8px; font-size: 12px; border: none; border-radius: 4px; cursor: pointer; background: #409eff; color: white;">
|
2026-01-08 16:29:47 +08:00
|
|
|
|
{{ task.status === 'failed' ? '重新执行' : '执行任务' }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-08 10:02:59 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
import OuterTagPreview from './LabelRender/OuterTagPreview.vue';
|
|
|
|
|
|
import { listMaterialCoil } from "@/api/wms/coil";
|
|
|
|
|
|
import jsPDF from 'jspdf';
|
2026-01-08 16:29:47 +08:00
|
|
|
|
import html2canvas from 'html2canvas';
|
|
|
|
|
|
import { Message } from 'element-ui';
|
2026-01-08 10:02:59 +08:00
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
name: 'ToolPanel',
|
|
|
|
|
|
components: {
|
2026-01-08 16:29:47 +08:00
|
|
|
|
OuterTagPreview
|
2026-01-08 10:02:59 +08:00
|
|
|
|
},
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
2026-01-08 16:29:47 +08:00
|
|
|
|
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'
|
|
|
|
|
|
},
|
2026-01-09 13:26:52 +08:00
|
|
|
|
pdfDoc: null, // 全局PDF实例,支持追加内容
|
2026-01-08 16:29:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
// 是否有任务正在执行
|
|
|
|
|
|
hasRunningTask() {
|
|
|
|
|
|
return this.taskPlan.runningTask > 0;
|
2026-01-08 10:02:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
mounted() {
|
2026-01-08 16:29:47 +08:00
|
|
|
|
this.fetchTotalData(); // 先获取总数量,规划任务
|
2026-01-08 10:02:59 +08:00
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
2026-01-08 16:29:47 +08:00
|
|
|
|
// ========== 配置校验 ==========
|
|
|
|
|
|
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;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 任务规划核心逻辑 ==========
|
|
|
|
|
|
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('获取数据总量失败,请刷新重试');
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
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' };
|
|
|
|
|
|
this.pdfDoc = null;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
replanTask() {
|
|
|
|
|
|
this.validatePageSize();
|
|
|
|
|
|
this.taskPlan.totalTask = Math.ceil(this.taskPlan.totalCount / this.config.pageSize);
|
|
|
|
|
|
this.initTaskList();
|
|
|
|
|
|
Message.success(`已重新规划任务!单次导出${this.config.pageSize}条,总计${this.taskPlan.totalTask}个任务`);
|
2026-01-08 10:02:59 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-01-08 16:29:47 +08:00
|
|
|
|
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);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 核心导出逻辑 ==========
|
2026-01-08 10:02:59 +08:00
|
|
|
|
async handleExportAll() {
|
2026-01-08 16:29:47 +08:00
|
|
|
|
if (this.exportLoading || this.hasRunningTask) return;
|
|
|
|
|
|
if (this.taskPlan.totalCount === 0) {
|
|
|
|
|
|
Message.warning('暂无外标签数据可导出');
|
2026-01-08 10:02:59 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.exportLoading = true;
|
2026-01-09 13:26:52 +08:00
|
|
|
|
// 初始化PDF 动态尺寸【核心修改】
|
|
|
|
|
|
this.pdfDoc = new jsPDF({
|
|
|
|
|
|
unit: 'mm',
|
|
|
|
|
|
orientation: 'portrait',
|
|
|
|
|
|
format: [0, 0]
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-08 16:29:47 +08:00
|
|
|
|
const uncompletedTasks = this.taskList.filter(t => t.status !== 'completed');
|
|
|
|
|
|
if (uncompletedTasks.length === 0) {
|
|
|
|
|
|
Message.info('所有任务均已完成,无需导出');
|
|
|
|
|
|
this.exportLoading = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-01-08 10:02:59 +08:00
|
|
|
|
|
2026-01-08 16:29:47 +08:00
|
|
|
|
for (const task of uncompletedTasks) {
|
|
|
|
|
|
await this.runSingleTaskCore(task.idx);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async handleRunSingleTask(taskIdx) {
|
|
|
|
|
|
if (this.exportLoading || this.hasRunningTask) {
|
|
|
|
|
|
Message.warning('有任务正在执行中,请等待完成后再操作');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
this.exportLoading = true;
|
2026-01-09 13:26:52 +08:00
|
|
|
|
// 初始化PDF 动态尺寸【核心修改】
|
2026-01-08 16:29:47 +08:00
|
|
|
|
if (!this.pdfDoc) {
|
2026-01-09 13:26:52 +08:00
|
|
|
|
this.pdfDoc = new jsPDF({
|
|
|
|
|
|
unit: 'mm',
|
|
|
|
|
|
orientation: 'portrait',
|
|
|
|
|
|
format: [0, 0]
|
|
|
|
|
|
});
|
2026-01-08 10:02:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 16:29:47 +08:00
|
|
|
|
await this.runSingleTaskCore(taskIdx);
|
|
|
|
|
|
|
|
|
|
|
|
const fileName = `外标签任务${taskIdx}_导出_${new Date().toLocaleString().replace(/[/: ]/g, '-')}.pdf`;
|
|
|
|
|
|
this.pdfDoc.save(fileName);
|
2026-01-09 13:26:52 +08:00
|
|
|
|
Message.success(`任务${taskIdx}导出成功!处理范围:${this.taskList[taskIdx - 1].startNum}-${this.taskList[taskIdx - 1].endNum}条`);
|
2026-01-08 10:02:59 +08:00
|
|
|
|
|
|
|
|
|
|
} catch (err) {
|
2026-01-08 16:29:47 +08:00
|
|
|
|
console.error(`任务${taskIdx}执行失败:`, err);
|
|
|
|
|
|
Message.error(`任务${taskIdx}执行失败,请查看控制台`);
|
|
|
|
|
|
this.updateTaskStatus(taskIdx, 'failed');
|
2026-01-08 10:02:59 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
this.exportLoading = false;
|
2026-01-08 16:29:47 +08:00
|
|
|
|
this.showProgress = false;
|
2026-01-08 10:02:59 +08:00
|
|
|
|
this.progress = 0;
|
|
|
|
|
|
this.currentIndex = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-01-09 13:26:52 +08:00
|
|
|
|
// 单个任务执行核心逻辑【修复后:彻底解决多页尺寸异常】
|
2026-01-08 16:29:47 +08:00
|
|
|
|
async runSingleTaskCore(taskIdx) {
|
|
|
|
|
|
this.updateTaskStatus(taskIdx, 'running', 0);
|
2026-01-09 13:26:52 +08:00
|
|
|
|
|
2026-01-08 16:29:47 +08:00
|
|
|
|
const pageNum = taskIdx;
|
|
|
|
|
|
const res = await listMaterialCoil({
|
|
|
|
|
|
status: 0,
|
|
|
|
|
|
dataType: 1,
|
|
|
|
|
|
materialType: '成品',
|
|
|
|
|
|
itemType: 'product',
|
|
|
|
|
|
selectType: 'raw_material',
|
|
|
|
|
|
pageSize: this.config.pageSize,
|
|
|
|
|
|
pageNum
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.list = res.rows || [];
|
|
|
|
|
|
this.totalCount = this.list.length;
|
|
|
|
|
|
if (this.totalCount === 0) {
|
|
|
|
|
|
this.updateTaskStatus(taskIdx, 'completed', 100);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.showProgress = true;
|
|
|
|
|
|
this.currentIndex = 0;
|
|
|
|
|
|
this.progress = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (let index = 0; index < this.list.length; index++) {
|
|
|
|
|
|
this.current = this.list[index];
|
|
|
|
|
|
await this.waitForDOMRender();
|
2026-01-09 13:26:52 +08:00
|
|
|
|
|
2026-01-08 16:29:47 +08:00
|
|
|
|
const previewComponent = this.$refs.outerTagPreview;
|
|
|
|
|
|
await this.waitForAllResources(previewComponent.$el);
|
|
|
|
|
|
|
|
|
|
|
|
// 克隆DOM到临时容器
|
|
|
|
|
|
const tempContainer = this.$refs.tempTagContainer;
|
|
|
|
|
|
const renderedDOM = previewComponent.$el.cloneNode(true);
|
|
|
|
|
|
tempContainer.innerHTML = '';
|
|
|
|
|
|
tempContainer.appendChild(renderedDOM);
|
|
|
|
|
|
|
2026-01-09 13:26:52 +08:00
|
|
|
|
// 修复克隆DOM丢失样式问题
|
|
|
|
|
|
const labelDom = tempContainer.querySelector('.label-container');
|
|
|
|
|
|
if (labelDom) {
|
|
|
|
|
|
labelDom.style.width = '100%';
|
|
|
|
|
|
labelDom.style.maxWidth = '100%';
|
|
|
|
|
|
labelDom.style.boxSizing = 'border-box';
|
|
|
|
|
|
labelDom.style.overflow = 'hidden';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 16:29:47 +08:00
|
|
|
|
await this.waitForDOMRender();
|
|
|
|
|
|
this.copyCanvasContent(previewComponent.$el, tempContainer);
|
|
|
|
|
|
|
|
|
|
|
|
if (!tempContainer.firstChild) {
|
2026-01-09 13:26:52 +08:00
|
|
|
|
console.warn(`任务${taskIdx}第${index + 1}个标签DOM为空`, this.list[index]);
|
2026-01-08 16:29:47 +08:00
|
|
|
|
this.updateProgress(index + 1);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成高清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,
|
2026-01-09 13:26:52 +08:00
|
|
|
|
scrollX: 0,
|
|
|
|
|
|
scrollY: 0,
|
|
|
|
|
|
letterRendering: true,
|
|
|
|
|
|
imageTimeout: 10000
|
2026-01-08 16:29:47 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-01-09 13:26:52 +08:00
|
|
|
|
// 裁剪画布极细空白
|
|
|
|
|
|
const cropCanvas = this.cropCanvasWhiteSpace(canvas);
|
|
|
|
|
|
|
|
|
|
|
|
// 计算当前标签的实际尺寸(每次都重新计算,避免缓存错误)
|
|
|
|
|
|
const canvasWidth = cropCanvas.width / this.config.renderScale;
|
|
|
|
|
|
const canvasHeight = cropCanvas.height / this.config.renderScale;
|
|
|
|
|
|
const imgWidthMM = canvasWidth * (25.4 / 96);
|
|
|
|
|
|
const imgHeightMM = canvasHeight * (25.4 / 96);
|
|
|
|
|
|
const currentPageFormat = [imgWidthMM, imgHeightMM];
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 修复核心:正确设置PDF页面尺寸 ==========
|
|
|
|
|
|
const totalPdfPages = this.pdfDoc.internal.getNumberOfPages();
|
|
|
|
|
|
// 情况1:PDF是“初始空白页”(仅1页且尺寸为0)→ 修改初始页尺寸
|
|
|
|
|
|
if (totalPdfPages === 1 && this.pdfDoc.internal.pageSize.getWidth() === 0) {
|
|
|
|
|
|
this.pdfDoc.setPage(1);
|
|
|
|
|
|
this.pdfDoc.internal.pageSize.setWidth(imgWidthMM);
|
|
|
|
|
|
this.pdfDoc.internal.pageSize.setHeight(imgHeightMM);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 情况2:其他所有情况→ 新增页面并使用当前标签的尺寸
|
|
|
|
|
|
else {
|
|
|
|
|
|
this.pdfDoc.addPage(currentPageFormat, 'portrait');
|
2026-01-08 16:29:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 13:26:52 +08:00
|
|
|
|
// 图片0边距填充当前页面
|
|
|
|
|
|
const currentPageNum = this.pdfDoc.internal.getNumberOfPages();
|
|
|
|
|
|
this.pdfDoc.setPage(currentPageNum); // 切换到当前页面
|
|
|
|
|
|
this.pdfDoc.internal.pageSize.setWidth(imgWidthMM);
|
|
|
|
|
|
this.pdfDoc.internal.pageSize.setHeight(imgHeightMM);
|
|
|
|
|
|
this.pdfDoc.addImage(cropCanvas.toDataURL('image/png', 1.0), 'PNG', 0, 0, imgWidthMM, imgHeightMM);
|
2026-01-08 16:29:47 +08:00
|
|
|
|
|
2026-01-09 13:26:52 +08:00
|
|
|
|
// 更新进度
|
2026-01-08 16:29:47 +08:00
|
|
|
|
this.updateProgress(index + 1);
|
|
|
|
|
|
this.updateTaskStatus(taskIdx, 'running', this.progress);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.updateTaskStatus(taskIdx, 'completed', 100);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-01-09 13:26:52 +08:00
|
|
|
|
// ========== 新增+保留 所有辅助方法 ==========
|
|
|
|
|
|
cropCanvasWhiteSpace(canvas) {
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
const width = canvas.width;
|
|
|
|
|
|
const height = canvas.height;
|
|
|
|
|
|
const imageData = ctx.getImageData(0, 0, width, height);
|
|
|
|
|
|
const data = imageData.data;
|
|
|
|
|
|
|
|
|
|
|
|
let left = width, top = height, right = 0, bottom = 0;
|
|
|
|
|
|
let pixelIndex, alpha;
|
|
|
|
|
|
|
|
|
|
|
|
for (let y = 0; y < height; y++) {
|
|
|
|
|
|
for (let x = 0; x < width; x++) {
|
|
|
|
|
|
pixelIndex = (y * width + x) * 4;
|
|
|
|
|
|
alpha = data[pixelIndex + 3];
|
|
|
|
|
|
if (alpha > 0) {
|
|
|
|
|
|
left = Math.min(left, x);
|
|
|
|
|
|
top = Math.min(top, y);
|
|
|
|
|
|
right = Math.max(right, x);
|
|
|
|
|
|
bottom = Math.max(bottom, y);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const cropWidth = right - left + 1;
|
|
|
|
|
|
const cropHeight = bottom - top + 1;
|
|
|
|
|
|
if (cropWidth <= 0 || cropHeight <= 0) return canvas;
|
|
|
|
|
|
|
|
|
|
|
|
const newCanvas = document.createElement('canvas');
|
|
|
|
|
|
newCanvas.width = cropWidth;
|
|
|
|
|
|
newCanvas.height = cropHeight;
|
|
|
|
|
|
const newCtx = newCanvas.getContext('2d');
|
|
|
|
|
|
newCtx.drawImage(canvas, left, top, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight);
|
|
|
|
|
|
return newCanvas;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-01-08 16:29:47 +08:00
|
|
|
|
async waitForAllResources(element) {
|
|
|
|
|
|
const images = element.querySelectorAll('img');
|
2026-01-09 13:26:52 +08:00
|
|
|
|
const imgPromises = Array.from(images).map(img =>
|
2026-01-08 16:29:47 +08:00
|
|
|
|
img.complete ? Promise.resolve() : new Promise(resolve => {
|
|
|
|
|
|
img.onload = resolve;
|
|
|
|
|
|
img.onerror = resolve;
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
await Promise.all(imgPromises);
|
|
|
|
|
|
await document.fonts.ready;
|
|
|
|
|
|
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);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-01-08 10:02:59 +08:00
|
|
|
|
updateProgress(current) {
|
|
|
|
|
|
this.currentIndex = current;
|
|
|
|
|
|
this.progress = Math.round((current / this.totalCount) * 100);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
waitForDOMRender(time = 300) {
|
|
|
|
|
|
return new Promise(resolve => setTimeout(resolve, time));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-01-08 16:29:47 +08:00
|
|
|
|
: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;
|
|
|
|
|
|
}
|
2026-01-09 13:26:52 +08:00
|
|
|
|
|
2026-01-08 16:29:47 +08:00
|
|
|
|
input:focus {
|
|
|
|
|
|
border-color: #409eff;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 10:02:59 +08:00
|
|
|
|
button {
|
|
|
|
|
|
border: 1px solid #409eff;
|
|
|
|
|
|
background: #409eff;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
}
|
2026-01-09 13:26:52 +08:00
|
|
|
|
|
2026-01-08 10:02:59 +08:00
|
|
|
|
button:disabled {
|
|
|
|
|
|
background: #a0cfff;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|