1703 lines
108 KiB
TypeScript
1703 lines
108 KiB
TypeScript
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||
import {
|
||
Truck,
|
||
ChevronDown,
|
||
ChevronRight,
|
||
Loader2,
|
||
Filter,
|
||
ArrowRightLeft,
|
||
MapPin,
|
||
} from 'lucide-react';
|
||
import { motion, AnimatePresence } from 'motion/react';
|
||
import * as XLSX from 'xlsx';
|
||
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, fetchSubjects, fetchFlowStats, type SubjectOption } from './api';
|
||
import type { FlowDetailItem, FlowStatsResponse, FlowType, WeeklyDetailItem } from './api';
|
||
import {
|
||
buildCustomerPieData,
|
||
buildVehicleModalRequest,
|
||
deriveCustomerView,
|
||
deriveDepartmentView,
|
||
deriveInventoryView,
|
||
deriveModalVehicleView,
|
||
formatLocalDateTime,
|
||
getWeeklyFlowRange,
|
||
selectFlowDetails,
|
||
type VehicleModalSelection,
|
||
} from './model';
|
||
import { SearchSelect } from '../../components/SearchSelect';
|
||
import { MultiSearchSelect } from '../../components/MultiSearchSelect';
|
||
import Blur from '../../components/Blur';
|
||
import RotatingFooterHint from '../../components/RotatingFooterHint';
|
||
import { ErrorState, LoadingState, PageFrame, SkeletonBlock, SurfaceCard } from '../../components/ui/surface';
|
||
import { AssetsHeader } from './components/AssetsHeader';
|
||
import { FlowDetailModal } from './components/FlowDetailModal';
|
||
import { VehicleDetailModal } from './components/VehicleDetailModal';
|
||
import { OverviewView } from './components/OverviewView';
|
||
|
||
|
||
|
||
export default function AssetsModule() {
|
||
const [activeTab, setActiveTab] = useState<'overview' | 'department' | 'region' | 'customer'>('overview');
|
||
const [tabReady, setTabReady] = useState(true);
|
||
const prevTabRef = useRef(activeTab);
|
||
useEffect(() => {
|
||
if (prevTabRef.current !== activeTab) {
|
||
setTabReady(false);
|
||
prevTabRef.current = activeTab;
|
||
const id = requestAnimationFrame(() => { setTabReady(true); });
|
||
return () => cancelAnimationFrame(id);
|
||
}
|
||
}, [activeTab]);
|
||
const [theme, setTheme] = useState<'soft' | 'minimal' | 'vibrant'>('soft');
|
||
|
||
// 所属公司(归属主体)筛选 —— 影响全页聚合
|
||
const [selectedSubject, setSelectedSubject] = useState<string | null>(null);
|
||
const [subjects, setSubjects] = useState<SubjectOption[]>([]);
|
||
const [subjectDropdownOpen, setSubjectDropdownOpen] = useState(false);
|
||
const [subjectSearch, setSubjectSearch] = useState('');
|
||
const subjectDropdownRef = useRef<HTMLDivElement>(null);
|
||
const [expandedModels, setExpandedModels] = useState<Set<string>>(new Set());
|
||
const [expandedAssetTypes, setExpandedAssetTypes] = useState<Set<string>>(new Set());
|
||
const [showPlateNumbers, setShowPlateNumbers] = useState<VehicleModalSelection | 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>(() => formatLocalDateTime(new Date()));
|
||
const [modalLoading, setModalLoading] = useState(false);
|
||
const [flowRange, setFlowRange] = useState(() => getWeeklyFlowRange());
|
||
const [flowStats, setFlowStats] = useState<FlowStatsResponse | null>(null);
|
||
const [flowLoading, setFlowLoading] = useState(false);
|
||
const [flowDailyExpanded, setFlowDailyExpanded] = useState(false);
|
||
const [selectedFlow, setSelectedFlow] = useState<{ date: string; type: FlowType } | null>(null);
|
||
|
||
// 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);
|
||
const [draftRegionFilters, setDraftRegionFilters] = useState({ region: '', city: '', customer: '' });
|
||
|
||
// Customer section state
|
||
const [expandedCustomers, setExpandedCustomers] = useState<Set<string>>(new Set());
|
||
const [customerFilters, setCustomerFilters] = useState({ customer: [] as string[], brand: '', department: '', manager: '', region: '' });
|
||
const [isCustomerFilterOpen, setIsCustomerFilterOpen] = useState(false);
|
||
const [draftCustomerFilters, setDraftCustomerFilters] = useState({ customer: [] as string[], brand: '', department: '', manager: '', region: '' });
|
||
|
||
// 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: '', type: '', model: '' });
|
||
const [isInventoryFilterOpen, setIsInventoryFilterOpen] = useState(false);
|
||
const [draftInventoryFilters, setDraftInventoryFilters] = useState({ region: '', city: '', brand: '', type: '', model: '' });
|
||
|
||
// Chart view states
|
||
const [customerChartView, setCustomerChartView] = useState<'region' | 'province'>('region');
|
||
const [regionChartView, setRegionChartView] = useState<'region' | 'province'>('region');
|
||
const [regionChartData, setRegionChartData] = useState<{ name: string; value: number }[]>([]);
|
||
|
||
// Modal filter state
|
||
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(selectedSubject),
|
||
fetchByType(selectedSubject),
|
||
fetchDeptStats(selectedSubject),
|
||
fetchRegionStats(undefined, selectedSubject),
|
||
fetchCustomerStats(selectedSubject),
|
||
fetchInventoryStats(selectedSubject),
|
||
]);
|
||
setSummary(s);
|
||
setProcessedData(byType);
|
||
setDeptData(dept);
|
||
setRegionData(region);
|
||
setCustomerData(cust);
|
||
setInventoryData(inv);
|
||
setLastUpdate(formatLocalDateTime(new Date()));
|
||
} catch (e) {
|
||
setError(e instanceof Error ? e.message : '数据加载失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [selectedSubject]);
|
||
|
||
useEffect(() => {
|
||
loadData();
|
||
const interval = setInterval(loadData, 60 * 1000);
|
||
return () => clearInterval(interval);
|
||
}, [loadData]);
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
setFlowLoading(true);
|
||
fetchFlowStats({ start: flowRange.start, end: flowRange.end, subject: selectedSubject })
|
||
.then((data) => {
|
||
if (!cancelled) setFlowStats(data);
|
||
})
|
||
.catch(() => {
|
||
if (!cancelled) setFlowStats(null);
|
||
})
|
||
.finally(() => {
|
||
if (!cancelled) setFlowLoading(false);
|
||
});
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [flowRange.start, flowRange.end, selectedSubject]);
|
||
|
||
// 归属公司列表(仅首次加载,公司集合相对稳定)
|
||
useEffect(() => {
|
||
fetchSubjects().then(setSubjects).catch(() => setSubjects([]));
|
||
}, []);
|
||
|
||
// 点击外部关闭归属公司下拉
|
||
useEffect(() => {
|
||
if (!subjectDropdownOpen) return;
|
||
const handler = (e: MouseEvent) => {
|
||
if (subjectDropdownRef.current && !subjectDropdownRef.current.contains(e.target as Node)) {
|
||
setSubjectDropdownOpen(false);
|
||
}
|
||
};
|
||
document.addEventListener('mousedown', handler);
|
||
return () => document.removeEventListener('mousedown', handler);
|
||
}, [subjectDropdownOpen]);
|
||
|
||
// Re-fetch region data when filters change
|
||
useEffect(() => {
|
||
const hasFilter = regionFilters.customer || regionFilters.city || regionFilters.region;
|
||
if (hasFilter) {
|
||
fetchRegionStats(
|
||
{ customer: regionFilters.customer || undefined, city: regionFilters.city || undefined, region: regionFilters.region || undefined },
|
||
selectedSubject,
|
||
).then(setRegionData).catch(() => {});
|
||
} else {
|
||
// No filters: use data from the main loadData cycle
|
||
fetchRegionStats(undefined, selectedSubject).then(setRegionData).catch(() => {});
|
||
}
|
||
}, [regionFilters, selectedSubject]);
|
||
|
||
// Fetch region chart data when view changes
|
||
useEffect(() => {
|
||
fetchRegionChart(regionChartView, regionChartView === 'province' ? 5 : 8, 'realtime', selectedSubject)
|
||
.then(setRegionChartData)
|
||
.catch(() => setRegionChartData([]));
|
||
}, [regionChartView, selectedSubject]);
|
||
|
||
// Load modal vehicles
|
||
useEffect(() => {
|
||
if (!showPlateNumbers) {
|
||
setModalVehicles([]);
|
||
setModalWeeklyDetail([]);
|
||
return;
|
||
}
|
||
setModalLoading(true);
|
||
const request = buildVehicleModalRequest(showPlateNumbers, selectedSubject);
|
||
if (request.kind === 'weekly') {
|
||
setModalVehicles([]);
|
||
fetchWeeklyDetail(request.type, request.filters)
|
||
.then(setModalWeeklyDetail)
|
||
.catch(() => setModalWeeklyDetail([]))
|
||
.finally(() => setModalLoading(false));
|
||
return;
|
||
}
|
||
|
||
setModalWeeklyDetail([]);
|
||
fetchVehicleList(request.params)
|
||
.then(setModalVehicles)
|
||
.catch(() => setModalVehicles([]))
|
||
.finally(() => setModalLoading(false));
|
||
}, [showPlateNumbers, selectedSubject]);
|
||
|
||
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);
|
||
};
|
||
|
||
const inventoryTypeFilter = isInventoryFilterOpen
|
||
? draftInventoryFilters.type
|
||
: inventoryFilters.type;
|
||
const {
|
||
filtered: filteredInventoryStats,
|
||
brands: uniqueInventoryBrands,
|
||
regions: uniqueInventoryRegions,
|
||
cities: uniqueInventoryCities,
|
||
types: uniqueInventoryTypes,
|
||
modelsForType: uniqueInventoryModelsForType,
|
||
byRegion: inventoryByRegion,
|
||
byModel: inventoryByModel,
|
||
} = useMemo(
|
||
() => deriveInventoryView(inventoryData, inventoryFilters, inventoryTypeFilter),
|
||
[inventoryData, inventoryFilters, inventoryTypeFilter],
|
||
);
|
||
|
||
const {
|
||
managers: allManagersList,
|
||
groupedManagers: managersGroupedByDept,
|
||
managerStats,
|
||
} = useMemo(
|
||
() => deriveDepartmentView(deptData, selectedManager),
|
||
[deptData, selectedManager],
|
||
);
|
||
|
||
const {
|
||
filtered: filteredCustomerStats,
|
||
brands: uniqueBrands,
|
||
departments: uniqueDepts,
|
||
regions: uniqueRegions,
|
||
cities: uniqueCities,
|
||
customerNames: uniqueCustomerNames,
|
||
managersByDepartment: customerManagersGroupedByDept,
|
||
} = useMemo(
|
||
() => deriveCustomerView(customerData, deptData, customerFilters),
|
||
[customerData, deptData, customerFilters],
|
||
);
|
||
|
||
const {
|
||
plates: uniqueModalPlates,
|
||
models: uniqueModalModels,
|
||
brands: uniqueModalBrands,
|
||
locations: uniqueModalLocations,
|
||
filteredVehicles: filteredModalVehicles,
|
||
filteredWeeklyDetails: filteredModalWeeklyDetail,
|
||
} = useMemo(
|
||
() => deriveModalVehicleView(modalVehicles, modalWeeklyDetail, modalFilters),
|
||
[modalVehicles, modalWeeklyDetail, modalFilters],
|
||
);
|
||
|
||
const selectedFlowDetails = useMemo(
|
||
() => selectFlowDetails(flowStats, selectedFlow),
|
||
[flowStats, selectedFlow],
|
||
);
|
||
|
||
const exportFlowDetails = useCallback((rows?: FlowDetailItem[], title = '资产流转明细') => {
|
||
const source = rows ?? flowStats?.details ?? [];
|
||
if (source.length === 0) return;
|
||
const table = source.map((item) => ({
|
||
日期: item.date,
|
||
类型: item.typeLabel,
|
||
车牌: item.plateNumber,
|
||
流转时间: item.eventTime || '',
|
||
提交时间: item.submitTime || '',
|
||
部门: item.department || '',
|
||
业务负责人: item.manager || '',
|
||
客户: item.customerName || '',
|
||
}));
|
||
const ws = XLSX.utils.json_to_sheet(table);
|
||
ws['!cols'] = [
|
||
{ wch: 14 },
|
||
{ wch: 8 },
|
||
{ wch: 14 },
|
||
{ wch: 20 },
|
||
{ wch: 20 },
|
||
{ wch: 16 },
|
||
{ wch: 14 },
|
||
{ wch: 24 },
|
||
];
|
||
const wb = XLSX.utils.book_new();
|
||
XLSX.utils.book_append_sheet(wb, ws, '明细');
|
||
XLSX.writeFile(wb, `${title}-${flowRange.start}-${flowRange.end}.xlsx`);
|
||
}, [flowRange.end, flowRange.start, flowStats]);
|
||
|
||
const [customerProvinceData, setCustomerProvinceData] = useState<{ name: string; value: number }[]>([]);
|
||
useEffect(() => {
|
||
if (customerChartView === 'province') {
|
||
fetchRegionChart('province', 5, 'vehicle', selectedSubject).then(setCustomerProvinceData).catch(() => setCustomerProvinceData([]));
|
||
}
|
||
}, [customerChartView, selectedSubject]);
|
||
|
||
const customerPieData = useMemo(
|
||
() => buildCustomerPieData(customerData, customerChartView, customerProvinceData),
|
||
[customerData, customerChartView, customerProvinceData],
|
||
);
|
||
|
||
|
||
if (loading && !summary) {
|
||
return (
|
||
<PageFrame
|
||
title="车辆资产中心"
|
||
subtitle="正在同步车辆、部门、区域与客户归属数据,加载完成后可继续穿透查看明细。"
|
||
icon={Truck}
|
||
eyebrow="ASSET INTELLIGENCE"
|
||
meta="数据准备中"
|
||
>
|
||
<SurfaceCard className="min-h-[360px]">
|
||
<div className="grid gap-4 md:grid-cols-4">
|
||
{[0, 1, 2, 3].map(item => (
|
||
<SkeletonBlock key={item} className="h-28" />
|
||
))}
|
||
</div>
|
||
<div className="mt-8">
|
||
<LoadingState label="正在加载车辆资产数据" />
|
||
</div>
|
||
</SurfaceCard>
|
||
</PageFrame>
|
||
);
|
||
}
|
||
|
||
if (error && !summary) {
|
||
return (
|
||
<PageFrame
|
||
title="车辆资产中心"
|
||
subtitle="车辆资产数据暂时没有返回,请重试或稍后再看。"
|
||
icon={Truck}
|
||
eyebrow="ASSET INTELLIGENCE"
|
||
meta="加载失败"
|
||
>
|
||
<SurfaceCard className="min-h-[360px]">
|
||
<div className="mt-6 flex flex-col items-center gap-4">
|
||
<ErrorState message={error} />
|
||
<button onClick={loadData} className="rounded-xl bg-slate-900 px-4 py-2 text-xs font-black text-white shadow-sm transition-colors hover:bg-slate-800">
|
||
重试加载
|
||
</button>
|
||
</div>
|
||
</SurfaceCard>
|
||
</PageFrame>
|
||
);
|
||
}
|
||
|
||
const SUMMARY = summary!;
|
||
const operatingRate = SUMMARY.totalAssets > 0 ? SUMMARY.operating.total / SUMMARY.totalAssets * 100 : 0;
|
||
const inventoryRate = SUMMARY.totalAssets > 0 ? SUMMARY.inventory.total / SUMMARY.totalAssets * 100 : 0;
|
||
const pendingRate = SUMMARY.totalAssets > 0 ? SUMMARY.pendingDelivery / SUMMARY.totalAssets * 100 : 0;
|
||
|
||
return (
|
||
<div className="min-h-screen bg-[var(--app-bg)] text-gray-800 font-sans p-3 md:p-6 relative">
|
||
<AssetsHeader
|
||
activeTab={activeTab}
|
||
setActiveTab={setActiveTab}
|
||
theme={theme}
|
||
setTheme={setTheme}
|
||
lastUpdate={lastUpdate}
|
||
loading={loading}
|
||
selectedSubject={selectedSubject}
|
||
setSelectedSubject={setSelectedSubject}
|
||
subjects={subjects}
|
||
subjectDropdownOpen={subjectDropdownOpen}
|
||
setSubjectDropdownOpen={setSubjectDropdownOpen}
|
||
subjectSearch={subjectSearch}
|
||
setSubjectSearch={setSubjectSearch}
|
||
subjectDropdownRef={subjectDropdownRef}
|
||
/>
|
||
|
||
{/* Main Content Area */}
|
||
<div className="flex flex-col gap-6">
|
||
|
||
{!tabReady && (
|
||
<div className="flex items-center justify-center py-20">
|
||
<Loader2 className="animate-spin text-blue-500" size={28} />
|
||
</div>
|
||
)}
|
||
|
||
{tabReady && activeTab === 'overview' && (
|
||
<OverviewView
|
||
summary={SUMMARY}
|
||
operatingRate={operatingRate}
|
||
inventoryRate={inventoryRate}
|
||
pendingRate={pendingRate}
|
||
setVehicleSelection={setShowPlateNumbers}
|
||
flowRange={flowRange}
|
||
setFlowRange={setFlowRange}
|
||
flowLoading={flowLoading}
|
||
flowStats={flowStats}
|
||
exportFlowDetails={exportFlowDetails}
|
||
flowDailyExpanded={flowDailyExpanded}
|
||
setFlowDailyExpanded={setFlowDailyExpanded}
|
||
setSelectedFlow={setSelectedFlow}
|
||
processedData={processedData}
|
||
theme={theme}
|
||
allTypesExpanded={allTypesExpanded}
|
||
toggleAllAssetTypes={toggleAllAssetTypes}
|
||
expandedAssetTypes={expandedAssetTypes}
|
||
toggleAssetType={toggleAssetType}
|
||
expandedModels={expandedModels}
|
||
toggleModel={toggleModel}
|
||
inventoryFilters={inventoryFilters}
|
||
setInventoryFilters={setInventoryFilters}
|
||
draftInventoryFilters={draftInventoryFilters}
|
||
setDraftInventoryFilters={setDraftInventoryFilters}
|
||
isInventoryFilterOpen={isInventoryFilterOpen}
|
||
setIsInventoryFilterOpen={setIsInventoryFilterOpen}
|
||
uniqueInventoryRegions={uniqueInventoryRegions}
|
||
uniqueInventoryCities={uniqueInventoryCities}
|
||
uniqueInventoryBrands={uniqueInventoryBrands}
|
||
uniqueInventoryTypes={uniqueInventoryTypes}
|
||
uniqueInventoryModelsForType={uniqueInventoryModelsForType}
|
||
filteredInventoryStats={filteredInventoryStats}
|
||
inventoryTab={inventoryTab}
|
||
setInventoryTab={setInventoryTab}
|
||
inventoryByRegion={inventoryByRegion}
|
||
expandedInventoryRegions={expandedInventoryRegions}
|
||
toggleInventoryRegion={toggleInventoryRegion}
|
||
inventoryByModel={inventoryByModel}
|
||
expandedInventoryTypes={expandedInventoryTypes}
|
||
toggleInventoryType={toggleInventoryType}
|
||
/>
|
||
)}
|
||
|
||
{tabReady && activeTab === 'department' && (
|
||
<section className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden mb-6">
|
||
<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 cursor-pointer hover:opacity-80 transition-opacity"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Operating', source: 'department', title: '部门运营统计' })}>
|
||
<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 cursor-pointer hover:opacity-80 transition-opacity"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', attendance: 'active', source: 'department', title: '部门运营统计 - 出勤车辆' })}>
|
||
<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 cursor-pointer hover:opacity-80 transition-opacity"
|
||
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', attendance: 'idle', source: 'department', title: '部门运营统计 - 闲置车辆' })}>
|
||
<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>
|
||
|
||
<div className="px-2 mb-1">
|
||
<span className="text-[10px] text-gray-400 font-medium">*说明:当天里程>0即为出勤。</span>
|
||
</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 pointer-events-none" size={14} />
|
||
<select
|
||
value={selectedManager}
|
||
onChange={(e) => setSelectedManager(e.target.value)}
|
||
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 appearance-none cursor-pointer font-bold text-gray-700"
|
||
>
|
||
<option value="All">所有业务负责人</option>
|
||
{managersGroupedByDept.filter(g => g.department !== '公务车').map(g => (
|
||
<optgroup key={g.department} label={g.department}>
|
||
{g.managers.map(m => <option key={m} value={m}>{m}</option>)}
|
||
</optgroup>
|
||
))}
|
||
</select>
|
||
<ChevronDown className="absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none" size={14} />
|
||
</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-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">
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, category: 'Operating', source: 'department', title: `部门运营统计 - ${dept.department}` });
|
||
}}
|
||
className="text-gray-800 hover:underline font-black"
|
||
>
|
||
{dept.totalAssets}
|
||
</button>
|
||
</td>
|
||
<td className="p-2 border-r border-gray-100 text-center font-black text-green-500 text-sm">
|
||
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, attendance: 'active', source: 'department', title: `部门运营统计 - ${dept.department} - 出勤车辆` }); }} className="text-green-500 hover:underline font-black">{dept.operatingCount}</button>
|
||
</td>
|
||
<td className="p-2 border-r border-gray-100 text-center font-black text-gray-400 text-sm">
|
||
<button onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, attendance: 'idle', source: 'department', title: `部门运营统计 - ${dept.department} - 闲置车辆` }); }} className="text-gray-400 hover:underline font-black">{dept.idleCount}</button>
|
||
</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={6} 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"><Blur>{m.manager}</Blur></span>
|
||
</div>
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department', title: `部门运营统计 - ${m.manager} - 正在运营` });
|
||
}}
|
||
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', title: `部门运营统计 - ${m.manager} - 4.5T` })}
|
||
>
|
||
<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', title: `部门运营统计 - ${m.manager} - 4.5T冷链` })}
|
||
>
|
||
<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', title: `部门运营统计 - ${m.manager} - 18T` })}
|
||
>
|
||
<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', title: `部门运营统计 - ${m.manager} - 49T` })}
|
||
>
|
||
<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', title: `部门运营统计 - ${m.manager} - 挂车` })}
|
||
>
|
||
<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', title: `部门运营统计 - ${m.manager} - 其他` })}
|
||
>
|
||
<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" />}
|
||
<Blur>{m.manager}</Blur>
|
||
</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', title: `部门运营统计 - ${m.manager} - 正在运营` });
|
||
}}
|
||
>
|
||
{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', title: `部门运营统计 - ${m.manager} - 正在运营` });
|
||
}}
|
||
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', title: `部门运营统计 - ${m.manager} - 4.5T` })}>
|
||
<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', title: `部门运营统计 - ${m.manager} - 4.5T冷链` })}>
|
||
<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', title: `部门运营统计 - ${m.manager} - 18T` })}>
|
||
<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', title: `部门运营统计 - ${m.manager} - 49T` })}>
|
||
<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', title: `部门运营统计 - ${m.manager} - 挂车` })}>
|
||
<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', title: `部门运营统计 - ${m.manager} - 其他` })}>
|
||
<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-3 gap-2">
|
||
<div className="text-center cursor-pointer hover:bg-gray-50 rounded p-1"
|
||
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, category: 'Operating', source: 'department', title: `部门运营统计 - ${dept.department}` }); }}>
|
||
<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 cursor-pointer hover:bg-green-50 rounded p-1"
|
||
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, attendance: 'active', source: 'department', title: `部门运营统计 - ${dept.department} - 出勤车辆` }); }}>
|
||
<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 cursor-pointer hover:bg-gray-100 rounded p-1"
|
||
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', department: dept.department, attendance: 'idle', source: 'department', title: `部门运营统计 - ${dept.department} - 闲置车辆` }); }}>
|
||
<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"><Blur>{m.manager}</Blur></span>
|
||
</div>
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department', title: `部门运营统计 - ${m.manager} - 正在运营` });
|
||
}}
|
||
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', title: `部门运营统计 - ${m.manager} - 4.5T` })}
|
||
>
|
||
<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', title: `部门运营统计 - ${m.manager} - 4.5T冷链` })}
|
||
>
|
||
<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', title: `部门运营统计 - ${m.manager} - 18T` })}
|
||
>
|
||
<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', title: `部门运营统计 - ${m.manager} - 49T` })}
|
||
>
|
||
<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', title: `部门运营统计 - ${m.manager} - 挂车` })}
|
||
>
|
||
<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', title: `部门运营统计 - ${m.manager} - 其他` })}
|
||
>
|
||
<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"><Blur>{m.manager}</Blur></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', title: `部门运营统计 - ${m.manager} - 正在运营` });
|
||
}}
|
||
>
|
||
资产: {m.total}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', manager: m.manager, category: 'Operating', source: 'department', title: `部门运营统计 - ${m.manager} - 正在运营` });
|
||
}}
|
||
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', title: `部门运营统计 - ${m.manager} - 4.5T` })}
|
||
>
|
||
<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', title: `部门运营统计 - ${m.manager} - 4.5T冷链` })}
|
||
>
|
||
<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', title: `部门运营统计 - ${m.manager} - 18T` })}
|
||
>
|
||
<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', title: `部门运营统计 - ${m.manager} - 49T` })}
|
||
>
|
||
<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', title: `部门运营统计 - ${m.manager} - 挂车` })}
|
||
>
|
||
<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', title: `部门运营统计 - ${m.manager} - 其他` })}
|
||
>
|
||
<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>
|
||
)}
|
||
|
||
{tabReady && 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('province')}
|
||
className={`px-3 py-1 text-xs font-medium rounded-md transition-all ${regionChartView === 'province' ? 'bg-white text-blue-700 shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
|
||
>按省份</button>
|
||
</div>
|
||
</div>
|
||
<div className="h-64 w-full">
|
||
<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 === 'province' ? 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 min-h-[420px]">
|
||
<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={() => { if (!isRegionFilterOpen) setDraftRegionFilters({...regionFilters}); 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" />
|
||
<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="fixed inset-x-4 top-20 max-h-[80vh] overflow-auto sm:inset-auto sm:top-20 sm:right-4 sm: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={() => setDraftRegionFilters({ 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>
|
||
<SearchSelect value={draftRegionFilters.customer} onChange={(v) => setDraftRegionFilters(prev => ({ ...prev, customer: v }))} options={uniqueCustomerNames} placeholder="所有客户" className="text-xs py-2 px-2" />
|
||
</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>
|
||
<select className="w-full bg-white 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 cursor-pointer shadow-sm" value={draftRegionFilters.region} onChange={(e) => setDraftRegionFilters(prev => ({ ...prev, region: e.target.value }))}>
|
||
<option value="">所有区域</option>
|
||
{uniqueRegions.map(r => <option key={r} value={r}>{r}</option>)}
|
||
</select>
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">城市</label>
|
||
<select className="w-full bg-white 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 cursor-pointer shadow-sm" value={draftRegionFilters.city} onChange={(e) => setDraftRegionFilters(prev => ({ ...prev, city: e.target.value }))}>
|
||
<option value="">所有城市</option>
|
||
{uniqueCities.map(c => <option key={c} value={c}>{c}</option>)}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<button onClick={() => { setRegionFilters({...draftRegionFilters}); setIsRegionFilterOpen(false); }} className="w-full mt-4 py-2 bg-slate-800 text-white rounded-lg text-xs font-bold hover:bg-slate-900 transition-colors">确认</button>
|
||
</motion.div>
|
||
</>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
</div>
|
||
|
||
{Object.values(regionFilters).some(v => v !== '') && (
|
||
<div className="px-3 py-2 border-b border-gray-100 flex flex-wrap gap-2 items-center">
|
||
{regionFilters.customer && (
|
||
<span className="px-2 py-0.5 bg-slate-100 text-slate-700 rounded text-[10px] flex items-center gap-1">
|
||
客户: {regionFilters.customer}
|
||
<button onClick={() => setRegionFilters(prev => ({...prev, customer: ''}))} className="hover:text-red-500 ml-0.5">×</button>
|
||
</span>
|
||
)}
|
||
{regionFilters.region && (
|
||
<span className="px-2 py-0.5 bg-slate-100 text-slate-700 rounded text-[10px] flex items-center gap-1">
|
||
区域: {regionFilters.region}
|
||
<button onClick={() => setRegionFilters(prev => ({...prev, region: ''}))} className="hover:text-red-500 ml-0.5">×</button>
|
||
</span>
|
||
)}
|
||
{regionFilters.city && (
|
||
<span className="px-2 py-0.5 bg-slate-100 text-slate-700 rounded text-[10px] flex items-center gap-1">
|
||
城市: {regionFilters.city}
|
||
<button onClick={() => setRegionFilters(prev => ({...prev, city: ''}))} className="hover:text-red-500 ml-0.5">×</button>
|
||
</span>
|
||
)}
|
||
<button onClick={() => setRegionFilters({ region: '', city: '', customer: '' })} className="text-[11px] text-red-500 font-bold ml-auto hover:text-red-600">清除</button>
|
||
</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.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, source: 'region', title: `区域运营统计 - ${r.region}` }); }}>{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', source: 'region', title: `区域运营统计 - ${r.region} - 正在运营` }); }}>{r.operatingCount}</td>
|
||
<td className="p-2 text-center text-orange-600 font-bold cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, category: 'Pending', source: 'region', title: `区域运营统计 - ${r.region} - 待交车` }); }}>{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, source: 'region', title: `区域运营统计 - ${city.city}` }); }}>{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', source: 'region', title: `区域运营统计 - ${city.city} - 正在运营` }); }}>{city.operatingCount}</td>
|
||
<td className="p-2 text-center text-orange-600 cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, category: 'Pending', source: 'region', title: `区域运营统计 - ${city.city} - 待交车` }); }}>{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, source: 'region', title: `区域运营统计 - ${city.city} - ${tb.type}` })}>{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', source: 'region', title: `区域运营统计 - ${city.city} - ${tb.type} - 正在运营` })}>{tb.operating}</td>
|
||
<td className="p-2 text-center text-orange-500 cursor-pointer hover:underline" onClick={() => { setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, vehicleType: tb.type, category: 'Pending', source: 'region', title: `区域运营统计 - ${city.city} - ${tb.type} - 待交车` }); }}>{tb.pending}</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.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: 'region', title: `区域运营统计 - ${r.region} - 正在运营` })}
|
||
>
|
||
<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: 'region', title: `区域运营统计 - ${r.region} - 待交车` })}
|
||
>
|
||
<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: 'region', title: `区域运营统计 - ${r.region} - ${tb.type} - 正在运营` })}
|
||
>
|
||
运:{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: 'region', title: `区域运营统计 - ${r.region} - ${tb.type} - 待交车` })}
|
||
>
|
||
待:{tb.pending}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
)}
|
||
|
||
{tabReady && activeTab === 'customer' && (
|
||
<div className="flex flex-col gap-6">
|
||
|
||
{/* Customer Operations Statistics Section */}
|
||
<section className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden mb-6 min-h-[420px]">
|
||
<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={() => { if (!isCustomerFilterOpen) setDraftCustomerFilters({...customerFilters}); 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 => Array.isArray(v) ? v.length > 0 : 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 => Array.isArray(v) ? v.length > 0 : v !== '') && (
|
||
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></span>
|
||
)}
|
||
</button>
|
||
|
||
<AnimatePresence>
|
||
{isCustomerFilterOpen && (
|
||
<>
|
||
{/* Backdrop */}
|
||
<div className="fixed inset-0 z-40" />
|
||
|
||
{/* 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="fixed inset-x-4 top-20 max-h-[80vh] overflow-auto sm:inset-auto sm:top-20 sm:right-4 sm: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>
|
||
<div className="flex items-center gap-2">
|
||
<button onClick={() => setDraftCustomerFilters({ customer: [], brand: '', department: '', manager: '', region: '' })} className="text-[10px] text-emerald-600 hover:text-emerald-700 font-medium">重置所有</button>
|
||
</div>
|
||
</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>
|
||
<MultiSearchSelect value={draftCustomerFilters.customer} onChange={(v) => setDraftCustomerFilters(prev => ({ ...prev, customer: v }))} options={uniqueCustomerNames} placeholder="所有客户" className="text-xs py-2 px-2" />
|
||
</div>
|
||
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">业务负责人</label>
|
||
<select className="w-full bg-white 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 cursor-pointer shadow-sm" value={draftCustomerFilters.manager} onChange={(e) => setDraftCustomerFilters(prev => ({ ...prev, manager: e.target.value }))}>
|
||
<option value="">所有负责人</option>
|
||
{customerManagersGroupedByDept
|
||
.filter(g => g.department !== '公务车')
|
||
.filter(g => !draftCustomerFilters.department || g.department === draftCustomerFilters.department)
|
||
.map(g => (
|
||
<optgroup key={g.department} label={g.department}>
|
||
{g.managers.map(m => <option key={m} value={m}>{m}</option>)}
|
||
</optgroup>
|
||
))}
|
||
</select>
|
||
</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>
|
||
<select className="w-full bg-white 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 cursor-pointer shadow-sm" value={draftCustomerFilters.brand} onChange={(e) => setDraftCustomerFilters(prev => ({ ...prev, brand: e.target.value }))}>
|
||
<option value="">所有品牌</option>
|
||
{uniqueBrands.map(b => <option key={b} value={b}>{b}</option>)}
|
||
</select>
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">部门</label>
|
||
<select className="w-full bg-white 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 cursor-pointer shadow-sm" value={draftCustomerFilters.department} onChange={(e) => setDraftCustomerFilters(prev => ({ ...prev, department: e.target.value, manager: '' }))}>
|
||
<option value="">所有部门</option>
|
||
{uniqueDepts.map(d => <option key={d} value={d}>{d}</option>)}
|
||
</select>
|
||
</div>
|
||
<div className="space-y-1.5">
|
||
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">区域</label>
|
||
<select className="w-full bg-white 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 cursor-pointer shadow-sm" value={draftCustomerFilters.region} onChange={(e) => setDraftCustomerFilters(prev => ({ ...prev, region: e.target.value }))}>
|
||
<option value="">所有区域</option>
|
||
{uniqueRegions.map(r => <option key={r} value={r}>{r}</option>)}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<button onClick={() => { setCustomerFilters({...draftCustomerFilters}); setIsCustomerFilterOpen(false); }} className="w-full mt-4 py-2 bg-emerald-600 text-white rounded-lg text-xs font-bold hover:bg-emerald-700 transition-colors">确认</button>
|
||
</motion.div>
|
||
</>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
</div>
|
||
|
||
{Object.values(customerFilters).some(v => Array.isArray(v) ? v.length > 0 : v !== '') && (
|
||
<div className="px-3 py-2 border-b border-gray-100 flex flex-wrap gap-2 items-center">
|
||
{customerFilters.customer.length > 0 && (
|
||
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-700 rounded text-[10px] flex items-center gap-1">
|
||
客户: {customerFilters.customer.join(', ')}
|
||
<button onClick={() => setCustomerFilters(prev => ({...prev, customer: []}))} className="hover:text-red-500 ml-0.5">×</button>
|
||
</span>
|
||
)}
|
||
{customerFilters.manager && (
|
||
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-700 rounded text-[10px] flex items-center gap-1">
|
||
负责人: {customerFilters.manager}
|
||
<button onClick={() => setCustomerFilters(prev => ({...prev, manager: ''}))} className="hover:text-red-500 ml-0.5">×</button>
|
||
</span>
|
||
)}
|
||
{customerFilters.brand && (
|
||
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-700 rounded text-[10px] flex items-center gap-1">
|
||
品牌: {customerFilters.brand}
|
||
<button onClick={() => setCustomerFilters(prev => ({...prev, brand: ''}))} className="hover:text-red-500 ml-0.5">×</button>
|
||
</span>
|
||
)}
|
||
{customerFilters.department && (
|
||
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-700 rounded text-[10px] flex items-center gap-1">
|
||
部门: {customerFilters.department}
|
||
<button onClick={() => setCustomerFilters(prev => ({...prev, department: ''}))} className="hover:text-red-500 ml-0.5">×</button>
|
||
</span>
|
||
)}
|
||
{customerFilters.region && (
|
||
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-700 rounded text-[10px] flex items-center gap-1">
|
||
区域: {customerFilters.region}
|
||
<button onClick={() => setCustomerFilters(prev => ({...prev, region: ''}))} className="hover:text-red-500 ml-0.5">×</button>
|
||
</span>
|
||
)}
|
||
<button onClick={() => setCustomerFilters({ customer: [], brand: '', department: '', manager: '', region: '' })} className="text-[11px] text-red-500 font-bold ml-auto hover:text-red-600">清除</button>
|
||
</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" />}
|
||
<Blur>{cust.customer}</Blur>
|
||
</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"><Blur>{cust.manager}</Blur></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', isColdChain: false, source: 'customer', title: `客户运营统计 - ${cust.customer} - 4.5T` }); }}>{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', isColdChain: true, source: 'customer', title: `客户运营统计 - ${cust.customer} - 4.5T冷链` }); }}>{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', title: `客户运营统计 - ${cust.customer} - 18T` }); }}>{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', title: `客户运营统计 - ${cust.customer} - 49T` }); }}>{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', title: `客户运营统计 - ${cust.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', title: `客户运营统计 - ${cust.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', title: `客户运营统计 - ${cust.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"><Blur>{cust.customer}</Blur></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"><Blur>{cust.manager}</Blur></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"><Blur>{cust.customer}</Blur></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 cursor-pointer hover:bg-emerald-200 transition-colors"
|
||
onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', customer: cust.customer, source: 'customer', title: `客户运营统计 - ${cust.customer}` }); }}
|
||
>
|
||
合计: {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"><Blur>{cust.customer}</Blur></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"><Blur>{cust.manager}</Blur></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', isColdChain: false, source: 'customer', title: `客户运营统计 - ${cust.customer} - 4.5T` })}
|
||
>
|
||
<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', isColdChain: true, source: 'customer', title: `客户运营统计 - ${cust.customer} - 4.5T冷链` })}
|
||
>
|
||
<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', title: `客户运营统计 - ${cust.customer} - 18T` })}
|
||
>
|
||
<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', title: `客户运营统计 - ${cust.customer} - 49T` })}
|
||
>
|
||
<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', title: `客户运营统计 - ${cust.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', title: `客户运营统计 - ${cust.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>
|
||
)}
|
||
|
||
|
||
<FlowDetailModal
|
||
selectedFlow={selectedFlow}
|
||
selectedFlowDetails={selectedFlowDetails}
|
||
setSelectedFlow={setSelectedFlow}
|
||
exportFlowDetails={exportFlowDetails}
|
||
/>
|
||
|
||
<VehicleDetailModal
|
||
selection={showPlateNumbers}
|
||
setSelection={setShowPlateNumbers}
|
||
isFilterExpanded={isModalFilterExpanded}
|
||
setIsFilterExpanded={setIsModalFilterExpanded}
|
||
filters={modalFilters}
|
||
setFilters={setModalFilters}
|
||
plates={uniqueModalPlates}
|
||
models={uniqueModalModels}
|
||
brands={uniqueModalBrands}
|
||
locations={uniqueModalLocations}
|
||
loading={modalLoading}
|
||
weeklyDetails={modalWeeklyDetail}
|
||
filteredWeeklyDetails={filteredModalWeeklyDetail}
|
||
filteredVehicles={filteredModalVehicles}
|
||
/>
|
||
|
||
</div>
|
||
<RotatingFooterHint className="pb-4" />
|
||
</div>
|
||
);
|
||
}
|