feat: add region operations statistics section
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
263
src/App.tsx
263
src/App.tsx
@@ -1356,6 +1356,269 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Region - Vehicle - Customer Section */}
|
||||||
|
<section className="bg-white rounded-sm shadow-sm border border-gray-100 overflow-hidden mb-6">
|
||||||
|
<div className="p-3 sm:p-4 border-b border-gray-50 flex items-center justify-between bg-slate-50 relative">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-1.5 h-6 bg-slate-400 rounded-full"></div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-bold text-slate-800">区域运营统计</h2>
|
||||||
|
<p className="text-[10px] text-slate-400 font-medium">*按区域—车型—客户维度统计</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsRegionFilterOpen(!isRegionFilterOpen)}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
||||||
|
isRegionFilterOpen || Object.values(regionFilters).some(v => v !== '')
|
||||||
|
? 'bg-slate-200 text-slate-900 shadow-sm'
|
||||||
|
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Filter size={14} />
|
||||||
|
<span>筛选</span>
|
||||||
|
{Object.values(regionFilters).some(v => v !== '') && (
|
||||||
|
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{isRegionFilterOpen && (
|
||||||
|
<>
|
||||||
|
<div className="fixed inset-0 z-40" onClick={() => setIsRegionFilterOpen(false)} />
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||||
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||||
|
className="absolute top-full right-4 mt-2 w-72 bg-white rounded-xl shadow-2xl border border-gray-100 z-50 p-4 text-gray-800"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-sm font-bold text-gray-900">区域筛选</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setRegionFilters({ region: '', city: '', customer: '' })}
|
||||||
|
className="text-[10px] text-slate-600 hover:text-slate-700 font-medium"
|
||||||
|
>
|
||||||
|
重置所有
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">客户名称</label>
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={14} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="搜索客户名称..."
|
||||||
|
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 pl-9 pr-3 text-xs focus:ring-2 focus:ring-slate-500/20 focus:border-slate-500 outline-none transition-all"
|
||||||
|
value={regionFilters.customer}
|
||||||
|
onChange={(e) => setRegionFilters(prev => ({ ...prev, customer: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-[10px] font-bold text-gray-400 uppercase tracking-wider">区域</label>
|
||||||
|
<select
|
||||||
|
className="w-full bg-gray-50 border border-gray-200 rounded-lg py-2 px-2 text-xs focus:ring-2 focus:ring-slate-500/20 focus:border-slate-500 outline-none transition-all cursor-pointer"
|
||||||
|
value={regionFilters.region}
|
||||||
|
onChange={(e) => setRegionFilters(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-gray-50 border border-gray-200 rounded-lg py-2 px-2 text-xs focus:ring-2 focus:ring-slate-500/20 focus:border-slate-500 outline-none transition-all cursor-pointer"
|
||||||
|
value={regionFilters.city}
|
||||||
|
onChange={(e) => setRegionFilters(prev => ({ ...prev, city: e.target.value }))}
|
||||||
|
>
|
||||||
|
<option value="">所有城市</option>
|
||||||
|
{uniqueCities.map(c => <option key={c} value={c}>{c}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 pt-4 border-t border-gray-50">
|
||||||
|
<button
|
||||||
|
onClick={() => setIsRegionFilterOpen(false)}
|
||||||
|
className="w-full bg-slate-800 text-white py-2 rounded-lg text-xs font-bold hover:bg-slate-900 transition-colors shadow-lg shadow-slate-900/20"
|
||||||
|
>
|
||||||
|
完成筛选
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="hidden lg:block overflow-x-auto">
|
||||||
|
<table className="w-full text-left border-collapse min-w-[1000px]">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-slate-50 text-slate-500 text-[11px] uppercase tracking-wider border-b border-slate-100">
|
||||||
|
<th className="p-2 font-semibold border-r border-slate-100 w-48">区域 / 车型 / 客户</th>
|
||||||
|
<th className="p-2 font-semibold border-r border-slate-100 text-center w-24">资产总数</th>
|
||||||
|
<th className="p-2 font-semibold border-r border-slate-100 text-center w-24 text-green-600">运营中</th>
|
||||||
|
<th className="p-2 font-semibold border-r border-slate-100 text-center w-24 text-orange-600">库存</th>
|
||||||
|
<th className="p-2 font-semibold text-center bg-slate-100/50 w-32">主要客户</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="text-xs">
|
||||||
|
{filteredRegionData.map((r) => {
|
||||||
|
if (r.totalAssets === 0) return null;
|
||||||
|
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">{r.totalAssets}</td>
|
||||||
|
<td
|
||||||
|
className="p-2 text-center text-green-600 font-bold cursor-pointer hover:bg-green-50"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Operating' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{r.operatingCount}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="p-2 text-center text-orange-600 font-bold cursor-pointer hover:bg-orange-50"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', category: 'Inventory' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{r.inventoryCount}
|
||||||
|
</td>
|
||||||
|
<td className="p-2 text-center text-slate-500 font-medium">
|
||||||
|
{r.customers.slice(0, 2).join(', ')}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{isExpanded && r.typeBreakdown.map(tb => {
|
||||||
|
if (tb.total === 0) return null;
|
||||||
|
const vehicleType = tb.type === '4.5T' ? '4.5T普货' : tb.type;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={tb.type}>
|
||||||
|
<tr className="border-b border-gray-50 hover:bg-gray-50">
|
||||||
|
<td className="p-2 pl-8 text-gray-500 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-600">{tb.total}</td>
|
||||||
|
<td
|
||||||
|
className="p-2 text-center text-green-600 cursor-pointer hover:bg-green-50"
|
||||||
|
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', vehicleType, category: 'Operating' })}
|
||||||
|
>
|
||||||
|
{tb.operating}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
className="p-2 text-center text-orange-600 cursor-pointer hover:bg-orange-50"
|
||||||
|
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', vehicleType, category: 'Inventory' })}
|
||||||
|
>
|
||||||
|
{tb.inventory}
|
||||||
|
</td>
|
||||||
|
<td className="p-2 text-center text-gray-400 italic">
|
||||||
|
{tb.customers.join(', ')}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile View (Region) */}
|
||||||
|
<div className="lg:hidden p-2 space-y-3">
|
||||||
|
{filteredRegionData.map((r) => {
|
||||||
|
if (r.totalAssets === 0) return null;
|
||||||
|
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: 'All', category: 'Operating' })}
|
||||||
|
>
|
||||||
|
<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: 'All', category: 'Inventory' })}
|
||||||
|
>
|
||||||
|
<div className="text-[9px] text-gray-400 uppercase">库存</div>
|
||||||
|
<div className="text-xs font-bold text-orange-600">{r.inventoryCount}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-2 pb-2 space-y-1">
|
||||||
|
{r.typeBreakdown.map(tb => {
|
||||||
|
if (tb.total === 0) return null;
|
||||||
|
const vehicleType = tb.type === '4.5T' ? '4.5T普货' : tb.type;
|
||||||
|
return (
|
||||||
|
<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: 'All', vehicleType, category: 'Operating' })}
|
||||||
|
>
|
||||||
|
运:{tb.operating}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="font-bold text-orange-600 cursor-pointer"
|
||||||
|
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', vehicleType, category: 'Inventory' })}
|
||||||
|
>
|
||||||
|
库:{tb.inventory}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Plate Number Modal */}
|
{/* Plate Number Modal */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showPlateNumbers && (
|
{showPlateNumbers && (
|
||||||
|
|||||||
Reference in New Issue
Block a user