chore: snapshot production code before Apple Design UI refinement
This commit is contained in:
@@ -0,0 +1,638 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { SpreadsheetFile, Workbook } from "@oai/artifact-tool";
|
||||
|
||||
const workspaceRoot = "/Users/lingniu/project/ai-coding/lingniu-vehicle-ingest";
|
||||
const workDir = path.join(
|
||||
workspaceRoot,
|
||||
"tmp/vehicle-mileage-selection-019fa1ac-db39-7933-bb0e-30dd63cc25bd",
|
||||
);
|
||||
const outputDir = path.join(
|
||||
workspaceRoot,
|
||||
"outputs/019fa1ac-db39-7933-bb0e-30dd63cc25bd",
|
||||
);
|
||||
const outputPath = path.join(
|
||||
outputDir,
|
||||
"江浙沪四类车型近三个月GPS里程清单_20260427-20260726.xlsx",
|
||||
);
|
||||
const masterPath = path.join(
|
||||
workspaceRoot,
|
||||
"outputs/jt808-provider-audit-20260717/oneos-vehicle-master.json",
|
||||
);
|
||||
|
||||
const dateFrom = "2026-04-27";
|
||||
const dateTo = "2026-07-26";
|
||||
const periodDays = 91;
|
||||
const queryAsOf = "2026-07-28";
|
||||
const mileageProtocol = "JT808";
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
function postProductionAPI(endpoint, body) {
|
||||
const remoteCommand =
|
||||
`. /opt/lingniu-vehicle-platform/env/access-tokens.env; ` +
|
||||
`curl -fsS -H "Authorization: Bearer $ADMIN_TOKEN" ` +
|
||||
`-H "Content-Type: application/json" --data-binary @- ` +
|
||||
`http://127.0.0.1:20300${endpoint}`;
|
||||
const result = spawnSync(
|
||||
"ssh",
|
||||
["-o", "BatchMode=yes", "root@115.29.187.205", remoteCommand],
|
||||
{
|
||||
input: JSON.stringify(body),
|
||||
encoding: "utf8",
|
||||
maxBuffer: 64 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`Production API failed for ${endpoint}: ${result.stderr || result.stdout}`,
|
||||
);
|
||||
}
|
||||
const payload = JSON.parse(result.stdout);
|
||||
if (!payload.data) {
|
||||
throw new Error(`Production API returned no data for ${endpoint}`);
|
||||
}
|
||||
return payload.data;
|
||||
}
|
||||
|
||||
function excelDate(isoDate) {
|
||||
return new Date(`${isoDate}T00:00:00+08:00`);
|
||||
}
|
||||
|
||||
function datesBetween(start, end) {
|
||||
const result = [];
|
||||
const cursor = new Date(`${start}T00:00:00+08:00`);
|
||||
const last = new Date(`${end}T00:00:00+08:00`);
|
||||
while (cursor <= last) {
|
||||
result.push(cursor.toLocaleDateString("en-CA", { timeZone: "Asia/Shanghai" }));
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function residentLocation(rawCity) {
|
||||
const parts = String(rawCity || "").split("-");
|
||||
const province = parts[0] || "";
|
||||
const city = parts.slice(1).join("-") || province;
|
||||
return { province, city };
|
||||
}
|
||||
|
||||
function vehicleCategory(model) {
|
||||
const normalized = String(model || "");
|
||||
if (normalized.includes("4.5吨") && !normalized.includes("冷链")) {
|
||||
return "4.5T普货";
|
||||
}
|
||||
if (normalized.includes("冷链")) return "冷链";
|
||||
if (normalized.includes("18吨")) return "18T";
|
||||
if (normalized.includes("49吨")) return "49T";
|
||||
return "";
|
||||
}
|
||||
|
||||
function isJiangZheHu(city) {
|
||||
return ["江苏省-", "浙江省-", "上海市-"].some((prefix) =>
|
||||
String(city || "").startsWith(prefix),
|
||||
);
|
||||
}
|
||||
|
||||
async function loadMasterVehicles() {
|
||||
const source = JSON.parse(await fs.readFile(masterPath, "utf8"));
|
||||
const rows = source.sheets?.[0]?.rows || [];
|
||||
assert(rows.length > 1, "OneOS vehicle master is empty");
|
||||
const headers = rows[0].values;
|
||||
const headerIndex = Object.fromEntries(headers.map((value, index) => [value, index]));
|
||||
const requiredHeaders = [
|
||||
"车牌号",
|
||||
"VIN",
|
||||
"运营城市",
|
||||
"品牌",
|
||||
"型号",
|
||||
"运营状态",
|
||||
"车辆状态",
|
||||
"客户名称",
|
||||
];
|
||||
for (const header of requiredHeaders) {
|
||||
assert(headerIndex[header] !== undefined, `Missing master header: ${header}`);
|
||||
}
|
||||
const result = [];
|
||||
for (const row of rows.slice(1)) {
|
||||
const values = [...row.values, ...Array(headers.length).fill("")];
|
||||
const cityRaw = values[headerIndex["运营城市"]];
|
||||
const model = values[headerIndex["型号"]];
|
||||
const category = vehicleCategory(model);
|
||||
if (!category || !isJiangZheHu(cityRaw)) continue;
|
||||
const vin = String(values[headerIndex["VIN"]] || "").trim();
|
||||
const plate = String(values[headerIndex["车牌号"]] || "").trim();
|
||||
if (!vin || !plate) continue;
|
||||
const location = residentLocation(cityRaw);
|
||||
result.push({
|
||||
category,
|
||||
plate,
|
||||
vin,
|
||||
brand: String(values[headerIndex["品牌"]] || "").trim(),
|
||||
model: String(model || "").trim(),
|
||||
province: location.province,
|
||||
city: location.city,
|
||||
cityRaw: String(cityRaw || "").trim(),
|
||||
operationStatus: String(values[headerIndex["运营状态"]] || "").trim(),
|
||||
vehicleStatus: String(values[headerIndex["车辆状态"]] || "").trim(),
|
||||
customerName: ["租赁", "自营"].includes(
|
||||
String(values[headerIndex["运营状态"]] || "").trim(),
|
||||
)
|
||||
? String(values[headerIndex["客户名称"]] || "").trim()
|
||||
: "",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function queryRanking(vins) {
|
||||
if (vins.length === 0) return { ranking: [], asOf: "" };
|
||||
return postProductionAPI("/api/v2/statistics/mileage", {
|
||||
dateFrom,
|
||||
dateTo,
|
||||
vins,
|
||||
protocol: mileageProtocol,
|
||||
});
|
||||
}
|
||||
|
||||
function rankingMap(data) {
|
||||
return new Map((data.ranking || []).map((row) => [row.vin, row]));
|
||||
}
|
||||
|
||||
function selectCategoryVehicles(category, candidates) {
|
||||
const categoryCandidates = candidates.filter((row) => row.category === category);
|
||||
assert(
|
||||
categoryCandidates.length >= 10,
|
||||
`${category} has only ${categoryCandidates.length} Jiang-Zhe-Hu candidates`,
|
||||
);
|
||||
const masterByVIN = new Map(categoryCandidates.map((row) => [row.vin, row]));
|
||||
const suzhouVINs = categoryCandidates
|
||||
.filter((row) => row.city === "苏州市")
|
||||
.map((row) => row.vin);
|
||||
const otherVINs = categoryCandidates
|
||||
.filter((row) => row.city !== "苏州市")
|
||||
.map((row) => row.vin);
|
||||
const suzhouRanking = queryRanking(suzhouVINs);
|
||||
const otherRanking = queryRanking(otherVINs);
|
||||
const combined = [
|
||||
...(suzhouRanking.ranking || []).map((row) => ({
|
||||
...row,
|
||||
suzhouPreferred: true,
|
||||
})),
|
||||
...(otherRanking.ranking || []).map((row) => ({
|
||||
...row,
|
||||
suzhouPreferred: false,
|
||||
})),
|
||||
];
|
||||
assert(combined.length >= 10, `${category} has fewer than 10 vehicles with mileage`);
|
||||
return combined.slice(0, 10).map((ranked, index) => ({
|
||||
...masterByVIN.get(ranked.vin),
|
||||
rank: index + 1,
|
||||
sourcePeriodMileageKm: Number(ranked.mileageKm || 0),
|
||||
sourceActiveDays: Number(ranked.activeDays || 0),
|
||||
suzhouPreferred: ranked.suzhouPreferred,
|
||||
statisticsAsOf: suzhouRanking.asOf || otherRanking.asOf || "",
|
||||
}));
|
||||
}
|
||||
|
||||
function queryDailyMileage(vins) {
|
||||
const items = [];
|
||||
let offset = 0;
|
||||
const limit = 1000;
|
||||
let total = Number.POSITIVE_INFINITY;
|
||||
while (offset < total) {
|
||||
const page = postProductionAPI("/api/mileage/daily", {
|
||||
dateFrom,
|
||||
dateTo,
|
||||
vins,
|
||||
protocol: mileageProtocol,
|
||||
deduplicate: true,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
const pageItems = page.items || [];
|
||||
items.push(...pageItems);
|
||||
total = Number(page.total || 0);
|
||||
offset += pageItems.length;
|
||||
if (pageItems.length === 0) break;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function styleTitle(sheet, rangeAddress, title) {
|
||||
const range = sheet.getRange(rangeAddress);
|
||||
range.merge();
|
||||
range.values = [[title]];
|
||||
range.format = {
|
||||
fill: "#0F4C5C",
|
||||
font: { bold: true, color: "#FFFFFF", size: 16 },
|
||||
verticalAlignment: "center",
|
||||
horizontalAlignment: "left",
|
||||
};
|
||||
range.format.rowHeight = 34;
|
||||
}
|
||||
|
||||
function styleHeader(range) {
|
||||
range.format = {
|
||||
fill: "#DCEEF2",
|
||||
font: { bold: true, color: "#12343B" },
|
||||
verticalAlignment: "center",
|
||||
horizontalAlignment: "center",
|
||||
wrapText: true,
|
||||
borders: {
|
||||
bottom: { style: "medium", color: "#78A7B2" },
|
||||
},
|
||||
};
|
||||
range.format.rowHeight = 30;
|
||||
}
|
||||
|
||||
function styleNote(range) {
|
||||
range.format = {
|
||||
fill: "#F3F8FA",
|
||||
font: { color: "#385A64", size: 10 },
|
||||
wrapText: true,
|
||||
verticalAlignment: "center",
|
||||
};
|
||||
}
|
||||
|
||||
function addCategoryConditionalFormatting(range) {
|
||||
range.conditionalFormats.addCustom('=$A5="4.5T普货"', {
|
||||
fill: "#FFF4CC",
|
||||
font: { bold: true, color: "#7A5300" },
|
||||
});
|
||||
range.conditionalFormats.addCustom('=$A5="冷链"', {
|
||||
fill: "#DDF3FF",
|
||||
font: { bold: true, color: "#075985" },
|
||||
});
|
||||
range.conditionalFormats.addCustom('=$A5="18T"', {
|
||||
fill: "#E8E1FF",
|
||||
font: { bold: true, color: "#5B21B6" },
|
||||
});
|
||||
range.conditionalFormats.addCustom('=$A5="49T"', {
|
||||
fill: "#DFF5E6",
|
||||
font: { bold: true, color: "#166534" },
|
||||
});
|
||||
}
|
||||
|
||||
const masterVehicles = await loadMasterVehicles();
|
||||
const categories = ["4.5T普货", "冷链", "18T", "49T"];
|
||||
const selectedVehicles = categories.flatMap((category) =>
|
||||
selectCategoryVehicles(category, masterVehicles),
|
||||
);
|
||||
assert(selectedVehicles.length === 40, "Expected exactly 40 selected vehicles");
|
||||
assert(
|
||||
new Set(selectedVehicles.map((row) => row.vin)).size === 40,
|
||||
"Selected VINs are not unique",
|
||||
);
|
||||
|
||||
const dailySourceRows = queryDailyMileage(selectedVehicles.map((row) => row.vin));
|
||||
const dailyByVehicleDate = new Map();
|
||||
for (const row of dailySourceRows) {
|
||||
assert(
|
||||
row.source === mileageProtocol,
|
||||
`Unexpected mileage source for ${row.vin} on ${row.date}: ${row.source}`,
|
||||
);
|
||||
const key = `${row.vin}|${row.date}`;
|
||||
assert(!dailyByVehicleDate.has(key), `Duplicate deduplicated daily row: ${key}`);
|
||||
dailyByVehicleDate.set(key, row);
|
||||
}
|
||||
|
||||
const dates = datesBetween(dateFrom, dateTo);
|
||||
assert(dates.length === periodDays, `Expected ${periodDays} dates, got ${dates.length}`);
|
||||
|
||||
const dailyRows = [];
|
||||
for (const vehicle of selectedVehicles) {
|
||||
for (const date of dates) {
|
||||
const source = dailyByVehicleDate.get(`${vehicle.vin}|${date}`);
|
||||
dailyRows.push({
|
||||
date,
|
||||
...vehicle,
|
||||
dailyMileageKm: source ? Number(source.dailyMileageKm || 0) : null,
|
||||
source: source?.source || "",
|
||||
recordStatus: source ? "有记录" : "缺失",
|
||||
});
|
||||
}
|
||||
}
|
||||
assert(dailyRows.length === 40 * periodDays, "Daily row count mismatch");
|
||||
|
||||
for (const vehicle of selectedVehicles) {
|
||||
const computed = dailyRows
|
||||
.filter((row) => row.vin === vehicle.vin)
|
||||
.reduce((sum, row) => sum + (row.dailyMileageKm ?? 0), 0);
|
||||
const difference = Math.abs(computed - vehicle.sourcePeriodMileageKm);
|
||||
assert(
|
||||
difference < 0.02,
|
||||
`${vehicle.plate} total mismatch: daily=${computed}, stats=${vehicle.sourcePeriodMileageKm}`,
|
||||
);
|
||||
}
|
||||
|
||||
const workbook = Workbook.create();
|
||||
const notesSheet = workbook.worksheets.add("口径说明");
|
||||
const summarySheet = workbook.worksheets.add("车辆汇总");
|
||||
const dailySheet = workbook.worksheets.add("每日里程");
|
||||
|
||||
for (const sheet of [notesSheet, summarySheet, dailySheet]) {
|
||||
sheet.showGridLines = false;
|
||||
}
|
||||
|
||||
// 口径说明
|
||||
styleTitle(notesSheet, "A1:H1", "江浙沪四类车型近三个月 GPS 里程清单");
|
||||
notesSheet.getRange("A3:B11").values = [
|
||||
["项目", "口径"],
|
||||
["统计区间", `${dateFrom} 至 ${dateTo}(${periodDays} 个自然日,完整日)`],
|
||||
["区域范围", "常驻市(OneOS 运营城市)属于江苏、浙江、上海"],
|
||||
["选车规则", "每类先按苏州区间总里程降序选取,不足 10 辆时按江浙沪其他城市区间总里程降序补齐"],
|
||||
["车型定义", "4.5T普货=型号含4.5吨且不含冷链;冷链=型号含冷链;18T=型号含18吨;49T=型号含49吨"],
|
||||
["里程口径", "仅使用 JT808 定位终端/GPS 侧累计里程计算,不使用 GB32960、宇通 MQTT 等仪表/车端累计里程"],
|
||||
["缺失处理", "逐日表保留全部车辆×日期组合;无生产记录的日期留空,并标记为“缺失”,不擅自按0计算"],
|
||||
["主数据快照", "OneOS车辆信息快照:2026-07-17;常驻市取“运营城市”字段"],
|
||||
["里程数据截至", `${queryAsOf} 查询,统计截止至 ${dateTo};来源协议固定为 ${mileageProtocol}`],
|
||||
];
|
||||
styleHeader(notesSheet.getRange("A3:B3"));
|
||||
styleNote(notesSheet.getRange("A4:B11"));
|
||||
notesSheet.getRange("A4:A11").format.font = { bold: true, color: "#12343B" };
|
||||
notesSheet.getRange("A3:B11").format.borders = {
|
||||
outside: { style: "thin", color: "#B9D3DA" },
|
||||
insideHorizontal: { style: "thin", color: "#DCE8EC" },
|
||||
};
|
||||
notesSheet.getRange("A13:E18").values = [
|
||||
["分类", "车辆数", "苏州优先数", "GPS区间总里程(km)", "GPS日均里程(km/天)"],
|
||||
["4.5T普货", null, null, null, null],
|
||||
["冷链", null, null, null, null],
|
||||
["18T", null, null, null, null],
|
||||
["49T", null, null, null, null],
|
||||
["合计", null, null, null, null],
|
||||
];
|
||||
styleHeader(notesSheet.getRange("A13:E13"));
|
||||
notesSheet.getRange("B14").formulas = [["=COUNTIF('车辆汇总'!$A$5:$A$44,A14)"]];
|
||||
notesSheet.getRange("B14:B17").fillDown();
|
||||
notesSheet.getRange("C14").formulas = [
|
||||
["=COUNTIFS('车辆汇总'!$A$5:$A$44,A14,'车辆汇总'!$I$5:$I$44,\"苏州市\")"],
|
||||
];
|
||||
notesSheet.getRange("C14:C17").fillDown();
|
||||
notesSheet.getRange("D14").formulas = [
|
||||
["=SUMIF('车辆汇总'!$A$5:$A$44,A14,'车辆汇总'!$O$5:$O$44)"],
|
||||
];
|
||||
notesSheet.getRange("D14:D17").fillDown();
|
||||
notesSheet.getRange("E14").formulas = [["=D14/(B14*'车辆汇总'!$N$5)"]];
|
||||
notesSheet.getRange("E14:E17").fillDown();
|
||||
notesSheet.getRange("B18").formulas = [["=SUM(B14:B17)"]];
|
||||
notesSheet.getRange("C18").formulas = [["=SUM(C14:C17)"]];
|
||||
notesSheet.getRange("D18").formulas = [["=SUM(D14:D17)"]];
|
||||
notesSheet.getRange("E18").formulas = [["=D18/(B18*'车辆汇总'!$N$5)"]];
|
||||
notesSheet.getRange("D14:E18").format.numberFormat = "#,##0.0";
|
||||
notesSheet.getRange("A18:E18").format = {
|
||||
fill: "#E8F1F4",
|
||||
font: { bold: true, color: "#12343B" },
|
||||
borders: { top: { style: "double", color: "#78A7B2" } },
|
||||
};
|
||||
notesSheet.getRange("A13:E18").format.borders = {
|
||||
outside: { style: "thin", color: "#B9D3DA" },
|
||||
insideHorizontal: { style: "thin", color: "#DCE8EC" },
|
||||
};
|
||||
notesSheet.getRange("A:A").format.columnWidth = 18;
|
||||
notesSheet.getRange("B:B").format.columnWidth = 64;
|
||||
notesSheet.getRange("C:E").format.columnWidth = 18;
|
||||
notesSheet.getRange("A4:B11").format.rowHeight = 34;
|
||||
notesSheet.freezePanes.freezeRows(3);
|
||||
|
||||
// 车辆汇总
|
||||
styleTitle(summarySheet, "A1:Q1", "四类车型各10辆|近三个月 GPS 里程汇总");
|
||||
summarySheet.getRange("A2:Q2").merge();
|
||||
summarySheet.getRange("A2").values = [[
|
||||
`范围:江浙沪常驻车辆;苏州优先;统计区间 ${dateFrom} 至 ${dateTo}。仅使用 JT808 GPS 里程,区间总里程由“每日里程”明细公式汇总。`,
|
||||
]];
|
||||
styleNote(summarySheet.getRange("A2:Q2"));
|
||||
const summaryHeaders = [
|
||||
"分类",
|
||||
"类内排名",
|
||||
"车牌",
|
||||
"VIN",
|
||||
"品牌",
|
||||
"型号",
|
||||
"常驻省",
|
||||
"常驻城市原值",
|
||||
"常驻市",
|
||||
"苏州优先",
|
||||
"运营状态",
|
||||
"车辆状态",
|
||||
"客户名称",
|
||||
"区间天数",
|
||||
"GPS区间总里程(km)",
|
||||
"GPS日均里程(km/天)",
|
||||
"有记录天数",
|
||||
];
|
||||
summarySheet.getRange("A4:Q4").values = [summaryHeaders];
|
||||
styleHeader(summarySheet.getRange("A4:Q4"));
|
||||
const summaryValues = selectedVehicles.map((vehicle) => [
|
||||
vehicle.category,
|
||||
vehicle.rank,
|
||||
vehicle.plate,
|
||||
vehicle.vin,
|
||||
vehicle.brand,
|
||||
vehicle.model,
|
||||
vehicle.province,
|
||||
vehicle.cityRaw,
|
||||
vehicle.city,
|
||||
vehicle.suzhouPreferred ? "是" : "否",
|
||||
vehicle.operationStatus,
|
||||
vehicle.vehicleStatus,
|
||||
vehicle.customerName,
|
||||
periodDays,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
]);
|
||||
summarySheet.getRange(`A5:Q${4 + summaryValues.length}`).values = summaryValues;
|
||||
const dailyEndRow = 4 + dailyRows.length;
|
||||
summarySheet.getRange("O5").formulas = [[
|
||||
`=SUMIFS('每日里程'!$J$5:$J$${dailyEndRow},'每日里程'!$E$5:$E$${dailyEndRow},D5)`,
|
||||
]];
|
||||
summarySheet.getRange("O5:O44").fillDown();
|
||||
summarySheet.getRange("P5").formulas = [["=O5/N5"]];
|
||||
summarySheet.getRange("P5:P44").fillDown();
|
||||
summarySheet.getRange("Q5").formulas = [[
|
||||
`=COUNTIFS('每日里程'!$E$5:$E$${dailyEndRow},D5,'每日里程'!$L$5:$L$${dailyEndRow},"有记录")`,
|
||||
]];
|
||||
summarySheet.getRange("Q5:Q44").fillDown();
|
||||
summarySheet.getRange("B5:B44").format.numberFormat = "0";
|
||||
summarySheet.getRange("N5:N44").format.numberFormat = "0";
|
||||
summarySheet.getRange("O5:P44").format.numberFormat = "#,##0.0";
|
||||
summarySheet.getRange("Q5:Q44").format.numberFormat = "0";
|
||||
summarySheet.getRange("A5:Q44").format.verticalAlignment = "center";
|
||||
summarySheet.getRange("A5:Q44").format.borders = {
|
||||
insideHorizontal: { style: "thin", color: "#E2ECEF" },
|
||||
};
|
||||
addCategoryConditionalFormatting(summarySheet.getRange("A5:A44"));
|
||||
summarySheet.getRange("O5:O44").conditionalFormats.add("dataBar", {
|
||||
color: "#2A9D8F",
|
||||
gradient: true,
|
||||
});
|
||||
summarySheet.getRange("J5:J44").conditionalFormats.add("containsText", {
|
||||
text: "是",
|
||||
format: {
|
||||
fill: "#E5F7ED",
|
||||
font: { bold: true, color: "#166534" },
|
||||
},
|
||||
});
|
||||
const summaryTable = summarySheet.tables.add("A4:Q44", true, "VehicleMileageSummary");
|
||||
summaryTable.style = "TableStyleMedium2";
|
||||
summaryTable.showFilterButton = true;
|
||||
summarySheet.freezePanes.freezeRows(4);
|
||||
summarySheet.freezePanes.freezeColumns(4);
|
||||
const summaryWidths = [
|
||||
14, 10, 14, 22, 12, 30, 12, 20, 12, 10, 12, 12, 30, 10, 18, 18, 14,
|
||||
];
|
||||
summaryWidths.forEach((width, index) => {
|
||||
summarySheet.getRangeByIndexes(0, index, 44, 1).format.columnWidth = width;
|
||||
});
|
||||
summarySheet.getRange("A5:Q44").format.rowHeight = 23;
|
||||
|
||||
// 每日里程
|
||||
styleTitle(dailySheet, "A1:L1", "40辆车逐日 GPS 里程明细");
|
||||
dailySheet.getRange("A2:L2").merge();
|
||||
dailySheet.getRange("A2").values = [[
|
||||
`共 ${selectedVehicles.length} 辆 × ${periodDays} 天 = ${dailyRows.length.toLocaleString("zh-CN")} 行;仅统计 JT808 GPS 里程,缺失日期留空并单独标记。`,
|
||||
]];
|
||||
styleNote(dailySheet.getRange("A2:L2"));
|
||||
const dailyHeaders = [
|
||||
"日期",
|
||||
"分类",
|
||||
"类内排名",
|
||||
"车牌",
|
||||
"VIN",
|
||||
"品牌",
|
||||
"型号",
|
||||
"常驻市",
|
||||
"客户名称",
|
||||
"GPS每日里程(km)",
|
||||
"来源",
|
||||
"记录状态",
|
||||
];
|
||||
dailySheet.getRange("A4:L4").values = [dailyHeaders];
|
||||
styleHeader(dailySheet.getRange("A4:L4"));
|
||||
const dailyValues = dailyRows.map((row) => [
|
||||
excelDate(row.date),
|
||||
row.category,
|
||||
row.rank,
|
||||
row.plate,
|
||||
row.vin,
|
||||
row.brand,
|
||||
row.model,
|
||||
row.city,
|
||||
row.customerName,
|
||||
row.dailyMileageKm,
|
||||
row.source,
|
||||
row.recordStatus,
|
||||
]);
|
||||
dailySheet.getRange(`A5:L${dailyEndRow}`).values = dailyValues;
|
||||
dailySheet.getRange(`A5:A${dailyEndRow}`).format.numberFormat = "yyyy-mm-dd";
|
||||
dailySheet.getRange(`C5:C${dailyEndRow}`).format.numberFormat = "0";
|
||||
dailySheet.getRange(`J5:J${dailyEndRow}`).format.numberFormat = "#,##0.0";
|
||||
dailySheet.getRange(`A5:L${dailyEndRow}`).format.borders = {
|
||||
insideHorizontal: { style: "thin", color: "#EDF2F4" },
|
||||
};
|
||||
dailySheet.getRange(`L5:L${dailyEndRow}`).conditionalFormats.add("containsText", {
|
||||
text: "缺失",
|
||||
format: {
|
||||
fill: "#FFF0F0",
|
||||
font: { bold: true, color: "#B42318" },
|
||||
},
|
||||
});
|
||||
dailySheet.getRange(`J5:J${dailyEndRow}`).conditionalFormats.add("dataBar", {
|
||||
color: "#5CA4A9",
|
||||
gradient: true,
|
||||
});
|
||||
const dailyTable = dailySheet.tables.add(
|
||||
`A4:L${dailyEndRow}`,
|
||||
true,
|
||||
"VehicleDailyMileage",
|
||||
);
|
||||
dailyTable.style = "TableStyleMedium2";
|
||||
dailyTable.showFilterButton = true;
|
||||
dailySheet.freezePanes.freezeRows(4);
|
||||
dailySheet.freezePanes.freezeColumns(5);
|
||||
const dailyWidths = [14, 14, 10, 14, 22, 12, 30, 12, 30, 18, 16, 12];
|
||||
dailyWidths.forEach((width, index) => {
|
||||
dailySheet.getRangeByIndexes(0, index, dailyEndRow, 1).format.columnWidth = width;
|
||||
});
|
||||
dailySheet.getRange(`A5:L${dailyEndRow}`).format.rowHeight = 21;
|
||||
|
||||
// Compact verification before export.
|
||||
const summaryInspect = await workbook.inspect({
|
||||
kind: "table",
|
||||
range: "车辆汇总!A1:Q16",
|
||||
include: "values,formulas",
|
||||
tableMaxRows: 16,
|
||||
tableMaxCols: 17,
|
||||
maxChars: 12000,
|
||||
});
|
||||
await fs.writeFile(path.join(workDir, "summary-inspect.ndjson"), summaryInspect.ndjson);
|
||||
|
||||
const dailyInspect = await workbook.inspect({
|
||||
kind: "table",
|
||||
range: "每日里程!A1:L20",
|
||||
include: "values,formulas",
|
||||
tableMaxRows: 20,
|
||||
tableMaxCols: 12,
|
||||
maxChars: 12000,
|
||||
});
|
||||
await fs.writeFile(path.join(workDir, "daily-inspect.ndjson"), dailyInspect.ndjson);
|
||||
|
||||
const formulaErrors = await workbook.inspect({
|
||||
kind: "match",
|
||||
searchTerm: "#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A",
|
||||
options: { useRegex: true, maxResults: 300 },
|
||||
summary: "final formula error scan",
|
||||
maxChars: 12000,
|
||||
});
|
||||
await fs.writeFile(path.join(workDir, "formula-errors.ndjson"), formulaErrors.ndjson);
|
||||
|
||||
for (const [sheetName, range, fileName] of [
|
||||
["口径说明", "A1:H19", "preview-notes.png"],
|
||||
["车辆汇总", "A1:Q18", "preview-summary.png"],
|
||||
["每日里程", "A1:L26", "preview-daily.png"],
|
||||
]) {
|
||||
const preview = await workbook.render({
|
||||
sheetName,
|
||||
range,
|
||||
scale: 1.25,
|
||||
format: "png",
|
||||
});
|
||||
await fs.writeFile(
|
||||
path.join(workDir, fileName),
|
||||
new Uint8Array(await preview.arrayBuffer()),
|
||||
);
|
||||
}
|
||||
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
const output = await SpreadsheetFile.exportXlsx(workbook);
|
||||
await output.save(outputPath);
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
outputPath,
|
||||
selected: selectedVehicles.map((vehicle) => ({
|
||||
category: vehicle.category,
|
||||
rank: vehicle.rank,
|
||||
plate: vehicle.plate,
|
||||
vin: vehicle.vin,
|
||||
brand: vehicle.brand,
|
||||
model: vehicle.model,
|
||||
residentCity: vehicle.city,
|
||||
suzhouPreferred: vehicle.suzhouPreferred,
|
||||
periodMileageKm: vehicle.sourcePeriodMileageKm,
|
||||
})),
|
||||
sourceDailyRows: dailySourceRows.length,
|
||||
expandedDailyRows: dailyRows.length,
|
||||
missingDailyRows: dailyRows.filter((row) => row.recordStatus === "缺失").length,
|
||||
customerFilledVehicles: selectedVehicles.filter((row) => row.customerName).length,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user