From 34a9def4ff7f6144328cea04586e89735d92fa8b Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 4 Jul 2026 17:07:51 +0800 Subject: [PATCH] feat(platform): add realtime auto refresh controls --- .../apps/web/src/pages/Realtime.tsx | 37 +++++++++++- .../apps/web/src/test/App.test.tsx | 57 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx index bead242d..75db611c 100644 --- a/vehicle-data-platform/apps/web/src/pages/Realtime.tsx +++ b/vehicle-data-platform/apps/web/src/pages/Realtime.tsx @@ -71,6 +71,15 @@ function realtimeMapPointId(row: VehicleRealtimeRow, index = 0) { return row.vin?.trim() || `${row.primaryProtocol || 'source'}-${index}`; } +function formatRefreshTime(date: Date) { + return date.toLocaleTimeString('zh-CN', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + }); +} + function amapMarkerURL(row: VehicleRealtimeRow) { const name = encodeURIComponent(row.plate || row.vin || '车辆位置'); return `https://uri.amap.com/marker?position=${row.longitude},${row.latitude}&name=${name}&src=lingniu-vehicle-platform`; @@ -200,6 +209,9 @@ export function Realtime({ const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 50, total: 0 }); const [opsHealth, setOpsHealth] = useState(null); const [selectedMapPointId, setSelectedMapPointId] = useState(''); + const [lastRefreshAt, setLastRefreshAt] = useState(''); + const [autoRefresh, setAutoRefresh] = useState(true); + const refreshIntervalSeconds = 15; const amapConfig = getAMapConfig(); const runtime = opsHealth?.runtime; const amapConfigured = runtime?.amapWebJsConfigured ?? isAMapConfigured(amapConfig); @@ -214,10 +226,11 @@ export function Realtime({ if (values?.protocol) params.set('protocol', values.protocol); if (values?.online) params.set('online', values.online); if (values?.serviceStatus) params.set('serviceStatus', values.serviceStatus); - api.vehicleRealtime(params) + return api.vehicleRealtime(params) .then((nextPage) => { setRows(nextPage.items); setPagination({ currentPage: page, pageSize, total: nextPage.total }); + setLastRefreshAt(formatRefreshTime(new Date())); }) .catch((error: Error) => Toast.error(error.message)) .finally(() => setLoading(false)); @@ -231,6 +244,16 @@ export function Realtime({ .catch(() => setOpsHealth(null)); }, [JSON.stringify(initialFilters)]); + useEffect(() => { + if (!autoRefresh) { + return undefined; + } + const timer = window.setInterval(() => { + load(filters, pagination.currentPage, pagination.pageSize); + }, refreshIntervalSeconds * 1000); + return () => window.clearInterval(timer); + }, [autoRefresh, JSON.stringify(filters), pagination.currentPage, pagination.pageSize]); + const applyFilters = (nextFilters: Record) => { setFilters(nextFilters); onFiltersChange?.(nextFilters); @@ -367,6 +390,18 @@ export function Realtime({ }}>重置 +
+ + 自动刷新 {refreshIntervalSeconds}秒 + 最后刷新:{lastRefreshAt || '等待首次刷新'} + + + +
{filterSummary.length > 0 ? ( diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx index 0f90ff23..41114f7c 100644 --- a/vehicle-data-platform/apps/web/src/test/App.test.tsx +++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx @@ -7569,6 +7569,63 @@ test('frames realtime page as one vehicle service with source evidence', async ( expect(screen.getAllByText('2/2 来源在线').length).toBeGreaterThan(0); }); +test('refreshes realtime vehicle data from the monitoring workspace', async () => { + window.history.replaceState(null, '', '/#/realtime'); + let realtimeCalls = 0; + vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => { + const path = String(input); + if (path.includes('/api/realtime/vehicles')) { + realtimeCalls += 1; + return { + ok: true, + json: async () => ({ + data: { + items: [{ + vin: `VIN-REFRESH-${realtimeCalls}`, + plate: `粤A刷新${realtimeCalls}`, + protocols: ['JT808'], + sourceStatus: [{ protocol: 'JT808', online: true, lastSeen: '2026-07-03 20:12:10', hasRealtime: true, hasHistory: true, hasRaw: true, hasMileage: true }], + sourceCount: 1, + onlineSourceCount: 1, + online: true, + primaryProtocol: 'JT808', + longitude: 113.2, + latitude: 23.1, + speedKmh: 30, + socPercent: 78, + totalMileageKm: 119925, + lastSeen: '2026-07-03 20:12:10', + bindingStatus: 'bound' + }], + total: 1, + limit: 50, + offset: 0 + }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + } + return { + ok: true, + json: async () => ({ + data: { linkHealth: [], kafkaLag: 0, redisOnlineKeys: 0, tdengineWritable: true, mysqlWritable: true, runtime: { requestTimeoutMs: 5000 } }, + traceId: 'trace-test', + timestamp: 1783094400000 + }) + } as Response; + }); + + render(); + + expect((await screen.findAllByText('粤A刷新1')).length).toBeGreaterThan(0); + expect(screen.getByText('自动刷新 15秒')).toBeInTheDocument(); + expect(screen.getByText((content) => content.includes('最后刷新'))).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '刷新实时数据' })); + expect((await screen.findAllByText('粤A刷新2')).length).toBeGreaterThan(0); + expect(realtimeCalls).toBeGreaterThanOrEqual(2); +}); + test('exports current realtime vehicles as CSV', async () => { window.history.replaceState(null, '', '/#/realtime?protocol=JT808'); const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:realtime-export');