Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/web/src/v2/domain/mileageWorkbook.ts
2026-07-16 09:08:04 +08:00

135 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { MileageExportInput, MileageWorkbookRow } from './mileageExport';
function excelColumn(index: number) {
let value = index;
let result = '';
while (value > 0) {
value -= 1;
result = String.fromCharCode(65 + value % 26) + result;
value = Math.floor(value / 26);
}
return result;
}
function localDateTime(value: Date) {
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
}).format(value).split('/').join('-');
}
export async function createMileageWorkbook(input: MileageExportInput) {
const { Workbook } = await import('exceljs');
const workbook = new Workbook();
workbook.creator = '灵牛车辆数据中台';
workbook.company = '灵牛科技';
workbook.created = input.exportedAt ?? new Date();
workbook.modified = input.exportedAt ?? new Date();
workbook.calcProperties.fullCalcOnLoad = true;
const sheet = workbook.addWorksheet('里程查询', {
views: [{ state: 'frozen', xSplit: 2, ySplit: 6, activeCell: 'C7', showGridLines: false }],
pageSetup: { orientation: 'landscape', fitToPage: true, fitToWidth: 1, fitToHeight: 0, paperSize: 9, margins: { left: .25, right: .25, top: .45, bottom: .45, header: .2, footer: .2 } },
properties: { defaultRowHeight: 21 }
});
const firstDateColumn = 3;
const lastDateColumn = firstDateColumn + input.dates.length - 1;
const totalColumn = lastDateColumn + 1;
const lastColumnLetter = excelColumn(totalColumn);
const headerRowNumber = 6;
const firstDataRow = headerRowNumber + 1;
const lastDataRow = firstDataRow + input.vehicles.length - 1;
const exportedAt = input.exportedAt ?? new Date();
const sourceDescription = input.sources.map((source, index) => `${index + 1}. ${source.label}${source.mileageType}`).join(' ');
const title = sheet.getCell('A1');
title.value = '车辆里程查询明细';
title.font = { name: 'Microsoft YaHei', size: 18, bold: true, color: { argb: 'FF17345C' } };
title.alignment = { vertical: 'middle', horizontal: 'left' };
title.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE8F1FC' } };
for (let column = 1; column <= totalColumn; column += 1) {
const cell = sheet.getCell(1, column);
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE8F1FC' } };
cell.border = { bottom: { style: 'medium', color: { argb: 'FFBDD0E8' } } };
}
sheet.getRow(1).height = 38;
sheet.getRow(2).values = ['日期范围', `${input.dateFrom}${input.dateTo}`, '车辆范围', `${input.vehicles.length}`];
sheet.getRow(3).values = ['来源优先级', sourceDescription];
const periodTotal = input.mileageRows.reduce((sum, row) => sum + (Number.isFinite(row.dailyMileageKm) ? row.dailyMileageKm : 0), 0);
sheet.getRow(4).values = ['区间总里程', null, '有效车辆日', `${input.mileageRows.length}`];
sheet.getCell('B4').value = input.vehicles.length ? { formula: `SUM(${lastColumnLetter}${firstDataRow}:${lastColumnLetter}${lastDataRow})`, result: periodTotal } : 0;
sheet.getCell('B4').numFmt = '#,##0.0" km"';
for (let rowNumber = 2; rowNumber <= 4; rowNumber += 1) {
const row = sheet.getRow(rowNumber);
row.height = 25;
row.eachCell({ includeEmpty: true }, (cell, columnNumber) => {
cell.font = { name: 'Microsoft YaHei', size: 10, bold: columnNumber === 1 || columnNumber === 3, color: { argb: columnNumber === 1 || columnNumber === 3 ? 'FF53657D' : 'FF213149' } };
cell.alignment = { vertical: 'middle', horizontal: columnNumber === 1 || columnNumber === 3 ? 'left' : 'right' };
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: rowNumber % 2 ? 'FFF8FAFD' : 'FFF2F6FB' } };
cell.border = { bottom: { style: 'thin', color: { argb: 'FFE1E8F1' } } };
});
}
sheet.getRow(5).height = 8;
const header = sheet.getRow(headerRowNumber);
header.values = ['车牌', 'VIN', ...input.dates.map((date) => new Date(`${date}T12:00:00Z`)), '区间总里程'];
header.height = 30;
header.eachCell((cell, columnNumber) => {
const isDateHeader = columnNumber >= firstDateColumn && columnNumber <= lastDateColumn;
cell.font = { name: 'Microsoft YaHei', size: 10, bold: true, color: { argb: columnNumber === totalColumn ? 'FF1C4F91' : 'FF304158' } };
cell.alignment = {
vertical: 'middle',
horizontal: columnNumber <= 2 ? 'left' : isDateHeader ? 'center' : 'right'
};
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: columnNumber === totalColumn ? 'FFDCEBFF' : 'FFEAF0F7' } };
cell.border = { bottom: { style: 'medium', color: { argb: 'FFC3D0DF' } } };
if (isDateHeader) cell.numFmt = 'm/d';
});
const rowsByVin = new Map<string, Map<string, MileageWorkbookRow>>();
for (const row of input.mileageRows) {
if (!rowsByVin.has(row.vin)) rowsByVin.set(row.vin, new Map());
rowsByVin.get(row.vin)!.set(row.date, row);
}
input.vehicles.forEach((vehicle, index) => {
const rowNumber = firstDataRow + index;
const mileageByDate = rowsByVin.get(vehicle.vin) ?? new Map<string, MileageWorkbookRow>();
const dailyValues = input.dates.map((date) => mileageByDate.get(date)?.dailyMileageKm ?? null);
const total = dailyValues.reduce<number>((sum, value) => sum + (value ?? 0), 0);
const row = sheet.getRow(rowNumber);
row.values = [vehicle.plate || '未绑定', vehicle.vin, ...dailyValues, null];
row.height = 25;
row.eachCell({ includeEmpty: true }, (cell, columnNumber) => {
cell.font = { name: columnNumber === 2 ? 'Consolas' : 'Microsoft YaHei', size: columnNumber === 2 ? 9 : 10, color: { argb: columnNumber === totalColumn ? 'FF1D4E89' : 'FF34445A' }, bold: columnNumber === 1 || columnNumber === totalColumn };
cell.alignment = { vertical: 'middle', horizontal: columnNumber <= 2 ? 'left' : 'right' };
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: index % 2 ? 'FFF9FBFD' : 'FFFFFFFF' } };
cell.border = { bottom: { style: 'thin', color: { argb: 'FFE6ECF3' } } };
if (columnNumber >= firstDateColumn) cell.numFmt = '#,##0.0" km"';
});
const dateStart = excelColumn(firstDateColumn);
const dateEnd = excelColumn(lastDateColumn);
const totalCell = sheet.getCell(rowNumber, totalColumn);
totalCell.value = { formula: `SUM(${dateStart}${rowNumber}:${dateEnd}${rowNumber})`, result: total };
totalCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: index % 2 ? 'FFEDF5FF' : 'FFF4F8FE' } };
row.commit();
});
sheet.getColumn(1).width = 15;
sheet.getColumn(2).width = 24;
for (let column = firstDateColumn; column <= lastDateColumn; column += 1) sheet.getColumn(column).width = 12;
sheet.getColumn(totalColumn).width = 17;
if (input.vehicles.length) {
sheet.autoFilter = { from: { row: headerRowNumber, column: 1 }, to: { row: lastDataRow, column: totalColumn } };
sheet.addConditionalFormatting({
ref: `${excelColumn(firstDateColumn)}${firstDataRow}:${excelColumn(lastDateColumn)}${lastDataRow}`,
rules: [{
type: 'colorScale', priority: 1,
cfvo: [{ type: 'min' }, { type: 'percentile', value: 50 }, { type: 'max' }],
color: [{ argb: 'FFFFFFFF' }, { argb: 'FFE8F2FF' }, { argb: 'FFB9D8FF' }]
}]
});
}
sheet.headerFooter.oddFooter = '&L灵牛车辆数据中台&C第 &P / &N 页&R导出于 ' + localDateTime(exportedAt);
return workbook;
}