+
导出配置
+
+
+
+
+
+
+ 范围:1.0-4.0(值越高越清晰,导出越慢)
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ 当前批次进度:{{ currentIndex }}/{{ totalCount }} ({{ progress }}%)
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+ 任务规划(总数量:{{ taskPlan.totalCount }})
+
+
+
+
+
+
总任务数
+
{{ taskPlan.totalTask }}
+
+
+
已完成
+
{{ taskPlan.completedTask }}
+
+
+
进行中
+
{{ taskPlan.runningTask }}
+
+
+
+
+
+
+ 整体导出进度
+ {{ taskPlan.totalProgress }}%
+
+
+
+
+
+
+
当前任务详情
+
+
任务序号:{{ currentTask.idx }}/{{ taskPlan.totalTask }}
+
处理范围:{{ currentTask.startNum }} - {{ currentTask.endNum }} 条
+
当前进度:{{ currentTask.progress }}%
+
状态:{{ currentTask.statusText }}
+
+
暂无进行中的任务
+
+
+
+
+
+
+ 任务 {{ idx + 1 }}
+ {{ task.statusText }}
+
+
+ 处理范围:{{ task.startNum }} - {{ task.endNum }} 条
+
+
+
+
+
+
+
@@ -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 {
\ No newline at end of file