diff --git a/src/App.tsx b/src/App.tsx index ca162a0..5fbfe86 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Truck, Warehouse, @@ -289,86 +289,88 @@ export default function App() { }; // Derived data for inventory section - const filteredInventoryStats = inventoryData.filter((s) => { + const filteredInventoryStats = useMemo(() => inventoryData.filter((s) => { const mr = !inventoryFilters.region || s.region === inventoryFilters.region; const mc = !inventoryFilters.city || s.city === inventoryFilters.city; const mb = !inventoryFilters.brand || s.brand === inventoryFilters.brand; const mbt = !inventoryFilters.batch || s.batch === inventoryFilters.batch; const mm = !inventoryFilters.model || s.model.toLowerCase().includes(inventoryFilters.model.toLowerCase()); return mr && mc && mb && mbt && mm; - }); + }), [inventoryData, inventoryFilters]); - const uniqueInventoryBrands = Array.from(new Set(inventoryData.map((s) => s.brand).filter(Boolean))); - const uniqueInventoryRegions = Array.from(new Set(inventoryData.map((s) => s.region))); - const uniqueInventoryCities = Array.from(new Set(inventoryData.map((s) => s.city).filter(Boolean))); - const uniqueInventoryBatches = Array.from(new Set(inventoryData.map((s) => s.batch).filter(Boolean))); + const uniqueInventoryBrands = useMemo(() => Array.from(new Set(inventoryData.map((s) => s.brand).filter(Boolean))), [inventoryData]); + const uniqueInventoryRegions = useMemo(() => Array.from(new Set(inventoryData.map((s) => s.region))), [inventoryData]); + const uniqueInventoryCities = useMemo(() => Array.from(new Set(inventoryData.map((s) => s.city).filter(Boolean))), [inventoryData]); + const uniqueInventoryBatches = useMemo(() => Array.from(new Set(inventoryData.map((s) => s.batch).filter(Boolean))), [inventoryData]); - const inventoryByRegion: Record> = {}; - for (const s of filteredInventoryStats) { - if (!inventoryByRegion[s.region]) inventoryByRegion[s.region] = {}; - if (!inventoryByRegion[s.region][s.city]) inventoryByRegion[s.region][s.city] = []; - inventoryByRegion[s.region][s.city].push(s); - } + const inventoryByRegion = useMemo(() => { + const result: Record> = {}; + for (const s of filteredInventoryStats) { + if (!result[s.region]) result[s.region] = {}; + if (!result[s.region][s.city]) result[s.region][s.city] = []; + result[s.region][s.city].push(s); + } + return result; + }, [filteredInventoryStats]); - const INVENTORY_TYPE_ORDER = ['4.5T普货', '4.5T冷链', '18T', '49T', '挂车', '其他']; - const inventoryByModelRaw: Record> = {}; - for (const s of filteredInventoryStats) { - if (!inventoryByModelRaw[s.type]) inventoryByModelRaw[s.type] = {}; - if (!inventoryByModelRaw[s.type][s.model]) inventoryByModelRaw[s.type][s.model] = []; - inventoryByModelRaw[s.type][s.model].push(s); - } - const inventoryByModel: Record> = {}; - for (const t of INVENTORY_TYPE_ORDER) { - if (inventoryByModelRaw[t]) inventoryByModel[t] = inventoryByModelRaw[t]; - } - for (const t of Object.keys(inventoryByModelRaw)) { - if (!inventoryByModel[t]) inventoryByModel[t] = inventoryByModelRaw[t]; - } + const inventoryByModel = useMemo(() => { + const INVENTORY_TYPE_ORDER = ['4.5T普货', '4.5T冷链', '18T', '49T', '挂车', '其他']; + const raw: Record> = {}; + for (const s of filteredInventoryStats) { + if (!raw[s.type]) raw[s.type] = {}; + if (!raw[s.type][s.model]) raw[s.type][s.model] = []; + raw[s.type][s.model].push(s); + } + const result: Record> = {}; + for (const t of INVENTORY_TYPE_ORDER) { if (raw[t]) result[t] = raw[t]; } + for (const t of Object.keys(raw)) { if (!result[t]) result[t] = raw[t]; } + return result; + }, [filteredInventoryStats]); // Derived data for dept section - const allManagersList = deptData.flatMap((d) => d.managers.map((m) => m.manager)).filter((v, i, a) => a.indexOf(v) === i).sort(); - const managerStats = deptData + const allManagersList = useMemo(() => deptData.flatMap((d) => d.managers.map((m) => m.manager)).filter((v, i, a) => a.indexOf(v) === i).sort(), [deptData]); + const managerStats = useMemo(() => deptData .flatMap((d) => d.managers) .filter((m) => selectedManager === 'All' || m.manager === selectedManager) - .sort((a, b) => b.total - a.total); + .sort((a, b) => b.total - a.total), [deptData, selectedManager]); // Derived data for customer section - const filteredCustomerStats = customerData.filter((s) => { + const filteredCustomerStats = useMemo(() => customerData.filter((s) => { const mc = !customerFilters.customer || s.customer.toLowerCase().includes(customerFilters.customer.toLowerCase()); const mb = !customerFilters.brand || s.brand === customerFilters.brand; const md = !customerFilters.department || s.department === customerFilters.department; const mm = !customerFilters.manager || s.manager.toLowerCase().includes(customerFilters.manager.toLowerCase()); const mr = !customerFilters.region || s.region === customerFilters.region; return mc && mb && md && mm && mr; - }); - const uniqueBrands = Array.from(new Set(customerData.map((s) => s.brand).filter(Boolean))); - const uniqueDepts = Array.from(new Set(customerData.map((s) => s.department).filter(Boolean))); - const uniqueRegions = Array.from(new Set(customerData.map((s) => s.region))); - const uniqueCities = Array.from(new Set(customerData.map((s) => s.city).filter(Boolean))); - const uniqueCustomerNames = Array.from(new Set(customerData.map((s) => s.customer).filter(Boolean))); - const uniqueCustomerManagers = Array.from(new Set(customerData.map((s) => s.manager).filter(Boolean))); - const uniqueInventoryModels = Array.from(new Set(inventoryData.map((s) => s.model).filter(Boolean))); - const uniqueModalPlates = Array.from(new Set(modalVehicles.map(v => v.plateNumber || v.vin).filter(Boolean))); - const uniqueModalModels = Array.from(new Set(modalVehicles.map(v => v.model).filter(Boolean))); - const uniqueModalBrands = Array.from(new Set(modalVehicles.map(v => v.brandLabel).filter((x): x is string => Boolean(x)))); - const uniqueModalLocations = Array.from(new Set(modalVehicles.map(v => v.location).filter(Boolean))); + }), [customerData, customerFilters]); + const uniqueBrands = useMemo(() => Array.from(new Set(customerData.map((s) => s.brand).filter(Boolean))), [customerData]); + const uniqueDepts = useMemo(() => Array.from(new Set(customerData.map((s) => s.department).filter(Boolean))), [customerData]); + const uniqueRegions = useMemo(() => Array.from(new Set(customerData.map((s) => s.region))), [customerData]); + const uniqueCities = useMemo(() => Array.from(new Set(customerData.map((s) => s.city).filter(Boolean))), [customerData]); + const uniqueCustomerNames = useMemo(() => Array.from(new Set(customerData.map((s) => s.customer).filter(Boolean))), [customerData]); + const uniqueCustomerManagers = useMemo(() => Array.from(new Set(customerData.map((s) => s.manager).filter(Boolean))), [customerData]); + const uniqueInventoryModels = useMemo(() => Array.from(new Set(inventoryData.map((s) => s.model).filter(Boolean))), [inventoryData]); + const uniqueModalPlates = useMemo(() => Array.from(new Set(modalVehicles.map(v => v.plateNumber || v.vin).filter(Boolean))), [modalVehicles]); + const uniqueModalModels = useMemo(() => Array.from(new Set(modalVehicles.map(v => v.model).filter(Boolean))), [modalVehicles]); + const uniqueModalBrands = useMemo(() => Array.from(new Set(modalVehicles.map(v => v.brandLabel).filter((x): x is string => Boolean(x)))), [modalVehicles]); + const uniqueModalLocations = useMemo(() => Array.from(new Set(modalVehicles.map(v => v.location).filter(Boolean))), [modalVehicles]); // Derived data for region section - const filteredRegionData = regionData.filter((r) => !regionFilters.region || r.region === regionFilters.region); + const filteredRegionData = useMemo(() => regionData.filter((r) => !regionFilters.region || r.region === regionFilters.region), [regionData, regionFilters.region]); // Filtered modal vehicles based on modal filters - const filteredModalVehicles = modalVehicles.filter((v) => { + const filteredModalVehicles = useMemo(() => modalVehicles.filter((v) => { const mp = !modalFilters.plateNumber || (v.plateNumber || v.vin || '').toLowerCase().includes(modalFilters.plateNumber.toLowerCase()); const mm = !modalFilters.model || v.model === modalFilters.model; const mb = !modalFilters.brand || v.brandLabel === modalFilters.brand; const ml = !modalFilters.location || v.location === modalFilters.location; return mp && mm && mb && ml; - }); + }), [modalVehicles, modalFilters]); - const filteredModalWeeklyDetail = modalWeeklyDetail.filter((v) => { + const filteredModalWeeklyDetail = useMemo(() => modalWeeklyDetail.filter((v) => { const mp = !modalFilters.plateNumber || v.plate_number.toLowerCase().includes(modalFilters.plateNumber.toLowerCase()); return mp; - }); + }), [modalWeeklyDetail, modalFilters.plateNumber]); if (loading && !summary) { return ( @@ -397,7 +399,29 @@ export default function App() { const SUMMARY = summary!; - const watermarkText = `羚牛氢能-${new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-')}`; + const customerPieData = useMemo(() => { + let data: { name: string; value: number }[] = []; + if (customerChartView === 'region') { + const map: Record = {}; + customerData.forEach(item => { map[item.region] = (map[item.region] || 0) + item.total; }); + data = Object.entries(map).map(([name, value]) => ({ name, value })).sort((a, b) => b.value - a.value); + } else { + const map: Record = {}; + customerData.forEach(item => { map[item.city] = (map[item.city] || 0) + item.total; }); + const tot = Object.values(map).reduce((a, b) => a + b, 0); + const threshold = tot * 0.05; + let other = 0; + Object.entries(map).forEach(([name, value]) => { + if (value >= threshold) data.push({ name, value }); + else other += value; + }); + if (other > 0) data.push({ name: '其他', value: other }); + data.sort((a, b) => b.value - a.value); + } + return data; + }, [customerData, customerChartView]); + + const watermarkText = useMemo(() => `羚牛氢能-${new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-')}`, [lastUpdate]); return (
@@ -2042,24 +2066,7 @@ export default function App() {
{(() => { const PIE_COLORS = ['#6366f1','#06b6d4','#f59e0b','#f43f5e','#10b981','#a855f7','#94a3b8']; - let pieData: { name: string; value: number }[] = []; - if (customerChartView === 'region') { - const map: { [k: string]: number } = {}; - customerData.forEach(item => { map[item.region] = (map[item.region] || 0) + item.total; }); - pieData = Object.entries(map).map(([name, value]) => ({ name, value })).sort((a,b) => b.value - a.value); - } else { - const map: { [k: string]: number } = {}; - customerData.forEach(item => { map[item.city] = (map[item.city] || 0) + item.total; }); - const tot = Object.values(map).reduce((a,b) => a+b, 0); - const threshold = tot * 0.05; - let other = 0; - Object.entries(map).forEach(([name, value]) => { - if (value >= threshold) pieData.push({ name, value }); - else other += value; - }); - if (other > 0) pieData.push({ name: '其他', value: other }); - pieData.sort((a,b) => b.value - a.value); - } + const pieData = customerPieData; const grandTotal = pieData.reduce((s,d) => s + d.value, 0); return (