fix: 区域运营移动端数据、下钻支持城市/车型、网页标题
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

- 移动端区域运营改用regionData真实数据(去掉Math.floor模拟)
- 区域/城市/车型行数字全部支持点击下钻
- 后端/list支持按车型大类过滤(如4.5T含普货+冷链)
- 网页标题改为"羚牛氢能车辆资产"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-03-28 18:28:46 +08:00
parent d9568f767a
commit dd01671d9e
3 changed files with 49 additions and 62 deletions

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>羚牛 BI 报表</title>
<title>羚牛氢能车辆资产</title>
</head>
<body>
<div id="root"></div>

View File

@@ -2225,9 +2225,9 @@ export default function App() {
<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">{r.operatingCount}</td>
<td className="p-2 text-center text-orange-600 font-bold">{r.pendingCount || ''}</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, category: 'Operating' }); }}>{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' }); }}>{r.operatingCount}</td>
<td className="p-2 text-center text-orange-600 font-bold cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); if (r.pendingCount) setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, category: 'Pending' }); }}>{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) => {
@@ -2244,9 +2244,9 @@ export default function App() {
<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">{city.totalAssets}</td>
<td className="p-2 text-center text-green-600">{city.operatingCount}</td>
<td className="p-2 text-center text-orange-600">{city.pendingCount || ''}</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, category: 'Operating' }); }}>{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' }); }}>{city.operatingCount}</td>
<td className="p-2 text-center text-orange-600 cursor-pointer hover:underline" onClick={(e) => { e.stopPropagation(); if (city.pendingCount) setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, category: 'Pending' }); }}>{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 => (
@@ -2255,9 +2255,9 @@ export default function App() {
<div className="w-1 h-1 bg-slate-300 rounded-full"></div>
{tb.type}
</td>
<td className="p-2 text-center text-gray-500">{tb.total}</td>
<td className="p-2 text-center text-green-500">{tb.operating}</td>
<td className="p-2 text-center text-orange-500">{tb.inventory || ''}</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, category: 'Operating' })}>{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' })}>{tb.operating}</td>
<td className="p-2 text-center text-orange-500 cursor-pointer hover:underline" onClick={() => { if (tb.inventory) setShowPlateNumbers({ batch: 'All', model: 'All', location: city.city, vehicleType: tb.type, category: 'Pending' }); }}>{tb.inventory || ''}</td>
<td className="p-2 text-center text-gray-400 text-[10px] italic">{tb.customers.slice(0, 2).join(', ')}</td>
</tr>
))}
@@ -2273,78 +2273,59 @@ export default function App() {
{/* Mobile View (Region) */}
<div className="lg:hidden p-2 space-y-3">
{uniqueRegions.filter(r => !regionFilters.region || r === regionFilters.region).map((region) => {
const regionStats = customerData.filter(s => {
const matchRegion = s.region === region;
const matchCity = !regionFilters.city || s.city === regionFilters.city;
const matchCustomer = !regionFilters.customer || s.customer.toLowerCase().includes(regionFilters.customer.toLowerCase());
return matchRegion && matchCity && matchCustomer;
});
const totalAssets = regionStats.reduce((acc, s) => acc + s.total, 0);
if (totalAssets === 0) return null;
const isExpanded = expandedRegions.has(region);
{regionData.filter(r => !regionFilters.region || r.region === regionFilters.region).map((r) => {
const isExpanded = expandedRegions.has(r.region);
return (
<div key={region} className="bg-slate-50/50 rounded-xl border border-slate-100 overflow-hidden">
<div
<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(region)}
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" />
{region}
{r.region}
</div>
<div className="text-xs font-bold text-slate-500">: {totalAssets}</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
<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', source: 'asset' })}
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, category: 'Operating', source: 'asset' })}
>
<div className="text-[9px] text-gray-400 uppercase"></div>
<div className="text-xs font-bold text-green-600">{Math.floor(totalAssets * 0.8)}</div>
<div className="text-xs font-bold text-green-600">{r.operatingCount}</div>
</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: 'Pending', source: 'asset' })}
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, category: 'Pending', source: 'asset' })}
>
<div className="text-[9px] text-gray-400 uppercase"></div>
<div className="text-xs font-bold text-orange-600">{Math.floor(totalAssets * 0.05)}</div>
<div className="text-xs font-bold text-orange-600">{r.pendingCount || ''}</div>
</div>
</div>
<div className="px-2 pb-2 space-y-1">
{['4.5T', '18T', '49T'].map(type => {
const typeTotal = regionStats.reduce((acc, s) => {
if (type === '4.5T') return acc + s.t4_5 + s.t4_5c;
if (type === '18T') return acc + s.t18;
if (type === '49T') return acc + s.t49;
return acc;
}, 0);
if (typeTotal === 0) return null;
return (
<div key={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">{type} </span>
<div className="flex gap-3">
<span
className="font-bold text-green-600 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', type, category: 'Operating', source: 'asset' })}
>
:{Math.floor(typeTotal * 0.8)}
</span>
<span
className="font-bold text-orange-600 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: 'All', type, category: 'Pending', source: 'asset' })}
>
:{Math.floor(typeTotal * 0.05)}
</span>
</div>
{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, type: tb.type, category: 'Operating', source: 'asset' })}
>
:{tb.operating}
</span>
<span
className="font-bold text-orange-600 cursor-pointer"
onClick={() => setShowPlateNumbers({ batch: 'All', model: 'All', location: r.region, type: tb.type, category: 'Pending', source: 'asset' })}
>
:{tb.inventory || ''}
</span>
</div>
);
})}
</div>
))}
</div>
</>
)}

View File

@@ -860,8 +860,14 @@ app.get('/list', async (c) => {
const { batch, model, location, status, category, vehicleType, manager, customer, isColdChain, isTrailer } = c.req.query();
let filtered = vehicles;
if (vehicleType && VEHICLE_TYPE_FILTERS[vehicleType]) {
filtered = filtered.filter(VEHICLE_TYPE_FILTERS[vehicleType]);
if (vehicleType) {
if (VEHICLE_TYPE_FILTERS[vehicleType]) {
filtered = filtered.filter(VEHICLE_TYPE_FILTERS[vehicleType]);
} else if (vehicleType === '4.5T') {
filtered = filtered.filter((v) => v.type === '4.5T');
} else {
filtered = filtered.filter((v) => v.type === vehicleType);
}
}
if (batch && batch !== 'All') {
filtered = filtered.filter((v) => (v.contractNo || '未知') === batch);