feat(cargo-type): 优化货物类型导入失败明细的错误标记和提示
- 新增导入失败明细的加工函数,给违规字段自动标红并丰富错误提示信息 - 在导入弹窗调用中增加失败明细装饰器处理,提升用户体验 - 将货物类型组件option配置提取为单独模块,简化代码逻辑 - 调整货物类型表单禁用逻辑,避免非新增时的错误操作 - 移除原有表单校验函数,相关验证由导入失败明细装饰器替代 - 引入exceljs依赖以支持导入失败Excel文件的读取和修改 - 修复导入失败明细导出时的文件命名和内容加工问题 - 优化代码结构,提升维护性和可读性
This commit is contained in:
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user