feat: 按城市改为按省份,数据从realtime表province获取,展示前5
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:
kkfluous
2026-03-29 09:26:54 +08:00
parent db1e37b8bf
commit e910deac51
2 changed files with 42 additions and 41 deletions

View File

@@ -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>
{(() => {