From 5d14a28b0f4e18c3655e00947b27b1eef0268baa Mon Sep 17 00:00:00 2001 From: dsh-agent Date: Fri, 11 Sep 2026 10:23:31 +0800 Subject: [PATCH] =?UTF-8?q?refactor(stage6):=20=E5=8E=BB=E6=8E=89=E7=83=AD?= =?UTF-8?q?=E5=8A=9B=E5=9B=BE=E5=86=99=E6=AD=BB=E7=9A=84=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=8C=BA=E9=97=B4=E4=B8=8E=E7=BA=BF=E4=B8=8A=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=9A=84=E6=BC=94=E7=A4=BA=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 热力图默认区间 - 新增 src/shared/date-range.ts(近 N 天滚动窗口,按本地自然日)。 - 前端两个热力图与后端两个 query 模型原先都把默认区间写死为 2026-01-01 ~ 2026-07-13,会随时间永久指向一段历史数据; 现改为"近 30 天"滚动,并把 now 作为可注入参数以便测试。 - 两个 model 测试改为注入固定时钟并断言滚动窗口,不再硬编码日期。 氢能看板去演示数据(board/EnergyBiBoardApp.tsx,6554 -> 3765 行) - 取消线上路径的 mock 兜底:省份下拉、站点汇总表、客户费用汇总表、 区域排行在 overview 未就绪时显示空,而不是假的 25 个站点 / Top30 账单。 - 删除已确认"零渲染点"的死代码(用 TypeScript AST 精确定位声明范围, 避免手工推断括号边界): KpiDrillModal / CustomerBillDrillModal / StationBillDrillModal / DrillPeriodControls / getYearTimeRange / getDrillTimeRange / MOCK_STATION_SUMMARY_LIST / MOCK_CUSTOMER_SUMMARY_LIST 这些组件内含大量写死的 2026-08-08 快照日期与假流水,均不可达。 线上 KPI 下钻走的是 PrototypeDrillModal(真实 API)。 - 单站日报"最后更新时间"初值不再用伪造时间戳,改为与接口空态一致的文案。 注:本次未改动任何 DOM 结构与 CSS 类名;删除的均为不可达分支。 lint / test(134) / build 全绿。 --- .../hydrogen/board/EnergyBiBoardApp.tsx | 2802 +---------------- .../station-daily/StationDailyApp.tsx | 3 +- .../HydrogenHeatmapModule.tsx | 19 +- .../vehicle-heatmap/VehicleHeatmapModule.tsx | 19 +- .../routes/hydrogen-heatmap-model.test.ts | 17 +- src/server/routes/hydrogen-heatmap-model.ts | 12 +- src/server/routes/hydrogen-heatmap.ts | 9 +- .../routes/vehicle-heatmap-model.test.ts | 17 +- src/server/routes/vehicle-heatmap-model.ts | 16 +- src/shared/date-range.ts | 29 + 10 files changed, 102 insertions(+), 2841 deletions(-) create mode 100644 src/shared/date-range.ts diff --git a/src/modules/energy/hydrogen/board/EnergyBiBoardApp.tsx b/src/modules/energy/hydrogen/board/EnergyBiBoardApp.tsx index 222b948..ff36934 100644 --- a/src/modules/energy/hydrogen/board/EnergyBiBoardApp.tsx +++ b/src/modules/energy/hydrogen/board/EnergyBiBoardApp.tsx @@ -291,69 +291,6 @@ function BiSearchSelect({ ); } -// 站加氢汇总假数据 (带省份归属,与用户截图高保真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; @@ -592,7 +529,7 @@ function OverviewTrendsDashboard({ year, fleetScope, verifyScope, onOpenDrill, o // 已有加氢站的省份去重列表 const availableProvinces = useMemo(() => { const list: string[] = ['all']; - (overview?.stations ?? MOCK_STATION_SUMMARY_LIST).forEach((st: any) => { + (overview?.stations ?? []).forEach((st: any) => { const province = st.province; if (province && !list.includes(province)) { list.push(province); @@ -612,7 +549,7 @@ function OverviewTrendsDashboard({ year, fleetScope, verifyScope, onOpenDrill, o incomePct: overview.kpis.customerRevenue ? (Number(st.customerRevenue) || 0) / Number(overview.kpis.customerRevenue) * 100 : 0, })) - : MOCK_STATION_SUMMARY_LIST; + : []; if (selectedProvince === 'all') return source; return source.filter((st: any) => st.province === selectedProvince); }, [selectedProvince, overview]); @@ -1234,7 +1171,7 @@ function OverviewTrendsDashboard({ year, fleetScope, verifyScope, onOpenDrill, o kgT: ((Number(item.kg) || 0) / 1000).toFixed(2), costWan: ((Number(item.cost) || 0) / 10000).toFixed(2), receivable: `¥${((Number(item.customerRevenue) || 0) / 10000).toFixed(2)} 万元`, - })) : MOCK_CUSTOMER_SUMMARY_LIST).map((cust: any) => ( + })) : []).map((cust: any) => ( onOpenCustomerBill(cust.name)} @@ -1528,10 +1465,6 @@ export const EnergyBiBoardApp: React.FC = () => { // 加氢站加氢量排名(高→低),跟随年份/车辆/核对筛选 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; if (liveOverview) { const list = liveOverview.stations.map((st: any) => ({ name: st.name, province: st.province || '未归属', kg: Number(st.kg) || 0, @@ -1540,22 +1473,9 @@ export const EnergyBiBoardApp: React.FC = () => { const totalKg = list.reduce((sum: number, item: any) => sum + item.kg, 0) || 1; return list.map((st: any, index: number) => ({ ...st, rank: index + 1, barPct: Math.round(st.kg / maxKg * 100), sharePct: Math.round(st.kg / totalKg * 1000) / 10 })); } - 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, liveOverview]); + // 没有真实数据时返回空列表:不再用原型演示数据按年份/归属系数编造排名。 + return []; + }, [liveOverview]); const top5SharePct = useMemo(() => { const top5 = stationRankList.slice(0, 5).reduce((s, x) => s + x.kg, 0); @@ -2378,40 +2298,6 @@ function HostKpi({ ); } -/** KPI 数据来源穿透 & 站 -> 客户 -> 车牌三级下钻 Modal 弹窗 */ -interface KpiDrillModalProps { - label: string; - year: number; - fleetScope: FleetScope; - verifyScope: 'all' | 'verified'; - onClose: () => void; -} - -function getYearTimeRange(year: number) { - return year === 2026 ? '2026-01-01 至 2026-08-08' : `${year}-01-01 至 ${year}-12-31`; -} - -function getDrillTimeRange(label: string, year: number) { - const monthMatch = label.match(/(\d{4})年(\d{1,2})月/); - if (monthMatch) { - const targetYear = Number(monthMatch[1]); - const targetMonth = Number(monthMatch[2]); - const month = String(targetMonth).padStart(2, '0'); - const lastDay = new Date(targetYear, targetMonth, 0).getDate(); - const endDay = targetYear === 2026 && targetMonth === 8 ? 8 : lastDay; - return `${targetYear}-${month}-01 至 ${targetYear}-${month}-${String(endDay).padStart(2, '0')}`; - } - if (label.includes('本月')) { - return year === 2026 ? '2026-08-01 至 2026-08-08' : `${year}-08-01 至 ${year}-08-31`; - } - if (label.includes('本日')) { - const day = year === 2026 ? '2026-08-08' : `${year}-08-08`; - const cutoffTime = year === 2026 ? '11:25' : '23:59'; - return `${day} 00:00 至 ${day} ${cutoffTime}`; - } - return getYearTimeRange(year); -} - function makeVehicleOrders( certPrefix: string, fleetCategory: FleetCategory, @@ -2622,1897 +2508,6 @@ function renderAggVerifyTag( ); } -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); - const [borneByFilter, setBorneByFilter] = useState<'all' | BorneBy>('all'); - const [treeAxis, setTreeAxis] = useState('station'); - const [mobileDrillFiltersOpen, setMobileDrillFiltersOpen] = useState(false); - - // 跟随顶栏车辆筛选:打开/切换时同步 - 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 drillTimeRange = getDrillTimeRange(label, year); - - 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; - - const raw = [ - { - 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) }, - ], - }, - ], - }, - ]; - - return raw.map((station) => ({ - ...station, - customers: station.customers.map((customer) => { - const borneBy = DRILL_CUSTOMER_BORNE[customer.customerId] ?? 'customer'; - return { - ...customer, - borneBy, - vehicles: customer.vehicles.map((vehicle) => ({ - ...vehicle, - borneBy, - orders: vehicle.orders.map((order) => ({ ...order, borneBy })), - })), - }; - }), - })); - }, [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; - if (borneByFilter !== 'all' && cust.borneBy !== borneByFilter) 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, borneByFilter, verifyScope]); - - const customerTree = useMemo(() => { - const customers = new Map; - }>(); - - filteredStations.forEach((station) => { - station.customers.forEach((customer) => { - const current = customers.get(customer.customerId) ?? { - customerId: customer.customerId, - customerName: customer.customerName, - borneBy: customer.borneBy, - totalKg: 0, - totalFeeWan: 0, - stations: [], - }; - current.totalKg += customer.totalKg; - current.totalFeeWan += Number(customer.totalFeeWan); - current.stations.push({ - stationId: station.stationId, - stationName: station.stationName, - totalKg: customer.totalKg, - totalFeeWan: customer.totalFeeWan, - vehicles: customer.vehicles, - }); - customers.set(customer.customerId, current); - }); - }); - - return Array.from(customers.values()); - }, [filteredStations]); - - // 级联选项:站 → 客户 → 车牌(选项池随上级选择收窄) - 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]); - - const mobileDrillFilterSummary = useMemo(() => { - const station = stationFilter === 'all' - ? '全部站点' - : stationOptions.find((item) => item.value === stationFilter)?.label || '全部站点'; - const customer = customerFilter === 'all' - ? '全部客户' - : customerOptions.find((item) => item.value === customerFilter)?.label || '全部客户'; - const plate = plateFilter === 'all' - ? '全部车辆' - : plateOptions.find((item) => item.value === plateFilter)?.label || '全部车辆'; - const ownership = fleetCategoryFilter === 'own' - ? '羚牛车辆' - : fleetCategoryFilter === 'external' - ? '外部车辆' - : null; - const borne = borneByFilter === 'all' ? null : BORNE_BY_LABEL[borneByFilter]; - return [station, customer, plate, ownership, borne].filter(Boolean).join(' · '); - }, [stationFilter, customerFilter, plateFilter, fleetCategoryFilter, borneByFilter, stationOptions, customerOptions, plateOptions]); - - const mobileDrillActiveFilterCount = [ - stationFilter !== 'all', - customerFilter !== 'all', - plateFilter !== 'all', - fleetCategoryFilter !== 'all', - borneByFilter !== 'all', - ].filter(Boolean).length; - - // 上级变更时清掉下级无效选中 - 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 isCumulativeDrill = !isProfitDrill && !isMonthDrill && !isDayDrill && !isFlatOverviewDrill; - 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 ? 9 : 8; - - /** 月度柱钻取:各站内部/外部车辆加氢量 + 合计(按当月占年份额缩放) */ - 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' ? '羚牛车辆' : '外部车辆', - BORNE_BY_LABEL[ord.borneBy], - 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' ? '羚牛车辆' : '外部车辆', - BORNE_BY_LABEL[ord.borneBy], - 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' ? '羚牛车辆' : '外部车辆', - BORNE_BY_LABEL[ord.borneBy], - 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' ? '羚牛车辆' : '外部车辆', - BORNE_BY_LABEL[ord.borneBy], - 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}明细 -
-
统计时间范围:{drillTimeRange}
-
-
- -
- -
-
- - {/* Modal 内容区 */} -
- {!isFlatOverviewDrill && ( -
- - -
- )} - {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')} -
-
- - ) - ) : ( - <> - {isCumulativeDrill ? ( -
-
-
- 数据归集总量 - {(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%

-
- ) : null} - {/* 汇总与追溯证明 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} - /> - - setBorneByFilter(value as 'all' | BorneBy)} - options={BORNE_BY_ORDER.map((borneBy) => ({ - value: borneBy, - label: BORNE_BY_LABEL[borneBy], - }))} - allLabel="全部承担方式" - placeholder="搜索承担方式…" - width={170} - /> -
- - - -
- -
- 提示:点击表格行可四级层层展开 【加氢站 → 客户 → 车牌 → 单笔订单与核对明细】 -
-
-
- -
- 点击一行 逐级展开明细 - 左右滑动 查看更多字段 -
- - {/* 穿透树形表格 */} -
- - - - - - - - - - {isProfitDrill ? ( - <> - - - - - ) : isMonthDrill ? ( - <> - - - - - ) : isDayDrill ? ( - <> - - - - - ) : ( - <> - - - - )} - - - - {treeAxis === 'customer' ? customerTree.map((customer) => { - const customerKey = `root_${customer.customerId}`; - const customerExpanded = !!expandedCustomers[customerKey]; - const customerOrderCount = customer.stations.reduce( - (sum, station) => sum + station.vehicles.reduce((vehicleSum, vehicle) => vehicleSum + vehicle.count, 0), - 0, - ); - - return ( - - toggleCustomer('root', customer.customerId)} - > - - - - - - - {renderMetricCells(customer.totalKg, feeWanToYuan(String(customer.totalFeeWan)))} - - - {customerExpanded && customer.stations.map((station) => { - const customerStationKey = `${customer.customerId}_${station.stationId}`; - const stationExpanded = !!expandedStations[customerStationKey]; - const stationOrderCount = station.vehicles.reduce((sum, vehicle) => sum + vehicle.count, 0); - return ( - - toggleStation(customerStationKey)} - > - - - - - - - {renderMetricCells(station.totalKg, feeWanToYuan(station.totalFeeWan))} - - - {stationExpanded && station.vehicles.map((vehicle) => { - const vehicleKey = `${station.stationId}_${customer.customerId}_${vehicle.plateNo}`; - const vehicleExpanded = !!expandedVehicles[vehicleKey]; - const showAllOrders = !!expandedAllVehicleOrders[vehicleKey]; - const orders = showAllOrders ? vehicle.orders : vehicle.orders.slice(0, 5); - const isNoPlate = !vehicle.plateNo || /无车牌/.test(vehicle.plateNo); - const isOwnFleet = !isNoPlate && vehicle.fleetCategory === 'own'; - return ( - - toggleVehicle(station.stationId, customer.customerId, vehicle.plateNo)} - > - - - - - - - {renderMetricCells(vehicle.kg, vehicle.amount)} - - {vehicleExpanded && orders.map((order) => ( - - - - - - - - {renderMetricCells(order.kg, order.amount)} - - ))} - {vehicleExpanded && vehicle.orders.length > 5 && ( - - - - )} - - ); - })} - - ); - })} - - ); - }) : 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)} - - ))} - - - - - - )} - - ); - })} - - ); - })} - - ); - })} - -
{treeAxis === 'customer' ? '客户 / 加氢站 / 车辆与来源明细' : '加氢站 / 客户 / 车辆与来源明细'}类型 / 归属承担方式数据来源及凭证号核对状态加氢笔数收入 (元)成本 (元)利润 (元)本月加氢量 (Kg)本月加氢费 (元)加氢费占累计本日加氢量 (Kg)本日加氢费 (元)加氢费占月比加氢总量 (Kg)加氢金额 (元)
- - {customerExpanded ? '▼' : '►'} - - 客户:{customer.customerName} - - ({customer.stations.length} 个加氢站) - - -{renderBorneTag(customer.borneBy)}跨站汇总-{customerOrderCount} 笔
- - {stationExpanded ? '▼' : '►'} - - └─ 加氢站:{station.stationName} - -{renderBorneTag(customer.borneBy)}{station.vehicles.length} 辆车挂载-{stationOrderCount} 笔
- - {vehicleExpanded ? '▼' : '►'} - - {isNoPlate ? '无车牌' : vehicle.plateNo} - {renderFleetTag(isOwnFleet)}{renderBorneTag(vehicle.borneBy ?? customer.borneBy)}{renderSourceTag(vehicle.source)}{renderVehicleVerifyTag(vehicle.fleetCategory, computeVehicleVerifyStatus(vehicle.orders, vehicle.fleetCategory))}{vehicle.count} 笔
└── 订单 {order.orderId} ({order.time})单价 ¥{order.unitPrice.toFixed(2)}/Kg{renderBorneTag(order.borneBy ?? customer.borneBy)}{renderSourceTag(order.source)}{renderOrderVerifyTag(vehicle.fleetCategory, order.verifyStatus)}1 笔
- -
- - {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} - -{renderBorneTag(cust.borneBy)}{cust.vehicles.length} 辆车挂载- - {cust.vehicles.reduce((sum, v) => sum + v.count, 0)} 笔 -
- - {isVhExpanded ? '▼' : '►'} - - {plateDisplay} - - ({vh.count} 笔订单) - - - {renderFleetTag(isOwnFleet)} - {renderBorneTag(vh.borneBy ?? cust.borneBy)} - {renderSourceTag(vh.source)} - - {renderVehicleVerifyTag(vh.fleetCategory, aggVerify)} - - {vh.count} 笔 -
- - └── - 订单编号 - - {ord.orderId} - - ({ord.time}) - - - - {ord.time} - {ord.kg.toLocaleString('zh-CN')} Kg - ¥{ord.amount.toLocaleString('zh-CN')} - {renderOrderVerifyTag(vh.fleetCategory, ord.verifyStatus)} - - - 订单 {ord.orderId} - {renderSourceTag(ord.source)} - - - - - 单价 ¥{ord.unitPrice.toFixed(2)}/Kg - - {renderBorneTag(ord.borneBy ?? cust.borneBy)} - {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; -} - type DrillPeriodMode = 'month' | 'custom'; function resolveDrillPeriod(mode: DrillPeriodMode, monthValue: string, customStart: string, customEnd: string) { @@ -4530,791 +2525,6 @@ function resolveDrillPeriod(mode: DrillPeriodMode, monthValue: string, customSta return { start: `${monthValue}-01`, end, label: `${monthValue}-01 至 ${end}` }; } -interface DrillPeriodControlsProps { - mode: DrillPeriodMode; - monthValue: string; - customStart: string; - customEnd: string; - onModeChange: (mode: DrillPeriodMode) => void; - onMonthChange: (value: string) => void; - onCustomStartChange: (value: string) => void; - onCustomEndChange: (value: string) => void; -} - -function DrillPeriodControls(props: DrillPeriodControlsProps) { - const [periodOpen, setPeriodOpen] = useState(false); - const periodOptions = [ - { value: '2026-08', label: '2026年8月' }, - { value: '2026-07', label: '2026年7月' }, - { value: '2026-06', label: '2026年6月' }, - { value: 'custom', label: '自定义日期' }, - ]; - const selectedPeriod = props.mode === 'custom' ? 'custom' : props.monthValue; - const selectedPeriodLabel = periodOptions.find((item) => item.value === selectedPeriod)?.label || '2026年8月'; - - const selectPeriod = (value: string) => { - setPeriodOpen(false); - if (value === 'custom') { - props.onModeChange('custom'); - return; - } - props.onModeChange('month'); - props.onMonthChange(value); - }; - - return ( -
-
- - {periodOpen && ( -
- {periodOptions.map((item) => ( - - ))} -
- )} -
- {props.mode === 'custom' && ( -
- props.onCustomStartChange(e.target.value)} /> - - props.onCustomEndChange(e.target.value)} /> -
- )} -
- ); -} - -function CustomerBillDrillModal({ customerName, year, onClose }: CustomerBillDrillModalProps) { - const [searchTerm, setSearchTerm] = useState(''); - const [periodMode, setPeriodMode] = useState('month'); - const [monthValue, setMonthValue] = useState(`${year}-08`); - const [customStart, setCustomStart] = useState(`${year}-08-01`); - const [customEnd, setCustomEnd] = useState(`${year}-08-08`); - const [expandedDates, setExpandedDates] = useState>({ - '2026-08-08': true, // 默认展开最新一日 - }); - - const custSummary = MOCK_CUSTOMER_SUMMARY_LIST.find((c) => c.name === customerName); - const customerBorneBy = custSummary ? summaryBorneBy(custSummary.bearer, custSummary.name) : undefined; - const summaryKgT = custSummary ? parseFloat(custSummary.kgT) : 0; - const summaryCostWan = custSummary ? parseFloat(custSummary.costWan) : 0; - const periodRange = useMemo(() => resolveDrillPeriod(periodMode, monthValue, customStart, customEnd), [periodMode, monthValue, customStart, customEnd]); - - 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:00', kg: 45.2, receivable: 1356.0 }, - { plateNo: '浙A66666F', fleetCategory: 'own' as const, stationName: '嘉兴嘉锦加氢站', time: '2026-08-08 09:30:00', kg: 54.8, receivable: 1644.0 }, - { plateNo: '粤B12345D', fleetCategory: 'external' as const, stationName: '嘉兴中石化滨海加氢站', time: '2026-08-08 10:10:00', kg: 350.0, receivable: 10500.0 }, - { plateNo: '粤B99881D', fleetCategory: 'external' as const, stationName: '嘉兴嘉锦加氢站', time: '2026-08-08 14:20:00', 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:00', kg: 48.0, receivable: 1440.0 }, - { plateNo: '浙A33333F', fleetCategory: 'own' as const, stationName: '嘉兴中石化滨海加氢站', time: '2026-08-07 16:45:00', kg: 52.0, receivable: 1560.0 }, - { plateNo: '沪A66128D', fleetCategory: 'external' as const, stationName: '嘉兴中石化滨海加氢站', time: '2026-08-07 17:30:00', 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:00', kg: 42.5, receivable: 1275.0 }, - { plateNo: '川A88901', fleetCategory: 'external' as const, stationName: '成都中石化天府机场高速北站加氢站', time: '2026-08-06 13:15:00', kg: 210.0, receivable: 6300.0 }, - { plateNo: '川A88902', fleetCategory: 'external' as const, stationName: '成都中石化天府机场高速北站加氢站', time: '2026-08-06 15:50:00', 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:00', kg: 50.0, receivable: 1500.0 }, - { plateNo: '渝A66881', fleetCategory: 'external' as const, stationName: '桐乡中石化绿能加氢站', time: '2026-08-05 14:05:00', kg: 310.0, receivable: 9300.0 }, - ], - }, - ]; - - return dates.filter((d) => d.date >= periodRange.start && d.date <= periodRange.end).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, periodRange.start, periodRange.end]); - - 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]); - - const totalCostSum = useMemo(() => billData.reduce((sum, d) => sum + d.totalCost, 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}」客户账单明细 -
-
统计时间范围:{periodRange.label}
-
-
- -
- - -
-
- - {/* Modal 内容区 */} -
- {/* 列表关键字段汇总 */} -
-
- 承担方 - - {customerBorneBy ? BORNE_BY_LABEL[customerBorneBy] : '-'} - -
-
- 加氢量 - - {(totalKgSum / 1000).toFixed(2)}{' '} - T - -
-
- 成本支出 - - ¥{(totalCostSum / 10000).toFixed(2)}{' '} - 万元 - -
-
- 应收 - - ¥{totalReceivableSum.toLocaleString('zh-CN', { maximumFractionDigits: 2 })} 元 - -
-
- 已收 - - 未接入 - -
-
- 未收 - - 未接入 - -
-
- - {/* 搜寻卡 */} -
-
- -
- - 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} 笔 - - {renderBorneTag(customerBorneBy)} - - {day.totalKg.toLocaleString('zh-CN')} Kg - - ¥{day.totalCost.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} - - ¥{day.totalReceivable.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} - - - 未接入 - - - - 未接入 - -
-
- └── - - {rec.plateNo || '无车牌(散车)'} - {rec.time.slice(11, 16)} - -
-
{rec.stationName} - {renderBorneTag(customerBorneBy)} - - {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 [periodMode, setPeriodMode] = useState('month'); - const [monthValue, setMonthValue] = useState(`${year}-08`); - const [customStart, setCustomStart] = useState(`${year}-08-01`); - const [customEnd, setCustomEnd] = useState(`${year}-08-08`); - 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 summaryIncomeWan = stSummary ? parseFloat(stSummary.incomeWan) : 0; - const periodRange = useMemo(() => resolveDrillPeriod(periodMode, monthValue, customStart, customEnd), [periodMode, monthValue, customStart, customEnd]); - - 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:00', kg: 450.2, income: 13506.0 }, - { plateNo: '浙A66666F', fleetCategory: 'own' as const, customerName: '嘉兴市乍浦港口经营有限公司', time: '2026-08-08 09:30:00', kg: 540.8, income: 16224.0 }, - { plateNo: '粤B12345D', fleetCategory: 'external' as const, customerName: '广东氢动力科技服务有限公司', time: '2026-08-08 10:10:00', kg: 350.0, income: 10500.0 }, - { plateNo: '沪A66128D', fleetCategory: 'external' as const, customerName: '上海明纳物流有限公司', time: '2026-08-08 14:20:00', kg: 280.0, income: 8400.0 }, - ], - }, - { - date: '2026-08-07', - records: [ - { plateNo: '浙A88888F', fleetCategory: 'own' as const, customerName: '羚牛氢能科技(广东)有限公司', time: '2026-08-07 11:20:00', kg: 480.0, income: 14400.0 }, - { plateNo: '浙A33333F', fleetCategory: 'own' as const, customerName: '嘉兴益顺冷链物流有限公司', time: '2026-08-07 16:45:00', kg: 520.0, income: 15600.0 }, - { plateNo: '粤B99881D', fleetCategory: 'external' as const, customerName: '嘉兴智奇供应链管理有限公司', time: '2026-08-07 17:30:00', kg: 410.0, income: 12300.0 }, - ], - }, - { - date: '2026-08-06', - records: [ - { plateNo: '浙A88888F', fleetCategory: 'own' as const, customerName: '羚牛氢能科技(广东)有限公司', time: '2026-08-06 09:10:00', kg: 425.0, income: 12750.0 }, - { plateNo: '川A88901', fleetCategory: 'external' as const, customerName: '四川群彬物流有限公司', time: '2026-08-06 13:15:00', kg: 610.0, income: 18300.0 }, - { plateNo: '川A88902', fleetCategory: 'external' as const, customerName: '四川拱照物流有限公司', time: '2026-08-06 15:50:00', kg: 580.0, income: 17400.0 }, - ], - }, - { - date: '2026-08-05', - records: [ - { plateNo: '浙A66666F', fleetCategory: 'own' as const, customerName: '嘉兴市乍浦港口经营有限公司', time: '2026-08-05 10:40:00', kg: 500.0, income: 15000.0 }, - { plateNo: '渝A66881', fleetCategory: 'external' as const, customerName: '重庆金时源供应链有限公司', time: '2026-08-05 14:05:00', kg: 710.0, income: 21300.0 }, - ], - }, - ]; - - const dailySummaries = dates.filter((d) => d.date >= periodRange.start && d.date <= periodRange.end).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); - - return { - ...d, - records: filteredRecords, - vehicleCount: new Set(filteredRecords.map((record) => record.plateNo || '无车牌')).size, - totalKg: Math.round(dayKg * 10) / 10, - totalIncome: Math.round(dayIncome * 100) / 100, - avgPrice: dayKg > 0 ? Math.round((dayIncome / dayKg) * 100) / 100 : 0, - }; - }).filter((d) => d.records.length > 0); - - return dailySummaries.map((day, index) => { - const previousDay = dailySummaries[index + 1]; - const dayChangePct = previousDay?.totalKg - ? Math.round(((day.totalKg - previousDay.totalKg) / previousDay.totalKg) * 1000) / 10 - : null; - - return { ...day, dayChangePct }; - }); - }, [searchTerm, periodRange.start, periodRange.end]); - - 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]); - - const periodRecordStats = useMemo(() => { - const records = stationData.flatMap((day) => day.records); - return { - vehicleCount: new Set(records.map((record) => record.plateNo || '无车牌')).size, - recordCount: records.length, - avgPrice: totalKgSum > 0 ? totalIncomeSum / totalKgSum : 0, - }; - }, [stationData, totalIncomeSum, totalKgSum]); - - // 导出加氢站按日账单 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}」加氢汇总明细 -
-
统计时间范围:{periodRange.label}
-
-
- -
- - -
-
- - {/* Modal 内容区 */} -
- {/* 当前筛选范围内用于经营判断的核心指标。 */} -
-
- 加氢量 - - {(totalKgSum / 1000).toFixed(2)} T - -
-
- 氢费收入 - - ¥{(totalIncomeSum / 10000).toFixed(2)} 万元 - -
-
- 平均单价 - - ¥{periodRecordStats.avgPrice.toFixed(2)} /Kg - -
-
- 车辆数 / 加氢笔数 - - {periodRecordStats.vehicleCount} - / - {periodRecordStats.recordCount} - -
-
- - {/* 搜寻卡 */} -
-
- -
- - setSearchTerm(e.target.value)} - /> - {searchTerm && ( - - )} -
- -
点击日期展开客户、车牌、加氢时间和单笔明细
-
-
- -
‹ 左右滑动查看完整数据与状态 ›
- - {/* 主表用于经营判断,展开层用于追查单笔原因。 */} -
- - - - - - - - - - - - - {stationData.map((day) => { - const isExpanded = !!expandedDates[day.date]; - - return ( - - {/* Level 1: 按日经营汇总 */} - toggleDate(day.date)} - > - - - - - - - - - {/* Level 2: 客户、车牌、时间和单笔经营明细 */} - {isExpanded && ( - - - - )} - - ); - })} - -
日期车辆数 / 加氢笔数加氢量(Kg)较前日氢费收入(元)平均单价(元/Kg)
-
- {isExpanded ? '▼' : '►'} - 📅 {day.date} -
-
- {day.vehicleCount} 辆 / {day.records.length} 笔 - - {day.totalKg.toLocaleString('zh-CN')} Kg - = 0 ? 'is-up' : 'is-down'}`} - style={{ textAlign: 'right' }} - > - {day.dayChangePct === null - ? '—' - : `${day.dayChangePct > 0 ? '+' : ''}${day.dayChangePct.toFixed(1)}%`} - - ¥{day.totalIncome.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} - - ¥{day.avgPrice.toFixed(2)} -
- - - - - - - - - - - - - - - - - - - {day.records.map((rec, rIdx) => ( - - - - - - - - ))} - -
车牌 / 时间客户车辆归属单笔加氢量(Kg)单笔氢费收入(元)
- - {rec.plateNo || '无车牌(散车)'} - {rec.time.slice(11, 16)} - - {rec.customerName} - - {rec.fleetCategory === 'own' ? '羚牛车辆' : '外部车辆'} - - - {rec.kg.toFixed(1)} - - ¥{rec.income.toLocaleString('zh-CN', { minimumFractionDigits: 2 })} -
-
-
-
-
-
- ); -} - function StationMonthTable({ rows, activeId, diff --git a/src/modules/energy/hydrogen/station-daily/StationDailyApp.tsx b/src/modules/energy/hydrogen/station-daily/StationDailyApp.tsx index 316bea8..4f8a59d 100644 --- a/src/modules/energy/hydrogen/station-daily/StationDailyApp.tsx +++ b/src/modules/energy/hydrogen/station-daily/StationDailyApp.tsx @@ -129,7 +129,8 @@ export const StationDailyApp: React.FC = ({ const setStartDate = onStartDateChange ?? setLocalStartDate; const setEndDate = onEndDateChange ?? setLocalEndDate; const [drillStationId, setDrillStationId] = useState(null); - const [updatedAt, setUpdatedAt] = useState('2026-08-12 10:00'); + // 初值不用伪造时间戳;成功回调会写入真实 latestLedgerTime,缺数据时与之一致。 + const [updatedAt, setUpdatedAt] = useState('暂无账本更新时间'); const [tick, setTick] = useState(0); const [kpiDrill, setKpiDrill] = useState(null); const [liveBoard, setLiveBoard] = useState(null); diff --git a/src/modules/hydrogen-heatmap/HydrogenHeatmapModule.tsx b/src/modules/hydrogen-heatmap/HydrogenHeatmapModule.tsx index 83a2dbd..a0359c6 100644 --- a/src/modules/hydrogen-heatmap/HydrogenHeatmapModule.tsx +++ b/src/modules/hydrogen-heatmap/HydrogenHeatmapModule.tsx @@ -4,10 +4,11 @@ import HydrogenAmapCanvas from './HydrogenAmapCanvas'; import HydrogenHeatmapDetailPanel from './HydrogenHeatmapDetailPanel'; import HydrogenHeatmapFilters from './HydrogenHeatmapFilters'; import { fetchAmapConfig, fetchHydrogenHeatmapMeta, fetchHydrogenHeatmapPoints, fetchNearbyHydrogenStations } from './api'; +import { recentDayRange } from '../../shared/date-range'; import type { AmapConfig, HydrogenHeatmapMeta, HydrogenHeatmapMetric, HydrogenHeatmapResponse, HydrogenNearbyResponse, HydrogenPayer } from './types'; -const DEFAULT_START = '2026-01-01'; -const DEFAULT_END = '2026-07-13'; +// 接口返回数据水位前的兜底范围:近 30 天滚动窗口(不再写死历史区间)。 +const FALLBACK_RANGE = recentDayRange(30); const integerFormat = new Intl.NumberFormat('zh-CN'); const kgFormat = new Intl.NumberFormat('zh-CN', { maximumFractionDigits: 1 }); @@ -37,8 +38,8 @@ export default function HydrogenHeatmapModule() { const [config, setConfig] = useState(null); const [data, setData] = useState(null); const [nearby, setNearby] = useState(null); - const [startDate, setStartDate] = useState(DEFAULT_START); - const [endDate, setEndDate] = useState(DEFAULT_END); + const [startDate, setStartDate] = useState(FALLBACK_RANGE.start); + const [endDate, setEndDate] = useState(FALLBACK_RANGE.end); const [query, setQuery] = useState(''); const [payer, setPayer] = useState('all'); const [metric, setMetric] = useState('kg'); @@ -112,8 +113,8 @@ export default function HydrogenHeatmapModule() { }, [deferredQuery, endDate, metric, payer, startDate]); const reset = () => { - setStartDate(meta?.startDate || DEFAULT_START); - setEndDate(meta?.endDate || DEFAULT_END); + setStartDate(meta?.startDate || FALLBACK_RANGE.start); + setEndDate(meta?.endDate || FALLBACK_RANGE.end); setQuery(''); setPayer('all'); setMetric('kg'); @@ -164,7 +165,7 @@ export default function HydrogenHeatmapModule() {
-

数据更新至 {meta?.endDate || DEFAULT_END}

+

数据更新至 {meta?.endDate || FALLBACK_RANGE.end}

@@ -180,8 +181,8 @@ export default function HydrogenHeatmapModule() { (null); const [data, setData] = useState(null); const [nearby, setNearby] = useState(null); - const [startDate, setStartDate] = useState(DEFAULT_START); - const [endDate, setEndDate] = useState(DEFAULT_END); + const [startDate, setStartDate] = useState(FALLBACK_RANGE.start); + const [endDate, setEndDate] = useState(FALLBACK_RANGE.end); const [query, setQuery] = useState(''); const [batchModel, setBatchModel] = useState(''); const [metric, setMetric] = useState('locations'); @@ -103,8 +104,8 @@ export default function VehicleHeatmapModule() { }, [batchModel, deferredQuery, endDate, startDate]); const reset = () => { - setStartDate(meta?.startDate || DEFAULT_START); - setEndDate(meta?.endDate || DEFAULT_END); + setStartDate(meta?.startDate || FALLBACK_RANGE.start); + setEndDate(meta?.endDate || FALLBACK_RANGE.end); setQuery(''); setBatchModel(''); setMetric('locations'); @@ -147,7 +148,7 @@ export default function VehicleHeatmapModule() {
-

数据更新至 {meta?.endDate || DEFAULT_END}

+

数据更新至 {meta?.endDate || FALLBACK_RANGE.end}