import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Building2, ChevronRight, Fuel, Plug, ReceiptText, TrendingUp, Truck, UserRound, X } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, Tooltip, ReferenceLine } from 'recharts'; import TrendBadge from './TrendBadge'; import { fetchHydrogenDaily } from './api'; import type { CustomerType, DateQuickPick, HydrogenDailyRow } from './types'; import RotatingFooterHint from '../../components/RotatingFooterHint'; import { EmptyState, ErrorState, LoadingState, MetricTile, SurfaceCard } from '../../components/ui/surface'; import { buildHydrogenDrillUrl, parseHydrogenDrillContext, type HydrogenDrillContext, } from './hydrogen-drill-context'; import HydrogenOrders from './HydrogenOrders'; import AnalysisDateFilters from './AnalysisDateFilters'; import AnalysisScopeSwitch, { type AnalysisScopeOption } from './AnalysisScopeSwitch'; import { activateDrillOnKeyDown } from './drill-interaction'; import { dateRangeModeLabel, getQuickDateRange, inferDateRangeMode, normalizeDateRange, type DateRangeMode, } from './date-range'; const VEHICLE_SCOPE_OPTIONS: readonly AnalysisScopeOption[] = [ { id: 'lingniu', label: '羚牛车辆' }, { id: 'external', label: '外部车辆' }, ]; const HYDROGEN_ORDERS_ID = 'hydrogen-orders'; export default function HydrogenDaily() { const [drillContext, setDrillContext] = useState(() => ( parseHydrogenDrillContext(window.location.search) )); const [pick, setPick] = useState(() => ( drillContext.startDate && drillContext.endDate ? inferDateRangeMode(drillContext.startDate, drillContext.endDate) : 'last15' )); const [dateRange, setDateRange] = useState(() => ( drillContext.startDate && drillContext.endDate ? normalizeDateRange(drillContext.startDate, drillContext.endDate) : getQuickDateRange('last15') )); const [vehicleScope, setVehicleScope] = useState(drillContext.vehicleScope); const [expanded, setExpanded] = useState>(new Set()); const [rows, setRows] = useState(null); const [error, setError] = useState(null); const [retryKey, setRetryKey] = useState(0); const orderTriggerRef = useRef(null); const effectiveRange = useMemo(() => normalizeDateRange(dateRange.start, dateRange.end), [dateRange.start, dateRange.end]); const selectedStationId = drillContext.level === 'station' ? drillContext.stationId : undefined; const selectedStationName = drillContext.level === 'station' ? drillContext.stationName || `站点 #${drillContext.stationId}` : undefined; const selectedCustomerName = drillContext.level === 'customer' ? drillContext.customerName : undefined; const selectedDate = drillContext.selectedDate; const selectedDailyRow = rows?.find(row => row.date === selectedDate); const commitDrillContext = useCallback((next: HydrogenDrillContext, mode: 'push' | 'replace') => { const url = buildHydrogenDrillUrl(window.location, next); window.history[mode === 'push' ? 'pushState' : 'replaceState'](null, '', url); setDrillContext(next); }, []); useEffect(() => { let cancelled = false; setError(null); const query = pick === 'custom' ? { startDate: effectiveRange.start, endDate: effectiveRange.end, stationId: selectedStationId, customerName: selectedCustomerName } : { range: pick, stationId: selectedStationId, customerName: selectedCustomerName }; fetchHydrogenDaily(query, vehicleScope) .then(r => { if (!cancelled) setRows(r); }) .catch(e => { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); }); return () => { cancelled = true; }; }, [pick, vehicleScope, effectiveRange.start, effectiveRange.end, selectedStationId, selectedCustomerName, retryKey]); // 柱图:按日期升序,用于"从左到右时间流" const trendData = useMemo(() => (rows ? [...rows].sort((a, b) => a.date.localeCompare(b.date)) : []), [rows]); const totalKg = (rows ?? []).reduce((a, r) => a + r.totalKg, 0); const activeDays = (rows ?? []).filter(r => r.totalKg > 0).length; const stationCount = useMemo(() => { const ids = new Set(); (rows ?? []).forEach(r => r.stations.forEach(s => ids.add(s.stationId))); return ids.size; }, [rows]); const avgKg = activeDays > 0 ? totalKg / activeDays : 0; const scopeLabel = dateRangeModeLabel(pick); const rangeText = `${effectiveRange.start} 至 ${effectiveRange.end}`; const orderScopeLabel = selectedCustomerName ? `客户 ${selectedCustomerName}` : selectedStationName ? `站点 ${selectedStationName}` : vehicleScope === 'external' ? '外部车辆' : '羚牛车辆'; const peakDay = trendData.reduce((best, item) => (!best || item.totalKg > best.totalKg ? item : best), null); const lowDay = trendData .filter(item => item.totalKg > 0) .reduce((low, item) => (!low || item.totalKg < low.totalKg ? item : low), null); const zeroDays = (rows ?? []).filter(r => r.totalKg === 0).length; const toggle = (date: string) => setExpanded(prev => { const next = new Set(prev); next.has(date) ? next.delete(date) : next.add(date); return next; }); const applyQuickPick = (nextPick: DateQuickPick) => { const nextRange = getQuickDateRange(nextPick); setPick(nextPick); setDateRange(nextRange); commitDrillContext({ ...drillContext, vehicleScope, startDate: nextRange.start, endDate: nextRange.end, selectedDate: undefined, orderPage: undefined, }, 'replace'); }; const updateDateRange = (field: 'start' | 'end', value: string) => { if (!value) return; const nextRange = { ...dateRange, [field]: value }; setPick('custom'); const normalized = normalizeDateRange(nextRange.start, nextRange.end); setDateRange(normalized); commitDrillContext({ ...drillContext, vehicleScope, startDate: normalized.start, endDate: normalized.end, selectedDate: undefined, orderPage: undefined, }, 'replace'); }; const updateVehicleScope = (next: CustomerType) => { setVehicleScope(next); commitDrillContext({ ...drillContext, vehicleScope: next, startDate: effectiveRange.start, endDate: effectiveRange.end, orderPage: undefined, }, 'replace'); }; const clearEntity = () => { commitDrillContext({ level: 'overview', year: drillContext.year, vehicleScope, startDate: effectiveRange.start, endDate: effectiveRange.end, }, 'replace'); }; const openOrders = (date: string) => { const activeElement = document.activeElement; if (activeElement instanceof HTMLElement || activeElement instanceof SVGElement) { orderTriggerRef.current = activeElement; } commitDrillContext({ ...drillContext, vehicleScope, startDate: effectiveRange.start, endDate: effectiveRange.end, selectedDate: date, orderPage: undefined, }, selectedDate === date ? 'replace' : 'push'); window.requestAnimationFrame(() => { const orders = document.getElementById(HYDROGEN_ORDERS_ID); orders?.focus({ preventScroll: true }); orders?.scrollIntoView({ behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth', block: 'start', }); }); }; const closeOrders = () => { commitDrillContext({ ...drillContext, vehicleScope, startDate: effectiveRange.start, endDate: effectiveRange.end, selectedDate: undefined, orderPage: undefined, }, 'replace'); window.requestAnimationFrame(() => orderTriggerRef.current?.focus()); }; const changeOrderPage = useCallback((page: number, mode: 'push' | 'replace') => { commitDrillContext({ ...drillContext, orderPage: page > 1 ? page : undefined, }, mode); }, [commitDrillContext, drillContext]); const retryLoad = () => { setRows(null); setError(null); setRetryKey(key => key + 1); }; useEffect(() => { const handlePopState = () => { const next = parseHydrogenDrillContext(window.location.search); setDrillContext(next); setVehicleScope(next.vehicleScope); if (next.startDate && next.endDate) { const normalized = normalizeDateRange(next.startDate, next.endDate); setPick(inferDateRangeMode(normalized.start, normalized.end)); setDateRange(normalized); } }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, []); return (
setPick('custom')} onDateChange={updateDateRange} /> {selectedStationName && (
{selectedStationName}
站点每日明细 · {rangeText}
)} {selectedCustomerName && (
{selectedCustomerName}
客户每日明细 · {rangeText}
)} {error && ( )} {!error && rows !== null &&
} {/* 外部车辆:新系统数据还没准备好 */} {!error && vehicleScope === 'external' && rows !== null && totalKg === 0 && (
外部车辆 · 数据未就绪
新系统的外部车辆加氢数据还在准备中
上线后此处将展示完整明细
)} {/* 时段加氢量柱图(外部车辆无数据时不渲染) */} {!error && !(vehicleScope === 'external' && totalKg === 0) && trendData.length > 0 && (
每日加氢量 时间单位:日 · 单位 Kg
峰值日
{peakDay ? `${peakDay.date.slice(5)} · ${peakDay.totalKg.toLocaleString('zh-CN', { maximumFractionDigits: 0 })}` : '—'}
低谷日
{lowDay ? `${lowDay.date.slice(5)} · ${lowDay.totalKg.toLocaleString('zh-CN', { maximumFractionDigits: 0 })}` : '—'}
零数据日
0 ? 'text-amber-600' : 'text-emerald-600'}`}> {zeroDays} 天
v.slice(5)} tick={{ fontSize: 10, fill: '#94a3b8' }} tickLine={false} axisLine={false} interval="preserveStartEnd" minTickGap={8} /> v >= 1000 ? `${Math.round(v / 1000)}k` : `${Math.round(v)}`} /> [`${Number(v ?? 0).toLocaleString('zh-CN', { maximumFractionDigits: 2 })} Kg`, '加氢量']} labelFormatter={(d) => `日期 ${d}`} contentStyle={{ borderRadius: 12, fontSize: 12 }} cursor={{ fill: 'rgba(34, 211, 238, 0.06)' }} /> {avgKg > 0 && ( )} {trendData.map(item => ( 0 ? 'cursor-pointer outline-none focus:stroke-blue-700 focus:stroke-2' : undefined} role={item.totalKg > 0 ? 'button' : undefined} tabIndex={item.totalKg > 0 ? 0 : -1} aria-expanded={item.totalKg > 0 ? selectedDate === item.date : undefined} aria-controls={item.totalKg > 0 ? HYDROGEN_ORDERS_ID : undefined} aria-label={`${item.date},加氢量 ${item.totalKg.toLocaleString('zh-CN', { maximumFractionDigits: 2 })} Kg${item.totalKg > 0 ? `,查看${orderScopeLabel}加氢订单` : ',无可下钻订单'}`} onClick={item.totalKg > 0 ? () => openOrders(item.date) : undefined} onKeyDown={item.totalKg > 0 ? event => activateDrillOnKeyDown(event, () => openOrders(item.date)) : undefined} /> ))}
)} {selectedDate && ( )} {/* 表格(外部车辆 + 全 0 时不渲染,由上方友好空状态替代) */} {!error && !(vehicleScope === 'external' && rows !== null && totalKg === 0) && (
{/* 表头 */}
日期 / 加氢站 单价 (元/Kg) 加氢量 (Kg) 环比
{/* 合计行 */}
合计 {totalKg.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}
{/* 主行 + 子行 */} {rows === null ? ( ) : rows.length === 0 ? ( ) : rows.map(r => { const open = expanded.has(r.date); const isAbnormal = Math.abs(r.chainPct) >= 0.3; const abnormalBg = isAbnormal ? r.chainPct > 0 ? 'bg-emerald-50/40' : 'bg-red-50/40' : ''; return (
{r.totalKg.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}
{open && ( {r.stations.map(s => (
{s.name}
{s.pricePerKg > 0 && (
单价 {s.pricePerKg} 元/Kg
)}
{s.pricePerKg > 0 ? s.pricePerKg : '—'} {s.kg.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}
))}
)}
); })}
)} {!error && }
); }