feat: 按城市改为按省份,数据从realtime表province获取,展示前5
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- 后端region-chart支持groupBy=province,从realtime表读取省份 - 区域柱状图和客户饼图"按城市"改为"按省份" - 省份展示前5,其余合入"其他" - 前端state类型从'city'改为'province' Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
56
src/App.tsx
56
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<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);
|
||||
return Object.entries(map).map(([name, value]) => ({ name, value })).sort((a, b) => b.value - a.value);
|
||||
} else {
|
||||
const cityMap: Record<string, number> = {};
|
||||
const cityToRegion: Record<string, string> = {};
|
||||
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'}`}
|
||||
>按区域</button>
|
||||
<button
|
||||
onClick={() => setRegionChartView('city')}
|
||||
className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${regionChartView === 'city' ? 'bg-white text-blue-700 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
|
||||
>按城市</button>
|
||||
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'}`}
|
||||
>按省份</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-64 w-full">
|
||||
@@ -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' }}
|
||||
/>
|
||||
<Bar dataKey="value" fill="#3b82f6" radius={[4, 4, 0, 0]} barSize={regionChartView === 'city' ? 20 : 40}>
|
||||
<Bar dataKey="value" fill="#3b82f6" radius={[4, 4, 0, 0]} barSize={regionChartView === 'province' ? 20 : 40}>
|
||||
<LabelList dataKey="value" position="top" style={{ fill: '#64748b', fontSize: 11, fontWeight: 600 }} />
|
||||
</Bar>
|
||||
</BarChart>
|
||||
@@ -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'}`}
|
||||
>按区域</button>
|
||||
<button
|
||||
onClick={() => setCustomerChartView('city')}
|
||||
className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${customerChartView === 'city' ? 'bg-white text-emerald-600 shadow-sm' : 'text-gray-400 hover:text-gray-600'}`}
|
||||
>按城市</button>
|
||||
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'}`}
|
||||
>按省份</button>
|
||||
</div>
|
||||
</div>
|
||||
{(() => {
|
||||
|
||||
@@ -1034,14 +1034,31 @@ 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<string, number>();
|
||||
let counts: Map<string, number>;
|
||||
if (groupBy === 'province') {
|
||||
// Get realtime province data
|
||||
const [rows] = await pool.query<any[]>(`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<string, string>();
|
||||
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<string, number>();
|
||||
for (const v of operating) {
|
||||
const key = groupBy === 'city' ? resolveCity(v.city, v.province) : mapMacroRegion(v.province, v.city);
|
||||
const prov = plateProvince.get(v.plateNumber) || '未知';
|
||||
counts.set(prov, (counts.get(prov) || 0) + 1);
|
||||
}
|
||||
} else {
|
||||
counts = new Map<string, number>();
|
||||
for (const v of operating) {
|
||||
const key = mapMacroRegion(v.province, v.city);
|
||||
counts.set(key, (counts.get(key) || 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 分离"其他",对非"其他"排序取 Top N,剩余全部合入"其他"
|
||||
const otherCount = counts.get('其他') || 0;
|
||||
|
||||
Reference in New Issue
Block a user