import { useEffect, useMemo, useState } from 'react'; import { ChevronRight, Fuel, Plug, TrendingUp, Truck } 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 { getQuickRange, getRangeModeLabel, normalizeRange, summarizeHydrogenRows, type RangeMode, } from './hydrogen-daily/model'; import DailyRangeControls from './daily-range/DailyRangeControls'; import RotatingFooterHint from '../../components/RotatingFooterHint'; import { EmptyState, ErrorState, LoadingState, MetricTile, SurfaceCard } from '../../components/ui/surface'; export default function HydrogenDaily() { const [pick, setPick] = useState('last15'); const [dateRange, setDateRange] = useState(() => getQuickRange('last15')); const [customer, setCustomer] = useState('lingniu'); const [expanded, setExpanded] = useState>(new Set()); const [rows, setRows] = useState(null); const [error, setError] = useState(null); const effectiveRange = useMemo(() => normalizeRange(dateRange.start, dateRange.end), [dateRange.start, dateRange.end]); useEffect(() => { let cancelled = false; setError(null); const query = pick === 'custom' ? { startDate: effectiveRange.start, endDate: effectiveRange.end } : { range: pick }; fetchHydrogenDaily(query, customer) .then(r => { if (!cancelled) setRows(r); }) .catch(e => { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); }); return () => { cancelled = true; }; }, [pick, customer, effectiveRange.start, effectiveRange.end]); const { trendData, totalKg, activeDays, stationCount, avgKg, peakDay, lowDay, zeroDays, } = useMemo(() => summarizeHydrogenRows(rows), [rows]); const scopeLabel = getRangeModeLabel(pick); const rangeText = `${effectiveRange.start} 至 ${effectiveRange.end}`; 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) => { setPick(nextPick); setDateRange(getQuickRange(nextPick)); }; const updateDateRange = (field: 'start' | 'end', value: string) => { if (!value) return; setPick('custom'); setDateRange(prev => ({ ...prev, [field]: value })); }; return (
setPick('custom')} onDateRangeChange={updateDateRange} onCustomerChange={setCustomer} />
{/* 外部车辆:新系统数据还没准备好 */} {customer === 'external' && rows !== null && totalKg === 0 && (
外部车辆 · 数据未就绪
新系统的外部车辆加氢数据还在准备中
上线后此处将展示完整明细
)} {/* 时段加氢量柱图(外部车辆无数据时不渲染) */} {!(customer === '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((_, i) => ( ))}
)} {/* 表格(外部车辆 + 全 0 时不渲染,由上方友好空状态替代) */} {!(customer === 'external' && rows !== null && totalKg === 0) && (
{/* 表头 */}
日期 / 加氢站 单价 (元/Kg) 加氢量 (Kg) 环比
{/* 合计行 */}
合计 {totalKg.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}
{/* 主行 + 子行 */} {error ? (
) : 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 (
{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 })}
))}
)}
); })}
)}
); }