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

@@ -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 业务名称(如「货物类型」)

View File

@@ -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();