import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Activity, Calendar, ChevronDown, ChevronLeft, ChevronRight, Download, Fuel, RefreshCw, Search, Shield, TrendingDown, TrendingUp, Truck, Wallet, X, Zap, } from 'lucide-react'; import { downloadExcelAoa } from '../../common/download-xls'; import { SOURCE_LABEL, companyRowsForStats, computeHostKpi, costDimCards, costDimLabel, customerAttrAgg, filterOrders, formatKg, formatYuan, pendingAmount, 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 type { FleetScope, HostView, H2OrderRow } from './types'; import { StationDailyApp } from '../energy-h2-station-daily/StationDailyApp'; import '../energy-h2-station-daily/styles.css'; import './styles/energy-bi-board.css'; type BoardScope = 'global' | 'station'; type StatsTab = 'siteMonth' | 'customer'; 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 &&
无匹配项
}
)}
); } // 站加氢汇总假数据 (带省份归属,与用户截图高保真100%对齐) const MOCK_STATION_SUMMARY_LIST = [ { rank: 1, name: '嘉兴中石化滨海加氢站', province: '浙江省', kgT: '243.66', kgPct: 35.0, incomeWan: '52.66', incomePct: 7.1 }, { rank: 2, name: '嘉兴嘉锦加氢站', province: '浙江省', kgT: '182.89', kgPct: 26.2, incomeWan: '119.53', incomePct: 16.1 }, { rank: 3, name: '嘉兴嘉燃加氢站', province: '浙江省', kgT: '28.23', kgPct: 4.0, incomeWan: '61.47', incomePct: 8.3 }, { rank: 4, name: '桐乡中石化绿能加氢站', province: '浙江省', kgT: '26.08', kgPct: 3.7, incomeWan: '35.76', incomePct: 4.8 }, { rank: 5, name: '成都中石化天府机场高速北站加氢站', province: '四川省', kgT: '22.93', kgPct: 3.3, incomeWan: '68.62', incomePct: 9.2 }, { rank: 6, name: '花桥中石油加氢站', province: '江苏省', kgT: '19.71', kgPct: 2.8, incomeWan: '35.15', incomePct: 4.7 }, { rank: 7, name: '常熟嘉化加氢站', province: '江苏省', kgT: '15.56', kgPct: 2.2, incomeWan: '25.81', incomePct: 3.5 }, { rank: 8, name: '嘉善中石化站前路加氢站', province: '浙江省', kgT: '14.61', kgPct: 2.1, incomeWan: '25.34', incomePct: 3.4 }, { rank: 9, name: '嘉兴东方港湾加氢站', province: '浙江省', kgT: '13.45', kgPct: 1.9, incomeWan: '7.01', incomePct: 0.9 }, { rank: 10, name: '成都中石化天府机场高速南站加氢站', province: '四川省', kgT: '12.86', kgPct: 1.8, incomeWan: '38.46', incomePct: 5.2 }, { rank: 11, name: '成都国氢华通加氢站', province: '四川省', kgT: '12.09', kgPct: 1.7, incomeWan: '36.26', incomePct: 4.9 }, { rank: 12, name: '广州新锋交通联新加氢站', province: '广东省', kgT: '9.33', kgPct: 1.3, incomeWan: '13.49', incomePct: 1.8 }, { rank: 13, name: '乌鲁木齐隆盛达沙坪加氢站', province: '新疆维吾尔自治区', kgT: '9.33', kgPct: 1.3, incomeWan: '21.12', incomePct: 2.8 }, { rank: 14, name: '佛山豪石油加氢站', province: '广东省', kgT: '8.96', kgPct: 1.3, incomeWan: '26.6', incomePct: 3.6 }, { rank: 15, name: '佛南海羚牛加氢站', province: '广东省', kgT: '7.10', kgPct: 1.0, incomeWan: '3.29', incomePct: 0.4 }, { rank: 16, name: '佛山中石化佛西加氢站', province: '广东省', kgT: '5.83', kgPct: 0.8, incomeWan: '20.32', incomePct: 2.7 }, { rank: 17, name: '广州中石化东明三路加氢站', province: '广东省', kgT: '5.62', kgPct: 0.8, incomeWan: '5.73', incomePct: 0.8 }, { rank: 18, name: '常熟AP银河路加氢站', province: '江苏省', kgT: '4.98', kgPct: 0.7, incomeWan: '20.06', incomePct: 2.7 }, { rank: 19, name: '武汉中石化革新加氢站', province: '湖北省', kgT: '3.95', kgPct: 0.6, incomeWan: '9.45', incomePct: 1.3 }, { rank: 20, name: '韶关韶钢加氢站', province: '广东省', kgT: '3.72', kgPct: 0.5, incomeWan: '10.81', incomePct: 1.5 }, { rank: 21, name: '成都博能加氢站', province: '四川省', kgT: '3.49', kgPct: 0.5, incomeWan: '10.21', incomePct: 1.4 }, { rank: 22, name: '无锡润硕氢能加氢站', province: '江苏省', kgT: '3.22', kgPct: 0.5, incomeWan: '11.98', incomePct: 1.6 }, { rank: 23, name: '上海安亭加氢站', province: '上海市', kgT: '3.10', kgPct: 0.4, incomeWan: '9.82', incomePct: 1.3 }, { rank: 24, name: '昆山千灯加氢站', province: '江苏省', kgT: '2.88', kgPct: 0.4, incomeWan: '8.90', incomePct: 1.2 }, { rank: 25, name: '宁波港区示范加氢站', province: '浙江省', kgT: '2.45', kgPct: 0.3, incomeWan: '7.65', incomePct: 1.0 }, ]; // 客户账单汇总假数据 (Top 30 与用户截图高保真100%对齐) const MOCK_CUSTOMER_SUMMARY_LIST = [ { rank: 1, name: '嘉兴市乍浦港口经营有限公司', bearer: 'cust' as const, kgT: '288.37', costWan: '807.94', receivable: '¥1,987 元' }, { rank: 2, name: '嘉兴益顺冷链物流有限公司', bearer: 'cust' as const, kgT: '38.93', costWan: '136.83', receivable: '¥66.25 万元' }, { rank: 3, name: '嘉兴智奇供应链管理有限公司', bearer: 'cust' as const, kgT: '35.45', costWan: '101.73', receivable: '¥14.44 万元' }, { rank: 4, name: '车辆异动', bearer: 'cust' as const, kgT: '28.10', costWan: '92.96', receivable: '¥2,503 元' }, { rank: 5, name: '四川群彬物流有限公司', bearer: 'cust' as const, kgT: '23.35', costWan: '69.98', receivable: '¥69.98 万元' }, { rank: 6, name: '浙江洋井供应链管理有限公司', bearer: 'cust' as const, kgT: '18.62', costWan: '61.08', receivable: '¥321 元' }, { rank: 7, name: '无锡铭康物流有限公司-1', bearer: 'lingniu' as const, kgT: '16.89', costWan: '58.38', receivable: '¥0 元' }, { rank: 8, name: '上海明纳物流有限公司', bearer: 'lingniu' as const, kgT: '12.44', costWan: '41.4', receivable: '¥0 元' }, { rank: 9, name: '嘉兴中外运物流有限公司', bearer: 'lingniu' as const, kgT: '11.39', costWan: '31.94', receivable: '¥0 元' }, { rank: 10, name: '重庆金时源供应链有限公司', bearer: 'cust' as const, kgT: '11.18', costWan: '27.96', receivable: '¥27.96 万元' }, { rank: 11, name: '四川拱照物流有限公司', bearer: 'cust' as const, kgT: '10.34', costWan: '31', receivable: '¥31 万元' }, { rank: 12, name: '无锡铭康物流有限公司', bearer: 'lingniu' as const, kgT: '9.21', costWan: '31.72', receivable: '¥0 元' }, { rank: 13, name: '嘉兴羚利供应链科技有限公司', bearer: 'cust' as const, kgT: '8.62', costWan: '24.13', receivable: '¥25.85 万元' }, { rank: 14, name: '宁波港集装箱运输有限公司嘉兴分公司', bearer: 'cust' as const, kgT: '8.09', costWan: '22.66', receivable: '¥23.01 万元' }, { rank: 15, name: '成都诺和物流有限公司', bearer: 'cust' as const, kgT: '6.67', costWan: '19.99', receivable: '¥19.99 万元' }, { rank: 16, name: '嘉兴市飞宇物流有限公司', bearer: 'cust' as const, kgT: '6.49', costWan: '18.22', receivable: '¥6,483 元' }, { rank: 17, name: '嘉兴港区众通快递有限公司', bearer: 'cust' as const, kgT: '6.27', costWan: '17.55', receivable: '¥18.81 万元' }, { rank: 18, name: '浙江集佑供应链有限公司', bearer: 'cust' as const, kgT: '6.20', costWan: '17.35', receivable: '¥18.59 万元' }, { rank: 19, name: '日邮物流(中国)有限公司', bearer: 'cust' as const, kgT: '5.90', costWan: '22.11', receivable: '¥22.2 万元' }, { rank: 20, name: '四川邦达蜀运供应链管理有限公司', bearer: 'cust' as const, kgT: '5.21', costWan: '15.63', receivable: '¥15.65 万元' }, { rank: 21, name: '嘉兴古道物流有限公司', bearer: 'cust' as const, kgT: '5.18', costWan: '14.49', receivable: '¥15.53 万元' }, { rank: 22, name: '宁波乐驰物流有限公司', bearer: 'cust' as const, kgT: '5.01', costWan: '14.01', receivable: '¥14.1 万元' }, { rank: 23, name: '广东清运物流专线', bearer: 'cust' as const, kgT: '4.82', costWan: '13.50', receivable: '¥13.50 万元' }, { rank: 24, name: '顺丰冷运嘉兴分线', bearer: 'cust' as const, kgT: '4.21', costWan: '11.78', receivable: '¥11.80 万元' }, { rank: 25, name: '极兔速递冷链事业部', bearer: 'cust' as const, kgT: '3.95', costWan: '11.06', receivable: '¥11.06 万元' }, { rank: 26, name: '武汉捷运货运有限公司', bearer: 'cust' as const, kgT: '3.62', costWan: '10.13', receivable: '¥10.15 万元' }, { rank: 27, name: '成都天府物流二部', bearer: 'cust' as const, kgT: '3.11', costWan: '8.70', receivable: '¥8.70 万元' }, { rank: 28, name: '广州黄埔冷链车队', bearer: 'cust' as const, kgT: '2.85', costWan: '7.98', receivable: '¥8.00 万元' }, { rank: 29, name: '常熟物流储运中心', bearer: 'cust' as const, kgT: '2.40', costWan: '6.72', receivable: '¥6.72 万元' }, { rank: 30, name: '无锡灵通运输公司', bearer: 'cust' as const, kgT: '2.10', costWan: '5.88', receivable: '¥5.90 万元' }, ]; interface OverviewTrendsProps { year: number; fleetScope: FleetScope; verifyScope: 'all' | 'verified'; onOpenDrill: (label: string) => void; onOpenCustomerBill: (custName: string) => void; onOpenStationBill: (stName: string, province: string) => void; } function OverviewTrendsDashboard({ year, fleetScope, verifyScope, onOpenDrill, onOpenCustomerBill, onOpenStationBill }: OverviewTrendsProps) { // 月度加氢量数据 (根据年份、车辆范围、核对范围加权) const monthlyData = useMemo(() => { 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]); 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(() => { 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]); const maxRevenueVal = useMemo(() => { return Math.max(...monthlyRevenueData.flatMap((d) => [d.income, d.cost]), 1); }, [monthlyRevenueData]); // Top5 站列表 (带内部 vs 外部堆积;跟随车辆/核对筛选) const topStations = useMemo(() => { 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]); // 区域维度控制: 按市 ('city') | 按省 ('province') const [regionGranularity, setRegionGranularity] = useState<'province' | 'city'>('city'); // 省份筛选控制: 'all' | '浙江省' | '四川省' | '广东省' | '江苏省' | '湖北省' 等 const [selectedProvince, setSelectedProvince] = useState('all'); // 已有加氢站的省份去重列表 const availableProvinces = useMemo(() => { const list: string[] = ['all']; MOCK_STATION_SUMMARY_LIST.forEach((st) => { if (st.province && !list.includes(st.province)) { list.push(st.province); } }); return list; }, []); // 根据选定省份精准过滤加氢站列表 const filteredStationList = useMemo(() => { if (selectedProvince === 'all') return MOCK_STATION_SUMMARY_LIST; return MOCK_STATION_SUMMARY_LIST.filter((st) => st.province === selectedProvince); }, [selectedProvince]); // 根据过滤结果计算总站数 (全国 65 站基准,按比例联动) const stationCountDisplay = useMemo(() => { 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]); // 按市区域占比数据 (规范地级市名称) const cityRegions = [ { label: '嘉兴市', pct: '65.2%', color: '#0284c7', dashArray: '155 238', dashOffset: '0' }, { label: '成都市', pct: '7.4%', color: '#38bdf8', dashArray: '18 238', dashOffset: '-156' }, { label: '佛山市', pct: '3.7%', color: '#10b981', dashArray: '9 238', dashOffset: '-175' }, { label: '昆山市', pct: '2.8%', color: '#f59e0b', dashArray: '7 238', dashOffset: '-185' }, { label: '常熟市', pct: '2.2%', color: '#8b5cf6', dashArray: '5 238', dashOffset: '-193' }, { label: '广州市', pct: '2.1%', color: '#ec4899', dashArray: '5 238', dashOffset: '-199' }, { label: '深圳市', pct: '1.9%', color: '#06b6d4', dashArray: '4 238', dashOffset: '-205' }, { label: '无锡市', pct: '1.9%', color: '#84cc16', dashArray: '4 238', dashOffset: '-210' }, { label: '其他城市', pct: '12.7%', color: '#94a3b8', dashArray: '30 238', dashOffset: '-215' }, ]; // 按省区域占比数据 const provinceRegions = [ { label: '浙江省', pct: '73.2%', color: '#0284c7', dashArray: '175 238', dashOffset: '0' }, { label: '四川省', pct: '11.8%', color: '#38bdf8', dashArray: '28 238', dashOffset: '-176' }, { label: '广东省', pct: '7.5%', color: '#10b981', dashArray: '18 238', dashOffset: '-205' }, { label: '江苏省', pct: '5.4%', color: '#f59e0b', dashArray: '13 238', dashOffset: '-224' }, { label: '其他省份', pct: '2.1%', color: '#94a3b8', dashArray: '5 238', dashOffset: '-238' }, ]; const activeRegions = regionGranularity === 'province' ? provinceRegions : cityRegions; return (
{/* 1. 月度加氢量趋势柱图 */}
{year} 年月度加氢量
内部客户 外部客户 统计范围:{year === 2026 ? '2026-01-01 至 2026-08-08' : `${year}-01-01 至 ${year}-12-31`} · 单位 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} 年月度收支对比
客户收入 成本支出 统计范围:{year === 2026 ? '2026-01-01 至 2026-08-08' : `${year}-01-01 至 ${year}-12-31`} · 单位 元
{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
内部客户 外部客户 统计范围:{year === 2026 ? '2026-01-01 至 2026-08-08' : `${year}-01-01 至 ${year}-12-31`} · 单位 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}各加氢站加氢总量与占比`} ))}
年合计
697.17T
{activeRegions.map((reg) => (
onOpenDrill(`区域${regionGranularity === 'city' ? '市' : '省'}:${reg.label}`) } style={{ cursor: 'pointer' }} title={`点击钻取${reg.label}各加氢站加氢总量与占比`} >
{reg.label}
{reg.pct}
))}
{/* 5. 趋势图下方:加氢站加氢汇总表 (支持区域按省筛选切换) */}
加氢站加氢汇总
{/* 区域省份切换控制 (仅展示已有加氢站的省份) */}
{availableProvinces.map((prov) => ( ))}
统计范围:{year === 2026 ? '2026-01-01 至 2026-08-08' : `${year}-01-01 至 ${year}-12-31`} · {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) */}
客户账单汇总 (已收 / 未收:等待客户能源账户和对账单打通后获取) (已收未收打通中)
统计范围:{year === 2026 ? '2026-01-01 至 2026-08-08' : `${year}-01-01 至 ${year}-12-31`} · Top 30
{MOCK_CUSTOMER_SUMMARY_LIST.map((cust) => ( onOpenCustomerBill(cust.name)} style={{ cursor: 'pointer' }} title="点击钻取:承担方 / 加氢量 / 成本支出 / 应收 / 已收 / 未收" > ))}
# 客户(点击钻取) 承担方 加氢量 成本支出 应收 已收 未收
{cust.rank} {cust.name} 钻取 › {cust.bearer === 'cust' ? '客户' : '羚牛'} {cust.kgT} T ¥{cust.costWan} 万元 {cust.receivable} 敬请期待 敬请期待
); } /** * 嵌入 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 [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); // 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 [dailyStartDate, setDailyStartDate] = useState('2026-07-25'); const [dailyEndDate, setDailyEndDate] = useState('2026-08-08'); // 全局看板时间范围(单站模式不展示:维度不同,由站内查询日期自管) const timeRangeLabel = '统计时间范围'; const timeRangeText = useMemo(() => { if (hostView === 'daily') { return `${dailyStartDate} 至 ${dailyEndDate}`; } return year === 2026 ? '2026-01-01 至 2026-08-08 11:25' : `${year}-01-01 至 ${year}-12-31 23:59`; }, [hostView, dailyStartDate, dailyEndDate, year]); 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( () => computeHostKpi(rows, year, MOCK_ORDERS, HOST_KPI), [rows, year], ); // 加氢站加氢量排名(高→低),跟随年份/车辆/核对筛选 const stationRankList = useMemo(() => { const yearFactor = year === 2026 ? 1 : year === 2025 ? 0.85 : 0.7; const fleetFactor = fleetScope === 'all' ? 1 : fleetScope === 'own' ? 0.67 : 0.33; const verifyFactor = verifyScope === 'verified' ? (fleetScope === 'external' ? 0 : 0.75) : 1; const scale = yearFactor * fleetFactor * verifyFactor; const list = MOCK_STATION_SUMMARY_LIST.map((st) => ({ name: st.name, province: st.province, kg: Math.round(parseFloat(st.kgT) * 1000 * scale), })) .filter((st) => st.kg > 0) .sort((a, b) => b.kg - a.kg); const maxKg = list[0]?.kg || 1; const totalKg = list.reduce((s, x) => s + x.kg, 0) || 1; return list.map((st, i) => ({ ...st, rank: i + 1, barPct: Math.round((st.kg / maxKg) * 100), sharePct: Math.round((st.kg / totalKg) * 1000) / 10, })); }, [year, fleetScope, verifyScope]); 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]); // 月度波动洞察副文案:当月 / 峰值月 / 月均(与图表月度口径一致,跟随筛选) const monthFluctuationDesc = useMemo(() => { const yearFactor = year === 2026 ? 1 : year === 2025 ? 0.85 : 0.7; 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 }, ]; const months = baseMonths.map((item) => { let ownKg = Math.round(item.own * yearFactor * ownVerifyFactor); let extKg = Math.round(item.ext * yearFactor * extVerifyFactor); if (fleetScope === 'own') extKg = 0; if (fleetScope === 'external') ownKg = 0; return { month: item.m, totalKg: ownKg + extKg }; }); const toT = (kg: number) => Math.round((kg / 1000) * 100) / 100; const current = months[months.length - 1]; const peak = months.reduce((best, cur) => (cur.totalKg > best.totalKg ? cur : best), months[0]); const avgKg = months.reduce((s, m) => s + m.totalKg, 0) / (months.length || 1); return `当月 ${toT(current.totalKg)} T · 峰值${peak.month} ${toT(peak.totalKg)} T · 月均 ${toT(avgKg)} T`; }, [year, fleetScope, verifyScope]); 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 pending = pendingAmount(rows); const risk = unverified(rows); 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 externalEmpty = fleetScope === 'external' && rows.length === 0; const [updatedAt, setUpdatedAt] = useState('2026-08-08 10:12'); const handleRefreshData = () => { const now = new Date(); const timeStr = now.toTimeString().split(' ')[0].slice(0, 5); setUpdatedAt(`2026-08-08 ${timeStr}`); }; return (
期初校准中:期初余额与成本单价未锁定前,本看板仅供内部核对,不作对外真源。
{boardScope === 'global' ? ( 📅 {timeRangeLabel}:{timeRangeText} ) : null}
{boardScope === 'global' ? (
) : null}
{boardScope === 'station' ? ( ) : ( <> {hostView === 'daily' ? ( ) : ( <> {/* 总览视角筛选条 (包含年份选择、核对筛选、车辆归属及刷新,样式与按日视角全面对齐) */}
{ setYear(y); clearEntity(); }} />
{updatedAt}
} tone="blue" label="累计加氢量" value={hostKpi.totalKgT} unit="T" left={`我司 ${hostKpi.companyKgT} T`} right={`客户 ${hostKpi.customerKgT} T`} onClick={() => setKpiDrillType('累计加氢量')} /> } tone="blue" label="累计加氢费" prefix="¥" value={hostKpi.totalFeeWan} unit="万" left={`我司 ¥${hostKpi.companyFeeWan} 万`} right={`客户 ¥${hostKpi.customerFeeWan} 万`} 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('本日加氢')} />
月度加氢异常波动
{hostKpi.monthYearPct > 0 ? `占年 ${hostKpi.monthYearPct}%` : '—'}
{monthFluctuationDesc}
setStationRankOpen((v) => !v)} style={{ cursor: 'pointer' }} title="点击查看加氢站加氢量排名" >
头部加氢站占比
Top5 {top5SharePct}%
累计 {hostKpi.totalKgT} T · 点击展开加氢量排名
{stationRankOpen && (
e.stopPropagation()} role="listbox" aria-label="加氢站加氢量排名" >
加氢站加氢量排名 高 → 低 · 共 {stationRankList.length} 站
{stationRankList.map((st) => ( ))} {stationRankList.length === 0 && (
当前筛选下暂无站点数据
)}
)}
加氢利润率
= 0 ? 'is-pos' : 'is-neg'}`}> {hostKpi.profitRatePct}%
加氢利润 {hostKpi.profitWan} 万 · 收入 {hostKpi.incomeWan} 万
{/* 经营趋势图表大盘:月度加氢量、月度收支对比、Top5站加氢量、各区域加氢占比 */} setKpiDrillType(lbl)} onOpenCustomerBill={(custName) => setSelectedBillCustomer(custName)} onOpenStationBill={(stName, prov) => setSelectedStationForDrill({ name: stName, province: prov })} /> )} )} {/* KPI 点击下钻数据来源穿透 Modal */} {boardScope === 'global' && kpiDrillType && ( setKpiDrillType(null)} /> )} {/* 客户账单专属下钻 Modal (客户 → 日期 → 车牌加氢记录) */} {boardScope === 'global' && selectedBillCustomer && ( setSelectedBillCustomer(null)} /> )} {/* 加氢站账单专属下钻 Modal (加氢站 → 所有日期的加氢量、占比、氢费收入、收入占比) */} {boardScope === 'global' && selectedStationForDrill && ( setSelectedStationForDrill(null)} /> )}
); }; function PlugZapHint() { return ; } function HostKpi({ icon, tone, label, value, prefix, unit, left, right, onClick, }: { icon: React.ReactNode; tone: 'blue' | 'green' | 'amber' | 'purple' | 'cyan'; label: string; value: React.ReactNode; prefix?: string; unit?: string; left: string; right: string; onClick?: () => void; }) { return (
{label} 钻取 {icon}
{prefix && {prefix}} {value} {unit && {unit}}
{left} {right}
); } /** KPI 数据来源穿透 & 站 -> 客户 -> 车牌三级下钻 Modal 弹窗 */ interface KpiDrillModalProps { label: string; year: number; fleetScope: FleetScope; verifyScope: 'all' | 'verified'; onClose: () => void; } 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', factor: 1.2, source: 'api' as const }, { time: '2026-08-08 14:30', factor: 0.9, source: 'api' as const }, { time: '2026-08-07 11:20', factor: 1.1, source: 'station_report' as const }, { time: '2026-08-06 16:45', factor: 0.8, source: 'lingniu_report' as const }, { time: '2026-08-05 10:10', factor: 1.05, source: 'api' as const }, { time: '2026-08-04 15:25', factor: 0.95, source: 'station_report' as const }, { time: '2026-08-03 08:50', factor: 1.15, source: 'api' as const }, { time: '2026-08-02 17:05', factor: 0.85, source: 'lingniu_report' as const }, { time: '2026-08-01 12:40', factor: 1.0, source: 'station_report' as const }, { time: '2026-07-31 09:30', 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 ( - ); } function KpiDrillModal({ label, year, fleetScope, verifyScope, onClose }: KpiDrillModalProps) { const [stationFilter, setStationFilter] = useState('all'); const [customerFilter, setCustomerFilter] = useState('all'); const [plateFilter, setPlateFilter] = useState('all'); const [fleetCategoryFilter, setFleetCategoryFilter] = useState<'all' | 'own' | 'external'>(fleetScope); // 跟随顶栏车辆筛选:打开/切换时同步 useEffect(() => { setFleetCategoryFilter(fleetScope); }, [fleetScope, label]); // 展开折叠状态 const [expandedStations, setExpandedStations] = useState>({ 'st-jx': true, // 默认展开嘉兴站 }); const [expandedCustomers, setExpandedCustomers] = useState>({ 'st-jx_c-zp': true, // 默认展开乍浦港口客户 }); const [expandedVehicles, setExpandedVehicles] = useState>({ 'st-jx_c-zp_浙F88888': true, // 默认展开首辆车,展现单笔订单与部分核对明细 }); const [expandedAllVehicleOrders, setExpandedAllVehicleOrders] = useState>({}); const toggleStation = (stId: string) => { setExpandedStations((prev) => ({ ...prev, [stId]: !prev[stId] })); }; const toggleCustomer = (stId: string, custId: string) => { const key = `${stId}_${custId}`; setExpandedCustomers((prev) => ({ ...prev, [key]: !prev[key] })); }; const toggleVehicle = (stId: string, custId: string, plateNo: string) => { const key = `${stId}_${custId}_${plateNo}`; setExpandedVehicles((prev) => ({ ...prev, [key]: !prev[key] })); }; const toggleShowAllOrders = (vhKey: string) => { setExpandedAllVehicleOrders((prev) => ({ ...prev, [vhKey]: !prev[vhKey] })); }; // 站 -> 客户 -> 车牌 & 接口凭证 高保真全链路数据 const drillStations = useMemo(() => { const is2026 = year === 2026; const factor = is2026 ? 1 : 0.85; return [ { stationId: 'st-jx', stationName: '嘉兴中石化滨海加氢站', stationType: 'self_use' as const, province: '浙江省', totalKg: Math.round(243661 * factor), totalFeeWan: (1096.47 * factor).toFixed(2), customers: [ { customerId: 'c-zp', customerName: '嘉兴市乍浦港口经营有限公司', category: 'internal' as const, totalKg: Math.round(163250 * factor), totalFeeWan: (734.63 * factor).toFixed(2), vehicles: [ { plateNo: '浙F88888', fleetCategory: 'own' as const, source: 'api' as const, certNo: 'API-20260808-9821', count: 142, kg: 42600, amount: 191700, orders: makeVehicleOrders('9821', 'own', 'partial', 42600) }, { plateNo: '浙F66666', fleetCategory: 'own' as const, source: 'api' as const, certNo: 'API-20260808-9822', count: 128, kg: 38400, amount: 172800, orders: makeVehicleOrders('9822', 'own', 'verified', 38400) }, { plateNo: '浙F77777', fleetCategory: 'own' as const, source: 'lingniu_report' as const, certNo: 'LN-REP-202608-012', count: 110, kg: 33000, amount: 148500, orders: makeVehicleOrders('012', 'own', 'verified', 33000) }, { plateNo: '浙F55555', fleetCategory: 'own' as const, source: 'station_report' as const, certNo: 'ST-REP-202608-045', count: 98, kg: 29400, amount: 132300, orders: makeVehicleOrders('045', 'own', 'unverified', 29400) }, { plateNo: '浙F33333', fleetCategory: 'own' as const, source: 'api' as const, certNo: 'API-20260808-9825', count: 66, kg: 19850, amount: 89325, orders: makeVehicleOrders('9825', 'own', 'partial', 19850) }, ], }, { customerId: 'c-ys', customerName: '嘉兴益顺冷链物流有限公司', category: 'external' as const, totalKg: Math.round(54869 * factor), totalFeeWan: (246.91 * factor).toFixed(2), vehicles: [ { plateNo: '浙F12345', fleetCategory: 'external' as const, source: 'api' as const, certNo: 'API-20260808-7712', count: 85, kg: 25500, amount: 114750, orders: makeVehicleOrders('7712', 'external', 'verified', 25500) }, { plateNo: '浙F67890', fleetCategory: 'external' as const, source: 'station_report' as const, certNo: 'ST-REP-202608-088', count: 62, kg: 18600, amount: 83700, orders: makeVehicleOrders('088', 'external', 'verified', 18600) }, { plateNo: '无车牌', fleetCategory: 'external' as const, source: 'station_report' as const, certNo: 'ST-REP-202608-099', count: 36, kg: 10769, amount: 48460, orders: makeVehicleOrders('099', 'external', 'verified', 10769) }, ], }, { customerId: 'c-zq', customerName: '嘉兴智奇供应链管理有限公司', category: 'external' as const, totalKg: Math.round(25542 * factor), totalFeeWan: (114.93 * factor).toFixed(2), vehicles: [ { plateNo: '沪A99881', fleetCategory: 'external' as const, source: 'api' as const, certNo: 'API-20260808-6623', count: 52, kg: 15600, amount: 70200, orders: makeVehicleOrders('6623', 'external', 'verified', 15600) }, { plateNo: '沪A99882', fleetCategory: 'external' as const, source: 'api' as const, certNo: 'API-20260808-6624', count: 33, kg: 9942, amount: 44739, orders: makeVehicleOrders('6624', 'external', 'verified', 9942) }, ], }, ], }, { stationId: 'st-jj', stationName: '嘉兴嘉锦加氢站', stationType: 'external_sale' as const, province: '浙江省', totalKg: Math.round(182889 * factor), totalFeeWan: (823.00 * factor).toFixed(2), customers: [ { customerId: 'c-ln', customerName: '羚牛自营车队', category: 'internal' as const, totalKg: Math.round(128020 * factor), totalFeeWan: (576.09 * factor).toFixed(2), vehicles: [ { plateNo: '浙A88881F', fleetCategory: 'own' as const, source: 'api' as const, certNo: 'API-20260808-1001', count: 180, kg: 54000, amount: 243000, orders: makeVehicleOrders('1001', 'own', 'verified', 54000) }, { plateNo: '浙A88882F', fleetCategory: 'own' as const, source: 'lingniu_report' as const, certNo: 'LN-REP-202608-102', count: 150, kg: 45000, amount: 202500, orders: makeVehicleOrders('102', 'own', 'verified', 45000) }, { plateNo: '浙A88883F', fleetCategory: 'own' as const, source: 'station_report' as const, certNo: 'ST-REP-202608-103', count: 96, kg: 29020, amount: 130590, orders: makeVehicleOrders('103', 'own', 'unverified', 29020) }, ], }, { customerId: 'c-qb', customerName: '四川群彬物流有限公司', category: 'external' as const, totalKg: Math.round(54869 * factor), totalFeeWan: (246.91 * factor).toFixed(2), vehicles: [ { plateNo: '川A77123', fleetCategory: 'external' as const, source: 'api' as const, certNo: 'API-20260808-3301', count: 110, kg: 33000, amount: 148500, orders: makeVehicleOrders('3301', 'external', 'verified', 33000) }, { plateNo: '川A77124', fleetCategory: 'external' as const, source: 'station_report' as const, certNo: 'ST-REP-202608-332', count: 72, kg: 21869, amount: 98410, orders: makeVehicleOrders('332', 'external', 'verified', 21869) }, ], }, ], }, { stationId: 'st-jr', stationName: '嘉兴嘉燃加氢站', stationType: 'self_use' as const, province: '浙江省', totalKg: Math.round(28234 * factor), totalFeeWan: (127.05 * factor).toFixed(2), customers: [ { customerId: 'c-yj', customerName: '浙江洋井供应链管理有限公司', category: 'external' as const, totalKg: Math.round(28234 * factor), totalFeeWan: (127.05 * factor).toFixed(2), vehicles: [ { plateNo: '浙B99112', fleetCategory: 'external' as const, source: 'api' as const, certNo: 'API-20260808-4411', count: 60, kg: 18350, amount: 82575, orders: makeVehicleOrders('4411', 'external', 'verified', 18350) }, { plateNo: '浙B99113', fleetCategory: 'external' as const, source: 'api' as const, certNo: 'API-20260808-4412', count: 32, kg: 9884, amount: 44478, orders: makeVehicleOrders('4412', 'external', 'verified', 9884) }, ], }, ], }, { stationId: 'st-ln', stationName: '桐乡中石化绿能加氢站', stationType: 'external_sale' as const, province: '浙江省', totalKg: Math.round(26080 * factor), totalFeeWan: (117.36 * factor).toFixed(2), customers: [ { customerId: 'c-js', customerName: '重庆金时源供应链有限公司', category: 'external' as const, totalKg: Math.round(26080 * factor), totalFeeWan: (117.36 * factor).toFixed(2), vehicles: [ { plateNo: '渝A66881', fleetCategory: 'external' as const, source: 'api' as const, certNo: 'API-20260808-5501', count: 52, kg: 15648, amount: 70416, orders: makeVehicleOrders('5501', 'external', 'verified', 15648) }, { plateNo: '渝A66882', fleetCategory: 'external' as const, source: 'station_report' as const, certNo: 'ST-REP-202608-552', count: 35, kg: 10432, amount: 46944, orders: makeVehicleOrders('552', 'external', 'verified', 10432) }, ], }, ], }, { stationId: 'st-tf', stationName: '成都中石化天府机场北站', stationType: 'external_sale' as const, province: '四川省', totalKg: Math.round(22929 * factor), totalFeeWan: (103.18 * factor).toFixed(2), customers: [ { customerId: 'c-gz', customerName: '四川拱照物流有限公司', category: 'external' as const, totalKg: Math.round(22929 * factor), totalFeeWan: (103.18 * factor).toFixed(2), vehicles: [ { plateNo: '川A88901', fleetCategory: 'external' as const, source: 'api' as const, certNo: 'API-20260808-8801', count: 53, kg: 16050, amount: 72225, orders: makeVehicleOrders('8801', 'external', 'verified', 16050) }, { plateNo: '川A88902', fleetCategory: 'external' as const, source: 'station_report' as const, certNo: 'ST-REP-202608-8802', count: 23, kg: 6879, amount: 30955, orders: makeVehicleOrders('8802', 'external', 'verified', 6879) }, ], }, ], }, ]; }, [year]); // 按条件过滤(站/客户/车牌可搜索选择 + 车辆归属 + 顶栏核对范围) const filteredStations = useMemo(() => { return drillStations .map((st) => { if (stationFilter !== 'all' && st.stationId !== stationFilter) return null; const filteredCustomers = st.customers .map((cust) => { if (customerFilter !== 'all' && cust.customerId !== customerFilter) return null; const filteredVehicles = cust.vehicles .map((vh) => { if (fleetCategoryFilter !== 'all' && vh.fleetCategory !== fleetCategoryFilter) return null; if (plateFilter !== 'all' && vh.plateNo !== plateFilter) return null; if (verifyScope === 'verified') { // 仅已核对:外部车不参与核对;羚牛车只保留已核订单 if (vh.fleetCategory !== 'own') return null; const orders = vh.orders.filter((o) => o.verifyStatus === 'verified'); if (orders.length === 0) return null; const kg = orders.reduce((s, o) => s + o.kg, 0); const amount = orders.reduce((s, o) => s + o.amount, 0); return { ...vh, orders, count: orders.length, kg, amount }; } return vh; }) .filter(Boolean) as typeof cust.vehicles; if (filteredVehicles.length === 0) return null; return { ...cust, vehicles: filteredVehicles, totalKg: filteredVehicles.reduce((sum, v) => sum + v.kg, 0), totalFeeWan: (filteredVehicles.reduce((sum, v) => sum + v.amount, 0) / 10000).toFixed(2), }; }) .filter(Boolean) as typeof st.customers; if (filteredCustomers.length === 0) return null; return { ...st, customers: filteredCustomers, totalKg: filteredCustomers.reduce((sum, c) => sum + c.totalKg, 0), totalFeeWan: (filteredCustomers.reduce((sum, c) => sum + parseFloat(c.totalFeeWan), 0)).toFixed(2), }; }) .filter(Boolean) as typeof drillStations; }, [drillStations, stationFilter, customerFilter, plateFilter, fleetCategoryFilter, verifyScope]); // 级联选项:站 → 客户 → 车牌(选项池随上级选择收窄) const stationOptions = useMemo( () => drillStations.map((st) => ({ value: st.stationId, label: st.stationName })), [drillStations], ); const customerOptions = useMemo(() => { const map = new Map(); drillStations.forEach((st) => { if (stationFilter !== 'all' && st.stationId !== stationFilter) return; st.customers.forEach((c) => { if (!map.has(c.customerId)) map.set(c.customerId, c.customerName); }); }); return Array.from(map.entries()).map(([value, label]) => ({ value, label })); }, [drillStations, stationFilter]); const plateOptions = useMemo(() => { const set = new Set(); drillStations.forEach((st) => { if (stationFilter !== 'all' && st.stationId !== stationFilter) return; st.customers.forEach((c) => { if (customerFilter !== 'all' && c.customerId !== customerFilter) return; c.vehicles.forEach((vh) => { if (fleetCategoryFilter !== 'all' && vh.fleetCategory !== fleetCategoryFilter) return; set.add(vh.plateNo); }); }); }); return Array.from(set).map((plate) => ({ value: plate, label: /无车牌/.test(plate) ? '无车牌' : plate, })); }, [drillStations, stationFilter, customerFilter, fleetCategoryFilter]); // 上级变更时清掉下级无效选中 useEffect(() => { if (customerFilter !== 'all' && !customerOptions.some((o) => o.value === customerFilter)) { setCustomerFilter('all'); setPlateFilter('all'); } }, [customerOptions, customerFilter]); useEffect(() => { if (plateFilter !== 'all' && !plateOptions.some((o) => o.value === plateFilter)) { setPlateFilter('all'); } }, [plateOptions, plateFilter]); // KPI 穿透列口径:默认量/金额;加氢利润→收入/成本/利润;本月→月量/月费/占年比;本日→日量/日费/占月比;月度柱→站×内外部客户量;收支柱→站×收入/成本 const isProfitDrill = label === '加氢利润'; const isMonthDrill = label === '本月加氢'; const isDayDrill = label === '本日加氢'; const monthMetricMatch = label.match(/^(\d{4})年(\d{1,2})月(加氢量|客户收入|成本支出)$/); const isMonthBarDrill = monthMetricMatch?.[3] === '加氢量'; const isMonthIncomeDrill = monthMetricMatch?.[3] === '客户收入'; const isMonthCostDrill = monthMetricMatch?.[3] === '成本支出'; const isStationMonthFlat = isMonthBarDrill || isMonthIncomeDrill || isMonthCostDrill; const stationCustMatch = label.match(/^加氢站(?:客户量)?:(.+)$/); const isStationCustomerDrill = Boolean(stationCustMatch); const stationCustTarget = stationCustMatch?.[1]?.trim() ?? ''; const regionMatch = label.match(/^区域(市|省):(.+)$/); const isRegionDrill = Boolean(regionMatch); const regionKind = regionMatch?.[1] as '市' | '省' | undefined; const regionLabel = regionMatch?.[2]?.trim() ?? ''; const isFlatOverviewDrill = isStationMonthFlat || isStationCustomerDrill || isRegionDrill; const monthBarIndex = monthMetricMatch ? Number(monthMetricMatch[2]) - 1 : -1; const MONTH_BAR_KG = [85200, 52000, 112800, 135000, 128000, 118000, 122000, 28000]; const monthBarYearKg = MONTH_BAR_KG.reduce((s, n) => s + n, 0) || 1; const monthBarShare = monthBarIndex >= 0 && monthBarIndex < MONTH_BAR_KG.length ? MONTH_BAR_KG[monthBarIndex] / monthBarYearKg : 1; const incomeRatio = HOST_KPI.incomeWan / (HOST_KPI.costWan || 1); const profitRatio = HOST_KPI.profitWan / (HOST_KPI.costWan || 1); const monthShare = HOST_KPI.monthKgT / (HOST_KPI.totalKgT || 1); const dayShare = HOST_KPI.dayKg / ((HOST_KPI.totalKgT || 1) * 1000); const toIncomeYuan = (costYuan: number) => Math.round(costYuan * incomeRatio * 100) / 100; const toProfitYuan = (costYuan: number) => Math.round(costYuan * profitRatio * 100) / 100; const toMonthKg = (kg: number) => Math.round(kg * monthShare * 100) / 100; const toMonthFee = (yuan: number) => Math.round(yuan * monthShare * 100) / 100; const toDayKg = (kg: number) => Math.round(kg * dayShare * 100) / 100; const toDayFee = (yuan: number) => Math.round(yuan * dayShare * 100) / 100; const feeWanToYuan = (wan: string | number) => parseFloat(String(wan)) * 10000; const filteredYearFeeYuan = filteredStations.reduce((s, st) => s + feeWanToYuan(st.totalFeeWan), 0); const filteredMonthFeeYuan = toMonthFee(filteredYearFeeYuan); const toFeeYearPct = (monthFeeYuan: number) => filteredYearFeeYuan > 0 ? Math.round((monthFeeYuan / filteredYearFeeYuan) * 10000) / 100 : 0; const toFeeMonthPct = (dayFeeYuan: number) => filteredMonthFeeYuan > 0 ? Math.round((dayFeeYuan / filteredMonthFeeYuan) * 10000) / 100 : 0; const colCount = isProfitDrill || isMonthDrill || isDayDrill ? 8 : 7; /** 月度柱钻取:各站内部/外部客户加氢量 + 合计(按当月占年份额缩放) */ const stationMonthRows = useMemo(() => { return filteredStations .map((st) => { const internalKg = st.customers .filter((c) => c.category === 'internal') .reduce((s, c) => s + c.totalKg, 0); const externalKg = st.customers .filter((c) => c.category === 'external') .reduce((s, c) => s + c.totalKg, 0); return { stationId: st.stationId, stationName: st.stationName, province: st.province, internalKg: Math.round(internalKg * monthBarShare), externalKg: Math.round(externalKg * monthBarShare), totalKg: Math.round((internalKg + externalKg) * monthBarShare), }; }) .sort((a, b) => b.totalKg - a.totalKg); }, [filteredStations, monthBarShare]); /** 月度收支柱钻取:各站客户收入 / 成本支出 */ const stationRevRows = useMemo(() => { return filteredStations .map((st) => { const monthFee = feeWanToYuan(st.totalFeeWan) * monthBarShare; return { stationId: st.stationId, stationName: st.stationName, province: st.province, incomeYuan: Math.round(monthFee * incomeRatio), costYuan: Math.round(monthFee), }; }) .sort((a, b) => isMonthCostDrill ? b.costYuan - a.costYuan : b.incomeYuan - a.incomeYuan, ); }, [filteredStations, monthBarShare, incomeRatio, isMonthCostDrill]); const monthBarInternalSum = stationMonthRows.reduce((s, r) => s + r.internalKg, 0); const monthBarExternalSum = stationMonthRows.reduce((s, r) => s + r.externalKg, 0); const monthBarTotalSum = stationMonthRows.reduce((s, r) => s + r.totalKg, 0); const monthIncomeSum = stationRevRows.reduce((s, r) => s + r.incomeYuan, 0); const monthCostSum = stationRevRows.reduce((s, r) => s + r.costYuan, 0); /** Top5 / 站名钻取:该站内部客户 vs 外部客户加氢总量 */ const stationCustomerRows = useMemo(() => { if (!isStationCustomerDrill || !stationCustTarget) return []; const st = drillStations.find((s) => s.stationName === stationCustTarget) || drillStations.find( (s) => s.stationName.includes(stationCustTarget.replace(/加氢站$/, '')) || stationCustTarget.includes(s.stationName.replace(/加氢站$/, '')), ); if (!st) { const top = [ { 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 }, ].find((t) => t.name === stationCustTarget || stationCustTarget.includes(t.name.slice(0, 6))); if (!top) return []; return [ { customerId: 'own', customerName: '内部客户合计', category: 'internal' as const, totalKg: top.ownKg }, { customerId: 'ext', customerName: '外部客户合计', category: 'external' as const, totalKg: top.extKg }, ]; } return st.customers .map((c) => ({ customerId: c.customerId, customerName: c.customerName, category: c.category, totalKg: c.totalKg, })) .sort((a, b) => b.totalKg - a.totalKg); }, [drillStations, isStationCustomerDrill, stationCustTarget]); const stationCustInternalKg = stationCustomerRows .filter((r) => r.category === 'internal') .reduce((s, r) => s + r.totalKg, 0); const stationCustExternalKg = stationCustomerRows .filter((r) => r.category === 'external') .reduce((s, r) => s + r.totalKg, 0); const stationCustTotalKg = stationCustInternalKg + stationCustExternalKg; /** 区域(市/省)钻取:区域内各站加氢总量与占比 */ const regionStationRows = useMemo(() => { if (!isRegionDrill || !regionLabel) return []; const CITY_KEYS: Record = { 嘉兴市: ['嘉兴', '桐乡', '嘉善'], 成都市: ['成都'], 佛山市: ['佛山'], 昆山市: ['昆山'], 常熟市: ['常熟'], 广州市: ['广州'], 深圳市: ['深圳'], 无锡市: ['无锡'], }; const matchCity = (name: string, city: string) => { if (city === '其他城市') { const keys = Object.values(CITY_KEYS).flat(); return !keys.some((k) => name.includes(k)); } const keys = CITY_KEYS[city] || [city.replace(/市$/, '')]; return keys.some((k) => name.includes(k)); }; const list = MOCK_STATION_SUMMARY_LIST.filter((st) => { if (regionKind === '省') { if (regionLabel === '其他省份') { return !['浙江省', '四川省', '广东省', '江苏省'].includes(st.province); } return st.province === regionLabel; } return matchCity(st.name, regionLabel); }); const totalKg = list.reduce((s, st) => s + parseFloat(st.kgT) * 1000, 0) || 1; return list .map((st) => { const kg = Math.round(parseFloat(st.kgT) * 1000); return { name: st.name, province: st.province, kg, kgT: st.kgT, pct: Math.round((kg / totalKg) * 10000) / 100, incomeWan: st.incomeWan, }; }) .sort((a, b) => b.kg - a.kg); }, [isRegionDrill, regionKind, regionLabel]); const regionKgSum = regionStationRows.reduce((s, r) => s + r.kg, 0); const renderMetricCells = (kg: number, feeYuan: number) => { if (isProfitDrill) { return ( <> ¥{toIncomeYuan(feeYuan).toLocaleString('zh-CN')} ¥{feeYuan.toLocaleString('zh-CN')} ¥{toProfitYuan(feeYuan).toLocaleString('zh-CN')} ); } if (isMonthDrill) { const mKg = toMonthKg(kg); const mFee = toMonthFee(feeYuan); return ( <> {mKg.toLocaleString('zh-CN')} Kg ¥{mFee.toLocaleString('zh-CN')} {toFeeYearPct(mFee).toFixed(2)}% ); } if (isDayDrill) { const dKg = toDayKg(kg); const dFee = toDayFee(feeYuan); return ( <> {dKg.toLocaleString('zh-CN')} Kg ¥{dFee.toLocaleString('zh-CN')} {toFeeMonthPct(dFee).toFixed(2)}% ); } return ( <> {kg.toLocaleString('zh-CN')} Kg ¥{feeYuan.toLocaleString('zh-CN')} ); }; const handleExportDrillExcel = () => { if (isMonthBarDrill) { const aoa: (string | number)[][] = [ ['加氢站名称', '所属省份', '内部客户加氢总量(Kg)', '外部客户加氢总量(Kg)', '合计加氢总量(Kg)'], ]; stationMonthRows.forEach((r) => { aoa.push([r.stationName, r.province, r.internalKg, r.externalKg, r.totalKg]); }); downloadExcelAoa(aoa, `${label}_各站内外部客户加氢量.xlsx`, '月度各站加氢量'); return; } if (isMonthIncomeDrill) { const aoa: (string | number)[][] = [['加氢站名称', '所属省份', '客户收入(元)']]; stationRevRows.forEach((r) => { aoa.push([r.stationName, r.province, r.incomeYuan]); }); downloadExcelAoa(aoa, `${label}_各站客户收入.xlsx`, '月度各站客户收入'); return; } if (isMonthCostDrill) { const aoa: (string | number)[][] = [['加氢站名称', '所属省份', '成本支出(元)']]; stationRevRows.forEach((r) => { aoa.push([r.stationName, r.province, r.costYuan]); }); downloadExcelAoa(aoa, `${label}_各站成本支出.xlsx`, '月度各站成本支出'); return; } if (isStationCustomerDrill) { const aoa: (string | number)[][] = [ ['加氢站', '客户名称', '客户类型', '加氢总量(Kg)'], ]; stationCustomerRows.forEach((r) => { aoa.push([ stationCustTarget, r.customerName, r.category === 'internal' ? '内部客户' : '外部客户', r.totalKg, ]); }); downloadExcelAoa(aoa, `${stationCustTarget}_内外部客户加氢量.xlsx`, '站客户加氢量'); return; } if (isRegionDrill) { const aoa: (string | number)[][] = [ ['区域', '加氢站名称', '所属省份', '加氢总量(Kg)', '区域内占比(%)'], ]; regionStationRows.forEach((r) => { aoa.push([regionLabel, r.name, r.province, r.kg, r.pct]); }); downloadExcelAoa(aoa, `${regionLabel}_各站加氢总量占比.xlsx`, '区域各站加氢量'); return; } const aoa: (string | number)[][] = [ isProfitDrill ? [ '加氢站名称', '客户名称', '车牌号', '车辆归属', '订单编号', '加氢时间', '数据来源', '订单核对状态', '加氢量(Kg)', '收入(元)', '成本(元)', '利润(元)', ] : isMonthDrill ? [ '加氢站名称', '客户名称', '车牌号', '车辆归属', '订单编号', '加氢时间', '数据来源', '订单核对状态', '本月加氢量(Kg)', '本月加氢费(元)', '加氢费占年比(%)', ] : isDayDrill ? [ '加氢站名称', '客户名称', '车牌号', '车辆归属', '订单编号', '加氢时间', '数据来源', '订单核对状态', '本日加氢量(Kg)', '本日加氢费(元)', '加氢费占月比(%)', ] : [ '加氢站名称', '加氢站类型', '客户名称', '客户类型', '车牌号', '车辆归属', '订单编号', '加氢时间', '数据来源', '来源凭证/接口流水号', '订单核对状态', '单价(元/Kg)', '加氢量(Kg)', '加氢金额(元)', ], ]; drillStations.forEach((st) => { st.customers.forEach((cust) => { cust.vehicles.forEach((vh) => { vh.orders.forEach((ord) => { if (isProfitDrill) { aoa.push([ st.stationName, cust.customerName, vh.plateNo, vh.fleetCategory === 'own' ? '羚牛车辆' : '外部车辆', ord.orderId, ord.time, SOURCE_TYPE_LABEL[ord.source] || ord.source, vh.fleetCategory === 'own' ? (DAILY_VERIFY_LABEL[ord.verifyStatus || 'unverified'] || '未核对') : '-', ord.kg, toIncomeYuan(ord.amount), ord.amount, toProfitYuan(ord.amount), ]); } else if (isMonthDrill) { const mFee = toMonthFee(ord.amount); aoa.push([ st.stationName, cust.customerName, vh.plateNo, vh.fleetCategory === 'own' ? '羚牛车辆' : '外部车辆', ord.orderId, ord.time, SOURCE_TYPE_LABEL[ord.source] || ord.source, vh.fleetCategory === 'own' ? (DAILY_VERIFY_LABEL[ord.verifyStatus || 'unverified'] || '未核对') : '-', toMonthKg(ord.kg), mFee, toFeeYearPct(mFee), ]); } else if (isDayDrill) { const dFee = toDayFee(ord.amount); aoa.push([ st.stationName, cust.customerName, vh.plateNo, vh.fleetCategory === 'own' ? '羚牛车辆' : '外部车辆', ord.orderId, ord.time, SOURCE_TYPE_LABEL[ord.source] || ord.source, vh.fleetCategory === 'own' ? (DAILY_VERIFY_LABEL[ord.verifyStatus || 'unverified'] || '未核对') : '-', toDayKg(ord.kg), dFee, toFeeMonthPct(dFee), ]); } else { aoa.push([ st.stationName, st.stationType === 'self_use' ? '自用消费' : '对外销售', cust.customerName, cust.category === 'internal' ? '内部客户' : '外部客户', vh.plateNo, vh.fleetCategory === 'own' ? '羚牛车辆' : '外部车辆', ord.orderId, ord.time, SOURCE_TYPE_LABEL[ord.source] || ord.source, ord.certNo, vh.fleetCategory === 'own' ? (DAILY_VERIFY_LABEL[ord.verifyStatus || 'unverified'] || '未核对') : '-', ord.unitPrice, ord.kg, ord.amount, ]); } }); }); }); }); const fileName = isProfitDrill ? `${year}年_加氢利润_收入成本穿透明细.xlsx` : isMonthDrill ? `${year}年_本月加氢_量费占年比穿透明细.xlsx` : isDayDrill ? `${year}年_本日加氢_量费占月比穿透明细.xlsx` : `${year}年_${label}_加氢站_客户_车牌_单笔订单穿透流水账单.xlsx`; const sheetName = isProfitDrill ? '加氢利润收入成本明细' : isMonthDrill ? '本月加氢穿透明细' : isDayDrill ? '本日加氢穿透明细' : 'KPI穿透订单明细'; downloadExcelAoa(aoa, fileName, sheetName); }; return (
e.stopPropagation()}> {/* Modal 头部 */}
「{year}」{label}明细
{/* Modal 内容区 */}
{isFlatOverviewDrill ? ( isStationCustomerDrill ? ( <>
加氢站 {stationCustTarget}
内部客户加氢总量 {stationCustInternalKg.toLocaleString('zh-CN')} Kg
外部客户加氢总量 {stationCustExternalKg.toLocaleString('zh-CN')} Kg
合计加氢总量 {stationCustTotalKg.toLocaleString('zh-CN')} Kg
该站内部客户与外部客户加氢总量(可纵向滚动)
{stationCustomerRows.length === 0 ? ( ) : ( stationCustomerRows.map((row) => ( )) )}
客户 类型 加氢总量 (Kg) 站内占比
暂无该站客户数据
{row.customerName} {row.category === 'internal' ? '内部客户' : '外部客户'} {row.totalKg.toLocaleString('zh-CN')} {stationCustTotalKg > 0 ? ((row.totalKg / stationCustTotalKg) * 100).toFixed(1) : '0.0'} %
) : isRegionDrill ? ( <>
区域 {regionLabel}
加氢总量 {(regionKgSum / 1000).toFixed(2)} T
覆盖加氢站数 {regionStationRows.length} 站
该{regionKind}各加氢站加氢总量与区域内占比(可纵向滚动)
{regionStationRows.length === 0 ? ( ) : ( regionStationRows.map((row, idx) => ( )) )}
# 加氢站 所属省份 加氢总量 (Kg) 区域内占比
该区域暂无加氢站数据
{idx + 1} {row.name} {row.province} {row.kg.toLocaleString('zh-CN')} {row.pct.toFixed(1)}%
) : ( <>
{isMonthBarDrill ? ( <>
内部客户加氢总量 {monthBarInternalSum.toLocaleString('zh-CN')} Kg
外部客户加氢总量 {monthBarExternalSum.toLocaleString('zh-CN')} Kg
合计加氢总量 {monthBarTotalSum.toLocaleString('zh-CN')} Kg
) : isMonthIncomeDrill ? ( <>
客户收入合计 ¥{monthIncomeSum.toLocaleString('zh-CN')}
站均收入 ¥ {(stationRevRows.length ? Math.round(monthIncomeSum / stationRevRows.length) : 0 ).toLocaleString('zh-CN')}
) : ( <>
成本支出合计 ¥{monthCostSum.toLocaleString('zh-CN')}
站均成本 ¥ {(stationRevRows.length ? Math.round(monthCostSum / stationRevRows.length) : 0 ).toLocaleString('zh-CN')}
)}
覆盖加氢站数 {(isMonthBarDrill ? stationMonthRows : stationRevRows).length} 站
{ setStationFilter(v); setCustomerFilter('all'); setPlateFilter('all'); }} options={stationOptions} allLabel="全部加氢站" placeholder="搜索加氢站…" width={220} />
{isMonthBarDrill ? '该月各加氢站:内部客户加氢总量 · 外部客户加氢总量 · 合计' : isMonthIncomeDrill ? '该月各加氢站客户收入' : '该月各加氢站成本支出'}
{isMonthBarDrill ? ( <> ) : ( )} {isMonthBarDrill ? ( stationMonthRows.length === 0 ? ( ) : ( stationMonthRows.map((row) => ( )) ) ) : stationRevRows.length === 0 ? ( ) : ( stationRevRows.map((row) => ( )) )}
加氢站 所属省份内部客户加氢总量 (Kg) 外部客户加氢总量 (Kg) 合计加氢总量 (Kg) {isMonthIncomeDrill ? '客户收入 (元)' : '成本支出 (元)'}
暂无符合筛选的加氢站数据
{row.stationName} {row.province} {row.internalKg.toLocaleString('zh-CN')} {row.externalKg.toLocaleString('zh-CN')} {row.totalKg.toLocaleString('zh-CN')}
暂无符合筛选的加氢站数据
{row.stationName} {row.province} ¥{(isMonthIncomeDrill ? row.incomeYuan : row.costYuan).toLocaleString('zh-CN')}
) ) : ( <> {/* 汇总与追溯证明 Card */}
{isProfitDrill ? ( <>
收入合计 ¥{toIncomeYuan( filteredStations.reduce((s, st) => s + feeWanToYuan(st.totalFeeWan), 0), ).toLocaleString('zh-CN')}
成本合计 ¥{filteredStations .reduce((s, st) => s + feeWanToYuan(st.totalFeeWan), 0) .toLocaleString('zh-CN')}
加氢利润 ¥{(HOST_KPI.profitWan * 10000).toLocaleString('zh-CN')}
覆盖加氢站数 {filteredStations.length} 站
) : isMonthDrill ? ( <>
本月加氢量 {(toMonthKg(filteredStations.reduce((s, st) => s + st.totalKg, 0)) / 1000).toFixed(2)} T
本月加氢费 ¥{(toMonthFee(filteredYearFeeYuan) / 10000).toFixed(2)} 万元
加氢费占年比 {(monthShare * 100).toFixed(2)}%
覆盖加氢站数 {filteredStations.length} 站
) : isDayDrill ? ( <>
本日加氢量 {toDayKg(filteredStations.reduce((s, st) => s + st.totalKg, 0)).toLocaleString('zh-CN')} Kg
本日加氢费 ¥{toDayFee(filteredYearFeeYuan).toLocaleString('zh-CN')}
加氢费占月比 {filteredMonthFeeYuan > 0 ? ((toDayFee(filteredYearFeeYuan) / filteredMonthFeeYuan) * 100).toFixed(2) : '0.00'} %
覆盖加氢站数 {filteredStations.length} 站
) : ( <>
数据归集总量 {(filteredStations.reduce((s, st) => s + st.totalKg, 0) / 1000).toFixed(2)} T
数据总金额 ¥{filteredStations.reduce((s, st) => s + parseFloat(st.totalFeeWan), 0).toFixed(2)} 万元
覆盖加氢站数 {filteredStations.length} 站
数据源可追溯率 100% (含API/站点上报凭证)
)}
{/* 过滤筛选条:加氢站 / 客户 / 车辆(可搜索)+ 车辆归属 + 导出 */}
{ setStationFilter(v); setCustomerFilter('all'); setPlateFilter('all'); }} options={stationOptions} allLabel="全部加氢站" placeholder="搜索加氢站…" width={200} /> { setCustomerFilter(v); setPlateFilter('all'); }} options={customerOptions} allLabel="全部客户" placeholder="搜索客户…" width={200} />
提示:点击表格行可四级层层展开 【加氢站 → 客户 → 车牌 → 单笔订单与核对明细】
‹ 左右滑动查看完整数据与凭证列 ›
{/* 穿透树形表格 */}
{isProfitDrill ? ( <> ) : isMonthDrill ? ( <> ) : isDayDrill ? ( <> ) : ( <> )} {filteredStations.map((st) => { const isStExpanded = !!expandedStations[st.stationId]; const stationVehicles = st.customers.flatMap((c) => c.vehicles); const stationVerify = aggregateVehiclesVerifyStatus(stationVehicles); return ( {/* Level 1: 加氢站 */} toggleStation(st.stationId)} > {renderMetricCells(st.totalKg, feeWanToYuan(st.totalFeeWan))} {/* Level 2: 客户层 */} {isStExpanded && st.customers.map((cust) => { const custKey = `${st.stationId}_${cust.customerId}`; const isCustExpanded = !!expandedCustomers[custKey]; return ( toggleCustomer(st.stationId, cust.customerId)} > {renderMetricCells(cust.totalKg, feeWanToYuan(cust.totalFeeWan))} {/* Level 3: 车辆及数据来源与凭证层 (支持折叠展开单笔订单) */} {isCustExpanded && cust.vehicles.map((vh, idx) => { const vhKey = `${st.stationId}_${cust.customerId}_${vh.plateNo}`; const isVhExpanded = !!expandedVehicles[vhKey]; const showAllOrders = !!expandedAllVehicleOrders[vhKey]; const displayOrders = showAllOrders ? vh.orders : vh.orders.slice(0, 5); const aggVerify = computeVehicleVerifyStatus(vh.orders, vh.fleetCategory); // 无法识别车牌 →「无车牌」类目,与有牌车辆同级;标签固定「外部车辆」。能识别 →「羚牛车辆」/「外部车辆」按归属。 const isNoPlate = !vh.plateNo || /无车牌/.test(vh.plateNo); const plateDisplay = isNoPlate ? '无车牌' : vh.plateNo; const isOwnFleet = !isNoPlate && vh.fleetCategory === 'own'; return ( toggleVehicle(st.stationId, cust.customerId, vh.plateNo)} > {renderMetricCells(vh.kg, vh.amount)} {/* Level 4: 单笔加氢订单流水与核对明细层 */} {isVhExpanded && ( <> {displayOrders.map((ord) => ( {renderMetricCells(ord.kg, ord.amount)} ))} )} ); })} ); })} ); })}
加氢站 / 客户 / 车辆与凭证链路 类型 / 归属 数据来源及凭证号 核对状态 加氢笔数收入 (元) 成本 (元) 利润 (元)本月加氢量 (Kg) 本月加氢费 (元) 加氢费占年比本日加氢量 (Kg) 本日加氢费 (元) 加氢费占月比加氢总量 (Kg) 加氢金额 (元)
{isStExpanded ? '▼' : '►'} {st.stationName} ({st.customers.length} 家客户) - 全量自动归集 {renderAggVerifyTag(stationVerify, '本站羚牛车辆')} {st.customers.reduce((sum, c) => sum + c.vehicles.reduce((vs, v) => vs + v.count, 0), 0)} 笔
{isCustExpanded ? '▼' : '►'} └─ 客户:{cust.customerName} - {cust.vehicles.length} 辆车挂载 - {cust.vehicles.reduce((sum, v) => sum + v.count, 0)} 笔
{isVhExpanded ? '▼' : '►'} {plateDisplay} ({vh.count} 笔订单) {renderFleetTag(isOwnFleet)} {renderSourceTag(vh.source)} {renderVehicleVerifyTag(vh.fleetCategory, aggVerify)} {vh.count} 笔
└── 订单编号 {ord.orderId} ({ord.time}) 单价 ¥{ord.unitPrice.toFixed(2)}/Kg {renderSourceTag(ord.source)} {renderOrderVerifyTag(vh.fleetCategory, ord.verifyStatus)} 1 笔
└── {showAllOrders ? `包含该车辆共 ${vh.count} 笔历史加氢订单,已展示全量 ${vh.orders.length} 笔穿透流水` : `包含该车辆共 ${vh.count} 笔历史加氢订单,默认展示近 ${displayOrders.length} 笔穿透核对流水明细`}
)}
); } interface CustomerBillDrillModalProps { customerName: string; year: number; onClose: () => void; } function CustomerBillDrillModal({ customerName, year, onClose }: CustomerBillDrillModalProps) { const [searchTerm, setSearchTerm] = useState(''); const [expandedDates, setExpandedDates] = useState>({ '2026-08-08': true, // 默认展开最新一日 }); const custSummary = MOCK_CUSTOMER_SUMMARY_LIST.find((c) => c.name === customerName); const bearerLabel = custSummary?.bearer === 'lingniu' ? '羚牛' : '客户'; const summaryKgT = custSummary ? parseFloat(custSummary.kgT) : 0; const summaryCostWan = custSummary ? parseFloat(custSummary.costWan) : 0; const summaryReceivableText = custSummary?.receivable ?? '—'; const toggleDate = (dateStr: string) => { setExpandedDates((prev) => ({ ...prev, [dateStr]: !prev[dateStr] })); }; // 根据客户名称与年份建立【客户 -> 日期 -> 车牌加氢记录(最小粒度)】层层下钻明细 const billData = useMemo(() => { const dates = [ { date: '2026-08-08', stationCount: 2, records: [ { plateNo: '浙A88888F', fleetCategory: 'own' as const, stationName: '嘉兴中石化滨海加氢站', time: '2026-08-08 08:15', kg: 45.2, receivable: 1356.0 }, { plateNo: '浙A66666F', fleetCategory: 'own' as const, stationName: '嘉兴嘉锦加氢站', time: '2026-08-08 09:30', kg: 54.8, receivable: 1644.0 }, { plateNo: '粤B12345D', fleetCategory: 'external' as const, stationName: '嘉兴中石化滨海加氢站', time: '2026-08-08 10:10', kg: 350.0, receivable: 10500.0 }, { plateNo: '粤B99881D', fleetCategory: 'external' as const, stationName: '嘉兴嘉锦加氢站', time: '2026-08-08 14:20', kg: 280.0, receivable: 8400.0 }, ], }, { date: '2026-08-07', stationCount: 1, records: [ { plateNo: '浙A88888F', fleetCategory: 'own' as const, stationName: '嘉兴中石化滨海加氢站', time: '2026-08-07 11:20', kg: 48.0, receivable: 1440.0 }, { plateNo: '浙A33333F', fleetCategory: 'own' as const, stationName: '嘉兴中石化滨海加氢站', time: '2026-08-07 16:45', kg: 52.0, receivable: 1560.0 }, { plateNo: '沪A66128D', fleetCategory: 'external' as const, stationName: '嘉兴中石化滨海加氢站', time: '2026-08-07 17:30', kg: 120.0, receivable: 3600.0 }, ], }, { date: '2026-08-06', stationCount: 2, records: [ { plateNo: '浙A88888F', fleetCategory: 'own' as const, stationName: '嘉兴中石化滨海加氢站', time: '2026-08-06 09:10', kg: 42.5, receivable: 1275.0 }, { plateNo: '川A88901', fleetCategory: 'external' as const, stationName: '成都中石化天府机场高速北站加氢站', time: '2026-08-06 13:15', kg: 210.0, receivable: 6300.0 }, { plateNo: '川A88902', fleetCategory: 'external' as const, stationName: '成都中石化天府机场高速北站加氢站', time: '2026-08-06 15:50', kg: 180.0, receivable: 5400.0 }, ], }, { date: '2026-08-05', stationCount: 1, records: [ { plateNo: '浙A66666F', fleetCategory: 'own' as const, stationName: '桐乡中石化绿能加氢站', time: '2026-08-05 10:40', kg: 50.0, receivable: 1500.0 }, { plateNo: '渝A66881', fleetCategory: 'external' as const, stationName: '桐乡中石化绿能加氢站', time: '2026-08-05 14:05', kg: 310.0, receivable: 9300.0 }, ], }, ]; return dates.map((d) => { const filteredRecords = d.records.filter((r) => { if (!searchTerm) return true; const term = searchTerm.toLowerCase(); return ( (r.plateNo && r.plateNo.toLowerCase().includes(term)) || r.stationName.toLowerCase().includes(term) ); }); const dayKg = filteredRecords.reduce((sum, r) => sum + r.kg, 0); const dayReceivable = filteredRecords.reduce((sum, r) => sum + r.receivable, 0); const dayCost = summaryKgT > 0 ? Math.round((dayKg / (summaryKgT * 1000)) * summaryCostWan * 10000 * 100) / 100 : Math.round(dayReceivable * 0.9 * 100) / 100; return { ...d, records: filteredRecords, totalKg: Math.round(dayKg * 10) / 10, totalReceivable: Math.round(dayReceivable * 100) / 100, totalCost: dayCost, }; }).filter((d) => d.records.length > 0); }, [searchTerm, summaryKgT, summaryCostWan]); const totalKgSum = useMemo(() => { return billData.reduce((sum, d) => sum + d.totalKg, 0); }, [billData]); const totalReceivableSum = useMemo(() => { return billData.reduce((sum, d) => sum + d.totalReceivable, 0); }, [billData]); // 导出客户账单 Excel (.xlsx) const handleExportExcel = () => { const aoa: (string | number)[][] = [ ['客户名称', '日期', '车牌号', '车辆归属', '加氢站', '加氢时间', '加氢量(Kg)', '应收(元)', '已收', '未收'], ]; billData.forEach((d) => { d.records.forEach((r) => { aoa.push([ customerName, d.date, r.plateNo || '无车牌(散车)', r.fleetCategory === 'own' ? '羚牛车辆' : '外部车辆', r.stationName, r.time, r.kg, r.receivable, '敬请期待 (对接账户)', '敬请期待 (对接账单)', ]); }); }); downloadExcelAoa(aoa, `客户账单穿透流水_${customerName}_${year}年.xlsx`, '客户账单穿透流水'); }; return (
e.stopPropagation()}> {/* Modal 头部 */}
「{customerName}」客户账单明细
{/* Modal 内容区 */}
{/* 列表关键字段汇总 */}
承担方 {bearerLabel}
加氢量 {summaryKgT > 0 ? summaryKgT.toFixed(2) : (totalKgSum / 1000).toFixed(2)}{' '} T
成本支出 ¥{summaryCostWan > 0 ? summaryCostWan.toFixed(2) : '—'}{' '} 万元
应收 {summaryReceivableText}
已收 敬请期待
未收 敬请期待
{/* 搜寻卡 */}
setSearchTerm(e.target.value)} /> {searchTerm && ( )}
按日展开:加氢量 · 成本支出 · 应收 · 已收 · 未收
‹ 左右滑动查看完整关键字段 ›
{/* 表格:对齐客户账单汇总关键字段 */}
{billData.map((day) => { const isExpanded = !!expandedDates[day.date]; return ( {/* Level 2: 日期层 */} toggleDate(day.date)} > {/* Level 3: 车牌加氢记录 (最小粒度) */} {isExpanded && day.records.map((rec, rIdx) => { const rowCost = day.totalKg > 0 ? Math.round((rec.kg / day.totalKg) * day.totalCost * 100) / 100 : 0; return ( ); })} ); })}
日期 / 车牌明细 加氢站 承担方 加氢量(Kg) 成本支出(元) 应收(元) 已收 未收
{isExpanded ? '▼' : '►'} 📅 {day.date}
涉及 {day.stationCount} 个加氢站 · {day.records.length} 笔 {bearerLabel} {day.totalKg.toLocaleString('zh-CN')} Kg ¥{day.totalCost.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} ¥{day.totalReceivable.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} 敬请期待 敬请期待
└── {rec.plateNo || '无车牌(散车)'} {rec.time}
{rec.stationName} {bearerLabel} {rec.kg.toFixed(1)} Kg ¥{rowCost.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} ¥{rec.receivable.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} 敬请期待 敬请期待
); } interface StationBillDrillModalProps { stationName: string; province: string; year: number; onClose: () => void; } function StationBillDrillModal({ stationName, province, year, onClose }: StationBillDrillModalProps) { const [searchTerm, setSearchTerm] = useState(''); const [expandedDates, setExpandedDates] = useState>({ '2026-08-08': true, // 默认展开最新一日 }); const stSummary = MOCK_STATION_SUMMARY_LIST.find((s) => s.name === stationName) || MOCK_STATION_SUMMARY_LIST.find((s) => stationName.includes(s.name.slice(0, 8)) || s.name.includes(stationName.slice(0, 8))); const summaryKgT = stSummary ? parseFloat(stSummary.kgT) : 0; const summaryKgPct = stSummary?.kgPct ?? 0; const summaryIncomeWan = stSummary ? parseFloat(stSummary.incomeWan) : 0; const summaryIncomePct = stSummary?.incomePct ?? 0; const toggleDate = (dateStr: string) => { setExpandedDates((prev) => ({ ...prev, [dateStr]: !prev[dateStr] })); }; // 全站累计基准总量 (Kg) 与 总氢费收入 (元) — 优先取汇总表关键字段 const totalStationKg = summaryKgT > 0 ? Math.round(summaryKgT * 1000) : 243660; const totalStationIncome = summaryIncomeWan > 0 ? Math.round(summaryIncomeWan * 10000) : 526600; // 根据加氢站构建【加氢站 -> 所有日期的加氢量、占比、氢费收入、收入占比】下钻明细 const stationData = useMemo(() => { const dates = [ { date: '2026-08-08', records: [ { plateNo: '浙A88888F', fleetCategory: 'own' as const, customerName: '羚牛氢能科技(广东)有限公司', time: '2026-08-08 08:15', kg: 450.2, income: 13506.0 }, { plateNo: '浙A66666F', fleetCategory: 'own' as const, customerName: '嘉兴市乍浦港口经营有限公司', time: '2026-08-08 09:30', kg: 540.8, income: 16224.0 }, { plateNo: '粤B12345D', fleetCategory: 'external' as const, customerName: '广东氢动力科技服务有限公司', time: '2026-08-08 10:10', kg: 350.0, income: 10500.0 }, { plateNo: '沪A66128D', fleetCategory: 'external' as const, customerName: '上海明纳物流有限公司', time: '2026-08-08 14:20', kg: 280.0, income: 8400.0 }, ], }, { date: '2026-08-07', records: [ { plateNo: '浙A88888F', fleetCategory: 'own' as const, customerName: '羚牛氢能科技(广东)有限公司', time: '2026-08-07 11:20', kg: 480.0, income: 14400.0 }, { plateNo: '浙A33333F', fleetCategory: 'own' as const, customerName: '嘉兴益顺冷链物流有限公司', time: '2026-08-07 16:45', kg: 520.0, income: 15600.0 }, { plateNo: '粤B99881D', fleetCategory: 'external' as const, customerName: '嘉兴智奇供应链管理有限公司', time: '2026-08-07 17:30', kg: 410.0, income: 12300.0 }, ], }, { date: '2026-08-06', records: [ { plateNo: '浙A88888F', fleetCategory: 'own' as const, customerName: '羚牛氢能科技(广东)有限公司', time: '2026-08-06 09:10', kg: 425.0, income: 12750.0 }, { plateNo: '川A88901', fleetCategory: 'external' as const, customerName: '四川群彬物流有限公司', time: '2026-08-06 13:15', kg: 610.0, income: 18300.0 }, { plateNo: '川A88902', fleetCategory: 'external' as const, customerName: '四川拱照物流有限公司', time: '2026-08-06 15:50', kg: 580.0, income: 17400.0 }, ], }, { date: '2026-08-05', records: [ { plateNo: '浙A66666F', fleetCategory: 'own' as const, customerName: '嘉兴市乍浦港口经营有限公司', time: '2026-08-05 10:40', kg: 500.0, income: 15000.0 }, { plateNo: '渝A66881', fleetCategory: 'external' as const, customerName: '重庆金时源供应链有限公司', time: '2026-08-05 14:05', kg: 710.0, income: 21300.0 }, ], }, ]; return dates.map((d) => { const filteredRecords = d.records.filter((r) => { if (!searchTerm) return true; const term = searchTerm.toLowerCase(); return ( d.date.includes(term) || (r.plateNo && r.plateNo.toLowerCase().includes(term)) || r.customerName.toLowerCase().includes(term) ); }); const dayKg = filteredRecords.reduce((sum, r) => sum + r.kg, 0); const dayIncome = filteredRecords.reduce((sum, r) => sum + r.income, 0); const dayKgPct = Math.round((dayKg / totalStationKg) * 10000) / 100; const dayIncomePct = Math.round((dayIncome / totalStationIncome) * 10000) / 100; return { ...d, records: filteredRecords, totalKg: Math.round(dayKg * 10) / 10, totalKgPct: dayKgPct, totalIncome: Math.round(dayIncome * 100) / 100, totalIncomePct: dayIncomePct, }; }).filter((d) => d.records.length > 0); }, [searchTerm]); const totalKgSum = useMemo(() => { return stationData.reduce((sum, d) => sum + d.totalKg, 0); }, [stationData]); const totalIncomeSum = useMemo(() => { return stationData.reduce((sum, d) => sum + d.totalIncome, 0); }, [stationData]); // 导出加氢站按日账单 Excel (.xlsx) const handleExportExcel = () => { const aoa: (string | number)[][] = [ ['加氢站名称', '所属省份', '日期', '车牌号', '车辆归属', '关联客户', '加氢时间', '加氢量(Kg)', '加氢量占比', '氢费收入(元)', '收入占比'], ]; stationData.forEach((d) => { d.records.forEach((r) => { const rKgPct = ((r.kg / totalStationKg) * 100).toFixed(2) + '%'; const rIncomePct = ((r.income / totalStationIncome) * 100).toFixed(2) + '%'; aoa.push([ stationName, province, d.date, r.plateNo || '无车牌(散车)', r.fleetCategory === 'own' ? '羚牛车辆' : '外部车辆', r.customerName, r.time, r.kg, rKgPct, r.income, rIncomePct, ]); }); }); downloadExcelAoa(aoa, `加氢站按日账单穿透_${stationName}_${year}年.xlsx`, '站按日账单穿透'); }; return (
e.stopPropagation()}> {/* Modal 头部 */}
「{stationName}」加氢汇总明细
{/* Modal 内容区 */}
{/* 列表关键字段:加氢量 / 占比 / 氢费收入 / 收入占比 */}
所属省份 {province}
加氢量 {(totalStationKg / 1000).toFixed(2)} T
占比 {summaryKgPct.toFixed(1)}%
氢费收入 ¥{(totalStationIncome / 10000).toFixed(2)} 万元
收入占比 {summaryIncomePct.toFixed(1)}%
{/* 搜寻卡 */}
setSearchTerm(e.target.value)} /> {searchTerm && ( )}
按日展开关键字段:加氢量 · 占比 · 氢费收入 · 收入占比
‹ 左右滑动查看完整数据与状态 ›
{/* 表格:对齐加氢站汇总关键字段(按日) */}
{stationData.map((day) => { const isExpanded = !!expandedDates[day.date]; return ( {/* Level 1: 所有日期的加氢量、占比、氢费收入、收入占比 */} toggleDate(day.date)} > {/* Level 2: 车牌/客户加氢记录 (最小粒度) */} {isExpanded && day.records.map((rec, rIdx) => { const rKgPct = Math.round((rec.kg / totalStationKg) * 10000) / 100; const rIncomePct = Math.round((rec.income / totalStationIncome) * 10000) / 100; return ( ); })} ); })}
日期 / 车牌明细 关联客户 / 车辆归属 加氢时间 加氢量(Kg) 加氢量占比 氢费收入(元) 收入占比
{isExpanded ? '▼' : '►'} 📅 {day.date}
{day.records.length} 笔车辆加氢发生 当天汇总 {day.totalKg.toLocaleString('zh-CN')} Kg
{day.totalKgPct.toFixed(2)}%
¥{day.totalIncome.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}
{day.totalIncomePct.toFixed(2)}%
└── {rec.plateNo || '无车牌(散车)'} {rec.fleetCategory === 'own' ? '羚牛' : '外部'}
{rec.customerName} {rec.time} {rec.kg.toFixed(1)} Kg {rKgPct.toFixed(2)}% ¥{rec.income.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} {rIncomePct.toFixed(2)}%
); } 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; } function HostDailyView({ updatedAt, onRefresh, startDate, endDate, onStartDateChange, onEndDateChange, }: HostDailyViewProps) { const [rangePreset, setRangePreset] = useState<'week' | 'month' | '15days' | 'custom'>('15days'); const [fleetType, setFleetType] = useState('all'); // 上方时间预设连动 KPI 卡片标题 const kpiRangeTitle = useMemo(() => { if (rangePreset === 'week') return '本周加氢量'; if (rangePreset === 'month') return '本月加氢量'; if (rangePreset === '15days') return '近 15 天加氢量'; return '自定义区间加氢量'; }, [rangePreset]); const handlePresetChange = (preset: 'week' | 'month' | '15days' | 'custom') => { setRangePreset(preset); if (preset === 'week') { onStartDateChange('2026-08-03'); onEndDateChange('2026-08-08'); } else if (preset === 'month') { onStartDateChange('2026-08-01'); onEndDateChange('2026-08-08'); } else if (preset === '15days') { onStartDateChange('2026-07-25'); onEndDateChange('2026-08-08'); } }; // 日期归一化转换(兼容手选 年 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]); // 1. 根据 startDate & endDate 动态生成或提取指定日期范围内的全量每日加氢数据列表 const dateFilteredList = useMemo(() => { return getDailyDataForRange(normStart, normEnd); }, [normStart, normEnd]); // 2. 根据 fleetType 过滤出对应车辆归属下的加氢列表 ('all' 时包含内部与外部合并显示) const filteredDailyList = useMemo(() => { return filterDailyDataByFleet(dateFilteredList, fleetType); }, [dateFilteredList, fleetType]); // 2. 动态计算关联的 KPI 及柱图统计数据 const dailyKpis = useMemo(() => { return calculateDailyKpis(filteredDailyList, fleetType); }, [filteredDailyList, fleetType]); // 深层折叠/展开状态 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]); 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' ? '羚牛车辆' : '外部车辆'; downloadExcelAoa(aoa, `每日加氢数据明细_${fleetName}_${fileDateStr}.xlsx`, '每日加氢明细'); }; return (
{/* 1. 顶栏时间/范围筛选器 */}
{ onStartDateChange(val); setRangePreset('custom'); }} /> { onEndDateChange(val); setRangePreset('custom'); }} />
{updatedAt && ( {updatedAt} )}
{/* 2. 4卡 Bento KPI */}
{kpiRangeTitle}
{dailyKpis.totalQuantityKg.toLocaleString('zh-CN')} Kg
{dailyKpis.dateRange}
车辆结构
{dailyKpis.fleetTypeLabel}
{dailyKpis.fleetSubLabel}
有效天数
{dailyKpis.activeDays}
日均 {dailyKpis.dailyAvgKg}
涉及加氢站
{dailyKpis.stationCount}
按明细站点去重
{/* 3. 每日加氢量堆积柱状图(分别显示内部客户与外部客户加氢量,点击柱子下锚定位) */}
每日加氢量 (点击柱体下锚定位到对应日期明细) (点击柱体定位)
内部客户 外部客户
时间单位:日 · 单位 Kg
峰值日 {dailyKpis.peakDayLabel}
低谷日 {dailyKpis.troughDayLabel}
零数日 {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; } }); }); }); 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.stations.length > 0 ? (isDateExpanded ? '▼' : '►') : '•'} {row.date} ({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.time} {vh.plateNo ? ( {vh.plateNo} ) : ( 无车牌(散车) )} {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 })} -
); }