feat(cargo-type): 优化货物类型导入失败明细的错误标记和提示

- 新增导入失败明细的加工函数,给违规字段自动标红并丰富错误提示信息
- 在导入弹窗调用中增加失败明细装饰器处理,提升用户体验
- 将货物类型组件option配置提取为单独模块,简化代码逻辑
- 调整货物类型表单禁用逻辑,避免非新增时的错误操作
- 移除原有表单校验函数,相关验证由导入失败明细装饰器替代
- 引入exceljs依赖以支持导入失败Excel文件的读取和修改
- 修复导入失败明细导出时的文件命名和内容加工问题
- 优化代码结构,提升维护性和可读性
This commit is contained in:
2026-07-27 23:58:29 +08:00
parent 1c7532e0de
commit e76cf70a20
7 changed files with 801 additions and 182 deletions

View File

@@ -73,7 +73,7 @@
clearable
:remote-method="loadParentOptions"
:loading="parentLoading"
:disabled="form.typeLevel !== 2"
:disabled="dialogType !== 'add' || form.typeLevel !== 2"
placeholder="请输入名称或编码搜索"
style="width: 100%"
@change="handleParentChange"
@@ -146,6 +146,7 @@ import {
remove,
submit,
} from '@/api/base/cargo-type';
import { getCargoTypeOption } from '@/option/base/cargo-type';
import { exportBlob } from '@/api/common';
import { mapGetters } from 'vuex';
import { downloadXls } from '@/utils/util';
@@ -154,45 +155,62 @@ import { getToken } from '@/utils/auth';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const typeLevelDic = [
{ label: '一级货物类型', value: 1 },
{ label: '二级货物类型', value: 2 },
// 导入失败明细中,每个错误文本对应的「标红列」与「规则说明」
const FAIL_RED_FILL = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFFFC7CE' } };
const CARGO_TYPE_FAIL_RULES = [
{ match: '请选择货物类型级别', col: '类型', rule: '仅允许:一级货物类型/二级货物类型' },
{ match: '货物类型编码格式不正确', col: '货物类型编码', rule: '一级2位数字二级4位数字' },
{ match: '该货物类型编码已存在', col: '货物类型编码', rule: '请更换唯一编码' },
{ match: '请选择上级货物类型', col: '上级货物类型', rule: '二级货物类型必须选择上级' },
{
match: '二级编码前2位必须与上级货物类型编码一致',
col: '货物类型编码',
rule: '二级编码前2位必须与上级货物类型编码一致',
},
{ match: '货物类型名称不能超过50字符', col: '货物类型', rule: '货物类型名称不能超过50字符' },
{ match: '备注不能超过200字', col: '备注', rule: '备注不能超过200字' },
];
/**
* 加工后端返回的导入失败明细 Excel
* - 依据「错误文本 -> 字段列」映射,给违规的那一栏标红
* - 将「导入失败原因」列改写为第N行错误规则格式
* 未匹配到的未知错误,标红原因列本身并原样提示
*/
export const decorateCargoTypeFailDetail = async workbook => {
const ws = workbook.worksheets[0];
if (!ws) return;
const headerRow = ws.getRow(1);
const colMap = {};
headerRow.eachCell((cell, colNumber) => {
const name = String(cell.value == null ? '' : cell.value).trim();
if (name) colMap[name] = colNumber;
});
const reasonCol = colMap['导入失败原因'];
if (!reasonCol) return;
ws.eachRow((row, rowNumber) => {
if (rowNumber === 1) return; // 跳过表头
const reasonCell = row.getCell(reasonCol);
const raw = String(reasonCell.value == null ? '' : reasonCell.value).trim();
if (!raw) return;
const parts = raw.split(/[;\n\r]+/).map(s => s.trim()).filter(Boolean);
if (parts.length === 0) return;
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('');
});
};
export default {
data() {
const validateCargoName = (rule, value, callback) => {
const cargoName = String(value || '').trim();
if (!cargoName) {
callback(new Error('请输入货物类型名称'));
} else if (cargoName.length > 50) {
callback(new Error('货物类型名称不能超过50字符'));
} else {
callback();
}
};
const validateCargoCode = (rule, value, callback) => {
const cargoCode = String(value || '').trim();
const pattern = this.form.typeLevel === 1 ? /^\d{2}$/ : /^\d{4}$/;
if (!pattern.test(cargoCode)) {
callback(new Error('货物类型编码格式不正确'));
} else if (
this.form.typeLevel === 2 &&
this.form.parentCargoCode &&
!cargoCode.startsWith(this.form.parentCargoCode)
) {
callback(new Error('二级编码前2位必须与上级货物类型编码一致'));
} else {
callback();
}
};
const validateRemark = (rule, value, callback) => {
if (String(value || '').length > 200) {
callback(new Error('备注不能超过200字'));
} else {
callback();
}
};
return {
form: {},
query: {},
@@ -202,6 +220,7 @@ export default {
excelForm: {},
parentOptions: [],
parentLoading: false,
dialogType: 'add',
page: {
pageSize: 10,
pageSizes: [10, 20, 50, 100],
@@ -209,137 +228,7 @@ export default {
total: 0,
},
selectionList: [],
option: {
height: 'auto',
calcHeight: 32,
dialogWidth: 760,
labelPosition: 'right',
labelWidth: 'auto',
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
saveBtnText: '提交',
updateBtnText: '提交',
searchShow: true,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 8,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
indexWidth: 70,
viewBtn: false,
delBtn: false,
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 220,
column: [
{
label: '类型',
prop: 'typeLevel',
type: 'radio',
dataType: 'number',
dicData: typeLevelDic,
search: true,
value: 1,
span: 24,
minWidth: 120,
rules: [{ required: true, message: '请选择货物类型级别', trigger: 'change' }],
change: ({ value }) => this.handleTypeLevelChange(value),
},
{
label: '上级货物类型',
prop: 'parentId',
formslot: true,
hide: true,
display: false,
span: 24,
rules: [{ required: true, message: '请选择上级货物类型', trigger: 'change' }],
},
{
label: '上级货物类型',
prop: 'parentCargoName',
slot: true,
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 160,
},
{
label: '上级货物类型编码',
prop: 'parentCargoCode',
slot: true,
readonly: true,
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 160,
rules: [{ required: true, message: '请选择上级货物类型', trigger: 'change' }],
},
{
label: '货物类型',
prop: 'cargoName',
minWidth: 150,
maxlength: 50,
showWordLimit: true,
rules: [{ validator: validateCargoName, trigger: 'blur' }],
},
{
label: '货物类型编码',
prop: 'cargoCode',
minWidth: 140,
search: true,
maxlength: 4,
rules: [{ validator: validateCargoCode, trigger: 'blur' }],
change: ({ value }) => this.handleCargoCodeChange(value),
},
{
label: '创建人',
prop: 'createUserName',
slot: true,
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 120,
},
{
label: '备注',
prop: 'remark',
type: 'textarea',
minRows: 4,
span: 24,
minWidth: 180,
maxlength: 200,
showWordLimit: true,
overHidden: true,
rules: [{ validator: validateRemark, trigger: 'blur' }],
},
{
label: '更新时间',
prop: 'updateTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 160,
},
{
label: '创建时间',
prop: 'createTime',
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
addDisplay: false,
editDisplay: false,
display: false,
minWidth: 160,
},
],
},
option: getCargoTypeOption(this),
excelOption: {
submitBtn: false,
emptyBtn: false,
@@ -506,7 +395,6 @@ export default {
done();
},
error => {
window.console.log(error);
loading();
}
);
@@ -524,7 +412,6 @@ export default {
done();
},
error => {
window.console.log(error);
loading();
}
);
@@ -565,6 +452,7 @@ export default {
this.onLoad(this.page, this.query);
},
beforeOpen(done, type) {
this.dialogType = type || 'add';
if (['edit', 'view'].includes(type)) {
getDetail(this.form.id).then(res => {
this.form = res.data.data || {};
@@ -575,7 +463,7 @@ export default {
return;
}
this.form = {
typeLevel: 1,
typeLevel: 2,
cargoName: '',
cargoCode: '',
parentId: undefined,
@@ -627,7 +515,9 @@ export default {
});
},
handleImport() {
openImportDialog(this, '货物类型');
openImportDialog(this, '货物类型', undefined, {
failDetailDecorator: decorateCargoTypeFailDetail,
});
},
buildExportParams() {
const params = { ...this.query };