feat: improve asset inventory statistics and responsive UX (v1.1.16)

This commit is contained in:
shishengliang
2026-09-23 11:07:36 +08:00
parent 9b0980e349
commit b0cba26fbb
30 changed files with 626 additions and 498 deletions
@@ -13,7 +13,7 @@ export function AssetSummaryDesktopTable({
setVehicleSelection: setShowPlateNumbers,
}: AssetSummarySectionProps) {
return (
<div className="hidden lg:block overflow-x-auto">
<div className="hidden lg:block asset-table-scroll overflow-x-auto">
<table className="w-full text-left border-collapse table-fixed min-w-[1300px]">
<thead>
<tr className="bg-gray-50 text-[11px] text-gray-500 uppercase tracking-wider border-b border-gray-100">
@@ -26,8 +26,8 @@ export function AssetSummaryDesktopTable({
<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 text-amber-600 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 text-amber-600 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>
@@ -43,7 +43,7 @@ export function AssetSummaryDesktopTable({
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)}>
tabIndex={0} onKeyDown={e=>{if(e.target===e.currentTarget && (e.key==='Enter'||e.key===' ')){e.preventDefault();toggleAssetType(typeGroup.type);}}} onClick={() => toggleAssetType(typeGroup.type)}>
<td colSpan={15} className={`p-3 font-bold ${theme === 'vibrant' ? 'text-white' : 'text-blue-700'}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -56,7 +56,7 @@ export function AssetSummaryDesktopTable({
<div className={`flex gap-6 text-[11px] font-normal mr-4 ${theme === 'vibrant' ? 'text-white/80' : 'text-gray-500'}`}>
<span>资产 <span className={theme === 'vibrant' ? 'font-bold text-white' : 'font-bold text-gray-700'}>{typeGroup.totalAssets}</span></span>
<span>库存 <span className={theme === 'vibrant' ? 'font-bold text-white' : 'font-bold text-blue-600'}>{typeGroup.totalInventory}</span></span>
<span>异动 <span className="font-bold">{typeGroup.totalAbnormal}</span></span>
<span>其中异动 <span className="font-bold">{typeGroup.totalAbnormal}</span></span>
<span>运营 <span className={theme === 'vibrant' ? 'font-bold text-white' : 'font-bold text-green-600'}>{typeGroup.totalOperating}</span></span>
</div>
</div>
@@ -70,7 +70,7 @@ export function AssetSummaryDesktopTable({
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)}
tabIndex={0} onKeyDown={e=>{if(e.target===e.currentTarget && (e.key==='Enter'||e.key===' ')){e.preventDefault();toggleModel(model.model);}}} 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 justify-between gap-2">
@@ -78,7 +78,7 @@ export function AssetSummaryDesktopTable({
{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>
</div>
<div className="flex gap-3 text-[9px] font-normal text-gray-400">
<div className="flex gap-3 text-[11px] font-normal text-gray-400">
<span>资产 <span className="font-bold text-gray-600">{model.total}</span></span>
<span>库存 <span className="font-bold text-blue-500">{model.inventory}</span></span>
<span>运营 <span className="font-bold text-green-500">{model.operating}</span></span>
@@ -21,7 +21,7 @@ export function AssetSummaryMobileCards({
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)}
tabIndex={0} onKeyDown={e=>{if(e.target===e.currentTarget && (e.key==='Enter'||e.key===' ')){e.preventDefault();toggleAssetType(typeGroup.type);}}} onClick={() => toggleAssetType(typeGroup.type)}
>
<div className="flex items-center gap-2">
{expandedAssetTypes.has(typeGroup.type) ?
@@ -30,10 +30,10 @@ export function AssetSummaryMobileCards({
}
<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'}`}>
<div className={`flex gap-3 text-[11px] 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="font-bold">{typeGroup.totalAbnormal}</span></span>
<span>其中异动 <span className="font-bold">{typeGroup.totalAbnormal}</span></span>
<span>运营 <span className={theme === 'vibrant' ? 'font-bold' : 'font-bold text-green-600'}>{typeGroup.totalOperating}</span></span>
</div>
</div>
@@ -50,7 +50,7 @@ export function AssetSummaryMobileCards({
<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)}
tabIndex={0} onKeyDown={e=>{if(e.target===e.currentTarget && (e.key==='Enter'||e.key===' ')){e.preventDefault();toggleModel(model.model);}}} 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" />}
@@ -59,7 +59,7 @@ export function AssetSummaryMobileCards({
<div className="flex flex-wrap justify-end gap-2">
<span className="text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-bold cursor-pointer active:bg-blue-100" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', source: 'asset', title: model.model }); }}>资产 {model.total}</span>
<span className="text-[10px] bg-orange-50 text-orange-600 px-1.5 py-0.5 rounded font-bold cursor-pointer active:bg-orange-100" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Inventory', source: 'asset', title: `${model.model} - 库存` }); }}>库存 {model.inventory}</span>
<button type="button" className="text-[10px] bg-amber-50 text-amber-700 px-1.5 py-0.5 rounded font-bold" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Abnormal', source: 'asset', title: `${model.model} - 异动` }); }}>异动 {model.abnormal}</button>
<button type="button" className="text-[10px] bg-amber-50 text-amber-700 px-1.5 py-0.5 rounded font-bold" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Abnormal', source: 'asset', title: `${model.model} - 异动` }); }}>其中异动 {model.abnormal}</button>
<span className="text-[10px] bg-green-50 text-green-600 px-1.5 py-0.5 rounded font-bold cursor-pointer active:bg-green-100" onClick={(e) => { e.stopPropagation(); setShowPlateNumbers({ batch: 'All', model: model.model, location: 'All', category: 'Operating', source: 'asset', title: `${model.model} - 在运营` }); }}>运营 {model.operating}</span>
</div>
</div>
@@ -74,13 +74,13 @@ export function AssetSummaryMobileCards({
</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-[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>
<div className="text-[11px] text-gray-400 mb-0.5">{reg === '嘉兴' ? '浙' : reg === '广东' ? '粤' : reg === '北京' ? '京' : reg === '新疆' ? '新' : '其'}</div>
{model.inventoryRegions[reg] > 0 ? (
<button
onClick={(e) => {
@@ -100,17 +100,17 @@ export function AssetSummaryMobileCards({
<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-[11px] 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-[11px] 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-[11px] text-gray-400 mb-1">已替换</span>
<span className="text-xs font-bold text-purple-600">{model.weeklyReplaced}</span>
</div>
</div>
@@ -1,3 +1,4 @@
import { InventoryStatusCounts } from './InventoryStatusCounts';
import React from 'react';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
@@ -26,7 +27,7 @@ export function InventoryDesktopTable({
setVehicleSelection: setShowPlateNumbers,
}: InventoryDesktopTableProps) {
return (
<div className="hidden lg:block overflow-x-auto">
<div className="hidden lg:block asset-table-scroll 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">
@@ -34,6 +35,8 @@ export function InventoryDesktopTable({
<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>
<th className="p-3 font-semibold text-center text-amber-700 w-28">其中异动</th>
<th className="p-3 font-semibold text-center text-blue-700 w-28">其中待交车</th>
</tr>
</thead>
<tbody className="text-xs">
@@ -42,9 +45,9 @@ export function InventoryDesktopTable({
<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)}
tabIndex={0} onKeyDown={e=>{if(e.target===e.currentTarget && (e.key==='Enter'||e.key===' ')){e.preventDefault();toggleInventoryRegion(region);}}} onClick={() => toggleInventoryRegion(region)}
>
<td colSpan={4} className="p-3 font-bold text-slate-700">
<td colSpan={6} 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>
@@ -55,6 +58,7 @@ export function InventoryDesktopTable({
}}>
(共 {Object.values(cities).flat().reduce((acc, s) => acc + s.quantity, 0)} 台)
</span>
<InventoryStatusCounts rows={Object.values(cities).flat()} />
</div>
</td>
</tr>
@@ -84,6 +88,8 @@ export function InventoryDesktopTable({
{stat.quantity}
</button>
</td>
<td className="p-3 text-center font-bold text-amber-700">{stat.abnormal ?? 0}</td>
<td className="p-3 text-center font-bold text-blue-700">{stat.pending ?? 0}</td>
</motion.tr>
))}
</React.Fragment>
@@ -96,9 +102,9 @@ export function InventoryDesktopTable({
<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)}
tabIndex={0} onKeyDown={e=>{if(e.target===e.currentTarget && (e.key==='Enter'||e.key===' ')){e.preventDefault();toggleInventoryType(type);}}} onClick={() => toggleInventoryType(type)}
>
<td colSpan={4} className="p-3 font-bold text-slate-700">
<td colSpan={6} 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>
@@ -109,6 +115,7 @@ export function InventoryDesktopTable({
}}>
(共 {Object.values(models).flat().reduce((acc, s) => acc + s.quantity, 0)} 台)
</span>
<InventoryStatusCounts rows={Object.values(models).flat()} />
</div>
</td>
</tr>
@@ -138,6 +145,8 @@ export function InventoryDesktopTable({
{stat.quantity}
</button>
</td>
<td className="p-3 text-center font-bold text-amber-700">{stat.abnormal ?? 0}</td>
<td className="p-3 text-center font-bold text-blue-700">{stat.pending ?? 0}</td>
</motion.tr>
))}
</React.Fragment>
@@ -1,3 +1,4 @@
import { InventoryStatusCounts } from './InventoryStatusCounts';
import { ChevronDown, ChevronRight } from 'lucide-react';
import type { InventoryStatisticsSectionProps } from './types';
@@ -29,8 +30,8 @@ export function InventoryMobileList({
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)}
className="bg-slate-50 p-3 flex flex-wrap gap-2 justify-between items-center cursor-pointer"
tabIndex={0} onKeyDown={e=>{if(e.target===e.currentTarget && (e.key==='Enter'||e.key===' ')){e.preventDefault();toggleInventoryRegion(region);}}} 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" />}
@@ -39,6 +40,7 @@ export function InventoryMobileList({
<span className="text-[10px] font-bold text-blue-600">
{Object.values(cities).flat().reduce((acc, s) => acc + s.quantity, 0)} 台
</span>
<InventoryStatusCounts rows={Object.values(cities).flat()} />
</div>
{expandedInventoryRegions.has(region) && (
@@ -48,9 +50,9 @@ export function InventoryMobileList({
<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 key={idx} className="flex flex-wrap gap-2 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-400 text-[11px]">{stat.brand}</span>
<span className="text-slate-700 font-medium">{stat.model}</span>
</div>
<button
@@ -59,6 +61,7 @@ export function InventoryMobileList({
>
{stat.quantity} 台
</button>
<div className="w-full border-t border-slate-100 pt-2"><InventoryStatusCounts rows={[stat]} /></div>
</div>
))}
</div>
@@ -72,8 +75,8 @@ export function InventoryMobileList({
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)}
className="bg-slate-50 p-3 flex flex-wrap gap-2 justify-between items-center cursor-pointer"
tabIndex={0} onKeyDown={e=>{if(e.target===e.currentTarget && (e.key==='Enter'||e.key===' ')){e.preventDefault();toggleInventoryType(type);}}} 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" />}
@@ -82,6 +85,7 @@ export function InventoryMobileList({
<span className="text-[10px] font-bold text-blue-600">
{Object.values(models).flat().reduce((acc, s) => acc + s.quantity, 0)} 台
</span>
<InventoryStatusCounts rows={Object.values(models).flat()} />
</div>
{expandedInventoryTypes.has(type) && (
@@ -91,9 +95,9 @@ export function InventoryMobileList({
<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 key={idx} className="flex flex-wrap gap-2 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-400 text-[11px]">{stat.brand}</span>
<span className="text-slate-700 font-medium">{stat.region} / {stat.city}</span>
</div>
<button
@@ -102,6 +106,7 @@ export function InventoryMobileList({
>
{stat.quantity} 台
</button>
<div className="w-full border-t border-slate-100 pt-2"><InventoryStatusCounts rows={[stat]} /></div>
</div>
))}
</div>
@@ -1,3 +1,4 @@
import { InventoryStatusCounts } from './InventoryStatusCounts';
import { Filter } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import { InventoryDesktopTable } from './InventoryDesktopTable';
@@ -39,20 +40,19 @@ export function InventoryStatisticsSection({
};
return (
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm mb-6 overflow-hidden min-h-[420px]">
<div data-testid="inventory-statistics" className="bg-white rounded-2xl border border-gray-100 shadow-sm mb-6 overflow-hidden min-h-[420px]">
<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>
<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>
<div className="flex items-center gap-3 ml-auto pr-2">
<div className="flex flex-col items-end">
<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>
@@ -78,14 +78,15 @@ export function InventoryStatisticsSection({
<AnimatePresence>
{isInventoryFilterOpen && (
<>
<button type="button" aria-label="关闭库存筛选" className="fixed inset-0 z-40 bg-slate-900/30" 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="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-slate-100 z-50 p-4"
className="asset-filter-panel 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-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>
<h3 className="text-sm font-bold text-slate-800">筛选库存</h3>
<button onClick={() => setDraftInventoryFilters({ region: '', city: '', brand: '', type: '', model: '' })} className="text-[10px] text-blue-500 hover:underline">重置所有</button>
</div>
@@ -135,6 +136,11 @@ export function InventoryStatisticsSection({
</div>
</div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 border-b border-slate-100 px-4 py-3" data-testid="inventory-status-summary">
<InventoryStatusCounts rows={filteredInventoryStats} />
<span className="text-[10px] text-slate-400">随当前筛选统计,已包含在库存总数中</span>
</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
@@ -193,6 +199,7 @@ export function InventoryStatisticsSection({
</div>
)}
{filteredInventoryStats.length === 0 && <div className="py-12 text-center"><p className="text-sm font-medium text-slate-700">没有符合条件的库存</p><button type="button" onClick={()=>setInventoryFilters({region:'',city:'',brand:'',type:'',model:''})} className="mt-3 rounded-xl bg-blue-50 px-4 py-2 text-sm text-blue-700">清除筛选</button></div>}
{/* Desktop View Table */}
<InventoryDesktopTable {...listProps} />
@@ -0,0 +1,11 @@
import type { RegionalInventoryStats } from '../../types';
/** Counts are subsets of inventory and use the same filtered rows as the total. */
export function InventoryStatusCounts({ rows }: { rows: RegionalInventoryStats[] }) {
const abnormal = rows.reduce((sum, row) => sum + (row.abnormal ?? 0), 0);
const pending = rows.reduce((sum, row) => sum + (row.pending ?? 0), 0);
return <span className="inline-flex flex-wrap gap-x-3 gap-y-1 text-[11px] font-normal" data-testid="inventory-status-counts">
<span className="text-amber-700">其中异动 <strong>{abnormal}</strong> 台</span>
<span className="text-blue-700">其中待交车 <strong>{pending}</strong> 台</span>
</span>;
}
@@ -1,105 +1,57 @@
import { Activity, TriangleAlert, PlusCircle, Truck, Warehouse } from 'lucide-react';
import { Activity, TriangleAlert, PlusCircle, Truck, Warehouse, ChevronRight } from 'lucide-react';
import type { SummaryMetricsProps } from './types';
export function SummaryMetrics({
summary: SUMMARY,
operatingRate,
inventoryRate,
pendingRate,
setVehicleSelection: setShowPlateNumbers,
}: SummaryMetricsProps) {
return (
<>
{/* Header Summary - Ultra Compact */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-2 mb-2">
{/* Total Assets */}
<div className="rounded-2xl border border-slate-100 bg-white p-3 shadow-sm flex items-center gap-2 cursor-pointer transition-all hover:-translate-y-0.5 hover:shadow-md"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', source: 'asset', title: '资产概览' })}>
<div className="w-8 h-8 rounded-xl bg-slate-50 flex items-center justify-center text-slate-500">
<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>
export function SummaryMetrics({ summary, operatingRate, inventoryRate, setVehicleSelection }: SummaryMetricsProps) {
const open = (category: 'Operating' | 'Inventory' | 'Abnormal' | 'Pending' | undefined, title: string) =>
setVehicleSelection({ batch: 'All', model: 'All', location: 'All', category, source: 'asset', title });
{/* Operating */}
<div className="rounded-2xl border border-slate-100 bg-white p-3 shadow-sm flex items-center gap-2 cursor-pointer transition-all hover:-translate-y-0.5 hover:shadow-md"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Operating', source: 'asset', title: '正在运营' })}>
<div className="w-8 h-8 rounded-xl 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="rounded-2xl border border-slate-100 bg-white p-3 shadow-sm flex items-center gap-2 cursor-pointer transition-all hover:-translate-y-0.5 hover:shadow-md"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Inventory', source: 'asset', title: '库存总数' })}>
<div className="w-8 h-8 rounded-xl bg-slate-50 flex items-center justify-center text-slate-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>
</div>
</div>
</div>
<button type="button" data-testid="asset-abnormal-card"
className="rounded-2xl border border-amber-100 bg-white p-3 shadow-sm flex items-center gap-2 text-left transition-all hover:-translate-y-0.5 hover:shadow-md"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Abnormal', source: 'asset', title: '异动车辆' })}>
<div className="w-8 h-8 rounded-xl bg-amber-50 flex items-center justify-center text-amber-600"><TriangleAlert size={14} /></div>
<div>
<div className="text-[9px] text-amber-600 font-bold leading-none mb-0.5">异动</div>
<div className="text-base font-bold text-amber-600 leading-none">{SUMMARY.inventory.abnormal}</div>
return <div className="mb-3 space-y-2">
<button type="button" onClick={() => open(undefined, '资产概览')}
className="flex items-center gap-2 px-1 py-2 text-xs text-slate-500 hover:text-blue-600">
<Truck size={15} /><span>资产总数</span><strong className="text-base text-slate-800">{summary.totalAssets.toLocaleString()}</strong><span>台</span><ChevronRight size={14} />
</button>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<section data-testid="asset-operating-group" className="overflow-hidden rounded-2xl border border-blue-100 bg-white shadow-sm">
<button type="button" onClick={() => open('Operating', '总运营车辆')}
className="flex w-full items-center justify-between gap-3 p-4 text-left hover:bg-blue-50/50">
<div className="flex items-center gap-3">
<span className="rounded-xl bg-blue-50 p-2.5 text-blue-600"><Activity size={20} /></span>
<div><h3 className="text-sm font-bold text-slate-800">总运营</h3><p className="mt-1 text-xs text-slate-500">租赁 + 自营</p></div>
</div>
<div className="flex items-center gap-2"><strong className="text-2xl font-bold text-blue-600">{summary.operating.total}</strong><span className="text-xs text-slate-400">台</span><ChevronRight size={16} className="text-slate-400" /></div>
</button>
{/* Pending */}
<div className="rounded-2xl border border-slate-100 bg-white p-3 shadow-sm flex items-center gap-2 cursor-pointer transition-all hover:-translate-y-0.5 hover:shadow-md"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Pending', source: 'asset', title: '待交车' })}>
<div className="w-8 h-8 rounded-xl bg-blue-50 flex items-center justify-center text-blue-500">
<PlusCircle size={14} />
<div className="grid grid-cols-2 divide-x divide-blue-100 border-t border-blue-50 bg-blue-50/30 px-4 py-3 text-sm">
<div className="flex items-center justify-between gap-2 pr-4"><span className="text-slate-500">租赁</span><strong className="text-slate-800">{summary.operating.leased}</strong></div>
<div className="flex items-center justify-between gap-2 pl-4"><span className="text-slate-500">自营</span><strong className="text-slate-800">{summary.operating.self}</strong></div>
</div>
</section>
<section data-testid="asset-inventory-group" className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
<button type="button" onClick={() => open('Inventory', '总库存车辆')}
className="flex w-full items-center justify-between gap-3 p-4 text-left hover:bg-slate-50">
<div className="flex items-center gap-3">
<span className="rounded-xl bg-slate-100 p-2.5 text-slate-600"><Warehouse size={20} /></span>
<div><h3 className="text-sm font-bold text-slate-800">总库存</h3><p className="mt-1 text-xs text-slate-500">待运营 + 可运营</p></div>
</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 className="flex items-center gap-2"><strong className="text-2xl font-bold text-slate-800">{summary.inventory.total}</strong><span className="text-xs text-slate-400">台</span><ChevronRight size={16} className="text-slate-400" /></div>
</button>
<div className="border-t border-slate-100 bg-slate-50/60 px-4 py-3">
<p className="mb-2 text-[11px] text-slate-500">以下车辆已包含在总库存中</p>
<div className="grid grid-cols-2 gap-2">
<button type="button" data-testid="asset-abnormal-card" onClick={() => open('Abnormal', '库存异动车辆')}
className="flex items-center justify-between gap-2 rounded-xl border border-amber-100 bg-amber-50 px-3 py-2 text-sm text-amber-700 hover:bg-amber-100">
<span className="flex items-center gap-1.5"><TriangleAlert size={14} />异动</span><span className="flex items-center gap-1 font-bold">{summary.inventory.abnormal}<ChevronRight size={14} /></span>
</button>
<button type="button" data-testid="asset-pending-card" onClick={() => open('Pending', '库存待交车')}
className="flex items-center justify-between gap-2 rounded-xl border border-blue-100 bg-blue-50 px-3 py-2 text-sm text-blue-700 hover:bg-blue-100">
<span className="flex items-center gap-1.5"><PlusCircle size={14} />待交车</span><span className="flex items-center gap-1 font-bold">{summary.pendingDelivery}<ChevronRight size={14} /></span>
</button>
</div>
</div>
</div>
<div data-testid="asset-operation-ratio-strip" className="mb-3 rounded-2xl border border-slate-100 bg-white/85 px-4 py-3 shadow-sm">
<div className="flex items-center justify-between gap-3">
<div className="shrink-0 text-[11px] font-black text-slate-400">运营概览</div>
<div className="grid min-w-0 flex-1 grid-cols-4 divide-x divide-slate-100 text-center">
<div className="px-2">
<div className="text-[10px] font-black text-slate-400">运营率</div>
<div className="mt-0.5 text-base font-black text-blue-600">{operatingRate.toFixed(1)}%</div>
</div>
<div className="px-2">
<div className="text-[10px] font-black text-slate-400">库存率</div>
<div className="mt-0.5 text-base font-black text-slate-700">{inventoryRate.toFixed(1)}%</div>
</div>
<div className="px-2">
<div className="text-[10px] font-black text-slate-400">异动率</div>
<div className="mt-0.5 text-base font-black text-amber-600">{(SUMMARY.totalAssets > 0 ? SUMMARY.inventory.abnormal / SUMMARY.totalAssets * 100 : 0).toFixed(1)}%</div>
</div>
<div className="px-2">
<div className="text-[10px] font-black text-slate-400">待交率</div>
<div className="mt-0.5 text-base font-black text-amber-600">{pendingRate.toFixed(1)}%</div>
</div>
</div>
</div>
</div>
</>
);
</section>
</div>
<div data-testid="asset-operation-ratio-strip" className="flex flex-wrap items-center gap-x-5 gap-y-1 px-1 py-1 text-xs text-slate-500">
<span>运营率 <strong className="text-blue-600">{operatingRate.toFixed(1)}%</strong></span>
<span>库存率 <strong className="text-slate-700">{inventoryRate.toFixed(1)}%</strong></span>
</div>
</div>;
}