perf: useMemo优化所有派生数据,解决页面操作卡顿
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- 所有filter/unique/grouped派生数据包裹useMemo,避免每次渲染重算 - 库存筛选、客户筛选、区域筛选、弹窗筛选的派生列表全部memoize - 饼图数据提取为customerPieData useMemo,不再inline IIFE - 水印文本memoize,仅lastUpdate变化时重算 - 预计减少每次交互的JS执行时间80%+ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
141
src/App.tsx
141
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<string, Record<string, RegionalInventoryStats[]>> = {};
|
||||
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<string, Record<string, RegionalInventoryStats[]>> = {};
|
||||
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<string, Record<string, RegionalInventoryStats[]>> = {};
|
||||
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<string, Record<string, RegionalInventoryStats[]>> = {};
|
||||
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<string, Record<string, RegionalInventoryStats[]>> = {};
|
||||
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<string, Record<string, RegionalInventoryStats[]>> = {};
|
||||
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<string, number> = {};
|
||||
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<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) 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 (
|
||||
<div className="min-h-screen bg-[#F8F9FB] text-gray-800 font-sans p-6 pb-20 md:pb-6 relative">
|
||||
@@ -2042,24 +2066,7 @@ export default function App() {
|
||||
</div>
|
||||
{(() => {
|
||||
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 (
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-8 items-center">
|
||||
|
||||
Reference in New Issue
Block a user