feat(platform): add realtime auto refresh controls

This commit is contained in:
lingniu
2026-07-04 17:07:51 +08:00
parent 5ca6b51d0b
commit 34a9def4ff
2 changed files with 93 additions and 1 deletions

View File

@@ -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<OpsHealth | null>(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<string, string>) => {
setFilters(nextFilters);
onFiltersChange?.(nextFilters);
@@ -367,6 +390,18 @@ export function Realtime({
}}></Button>
</Space>
</Form>
<div className="vp-table-toolbar">
<Space wrap>
<Tag color={autoRefresh ? 'green' : 'grey'}> {refreshIntervalSeconds}</Tag>
<Typography.Text type="secondary">{lastRefreshAt || '等待首次刷新'}</Typography.Text>
<Button size="small" theme="light" aria-label="刷新实时数据" loading={loading} onClick={() => load(filters, pagination.currentPage, pagination.pageSize)}>
</Button>
<Button size="small" theme="light" aria-label={autoRefresh ? '暂停自动刷新' : '开启自动刷新'} onClick={() => setAutoRefresh((value) => !value)}>
{autoRefresh ? '暂停自动刷新' : '开启自动刷新'}
</Button>
</Space>
</div>
{filterSummary.length > 0 ? (
<Card bordered title="当前实时筛选" style={{ marginBottom: 12 }}>
<Space wrap>

View File

@@ -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(<App />);
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');