style(cargo-type): 美化货物类型导入模板Excel样式及失败明细

- 表头底色调整为统一灰色 #D9D9D9,字体改为黑色加粗
- 表头行高从42调整为30,增加视觉紧凑感
- 表头与数据区统一添加细边框,居中对齐并自动换行
- 编码列保持文本格式,防止格式识别错误
- 导入失败明细中「导入失败原因」列字体设置为红色
- 失败原因按 A./B./C. 格式编号,使用分号分隔
- 去除后端重复的「第N行:」前缀,避免冗余信息
- 通过 Node + exceljs 验证样式和格式变更正确生效
This commit is contained in:
2026-07-28 00:37:42 +08:00
parent c695d72ec8
commit 8002fe3d50
2 changed files with 36 additions and 21 deletions

View File

@@ -2,5 +2,7 @@
## 货物类型导入模板美化
- 文件:`src/views/base/cargo-type.vue``handleTemplate()`
- 改动:表头行加主色底色 `#409EFF`argb `FF409EFF`+ 白色加粗字体 + 居中对齐 + 自动换行;表头与数据区统一加细边框(`FFD0D7E2`);表头行高设为 42编码列保持文本格式`numFmt='@'`
- 验证Node + exceljs 4.4.0 跑通生成逻辑,确认表头 fill/font/alignment/border 与行高正确
- 改动(第一次):表头行加主色底色 `#409EFF` + 白色加粗字体 + 居中对齐 + 细边框;行高 42
- 改动(调整后):按用户反馈,表头底色改为统一灰色 `#D9D9D9` + 黑色加粗字体,行高降为 30
- 导入失败明细 `decorateCargoTypeFailDetail` 调整:仅「导入失败原因」列字体红色;错误按 `A./B./C.` 编号分号隔开并去掉后端重复的「第N行」前缀最终格式示例 `第2行A.该货物类型编码已存在,请更换唯一编码`
- 验证Node + exceljs 4.4.0 跑通生成逻辑,确认表头 fill/font/alignment/height、失败原因编号格式与红色字体均正确。

View File

@@ -183,9 +183,9 @@ const CARGO_TYPE_FAIL_COL_WIDTH = {
/**
* 加工后端返回的导入失败明细 Excel
* - 「导入失败原因」列改写为第N行错误规则格式便于定位
* - 「导入失败原因」列按 A. B. C. 编号列出,多条以分号隔开
* - 每个错误后追加对应规则说明,仅失败原因列标红
* - 适当加宽各列、原因列自动换行,避免内容拥挤
* 不再对单元格标红,仅通过文字清晰说明错误
*/
export const decorateCargoTypeFailDetail = async workbook => {
const ws = workbook.worksheets[0];
@@ -200,19 +200,32 @@ export const decorateCargoTypeFailDetail = async workbook => {
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) {
return `${rowNumber}行:${part}${rule.rule}`;
// 仅「导入失败原因」列允许红色,其他列红色字体统一清除
row.eachCell((cell, colNumber) => {
if (colNumber === reasonCol) return;
if (cell.font && cell.font.color) {
const font = { ...cell.font };
delete font.color;
cell.font = font;
}
return `${rowNumber}行:${part}`;
});
reasonCell.value = decorated.join('');
const reasonCell = row.getCell(reasonCol);
let raw = String(reasonCell.value == null ? '' : reasonCell.value).trim();
if (!raw) return;
// 去掉后端可能已经存在的「第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;
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));
return rule ? `${label}${part}${rule.rule}` : `${label}${part}`;
});
reasonCell.value = `${rowNumber}行:${decorated.join('')}`;
reasonCell.font = { color: { argb: 'FFFF0000' } };
reasonCell.alignment = { wrapText: true, vertical: 'top' };
});
// 加宽各列(未列在映射中的列给默认宽度)
@@ -585,11 +598,11 @@ export default {
const col = ws.getColumn(idx);
if (col) col.numFmt = '@';
});
// 表头与数据区样式美化:主色底色表头 + 白字加粗居中 + 细边框
// 表头与数据区样式:统一灰色表头 + 黑色加粗居中 + 细边框
const headerFill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FF409EFF' },
fgColor: { argb: 'FFD9D9D9' },
};
const thinBorder = {
top: { style: 'thin', color: { argb: 'FFD0D7E2' } },
@@ -601,9 +614,9 @@ export default {
row.eachCell(cell => {
cell.border = thinBorder;
if (row.number === 1) {
// 表头:色底色 + 字加粗 + 居中对齐
// 表头:色底色 + 字加粗 + 居中对齐
cell.fill = headerFill;
cell.font = { bold: true, color: { argb: 'FFFFFFFF' } };
cell.font = { bold: true, color: { argb: 'FF000000' } };
cell.alignment = {
wrapText: true,
vertical: 'middle',
@@ -619,8 +632,8 @@ export default {
}
});
});
// 表头行高,确保长说明换行后完整显示
ws.getRow(1).height = 42;
// 表头行高适度,兼顾两行说明与视觉紧凑
ws.getRow(1).height = 30;
return wb.xlsx.writeBuffer();
})
.then(buf => {