feat: polish BI dashboards and bump version
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
lingniu
2026-06-27 21:59:33 +08:00
parent 5377d2c225
commit b0caa5afcb
33 changed files with 2363 additions and 483 deletions

View File

@@ -2,13 +2,15 @@ import * as XLSX from 'xlsx';
import type { MonitoringVehicle } from './types';
interface ExportContext {
date: string;
date?: string;
startDate?: string;
endDate?: string;
sortBy: 'today' | 'total';
}
const HEADERS = [
'状态', '车牌号', '客户', '业务部门', '项目', '租赁状态',
'运营区域', '今日里程(km)', '累计里程(km)',
'运营区域', '区间里程(km)', '累计里程(km)',
] as const;
function statusLabel(v: MonitoringVehicle): string {
@@ -18,7 +20,7 @@ function statusLabel(v: MonitoringVehicle): string {
function mileageCell(v: MonitoringVehicle, kind: 'today' | 'total'): string | number {
if (kind === 'today') {
// 当日未对接但有历史累计,视作今日 0只有完全无数据才标「未对接」
// 区间内未对接但有历史累计,视作区间 0只有完全无数据才标「未对接」
if (!v.isDataSynced && v.totalKm == null) return '未对接';
return Math.max(0, v.dailyKm || 0);
}
@@ -26,7 +28,10 @@ function mileageCell(v: MonitoringVehicle, kind: 'today' | 'total'): string | nu
}
export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportContext): void {
const data: (string | number)[][] = [
const start = ctx.startDate || ctx.date || '';
const end = ctx.endDate || ctx.date || '';
const isRange = !!start && !!end && start !== end;
const summaryData: (string | number)[][] = [
[...HEADERS],
...vehicles.map(v => [
statusLabel(v),
@@ -41,7 +46,7 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont
]),
];
const ws = XLSX.utils.aoa_to_sheet(data);
const ws = XLSX.utils.aoa_to_sheet(summaryData);
ws['!cols'] = [
{ wch: 8 }, // 状态
@@ -51,13 +56,13 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont
{ wch: 16 }, // 项目
{ wch: 10 }, // 租赁状态
{ wch: 12 }, // 运营区域
{ wch: 14 }, // 今日里程
{ wch: 14 }, // 区间里程
{ wch: 14 }, // 累计里程
];
ws['!freeze'] = { xSplit: 0, ySplit: 1 } as never;
for (let r = 1; r < data.length; r++) {
for (let r = 1; r < summaryData.length; r++) {
for (const c of [7, 8]) {
const ref = XLSX.utils.encode_cell({ r, c });
if (ws[ref]?.t === 'n') ws[ref].z = '0.##########';
@@ -77,7 +82,53 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont
}
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '里程明细');
XLSX.utils.book_append_sheet(wb, ws, '车辆汇总');
const dayKeys = Array.from(
new Set(vehicles.flatMap(v => Object.keys(v.dailyMileage || {})))
).sort();
if (dayKeys.length > 0) {
const detailHeaders = [
'车牌号', '客户', '业务部门', '项目', '租赁状态', '运营区域',
...dayKeys.map(day => `${day}里程(km)`),
'区间合计(km)',
'累计里程(km)',
];
const detailData: (string | number)[][] = [
detailHeaders,
...vehicles.map(v => [
v.plate,
v.customer || '',
v.department || '',
v.project || '',
v.rentStatus || '',
v.region || '',
...dayKeys.map(day => v.dailyMileage?.[day] || 0),
Math.max(0, v.dailyKm || 0),
v.totalKm != null ? v.totalKm : '',
]),
];
const detailWs = XLSX.utils.aoa_to_sheet(detailData);
detailWs['!cols'] = [
{ wch: 12 },
{ wch: 28 },
{ wch: 14 },
{ wch: 16 },
{ wch: 10 },
{ wch: 12 },
...dayKeys.map(() => ({ wch: 14 })),
{ wch: 14 },
{ wch: 14 },
];
detailWs['!freeze'] = { xSplit: 6, ySplit: 1 } as never;
for (let r = 1; r < detailData.length; r++) {
for (let c = 6; c < detailHeaders.length; c++) {
const ref = XLSX.utils.encode_cell({ r, c });
if (detailWs[ref]?.t === 'n') detailWs[ref].z = '0.##########';
}
}
XLSX.utils.book_append_sheet(wb, detailWs, '每日明细');
}
const now = new Date();
const y = now.getFullYear();
@@ -85,7 +136,9 @@ export function exportMileageXlsx(vehicles: MonitoringVehicle[], ctx: ExportCont
const d = String(now.getDate()).padStart(2, '0');
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
const dateTag = ctx.date ? ctx.date.replace(/-/g, '') : `${y}${m}${d}`;
const filename = `里程看板_${dateTag}_${hh}${mm}_${ctx.sortBy === 'today' ? '今日' : '累计'}.xlsx`;
const dateTag = start && end
? `${start.replace(/-/g, '')}-${end.replace(/-/g, '')}`
: `${y}${m}${d}`;
const filename = `里程看板_${dateTag}_${hh}${mm}_${ctx.sortBy === 'today' ? (isRange ? '区间' : '今日') : '累计'}.xlsx`;
XLSX.writeFile(wb, filename);
}