const XLSX = require('xlsx'); const fs = require('fs'); const path = require('path'); /** * 合并「团队开票申请表」脚本 * * 目录结构假设: * <根目录>/ * 7.xxx/ * *团队开票申请表*.xlsx (含4个sheet,其中一个含"商旅",一个含"吉智") * 其它两个表格... * 7.yyy/ * ... * * 用法: * node merge_kaipiao.js <根目录> [输出文件路径] * * 例如: * node merge_kaipiao.js "D:\\某个目录" "D:\\合并结果.xlsx" */ // ---------- 参数 ---------- const rootDir = process.argv[2]; const outFile = process.argv[3] || path.join(process.cwd(), '团队开票申请表-合并.xlsx'); if (!rootDir) { console.error('用法: node merge_kaipiao.js <根目录> [输出文件路径]'); process.exit(1); } if (!fs.existsSync(rootDir) || !fs.statSync(rootDir).isDirectory()) { console.error(`根目录不存在或不是文件夹: ${rootDir}`); process.exit(1); } // ---------- 工具函数 ---------- // 判断是否为 Excel 文件(排除临时文件 ~$ 开头) function isExcelFile(name) { return /\.(xlsx|xls)$/i.test(name) && !name.startsWith('~$'); } // 在一个工作簿里按关键字查找 sheet 名 function findSheetName(workbook, keyword) { return workbook.SheetNames.find(n => n.includes(keyword)); } // 把一个 sheet 读成二维数组(含表头行) function sheetToRows(sheet) { return XLSX.utils.sheet_to_json(sheet, { header: 1, defval: null, blankrows: false }); } // 去掉指定列(按表头名精确匹配)单元格值中的所有冒号(含全角) function stripColonInColumn(rows, columnName) { if (!rows || rows.length === 0) return 0; const header = rows[0]; const colIndex = header.findIndex(h => h != null && String(h).trim() === columnName); if (colIndex === -1) { console.warn(` [警告] 未找到列 "${columnName}",跳过冒号清理`); return 0; } let changed = 0; for (let i = 1; i < rows.length; i++) { const v = rows[i][colIndex]; if (v == null) continue; const str = String(v); if (str.includes(':') || str.includes(':')) { rows[i][colIndex] = str.replace(/[::]/g, ''); changed++; } } return changed; } // ---------- 收集子文件夹 ---------- const subDirs = fs.readdirSync(rootDir, { withFileTypes: true }) .filter(d => d.isDirectory() && d.name.startsWith('7.')) .map(d => d.name) .sort(); if (subDirs.length === 0) { console.error('未找到以 "7." 开头的子文件夹'); process.exit(1); } console.log(`找到 ${subDirs.length} 个 "7." 子文件夹`); // 合并结果:分别累积 商旅 / 吉智 两组数据 let shangluHeader = null; const shangluRows = []; let jizhiHeader = null; const jizhiRows = []; let processedCount = 0; for (const sub of subDirs) { const subPath = path.join(rootDir, sub); // 找到名字含「团队开票申请表」的表格 const files = fs.readdirSync(subPath).filter(isExcelFile); const targetFile = files.find(f => f.includes('团队开票申请表')); if (!targetFile) { console.warn(` [跳过] ${sub}: 未找到含"团队开票申请表"的表格`); continue; } const filePath = path.join(subPath, targetFile); let workbook; try { workbook = XLSX.readFile(filePath); } catch (e) { console.warn(` [跳过] ${sub}: 读取失败 - ${e.message}`); continue; } const shangluName = findSheetName(workbook, '商旅'); const jizhiName = findSheetName(workbook, '吉智'); if (!shangluName) console.warn(` [警告] ${sub}: 未找到含"商旅"的sheet`); if (!jizhiName) console.warn(` [警告] ${sub}: 未找到含"吉智"的sheet`); // 处理 商旅 if (shangluName) { const rows = sheetToRows(workbook.Sheets[shangluName]); if (rows.length > 0) { const [header, ...body] = rows; if (!shangluHeader) shangluHeader = header; shangluRows.push(...body); } } // 处理 吉智 if (jizhiName) { const rows = sheetToRows(workbook.Sheets[jizhiName]); if (rows.length > 0) { const [header, ...body] = rows; if (!jizhiHeader) jizhiHeader = header; jizhiRows.push(...body); } } processedCount++; console.log(` [完成] ${sub}: ${targetFile}`); } if (processedCount === 0) { console.error('没有可合并的数据'); process.exit(1); } // ---------- 生成输出工作簿 ---------- const outWb = XLSX.utils.book_new(); if (shangluHeader) { const data = [shangluHeader, ...shangluRows]; const n1 = stripColonInColumn(data, '结算账户'); if (n1) console.log(` 商旅"结算账户"列清理冒号: ${n1} 个单元格`); const ws = XLSX.utils.aoa_to_sheet(data); XLSX.utils.book_append_sheet(outWb, ws, '商旅'); } else { console.warn('未收集到任何"商旅"数据'); } if (jizhiHeader) { const data = [jizhiHeader, ...jizhiRows]; const n2 = stripColonInColumn(data, '*对账单/项目名称'); if (n2) console.log(` 吉智"*对账单/项目名称"列清理冒号: ${n2} 个单元格`); const ws = XLSX.utils.aoa_to_sheet(data); XLSX.utils.book_append_sheet(outWb, ws, '吉智'); } else { console.warn('未收集到任何"吉智"数据'); } if (outWb.SheetNames.length === 0) { console.error('没有生成任何 sheet,未写出文件'); process.exit(1); } XLSX.writeFile(outWb, outFile); console.log('\n合并完成:'); console.log(` 商旅: ${shangluRows.length} 行数据`); console.log(` 吉智: ${jizhiRows.length} 行数据`); console.log(` 输出文件: ${outFile}`);