feat: 支持查询指定日期里程+删除搜索关键词和车牌号筛选

- 后端支持 date 参数,指定日期时实时查询数据库(不用缓存)
- 同时查询前一天数据计算环比
- 高级筛选添加"查询日期"日期选择器
- 删除高级筛选中的"搜索关键词"和"车牌号"(已有快捷筛选)
- 筛选标签支持显示日期条件

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-01 23:41:03 +08:00
parent cbf0e18634
commit 0b8bbbb063
3 changed files with 92 additions and 38 deletions

View File

@@ -104,6 +104,7 @@ export default function MonitoringView() {
const [filterRegionCode, setFilterRegionCode] = useState('All');
const [filterMileageRange, setFilterMileageRange] = useState({ min: '', max: '' });
const [appliedMileageRange, setAppliedMileageRange] = useState({ min: '', max: '' });
const [filterDate, setFilterDate] = useState('');
const [vehicles, setVehicles] = useState<MonitoringVehicle[]>([]);
const [stats, setStats] = useState<MonitoringStats>({ totalToday: 0, totalAll: 0, vehicleCount: 0, yesterdayTotal: 0 });
@@ -135,6 +136,7 @@ export default function MonitoringView() {
plate: filterPlate !== 'All' ? filterPlate : undefined,
mileageMin: appliedMileageRange.min || undefined,
mileageMax: appliedMileageRange.max || undefined,
date: filterDate || undefined,
}).then(d => {
setVehicles(d.vehicles);
setStats(d.stats);
@@ -143,7 +145,7 @@ export default function MonitoringView() {
setPage(1);
setHasMore(d.page < d.totalPages);
}).catch(() => {});
}, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterPlate, appliedMileageRange]);
}, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterPlate, appliedMileageRange, filterDate]);
// 加载更多
const loadMore = useCallback(() => {
@@ -163,12 +165,13 @@ export default function MonitoringView() {
plate: filterPlate !== 'All' ? filterPlate : undefined,
mileageMin: appliedMileageRange.min || undefined,
mileageMax: appliedMileageRange.max || undefined,
date: filterDate || undefined,
}).then(d => {
setVehicles(prev => [...prev, ...d.vehicles]);
setPage(nextPage);
setHasMore(nextPage < d.totalPages);
}).catch(() => {}).finally(() => setLoadingMore(false));
}, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterPlate, appliedMileageRange, page, loadingMore, hasMore]);
}, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterPlate, appliedMileageRange, filterDate, page, loadingMore, hasMore]);
// 筛选/排序变化时重新加载
useEffect(() => {
@@ -529,35 +532,15 @@ export default function MonitoringView() {
className="overflow-hidden"
>
<div className="bg-white p-4 rounded-2xl border border-gray-100 shadow-sm mb-2 space-y-4">
{/* Search Key */}
{/* Date */}
<div className="space-y-1.5">
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-wider"></label>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-300" size={14} />
<input
type="text"
placeholder="车牌、项目、车型..."
className="w-full pl-9 pr-4 py-2 bg-slate-50 border-none rounded-xl text-xs focus:ring-2 focus:ring-blue-500/20"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
{/* Plate Number */}
<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={filterPlate}
onChange={(e) => setFilterPlate(e.target.value)}
>
<option value="All"></option>
{plateNumbers.map(p => <option key={p} value={p}>{p}</option>)}
</select>
</div>
<label className="text-[10px] font-bold text-slate-400 uppercase tracking-wider"></label>
<input
type="date"
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={filterDate}
onChange={(e) => setFilterDate(e.target.value)}
/>
</div>
{/* Project */}
@@ -684,11 +667,13 @@ export default function MonitoringView() {
if (appliedMileageRange.min) tags.push({ label: `里程≥${appliedMileageRange.min}`, onClear: () => { setFilterMileageRange(prev => ({ ...prev, min: '' })); setAppliedMileageRange(prev => ({ ...prev, min: '' })); } });
if (appliedMileageRange.max) tags.push({ label: `里程≤${appliedMileageRange.max}`, onClear: () => { setFilterMileageRange(prev => ({ ...prev, max: '' })); setAppliedMileageRange(prev => ({ ...prev, max: '' })); } });
if (filterRegionCode !== 'All') tags.push({ label: `地区: ${filterRegionCode}`, onClear: () => setFilterRegionCode('All') });
if (filterDate) tags.push({ label: `日期: ${filterDate}`, onClear: () => setFilterDate('') });
if (tags.length === 0) return null;
const clearAll = () => {
setFilterDept('All'); setFilterCustomer('All'); setFilterProject('All'); setFilterEntity('All');
setFilterPlate('All'); setSearchTerm(''); setFilterRegionCode('All');
setFilterMileageRange({ min: '', max: '' }); setAppliedMileageRange({ min: '', max: '' });
setFilterDate('');
};
return (
<div className="flex items-center gap-2 flex-wrap">

View File

@@ -21,6 +21,7 @@ export async function fetchMonitoring(params?: {
plate?: string;
mileageMin?: string;
mileageMax?: string;
date?: string;
}): Promise<MonitoringData> {
const query = new URLSearchParams();
if (params?.sortBy) query.set('sortBy', params.sortBy);
@@ -35,6 +36,7 @@ export async function fetchMonitoring(params?: {
if (params?.plate) query.set('plate', params.plate);
if (params?.mileageMin) query.set('mileageMin', params.mileageMin);
if (params?.mileageMax) query.set('mileageMax', params.mileageMax);
if (params?.date) query.set('date', params.date);
const qs = query.toString();
return fetchJson<MonitoringData>(`${BASE}/monitoring${qs ? `?${qs}` : ''}`);
}

View File

@@ -152,11 +152,43 @@ async function refreshMonitoringCache() {
refreshMonitoringCache();
setInterval(refreshMonitoringCache, 2 * 60 * 1000);
// GET /monitoring — 从缓存取数据,支持筛选/排序/分页
app.get('/monitoring', (c) => {
if (!monitoringCache) {
return c.json({ 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() });
// 查询指定日期的里程数据(非缓存)
async function queryDateMileage(dateStr: string): Promise<{ vehicles: CachedVehicle[]; yesterdayTotal: number }> {
const [mileageRows] = await mileagePool.execute(
`SELECT plate, vin, daily_km, total_km, source FROM v_vehicle_daily_stats WHERE stat_date = ?`,
[dateStr]
) as any;
const [yesterdayRows] = await mileagePool.execute(
`SELECT SUM(daily_km) as total FROM v_vehicle_daily_stats WHERE stat_date = DATE_SUB(?, INTERVAL 1 DAY)`,
[dateStr]
) as any;
const [infoRows] = await pool.execute(VEHICLE_INFO_SQL) as any;
const infoMap = new Map<string, any>();
for (const row of infoRows) infoMap.set(row.plate, row);
const mileageMap = new Map<string, any>();
for (const row of mileageRows) {
const existing = mileageMap.get(row.plate);
if (!existing || Number(row.daily_km) > Number(existing.daily_km)) mileageMap.set(row.plate, row);
}
const vehicles: CachedVehicle[] = Array.from(mileageMap.values()).map((m: any) => {
const info = infoMap.get(m.plate);
const dailyKm = Number(m.daily_km) || 0;
const source = m.source || 'NONE';
return {
plate: m.plate, vin: m.vin, dailyKm,
totalKm: m.total_km !== null ? Number(m.total_km) : null,
source, isOnline: source !== 'NONE' && dailyKm > 0, isDataSynced: source !== 'NONE',
customer: info?.customer || null, department: info?.department || null,
manager: info?.manager || null, rentStatus: info?.rent_status || null,
entity: info?.entity || null, project: info?.project || null,
};
});
return { vehicles, yesterdayTotal: Number(yesterdayRows[0]?.total) || 0 };
}
// GET /monitoring — 从缓存取数据(或指定日期实时查询),支持筛选/排序/分页
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 sortBy = c.req.query('sortBy') || 'today';
const sortOrder = c.req.query('sortOrder') || 'desc';
@@ -170,8 +202,43 @@ app.get('/monitoring', (c) => {
const mileageMin = c.req.query('mileageMin') || '';
const mileageMax = c.req.query('mileageMax') || '';
const plate = c.req.query('plate') || '';
const date = c.req.query('date') || '';
let vehicles = monitoringCache.vehicles;
let allVehicles: CachedVehicle[];
let yesterdayTotal: number;
let filters: MonitoringCache['filters'];
if (date) {
// 指定日期:实时查询
try {
const result = await queryDateMileage(date);
allVehicles = result.vehicles;
yesterdayTotal = result.yesterdayTotal;
// 从查询结果提取筛选选项
const deptOrder = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
filters = {
departments: (Array.from(new Set(allVehicles.map(v => v.department).filter(Boolean))) as string[]).sort((a, b) => {
const ai = deptOrder.findIndex(d => a.includes(d)); const bi = deptOrder.findIndex(d => b.includes(d));
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
}),
customers: Array.from(new Set(allVehicles.map(v => v.customer).filter(Boolean))) as string[],
plates: allVehicles.map(v => v.plate),
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[],
};
} catch (e) {
console.error('monitoring date query error:', e);
return c.json(emptyResponse, 500);
}
} else {
// 默认:从缓存取
if (!monitoringCache) return c.json(emptyResponse);
allVehicles = monitoringCache.vehicles;
yesterdayTotal = monitoringCache.stats.yesterdayTotal;
filters = monitoringCache.filters;
}
let vehicles = allVehicles;
// 筛选
if (search) {
@@ -197,7 +264,7 @@ app.get('/monitoring', (c) => {
totalToday: vehicles.reduce((sum, v) => sum + v.dailyKm, 0),
totalAll: vehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0),
vehicleCount: vehicles.length,
yesterdayTotal: monitoringCache.stats.yesterdayTotal,
yesterdayTotal,
};
// 排序
@@ -214,11 +281,11 @@ app.get('/monitoring', (c) => {
return c.json({
vehicles: paged,
stats: filteredStats,
filters: monitoringCache.filters,
filters,
total,
page,
totalPages: Math.ceil(total / limit),
updatedAt: monitoringCache.updatedAt,
updatedAt: date || monitoringCache?.updatedAt || new Date().toISOString(),
});
});