// @ts-nocheck — verbatim 8113 prototype source; runtime DOM/CSS is intentionally preserved. import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Activity, AlertTriangle, Calendar, ChevronDown, ChevronLeft, ChevronRight, ChevronsUpDown, Download, Fuel, Maximize2, ReceiptText, RefreshCw, Search, TrendingUp, Truck, Wallet, X, Zap, } from 'lucide-react'; import { exportAoaSheet } from '../../../../shared/xlsx'; import { HYDROGEN_VERIFY_START_DATE } from '../../../../shared/hydrogen-verify'; import { MobileListFullscreenButton } from '../common/MobileListFullscreenButton'; import { SOURCE_LABEL, companyRowsForStats, computeHostKpi, costDimCards, costDimLabel, customerAttrAgg, filterOrders, formatKg, formatYuan, stationMonthAgg, type DimFilter, unverified, } from './data/aggregates'; import { DEFAULT_YEAR, HOST_KPI, MOCK_ORDERS } from './data/mockBoard'; import { DAILY_VERIFY_LABEL, MOCK_DAILY_15DAYS, SOURCE_TYPE_LABEL, STATION_TYPE_LABEL, calculateDailyKpis, filterDailyDataByFleet, getDailyDataForRange, type FleetCategory, type FleetCategoryFilter, } from './data/mockDaily'; import { BORNE_BY_LABEL, BORNE_BY_ORDER, type BorneBy, type FleetScope, type HostView, type H2OrderRow, } from './types'; import { StationDailyApp } from '../station-daily/StationDailyApp'; import '../station-daily/styles.css'; import './styles/energy-bi-board.css'; import { fetchH2BiDaily, fetchH2BiDailyTree, fetchH2BiMeta, fetchH2BiOverview } from '../api'; import { PrototypeRealDailyView } from '../drill/prototype-real-daily'; import { PrototypeDrillModal, prototypeFleetScope } from '../drill/prototype-real-drills'; type BoardScope = 'global' | 'station'; type StatsTab = 'siteMonth' | 'customer'; type DailyRangePreset = 'week' | 'month' | '15days' | 'custom'; type DrillTreeAxis = 'station' | 'customer'; const CHART_BLUE = '#2f6bff'; const CHART_EXTERNAL = '#8fb4ff'; const CHART_INCOME = '#2f9fb3'; const CHART_COST = '#7c83e6'; // Keep the compact desktop treatment while ensuring keyboard/touch users get a reliable target. const ACCESSIBLE_CONTROL_STYLE = { minHeight: 44 }; const DRILL_CUSTOMER_BORNE: Record = { 'c-zp': 'customer', 'c-ys': 'customer', 'c-zq': 'customer', 'c-ln': 'company', 'c-qb': 'customer', 'c-yj': 'customer', 'c-js': 'pending', 'c-gz': 'customer', }; function renderBorneTag(borneBy: BorneBy | null | undefined) { if (!borneBy) return -; const compactLabel: Record = { company: '羚牛', customer: '客户', pending: '待核', }; return ( {compactLabel[borneBy]} ); } function summaryBorneBy(bearer: 'cust' | 'lingniu', customerName: string): BorneBy { if (customerName === '车辆异动') return 'pending'; return bearer === 'cust' ? 'customer' : 'company'; } function mobileProvinceLabel(province: string) { return province === 'all' ? '全国' : province.slice(0, 2); } interface BiYearSelectProps { value: number; onChange: (year: number) => void; } function BiYearSelect({ value, onChange }: BiYearSelectProps) { const [isOpen, setIsOpen] = useState(false); const ref = useRef(null); // 提供从 2026 到 2020 年份列表,满足历史多年数据查阅诉求 const years = [2026, 2025, 2024, 2023, 2022, 2021, 2020]; useEffect(() => { function handleClickOutside(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) { setIsOpen(false); } } if (isOpen) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [isOpen]); return (
{isOpen && (
切换数据年份
{years.map((y) => ( ))}
)}
); } /** BI 皮 · 可搜索选择器(禁 V2;穿透筛专用) */ interface BiSearchSelectOption { value: string; label: string; } function BiSearchSelect({ value, onChange, options, allLabel, placeholder = '搜索…', width = 180, disabled = false, }: { value: string; onChange: (next: string) => void; options: BiSearchSelectOption[]; allLabel: string; placeholder?: string; width?: number; disabled?: boolean; }) { const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const ref = useRef(null); const inputRef = useRef(null); useEffect(() => { function onDoc(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); } if (open) document.addEventListener('mousedown', onDoc); return () => document.removeEventListener('mousedown', onDoc); }, [open]); useEffect(() => { if (open) { setQuery(''); requestAnimationFrame(() => inputRef.current?.focus()); } }, [open]); const selectedLabel = value === 'all' ? allLabel : options.find((o) => o.value === value)?.label || allLabel; const filtered = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return options; return options.filter((o) => o.label.toLowerCase().includes(q) || o.value.toLowerCase().includes(q)); }, [options, query]); return (
{open && !disabled && (
setQuery(e.target.value)} placeholder={placeholder} onClick={(e) => e.stopPropagation()} />
{filtered.map((o) => ( ))} {filtered.length === 0 &&
无匹配项
}
)}
); } interface OverviewTrendsProps { year: number; fleetScope: FleetScope; verifyScope: 'all' | 'verified'; onOpenDrill: (label: string) => void; onOpenCustomerBill: (custName: string) => void; onOpenStationBill: (stName: string, province: string) => void; overview: any; } function OverviewTrendsDashboard({ year, fleetScope, verifyScope, onOpenDrill, onOpenCustomerBill, onOpenStationBill, overview }: OverviewTrendsProps) { const overviewRangeText = overview?.range?.startDate && overview?.range?.endDate ? `${overview.range.startDate} 至 ${overview.range.endDate}` : `${year}-01-01 至 ${year}-12-31`; // 月度加氢量数据 (根据年份、车辆范围、核对范围加权) const monthlyData = useMemo(() => { if (overview) { return overview.monthly.map((item: any) => ({ month: `${Number(String(item.month).slice(-2))}月`, ownKg: Number(item.lingniuKg) || 0, extKg: Number(item.externalKg) || 0, totalKg: Number(item.totalKg) || 0, })); } const is2026 = year === 2026; const is2025 = year === 2025; const factor = is2026 ? 1 : is2025 ? 0.85 : 0.7; // 仅已核对:外部车在示意数据中不参与核对 → 外部归零;羚牛约 3/4 已核 const ownVerifyFactor = verifyScope === 'verified' ? 0.75 : 1; const extVerifyFactor = verifyScope === 'verified' ? 0 : 1; const baseMonths = [ { m: '1月', own: 56800, ext: 28400 }, { m: '2月', own: 34600, ext: 17400 }, { m: '3月', own: 75200, ext: 37600 }, { m: '4月', own: 90000, ext: 45000 }, { m: '5月', own: 85300, ext: 42700 }, { m: '6月', own: 78600, ext: 39400 }, { m: '7月', own: 81300, ext: 40700 }, { m: '8月', own: 18600, ext: 9400 }, ]; return baseMonths.map((item) => { let ownKg = Math.round(item.own * factor * ownVerifyFactor); let extKg = Math.round(item.ext * factor * extVerifyFactor); if (fleetScope === 'own') extKg = 0; if (fleetScope === 'external') ownKg = 0; const totalKg = ownKg + extKg; return { month: item.m, ownKg, extKg, totalKg }; }); }, [year, fleetScope, verifyScope, overview]); const maxMonthlyKg = useMemo(() => { return Math.max(...monthlyData.map((d) => d.totalKg), 1); }, [monthlyData]); // 基础客户列表池,用于计算月度客户加氢金额排行 const baseCustomers = [ { name: '嘉兴市乍浦港口经营有限公司', ratio: 0.408 }, { name: '嘉兴益顺冷链物流有限公司', ratio: 0.176 }, { name: '嘉兴智奇供应链管理有限公司', ratio: 0.118 }, { name: '四川群彬物流有限公司', ratio: 0.078 }, { name: '浙江洋井供应链管理有限公司', ratio: 0.053 }, { name: '重庆金时源供应链有限公司', ratio: 0.039 }, { name: '四川拱照物流有限公司', ratio: 0.030 }, { name: '嘉兴羚利供应链科技有限公司', ratio: 0.027 }, { name: '宁波港集装箱运输嘉兴分公司', ratio: 0.023 }, { name: '成都诺和物流有限公司', ratio: 0.015 }, { name: '嘉兴市飞宇物流有限公司', ratio: 0.012 }, { name: '嘉兴港区众通快递有限公司', ratio: 0.010 }, { name: '浙江集佑供应链有限公司', ratio: 0.008 }, { name: '日邮物流(中国)有限公司', ratio: 0.003 }, ]; // 月度收支对比数据及客户加氢金额明细 & 成本支出结构明细 const monthlyRevenueData = useMemo(() => { if (overview) { return overview.monthly.map((item: any) => { const income = Number(item.customerRevenue) || 0; const cost = Number(item.cost) || 0; return { m: `${Number(String(item.month).slice(-2))}月`, income, cost, top9: [], restAmount: 0, restCount: 0, costDetails: [ { label: '客户承担', amount: Number(item.customerCost) || 0 }, { label: '我司承担', amount: Number(item.companyCost) || 0 }, { label: '其他成本', amount: Number(item.otherCost) || 0 }, ], }; }); } const is2026 = year === 2026; const factor = is2026 ? 1 : 0.8; const fleetFactor = fleetScope === 'all' ? 1 : fleetScope === 'own' ? 0.67 : 0.33; const verifyFactor = verifyScope === 'verified' ? (fleetScope === 'external' ? 0 : 0.75) : 1; const scale = factor * fleetFactor * verifyFactor; const base = [ { m: '1月', income: 82000, cost: 78000 }, { m: '2月', income: 38000, cost: 36000 }, { m: '3月', income: 98000, cost: 92000 }, { m: '4月', income: 105000, cost: 99000 }, { m: '5月', income: 112000, cost: 104000 }, { m: '6月', income: 118000, cost: 109000 }, { m: '7月', income: 115000, cost: 108000 }, { m: '8月', income: 16500, cost: 15800 }, ]; return base.map((b) => { const income = Math.round(b.income * scale); const cost = Math.round(b.cost * scale); // 计算每个客户当月收入金额 const rawList = baseCustomers.map((c) => ({ name: c.name, amount: Math.round(income * c.ratio), })); // 按从高到低排序 rawList.sort((x, y) => y.amount - x.amount); // 截取 TOP9 const top9 = rawList.slice(0, 9); const restList = rawList.slice(9); const restAmount = restList.reduce((sum, item) => sum + item.amount, 0); // 计算成本支出结构细项 (包氢、我司承担、物流、运维异动、运维调拨) const costDetails = [ { label: '包氢项目', amount: Math.round(cost * 0.36) }, { label: '我司承担', amount: Math.round(cost * 0.32) }, { label: '物流成本', amount: Math.round(cost * 0.18) }, { label: '运维异动', amount: Math.round(cost * 0.08) }, { label: '运维调拨', amount: Math.round(cost * 0.06) }, ]; return { m: b.m, income, cost, top9, restAmount, restCount: restList.length, costDetails, }; }); }, [year, fleetScope, verifyScope, overview]); const maxRevenueVal = useMemo(() => { return Math.max(...monthlyRevenueData.flatMap((d) => [d.income, d.cost]), 1); }, [monthlyRevenueData]); // Top5 站列表 (带内部 vs 外部堆积;跟随车辆/核对筛选) const topStations = useMemo(() => { if (overview) { const rows = overview.topStations.slice(0, 5).map((item: any, index: number) => ({ rank: index + 1, name: item.name, ownKg: Number(item.lingniuKg) || 0, extKg: Number(item.externalKg) || 0, val: Number(item.kg) || 0, })); const maxVal = Math.max(...rows.map((item: any) => item.val), 1); return rows.map((item: any) => ({ ...item, pct: Math.round(item.val / maxVal * 100) })); } const ownVerifyFactor = verifyScope === 'verified' ? 0.75 : 1; const extVerifyFactor = verifyScope === 'verified' ? 0 : 1; const raw = [ { rank: 1, name: '嘉兴中石化滨海加氢站', ownKg: 163250, extKg: 80411 }, { rank: 2, name: '嘉兴嘉锦加氢站', ownKg: 128020, extKg: 54869 }, { rank: 3, name: '嘉兴嘉燃加氢站', ownKg: 18350, extKg: 9884 }, { rank: 4, name: '桐乡中石化绿能加氢站', ownKg: 15648, extKg: 10432 }, { rank: 5, name: '成都中石化天府机场北站', ownKg: 16050, extKg: 6879 }, ]; return raw .map((st) => { let ownKg = Math.round(st.ownKg * ownVerifyFactor); let extKg = Math.round(st.extKg * extVerifyFactor); if (fleetScope === 'own') extKg = 0; if (fleetScope === 'external') ownKg = 0; const val = ownKg + extKg; return { ...st, ownKg, extKg, val, pct: 100 }; }) .map((st, _, arr) => { const maxVal = Math.max(...arr.map((x) => x.val), 1); return { ...st, pct: Math.round((st.val / maxVal) * 100) }; }); }, [fleetScope, verifyScope, overview]); // 区域维度控制: 按市 ('city') | 按省 ('province') const [regionGranularity, setRegionGranularity] = useState<'province' | 'city'>('city'); // 省份筛选控制: 'all' | '浙江省' | '四川省' | '广东省' | '江苏省' | '湖北省' 等 const [selectedProvince, setSelectedProvince] = useState('all'); const [mobileDetailTab, setMobileDetailTab] = useState<'station' | 'customer'>('station'); const [stationFullscreenOpen, setStationFullscreenOpen] = useState(false); useEffect(() => { if (!stationFullscreenOpen) return; const previousOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; const onKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') setStationFullscreenOpen(false); }; const onFullscreenChange = () => { if (!document.fullscreenElement) setStationFullscreenOpen(false); }; document.addEventListener('keydown', onKeyDown); document.addEventListener('fullscreenchange', onFullscreenChange); return () => { document.body.style.overflow = previousOverflow; document.removeEventListener('keydown', onKeyDown); document.removeEventListener('fullscreenchange', onFullscreenChange); document.documentElement.classList.remove('ehb-landscape-session'); screen.orientation?.unlock?.(); }; }, [stationFullscreenOpen]); const openStationFullscreen = async () => { setStationFullscreenOpen(true); document.documentElement.classList.add('ehb-landscape-session'); const root = document.documentElement; if (root.requestFullscreen && !document.fullscreenElement) { await root.requestFullscreen().catch(() => undefined); } const orientation = screen.orientation as ScreenOrientation & { lock?: (mode: string) => Promise; }; if (orientation?.lock) await orientation.lock('landscape').catch(() => undefined); }; const closeStationFullscreen = () => { setStationFullscreenOpen(false); document.documentElement.classList.remove('ehb-landscape-session'); if (document.fullscreenElement) void document.exitFullscreen().catch(() => undefined); }; // 已有加氢站的省份去重列表 const availableProvinces = useMemo(() => { const list: string[] = ['all']; (overview?.stations ?? []).forEach((st: any) => { const province = st.province; if (province && !list.includes(province)) { list.push(province); } }); return list; }, [overview]); // 根据选定省份精准过滤加氢站列表 const filteredStationList = useMemo(() => { const source = overview ? overview.stations.map((st: any, idx: number) => ({ rank: idx + 1, name: st.name, province: st.province || '未归属', kgT: ((Number(st.kg) || 0) / 1000).toFixed(2), kgPct: Number(st.share) || 0, incomeWan: ((Number(st.customerRevenue) || 0) / 10000).toFixed(2), incomePct: overview.kpis.customerRevenue ? (Number(st.customerRevenue) || 0) / Number(overview.kpis.customerRevenue) * 100 : 0, })) : []; if (selectedProvince === 'all') return source; return source.filter((st: any) => st.province === selectedProvince); }, [selectedProvince, overview]); // 根据过滤结果计算总站数 (全国 65 站基准,按比例联动) const stationCountDisplay = useMemo(() => { if (overview) return `共 ${filteredStationList.length} 站`; if (selectedProvince === 'all') return '共 65 站'; if (selectedProvince === '浙江省') return '共 28 站'; if (selectedProvince === '广东省') return '共 16 站'; if (selectedProvince === '四川省') return '共 11 站'; if (selectedProvince === '江苏省') return '共 7 站'; return `共 ${filteredStationList.length} 站`; }, [selectedProvince, filteredStationList, overview]); // 按市区域占比数据 (规范地级市名称) const cityRegions = [ { label: '嘉兴市', pct: '65.2%', color: CHART_BLUE, dashArray: '155 238', dashOffset: '0' }, { label: '成都市', pct: '7.4%', color: CHART_EXTERNAL, dashArray: '18 238', dashOffset: '-156' }, { label: '佛山市', pct: '3.7%', color: '#35a889', dashArray: '9 238', dashOffset: '-175' }, { label: '昆山市', pct: '2.8%', color: '#f09a61', dashArray: '7 238', dashOffset: '-185' }, { label: '常熟市', pct: '2.2%', color: '#8c7bd6', dashArray: '5 238', dashOffset: '-193' }, { label: '广州市', pct: '2.1%', color: '#d47c9b', dashArray: '5 238', dashOffset: '-199' }, { label: '深圳市', pct: '1.9%', color: '#55aebc', dashArray: '4 238', dashOffset: '-205' }, { label: '无锡市', pct: '1.9%', color: '#8dbd68', dashArray: '4 238', dashOffset: '-210' }, { label: '其他城市', pct: '12.7%', color: '#9aa7b8', dashArray: '30 238', dashOffset: '-215' }, ]; // 按省区域占比数据 const provinceRegions = [ { label: '浙江省', pct: '73.2%', color: CHART_BLUE, dashArray: '175 238', dashOffset: '0' }, { label: '四川省', pct: '11.8%', color: CHART_EXTERNAL, dashArray: '28 238', dashOffset: '-176' }, { label: '广东省', pct: '7.5%', color: '#35a889', dashArray: '18 238', dashOffset: '-205' }, { label: '江苏省', pct: '5.4%', color: '#f09a61', dashArray: '13 238', dashOffset: '-224' }, { label: '其他省份', pct: '2.1%', color: '#9aa7b8', dashArray: '5 238', dashOffset: '-238' }, ]; const liveCityRegions = overview?.regions?.map((item: any, index: number) => ({ label: item.region || '未归属', pct: `${Number(item.share || 0).toFixed(1)}%`, kg: Number(item.kg) || 0, color: [CHART_BLUE, CHART_EXTERNAL, '#35a889', '#f09a61', '#8c7bd6', '#d47c9b', '#55aebc', '#8dbd68', '#9aa7b8'][index % 9], dashArray: `${Math.max(0, Number(item.share || 0) * 2.38)} 238`, dashOffset: '0', })); const liveProvinceRegions = overview ? Object.values(overview.stations.reduce((acc: any, st: any) => { const label = st.province || '未归属'; acc[label] = acc[label] || { label, kg: 0 }; acc[label].kg += Number(st.kg) || 0; return acc; }, {})).sort((a: any, b: any) => b.kg - a.kg).map((item: any, index: number) => { const share = overview.kpis.totalKg ? item.kg / overview.kpis.totalKg * 100 : 0; return { label: item.label, kg: item.kg, pct: `${share.toFixed(1)}%`, color: [CHART_BLUE, CHART_EXTERNAL, '#35a889', '#f09a61', '#9aa7b8'][index % 5], dashArray: `${share * 2.38} 238`, dashOffset: '0' }; }) : null; const uncollapsedRegionBase = regionGranularity === 'province' ? (liveProvinceRegions ?? provinceRegions) : (liveCityRegions ?? cityRegions); const regionLimit = regionGranularity === 'province' ? 4 : 8; const activeRegionBase = overview && uncollapsedRegionBase.length > regionLimit ? [ ...uncollapsedRegionBase.slice(0, regionLimit), { label: '其他', kg: uncollapsedRegionBase.slice(regionLimit).reduce((sum: number, item: any) => sum + Number(item.kg || 0), 0), pct: `${uncollapsedRegionBase.slice(regionLimit).reduce((sum: number, item: any) => sum + parseFloat(item.pct || '0'), 0).toFixed(1)}%`, color: '#9aa7b8', dashArray: '0 238', dashOffset: '0', }, ] : uncollapsedRegionBase; let liveDashCursor = 0; const activeRegions = activeRegionBase.map((region: any) => { if (!overview) return region; const segment = Math.max(0, parseFloat(region.pct) * 2.38); const mapped = { ...region, dashArray: `${segment} 238`, dashOffset: `${-liveDashCursor}` }; liveDashCursor += segment; return mapped; }); return (
{/* 1. 月度加氢量趋势柱图 */}
{year} 年月度加氢量
羚牛车辆羚牛车辆 外部车辆外部车辆 统计范围:{overviewRangeText} · 单位 Kg
{monthlyData.map((d) => { const heightPct = Math.min(100, Math.round((d.totalKg / maxMonthlyKg) * 100)); const ownRatio = d.totalKg > 0 ? Math.round((d.ownKg / d.totalKg) * 100) : 60; const extRatio = Math.max(0, 100 - ownRatio); return (
onOpenDrill(`${year}年${d.month}加氢量`)} style={{ cursor: 'pointer' }} title="查看该月各加氢站内部/外部车辆加氢量" > {/* 悬浮柱状图时显示羚牛车辆、外部车辆加氢量卡片 */}
{year}年{d.month}
羚牛车辆 {d.ownKg.toLocaleString('zh-CN')} Kg
外部车辆 {d.extKg.toLocaleString('zh-CN')} Kg
月度合计 {d.totalKg.toLocaleString('zh-CN')} Kg
{(d.totalKg / 1000).toFixed(1)}k
{d.month}
); })}
{/* 2. 月度收支对比柱图 */}
{year} 年月度收支对比
客户收入 成本支出 统计范围:{overviewRangeText} · 单位 元
{monthlyRevenueData.map((d) => { const incPct = Math.min(100, Math.round((d.income / maxRevenueVal) * 100)); const costPct = Math.min(100, Math.round((d.cost / maxRevenueVal) * 100)); return (
onOpenDrill(`${year}年${d.m}成本支出`)} title="查看该月各加氢站成本支出明细" > {/* 悬浮成本支出时,显示包氢、物流、运维异动等成本结构明细 */}
{year}年{d.m} 成本支出明细 成本构成
{d.costDetails.map((item) => (
{item.label} ¥{item.amount.toLocaleString('zh-CN')}
))}
成本合计 ¥{d.cost.toLocaleString('zh-CN')}
onOpenDrill(`${year}年${d.m}客户收入`)} title="查看该月各加氢站客户收入明细" > {/* 悬浮客户收入时,显示按金额从高到低排列的客户明细 (TOP9 + 其他客户) */}
{year}年{d.m} 客户加氢收入明细 金额高→低
{d.top9.map((item, idx) => (
{idx + 1}. {item.name} ¥{item.amount.toLocaleString('zh-CN')}
))} {d.restCount > 0 && (
10. 其他客户 ({d.restCount}家) ¥{d.restAmount.toLocaleString('zh-CN')}
)}
收入合计 ¥{d.income.toLocaleString('zh-CN')}
{d.m}
); })}
{/* 3 & 4. 下方并排:Top5 站加氢量 + 各区域加氢占比 */}
{/* Top5 站 */}
加氢站加氢量 Top5
羚牛车辆羚牛车辆 外部车辆外部车辆 统计范围:{overviewRangeText} · 单位 Kg
{topStations.map((st) => { const ownRatio = Math.round((st.ownKg / st.val) * 100); const extRatio = 100 - ownRatio; return (
onOpenDrill(`加氢站客户量:${st.name}`)} style={{ cursor: 'pointer' }} title="查看该加氢站内部/外部车辆加氢总量" > 2 ? 'is-sub' : ''}`}>{st.rank} {st.name}
{/* 精细高保真 Hover 悬浮弹出卡片 */}
{st.name}
羚牛车辆 {st.ownKg.toLocaleString('zh-CN')} Kg ({ownRatio}%)
外部车辆 {st.extKg.toLocaleString('zh-CN')} Kg ({extRatio}%)
加氢总量 {st.val.toLocaleString('zh-CN')} Kg
{st.val.toLocaleString('zh-CN')}
); })}
{/* 各区域加氢占比 (支持按省 / 按市快速切换) */}
各区域加氢占比
{activeRegions.map((reg) => ( onOpenDrill( `区域${regionGranularity === 'city' ? '市' : '省'}:${reg.label}`, ) } > {`查看${reg.label}各加氢站加氢总量与占比`} ))}
年合计
{overview ? `${(Number(overview.kpis.totalKg || 0) / 1000).toFixed(2)}T` : '697.17T'}
{activeRegions.map((reg) => (
onOpenDrill(`区域${regionGranularity === 'city' ? '市' : '省'}:${reg.label}`) } style={{ cursor: 'pointer' }} title={`查看${reg.label}各加氢站加氢总量与占比`} >
{reg.label}
{reg.pct}
))}
数据明细
{/* 5. 趋势图下方:加氢站加氢汇总表 (支持区域按省筛选切换) */}
加氢站加氢汇总
{/* 区域省份切换控制 (仅展示已有加氢站的省份) */}
{availableProvinces.map((prov) => ( ))}
统计范围:{overviewRangeText} · {stationCountDisplay}
{filteredStationList.map((st, idx) => ( onOpenStationBill(st.name, st.province)} style={{ cursor: 'pointer' }} title="查看加氢量、占比、氢费收入和收入占比" > ))}
# 加氢站(查看明细) 所属省份 加氢量 占比 氢费收入 收入占比
{idx + 1} {st.name}{' '} 查看 › {st.province} {st.kgT} T
{st.kgPct.toFixed(1)}%
¥{st.incomeWan} 万元
{st.incomePct.toFixed(1)}%
{/* 6. 趋势图下方:客户费用汇总表 (Top 30) */}
客户费用汇总
统计范围:{overviewRangeText} · 共 {overview?.customers?.length ?? 30} 家
{(overview ? overview.customers.map((item: any, index: number) => ({ rank: index + 1, name: item.name, bearer: item.bearer === 'company' ? 'lingniu' : 'cust', kgT: ((Number(item.kg) || 0) / 1000).toFixed(2), costWan: ((Number(item.cost) || 0) / 10000).toFixed(2), receivable: `¥${((Number(item.customerRevenue) || 0) / 10000).toFixed(2)} 万元`, })) : []).map((cust: any) => ( onOpenCustomerBill(cust.name)} style={{ cursor: 'pointer' }} title="查看承担方、加氢量、成本支出与收款明细" > ))}
# 客户(查看明细) 承担方 加氢量 成本支出 应收
{cust.rank} {cust.name}{' '} 查看 › {renderBorneTag(summaryBorneBy(cust.bearer, cust.name))} {cust.kgT} T ¥{cust.costWan} 万元 {cust.receivable}
{stationFullscreenOpen ? (
加氢站加氢汇总 统计范围:{overviewRangeText} · {stationCountDisplay}
{availableProvinces.map((prov) => ( ))}
{filteredStationList.map((st, idx) => ( ))}
#加氢站所属省份加氢量占比氢费收入收入占比操作
{idx + 1} {st.name} {st.province} {st.kgT} T {st.kgPct.toFixed(1)}% ¥{st.incomeWan} 万元 {st.incomePct.toFixed(1)}%
) : null}
); } function RegionRemainderModal({ kind, items, onSelect, onClose, }: { kind: '市' | '省'; items: Array<{ label: string; kg: number; share: number }>; onSelect: (label: string) => void; onClose: () => void; }) { const totalKg = items.reduce((sum, item) => sum + item.kg, 0); return (
event.stopPropagation()}>
其他{kind === '市' ? '城市' : '省份'}明细
点击区域继续查看其加氢站明细
归并区域{items.length} 个
归并加氢量{(totalKg / 1000).toFixed(2)} T
{items.map((item) => ( onSelect(item.label)} style={{ cursor: 'pointer' }} title={`查看${item.label}加氢站明细`}> ))}
区域加氢量 (Kg)全局占比操作
{item.label} {item.kg.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {item.share.toFixed(1)}% 继续下钻 ›
); } /** * 嵌入 bi-next `#hydrogen/overview`: * - 壳 / KPI / 洞察对齐宿主 zip 视觉 * - 独立功能块:三维度 + 站月/客户汇总 + 订单明细 · 禁用 OneOS V2 */ export const EnergyBiBoardApp: React.FC = () => { const [boardScope, setBoardScope] = useState('global'); const [hostView, setHostView] = useState('overview'); const [year, setYear] = useState(DEFAULT_YEAR); const [fleetScope, setFleetScope] = useState('all'); const [verifyScope, setVerifyScope] = useState<'all' | 'verified'>('all'); const [filtersOpen, setFiltersOpen] = useState(false); const [dimFilter, setDimFilter] = useState(null); const [statsTab, setStatsTab] = useState('siteMonth'); const [stationId, setStationId] = useState(null); const [stationLabel, setStationLabel] = useState(null); const [customerId, setCustomerId] = useState(null); const [customerLabel, setCustomerLabel] = useState(null); const [liveOverview, setLiveOverview] = useState(null); const [livePendingOverview, setLivePendingOverview] = useState(null); const [liveMeta, setLiveMeta] = useState(null); const [liveError, setLiveError] = useState(null); const [liveLoading, setLiveLoading] = useState(true); const [liveReloadToken, setLiveReloadToken] = useState(0); // KPI 点击下钻 Modal 状态 const [kpiDrillType, setKpiDrillType] = useState(null); // 头部加氢站占比 → 加氢量排名下拉 const [stationRankOpen, setStationRankOpen] = useState(false); const stationRankRef = useRef(null); // 客户账单专属下钻 Modal 状态 (客户 → 日期 → 车牌加氢记录) const [selectedBillCustomer, setSelectedBillCustomer] = useState(null); // 加氢站账单专属下钻 Modal 状态(按日经营汇总 → 单笔加氢明细) const [selectedStationForDrill, setSelectedStationForDrill] = useState<{ name: string; province: string } | null>(null); // 按日视角日期区间状态 (提升至顶层供标题旁时间范围联动) const localIsoDate = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; const [dailyEndDate, setDailyEndDate] = useState(() => localIsoDate(new Date())); const [dailyStartDate, setDailyStartDate] = useState(() => { const date = new Date(); date.setDate(date.getDate() - 14); return localIsoDate(date); }); const [dailyRangePreset, setDailyRangePreset] = useState('15days'); const [dailyFleetType, setDailyFleetType] = useState('all'); const [dailyReloadToken, setDailyReloadToken] = useState(0); const [dailyRefreshing, setDailyRefreshing] = useState(false); useEffect(() => { let active = true; setLiveLoading(true); setLiveError(null); setLiveMeta(null); setLiveOverview(null); setLivePendingOverview(null); const vehicleScope = fleetScope === 'own' ? 'lingniu' : fleetScope; Promise.all([ fetchH2BiMeta(), fetchH2BiOverview({ year, vehicleScope, verifyScope, regionGranularity: 'city' }), fetchH2BiOverview({ year, vehicleScope, verifyScope: 'unverified', regionGranularity: 'city' }), ]).then(([meta, overview, pendingOverview]) => { if (!active) return; setLiveMeta(meta); setLiveOverview(overview); setLivePendingOverview(pendingOverview); setLiveLoading(false); }).catch((error) => { if (!active) return; setLiveMeta(null); setLiveOverview(null); setLivePendingOverview(null); setLiveError(error instanceof Error ? error.message : String(error)); setLiveLoading(false); }); return () => { active = false; }; }, [year, fleetScope, verifyScope, liveReloadToken]); const handleDailyPresetChange = (preset: DailyRangePreset) => { setDailyRangePreset(preset); const end = new Date(); const start = new Date(end); if (preset === 'week') { const weekday = (end.getDay() + 6) % 7; start.setDate(end.getDate() - weekday); } else if (preset === 'month') { start.setDate(1); } else if (preset === '15days') { start.setDate(end.getDate() - 14); } else { return; } setDailyStartDate(localIsoDate(start)); setDailyEndDate(localIsoDate(end)); }; // 全局看板时间范围(单站模式不展示:维度不同,由站内查询日期自管) const timeRangeLabel = '统计时间范围'; const timeRangeText = useMemo(() => { // 单站内容由 StationDailyApp 使用 dailyStartDate/dailyEndDate 驱动;不能沿用全局年度 overview 范围。 if (boardScope === 'station' || hostView === 'daily') { return `${dailyStartDate} 至 ${dailyEndDate}`; } if (liveOverview?.range?.startDate && liveOverview?.range?.endDate) { return `${liveOverview.range.startDate} 至 ${liveOverview.range.endDate}`; } return `${year}-01-01 至 ${year}-12-31`; }, [boardScope, hostView, dailyStartDate, dailyEndDate, year, liveOverview]); const mobileView: HostView = boardScope === 'station' ? 'daily' : hostView; const activeMobileFleet = mobileView === 'daily' ? dailyFleetType : fleetScope; const mobileFilterCount = mobileView === 'daily' ? (dailyRangePreset === 'custom' ? 1 : 0) + (dailyFleetType === 'all' ? 0 : 1) : (verifyScope === 'verified' ? 1 : 0) + (fleetScope === 'all' ? 0 : 1); const handleMobileViewChange = (nextView: HostView) => { if (boardScope === 'station' && nextView === 'overview') { setBoardScope('global'); setHostView('overview'); } else { setHostView(nextView); } setFiltersOpen(false); }; const clearEntity = () => { setStationId(null); setStationLabel(null); setCustomerId(null); setCustomerLabel(null); }; const rows = useMemo( () => filterOrders(MOCK_ORDERS, year, verifyScope, fleetScope), [year, verifyScope, fleetScope], ); const hostKpi = useMemo(() => { if (!liveOverview) return computeHostKpi(rows, year, MOCK_ORDERS, HOST_KPI); const k = liveOverview.kpis; return { totalKgT: Number((k.totalKg / 1000).toFixed(2)), companyKgT: Number((k.companyBearingKg / 1000).toFixed(2)), customerKgT: Number((k.customerBearingKg / 1000).toFixed(2)), pendingKgT: Number((k.otherBearingKg / 1000).toFixed(2)), totalFeeWan: Number((k.totalCost / 10000).toFixed(2)), companyFeeWan: Number((k.companyCost / 10000).toFixed(2)), customerFeeWan: Number((k.customerCost / 10000).toFixed(2)), pendingFeeWan: Number((k.otherCost / 10000).toFixed(2)), profitWan: Number((k.customerGrossProfit / 10000).toFixed(2)), incomeWan: Number((k.customerRevenue / 10000).toFixed(2)), costWan: Number((k.customerCost / 10000).toFixed(2)), monthKgT: Number((k.monthKg / 1000).toFixed(2)), monthFeeWan: Number((k.monthCost / 10000).toFixed(2)), monthYearPct: Number(k.monthShareOfRange || 0).toFixed(2), dayKg: Number(k.todayKg || 0), dayFee: Number(k.todayCost || 0), dayMonthPct: Number(k.todayShareOfMonth || 0).toFixed(2), }; }, [rows, year, liveOverview]); const totalKgForShare = hostKpi.totalKgT || 1; const bearerShares = { company: Number(((hostKpi.companyKgT / totalKgForShare) * 100).toFixed(2)), customer: Number(((hostKpi.customerKgT / totalKgForShare) * 100).toFixed(2)), pending: Number(((hostKpi.pendingKgT / totalKgForShare) * 100).toFixed(2)), }; // 加氢站加氢量排名(高→低),跟随年份/车辆/核对筛选 const stationRankList = useMemo(() => { if (liveOverview) { const list = liveOverview.stations.map((st: any) => ({ name: st.name, province: st.province || '未归属', kg: Number(st.kg) || 0, })).filter((st: any) => st.kg > 0).sort((a: any, b: any) => b.kg - a.kg); const maxKg = list[0]?.kg || 1; const totalKg = list.reduce((sum: number, item: any) => sum + item.kg, 0) || 1; return list.map((st: any, index: number) => ({ ...st, rank: index + 1, barPct: Math.round(st.kg / maxKg * 100), sharePct: Math.round(st.kg / totalKg * 1000) / 10 })); } // 没有真实数据时返回空列表:不再用原型演示数据按年份/归属系数编造排名。 return []; }, [liveOverview]); const top5SharePct = useMemo(() => { const top5 = stationRankList.slice(0, 5).reduce((s, x) => s + x.kg, 0); const total = stationRankList.reduce((s, x) => s + x.kg, 0) || 1; return Math.round((top5 / total) * 1000) / 10; }, [stationRankList]); useEffect(() => { if (!stationRankOpen) return; function onDoc(e: MouseEvent) { if (stationRankRef.current && !stationRankRef.current.contains(e.target as Node)) { setStationRankOpen(false); } } document.addEventListener('mousedown', onDoc); return () => document.removeEventListener('mousedown', onDoc); }, [stationRankOpen]); const dims = useMemo(() => costDimCards(rows), [rows]); const risk = livePendingOverview ? { count: Number(livePendingOverview.kpis.recordCount || 0), amount: Number(livePendingOverview.kpis.totalCost || 0), } : { count: 0, amount: 0 }; const unitProfitYuan = hostKpi.totalKgT > 0 ? ((hostKpi.profitWan * 10000) / (hostKpi.totalKgT * 1000)).toFixed(2) : '—'; const unverifiedWan = (risk.amount / 10000).toFixed(2); const liveMonthComparison = useMemo(() => { if (!liveOverview?.monthly?.length) return null; let points = liveOverview.monthly.filter((item: any) => Number(item.totalKg) > 0); const endMonth = String(liveOverview.range?.endDate || '').slice(0, 7); if (points.length > 2 && points[points.length - 1]?.month === endMonth) points = points.slice(0, -1); const current = points[points.length - 1]; const previous = points[points.length - 2]; if (!current || !previous || !Number(previous.totalKg)) return null; const value = (Number(current.totalKg) - Number(previous.totalKg)) / Number(previous.totalKg) * 100; return { value, label: `${Number(String(current.month).slice(-2))}月较${Number(String(previous.month).slice(-2))}月`, }; }, [liveOverview]); const companyScoped = useMemo( () => companyRowsForStats(rows, dimFilter), [rows, dimFilter], ); /** 客户归属表:无维度筛时看全量归属;有维度筛时只看对应我司成本行 */ const customerSource = useMemo(() => { if (!dimFilter) return rows; return companyScoped; }, [rows, dimFilter, companyScoped]); const siteMonthRows = useMemo(() => stationMonthAgg(companyScoped), [companyScoped]); const customerRows = useMemo(() => customerAttrAgg(customerSource), [customerSource]); const detailRows = useMemo(() => { let list = companyScoped; if (stationId) list = list.filter((r) => r.stationId === stationId); if (customerId) list = list.filter((r) => r.customerId === customerId); return list; }, [companyScoped, stationId, customerId]); const remainderRegions = useMemo(() => { if (!liveOverview) return { city: [], province: [] }; const city = (liveOverview.regions ?? []) .map((item: any) => ({ label: String(item.region || '未归属'), kg: Number(item.kg) || 0, share: Number(item.share) || 0, })) .sort((left: any, right: any) => right.kg - left.kg) .slice(8); const totalKg = Number(liveOverview.kpis?.totalKg) || 0; const provinceMap = new Map(); (liveOverview.stations ?? []).forEach((station: any) => { const label = String(station.province || '未归属'); provinceMap.set(label, (provinceMap.get(label) || 0) + (Number(station.kg) || 0)); }); const province = [...provinceMap.entries()] .map(([label, kg]) => ({ label, kg, share: totalKg ? kg / totalKg * 100 : 0 })) .sort((left, right) => right.kg - left.kg) .slice(4); return { city, province }; }, [liveOverview]); const externalEmpty = fleetScope === 'external' && rows.length === 0; const [updatedAt, setUpdatedAt] = useState('—'); useEffect(() => { if (liveOverview?.watermark?.ledgerAt) setUpdatedAt(liveOverview.watermark.ledgerAt); }, [liveOverview]); useEffect(() => { if (boardScope === 'station' && !stationId && liveOverview?.stations?.length) { setStationId(String(liveOverview.stations[0].id)); setStationLabel(liveOverview.stations[0].name); } }, [boardScope, liveOverview, stationId]); const handleRefreshData = () => { if (mobileView === 'daily') { setDailyRefreshing(true); setDailyReloadToken((value) => value + 1); return; } setLiveReloadToken((value) => value + 1); }; if (liveLoading || liveError || !liveOverview) { return (
{liveError ? (
数据服务暂时不可用

后端接口请求失败,本页已停止展示业务数据,避免将缓存值或模拟值误认为真实结果。

{liveError}
) : (
氢能数据加载中 请稍候
)}
); } return (
{liveLoading ? (
氢能数据加载中 请稍候
) : null} {liveError ?
统计数据加载失败:{liveError}
: null}
羚牛氢能 BI / 氢能

氢能经营看板

实时运营 {boardScope === 'global' ? ( 📅 {timeRangeLabel}:{timeRangeText} ) : null}
统计时间范围:{timeRangeText}
数据更新:{updatedAt}
范围
{boardScope === 'global' ? (
全局视图
) : null}
查看方式
{boardScope === 'global' ? { setYear(y); clearEntity(); }} /> : null} {boardScope === 'global' ? : null} {boardScope === 'global' ? : null}
当前范围:{boardScope === 'global' ? '全部站点' : '当前站点'} · {mobileView === 'daily' ? `${dailyStartDate} 至 ${dailyEndDate}` : `${activeMobileFleet === 'all' ? '全部车辆' : activeMobileFleet === 'own' ? '羚牛车辆' : '外部车辆'} · ${verifyScope === 'all' ? '全量订单' : '仅已核对订单'}`}
{mobileView === 'daily' ? (
) : null} {filtersOpen ? (
{mobileView === 'overview' ? ( <>
订单范围
车辆范围
) : ( <>
{ setDailyStartDate(val); setDailyRangePreset('custom'); }} /> { setDailyEndDate(val); setDailyRangePreset('custom'); }} />
{boardScope === 'global' ?
车辆范围
: null} )}
) : null}
{boardScope === 'station' ? ( { setDailyStartDate(value); setDailyRangePreset('custom'); }} onEndDateChange={(value: string) => { setDailyEndDate(value); setDailyRangePreset('custom'); }} refreshToken={dailyReloadToken} onLoadingChange={setDailyRefreshing} /> ) : ( <> {hostView === 'daily' ? ( ) : ( <> {/* 总览视角筛选条 (包含年份选择、核对筛选、车辆归属及刷新,样式与按日视角全面对齐) */}
{ setYear(y); clearEntity(); }} />
{updatedAt}

核心经营指标

累计经营概览 {year} 年累计
我司{hostKpi.companyKgT} T({bearerShares.company}%)
客户{hostKpi.customerKgT} T({bearerShares.customer}%)
待核准{hostKpi.pendingKgT} T({bearerShares.pending}%)
} tone="blue" label="累计加氢量" value={hostKpi.totalKgT} unit="T" parts={[ { label: '我司承担', value: `${hostKpi.companyKgT} T` }, { label: '客户承担', value: `${hostKpi.customerKgT} T` }, { label: '待核准', value: `${hostKpi.pendingKgT} T` }, ]} onClick={() => setKpiDrillType('累计加氢量')} /> } tone="blue" label="累计加氢费" prefix="¥" value={hostKpi.totalFeeWan} unit="万" parts={[ { label: '我司承担', value: `¥${hostKpi.companyFeeWan} 万` }, { label: '客户承担', value: `¥${hostKpi.customerFeeWan} 万` }, { label: '待核准', value: `¥${hostKpi.pendingFeeWan} 万` }, ]} onClick={() => setKpiDrillType('累计加氢费')} /> } tone="green" label="加氢利润" prefix="¥" value={hostKpi.profitWan} unit="万" left={`收入 ¥${hostKpi.incomeWan} 万`} right={`成本 ¥${hostKpi.costWan} 万`} onClick={() => setKpiDrillType('加氢利润')} />
} tone="amber" label="本月加氢量" value={hostKpi.monthKgT} unit="T" left={`加氢费 ¥${hostKpi.monthFeeWan} 万`} right={`占累计 ${hostKpi.monthYearPct}%`} onClick={() => setKpiDrillType('本月加氢')} /> } tone="purple" label="今日加氢量" value={hostKpi.dayKg} unit="Kg" left={`加氢费 ¥${hostKpi.dayFee.toLocaleString('zh-CN')}`} right={`占本月 ${hostKpi.dayMonthPct}%`} onClick={() => setKpiDrillType('本日加氢')} />
经营诊断
月度环比 {liveMonthComparison ? `${liveMonthComparison.value >= 0 ? '+' : ''}${liveMonthComparison.value.toFixed(1)}%` : '—'} {liveMonthComparison?.label ?? '暂无可比月份'}
单公斤毛利 ¥{unitProfitYuan}/kg 按累计加氢量计算
))} {stationRankList.length === 0 && (
当前筛选下暂无站点数据
)}
)}
经营诊断 展开查看 收起
月度环比{liveMonthComparison ? `${liveMonthComparison.value >= 0 ? '+' : ''}${liveMonthComparison.value.toFixed(1)}%` : '—'}{liveMonthComparison?.label ?? '暂无可比月份'}
单公斤毛利¥{unitProfitYuan}/kg按累计加氢量计算
{/* 经营趋势图表大盘:月度加氢量、月度收支对比、Top5站加氢量、各区域加氢占比 */} setKpiDrillType(lbl)} onOpenCustomerBill={(custName) => setSelectedBillCustomer(custName)} onOpenStationBill={(stName, prov) => setSelectedStationForDrill({ name: stName, province: prov })} /> )} )} {/* KPI 点击下钻数据来源穿透 Modal */} {boardScope === 'global' && kpiDrillType === '区域市:其他' && ( setKpiDrillType(`区域市:${label}`)} onClose={() => setKpiDrillType(null)} /> )} {boardScope === 'global' && kpiDrillType === '区域省:其他' && ( setKpiDrillType(`区域省:${label}`)} onClose={() => setKpiDrillType(null)} /> )} {boardScope === 'global' && kpiDrillType && !/^区域(?:市|省):其他$/.test(kpiDrillType) && ( setKpiDrillType(null)} /> )} {/* 客户账单专属下钻 Modal (客户 → 日期 → 车牌加氢记录) */} {boardScope === 'global' && selectedBillCustomer && ( setSelectedBillCustomer(null)} /> )} {/* 加氢站账单专属下钻 Modal(按日经营汇总 → 单笔加氢明细) */} {boardScope === 'global' && selectedStationForDrill && ( station.name === selectedStationForDrill.name)?.id ?? null, }} onClose={() => setSelectedStationForDrill(null)} /> )}
); }; function PlugZapHint() { return ; } function HostKpi({ icon, tone, label, value, prefix, unit, left, right, parts, onClick, }: { icon: React.ReactNode; tone: 'blue' | 'green' | 'amber' | 'purple' | 'cyan'; label: string; value: React.ReactNode; prefix?: string; unit?: string; left?: string; right?: string; parts?: Array<{ label: string; value: string }>; onClick?: () => void; }) { return (
{ if (onClick && (event.key === 'Enter' || event.key === ' ')) onClick(); }} role={onClick ? 'button' : undefined} tabIndex={onClick ? 0 : undefined} title="点击展开站 → 客户 → 车牌数据来源穿透明细" >
{label} 查看明细 {icon}
{prefix && {prefix}} {value} {unit && {unit}}
{parts?.length ? parts.map((part) => ( {part.label}{part.value} )) : <>{left}{right}}
); } function makeVehicleOrders( certPrefix: string, fleetCategory: FleetCategory, mode: 'verified' | 'unverified' | 'partial', totalKg: number, ) { const isOwn = fleetCategory === 'own'; const unitPrice = 4.5; const list = [ { time: '2026-08-08 09:15:00', factor: 1.2, source: 'api' as const }, { time: '2026-08-08 14:30:00', factor: 0.9, source: 'api' as const }, { time: '2026-08-07 11:20:00', factor: 1.1, source: 'station_report' as const }, { time: '2026-08-06 16:45:00', factor: 0.8, source: 'lingniu_report' as const }, { time: '2026-08-05 10:10:00', factor: 1.05, source: 'api' as const }, { time: '2026-08-04 15:25:00', factor: 0.95, source: 'station_report' as const }, { time: '2026-08-03 08:50:00', factor: 1.15, source: 'api' as const }, { time: '2026-08-02 17:05:00', factor: 0.85, source: 'lingniu_report' as const }, { time: '2026-08-01 12:40:00', factor: 1.0, source: 'station_report' as const }, { time: '2026-07-31 09:30:00', factor: 0.9, source: 'api' as const }, ]; return list.map((item, idx) => { const seq = String(idx + 1).padStart(2, '0'); const kg = Math.round(((totalKg / 100) * item.factor) * 10) / 10; const certNo = item.source === 'api' ? `API-20260808-${certPrefix}-${seq}` : item.source === 'station_report' ? `ST-20260808-${certPrefix}-${seq}` : `LN-20260808-${certPrefix}-${seq}`; let verifyStatus: 'verified' | 'unverified' | null = null; if (isOwn) { if (mode === 'verified') verifyStatus = 'verified'; else if (mode === 'unverified') verifyStatus = 'unverified'; else { verifyStatus = idx % 2 === 0 ? 'verified' : 'unverified'; } } return { orderId: `ORD-20260808-${certPrefix}-${seq}`, time: item.time, kg, unitPrice, amount: Math.round(kg * unitPrice), source: item.source, certNo, verifyStatus, }; }); } function computeVehicleVerifyStatus( orders: { verifyStatus?: 'verified' | 'unverified' | null }[], fleetCategory: FleetCategory, ): 'verified' | 'unverified' | 'partial' | null { if (fleetCategory !== 'own') return null; if (!orders || orders.length === 0) return 'unverified'; const verifiedCount = orders.filter((o) => o.verifyStatus === 'verified').length; const unverifiedCount = orders.filter((o) => o.verifyStatus === 'unverified').length; if (verifiedCount > 0 && unverifiedCount > 0) return 'partial'; if (verifiedCount > 0 && unverifiedCount === 0) return 'verified'; return 'unverified'; } /** 站/客户层:按下属车辆核对态汇总。全已核→已核对;全未核→未核对;有混杂或任一带部分→部分核对。外部车不参与。 */ function aggregateVehiclesVerifyStatus( vehicles: { orders: { verifyStatus?: 'verified' | 'unverified' | null }[]; fleetCategory: FleetCategory; plateNo?: string }[], ): 'verified' | 'unverified' | 'partial' | null { const statuses = vehicles .map((vh) => computeVehicleVerifyStatus(vh.orders, vh.fleetCategory)) .filter((s): s is 'verified' | 'unverified' | 'partial' => s !== null); if (statuses.length === 0) return null; if (statuses.every((s) => s === 'verified')) return 'verified'; if (statuses.every((s) => s === 'unverified')) return 'unverified'; return 'partial'; } /** 穿透表标签悬浮说明 */ const FLEET_TAG_TIP = { own: '羚牛车辆:车牌可识别,且归属羚牛自有/合作车队', external: '外部车辆:非羚牛车队;无法识别车牌的归入「无车牌」并标外部车辆', } as const; const SOURCE_TAG_TIP: Record<'api' | 'station_report' | 'lingniu_report', string> = { api: 'API接入:加氢数据由接口自动归集,可按接口流水追溯', station_report: '站点上报:由加氢站报送的加氢数据', lingniu_report: '羚牛上报:从 OneOS 归集的加氢记录', }; const VERIFY_TAG_TIP = { verified: '已核对:范围内加氢订单均已完成核对', partial: '部分核对:范围内既有已核对,也有未核对订单', unverified: '未核对:范围内加氢订单均尚未核对', order_verified: '已核对:该笔加氢订单已完成核对', order_unverified: '未核对:该笔加氢订单尚未核对', external_skip: '外部车辆不参与核对,故无核对状态', } as const; function renderFleetTag(isOwnFleet: boolean) { return ( {isOwnFleet ? '羚牛车辆' : '外部车辆'} ); } function renderSourceTag(source: 'api' | 'station_report' | 'lingniu_report' | string) { const key = source === 'api' || source === 'station_report' || source === 'lingniu_report' ? source : 'api'; const mod = key === 'api' ? 'api' : key === 'station_report' ? 'station' : 'lingniu'; return ( {SOURCE_TYPE_LABEL[key] || source} ); } function renderOrderVerifyTag( fleetCategory: FleetCategory, verifyStatus: 'verified' | 'unverified' | null | undefined, ) { if (fleetCategory !== 'own') { return ( - ); } if (verifyStatus === 'verified') { return ( 已核对 ); } return ( 未核对 ); } function renderVehicleVerifyTag( fleetCategory: FleetCategory, status: 'verified' | 'unverified' | 'partial' | null, ) { if (fleetCategory !== 'own' || status === null) { return ( - ); } if (status === 'verified') { return ( 已核对 ); } if (status === 'partial') { return ( 部分核对 ); } return ( 未核对 ); } function renderAggVerifyTag( status: 'verified' | 'unverified' | 'partial' | null, titlePrefix = '下属车辆', ) { if (status === 'verified') { return ( 已核对 ); } if (status === 'partial') { return ( 部分核对 ); } if (status === 'unverified') { return ( 未核对 ); } return ( - ); } type DrillPeriodMode = 'month' | 'custom'; function resolveDrillPeriod(mode: DrillPeriodMode, monthValue: string, customStart: string, customEnd: string) { if (mode === 'custom') { const start = customStart <= customEnd ? customStart : customEnd; const end = customStart <= customEnd ? customEnd : customStart; return { start, end, label: `${start} 至 ${end}` }; } const [yearText, monthText] = monthValue.split('-'); const lastDay = new Date(Number(yearText), Number(monthText), 0).getDate(); const monthEnd = `${monthValue}-${String(lastDay).padStart(2, '0')}`; const dataSnapshotEnd = '2026-08-08'; const end = monthEnd > dataSnapshotEnd ? dataSnapshotEnd : monthEnd; return { start: `${monthValue}-01`, end, label: `${monthValue}-01 至 ${end}` }; } function StationMonthTable({ rows, activeId, onOpen, }: { rows: ReturnType; activeId: string | null; onOpen: (id: string, label: string) => void; }) { if (!rows.length) return
本筛选下暂无站月发生额
; return (
{rows.map((r, i) => ( onOpen(r.stationId, r.stationName)} > ))}
# 加氢站 月份 发生额 加氢量 未核金额
{i + 1} {r.stationName} {r.month} {formatYuan(r.amount)} {formatKg(r.quantityKg)} {formatYuan(r.unverifiedAmount)}
); } function CustomerAttrTable({ rows, activeId, onOpen, }: { rows: ReturnType; activeId: string | null; onOpen: (id: string, label: string) => void; }) { if (!rows.length) return
本筛选下暂无客户归属
; return (
{rows.map((r, i) => ( onOpen(r.customerId, r.customerName)} > ))}
# 客户 承担方 加氢量 我司成本 未核
{i + 1} {r.customerName} {r.borneLabel} {formatKg(r.quantityKg)} {formatYuan(r.companyCost)} {formatYuan(r.unverifiedAmount)}
); } function OrderTable({ rows }: { rows: H2OrderRow[] }) { if (!rows.length) { return
本筛选下暂无我司成本明细
; } return (
{rows.map((r) => ( ))}
时间 加氢站 车牌 客户 加氢量 金额 成本维度 核对 来源
{r.occurredAt} {r.stationName} {r.plateNo} {r.customerName} {formatKg(r.quantityKg)} {formatYuan(r.amount)} {costDimLabel(r)} {r.verifyStatus === 'verified' ? '已核对' : '未核对'} {SOURCE_LABEL[r.source]}
); } interface BiCustomDatePickerProps { label: string; value: string; // YYYY-MM-DD | YYYY-MM | YYYY onChange: (dateStr: string) => void; } function BiCustomDatePicker({ label, value, onChange }: BiCustomDatePickerProps) { const [isOpen, setIsOpen] = useState(false); const containerRef = useRef(null); // 面板视图模式:'day' | 'month' | 'year' const [pickerMode, setPickerMode] = useState<'day' | 'month' | 'year'>('day'); const parsedDate = useMemo(() => { const parts = value.split('-'); const year = parseInt(parts[0], 10) || 2026; const month = parseInt(parts[1], 10) || 8; const day = parseInt(parts[2], 10) || 1; return { year, month, day }; }, [value]); const [viewYear, setViewYear] = useState(parsedDate.year); const [viewMonth, setViewMonth] = useState(parsedDate.month); useEffect(() => { if (isOpen) { setViewYear(parsedDate.year); setViewMonth(parsedDate.month); const parts = value.split('-'); if (parts.length === 1 && value.length === 4) { setPickerMode('year'); } else if (parts.length === 2) { setPickerMode('month'); } else { setPickerMode('day'); } } }, [isOpen, value, parsedDate]); useEffect(() => { function handleClickOutside(e: MouseEvent) { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setIsOpen(false); } } if (isOpen) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [isOpen]); const yearsList = [2026, 2025, 2024, 2023, 2022, 2021, 2020]; const monthsList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; const handleSelectYear = (y: number, e: React.MouseEvent) => { e.stopPropagation(); setViewYear(y); if (pickerMode === 'year') { onChange(`${y}`); setIsOpen(false); } else { setPickerMode(pickerMode === 'day' ? 'month' : 'day'); } }; const handleSelectMonth = (m: number, e: React.MouseEvent) => { e.stopPropagation(); setViewMonth(m); const mm = m < 10 ? `0${m}` : `${m}`; if (pickerMode === 'month') { onChange(`${viewYear}-${mm}`); setIsOpen(false); } else { setPickerMode('day'); } }; const handleSelectDay = (d: number, e: React.MouseEvent) => { e.stopPropagation(); const mm = viewMonth < 10 ? `0${viewMonth}` : `${viewMonth}`; const dd = d < 10 ? `0${d}` : `${d}`; onChange(`${viewYear}-${mm}-${dd}`); setIsOpen(false); }; const handlePrev = (e: React.MouseEvent) => { e.stopPropagation(); if (pickerMode === 'day') { if (viewMonth === 1) { setViewYear((prev) => prev - 1); setViewMonth(12); } else { setViewMonth((prev) => prev - 1); } } else { setViewYear((prev) => prev - 1); } }; const handleNext = (e: React.MouseEvent) => { e.stopPropagation(); if (pickerMode === 'day') { if (viewMonth === 12) { setViewYear((prev) => prev + 1); setViewMonth(1); } else { setViewMonth((prev) => prev + 1); } } else { setViewYear((prev) => prev + 1); } }; const daysInMonth = new Date(viewYear, viewMonth, 0).getDate(); const firstDayWeek = new Date(viewYear, viewMonth - 1, 1).getDay(); const daysArray = Array.from({ length: daysInMonth }, (_, i) => i + 1); const emptyPrefixSlots = Array.from({ length: firstDayWeek }, (_, i) => i); return (
setIsOpen(!isOpen)} > {label} {value}
{isOpen && (
{/* 1. 粒度模式选择器: 按日 | 按月 | 按年 */}
{/* 2. 标头快速切年月 */}
{pickerMode === 'day' && ( )}
{/* 3. 日视图 */} {pickerMode === 'day' && ( <>
{emptyPrefixSlots.map((s) => ( ))} {daysArray.map((d) => { const mm = viewMonth < 10 ? `0${viewMonth}` : `${viewMonth}`; const dd = d < 10 ? `0${d}` : `${d}`; const isSelected = value === `${viewYear}-${mm}-${dd}`; return ( ); })}
)} {/* 4. 月视图 */} {pickerMode === 'month' && (
{monthsList.map((m) => { const mm = m < 10 ? `0${m}` : `${m}`; const isSelected = value === `${viewYear}-${mm}` || (value.split('-').length === 3 && parsedDate.year === viewYear && parsedDate.month === m); return ( ); })}
)} {/* 5. 年视图 */} {pickerMode === 'year' && (
{yearsList.map((y) => { const isSelected = value === `${y}` || parsedDate.year === y; return ( ); })}
)}
)}
); } interface HostDailyViewProps { updatedAt?: string; onRefresh?: () => void; startDate: string; endDate: string; onStartDateChange: (val: string) => void; onEndDateChange: (val: string) => void; rangePreset: DailyRangePreset; onRangePresetChange: (preset: DailyRangePreset) => void; fleetType: FleetCategoryFilter; onFleetTypeChange: (fleet: FleetCategoryFilter) => void; } function HostDailyView({ updatedAt, onRefresh, startDate, endDate, onStartDateChange, onEndDateChange, rangePreset, onRangePresetChange, fleetType, onFleetTypeChange, }: HostDailyViewProps) { const [remoteDaily, setRemoteDaily] = useState(null); const [remoteAllDaily, setRemoteAllDaily] = useState(null); const [remotePreviousTotal, setRemotePreviousTotal] = useState(null); const [remoteTrees, setRemoteTrees] = useState>({}); const [remoteDailyError, setRemoteDailyError] = useState(null); // 上方时间预设连动 KPI 卡片标题 const kpiRangeTitle = useMemo(() => { if (rangePreset === 'week') return '本周加氢量'; if (rangePreset === 'month') return '本月加氢量'; if (rangePreset === '15days') return '近 15 天加氢量'; return '自定义区间加氢量'; }, [rangePreset]); // 日期归一化转换(兼容手选 年 YYYY、月 YYYY-MM、日 YYYY-MM-DD) const normalizeDateStr = (dateStr: string, isEnd: boolean) => { if (!dateStr) return isEnd ? '9999-12-31' : '0000-01-01'; const parts = dateStr.split('-'); if (parts.length === 1) { return isEnd ? `${parts[0]}-12-31` : `${parts[0]}-01-01`; } if (parts.length === 2) { const y = parseInt(parts[0], 10); const m = parseInt(parts[1], 10); if (isEnd) { const lastDay = new Date(y, m, 0).getDate(); const dd = lastDay < 10 ? `0${lastDay}` : `${lastDay}`; return `${parts[0]}-${parts[1]}-${dd}`; } return `${parts[0]}-${parts[1]}-01`; } return dateStr; }; const normStart = useMemo(() => normalizeDateStr(startDate, false), [startDate]); const normEnd = useMemo(() => normalizeDateStr(endDate, true), [endDate]); useEffect(() => { let active = true; const vehicleScope = fleetType === 'own' ? 'lingniu' : fleetType; const currentStart = new Date(`${normStart}T00:00:00`); const currentEnd = new Date(`${normEnd}T00:00:00`); const rangeDays = Math.max(1, Math.round((currentEnd.getTime() - currentStart.getTime()) / 86400000) + 1); const previousEnd = new Date(currentStart); previousEnd.setDate(previousEnd.getDate() - 1); const previousStart = new Date(currentStart); previousStart.setDate(previousStart.getDate() - rangeDays); const toIso = (value: Date) => `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`; const base = { year: Number(normEnd.slice(0, 4)), startDate: normStart, endDate: normEnd, vehicleScope, verifyScope: 'all' as const }; setRemoteDailyError(null); Promise.all([ fetchH2BiDaily(base), vehicleScope === 'all' ? fetchH2BiDaily(base) : fetchH2BiDaily({ ...base, vehicleScope: 'all' }), fetchH2BiDaily({ ...base, startDate: toIso(previousStart), endDate: toIso(previousEnd) }), ]).then(([daily, allDaily, previous]) => { if (!active) return; setRemoteDaily(daily); setRemoteAllDaily(allDaily); setRemotePreviousTotal(Number(previous.kpis.totalKg) || 0); }).catch((error) => { if (!active) return; setRemoteDailyError(error instanceof Error ? error.message : String(error)); }); return () => { active = false; }; }, [normStart, normEnd, fleetType]); // 1. 根据 startDate & endDate 动态生成或提取指定日期范围内的全量每日加氢数据列表 const dateFilteredList = useMemo(() => { if (remoteDaily) { return remoteDaily.days.map((item: any) => { const tree = remoteTrees[item.date]; const stations = tree?.stations?.map((station: any) => ({ stationId: String(station.id), stationName: station.name, stationType: 'self_use', unitPrice: station.kg ? station.cost / station.kg : 0, quantityKg: Number(station.kg) || 0, amountYuan: Number(station.cost) || 0, customers: station.customers.map((customer: any) => ({ customerId: String(customer.id), customerName: customer.name, customerCategory: 'internal', quantityKg: Number(customer.kg) || 0, amountYuan: Number(customer.cost) || 0, vehicles: [], })), })) ?? []; return { date: item.date, shortDate: item.date.slice(5), unitPrice: item.kg ? item.cost / item.kg : 0, quantityKg: Number(item.kg) || 0, amountYuan: Number(item.cost) || 0, momPct: item.chainPct === null || item.chainPct === undefined ? null : Number(item.chainPct), stations, _stationCount: Number(item.stationCount) || 0, _ownKg: Number(item.lingniuKg) || 0, _extKg: Number(item.externalKg) || 0, }; }); } return getDailyDataForRange(normStart, normEnd); }, [normStart, normEnd, remoteDaily, remoteTrees]); // 2. 根据 fleetType 过滤出对应车辆归属下的加氢列表 ('all' 时包含内部与外部合并显示) const filteredDailyList = useMemo(() => { if (remoteDaily) return dateFilteredList; return filterDailyDataByFleet(dateFilteredList, fleetType); }, [dateFilteredList, fleetType, remoteDaily]); // 2. 动态计算关联的 KPI 及柱图统计数据 const dailyKpis = useMemo(() => { const calculated = calculateDailyKpis(filteredDailyList, fleetType); if (!remoteDaily) return calculated; const nonZero = filteredDailyList.filter((item: any) => item.quantityKg > 0); const peak = nonZero.slice().sort((a: any, b: any) => b.quantityKg - a.quantityKg)[0]; const trough = nonZero.slice().sort((a: any, b: any) => a.quantityKg - b.quantityKg)[0]; return { ...calculated, totalQuantityKg: Number(remoteDaily.kpis.totalKg) || 0, dailyAvgKgNum: Number(remoteDaily.kpis.averageDailyKg) || 0, dailyAvgKg: `${Number(remoteDaily.kpis.averageDailyKg || 0).toLocaleString('zh-CN')} Kg`, activeDays: `${Number(remoteDaily.kpis.activeDays) || 0} 天`, stationCount: Number(remoteDaily.kpis.stationCount) || 0, ownKg: filteredDailyList.reduce((sum: number, item: any) => sum + item._ownKg, 0), extKg: filteredDailyList.reduce((sum: number, item: any) => sum + item._extKg, 0), peakDayLabel: peak ? `${peak.shortDate} · ${Math.round(peak.quantityKg).toLocaleString('zh-CN')}` : '-', troughDayLabel: trough ? `${trough.shortDate} · ${Math.round(trough.quantityKg).toLocaleString('zh-CN')}` : '-', zeroDaysCount: Math.max(0, filteredDailyList.length - Number(remoteDaily.kpis.activeDays || 0)), }; }, [filteredDailyList, fleetType, remoteDaily]); const [peakDate = '-', peakValue = '-'] = dailyKpis.peakDayLabel.split(' · '); const [troughDate = '-', troughValue = '-'] = dailyKpis.troughDayLabel.split(' · '); const rangeFleetKpis = useMemo(() => { if (remoteAllDaily) { return { ownKg: remoteAllDaily.days.reduce((sum: number, item: any) => sum + Number(item.lingniuKg || 0), 0), extKg: remoteAllDaily.days.reduce((sum: number, item: any) => sum + Number(item.externalKg || 0), 0), }; } return calculateDailyKpis(dateFilteredList, 'all'); }, [dateFilteredList, remoteAllDaily]); const previousPeriod = useMemo(() => { if (remotePreviousTotal !== null) { const changeKg = Math.round((dailyKpis.totalQuantityKg - remotePreviousTotal) * 10) / 10; const changePct = remotePreviousTotal > 0 ? Math.round(changeKg / remotePreviousTotal * 1000) / 10 : 0; return { changeKg, changePct }; } const toDate = (value: string) => new Date(`${value}T00:00:00`); const toIso = (value: Date) => { const year = value.getFullYear(); const month = String(value.getMonth() + 1).padStart(2, '0'); const day = String(value.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; const currentStart = toDate(normStart); const currentEnd = toDate(normEnd); const rangeDays = Math.max(1, Math.round((currentEnd.getTime() - currentStart.getTime()) / 86400000) + 1); const previousEnd = new Date(currentStart); previousEnd.setDate(previousEnd.getDate() - 1); const previousStart = new Date(currentStart); previousStart.setDate(previousStart.getDate() - rangeDays); const previousItems = filterDailyDataByFleet( getDailyDataForRange(toIso(previousStart), toIso(previousEnd)), fleetType, ); const previousTotalKg = Math.round( previousItems.reduce((sum, item) => sum + item.quantityKg, 0) * 10, ) / 10; const changeKg = Math.round((dailyKpis.totalQuantityKg - previousTotalKg) * 10) / 10; const changePct = previousTotalKg > 0 ? Math.round((changeKg / previousTotalKg) * 1000) / 10 : 0; return { changeKg, changePct }; }, [dailyKpis.totalQuantityKg, fleetType, normEnd, normStart, remotePreviousTotal]); const totalNetworkStations = 65; const stationCoveragePct = Math.round((dailyKpis.stationCount / totalNetworkStations) * 1000) / 10; const fleetTotalKg = rangeFleetKpis.ownKg + rangeFleetKpis.extKg; const ownFleetPct = fleetTotalKg > 0 ? Math.round((rangeFleetKpis.ownKg / fleetTotalKg) * 1000) / 10 : 0; const externalFleetPct = fleetTotalKg > 0 ? Math.round((rangeFleetKpis.extKg / fleetTotalKg) * 1000) / 10 : 0; // 深层折叠/展开状态 const [expandedDate, setExpandedDate] = useState('2026-08-08'); // 默认展开最新一天 const [expandedStations, setExpandedStations] = useState>({ '2026-08-08_st-jx': true, // 默认展开嘉兴站,演示效果 }); const [expandedCustomers, setExpandedCustomers] = useState>({ '2026-08-08_st-jx_c-ln': true, // 默认展开羚牛客户,直观展示车辆与数据来源 }); // 点击柱状图后的高亮锚点状态 const [highlightedDate, setHighlightedDate] = useState(null); // 保证当前选中的展开日期始终在当前过滤数据集中 useEffect(() => { if (filteredDailyList.length > 0 && (!expandedDate || !filteredDailyList.some((d) => d.date === expandedDate))) { setExpandedDate(filteredDailyList[0].date); } }, [filteredDailyList, expandedDate]); useEffect(() => { if (!remoteDaily || !expandedDate || remoteTrees[expandedDate]) return; const vehicleScope = fleetType === 'own' ? 'lingniu' : fleetType; fetchH2BiDailyTree(expandedDate, { vehicleScope, verifyScope: 'all', stationId: null }) .then((tree) => setRemoteTrees((current) => ({ ...current, [expandedDate]: tree }))) .catch((error) => setRemoteDailyError(error instanceof Error ? error.message : String(error))); }, [expandedDate, fleetType, remoteDaily, remoteTrees]); const maxQty = useMemo(() => { if (!filteredDailyList.length) return 4000; return Math.max(...filteredDailyList.map((d) => d.quantityKg), 3000); }, [filteredDailyList]); const totalSum = useMemo(() => { const sum = filteredDailyList.reduce((acc, item) => acc + item.quantityKg, 0); return sum.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }, [filteredDailyList]); /** 点击柱状图上的柱子:展开该日、锚点平滑滚动并高亮 */ const handleBarClick = (date: string) => { setExpandedDate(date); setHighlightedDate(date); setTimeout(() => { const el = document.getElementById(`daily-row-${date}`); if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, 60); setTimeout(() => { setHighlightedDate((prev) => (prev === date ? null : prev)); }, 2000); }; const toggleStation = (dateKey: string, stationId: string, e: React.MouseEvent) => { e.stopPropagation(); const key = `${dateKey}_${stationId}`; setExpandedStations((prev) => ({ ...prev, [key]: !prev[key] })); }; const toggleCustomer = (dateKey: string, stationId: string, customerId: string, e: React.MouseEvent) => { e.stopPropagation(); const key = `${dateKey}_${stationId}_${customerId}`; setExpandedCustomers((prev) => ({ ...prev, [key]: !prev[key] })); }; /** 导出按日加氢数据明细为 Excel (.xlsx) */ const handleExportExcel = () => { const aoa: (string | number)[][] = [ ['日期', '加氢站名称', '加氢站类型', '客户名称', '客户属性', '加氢时间', '车牌号', '车辆归属', '数据来源', '核对状态', '单价(元/Kg)', '加氢量(Kg)', '加氢金额(元)', '预充值余额'], ]; filteredDailyList.forEach((d) => { d.stations.forEach((st) => { const stationPrecharge = st.prechargeBalance ?? (st.stationId.includes('jx') ? 128500 : st.stationId.includes('tx') ? 86200 : 45000); st.customers?.forEach((cust) => { const isInternalCust = cust.customerCategory === 'internal' || cust.customerId === 'c-ln'; cust.vehicles?.forEach((vh) => { aoa.push([ d.date, st.stationName, STATION_TYPE_LABEL[st.stationType] || st.stationType, cust.customerName, isInternalCust ? '羚牛车辆' : '外部车辆', vh.time, vh.plateNo || '无车牌(散车)', vh.fleetCategory === 'own' ? '羚牛车辆' : '外部车辆', SOURCE_TYPE_LABEL[vh.source] || vh.source, vh.fleetCategory === 'own' ? (DAILY_VERIFY_LABEL[vh.verifyStatus || 'unverified'] || '未核对') : '-', vh.unitPrice, vh.quantityKg, vh.amountYuan, `¥${stationPrecharge.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} (对接站点管理)`, ]); }); }); }); }); const fileDateStr = `${startDate.replace(/[/]/g, '')}-${endDate.replace(/[/]/g, '')}`; const fleetName = fleetType === 'all' ? '全部车辆' : fleetType === 'own' ? '羚牛车辆' : '外部车辆'; exportAoaSheet(aoa, `每日加氢数据明细_${fleetName}_${fileDateStr}.xlsx`, '每日加氢明细'); }; return (
{remoteDailyError ?
按日统计加载失败:{remoteDailyError}
: null} {/* 1. 顶栏时间/范围筛选器 */}
{ onStartDateChange(val); onRangePresetChange('custom'); }} /> { onEndDateChange(val); onRangePresetChange('custom'); }} />
{updatedAt && ( {updatedAt} )}
区间车辆构成 羚牛车辆 {rangeFleetKpis.ownKg.toLocaleString('zh-CN')} Kg({ownFleetPct}%) · 外部车辆 {rangeFleetKpis.extKg.toLocaleString('zh-CN')} Kg({externalFleetPct}%)
{/* 2. 4卡 Bento KPI */}
{kpiRangeTitle}
{dailyKpis.totalQuantityKg.toLocaleString('zh-CN')} Kg
{dailyKpis.dateRange}
日均加氢量
{dailyKpis.dailyAvgKgNum.toLocaleString('zh-CN')} Kg
{dailyKpis.activeDays}有加氢记录
较上一周期 = 0 ? 'is-green' : 'is-amber'}`}>
= 0 ? 'ehb-value-up' : 'ehb-value-down'}`}> {previousPeriod.changePct >= 0 ? '+' : ''}{previousPeriod.changePct}%
{previousPeriod.changeKg >= 0 ? '增加' : '减少'} {Math.abs(previousPeriod.changeKg).toLocaleString('zh-CN')} Kg
活跃加氢站
{dailyKpis.stationCount} / {totalNetworkStations}
覆盖率 {stationCoveragePct}% · 有加氢记录
{/* 3. 每日加氢量堆积柱状图(分别显示羚牛车辆与外部车辆加氢量,点击柱子下锚定位) */}
每日加氢量 (点击柱体下锚定位到对应日期明细) (点击柱体定位)
羚牛车辆羚牛车辆 外部车辆外部车辆
时间单位:日 · 单位 Kg
峰值 {peakDate} {peakValue} Kg
低谷 {troughDate} {troughValue} Kg
零记录 统计区间 {dailyKpis.zeroDaysCount}
‹ 左右滑动查看每日加氢趋势 ›
0 ? Math.min(92, Math.round((dailyKpis.dailyAvgKgNum / maxQty) * 100)) : 50}%`, }} > 均值 {dailyKpis.dailyAvgKg}
{[...filteredDailyList].reverse().map((item) => { // 计算当天羚牛车辆加氢量与外部车辆加氢量 let dayOwnKg = 0; let dayExtKg = 0; item.stations.forEach((st) => { st.customers.forEach((cust) => { cust.vehicles.forEach((vh) => { if (vh.fleetCategory === 'own') { dayOwnKg += vh.quantityKg; } else { dayExtKg += vh.quantityKg; } }); }); }); if (remoteDaily) { dayOwnKg = Number(item._ownKg) || 0; dayExtKg = Number(item._extKg) || 0; } dayOwnKg = Math.round(dayOwnKg * 10) / 10; dayExtKg = Math.round(dayExtKg * 10) / 10; const totalKg = item.quantityKg > 0 ? item.quantityKg : 1; const pct = Math.min(100, Math.round((item.quantityKg / maxQty) * 100)); const ownRatio = Math.round((dayOwnKg / totalKg) * 100); const extRatio = Math.max(0, 100 - ownRatio); const isBarActive = expandedDate === item.date; const tooltipText = `${item.date} 加氢总量 ${Math.round(item.quantityKg).toLocaleString('zh-CN')} Kg\n├─ 羚牛车辆: ${Math.round(dayOwnKg).toLocaleString('zh-CN')} Kg (${ownRatio}%)\n└─ 外部车辆: ${Math.round(dayExtKg).toLocaleString('zh-CN')} Kg (${extRatio}%)\n(点击下锚定位到该日明细)`; return (
handleBarClick(item.date)} >
{Math.round(item.quantityKg)}
{/* 堆积柱体:上部外部车辆,下部羚牛车辆 */}
{dayExtKg > 0 && (
)} {dayOwnKg > 0 && (
)}
{item.shortDate}
); })}
{/* 4. 每日数据明细多层钻取表格 */}
每日加氢数据明细 (可多层下钻:按日 → 加氢站 → 客户 → 车辆及数据源) (可逐级下钻)
{/* 合计行 */} {filteredDailyList.map((row) => { const isDateExpanded = expandedDate === row.date; const isHighlighted = highlightedDate === row.date; return ( {/* Level 1: 日期行 */} setExpandedDate(isDateExpanded ? null : row.date)} > {/* Level 2: 加氢站层 */} {isDateExpanded && row.stations.map((st) => { const stKey = `${row.date}_${st.stationId}`; const isStExpanded = !!expandedStations[stKey]; const stPrecharge = st.prechargeBalance ?? (st.stationId.includes('jx') ? 128500 : st.stationId.includes('tx') ? 86200 : 45000); return ( toggleStation(row.date, st.stationId, e)} > {/* Level 3: 客户层 */} {isStExpanded && st.customers?.map((cust) => { const custKey = `${row.date}_${st.stationId}_${cust.customerId}`; const isCustExpanded = !!expandedCustomers[custKey]; const isInternalCust = cust.customerCategory === 'internal' || cust.customerId === 'c-ln'; return ( toggleCustomer(row.date, st.stationId, cust.customerId, e)} > {/* Level 4: 车辆及数据来源明细层 */} {isCustExpanded && cust.vehicles?.map((vh) => ( ))} ); })} ); })} ); })}
日期 / 加氢站 / 客户 / 车辆明细 单价 (元/Kg) 加氢量 (Kg) 金额 (元) / 环比 预充值余额
合计 {totalSum} 对接站点管理
{(row._stationCount ?? row.stations.length) > 0 ? (isDateExpanded ? '▼' : '►') : '•'} {row.date} ({row._stationCount ?? row.stations.length} 个加氢站) {row.unitPrice.toFixed(2)} {row.quantityKg.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} {row.momPct !== null ? ( {row.momPct > 0 ? `+${row.momPct.toFixed(1)}%` : `${row.momPct.toFixed(1)}%`} ) : ( '-' )} -
{st.customers?.length ? (isStExpanded ? '▼' : '►') : '•'} └ {st.stationName} {st.unitPrice.toFixed(2)} {st.quantityKg.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} ¥{st.amountYuan.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} ¥{stPrecharge.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}
{cust.vehicles?.length ? (isCustExpanded ? '▼' : '►') : '•'} └─ 客户:{cust.customerName} {isInternalCust ? ( 羚牛车辆 {cust.quantityKg.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} Kg ¥{cust.amountYuan.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} ) : ( 外部车辆 {cust.quantityKg.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} Kg ¥{cust.amountYuan.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} )}
- {cust.quantityKg.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} ¥{cust.amountYuan.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} -
└── {vh.plateNo || '无车牌(散车)'} {vh.time.slice(0, 5)} {renderFleetTag(vh.fleetCategory === 'own')} {renderSourceTag(vh.source)} {/* 规则:仅内部车辆(羚牛车辆)展示核对状态;外部车辆不参与核对 */} {vh.fleetCategory === 'own' && vh.verifyStatus && ( {renderOrderVerifyTag(vh.fleetCategory, vh.verifyStatus)} )} {vh.unitPrice.toFixed(2)} {vh.quantityKg.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} ¥{vh.amountYuan.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} -
); }