Compare commits
4 Commits
e76cf70a20
...
38ce8fa2bc
| Author | SHA1 | Date | |
|---|---|---|---|
| 38ce8fa2bc | |||
| 8002fe3d50 | |||
| c695d72ec8 | |||
| 8ff9f6bb66 |
@@ -44,3 +44,34 @@
|
|||||||
- 同期发现 2888 dev server 进程退出(lsof 无监听),导致 Vite 代理对 localhost:2888 全返回 502(含根路径)。已重启 `pnpm dev`(端口 2888,proxy target 172.16.203.228:8000),代理恢复 200。后端直连一直正常。
|
- 同期发现 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),文本格式保留前导零。三份文件(一级/二级/合并)已更新。
|
- 改用新编码重造:一级 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.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` 两条演示数据;用户应按需删除/替换示例再填自己的数据。
|
||||||
|
|||||||
14
.workbuddy/memory/2026-07-28.md
Normal file
14
.workbuddy/memory/2026-07-28.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# 2026-07-28 工作日志
|
||||||
|
|
||||||
|
## 货物类型导入模板美化
|
||||||
|
- 文件:`src/views/base/cargo-type.vue` 的 `handleTemplate()`
|
||||||
|
- 改动(第一次):表头行加主色底色 `#409EFF` + 白色加粗字体 + 居中对齐 + 细边框;行高 42。
|
||||||
|
- 改动(调整后):按用户反馈,表头底色改为统一灰色 `#D9D9D9` + 黑色加粗字体,行高降为 30。
|
||||||
|
- 导入失败明细 `decorateCargoTypeFailDetail` 调整:仅「导入失败原因」列字体红色;错误按 `A./B./C.` 编号,分号(;)隔开,并去掉后端重复的「第N行:」前缀;最终格式示例 `第2行:A.该货物类型编码已存在,请更换唯一编码`。
|
||||||
|
- 验证:Node + exceljs 4.4.0 跑通生成逻辑,确认表头 fill/font/alignment/height、失败原因编号格式与红色字体均正确。
|
||||||
|
|
||||||
|
## 货物类型导入失败明细 - 备注超长与标红强化
|
||||||
|
- 确认需求:除「导入失败原因」列外,其他列一律不标红;已增加 decorator 清除所有非失败原因列红色字体(含类型列,不论对错)。
|
||||||
|
- 失败原因按 A./B./C. 编号、分号(;)隔开;并优化:错误文本已包含规则说明时不重复拼接(避免「备注不能超过200字,备注不能超过200字」)。
|
||||||
|
- 新增兜底:失败明细行若「备注」列文本长度 > 200 且失败原因未含备注错误,自动追加一条「备注不能超过200字」编号项(Node 验证通过)。
|
||||||
|
- 后端根因(已读 /Users/gxwebsoft/JAVA/tms-erp-api-ws/blade-service/blade-system/.../CargoTypeServiceImpl.java):validate() 用 throw 实现,一行多个错误时只抛第一个,备注超长校验(第238行)可能被前置的编码/名称/上级错误挡住,导致失败明细不显示备注原因。彻底方案需后端改收集式校验(列出全部错误),待用户确认是否改 Java。
|
||||||
@@ -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 业务名称(如「货物类型」)
|
||||||
|
|||||||
@@ -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:
|
||||||
* - 依据「错误文本 -> 字段列」映射,给违规的那一栏标红
|
* - 「导入失败原因」列按 A. B. C. 编号列出,多条以分号隔开
|
||||||
* - 将「导入失败原因」列改写为(第N行:错误,规则)格式
|
* - 每个错误后追加对应规则说明,仅失败原因列标红
|
||||||
* 未匹配到的未知错误,标红原因列本身并原样提示
|
* - 适当加宽各列、原因列自动换行,避免内容拥挤
|
||||||
*/
|
*/
|
||||||
export const decorateCargoTypeFailDetail = async workbook => {
|
export const decorateCargoTypeFailDetail = async workbook => {
|
||||||
const ws = workbook.worksheets[0];
|
const ws = workbook.worksheets[0];
|
||||||
@@ -190,22 +200,50 @@ export const decorateCargoTypeFailDetail = async workbook => {
|
|||||||
if (!reasonCol) return;
|
if (!reasonCol) return;
|
||||||
ws.eachRow((row, rowNumber) => {
|
ws.eachRow((row, rowNumber) => {
|
||||||
if (rowNumber === 1) return; // 跳过表头
|
if (rowNumber === 1) return; // 跳过表头
|
||||||
|
// 仅「导入失败原因」列允许红色,其他列红色字体统一清除
|
||||||
|
row.eachCell((cell, colNumber) => {
|
||||||
|
if (colNumber === reasonCol) return;
|
||||||
|
if (cell.font && cell.font.color) {
|
||||||
|
const font = { ...cell.font };
|
||||||
|
delete font.color;
|
||||||
|
cell.font = font;
|
||||||
|
}
|
||||||
|
});
|
||||||
const reasonCell = row.getCell(reasonCol);
|
const reasonCell = row.getCell(reasonCol);
|
||||||
const raw = String(reasonCell.value == null ? '' : reasonCell.value).trim();
|
let raw = String(reasonCell.value == null ? '' : reasonCell.value).trim();
|
||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
const parts = raw.split(/[;;\n\r]+/).map(s => s.trim()).filter(Boolean);
|
// 去掉后端可能已经存在的「第N行:」前缀,避免重复
|
||||||
|
raw = raw.replace(/^第\s*\d+\s*行[::]\s*/, '');
|
||||||
|
const parts = raw
|
||||||
|
.split(/[;;\n\r]+/)
|
||||||
|
.map(s => s.trim().replace(/^第\s*\d+\s*行[::]\s*/, ''))
|
||||||
|
.filter(Boolean);
|
||||||
if (parts.length === 0) return;
|
if (parts.length === 0) return;
|
||||||
const decorated = parts.map(part => {
|
const decorated = parts.map((part, index) => {
|
||||||
|
const label = `${String.fromCharCode(65 + index)}.`; // A. B. C.
|
||||||
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 part.includes(rule.rule) ? `${label}${part}` : `${label}${part},${rule.rule}`;
|
||||||
return `第${rowNumber}行:${part},${rule.rule}`;
|
|
||||||
}
|
}
|
||||||
reasonCell.fill = FAIL_RED_FILL;
|
return `${label}${part}`;
|
||||||
return `第${rowNumber}行:${part}`;
|
|
||||||
});
|
});
|
||||||
reasonCell.value = decorated.join(';');
|
// 兜底:失败行的备注列超长(>200字)时,补充备注超长提示
|
||||||
|
const remarkCol = colMap['备注'];
|
||||||
|
if (remarkCol) {
|
||||||
|
const remarkVal = String(row.getCell(remarkCol).value == null ? '' : row.getCell(remarkCol).value).trim();
|
||||||
|
if (remarkVal.length > 200 && !decorated.some(d => d.includes('备注不能超过200字'))) {
|
||||||
|
decorated.push(`${String.fromCharCode(65 + decorated.length)}.备注不能超过200字`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reasonCell.value = `第${rowNumber}行:${decorated.join(';')}`;
|
||||||
|
reasonCell.font = { color: { argb: 'FFFF0000' } };
|
||||||
|
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 +582,75 @@ 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 = '@';
|
||||||
|
});
|
||||||
|
// 表头与数据区样式:统一灰色表头 + 黑色加粗居中 + 细边框
|
||||||
|
const headerFill = {
|
||||||
|
type: 'pattern',
|
||||||
|
pattern: 'solid',
|
||||||
|
fgColor: { argb: 'FFD9D9D9' },
|
||||||
|
};
|
||||||
|
const thinBorder = {
|
||||||
|
top: { style: 'thin', color: { argb: 'FFD0D7E2' } },
|
||||||
|
left: { style: 'thin', color: { argb: 'FFD0D7E2' } },
|
||||||
|
bottom: { style: 'thin', color: { argb: 'FFD0D7E2' } },
|
||||||
|
right: { style: 'thin', color: { argb: 'FFD0D7E2' } },
|
||||||
|
};
|
||||||
|
ws.eachRow(row => {
|
||||||
|
row.eachCell(cell => {
|
||||||
|
cell.border = thinBorder;
|
||||||
|
if (row.number === 1) {
|
||||||
|
// 表头:灰色底色 + 黑字加粗 + 居中对齐
|
||||||
|
cell.fill = headerFill;
|
||||||
|
cell.font = { bold: true, color: { argb: 'FF000000' } };
|
||||||
|
cell.alignment = {
|
||||||
|
wrapText: true,
|
||||||
|
vertical: 'middle',
|
||||||
|
horizontal: 'center',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 数据行:左对齐,垂直居中
|
||||||
|
cell.alignment = {
|
||||||
|
wrapText: true,
|
||||||
|
vertical: 'middle',
|
||||||
|
horizontal: 'left',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
// 表头行高适度,兼顾两行说明与视觉紧凑
|
||||||
|
ws.getRow(1).height = 30;
|
||||||
|
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();
|
||||||
|
|||||||
Reference in New Issue
Block a user