feat(import): 优化货物类型导入失败明细及下载模板功能

- 引入 exceljs 动态加载,支持对后端失败明细 Excel 进行内容加工
- 导入失败明细取消标红错误字段,改为清晰错误原因文本说明
- 调整失败明细各列宽度,增加失败原因列换行提升可读性
- 失败明细原因列格式统一为“第N行:错误,规则”方式,便于定位
- 下载模板由后端生成改为前端本地生成,并内置示例数据
- 模板表头严格使用真实模板列名,示例数据涵盖一级及二级货物类型
- 模板编码列设置为文本格式避免前导零丢失,表头自动换行显示完整说明
This commit is contained in:
2026-07-28 00:16:43 +08:00
parent e76cf70a20
commit 8ff9f6bb66
3 changed files with 90 additions and 12 deletions

View File

@@ -44,3 +44,34 @@
- 同期发现 2888 dev server 进程退出lsof 无监听),导致 Vite 代理对 localhost:2888 全返回 502含根路径。已重启 `pnpm dev`(端口 2888proxy target 172.16.203.228:8000代理恢复 200。后端直连一直正常。 - 同期发现 2888 dev server 进程退出lsof 无监听),导致 Vite 代理对 localhost:2888 全返回 502含根路径。已重启 `pnpm dev`(端口 2888proxy target 172.16.203.228:8000代理恢复 200。后端直连一直正常。
- 改用新编码重造:一级 11/12电子产品/生鲜食品),二级 1101/1102父11、1201父12文本格式保留前导零。三份文件一级/二级/合并)已更新。 - 改用新编码重造:一级 11/12电子产品/生鲜食品),二级 1101/1102父11、1201父12文本格式保留前导零。三份文件一级/二级/合并)已更新。
- 结论:导入功能本身 OK问题一直是造数类型值、编码格式、编码冲突+ dev server 偶发退出。建议导入前确认 dev server 在跑。 - 结论:导入功能本身 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.jsHTTP 2001.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行」、导入失败原因列宽=60cargo-type.vue 与 import-excel.js vite 编译均 200FAIL_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` 两条演示数据;用户应按需删除/替换示例再填自己的数据。

View File

@@ -18,7 +18,7 @@ const parseBlobJson = async blob => {
/** /**
* 加工后端返回的导入失败明细 Excel。 * 加工后端返回的导入失败明细 Excel。
* 若传入 failDetailDecorator函数则动态加载 exceljs 读取 blob 流, * 若传入 failDetailDecorator函数则动态加载 exceljs 读取 blob 流,
* 交由 decorator 完成标红 / 改写原因列等加工,再导出新的 blob * 交由 decorator 完成改写原因列、调整列宽等加工,再导出新的 blob
* 否则原样返回 blob保持旧行为不影响未使用 decorator 的模块)。 * 否则原样返回 blob保持旧行为不影响未使用 decorator 的模块)。
* @param {Blob} blob 后端返回的失败明细 Excel 流 * @param {Blob} blob 后端返回的失败明细 Excel 流
* @param {String} businessName 业务名称(如「货物类型」) * @param {String} businessName 业务名称(如「货物类型」)

View File

@@ -155,8 +155,7 @@ import { getToken } from '@/utils/auth';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
// 导入失败明细中,每个错误文本对应的「标红列」与「规则说明」 // 导入失败明细中,每个错误文本对应的「字段列」与「规则说明」
const FAIL_RED_FILL = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFC7CE' } };
const CARGO_TYPE_FAIL_RULES = [ const CARGO_TYPE_FAIL_RULES = [
{ match: '请选择货物类型级别', col: '类型', rule: '仅允许:一级货物类型/二级货物类型' }, { match: '请选择货物类型级别', col: '类型', rule: '仅允许:一级货物类型/二级货物类型' },
{ match: '货物类型编码格式不正确', col: '货物类型编码', rule: '一级2位数字二级4位数字' }, { match: '货物类型编码格式不正确', col: '货物类型编码', rule: '一级2位数字二级4位数字' },
@@ -171,11 +170,22 @@ const CARGO_TYPE_FAIL_RULES = [
{ match: '备注不能超过200字', col: '备注', rule: '备注不能超过200字' }, { match: '备注不能超过200字', col: '备注', rule: '备注不能超过200字' },
]; ];
// 失败明细各列目标宽度Excel 字符数),避免内容拥挤
const CARGO_TYPE_FAIL_COL_WIDTH = {
类型: 14,
上级货物类型: 16,
上级货物类型编码: 20,
货物类型: 16,
货物类型编码: 16,
备注: 18,
导入失败原因: 60,
};
/** /**
* 加工后端返回的导入失败明细 Excel * 加工后端返回的导入失败明细 Excel
* - 依据「错误文本 -> 字段列」映射,给违规的那一栏标红 * - 将「导入失败原因」列改写为第N行错误规则格式便于定位
* - 将「导入失败原因」列改写为第N行错误规则格式 * - 适当加宽各列、原因列自动换行,避免内容拥挤
* 未匹配到的未知错误,标红原因列本身并原样提示 * 不再对单元格标红,仅通过文字清晰说明错误
*/ */
export const decorateCargoTypeFailDetail = async workbook => { export const decorateCargoTypeFailDetail = async workbook => {
const ws = workbook.worksheets[0]; const ws = workbook.worksheets[0];
@@ -198,14 +208,17 @@ export const decorateCargoTypeFailDetail = async workbook => {
const decorated = parts.map(part => { const decorated = parts.map(part => {
const rule = CARGO_TYPE_FAIL_RULES.find(r => part.includes(r.match)); const rule = CARGO_TYPE_FAIL_RULES.find(r => part.includes(r.match));
if (rule) { if (rule) {
const targetCol = colMap[rule.col];
if (targetCol) row.getCell(targetCol).fill = FAIL_RED_FILL;
return `${rowNumber}行:${part}${rule.rule}`; return `${rowNumber}行:${part}${rule.rule}`;
} }
reasonCell.fill = FAIL_RED_FILL;
return `${rowNumber}行:${part}`; return `${rowNumber}行:${part}`;
}); });
reasonCell.value = decorated.join(''); 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() { handleTemplate() {
NProgress.start(); NProgress.start();
exportBlob('/blade-system/cargo-type/export-template', {}) // 本地生成带示例的导入模板(不依赖后端空模板),表头与可正常导入的模板一致
.then(res => { import('exceljs')
downloadXls(res.data, '货物类型导入模板.xlsx'); .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(() => { .finally(() => {
NProgress.done(); NProgress.done();