perf: useMemo优化所有派生数据,解决页面操作卡顿
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:
kkfluous
2026-03-28 23:27:39 +08:00
parent 4e859423ee
commit 454b2f0913

View File

@@ -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">