+ useLayoutEffect(() => {
+ const root = recordRootRef.current;
+ if (!root) return;
+ let cancelled = false;
+ const timers: number[] = [];
+ const revealIdentity = () => {
+ if (!cancelled) revealVehicleIdentity(root);
+ };
+ const cancelEntryReset = () => {
+ cancelled = true;
+ for (const timer of timers) window.clearTimeout(timer);
+ };
+ const interactionEvents = ['wheel', 'touchstart', 'pointerdown', 'keydown'] as const;
+ for (const eventName of interactionEvents) window.addEventListener(eventName, cancelEntryReset, { capture: true, passive: true });
+ revealIdentity();
+ const frame = window.requestAnimationFrame(revealIdentity);
+ // The map and live evidence arrive independently. Keep the identity band
+ // anchored only during initial settlement, then immediately yield to input.
+ for (const delay of [80, 180, 360, 720, 1200, 1800, 2400]) timers.push(window.setTimeout(revealIdentity, delay));
+ return () => {
+ cancelled = true;
+ window.cancelAnimationFrame(frame);
+ for (const timer of timers) window.clearTimeout(timer);
+ for (const eventName of interactionEvents) window.removeEventListener(eventName, cancelEntryReset, true);
+ };
+ }, [detail.vin]);
+ return
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehicleSearchWorkspace.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehicleSearchWorkspace.tsx
index 41d126ee..99f0d604 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/VehicleSearchWorkspace.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/VehicleSearchWorkspace.tsx
@@ -20,11 +20,20 @@ import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
const VehicleProfileSyncPanel = lazy(() => import('./VehicleProfileSyncPanel'));
function resetWorkspaceScroll() {
- const content = document.querySelector
('.v2-content');
- if (!content) return;
- content.scrollTop = 0;
- content.scrollLeft = 0;
- content.scrollTo?.({ top: 0, left: 0, behavior: 'auto' });
+ const targets = [
+ document.scrollingElement,
+ document.documentElement,
+ document.body,
+ document.querySelector('.v2-shell'),
+ document.querySelector('.v2-main'),
+ document.querySelector('.v2-content')
+ ];
+ for (const target of targets) {
+ if (!(target instanceof HTMLElement)) continue;
+ target.scrollTop = 0;
+ target.scrollLeft = 0;
+ target.scrollTo?.({ top: 0, left: 0, behavior: 'auto' });
+ }
}
function vehicleLastSeenLabel(value?: string) {
@@ -61,6 +70,8 @@ export default function VehicleSearchWorkspace() {
const pageVehicles = useMemo(() => options.slice(0, pageSize), [options, pageSize]);
const total = candidates.data?.total ?? 0;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
+ const onlineOnPage = useMemo(() => pageVehicles.filter((vehicle) => vehicle.online).length, [pageVehicles]);
+ const multiSourceOnPage = useMemo(() => pageVehicles.filter((vehicle) => vehicle.protocols.length > 1).length, [pageVehicles]);
const administrator = canAdminister(session);
useEffect(() => () => window.clearTimeout(closeTimerRef.current), []);
useEffect(() => {
@@ -79,6 +90,7 @@ export default function VehicleSearchWorkspace() {
const normalized = value.trim();
if (!normalized) return;
setCandidatesOpen(false);
+ if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
resetWorkspaceScroll();
navigate(`/vehicles/${encodeURIComponent(normalized)}`);
window.queueMicrotask(resetWorkspaceScroll);
@@ -91,64 +103,70 @@ export default function VehicleSearchWorkspace() {
};
const vehicleScope = candidates.data ? `${candidates.data.total.toLocaleString('zh-CN')} 辆授权` : '授权范围';
return
- {!mobileLayout || administrator ? : undefined} onClick={() => setSyncOpen((value) => !value)}>{syncOpen ? '收起同步' : mobileLayout ? '主档同步' : '批量同步主档'} : null}
- /> : null}
- setFiltersCollapsed((value) => !value)}
- >
-
-
+
+ {!mobileLayout || administrator ?
: undefined} onClick={() => setSyncOpen((value) => !value)}>{syncOpen ? '收起同步' : mobileLayout ? '主档同步' : '批量同步主档'} : null}
+ /> : null}
+
setFiltersCollapsed((value) => !value)}
+ >
+
+
+
{candidates.data ? `本页 ${formatZhNumber(pageVehicles.length)} 辆` : '授权范围'}}
+ meta={
+ {candidates.data ? `本页 ${formatZhNumber(pageVehicles.length)} 辆` : '授权范围'}
+ {candidates.data ? 本页在线 {formatZhNumber(onlineOnPage)} : null}
+ {candidates.data ? 多源车辆 {formatZhNumber(multiSourceOnPage)} : null}
+ }
actions={} loading={candidates.isFetching} onClick={() => candidates.refetch()}>刷新}
/>
{candidates.isFetching && !candidates.data
@@ -186,10 +204,16 @@ export default function VehicleSearchWorkspace() {
width: 220,
render: (_value: string, vehicle: VehicleCoverageRow) => {vehicle.plate || '未绑定车牌'}{vehicle.vin}
},
+ {
+ title: '车辆信息',
+ dataIndex: 'oem',
+ width: 260,
+ render: (_value: string, vehicle: VehicleCoverageRow) => {vehicle.oem || '品牌待维护'}{vehicle.phone || '暂无终端手机号'}
+ },
{
title: '实时状态',
dataIndex: 'online',
- width: 150,
+ width: 180,
render: (_value: boolean, vehicle: VehicleCoverageRow) => {vehicle.online ? '在线' : '离线'}{vehicleLastSeenLabel(vehicle.lastSeen)}
},
{
@@ -200,13 +224,22 @@ export default function VehicleSearchWorkspace() {
{
title: '',
dataIndex: 'vin',
- width: 104,
+ width: 116,
align: 'right',
- render: (vehicleVin: string) => } iconPosition="right" aria-label={`打开 ${vehicleVin} 车辆档案`} onClick={() => openVehicle(vehicleVin)}>查看档案
+ render: (vehicleVin: string) => } iconPosition="right" aria-label={`打开 ${vehicleVin} 车辆档案`} onClick={(event) => { event.stopPropagation(); openVehicle(vehicleVin); }}>查看档案
}
]}
dataSource={pageVehicles}
rowKey="vin"
+ onRow={(vehicle) => vehicle ? ({
+ tabIndex: 0,
+ onClick: () => openVehicle(vehicle.vin),
+ onKeyDown: (event) => {
+ if (event.key !== 'Enter' && event.key !== ' ') return;
+ event.preventDefault();
+ openVehicle(vehicle.vin);
+ }
+ }) : ({})}
pagination={false}
loading={candidates.isFetching}
empty={null}
diff --git a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css
index 09bbc347..2bcc2546 100644
--- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css
+++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css
@@ -1983,10 +1983,11 @@
align-content: start;
place-items: start center;
gap: 12px;
- overflow: auto;
+ overflow: visible;
padding: 16px var(--v2-page-gutter) 28px;
}
+.v2-vehicle-discovery-shell,
.v2-vehicle-search-page > .v2-page-heading,
.v2-vehicle-search-page > .v2-mobile-filter-toggle,
.v2-vehicle-search-page > .v2-vehicle-query-panel,
@@ -1996,6 +1997,32 @@
width: min(1280px, 100%);
}
+.v2-vehicle-discovery-shell {
+ display: grid;
+ overflow: visible;
+ gap: 0;
+ border: 1px solid #dce4ee;
+ border-radius: 14px;
+ background: #fff;
+ box-shadow: 0 9px 28px rgba(31, 53, 80, .055);
+}
+
+.v2-vehicle-discovery-shell > .v2-workspace-command-bar {
+ width: 100%;
+ min-height: 50px;
+ border: 0;
+ border-bottom: 1px solid #e6ecf3;
+ border-radius: 14px 14px 0 0;
+ box-shadow: none;
+}
+
+.v2-vehicle-discovery-shell > .v2-vehicle-query-panel.semi-card {
+ width: 100%;
+ border: 0;
+ border-radius: 0 0 14px 14px;
+ box-shadow: none;
+}
+
.v2-vehicle-query-panel.v2-workspace-filter-panel.semi-card {
overflow: visible;
}
@@ -2062,27 +2089,35 @@
}
.v2-vehicle-directory-table .semi-table-tbody > .semi-table-row {
- cursor: default;
+ cursor: pointer;
transition: background-color .15s ease;
}
-.v2-vehicle-directory-table .semi-table-tbody > .semi-table-row:hover {
+.v2-vehicle-directory-table .semi-table-tbody > .semi-table-row:hover,
+.v2-vehicle-directory-table .semi-table-tbody > .semi-table-row:focus-visible {
background: #f7faff;
}
+.v2-vehicle-directory-table .semi-table-tbody > .semi-table-row:focus-visible {
+ outline: 2px solid rgba(18, 104, 243, .28);
+ outline-offset: -2px;
+}
+
.v2-vehicle-directory-table .semi-table-tbody > .semi-table-row > .semi-table-row-cell {
height: 48px;
border-bottom-color: #edf1f6;
padding-block: 6px;
}
-.v2-vehicle-directory-identity {
+.v2-vehicle-directory-identity,
+.v2-vehicle-directory-profile {
display: grid;
min-width: 0;
gap: 2px;
}
-.v2-vehicle-directory-identity strong {
+.v2-vehicle-directory-identity strong,
+.v2-vehicle-directory-profile strong {
overflow: hidden;
color: #1b2a3e;
font-size: 14px;
@@ -2091,7 +2126,14 @@
white-space: nowrap;
}
+.v2-vehicle-directory-profile strong {
+ color: #40536a;
+ font-size: 11px;
+ font-weight: 650;
+}
+
.v2-vehicle-directory-identity small,
+.v2-vehicle-directory-profile small,
.v2-vehicle-directory-status small {
overflow: hidden;
color: #7f8da1;
@@ -2101,6 +2143,16 @@
white-space: nowrap;
}
+.v2-vehicle-directory-summary {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+}
+
+.v2-vehicle-directory-summary > .semi-tag {
+ margin: 0;
+}
+
.v2-vehicle-directory-status {
display: flex;
min-width: 0;
@@ -2148,6 +2200,11 @@
}
.v2-vehicle-record-page {
+ overflow: visible !important;
+ overflow-anchor: none;
+}
+
+.v2-content {
overflow-anchor: none;
}
@@ -2236,9 +2293,14 @@
@media (max-width: 700px) {
.v2-vehicle-search-page {
gap: 8px;
+ overflow: visible;
padding: 9px 8px 92px;
}
+ .v2-vehicle-discovery-shell {
+ display: contents;
+ }
+
.v2-vehicle-command-bar.is-mobile-admin {
min-height: 50px;
border-color: #dce6f2;
@@ -2309,6 +2371,10 @@
margin-left: auto;
}
+ .v2-vehicle-directory-summary > .is-secondary {
+ display: none;
+ }
+
.v2-track-detail-sidesheet .semi-sidesheet-inner {
overflow: hidden;
border-radius: 18px 18px 0 0;