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,剩余全部合入"其他"