fix: 区域运营移动端数据、下钻支持城市/车型、网页标题
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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:
@@ -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>
|
||||
|
||||
99
src/App.tsx
99
src/App.tsx
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user