feat(platform): add map situation workspace
This commit is contained in:
@@ -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} />,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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: '顶部轨迹回放' }));
|
||||
|
||||
Reference in New Issue
Block a user