feat: 支持查询指定日期里程+删除搜索关键词和车牌号筛选
- 后端支持 date 参数,指定日期时实时查询数据库(不用缓存) - 同时查询前一天数据计算环比 - 高级筛选添加"查询日期"日期选择器 - 删除高级筛选中的"搜索关键词"和"车牌号"(已有快捷筛选) - 筛选标签支持显示日期条件 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -104,6 +104,7 @@ export default function MonitoringView() {
|
|||||||
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: '' });
|
||||||
|
const [filterDate, setFilterDate] = useState('');
|
||||||
|
|
||||||
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 });
|
||||||
@@ -135,6 +136,7 @@ export default function MonitoringView() {
|
|||||||
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,
|
||||||
|
date: filterDate || undefined,
|
||||||
}).then(d => {
|
}).then(d => {
|
||||||
setVehicles(d.vehicles);
|
setVehicles(d.vehicles);
|
||||||
setStats(d.stats);
|
setStats(d.stats);
|
||||||
@@ -143,7 +145,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]);
|
}, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterPlate, appliedMileageRange, filterDate]);
|
||||||
|
|
||||||
// 加载更多
|
// 加载更多
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
@@ -163,12 +165,13 @@ export default function MonitoringView() {
|
|||||||
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,
|
||||||
|
date: filterDate || undefined,
|
||||||
}).then(d => {
|
}).then(d => {
|
||||||
setVehicles(prev => [...prev, ...d.vehicles]);
|
setVehicles(prev => [...prev, ...d.vehicles]);
|
||||||
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, page, loadingMore, hasMore]);
|
}, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterPlate, appliedMileageRange, filterDate, page, loadingMore, hasMore]);
|
||||||
|
|
||||||
// 筛选/排序变化时重新加载
|
// 筛选/排序变化时重新加载
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -529,35 +532,15 @@ export default function MonitoringView() {
|
|||||||
className="overflow-hidden"
|
className="overflow-hidden"
|
||||||
>
|
>
|
||||||
<div className="bg-white p-4 rounded-2xl border border-gray-100 shadow-sm mb-2 space-y-4">
|
<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">
|
<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>
|
||||||
<div className="relative">
|
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-300" size={14} />
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="date"
|
||||||
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"
|
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}
|
value={filterDate}
|
||||||
onChange={(e) => setFilterPlate(e.target.value)}
|
onChange={(e) => setFilterDate(e.target.value)}
|
||||||
>
|
/>
|
||||||
<option value="All">无限制</option>
|
|
||||||
{plateNumbers.map(p => <option key={p} value={p}>{p}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Project */}
|
{/* 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.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 (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 (filterRegionCode !== 'All') tags.push({ label: `地区: ${filterRegionCode}`, onClear: () => setFilterRegionCode('All') });
|
||||||
|
if (filterDate) tags.push({ label: `日期: ${filterDate}`, onClear: () => setFilterDate('') });
|
||||||
if (tags.length === 0) return null;
|
if (tags.length === 0) return null;
|
||||||
const clearAll = () => {
|
const clearAll = () => {
|
||||||
setFilterDept('All'); setFilterCustomer('All'); setFilterProject('All'); setFilterEntity('All');
|
setFilterDept('All'); setFilterCustomer('All'); setFilterProject('All'); setFilterEntity('All');
|
||||||
setFilterPlate('All'); setSearchTerm(''); setFilterRegionCode('All');
|
setFilterPlate('All'); setSearchTerm(''); setFilterRegionCode('All');
|
||||||
setFilterMileageRange({ min: '', max: '' }); setAppliedMileageRange({ min: '', max: '' });
|
setFilterMileageRange({ min: '', max: '' }); setAppliedMileageRange({ min: '', max: '' });
|
||||||
|
setFilterDate('');
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export async function fetchMonitoring(params?: {
|
|||||||
plate?: string;
|
plate?: string;
|
||||||
mileageMin?: string;
|
mileageMin?: string;
|
||||||
mileageMax?: string;
|
mileageMax?: string;
|
||||||
|
date?: string;
|
||||||
}): Promise<MonitoringData> {
|
}): Promise<MonitoringData> {
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
if (params?.sortBy) query.set('sortBy', params.sortBy);
|
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?.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);
|
||||||
|
if (params?.date) query.set('date', params.date);
|
||||||
const qs = query.toString();
|
const qs = query.toString();
|
||||||
return fetchJson<MonitoringData>(`${BASE}/monitoring${qs ? `?${qs}` : ''}`);
|
return fetchJson<MonitoringData>(`${BASE}/monitoring${qs ? `?${qs}` : ''}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,11 +152,43 @@ async function refreshMonitoringCache() {
|
|||||||
refreshMonitoringCache();
|
refreshMonitoringCache();
|
||||||
setInterval(refreshMonitoringCache, 2 * 60 * 1000);
|
setInterval(refreshMonitoringCache, 2 * 60 * 1000);
|
||||||
|
|
||||||
// GET /monitoring — 从缓存取数据,支持筛选/排序/分页
|
// 查询指定日期的里程数据(非缓存)
|
||||||
app.get('/monitoring', (c) => {
|
async function queryDateMileage(dateStr: string): Promise<{ vehicles: CachedVehicle[]; yesterdayTotal: number }> {
|
||||||
if (!monitoringCache) {
|
const [mileageRows] = await mileagePool.execute(
|
||||||
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() });
|
`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 sortBy = c.req.query('sortBy') || 'today';
|
||||||
const sortOrder = c.req.query('sortOrder') || 'desc';
|
const sortOrder = c.req.query('sortOrder') || 'desc';
|
||||||
@@ -170,8 +202,43 @@ app.get('/monitoring', (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 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) {
|
if (search) {
|
||||||
@@ -197,7 +264,7 @@ app.get('/monitoring', (c) => {
|
|||||||
totalToday: vehicles.reduce((sum, v) => sum + v.dailyKm, 0),
|
totalToday: vehicles.reduce((sum, v) => sum + v.dailyKm, 0),
|
||||||
totalAll: vehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0),
|
totalAll: vehicles.reduce((sum, v) => sum + (v.totalKm || 0), 0),
|
||||||
vehicleCount: vehicles.length,
|
vehicleCount: vehicles.length,
|
||||||
yesterdayTotal: monitoringCache.stats.yesterdayTotal,
|
yesterdayTotal,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 排序
|
// 排序
|
||||||
@@ -214,11 +281,11 @@ app.get('/monitoring', (c) => {
|
|||||||
return c.json({
|
return c.json({
|
||||||
vehicles: paged,
|
vehicles: paged,
|
||||||
stats: filteredStats,
|
stats: filteredStats,
|
||||||
filters: monitoringCache.filters,
|
filters,
|
||||||
total,
|
total,
|
||||||
page,
|
page,
|
||||||
totalPages: Math.ceil(total / limit),
|
totalPages: Math.ceil(total / limit),
|
||||||
updatedAt: monitoringCache.updatedAt,
|
updatedAt: date || monitoringCache?.updatedAt || new Date().toISOString(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user