diff --git a/.workbuddy/memory/2026-07-28.md b/.workbuddy/memory/2026-07-28.md index c4174b7..297ba6a 100644 --- a/.workbuddy/memory/2026-07-28.md +++ b/.workbuddy/memory/2026-07-28.md @@ -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、失败原因编号格式与红色字体均正确。 diff --git a/src/views/base/cargo-type.vue b/src/views/base/cargo-type.vue index ee377e5..ec3d563 100644 --- a/src/views/base/cargo-type.vue +++ b/src/views/base/cargo-type.vue @@ -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 => {