diff --git a/design-qa.md b/design-qa.md index d78ac009..03f49658 100644 --- a/design-qa.md +++ b/design-qa.md @@ -1,67 +1,78 @@ -# Historical Data Semi UI Design QA +# Alert Center 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. +- Accepted visual direction: `/tmp/semi-ui-reference-1440.png`. +- Existing flow baseline: `/tmp/alerts-before-desktop.jpg`. +- Final desktop implementation: `/tmp/alerts-after-desktop-v2.jpg`. +- Final desktop inspector state: `/tmp/alerts-after-inspector.jpg`. +- Final mobile queue: `/tmp/alerts-after-mobile-v2.jpg`. +- Final mobile disposition sheet: `/tmp/alerts-after-mobile-detail.jpg`. +- System comparison: `/tmp/semi-alerts-design-comparison.jpg`. +- Same-route comparison: `/tmp/alerts-before-after-comparison.jpg`. +- Desktop capture: 1280 × 720 (the embedded browser's available desktop surface). +- Responsive viewport: 390 × 844. ## 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 final alert center preserves the accepted Semi UI product skeleton: persistent navigation, white operational surfaces, cool-gray canvas, blue primary actions, compact segmented tabs, restrained status colors, and one list-first work surface. -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. +Compared with the baseline, the filter band is 14px shorter, the misleading radio-selection column is removed, row priority can be scanned through semantic left rails, and the desktop table no longer produces 160px of avoidable horizontal overflow. The recovered height is returned to the event queue. ## 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. +The selected-event state was checked independently because the evidence inspector changes the page grid. At 1280 × 720 the final split is 628px / 390px with zero page overflow; the list remains interactive and the inspector keeps vehicle identity, trigger evidence, disposition controls and audit progress visible. + +The 390 × 844 mobile state shows four high-density event cards in one screen. Opening a card produces one bottom SideSheet with the same evidence and disposition order as desktop, 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 | +| Typography | Strong task title, compact utility copy, tabular values | Command title, queue title, vehicle identity, evidence values and helper copy retain the shared Semi hierarchy | Passed | +| Spacing and density | 12–18px workspace gutters and compact operational controls | Filter height reduced from 68px to 54px; table rows remain 52px and mobile cards preserve four visible records | Passed | +| Colors and states | White/cool-gray surfaces, blue action state, restrained semantic colors | Severity rails and tags use existing critical/major/minor tokens; no new palette or decorative gradient was introduced | Passed | +| Icons and assets | Brand logo plus Semi icon family | Existing logo and alarm, filter, refresh, settings, bell and chevron icons are retained | Passed | +| Table and queue | List-first operational workspace | Radio chrome removed; fixed table layout eliminates desktop horizontal overflow and selected rows use the shared blue focus state | Passed | +| Evidence inspector | On-demand detail without replacing the queue | Desktop 628/390 split and mobile SideSheet preserve trigger, threshold, delta, notes and audit timeline | Passed | +| Responsive behavior | Dense desktop with focused mobile disclosure | 1280 × 720 and 390 × 844 have zero page-level horizontal overflow; mobile title, metrics, cards and pagination remain reachable | Passed | +| Interaction | Rows, filters, tabs and actions must remain functional | Whole-row keyboard/click selection, advanced filter SideSheet, status metrics, rule tabs and mobile disposition sheet are test-covered | 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. +- Changed `告警事件` to `当前事件队列` to clarify the primary work surface. +- Changed table header `严重程度` to `级别` and `触发 / 阈值` to `触发证据` for faster scanning. +- Changed the list meta from only `共 N 条` to `N 条待处置 · 共 N 条`. +- Existing business claims, tab names, statuses, rules, routes and disposition actions are preserved. ## 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. +1. P2 — The baseline desktop table had a 160px horizontal overflow at 1280px, despite all columns being operationally compact. + - Fix: removed the redundant radio column, tightened column widths and applied a fixed table layout. + - Post-fix measurement: `scrollWidth 1015px = clientWidth 1015px`. +2. P2 — The first compact-inspector pass still overflowed because the final cascade restored a 650px + 420px grid. + - Fix: changed the final grid to `minmax(0, 1fr) minmax(360px, 390px)` and tightened compact columns. + - Post-fix measurement: `628px / 390px`, page overflow `0`. +3. P2 — The first mobile capture hid the queue title and left a visually empty header area. + - Fix: hide only the description on mobile while retaining the `当前事件队列` heading. + - Post-fix evidence: `/tmp/alerts-after-mobile-v2.jpg`. ## 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. +- P3 follow-up: Semi UI 2.101 still emits its existing `findDOMNode` StrictMode warning in tests; it does not affect this layout or interaction path. ## 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. +- Desktop keyword, severity and status query. +- Advanced filter SideSheet open, apply and reset. +- Metric shortcuts for active and ended states. +- Whole-row mouse and keyboard selection. +- Desktop inspector open, switch event and close. +- Mobile filter sheet open/apply and mobile event SideSheet open/close. +- Rule configuration and notification tabs remain covered by the full suite. +- Full suite: 73 files, 366 tests passed. +- Production build gate: passed. ## Final result diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx index 9609f781..b2af0377 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx @@ -157,9 +157,12 @@ test('clears old alert rows and inspector evidence when the event scope changes' expect(screen.getByRole('button', { name: '查看未读通知,共 0 条' }).closest('[role="listitem"]')).toHaveClass('is-warning', 'is-secondary'); expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).toBeInTheDocument(); expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).not.toHaveClass('is-compact'); - expect(screen.getByRole('columnheader', { name: '触发 / 阈值' })).toBeInTheDocument(); + expect(screen.getByRole('heading', { level: 5, name: '当前事件队列' })).toBeInTheDocument(); + expect(screen.getByText('1 条待处置 · 共 1 条')).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: '触发证据' })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: '位置' })).toBeInTheDocument(); expect(view.container.querySelector('.v2-alert-event-table .v2-alert-protocol-tag.semi-tag')).toHaveTextContent('JT/T 808'); + expect(view.container.querySelector('.v2-alert-event-table input[type="radio"]')).not.toBeInTheDocument(); expect(view.container.querySelector('.v2-alert-table-scroll > table')).not.toBeInTheDocument(); expect(view.container.querySelector('.v2-alert-mobile-list')).not.toBeInTheDocument(); expect(view.container.querySelector('time[datetime="2026-07-16T04:00:00Z"]')).toHaveTextContent('07-16 12:00:00'); @@ -172,7 +175,8 @@ test('clears old alert rows and inspector evidence when the event scope changes' expect(view.container.querySelector('.v2-alert-inspector.semi-card')).toBeInTheDocument(); expect(view.container.querySelector('.v2-alert-workspace')).toHaveClass('is-inspector-open'); expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).toHaveClass('is-compact'); - expect(screen.queryByRole('columnheader', { name: '触发 / 阈值' })).not.toBeInTheDocument(); + expect(desktopAlertRow).toHaveClass('is-severity-major', 'is-selected'); + expect(screen.queryByRole('columnheader', { name: '触发证据' })).not.toBeInTheDocument(); expect(screen.queryByRole('columnheader', { name: '位置' })).not.toBeInTheDocument(); expect(screen.getByText('已展开处置详情 · 点击其他事件快速切换')).toBeInTheDocument(); expect(desktopAlertRow).toHaveAttribute('aria-expanded', 'true'); @@ -194,7 +198,7 @@ test('clears old alert rows and inspector evidence when the event scope changes' fireEvent.click(screen.getByRole('button', { name: '关闭告警详情' })); expect(view.container.querySelector('.v2-alert-inspector')).not.toBeInTheDocument(); expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).not.toHaveClass('is-compact'); - expect(screen.getByRole('columnheader', { name: '触发 / 阈值' })).toBeInTheDocument(); + expect(screen.getByRole('columnheader', { name: '触发证据' })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: '位置' })).toBeInTheDocument(); expect(desktopAlertRow).toHaveAttribute('aria-expanded', 'false'); fireEvent.click(desktopAlertRow); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx index c1026986..078bbfa1 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx @@ -1,5 +1,5 @@ import { IconAlarm, IconBell, IconChevronRight, IconClose, IconFilter, IconPlus, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons'; -import { Button, Card, CardGroup, Collapse, Descriptions, Empty, Input, Radio, Select, Spin, Table, Tag, TextArea, Timeline, Typography } from '@douyinfe/semi-ui'; +import { Button, Card, CardGroup, Collapse, Descriptions, Empty, Input, Select, Spin, Table, Tag, TextArea, Timeline, Typography } from '@douyinfe/semi-ui'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { FormEvent, memo, useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'; import { Link, useSearchParams } from 'react-router-dom'; @@ -32,13 +32,12 @@ const PROTOCOLS = ['GB32960', 'JT808', 'YUTONG_MQTT']; const NUMERIC_OPERATORS = ['gt', 'gte', 'lt', 'lte', 'eq', 'neq', 'between', 'outside']; const BOOLEAN_OPERATORS = ['eq', 'neq', 'changed']; const COMPACT_ALERT_COLUMN_WIDTHS: Record = { - selection: 40, - severity: 80, - plate: 132, - ruleName: 148, - protocol: 100, - triggeredAt: 120, - status: 78 + severity: 70, + plate: 116, + ruleName: 124, + protocol: 86, + triggeredAt: 110, + status: 72 }; type RuleTemplate = { id: string; title: string; summary: string; draft: AlertRuleInput }; @@ -90,24 +89,20 @@ function AlertTime({ value, className = '' }: { value?: string; className?: stri const AlertEventTable = memo(function AlertEventTable({ rows, selectedID, compact, onSelect }: { rows: AlertEvent[]; selectedID: string; compact: boolean; onSelect: (id: string) => void }) { const columns = useMemo(() => { const allColumns = [ + { title: '级别', dataIndex: 'severity', width: 78, render: (_: AlertEvent['severity'], event: AlertEvent) => }, { - title: '', dataIndex: 'selection', width: 42, - render: (_: unknown, event: AlertEvent) => onSelect(event.id)} aria-label={`选择 ${event.plate || event.vin} ${event.ruleName}`} /> - }, - { title: '严重程度', dataIndex: 'severity', width: 90, render: (_: AlertEvent['severity'], event: AlertEvent) => }, - { - title: '车牌 / VIN', dataIndex: 'plate', width: 142, + title: '车牌 / VIN', dataIndex: 'plate', width: 132, render: (_: string, event: AlertEvent) =>
{event.plate || '未绑定车牌'}{event.vin}
}, - { title: '规则', dataIndex: 'ruleName', width: 170, render: (value: string) => {value} }, - { title: '协议', dataIndex: 'protocol', width: 112, render: (value: string) => }, - { title: '触发时间', dataIndex: 'triggeredAt', width: 132, render: (value: string) => }, - { title: '状态', dataIndex: 'status', width: 90, render: (_: AlertEvent['status'], event: AlertEvent) => }, + { title: '规则', dataIndex: 'ruleName', width: 146, render: (value: string) => {value} }, + { title: '协议', dataIndex: 'protocol', width: 98, render: (value: string) => }, + { title: '触发时间', dataIndex: 'triggeredAt', width: 124, render: (value: string) => }, + { title: '状态', dataIndex: 'status', width: 78, render: (_: AlertEvent['status'], event: AlertEvent) => }, { - title: '触发 / 阈值', dataIndex: 'evidence', width: 164, + title: '触发证据', dataIndex: 'evidence', width: 150, render: (_: unknown, event: AlertEvent) =>
{alertValue(event)}{thresholdText(event)}
}, - { title: '位置', dataIndex: 'location', width: 180, render: (value: string) => {value || '—'} }, + { title: '位置', dataIndex: 'location', width: 164, render: (value: string) => {value || '—'} }, ]; return compact ? allColumns @@ -124,7 +119,7 @@ const AlertEventTable = memo(function AlertEventTable({ rows, selectedID, compac pagination={false} empty={null} onRow={(event) => event ? detailTriggerRow({ - className: selectedID === event.id ? 'is-selected' : '', + className: [`is-severity-${event.severity}`, selectedID === event.id ? 'is-selected' : ''].filter(Boolean).join(' '), expanded: selectedID === event.id, label: `查看 ${event.plate || event.vin} ${event.ruleName} 告警详情`, testId: `alert-row-${event.id}`, @@ -354,9 +349,9 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
} loading={events.isFetching} onClick={() => Promise.all([events.refetch(), summary.refetch(), ...(selectedID ? [detail.refetch()] : [])])}>刷新} />
{ expect(corePageSources.AlertsPage).toContain(' .semi-card-body > .v2-workspace-filter-heading { @@ -8510,24 +8511,36 @@ } .v2-alert-filter.v2-alert-filter-primary { - min-height: 70px; + min-height: 54px; grid-template-columns: minmax(250px, 1.45fr) minmax(122px, .62fr) minmax(136px, .68fr) auto auto auto; gap: 10px; - padding: 11px 13px; + align-items: center; + padding: 8px 10px; } .v2-alert-filter.v2-alert-filter-primary label { + position: relative; gap: 5px; color: #66778e; font-size: 11px; font-weight: 600; } +.v2-alert-filter.v2-alert-filter-primary label > span { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + clip-path: inset(50%); + white-space: nowrap; +} + .v2-alert-filter.v2-alert-filter-primary label > div, .v2-alert-filter.v2-alert-filter-primary .semi-select, .v2-alert-filter.v2-alert-filter-primary .semi-input-wrapper, .v2-alert-filter.v2-alert-filter-primary > .semi-button { - min-height: 38px; + min-height: 36px; border-radius: 8px; } @@ -8588,7 +8601,7 @@ } .v2-alert-workspace.is-inspector-open { - grid-template-columns: minmax(680px, 1fr) 390px; + grid-template-columns: minmax(0, 1fr) minmax(360px, 390px); } .v2-alert-table-card > .semi-card-body > .v2-workspace-panel-header { @@ -8646,12 +8659,51 @@ font-size: 12px; } +.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row { + position: relative; + transition: background-color .16s ease, box-shadow .16s ease; +} + +.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row::before { + position: absolute; + z-index: 2; + top: 7px; + bottom: 7px; + left: 0; + width: 3px; + border-radius: 0 4px 4px 0; + background: #c2cad5; + content: ""; + pointer-events: none; +} + +.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row.is-severity-critical::before { + background: #e5484d; +} + +.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row.is-severity-major::before { + background: #ed8d32; +} + +.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row.is-severity-minor::before { + background: #d9a31d; +} + +.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row:hover { + background: #f7faff; +} + +.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row.is-selected { + background: #eef5ff; + box-shadow: inset 0 0 0 1px rgba(18, 104, 243, .16); +} + .v2-alert-event-table.semi-table-wrapper { - min-width: 1102px; + min-width: 970px; } .v2-alert-event-table.is-compact.semi-table-wrapper { - min-width: 758px; + min-width: 660px; } .v2-alert-event-evidence { @@ -9529,10 +9581,15 @@ padding: 5px 8px; } - .v2-alert-table-card .v2-workspace-panel-copy > .semi-typography { + .v2-alert-table-card .v2-workspace-panel-copy > .semi-typography:not(h5) { display: none; } + .v2-alert-table-card .v2-workspace-panel-copy > h5.semi-typography { + display: block; + font-size: 13px; + } + .v2-alert-table-scroll.is-mobile-scroll { min-height: 0; flex: 1 1 0; @@ -25382,8 +25439,8 @@ } .v2-alert-filter.v2-alert-filter-primary { - min-height: 66px; - padding: 9px 13px; + min-height: 52px; + padding: 7px 10px; } .v2-alert-filter.v2-alert-filter-primary label > div, @@ -25417,13 +25474,18 @@ } .v2-alert-table-scroll .v2-alert-event-table.semi-table-wrapper .semi-table-tbody > .semi-table-row > .semi-table-row-cell { - height: 56px; - padding: 8px 10px; + height: 52px; + padding: 7px 10px; color: #405269; font-size: 12.5px; transition: background-color .16s ease; } +.v2-alert-event-table .semi-table { + width: 100%; + table-layout: fixed; +} + .v2-alert-table-scroll .v2-alert-event-table.semi-table-wrapper .semi-table-tbody > .semi-table-row:hover > .semi-table-row-cell { background: #f6f9fe; } @@ -25445,7 +25507,7 @@ } .v2-alert-workspace.is-inspector-open { - grid-template-columns: minmax(650px, 1fr) 420px; + grid-template-columns: minmax(0, 1fr) minmax(360px, 390px); } .v2-alert-workspace.is-inspector-open .v2-alert-event-table.is-compact.semi-table-wrapper {