feat(platform): persist realtime source filters

This commit is contained in:
lingniu
2026-07-04 06:45:38 +08:00
parent 2feaf246be
commit b9534af233
3 changed files with 142 additions and 10 deletions

View File

@@ -23,6 +23,9 @@ export default function App() {
const [vehicleFilters, setVehicleFilters] = useState<Record<string, string>>(
initialRoute.page === 'vehicles' ? vehicleFiltersFromRoute(initialRoute) : {}
);
const [realtimeFilters, setRealtimeFilters] = useState<Record<string, string>>(
initialRoute.page === 'realtime' ? realtimeFiltersFromRoute(initialRoute) : {}
);
const [qualityFilters, setQualityFilters] = useState<Record<string, string>>(
initialRoute.page === 'quality' ? qualityFiltersFromRoute(initialRoute) : {}
);
@@ -98,6 +101,9 @@ export default function App() {
if (route.page === 'vehicles') {
setVehicleFilters(vehicleFiltersFromRoute(route));
}
if (route.page === 'realtime') {
setRealtimeFilters(realtimeFiltersFromRoute(route));
}
if (route.page === 'quality') {
setQualityFilters(qualityFiltersFromRoute(route));
}
@@ -133,6 +139,10 @@ export default function App() {
replaceVehicleHash(vehicleFilters);
return;
}
if (page === 'realtime') {
replaceRealtimeHash(realtimeFilters);
return;
}
replaceHash(page);
};
@@ -152,6 +162,16 @@ export default function App() {
replaceHash('vehicles', keyword, protocol, restFilters);
};
const updateRealtimeFilters = (filters: Record<string, string> = {}) => {
setRealtimeFilters(filters);
replaceRealtimeHash(filters);
};
const replaceRealtimeHash = (filters: Record<string, string> = {}) => {
const { keyword, protocol, ...restFilters } = filters;
replaceHash('realtime', keyword, protocol, restFilters);
};
const replaceQualityHash = (filters: Record<string, string> = {}) => {
const keyword = filters.keyword;
const protocol = filters.protocol;
@@ -241,7 +261,7 @@ export default function App() {
const pages: Record<PageKey, JSX.Element> = {
dashboard: <Dashboard onOpenVehicle={openVehicle} onOpenQuality={() => navigatePage('quality')} onOpenVehicles={openVehicles} />,
vehicles: <Vehicles onOpenVehicle={openVehicle} onFiltersChange={updateVehicleFilters} initialFilters={vehicleFilters} />,
realtime: <Realtime onOpenVehicle={openVehicle} />,
realtime: <Realtime onOpenVehicle={openVehicle} onFiltersChange={updateRealtimeFilters} initialFilters={realtimeFilters} />,
detail: <VehicleDetail vin={activeVin} protocol={activeProtocol} onOpenHistory={openHistoryForVehicle} onOpenRaw={openRawForVehicle} onOpenMileage={openMileageForVehicle} onOpenVehicles={openVehicles} onQueryChange={updateVehicleDetailQuery} />,
history: <History initialVin={analysisVin} initialProtocol={activeProtocol} initialTab={historyTab} onOpenVehicle={openVehicle} />,
mileage: <Mileage initialVin={analysisVin} initialProtocol={activeProtocol} onOpenVehicle={openVehicle} />,
@@ -271,6 +291,15 @@ function vehicleFiltersFromRoute(route: ReturnType<typeof parseAppHash>): Record
};
}
function realtimeFiltersFromRoute(route: ReturnType<typeof parseAppHash>): Record<string, string> {
return {
...(route.keyword ? { keyword: route.keyword } : {}),
...(route.protocol ? { protocol: route.protocol } : {}),
...(route.filters?.online ? { online: route.filters.online } : {}),
...(route.filters?.serviceStatus ? { serviceStatus: route.filters.serviceStatus } : {})
};
}
function serviceStatusFromOverview(overview: VehicleServiceOverview): VehicleServiceStatus {
const sourceCount = overview.sourceCount;
const onlineSourceCount = overview.onlineSourceCount;

View File

@@ -31,10 +31,18 @@ function sourceEvidenceText(row: VehicleRealtimeRow) {
return `${row.onlineSourceCount}/${row.sourceCount} 来源在线`;
}
export function Realtime({ onOpenVehicle }: { onOpenVehicle: (vin: string, protocol?: string) => void }) {
export function Realtime({
onOpenVehicle,
onFiltersChange,
initialFilters = {}
}: {
onOpenVehicle: (vin: string, protocol?: string) => void;
onFiltersChange?: (filters: Record<string, string>) => void;
initialFilters?: Record<string, string>;
}) {
const [rows, setRows] = useState<VehicleRealtimeRow[]>([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState<Record<string, string>>({});
const [filters, setFilters] = useState<Record<string, string>>(initialFilters);
const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 50, total: 0 });
const load = (values: Record<string, string> = filters, page = pagination.currentPage, pageSize = pagination.pageSize) => {
@@ -54,17 +62,23 @@ export function Realtime({ onOpenVehicle }: { onOpenVehicle: (vin: string, proto
};
useEffect(() => {
load({}, 1, pagination.pageSize);
}, []);
setFilters(initialFilters);
load(initialFilters, 1, pagination.pageSize);
}, [JSON.stringify(initialFilters)]);
const applyFilters = (nextFilters: Record<string, string>) => {
setFilters(nextFilters);
onFiltersChange?.(nextFilters);
load(nextFilters, 1, pagination.pageSize);
};
return (
<div className="vp-page">
<PageHeader title="车辆服务实时态" description="以车辆为主对象查看最新位置、在线来源和核心实时数据,协议只作为来源证据" />
<Card bordered>
<Form layout="horizontal" onSubmit={(values) => {
<Form key={JSON.stringify(filters)} initValues={filters} layout="horizontal" onSubmit={(values) => {
const nextFilters = values as Record<string, string>;
setFilters(nextFilters);
load(nextFilters, 1, pagination.pageSize);
applyFilters(nextFilters);
}} style={{ marginBottom: 12 }}>
<Form.Input field="keyword" label="车辆关键词" placeholder="VIN / 车牌 / 手机号 / OEM" style={{ width: 260 }} />
<Form.Select field="protocol" label="数据来源" placeholder="全部来源" style={{ width: 180 }}>
@@ -85,8 +99,7 @@ export function Realtime({ onOpenVehicle }: { onOpenVehicle: (vin: string, proto
<Space>
<Button htmlType="submit" theme="solid" type="primary"></Button>
<Button onClick={() => {
setFilters({});
load({}, 1, pagination.pageSize);
applyFilters({});
}}></Button>
</Space>
</Form>

View File

@@ -2393,6 +2393,96 @@ test('frames realtime page as one vehicle service with source evidence', async (
expect(screen.getByText('2/2 来源在线')).toBeInTheDocument();
});
test('loads realtime vehicles from shareable source filter hash', async () => {
window.history.replaceState(null, '', '/#/realtime?keyword=%E7%B2%A4ART002&protocol=JT808&online=online&serviceStatus=degraded');
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const path = String(input);
if (path.includes('/api/realtime/vehicles')) {
return {
ok: true,
json: async () => ({
data: {
items: [{
vin: 'VIN-RT-FILTERED',
plate: '粤ART002',
phone: '',
oem: 'G7s',
protocols: ['JT808'],
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: { items: [], total: 0, limit: 10, offset: 0 },
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
});
render(<App />);
expect(await screen.findByText('VIN-RT-FILTERED')).toBeInTheDocument();
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/realtime/vehicles?limit=50&offset=0&keyword=%E7%B2%A4ART002&protocol=JT808&online=online&serviceStatus=degraded'), undefined);
});
test('updates realtime hash when source filters are submitted', async () => {
window.history.replaceState(null, '', '/#/realtime');
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const path = String(input);
if (path.includes('/api/realtime/vehicles')) {
return {
ok: true,
json: async () => ({
data: { items: [], total: 0, limit: 50, offset: 0 },
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
}
return {
ok: true,
json: async () => ({
data: { items: [], total: 0, limit: 10, offset: 0 },
traceId: 'trace-test',
timestamp: 1783094400000
})
} as Response;
});
render(<App />);
await screen.findByRole('heading', { name: '车辆服务实时态' });
fireEvent.change(screen.getByPlaceholderText('VIN / 车牌 / 手机号 / OEM'), { target: { value: '粤ART002' } });
fireEvent.click(screen.getByText('全部来源'));
fireEvent.click(await screen.findByText('JT808'));
fireEvent.click(screen.getByRole('button', { name: '查询' }));
await waitFor(() => {
expect(window.location.hash).toBe('#/realtime?keyword=%E7%B2%A4ART002&protocol=JT808');
});
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/realtime/vehicles?limit=50&offset=0&keyword=%E7%B2%A4ART002&protocol=JT808'), undefined);
});
test('shows vehicle service status in vehicle list', async () => {
window.history.replaceState(null, '', '/#/vehicles');
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {