feat: add region operations statistics section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-03-27 18:24:54 +08:00
parent 4cac404f49
commit 17f2222e52

View File

@@ -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 && (