import { ChevronDown, ChevronRight, Database, Download, Search, Truck, X } from 'lucide-react'; import { Fragment, useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'; import { createPortal } from 'react-dom'; import * as XLSX from 'xlsx'; import { SortableColumnHeader, sortBy, toggleSort, type SortDirection } from '../../components/SortableColumnHeader'; import type { HydrogenOverviewDetailGroup, HydrogenOverviewDetailGroupBy, HydrogenOverviewDetailRecord, HydrogenOverviewDetailResponse, } from '../../types'; import type { HydrogenVehicleScope } from '../../api'; type Selection = { stationId?: number | null; customerId?: number | null; customerName?: string | null; plateNo?: string | null; }; type TreeSortKey = 'name' | 'ownership' | 'source' | 'verify' | 'recordCount' | 'kg' | 'cost' | 'revenue'; interface OverviewDrillTreeDialogProps { title: string; initialVehicleScope: HydrogenVehicleScope; initialSelection?: Selection; load: ( groupBy: HydrogenOverviewDetailGroupBy | null, selection: Selection, vehicleScope: HydrogenVehicleScope, includeAll?: boolean, ) => Promise; onClose: () => void; } const number = (value: number, digits = 2) => value.toLocaleString('zh-CN', { minimumFractionDigits: digits, maximumFractionDigits: digits }); const customerKey = (stationId: number | string, customer: HydrogenOverviewDetailGroup) => `${stationId}:${customer.id}:${customer.name}`; const vehicleKey = (stationId: number | string, customer: HydrogenOverviewDetailGroup, vehicle: HydrogenOverviewDetailGroup) => `${customerKey(stationId, customer)}:${vehicle.name}`; /** 原型定义的四层账本树:加氢站 -> 客户 -> 车辆 -> 单笔订单与核对明细。 */ export function OverviewDrillTreeDialog({ title, initialVehicleScope, initialSelection = {}, load, onClose }: OverviewDrillTreeDialogProps) { const [root, setRoot] = useState(null); const [customers, setCustomers] = useState>({}); const [vehicles, setVehicles] = useState>({}); const [orders, setOrders] = useState>({}); const [openStations, setOpenStations] = useState>({}); const [openCustomers, setOpenCustomers] = useState>({}); const [openVehicles, setOpenVehicles] = useState>({}); const [loading, setLoading] = useState('root'); const [stationFilter, setStationFilter] = useState('all'); const [customerFilter, setCustomerFilter] = useState('all'); const [plateFilter, setPlateFilter] = useState('all'); const [vehicleScope, setVehicleScope] = useState(initialVehicleScope); const [exporting, setExporting] = useState(false); const [sortKey, setSortKey] = useState('kg'); const [sortDirection, setSortDirection] = useState('desc'); const loadRoot = useCallback(async (scope: HydrogenVehicleScope) => { setLoading('root'); setCustomers({}); setVehicles({}); setOrders({}); setOpenStations({}); setOpenCustomers({}); setOpenVehicles({}); try { setRoot(await load('station', initialSelection, scope)); } catch { setRoot(null); } finally { setLoading(null); } }, [initialSelection, load]); useEffect(() => { void loadRoot(vehicleScope); }, [loadRoot, vehicleScope]); const toggleStation = async (station: HydrogenOverviewDetailGroup) => { const key = String(station.id); if (openStations[key]) { setOpenStations(value => ({ ...value, [key]: false })); return; } setOpenStations(value => ({ ...value, [key]: true })); if (customers[key]) return; setLoading(`station:${key}`); try { const data = await load('customer', { ...initialSelection, stationId: Number(station.id) || null }, vehicleScope); setCustomers(value => ({ ...value, [key]: data.groups })); } finally { setLoading(null); } }; const toggleCustomer = async (stationId: number | string, customer: HydrogenOverviewDetailGroup) => { const key = customerKey(stationId, customer); if (openCustomers[key]) { setOpenCustomers(value => ({ ...value, [key]: false })); return; } setOpenCustomers(value => ({ ...value, [key]: true })); if (vehicles[key]) return; setLoading(`customer:${key}`); try { const data = await load('vehicle', { ...initialSelection, stationId: Number(stationId) || null, customerId: Number(customer.id) || 0, customerName: customer.name, }, vehicleScope); setVehicles(value => ({ ...value, [key]: data.groups })); } finally { setLoading(null); } }; const toggleVehicle = async (stationId: number | string, customer: HydrogenOverviewDetailGroup, vehicle: HydrogenOverviewDetailGroup) => { const key = vehicleKey(stationId, customer, vehicle); if (openVehicles[key]) { setOpenVehicles(value => ({ ...value, [key]: false })); return; } setOpenVehicles(value => ({ ...value, [key]: true })); if (orders[key]) return; setLoading(`vehicle:${key}`); try { const data = await load(null, { ...initialSelection, stationId: Number(stationId) || null, customerId: Number(customer.id) || 0, customerName: customer.name, plateNo: vehicle.name, }, vehicleScope); setOrders(value => ({ ...value, [key]: data.records })); } finally { setLoading(null); } }; const stationOptions = root?.groups ?? []; const selectedStation = stationFilter === 'all' ? null : stationOptions.find(station => String(station.id) === stationFilter) ?? null; const customerOptions = selectedStation ? customers[String(selectedStation.id)] ?? [] : []; const selectedCustomer = customerFilter === 'all' ? null : customerOptions.find(customer => customerKey(selectedStation?.id ?? '', customer) === customerFilter) ?? null; const plateOptions = selectedStation && selectedCustomer ? vehicles[customerKey(selectedStation.id, selectedCustomer)] ?? [] : []; const visibleStations = useMemo(() => { const filtered = stationFilter === 'all' ? stationOptions : stationOptions.filter(station => String(station.id) === stationFilter); return sortTreeGroups(filtered, sortKey, sortDirection); }, [sortDirection, sortKey, stationFilter, stationOptions]); const selectStation = (value: string) => { setStationFilter(value); setCustomerFilter('all'); setPlateFilter('all'); const station = stationOptions.find(item => String(item.id) === value); if (station && !openStations[String(station.id)]) void toggleStation(station); }; const selectCustomer = (value: string) => { setCustomerFilter(value); setPlateFilter('all'); const customer = customerOptions.find(item => customerKey(selectedStation?.id ?? '', item) === value); if (selectedStation && customer && !openCustomers[value]) void toggleCustomer(selectedStation.id, customer); }; const changeVehicleScope = (scope: HydrogenVehicleScope) => { if (scope === vehicleScope) return; setVehicleScope(scope); setStationFilter('all'); setCustomerFilter('all'); setPlateFilter('all'); }; const changeSort = (nextKey: TreeSortKey) => { const next = toggleSort(sortKey, sortDirection, nextKey); setSortKey(next.key); setSortDirection(next.direction); }; const exportAll = async () => { setExporting(true); try { const detail = await load(null, { ...initialSelection, stationId: selectedStation ? Number(selectedStation.id) || null : null, customerId: selectedCustomer ? Number(selectedCustomer.id) || 0 : null, customerName: selectedCustomer?.name ?? null, plateNo: plateFilter === 'all' ? null : plateFilter, }, vehicleScope, true); const rows = detail.records.map(record => ({ 加氢时间: record.refuelTime, 加氢站: record.stationName, 客户: record.customerName, 车牌: record.plateNo, 车辆归属: record.vehicleScope === 'lingniu' ? '羚牛车辆' : '外部车辆', 数据来源: record.source, 核对状态: verifyText(record.verifyStatus), 订单编号: record.orderNo, 加氢量Kg: record.kg, 成本元: record.cost, 客户收入元: record.revenue, })); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet(rows), '穿透账单'); XLSX.writeFile(workbook, `${title.replaceAll(/[\\/:*?"<>|]/g, '_')}_穿透账单.xlsx`); if (detail.truncated) window.alert('当前筛选范围超过 20,000 笔,导出已截取前 20,000 笔。请收窄筛选范围后再次导出。'); } finally { setExporting(false); } }; if (typeof document === 'undefined') return null; const summary = root?.summary; return createPortal(
{ if (event.target === event.currentTarget) onClose(); }}>

{title}

加氢站 → 客户 → 车辆 → 单笔订单与核对明细

({ value: String(item.id), label: item.name }))} /> ({ value: customerKey(selectedStation?.id ?? '', item), label: item.name }))} /> ({ value: item.name, label: item.name }))} />
{([{ key: 'all', label: '全部车辆' }, { key: 'lingniu', label: '仅羚牛车辆' }, { key: 'external', label: '仅外部车辆' }] as const).map(item => ( ))}

提示:点击表格行可四级层层展开

左右滑动查看完整数据与凭证列

{visibleStations.map(station => { const stationKey = String(station.id); return void toggleStation(station)} onCustomer={customer => void toggleCustomer(station.id, customer)} onVehicle={(customer, vehicle) => void toggleVehicle(station.id, customer, vehicle)} />; })}
{!root && loading === null ?
真实账本读取失败,请关闭后重试。
: null}
, document.body, ); } function Metric({ label, value, tone = 'text-slate-900' }: { label: string; value: string; tone?: string }) { return
{label}
{value}
; } function SearchSelect({ allLabel, value, onChange, options, disabled = false }: { allLabel: string; value: string; onChange: (value: string) => void; options: { value: string; label: string }[]; disabled?: boolean }) { const [open, setOpen] = useState(false); const [keyword, setKeyword] = useState(''); const selected = options.find(option => option.value === value); const filtered = options.filter(option => option.label.toLowerCase().includes(keyword.trim().toLowerCase())); const pick = (next: string) => { onChange(next); setOpen(false); setKeyword(''); }; return
{open ?
{filtered.map(option => )} {filtered.length === 0 ?

暂无匹配结果

: null}
: null}
; } function ExpandMark({ open }: { open: boolean }) { return open ? : ; } function StationBranch({ station, expanded, loadingKey, customers, customerFilter, plateFilter, openCustomers, vehicles, orders, openVehicles, sortKey, sortDirection, onStation, onCustomer, onVehicle }: { station: HydrogenOverviewDetailGroup; expanded: boolean; loadingKey: string | null; customers: HydrogenOverviewDetailGroup[]; customerFilter: string; plateFilter: string; openCustomers: Record; vehicles: Record; orders: Record; openVehicles: Record; sortKey: TreeSortKey; sortDirection: SortDirection; onStation: () => void; onCustomer: (customer: HydrogenOverviewDetailGroup) => void; onVehicle: (customer: HydrogenOverviewDetailGroup, vehicle: HydrogenOverviewDetailGroup) => void; }) { const stationKey = String(station.id); const visibleCustomers = sortTreeGroups(customerFilter === 'all' ? customers : customers.filter(customer => customerKey(station.id, customer) === customerFilter), sortKey, sortDirection); return <> {station.name}({station.customerCount} 家客户)} ownership="-" source="全量自动归集" verify="-" group={station} /> {expanded && visibleCustomers.map(customer => { const key = customerKey(stationKey, customer); const childVehicles = vehicles[key] ?? []; const visibleVehicles = sortTreeGroups(plateFilter === 'all' ? childVehicles : childVehicles.filter(vehicle => vehicle.name === plateFilter), sortKey, sortDirection); return onCustomer(customer)} indent={1} label={<>└─ 客户:{customer.name}} ownership="客户" source={`${childVehicles.length || '待'} 辆车挂载`} verify="-" group={customer} /> {openCustomers[key] && visibleVehicles.map(vehicle => { const key = vehicleKey(stationKey, customer, vehicle); return onVehicle(customer, vehicle)} indent={2} label={<>{vehicle.name}({vehicle.recordCount} 笔订单)} ownership={vehicle.vehicleScope === 'lingniu' ? '羚牛车辆' : '外部车辆'} source={vehicle.source ?? '未知来源'} verify={verifyText(vehicle.verifyStatus)} group={vehicle} /> {openVehicles[key] && } ; })} {openCustomers[key] && loadingKey === `customer:${key}` ? : null} ; })} {expanded && loadingKey === `station:${stationKey}` ? : null} ; } function TreeRow({ label, ownership, source, verify, group, indent = 0, className, onClick }: { label: ReactNode; ownership: string; source: string; verify: string; group: HydrogenOverviewDetailGroup; indent?: number; className: string; onClick: () => void }) { return {label}{ownership}{source}{verify === '-' ? - : }{group.recordCount} 笔{number(group.kg, 3)}{number(group.cost)}{number(group.revenue)}; } function OrderRows({ orders, sortKey, sortDirection }: { orders: HydrogenOverviewDetailRecord[]; sortKey: TreeSortKey; sortDirection: SortDirection }) { return <>{sortTreeOrders(orders, sortKey, sortDirection).map(order => └──订单编号{order.orderNo || order.id}({order.refuelTime})单价 ¥{order.costPrice.toFixed(2)}/Kg{order.source}1 笔{number(order.kg, 3)}{number(order.cost)}{number(order.revenue)})}; } function sortTreeGroups(rows: HydrogenOverviewDetailGroup[], sortKey: TreeSortKey, sortDirection: SortDirection) { return sortBy(rows, sortKey, sortDirection, (row, key) => { if (key === 'ownership') return row.vehicleScope ?? ''; if (key === 'verify') return row.verifyStatus ?? ''; return row[key === 'name' ? 'name' : key] ?? ''; }); } function sortTreeOrders(rows: HydrogenOverviewDetailRecord[], sortKey: TreeSortKey, sortDirection: SortDirection) { return sortBy(rows, sortKey, sortDirection, (row, key) => { if (key === 'name') return row.orderNo || row.id; if (key === 'ownership') return row.vehicleScope; if (key === 'verify') return row.verifyStatus; if (key === 'recordCount') return 1; return row[key] ?? ''; }); } function LoadingRow({ text }: { text: string }) { return {text}; } function verifyText(value?: string) { const normalized = (value ?? '').toUpperCase(); if (normalized === 'VERIFIED' || normalized === 'PASS') return '已核对'; if (normalized === 'PARTIAL') return '部分核对'; if (normalized === 'FAILED' || normalized === 'REJECT') return '异常'; return '未核对'; } function VerifyTag({ value }: { value: string }) { const color = value === '已核对' ? 'border-emerald-100 bg-emerald-50 text-emerald-700' : value === '部分核对' ? 'border-amber-100 bg-amber-50 text-amber-700' : value === '异常' ? 'border-rose-100 bg-rose-50 text-rose-700' : 'border-slate-200 bg-slate-50 text-slate-500'; return {value}; }