365 lines
20 KiB
JavaScript
365 lines
20 KiB
JavaScript
import fs from "node:fs/promises";
|
||
import path from "node:path";
|
||
import { SpreadsheetFile, Workbook } from "@oai/artifact-tool";
|
||
|
||
const taskDir = process.env.GB32960_TASK_DIR || "/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/tmp/gb32960-export-20260817";
|
||
const outputDir = process.env.GB32960_OUTPUT_DIR || "/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest/outputs/gb32960-export-20260817-agp4377";
|
||
const data = JSON.parse(await fs.readFile(path.join(taskDir, "raw-frames.json"), "utf8"));
|
||
const exportDate = data.query.dateFrom.slice(0, 10);
|
||
const outputPath = path.join(outputDir, `${data.query.plate}_${exportDate}_GB32960原始数据_中文字段.xlsx`);
|
||
|
||
const categoryNames = {
|
||
alarm: "报警数据",
|
||
drive_motor: "驱动电机数据",
|
||
engine: "发动机数据",
|
||
extreme: "极值数据",
|
||
fuel_cell: "燃料电池数据",
|
||
gd_fc_air_conditioner: "广东燃料电池空调数据",
|
||
gd_fc_auxiliary: "广东燃料电池辅助系统数据",
|
||
gd_fc_dcdc: "广东燃料电池 DC/DC 数据",
|
||
gd_fc_demo_extension: "广东燃料电池演示扩展数据",
|
||
gd_fc_stack: "广东燃料电池电堆数据",
|
||
gd_fc_vehicle_info: "广东燃料电池车辆信息数据",
|
||
gd_fc_vendor_tlv: "广东燃料电池厂家 TLV 扩展",
|
||
header: "报文头",
|
||
platform: "平台信息",
|
||
position: "车辆位置数据",
|
||
temperature: "可充电储能装置温度数据",
|
||
vehicle: "整车数据",
|
||
voltage: "可充电储能装置电压数据",
|
||
};
|
||
|
||
const fieldNames = {
|
||
battery_faults: "可充电储能装置故障码",
|
||
engine_faults: "发动机故障码",
|
||
general_alarm_flag: "通用报警标志",
|
||
max_alarm_level: "最高报警等级",
|
||
motor_faults: "驱动电机故障码",
|
||
other_faults: "其他故障码",
|
||
type: "数据单元类型",
|
||
count: "数量",
|
||
controller_current_a: "控制器直流母线电流",
|
||
controller_temperature_c: "控制器温度",
|
||
controller_voltage_v: "控制器输入电压",
|
||
motor_temperature_c: "电机温度",
|
||
serial_no: "驱动电机序号",
|
||
speed_rpm: "转速",
|
||
state: "状态",
|
||
torque_nm: "转矩",
|
||
engine_status: "发动机状态",
|
||
fuel_rate: "燃料消耗率",
|
||
max_temp_c: "最高温度",
|
||
max_temp_probe_no: "最高温度探针序号",
|
||
max_temp_subsystem_no: "最高温度子系统号",
|
||
max_voltage_cell_no: "最高电压单体序号",
|
||
max_voltage_subsystem_no: "最高电压子系统号",
|
||
max_voltage_v: "最高电压",
|
||
min_temp_c: "最低温度",
|
||
min_temp_probe_no: "最低温度探针序号",
|
||
min_temp_subsystem_no: "最低温度子系统号",
|
||
min_voltage_cell_no: "最低电压单体序号",
|
||
min_voltage_subsystem_no: "最低电压子系统号",
|
||
min_voltage_v: "最低电压",
|
||
dc_dc_status: "高压 DC/DC 状态",
|
||
fuel_cell_current_a: "燃料电池电流",
|
||
fuel_cell_voltage_v: "燃料电池电压",
|
||
hydrogen_consumption_kg_per_100km: "氢耗",
|
||
max_hydrogen_concentration_fraction: "最高氢浓度比例",
|
||
max_hydrogen_concentration_percent: "最高氢浓度",
|
||
max_hydrogen_concentration_ppm: "最高氢浓度原始值",
|
||
max_hydrogen_concentration_probe_id: "最高氢浓度探针编号",
|
||
max_hydrogen_pressure_mpa: "最高氢压力",
|
||
max_hydrogen_pressure_probe_id: "最高氢压探针编号",
|
||
max_hydrogen_temperature_c: "氢系统最高温度",
|
||
max_hydrogen_temperature_probe_id: "最高氢温探针编号",
|
||
temperature_probe_count: "温度探针数量",
|
||
temperature_probe_values_c: "温度探针值列表",
|
||
status: "状态",
|
||
subsystem_count: "子系统数量",
|
||
air_compressor_motor_voltage_v: "空气压缩机电机电压",
|
||
air_compressor_power_kw: "空气压缩机功率",
|
||
low_voltage_battery_voltage_v: "低压蓄电池电压",
|
||
water_pump_voltage_v: "水泵电压",
|
||
controller_temp_c: "控制器温度",
|
||
input_current_a: "输入电流",
|
||
input_voltage_v: "输入电压",
|
||
output_current_a: "输出电流",
|
||
output_voltage_v: "输出电压",
|
||
air_compressor_current_a: "空气压缩机电流",
|
||
air_compressor_voltage_v: "空气压缩机电压",
|
||
declared_length: "声明长度",
|
||
hydrogen_pump_current_a: "氢气循环泵电流",
|
||
hydrogen_pump_voltage_v: "氢气循环泵电压",
|
||
stack_temp_c: "电堆温度",
|
||
air_inlet_pressure_kpa: "空气入口压力",
|
||
avg_cell_voltage_v: "单体平均电压",
|
||
cell_count: "单体总数",
|
||
engine_work_state: "发动机工作状态",
|
||
frame_cell_voltages_v: "本帧单体电压列表",
|
||
hydrogen_inlet_pressure_kpa: "氢气入口压力",
|
||
max_cell_voltage_id: "最高电压单体编号",
|
||
max_cell_voltage_v: "单体最高电压",
|
||
min_cell_voltage_id: "最低电压单体编号",
|
||
min_cell_voltage_v: "单体最低电压",
|
||
stack_count: "电堆数量",
|
||
stack_water_outlet_temp_c: "电堆出水温度",
|
||
collision_alarm: "碰撞报警",
|
||
hydrogen_mass_kg: "车载氢量",
|
||
payload_hex: "原始载荷(十六进制)",
|
||
actual_body_length: "实际报文体长度",
|
||
body_length: "报文体长度",
|
||
command: "命令标识",
|
||
encrypt: "加密方式",
|
||
response_flag: "应答标志",
|
||
version: "协议版本",
|
||
vin: "车辆识别代号",
|
||
platform_name: "平台名称",
|
||
latitude: "纬度",
|
||
longitude: "经度",
|
||
position_status: "定位状态标志",
|
||
accelerator_pct: "加速踏板行程",
|
||
brake_pct: "制动踏板状态",
|
||
charge_status: "充电状态",
|
||
gear: "挡位原始值",
|
||
insulation_kohm: "绝缘电阻",
|
||
running_mode: "运行模式",
|
||
soc_percent: "SOC",
|
||
speed_kmh: "车速",
|
||
total_current_a: "总电流",
|
||
total_mileage_km: "累计里程",
|
||
total_voltage_v: "总电压",
|
||
vehicle_status: "车辆状态",
|
||
max_cell_v: "单体最高电压",
|
||
min_cell_v: "单体最低电压",
|
||
};
|
||
|
||
const unitBySuffix = {
|
||
controller_current_a: "A", controller_voltage_v: "V", controller_temperature_c: "℃",
|
||
motor_temperature_c: "℃", speed_rpm: "rpm", torque_nm: "N·m", fuel_rate: "L/100km",
|
||
max_temp_c: "℃", min_temp_c: "℃", max_voltage_v: "V", min_voltage_v: "V",
|
||
fuel_cell_current_a: "A", fuel_cell_voltage_v: "V", hydrogen_consumption_kg_per_100km: "kg/100km",
|
||
max_hydrogen_concentration_fraction: "比例", max_hydrogen_concentration_percent: "%", max_hydrogen_concentration_ppm: "ppm",
|
||
max_hydrogen_pressure_mpa: "MPa", max_hydrogen_temperature_c: "℃", temperature_probe_values_c: "℃",
|
||
air_compressor_motor_voltage_v: "V", air_compressor_power_kw: "kW", low_voltage_battery_voltage_v: "V", water_pump_voltage_v: "V",
|
||
controller_temp_c: "℃", input_current_a: "A", input_voltage_v: "V", output_current_a: "A", output_voltage_v: "V",
|
||
air_compressor_current_a: "A", air_compressor_voltage_v: "V", hydrogen_pump_current_a: "A", hydrogen_pump_voltage_v: "V", stack_temp_c: "℃",
|
||
air_inlet_pressure_kpa: "kPa", avg_cell_voltage_v: "V", frame_cell_voltages_v: "V", hydrogen_inlet_pressure_kpa: "kPa",
|
||
max_cell_voltage_v: "V", min_cell_voltage_v: "V", stack_water_outlet_temp_c: "℃", hydrogen_mass_kg: "kg",
|
||
latitude: "°", longitude: "°", accelerator_pct: "%", brake_pct: "%", insulation_kohm: "kΩ", soc_percent: "%",
|
||
speed_kmh: "km/h", total_current_a: "A", total_mileage_km: "km", total_voltage_v: "V", max_cell_v: "V", min_cell_v: "V",
|
||
};
|
||
|
||
const valueMappings = {
|
||
"gb32960.vehicle.vehicle_status": { "1": "启动", "2": "熄火", "3": "其他", "254": "异常", "255": "无效" },
|
||
"gb32960.vehicle.charge_status": { "1": "停车充电", "2": "行驶充电", "3": "未充电", "4": "充电完成", "254": "异常", "255": "无效" },
|
||
"gb32960.vehicle.running_mode": { "1": "纯电", "2": "混合动力", "3": "燃料电池", "254": "异常", "255": "无效" },
|
||
"gb32960.vehicle.dc_dc_status": { "1": "工作", "2": "断开", "254": "异常", "255": "无效" },
|
||
"gb32960.fuel_cell.dc_dc_status": { "1": "工作", "2": "断开", "254": "异常", "255": "无效" },
|
||
"gb32960.drive_motor.motors.motor_1.state": { "1": "耗电", "2": "发电", "3": "关闭", "4": "准备", "254": "异常", "255": "无效" },
|
||
"gb32960.alarm.max_alarm_level": { "0": "无故障", "1": "一级故障", "2": "二级故障", "3": "三级故障" },
|
||
"gb32960.gd_fc_air_conditioner.status": { "0": "关闭", "1": "启动", "254": "异常", "255": "无效" },
|
||
"gb32960.gd_fc_vehicle_info.collision_alarm": { "0": "无碰撞报警", "1": "有碰撞报警", "254": "异常", "255": "无效" },
|
||
"gb32960.position.position_status": {
|
||
"0": "有效定位 · 北纬 · 东经", "1": "无效定位 · 北纬 · 东经", "2": "有效定位 · 南纬 · 东经", "3": "无效定位 · 南纬 · 东经",
|
||
"4": "有效定位 · 北纬 · 西经", "5": "无效定位 · 北纬 · 西经", "6": "有效定位 · 南纬 · 西经", "7": "无效定位 · 南纬 · 西经",
|
||
},
|
||
};
|
||
|
||
function fieldParts(key) {
|
||
const parts = key.split(".");
|
||
const category = parts[1] ?? "";
|
||
const suffix = parts.at(-1) ?? key;
|
||
const motorMatch = key.match(/\.motors\.motor_(\d+)\./);
|
||
const subsystemMatch = key.match(/\.subsystems\.subsystem_(\d+)\./);
|
||
const scope = motorMatch ? `电机${motorMatch[1]}-` : subsystemMatch ? `子系统${subsystemMatch[1]}-` : "";
|
||
const categoryName = categoryNames[category] ?? "GB/T 32960 扩展数据";
|
||
const label = fieldNames[suffix];
|
||
if (!label) throw new Error(`missing Chinese field mapping for ${key}`);
|
||
const unit = unitBySuffix[suffix] ?? "";
|
||
return { category, categoryName, suffix, label: `${categoryName}-${scope}${label}${unit ? `(${unit})` : ""}`, unit };
|
||
}
|
||
|
||
function displayValue(key, value) {
|
||
if (value == null || value === "") return null;
|
||
const raw = String(value);
|
||
const mapped = valueMappings[key]?.[raw];
|
||
if (mapped) return `${mapped}(${raw})`;
|
||
if (/^0x[0-9a-f]+$/i.test(raw)) return `十六进制 ${raw}`;
|
||
if (/^-?\d+(?:\.\d+)?$/.test(raw) && !key.endsWith(".vin")) return Number(raw);
|
||
return raw;
|
||
}
|
||
|
||
function mappingText(key) {
|
||
const mapping = valueMappings[key];
|
||
return mapping ? Object.entries(mapping).map(([value, label]) => `${value}=${label}`).join(";") : "";
|
||
}
|
||
|
||
function columnName(index) {
|
||
let value = index + 1;
|
||
let out = "";
|
||
while (value > 0) {
|
||
const remainder = (value - 1) % 26;
|
||
out = String.fromCharCode(65 + remainder) + out;
|
||
value = Math.floor((value - 1) / 26);
|
||
}
|
||
return out;
|
||
}
|
||
|
||
const fieldKeys = data.fieldKeys;
|
||
const mappedFields = fieldKeys.map((key) => ({ key, ...fieldParts(key) }));
|
||
const metadata = [
|
||
["序号", (item, index) => index + 1],
|
||
["车牌", () => data.query.plate],
|
||
["VIN", (item) => item.vin],
|
||
["协议", (item) => item.protocol],
|
||
["设备时间(北京时间)", (item) => item.event_time],
|
||
["接收时间(北京时间)", (item) => item.received_at],
|
||
["存储时间(北京时间)", (item) => item.ts],
|
||
["消息ID", (item) => item.message_id],
|
||
["消息ID(十六进制)", (item) => item.message_id_hex ? `十六进制 ${item.message_id_hex}` : null],
|
||
["帧ID", (item) => item.frame_id],
|
||
["事件ID", (item) => item.event_id],
|
||
["解析状态", (item) => item.parse_status],
|
||
["解析错误", (item) => item.parse_error ?? null],
|
||
["来源端点", (item) => item.source_endpoint],
|
||
["原始报文字节数", (item) => item.raw_size_bytes],
|
||
["车辆标识", (item) => item.vehicle_key],
|
||
["手机号", (item) => item.phone ?? null],
|
||
["设备ID", (item) => item.device_id ?? null],
|
||
["原始文本", (item) => item.raw_text ?? null],
|
||
["原始报文(十六进制)", (item) => item.raw_hex ?? null],
|
||
];
|
||
|
||
const workbook = Workbook.create();
|
||
const summary = workbook.worksheets.add("导出说明");
|
||
const detail = workbook.worksheets.add("完整数据");
|
||
const mapping = workbook.worksheets.add("字段映射");
|
||
|
||
summary.showGridLines = false;
|
||
summary.getRange("A1:F1").merge();
|
||
summary.getRange("A1").values = [[`${data.query.plate} · ${exportDate} · GB/T 32960 原始数据导出`]];
|
||
summary.getRange("A1:F1").format = { fill: "#163A5F", font: { bold: true, color: "#FFFFFF", size: 16 }, verticalAlignment: "center" };
|
||
summary.getRange("A1:F1").format.rowHeight = 32;
|
||
summary.getRange("A3:B15").values = [
|
||
["车牌", data.query.plate],
|
||
["VIN", data.query.vin],
|
||
["协议", data.query.protocol],
|
||
["查询日期", `${exportDate}(北京时间)`],
|
||
["查询时间窗", `${data.query.dateFrom} 至 ${data.query.dateTo}`],
|
||
["接口统计总帧数", data.verification.apiTotal],
|
||
["实际导出帧数", data.verification.fetchedRows],
|
||
["唯一帧ID数", data.verification.uniqueFrameIds],
|
||
["唯一事件ID数", data.verification.uniqueEventIds],
|
||
["首条存储时间", data.verification.earliestTs],
|
||
["末条存储时间", data.verification.latestTs],
|
||
["解析字段数", data.verification.parsedFieldCount],
|
||
["消息类型统计", Object.entries(data.verification.messageCounts).map(([key, count]) => `${key}: ${count}帧`).join(";")],
|
||
];
|
||
summary.getRange("A3:A15").format = { fill: "#DCE8F2", font: { bold: true, color: "#163A5F" } };
|
||
summary.getRange("A3:B15").format.borders = { preset: "inside", style: "thin", color: "#C8D4E0" };
|
||
summary.getRange("A17:F17").merge();
|
||
summary.getRange("A17").values = [["口径说明"]];
|
||
summary.getRange("A17:F17").format = { fill: "#2B6F91", font: { bold: true, color: "#FFFFFF" } };
|
||
summary.getRange("A18:F22").merge(true);
|
||
summary.getRange("A18:F22").values = [
|
||
[`1. 数据源为生产实时 API 的 GB32960 原始帧历史;导出保留原始报文十六进制、帧/事件标识、解析状态及全部 ${data.verification.parsedFieldCount} 个已出现的解析字段。`],
|
||
["2. 完整数据工作表使用中文字段名;字段映射工作表保留原始字段键、单位及状态值映射,便于审计和程序回溯。"],
|
||
["3. 状态类字段显示为“中文含义(协议值)”;普通测量值保持数值,列表/位图/十六进制载荷保持原始文本。"],
|
||
[`4. 查询边界为北京时间 ${data.query.dateFrom} 至 ${data.query.dateTo};该车辆实际存储时间覆盖 ${data.verification.earliestTs} 至 ${data.verification.latestTs}。`],
|
||
["5. 中文术语参考:项目 GB/T 32960 字段展示基准及【字段】32960+广东燃料电池协议.json(SHA-256: 0f0847b86cc692f2130255babbee37e6cb3cf36d717b18d6698164289f66eb82)。"],
|
||
];
|
||
summary.getRange("A18:F22").format = { wrapText: true, verticalAlignment: "top", font: { color: "#334155", size: 10 } };
|
||
summary.getRange("A18:F22").format.rowHeight = 34;
|
||
summary.getRange("A:A").format.columnWidth = 22;
|
||
summary.getRange("B:F").format.columnWidth = 18;
|
||
summary.getRange("B:B").format.columnWidth = 78;
|
||
|
||
const detailHeaders = [...metadata.map(([label]) => label), ...mappedFields.map((field) => field.label)];
|
||
const detailRows = data.items.map((item, index) => [
|
||
...metadata.map(([, getter]) => getter(item, index)),
|
||
...mappedFields.map(({ key }) => displayValue(key, item.parsed_fields?.[key])),
|
||
]);
|
||
const detailMatrix = [detailHeaders, ...detailRows];
|
||
const detailEndColumn = columnName(detailHeaders.length - 1);
|
||
detail.getRangeByIndexes(0, 0, detailMatrix.length, detailHeaders.length).values = detailMatrix;
|
||
detail.showGridLines = false;
|
||
detail.freezePanes.freezeRows(1);
|
||
detail.freezePanes.freezeColumns(3);
|
||
const dataTable = detail.tables.add(`A1:${detailEndColumn}${detailMatrix.length}`, true, "GB32960DataTable");
|
||
dataTable.style = "TableStyleMedium2";
|
||
dataTable.showFilterButton = true;
|
||
dataTable.showBandedColumns = false;
|
||
detail.getRangeByIndexes(0, 0, 1, detailHeaders.length).format = {
|
||
fill: "#1F4E78", font: { bold: true, color: "#FFFFFF", size: 9 }, wrapText: true,
|
||
horizontalAlignment: "center", verticalAlignment: "center",
|
||
};
|
||
detail.getRangeByIndexes(0, 0, 1, detailHeaders.length).format.rowHeight = 48;
|
||
detail.getRangeByIndexes(1, 0, detailRows.length, detailHeaders.length).format.font = { size: 9, color: "#1F2937" };
|
||
detail.getRangeByIndexes(0, 0, detailMatrix.length, 1).format.columnWidth = 8;
|
||
detail.getRangeByIndexes(0, 1, detailMatrix.length, 1).format.columnWidth = 13;
|
||
detail.getRangeByIndexes(0, 2, detailMatrix.length, 1).format.columnWidth = 22;
|
||
for (const col of [3, 7, 8, 11, 14]) detail.getRangeByIndexes(0, col, detailMatrix.length, 1).format.columnWidth = 14;
|
||
for (const col of [4, 5, 6]) detail.getRangeByIndexes(0, col, detailMatrix.length, 1).format.columnWidth = 21;
|
||
for (const col of [9, 10]) detail.getRangeByIndexes(0, col, detailMatrix.length, 1).format.columnWidth = 36;
|
||
for (const col of [12, 13, 15, 16, 17]) detail.getRangeByIndexes(0, col, detailMatrix.length, 1).format.columnWidth = 20;
|
||
detail.getRangeByIndexes(0, 18, detailMatrix.length, 1).format.columnWidth = 28;
|
||
detail.getRangeByIndexes(0, 19, detailMatrix.length, 1).format.columnWidth = 44;
|
||
for (let col = metadata.length; col < detailHeaders.length; col += 1) {
|
||
const key = mappedFields[col - metadata.length].key;
|
||
const wide = key.includes("voltages_v") || key.endsWith("faults") || key.endsWith("payload_hex");
|
||
detail.getRangeByIndexes(0, col, detailMatrix.length, 1).format.columnWidth = wide ? 38 : 18;
|
||
}
|
||
detail.getRangeByIndexes(1, 0, detailRows.length, 1).format.numberFormat = "0";
|
||
detail.getRangeByIndexes(1, 7, detailRows.length, 1).format.numberFormat = "0";
|
||
detail.getRangeByIndexes(1, 14, detailRows.length, 1).format.numberFormat = "0";
|
||
|
||
mapping.showGridLines = false;
|
||
mapping.freezePanes.freezeRows(1);
|
||
const mappingHeaders = ["序号", "中文字段名", "原始字段键", "数据类别", "单位", "状态值映射", "说明"];
|
||
const mappingRows = mappedFields.map((field, index) => [
|
||
index + 1,
|
||
field.label,
|
||
field.key,
|
||
field.categoryName,
|
||
field.unit,
|
||
mappingText(field.key),
|
||
valueMappings[field.key] ? "完整数据中显示中文含义并保留括号内协议值。" : "值按协议解析结果原样导出;数值字段保持数值类型。",
|
||
]);
|
||
mapping.getRangeByIndexes(0, 0, mappingRows.length + 1, mappingHeaders.length).values = [mappingHeaders, ...mappingRows];
|
||
const mappingTable = mapping.tables.add(`A1:G${mappingRows.length + 1}`, true, "GB32960FieldMappingTable");
|
||
mappingTable.style = "TableStyleMedium2";
|
||
mapping.getRange("A1:G1").format = { fill: "#1F4E78", font: { bold: true, color: "#FFFFFF" }, wrapText: true, horizontalAlignment: "center" };
|
||
mapping.getRange("A1:G1").format.rowHeight = 30;
|
||
mapping.getRange("A:A").format.columnWidth = 8;
|
||
mapping.getRange("B:B").format.columnWidth = 42;
|
||
mapping.getRange("C:C").format.columnWidth = 58;
|
||
mapping.getRange("D:D").format.columnWidth = 30;
|
||
mapping.getRange("E:E").format.columnWidth = 12;
|
||
mapping.getRange("F:F").format.columnWidth = 68;
|
||
mapping.getRange("G:G").format.columnWidth = 48;
|
||
mapping.getRange(`B2:G${mappingRows.length + 1}`).format.wrapText = true;
|
||
|
||
await fs.mkdir(outputDir, { recursive: true });
|
||
|
||
const summaryInspect = await workbook.inspect({ kind: "table", range: "导出说明!A1:F22", include: "values,formulas", tableMaxRows: 24, tableMaxCols: 8 });
|
||
const detailInspect = await workbook.inspect({ kind: "table", range: "完整数据!A1:Z8", include: "values,formulas", tableMaxRows: 8, tableMaxCols: 26 });
|
||
const mappingInspect = await workbook.inspect({ kind: "table", range: "字段映射!A1:G16", include: "values,formulas", tableMaxRows: 16, tableMaxCols: 8 });
|
||
const errors = await workbook.inspect({ kind: "match", searchTerm: "#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A", options: { useRegex: true, maxResults: 300 }, summary: "final formula error scan" });
|
||
await fs.writeFile(path.join(taskDir, "summary-inspect.ndjson"), summaryInspect.ndjson, "utf8");
|
||
await fs.writeFile(path.join(taskDir, "detail-inspect.ndjson"), detailInspect.ndjson, "utf8");
|
||
await fs.writeFile(path.join(taskDir, "mapping-inspect.ndjson"), mappingInspect.ndjson, "utf8");
|
||
await fs.writeFile(path.join(taskDir, "formula-errors.ndjson"), errors.ndjson, "utf8");
|
||
|
||
for (const [sheetName, range, filename] of [
|
||
["导出说明", "A1:F22", "preview-summary.png"],
|
||
["完整数据", "A1:T18", "preview-detail.png"],
|
||
["字段映射", "A1:G28", "preview-mapping.png"],
|
||
]) {
|
||
const preview = await workbook.render({ sheetName, range, scale: 1.25, format: "png" });
|
||
await fs.writeFile(path.join(taskDir, filename), new Uint8Array(await preview.arrayBuffer()));
|
||
}
|
||
|
||
const xlsx = await SpreadsheetFile.exportXlsx(workbook);
|
||
await xlsx.save(outputPath);
|
||
console.log(JSON.stringify({ outputPath, sheets: ["导出说明", "完整数据", "字段映射"], rows: detailRows.length, parsedFields: mappedFields.length, columns: detailHeaders.length }, null, 2));
|