feat(标签打印): 优化标签打印功能并新增批量导出

- 新增html2canvas依赖以支持高清打印
- 重构标签打印逻辑,解决二维码丢失和文字模糊问题
- 优化外标签样式布局和公司名称显示
- 新增标签预览功能,可在物料列表中直接查看
- 实现批量导出功能,支持任务规划和进度展示
- 添加导出配置选项,可调整清晰度和单次导出数量
This commit is contained in:
砂糖
2026-01-08 16:29:47 +08:00
parent 624482a610
commit 0bf61a3dd6
6 changed files with 788 additions and 209 deletions

View File

@@ -1,34 +1,167 @@
<template>
<div>
<button
@click="handleExportAll"
:disabled="exportLoading"
style="padding: 8px 16px; cursor: pointer; margin-bottom: 20px;"
>
{{ exportLoading ? '导出中...' : '导出所有外标签' }}
</button>
<div style="display: flex; gap: 20px; padding: 20px; width: 100%; box-sizing: border-box;">
<!-- 左侧原有导出功能区域 -->
<div style="flex: 1; max-width: 700px;">
<!-- 导出配置区域 -->
<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>
<!-- 清晰度配置 -->
<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="progress-text" style="margin-bottom: 8px; font-size: 14px; color: #666;">
导出进度{{ currentIndex }}/{{ totalCount }} ({{ progress }}%)
<!-- 单次导出数量配置 -->
<div class="config-item" style="display: flex; align-items: center;">
<label style="width: 120px; font-size: 14px; color: #333;">单次导出数量</label>
<input
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 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>
<!-- 导出按钮批量导出所有未完成任务 -->
<button
@click="handleExportAll"
:disabled="exportLoading || taskPlan.totalTask === 0 || hasRunningTask"
style="padding: 8px 16px; cursor: pointer; margin-bottom: 20px;"
>
{{ 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>
<!-- 预览组件 -->
<OuterTagPreview ref="outerTagPreview" :content="current" />
<!-- 临时渲染容器 -->
<div
ref="tempTagContainer"
style="position: fixed; top: -9999px; left: -9999px; z-index: -9999; width: 794px; padding: 10px;"
></div>
</div>
<!-- 预览组件保留预览同时用于复用渲染逻辑 -->
<OuterTagPreview ref="outerTagPreview" :content="current" />
<!-- 临时渲染容器用于逐个渲染标签并转换图片 -->
<div ref="tempTagContainer" style="position: fixed; top: -9999px; left: -9999px; z-index: -9999;"></div>
<!-- 右侧任务规划+进度展示区域 -->
<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;">
<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>
</template>
@@ -36,155 +169,436 @@
import OuterTagPreview from './LabelRender/OuterTagPreview.vue';
import { listMaterialCoil } from "@/api/wms/coil";
import jsPDF from 'jspdf';
import domtoimage from 'dom-to-image';
import html2canvas from 'html2canvas';
import { Message } from 'element-ui';
export default {
name: 'ToolPanel',
components: {
OuterTagPreview,
OuterTagPreview
},
data() {
return {
list: [],
pos: 0,
current: {},
exportLoading: false,
// 进度条相关变量
showProgress: false, // 是否显示进度条
progress: 0, // 进度值0-100
currentIndex: 0, // 当前处理的标签索引
totalCount: 0, // 总标签数量
list: [], // 当前批次数据
current: {}, // 当前预览数据
exportLoading: false, // 全局导出加载状态
showProgress: false, // 单批进度条显示
progress: 0, // 单批进度百分比
currentIndex: 0, // 单批当前处理索引
totalCount: 0, // 当前批次总数
// 导出配置
config: {
renderScale: 1.2, // 清晰度
pageSize: 30 // 单次导出数量
},
// 任务规划数据
taskPlan: {
totalCount: 0, // 总数据量接口返回的res.total
totalTask: 0, // 总任务数Math.ceil(totalCount/pageSize)
completedTask: 0, // 已完成任务数
runningTask: 0, // 进行中任务数
totalProgress: 0 // 整体进度百分比
},
taskList: [], // 任务列表(每个任务的详情)
currentTask: { // 当前任务详情
idx: 0, // 任务序号
startNum: 0, // 起始条数
endNum: 0, // 结束条数
progress: 0, // 当前任务进度
status: 'pending',// 状态pending/running/completed/failed
statusText: '未开始',
statusColor: '#909399'
},
pdfDoc: null // 全局PDF实例支持追加内容
}
},
computed: {
// 是否有任务正在执行
hasRunningTask() {
return this.taskPlan.runningTask > 0;
}
},
mounted() {
this.fetchData();
this.fetchTotalData(); // 先获取总数量,规划任务
},
methods: {
// 获取标签数据
fetchData() {
listMaterialCoil({
// ========== 配置校验 ==========
validateRenderScale() {
let scale = this.config.renderScale;
if (!scale || isNaN(scale)) {
this.config.renderScale = 1.5;
Message.warning('清晰度请输入1.0-4.0之间的数字已重置为默认值1.5');
return;
}
if (scale < 1.0) this.config.renderScale = 1.0;
else if (scale > 4.0) this.config.renderScale = 4.0;
},
validatePageSize() {
let size = this.config.pageSize;
if (!size || isNaN(size) || size < 1) {
this.config.pageSize = 10;
Message.warning('每页数量请输入1-200之间的正整数已重置为默认值10');
return;
}
if (size > 200) this.config.pageSize = 200;
},
// ========== 任务规划核心逻辑 ==========
// 1. 获取总数据量,初始化任务规划
async fetchTotalData() {
try {
const res = await listMaterialCoil({
status: 0,
dataType: 1,
materialType: '成品',
itemType: 'product',
selectType: 'raw_material',
pageSize: 1, // 仅获取总数量pageSize设为1
pageNum: 1
});
// 初始化任务规划
this.taskPlan.totalCount = res.total || 0;
this.taskPlan.totalTask = Math.ceil(this.taskPlan.totalCount / this.config.pageSize);
this.initTaskList(); // 初始化任务列表
} catch (err) {
console.error('获取总数据量失败:', err);
Message.error('获取数据总量失败,请刷新重试');
}
},
// 2. 初始化任务列表
initTaskList() {
this.taskList = [];
for (let i = 0; i < this.taskPlan.totalTask; i++) {
const startNum = i * this.config.pageSize + 1;
const endNum = Math.min((i + 1) * this.config.pageSize, this.taskPlan.totalCount);
this.taskList.push({
idx: i + 1, // 任务序号
startNum, // 起始条数
endNum, // 结束条数
progress: 0, // 任务进度
status: 'pending', // 状态pending/running/completed/failed
statusText: '未开始',
statusColor: '#909399'
});
}
// 重置任务统计
this.taskPlan.completedTask = 0;
this.taskPlan.runningTask = 0;
this.taskPlan.totalProgress = 0;
// 重置当前任务
this.currentTask = { idx: 0, startNum: 0, endNum: 0, progress: 0, status: 'pending', statusText: '未开始', statusColor: '#909399' };
// 重置PDF实例
this.pdfDoc = null;
},
// 3. 修改pageSize后重新规划任务
replanTask() {
// 先校验参数
this.validatePageSize();
// 重新计算总任务数
this.taskPlan.totalTask = Math.ceil(this.taskPlan.totalCount / this.config.pageSize);
// 重新初始化任务列表
this.initTaskList();
Message.success(`已重新规划任务!单次导出${this.config.pageSize}条,总计${this.taskPlan.totalTask}个任务`);
},
// 4. 更新任务状态
updateTaskStatus(taskIdx, status, progress = 0) {
const task = this.taskList[taskIdx - 1];
if (!task) return;
// 更新任务状态
task.status = status;
task.progress = progress;
switch (status) {
case 'running':
task.statusText = '进行中';
task.statusColor = '#409eff';
// 更新当前任务
this.currentTask = { ...task };
break;
case 'completed':
task.statusText = '已完成';
task.statusColor = '#67c23a';
break;
case 'failed':
task.statusText = '失败';
task.statusColor = '#f56c6c';
break;
default:
task.statusText = '未开始';
task.statusColor = '#909399';
}
// 更新任务统计
this.taskPlan.completedTask = this.taskList.filter(t => t.status === 'completed').length;
this.taskPlan.runningTask = this.taskList.filter(t => t.status === 'running').length;
// 计算整体进度
const totalCompleted = this.taskList.reduce((sum, t) => sum + (t.progress / 100) * (t.endNum - t.startNum + 1), 0);
this.taskPlan.totalProgress = Math.round((totalCompleted / this.taskPlan.totalCount) * 100);
},
// ========== 核心导出逻辑 ==========
// 1. 批量导出所有未完成任务
async handleExportAll() {
if (this.exportLoading || this.hasRunningTask) return;
if (this.taskPlan.totalCount === 0) {
Message.warning('暂无外标签数据可导出');
return;
}
try {
this.exportLoading = true;
// 初始化PDF文档jsPDF默认创建1个空白页
this.pdfDoc = new jsPDF({ unit: 'mm', format: 'a4', orientation: 'portrait' });
// 筛选出未完成的任务pending/failed
const uncompletedTasks = this.taskList.filter(t => t.status !== 'completed');
if (uncompletedTasks.length === 0) {
Message.info('所有任务均已完成,无需导出');
this.exportLoading = false;
return;
}
// 循环处理每个未完成任务
for (const task of uncompletedTasks) {
await this.runSingleTaskCore(task.idx);
}
// 保存最终PDF
const fileName = `外标签批量导出_${new Date().toLocaleString().replace(/[/: ]/g, '-')}.pdf`;
this.pdfDoc.save(fileName);
Message.success(`PDF批量导出成功共完成${uncompletedTasks.length}个任务,总计${this.taskPlan.totalCount}条数据`);
} catch (err) {
console.error('批量导出失败:', err);
Message.error('批量导出失败,请查看控制台');
} finally {
this.exportLoading = false;
this.showProgress = false;
this.progress = 0;
this.currentIndex = 0;
}
},
// 2. 单独执行单个任务
async handleRunSingleTask(taskIdx) {
if (this.exportLoading || this.hasRunningTask) {
Message.warning('有任务正在执行中,请等待完成后再操作');
return;
}
try {
this.exportLoading = true;
// 初始化PDF文档如果未初始化
if (!this.pdfDoc) {
this.pdfDoc = new jsPDF({ unit: 'mm', format: 'a4', orientation: 'portrait' });
}
// 执行单个任务核心逻辑
await this.runSingleTaskCore(taskIdx);
// 保存单个任务的PDF
const fileName = `外标签任务${taskIdx}_导出_${new Date().toLocaleString().replace(/[/: ]/g, '-')}.pdf`;
this.pdfDoc.save(fileName);
Message.success(`任务${taskIdx}导出成功!处理范围:${this.taskList[taskIdx-1].startNum}-${this.taskList[taskIdx-1].endNum}`);
} catch (err) {
console.error(`任务${taskIdx}执行失败:`, err);
Message.error(`任务${taskIdx}执行失败,请查看控制台`);
this.updateTaskStatus(taskIdx, 'failed');
} finally {
this.exportLoading = false;
this.showProgress = false;
this.progress = 0;
this.currentIndex = 0;
}
},
// 3. 单个任务执行核心逻辑(修复空白页关键)
async runSingleTaskCore(taskIdx) {
// 更新当前任务为“进行中”
this.updateTaskStatus(taskIdx, 'running', 0);
// 获取当前任务的批次数据
const pageNum = taskIdx;
const res = await listMaterialCoil({
status: 0,
dataType: 1,
materialType: '成品',
itemType: 'product',
selectType: 'raw_material',
pageSize: 100,
pageNum: 10
}).then(res => {
this.list = res.rows || [];
if (this.list.length > 0) {
this.current = this.list[0];
}
}).catch(err => {
console.error('获取标签数据失败:', err);
this.$message.error('获取标签数据失败,请刷新重试');
})
},
pageSize: this.config.pageSize,
pageNum
});
// 核心复用OuterTagPreview组件渲染标签再转图片导出PDF
async handleExportAll() {
if (this.exportLoading) return;
if (this.list.length === 0) {
this.$message.warning('暂无外标签数据可导出');
this.list = res.rows || [];
this.totalCount = this.list.length;
if (this.totalCount === 0) {
this.updateTaskStatus(taskIdx, 'completed', 100);
return;
}
try {
// 初始化进度条
this.exportLoading = true;
this.showProgress = true; // 显示进度条
this.totalCount = this.list.length; // 总数量
this.currentIndex = 0; // 重置当前索引
this.progress = 0; // 重置进度
// 处理当前批次的每个标签
this.showProgress = true;
this.currentIndex = 0;
this.progress = 0;
const doc = new jsPDF({
unit: 'mm',
format: 'a4',
orientation: 'portrait'
});
const tempContainer = this.$refs.tempTagContainer;
for (let index = 0; index < this.list.length; index++) {
this.current = this.list[index];
await this.waitForDOMRender();
// 等待资源加载(图片+字体+二维码)
const previewComponent = this.$refs.outerTagPreview;
await this.waitForAllResources(previewComponent.$el);
// 循环处理每个标签
for (let index = 0; index < this.list.length; index++) {
const item = this.list[index];
this.current = item; // 更新预览组件的content触发重新渲染
// 等待组件渲染完成
await this.waitForDOMRender();
// 克隆DOM到临时容器
const tempContainer = this.$refs.tempTagContainer;
const renderedDOM = previewComponent.$el.cloneNode(true);
tempContainer.innerHTML = '';
tempContainer.appendChild(renderedDOM);
// 复制预览组件的DOM到临时容器
const renderedDOM = previewComponent.$el.cloneNode(true);
tempContainer.innerHTML = '';
tempContainer.appendChild(renderedDOM);
await this.waitForDOMRender();
this.copyCanvasContent(previewComponent.$el, tempContainer);
// 再次等待临时容器DOM渲染
await this.waitForDOMRender();
// 检查临时容器是否有内容
if (!tempContainer.firstChild) {
console.warn(`${index+1}个标签DOM为空`, item);
// 即使当前标签为空,也更新进度
this.updateProgress(index + 1);
continue;
}
// 将临时容器的DOM转为图片
const imgData = await domtoimage.toPng(previewComponent.$el);
// 分页:非第一个标签新增页面
if (index > 0) {
doc.addPage();
}
// 计算图片在PDF中的尺寸适配A4保持原比例
const a4Width = 210;
const a4Height = 297;
const imgRatio = (previewComponent.$el.offsetWidth || 800) / (previewComponent.$el.offsetHeight || 1100);
const imgWidth = a4Width - 20;
const imgHeight = imgWidth / imgRatio;
// 确保图片高度不超过A4高度
const finalImgHeight = imgHeight > a4Height - 20 ? (a4Height - 20) : imgHeight;
const finalImgWidth = finalImgHeight * imgRatio;
// 居中显示
const xPos = (a4Width - finalImgWidth) / 2;
const yPos = (a4Height - finalImgHeight) / 2;
// 插入图片到PDF
doc.addImage(imgData, 'PNG', xPos, yPos, finalImgWidth, finalImgHeight);
// 更新进度条(关键:处理完一个标签就更新)
if (!tempContainer.firstChild) {
console.warn(`任务${taskIdx}${index+1}个标签DOM为空`, this.list[index]);
this.updateProgress(index + 1);
continue;
}
// 保存PDF
const fileName = `外标签导出_${new Date().toLocaleString().replace(/[/: ]/g, '-')}.pdf`;
doc.save(fileName);
this.$message.success('PDF导出成功');
// 生成高清Canvas
const canvas = await html2canvas(tempContainer, {
scale: this.config.renderScale,
backgroundColor: '#ffffff',
useCORS: true,
width: tempContainer.offsetWidth,
height: tempContainer.offsetHeight,
windowWidth: tempContainer.offsetWidth * this.config.renderScale,
windowHeight: tempContainer.offsetHeight * this.config.renderScale,
logging: false,
allowTaint: true,
taintTest: false,
});
} catch (err) {
console.error('PDF导出失败详情', err);
this.$message.error('PDF导出失败详情请查看控制台');
} finally {
// 重置进度条和加载状态
this.$refs.tempTagContainer.innerHTML = '';
this.exportLoading = false;
this.showProgress = false; // 隐藏进度条
this.progress = 0;
this.currentIndex = 0;
this.totalCount = 0;
// ========== 修复空白页核心逻辑 ==========
// 关键规则:
// 1. jsPDF实例化时默认创建1个空白页
// 2. 第一个任务的第一个标签:直接使用默认空白页,不新增
// 3. 第一个任务的非第一个标签:新增页面
// 4. 非第一个任务的所有标签:新增页面
const needAddPage = index > 0 || taskIdx > 1;
if (needAddPage) {
this.pdfDoc.addPage();
}
// 尺寸计算
const domWidth = tempContainer.offsetWidth;
const domHeight = tempContainer.offsetHeight;
const domWidthMM = domWidth * (25.4 / 96);
const domHeightMM = domHeight * (25.4 / 96);
const a4Width = 210;
const a4Height = 297;
const margin = 5;
const availableWidth = a4Width - 2 * margin;
const availableHeight = a4Height - 2 * margin;
const scaleRatio = Math.min(availableWidth / domWidthMM, availableHeight / domHeightMM);
const finalWidth = domWidthMM * scaleRatio;
const finalHeight = domHeightMM * scaleRatio;
const x = (a4Width - finalWidth) / 2;
const y = (a4Height - finalHeight) / 2;
// 插入图片到PDF
this.pdfDoc.addImage(canvas.toDataURL('image/png', 1.0), 'PNG', x, y, finalWidth, finalHeight);
// 更新单批进度 + 任务进度
this.updateProgress(index + 1);
this.updateTaskStatus(taskIdx, 'running', this.progress);
}
// 当前任务完成
this.updateTaskStatus(taskIdx, 'completed', 100);
},
// 辅助方法:更新进度条
// ========== 辅助方法 ==========
// 等待二维码/图片/字体加载
async waitForAllResources(element) {
// 等待图片加载
const images = element.querySelectorAll('img');
const imgPromises = Array.from(images).map(img =>
img.complete ? Promise.resolve() : new Promise(resolve => {
img.onload = resolve;
img.onerror = resolve;
})
);
await Promise.all(imgPromises);
// 等待字体加载
await document.fonts.ready;
// 等待二维码Canvas渲染
await this.waitForQRCodeRender(element);
// 最终等待
await new Promise(resolve => setTimeout(resolve, 300));
},
// 等待二维码渲染
async waitForQRCodeRender(element) {
const qrCanvasList = element.querySelectorAll('canvas');
if (qrCanvasList.length === 0) return;
const qrLoadPromises = Array.from(qrCanvasList).map(canvas => {
return new Promise(resolve => {
const checkInterval = setInterval(() => {
const ctx = canvas.getContext('2d');
if (!ctx) {
clearInterval(checkInterval);
resolve();
return;
}
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const hasContent = imageData.data.some(value => value !== 0);
if (hasContent || Date.now() - (ctx.startTime || 0) > 2000) {
clearInterval(checkInterval);
resolve();
}
}, 100);
});
});
await Promise.all(qrLoadPromises);
},
// 复制Canvas内容
copyCanvasContent(sourceEl, targetEl) {
const sourceCanvases = sourceEl.querySelectorAll('canvas');
const targetCanvases = targetEl.querySelectorAll('canvas');
sourceCanvases.forEach((canvas, index) => {
const targetCanvas = targetCanvases[index];
if (!targetCanvas) return;
targetCanvas.width = canvas.width;
targetCanvas.height = canvas.height;
const targetCtx = targetCanvas.getContext('2d');
targetCtx.drawImage(canvas, 0, 0);
});
},
// 更新单批进度
updateProgress(current) {
this.currentIndex = current;
// 计算进度百分比(保留整数)
this.progress = Math.round((current / this.totalCount) * 100);
},
// 辅助方法:等待DOM渲染完成
// 等待DOM渲染
waitForDOMRender(time = 300) {
return new Promise(resolve => setTimeout(resolve, time));
}
@@ -193,6 +607,35 @@ export default {
</script>
<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 {
border: 1px solid #409eff;
background: #409eff;
@@ -204,33 +647,4 @@ button:disabled {
background: #a0cfff;
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>