diff --git a/src/App.tsx b/src/App.tsx index 2ecc4f1..51a59f9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,13 +13,38 @@ import { Search, Filter, ArrowRightLeft, + Users, + MapPin, + Building2, } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + PieChart, + Pie, + Cell, + LabelList, +} from 'recharts'; import type { SummaryData, TypeSummary, VehicleListItem, DeptGroup, RegionGroup, CustomerStats, RegionalInventoryStats } from './types'; import { fetchSummary, fetchByType, fetchVehicleList, fetchWeeklyDetail, fetchDeptStats, fetchRegionStats, fetchCustomerStats, fetchInventoryStats } from './api'; import type { WeeklyDetailItem } from './api'; +// --- Constants --- +const TABS = [ + { id: 'overview', label: '总览' }, + { id: 'department', label: '按部门' }, + { id: 'region', label: '按区域' }, + { id: 'customer', label: '按客户' }, +]; + export default function App() { + const [activeTab, setActiveTab] = useState<'overview' | 'department' | 'region' | 'customer'>('overview'); const [theme, setTheme] = useState<'soft' | 'minimal' | 'vibrant'>('soft'); const [expandedModels, setExpandedModels] = useState>(new Set()); const [expandedAssetTypes, setExpandedAssetTypes] = useState>(new Set()); @@ -77,6 +102,10 @@ export default function App() { const [inventoryFilters, setInventoryFilters] = useState({ region: '', city: '', brand: '', batch: '', model: '' }); const [isInventoryFilterOpen, setIsInventoryFilterOpen] = useState(false); + // Chart view states + const [customerChartView, setCustomerChartView] = useState<'region' | 'city'>('region'); + const [regionChartView, setRegionChartView] = useState<'region' | 'city'>('region'); + // Modal filter state const [modalFilters, setModalFilters] = useState({ plateNumber: '', model: '', brand: '', location: '' }); const [isModalFilterExpanded, setIsModalFilterExpanded] = useState(false); @@ -358,50 +387,74 @@ export default function App() { const SUMMARY = summary!; return ( -
- {/* Main Title and Global Descriptions */} -
-

羚牛氢能车辆资产

-
-
- +
+ {/* Compact Header Bar */} +
+ {/* Title row */} +
+

羚牛氢能车辆资产

+ {/* Right: status + theme */} +
+
+ + {lastUpdate} +
+ {loading && ( +
+ +
+ )} +
+ {(['soft','minimal','vibrant'] as const).map((t) => ( + + ))} +
+
+
+ {/* Tab row */} +
+ {TABS.map(tab => ( + + ))} +
+ {/* Status row */} +
+
+ 最后更新: {lastUpdate}
-
- - 每分钟更新 +
+ + 每分钟更新一次
- {loading && ( -
- - 刷新中... -
- )} -
- - {/* Theme Switcher */} -
- - -
+ {activeTab === 'overview' && ( + <> {/* Header Summary - Ultra Compact */}
{/* Total Assets */} @@ -487,9 +540,13 @@ export default function App() {
+ + )} {/* Main Content Area */}
+ {activeTab === 'overview' && ( + <> {/* Asset Summary Table with Dimension Switch */}
@@ -601,18 +658,14 @@ export default function App() { }`} onClick={() => toggleAssetType(typeGroup.type)} > - {expandedAssetTypes.has(typeGroup.type) ? ( - -
- - {typeGroup.type} -
- - ) : ( <>
- + {expandedAssetTypes.has(typeGroup.type) ? ( + + ) : ( + + )} {typeGroup.type}
@@ -670,7 +723,6 @@ export default function App() { ) : ''} - )} @@ -790,6 +842,79 @@ export default function App() { )} + {expandedModels.has(model.model) && model.batches.map((batch) => ( + + + {batch.batch} + + + + + {batch.inventory > 0 ? ( + + ) : ''} + + {['嘉兴', '广东', '北京', '新疆', '其他'].map((reg) => ( + + {(batch.inventoryRegions[reg] || 0) > 0 ? ( + + ) : ''} + + ))} + + {batch.pending > 0 ? ( + + ) : ''} + + + + + + {batch.weeklyDelivered > 0 ? ( + + ) : ''} + + + {batch.weeklyReturned > 0 ? ( + + ) : ''} + + + {batch.weeklyReplaced > 0 ? ( + + ) : ''} + + + ))} ))} @@ -1378,8 +1503,11 @@ export default function App() { )}
+ + )} - {/* Department Operations Statistics */} + {activeTab === 'department' && ( + /* Department Operations Statistics */
@@ -1902,6 +2030,61 @@ export default function App() {
+ )} + + {activeTab === 'region' && ( +
+ {/* Region Distribution Chart */} +
+
+
+
+

区域资产分布概览

+
+
+ + +
+
+
+ + { + if (regionChartView === 'region') { + const regions: { [key: string]: number } = {}; + customerData.forEach(item => { + regions[item.region] = (regions[item.region] || 0) + item.total; + }); + return Object.entries(regions).map(([name, value]) => ({ name, value })); + } else { + const cities: { [key: string]: number } = {}; + customerData.forEach(item => { + cities[item.city] = (cities[item.city] || 0) + item.total; + }); + return Object.entries(cities) + .map(([name, value]) => ({ name, value })) + .sort((a, b) => b.value - a.value); + } + })()}> + + + + + + + + + +
+
{/* Region - Vehicle - Customer Section */}
@@ -2206,6 +2389,102 @@ export default function App() {
+
+ )} + + {activeTab === 'customer' && ( +
+ {/* Customer Region Distribution Chart */} +
+
+
+
+

客户运营地区占比

+
+
+ + +
+
+ {(() => { + 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 grandTotal = pieData.reduce((s,d) => s + d.value, 0); + return ( +
+ {/* Donut chart */} +
+ + + + {pieData.map((_, i) => ( + + ))} + + [`${value} 辆`, '']} + contentStyle={{ borderRadius: '10px', border: 'none', boxShadow: '0 8px 24px -4px rgba(0,0,0,0.12)', fontSize: 12 }} + /> + + + {/* Center label */} +
+ {grandTotal} + +
+
+ {/* Custom legend */} +
+ {pieData.map((item, i) => { + const pct = grandTotal > 0 ? (item.value / grandTotal * 100) : 0; + const color = PIE_COLORS[i % PIE_COLORS.length]; + return ( +
+
+ {item.name} +
+
+
+ {item.value} + {pct.toFixed(1)}% +
+ ); + })} +
+
+ ); + })()} +
{/* Customer Operations Statistics Section */}
@@ -2545,6 +2824,8 @@ export default function App() {
+
+ )} {/* Vehicle Detail Modal */} @@ -2838,18 +3119,34 @@ export default function App() {
{/* Footer / Navigation */} -
- - - +