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; // Create the selected view first so Excel/WPS opens the requested export. const primaryHydrogenSheet = input.metricView === 'hydrogen' ? workbook.addWorksheet('氢能明细') : undefined; const sheet = workbook.addWorksheet('里程查询', { views: [{ state: 'frozen', xSplit: 4, ySplit: 6, activeCell: 'E7', 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 = 5; 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 <= 4 ? '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>(); 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(); const dailyValues = input.dates.map((date) => mileageByDate.get(date)?.dailyMileageKm ?? null); const total = dailyValues.reduce((sum, value) => sum + (value ?? 0), 0); const row = sheet.getRow(rowNumber); row.values = [vehicle.plate || '未绑定', vehicle.vin, vehicle.brandName || '待维护', vehicle.modelName || '待维护', ...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 <= 4 ? '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; sheet.getColumn(3).width = 18; sheet.getColumn(4).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); const hydrogenSheet = primaryHydrogenSheet ?? workbook.addWorksheet('氢能明细'); hydrogenSheet.views = [{ state: 'frozen', xSplit: 2, ySplit: 1, activeCell: 'C2', showGridLines: false }]; hydrogenSheet.properties.defaultRowHeight = 21; const hydrogenHeaders = [ '日期', '车牌', '当天所在省', '当天所在市', '大区', 'VIN', '品牌', '车型', '每日氢耗 (kg/100km)', '结果状态', '总里程 (km)', '纯电里程 (km)', '混动里程 (km)', '电SOC差值 (百分点)', '充电次数', '充电量 (kWh)', '加氢次数', '加氢量 (kg)', '修正用氢量 (kg)', '原因', '物理用氢量 (kg)', '来源', '定位时间(当日最后有效点)', '位置状态' ]; hydrogenSheet.addRow(hydrogenHeaders); const hydrogenHeader = hydrogenSheet.getRow(1); hydrogenHeader.height = 30; hydrogenHeader.eachCell((cell) => { cell.font = { name: 'Microsoft YaHei', size: 10, bold: true, color: { argb: 'FF304158' } }; cell.alignment = { vertical: 'middle', horizontal: 'center' }; cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFEAF0F7' } }; cell.border = { bottom: { style: 'medium', color: { argb: 'FFC3D0DF' } } }; }); const vehicleByVIN = new Map(input.vehicles.map((vehicle) => [vehicle.vin, vehicle])); const energyRows = [...input.mileageRows].sort((left, right) => left.date.localeCompare(right.date) || left.vin.localeCompare(right.vin)); energyRows.forEach((value, index) => { const vehicle = vehicleByVIN.get(value.vin); const row = hydrogenSheet.addRow([ value.date, vehicle?.plate || value.plate || '未绑定', value.province || '未知', value.city || '未知', value.region || '未知', value.vin, vehicle?.brandName || '待维护', vehicle?.modelName || '待维护', value.hydrogenConsumptionKgPer100Km ?? null, value.hydrogenQualityStatus || 'NO_DATA', value.dailyMileageKm, value.pureElectricMileageKm ?? null, value.mixedMileageKm ?? null, value.batterySocDeltaPct ?? null, value.chargeCount ?? null, value.chargeEnergyKWh ?? null, value.refuelCount ?? null, value.refuelAmountKg ?? null, value.hydrogenSocBalancedKg ?? null, value.hydrogenQualityReason || '', value.hydrogenPhysicalConsumptionKg ?? null, value.source ?? '', value.locationTime || '', value.locationStatus || '未查询' ]); row.height = 24; row.eachCell({ includeEmpty: true }, (cell, columnNumber) => { cell.font = { name: columnNumber === 6 ? 'Consolas' : 'Microsoft YaHei', size: columnNumber === 6 ? 9 : 10, color: { argb: 'FF34445A' } }; cell.alignment = { vertical: 'middle', horizontal: columnNumber >= 9 && columnNumber <= 19 ? 'right' : 'left' }; cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: index % 2 ? 'FFF9FBFD' : 'FFFFFFFF' } }; cell.border = { bottom: { style: 'thin', color: { argb: 'FFE6ECF3' } } }; if ([9, 11, 12, 13, 14, 16, 18, 19, 21].includes(columnNumber)) cell.numFmt = '#,##0.000'; if ([15, 17].includes(columnNumber)) cell.numFmt = '0'; }); }); [13, 15, 18, 18, 12, 24, 18, 24, 22, 14, 15, 17, 17, 22, 12, 17, 12, 16, 19, 52, 19, 16, 30, 24].forEach((width, index) => { hydrogenSheet.getColumn(index + 1).width = width; }); if (energyRows.length) { hydrogenSheet.autoFilter = { from: { row: 1, column: 1 }, to: { row: energyRows.length + 1, column: hydrogenHeaders.length } }; } hydrogenSheet.headerFooter.oddFooter = sheet.headerFooter.oddFooter; return workbook; }