feat: improve asset inventory statistics and responsive UX (v1.1.16)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import './assets.css';
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import {
|
||||
Truck,
|
||||
@@ -129,6 +130,8 @@ export default function AssetsModule() {
|
||||
const [regionChartData, setRegionChartData] = useState<{ name: string; value: number }[]>([]);
|
||||
|
||||
// Modal filter state
|
||||
const [modalError, setModalError] = useState<string | null>(null);
|
||||
const [modalRetry, setModalRetry] = useState(0);
|
||||
const [modalFilters, setModalFilters] = useState({ plateNumber: '', model: '', brand: '', location: '' });
|
||||
const [isModalFilterExpanded, setIsModalFilterExpanded] = useState(false);
|
||||
|
||||
@@ -227,30 +230,22 @@ export default function AssetsModule() {
|
||||
.catch(() => setRegionChartData([]));
|
||||
}, [regionChartView, selectedSubject]);
|
||||
|
||||
// Load modal vehicles
|
||||
// Keep stale requests from overwriting a newer selection or a closed dialog.
|
||||
useEffect(() => {
|
||||
if (!showPlateNumbers) {
|
||||
setModalVehicles([]);
|
||||
setModalWeeklyDetail([]);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setModalVehicles([]);
|
||||
setModalWeeklyDetail([]);
|
||||
setModalError(null);
|
||||
if (!showPlateNumbers) return;
|
||||
setModalLoading(true);
|
||||
const request = buildVehicleModalRequest(showPlateNumbers, selectedSubject);
|
||||
if (request.kind === 'weekly') {
|
||||
setModalVehicles([]);
|
||||
fetchWeeklyDetail(request.type, request.filters)
|
||||
.then(setModalWeeklyDetail)
|
||||
.catch(() => setModalWeeklyDetail([]))
|
||||
.finally(() => setModalLoading(false));
|
||||
return;
|
||||
}
|
||||
|
||||
setModalWeeklyDetail([]);
|
||||
fetchVehicleList(request.params)
|
||||
.then(setModalVehicles)
|
||||
.catch(() => setModalVehicles([]))
|
||||
.finally(() => setModalLoading(false));
|
||||
}, [showPlateNumbers, selectedSubject]);
|
||||
const load = request.kind === 'weekly'
|
||||
? fetchWeeklyDetail(request.type, request.filters).then(rows=>{if(!cancelled)setModalWeeklyDetail(rows);})
|
||||
: fetchVehicleList(request.params).then(rows=>{if(!cancelled)setModalVehicles(rows);});
|
||||
load.catch(()=>{if(!cancelled)setModalError('车辆明细加载失败,请重试。');})
|
||||
.finally(()=>{if(!cancelled)setModalLoading(false);});
|
||||
return ()=>{cancelled=true;};
|
||||
}, [showPlateNumbers, selectedSubject, modalRetry]);
|
||||
|
||||
const allTypesExpanded = processedData.length > 0 && processedData.every((t) => expandedAssetTypes.has(t.type));
|
||||
|
||||
@@ -473,7 +468,7 @@ export default function AssetsModule() {
|
||||
const pendingRate = SUMMARY.totalAssets > 0 ? SUMMARY.pendingDelivery / SUMMARY.totalAssets * 100 : 0;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[var(--app-bg)] text-gray-800 font-sans p-3 md:p-6 relative">
|
||||
<div className="asset-workspace min-h-screen bg-[var(--app-bg)] text-gray-800 font-sans p-3 md:p-6 relative">
|
||||
<AssetsHeader
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
@@ -481,6 +476,7 @@ export default function AssetsModule() {
|
||||
setTheme={setTheme}
|
||||
lastUpdate={lastUpdate}
|
||||
loading={loading}
|
||||
onRefresh={loadData}
|
||||
selectedSubject={selectedSubject}
|
||||
setSelectedSubject={setSelectedSubject}
|
||||
subjects={subjects}
|
||||
@@ -491,6 +487,8 @@ export default function AssetsModule() {
|
||||
subjectDropdownRef={subjectDropdownRef}
|
||||
/>
|
||||
|
||||
{error && summary && <div role="alert" className="mb-4 flex items-center justify-between gap-3 rounded-xl border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800"><span>更新失败,当前显示上次成功加载的数据。</span><button type="button" onClick={loadData} className="shrink-0 rounded-lg px-3 py-2 font-semibold underline">重试</button></div>}
|
||||
{activeTab !== 'overview' && <p className="mb-4 text-xs text-slate-500">{activeTab==='department'?'按部门与负责人查看运营车辆,点击数量查看明细。':activeTab==='region'?'查看各区域的车辆分布,展开区域继续查看城市与车型。':'按客户查看运营车辆,客户名称已脱敏;点击合计查看车辆明细。'}</p>}
|
||||
{/* Main Content Area */}
|
||||
<div className="flex flex-col gap-6">
|
||||
|
||||
@@ -1135,12 +1133,12 @@ export default function AssetsModule() {
|
||||
<AnimatePresence>
|
||||
{isRegionFilterOpen && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" />
|
||||
<button type="button" aria-label="关闭筛选" className="fixed inset-0 z-40 bg-slate-900/30" 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="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-gray-100 z-50 p-4 text-gray-800"
|
||||
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-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>
|
||||
@@ -1346,7 +1344,7 @@ export default function AssetsModule() {
|
||||
|
||||
{/* Customer Operations Statistics Section */}
|
||||
<section className="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden mb-6 min-h-[420px]">
|
||||
<div className="p-3 sm:p-4 border-b border-gray-50 flex items-center justify-between bg-emerald-800 text-white relative">
|
||||
<div className="p-3 sm:p-4 border-b border-gray-50 flex items-center justify-between bg-blue-50 text-slate-800 relative">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-1.5 h-6 bg-emerald-400 rounded-full"></div>
|
||||
<div>
|
||||
@@ -1361,7 +1359,7 @@ export default function AssetsModule() {
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all ${
|
||||
isCustomerFilterOpen || Object.values(customerFilters).some(v => Array.isArray(v) ? v.length > 0 : v !== '')
|
||||
? 'bg-emerald-400 text-emerald-900 shadow-lg shadow-emerald-900/20'
|
||||
: 'bg-emerald-700/50 text-emerald-100 hover:bg-emerald-700'
|
||||
: 'bg-white text-blue-700 hover:bg-blue-100'
|
||||
}`}
|
||||
>
|
||||
<Filter size={14} />
|
||||
@@ -1375,14 +1373,14 @@ export default function AssetsModule() {
|
||||
{isCustomerFilterOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div className="fixed inset-0 z-40" />
|
||||
<button type="button" aria-label="关闭筛选" className="fixed inset-0 z-40 bg-slate-900/30" onClick={()=>setIsCustomerFilterOpen(false)} />
|
||||
|
||||
{/* Popover Content */}
|
||||
<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="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-gray-100 z-50 p-4 text-gray-800"
|
||||
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-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>
|
||||
@@ -1485,7 +1483,7 @@ export default function AssetsModule() {
|
||||
<div className="hidden lg:block overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse min-w-[1000px]">
|
||||
<thead>
|
||||
<tr className="bg-emerald-700 text-white text-[11px] uppercase tracking-wider border-b border-emerald-800">
|
||||
<tr className="bg-slate-100 text-slate-600 text-[11px] uppercase tracking-wider border-b border-emerald-800">
|
||||
<th className="p-2 font-semibold border-r border-emerald-600 w-40">客户名称</th>
|
||||
<th className="p-2 font-semibold border-r border-emerald-600 w-24">所在区域</th>
|
||||
<th className="p-2 font-semibold border-r border-emerald-600 w-24">关联业务负责人</th>
|
||||
@@ -1691,6 +1689,8 @@ export default function AssetsModule() {
|
||||
brands={uniqueModalBrands}
|
||||
locations={uniqueModalLocations}
|
||||
loading={modalLoading}
|
||||
error={modalError}
|
||||
onRetry={()=>setModalRetry(v=>v+1)}
|
||||
weeklyDetails={modalWeeklyDetail}
|
||||
filteredWeeklyDetails={filteredModalWeeklyDetail}
|
||||
filteredVehicles={filteredModalVehicles}
|
||||
|
||||
Reference in New Issue
Block a user