186 lines
5.7 KiB
JavaScript
186 lines
5.7 KiB
JavaScript
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}`);
|