perf(monitor): defer QR generator

This commit is contained in:
lingniu
2026-07-16 05:41:05 +08:00
parent 9c6cd09413
commit 3f7619c7cd
5 changed files with 70 additions and 5 deletions

View File

@@ -20,6 +20,7 @@ const monitorMap = { clusters: [], points: [], total: 2 };
const fleetMapRenderSpy = vi.hoisted(() => vi.fn());
const vehicleCardArgsSpy = vi.hoisted(() => vi.fn());
const monitorDataArgsSpy = vi.hoisted(() => vi.fn());
const qrToDataURLSpy = vi.hoisted(() => vi.fn());
const monitorQueryFlags = vi.hoisted(() => ({ isLoading: false, isFetching: false, isPlaceholderData: false }));
vi.mock('../map/FleetMap', () => ({
@@ -31,6 +32,8 @@ vi.mock('../map/FleetMap', () => ({
}
}));
vi.mock('qrcode', () => ({ default: { toDataURL: qrToDataURLSpy } }));
vi.mock('../hooks/useMonitorData', () => ({
MAX_MONITOR_SEARCH_TERMS: 100,
MONITOR_REFRESH: { selected: 10_000, fleet: 15_000, summary: 30_000 },
@@ -73,6 +76,7 @@ afterEach(() => {
monitorQueryFlags.isPlaceholderData = false;
vehicleCardArgsSpy.mockClear();
monitorDataArgsSpy.mockClear();
qrToDataURLSpy.mockReset();
vi.restoreAllMocks();
});
@@ -166,6 +170,33 @@ test('pauses selected-vehicle polling in list mode and resumes it when returning
expect(monitorDataArgsSpy).toHaveBeenLastCalledWith(expect.any(Object), expect.any(Object), 'LTEST000000000001', true, true);
});
test('loads the QR generator only after opening the mobile entry', async () => {
qrToDataURLSpy.mockResolvedValue('data:image/png;base64,vehicle-monitor');
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
expect(qrToDataURLSpy).not.toHaveBeenCalled();
fireEvent.click(screen.getByRole('button', { name: /手机端/ }));
expect(await screen.findByRole('img', { name: '全局监控手机端二维码' })).toHaveAttribute('src', 'data:image/png;base64,vehicle-monitor');
expect(qrToDataURLSpy).toHaveBeenCalledTimes(1);
expect(qrToDataURLSpy.mock.calls[0][0]).toMatch(/\/monitor$/);
expect(qrToDataURLSpy.mock.calls[0][0]).not.toContain('token');
});
test('shows a retry action when on-demand QR generation fails', async () => {
qrToDataURLSpy.mockRejectedValueOnce(new Error('canvas unavailable')).mockResolvedValue('data:image/png;base64,recovered');
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={queryClient}><MemoryRouter future={ROUTER_FUTURE}><MonitorPage /></MemoryRouter></QueryClientProvider>);
fireEvent.click(screen.getByRole('button', { name: /手机端/ }));
expect(await screen.findByRole('alert')).toHaveTextContent('二维码生成失败');
fireEvent.click(screen.getByRole('button', { name: '重新生成' }));
expect(await screen.findByRole('img', { name: '全局监控手机端二维码' })).toHaveAttribute('src', 'data:image/png;base64,recovered');
expect(qrToDataURLSpy).toHaveBeenCalledTimes(2);
});
test('mounts only the mobile list representation and removes its viewport listener', async () => {
const addEventListener = vi.fn();
const removeEventListener = vi.fn();

View File

@@ -1,6 +1,5 @@
import { IconChevronLeft, IconChevronRight, IconClose, IconFilter, IconList, IconMapPin, IconQrCode, IconRefresh, IconSearch } from '@douyinfe/semi-icons';
import { useQuery } from '@tanstack/react-query';
import QRCode from 'qrcode';
import { memo, useCallback, useDeferredValue, useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../../api/client';
@@ -106,13 +105,20 @@ function MonitorVehicleTable({ rows, total, page, totalPages, limit, loading, on
function MobileEntry({ onClose }: { onClose: () => void }) {
const [qr, setQr] = useState('');
const [error, setError] = useState('');
const [attempt, setAttempt] = useState(0);
const url = `${window.location.origin}/monitor`;
useEffect(() => {
let cancelled = false;
void QRCode.toDataURL(url, { width: 240, margin: 2, color: { dark: '#122033', light: '#ffffff' } }).then((value) => { if (!cancelled) setQr(value); });
setQr('');
setError('');
void import('qrcode')
.then(({ default: QRCode }) => QRCode.toDataURL(url, { width: 240, margin: 2, color: { dark: '#122033', light: '#ffffff' } }))
.then((value) => { if (!cancelled) setQr(value); })
.catch(() => { if (!cancelled) setError('二维码生成失败,请检查浏览器环境后重试'); });
return () => { cancelled = true; };
}, [url]);
return <div className="v2-monitor-qr-backdrop" role="dialog" aria-modal="true" aria-label="手机端入口"><section><button type="button" onClick={onClose} aria-label="关闭手机端入口"><IconClose /></button><IconQrCode /><h3></h3><p>使 Token</p>{qr ? <img src={qr} alt="全局监控手机端二维码" /> : <span className="v2-spinner" />}<code>{url}</code><button type="button" onClick={() => void navigator.clipboard?.writeText(url)}>访</button></section></div>;
}, [attempt, url]);
return <div className="v2-monitor-qr-backdrop" role="dialog" aria-modal="true" aria-label="手机端入口"><section><button type="button" onClick={onClose} aria-label="关闭手机端入口"><IconClose /></button><IconQrCode /><h3></h3><p>使 Token</p>{qr ? <img src={qr} alt="全局监控手机端二维码" /> : error ? <div className="v2-monitor-qr-error" role="alert"><span>{error}</span><button type="button" onClick={() => setAttempt((value) => value + 1)}></button></div> : <span className="v2-spinner" role="status" aria-label="正在生成二维码" />}<code>{url}</code><button type="button" onClick={() => void navigator.clipboard?.writeText(url)}>访</button></section></div>;
}
const VehicleRow = memo(function VehicleRow({ vehicle, selected, onSelect }: { vehicle: VehicleRealtimeRow; selected: boolean; onSelect: (vin: string) => void }) {