diff --git a/backend/public/templates/财务记账导入模板.xlsx b/backend/public/templates/财务记账导入模板.xlsx index 308a655..cf4b8a4 100644 Binary files a/backend/public/templates/财务记账导入模板.xlsx and b/backend/public/templates/财务记账导入模板.xlsx differ diff --git a/backend/scripts/generate-excel-template.js b/backend/scripts/generate-excel-template.js index bf35d44..8f2fc89 100644 --- a/backend/scripts/generate-excel-template.js +++ b/backend/scripts/generate-excel-template.js @@ -17,14 +17,18 @@ async function generateTemplate() { const projectCategories = categories.filter(c => c.category_level1 === 'project').map(c => c.label); const companyCategories = categories.filter(c => c.category_level1 === 'company').map(c => c.label); - // ========== Sheet2: 项目名称列表 ========== - const sheet2 = workbook.addWorksheet('项目列表', { + const MAX_PROJECTS = 100; + const MAX_CATEGORIES = 20; + + // ========== Sheet2: 参考数据 ========== + const sheet2 = workbook.addWorksheet('参考数据', { properties: { tabColor: { argb: '4472C4' } } }); + // A列: 项目名称(预留100行,用户可在此添加新项目) sheet2.getColumn(1).width = 40; - sheet2.getCell('A1').value = '项目名称'; - sheet2.getCell('A1').font = { bold: true, size: 12, color: { argb: 'FFFFFF' } }; + sheet2.getCell('A1').value = '项目名称(可在此添加新项目)'; + sheet2.getCell('A1').font = { bold: true, size: 11, color: { argb: 'FFFFFF' } }; sheet2.getCell('A1').fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: '4472C4' } }; sheet2.getCell('A1').alignment = { horizontal: 'center' }; @@ -33,37 +37,65 @@ async function generateTemplate() { cell.value = name; cell.border = { bottom: { style: 'thin', color: { argb: 'D9D9D9' } } }; }); + for (let i = projectNames.length; i < MAX_PROJECTS; i++) { + sheet2.getCell(`A${i + 2}`).border = { bottom: { style: 'thin', color: { argb: 'E8E8E8' } } }; + } - sheet2.getCell('C1').value = '收支类型'; - sheet2.getCell('C1').font = { bold: true }; - ['收入', '支出'].forEach((v, i) => { sheet2.getCell(`C${i + 2}`).value = v; }); + // C列: 收入分类 + sheet2.getColumn(3).width = 18; + sheet2.getCell('C1').value = '收入'; + sheet2.getCell('C1').font = { bold: true, size: 11, color: { argb: 'FFFFFF' } }; + sheet2.getCell('C1').fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: '52C41A' } }; + sheet2.getCell('C1').alignment = { horizontal: 'center' }; + incomeCategories.forEach((v, i) => { + sheet2.getCell(`C${i + 2}`).value = v; + sheet2.getCell(`C${i + 2}`).border = { bottom: { style: 'thin', color: { argb: 'D9D9D9' } } }; + }); - sheet2.getCell('D1').value = '一级分类'; - sheet2.getCell('D1').font = { bold: true }; - ['收入', '项目支出', '公司支出'].forEach((v, i) => { sheet2.getCell(`D${i + 2}`).value = v; }); + // D列: 项目支出分类 + sheet2.getColumn(4).width = 18; + sheet2.getCell('D1').value = '项目支出'; + sheet2.getCell('D1').font = { bold: true, size: 11, color: { argb: 'FFFFFF' } }; + sheet2.getCell('D1').fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: '1890FF' } }; + sheet2.getCell('D1').alignment = { horizontal: 'center' }; + projectCategories.forEach((v, i) => { + sheet2.getCell(`D${i + 2}`).value = v; + sheet2.getCell(`D${i + 2}`).border = { bottom: { style: 'thin', color: { argb: 'D9D9D9' } } }; + }); - sheet2.getCell('F1').value = '收入分类'; - sheet2.getCell('F1').font = { bold: true }; - incomeCategories.forEach((v, i) => { sheet2.getCell(`F${i + 2}`).value = v; }); + // E列: 公司支出分类 + sheet2.getColumn(5).width = 18; + sheet2.getCell('E1').value = '公司支出'; + sheet2.getCell('E1').font = { bold: true, size: 11, color: { argb: 'FFFFFF' } }; + sheet2.getCell('E1').fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FA8C16' } }; + sheet2.getCell('E1').alignment = { horizontal: 'center' }; + companyCategories.forEach((v, i) => { + sheet2.getCell(`E${i + 2}`).value = v; + sheet2.getCell(`E${i + 2}`).border = { bottom: { style: 'thin', color: { argb: 'D9D9D9' } } }; + }); - sheet2.getCell('G1').value = '项目支出分类'; + // G列: 币种 + sheet2.getColumn(7).width = 10; + sheet2.getCell('G1').value = '币种'; sheet2.getCell('G1').font = { bold: true }; - projectCategories.forEach((v, i) => { sheet2.getCell(`G${i + 2}`).value = v; }); + ['CNY', 'LAK', 'USD', 'THB'].forEach((v, i) => { sheet2.getCell(`G${i + 2}`).value = v; }); - sheet2.getCell('H1').value = '公司支出分类'; + // H列: 对方类型 + sheet2.getColumn(8).width = 12; + sheet2.getCell('H1').value = '对方类型'; sheet2.getCell('H1').font = { bold: true }; - companyCategories.forEach((v, i) => { sheet2.getCell(`H${i + 2}`).value = v; }); + ['供应商', '分包商', '客户', '员工', '物流公司', '股东', '其他'].forEach((v, i) => { sheet2.getCell(`H${i + 2}`).value = v; }); - sheet2.getCell('J1').value = '币种'; - sheet2.getCell('J1').font = { bold: true }; - ['CNY', 'LAK', 'USD', 'THB'].forEach((v, i) => { sheet2.getCell(`J${i + 2}`).value = v; }); + // 定义命名范围(用于 INDIRECT 联动下拉) + // ExcelJS 不直接支持 named ranges,我们通过 workbook.definedNames 添加 + // 格式: 'SheetName!$Col$Start:$Col$End' - sheet2.getCell('K1').value = '对方类型'; - sheet2.getCell('K1').font = { bold: true }; - ['供应商', '分包商', '客户', '员工', '物流公司', '股东', '其他'].forEach((v, i) => { sheet2.getCell(`K${i + 2}`).value = v; }); - - const projectListEnd = Math.max(projectNames.length + 1, 2); - const categoryListEnd = Math.max(incomeCategories.length, projectCategories.length, companyCategories.length) + 1; + // 收入分类命名范围 + workbook.definedNames.add(`参考数据!$C$2:$C$${incomeCategories.length + 1}`, '收入'); + // 项目支出分类命名范围 + workbook.definedNames.add(`参考数据!$D$2:$D$${projectCategories.length + 1}`, '项目支出'); + // 公司支出分类命名范围 + workbook.definedNames.add(`参考数据!$E$2:$E$${companyCategories.length + 1}`, '公司支出'); // ========== Sheet1: 记账主表 ========== const sheet1 = workbook.addWorksheet('财务记账', { @@ -107,28 +139,21 @@ async function generateTemplate() { for (let i = 2; i <= dataRows + 1; i++) { const row = sheet1.getRow(i); - // I列: 等效人民币 = 金额 × 汇率(自动计算) const cnyCell = row.getCell(9); cnyCell.value = { formula: `F${i}*H${i}` }; cnyCell.numFmt = '#,##0.00'; cnyCell.protection = { locked: true }; - // 金额列格式 row.getCell(6).numFmt = '#,##0.00'; - // 汇率列格式 row.getCell(8).numFmt = '0.000000'; - - // 日期列格式 row.getCell(1).numFmt = 'YYYY-MM-DD'; - // 交替行背景色 if (i % 2 === 0) { row.eachCell({ includeEmpty: true }, (cell) => { cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'F2F7FB' } }; }); } - // 边框 row.eachCell({ includeEmpty: true }, (cell) => { cell.border = { bottom: { style: 'thin', color: { argb: 'D9D9D9' } }, @@ -137,8 +162,6 @@ async function generateTemplate() { }); } - // 数据验证 - 收支类型 - sheet1.getDataValidator = function() {}; for (let i = 2; i <= dataRows + 1; i++) { // B列: 收支类型 sheet1.getCell(`B${i}`).dataValidation = { @@ -147,7 +170,7 @@ async function generateTemplate() { formulae: ['"收入,支出"'], showErrorMessage: true, errorTitle: '输入错误', - error: '请从下拉列表中选择:收入 或 支出' + error: '请选择:收入 或 支出' }; // C列: 一级分类 @@ -157,29 +180,25 @@ async function generateTemplate() { formulae: ['"收入,项目支出,公司支出"'], showErrorMessage: true, errorTitle: '输入错误', - error: '请从下拉列表中选择' + error: '请选择:收入 / 项目支出 / 公司支出' }; - // D列: 二级分类(收入+项目+公司 全部列出,用户根据一级分类选择对应的) - const allCategories = [...incomeCategories, ...projectCategories, ...companyCategories]; - const catStr = allCategories.join(','); + // D列: 二级分类(联动!根据C列一级分类的值,用INDIRECT引用对应命名范围) sheet1.getCell(`D${i}`).dataValidation = { type: 'list', allowBlank: true, - formulae: [`"${catStr}"`], + formulae: [`INDIRECT(C${i})`], showErrorMessage: true, errorTitle: '输入错误', - error: '请从下拉列表中选择二级分类' + error: '请先选择一级分类,再选择对应的二级分类' }; - // E列: 项目名称(从Sheet2引用) + // E列: 项目名称(从Sheet2引用,预留100行可扩展) sheet1.getCell(`E${i}`).dataValidation = { type: 'list', allowBlank: true, - formulae: [`项目列表!$A$2:$A$${projectListEnd}`], - showErrorMessage: true, - errorTitle: '输入错误', - error: '请从下拉列表中选择项目名称' + formulae: [`参考数据!$A$2:$A$${MAX_PROJECTS + 1}`], + showErrorMessage: false }; // G列: 币种 @@ -189,7 +208,7 @@ async function generateTemplate() { formulae: ['"CNY,LAK,USD,THB"'], showErrorMessage: true, errorTitle: '输入错误', - error: '请选择币种:CNY / LAK / USD / THB' + error: '请选择:CNY / LAK / USD / THB' }; // K列: 对方类型 @@ -199,10 +218,9 @@ async function generateTemplate() { formulae: ['"供应商,分包商,客户,员工,物流公司,股东,其他"'], showErrorMessage: true, errorTitle: '输入错误', - error: '请从下拉列表中选择对方类型' + error: '请从下拉列表中选择' }; - // H列: 汇率 - CNY时自动填1 // A列: 日期验证 sheet1.getCell(`A${i}`).dataValidation = { type: 'date', @@ -211,11 +229,11 @@ async function generateTemplate() { formulae: [new Date(2020, 0, 1)], showErrorMessage: true, errorTitle: '日期格式错误', - error: '请输入有效日期,格式:YYYY-MM-DD' + error: '请输入有效日期' }; } - // 示例数据(第2行) + // 示例数据 const exampleRow = sheet1.getRow(2); exampleRow.getCell(1).value = new Date(2026, 2, 15); exampleRow.getCell(2).value = '支出'; @@ -229,7 +247,6 @@ async function generateTemplate() { exampleRow.getCell(11).value = '供应商'; exampleRow.getCell(13).value = '采购190×12m电杆50根'; - // 示例数据(第3行) const exampleRow2 = sheet1.getRow(3); exampleRow2.getCell(1).value = new Date(2026, 2, 20); exampleRow2.getCell(2).value = '支出'; @@ -244,7 +261,6 @@ async function generateTemplate() { exampleRow2.getCell(12).value = '李四'; exampleRow2.getCell(13).value = '3月驻现场住宿费'; - // 示例数据(第4行) const exampleRow3 = sheet1.getRow(4); exampleRow3.getCell(1).value = new Date(2026, 1, 20); exampleRow3.getCell(2).value = '收入'; @@ -258,7 +274,18 @@ async function generateTemplate() { exampleRow3.getCell(11).value = '客户'; exampleRow3.getCell(13).value = '合同首付款30%'; - // 保护工作表(锁定I列公式) + const exampleRow4 = sheet1.getRow(5); + exampleRow4.getCell(1).value = new Date(2026, 3, 1); + exampleRow4.getCell(2).value = '支出'; + exampleRow4.getCell(3).value = '公司支出'; + exampleRow4.getCell(4).value = '工资薪酬'; + exampleRow4.getCell(6).value = 25000; + exampleRow4.getCell(7).value = 'CNY'; + exampleRow4.getCell(8).value = 1; + exampleRow4.getCell(12).value = '赵六'; + exampleRow4.getCell(13).value = '3月份办公室人员工资'; + + // 保护工作表 sheet1.protect('', { selectLockedCells: true, selectUnlockedCells: true, @@ -273,7 +300,6 @@ async function generateTemplate() { autoFilter: true, }); - // 设置可编辑列的单元格为未锁定 for (let i = 2; i <= dataRows + 1; i++) { [1,2,3,4,5,6,7,8,10,11,12,13,14].forEach(col => { sheet1.getRow(i).getCell(col).protection = { locked: false }; diff --git a/frontend/public/templates/财务记账导入模板.xlsx b/frontend/public/templates/财务记账导入模板.xlsx index 308a655..cf4b8a4 100644 Binary files a/frontend/public/templates/财务记账导入模板.xlsx and b/frontend/public/templates/财务记账导入模板.xlsx differ