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