From e910deac51a540acd4128489d47bfc2fe7b5d855 Mon Sep 17 00:00:00 2001 From: kkfluous Date: Sun, 29 Mar 2026 09:26:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8C=89=E5=9F=8E=E5=B8=82=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=8C=89=E7=9C=81=E4=BB=BD=EF=BC=8C=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E4=BB=8Erealtime=E8=A1=A8province=E8=8E=B7=E5=8F=96=EF=BC=8C?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E5=89=8D5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端region-chart支持groupBy=province,从realtime表读取省份 - 区域柱状图和客户饼图"按城市"改为"按省份" - 省份展示前5,其余合入"其他" - 前端state类型从'city'改为'province' Co-Authored-By: Claude Opus 4.6 (1M context) --- src/App.tsx | 56 +++++++++++++---------------------- src/server/routes/vehicles.ts | 27 +++++++++++++---- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7de7a1b..78bcd0a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -189,8 +189,8 @@ export default function App() { const [draftInventoryFilters, setDraftInventoryFilters] = useState({ region: '', city: '', brand: '', type: '', model: '' }); // Chart view states - const [customerChartView, setCustomerChartView] = useState<'region' | 'city'>('region'); - const [regionChartView, setRegionChartView] = useState<'region' | 'city'>('region'); + const [customerChartView, setCustomerChartView] = useState<'region' | 'province'>('region'); + const [regionChartView, setRegionChartView] = useState<'region' | 'province'>('region'); const [regionChartData, setRegionChartData] = useState<{ name: string; value: number }[]>([]); // Modal filter state @@ -250,7 +250,7 @@ export default function App() { // Fetch region chart data when view changes useEffect(() => { - fetchRegionChart(regionChartView, regionChartView === 'city' ? 5 : 8).then(setRegionChartData).catch(() => setRegionChartData([])); + fetchRegionChart(regionChartView, regionChartView === 'province' ? 5 : 8).then(setRegionChartData).catch(() => setRegionChartData([])); }, [regionChartView]); // Load modal vehicles @@ -505,38 +505,22 @@ export default function App() { return mp; }), [modalWeeklyDetail, modalFilters.plateNumber]); + const [customerProvinceData, setCustomerProvinceData] = useState<{ name: string; value: number }[]>([]); + useEffect(() => { + if (customerChartView === 'province') { + fetchRegionChart('province', 5).then(setCustomerProvinceData).catch(() => setCustomerProvinceData([])); + } + }, [customerChartView]); + 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); + return Object.entries(map).map(([name, value]) => ({ name, value })).sort((a, b) => b.value - a.value); } else { - const cityMap: Record = {}; - const cityToRegion: Record = {}; - customerData.forEach(item => { - cityMap[item.city] = (cityMap[item.city] || 0) + item.total; - if (!cityToRegion[item.city]) cityToRegion[item.city] = item.region; - }); - const tot = Object.values(cityMap).reduce((a, b) => a + b, 0); - const threshold = tot * 0.05; - let other = 0; - Object.entries(cityMap).forEach(([name, value]) => { - if (value >= threshold) data.push({ name, value }); - else other += value; - }); - if (other > 0) data.push({ name: '其他', value: other }); - // Sort by region group, then by value within group - const regionOrder = ['华东', '华南', '华中', '华北', '西北', '西南', '其他']; - data.sort((a, b) => { - const ra = regionOrder.indexOf(cityToRegion[a.name] || '其他'); - const rb = regionOrder.indexOf(cityToRegion[b.name] || '其他'); - if (ra !== rb) return ra - rb; - return b.value - a.value; - }); + return customerProvinceData; } - return data; - }, [customerData, customerChartView]); + }, [customerData, customerChartView, customerProvinceData]); 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]); @@ -1953,9 +1937,9 @@ export default function App() { className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${regionChartView === 'region' ? 'bg-white text-blue-700 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`} >按区域 + onClick={() => setRegionChartView('province')} + className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${regionChartView === 'province' ? 'bg-white text-blue-700 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`} + >按省份
@@ -1968,7 +1952,7 @@ export default function App() { contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} cursor={{ fill: '#f8fafc' }} /> - + @@ -2227,9 +2211,9 @@ export default function App() { className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${customerChartView === 'region' ? 'bg-white text-emerald-600 shadow-sm' : 'text-gray-400 hover:text-gray-600'}`} >按区域 + onClick={() => setCustomerChartView('province')} + className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${customerChartView === 'province' ? 'bg-white text-emerald-600 shadow-sm' : 'text-gray-400 hover:text-gray-600'}`} + >按省份
{(() => { diff --git a/src/server/routes/vehicles.ts b/src/server/routes/vehicles.ts index c99a004..614f1ee 100644 --- a/src/server/routes/vehicles.ts +++ b/src/server/routes/vehicles.ts @@ -1034,13 +1034,30 @@ app.get('/debug', async (c) => { app.get('/region-chart', async (c) => { const vehicles = await getVehicles(); const operating = vehicles.filter((v) => v.status === 'Operating'); - const groupBy = c.req.query('groupBy') || 'region'; // 'region' | 'city' + const groupBy = c.req.query('groupBy') || 'region'; // 'region' | 'province' const top = Number(c.req.query('top')) || 8; - const counts = new Map(); - for (const v of operating) { - const key = groupBy === 'city' ? resolveCity(v.city, v.province) : mapMacroRegion(v.province, v.city); - counts.set(key, (counts.get(key) || 0) + 1); + let counts: Map; + if (groupBy === 'province') { + // Get realtime province data + const [rows] = await pool.query(`SELECT plate_number, province FROM tab_truck_remote_sync_realtime_info WHERE is_deleted = 0 AND plate_number IS NOT NULL`); + const plateProvince = new Map(); + for (const row of rows as any[]) { + const plate = (row.plate_number || '').trim(); + const prov = (row.province || '').replace(/省|市$/, '').trim(); + if (plate && prov) plateProvince.set(plate, prov); + } + counts = new Map(); + for (const v of operating) { + const prov = plateProvince.get(v.plateNumber) || '未知'; + counts.set(prov, (counts.get(prov) || 0) + 1); + } + } else { + counts = new Map(); + for (const v of operating) { + const key = mapMacroRegion(v.province, v.city); + counts.set(key, (counts.get(key) || 0) + 1); + } } // 分离"其他",对非"其他"排序取 Top N,剩余全部合入"其他"