feat(platform): add realtime auto refresh controls
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user