import { useMemo, useState } from 'react'; import { ChevronRight, Download, RefreshCw } from 'lucide-react'; import { AnimatePresence, motion } from 'motion/react'; import * as XLSX from 'xlsx'; import TrendBadge from '../../TrendBadge'; import type { HydrogenDailyDetailCustomer, HydrogenDailyDetailStation, HydrogenDailyRow } from '../../types'; import type { DailyDetailState } from '../../HydrogenDaily'; import { SortableColumnHeader, sortBy, toggleSort, type SortDirection } from '../../components/SortableColumnHeader'; type DailySortKey = 'date' | 'price' | 'kg' | 'fee' | 'chainPct'; interface DailyDetailTableProps { rows: HydrogenDailyRow[]; totalKg: number; totalFee: number; expanded: Set; highlightedDate?: string | null; details: Record; onToggle: (date: string) => void; onRetryDetail: (date: string) => void; } export function DailyDetailTable({ rows, totalKg, totalFee, expanded, highlightedDate, details, onToggle, onRetryDetail, }: DailyDetailTableProps) { const [sortKey, setSortKey] = useState('date'); const [sortDirection, setSortDirection] = useState('desc'); const sortedRows = useMemo(() => sortBy(rows, sortKey, sortDirection, (row, key) => { if (key === 'price') return row.totalKg > 0 ? row.totalFee / row.totalKg : 0; return row[key === 'kg' ? 'totalKg' : key === 'fee' ? 'totalFee' : key]; }), [rows, sortDirection, sortKey]); const changeSort = (nextKey: DailySortKey) => { const next = toggleSort(sortKey, sortDirection, nextKey); setSortKey(next.key); setSortDirection(next.direction); }; return (

每日加氢数据明细 (日期 → 加氢站 → 客户 → 车辆/来源)

{rows.length} 天
站点余额
合计 {formatNumber(totalKg, 2)} ¥{formatNumber(totalFee, 0)} 以源表为准
{sortedRows.map(row => { const open = expanded.has(row.date); const highlighted = highlightedDate === row.date; const abnormal = Math.abs(row.chainPct) >= 0.3; const background = highlighted ? 'bg-sky-50 ring-1 ring-inset ring-sky-200' : abnormal ? row.chainPct > 0 ? 'bg-emerald-50/35' : 'bg-red-50/35' : ''; return (
{open ? ( onRetryDetail(row.date)} sortKey={sortKey} sortDirection={sortDirection} /> ) : null}
); })}
); } interface StationRowsProps { date: string; fallbackStations: HydrogenDailyRow['stations']; detail?: DailyDetailState; onRetry: () => void; sortKey: DailySortKey; sortDirection: SortDirection; } function StationRows({ date, fallbackStations, detail, onRetry, sortKey, sortDirection }: StationRowsProps) { const [openStations, setOpenStations] = useState>(new Set()); const [openCustomers, setOpenCustomers] = useState>(new Set()); const stations = detail?.data?.stations; const sortedStations = useMemo(() => sortBy(stations ?? [], sortKey, sortDirection, (station, key) => dailyDetailValue(station, key)), [sortDirection, sortKey, stations]); const toggleStation = (station: HydrogenDailyDetailStation) => { const key = `${date}:${station.id}`; setOpenStations(previous => toggleSet(previous, key)); }; const toggleCustomer = (station: HydrogenDailyDetailStation, customer: HydrogenDailyDetailCustomer) => { const key = `${date}:${station.id}:${customer.id}:${customer.name}`; setOpenCustomers(previous => toggleSet(previous, key)); }; return ( {detail?.loading ? : null} {detail?.error ? (
明细读取失败,请重试。
) : null} {!detail ? : null} {stations?.length === 0 ? (
当日无站点明细
) : sortedStations.map((station, index) => { const stationKey = `${date}:${station.id}`; const stationOpen = openStations.has(stationKey); return (
{stationOpen ? ( toggleCustomer(station, customer)} sortKey={sortKey} sortDirection={sortDirection} /> ) : null}
); })}
); } function CustomerRows({ date, station, openCustomers, onToggle, sortKey, sortDirection, }: { date: string; station: HydrogenDailyDetailStation; openCustomers: Set; onToggle: (customer: HydrogenDailyDetailCustomer) => void; sortKey: DailySortKey; sortDirection: SortDirection; }) { const sortedCustomers = useMemo(() => sortBy(station.customers, sortKey, sortDirection, (customer, key) => dailyDetailValue(customer, key)), [sortDirection, sortKey, station.customers]); return ( {sortedCustomers.map(customer => { const customerKey = `${date}:${station.id}:${customer.id}:${customer.name}`; const customerOpen = openCustomers.has(customerKey); return (
{customerOpen ? : null}
); })}
); } function VehicleRows({ customer, sortKey, sortDirection }: { customer: HydrogenDailyDetailCustomer; sortKey: DailySortKey; sortDirection: SortDirection }) { const sortedVehicles = useMemo(() => sortBy(customer.vehicles, sortKey, sortDirection, (vehicle, key) => dailyDetailValue(vehicle, key)), [customer.vehicles, sortDirection, sortKey]); return ( {sortedVehicles.map(vehicle => (
{vehicle.time} {vehicle.plateNo} {vehicle.vehicleScope === 'lingniu' ? '羚牛' : '外部'}
{vehicle.source} {formatVerifyStatus(vehicle.verifyStatus)}
{formatNumber(vehicle.kg, 3)} ¥{formatNumber(vehicle.fee, 2)}
))}
); } function dailyDetailValue( row: HydrogenDailyDetailStation | HydrogenDailyDetailCustomer | HydrogenDailyDetailCustomer['vehicles'][number], key: DailySortKey, ) { if (key === 'date') return 'time' in row ? `${row.time} ${row.plateNo}` : 'name' in row ? row.name : ''; if (key === 'price') return row.kg > 0 ? row.fee / row.kg : 0; if (key === 'chainPct') return row.kg; return row[key]; } function DetailStatus({ label }: { label: string }) { return
{label}
; } function toggleSet(previous: Set, key: string) { const next = new Set(previous); next.has(key) ? next.delete(key) : next.add(key); return next; } function formatVerifyStatus(value: string) { const normalized = value.toUpperCase(); if (normalized === 'VERIFIED' || normalized === 'PASS') return '已核验'; if (normalized === 'FAILED' || normalized === 'REJECT') return '异常'; return '待核验'; } function formatStationType(value: string) { const normalized = value.toLowerCase(); if (normalized === 'self' || normalized === 'internal') return '自营站'; if (normalized === 'external' || normalized === 'partner') return '合作站'; return '加氢站'; } function exportDailyRows(rows: HydrogenDailyRow[], details: Record) { const workbook = XLSX.utils.book_new(); const summaryRows = rows.flatMap(row => row.stations.length > 0 ? row.stations.map(station => ({ 日期: row.date, 加氢站: station.name, 单价元每Kg: station.pricePerKg, 加氢量Kg: station.kg, 成本元: station.fee, 日环比: row.chainPct, })) : [{ 日期: row.date, 加氢站: '', 单价元每Kg: 0, 加氢量Kg: row.totalKg, 成本元: row.totalFee, 日环比: row.chainPct }]); XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(summaryRows), '日报汇总'); const recordRows = Object.values(details).flatMap(state => { if (!state.data) return []; return state.data.stations.flatMap(station => station.customers.flatMap(customer => ( customer.vehicles.map(vehicle => ({ 日期: state.data?.date, 时间: vehicle.time, 加氢站: station.name, 客户: customer.name, 车牌: vehicle.plateNo, 车辆归属: vehicle.vehicleScope === 'lingniu' ? '羚牛' : '外部', 来源: vehicle.source, 核验状态: formatVerifyStatus(vehicle.verifyStatus), 单价元每Kg: vehicle.unitPrice, 加氢量Kg: vehicle.kg, 成本元: vehicle.fee, })) ))); }); if (recordRows.length > 0) XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(recordRows), '已下钻流水'); const start = rows.at(-1)?.date ?? '开始'; const end = rows[0]?.date ?? '结束'; XLSX.writeFile(workbook, `氢能按日_${start}_${end}.xlsx`); } function formatNumber(value: number, digits: number): string { return value.toLocaleString('zh-CN', { minimumFractionDigits: digits, maximumFractionDigits: digits }); }