diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx index 41696da2..6af85090 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx @@ -22,6 +22,7 @@ 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 })); +const originalClipboard = Object.getOwnPropertyDescriptor(navigator, 'clipboard'); vi.mock('../map/FleetMap', () => ({ FleetMap: ({ selectedVin, onSelectVin }: { selectedVin?: string; onSelectVin?: (vin: string) => void }) => { @@ -77,6 +78,8 @@ afterEach(() => { vehicleCardArgsSpy.mockClear(); monitorDataArgsSpy.mockClear(); qrToDataURLSpy.mockReset(); + if (originalClipboard) Object.defineProperty(navigator, 'clipboard', originalClipboard); + else Reflect.deleteProperty(navigator, 'clipboard'); vi.restoreAllMocks(); }); @@ -186,6 +189,23 @@ test('loads the QR generator only after opening the mobile entry', async () => { expect(qrToDataURLSpy.mock.calls[0][0]).not.toContain('token'); }); +test('closes the mobile entry with Escape and reports clipboard success', async () => { + qrToDataURLSpy.mockResolvedValue('data:image/png;base64,vehicle-monitor'); + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + render(); + + fireEvent.click(screen.getByRole('button', { name: /手机端/ })); + expect(await screen.findByRole('img', { name: '全局监控手机端二维码' })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '复制访问地址' })); + expect(await screen.findByRole('button', { name: '已复制访问地址' })).toBeInTheDocument(); + expect(writeText).toHaveBeenCalledWith(expect.stringMatching(/\/monitor$/)); + + fireEvent.keyDown(window, { key: 'Escape' }); + expect(screen.queryByRole('dialog', { name: '手机端入口' })).not.toBeInTheDocument(); +}); + 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 } } }); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx index 37dcca7e..20b0066a 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx @@ -106,8 +106,19 @@ function MonitorVehicleTable({ rows, total, page, totalPages, limit, loading, on function MobileEntry({ onClose }: { onClose: () => void }) { const [qr, setQr] = useState(''); const [error, setError] = useState(''); + const [copyState, setCopyState] = useState<'idle' | 'copied' | 'error'>('idle'); const [attempt, setAttempt] = useState(0); const url = `${window.location.origin}/monitor`; + useEffect(() => { + const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') onClose(); }; + window.addEventListener('keydown', closeOnEscape); + return () => window.removeEventListener('keydown', closeOnEscape); + }, [onClose]); + useEffect(() => { + if (copyState === 'idle') return; + const timer = window.setTimeout(() => setCopyState('idle'), 1_800); + return () => window.clearTimeout(timer); + }, [copyState]); useEffect(() => { let cancelled = false; setQr(''); @@ -118,7 +129,16 @@ function MobileEntry({ onClose }: { onClose: () => void }) { .catch(() => { if (!cancelled) setError('二维码生成失败,请检查浏览器环境后重试'); }); return () => { cancelled = true; }; }, [attempt, url]); - return

手机端全局监控

扫码后使用现有账号鉴权,不在二维码中保存 Token

{qr ? 全局监控手机端二维码 : error ?
{error}
: }{url}
; + const copyURL = async () => { + try { + if (!navigator.clipboard?.writeText) throw new Error('clipboard unavailable'); + await navigator.clipboard.writeText(url); + setCopyState('copied'); + } catch { + setCopyState('error'); + } + }; + return
{ if (event.target === event.currentTarget) onClose(); }}>

手机端全局监控

扫码后使用现有账号鉴权,不在二维码中保存 Token

{qr ? 全局监控手机端二维码 : error ?
{error}
: }{url}
; } const VehicleRow = memo(function VehicleRow({ vehicle, selected, onSelect }: { vehicle: VehicleRealtimeRow; selected: boolean; onSelect: (vin: string) => void }) {