feat(platform): persist realtime source filters
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user