From 58f2a770336799051f05e8203a86e2add7870258 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 19 Jul 2026 19:27:43 +0800 Subject: [PATCH] refactor(ui): refine semi access workspace --- .../apps/web/src/v2/pages/AccessPage.test.tsx | 14 +++ .../apps/web/src/v2/pages/AccessPage.tsx | 22 ++-- .../apps/web/src/v2/styles/workspace.css | 78 ++++++++++++++ vehicle-data-platform/design-qa.md | 102 ++++++++---------- 4 files changed, 151 insertions(+), 65 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx index 898158e2..36254c08 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx @@ -91,6 +91,16 @@ test('removes old access rows immediately when the vehicle filter scope changes' expect(scrollBy).toHaveBeenCalledWith({ left: -260, behavior: 'smooth' }); expect(screen.getByRole('columnheader', { name: '车辆' })).toHaveClass('semi-table-cell-fixed-left', 'v2-access-vehicle-column'); expect(screen.getByRole('columnheader', { name: '品牌 / 车型' })).toHaveClass('semi-table-cell-fixed-left', 'v2-access-model-column'); + const protocolHeaders = [ + { label: 'GB/T 32960', description: '国标远程服务' }, + { label: 'JT/T 808', description: '道路运输终端' }, + { label: '宇通 MQTT', description: '宇通车辆平台' } + ]; + for (const protocol of protocolHeaders) { + const header = screen.getByRole('columnheader', { name: new RegExp(protocol.label.replace('/', '\\/')) }); + expect(within(header).getByText(protocol.label).closest('.semi-tag')).toHaveClass('v2-protocol-tag', 'is-compact'); + expect(within(header).getByText(protocol.description)).toBeInTheDocument(); + } const desktopRow = screen.getByTestId('access-row-OLDVIN'); expect(desktopRow).toHaveAttribute('role', 'button'); expect(desktopRow).toHaveAttribute('aria-expanded', 'false'); @@ -108,6 +118,10 @@ test('removes old access rows immediately when the vehicle filter scope changes' expect(healthOverview).toHaveTextContent('当前在线1'); expect(healthOverview).toHaveTextContent('异常来源0'); expect(healthOverview).toHaveTextContent('资料状态已维护'); + expect(detailDialog.querySelectorAll('.v2-access-protocol-title .v2-protocol-tag')).toHaveLength(3); + expect(within(detailDialog).getByText('GB/T 32960')).toBeInTheDocument(); + expect(within(detailDialog).getByText('JT/T 808')).toBeInTheDocument(); + expect(within(detailDialog).getByText('宇通 MQTT')).toBeInTheDocument(); expect(detailDialog.querySelector('.v2-access-inspector-header')).not.toBeInTheDocument(); fireEvent.change(screen.getByRole('textbox', { name: '车辆' }), { target: { value: 'NEWVIN' } }); fireEvent.click(screen.getByRole('button', { name: '查询' })); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx index 89024e06..b13f5f26 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx @@ -25,7 +25,8 @@ import { downloadBlob } from '../domain/download'; import { useMobileLayout } from '../hooks/useMobileLayout'; const PROTOCOLS = ['GB32960', 'JT808', 'YUTONG_MQTT'] as const; -const protocolThresholdMeta: Record<(typeof PROTOCOLS)[number], { label: string; description: string }> = { +type AccessProtocol = (typeof PROTOCOLS)[number]; +const protocolThresholdMeta: Record = { GB32960: { label: 'GB32960', description: '国标远程服务' }, JT808: { label: 'JT808', description: '道路运输终端' }, YUTONG_MQTT: { label: 'YUTONG MQTT', description: '宇通车辆平台' } @@ -82,14 +83,23 @@ function protocolPriority(status?: AccessProtocolStatus) { return 3; } +function AccessProtocolTitle({ protocol, compact = false }: { protocol: AccessProtocol; compact?: boolean }) { + return + + {protocolThresholdMeta[protocol].description} + ; +} + const ProtocolState = memo(function ProtocolState({ status, + protocol, detailed = false, protocolLabel, expanded = false, onToggle }: { status?: AccessProtocolStatus; + protocol: AccessProtocol; detailed?: boolean; protocolLabel?: string; expanded?: boolean; @@ -99,12 +109,11 @@ const ProtocolState = memo(function ProtocolState({ const label = state === 'missing' ? '无来源' : state === 'online' ? '在线' : state === 'offline' ? '离线' : state === 'unknown' ? '未知' : '从未上报'; const color = state === 'online' ? 'green' : state === 'offline' || state === 'unknown' ? 'orange' : 'grey'; if (detailed) { - const protocol = status?.protocol || '未知协议'; const evidenceId = `access-protocol-${protocol.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-evidence`; const evidence = status?.firstSeenEvidence || '当前未发现该协议来源'; return {protocol}} + title={} headerExtraContent={{label}} headerLine bodyStyle={{ padding: 0 }} @@ -196,8 +205,8 @@ const AccessVehicleTable = memo(function AccessVehicleTable({ rows, selectedVIN, render: (_: string, row: AccessVehicleRow) =>
{row.oem || '品牌未维护'}{row.model || row.company || '车型未维护'}
}, ...PROTOCOLS.map((protocol) => ({ - title: protocol, dataIndex: protocol, width: 160, - render: (_: unknown, row: AccessVehicleRow) => + title: , dataIndex: protocol, width: 160, + render: (_: unknown, row: AccessVehicleRow) => })), { title: '差异摘要', dataIndex: 'connectionState', width: 220, @@ -262,6 +271,7 @@ function VehicleInspector({ row, onClose, sheet = false, mobile = false }: { row {protocols.map((protocol) => {mobileLayout - ?
{rows.map((row) => )}
+ ?
{rows.map((row) => )}
: } {vehiclesQuery.isFetching ? : null} {!vehiclesQuery.isFetching && !rows.length ? : 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 528a3736..becee43d 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -10960,6 +10960,39 @@ line-height: 14px; } +.v2-access-protocol-title { + display: inline-grid; + min-width: 0; + justify-items: start; + gap: 3px; +} + +.v2-access-protocol-title > small { + overflow: hidden; + max-width: 100%; + color: #8491a4; + font-size: 9px; + font-weight: 500; + line-height: 11px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.v2-access-protocol-title.is-compact { + gap: 2px; +} + +.v2-access-protocol-title.is-compact .v2-protocol-tag.semi-tag { + min-height: 19px; + padding-inline: 6px; + font-size: 9px; +} + +.v2-access-protocol-title.is-compact > small { + font-size: 8px; + line-height: 10px; +} + .v2-access-table-v3 .semi-table-tbody > .semi-table-row > .semi-table-row-cell { height: 58px; padding-block: 6px; @@ -11011,10 +11044,45 @@ background: #f2f7ff; } +.v2-access-semi-table .semi-table-tbody > .semi-table-row.is-selected > .semi-table-row-cell:first-child { + box-shadow: inset 3px 0 0 #2f7de1; +} + +.v2-access-semi-table .semi-table-tbody > .semi-table-row.is-selected > .semi-table-row-cell:not(.semi-table-cell-fixed-left) { + background: linear-gradient(90deg, #f2f7ff 0%, #f7faff 100%); +} + .v2-access-semi-table :is(.semi-table-row-head, .semi-table-row-cell).v2-access-model-column { box-shadow: 12px 0 18px -18px rgba(35, 55, 82, .72); } +.v2-access-semi-table .v2-access-protocol-cell { + gap: 2px; + border: 1px solid #e6ebf2; + border-radius: 7px; + background: #fafbfd; + padding: 4px 7px; +} + +.v2-access-semi-table .v2-access-protocol-cell.is-online { + border-color: #d9ece5; + background: #f7fcfa; +} + +.v2-access-semi-table .v2-access-protocol-cell.is-offline, +.v2-access-semi-table .v2-access-protocol-cell.is-unknown { + border-color: #f1e2ca; + background: #fffbf5; +} + +.v2-access-semi-table .v2-access-protocol-cell > span { + font-weight: 650; +} + +.v2-access-semi-table .v2-access-protocol-cell > strong { + font-variant-numeric: tabular-nums; +} + .v2-access-semi-table .semi-table-body::-webkit-scrollbar { width: 10px; height: 10px; @@ -11106,6 +11174,16 @@ box-shadow: 0 7px 20px rgba(42, 72, 110, .055); } +.v2-access-protocol-detail > .semi-card-header .v2-access-protocol-title { + padding-block: 5px; +} + +.v2-access-protocol-detail > .semi-card-header .v2-access-protocol-title .v2-protocol-tag.semi-tag { + min-height: 20px; + padding-inline: 7px; + font-size: 9px; +} + .v2-access-inspector-focus.is-degraded.semi-card, .v2-access-inspector-focus.is-incomplete.semi-card { border-color: #f0d9b7; diff --git a/vehicle-data-platform/design-qa.md b/vehicle-data-platform/design-qa.md index cc419951..8632bf57 100644 --- a/vehicle-data-platform/design-qa.md +++ b/vehicle-data-platform/design-qa.md @@ -1,77 +1,61 @@ -# 轨迹回放 Design QA +# 接入管理 Semi UI Design QA -- source visual truth path: `/Users/lingniu/.codex/attachments/70622989-d5e8-4010-9aba-11b1c6974c78/image-1.png` -- implementation route: `http://115.29.187.205:20300/tracks?keyword=LB9A32A23R0LS1529&dateFrom=2026-07-15T00%3A00&dateTo=2026-07-15T23%3A59` -- implementation screenshot path: `/tmp/tracks-production-desktop.png` -- current browser screenshot path: `/tmp/tracks-production-current.png` -- mobile screenshots: `/tmp/tracks-production-mobile.png`, `/tmp/tracks-production-mobile-open.png` -- viewport and state: desktop `2560 × 1285`, loaded vehicle `粤AGR9866`, first point, rail expanded, stop points visible, follow enabled, speed `1×`; current in-app viewport `815 × 874`; mobile `390 × 844`, both map-first collapsed state and query-sheet expanded state. -- source native size: `5120 × 2570`. Browser viewport override uses DPR 1, so the production capture used the exact same 1.992:1 logical aspect at `2560 × 1285`; the source was normalized to that size for comparison. A literal `5120 × 2570` CSS viewport would change the responsive composition instead of reproducing the source's logical viewport. +- source visual truth: Git `2c9399941` 的 `/access` 页面 +- source screenshots: + - `/tmp/access-desktop-baseline.png` + - `/tmp/access-detail-baseline.png` + - `/tmp/access-mobile-baseline-full.jpg` +- implementation screenshots: + - `/tmp/access-desktop-updated.jpg` + - `/tmp/access-detail-updated.jpg` + - `/tmp/access-mobile-updated.jpg` +- viewport and state: + - desktop `1440 × 900`,全部主车辆,未选中车辆 + - desktop detail `1440 × 900`,选中 `粤AG18312` + - mobile `390 × 844`,全部主车辆,列表首屏 +- data source: local Vite UI plus the same `20302` mock API for both source and implementation ## Comparison Evidence -- full-view combined comparison: `/tmp/tracks-reference-vs-production.png` -- focused feature-rail comparison: `/tmp/tracks-reference-vs-production-rail.png` -- focused playback-dock comparison: `/tmp/tracks-reference-vs-production-dock.png` -- both the native source and the latest production desktop screenshot were opened with `view_image` in the same QA pass, followed by the combined full, rail, dock, and mobile comparisons. +- desktop before/after: `/tmp/access-desktop-comparison.jpg` +- detail before/after: `/tmp/access-detail-comparison.jpg` +- mobile before/after: `/tmp/access-mobile-comparison.jpg` +- every comparison uses the same viewport, API data and interaction state; all three combined images were inspected with `view_image`. ## Fidelity Ledger -| Surface | Source evidence | Production evidence | Result | +| Surface | Source evidence | Implementation evidence | Result | | --- | --- | --- | --- | -| Information architecture | G7 uses a map-first canvas with a fixed query/detail rail and a persistent bottom playback strip. | Production uses the same three-region skeleton: query/detail rail, dominant map canvas, and persistent playback dock. | Matched. | -| Feature rail | G7 exposes vehicle/time query and stop/event detail lists in one continuous left rail. | Production keeps vehicle suggestions, time presets, source selection, stop/event/overview tabs and selectable evidence rows in one rail. | Matched; `概览` intentionally replaces G7's implementation-specific cache tab. | -| Map and route | G7 keeps the map visually dominant, with route, stop markers and a dark current-vehicle overlay. | Production uses the required AMap `whitesmoke` style, full route, passed route, start/end/stop/current markers, follow behavior and a dark telemetry/address card. | Matched interaction model; palette intentionally follows the platform's requested global map style. | -| Playback dock | G7 shows a full-width time strip and centered playback controls. | Production shows segment-colored activity rail, range seek, start/end times, previous/play/next, `0.5×–4×`, reset, mileage/source/status metrics. | Matched and functionally expanded. | -| Typography | G7 uses compact utility typography and dense tabular labels. | Production uses the existing platform font stack with compact 8–14px utility hierarchy on the desktop rail and 16px mobile form controls. | Matched density while preserving product typography. | -| Color and surfaces | G7 has a dark global header, white rail and colorful default map. | Production preserves the existing light global shell, white rail, blue action system, dark telemetry card and `whitesmoke` map. | Intentional product-system deviation; feature hierarchy remains faithful. | -| Icons | G7 uses compact utility icons for map and playback actions. | Production reuses the platform's Semi icon family; mobile tools now retain visible icons and explicit accessible names. | Matched and accessible. | -| Responsive behavior | Source only provides desktop evidence. | Production was verified at `815 × 874` and `390 × 844`; mobile becomes a collapsible bottom sheet without horizontal overflow and preserves the map/dock task. | Passed; mobile is a product extension of the source. | -| Motion | Source implies moving vehicle playback and map following. | Production uses `requestAnimationFrame`, 0.5×/1×/2×/4× speeds, smooth map panning and manual-drag follow cancellation. | Matched and performance-safe. | +| Information hierarchy | The vehicle list already owns the viewport and the two top metrics stay restrained. | The list-first structure, metric count, filters, pagination and governance entry remain unchanged. | Preserved. | +| Protocol identity | Desktop headers and protocol detail cards exposed internal names such as `GB32960`, `JT808` and `YUTONG_MQTT`. | Both surfaces now use the shared Semi protocol identity: `GB/T 32960`, `JT/T 808`, `宇通 MQTT`, with one consistent color grammar and human-readable descriptions. | Improved. | +| Status scanability | Protocol state, time and provider were presented as loose text in wide table cells. | Each status is contained in a subtle semantic surface; online, offline and missing states remain distinguishable without increasing row height. | Improved. | +| Selection feedback | Selection relied mainly on a light row fill. | The selected row keeps the light fill and adds a restrained blue leading accent; keyboard focus remains independently visible. | Improved. | +| Detail evidence | Detail cards had raw protocol headings while their state tag used Semi UI. | Protocol identity and state are now both Semi UI primitives, with the protocol description directly beneath the tag. | Improved. | +| Mobile density | Four vehicles were visible in the `390 × 844` viewport. | The same four-vehicle density, fixed pagination, touch targets and vertical-only scrolling are preserved. | Preserved. | +| Responsive behavior | No horizontal overflow at desktop or mobile. | Browser verification reports no document-level horizontal overflow at `390 × 844`; desktop fixed columns and protocol scrolling remain functional. | Passed. | -## Above-the-fold Copy Diff +## Interaction And Accessibility Checks -The source's functional labels `轨迹回放`, vehicle/time query, `停留点`, `事件点`, distance, and playback controls are retained in product-appropriate Chinese. Intentional additions are `数据来源`, `概览`, `跟随车辆`, `停留点` visibility, `导出`, and bounded-coverage evidence because they expose source priority, playback state, and data completeness required by this platform. G7-specific brand/navigation strings, `缓存`, and its proprietary map tools were not copied. - -## Primary Interactions Tested in Browser/IAB - -- stop/event/overview tab switching and selecting an event to move the active point; -- playback at `4×`, verified progress advanced from point `0` to point `11`, then pause; -- follow vehicle / free browse toggle; -- stop-marker show/hide toggle; -- desktop and mobile query/detail rail collapse and expand; -- reset to start, speed reset to `1×`, and sample query loading with `1600` returned points; -- mobile toolbar accessible names: `展开查询与明细`, `关闭车辆跟随`, `隐藏停留点`, `导出轨迹 CSV`; -- console errors checked: none. +- desktop protocol headers expose complete accessible names including protocol and Chinese description; +- table row opens the vehicle detail sheet by pointer or keyboard; +- selected row exposes `aria-expanded=true`; +- protocol evidence expands and collapses without an API refetch; +- mobile list remains vertically scrollable and has no document-level horizontal overflow; +- existing filter, export, pagination and governance contracts remain covered by `AccessPage.test.tsx`. ## Comparison History -### Pass 1 — blocked +### Pass 1 — passed -- [P1] Mobile map toolbar icons were blank because a broad rule hid every nested `span`, including the Semi icon wrapper. - - Fix: hide only the text span and explicitly retain `.semi-icon` at 17px. - - Post-fix evidence: `/tmp/tracks-production-mobile.png` visibly shows list, follow, stop visibility and export icons. -- [P2] Two-column mobile `datetime-local` inputs clipped the minute value at 390px. - - Fix: stack start and end time fields into one column while keeping 16px touch-safe input text. - - Post-fix evidence: `/tmp/tracks-production-mobile-open.png` shows complete `2026/07/15 00:00` and `2026/07/15 23:59` values. -- [P2] Mobile icon-only controls lost meaningful accessible names when their visible text was hidden. - - Fix: add explicit dynamic `aria-label` values to expand, follow, stop and export controls. - - Post-fix evidence: Browser/IAB resolved each label exactly once. +- No P0/P1/P2 visual or interaction issue was found. +- The desktop protocol columns are materially easier to scan while retaining the existing six-row viewport. +- The detail sheet now matches the shared protocol language used by alert and history evidence surfaces. +- Mobile structure is intentionally unchanged because the existing density and task priority already meet the product requirement. -### Pass 2 — passed +## Intentional Constraints -- Full desktop, current in-app viewport, mobile collapsed map and mobile expanded query sheet were recaptured from the final ECS release. -- No actionable P0/P1/P2 layout, typography, color-token, icon, image-quality, copy, interaction, accessibility or responsiveness findings remain. - -## Intentional Deviations - -- The platform's existing light top bar and vertical application navigation are preserved instead of cloning G7's dark global header. -- The required AMap `whitesmoke` map style is preserved instead of G7's colorful default map. -- A source/quality overview replaces G7's product-specific cache tab; data completeness is exposed as an on-map evidence strip. -- The route color is platform blue with state overlays rather than G7 red/green, keeping consistency with existing monitoring pages. - -## Follow-up Polish - -- P3: a future product iteration may add a paged sampled-point list if operations users confirm they need row-level point inspection in addition to the seek rail and event list. +- The compact coverage strip retains `32960 / 808 / 宇通` abbreviations because full tags would crowd the 292px context rail. +- Semantic state surfaces use low-contrast borders and tints; they do not become large cards or compete with the vehicle and discrepancy columns. +- No additional runtime query, animation or component state was introduced. final result: passed