feat(assets): migrate asset views and reconcile inventory drilldowns

This commit is contained in:
shishengliang
2026-09-23 14:16:22 +08:00
parent b0cba26fbb
commit 822aabf2ca
77 changed files with 14669 additions and 3856 deletions
@@ -1,210 +0,0 @@
import { InventoryStatusCounts } from './InventoryStatusCounts';
import { Filter } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import { InventoryDesktopTable } from './InventoryDesktopTable';
import { InventoryMobileList } from './InventoryMobileList';
import type { InventoryStatisticsSectionProps } from './types';
export function InventoryStatisticsSection({
inventoryFilters,
setInventoryFilters,
draftInventoryFilters,
setDraftInventoryFilters,
isInventoryFilterOpen,
setIsInventoryFilterOpen,
uniqueInventoryRegions,
uniqueInventoryCities,
uniqueInventoryBrands,
uniqueInventoryTypes,
uniqueInventoryModelsForType,
filteredInventoryStats,
inventoryTab,
setInventoryTab,
inventoryByRegion,
expandedInventoryRegions,
toggleInventoryRegion,
inventoryByModel,
expandedInventoryTypes,
toggleInventoryType,
setVehicleSelection: setShowPlateNumbers,
}: InventoryStatisticsSectionProps) {
const listProps = {
inventoryTab,
inventoryByRegion,
expandedInventoryRegions,
toggleInventoryRegion,
inventoryByModel,
expandedInventoryTypes,
toggleInventoryType,
setVehicleSelection: setShowPlateNumbers,
};
return (
<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>
</div>
</div>
<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>
</span>
</div>
<div className="relative">
<button
onClick={() => { if (!isInventoryFilterOpen) setDraftInventoryFilters({...inventoryFilters}); setIsInventoryFilterOpen(!isInventoryFilterOpen); }}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${
isInventoryFilterOpen || (inventoryFilters.region || inventoryFilters.city || inventoryFilters.brand || inventoryFilters.type || inventoryFilters.model)
? 'bg-blue-600 text-white shadow-md'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
<Filter size={14} />
<span>筛选</span>
{(inventoryFilters.region || inventoryFilters.city || inventoryFilters.brand || inventoryFilters.type || inventoryFilters.model) && (
<span className="w-2 h-2 bg-white rounded-full animate-pulse"></span>
)}
</button>
<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="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-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>
<div className="space-y-3 text-left">
<div>
<label className="text-[10px] text-slate-400 block mb-1">区域</label>
<select value={draftInventoryFilters.region} onChange={(e) => setDraftInventoryFilters({...draftInventoryFilters, region: e.target.value})} className="w-full text-xs bg-white border border-slate-200 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm cursor-pointer">
<option value="">全部区域</option>
{uniqueInventoryRegions.map(r => <option key={r} value={r}>{r}</option>)}
</select>
</div>
<div>
<label className="text-[10px] text-slate-400 block mb-1">城市</label>
<select value={draftInventoryFilters.city} onChange={(e) => setDraftInventoryFilters({...draftInventoryFilters, city: e.target.value})} className="w-full text-xs bg-white border border-slate-200 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm cursor-pointer">
<option value="">全部城市</option>
{uniqueInventoryCities.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</div>
<div>
<label className="text-[10px] text-slate-400 block mb-1">品牌</label>
<select value={draftInventoryFilters.brand} onChange={(e) => setDraftInventoryFilters({...draftInventoryFilters, brand: e.target.value})} className="w-full text-xs bg-white border border-slate-200 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm cursor-pointer">
<option value="">全部品牌</option>
{uniqueInventoryBrands.map(b => <option key={b} value={b}>{b}</option>)}
</select>
</div>
<div>
<label className="text-[10px] text-slate-400 block mb-1">车型</label>
<select value={draftInventoryFilters.type} onChange={(e) => setDraftInventoryFilters({...draftInventoryFilters, type: e.target.value, model: ''})} className="w-full text-xs bg-white border border-slate-200 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm cursor-pointer">
<option value="">全部车型</option>
{uniqueInventoryTypes.map(t => <option key={t} value={t}>{t}</option>)}
</select>
</div>
<div>
<label className="text-[10px] text-slate-400 block mb-1">批次</label>
<select value={draftInventoryFilters.model} onChange={(e) => setDraftInventoryFilters({...draftInventoryFilters, model: e.target.value})} className="w-full text-xs bg-white border border-slate-200 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all shadow-sm cursor-pointer">
<option value="">全部批次</option>
{uniqueInventoryModelsForType.map(m => <option key={m} value={m}>{m}</option>)}
</select>
</div>
</div>
<button onClick={() => { setInventoryFilters({...draftInventoryFilters}); setIsInventoryFilterOpen(false); }} className="w-full mt-4 py-2 bg-blue-600 text-white rounded-lg text-xs font-bold hover:bg-blue-700 transition-colors">确认</button>
</motion.div>
</>
)}
</AnimatePresence>
</div>
</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
onClick={() => setInventoryTab('region')}
className={`px-6 py-1.5 rounded-md text-xs font-bold transition-all ${
inventoryTab === 'region' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'
}`}
>
按区域
</button>
<button
onClick={() => setInventoryTab('model')}
className={`px-6 py-1.5 rounded-md text-xs font-bold transition-all ${
inventoryTab === 'model' ? 'bg-white text-blue-600 shadow-sm' : 'text-gray-500 hover:text-gray-700'
}`}
>
按车型
</button>
</div>
</div>
{/* Active Filters Bar */}
{(inventoryFilters.region || inventoryFilters.city || inventoryFilters.brand || inventoryFilters.type || inventoryFilters.model) && (
<div className="px-4 py-2 border-b border-gray-100 flex flex-wrap gap-2 items-center">
{inventoryFilters.region && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-[10px] flex items-center gap-1">
区域: {inventoryFilters.region}
<button onClick={() => setInventoryFilters({...inventoryFilters, region: ''})} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
{inventoryFilters.city && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-[10px] flex items-center gap-1">
城市: {inventoryFilters.city}
<button onClick={() => setInventoryFilters({...inventoryFilters, city: ''})} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
{inventoryFilters.brand && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-[10px] flex items-center gap-1">
品牌: {inventoryFilters.brand}
<button onClick={() => setInventoryFilters({...inventoryFilters, brand: ''})} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
{inventoryFilters.type && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-[10px] flex items-center gap-1">
车型: {inventoryFilters.type}
<button onClick={() => setInventoryFilters({...inventoryFilters, type: '', model: ''})} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
{inventoryFilters.model && (
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-[10px] flex items-center gap-1">
批次: {inventoryFilters.model}
<button onClick={() => setInventoryFilters({...inventoryFilters, model: ''})} className="hover:text-red-500 ml-0.5">×</button>
</span>
)}
<button onClick={() => setInventoryFilters({ region: '', city: '', brand: '', type: '', model: '' })} className="text-[11px] text-red-500 font-bold ml-auto hover:text-red-600">清除</button>
</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} />
{/* Mobile View */}
<InventoryMobileList {...listProps} />
</div>
);
}