From 8ff9f6bb6693e218a2022b30409bf124d545beaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Tue, 28 Jul 2026 00:16:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(import):=20=E4=BC=98=E5=8C=96=E8=B4=A7?= =?UTF-8?q?=E7=89=A9=E7=B1=BB=E5=9E=8B=E5=AF=BC=E5=85=A5=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E6=98=8E=E7=BB=86=E5=8F=8A=E4=B8=8B=E8=BD=BD=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 引入 exceljs 动态加载,支持对后端失败明细 Excel 进行内容加工 - 导入失败明细取消标红错误字段,改为清晰错误原因文本说明 - 调整失败明细各列宽度,增加失败原因列换行提升可读性 - 失败明细原因列格式统一为“第N行:错误,规则”方式,便于定位 - 下载模板由后端生成改为前端本地生成,并内置示例数据 - 模板表头严格使用真实模板列名,示例数据涵盖一级及二级货物类型 - 模板编码列设置为文本格式避免前导零丢失,表头自动换行显示完整说明 --- .workbuddy/memory/2026-07-27.md | 31 +++++++++++++++ src/utils/import-excel.js | 2 +- src/views/base/cargo-type.vue | 69 +++++++++++++++++++++++++++------ 3 files changed, 90 insertions(+), 12 deletions(-) diff --git a/.workbuddy/memory/2026-07-27.md b/.workbuddy/memory/2026-07-27.md index ac773f2..cb52001 100644 --- a/.workbuddy/memory/2026-07-27.md +++ b/.workbuddy/memory/2026-07-27.md @@ -44,3 +44,34 @@ - 同期发现 2888 dev server 进程退出(lsof 无监听),导致 Vite 代理对 localhost:2888 全返回 502(含根路径)。已重启 `pnpm dev`(端口 2888,proxy target 172.16.203.228:8000),代理恢复 200。后端直连一直正常。 - 改用新编码重造:一级 11/12(电子产品/生鲜食品),二级 1101/1102(父11)、1201(父12),文本格式保留前导零。三份文件(一级/二级/合并)已更新。 - 结论:导入功能本身 OK,问题一直是造数(类型值、编码格式、编码冲突)+ dev server 偶发退出。建议导入前确认 dev server 在跑。 + +## 导入失败明细标红 + 原因改写(23:50) + +- 需求:导入失败后,失败明细 Excel 中**仅违规那一栏标红**,且「导入失败原因」列改写为清晰格式,如`(第2行:货物类型编码格式不正确,一级2位数字,二级4位数字)`。 +- 现状:失败明细 Excel 此前是后端生成、前端 `import-excel.js` 直接 `downloadXls` 原样下载,无加工。 +- 改动: + 1. `pnpm add exceljs`(4.4.0,运行时依赖;`import('exceljs')` 动态加载,不进主包)。 + 2. `src/utils/import-excel.js`:`openImportDialog` 增加可选 `options.failDetailDecorator`;`handleImportExcel` 在 Excel 响应分支用 exceljs 读取后端失败明细 blob → 调用 decorator 加工(标红+改写原因)→ 再下载。**未传 decorator 的模块保持原行为(向后兼容、通用)**。 + 3. `src/views/base/cargo-type.vue`:新增模块级 `decorateCargoTypeFailDetail(workbook)`,建立「表头列名→列号」映射,按错误文本匹配规则(7 类错误 → 标红列 + 规则说明),遍历数据行标红违规栏并将原因列改写为`(第N行:错误,规则)`;`handleImport` 传入 `{ failDetailDecorator: decorateCargoTypeFailDetail }`。标红填充色 `FFFFC7CE`(淡红),物理行号(表头第1行,数据第2行起)。 +- 验证:Node + exceljs 跑 decorator 等价逻辑全 PASS(标红列命中、原因格式正确);vite 将 `import('exceljs')` 重写为浏览器版 chunk(/node_modules/.vite/deps/exceljs.js,HTTP 200,1.46MB),浏览器运行时无 polyfill 报错;cargo-type.vue 与 import-excel.js 经 vite 编译均 200。 +- 注意:decorator 依赖后端失败明细 Excel 的「导入失败原因」列文本与模板列名(类型/上级货物类型/上级货物类型编码/货物类型/货物类型编码/备注/导入失败原因)精确一致;若后端改列名需同步更新 `CARGO_TYPE_FAIL_RULES` 的 col 值。 + +## 导入失败明细改为「不标红 + 加列宽」(00:08 次晨) + +- 用户反馈两点:① 不要标红,只写清错误原因即可;② 失败明细 xls 列太挤,需加宽。 +- 改动(同一对文件): + - `src/views/base/cargo-type.vue`:删除 `FAIL_RED_FILL` 常量与所有 `cell.fill` 标红逻辑,仅保留原因列改写为`(第N行:错误,规则)`;新增 `CARGO_TYPE_FAIL_COL_WIDTH` 列宽映射(类型14/上级货物类型16/上级货物类型编码20/货物类型16/货物类型编码16/备注18/导入失败原因60),遍历 colMap 设置 `ws.getColumn(n).width`;原因列额外 `alignment={wrapText:true,vertical:'top'}` 提升可读性。 + - `src/utils/import-excel.js`:注释由「标红 / 改写原因列」改为中性「改写原因列、调整列宽等加工」。 +- 验证:Node + exceljs 等价逻辑全 PASS(无红色填充=✅、原因含「第N行」、导入失败原因列宽=60);cargo-type.vue 与 import-excel.js vite 编译均 200,FAIL_RED_FILL 残留=0。 + +## 下载模板改为「前端本地生成 + 带示例」(00:11 次晨) + +- 需求:点击「下载模板」给的导入模板要自带一个数据示例(参照可正常导入的 `货物类型导入模板_模拟数据.xlsx`)。 +- 现状:`handleTemplate` 原调用后端 `/blade-system/cargo-type/export-template` 下载空模板(无示例)。 +- 改动 `src/views/base/cargo-type.vue` 的 `handleTemplate`:改为 `import('exceljs')` 本地生成模板,不再依赖后端空模板。 + - 表头严格使用真实模板列名(含 `*` 与括号说明):`*类型` / `上级货物类型(如为一级则不需填写)` / `上级货物类型编码(如为一级则不需填写)` / `*货物类型` / `*货物类型编码` / `备注`(与可正常导入的本地模板一致)。 + - 示例数据 2 条:一级 `电子产品2/117` + 二级 `手机2/11014`(父117),演示层级,取值与用户确认可导入的模拟数据一致。 + - 列宽加宽(类型14/上级货物类型24/上级货物类型编码26/货物类型16/货物类型编码16/备注18);编码列(3、5) `numFmt='@'` 文本格式防前导零丢失;表头行 `wrapText` 完整显示长说明。 + - 下载文件名仍为 `货物类型导入模板.xlsx`。`exportBlob` 仍被 `handleExport` 使用,import 不冗余。 +- 验证:Node + exceljs 等价逻辑确认表头/示例行/列宽/文本格式正确(稀疏 eachCell 跳过空单元格导致一次断言误报,按列号复核确认数据无误);cargo-type.vue vite 编译 200。 +- 注意:示例行若被直接导入会创建 `电子产品2/手机2` 两条演示数据;用户应按需删除/替换示例再填自己的数据。 diff --git a/src/utils/import-excel.js b/src/utils/import-excel.js index 4cbed0a..f3804b3 100644 --- a/src/utils/import-excel.js +++ b/src/utils/import-excel.js @@ -18,7 +18,7 @@ const parseBlobJson = async blob => { /** * 加工后端返回的导入失败明细 Excel。 * 若传入 failDetailDecorator(函数),则动态加载 exceljs 读取 blob 流, - * 交由 decorator 完成标红 / 改写原因列等加工,再导出新的 blob; + * 交由 decorator 完成改写原因列、调整列宽等加工,再导出新的 blob; * 否则原样返回 blob(保持旧行为,不影响未使用 decorator 的模块)。 * @param {Blob} blob 后端返回的失败明细 Excel 流 * @param {String} businessName 业务名称(如「货物类型」) diff --git a/src/views/base/cargo-type.vue b/src/views/base/cargo-type.vue index 2678d8d..c0ce5e8 100644 --- a/src/views/base/cargo-type.vue +++ b/src/views/base/cargo-type.vue @@ -155,8 +155,7 @@ import { getToken } from '@/utils/auth'; import NProgress from 'nprogress'; import 'nprogress/nprogress.css'; -// 导入失败明细中,每个错误文本对应的「标红列」与「规则说明」 -const FAIL_RED_FILL = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFC7CE' } }; +// 导入失败明细中,每个错误文本对应的「字段列」与「规则说明」 const CARGO_TYPE_FAIL_RULES = [ { match: '请选择货物类型级别', col: '类型', rule: '仅允许:一级货物类型/二级货物类型' }, { match: '货物类型编码格式不正确', col: '货物类型编码', rule: '一级2位数字,二级4位数字' }, @@ -171,11 +170,22 @@ const CARGO_TYPE_FAIL_RULES = [ { match: '备注不能超过200字', col: '备注', rule: '备注不能超过200字' }, ]; +// 失败明细各列目标宽度(Excel 字符数),避免内容拥挤 +const CARGO_TYPE_FAIL_COL_WIDTH = { + 类型: 14, + 上级货物类型: 16, + 上级货物类型编码: 20, + 货物类型: 16, + 货物类型编码: 16, + 备注: 18, + 导入失败原因: 60, +}; + /** * 加工后端返回的导入失败明细 Excel: - * - 依据「错误文本 -> 字段列」映射,给违规的那一栏标红 - * - 将「导入失败原因」列改写为(第N行:错误,规则)格式 - * 未匹配到的未知错误,标红原因列本身并原样提示 + * - 将「导入失败原因」列改写为(第N行:错误,规则)格式,便于定位 + * - 适当加宽各列、原因列自动换行,避免内容拥挤 + * 不再对单元格标红,仅通过文字清晰说明错误 */ export const decorateCargoTypeFailDetail = async workbook => { const ws = workbook.worksheets[0]; @@ -198,14 +208,17 @@ export const decorateCargoTypeFailDetail = async workbook => { const decorated = parts.map(part => { const rule = CARGO_TYPE_FAIL_RULES.find(r => part.includes(r.match)); if (rule) { - const targetCol = colMap[rule.col]; - if (targetCol) row.getCell(targetCol).fill = FAIL_RED_FILL; return `第${rowNumber}行:${part},${rule.rule}`; } - reasonCell.fill = FAIL_RED_FILL; return `第${rowNumber}行:${part}`; }); reasonCell.value = decorated.join(';'); + reasonCell.alignment = { wrapText: true, vertical: 'top' }; + }); + // 加宽各列(未列在映射中的列给默认宽度) + Object.keys(colMap).forEach(name => { + const col = ws.getColumn(colMap[name]); + col.width = CARGO_TYPE_FAIL_COL_WIDTH[name] || 18; }); }; @@ -544,9 +557,43 @@ export default { }, handleTemplate() { NProgress.start(); - exportBlob('/blade-system/cargo-type/export-template', {}) - .then(res => { - downloadXls(res.data, '货物类型导入模板.xlsx'); + // 本地生成带示例的导入模板(不依赖后端空模板),表头与可正常导入的模板一致 + import('exceljs') + .then(ExcelJS => { + const ExcelJSLib = ExcelJS.default || ExcelJS; + const wb = new ExcelJSLib.Workbook(); + const ws = wb.addWorksheet('货物类型导入模板'); + const headers = [ + '*类型', + '上级货物类型(如为一级则不需填写)', + '上级货物类型编码(如为一级则不需填写)', + '*货物类型', + '*货物类型编码', + '备注', + ]; + ws.addRow(headers); + // 示例数据:一级 + 二级各一条,演示层级关系与填写格式(当前环境可正常导入) + ws.addRow(['一级货物类型', null, null, '电子产品2', '117', '消费类电子']); + ws.addRow(['二级货物类型', '电子产品2', '117', '手机2', '11014', '智能手机']); + // 列宽:表头含长说明,适度加宽 + const widths = [14, 24, 26, 16, 16, 18]; + ws.columns.forEach((col, i) => { + col.width = widths[i] || 16; + }); + // 编码列设为文本,避免前导零丢失 + [3, 5].forEach(idx => { + const col = ws.getColumn(idx); + if (col) col.numFmt = '@'; + }); + // 表头自动换行,长说明完整显示 + ws.getRow(1).eachCell(cell => { + cell.alignment = { wrapText: true, vertical: 'middle' }; + }); + return wb.xlsx.writeBuffer(); + }) + .then(buf => { + const blob = new Blob([buf], { type: 'application/vnd.ms-excel' }); + downloadXls(blob, '货物类型导入模板.xlsx'); }) .finally(() => { NProgress.done();