feat(platform): add map situation workspace

This commit is contained in:
lingniu
2026-07-04 17:01:49 +08:00
parent 9ac2b90d12
commit 255afe8a84
6 changed files with 45 additions and 5 deletions

View File

@@ -26,7 +26,7 @@ export default function App() {
initialRoute.page === 'vehicles' ? vehicleFiltersFromRoute(initialRoute) : {}
);
const [realtimeFilters, setRealtimeFilters] = useState<Record<string, string>>(
initialRoute.page === 'realtime' ? realtimeFiltersFromRoute(initialRoute) : {}
initialRoute.page === 'realtime' || initialRoute.page === 'map' ? realtimeFiltersFromRoute(initialRoute) : {}
);
const [historyFilters, setHistoryFilters] = useState<Record<string, string>>(
initialRoute.page === 'history' ? historyFiltersFromRoute(initialRoute) : {}
@@ -111,7 +111,7 @@ export default function App() {
if (route.page === 'vehicles') {
setVehicleFilters(vehicleFiltersFromRoute(route));
}
if (route.page === 'realtime') {
if (route.page === 'realtime' || route.page === 'map') {
setRealtimeFilters(realtimeFiltersFromRoute(route));
}
if (route.page === 'history') {
@@ -167,6 +167,10 @@ export default function App() {
replaceRealtimeHash(realtimeFilters);
return;
}
if (page === 'map') {
replaceMapHash(realtimeFilters);
return;
}
replaceHash(page);
};
@@ -191,11 +195,21 @@ export default function App() {
replaceRealtimeHash(filters);
};
const updateMapFilters = (filters: Record<string, string> = {}) => {
setRealtimeFilters(filters);
replaceMapHash(filters);
};
const replaceRealtimeHash = (filters: Record<string, string> = {}) => {
const { keyword, protocol, ...restFilters } = filters;
replaceHash('realtime', keyword, protocol, restFilters);
};
const replaceMapHash = (filters: Record<string, string> = {}) => {
const { keyword, protocol, ...restFilters } = filters;
replaceHash('map', keyword, protocol, restFilters);
};
const updateHistoryFilters = (filters: Record<string, unknown> = {}, tab = historyTab) => {
const hasKeywordInput = Object.prototype.hasOwnProperty.call(filters, 'keyword');
const nextFilters = normalizeHistoryFilterValues(filters);
@@ -408,6 +422,7 @@ export default function App() {
const pages: Record<PageKey, JSX.Element> = {
dashboard: <Dashboard onOpenVehicle={openVehicle} onOpenQuality={openQuality} onOpenRealtime={openRealtime} onOpenVehicles={openVehicles} onOpenHistory={openHistoryWithFilters} onOpenMileage={openMileageWithFilters} />,
vehicles: <Vehicles onOpenVehicle={openVehicle} onOpenQuality={openQuality} onFiltersChange={updateVehicleFilters} initialFilters={vehicleFilters} />,
map: <Realtime title="地图态势" description="以车辆服务为中心查看实时位置、在线状态、来源一致性和高德地图接入状态" onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateMapFilters} initialFilters={realtimeFilters} />,
realtime: <Realtime onOpenVehicle={openVehicle} onOpenHistory={openHistoryForVehicle} onOpenQuality={openQuality} onFiltersChange={updateRealtimeFilters} initialFilters={realtimeFilters} />,
detail: <VehicleDetail vin={activeVin} protocol={activeProtocol} platformRelease={platformRelease} onOpenRealtime={openRealtimeForVehicle} onOpenHistory={openHistoryForVehicle} onOpenRaw={openRawForVehicle} onOpenMileage={openMileageForVehicle} onOpenVehicles={openVehicles} onOpenQuality={openQuality} onOpenHistoryEvidence={openHistoryWithFilters} onOpenRawEvidence={openRawWithFilters} onQueryChange={updateVehicleDetailQuery} />,
history: <History initialVin={analysisVin} initialProtocol={activeProtocol} initialTab={historyTab} initialFilters={historyFilters} onFiltersChange={updateHistoryFilters} onOpenVehicle={openVehicle} onOpenMileage={openMileageWithFilters} onOpenRaw={openRawWithFilters} />,

View File

@@ -55,6 +55,17 @@ describe('parseAppHash', () => {
});
});
test('parses map situation page', () => {
expect(parseAppHash('#/map?keyword=VIN001&protocol=GB32960&online=online')).toEqual({
page: 'map',
keyword: 'VIN001',
protocol: 'GB32960',
filters: {
online: 'online'
}
});
});
test('parses ops quality page', () => {
expect(parseAppHash('#/ops-quality')).toEqual({
page: 'ops-quality',
@@ -88,6 +99,10 @@ describe('buildAppHash', () => {
expect(buildAppHash({ page: 'notification-rules' })).toBe('#/notification-rules');
});
test('builds map situation page hash', () => {
expect(buildAppHash({ page: 'map', keyword: 'VIN001', protocol: 'GB32960', filters: { online: 'online' } })).toBe('#/map?keyword=VIN001&protocol=GB32960&online=online');
});
test('builds ops quality page hash', () => {
expect(buildAppHash({ page: 'ops-quality' })).toBe('#/ops-quality');
});

View File

@@ -1,6 +1,6 @@
import type { PageKey } from '../layout/AppShell';
const pageKeys = new Set<PageKey>(['dashboard', 'vehicles', 'realtime', 'detail', 'history', 'mileage', 'quality', 'notification-rules', 'ops-quality']);
const pageKeys = new Set<PageKey>(['dashboard', 'vehicles', 'map', 'realtime', 'detail', 'history', 'mileage', 'quality', 'notification-rules', 'ops-quality']);
export type AppRoute = {
page?: PageKey;

View File

@@ -16,11 +16,12 @@ import { useState } from 'react';
import type { VehicleSourceConsistency, VehicleServiceStatus } from '../api/types';
import { isAMapConfigured } from '../config/appConfig';
export type PageKey = 'dashboard' | 'vehicles' | 'realtime' | 'detail' | 'history' | 'mileage' | 'quality' | 'notification-rules' | 'ops-quality';
export type PageKey = 'dashboard' | 'vehicles' | 'map' | 'realtime' | 'detail' | 'history' | 'mileage' | 'quality' | 'notification-rules' | 'ops-quality';
const navItems = [
{ itemKey: 'dashboard', text: '运营驾驶舱', icon: <IconHome /> },
{ itemKey: 'vehicles', text: '车辆中心', icon: <IconServer /> },
{ itemKey: 'map', text: '地图态势', icon: <IconMapPin /> },
{ itemKey: 'realtime', text: '实时监控', icon: <IconActivity /> },
{ itemKey: 'detail', text: '车辆档案', icon: <IconSetting /> },
{ itemKey: 'history', text: '轨迹回放', icon: <IconMapPin /> },
@@ -116,6 +117,7 @@ export function AppShell({
{platformRelease ? <Tag color="blue"> {platformRelease}</Tag> : null}
<Tag color="grey"> / </Tag>
<Tag color={amapConfigured ? 'green' : 'orange'}>{amapConfigured ? '地图就绪' : '地图待配置'}</Tag>
<Button size="small" aria-label="顶部地图态势" onClick={() => onChange('map')}></Button>
<Button size="small" aria-label="顶部实时监控" onClick={() => onChange('realtime')}></Button>
<Button size="small" aria-label="顶部轨迹回放" onClick={() => onChange('history')}></Button>
<Button size="small" aria-label="顶部历史查询" onClick={() => onChange('history')}></Button>

View File

@@ -178,12 +178,16 @@ async function copyText(value: string, label: string) {
}
export function Realtime({
title = '实时监控',
description = '以车辆为主对象查看最新位置、在线来源、核心实时数据和地图作业状态',
onOpenVehicle,
onOpenHistory,
onOpenQuality,
onFiltersChange,
initialFilters = {}
}: {
title?: string;
description?: string;
onOpenVehicle: (vin: string, protocol?: string) => void;
onOpenHistory?: (vin: string, protocol?: string) => void;
onOpenQuality?: (filters: Record<string, string>) => void;
@@ -334,7 +338,7 @@ export function Realtime({
return (
<div className="vp-page">
<PageHeader title="实时监控" description="以车辆为主对象查看最新位置、在线来源、核心实时数据和地图作业状态" />
<PageHeader title={title} description={description} />
<Card bordered>
<Form key={JSON.stringify(filters)} initValues={filters} layout="horizontal" onSubmit={(values) => {
const nextFilters = values as Record<string, string>;

View File

@@ -80,6 +80,10 @@ test('exposes AMap operations shortcuts when map key is configured', async () =>
expect(await screen.findByText('地图就绪')).toBeInTheDocument();
expect(screen.getByText('版本 platform-20260704153357')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '顶部地图态势' }));
expect(window.location.hash).toBe('#/map');
expect(await screen.findByRole('heading', { name: '地图态势' })).toBeInTheDocument();
expect(screen.getByText('地图车辆服务队列')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '顶部实时监控' }));
expect(window.location.hash).toBe('#/realtime');
fireEvent.click(screen.getByRole('button', { name: '顶部轨迹回放' }));