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 RotatingFooterHint from '../../components/RotatingFooterHint'; import { EmptyState, ErrorState, LoadingState, MetricTile, SurfaceCard } from '../../components/ui/surface'; const QUICK_PICK_OPTIONS: Array<{ id: DateQuickPick; label: string }> = [ { id: 'thisWeek', label: '本周' }, { id: 'thisMonth', label: '本月' }, { id: 'last15', label: '近 15 天' }, ]; type RangeMode = DateQuickPick | 'custom'; function fmtYmd(d: Date): string { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; } function addDays(d: Date, days: number): Date { const next = new Date(d); next.setDate(next.getDate() + days); return next; } function getQuickRange(pick: DateQuickPick): { start: string; end: string } { const today = new Date(); today.setHours(0, 0, 0, 0); if (pick === 'thisWeek') { const day = today.getDay() || 7; return { start: fmtYmd(addDays(today, -(day - 1))), end: fmtYmd(today) }; } if (pick === 'thisMonth') { return { start: fmtYmd(new Date(today.getFullYear(), today.getMonth(), 1)), end: fmtYmd(today) }; } return { start: fmtYmd(addDays(today, -14)), end: fmtYmd(today) }; } function normalizeRange(start: string, end: string): { start: string; end: string } { return start <= end ? { start, end } : { start: end, end: start }; } 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 = 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 names = new Set(); (rows ?? []).forEach(r => r.stations.forEach(s => names.add(s.name))); return names.size; }, [rows]); const avgKg = activeDays > 0 ? totalKg / activeDays : 0; const scopeLabel = pick === 'custom' ? '自定义区间' : QUICK_PICK_OPTIONS.find(item => item.id === pick)?.label ?? '当前时段'; const rangeText = `${effectiveRange.start} 至 ${effectiveRange.end}`; 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) => { setPick(nextPick); setDateRange(getQuickRange(nextPick)); }; const updateDateRange = (field: 'start' | 'end', value: string) => { if (!value) return; setPick('custom'); setDateRange(prev => ({ ...prev, [field]: value })); }; return (
{QUICK_PICK_OPTIONS.map(opt => ( ))}
{(['lingniu', 'external'] as const).map(c => ( ))}
{/* 外部车辆:新系统数据还没准备好 */} {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 })}
))}
)}
); })}
)}
); }