feat(mileage): add daily mileage columns to main export sheet
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

- '车辆汇总' sheet now includes per-day mileage columns for date ranges
- Daily columns shown as 'MM-DD里程(km)' between base info and summary cols
- Freeze pane fixed at 7 base columns for horizontal scrolling
- '每日明细' sheet retained as detailed backup
- Bump version to 1.1.9
This commit is contained in:
kkfluous
2026-07-15 14:30:16 +08:00
parent 2b31f8efe8
commit 4d523cbce0
2 changed files with 48 additions and 26 deletions

View File

@@ -1,7 +1,7 @@
{ {
"name": "ln-bi", "name": "ln-bi",
"private": true, "private": true,
"version": "1.1.8", "version": "1.1.9",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "concurrently -n server,client -c blue,green \"npm run dev:server\" \"npm run dev:client\"", "dev": "concurrently -n server,client -c blue,green \"npm run dev:server\" \"npm run dev:client\"",

View File

@@ -8,9 +8,9 @@ interface ExportContext {
sortBy: 'today' | 'total'; sortBy: 'today' | 'total';
} }
const HEADERS = [ const BASE_HEADERS = [
'状态', '车牌号', '客户', '业务部门', '项目', '租赁状态', '状态', '车牌号', '客户', '业务部门', '项目', '租赁状态',
'运营区域', '区间里程(km)', '累计里程(km)', '运营区域',
] as const; ] as const;
function statusLabel(v: MonitoringVehicle): string { function statusLabel(v: MonitoringVehicle): string {
@@ -20,7 +20,6 @@ function statusLabel(v: MonitoringVehicle): string {
function mileageCell(v: MonitoringVehicle, kind: 'today' | 'total'): string | number { function mileageCell(v: MonitoringVehicle, kind: 'today' | 'total'): string | number {
if (kind === 'today') { if (kind === 'today') {
// 区间内未对接但有历史累计,视作区间 0只有完全无数据才标「未对接」。
if (!v.isDataSynced && v.totalKm == null) return '未对接'; if (!v.isDataSynced && v.totalKm == null) return '未对接';
return Math.max(0, v.dailyKm || 0); return Math.max(0, v.dailyKm || 0);
} }
@@ -31,9 +30,24 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont
const start = ctx.startDate || ctx.date || ''; const start = ctx.startDate || ctx.date || '';
const end = ctx.endDate || ctx.date || ''; const end = ctx.endDate || ctx.date || '';
const isRange = !!start && !!end && start !== end; const isRange = !!start && !!end && start !== end;
const dayKeys = Array.from(
new Set(vehicles.flatMap(v => Object.keys(v.dailyMileage || {})))
).sort();
// 车辆汇总 sheet基础列 + 每日里程列 + 区间合计 + 累计里程
const dailyHeaders = dayKeys.map(day => `${day.slice(5)}里程(km)`);
const summaryHeaders = [
...BASE_HEADERS,
...(dayKeys.length > 0 ? dailyHeaders : []),
isRange ? '区间合计(km)' : '当日里程(km)',
'累计里程(km)',
];
const summaryData: (string | number)[][] = [ const summaryData: (string | number)[][] = [
[...HEADERS], summaryHeaders,
...vehicles.map(v => [ ...vehicles.map(v => {
const baseRow = [
statusLabel(v), statusLabel(v),
v.plate, v.plate,
v.customer || '', v.customer || '',
@@ -41,14 +55,21 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont
v.project || '', v.project || '',
v.rentStatus || '', v.rentStatus || '',
v.region || '', v.region || '',
];
const dailyCols = dayKeys.map(day => v.dailyMileage?.[day] ?? 0);
return [
...baseRow,
...dailyCols,
mileageCell(v, 'today'), mileageCell(v, 'today'),
mileageCell(v, 'total'), mileageCell(v, 'total'),
]), ];
}),
]; ];
const ws = XLSX.utils.aoa_to_sheet(summaryData); const ws = XLSX.utils.aoa_to_sheet(summaryData);
ws['!cols'] = [ const numFixedCols = BASE_HEADERS.length;
const wsCols: { wch: number }[] = [
{ wch: 8 }, // 状态 { wch: 8 }, // 状态
{ wch: 12 }, // 车牌号 { wch: 12 }, // 车牌号
{ wch: 28 }, // 客户 { wch: 28 }, // 客户
@@ -56,21 +77,24 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont
{ wch: 16 }, // 项目 { wch: 16 }, // 项目
{ wch: 10 }, // 租赁状态 { wch: 10 }, // 租赁状态
{ wch: 12 }, // 运营区域 { wch: 12 }, // 运营区域
{ wch: 14 }, // 区间里程
{ wch: 14 }, // 累计里程
]; ];
for (let i = 0; i < dayKeys.length; i++) wsCols.push({ wch: 12 });
wsCols.push({ wch: 14 }); // 区间合计
wsCols.push({ wch: 14 }); // 累计里程
ws['!cols'] = wsCols;
ws['!freeze'] = { xSplit: 0, ySplit: 1 } as never; // 冻结首行 + 固定列
ws['!freeze'] = { xSplit: numFixedCols, ySplit: 1 } as never;
for (let r = 1; r < summaryData.length; r++) { for (let r = 1; r < summaryData.length; r++) {
for (const c of [7, 8]) { for (let c = numFixedCols; c < summaryHeaders.length; c++) {
const ref = XLSX.utils.encode_cell({ r, c }); const ref = XLSX.utils.encode_cell({ r, c });
if (ws[ref]?.t === 'n') ws[ref].z = '0.##########'; if (ws[ref]?.t === 'n') ws[ref].z = '0.##########';
} }
} }
// 表头样式(在客户端 SheetJS 社区版仅基本样式生效) // 表头样式
for (let c = 0; c < HEADERS.length; c++) { for (let c = 0; c < summaryHeaders.length; c++) {
const ref = XLSX.utils.encode_cell({ r: 0, c }); const ref = XLSX.utils.encode_cell({ r: 0, c });
if (ws[ref]) { if (ws[ref]) {
(ws[ref] as { s?: unknown }).s = { (ws[ref] as { s?: unknown }).s = {
@@ -84,9 +108,7 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont
const wb = XLSX.utils.book_new(); const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '车辆汇总'); XLSX.utils.book_append_sheet(wb, ws, '车辆汇总');
const dayKeys = Array.from( // 每日明细 sheet保留原有格式
new Set(vehicles.flatMap(v => Object.keys(v.dailyMileage || {})))
).sort();
if (dayKeys.length > 0) { if (dayKeys.length > 0) {
const detailHeaders = [ const detailHeaders = [
'车牌号', '客户', '业务部门', '项目', '租赁状态', '运营区域', '车牌号', '客户', '业务部门', '项目', '租赁状态', '运营区域',