All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3073 lines
197 KiB
TypeScript
3073 lines
197 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
||
import {
|
||
Truck,
|
||
Warehouse,
|
||
Activity,
|
||
PlusCircle,
|
||
MinusCircle,
|
||
History,
|
||
ChevronDown,
|
||
ChevronRight,
|
||
Info,
|
||
Loader2,
|
||
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, fetchRegionChart } 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<Set<string>>(new Set());
|
||
const [expandedAssetTypes, setExpandedAssetTypes] = useState<Set<string>>(new Set());
|
||
const [showPlateNumbers, setShowPlateNumbers] = useState<{
|
||
batch: string;
|
||
model: string;
|
||
location: string;
|
||
category?: 'Inventory' | 'Pending' | 'Delivered' | 'Returned' | 'Replaced' | 'Operating';
|
||
vehicleType?: string;
|
||
manager?: string;
|
||
customer?: string;
|
||
isColdChain?: boolean;
|
||
isTrailer?: boolean;
|
||
type?: string;
|
||
source?: string;
|
||
title?: string;
|
||
} | null>(null);
|
||
|
||
// Data state
|
||
const [summary, setSummary] = useState<SummaryData | null>(null);
|
||
const [processedData, setProcessedData] = useState<TypeSummary[]>([]);
|
||
const [modalVehicles, setModalVehicles] = useState<VehicleListItem[]>([]);
|
||
const [modalWeeklyDetail, setModalWeeklyDetail] = useState<WeeklyDetailItem[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [lastUpdate, setLastUpdate] = useState<string>('');
|
||
const [modalLoading, setModalLoading] = useState(false);
|
||
|
||
// Dept/Region/Customer data
|
||
const [deptData, setDeptData] = useState<DeptGroup[]>([]);
|
||
const [regionData, setRegionData] = useState<RegionGroup[]>([]);
|
||
const [customerData, setCustomerData] = useState<CustomerStats[]>([]);
|
||
|
||
// Dept section state
|
||
const [deptViewMode, setDeptViewMode] = useState<'department' | 'manager'>('department');
|
||
const [expandedDepts, setExpandedDepts] = useState<Set<string>>(new Set());
|
||
const [expandedManagerDetails, setExpandedManagerDetails] = useState<Set<string>>(new Set());
|
||
const [selectedManager, setSelectedManager] = useState<string>('All');
|
||
|
||
// Region section state
|
||
const [expandedRegions, setExpandedRegions] = useState<Set<string>>(new Set());
|
||
const [expandedRegionCities, setExpandedRegionCities] = useState<Set<string>>(new Set());
|
||
const [regionFilters, setRegionFilters] = useState({ region: '', city: '', customer: '' });
|
||
const [isRegionFilterOpen, setIsRegionFilterOpen] = useState(false);
|
||
|
||
// Customer section state
|
||
const [expandedCustomers, setExpandedCustomers] = useState<Set<string>>(new Set());
|
||
const [customerFilters, setCustomerFilters] = useState({ customer: '', brand: '', department: '', manager: '', region: '' });
|
||
const [isCustomerFilterOpen, setIsCustomerFilterOpen] = useState(false);
|
||
|
||
// Inventory statistics section state
|
||
const [inventoryData, setInventoryData] = useState<RegionalInventoryStats[]>([]);
|
||
const [inventoryTab, setInventoryTab] = useState<'region' | 'model'>('region');
|
||
const [expandedInventoryRegions, setExpandedInventoryRegions] = useState<Set<string>>(new Set());
|
||
const [expandedInventoryTypes, setExpandedInventoryTypes] = useState<Set<string>>(new Set(['4.5T普货']));
|
||
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');
|
||
const [regionChartData, setRegionChartData] = useState<{ name: string; value: number }[]>([]);
|
||
|
||
// Modal filter state
|
||
const [modalFilters, setModalFilters] = useState({ plateNumber: '', model: '', brand: '', location: '' });
|
||
const [isModalFilterExpanded, setIsModalFilterExpanded] = useState(false);
|
||
|
||
// Reset modal filters when modal opens
|
||
useEffect(() => {
|
||
if (showPlateNumbers) {
|
||
setModalFilters({ plateNumber: '', model: '', brand: '', location: '' });
|
||
}
|
||
}, [showPlateNumbers]);
|
||
|
||
const loadData = useCallback(async () => {
|
||
try {
|
||
setLoading(true);
|
||
setError(null);
|
||
const [s, byType, dept, region, cust, inv] = await Promise.all([
|
||
fetchSummary(),
|
||
fetchByType(),
|
||
fetchDeptStats(),
|
||
fetchRegionStats(),
|
||
fetchCustomerStats(),
|
||
fetchInventoryStats(),
|
||
]);
|
||
setSummary(s);
|
||
setProcessedData(byType);
|
||
setDeptData(dept);
|
||
setRegionData(region);
|
||
setCustomerData(cust);
|
||
setInventoryData(inv);
|
||
setLastUpdate(new Date().toLocaleString('zh-CN'));
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : '数据加载失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
loadData();
|
||
const interval = setInterval(loadData, 60 * 1000);
|
||
return () => clearInterval(interval);
|
||
}, [loadData]);
|
||
|
||
// Fetch region chart data when view changes
|
||
useEffect(() => {
|
||
fetchRegionChart(regionChartView, regionChartView === 'city' ? 5 : 8).then(setRegionChartData).catch(() => setRegionChartData([]));
|
||
}, [regionChartView]);
|
||
|
||
// Load modal vehicles
|
||
useEffect(() => {
|
||
if (!showPlateNumbers) {
|
||
setModalVehicles([]);
|
||
setModalWeeklyDetail([]);
|
||
return;
|
||
}
|
||
setModalLoading(true);
|
||
const cat = showPlateNumbers.category;
|
||
|
||
// Weekly categories use the dedicated weekly-detail endpoint
|
||
const weeklyTypes: Record<string, string> = { Delivered: 'delivered', Returned: 'returned', Replaced: 'replaced', Pending: 'pending' };
|
||
if (cat && weeklyTypes[cat]) {
|
||
setModalVehicles([]);
|
||
fetchWeeklyDetail(weeklyTypes[cat])
|
||
.then(setModalWeeklyDetail)
|
||
.catch(() => setModalWeeklyDetail([]))
|
||
.finally(() => setModalLoading(false));
|
||
return;
|
||
}
|
||
|
||
// Normal vehicle list
|
||
setModalWeeklyDetail([]);
|
||
const params: Record<string, string> = {};
|
||
if (showPlateNumbers.vehicleType) params.vehicleType = showPlateNumbers.vehicleType;
|
||
if (showPlateNumbers.batch !== 'All') params.batch = showPlateNumbers.batch;
|
||
if (showPlateNumbers.model !== 'All') params.model = showPlateNumbers.model;
|
||
if (showPlateNumbers.location !== 'All') params.location = showPlateNumbers.location;
|
||
if (cat === 'Inventory') params.status = 'Inventory';
|
||
if (cat === 'Operating') params.category = 'Operating';
|
||
if (showPlateNumbers.manager) params.manager = showPlateNumbers.manager;
|
||
if (showPlateNumbers.customer) params.customer = showPlateNumbers.customer;
|
||
if (!showPlateNumbers.type) {
|
||
if (showPlateNumbers.isColdChain !== undefined) params.isColdChain = String(showPlateNumbers.isColdChain);
|
||
if (showPlateNumbers.isTrailer !== undefined) params.isTrailer = String(showPlateNumbers.isTrailer);
|
||
}
|
||
// Map type field to backend vehicleType
|
||
if (showPlateNumbers.type) {
|
||
const t = showPlateNumbers.type;
|
||
if (t === '4.5T') {
|
||
if (showPlateNumbers.isColdChain === true) params.vehicleType = '4.5T冷链';
|
||
else if (showPlateNumbers.isColdChain === false) params.vehicleType = '4.5T普货';
|
||
} else if (t === '4.5T普货' || t === '4.5T冷链' || t === '18T' || t === '49T' || t === '挂车' || t === '其他') {
|
||
params.vehicleType = t;
|
||
} else if (t === '其他车型') {
|
||
if (showPlateNumbers.isTrailer === true) params.isTrailer = 'true';
|
||
else if (showPlateNumbers.isTrailer === false) params.vehicleType = '其他';
|
||
}
|
||
}
|
||
fetchVehicleList(params)
|
||
.then(setModalVehicles)
|
||
.catch(() => setModalVehicles([]))
|
||
.finally(() => setModalLoading(false));
|
||
}, [showPlateNumbers]);
|
||
|
||
const allTypesExpanded = processedData.length > 0 && processedData.every((t) => expandedAssetTypes.has(t.type));
|
||
|
||
const toggleAllAssetTypes = () => {
|
||
if (allTypesExpanded) {
|
||
setExpandedAssetTypes(new Set());
|
||
} else {
|
||
setExpandedAssetTypes(new Set(processedData.map((t) => t.type)));
|
||
}
|
||
};
|
||
|
||
const toggleAssetType = (type: string) => {
|
||
const newSet = new Set(expandedAssetTypes);
|
||
if (newSet.has(type)) newSet.delete(type);
|
||
else newSet.add(type);
|
||
setExpandedAssetTypes(newSet);
|
||
};
|
||
|
||
const toggleModel = (model: string) => {
|
||
const newSet = new Set(expandedModels);
|
||
if (newSet.has(model)) newSet.delete(model);
|
||
else newSet.add(model);
|
||
setExpandedModels(newSet);
|
||
};
|
||
|
||
const toggleDept = (dept: string) => {
|
||
const newSet = new Set(expandedDepts);
|
||
if (newSet.has(dept)) newSet.delete(dept);
|
||
else newSet.add(dept);
|
||
setExpandedDepts(newSet);
|
||
};
|
||
|
||
const toggleManagerDetails = (manager: string) => {
|
||
const newSet = new Set(expandedManagerDetails);
|
||
if (newSet.has(manager)) newSet.delete(manager);
|
||
else newSet.add(manager);
|
||
setExpandedManagerDetails(newSet);
|
||
};
|
||
|
||
const toggleRegion = (region: string) => {
|
||
const newSet = new Set(expandedRegions);
|
||
if (newSet.has(region)) newSet.delete(region);
|
||
else newSet.add(region);
|
||
setExpandedRegions(newSet);
|
||
};
|
||
|
||
const toggleRegionCity = (key: string) => {
|
||
const newSet = new Set(expandedRegionCities);
|
||
if (newSet.has(key)) newSet.delete(key);
|
||
else newSet.add(key);
|
||
setExpandedRegionCities(newSet);
|
||
};
|
||
|
||
const toggleCustomer = (customer: string) => {
|
||
const newSet = new Set(expandedCustomers);
|
||
if (newSet.has(customer)) newSet.delete(customer);
|
||
else newSet.add(customer);
|
||
setExpandedCustomers(newSet);
|
||
};
|
||
|
||
const toggleInventoryRegion = (region: string) => {
|
||
const newSet = new Set(expandedInventoryRegions);
|
||
if (newSet.has(region)) newSet.delete(region);
|
||
else newSet.add(region);
|
||
setExpandedInventoryRegions(newSet);
|
||
};
|
||
|
||
const toggleInventoryType = (type: string) => {
|
||
const newSet = new Set(expandedInventoryTypes);
|
||
if (newSet.has(type)) newSet.delete(type);
|
||
else newSet.add(type);
|
||
setExpandedInventoryTypes(newSet);
|
||
};
|
||
|
||
// Derived data for inventory section
|
||
const filteredInventoryStats = inventoryData.filter((s) => {
|
||
const mr = !inventoryFilters.region || s.region === inventoryFilters.region;
|
||
const mc = !inventoryFilters.city || s.city === inventoryFilters.city;
|
||
const mb = !inventoryFilters.brand || s.brand === inventoryFilters.brand;
|
||
const mbt = !inventoryFilters.batch || s.batch === inventoryFilters.batch;
|
||
const mm = !inventoryFilters.model || s.model.toLowerCase().includes(inventoryFilters.model.toLowerCase());
|
||
return mr && mc && mb && mbt && mm;
|
||
});
|
||
|
||
const uniqueInventoryBrands = Array.from(new Set(inventoryData.map((s) => s.brand).filter(Boolean)));
|
||
const uniqueInventoryRegions = Array.from(new Set(inventoryData.map((s) => s.region)));
|
||
const uniqueInventoryCities = Array.from(new Set(inventoryData.map((s) => s.city).filter(Boolean)));
|
||
const uniqueInventoryBatches = Array.from(new Set(inventoryData.map((s) => s.batch).filter(Boolean)));
|
||
|
||
const inventoryByRegion: Record<string, Record<string, RegionalInventoryStats[]>> = {};
|
||
for (const s of filteredInventoryStats) {
|
||
if (!inventoryByRegion[s.region]) inventoryByRegion[s.region] = {};
|
||
if (!inventoryByRegion[s.region][s.city]) inventoryByRegion[s.region][s.city] = [];
|
||
inventoryByRegion[s.region][s.city].push(s);
|
||
}
|
||
|
||
const INVENTORY_TYPE_ORDER = ['4.5T普货', '4.5T冷链', '18T', '49T', '挂车', '其他'];
|
||
const inventoryByModelRaw: Record<string, Record<string, RegionalInventoryStats[]>> = {};
|
||
for (const s of filteredInventoryStats) {
|
||
if (!inventoryByModelRaw[s.type]) inventoryByModelRaw[s.type] = {};
|
||
if (!inventoryByModelRaw[s.type][s.model]) inventoryByModelRaw[s.type][s.model] = [];
|
||
inventoryByModelRaw[s.type][s.model].push(s);
|
||
}
|
||
const inventoryByModel: Record<string, Record<string, RegionalInventoryStats[]>> = {};
|
||
for (const t of INVENTORY_TYPE_ORDER) {
|
||
if (inventoryByModelRaw[t]) inventoryByModel[t] = inventoryByModelRaw[t];
|
||
}
|
||
for (const t of Object.keys(inventoryByModelRaw)) {
|
||
if (!inventoryByModel[t]) inventoryByModel[t] = inventoryByModelRaw[t];
|
||
}
|
||
|
||
// Derived data for dept section
|
||
const allManagersList = deptData.flatMap((d) => d.managers.map((m) => m.manager)).filter((v, i, a) => a.indexOf(v) === i).sort();
|
||
const managerStats = deptData
|
||
.flatMap((d) => d.managers)
|
||
.filter((m) => selectedManager === 'All' || m.manager === selectedManager)
|
||
.sort((a, b) => b.total - a.total);
|
||
|
||
// Derived data for customer section
|
||
const filteredCustomerStats = customerData.filter((s) => {
|
||
const mc = !customerFilters.customer || s.customer.toLowerCase().includes(customerFilters.customer.toLowerCase());
|
||
const mb = !customerFilters.brand || s.brand === customerFilters.brand;
|
||
const md = !customerFilters.department || s.department === customerFilters.department;
|
||
const mm = !customerFilters.manager || s.manager.toLowerCase().includes(customerFilters.manager.toLowerCase());
|
||
const mr = !customerFilters.region || s.region === customerFilters.region;
|
||
return mc && mb && md && mm && mr;
|
||
});
|
||
const uniqueBrands = Array.from(new Set(customerData.map((s) => s.brand).filter(Boolean)));
|
||
const uniqueDepts = Array.from(new Set(customerData.map((s) => s.department).filter(Boolean)));
|
||
const uniqueRegions = Array.from(new Set(customerData.map((s) => s.region)));
|
||
const uniqueCities = Array.from(new Set(customerData.map((s) => s.city).filter(Boolean)));
|
||
const uniqueCustomerNames = Array.from(new Set(customerData.map((s) => s.customer).filter(Boolean)));
|
||
const uniqueCustomerManagers = Array.from(new Set(customerData.map((s) => s.manager).filter(Boolean)));
|
||
const uniqueInventoryModels = Array.from(new Set(inventoryData.map((s) => s.model).filter(Boolean)));
|
||
const uniqueModalPlates = Array.from(new Set(modalVehicles.map(v => v.plateNumber || v.vin).filter(Boolean)));
|
||
const uniqueModalModels = Array.from(new Set(modalVehicles.map(v => v.model).filter(Boolean)));
|
||
const uniqueModalBrands = Array.from(new Set(modalVehicles.map(v => v.brandLabel).filter((x): x is string => Boolean(x))));
|
||
const uniqueModalLocations = Array.from(new Set(modalVehicles.map(v => v.location).filter(Boolean)));
|
||
|
||
// Derived data for region section
|
||
const filteredRegionData = regionData.filter((r) => !regionFilters.region || r.region === regionFilters.region);
|
||
|
||
// Filtered modal vehicles based on modal filters
|
||
const filteredModalVehicles = modalVehicles.filter((v) => {
|
||
const mp = !modalFilters.plateNumber || v.plateNumber.toLowerCase().includes(modalFilters.plateNumber.toLowerCase()) || v.vin.toLowerCase().includes(modalFilters.plateNumber.toLowerCase());
|
||
const mm = !modalFilters.model || v.model.toLowerCase().includes(modalFilters.model.toLowerCase());
|
||
const mb = !modalFilters.brand || (v.brandLabel || '').toLowerCase().includes(modalFilters.brand.toLowerCase());
|
||
const ml = !modalFilters.location || v.location.toLowerCase().includes(modalFilters.location.toLowerCase());
|
||
return mp && mm && mb && ml;
|
||
});
|
||
|
||
const filteredModalWeeklyDetail = modalWeeklyDetail.filter((v) => {
|
||
const mp = !modalFilters.plateNumber || v.plate_number.toLowerCase().includes(modalFilters.plateNumber.toLowerCase());
|
||
return mp;
|
||
});
|
||
|
||
if (loading && !summary) {
|
||
return (
|
||
<div className="min-h-screen bg-[#F8F9FB] flex items-center justify-center">
|
||
<div className="flex flex-col items-center gap-3">
|
||
<Loader2 className="animate-spin text-blue-500" size={32} />
|
||
<span className="text-sm text-gray-500">正在加载数据...</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error && !summary) {
|
||
return (
|
||
<div className="min-h-screen bg-[#F8F9FB] flex items-center justify-center">
|
||
<div className="flex flex-col items-center gap-3 text-center">
|
||
<div className="text-red-500 text-lg font-bold">加载失败</div>
|
||
<div className="text-sm text-gray-500">{error}</div>
|
||
<button onClick={loadData} className="mt-2 px-4 py-2 bg-blue-500 text-white rounded text-sm hover:bg-blue-600">
|
||
重试
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const SUMMARY = summary!;
|
||
|
||
const watermarkText = `羚牛氢能-${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, '-')}`;
|
||
|
||
return (
|
||
<div className="min-h-screen bg-[#F8F9FB] text-gray-800 font-sans p-6 pb-20 md:pb-6 relative">
|
||
{/* Watermark */}
|
||
<div className="fixed inset-0 pointer-events-none z-[9999] overflow-hidden" style={{ opacity: 0.06 }}>
|
||
<div className="absolute inset-0" style={{
|
||
backgroundImage: `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns='http://www.w3.org/2000/svg' width='320' height='200'><text x='50%' y='50%' text-anchor='middle' dominant-baseline='middle' font-size='14' font-family='sans-serif' fill='%23000' transform='rotate(-25 160 100)'>${watermarkText}</text></svg>`)}")`,
|
||
backgroundRepeat: 'repeat',
|
||
}} />
|
||
</div>
|
||
{/* Compact Header Bar */}
|
||
<div className="sticky top-0 z-40 -mx-6 -mt-6 mb-4 bg-white/95 backdrop-blur-sm border-b border-gray-100/80">
|
||
{/* Title row */}
|
||
<div className="relative flex items-center justify-center px-4 pt-3 pb-1">
|
||
<h1 className="text-base font-semibold text-gray-800 tracking-wide">羚牛氢能车辆资产</h1>
|
||
{/* Right: status + theme */}
|
||
<div className="absolute right-4 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||
<div className="hidden sm:flex items-center gap-1 text-[10px] text-gray-400">
|
||
<span className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse inline-block" />
|
||
<span>{lastUpdate}</span>
|
||
</div>
|
||
{loading && (
|
||
<div className="flex items-center gap-1 text-[10px] text-gray-400">
|
||
<Loader2 className="animate-spin" size={10} />
|
||
</div>
|
||
)}
|
||
<div className="hidden sm:flex bg-gray-100 p-0.5 rounded-lg text-[10px]">
|
||
{(['soft','minimal','vibrant'] as const).map((t) => (
|
||
<button
|
||
key={t}
|
||
onClick={() => setTheme(t)}
|
||
className={`px-2 py-0.5 rounded-md transition-all ${theme === t ? 'bg-white text-blue-600 shadow-sm font-semibold' : 'text-gray-400 hover:text-gray-600'}`}
|
||
>
|
||
{t === 'soft' ? '柔和' : t === 'minimal' ? '简约' : '经典'}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/* Tab row */}
|
||
<div className="flex items-center justify-center gap-1 px-4 pb-0 overflow-x-auto no-scrollbar">
|
||
{TABS.map(tab => (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => setActiveTab(tab.id as typeof activeTab)}
|
||
className={`relative px-4 py-2 text-[13px] font-normal transition-all whitespace-nowrap ${
|
||
activeTab === tab.id
|
||
? 'text-blue-600 font-medium'
|
||
: 'text-gray-400 hover:text-gray-500'
|
||
}`}
|
||
>
|
||
{tab.label}
|
||
{activeTab === tab.id && (
|
||
<motion.div
|
||
layoutId="activeTab"
|
||
className="absolute bottom-0 left-2 right-2 h-[1.5px] bg-blue-600 rounded-full"
|
||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||
/>
|
||
)}
|
||
</button>
|
||
))}
|
||
</div>
|
||
{/* Status row */}
|
||
<div className="flex items-center justify-center gap-4 py-1.5 text-[10px] text-gray-400">
|
||
<div className="flex items-center gap-1">
|
||
<span className="w-1 h-1 rounded-full bg-blue-400 inline-block" />
|
||
最后更新: {lastUpdate}
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
<span className="w-1 h-1 rounded-full bg-green-400 animate-pulse inline-block" />
|
||
每分钟更新一次
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{activeTab === 'overview' && (
|
||
<>
|
||
{/* Header Summary - Ultra Compact */}
|
||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-2 mb-6">
|
||
{/* Total Assets */}
|
||
<div className="bg-white p-2 rounded-sm border border-gray-100 shadow-sm flex items-center gap-2">
|
||
<div className="w-7 h-7 rounded bg-gray-50 flex items-center justify-center text-gray-400">
|
||
<Truck size={14} />
|
||
</div>
|
||
<div>
|
||
<div className="text-[9px] text-gray-400 font-medium uppercase leading-none mb-0.5">资产总数</div>
|
||
<div className="text-base font-bold text-gray-800 leading-none">{SUMMARY.totalAssets.toLocaleString()}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Operating */}
|
||
<div className="bg-white p-2 rounded-sm border border-gray-100 shadow-sm flex items-center gap-2">
|
||
<div className="w-7 h-7 rounded bg-blue-50 flex items-center justify-center text-blue-500">
|
||
<Activity size={14} />
|
||
</div>
|
||
<div>
|
||
<div className="text-[9px] text-gray-400 font-medium uppercase leading-none mb-0.5">总运营</div>
|
||
<div className="flex items-baseline gap-1">
|
||
<span className="text-base font-bold text-blue-600 leading-none">{SUMMARY.operating.total}</span>
|
||
<span className="text-[8px] text-gray-400 leading-none">自{SUMMARY.operating.self} 租{SUMMARY.operating.leased}{SUMMARY.operating.hanging > 0 ? ` 挂${SUMMARY.operating.hanging}` : ''}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Inventory */}
|
||
<div className="bg-white p-2 rounded-sm border border-gray-100 shadow-sm flex items-center gap-2">
|
||
<div className="w-7 h-7 rounded bg-gray-50 flex items-center justify-center text-gray-500">
|
||
<Warehouse size={14} />
|
||
</div>
|
||
<div>
|
||
<div className="text-[9px] text-gray-400 font-medium uppercase leading-none mb-0.5">总库存</div>
|
||
<div className="flex items-baseline gap-1">
|
||
<span className="text-base font-bold text-gray-800 leading-none">{SUMMARY.inventory.total}</span>
|
||
<span className="text-[8px] text-gray-400 leading-none">库{SUMMARY.inventory.inStock} 异{SUMMARY.inventory.abnormal}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Pending */}
|
||
<div className="bg-white p-2 rounded-sm border border-gray-100 shadow-sm flex items-center gap-2 cursor-pointer hover:bg-blue-50 transition-colors"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Pending' })}>
|
||
<div className="w-7 h-7 rounded bg-blue-50 flex items-center justify-center text-blue-500">
|
||
<PlusCircle size={14} />
|
||
</div>
|
||
<div>
|
||
<div className="text-[9px] text-blue-500 font-bold uppercase leading-none mb-0.5">待交车</div>
|
||
<div className="text-base font-bold text-blue-600 leading-none">{SUMMARY.pendingDelivery}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Dynamics */}
|
||
<div className="bg-white p-2 rounded-sm border border-gray-100 shadow-sm col-span-2">
|
||
<div className="flex items-center justify-between mb-1.5">
|
||
<div className="text-[9px] text-gray-400 font-bold uppercase tracking-tight">本周动态</div>
|
||
<div className="text-[7px] text-gray-300 font-normal italic">上周六-本周五</div>
|
||
</div>
|
||
<div className="flex justify-between items-center gap-1">
|
||
<div className="flex-1 flex flex-col items-center cursor-pointer hover:bg-green-50 py-1 rounded transition-all group">
|
||
<span className="text-xs font-bold text-gray-800 group-hover:text-green-600">{SUMMARY.weeklyNew}</span>
|
||
<span className="text-[8px] text-green-500/80 font-bold mt-0.5">新增</span>
|
||
</div>
|
||
<div className="w-[1px] h-3 bg-gray-100"></div>
|
||
<div className="flex-1 flex flex-col items-center cursor-pointer hover:bg-blue-50 py-1 rounded transition-all group"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Delivered' })}>
|
||
<span className="text-xs font-bold text-gray-800 group-hover:text-blue-600">{SUMMARY.weeklyDelivered}</span>
|
||
<span className="text-[8px] text-blue-500/80 font-bold mt-0.5">交车</span>
|
||
</div>
|
||
<div className="w-[1px] h-3 bg-gray-100"></div>
|
||
<div className="flex-1 flex flex-col items-center cursor-pointer hover:bg-orange-50 py-1 rounded transition-all group"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Returned' })}>
|
||
<span className="text-xs font-bold text-gray-800 group-hover:text-orange-600">{SUMMARY.weeklyReturned}</span>
|
||
<span className="text-[8px] text-orange-500/80 font-bold mt-0.5">还车</span>
|
||
</div>
|
||
<div className="w-[1px] h-3 bg-gray-100"></div>
|
||
<div className="flex-1 flex flex-col items-center cursor-pointer hover:bg-purple-50 py-1 rounded transition-all group"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Replaced' })}>
|
||
<span className="text-xs font-bold text-gray-800 group-hover:text-purple-600">{SUMMARY.weeklyReplaced}</span>
|
||
<span className="text-[8px] text-purple-500/80 font-bold mt-0.5">替换</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* Main Content Area */}
|
||
<div className="flex flex-col gap-6">
|
||
{activeTab === 'overview' && (
|
||
<>
|
||
{/* Asset Summary Table with Dimension Switch */}
|
||
<div className="bg-white rounded-sm border border-gray-100 shadow-sm overflow-hidden mb-6">
|
||
<div className="p-4 border-b border-gray-50 bg-gray-50/50 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3">
|
||
<div className="flex flex-wrap items-center gap-4 sm:gap-6">
|
||
<h2 className="text-sm font-bold text-gray-700">资产数据实时汇总</h2>
|
||
<div className="hidden md:flex items-center gap-1 text-[10px] text-blue-500 bg-blue-50 px-2 py-0.5 rounded">
|
||
<Info size={10} />
|
||
点击车型展开品牌型号明细
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Desktop View Table */}
|
||
<div className="hidden lg:block overflow-x-auto">
|
||
<table className="w-full text-left border-collapse table-fixed min-w-[1200px]">
|
||
<thead>
|
||
<tr className="bg-gray-50 text-[11px] text-gray-500 uppercase tracking-wider border-b border-gray-100">
|
||
<th className="p-3 font-semibold border-r border-gray-100 w-24">
|
||
<button onClick={toggleAllAssetTypes} className="flex items-center gap-1 hover:text-blue-600 transition-colors">
|
||
{allTypesExpanded ? <MinusCircle size={12} /> : <PlusCircle size={12} />}
|
||
<span>车型</span>
|
||
</button>
|
||
</th>
|
||
<th className="p-3 font-semibold border-r border-gray-100 w-48">品牌型号</th>
|
||
<th className="p-3 font-semibold border-r border-gray-100 text-center bg-blue-50/30 w-24">车辆总资产</th>
|
||
<th className="p-3 font-semibold border-r border-gray-100 text-center w-24">库存总数</th>
|
||
<th className="p-3 font-semibold border-r border-gray-100 text-center w-24">库存-江浙沪</th>
|
||
<th className="p-3 font-semibold border-r border-gray-100 text-center w-24">库存-广东</th>
|
||
<th className="p-3 font-semibold border-r border-gray-100 text-center w-24">库存-北京</th>
|
||
<th className="p-3 font-semibold border-r border-gray-100 text-center w-24">库存-新疆</th>
|
||
<th className="p-3 font-semibold border-r border-gray-100 text-center w-24">库存-其他</th>
|
||
<th className="p-3 font-semibold border-r border-gray-100 text-center w-24">待交车</th>
|
||
<th className="p-3 font-semibold border-r border-gray-100 text-center bg-green-50/30 w-24">当前在运营</th>
|
||
<th className="p-3 font-semibold border-r border-gray-100 text-center bg-blue-50/20 w-24">本周交车</th>
|
||
<th className="p-3 font-semibold border-r border-gray-100 text-center bg-orange-50/20 w-24">本周还车</th>
|
||
<th className="p-3 font-semibold text-center bg-purple-50/20 w-24">本周替换</th>
|
||
</tr>
|
||
<tr className="bg-yellow-50/50 text-xs font-bold border-b border-gray-200">
|
||
<td className="p-3 border-r border-gray-100 text-gray-700">总计</td>
|
||
<td className="p-3 border-r border-gray-100"></td>
|
||
<td className="p-3 text-center border-r border-gray-100">
|
||
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All' })} className="text-gray-800 hover:text-blue-600 hover:underline font-bold">
|
||
{processedData.reduce((s, t) => s + t.totalAssets, 0)}
|
||
</button>
|
||
</td>
|
||
<td className="p-3 text-center border-r border-gray-100">
|
||
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Inventory' })} className="text-blue-700 hover:underline font-bold">
|
||
{processedData.reduce((s, t) => s + t.totalInventory, 0)}
|
||
</button>
|
||
</td>
|
||
{['嘉兴', '广东', '北京', '新疆', '其他'].map((reg) => {
|
||
const val = processedData.reduce((s, t) => s + (t.inventoryRegions?.[reg] || 0), 0);
|
||
return (
|
||
<td key={reg} className="p-3 text-center border-r border-gray-100">
|
||
{val > 0 ? (
|
||
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: reg, category: 'Inventory' })} className="text-blue-700 hover:underline font-bold">
|
||
{val}
|
||
</button>
|
||
) : ''}
|
||
</td>
|
||
);
|
||
})}
|
||
<td className="p-3 text-center border-r border-gray-100">
|
||
{processedData.reduce((s, t) => s + t.pending, 0) > 0 ? (
|
||
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Pending' })} className="text-gray-700 hover:text-blue-600 hover:underline font-bold">
|
||
{processedData.reduce((s, t) => s + t.pending, 0)}
|
||
</button>
|
||
) : ''}
|
||
</td>
|
||
<td className="p-3 text-center border-r border-gray-100 bg-green-50/10">
|
||
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Operating' })} className="text-green-700 hover:underline font-bold">
|
||
{processedData.reduce((s, t) => s + t.totalOperating, 0)}
|
||
</button>
|
||
</td>
|
||
<td className="p-3 text-center border-r border-gray-100 bg-blue-50/5">
|
||
{processedData.reduce((s, t) => s + t.weeklyDelivered, 0) > 0 ? (
|
||
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Delivered' })} className="text-blue-700 hover:underline font-bold">
|
||
{processedData.reduce((s, t) => s + t.weeklyDelivered, 0)}
|
||
</button>
|
||
) : ''}
|
||
</td>
|
||
<td className="p-3 text-center border-r border-gray-100 bg-orange-50/5">
|
||
{processedData.reduce((s, t) => s + t.weeklyReturned, 0) > 0 ? (
|
||
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Returned' })} className="text-orange-700 hover:underline font-bold">
|
||
{processedData.reduce((s, t) => s + t.weeklyReturned, 0)}
|
||
</button>
|
||
) : ''}
|
||
</td>
|
||
<td className="p-3 text-center bg-purple-50/5">
|
||
{processedData.reduce((s, t) => s + t.weeklyReplaced, 0) > 0 ? (
|
||
<button onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Replaced' })} className="text-purple-700 hover:underline font-bold">
|
||
{processedData.reduce((s, t) => s + t.weeklyReplaced, 0)}
|
||
</button>
|
||
) : ''}
|
||
</td>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="text-xs">
|
||
{processedData.map((typeGroup) => (
|
||
<React.Fragment key={typeGroup.type}>
|
||
{/* Category Header Row */}
|
||
<tr
|
||
className={`border-b border-gray-100 cursor-pointer transition-all ${
|
||
theme === 'vibrant'
|
||
? 'bg-blue-600 text-white hover:bg-blue-700'
|
||
: theme === 'minimal'
|
||
? 'bg-white border-l-4 border-blue-500 hover:bg-gray-50'
|
||
: 'bg-blue-50/50 hover:bg-blue-50 transition-colors'
|
||
}`}
|
||
onClick={() => toggleAssetType(typeGroup.type)}
|
||
>
|
||
<>
|
||
<td className={`p-3 font-bold border-r border-gray-100 ${theme === 'vibrant' ? 'text-white' : 'text-blue-700'}`}>
|
||
<div className="flex items-center gap-2">
|
||
{expandedAssetTypes.has(typeGroup.type) ? (
|
||
<ChevronDown size={16} className={theme === 'vibrant' ? 'text-white' : 'text-blue-500'} />
|
||
) : (
|
||
<ChevronRight size={16} className={theme === 'vibrant' ? 'text-white/70' : 'text-gray-400'} />
|
||
)}
|
||
<span>{typeGroup.type}</span>
|
||
</div>
|
||
</td>
|
||
<td className={`p-3 border-r border-gray-100 text-[11px] ${theme === 'vibrant' ? 'text-white/70' : 'text-gray-400'} italic`}>小计</td>
|
||
<td className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white' : ''}`}>
|
||
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white' : 'text-gray-700 hover:text-blue-600'}`}>
|
||
{typeGroup.totalAssets}
|
||
</button>
|
||
</td>
|
||
<td className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white' : ''}`}>
|
||
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Inventory', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white' : 'text-blue-600'}`}>
|
||
{typeGroup.totalInventory}
|
||
</button>
|
||
</td>
|
||
{['嘉兴', '广东', '北京', '新疆', '其他'].map((reg) => (
|
||
<td key={reg} className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white/80' : ''}`}>
|
||
{(typeGroup.inventoryRegions?.[reg] || 0) > 0 ? (
|
||
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: reg, category: 'Inventory', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white/80' : 'text-blue-600'}`}>
|
||
{typeGroup.inventoryRegions[reg]}
|
||
</button>
|
||
) : ''}
|
||
</td>
|
||
))}
|
||
<td className={`p-3 text-center border-r border-gray-100 font-bold ${theme === 'vibrant' ? 'text-white' : ''}`}>
|
||
{typeGroup.pending > 0 ? (
|
||
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Pending', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white' : 'text-gray-600 hover:text-blue-600'}`}>
|
||
{typeGroup.pending}
|
||
</button>
|
||
) : ''}
|
||
</td>
|
||
<td className={`p-3 text-center border-r border-gray-100 font-bold bg-green-50/10 ${theme === 'vibrant' ? 'text-white' : ''}`}>
|
||
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Operating', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white' : 'text-green-600'}`}>
|
||
{typeGroup.totalOperating}
|
||
</button>
|
||
</td>
|
||
<td className={`p-3 text-center border-r border-gray-100 font-bold bg-blue-50/5 ${theme === 'vibrant' ? 'text-white/80' : ''}`}>
|
||
{typeGroup.weeklyDelivered > 0 ? (
|
||
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Delivered', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white/80' : 'text-blue-600'}`}>
|
||
{typeGroup.weeklyDelivered}
|
||
</button>
|
||
) : ''}
|
||
</td>
|
||
<td className={`p-3 text-center border-r border-gray-100 font-bold bg-orange-50/5 ${theme === 'vibrant' ? 'text-white/80' : ''}`}>
|
||
{typeGroup.weeklyReturned > 0 ? (
|
||
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Returned', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white/80' : 'text-orange-600'}`}>
|
||
{typeGroup.weeklyReturned}
|
||
</button>
|
||
) : ''}
|
||
</td>
|
||
<td className={`p-3 text-center font-bold bg-purple-50/5 ${theme === 'vibrant' ? 'text-white/80' : ''}`}>
|
||
{typeGroup.weeklyReplaced > 0 ? (
|
||
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Replaced', vehicleType: typeGroup.type }); }} className={`hover:underline font-bold ${theme === 'vibrant' ? 'text-white/80' : 'text-purple-600'}`}>
|
||
{typeGroup.weeklyReplaced}
|
||
</button>
|
||
) : ''}
|
||
</td>
|
||
</>
|
||
</tr>
|
||
|
||
<AnimatePresence>
|
||
{expandedAssetTypes.has(typeGroup.type) &&
|
||
typeGroup.models.map((model) => (
|
||
<React.Fragment key={model.model}>
|
||
<motion.tr
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
className={`border-b border-gray-50 hover:bg-gray-50/50 transition-colors cursor-pointer ${expandedModels.has(model.model) ? 'bg-blue-50/10' : ''}`}
|
||
onClick={() => toggleModel(model.model)}
|
||
>
|
||
<td className="p-3 border-r border-gray-100 text-gray-300 text-center italic">{typeGroup.type}</td>
|
||
<td className="p-3 border-r border-gray-100 flex items-center gap-2">
|
||
{expandedModels.has(model.model) ? (
|
||
<ChevronDown size={14} className="text-blue-500" />
|
||
) : (
|
||
<ChevronRight size={14} className="text-gray-300" />
|
||
)}
|
||
<span className={expandedModels.has(model.model) ? 'font-bold text-blue-700' : ''}>{model.model}</span>
|
||
</td>
|
||
<td className="p-3 text-center border-r border-gray-100 font-medium">
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All' }); }}
|
||
className="text-blue-500 hover:underline font-medium"
|
||
>{model.total}</button>
|
||
</td>
|
||
<td className="p-3 text-center border-r border-gray-100">
|
||
{model.inventory > 0 ? (
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Inventory' }); }}
|
||
className="text-blue-500 hover:underline font-medium"
|
||
>{model.inventory}</button>
|
||
) : model.inventory}
|
||
</td>
|
||
{['嘉兴', '广东', '北京', '新疆', '其他'].map((reg) => (
|
||
<td key={reg} className="p-3 text-center border-r border-gray-100">
|
||
{(model.inventoryRegions[reg] || 0) > 0 ? (
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: model.model, location: reg, category: 'Inventory' });
|
||
}}
|
||
className="text-blue-500 hover:underline font-medium"
|
||
>
|
||
{model.inventoryRegions[reg]}
|
||
</button>
|
||
) : (
|
||
''
|
||
)}
|
||
</td>
|
||
))}
|
||
<td className="p-3 text-center border-r border-gray-100">
|
||
{model.pending > 0 ? (
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Pending' });
|
||
}}
|
||
className="text-blue-500 hover:underline font-medium"
|
||
>
|
||
{model.pending}
|
||
</button>
|
||
) : (
|
||
''
|
||
)}
|
||
</td>
|
||
<td className="p-3 text-center border-r border-gray-100 font-bold bg-green-50/10">
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Operating' }); }}
|
||
className="text-green-600 hover:underline font-bold"
|
||
>{model.operating}</button>
|
||
</td>
|
||
<td className="p-3 text-center border-r border-gray-100 text-blue-600 bg-blue-50/5">
|
||
{model.weeklyDelivered > 0 ? (
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Delivered' });
|
||
}}
|
||
className="text-blue-500 hover:underline font-medium"
|
||
>
|
||
{model.weeklyDelivered}
|
||
</button>
|
||
) : (
|
||
''
|
||
)}
|
||
</td>
|
||
<td className="p-3 text-center border-r border-gray-100 text-orange-600 bg-orange-50/5">
|
||
{model.weeklyReturned > 0 ? (
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Returned' });
|
||
}}
|
||
className="text-blue-500 hover:underline font-medium"
|
||
>
|
||
{model.weeklyReturned}
|
||
</button>
|
||
) : (
|
||
''
|
||
)}
|
||
</td>
|
||
<td className="p-3 text-center text-purple-600 bg-purple-50/5 font-medium">
|
||
{model.weeklyReplaced > 0 ? (
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Replaced' });
|
||
}}
|
||
className="text-blue-500 hover:underline font-medium"
|
||
>
|
||
{model.weeklyReplaced}
|
||
</button>
|
||
) : (
|
||
''
|
||
)}
|
||
</td>
|
||
</motion.tr>
|
||
{expandedModels.has(model.model) && model.batches.map((batch) => (
|
||
<motion.tr
|
||
key={batch.batch}
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
className="border-b border-gray-50 bg-purple-50/20"
|
||
>
|
||
<td className="p-3 border-r border-gray-100"></td>
|
||
<td className="p-3 border-r border-gray-100 pl-10 text-[10px] text-purple-600 italic truncate">{batch.batch}</td>
|
||
<td className="p-3 text-center border-r border-gray-100">
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: 'All' }); }}
|
||
className="text-purple-500 hover:underline font-medium"
|
||
>{batch.total}</button>
|
||
</td>
|
||
<td className="p-3 text-center border-r border-gray-100">
|
||
{batch.inventory > 0 ? (
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: 'All', category: 'Inventory' }); }}
|
||
className="text-purple-500 hover:underline font-medium"
|
||
>{batch.inventory}</button>
|
||
) : ''}
|
||
</td>
|
||
{['嘉兴', '广东', '北京', '新疆', '其他'].map((reg) => (
|
||
<td key={reg} className="p-3 text-center border-r border-gray-100">
|
||
{(batch.inventoryRegions[reg] || 0) > 0 ? (
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: reg, category: 'Inventory' }); }}
|
||
className="text-purple-500 hover:underline font-medium"
|
||
>{batch.inventoryRegions[reg]}</button>
|
||
) : ''}
|
||
</td>
|
||
))}
|
||
<td className="p-3 text-center border-r border-gray-100">
|
||
{batch.pending > 0 ? (
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: 'All', category: 'Pending' }); }}
|
||
className="text-purple-500 hover:underline font-medium"
|
||
>{batch.pending}</button>
|
||
) : ''}
|
||
</td>
|
||
<td className="p-3 text-center border-r border-gray-100 bg-green-50/5">
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: 'All', category: 'Operating' }); }}
|
||
className="text-purple-600 hover:underline font-medium"
|
||
>{batch.operating}</button>
|
||
</td>
|
||
<td className="p-3 text-center border-r border-gray-100 bg-blue-50/5">
|
||
{batch.weeklyDelivered > 0 ? (
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: 'All', category: 'Delivered' }); }}
|
||
className="text-purple-500 hover:underline font-medium"
|
||
>{batch.weeklyDelivered}</button>
|
||
) : ''}
|
||
</td>
|
||
<td className="p-3 text-center border-r border-gray-100 bg-orange-50/5">
|
||
{batch.weeklyReturned > 0 ? (
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: 'All', category: 'Returned' }); }}
|
||
className="text-purple-500 hover:underline font-medium"
|
||
>{batch.weeklyReturned}</button>
|
||
) : ''}
|
||
</td>
|
||
<td className="p-3 text-center bg-purple-50/5">
|
||
{batch.weeklyReplaced > 0 ? (
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: batch.batch, model: model.model, location: 'All', category: 'Replaced' }); }}
|
||
className="text-purple-500 hover:underline font-medium"
|
||
>{batch.weeklyReplaced}</button>
|
||
) : ''}
|
||
</td>
|
||
</motion.tr>
|
||
))}
|
||
</React.Fragment>
|
||
))}
|
||
</AnimatePresence>
|
||
</React.Fragment>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Mobile View Cards for Asset Summary */}
|
||
<div className="lg:hidden p-4 space-y-4">
|
||
{processedData.map((typeGroup) => (
|
||
<div key={typeGroup.type} className="space-y-3">
|
||
<div
|
||
className={`px-3 py-2 rounded flex justify-between items-center shadow-sm cursor-pointer transition-all ${
|
||
theme === 'vibrant'
|
||
? 'bg-blue-600 text-white active:bg-blue-700'
|
||
: theme === 'minimal'
|
||
? 'bg-white border-l-4 border-blue-500 text-gray-800 active:bg-gray-50'
|
||
: 'bg-blue-50 border border-blue-100 text-blue-700 active:bg-blue-100'
|
||
}`}
|
||
onClick={() => toggleAssetType(typeGroup.type)}
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
{expandedAssetTypes.has(typeGroup.type) ? (
|
||
<ChevronDown size={16} className={theme === 'vibrant' ? 'text-white' : 'text-blue-500'} />
|
||
) : (
|
||
<ChevronRight size={16} className={theme === 'vibrant' ? 'text-white/70' : 'text-blue-300'} />
|
||
)}
|
||
<span className="text-xs font-bold">{typeGroup.type}</span>
|
||
</div>
|
||
<div className={`flex gap-3 text-[9px] font-normal ${theme === 'vibrant' ? 'opacity-90' : 'text-gray-500'}`}>
|
||
<span>
|
||
资产 <span className={theme === 'vibrant' ? 'font-bold' : 'font-bold text-gray-700'}>{typeGroup.totalAssets}</span>
|
||
</span>
|
||
<span>
|
||
库存{' '}
|
||
<span className={theme === 'vibrant' ? 'font-bold' : 'font-bold text-blue-600'}>{typeGroup.totalInventory}</span>
|
||
</span>
|
||
<span>
|
||
运营{' '}
|
||
<span className={theme === 'vibrant' ? 'font-bold' : 'font-bold text-green-600'}>{typeGroup.totalOperating}</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<AnimatePresence>
|
||
{expandedAssetTypes.has(typeGroup.type) && (
|
||
<motion.div
|
||
initial={{ height: 0, opacity: 0 }}
|
||
animate={{ height: 'auto', opacity: 1 }}
|
||
exit={{ height: 0, opacity: 0 }}
|
||
className="space-y-3 overflow-hidden"
|
||
>
|
||
{typeGroup.models.map((model) => (
|
||
<div key={model.model} className="bg-white rounded-lg border border-gray-100 shadow-sm overflow-hidden">
|
||
<div
|
||
className="p-3 flex justify-between items-center cursor-pointer active:bg-gray-50"
|
||
onClick={() => toggleModel(model.model)}
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
{expandedModels.has(model.model) ? (
|
||
<ChevronDown size={14} className="text-blue-500" />
|
||
) : (
|
||
<ChevronRight size={14} className="text-gray-300" />
|
||
)}
|
||
<span className="text-xs font-bold text-gray-700">{model.model}</span>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<span className="text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-bold">资产 {model.total}</span>
|
||
<span className="text-[10px] bg-orange-50 text-orange-600 px-1.5 py-0.5 rounded font-bold">库存 {model.inventory}</span>
|
||
<span className="text-[10px] bg-green-50 text-green-600 px-1.5 py-0.5 rounded font-bold">运营 {model.operating}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{expandedModels.has(model.model) && (
|
||
<div className="px-3 pb-3 pt-1 border-t border-gray-50 bg-gray-50/30">
|
||
<div className="grid grid-cols-2 gap-y-3 gap-x-4 mt-2">
|
||
<div className="flex justify-between items-center cursor-pointer hover:bg-gray-100 p-1 rounded transition-colors"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Inventory', source: 'asset', title: `${model.model} - 库存` })}>
|
||
<span className="text-[10px] text-gray-400">总库存</span>
|
||
<span className="text-xs font-bold text-blue-600">{model.inventory}</span>
|
||
</div>
|
||
<div className="flex justify-between items-center cursor-pointer hover:bg-gray-100 p-1 rounded transition-colors"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Pending', source: 'asset', title: `${model.model} - 待交车` })}>
|
||
<span className="text-[10px] text-gray-400">待交车</span>
|
||
<span className="text-xs font-bold text-gray-600">{model.pending}</span>
|
||
</div>
|
||
<div className="col-span-2 grid grid-cols-5 gap-1 py-2 border-y border-gray-100">
|
||
{['嘉兴', '广东', '北京', '新疆', '其他'].map(reg => (
|
||
<div key={reg} className="text-center">
|
||
<div className="text-[8px] text-gray-400 mb-0.5">{reg === '嘉兴' ? '浙' : reg === '广东' ? '粤' : reg === '北京' ? '京' : reg === '新疆' ? '新' : '其'}</div>
|
||
{(model.inventoryRegions[reg] || 0) > 0 ? (
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: model.model, location: reg, source: 'asset', title: `${model.model} - ${reg}` });
|
||
}}
|
||
className="text-[10px] font-bold text-blue-500 hover:underline"
|
||
>
|
||
{model.inventoryRegions[reg]}
|
||
</button>
|
||
) : (
|
||
<div className="text-[10px] font-bold text-gray-300">-</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="col-span-2 grid grid-cols-3 gap-2 pt-1">
|
||
<div className="bg-blue-50/50 p-2 rounded flex flex-col items-center cursor-pointer hover:bg-blue-100/50 transition-colors"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Delivered', source: 'asset', title: `${model.model} - 本周交车` })}>
|
||
<span className="text-[8px] text-gray-400 mb-1">本周已交车</span>
|
||
<span className="text-xs font-bold text-blue-600">{model.weeklyDelivered}</span>
|
||
</div>
|
||
<div className="bg-orange-50/50 p-2 rounded flex flex-col items-center cursor-pointer hover:bg-orange-100/50 transition-colors"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Returned', source: 'asset', title: `${model.model} - 本周还车` })}>
|
||
<span className="text-[8px] text-gray-400 mb-1">已还车</span>
|
||
<span className="text-xs font-bold text-orange-600">{model.weeklyReturned}</span>
|
||
</div>
|
||
<div className="bg-purple-50/50 p-2 rounded flex flex-col items-center cursor-pointer hover:bg-purple-100/50 transition-colors"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Replaced', source: 'asset', title: `${model.model} - 本周替换` })}>
|
||
<span className="text-[8px] text-gray-400 mb-1">已替换</span>
|
||
<span className="text-xs font-bold text-purple-600">{model.weeklyReplaced}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Inventory Statistics */}
|
||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm mb-6 overflow-hidden">
|
||
<div className="p-3 sm:p-4 border-b border-gray-50 bg-white flex items-center justify-between relative">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-1.5 h-6 bg-blue-600 rounded-full"></div>
|
||
<div className="flex flex-col">
|
||
<h2 className="text-lg font-bold text-gray-800 leading-tight">库存统计</h2>
|
||
<span className="text-[10px] text-gray-400 font-medium">实时更新库存数据</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-6 ml-auto pr-2">
|
||
<div className="flex flex-col items-end cursor-pointer hover:opacity-80 transition-opacity"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Inventory', source: 'asset', title: '库存总数' })}>
|
||
<span className="text-[10px] text-gray-400 font-bold tracking-wider uppercase mb-0.5">总库存数据</span>
|
||
<span className="text-2xl font-black text-gray-900 leading-none">
|
||
{filteredInventoryStats.reduce((acc, s) => acc + s.quantity, 0)}
|
||
<span className="text-xs font-bold text-gray-400 ml-1">台</span>
|
||
</span>
|
||
</div>
|
||
|
||
<div className="relative">
|
||
<button
|
||
onClick={() => setIsInventoryFilterOpen(!isInventoryFilterOpen)}
|
||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
|
||
isInventoryFilterOpen || (inventoryFilters.region || inventoryFilters.city || inventoryFilters.brand || inventoryFilters.batch || inventoryFilters.model)
|
||
? 'bg-blue-600 text-white shadow-md'
|
||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||
}`}
|
||
>
|
||
<Filter size={14} />
|
||
<span>筛选</span>
|
||
{(inventoryFilters.region || inventoryFilters.city || inventoryFilters.brand || inventoryFilters.batch || inventoryFilters.model) && (
|
||
<span className="w-2 h-2 bg-white rounded-full animate-pulse"></span>
|
||
)}
|
||
</button>
|
||
|
||
<AnimatePresence>
|
||
{isInventoryFilterOpen && (
|
||
<>
|
||
<div className="fixed inset-0 z-40" onClick={() => setIsInventoryFilterOpen(false)} />
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||
className="absolute right-0 top-full mt-2 w-72 bg-white rounded-xl shadow-2xl border border-slate-100 z-50 p-4"
|
||
>
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h3 className="text-xs font-bold text-slate-800">库存统计 - 数据筛选</h3>
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => setInventoryFilters({ region: '', city: '', brand: '', batch: '', model: '' })}
|
||
className="text-[10px] text-blue-500 hover:underline"
|
||
>
|
||
重置所有
|
||
</button>
|
||
<button onClick={() => setIsInventoryFilterOpen(false)} className="text-slate-400 hover:text-slate-600">
|
||
<PlusCircle className="rotate-45" size={16} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-3 text-left">
|
||
<div>
|
||
<label className="text-[10px] text-slate-400 block mb-1">区域</label>
|
||
<input
|
||
list="dl-inv-region"
|
||
type="text"
|
||
placeholder="搜索或选择..."
|
||
value={inventoryFilters.region}
|
||
onChange={(e) => setInventoryFilters({...inventoryFilters, region: e.target.value})}
|
||
className="w-full text-xs bg-white border border-slate-200 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm"
|
||
/>
|
||
<datalist id="dl-inv-region">
|
||
{uniqueInventoryRegions.map(r => <option key={r} value={r} />)}
|
||
</datalist>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] text-slate-400 block mb-1">城市</label>
|
||
<input
|
||
list="dl-inv-city"
|
||
type="text"
|
||
placeholder="搜索或选择..."
|
||
value={inventoryFilters.city}
|
||
onChange={(e) => setInventoryFilters({...inventoryFilters, city: e.target.value})}
|
||
className="w-full text-xs bg-white border border-slate-200 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm"
|
||
/>
|
||
<datalist id="dl-inv-city">
|
||
{uniqueInventoryCities.map(c => <option key={c} value={c} />)}
|
||
</datalist>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] text-slate-400 block mb-1">品牌</label>
|
||
<input
|
||
list="dl-inv-brand"
|
||
type="text"
|
||
placeholder="搜索或选择..."
|
||
value={inventoryFilters.brand}
|
||
onChange={(e) => setInventoryFilters({...inventoryFilters, brand: e.target.value})}
|
||
className="w-full text-xs bg-white border border-slate-200 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm"
|
||
/>
|
||
<datalist id="dl-inv-brand">
|
||
{uniqueInventoryBrands.map(b => <option key={b} value={b} />)}
|
||
</datalist>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] text-slate-400 block mb-1">批次</label>
|
||
<input
|
||
list="dl-inv-batch"
|
||
type="text"
|
||
placeholder="搜索或选择..."
|
||
value={inventoryFilters.batch}
|
||
onChange={(e) => setInventoryFilters({...inventoryFilters, batch: e.target.value})}
|
||
className="w-full text-xs bg-white border border-slate-200 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm"
|
||
/>
|
||
<datalist id="dl-inv-batch">
|
||
{uniqueInventoryBatches.map(b => <option key={b} value={b} />)}
|
||
</datalist>
|
||
</div>
|
||
<div>
|
||
<label className="text-[10px] text-slate-400 block mb-1">车型名称</label>
|
||
<div className="relative">
|
||
<Search size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-slate-400" />
|
||
<input
|
||
list="dl-inv-model"
|
||
type="text"
|
||
placeholder="搜索车型..."
|
||
value={inventoryFilters.model}
|
||
onChange={(e) => setInventoryFilters({...inventoryFilters, model: e.target.value})}
|
||
className="w-full text-xs bg-white border border-slate-200 rounded-lg pl-7 pr-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm"
|
||
/>
|
||
<datalist id="dl-inv-model">
|
||
{uniqueInventoryModels.map(m => <option key={m} value={m} />)}
|
||
</datalist>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
</>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="px-4 py-3 bg-gray-50/50 border-b border-gray-100">
|
||
<div className="flex bg-gray-200/50 p-1 rounded-lg w-fit shadow-inner">
|
||
<button
|
||
onClick={() => setInventoryTab('region')}
|
||
className={`px-6 py-1.5 rounded-md text-xs font-bold transition-all ${
|
||
inventoryTab === 'region' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'
|
||
}`}
|
||
>
|
||
按区域
|
||
</button>
|
||
<button
|
||
onClick={() => setInventoryTab('model')}
|
||
className={`px-6 py-1.5 rounded-md text-xs font-bold transition-all ${
|
||
inventoryTab === 'model' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'
|
||
}`}
|
||
>
|
||
按车型
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Active Filters Bar */}
|
||
{(inventoryFilters.region || inventoryFilters.city || inventoryFilters.brand || inventoryFilters.batch || inventoryFilters.model) && (
|
||
<div className="px-4 py-2 bg-white border-b border-slate-50 flex flex-wrap gap-2 items-center">
|
||
<span className="text-[10px] text-slate-400 mr-1">当前筛选:</span>
|
||
{inventoryFilters.region && (
|
||
<span className="px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-[10px] flex items-center gap-1">
|
||
区域: {inventoryFilters.region}
|
||
<button onClick={() => setInventoryFilters({...inventoryFilters, region: ''})} className="hover:text-blue-800">×</button>
|
||
</span>
|
||
)}
|
||
{inventoryFilters.city && (
|
||
<span className="px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-[10px] flex items-center gap-1">
|
||
城市: {inventoryFilters.city}
|
||
<button onClick={() => setInventoryFilters({...inventoryFilters, city: ''})} className="hover:text-blue-800">×</button>
|
||
</span>
|
||
)}
|
||
{inventoryFilters.brand && (
|
||
<span className="px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-[10px] flex items-center gap-1">
|
||
品牌: {inventoryFilters.brand}
|
||
<button onClick={() => setInventoryFilters({...inventoryFilters, brand: ''})} className="hover:text-blue-800">×</button>
|
||
</span>
|
||
)}
|
||
{inventoryFilters.batch && (
|
||
<span className="px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-[10px] flex items-center gap-1">
|
||
批次: {inventoryFilters.batch}
|
||
<button onClick={() => setInventoryFilters({...inventoryFilters, batch: ''})} className="hover:text-blue-800">×</button>
|
||
</span>
|
||
)}
|
||
{inventoryFilters.model && (
|
||
<span className="px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-[10px] flex items-center gap-1">
|
||
车型: {inventoryFilters.model}
|
||
<button onClick={() => setInventoryFilters({...inventoryFilters, model: ''})} className="hover:text-blue-800">×</button>
|
||
</span>
|
||
)}
|
||
<button
|
||
onClick={() => setInventoryFilters({ region: '', city: '', brand: '', batch: '', model: '' })}
|
||
className="text-[10px] text-slate-400 hover:text-red-500 ml-auto"
|
||
>
|
||
清除全部
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Desktop View Table */}
|
||
<div className="hidden lg:block overflow-x-auto">
|
||
<table className="w-full text-left border-collapse">
|
||
<thead>
|
||
<tr className="bg-slate-50 text-[11px] text-slate-500 uppercase tracking-wider border-b border-slate-100">
|
||
<th className="p-3 font-semibold w-64">{inventoryTab === 'region' ? '区域 / 城市' : '车型分类 / 型号'}</th>
|
||
<th className="p-3 font-semibold">{inventoryTab === 'region' ? '品牌' : '品牌'}</th>
|
||
<th className="p-3 font-semibold">{inventoryTab === 'region' ? '车型' : '所在区域/城市'}</th>
|
||
<th className="p-3 font-semibold text-center w-32">库存数量</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="text-xs">
|
||
{inventoryTab === 'region' ? (
|
||
Object.entries(inventoryByRegion).map(([region, cities]) => (
|
||
<React.Fragment key={region}>
|
||
<tr
|
||
className="bg-slate-50/30 border-b border-slate-100 cursor-pointer hover:bg-slate-50 transition-colors"
|
||
onClick={() => toggleInventoryRegion(region)}
|
||
>
|
||
<td colSpan={4} className="p-3 font-bold text-slate-700">
|
||
<div className="flex items-center gap-2">
|
||
{expandedInventoryRegions.has(region) ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
|
||
<span>{region}</span>
|
||
<span className="text-[10px] font-normal text-slate-400 ml-2 cursor-pointer hover:text-blue-500 transition-colors"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: 'All', location: region, category: 'Inventory', source: 'asset', title: `库存统计 - ${region}` });
|
||
}}>
|
||
(共 {Object.values(cities).flat().reduce((acc, s) => acc + s.quantity, 0)} 台)
|
||
</span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<AnimatePresence>
|
||
{expandedInventoryRegions.has(region) && Object.entries(cities).map(([city, stats]) => (
|
||
<React.Fragment key={city}>
|
||
{stats.map((stat, idx) => (
|
||
<motion.tr
|
||
key={`${city}-${stat.model}-${idx}`}
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
className="border-b border-slate-50 hover:bg-slate-50/20 transition-colors"
|
||
>
|
||
<td className="p-3 pl-8 text-slate-500 border-r border-slate-50">
|
||
{idx === 0 ? <span className="font-medium text-slate-600">{city}</span> : ''}
|
||
</td>
|
||
<td className="p-3 text-slate-600 border-r border-slate-50">{stat.brand}</td>
|
||
<td className="p-3 text-slate-600 border-r border-slate-50">{stat.model}</td>
|
||
<td className="p-3 text-center font-bold text-blue-600">
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: stat.model, location: stat.city, category: 'Inventory', source: 'asset', title: `库存统计 - ${stat.model} - ${stat.city}` });
|
||
}}
|
||
className="text-blue-600 hover:underline font-bold"
|
||
>
|
||
{stat.quantity}
|
||
</button>
|
||
</td>
|
||
</motion.tr>
|
||
))}
|
||
</React.Fragment>
|
||
))}
|
||
</AnimatePresence>
|
||
</React.Fragment>
|
||
))
|
||
) : (
|
||
Object.entries(inventoryByModel).map(([type, models]) => (
|
||
<React.Fragment key={type}>
|
||
<tr
|
||
className="bg-slate-50/30 border-b border-slate-100 cursor-pointer hover:bg-slate-50 transition-colors"
|
||
onClick={() => toggleInventoryType(type)}
|
||
>
|
||
<td colSpan={4} className="p-3 font-bold text-slate-700">
|
||
<div className="flex items-center gap-2">
|
||
{expandedInventoryTypes.has(type) ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
|
||
<span>{type}</span>
|
||
<span className="text-[10px] font-normal text-slate-400 ml-2 cursor-pointer hover:text-blue-500 transition-colors"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', type: type, category: 'Inventory', source: 'asset', title: `库存统计 - ${type}` });
|
||
}}>
|
||
(共 {Object.values(models).flat().reduce((acc, s) => acc + s.quantity, 0)} 台)
|
||
</span>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<AnimatePresence>
|
||
{expandedInventoryTypes.has(type) && Object.entries(models).map(([model, stats]) => (
|
||
<React.Fragment key={model}>
|
||
{stats.map((stat, idx) => (
|
||
<motion.tr
|
||
key={`${model}-${stat.region}-${stat.city}-${idx}`}
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
className="border-b border-slate-50 hover:bg-slate-50/20 transition-colors"
|
||
>
|
||
<td className="p-3 pl-8 text-slate-500 border-r border-slate-50">
|
||
{idx === 0 ? <span className="font-medium text-slate-600">{model}</span> : ''}
|
||
</td>
|
||
<td className="p-3 text-slate-600 border-r border-slate-50">{stat.brand}</td>
|
||
<td className="p-3 text-slate-600 border-r border-slate-50">{stat.region} / {stat.city}</td>
|
||
<td className="p-3 text-center font-bold text-blue-600">
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: stat.model, location: stat.city, category: 'Inventory', source: 'asset', title: `库存统计 - ${stat.model} - ${stat.city}` });
|
||
}}
|
||
className="text-blue-600 hover:underline font-bold"
|
||
>
|
||
{stat.quantity}
|
||
</button>
|
||
</td>
|
||
</motion.tr>
|
||
))}
|
||
</React.Fragment>
|
||
))}
|
||
</AnimatePresence>
|
||
</React.Fragment>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Mobile View */}
|
||
<div className="lg:hidden p-3 space-y-3">
|
||
{inventoryTab === 'region' ? (
|
||
Object.entries(inventoryByRegion).map(([region, cities]) => (
|
||
<div key={region} className="border border-slate-100 rounded overflow-hidden">
|
||
<div
|
||
className="bg-slate-50 p-3 flex justify-between items-center cursor-pointer"
|
||
onClick={() => toggleInventoryRegion(region)}
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
{expandedInventoryRegions.has(region) ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
|
||
<span className="text-xs font-bold text-slate-700">{region}</span>
|
||
</div>
|
||
<span className="text-[10px] font-bold text-blue-600">
|
||
{Object.values(cities).flat().reduce((acc, s) => acc + s.quantity, 0)} 台
|
||
</span>
|
||
</div>
|
||
|
||
{expandedInventoryRegions.has(region) && (
|
||
<div className="p-2 space-y-2 bg-white">
|
||
{Object.entries(cities).map(([city, stats]) => (
|
||
<div key={city} className="border-l-2 border-slate-200 pl-3 py-1">
|
||
<div className="text-[10px] font-bold text-slate-500 mb-2">{city}</div>
|
||
<div className="space-y-2">
|
||
{stats.map((stat, idx) => (
|
||
<div key={idx} className="flex justify-between items-center text-[11px] bg-slate-50/50 p-2 rounded">
|
||
<div className="flex flex-col">
|
||
<span className="text-slate-400 text-[9px]">{stat.brand}</span>
|
||
<span className="text-slate-700 font-medium">{stat.model}</span>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: stat.model, location: stat.city, category: 'Inventory', source: 'asset', title: `库存统计 - ${stat.model} - ${stat.city}` })}
|
||
className="font-bold text-blue-600 hover:underline"
|
||
>
|
||
{stat.quantity} 台
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
))
|
||
) : (
|
||
Object.entries(inventoryByModel).map(([type, models]) => (
|
||
<div key={type} className="border border-slate-100 rounded overflow-hidden">
|
||
<div
|
||
className="bg-slate-50 p-3 flex justify-between items-center cursor-pointer"
|
||
onClick={() => toggleInventoryType(type)}
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
{expandedInventoryTypes.has(type) ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
|
||
<span className="text-xs font-bold text-slate-700">{type}</span>
|
||
</div>
|
||
<span className="text-[10px] font-bold text-blue-600">
|
||
{Object.values(models).flat().reduce((acc, s) => acc + s.quantity, 0)} 台
|
||
</span>
|
||
</div>
|
||
|
||
{expandedInventoryTypes.has(type) && (
|
||
<div className="p-2 space-y-2 bg-white">
|
||
{Object.entries(models).map(([model, stats]) => (
|
||
<div key={model} className="border-l-2 border-slate-200 pl-3 py-1">
|
||
<div className="text-[10px] font-bold text-slate-500 mb-2">{model}</div>
|
||
<div className="space-y-2">
|
||
{stats.map((stat, idx) => (
|
||
<div key={idx} className="flex justify-between items-center text-[11px] bg-slate-50/50 p-2 rounded">
|
||
<div className="flex flex-col">
|
||
<span className="text-slate-400 text-[9px]">{stat.brand}</span>
|
||
<span className="text-slate-700 font-medium">{stat.region} / {stat.city}</span>
|
||
</div>
|
||
<button
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: stat.model, location: stat.city, category: 'Inventory', source: 'asset', title: `库存统计 - ${stat.model} - ${stat.city}` })}
|
||
className="font-bold text-blue-600 hover:underline"
|
||
>
|
||
{stat.quantity} 台
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{activeTab === 'department' && (
|
||
/* Department Operations Statistics */
|
||
<section className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden mb-6">
|
||
<div className="p-3 sm:p-4 border-b border-gray-50 flex items-center justify-between gap-4">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-1.5 h-6 bg-blue-600 rounded-full"></div>
|
||
<div>
|
||
<h2 className="text-lg font-bold text-gray-800">部门运营统计</h2>
|
||
<p className="text-[10px] text-gray-400 font-medium">实时更新部门运营核心指标</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-0 sm:p-2 bg-gray-50/30">
|
||
{/* Overall Total Summary (Compact) - Moved to Top */}
|
||
<div className="m-2 bg-slate-800 rounded-xl p-3 text-white shadow-lg">
|
||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||
<div className="flex flex-col">
|
||
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5">总资产</span>
|
||
<span className="text-xl font-black">{deptData.reduce((s, d) => s + d.totalAssets, 0)}</span>
|
||
</div>
|
||
<div className="flex flex-col">
|
||
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5 text-green-400">运营中</span>
|
||
<span className="text-xl font-black text-green-400">
|
||
{deptData.reduce((acc, d) => acc + d.operatingCount, 0)}
|
||
</span>
|
||
</div>
|
||
<div className="flex flex-col">
|
||
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5 text-slate-400">闲置中</span>
|
||
<span className="text-xl font-black text-slate-400">
|
||
{deptData.reduce((acc, d) => acc + d.idleCount, 0)}
|
||
</span>
|
||
</div>
|
||
<div className="flex flex-col">
|
||
<span className="text-[9px] opacity-50 uppercase font-bold tracking-widest mb-0.5 text-blue-400">平均出勤</span>
|
||
<span className="text-xl font-black text-blue-400">
|
||
{deptData.length > 0 ? (deptData.reduce((acc, d) => acc + d.attendanceRate, 0) / deptData.length).toFixed(1) : 0}%
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Controls Row: Toggles Left, Filter Right */}
|
||
<div className="px-2 mb-2 flex items-center justify-between gap-4">
|
||
<div className="flex bg-gray-200/50 p-1 rounded-lg shadow-inner">
|
||
<button
|
||
onClick={() => setDeptViewMode('department')}
|
||
className={`px-4 py-1.5 text-xs font-bold rounded-md transition-all ${deptViewMode === 'department' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
|
||
>
|
||
按部门
|
||
</button>
|
||
<button
|
||
onClick={() => setDeptViewMode('manager')}
|
||
className={`px-4 py-1.5 text-xs font-bold rounded-md transition-all ${deptViewMode === 'manager' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
|
||
>
|
||
按业务负责人
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex-1 max-w-[240px]">
|
||
{deptViewMode === 'manager' && (
|
||
<div className="relative">
|
||
<Filter className="absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400" size={14} />
|
||
<input
|
||
list="dl-dept-manager"
|
||
type="text"
|
||
placeholder="搜索或选择..."
|
||
value={selectedManager === 'All' ? '' : selectedManager}
|
||
onChange={(e) => setSelectedManager(e.target.value || 'All')}
|
||
className="w-full pl-9 pr-8 py-1.5 bg-white border border-gray-200 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm font-bold text-gray-700"
|
||
/>
|
||
<datalist id="dl-dept-manager">
|
||
<option value="All">所有业务负责人</option>
|
||
{allManagersList.map(m => (
|
||
<option key={m} value={m} />
|
||
))}
|
||
</datalist>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Desktop Table View */}
|
||
<div className="hidden lg:block overflow-x-auto">
|
||
<table className="w-full text-left border-collapse min-w-[900px]">
|
||
<thead>
|
||
<tr className="bg-gray-100/50 text-[11px] text-gray-500 uppercase tracking-wider border-b border-gray-200">
|
||
<th className="p-2 font-bold border-r border-gray-100 w-48">{deptViewMode === 'department' ? '部门名称' : '业务负责人'}</th>
|
||
{deptViewMode === 'manager' && <th className="p-2 font-bold border-r border-gray-100 w-32">所属部门</th>}
|
||
<th className="p-2 font-bold border-r border-gray-100 text-center w-24">{deptViewMode === 'department' ? '出勤率' : '合计资产'}</th>
|
||
{deptViewMode === 'department' && (
|
||
<>
|
||
<th className="p-2 font-bold border-r border-gray-100 text-center w-24">资产总数</th>
|
||
<th className="p-2 font-bold border-r border-gray-100 text-center w-28">日均里程</th>
|
||
<th className="p-2 font-bold border-r border-gray-100 text-center w-24 text-green-500">运营中</th>
|
||
<th className="p-2 font-bold border-r border-gray-100 text-center w-24 text-gray-400">闲置中</th>
|
||
</>
|
||
)}
|
||
{deptViewMode === 'manager' && (
|
||
<>
|
||
<th className="p-2 font-bold border-r border-gray-100 text-center w-20">4.5T</th>
|
||
<th className="p-2 font-bold border-r border-gray-100 text-center w-20">冷链</th>
|
||
<th className="p-2 font-bold border-r border-gray-100 text-center w-20">18T</th>
|
||
<th className="p-2 font-bold border-r border-gray-100 text-center w-20">49T</th>
|
||
<th className="p-2 font-bold border-r border-gray-100 text-center w-20">挂车</th>
|
||
<th className="p-2 font-bold border-r border-gray-100 text-center w-20">其他</th>
|
||
</>
|
||
)}
|
||
<th className="p-2 font-bold text-center w-16">详情</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="text-xs">
|
||
{deptViewMode === 'department' ? (
|
||
deptData.map((dept) => {
|
||
const isExpanded = expandedDepts.has(dept.department);
|
||
return (
|
||
<React.Fragment key={dept.department}>
|
||
<tr
|
||
className={`cursor-pointer transition-all border-b border-gray-100 ${
|
||
isExpanded ? 'bg-blue-50/50' : 'hover:bg-gray-50'
|
||
}`}
|
||
onClick={() => toggleDept(dept.department)}
|
||
>
|
||
<td className="p-2 border-r border-gray-100 font-bold text-gray-800">
|
||
{dept.department}
|
||
</td>
|
||
<td className="p-2 border-r border-gray-100 text-center">
|
||
<span className="bg-blue-50 text-blue-600 text-[10px] font-bold px-2 py-0.5 rounded-full">
|
||
{dept.attendanceRate}%
|
||
</span>
|
||
</td>
|
||
<td className="p-2 border-r border-gray-100 text-center font-black text-gray-800 text-sm">
|
||
{dept.totalAssets}
|
||
</td>
|
||
<td className="p-2 border-r border-gray-100 text-center">
|
||
<div className="flex items-baseline justify-center gap-1">
|
||
<span className="font-black text-gray-800 text-sm">{dept.avgMileage}</span>
|
||
<span className="text-[9px] text-gray-400 font-bold">km</span>
|
||
</div>
|
||
</td>
|
||
<td className="p-2 border-r border-gray-100 text-center font-black text-green-500 text-sm">
|
||
{dept.operatingCount}
|
||
</td>
|
||
<td className="p-2 border-r border-gray-100 text-center font-black text-gray-400 text-sm">
|
||
{dept.idleCount}
|
||
</td>
|
||
<td className="p-2 text-center">
|
||
{isExpanded ? <ChevronDown size={16} className="text-blue-500 inline" /> : <ChevronRight size={16} className="text-gray-300 inline" />}
|
||
</td>
|
||
</tr>
|
||
{isExpanded && (
|
||
<tr className="bg-gray-50/50">
|
||
<td colSpan={7} className="p-2">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
|
||
{dept.managers.map(m => {
|
||
const isManagerExpanded = expandedManagerDetails.has(m.manager);
|
||
return (
|
||
<div key={m.manager} className="bg-white rounded-lg border border-gray-100 shadow-sm overflow-hidden">
|
||
<div
|
||
className="p-2 flex justify-between items-center cursor-pointer hover:bg-gray-50 transition-colors"
|
||
onClick={() => toggleManagerDetails(m.manager)}
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
{isManagerExpanded ? <ChevronDown size={14} className="text-blue-500" /> : <ChevronRight size={14} className="text-gray-300" />}
|
||
<span className="font-bold text-gray-700 text-xs">{m.manager}</span>
|
||
</div>
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department' });
|
||
}}
|
||
className="text-[10px] font-bold text-blue-600 bg-blue-50 px-2 py-0.5 rounded hover:bg-blue-100 transition-colors"
|
||
>
|
||
合计: {m.total}
|
||
</button>
|
||
</div>
|
||
|
||
{isManagerExpanded && (
|
||
<div className="p-2 pt-0 border-t border-gray-50 bg-gray-50/30">
|
||
<div className="grid grid-cols-3 gap-1 mt-2">
|
||
<div
|
||
className="text-center bg-white p-1 rounded cursor-pointer hover:bg-blue-50 transition-colors border border-gray-100"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: false, category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">4.5T</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.t4_5}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-white p-1 rounded cursor-pointer hover:bg-blue-50 transition-colors border border-gray-100"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: true, category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">冷链</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.t4_5c}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-white p-1 rounded cursor-pointer hover:bg-blue-50 transition-colors border border-gray-100"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '18T', category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">18T</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.t18}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-white p-1 rounded cursor-pointer hover:bg-blue-50 transition-colors border border-gray-100"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '49T', category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">49T</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.t49}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-white p-1 rounded cursor-pointer hover:bg-blue-50 transition-colors border border-gray-100"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: true, category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">挂车</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.trailer}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-white p-1 rounded cursor-pointer hover:bg-blue-50 transition-colors border border-gray-100"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: false, category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">其他</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.other}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</React.Fragment>
|
||
);
|
||
})
|
||
) : (
|
||
managerStats.map((m) => {
|
||
const isManagerExpanded = expandedManagerDetails.has(m.manager);
|
||
return (
|
||
<React.Fragment key={m.manager}>
|
||
<tr
|
||
className="border-b border-gray-100 hover:bg-gray-50 transition-colors cursor-pointer"
|
||
onClick={() => toggleManagerDetails(m.manager)}
|
||
>
|
||
<td className="p-2 border-r border-gray-100 font-bold text-gray-800 flex items-center gap-1">
|
||
{isManagerExpanded ? <ChevronDown size={12} className="text-blue-400" /> : <ChevronRight size={12} className="text-gray-300" />}
|
||
{m.manager}
|
||
</td>
|
||
<td className="p-2 border-r border-gray-100 text-gray-600">{m.department}</td>
|
||
<td
|
||
className="p-2 border-r border-gray-100 text-center font-black text-blue-600 text-sm cursor-pointer hover:bg-blue-50"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department' });
|
||
}}
|
||
>
|
||
{m.total}
|
||
</td>
|
||
<td className="p-2 border-r border-gray-100 text-center text-gray-400">-</td>
|
||
<td className="p-2 border-r border-gray-100 text-center text-gray-400">-</td>
|
||
<td className="p-2 border-r border-gray-100 text-center text-gray-400">-</td>
|
||
<td className="p-2 border-r border-gray-100 text-center text-gray-400">-</td>
|
||
<td className="p-2 border-r border-gray-100 text-center text-gray-400">-</td>
|
||
<td className="p-2 border-r border-gray-100 text-center text-gray-400">-</td>
|
||
<td className="p-2 text-center">
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department' });
|
||
}}
|
||
className="text-blue-500 hover:text-blue-700 transition-colors"
|
||
>
|
||
<ArrowRightLeft size={14} className="inline" />
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
{isManagerExpanded && (
|
||
<tr className="bg-gray-50/50 border-b border-gray-100">
|
||
<td colSpan={10} className="p-0">
|
||
<div className="grid grid-cols-6 text-[10px] bg-white/50">
|
||
<div className="p-2 border-r border-gray-100 flex flex-col items-center cursor-pointer hover:bg-blue-50" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: false, category: 'Operating', source: 'department' })}>
|
||
<span className="text-gray-400 uppercase mb-1">4.5T</span>
|
||
<span className="font-bold text-gray-600">{m.t4_5}</span>
|
||
</div>
|
||
<div className="p-2 border-r border-gray-100 flex flex-col items-center cursor-pointer hover:bg-blue-50" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: true, category: 'Operating', source: 'department' })}>
|
||
<span className="text-gray-400 uppercase mb-1">冷链</span>
|
||
<span className="font-bold text-gray-600">{m.t4_5c}</span>
|
||
</div>
|
||
<div className="p-2 border-r border-gray-100 flex flex-col items-center cursor-pointer hover:bg-blue-50" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '18T', category: 'Operating', source: 'department' })}>
|
||
<span className="text-gray-400 uppercase mb-1">18T</span>
|
||
<span className="font-bold text-gray-600">{m.t18}</span>
|
||
</div>
|
||
<div className="p-2 border-r border-gray-100 flex flex-col items-center cursor-pointer hover:bg-blue-50" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '49T', category: 'Operating', source: 'department' })}>
|
||
<span className="text-gray-400 uppercase mb-1">49T</span>
|
||
<span className="font-bold text-gray-600">{m.t49}</span>
|
||
</div>
|
||
<div className="p-2 border-r border-gray-100 flex flex-col items-center cursor-pointer hover:bg-blue-50" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: true, category: 'Operating', source: 'department' })}>
|
||
<span className="text-gray-400 uppercase mb-1">挂车</span>
|
||
<span className="font-bold text-gray-600">{m.trailer}</span>
|
||
</div>
|
||
<div className="p-2 flex flex-col items-center cursor-pointer hover:bg-blue-50" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: false, category: 'Operating', source: 'department' })}>
|
||
<span className="text-gray-400 uppercase mb-1">其他</span>
|
||
<span className="font-bold text-gray-600">{m.other}</span>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</React.Fragment>
|
||
);
|
||
})
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Mobile Card View */}
|
||
<div className="lg:hidden p-2 space-y-2">
|
||
{deptViewMode === 'department' ? (
|
||
deptData.map((dept) => {
|
||
const isExpanded = expandedDepts.has(dept.department);
|
||
return (
|
||
<div key={dept.department} className="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
|
||
<div
|
||
className="p-3 cursor-pointer"
|
||
onClick={() => toggleDept(dept.department)}
|
||
>
|
||
<div className="flex justify-between items-center mb-2">
|
||
<h3 className="text-sm font-bold text-gray-800">{dept.department}</h3>
|
||
<span className="bg-blue-50 text-blue-600 text-[9px] font-bold px-2 py-0.5 rounded-full">
|
||
出勤率: {dept.attendanceRate}%
|
||
</span>
|
||
</div>
|
||
<div className="grid grid-cols-4 gap-2">
|
||
<div className="text-center">
|
||
<div className="text-[8px] text-gray-400 uppercase font-bold mb-0.5">资产</div>
|
||
<div className="text-xs font-black text-gray-800">{dept.totalAssets}</div>
|
||
</div>
|
||
<div className="text-center">
|
||
<div className="text-[8px] text-gray-400 uppercase font-bold mb-0.5">里程</div>
|
||
<div className="text-xs font-black text-gray-800">{dept.avgMileage}</div>
|
||
</div>
|
||
<div className="text-center">
|
||
<div className="text-[8px] text-green-500 uppercase font-bold mb-0.5">运营</div>
|
||
<div className="text-xs font-black text-green-500">{dept.operatingCount}</div>
|
||
</div>
|
||
<div className="text-center">
|
||
<div className="text-[8px] text-gray-400 uppercase font-bold mb-0.5">闲置</div>
|
||
<div className="text-xs font-black text-gray-400">{dept.idleCount}</div>
|
||
</div>
|
||
</div>
|
||
<div className="mt-1 flex justify-center">
|
||
{isExpanded ? <ChevronDown size={14} className="text-blue-500" /> : <ChevronRight size={14} className="text-gray-300" />}
|
||
</div>
|
||
</div>
|
||
{isExpanded && (
|
||
<div className="bg-gray-50/50 p-2 border-t border-gray-50 space-y-2">
|
||
{dept.managers.map(m => {
|
||
const isManagerExpanded = expandedManagerDetails.has(m.manager);
|
||
return (
|
||
<div key={m.manager} className="bg-white rounded border border-gray-100 shadow-sm overflow-hidden">
|
||
<div
|
||
className="p-2 flex justify-between items-center cursor-pointer"
|
||
onClick={() => toggleManagerDetails(m.manager)}
|
||
>
|
||
<div className="flex items-center gap-1">
|
||
{isManagerExpanded ? <ChevronDown size={12} className="text-blue-400" /> : <ChevronRight size={12} className="text-gray-300" />}
|
||
<span className="text-[11px] font-bold text-gray-700">{m.manager}</span>
|
||
</div>
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department' });
|
||
}}
|
||
className="text-[10px] font-bold text-blue-600 bg-blue-50 px-2 py-0.5 rounded"
|
||
>
|
||
合计: {m.total}
|
||
</button>
|
||
</div>
|
||
|
||
{isManagerExpanded && (
|
||
<div className="p-2 border-t border-gray-50 bg-gray-50/30 grid grid-cols-3 gap-1">
|
||
<div
|
||
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: false, category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">4.5T</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.t4_5}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: true, category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">冷链</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.t4_5c}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '18T', category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">18T</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.t18}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '49T', category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">49T</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.t49}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: true, category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">挂车</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.trailer}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: false, category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">其他</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.other}</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})
|
||
) : (
|
||
managerStats.map((m) => {
|
||
const isManagerExpanded = expandedManagerDetails.has(m.manager);
|
||
return (
|
||
<div key={m.manager} className="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
|
||
<div
|
||
className="p-2 cursor-pointer flex items-center justify-between gap-2"
|
||
onClick={() => toggleManagerDetails(m.manager)}
|
||
>
|
||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||
{isManagerExpanded ? <ChevronDown size={14} className="text-blue-500" /> : <ChevronRight size={14} className="text-gray-300" />}
|
||
<div className="flex items-center gap-3 min-w-0">
|
||
<h3 className="text-sm font-bold text-gray-800 shrink-0">{m.manager}</h3>
|
||
<span className="text-[11px] text-gray-500 shrink-0">{m.department}</span>
|
||
<div
|
||
className="text-[11px] font-bold text-blue-600 whitespace-nowrap"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department' });
|
||
}}
|
||
>
|
||
资产: {m.total}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department' });
|
||
}}
|
||
className="text-blue-500 p-1 hover:bg-blue-50 rounded transition-colors flex-shrink-0"
|
||
>
|
||
<ArrowRightLeft size={14} />
|
||
</button>
|
||
</div>
|
||
|
||
{isManagerExpanded && (
|
||
<div className="p-2 border-t border-gray-50 bg-gray-50/30 grid grid-cols-3 gap-1">
|
||
<div
|
||
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: false, category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">4.5T</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.t4_5}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '4.5T', isColdChain: true, category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">冷链</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.t4_5c}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '18T', category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">18T</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.t18}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '49T', category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">49T</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.t49}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: true, category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">挂车</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.trailer}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-white p-1 rounded border border-gray-100 cursor-pointer"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, type: '其他车型', isTrailer: false, category: 'Operating', source: 'department' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">其他</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{m.other}</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})
|
||
)}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{activeTab === 'region' && (
|
||
<div className="flex flex-col gap-6">
|
||
{/* Region Distribution Chart */}
|
||
<section className="bg-white rounded-2xl shadow-sm border border-gray-100 p-4 sm:p-6">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-1.5 h-6 bg-blue-600 rounded-full"></div>
|
||
<h2 className="text-lg font-bold text-gray-800">区域资产分布概览</h2>
|
||
</div>
|
||
<div className="flex items-center bg-gray-100 rounded-lg p-0.5">
|
||
<button
|
||
onClick={() => setRegionChartView('region')}
|
||
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>
|
||
</div>
|
||
</div>
|
||
<div className="h-72 w-full">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<BarChart data={regionChartData} margin={{ top: 20 }}>
|
||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f0f0f0" />
|
||
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fill: '#94a3b8', fontSize: 12 }} />
|
||
<YAxis axisLine={false} tickLine={false} tick={{ fill: '#94a3b8', fontSize: 12 }} />
|
||
<Tooltip
|
||
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}>
|
||
<LabelList dataKey="value" position="top" style={{ fill: '#64748b', fontSize: 11, fontWeight: 600 }} />
|
||
</Bar>
|
||
</BarChart>
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Region - Vehicle - Customer Section */}
|
||
<section className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden mb-6">
|
||
<div className="p-3 sm:p-4 border-b border-gray-50 flex items-center justify-between bg-slate-50 relative">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-1.5 h-6 bg-slate-400 rounded-full"></div>
|
||
<div>
|
||
<h2 className="text-lg font-bold text-slate-800">区域运营统计</h2>
|
||
<p className="text-[10px] text-slate-400 font-medium">*按区域—车型—客户维度统计</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => setIsRegionFilterOpen(!isRegionFilterOpen)}
|
||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
||
isRegionFilterOpen || Object.values(regionFilters).some(v => v !== '')
|
||
? 'bg-slate-200 text-slate-900 shadow-sm'
|
||
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||
}`}
|
||
>
|
||
<Filter size={14} />
|
||
<span>筛选</span>
|
||
{Object.values(regionFilters).some(v => v !== '') && (
|
||
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></span>
|
||
)}
|
||
</button>
|
||
|
||
<AnimatePresence>
|
||
{isRegionFilterOpen && (
|
||
<>
|
||
<div className="fixed inset-0 z-40" onClick={() => setIsRegionFilterOpen(false)} />
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||
className="absolute top-full right-4 mt-2 w-72 bg-white rounded-xl shadow-2xl border border-gray-100 z-50 p-4 text-gray-800"
|
||
>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3 className="text-sm font-bold text-gray-900">区域筛选</h3>
|
||
<button
|
||
onClick={() => setRegionFilters({ region: '', city: '', customer: '' })}
|
||
className="text-[10px] text-slate-600 hover:text-slate-700 font-medium"
|
||
>
|
||
重置所有
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">客户名称</label>
|
||
<div className="relative">
|
||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={14} />
|
||
<input
|
||
list="dl-region-customer"
|
||
type="text"
|
||
placeholder="搜索客户名称..."
|
||
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 pl-9 pr-3 text-xs focus:ring-2 focus:ring-slate-500/20 focus:border-slate-500 outline-none transition-all"
|
||
value={regionFilters.customer}
|
||
onChange={(e) => setRegionFilters(prev => ({ ...prev, customer: e.target.value }))}
|
||
/>
|
||
<datalist id="dl-region-customer">
|
||
{uniqueCustomerNames.map(c => <option key={c} value={c} />)}
|
||
</datalist>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">区域</label>
|
||
<input
|
||
list="dl-region-region"
|
||
type="text"
|
||
placeholder="搜索或选择..."
|
||
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 px-2 text-xs focus:ring-2 focus:ring-slate-500/20 focus:border-slate-500 outline-none transition-all"
|
||
value={regionFilters.region}
|
||
onChange={(e) => setRegionFilters(prev => ({ ...prev, region: e.target.value }))}
|
||
/>
|
||
<datalist id="dl-region-region">
|
||
{uniqueRegions.map(r => <option key={r} value={r} />)}
|
||
</datalist>
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">城市</label>
|
||
<input
|
||
list="dl-region-city"
|
||
type="text"
|
||
placeholder="搜索或选择..."
|
||
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 px-2 text-xs focus:ring-2 focus:ring-slate-500/20 focus:border-slate-500 outline-none transition-all"
|
||
value={regionFilters.city}
|
||
onChange={(e) => setRegionFilters(prev => ({ ...prev, city: e.target.value }))}
|
||
/>
|
||
<datalist id="dl-region-city">
|
||
{uniqueCities.map(c => <option key={c} value={c} />)}
|
||
</datalist>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-6 pt-4 border-t border-gray-50">
|
||
<button
|
||
onClick={() => setIsRegionFilterOpen(false)}
|
||
className="w-full bg-slate-800 text-white py-2 rounded-lg text-xs font-bold hover:bg-slate-900 transition-colors shadow-lg shadow-slate-900/20"
|
||
>
|
||
完成筛选
|
||
</button>
|
||
</div>
|
||
</motion.div>
|
||
</>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-2">
|
||
<div className="hidden lg:block overflow-x-auto">
|
||
<table className="w-full text-left border-collapse min-w-[1000px]">
|
||
<thead>
|
||
<tr className="bg-slate-50 text-slate-500 text-[11px] uppercase tracking-wider border-b border-slate-100">
|
||
<th className="p-2 font-semibold border-r border-slate-100 w-48">区域 / 车型 / 客户</th>
|
||
<th className="p-2 font-semibold border-r border-slate-100 text-center w-24">资产总数</th>
|
||
<th className="p-2 font-semibold border-r border-slate-100 text-center w-24 text-green-600">运营中</th>
|
||
<th className="p-2 font-semibold border-r border-slate-100 text-center w-24 text-orange-600">待交车</th>
|
||
<th className="p-2 font-semibold text-center bg-slate-100/50 w-32">主要客户</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="text-xs">
|
||
{regionData.filter(r => !regionFilters.region || r.region === regionFilters.region).map((r) => {
|
||
const isExpanded = expandedRegions.has(r.region);
|
||
return (
|
||
<React.Fragment key={r.region}>
|
||
<tr
|
||
className={`border-b border-slate-100 cursor-pointer transition-colors ${isExpanded ? 'bg-slate-50' : 'bg-white hover:bg-slate-50/50'}`}
|
||
onClick={() => toggleRegion(r.region)}
|
||
>
|
||
<td className="p-2 font-bold text-slate-700 flex items-center gap-2">
|
||
{isExpanded ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
|
||
<Truck size={14} className="text-slate-400" />
|
||
{r.region}区域
|
||
</td>
|
||
<td className="p-2 text-center font-bold text-slate-600 cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, category: 'Operating' }); }}>{r.totalAssets}</td>
|
||
<td className="p-2 text-center text-green-600 font-bold cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, category: 'Operating' }); }}>{r.operatingCount}</td>
|
||
<td className="p-2 text-center text-orange-600 font-bold cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); if (r.pendingCount) setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, category: 'Pending' }); }}>{r.pendingCount || ''}</td>
|
||
<td className="p-2 text-center text-slate-500 font-medium">{r.customers.slice(0, 2).join(', ')}</td>
|
||
</tr>
|
||
{isExpanded && r.cities.map((city) => {
|
||
const cityKey = `${r.region}-${city.city}`;
|
||
const isCityExpanded = expandedRegionCities.has(cityKey);
|
||
return (
|
||
<React.Fragment key={city.city}>
|
||
<tr
|
||
className="border-b border-gray-50 hover:bg-gray-50 cursor-pointer"
|
||
onClick={() => toggleRegionCity(cityKey)}
|
||
>
|
||
<td className="p-2 pl-8 text-slate-600 flex items-center gap-2">
|
||
{isCityExpanded ? <ChevronDown size={12} className="text-slate-400" /> : <ChevronRight size={12} className="text-slate-400" />}
|
||
<MapPin size={12} className="text-slate-300" />
|
||
<span className="font-medium">{city.city}</span>
|
||
</td>
|
||
<td className="p-2 text-center text-slate-600 font-medium cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, category: 'Operating' }); }}>{city.totalAssets}</td>
|
||
<td className="p-2 text-center text-green-600 cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, category: 'Operating' }); }}>{city.operatingCount}</td>
|
||
<td className="p-2 text-center text-orange-600 cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); if (city.pendingCount) setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, category: 'Pending' }); }}>{city.pendingCount || ''}</td>
|
||
<td className="p-2 text-center text-slate-400 text-[10px] italic">{city.customers.slice(0, 2).join(', ')}</td>
|
||
</tr>
|
||
{isCityExpanded && city.typeBreakdown.map(tb => (
|
||
<tr key={tb.type} className="border-b border-gray-50/50 bg-slate-50/30">
|
||
<td className="p-2 pl-14 text-gray-400 flex items-center gap-2">
|
||
<div className="w-1 h-1 bg-slate-300 rounded-full"></div>
|
||
{tb.type} 车型
|
||
</td>
|
||
<td className="p-2 text-center text-gray-500 cursor-pointer hover:underline" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, vehicleType: tb.type, category: 'Operating' })}>{tb.total}</td>
|
||
<td className="p-2 text-center text-green-500 cursor-pointer hover:underline" onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, vehicleType: tb.type, category: 'Operating' })}>{tb.operating}</td>
|
||
<td className="p-2 text-center text-orange-500 cursor-pointer hover:underline" onClick={() => { if (tb.inventory) setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, vehicleType: tb.type, category: 'Pending' }); }}>{tb.inventory || ''}</td>
|
||
<td className="p-2 text-center text-gray-400 text-[10px] italic">{tb.customers.slice(0, 2).join(', ')}</td>
|
||
</tr>
|
||
))}
|
||
</React.Fragment>
|
||
);
|
||
})}
|
||
</React.Fragment>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Mobile View (Region) */}
|
||
<div className="lg:hidden p-2 space-y-3">
|
||
{regionData.filter(r => !regionFilters.region || r.region === regionFilters.region).map((r) => {
|
||
const isExpanded = expandedRegions.has(r.region);
|
||
return (
|
||
<div key={r.region} className="bg-slate-50/50 rounded-xl border border-slate-100 overflow-hidden">
|
||
<div
|
||
className="bg-white p-3 flex justify-between items-center cursor-pointer"
|
||
onClick={() => toggleRegion(r.region)}
|
||
>
|
||
<div className="flex items-center gap-2 font-bold text-slate-700">
|
||
{isExpanded ? <ChevronDown size={14} className="text-slate-400" /> : <ChevronRight size={14} className="text-slate-400" />}
|
||
<Truck size={14} className="text-slate-400" />
|
||
{r.region}区域
|
||
</div>
|
||
<div className="text-xs font-bold text-slate-500">资产: {r.totalAssets}</div>
|
||
</div>
|
||
{isExpanded && (
|
||
<>
|
||
<div className="p-2 grid grid-cols-2 gap-2 text-center border-t border-slate-100">
|
||
<div
|
||
className="bg-white p-2 rounded border border-slate-100 cursor-pointer active:bg-green-50"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, category: 'Operating', source: 'asset' })}
|
||
>
|
||
<div className="text-[9px] text-gray-400 uppercase">运营中</div>
|
||
<div className="text-xs font-bold text-green-600">{r.operatingCount}</div>
|
||
</div>
|
||
<div
|
||
className="bg-white p-2 rounded border border-slate-100 cursor-pointer active:bg-orange-50"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, category: 'Pending', source: 'asset' })}
|
||
>
|
||
<div className="text-[9px] text-gray-400 uppercase">待交车</div>
|
||
<div className="text-xs font-bold text-orange-600">{r.pendingCount || ''}</div>
|
||
</div>
|
||
</div>
|
||
<div className="px-2 pb-2 space-y-1">
|
||
{r.typeBreakdown.map(tb => (
|
||
<div key={tb.type} className="flex justify-between items-center text-[10px] bg-white/80 px-2 py-1.5 rounded border border-slate-50">
|
||
<span className="text-gray-500">{tb.type} 车型</span>
|
||
<div className="flex gap-3">
|
||
<span
|
||
className="font-bold text-green-600 cursor-pointer"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, vehicleType: tb.type, category: 'Operating', source: 'asset' })}
|
||
>
|
||
运:{tb.operating}
|
||
</span>
|
||
<span
|
||
className="font-bold text-orange-600 cursor-pointer"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, vehicleType: tb.type, category: 'Pending', source: 'asset' })}
|
||
>
|
||
待:{tb.inventory || ''}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
)}
|
||
|
||
{activeTab === 'customer' && (
|
||
<div className="flex flex-col gap-6">
|
||
{/* Customer Region Distribution Chart */}
|
||
<section className="bg-white rounded-2xl shadow-sm border border-gray-100 p-4 sm:p-6">
|
||
<div className="flex items-center justify-between mb-5">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-1.5 h-6 bg-emerald-500 rounded-full"></div>
|
||
<h2 className="text-lg font-bold text-gray-800">客户运营地区占比</h2>
|
||
</div>
|
||
<div className="flex items-center bg-gray-100 rounded-lg p-0.5">
|
||
<button
|
||
onClick={() => setCustomerChartView('region')}
|
||
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>
|
||
</div>
|
||
</div>
|
||
{(() => {
|
||
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 (
|
||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-8 items-center">
|
||
{/* Donut chart */}
|
||
<div className="relative flex-shrink-0" style={{ width: 200, height: 200 }}>
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<PieChart>
|
||
<Pie
|
||
data={pieData}
|
||
cx="50%" cy="50%"
|
||
innerRadius={68} outerRadius={90}
|
||
paddingAngle={3}
|
||
startAngle={90} endAngle={-270}
|
||
dataKey="value"
|
||
>
|
||
{pieData.map((_, i) => (
|
||
<Cell key={i} fill={PIE_COLORS[i % PIE_COLORS.length]} stroke="white" strokeWidth={2} />
|
||
))}
|
||
</Pie>
|
||
<Tooltip
|
||
formatter={(value) => [`${value} 辆`, '']}
|
||
contentStyle={{ borderRadius: '10px', border: 'none', boxShadow: '0 8px 24px -4px rgba(0,0,0,0.12)', fontSize: 12 }}
|
||
/>
|
||
</PieChart>
|
||
</ResponsiveContainer>
|
||
{/* Center label */}
|
||
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
|
||
<span className="text-2xl font-bold text-gray-800">{grandTotal}</span>
|
||
<span className="text-xs text-gray-400 mt-0.5">辆</span>
|
||
</div>
|
||
</div>
|
||
{/* Custom legend */}
|
||
<div className="flex-1 w-full space-y-2.5">
|
||
{pieData.map((item, i) => {
|
||
const pct = grandTotal > 0 ? (item.value / grandTotal * 100) : 0;
|
||
const color = PIE_COLORS[i % PIE_COLORS.length];
|
||
return (
|
||
<div key={i} className="flex items-center gap-2.5">
|
||
<div className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ background: color }} />
|
||
<span className="text-sm text-gray-600 flex-1 min-w-0 truncate">{item.name}</span>
|
||
<div className="w-20 h-1.5 bg-gray-100 rounded-full overflow-hidden flex-shrink-0">
|
||
<div className="h-full rounded-full transition-all" style={{ width: `${pct}%`, background: color }} />
|
||
</div>
|
||
<span className="text-xs font-semibold text-gray-700 w-6 text-right flex-shrink-0">{item.value}</span>
|
||
<span className="text-xs text-gray-400 w-9 text-right flex-shrink-0">{pct.toFixed(1)}%</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
})()}
|
||
</section>
|
||
|
||
{/* Customer Operations Statistics Section */}
|
||
<section className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden mb-6">
|
||
<div className="p-3 sm:p-4 border-b border-gray-50 flex items-center justify-between bg-emerald-800 text-white relative">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-1.5 h-6 bg-emerald-400 rounded-full"></div>
|
||
<div>
|
||
<h2 className="text-lg font-bold">客户运营统计</h2>
|
||
<p className="text-[10px] opacity-70 font-medium">*按客户维度统计资产分布</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2">
|
||
<button
|
||
onClick={() => setIsCustomerFilterOpen(!isCustomerFilterOpen)}
|
||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
||
isCustomerFilterOpen || Object.values(customerFilters).some(v => v !== '')
|
||
? 'bg-emerald-400 text-emerald-900 shadow-lg shadow-emerald-900/20'
|
||
: 'bg-emerald-700/50 text-emerald-100 hover:bg-emerald-700'
|
||
}`}
|
||
>
|
||
<Filter size={14} />
|
||
<span>筛选</span>
|
||
{Object.values(customerFilters).some(v => v !== '') && (
|
||
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></span>
|
||
)}
|
||
</button>
|
||
|
||
<AnimatePresence>
|
||
{isCustomerFilterOpen && (
|
||
<>
|
||
{/* Backdrop for closing */}
|
||
<div
|
||
className="fixed inset-0 z-40"
|
||
onClick={() => setIsCustomerFilterOpen(false)}
|
||
/>
|
||
|
||
{/* Popover Content */}
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||
className="absolute top-full right-4 mt-2 w-72 bg-white rounded-xl shadow-2xl border border-gray-100 z-50 p-4 text-gray-800"
|
||
>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3 className="text-sm font-bold text-gray-900">数据筛选</h3>
|
||
<button
|
||
onClick={() => setCustomerFilters({ customer: '', brand: '', department: '', manager: '', region: '' })}
|
||
className="text-[10px] text-emerald-600 hover:text-emerald-700 font-medium"
|
||
>
|
||
重置所有
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">客户名称</label>
|
||
<div className="relative">
|
||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400" size={12} />
|
||
<input
|
||
list="dl-cust-customer"
|
||
type="text"
|
||
placeholder="搜索客户..."
|
||
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 pl-8 pr-2 text-xs focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none transition-all"
|
||
value={customerFilters.customer}
|
||
onChange={(e) => setCustomerFilters(prev => ({ ...prev, customer: e.target.value }))}
|
||
/>
|
||
<datalist id="dl-cust-customer">
|
||
{uniqueCustomerNames.map(c => <option key={c} value={c} />)}
|
||
</datalist>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">业务负责人</label>
|
||
<div className="relative">
|
||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400" size={12} />
|
||
<input
|
||
list="dl-cust-manager"
|
||
type="text"
|
||
placeholder="搜索负责人..."
|
||
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 pl-8 pr-2 text-xs focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none transition-all"
|
||
value={customerFilters.manager}
|
||
onChange={(e) => setCustomerFilters(prev => ({ ...prev, manager: e.target.value }))}
|
||
/>
|
||
<datalist id="dl-cust-manager">
|
||
{uniqueCustomerManagers.map(m => <option key={m} value={m} />)}
|
||
</datalist>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">品牌</label>
|
||
<input
|
||
list="dl-cust-brand"
|
||
type="text"
|
||
placeholder="搜索或选择..."
|
||
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 px-2 text-xs focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none transition-all"
|
||
value={customerFilters.brand}
|
||
onChange={(e) => setCustomerFilters(prev => ({ ...prev, brand: e.target.value }))}
|
||
/>
|
||
<datalist id="dl-cust-brand">
|
||
{uniqueBrands.map(b => <option key={b} value={b} />)}
|
||
</datalist>
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">部门</label>
|
||
<input
|
||
list="dl-cust-dept"
|
||
type="text"
|
||
placeholder="搜索或选择..."
|
||
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 px-2 text-xs focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none transition-all"
|
||
value={customerFilters.department}
|
||
onChange={(e) => setCustomerFilters(prev => ({ ...prev, department: e.target.value }))}
|
||
/>
|
||
<datalist id="dl-cust-dept">
|
||
{uniqueDepts.map(d => <option key={d} value={d} />)}
|
||
</datalist>
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">区域</label>
|
||
<input
|
||
list="dl-cust-region"
|
||
type="text"
|
||
placeholder="搜索或选择..."
|
||
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 px-2 text-xs focus:ring-2 focus:ring-emerald-500/20 focus:border-emerald-500 outline-none transition-all"
|
||
value={customerFilters.region}
|
||
onChange={(e) => setCustomerFilters(prev => ({ ...prev, region: e.target.value }))}
|
||
/>
|
||
<datalist id="dl-cust-region">
|
||
{uniqueRegions.map(r => <option key={r} value={r} />)}
|
||
</datalist>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-6 pt-4 border-t border-gray-50">
|
||
<button
|
||
onClick={() => setIsCustomerFilterOpen(false)}
|
||
className="w-full bg-emerald-600 text-white py-2 rounded-lg text-xs font-bold hover:bg-emerald-700 transition-colors shadow-lg shadow-emerald-600/20"
|
||
>
|
||
完成筛选
|
||
</button>
|
||
</div>
|
||
</motion.div>
|
||
</>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-0 sm:p-2">
|
||
{/* Desktop Table View (Customer) */}
|
||
<div className="hidden lg:block overflow-x-auto">
|
||
<table className="w-full text-left border-collapse min-w-[1000px]">
|
||
<thead>
|
||
<tr className="bg-emerald-700 text-white text-[11px] uppercase tracking-wider border-b border-emerald-800">
|
||
<th className="p-2 font-semibold border-r border-emerald-600 w-40">客户名称</th>
|
||
<th className="p-2 font-semibold border-r border-emerald-600 w-24">所在区域</th>
|
||
<th className="p-2 font-semibold border-r border-emerald-600 w-24">关联业务负责人</th>
|
||
<th className="p-2 font-semibold border-r border-emerald-600 text-center w-20">4.5T</th>
|
||
<th className="p-2 font-semibold border-r border-emerald-600 text-center w-20">4.5T冷链</th>
|
||
<th className="p-2 font-semibold border-r border-emerald-600 text-center w-20">18T</th>
|
||
<th className="p-2 font-semibold border-r border-emerald-600 text-center w-20">49T</th>
|
||
<th className="p-2 font-semibold border-r border-emerald-600 text-center w-20">挂车</th>
|
||
<th className="p-2 font-semibold border-r border-emerald-600 text-center w-20">其他</th>
|
||
<th className="p-2 font-semibold text-center bg-emerald-900/40 w-24">合计</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="text-xs">
|
||
{filteredCustomerStats.map((cust) => {
|
||
const isExpanded = expandedCustomers.has(cust.customer);
|
||
return (
|
||
<React.Fragment key={cust.customer}>
|
||
<tr
|
||
className={`cursor-pointer transition-all border-b border-gray-100 ${
|
||
isExpanded ? 'bg-emerald-50/50' : 'hover:bg-gray-50'
|
||
}`}
|
||
onClick={() => toggleCustomer(cust.customer)}
|
||
>
|
||
<td className="p-2 border-r border-gray-100 font-bold text-gray-800 flex items-center gap-2">
|
||
{isExpanded ? <ChevronDown size={14} className="text-emerald-600" /> : <ChevronRight size={14} className="text-gray-400" />}
|
||
{cust.customer}
|
||
</td>
|
||
<td className="p-2 border-r border-gray-100 text-gray-600 text-center">
|
||
<span className="bg-gray-100 px-2 py-0.5 rounded text-[10px] font-medium">{cust.region}</span>
|
||
</td>
|
||
<td className="p-2 border-r border-gray-100 text-gray-600 text-center">{cust.manager}</td>
|
||
<td className="p-2 border-r border-gray-100 text-center text-gray-500 cursor-pointer hover:bg-emerald-50 transition-colors" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '4.5T', source: 'customer' }); }}>{cust.t4_5}</td>
|
||
<td className="p-2 border-r border-gray-100 text-center text-gray-500 cursor-pointer hover:bg-emerald-50 transition-colors" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '4.5T', source: 'customer' }); }}>{cust.t4_5c}</td>
|
||
<td className="p-2 border-r border-gray-100 text-center text-gray-500 cursor-pointer hover:bg-emerald-50 transition-colors" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '18T', source: 'customer' }); }}>{cust.t18}</td>
|
||
<td className="p-2 border-r border-gray-100 text-center text-gray-500 cursor-pointer hover:bg-emerald-50 transition-colors" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '49T', source: 'customer' }); }}>{cust.t49}</td>
|
||
<td className="p-2 border-r border-gray-100 text-center text-gray-500 cursor-pointer hover:bg-emerald-50 transition-colors" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '其他车型', source: 'customer' }); }}>{cust.trailer}</td>
|
||
<td className="p-2 border-r border-gray-100 text-center text-gray-500 cursor-pointer hover:bg-emerald-50 transition-colors" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '其他车型', source: 'customer' }); }}>{cust.other}</td>
|
||
<td className="p-2 text-center font-bold bg-emerald-50 text-emerald-800 cursor-pointer hover:bg-emerald-100 transition-colors" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, source: 'customer' }); }}>{cust.total}</td>
|
||
</tr>
|
||
{isExpanded && (
|
||
<tr className="bg-gray-50/30">
|
||
<td colSpan={9} className="p-2">
|
||
<div className="grid grid-cols-4 gap-2">
|
||
<div className="bg-white p-2 rounded border border-gray-100 shadow-sm">
|
||
<div className="text-[10px] text-gray-400 uppercase mb-1">客户详情</div>
|
||
<div className="text-sm font-bold text-gray-700">{cust.customer}</div>
|
||
</div>
|
||
<div className="bg-white p-2 rounded border border-gray-100 shadow-sm">
|
||
<div className="text-[10px] text-gray-400 uppercase mb-1">主要车型</div>
|
||
<div className="text-sm font-bold text-emerald-600">
|
||
{cust.t49 > cust.t18 ? '49T 重卡' : (cust.t18 > cust.t4_5c ? '18T 货车' : '4.5T 轻卡')}
|
||
</div>
|
||
</div>
|
||
<div className="bg-white p-2 rounded border border-gray-100 shadow-sm">
|
||
<div className="text-[10px] text-gray-400 uppercase mb-1">业务经理</div>
|
||
<div className="text-sm font-bold text-gray-700">{cust.manager}</div>
|
||
</div>
|
||
<div className="bg-white p-2 rounded border border-gray-100 shadow-sm">
|
||
<div className="text-[10px] text-gray-400 uppercase mb-1">资产占比</div>
|
||
<div className="text-sm font-bold text-gray-700">
|
||
{((cust.total / deptData.reduce((s, d) => s + d.totalAssets, 0)) * 100).toFixed(1)}%
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</React.Fragment>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* Mobile Card View (Customer) */}
|
||
<div className="lg:hidden p-2 space-y-2">
|
||
{filteredCustomerStats.map((cust) => {
|
||
const isExpanded = expandedCustomers.has(cust.customer);
|
||
return (
|
||
<div key={cust.customer} className="bg-white rounded border border-gray-100 shadow-sm overflow-hidden">
|
||
<div
|
||
className={`p-2 flex justify-between items-center cursor-pointer transition-colors ${
|
||
isExpanded ? 'bg-emerald-50' : 'bg-gray-50'
|
||
}`}
|
||
onClick={() => toggleCustomer(cust.customer)}
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
{isExpanded ? <ChevronDown size={16} className="text-emerald-600" /> : <ChevronRight size={16} className="text-gray-400" />}
|
||
<div className="flex flex-col">
|
||
<span className="font-bold text-gray-800 text-sm">{cust.customer}</span>
|
||
<span className="text-[10px] text-emerald-600 font-medium">{cust.region}区域</span>
|
||
</div>
|
||
</div>
|
||
<div className="text-xs font-bold text-emerald-700 bg-emerald-100 px-2 py-0.5 rounded-full">
|
||
合计: {cust.total}
|
||
</div>
|
||
</div>
|
||
|
||
{isExpanded && (
|
||
<div className="p-2 space-y-3 bg-white">
|
||
{/* Details Cards for Mobile */}
|
||
<div className="grid grid-cols-2 gap-2">
|
||
<div className="bg-gray-50 p-2 rounded border border-gray-100">
|
||
<div className="text-[8px] text-gray-400 uppercase mb-1">客户详情</div>
|
||
<div className="text-[10px] font-bold text-gray-700">{cust.customer}</div>
|
||
</div>
|
||
<div className="bg-gray-50 p-2 rounded border border-gray-100">
|
||
<div className="text-[8px] text-gray-400 uppercase mb-1">主要车型</div>
|
||
<div className="text-xs font-bold text-emerald-600">
|
||
{cust.t49 > cust.t18 ? '49T 重卡' : (cust.t18 > cust.t4_5c ? '18T 货车' : '4.5T 轻卡')}
|
||
</div>
|
||
</div>
|
||
<div className="bg-gray-50 p-2 rounded border border-gray-100">
|
||
<div className="text-[8px] text-gray-400 uppercase mb-1">业务经理</div>
|
||
<div className="text-xs font-bold text-gray-700">{cust.manager}</div>
|
||
</div>
|
||
<div className="bg-gray-50 p-2 rounded border border-gray-100">
|
||
<div className="text-[8px] text-gray-400 uppercase mb-1">资产占比</div>
|
||
<div className="text-xs font-bold text-gray-700">
|
||
{((cust.total / deptData.reduce((s, d) => s + d.totalAssets, 0)) * 100).toFixed(1)}%
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="border-t border-gray-50 pt-2">
|
||
<div className="text-[8px] text-gray-400 uppercase mb-2">车型分布</div>
|
||
<div className="grid grid-cols-3 gap-2">
|
||
<div
|
||
className="text-center bg-gray-50 p-1 rounded cursor-pointer hover:bg-emerald-50 transition-colors"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '4.5T', source: 'customer' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">4.5T</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{cust.t4_5}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-gray-50 p-1 rounded cursor-pointer hover:bg-emerald-50 transition-colors"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '4.5T', source: 'customer' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">冷链</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{cust.t4_5c}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-gray-50 p-1 rounded cursor-pointer hover:bg-emerald-50 transition-colors"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '18T', source: 'customer' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">18T</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{cust.t18}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-gray-50 p-1 rounded cursor-pointer hover:bg-emerald-50 transition-colors"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '49T', source: 'customer' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">49T</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{cust.t49}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-gray-50 p-1 rounded cursor-pointer hover:bg-emerald-50 transition-colors"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '其他车型', source: 'customer' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">挂车</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{cust.trailer}</div>
|
||
</div>
|
||
<div
|
||
className="text-center bg-gray-50 p-1 rounded cursor-pointer hover:bg-emerald-50 transition-colors"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, type: '其他车型', source: 'customer' })}
|
||
>
|
||
<div className="text-[8px] text-gray-400 uppercase">其他</div>
|
||
<div className="text-[10px] font-bold text-gray-600">{cust.other}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
)}
|
||
|
||
|
||
{/* Vehicle Detail Modal */}
|
||
<AnimatePresence>
|
||
{showPlateNumbers && (
|
||
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||
<motion.div
|
||
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||
className={`bg-white rounded-xl shadow-2xl w-full max-w-[95vw] ${showPlateNumbers.source === 'customer' ? 'lg:max-w-6xl' : 'lg:max-w-4xl'} overflow-hidden flex flex-col max-h-[85vh] sm:max-h-[90vh] min-h-[40vh]`}
|
||
>
|
||
<div className="p-4 border-b border-gray-100 flex justify-between items-center bg-slate-800 text-white shrink-0">
|
||
<div>
|
||
<h3 className="font-bold text-base flex items-center gap-2">
|
||
<Truck size={18} className="text-blue-400" />
|
||
{showPlateNumbers.title || (
|
||
(showPlateNumbers.manager ? `${showPlateNumbers.manager} 的${showPlateNumbers.type || ''}车辆` :
|
||
showPlateNumbers.customer ? `${showPlateNumbers.customer} 的${showPlateNumbers.type || ''}车辆` :
|
||
showPlateNumbers.batch === 'All' ? '全量批次' : `${showPlateNumbers.batch} 批次`) + ' - 运营明细'
|
||
)}
|
||
</h3>
|
||
<p className="text-[10px] opacity-60 mt-0.5">
|
||
{showPlateNumbers.model === 'All' ? '全量型号' : showPlateNumbers.model} |
|
||
{showPlateNumbers.category === 'Pending' ? '待交车' :
|
||
showPlateNumbers.category === 'Delivered' ? '本周已交车' :
|
||
showPlateNumbers.category === 'Returned' ? '已还车' :
|
||
showPlateNumbers.category === 'Replaced' ? '已替换' :
|
||
showPlateNumbers.category === 'Inventory' ? `${showPlateNumbers.location}库存` :
|
||
showPlateNumbers.category === 'Operating' ? '正在运营' : '全部状态'}
|
||
</p>
|
||
</div>
|
||
<button onClick={() => setShowPlateNumbers(null)} className="hover:bg-white/10 p-2 rounded-full transition-colors">
|
||
<PlusCircle className="rotate-45" size={24} />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Modal Filters */}
|
||
<div className="px-4 py-2 bg-slate-50 border-b border-gray-200 shrink-0">
|
||
<div
|
||
className="flex justify-between items-center cursor-pointer py-1"
|
||
onClick={() => setIsModalFilterExpanded(!isModalFilterExpanded)}
|
||
>
|
||
<div className="flex items-center gap-2 text-slate-600">
|
||
<Filter size={14} />
|
||
<span className="text-xs font-bold">筛选条件</span>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
{/* Quick Search always visible when collapsed */}
|
||
{!isModalFilterExpanded && (
|
||
<div className="relative w-40 sm:w-64" onClick={(e) => e.stopPropagation()}>
|
||
<Search size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400" />
|
||
<input
|
||
type="text"
|
||
value={modalFilters.plateNumber}
|
||
onChange={(e) => setModalFilters({...modalFilters, plateNumber: e.target.value})}
|
||
placeholder="快速搜索车牌..."
|
||
className="w-full text-[11px] pl-7 pr-2 py-1 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm"
|
||
/>
|
||
</div>
|
||
)}
|
||
<motion.div
|
||
animate={{ rotate: isModalFilterExpanded ? 180 : 0 }}
|
||
transition={{ duration: 0.2 }}
|
||
>
|
||
<ChevronDown size={16} className="text-slate-400" />
|
||
</motion.div>
|
||
</div>
|
||
</div>
|
||
|
||
<AnimatePresence>
|
||
{isModalFilterExpanded && (
|
||
<motion.div
|
||
initial={{ height: 0, opacity: 0 }}
|
||
animate={{ height: 'auto', opacity: 1 }}
|
||
exit={{ height: 0, opacity: 0 }}
|
||
className="overflow-hidden"
|
||
>
|
||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 py-3 border-t border-gray-100 mt-1">
|
||
<div className="space-y-1">
|
||
<label className="block text-[10px] text-gray-500 font-medium">车牌号码</label>
|
||
<div className="relative">
|
||
<Search size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400" />
|
||
<input
|
||
list="dl-modal-plate"
|
||
type="text"
|
||
value={modalFilters.plateNumber}
|
||
onChange={(e) => setModalFilters({...modalFilters, plateNumber: e.target.value})}
|
||
placeholder="搜索车牌..."
|
||
className="w-full text-[11px] pl-7 pr-2 py-1.5 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm"
|
||
/>
|
||
<datalist id="dl-modal-plate">
|
||
{uniqueModalPlates.map(p => <option key={p} value={p} />)}
|
||
</datalist>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="block text-[10px] text-gray-500 font-medium">车型型号</label>
|
||
<div className="relative">
|
||
<Search size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400" />
|
||
<input
|
||
list="dl-modal-model"
|
||
type="text"
|
||
value={modalFilters.model}
|
||
onChange={(e) => setModalFilters({...modalFilters, model: e.target.value})}
|
||
placeholder="搜索车型..."
|
||
className="w-full text-[11px] pl-7 pr-2 py-1.5 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm"
|
||
/>
|
||
<datalist id="dl-modal-model">
|
||
{uniqueModalModels.map(m => <option key={m} value={m} />)}
|
||
</datalist>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="block text-[10px] text-gray-500 font-medium">品牌名称</label>
|
||
<div className="relative">
|
||
<Search size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400" />
|
||
<input
|
||
list="dl-modal-brand"
|
||
type="text"
|
||
value={modalFilters.brand}
|
||
onChange={(e) => setModalFilters({...modalFilters, brand: e.target.value})}
|
||
placeholder="搜索品牌..."
|
||
className="w-full text-[11px] pl-7 pr-2 py-1.5 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm"
|
||
/>
|
||
<datalist id="dl-modal-brand">
|
||
{uniqueModalBrands.map(b => <option key={b} value={b} />)}
|
||
</datalist>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-1">
|
||
<label className="block text-[10px] text-gray-500 font-medium">运营所在地</label>
|
||
<div className="relative">
|
||
<Search size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400" />
|
||
<input
|
||
list="dl-modal-location"
|
||
type="text"
|
||
value={modalFilters.location}
|
||
onChange={(e) => setModalFilters({...modalFilters, location: e.target.value})}
|
||
placeholder="搜索所在地..."
|
||
className="w-full text-[11px] pl-7 pr-2 py-1.5 bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm"
|
||
/>
|
||
<datalist id="dl-modal-location">
|
||
{uniqueModalLocations.map(l => <option key={l} value={l} />)}
|
||
</datalist>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex justify-end pb-2">
|
||
<button
|
||
onClick={() => setModalFilters({ plateNumber: '', model: '', brand: '', location: '' })}
|
||
className="text-[10px] text-blue-500 hover:text-blue-600 font-medium"
|
||
>
|
||
重置筛选
|
||
</button>
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
|
||
<div className="flex-1 overflow-auto p-0 sm:p-4 bg-gray-50 min-h-0 overscroll-contain">
|
||
{modalLoading ? (
|
||
<div className="flex items-center justify-center py-16">
|
||
<Loader2 className="animate-spin text-blue-500" size={32} />
|
||
</div>
|
||
) : modalWeeklyDetail.length > 0 ? (
|
||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden w-full">
|
||
<table className="w-full text-left border-collapse">
|
||
<thead>
|
||
<tr className="bg-slate-700 text-white text-[10px] uppercase tracking-wider">
|
||
<th className="p-2 font-semibold border-r border-slate-600 text-center">车牌</th>
|
||
<th className="p-2 font-semibold border-r border-slate-600 text-center">客户名称</th>
|
||
<th className="p-2 font-semibold text-center">日期</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="text-[11px]">
|
||
{filteredModalWeeklyDetail.map((v, i) => (
|
||
<tr key={`${v.truck_id}-${i}`} className={`border-b border-gray-100 hover:bg-blue-50/50 transition-colors ${i % 2 === 0 ? 'bg-white' : 'bg-gray-50/30'}`}>
|
||
<td className="p-2 border-r border-gray-100 font-mono font-bold text-blue-700 text-center">{v.plate_number}</td>
|
||
<td className="p-2 border-r border-gray-100 text-gray-600 text-center">{v.customer_name || '—'}</td>
|
||
<td className="p-2 text-gray-500 text-center">{v.handover_date || '—'}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
) : (
|
||
<div className="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden w-full">
|
||
<table className="w-full text-left border-collapse">
|
||
<thead>
|
||
<tr className="bg-slate-700 text-white text-[10px] uppercase tracking-wider">
|
||
{showPlateNumbers.source === 'customer' ? (
|
||
<>
|
||
<th className="p-2 font-semibold border-r border-slate-600 sticky left-0 bg-slate-700 z-10 w-10 text-center">月</th>
|
||
<th className="p-2 font-semibold border-r border-slate-600 w-24">业务部门</th>
|
||
<th className="p-2 font-semibold border-r border-slate-600 w-24">业务负责人</th>
|
||
<th className="p-2 font-semibold border-r border-slate-600 w-16">品牌</th>
|
||
<th className="p-2 font-semibold border-r border-slate-600 w-16">车型</th>
|
||
<th className="p-2 font-semibold border-r border-slate-600 w-48">资产归属</th>
|
||
<th className="p-2 font-semibold border-r border-slate-600 w-48">客户名称</th>
|
||
<th className="p-2 font-semibold border-r border-slate-600 w-24">车牌</th>
|
||
<th className="p-2 font-semibold border-r border-slate-600 w-16 text-center">状态</th>
|
||
<th className="p-2 font-semibold border-r border-slate-600 w-24 text-center">提车时间</th>
|
||
<th className="p-2 font-semibold border-r border-slate-600 w-24 text-center">合同到期时间</th>
|
||
<th className="p-2 font-semibold border-r border-slate-600 w-24">运营区域</th>
|
||
<th className="p-2 font-semibold border-r border-slate-600 w-20 text-center">离到期</th>
|
||
<th className="p-2 font-semibold w-48">签约公司</th>
|
||
</>
|
||
) : (
|
||
<>
|
||
<th className="p-2 font-semibold border-r border-slate-600 text-center">车牌</th>
|
||
{showPlateNumbers.source !== 'asset' && (
|
||
<th className="p-2 font-semibold border-r border-slate-600 text-center">客户名称</th>
|
||
)}
|
||
<th className="p-2 font-semibold border-r border-slate-600 text-center">品牌</th>
|
||
<th className="p-2 font-semibold border-r border-slate-600 text-center">车型</th>
|
||
<th className="p-2 font-semibold text-center">所在地</th>
|
||
</>
|
||
)}
|
||
</tr>
|
||
</thead>
|
||
<tbody className="text-[11px]">
|
||
{filteredModalVehicles.map((v, idx) => (
|
||
<tr key={v.id} className={`border-b border-gray-100 hover:bg-blue-50/50 transition-colors ${idx % 2 === 0 ? 'bg-white' : 'bg-gray-50/30'}`}>
|
||
{showPlateNumbers.source === 'customer' ? (
|
||
<>
|
||
<td className="p-2 border-r border-gray-100 text-center sticky left-0 bg-inherit z-10 font-bold text-gray-400">{'—'}</td>
|
||
<td className="p-2 border-r border-gray-100 text-gray-600">{v.departmentName || '—'}</td>
|
||
<td className="p-2 border-r border-gray-100 font-medium text-gray-700">{v.customerManager || '—'}</td>
|
||
<td className="p-2 border-r border-gray-100 text-gray-600">{v.brandLabel || '—'}</td>
|
||
<td className="p-2 border-r border-gray-100 text-gray-600">{v.type}</td>
|
||
<td className="p-2 border-r border-gray-100 text-gray-500 text-[10px] leading-tight">{v.subjectOrg || '—'}</td>
|
||
<td className="p-2 border-r border-gray-100 font-bold text-gray-800">{v.customerName || '—'}</td>
|
||
<td className={`p-2 border-r border-gray-100 font-mono font-bold ${v.plateNumber ? 'text-blue-700' : 'text-orange-500'}`}>{v.plateNumber || v.vin || '—'}</td>
|
||
<td className="p-2 border-r border-gray-100 text-center">
|
||
<span className={`px-1.5 py-0.5 rounded-full text-[9px] font-bold ${
|
||
v.status === 'Operating' ? 'bg-green-100 text-green-700' :
|
||
v.status === 'Inventory' ? 'bg-blue-100 text-blue-700' : 'bg-red-100 text-red-700'
|
||
}`}>
|
||
{v.status === 'Operating' ? '在租' : v.status === 'Inventory' ? '库存' : '异常'}
|
||
</span>
|
||
</td>
|
||
<td className="p-2 border-r border-gray-100 text-center text-gray-500">{'—'}</td>
|
||
<td className="p-2 border-r border-gray-100 text-center text-gray-500">{'—'}</td>
|
||
<td className="p-2 border-r border-gray-100 text-gray-600">{v.location}</td>
|
||
<td className="p-2 border-r border-gray-100 text-center font-bold text-orange-600">{'—'}</td>
|
||
<td className="p-2 text-gray-500 text-[10px] leading-tight">{v.orgName || '—'}</td>
|
||
</>
|
||
) : (
|
||
<>
|
||
<td className={`p-2 border-r border-gray-100 font-mono font-bold text-center ${v.plateNumber ? 'text-blue-700' : 'text-orange-500'}`}>{v.plateNumber || v.vin || '—'}</td>
|
||
{showPlateNumbers.source !== 'asset' && (
|
||
<td className="p-2 border-r border-gray-100 font-bold text-gray-800 text-center">{v.customerName || '—'}</td>
|
||
)}
|
||
<td className="p-2 border-r border-gray-100 text-gray-600 text-center">{v.brandLabel || '—'}</td>
|
||
<td className="p-2 border-r border-gray-100 text-gray-600 text-center">{v.type}</td>
|
||
<td className="p-2 text-gray-600 text-center">{v.location}</td>
|
||
</>
|
||
)}
|
||
</tr>
|
||
))}
|
||
{filteredModalVehicles.length === 0 && (
|
||
<tr>
|
||
<td colSpan={showPlateNumbers.source === 'customer' ? 14 : (showPlateNumbers.source === 'asset' ? 4 : 5)} className="p-8 text-center text-gray-400 italic">
|
||
暂无符合条件的车辆数据
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="p-4 bg-white border-t border-gray-100 flex justify-between items-center shrink-0">
|
||
<div className="text-xs text-gray-500">
|
||
共计 <span className="font-bold text-blue-600">{filteredModalWeeklyDetail.length > 0 ? filteredModalWeeklyDetail.length : filteredModalVehicles.length}</span> 台车辆
|
||
</div>
|
||
<button
|
||
onClick={() => setShowPlateNumbers(null)}
|
||
className="px-6 py-2 bg-slate-800 text-white rounded-lg text-xs font-bold hover:bg-slate-700 transition-colors shadow-sm"
|
||
>
|
||
确认并关闭
|
||
</button>
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
</div>
|
||
|
||
{/* Footer / Navigation */}
|
||
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 p-2 flex justify-around items-center md:hidden z-40">
|
||
<button
|
||
onClick={() => setActiveTab('overview')}
|
||
className={`flex flex-col items-center ${activeTab === 'overview' ? 'text-blue-600' : 'text-gray-400'}`}
|
||
>
|
||
<Truck size={20} />
|
||
<span className="text-[10px] mt-1">资产总览</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('department')}
|
||
className={`flex flex-col items-center ${activeTab === 'department' ? 'text-blue-600' : 'text-gray-400'}`}
|
||
>
|
||
<Users size={20} />
|
||
<span className="text-[10px] mt-1">运营部门</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('region')}
|
||
className={`flex flex-col items-center ${activeTab === 'region' ? 'text-blue-600' : 'text-gray-400'}`}
|
||
>
|
||
<MapPin size={20} />
|
||
<span className="text-[10px] mt-1">运营区域</span>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('customer')}
|
||
className={`flex flex-col items-center ${activeTab === 'customer' ? 'text-blue-600' : 'text-gray-400'}`}
|
||
>
|
||
<Building2 size={20} />
|
||
<span className="text-[10px] mt-1">运营客户</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|