feat(platform-web): preserve protocol in vehicle links

This commit is contained in:
lingniu
2026-07-04 01:15:39 +08:00
parent 8e5cb5b8c7
commit 657674f233
4 changed files with 41 additions and 15 deletions

View File

@@ -17,6 +17,7 @@ export default function App() {
const [activePage, setActivePage] = useState<PageKey>(initialRoute.page ?? 'dashboard'); const [activePage, setActivePage] = useState<PageKey>(initialRoute.page ?? 'dashboard');
const [activeVin, setActiveVin] = useState(initialVehicleKey); const [activeVin, setActiveVin] = useState(initialVehicleKey);
const [analysisVin, setAnalysisVin] = useState(initialVehicleKey); const [analysisVin, setAnalysisVin] = useState(initialVehicleKey);
const [activeProtocol, setActiveProtocol] = useState(initialRoute.protocol ?? '');
const [linkIssueCount, setLinkIssueCount] = useState<number | null>(null); const [linkIssueCount, setLinkIssueCount] = useState<number | null>(null);
const refreshOpsHealth = useCallback((showError = true) => { const refreshOpsHealth = useCallback((showError = true) => {
@@ -55,13 +56,14 @@ export default function App() {
setAnalysisVin(route.keyword); setAnalysisVin(route.keyword);
} }
} }
setActiveProtocol(route.protocol ?? '');
}; };
window.addEventListener('hashchange', applyHashRoute); window.addEventListener('hashchange', applyHashRoute);
return () => window.removeEventListener('hashchange', applyHashRoute); return () => window.removeEventListener('hashchange', applyHashRoute);
}, []); }, []);
const replaceHash = (page: PageKey, keyword?: string) => { const replaceHash = (page: PageKey, keyword?: string, protocol?: string) => {
const nextHash = buildAppHash({ page, keyword }); const nextHash = buildAppHash({ page, keyword, protocol });
if (window.location.hash !== nextHash) { if (window.location.hash !== nextHash) {
window.history.replaceState(null, '', nextHash); window.history.replaceState(null, '', nextHash);
} }
@@ -70,11 +72,11 @@ export default function App() {
const navigatePage = (page: PageKey) => { const navigatePage = (page: PageKey) => {
setActivePage(page); setActivePage(page);
if (page === 'detail') { if (page === 'detail') {
replaceHash(page, activeVin); replaceHash(page, activeVin, activeProtocol);
return; return;
} }
if (page === 'history' || page === 'mileage') { if (page === 'history' || page === 'mileage') {
replaceHash(page, analysisVin); replaceHash(page, analysisVin, activeProtocol);
return; return;
} }
replaceHash(page); replaceHash(page);
@@ -89,6 +91,7 @@ export default function App() {
const resolved = await api.vehicleResolve(new URLSearchParams({ keyword: lookupKey })); const resolved = await api.vehicleResolve(new URLSearchParams({ keyword: lookupKey }));
const nextKey = resolved.resolved && resolved.vin ? resolved.vin : lookupKey; const nextKey = resolved.resolved && resolved.vin ? resolved.vin : lookupKey;
setActiveVin(nextKey); setActiveVin(nextKey);
setActiveProtocol('');
setActivePage('detail'); setActivePage('detail');
replaceHash('detail', nextKey); replaceHash('detail', nextKey);
if (!resolved.resolved) { if (!resolved.resolved) {
@@ -106,7 +109,7 @@ export default function App() {
} }
setAnalysisVin(nextVin); setAnalysisVin(nextVin);
setActivePage('history'); setActivePage('history');
replaceHash('history', nextVin); replaceHash('history', nextVin, activeProtocol);
}; };
const openMileageForVehicle = (vin: string) => { const openMileageForVehicle = (vin: string) => {
@@ -116,14 +119,25 @@ export default function App() {
} }
setAnalysisVin(nextVin); setAnalysisVin(nextVin);
setActivePage('mileage'); setActivePage('mileage');
replaceHash('mileage', nextVin); replaceHash('mileage', nextVin, activeProtocol);
};
const updateVehicleDetailQuery = (keyword: string, protocol?: string) => {
const nextKeyword = keyword.trim();
const nextProtocol = protocol?.trim() ?? '';
if (!nextKeyword) {
return;
}
setActiveVin(nextKeyword);
setActiveProtocol(nextProtocol);
replaceHash('detail', nextKeyword, nextProtocol);
}; };
const pages: Record<PageKey, JSX.Element> = { const pages: Record<PageKey, JSX.Element> = {
dashboard: <Dashboard onOpenVehicle={openVehicle} onOpenQuality={() => navigatePage('quality')} />, dashboard: <Dashboard onOpenVehicle={openVehicle} onOpenQuality={() => navigatePage('quality')} />,
vehicles: <Vehicles onOpenVehicle={openVehicle} />, vehicles: <Vehicles onOpenVehicle={openVehicle} />,
realtime: <Realtime onOpenVehicle={openVehicle} />, realtime: <Realtime onOpenVehicle={openVehicle} />,
detail: <VehicleDetail vin={activeVin} onOpenHistory={openHistoryForVehicle} onOpenMileage={openMileageForVehicle} />, detail: <VehicleDetail vin={activeVin} protocol={activeProtocol} onOpenHistory={openHistoryForVehicle} onOpenMileage={openMileageForVehicle} onQueryChange={updateVehicleDetailQuery} />,
history: <History initialVin={analysisVin} onOpenVehicle={openVehicle} />, history: <History initialVin={analysisVin} onOpenVehicle={openVehicle} />,
mileage: <Mileage initialVin={analysisVin} onOpenVehicle={openVehicle} />, mileage: <Mileage initialVin={analysisVin} onOpenVehicle={openVehicle} />,
quality: <Quality onOpenVehicle={openVehicle} onHealthLoaded={(health) => setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length)} /> quality: <Quality onOpenVehicle={openVehicle} onHealthLoaded={(health) => setLinkIssueCount(health.linkHealth.filter((item) => item.status !== 'ok').length)} />

View File

@@ -3,9 +3,10 @@ import { buildAppHash, parseAppHash } from './appRoute';
describe('parseAppHash', () => { describe('parseAppHash', () => {
test('parses detail page keyword from hash query', () => { test('parses detail page keyword from hash query', () => {
expect(parseAppHash('#/detail?keyword=%E7%B2%A4AG18312')).toEqual({ expect(parseAppHash('#/detail?keyword=%E7%B2%A4AG18312&protocol=JT808')).toEqual({
page: 'detail', page: 'detail',
keyword: '粤AG18312' keyword: '粤AG18312',
protocol: 'JT808'
}); });
}); });
@@ -16,7 +17,7 @@ describe('parseAppHash', () => {
describe('buildAppHash', () => { describe('buildAppHash', () => {
test('builds shareable vehicle page hash with encoded keyword', () => { test('builds shareable vehicle page hash with encoded keyword', () => {
expect(buildAppHash({ page: 'history', keyword: '粤AG18312' })).toBe('#/history?keyword=%E7%B2%A4AG18312'); expect(buildAppHash({ page: 'history', keyword: '粤AG18312', protocol: 'GB32960' })).toBe('#/history?keyword=%E7%B2%A4AG18312&protocol=GB32960');
}); });
test('builds page-only hash when keyword is empty', () => { test('builds page-only hash when keyword is empty', () => {

View File

@@ -5,6 +5,7 @@ const pageKeys = new Set<PageKey>(['dashboard', 'vehicles', 'realtime', 'detail'
export type AppRoute = { export type AppRoute = {
page?: PageKey; page?: PageKey;
keyword?: string; keyword?: string;
protocol?: string;
}; };
export function parseAppHash(hash: string): AppRoute { export function parseAppHash(hash: string): AppRoute {
@@ -18,7 +19,8 @@ export function parseAppHash(hash: string): AppRoute {
} }
const params = new URLSearchParams(queryPart); const params = new URLSearchParams(queryPart);
const keyword = params.get('keyword')?.trim() || undefined; const keyword = params.get('keyword')?.trim() || undefined;
return { page: pagePart as PageKey, keyword }; const protocol = params.get('protocol')?.trim() || undefined;
return { page: pagePart as PageKey, keyword, protocol };
} }
export function buildAppHash(route: AppRoute): string { export function buildAppHash(route: AppRoute): string {
@@ -28,6 +30,10 @@ export function buildAppHash(route: AppRoute): string {
if (keyword) { if (keyword) {
params.set('keyword', keyword); params.set('keyword', keyword);
} }
const protocol = route.protocol?.trim();
if (protocol) {
params.set('protocol', protocol);
}
const query = params.toString(); const query = params.toString();
return query ? `#/${page}?${query}` : `#/${page}`; return query ? `#/${page}?${query}` : `#/${page}`;
} }

View File

@@ -21,14 +21,18 @@ const sourceCapabilities: Array<{ key: keyof Pick<VehicleSourceStatus, 'hasRealt
export function VehicleDetail({ export function VehicleDetail({
vin, vin,
protocol,
onOpenHistory, onOpenHistory,
onOpenMileage onOpenMileage,
onQueryChange
}: { }: {
vin: string; vin: string;
protocol?: string;
onOpenHistory: (vin: string) => void; onOpenHistory: (vin: string) => void;
onOpenMileage: (vin: string) => void; onOpenMileage: (vin: string) => void;
onQueryChange?: (keyword: string, protocol?: string) => void;
}) { }) {
const [query, setQuery] = useState<VehicleQuery>({ keyword: vin }); const [query, setQuery] = useState<VehicleQuery>({ keyword: vin, protocol });
const [detail, setDetail] = useState<VehicleDetailData | null>(null); const [detail, setDetail] = useState<VehicleDetailData | null>(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const requestSeq = useRef(0); const requestSeq = useRef(0);
@@ -65,10 +69,10 @@ export function VehicleDetail({
}; };
useEffect(() => { useEffect(() => {
const nextQuery = { keyword: vin }; const nextQuery = { keyword: vin, protocol };
setQuery(nextQuery); setQuery(nextQuery);
load(nextQuery); load(nextQuery);
}, [vin]); }, [vin, protocol]);
const identity = detail?.identity; const identity = detail?.identity;
const summary = detail?.realtimeSummary; const summary = detail?.realtimeSummary;
@@ -107,6 +111,7 @@ export function VehicleDetail({
<Form key={formKey} initValues={query} layout="horizontal" onSubmit={(values) => { <Form key={formKey} initValues={query} layout="horizontal" onSubmit={(values) => {
const nextQuery = { keyword: String(values.keyword ?? ''), protocol: String(values.protocol ?? '') }; const nextQuery = { keyword: String(values.keyword ?? ''), protocol: String(values.protocol ?? '') };
setQuery(nextQuery); setQuery(nextQuery);
onQueryChange?.(nextQuery.keyword, nextQuery.protocol);
load(nextQuery); load(nextQuery);
}}> }}>
<Form.Input field="keyword" label="车辆关键词" placeholder="VIN / 车牌 / 手机号" style={{ width: 280 }} /> <Form.Input field="keyword" label="车辆关键词" placeholder="VIN / 车牌 / 手机号" style={{ width: 280 }} />