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}`;
|
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) {
|
function amapMarkerURL(row: VehicleRealtimeRow) {
|
||||||
const name = encodeURIComponent(row.plate || row.vin || '车辆位置');
|
const name = encodeURIComponent(row.plate || row.vin || '车辆位置');
|
||||||
return `https://uri.amap.com/marker?position=${row.longitude},${row.latitude}&name=${name}&src=lingniu-vehicle-platform`;
|
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 [pagination, setPagination] = useState({ currentPage: 1, pageSize: 50, total: 0 });
|
||||||
const [opsHealth, setOpsHealth] = useState<OpsHealth | null>(null);
|
const [opsHealth, setOpsHealth] = useState<OpsHealth | null>(null);
|
||||||
const [selectedMapPointId, setSelectedMapPointId] = useState('');
|
const [selectedMapPointId, setSelectedMapPointId] = useState('');
|
||||||
|
const [lastRefreshAt, setLastRefreshAt] = useState('');
|
||||||
|
const [autoRefresh, setAutoRefresh] = useState(true);
|
||||||
|
const refreshIntervalSeconds = 15;
|
||||||
const amapConfig = getAMapConfig();
|
const amapConfig = getAMapConfig();
|
||||||
const runtime = opsHealth?.runtime;
|
const runtime = opsHealth?.runtime;
|
||||||
const amapConfigured = runtime?.amapWebJsConfigured ?? isAMapConfigured(amapConfig);
|
const amapConfigured = runtime?.amapWebJsConfigured ?? isAMapConfigured(amapConfig);
|
||||||
@@ -214,10 +226,11 @@ export function Realtime({
|
|||||||
if (values?.protocol) params.set('protocol', values.protocol);
|
if (values?.protocol) params.set('protocol', values.protocol);
|
||||||
if (values?.online) params.set('online', values.online);
|
if (values?.online) params.set('online', values.online);
|
||||||
if (values?.serviceStatus) params.set('serviceStatus', values.serviceStatus);
|
if (values?.serviceStatus) params.set('serviceStatus', values.serviceStatus);
|
||||||
api.vehicleRealtime(params)
|
return api.vehicleRealtime(params)
|
||||||
.then((nextPage) => {
|
.then((nextPage) => {
|
||||||
setRows(nextPage.items);
|
setRows(nextPage.items);
|
||||||
setPagination({ currentPage: page, pageSize, total: nextPage.total });
|
setPagination({ currentPage: page, pageSize, total: nextPage.total });
|
||||||
|
setLastRefreshAt(formatRefreshTime(new Date()));
|
||||||
})
|
})
|
||||||
.catch((error: Error) => Toast.error(error.message))
|
.catch((error: Error) => Toast.error(error.message))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
@@ -231,6 +244,16 @@ export function Realtime({
|
|||||||
.catch(() => setOpsHealth(null));
|
.catch(() => setOpsHealth(null));
|
||||||
}, [JSON.stringify(initialFilters)]);
|
}, [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>) => {
|
const applyFilters = (nextFilters: Record<string, string>) => {
|
||||||
setFilters(nextFilters);
|
setFilters(nextFilters);
|
||||||
onFiltersChange?.(nextFilters);
|
onFiltersChange?.(nextFilters);
|
||||||
@@ -367,6 +390,18 @@ export function Realtime({
|
|||||||
}}>重置</Button>
|
}}>重置</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Form>
|
</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 ? (
|
{filterSummary.length > 0 ? (
|
||||||
<Card bordered title="当前实时筛选" style={{ marginBottom: 12 }}>
|
<Card bordered title="当前实时筛选" style={{ marginBottom: 12 }}>
|
||||||
<Space wrap>
|
<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);
|
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 () => {
|
test('exports current realtime vehicles as CSV', async () => {
|
||||||
window.history.replaceState(null, '', '/#/realtime?protocol=JT808');
|
window.history.replaceState(null, '', '/#/realtime?protocol=JT808');
|
||||||
const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:realtime-export');
|
const createObjectURL = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:realtime-export');
|
||||||
|
|||||||
Reference in New Issue
Block a user