diff --git a/design-qa.md b/design-qa.md new file mode 100644 index 00000000..d78ac009 --- /dev/null +++ b/design-qa.md @@ -0,0 +1,68 @@ +# Historical Data Semi UI Design QA + +## Evidence + +- Source visual truth: `/tmp/semi-ui-reference-1440.png` (accepted Semi UI workspace direction). +- Existing flow source: `/tmp/history-before-desktop.jpg` (historical data before this iteration). +- Implementation screenshot: `/tmp/history-after-desktop.jpg`. +- Responsive screenshots: `/tmp/history-after-1024.jpg`, `/tmp/history-after-mobile-sheet.jpg`. +- Full-view comparison: `/tmp/semi-history-design-comparison.jpg`. +- Same-route iteration comparison: `/tmp/history-before-after-comparison.jpg`. +- Requested desktop viewport: 1440 × 900. The embedded browser surface captured 1280 × 720; an additional exact 1024 × 768 capture was checked. +- Mobile viewport: 390 × 844. +- State: location history query for one vehicle, empty local result, desktop query collapsed; mobile filter sheet open. + +## Full-view comparison + +The implementation keeps the accepted product skeleton: persistent left navigation, white header, blue/gray visual hierarchy, restrained cards, compact controls, table-first workspace, and the same icon family. The historical-data workflow intentionally keeps its evidence table instead of copying the mileage-specific hero and mileage matrix. + +The before/after comparison shows the material layout change: the applied query remains readable in the command rail while the full desktop filter panel can collapse, returning the vertical workspace to the historical detail table. + +## Focused comparison + +The mobile filter sheet was checked separately because its labels and controls are too small to assess reliably in the desktop comparison. At 390 × 844, the summary rail, date range, type/source selects, export action, and sticky reset/apply actions remain visible and touch-safe without horizontal overflow. + +## Fidelity ledger + +| Surface | Source evidence | Render evidence | Result | +| --- | --- | --- | --- | +| Typography | Compact Semi UI chrome with strong page/task titles and smaller utility copy | History title, command rail, metric values, panel title, filters and mobile sheet preserve the same hierarchy | Passed | +| Layout rhythm | Persistent navigation, 12–18px page gutters, one main data surface | Query rail, three metrics and the table align on one grid; collapsed state increases table height without adding another card family | Passed | +| Colors/tokens | White surfaces, cool gray canvas, blue primary, green healthy state | Existing shared tokens and Semi UI variants are reused; no new palette or gradient was introduced | Passed | +| Icons/assets | Supplied brand logo and Semi icon family | Brand assets are unchanged; filter, calendar, search, setting, export and more actions use the same Semi icon family | Passed | +| Copy/content | Query, range, data type/source and result actions are business-native | No business fields or routes were added or removed; the applied scope reuses existing query values | Passed | +| Responsive behavior | Dense desktop workspace with task-focused mobile disclosure | 1280 × 720, 1024 × 768 and 390 × 844 checks show no clipped primary action or horizontal page overflow | Passed | +| Interaction | Query controls should not compete with results after submission | Collapse/expand, apply/reset, mobile filter sheet and row/detail paths are covered by interaction tests | Passed | + +## Above-the-fold copy diff + +- Added visible control labels: `收起条件` and `修改条件`, required for the new query disclosure. +- Existing task title, description, status, query labels and result labels are preserved. +- No unapproved marketing copy, badge, module, navigation item or business claim was added. + +## Comparison history + +1. Earlier finding — P2: the desktop filter panel permanently occupied roughly 108px after a query, reducing the primary evidence-table viewport. + - Fix: added a lightweight desktop-only disclosure control and an applied-scope summary for time, category and source. + - Post-fix evidence: `/tmp/history-before-after-comparison.jpg`; the table begins materially higher while the query remains recoverable through `修改条件`. +2. Earlier finding — P2: mobile record quality relied on a small text indicator only. + - Fix: added a three-pixel semantic quality rail with selected-state override while retaining the existing quality label. + - Post-fix evidence: component test confirms semantic quality classes; no additional card height is introduced. + +## Findings + +- No actionable P0, P1 or P2 visual differences remain. +- P3 follow-up: Semi UI 2.101 still emits its existing `findDOMNode` StrictMode warning; it does not affect the rendered layout or interaction path in this iteration. + +## Core interaction verification + +- Desktop query collapse and re-open. +- Query submission collapses the desktop panel and preserves applied scope. +- Reset restores the full desktop query panel. +- Mobile filter sheet open, reset, apply and close. +- Historical trend, field settings, export task menu, pagination and row-detail interaction remain covered by the full test suite. +- Browser console contains no application exception or framework overlay; only the existing Semi UI StrictMode deprecation warning appears. + +## Final result + +final result: passed diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx index 54c43719..bf573745 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx @@ -139,7 +139,16 @@ test('clears stale rows, charts, and evidence as soon as the history scope chang } expect(view.container.querySelector('.v2-history-discovery-shell')).toContainElement(screen.getByLabelText('历史数据查询操作')); expect(screen.getByLabelText('历史数据查询操作')).toHaveTextContent('遥测证据检索'); - expect(screen.getByLabelText('历史数据查询操作')).toHaveTextContent('上海时区'); + expect(screen.getByLabelText('历史数据查询操作')).toHaveTextContent('07-16 00:00 – 07-16 05:00'); + expect(screen.getByLabelText('历史数据查询操作')).toHaveTextContent('位置数据'); + expect(screen.getByLabelText('历史数据查询操作')).toHaveTextContent('全部来源'); + const desktopQueryToggle = screen.getByRole('button', { name: '收起查询条件' }); + expect(desktopQueryToggle).toHaveAttribute('aria-expanded', 'true'); + fireEvent.click(desktopQueryToggle); + expect(view.container.querySelector('.v2-history-discovery-shell')).toHaveAttribute('data-query-collapsed', 'true'); + expect(screen.getByRole('button', { name: '修改查询条件' })).toHaveAttribute('aria-expanded', 'false'); + fireEvent.click(screen.getByRole('button', { name: '修改查询条件' })); + expect(view.container.querySelector('.v2-history-discovery-shell')).toHaveAttribute('data-query-collapsed', 'false'); expect(view.container.querySelector('.v2-history-date-time-range.semi-datepicker')).toBeInTheDocument(); expect(screen.getByRole('group', { name: '快捷时间范围' })).toHaveClass('semi-button-group'); expect(screen.getByPlaceholderText('开始时间')).toHaveValue('2026-07-16 00:00'); @@ -275,7 +284,7 @@ test('renders only selectable Semi history cards on mobile', async () => { const action = await screen.findByRole('button', { name: '查看 粤A移动历史 2026-07-16 04:00:00 数据详情' }); expect(action).toHaveClass('semi-button', 'v2-history-mobile-action'); - expect(action.closest('.semi-card')).toHaveClass('v2-history-mobile-card'); + expect(action.closest('.semi-card')).toHaveClass('v2-history-mobile-card', 'is-quality-normal'); expect(view.container.querySelector('.v2-history-data-table')).not.toBeInTheDocument(); expect(action).toHaveAttribute('aria-pressed', 'false'); expect(action).toHaveAttribute('aria-expanded', 'false'); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx index 31935d36..64f4efdd 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx @@ -1,5 +1,5 @@ import { useMutation, useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query'; -import { IconCalendar, IconChevronRight, IconClose, IconDownload, IconMapPin, IconMore, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons'; +import { IconCalendar, IconChevronRight, IconClose, IconDownload, IconFilter, IconMapPin, IconMore, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons'; import { Button, Card, CardGroup, Checkbox, Descriptions, Dropdown, Empty, Input, Progress, Select, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui'; import { FormEvent, KeyboardEvent, useCallback, useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; @@ -369,6 +369,7 @@ export default function HistoryPage() { const [columnSettingsOpen, setColumnSettingsOpen] = useState(false); const [trendExpanded, setTrendExpanded] = useState(false); const [filtersCollapsed, setFiltersCollapsed] = useState(true); + const [desktopQueryCollapsed, setDesktopQueryCollapsed] = useState(false); const [exportJobsOpen, setExportJobsOpen] = useState(false); const [toolsOpen, setToolsOpen] = useState(false); useEffect(() => { @@ -445,6 +446,7 @@ export default function HistoryPage() { const next = { ...draft, keywords: parsed.join(',') }; setCriteria(next); setOffset(0); setFiltersCollapsed(true); + setDesktopQueryCollapsed(true); const url = new URLSearchParams({ keywords: next.keywords, category: next.category }); if (next.dateFrom) url.set('dateFrom', next.dateFrom); if (next.dateTo) url.set('dateTo', next.dateTo); @@ -453,7 +455,7 @@ export default function HistoryPage() { }; const submit = (event: FormEvent) => { event.preventDefault(); applyDraft(); }; const resetDraft = () => setDraft({ keywords: '', ...currentHistoryWindow(), category: 'location', protocol: '' }); - const reset = () => { const next = { keywords: '', ...currentHistoryWindow(), category: 'location', protocol: '' }; setDraft(next); setCriteria(next); setOffset(0); setFiltersCollapsed(true); setSearchParams(preserveMonitorReturn(new URLSearchParams(), monitorReturn), { replace: true }); }; + const reset = () => { const next = { keywords: '', ...currentHistoryWindow(), category: 'location', protocol: '' }; setDraft(next); setCriteria(next); setOffset(0); setFiltersCollapsed(true); setDesktopQueryCollapsed(false); setSearchParams(preserveMonitorReturn(new URLSearchParams(), monitorReturn), { replace: true }); }; const toggleMetric = (key: string) => setVisibleByCategory((current) => { const baseline = current[criteria.category] ?? allMetrics.filter((metric) => metric.defaultVisible).map((metric) => metric.key); const next = baseline.includes(key) ? baseline.filter((item) => item !== key) : [...baseline, key]; @@ -471,6 +473,11 @@ export default function HistoryPage() { const page = Math.floor(offset / limit) + 1; const summarySources = resultSummary?.sources?.join('、') || '—'; const summaryLoading = keywords.length > 0 && dataQuery.isFetching && !result; + const appliedCategoryLabel = catalogQuery.data?.categories.find((item) => item.key === criteria.category)?.label + ?? (criteria.category === 'raw' ? '原始报文' : criteria.category === 'mileage' ? '日里程' : '位置数据'); + const appliedWindowLabel = criteria.dateFrom && criteria.dateTo + ? `${criteria.dateFrom.replace('T', ' ').slice(5, 16)} – ${criteria.dateTo.replace('T', ' ').slice(5, 16)}` + : '时间待选择'; const historyMetrics: WorkspaceQueueMetricRailItem[] = [ { label: '结果行数', @@ -513,7 +520,7 @@ export default function HistoryPage() { return
-
+
上海时区 · 最多 5 台车辆} + meta={keywords.length ? + {appliedWindowLabel} + {appliedCategoryLabel} + {criteria.protocol || '全部来源'} + : 上海时区 · 最多 5 台车辆} + actions={!mobileLayout ? : undefined} /> {mobileLayout ?
setFiltersCollapsed((value) => !value)} > -
+ {historyFilterFields}
{!exportAllowed ? 只读 : null}
@@ -648,7 +669,7 @@ export default function HistoryPage() { onKeyDown={scrollHistoryTable} > {mobileLayout - ?
{resultRows.map((row) => )}
+ ?
{resultRows.map((row) => )}
: resultRows.length ? : null} {dataQuery.isPending && keywords.length ? 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 2460d5ec..7d9f9570 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -8260,13 +8260,41 @@ } .v2-history-mobile-list > .v2-history-mobile-card.semi-card { + position: relative; + overflow: hidden; border-radius: 8px; box-shadow: none; contain-intrinsic-size: auto 108px; } + .v2-history-mobile-list > .v2-history-mobile-card.semi-card::before { + position: absolute; + inset: 7px auto 7px 0; + z-index: 2; + width: 3px; + border-radius: 0 3px 3px 0; + background: #18a76f; + content: ''; + pointer-events: none; + } + + .v2-history-mobile-list > .v2-history-mobile-card:is(.is-quality-warning, .is-quality-unknown).semi-card::before { + background: #ed9f31; + } + + .v2-history-mobile-list > .v2-history-mobile-card:is(.is-quality-error, .is-quality-critical).semi-card::before { + background: #e95a52; + } + + .v2-history-mobile-list > .v2-history-mobile-card.is-selected.semi-card::before { + inset-block: 0; + width: 4px; + border-radius: 0; + background: var(--v2-blue); + } + .v2-history-mobile-card-content { - padding: 8px 9px; + padding: 8px 9px 8px 11px; } .v2-history-mobile-card-content header strong { @@ -15546,6 +15574,64 @@ box-shadow: none; } +.v2-history-command-bar .v2-workspace-command-actions { + min-width: 0; +} + +.v2-history-applied-scope { + display: inline-flex; + min-width: 0; + align-items: center; + color: #64758b; + font-size: 10px; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.v2-history-applied-scope > span { + display: inline-flex; + min-width: 0; + align-items: center; + gap: 4px; +} + +.v2-history-applied-scope > span + span { + margin-left: 8px; + border-left: 1px solid #dce4ee; + padding-left: 8px; +} + +.v2-history-applied-scope > span:first-child { + max-width: min(28vw, 330px); + overflow: hidden; + text-overflow: ellipsis; +} + +.v2-history-applied-scope .semi-icon { + flex: 0 0 auto; + color: #6d91bf; + font-size: 12px; +} + +.v2-history-query-toggle.semi-button { + height: 32px; + min-height: 32px; + flex: 0 0 auto; + border-color: #d7e2ef; + border-radius: 8px; + background: #f7faff; + padding-inline: 10px; + color: #4f657e; + font-size: 11px; + font-weight: 700; +} + +.v2-history-query-toggle.semi-button:hover { + border-color: #b9cee9; + background: #edf5ff; + color: #1d64c5; +} + .v2-history-discovery-shell > .v2-history-filter-card.v2-workspace-filter-panel.semi-card { width: 100%; overflow: visible; @@ -15646,6 +15732,21 @@ box-shadow: none; } +@media (min-width: 681px) { + .v2-history-discovery-shell[data-query-collapsed='true'], + .v2-history-discovery-shell[data-query-collapsed='true'] > .v2-history-command-bar { + border-radius: 14px; + } + + .v2-history-discovery-shell[data-query-collapsed='true'] > .v2-history-command-bar { + border-bottom: 0; + } + + .v2-history-discovery-shell[data-query-collapsed='true'] > .v2-history-filter-card { + display: none; + } +} + @media (min-width: 1101px) { .v2-history-discovery-shell .v2-history-toolbar { grid-template-columns: minmax(220px, 1.05fr) minmax(400px, 1.85fr) minmax(112px, .55fr) minmax(126px, .6fr) auto; @@ -15659,6 +15760,24 @@ } } +@media (min-width: 681px) and (max-width: 980px) { + .v2-history-command-bar .v2-workspace-command-copy > .semi-typography:last-child { + display: none; + } + + .v2-history-command-bar .v2-workspace-command-actions { + gap: 6px; + } + + .v2-history-applied-scope > span:nth-child(n + 2) { + display: none; + } + + .v2-history-query-toggle.semi-button { + padding-inline: 8px; + } +} + .v2-history-time { color: inherit; font-variant-numeric: tabular-nums;