feat: 租赁状态与部门分列筛选,未同步车辆显示-,卡片增加今/总标签,全屏监控压缩优化
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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:
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user