feat: 租赁状态与部门分列筛选,未同步车辆显示-,卡片增加今/总标签,全屏监控压缩优化
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-02 10:01:17 +08:00
parent affe356f43
commit 06a2edc470
4 changed files with 110 additions and 100 deletions

View File

@@ -101,6 +101,7 @@ export default function MonitoringView() {
const [filterCustomer, setFilterCustomer] = useState('All'); const [filterCustomer, setFilterCustomer] = useState('All');
const [filterProject, setFilterProject] = useState('All'); const [filterProject, setFilterProject] = useState('All');
const [filterEntity, setFilterEntity] = useState('All'); const [filterEntity, setFilterEntity] = useState('All');
const [filterRentStatus, setFilterRentStatus] = useState('All');
const [filterRegionCode, setFilterRegionCode] = useState('All'); const [filterRegionCode, setFilterRegionCode] = useState('All');
const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' }); const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' });
const [appliedMileageRange, setAppliedMileageRange] = useState({ min: '', max: '' }); const [appliedMileageRange, setAppliedMileageRange] = useState({ min: '', max: '' });
@@ -112,7 +113,7 @@ export default function MonitoringView() {
const [vehicles, setVehicles] = useState<MonitoringVehicle[]>([]); const [vehicles, setVehicles] = useState<MonitoringVehicle[]>([]);
const [stats, setStats] = useState<MonitoringStats>({ totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }); const [stats, setStats] = useState<MonitoringStats>({ totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 });
const [filterOptions, setFilterOptions] = useState<MonitoringFilters>({ departments: [], customers: [], plates: [], projects: [], entities: [] }); const [filterOptions, setFilterOptions] = useState<MonitoringFilters>({ departments: [], customers: [], plates: [], projects: [], entities: [], rentStatuses: [] });
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
@@ -135,6 +136,7 @@ export default function MonitoringView() {
customer: filterCustomer !== 'All' ? filterCustomer : undefined, customer: filterCustomer !== 'All' ? filterCustomer : undefined,
project: filterProject !== 'All' ? filterProject : undefined, project: filterProject !== 'All' ? filterProject : undefined,
entity: filterEntity !== 'All' ? filterEntity : undefined, entity: filterEntity !== 'All' ? filterEntity : undefined,
rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined,
plate: filterPlate !== 'All' ? filterPlate : undefined, plate: filterPlate !== 'All' ? filterPlate : undefined,
mileageMin: appliedMileageRange.min || undefined, mileageMin: appliedMileageRange.min || undefined,
mileageMax: appliedMileageRange.max || undefined, mileageMax: appliedMileageRange.max || undefined,
@@ -147,7 +149,7 @@ export default function MonitoringView() {
setPage(1); setPage(1);
setHasMore(d.page < d.totalPages); setHasMore(d.page < d.totalPages);
}).catch(() => {}); }).catch(() => {});
}, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterPlate, appliedMileageRange, filterDate]); }, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlate, appliedMileageRange, filterDate]);
// 加载更多 // 加载更多
const loadMore = useCallback(() => { const loadMore = useCallback(() => {
@@ -164,6 +166,7 @@ export default function MonitoringView() {
customer: filterCustomer !== 'All' ? filterCustomer : undefined, customer: filterCustomer !== 'All' ? filterCustomer : undefined,
project: filterProject !== 'All' ? filterProject : undefined, project: filterProject !== 'All' ? filterProject : undefined,
entity: filterEntity !== 'All' ? filterEntity : undefined, entity: filterEntity !== 'All' ? filterEntity : undefined,
rentStatus: filterRentStatus !== 'All' ? filterRentStatus : undefined,
plate: filterPlate !== 'All' ? filterPlate : undefined, plate: filterPlate !== 'All' ? filterPlate : undefined,
mileageMin: appliedMileageRange.min || undefined, mileageMin: appliedMileageRange.min || undefined,
mileageMax: appliedMileageRange.max || undefined, mileageMax: appliedMileageRange.max || undefined,
@@ -173,7 +176,7 @@ export default function MonitoringView() {
setPage(nextPage); setPage(nextPage);
setHasMore(nextPage < d.totalPages); setHasMore(nextPage < d.totalPages);
}).catch(() => {}).finally(() => setLoadingMore(false)); }).catch(() => {}).finally(() => setLoadingMore(false));
}, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterPlate, appliedMileageRange, filterDate, page, loadingMore, hasMore]); }, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlate, appliedMileageRange, filterDate, page, loadingMore, hasMore]);
// 筛选/排序变化时重新加载 // 筛选/排序变化时重新加载
useEffect(() => { useEffect(() => {
@@ -247,76 +250,66 @@ export default function MonitoringView() {
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] bg-slate-950 flex flex-col overflow-hidden" className="fixed inset-0 z-[100] bg-slate-950 flex flex-col overflow-hidden"
> >
{/* Top bar: title + KPI row + close */} {/* Top bar: compact inline KPI */}
<div className="flex-shrink-0 p-3 border-b border-slate-800 space-y-2"> <div className="flex-shrink-0 px-3 py-2 border-b border-slate-800/60 flex items-center justify-between">
<div className="flex items-center justify-between"> <div className="flex items-center gap-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
<div className="w-1 h-5 bg-blue-500 rounded-full"></div> <div className="w-0.5 h-4 bg-blue-500 rounded-full"></div>
<h2 className="text-white font-bold text-sm"></h2> <h2 className="text-white font-bold text-xs"></h2>
</div>
<div className="flex items-center gap-3 text-[10px]">
<span className="text-slate-500"> <span className="text-white font-black">{Math.round(stats.totalToday).toLocaleString()}</span> <span className="text-blue-400">km</span></span>
<span className="text-slate-700">|</span>
<span className="text-slate-500"> <span className="text-white font-black">{Math.round(stats.totalAll).toLocaleString()}</span> <span className="text-blue-400">km</span></span>
<span className="text-slate-700">|</span>
<span className="text-slate-500"> <span className="text-white font-black">{stats.vehicleCount}</span> </span>
<span className="text-slate-700">|</span>
<span className="text-slate-500"> <span className="text-white font-black">{(stats.vehicleCount > 0 ? (sortBy === 'today' ? stats.totalToday : stats.totalAll) / stats.vehicleCount : 0).toFixed(0)}</span> <span className="text-blue-400">km</span></span>
</div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => { setFilterDept('All'); setFilterCustomer('All'); setFilterPlate('All'); setSearchTerm(''); }} onClick={() => { setFilterDept('All'); setFilterCustomer('All'); setFilterRentStatus('All'); setFilterPlate('All'); setSearchTerm(''); }}
className="p-1.5 bg-slate-800 text-slate-400 rounded-full hover:text-blue-400 transition-colors" className="p-1.5 text-slate-500 hover:text-blue-400 transition-colors"
> >
<RotateCcw size={14} /> <RotateCcw size={13} />
</button> </button>
<button onClick={toggleFullscreen} className="p-1.5 bg-slate-800 text-slate-400 rounded-full hover:text-white transition-colors"> <button onClick={toggleFullscreen} className="p-1.5 text-slate-500 hover:text-white transition-colors">
<Minimize2 size={16} /> <Minimize2 size={14} />
</button> </button>
</div> </div>
</div> </div>
{/* KPI — single row */}
<div className="flex gap-2">
<div className={`flex-1 bg-slate-900/50 border px-3 py-2 rounded-xl ${sortBy === 'today' ? 'border-blue-500/50' : 'border-slate-800'}`}>
<div className="text-[8px] font-bold text-slate-500 uppercase"></div>
<div className="text-base font-black text-white">{Math.round(stats.totalToday).toLocaleString()} <span className="text-blue-400 text-[9px]">km</span></div>
</div>
<div className={`flex-1 bg-slate-900/50 border px-3 py-2 rounded-xl ${sortBy === 'total' ? 'border-blue-500/50' : 'border-slate-800'}`}>
<div className="text-[8px] font-bold text-slate-500 uppercase"></div>
<div className="text-base font-black text-white">{Math.round(stats.totalAll).toLocaleString()} <span className="text-blue-400 text-[9px]">km</span></div>
</div>
<div className="flex-1 bg-slate-900/50 border border-slate-800 px-3 py-2 rounded-xl">
<div className="text-[8px] font-bold text-slate-500 uppercase"></div>
<div className="text-base font-black text-white">{stats.vehicleCount} <span className="text-blue-400 text-[9px]"></span></div>
</div>
<div className="flex-1 bg-slate-900/50 border border-slate-800 px-3 py-2 rounded-xl">
<div className="text-[8px] font-bold text-slate-500 uppercase"></div>
<div className="text-base font-black text-white">{(stats.vehicleCount > 0 ? (sortBy === 'today' ? stats.totalToday : stats.totalAll) / stats.vehicleCount : 0).toFixed(0)} <span className="text-blue-400 text-[9px]">km</span></div>
</div>
</div>
</div>
{/* Table Area */} {/* Table Area */}
<div className="flex-1 overflow-hidden flex flex-col"> <div className="flex-1 overflow-hidden flex flex-col">
<div className="px-3 py-2 border-b border-slate-800 flex justify-between items-center flex-shrink-0"> <div className="px-3 py-1.5 border-b border-slate-800/60 flex justify-end items-center flex-shrink-0">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest"></span>
<div className="flex items-center gap-3 text-[9px] text-slate-500"> <div className="flex items-center gap-3 text-[9px] text-slate-500">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-2 h-2 rounded-full bg-green-500"></div> <div className="w-1.5 h-1.5 rounded-full bg-green-500"></div>
<span>线</span> <span>线</span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-2 h-2 rounded-full bg-slate-500"></div> <div className="w-1.5 h-1.5 rounded-full bg-slate-500"></div>
<span>线</span> <span>线</span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-2 h-2 rounded-full bg-amber-400"></div> <div className="w-1.5 h-1.5 rounded-full bg-amber-400"></div>
<span></span> <span></span>
</div> </div>
<span>: {new Date().toLocaleTimeString()}</span> <span className="text-slate-600">{new Date().toLocaleTimeString()}</span>
</div> </div>
</div> </div>
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<table className="w-full text-left border-collapse"> <table className="w-full text-left border-collapse">
<thead className="sticky top-0 bg-slate-900 z-10"> <thead className="sticky top-0 bg-slate-900 z-10">
<tr className="border-b border-slate-800"> <tr className="border-b border-slate-800/60">
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase"> <th className="px-3 py-2 text-[10px] font-bold text-slate-500 uppercase w-12 text-center"></th>
<div className="flex flex-col gap-1.5"> <th className="px-3 py-2 text-[10px] font-bold text-slate-500 uppercase">
<div className="flex flex-col gap-1">
<span></span> <span></span>
<select <select
className="bg-slate-800 border-none rounded-lg px-2 py-1 text-[9px] text-slate-300 outline-none focus:ring-1 focus:ring-blue-500/30" className="bg-slate-800 border-none rounded px-2 py-0.5 text-[9px] text-slate-300 outline-none focus:ring-1 focus:ring-blue-500/30"
value={filterPlate} value={filterPlate}
onChange={(e) => setFilterPlate(e.target.value)} onChange={(e) => setFilterPlate(e.target.value)}
> >
@@ -325,12 +318,11 @@ export default function MonitoringView() {
</select> </select>
</div> </div>
</th> </th>
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase">线</th> <th className="px-3 py-2 text-[10px] font-bold text-slate-500 uppercase">
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase"> <div className="flex flex-col gap-1">
<div className="flex flex-col gap-1.5">
<span></span> <span></span>
<select <select
className="bg-slate-800 border-none rounded-lg px-2 py-1 text-[9px] text-slate-300 outline-none focus:ring-1 focus:ring-blue-500/30" className="bg-slate-800 border-none rounded px-2 py-0.5 text-[9px] text-slate-300 outline-none focus:ring-1 focus:ring-blue-500/30"
value={filterCustomer} value={filterCustomer}
onChange={(e) => setFilterCustomer(e.target.value)} onChange={(e) => setFilterCustomer(e.target.value)}
> >
@@ -339,11 +331,24 @@ export default function MonitoringView() {
</select> </select>
</div> </div>
</th> </th>
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase"> <th className="px-3 py-2 text-[10px] font-bold text-slate-500 uppercase">
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1">
<span></span> <span></span>
<select <select
className="bg-slate-800 border-none rounded-lg px-2 py-1 text-[9px] text-slate-300 outline-none focus:ring-1 focus:ring-blue-500/30" className="bg-slate-800 border-none rounded px-2 py-0.5 text-[9px] text-slate-300 outline-none focus:ring-1 focus:ring-blue-500/30"
value={filterRentStatus}
onChange={(e) => setFilterRentStatus(e.target.value)}
>
<option value="All"></option>
{filterOptions.rentStatuses.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
</th>
<th className="px-3 py-2 text-[10px] font-bold text-slate-500 uppercase">
<div className="flex flex-col gap-1">
<span></span>
<select
className="bg-slate-800 border-none rounded px-2 py-0.5 text-[9px] text-slate-300 outline-none focus:ring-1 focus:ring-blue-500/30"
value={filterDept} value={filterDept}
onChange={(e) => setFilterDept(e.target.value)} onChange={(e) => setFilterDept(e.target.value)}
> >
@@ -353,7 +358,7 @@ export default function MonitoringView() {
</div> </div>
</th> </th>
<th <th
className="p-4 text-[10px] font-bold text-slate-500 uppercase text-right cursor-pointer hover:text-blue-400 transition-colors" className="px-3 py-2 text-[10px] font-bold text-slate-500 uppercase text-right cursor-pointer hover:text-blue-400 transition-colors"
onClick={() => { onClick={() => {
if (sortBy === 'today') { if (sortBy === 'today') {
setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc'); setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc');
@@ -371,7 +376,7 @@ export default function MonitoringView() {
</div> </div>
</th> </th>
<th <th
className="p-4 text-[10px] font-bold text-slate-500 uppercase text-right cursor-pointer hover:text-blue-400 transition-colors" className="px-3 py-2 text-[10px] font-bold text-slate-500 uppercase text-right cursor-pointer hover:text-blue-400 transition-colors"
onClick={() => { onClick={() => {
if (sortBy === 'total') { if (sortBy === 'total') {
setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc'); setSortOrder(sortOrder === 'desc' ? 'asc' : 'desc');
@@ -388,46 +393,26 @@ export default function MonitoringView() {
)} )}
</div> </div>
</th> </th>
<th className="p-4 text-[10px] font-bold text-slate-500 uppercase"></th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-slate-800/50"> <tbody className="divide-y divide-slate-800/30">
{filteredVehicles.map((v) => ( {filteredVehicles.map((v) => (
<tr key={v.plate} className="hover:bg-slate-800/30 transition-colors group"> <tr key={v.plate} className="hover:bg-slate-800/20 transition-colors">
<td className="p-4 text-sm font-bold text-white">{v.plate}</td> <td className="px-3 py-2 text-center">
<td className="p-4"> <div className={`w-2 h-2 rounded-full mx-auto ${v.isOnline ? 'bg-green-500 shadow-[0_0_6px_rgba(34,197,94,0.4)]' : v.isDataSynced ? 'bg-slate-600' : 'bg-amber-400 animate-pulse'}`}></div>
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${v.isOnline ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.4)]' : 'bg-slate-600'}`}></div>
<span className={`text-[10px] font-bold ${v.isOnline ? 'text-green-500' : 'text-slate-500'}`}>
{v.isOnline ? '在线' : '离线'}
</span>
</div>
</td> </td>
<td className="p-4 text-xs text-slate-400">{v.customer}</td> <td className="px-3 py-2 text-xs font-bold text-white">{v.plate}</td>
<td className="p-4 text-xs text-slate-400">{v.department || v.rentStatus || ''}</td> <td className="px-3 py-2 text-[11px] text-slate-400">{v.customer || '-'}</td>
<td className="p-4 text-right"> <td className="px-3 py-2 text-[11px] text-slate-400">{v.rentStatus || '-'}</td>
<div className="flex flex-col items-end"> <td className="px-3 py-2 text-[11px] text-slate-400">{v.department || '-'}</td>
<div className="flex items-center gap-1.5"> <td className="px-3 py-2 text-right">
{!v.isDataSynced && ( <span className={`text-xs font-mono font-bold ${v.isDataSynced ? 'text-blue-400' : 'text-amber-400'}`}>
<div className="w-1.5 h-1.5 rounded-full bg-amber-400 animate-pulse"></div> {v.isDataSynced ? <>{v.dailyKm?.toLocaleString()} <span className="text-[8px] text-slate-500">km</span></> : '-'}
)}
<span className={`text-sm font-mono font-bold ${v.isDataSynced ? 'text-blue-400' : 'text-amber-400'}`}>
{v.dailyKm?.toLocaleString()} <span className="text-[8px] text-slate-500 font-bold">km</span>
</span> </span>
</div>
{!v.isDataSynced && <span className="text-[8px] text-amber-500/50 font-bold"></span>}
</div>
</td> </td>
<td className="p-4 text-right"> <td className="px-3 py-2 text-right">
<div className="flex flex-col items-end"> <span className={`text-xs font-mono font-bold ${v.isDataSynced ? 'text-slate-300' : 'text-slate-600'}`}>
<span className={`text-sm font-mono font-bold ${v.isDataSynced ? 'text-slate-300' : 'text-amber-400/70'}`}> {v.isDataSynced && v.totalKm != null ? <>{v.totalKm.toLocaleString()} <span className="text-[8px] text-slate-500">km</span></> : '-'}
{v.totalKm?.toLocaleString()} <span className="text-[8px] text-slate-500 font-bold">km</span>
</span>
</div>
</td>
<td className="p-4">
<span className={`px-2 py-0.5 rounded-full ${v.isOnline ? 'bg-green-500/10 text-green-500' : 'bg-slate-500/10 text-slate-500'} text-[9px] font-bold uppercase`}>
{v.isOnline ? '运行中' : '静止/离线'}
</span> </span>
</td> </td>
</tr> </tr>
@@ -514,7 +499,7 @@ export default function MonitoringView() {
<button <button
onClick={() => setIsFilterOpen(!isFilterOpen)} onClick={() => setIsFilterOpen(!isFilterOpen)}
className={`p-1.5 rounded-lg transition-all flex-shrink-0 ${isFilterOpen || searchTerm || filterDept !== 'All' || filterCustomer !== 'All' || filterPlate !== 'All' || filterProject !== 'All' ? 'bg-blue-50 text-blue-600 border border-blue-100' : 'bg-slate-50 text-slate-400 border border-transparent'}`} className={`p-1.5 rounded-lg transition-all flex-shrink-0 ${isFilterOpen || searchTerm || filterDept !== 'All' || filterCustomer !== 'All' || filterRentStatus !== 'All' || filterPlate !== 'All' || filterProject !== 'All' ? 'bg-blue-50 text-blue-600 border border-blue-100' : 'bg-slate-50 text-slate-400 border border-transparent'}`}
> >
<Filter size={16} /> <Filter size={16} />
</button> </button>
@@ -569,6 +554,21 @@ export default function MonitoringView() {
</select> </select>
</div> </div>
{/* Rent Status */}
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-wider"></label>
<select
className="w-full bg-slate-50 border-none rounded-xl py-2 px-3 text-xs focus:ring-2 focus:ring-blue-500/20 outline-none"
value={filterRentStatus}
onChange={(e) => setFilterRentStatus(e.target.value)}
>
<option value="All"></option>
{filterOptions.rentStatuses.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
{/* Entity */} {/* Entity */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-wider"></label> <label className="text-[10px] font-bold text-slate-400 uppercase tracking-wider"></label>
@@ -657,6 +657,7 @@ export default function MonitoringView() {
{/* Active Filter Tags */} {/* Active Filter Tags */}
{(() => { {(() => {
const tags: { label: string; onClear: () => void }[] = []; const tags: { label: string; onClear: () => void }[] = [];
if (filterRentStatus !== 'All') tags.push({ label: `状态: ${filterRentStatus}`, onClear: () => setFilterRentStatus('All') });
if (filterDept !== 'All') tags.push({ label: `部门: ${filterDept}`, onClear: () => setFilterDept('All') }); if (filterDept !== 'All') tags.push({ label: `部门: ${filterDept}`, onClear: () => setFilterDept('All') });
if (filterCustomer !== 'All') tags.push({ label: `客户: ${filterCustomer}`, onClear: () => setFilterCustomer('All') }); if (filterCustomer !== 'All') tags.push({ label: `客户: ${filterCustomer}`, onClear: () => setFilterCustomer('All') });
if (filterProject !== 'All') tags.push({ label: `项目: ${filterProject}`, onClear: () => setFilterProject('All') }); if (filterProject !== 'All') tags.push({ label: `项目: ${filterProject}`, onClear: () => setFilterProject('All') });
@@ -750,7 +751,7 @@ export default function MonitoringView() {
</span> </span>
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="text-[8px] text-slate-300 font-bold">{v.department ? v.department.replace('业务', '') : v.rentStatus || ''}</span> <span className="text-[8px] text-slate-300 font-bold">{v.rentStatus || ''}{v.department ? ` · ${v.department.replace('业务', '')}` : ''}</span>
<span className="text-[9px] font-bold text-slate-600 truncate">{v.customer || '-'}</span> <span className="text-[9px] font-bold text-slate-600 truncate">{v.customer || '-'}</span>
</div> </div>
</div> </div>
@@ -760,13 +761,15 @@ export default function MonitoringView() {
{!v.isDataSynced && ( {!v.isDataSynced && (
<div className="w-2 h-2 rounded-full bg-amber-400 animate-pulse" title="未对接车机数据"></div> <div className="w-2 h-2 rounded-full bg-amber-400 animate-pulse" title="未对接车机数据"></div>
)} )}
<span className="text-[7px] font-black text-blue-600/40 bg-blue-50 w-3 h-3 rounded flex items-center justify-center leading-none"></span>
<div className={`text-sm font-black leading-none ${v.isDataSynced ? 'text-blue-600' : 'text-amber-600'}`}> <div className={`text-sm font-black leading-none ${v.isDataSynced ? 'text-blue-600' : 'text-amber-600'}`}>
{v.dailyKm?.toLocaleString()} <span className="text-[8px] text-slate-400">km</span> {v.isDataSynced ? <>{v.dailyKm?.toLocaleString()} <span className="text-[8px] text-slate-400">km</span></> : '-'}
</div> </div>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<span className="text-[7px] font-black text-slate-400/60 bg-slate-100 w-3 h-3 rounded flex items-center justify-center leading-none"></span>
<span className="text-[8px] font-bold text-slate-300"> <span className="text-[8px] font-bold text-slate-300">
{v.totalKm?.toLocaleString()} km {v.isDataSynced && v.totalKm != null ? `${v.totalKm.toLocaleString()} km` : '-'}
</span> </span>
{!v.isDataSynced && ( {!v.isDataSynced && (
<span className="text-[7px] font-bold text-amber-500/70 bg-amber-50 px-1 rounded"></span> <span className="text-[7px] font-bold text-amber-500/70 bg-amber-50 px-1 rounded"></span>

View File

@@ -18,6 +18,7 @@ export async function fetchMonitoring(params?: {
customer?: string; customer?: string;
project?: string; project?: string;
entity?: string; entity?: string;
rentStatus?: string;
plate?: string; plate?: string;
mileageMin?: string; mileageMin?: string;
mileageMax?: string; mileageMax?: string;
@@ -33,6 +34,7 @@ export async function fetchMonitoring(params?: {
if (params?.customer) query.set('customer', params.customer); if (params?.customer) query.set('customer', params.customer);
if (params?.project) query.set('project', params.project); if (params?.project) query.set('project', params.project);
if (params?.entity) query.set('entity', params.entity); if (params?.entity) query.set('entity', params.entity);
if (params?.rentStatus) query.set('rentStatus', params.rentStatus);
if (params?.plate) query.set('plate', params.plate); if (params?.plate) query.set('plate', params.plate);
if (params?.mileageMin) query.set('mileageMin', params.mileageMin); if (params?.mileageMin) query.set('mileageMin', params.mileageMin);
if (params?.mileageMax) query.set('mileageMax', params.mileageMax); if (params?.mileageMax) query.set('mileageMax', params.mileageMax);

View File

@@ -27,6 +27,7 @@ export interface MonitoringFilters {
plates: string[]; plates: string[];
projects: string[]; projects: string[];
entities: string[]; entities: string[];
rentStatuses: string[];
} }
export interface MonitoringData { export interface MonitoringData {

View File

@@ -45,7 +45,7 @@ interface CachedVehicle {
interface MonitoringCache { interface MonitoringCache {
vehicles: CachedVehicle[]; vehicles: CachedVehicle[];
stats: { totalToday: number; totalAll: number; vehicleCount: number }; stats: { totalToday: number; totalAll: number; vehicleCount: number };
filters: { departments: string[]; customers: string[]; plates: string[]; projects: string[]; entities: string[] }; filters: { departments: string[]; customers: string[]; plates: string[]; projects: string[]; entities: string[]; rentStatuses: string[] };
updatedAt: string; updatedAt: string;
} }
@@ -142,11 +142,12 @@ async function refreshMonitoringCache() {
const plates = vehicles.map(v => v.plate); const plates = vehicles.map(v => v.plate);
const projects = Array.from(new Set(vehicles.map(v => v.project).filter(Boolean))) as string[]; const projects = Array.from(new Set(vehicles.map(v => v.project).filter(Boolean))) as string[];
const entities = Array.from(new Set(vehicles.map(v => v.entity).filter(Boolean))) as string[]; const entities = Array.from(new Set(vehicles.map(v => v.entity).filter(Boolean))) as string[];
const rentStatuses = Array.from(new Set(vehicles.map(v => v.rentStatus).filter(Boolean))) as string[];
monitoringCache = { monitoringCache = {
vehicles, vehicles,
stats: { totalToday, totalAll, vehicleCount: vehicles.length }, stats: { totalToday, totalAll, vehicleCount: vehicles.length },
filters: { departments, customers, plates, projects, entities }, filters: { departments, customers, plates, projects, entities, rentStatuses },
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}; };
@@ -199,7 +200,7 @@ async function queryDateMileage(dateStr: string): Promise<{ vehicles: CachedVehi
// GET /monitoring — 从缓存取数据(或指定日期实时查询),支持筛选/排序/分页 // GET /monitoring — 从缓存取数据(或指定日期实时查询),支持筛选/排序/分页
app.get('/monitoring', async (c) => { app.get('/monitoring', async (c) => {
const emptyResponse = { vehicles: [], stats: { totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }, filters: { departments: [], customers: [], plates: [], projects: [], entities: [] }, total: 0, page: 1, totalPages: 1, updatedAt: new Date().toISOString() }; const emptyResponse = { vehicles: [], stats: { totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 }, filters: { departments: [], customers: [], plates: [], projects: [], entities: [], rentStatuses: [] }, total: 0, page: 1, totalPages: 1, updatedAt: new Date().toISOString() };
const sortBy = c.req.query('sortBy') || 'today'; const sortBy = c.req.query('sortBy') || 'today';
const sortOrder = c.req.query('sortOrder') || 'desc'; const sortOrder = c.req.query('sortOrder') || 'desc';
@@ -213,6 +214,7 @@ app.get('/monitoring', async (c) => {
const mileageMin = c.req.query('mileageMin') || ''; const mileageMin = c.req.query('mileageMin') || '';
const mileageMax = c.req.query('mileageMax') || ''; const mileageMax = c.req.query('mileageMax') || '';
const plate = c.req.query('plate') || ''; const plate = c.req.query('plate') || '';
const rentStatus = c.req.query('rentStatus') || '';
const date = c.req.query('date') || ''; const date = c.req.query('date') || '';
let allVehicles: CachedVehicle[]; let allVehicles: CachedVehicle[];
@@ -233,6 +235,7 @@ app.get('/monitoring', async (c) => {
plates: allVehicles.map(v => v.plate), plates: allVehicles.map(v => v.plate),
projects: Array.from(new Set(allVehicles.map(v => v.project).filter(Boolean))) as string[], projects: Array.from(new Set(allVehicles.map(v => v.project).filter(Boolean))) as string[],
entities: Array.from(new Set(allVehicles.map(v => v.entity).filter(Boolean))) as string[], entities: Array.from(new Set(allVehicles.map(v => v.entity).filter(Boolean))) as string[],
rentStatuses: Array.from(new Set(allVehicles.map(v => v.rentStatus).filter(Boolean))) as string[],
}; };
} catch (e) { } catch (e) {
console.error('monitoring date query error:', e); console.error('monitoring date query error:', e);
@@ -259,6 +262,7 @@ app.get('/monitoring', async (c) => {
if (customer) vehicles = vehicles.filter(v => v.customer === customer); if (customer) vehicles = vehicles.filter(v => v.customer === customer);
if (project) vehicles = vehicles.filter(v => v.project === project); if (project) vehicles = vehicles.filter(v => v.project === project);
if (entity) vehicles = vehicles.filter(v => v.entity === entity); if (entity) vehicles = vehicles.filter(v => v.entity === entity);
if (rentStatus) vehicles = vehicles.filter(v => v.rentStatus === rentStatus);
if (plate) vehicles = vehicles.filter(v => v.plate === plate); if (plate) vehicles = vehicles.filter(v => v.plate === plate);
if (mileageMin) vehicles = vehicles.filter(v => v.dailyKm >= Number(mileageMin)); if (mileageMin) vehicles = vehicles.filter(v => v.dailyKm >= Number(mileageMin));
if (mileageMax) vehicles = vehicles.filter(v => v.dailyKm <= Number(mileageMax)); if (mileageMax) vehicles = vehicles.filter(v => v.dailyKm <= Number(mileageMax));