feat(web): refine Semi UI access workspace
This commit is contained in:
86
design-qa.md
86
design-qa.md
@@ -1,76 +1,78 @@
|
|||||||
# Alert Center Semi UI Design QA
|
# Access Management Semi UI Design QA
|
||||||
|
|
||||||
## Evidence
|
## Evidence
|
||||||
|
|
||||||
- Accepted visual direction: `/tmp/semi-ui-reference-1440.png`.
|
- Accepted visual direction: `/tmp/semi-ui-reference-1440.png`.
|
||||||
- Existing flow baseline: `/tmp/alerts-before-desktop.jpg`.
|
- Existing flow baseline: `/tmp/access-before-desktop.jpg`.
|
||||||
- Final desktop implementation: `/tmp/alerts-after-desktop-v2.jpg`.
|
- Final desktop implementation: `/tmp/access-after-desktop-final.jpg`.
|
||||||
- Final desktop inspector state: `/tmp/alerts-after-inspector.jpg`.
|
- Final mobile queue: `/tmp/access-after-mobile-visible.jpg`.
|
||||||
- Final mobile queue: `/tmp/alerts-after-mobile-v2.jpg`.
|
- Final mobile evidence sheet: `/tmp/access-after-mobile-detail.jpg`.
|
||||||
- Final mobile disposition sheet: `/tmp/alerts-after-mobile-detail.jpg`.
|
- Final mobile governance sheet: `/tmp/access-after-mobile-governance.jpg`.
|
||||||
- System comparison: `/tmp/semi-alerts-design-comparison.jpg`.
|
- Final mobile filter sheet: `/tmp/access-after-mobile-filter.jpg`.
|
||||||
- Same-route comparison: `/tmp/alerts-before-after-comparison.jpg`.
|
- System comparison: `/tmp/semi-access-design-comparison.jpg`.
|
||||||
- Desktop capture: 1280 × 720 (the embedded browser's available desktop surface).
|
- Same-route comparison: `/tmp/access-before-after-comparison.jpg`.
|
||||||
- Responsive viewport: 390 × 844.
|
- Desktop viewport and state: 1280 × 720, all authoritative vehicles, no row selected.
|
||||||
|
- Mobile viewport and states: 390 × 844, queue, filter, vehicle evidence and governance SideSheets.
|
||||||
|
|
||||||
## Full-view comparison
|
## Full-view comparison
|
||||||
|
|
||||||
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 final access workspace retains the accepted Semi UI skeleton: persistent navigation, white operational surfaces, cool-gray canvas, compact filters, restrained semantic colors, one dominant data list and on-demand SideSheets.
|
||||||
|
|
||||||
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.
|
The primary hierarchy changed from vehicle → brand/model → protocols → difference to vehicle → difference conclusion → protocols → brand/model. The actionable conclusion is now always adjacent to the vehicle and remains fixed during horizontal table movement. The 1280px state has no page or table overflow and shows five vehicle rows in the active viewport.
|
||||||
|
|
||||||
## Focused comparison
|
## Focused comparison
|
||||||
|
|
||||||
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 mobile queue was checked separately because the protocol matrix becomes a card list at 390px. Three protocol states now share one evidence band instead of three competing nested cards. Semantic rails make healthy, degraded, offline and selected vehicles scannable without increasing card height.
|
||||||
|
|
||||||
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.
|
The filter, vehicle evidence and governance sheets were each opened in the browser. Primary reset/apply, evidence disclosure, configuration summary and completion actions remain visible without horizontal overflow.
|
||||||
|
|
||||||
## Fidelity ledger
|
## Fidelity ledger
|
||||||
|
|
||||||
| Surface | Source evidence | Render evidence | Result |
|
| Surface | Source evidence | Render evidence | Result |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 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 |
|
| Typography | Strong work-surface title, compact labels and tabular evidence values | Command title, difference conclusion, vehicle identity, protocol timestamps and inspector hierarchy use the shared Semi scale | 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 |
|
| Spacing and layout rhythm | Compact command/filter band with one dominant list | Filter height remains 54px; table rows are reduced to 54px; five desktop rows and roughly four mobile cards are visible | 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 |
|
| Colors and tokens | White/cool-gray surfaces, blue actions and restrained semantic state colors | Existing green, amber, red and blue tokens are reused for row rails, status tags and selected state | Passed |
|
||||||
| Icons and assets | Brand logo plus Semi icon family | Existing logo and alarm, filter, refresh, settings, bell and chevron icons are retained | Passed |
|
| Image and asset fidelity | Supplied brand logo and Semi icon family | Brand logo is unchanged; search, settings, refresh, filter, download and disclosure icons remain Semi assets | 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 |
|
| Table hierarchy | Differences should dominate the access workflow | `差异结论` is now the second fixed column; protocol evidence follows and brand/model is explicitly secondary | 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 |
|
| Mobile density | Small screens need concise but complete vehicle evidence | Three protocol cells are grouped into one band; 390 × 844 has zero horizontal overflow and keeps pagination reachable | 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 |
|
| Detail and governance | Evidence and configuration should not replace the list permanently | Vehicle and governance tasks use focused SideSheets with summaries, evidence and stable close/complete actions | 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 |
|
| Interaction and accessibility | Rows, filters, disclosures and actions must be keyboard/touch operable | Whole-row selection, arrow-key table scrolling, filter SideSheet, evidence disclosure and governance actions are test-covered | Passed |
|
||||||
|
|
||||||
## Above-the-fold copy diff
|
## Above-the-fold copy diff
|
||||||
|
|
||||||
- Changed `告警事件` to `当前事件队列` to clarify the primary work surface.
|
- Changed `真实来源与接入健康` to `接入健康工作台`.
|
||||||
- Changed table header `严重程度` to `级别` and `触发 / 阈值` to `触发证据` for faster scanning.
|
- Changed `全部主车辆` panel title to `接入差异清单`; the current scope now appears in its description.
|
||||||
- Changed the list meta from only `共 N 条` to `N 条待处置 · 共 N 条`.
|
- Changed table header `差异摘要` to `差异结论`.
|
||||||
- Existing business claims, tab names, statuses, rules, routes and disposition actions are preserved.
|
- Existing filter fields, protocol names, vehicle facts, governance actions and business claims are preserved.
|
||||||
|
|
||||||
## Comparison history
|
## Comparison history
|
||||||
|
|
||||||
1. P2 — The baseline desktop table had a 160px horizontal overflow at 1280px, despite all columns being operationally compact.
|
1. P2 — The baseline placed the actionable difference summary after brand/model and three protocol columns.
|
||||||
- Fix: removed the redundant radio column, tightened column widths and applied a fixed table layout.
|
- Fix: moved `差异结论` next to vehicle identity and fixed both columns.
|
||||||
- Post-fix measurement: `scrollWidth 1015px = clientWidth 1015px`.
|
- Post-fix evidence: `/tmp/access-before-after-comparison.jpg`; the first three headers are `车辆`, `差异结论`, `GB/T 32960`.
|
||||||
2. P2 — The first compact-inspector pass still overflowed because the final cascade restored a 650px + 420px grid.
|
2. P2 — Mobile protocol status appeared as three independent mini-cards and made each vehicle feel fragmented.
|
||||||
- Fix: changed the final grid to `minmax(0, 1fr) minmax(360px, 390px)` and tightened compact columns.
|
- Fix: grouped the three protocol cells into one bordered evidence band with internal dividers.
|
||||||
- Post-fix measurement: `628px / 390px`, page overflow `0`.
|
- Post-fix evidence: `/tmp/access-after-mobile-visible.jpg`.
|
||||||
3. P2 — The first mobile capture hid the queue title and left a visually empty header area.
|
3. P2 — Row status depended mainly on small status tags and required close reading.
|
||||||
- Fix: hide only the description on mobile while retaining the `当前事件队列` heading.
|
- Fix: added semantic row/card rails for healthy, degraded/incomplete, offline/not-connected and selected states.
|
||||||
- Post-fix evidence: `/tmp/alerts-after-mobile-v2.jpg`.
|
- Post-fix evidence: desktop and mobile final captures; no added row or card height.
|
||||||
|
|
||||||
## Findings
|
## Findings
|
||||||
|
|
||||||
- No actionable P0, P1 or P2 visual differences remain.
|
- No actionable P0, P1 or P2 visual differences remain.
|
||||||
- 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.
|
- P3 follow-up: Semi UI 2.101 continues to emit its existing `findDOMNode` StrictMode warning in tests; it does not affect this layout or workflow.
|
||||||
|
|
||||||
## Core interaction verification
|
## Core interaction verification
|
||||||
|
|
||||||
- Desktop keyword, severity and status query.
|
- Desktop vehicle, status, protocol and brand query.
|
||||||
- Advanced filter SideSheet open, apply and reset.
|
- Difference and all-vehicle metric shortcuts.
|
||||||
- Metric shortcuts for active and ended states.
|
|
||||||
- Whole-row mouse and keyboard selection.
|
- Whole-row mouse and keyboard selection.
|
||||||
- Desktop inspector open, switch event and close.
|
- Horizontal protocol browsing with fixed vehicle and difference columns.
|
||||||
- Mobile filter sheet open/apply and mobile event SideSheet open/close.
|
- Mobile filter open, reset/apply affordances and close.
|
||||||
- Rule configuration and notification tabs remain covered by the full suite.
|
- Mobile vehicle evidence open, protocol disclosure and close.
|
||||||
|
- Mobile governance open, pending identity summary, threshold disclosure and complete.
|
||||||
|
- Browser state: no application error overlay, no visible error alert and no page-level horizontal overflow.
|
||||||
- Full suite: 73 files, 366 tests passed.
|
- Full suite: 73 files, 366 tests passed.
|
||||||
- Production build gate: passed.
|
- Production build gate: passed.
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ test('removes old access rows immediately when the vehicle filter scope changes'
|
|||||||
const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/access?keyword=OLDVIN']}><AccessPage /></MemoryRouter></QueryClientProvider>);
|
const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/access?keyword=OLDVIN']}><AccessPage /></MemoryRouter></QueryClientProvider>);
|
||||||
|
|
||||||
expect(await screen.findByText('旧接入车牌')).toBeInTheDocument();
|
expect(await screen.findByText('旧接入车牌')).toBeInTheDocument();
|
||||||
expect(screen.getByRole('heading', { name: '真实来源与接入健康', level: 5 })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: '接入健康工作台', level: 5 })).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText('接入管理操作')).toHaveClass('v2-workspace-command-bar', 'v2-access-command-bar');
|
expect(screen.getByLabelText('接入管理操作')).toHaveClass('v2-workspace-command-bar', 'v2-access-command-bar');
|
||||||
expect(view.container.querySelector('.v2-access-discovery-shell')).toBeInTheDocument();
|
expect(view.container.querySelector('.v2-access-discovery-shell')).toBeInTheDocument();
|
||||||
for (const className of ['v2-access-filter-card-v3', 'v2-access-table-v3']) {
|
for (const className of ['v2-access-filter-card-v3', 'v2-access-table-v3']) {
|
||||||
@@ -67,7 +67,10 @@ test('removes old access rows immediately when the vehicle filter scope changes'
|
|||||||
expect(protocolCoverage).toHaveTextContent('32960');
|
expect(protocolCoverage).toHaveTextContent('32960');
|
||||||
expect(protocolCoverage).toHaveTextContent('808');
|
expect(protocolCoverage).toHaveTextContent('808');
|
||||||
expect(protocolCoverage).toHaveTextContent('宇通');
|
expect(protocolCoverage).toHaveTextContent('宇通');
|
||||||
expect(screen.getByRole('columnheader', { name: '差异摘要' })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: '接入差异清单', level: 5 })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('当前范围:全部主车辆 · 缺席协议仅表示当前未发现')).toBeInTheDocument();
|
||||||
|
const differenceHeader = screen.getByRole('columnheader', { name: '差异结论' });
|
||||||
|
expect(differenceHeader).toHaveClass('semi-table-cell-fixed-left', 'v2-access-difference-column');
|
||||||
expect(screen.queryByRole('columnheader', { name: '真实来源' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('columnheader', { name: '真实来源' })).not.toBeInTheDocument();
|
||||||
expect(screen.getByText('已接来源状态正常')).toBeInTheDocument();
|
expect(screen.getByText('已接来源状态正常')).toBeInTheDocument();
|
||||||
const filterActions = view.container.querySelector<HTMLElement>('.v2-access-filter-actions');
|
const filterActions = view.container.querySelector<HTMLElement>('.v2-access-filter-actions');
|
||||||
@@ -90,7 +93,8 @@ test('removes old access rows immediately when the vehicle filter scope changes'
|
|||||||
fireEvent.keyDown(desktopAccessScroll!, { key: 'ArrowLeft' });
|
fireEvent.keyDown(desktopAccessScroll!, { key: 'ArrowLeft' });
|
||||||
expect(scrollBy).toHaveBeenCalledWith({ left: -260, behavior: 'smooth' });
|
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-vehicle-column');
|
||||||
expect(screen.getByRole('columnheader', { name: '品牌 / 车型' })).toHaveClass('semi-table-cell-fixed-left', 'v2-access-model-column');
|
expect(screen.getByRole('columnheader', { name: '品牌 / 车型' })).toHaveClass('v2-access-model-column');
|
||||||
|
expect(screen.getByRole('columnheader', { name: '品牌 / 车型' })).not.toHaveClass('semi-table-cell-fixed-left');
|
||||||
const protocolHeaders = [
|
const protocolHeaders = [
|
||||||
{ label: 'GB/T 32960', description: '国标远程服务' },
|
{ label: 'GB/T 32960', description: '国标远程服务' },
|
||||||
{ label: 'JT/T 808', description: '道路运输终端' },
|
{ label: 'JT/T 808', description: '道路运输终端' },
|
||||||
@@ -102,6 +106,7 @@ test('removes old access rows immediately when the vehicle filter scope changes'
|
|||||||
expect(within(header).getByText(protocol.description)).toBeInTheDocument();
|
expect(within(header).getByText(protocol.description)).toBeInTheDocument();
|
||||||
}
|
}
|
||||||
const desktopRow = screen.getByTestId('access-row-OLDVIN');
|
const desktopRow = screen.getByTestId('access-row-OLDVIN');
|
||||||
|
expect(desktopRow).toHaveClass('is-state-healthy');
|
||||||
expect(desktopRow).toHaveAttribute('role', 'button');
|
expect(desktopRow).toHaveAttribute('role', 'button');
|
||||||
expect(desktopRow).toHaveAttribute('aria-expanded', 'false');
|
expect(desktopRow).toHaveAttribute('aria-expanded', 'false');
|
||||||
fireEvent.keyDown(desktopRow, { key: 'Enter' });
|
fireEvent.keyDown(desktopRow, { key: 'Enter' });
|
||||||
|
|||||||
@@ -201,30 +201,30 @@ const AccessVehicleTable = memo(function AccessVehicleTable({ rows, selectedVIN,
|
|||||||
render: (_: string, row: AccessVehicleRow) => <div className="v2-access-primary-cell"><strong>{row.plate || '未绑定车牌'}</strong><span>{row.vin}</span></div>
|
render: (_: string, row: AccessVehicleRow) => <div className="v2-access-primary-cell"><strong>{row.plate || '未绑定车牌'}</strong><span>{row.vin}</span></div>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '品牌 / 车型', dataIndex: 'oem', width: 140, fixed: 'left' as const, className: 'v2-access-model-column',
|
title: '差异结论', dataIndex: 'connectionState', width: 220, fixed: 'left' as const, className: 'v2-access-difference-column',
|
||||||
render: (_: string, row: AccessVehicleRow) => <div className="v2-access-primary-cell"><strong>{row.oem || '品牌未维护'}</strong><span>{row.model || row.company || '车型未维护'}</span></div>
|
render: (_: AccessVehicleRow['connectionState'], row: AccessVehicleRow) => <ConnectionState row={row} />
|
||||||
},
|
},
|
||||||
...PROTOCOLS.map((protocol) => ({
|
...PROTOCOLS.map((protocol) => ({
|
||||||
title: <AccessProtocolTitle protocol={protocol} compact />, dataIndex: protocol, width: 160,
|
title: <AccessProtocolTitle protocol={protocol} compact />, dataIndex: protocol, width: 150,
|
||||||
render: (_: unknown, row: AccessVehicleRow) => <ProtocolState protocol={protocol} status={statusByProtocol(row, protocol)} />
|
render: (_: unknown, row: AccessVehicleRow) => <ProtocolState protocol={protocol} status={statusByProtocol(row, protocol)} />
|
||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
title: '差异摘要', dataIndex: 'connectionState', width: 220,
|
title: '品牌 / 车型', dataIndex: 'oem', width: 140, className: 'v2-access-model-column',
|
||||||
render: (_: AccessVehicleRow['connectionState'], row: AccessVehicleRow) => <ConnectionState row={row} />
|
render: (_: string, row: AccessVehicleRow) => <div className="v2-access-primary-cell"><strong>{row.oem || '品牌未维护'}</strong><span>{row.model || row.company || '车型未维护'}</span></div>
|
||||||
}
|
}
|
||||||
], []);
|
], []);
|
||||||
|
|
||||||
return <Table
|
return <Table
|
||||||
className="v2-access-semi-table"
|
className="v2-access-semi-table"
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
scroll={{ x: 1005 }}
|
scroll={{ x: 975 }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={rows}
|
dataSource={rows}
|
||||||
rowKey="vin"
|
rowKey="vin"
|
||||||
pagination={false}
|
pagination={false}
|
||||||
empty={null}
|
empty={null}
|
||||||
onRow={(row) => row ? detailTriggerRow({
|
onRow={(row) => row ? detailTriggerRow({
|
||||||
className: selectedVIN === row.vin ? 'is-selected' : '',
|
className: [`is-state-${row.connectionState}`, selectedVIN === row.vin ? 'is-selected' : ''].filter(Boolean).join(' '),
|
||||||
expanded: selectedVIN === row.vin,
|
expanded: selectedVIN === row.vin,
|
||||||
label: `查看 ${row.plate || row.vin} 接入详情`,
|
label: `查看 ${row.plate || row.vin} 接入详情`,
|
||||||
testId: `access-row-${row.vin}`,
|
testId: `access-row-${row.vin}`,
|
||||||
@@ -433,10 +433,10 @@ export default function AccessPage() {
|
|||||||
<WorkspaceCommandBar
|
<WorkspaceCommandBar
|
||||||
className="v2-access-command-bar"
|
className="v2-access-command-bar"
|
||||||
ariaLabel="接入管理操作"
|
ariaLabel="接入管理操作"
|
||||||
title="真实来源与接入健康"
|
title="接入健康工作台"
|
||||||
description={mobileLayout
|
description={mobileLayout
|
||||||
? summary ? `截至 ${compactTime(summary.asOf)} · ${summary.totalVehicles.toLocaleString('zh-CN')} 辆主车辆` : '正在读取最新接入状态'
|
? summary ? `截至 ${compactTime(summary.asOf)} · ${summary.totalVehicles.toLocaleString('zh-CN')} 辆主车辆` : '正在读取最新接入状态'
|
||||||
: '真实来源、在线健康与资料差异一处核对'}
|
: '核对真实来源、在线状态与车辆档案差异'}
|
||||||
status={summary ? `${summary.totalVehicles.toLocaleString('zh-CN')} 辆主车辆` : '正在读取车辆'}
|
status={summary ? `${summary.totalVehicles.toLocaleString('zh-CN')} 辆主车辆` : '正在读取车辆'}
|
||||||
meta={<Typography.Text type="tertiary">数据时间 <AccessTime value={summary?.asOf} /></Typography.Text>}
|
meta={<Typography.Text type="tertiary">数据时间 <AccessTime value={summary?.asOf} /></Typography.Text>}
|
||||||
actions={mobileLayout ? <>
|
actions={mobileLayout ? <>
|
||||||
@@ -509,8 +509,8 @@ export default function AccessPage() {
|
|||||||
<div className="v2-access-workspace-v3">
|
<div className="v2-access-workspace-v3">
|
||||||
<Card className="v2-access-table-v3" bodyStyle={{ padding: 0 }}>
|
<Card className="v2-access-table-v3" bodyStyle={{ padding: 0 }}>
|
||||||
<WorkspacePanelHeader
|
<WorkspacePanelHeader
|
||||||
title={scopeLabels[criteria.connectionState] || '车辆接入清单'}
|
title="接入差异清单"
|
||||||
description="缺席协议仅表示当前未发现;优先核对异常、离线与资料差异"
|
description={`当前范围:${scopeLabels[criteria.connectionState] || '全部主车辆'} · 缺席协议仅表示当前未发现`}
|
||||||
actionsClassName="v2-access-table-actions"
|
actionsClassName="v2-access-table-actions"
|
||||||
actions={mobileLayout ? null : <Button theme="light" icon={<IconDownload />} onClick={() => downloadRows(rows)} disabled={!rows.length}>导出当前页</Button>}
|
actions={mobileLayout ? null : <Button theme="light" icon={<IconDownload />} onClick={() => downloadRows(rows)} disabled={!rows.length}>导出当前页</Button>}
|
||||||
/>
|
/>
|
||||||
@@ -530,7 +530,7 @@ export default function AccessPage() {
|
|||||||
onKeyDown={scrollAccessTable}
|
onKeyDown={scrollAccessTable}
|
||||||
>
|
>
|
||||||
{mobileLayout
|
{mobileLayout
|
||||||
? <div className="v2-access-mobile-list">{rows.map((row) => <Card key={row.vin} className={`v2-access-mobile-card${selected?.vin === row.vin ? ' is-selected' : ''}`} bodyStyle={{ padding: 0 }}><Button theme="borderless" type="tertiary" aria-pressed={selected?.vin === row.vin} aria-expanded={selected?.vin === row.vin} aria-label={`查看 ${row.plate || row.vin} 接入详情`} className="v2-access-mobile-action" onClick={() => setSelectedVIN(row.vin)}><span className="v2-access-mobile-card-content"><header><span><strong>{row.plate || '未绑定车牌'}</strong><small>{row.vin}</small></span><ConnectionState row={row} /></header><p className={row.connectionState === 'healthy' ? '' : 'is-issue'} title={row.connectionState === 'healthy' ? undefined : accessIssueSummary(row)}>{row.connectionState === 'healthy' ? `${row.oem || '品牌未维护'} · ${row.model || row.company || '车型未维护'}` : `${accessIssueSummary(row)} · ${row.oem || '品牌未维护'} ${row.model || row.company || '车型未维护'}`}</p><span className="v2-access-mobile-protocols">{PROTOCOLS.map((protocol) => <ProtocolState key={protocol} protocol={protocol} protocolLabel={compactProtocolLabel(protocol)} status={statusByProtocol(row, protocol)} />)}</span><footer>查看接入详情<IconChevronRight /></footer></span></Button></Card>)}</div>
|
? <div className="v2-access-mobile-list">{rows.map((row) => <Card key={row.vin} className={`v2-access-mobile-card is-state-${row.connectionState}${selected?.vin === row.vin ? ' is-selected' : ''}`} bodyStyle={{ padding: 0 }}><Button theme="borderless" type="tertiary" aria-pressed={selected?.vin === row.vin} aria-expanded={selected?.vin === row.vin} aria-label={`查看 ${row.plate || row.vin} 接入详情`} className="v2-access-mobile-action" onClick={() => setSelectedVIN(row.vin)}><span className="v2-access-mobile-card-content"><header><span><strong>{row.plate || '未绑定车牌'}</strong><small>{row.vin}</small></span><ConnectionState row={row} /></header><p className={row.connectionState === 'healthy' ? '' : 'is-issue'} title={row.connectionState === 'healthy' ? undefined : accessIssueSummary(row)}>{row.connectionState === 'healthy' ? `${row.oem || '品牌未维护'} · ${row.model || row.company || '车型未维护'}` : `${accessIssueSummary(row)} · ${row.oem || '品牌未维护'} ${row.model || row.company || '车型未维护'}`}</p><span className="v2-access-mobile-protocols">{PROTOCOLS.map((protocol) => <ProtocolState key={protocol} protocol={protocol} protocolLabel={compactProtocolLabel(protocol)} status={statusByProtocol(row, protocol)} />)}</span><footer>查看接入详情<IconChevronRight /></footer></span></Button></Card>)}</div>
|
||||||
: <AccessVehicleTable rows={rows} selectedVIN={selectedVIN} onSelect={setSelectedVIN} />}
|
: <AccessVehicleTable rows={rows} selectedVIN={selectedVIN} onSelect={setSelectedVIN} />}
|
||||||
{vehiclesQuery.isFetching ? <PanelLoading className="v2-access-loading" title="正在更新车辆接入状态…" description="当前列表返回后会自动替换。" compact={Boolean(rows.length)} /> : null}
|
{vehiclesQuery.isFetching ? <PanelLoading className="v2-access-loading" title="正在更新车辆接入状态…" description="当前列表返回后会自动替换。" compact={Boolean(rows.length)} /> : null}
|
||||||
{!vehiclesQuery.isFetching && !rows.length ? <PanelEmpty className="v2-access-empty" title="没有匹配车辆" description="调整车牌、协议或接入状态筛选后重试。" /> : null}
|
{!vehiclesQuery.isFetching && !rows.length ? <PanelEmpty className="v2-access-empty" title="没有匹配车辆" description="调整车牌、协议或接入状态筛选后重试。" /> : null}
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ describe('V2 production entry', () => {
|
|||||||
expect(corePageSources.AccessPage).not.toContain('<aside className="v2-access-inspector-v3"');
|
expect(corePageSources.AccessPage).not.toContain('<aside className="v2-access-inspector-v3"');
|
||||||
expect(corePageSources.AccessPage).toContain('<Card key={row.vin} className={`v2-access-mobile-card');
|
expect(corePageSources.AccessPage).toContain('<Card key={row.vin} className={`v2-access-mobile-card');
|
||||||
expect(corePageSources.AccessPage).toContain('<Card className={`v2-access-inspector-focus');
|
expect(corePageSources.AccessPage).toContain('<Card className={`v2-access-inspector-focus');
|
||||||
expect(corePageSources.AccessPage).toContain("title: '差异摘要'");
|
expect(corePageSources.AccessPage).toContain("title: '差异结论'");
|
||||||
expect(corePageSources.AccessPage).not.toContain("title: '真实来源'");
|
expect(corePageSources.AccessPage).not.toContain("title: '真实来源'");
|
||||||
expect(corePageSources.AccessPage).toContain('<Descriptions className="v2-access-inspector-summary"');
|
expect(corePageSources.AccessPage).toContain('<Descriptions className="v2-access-inspector-summary"');
|
||||||
expect(corePageSources.AccessPage).toContain('<Descriptions className="v2-access-protocol-descriptions"');
|
expect(corePageSources.AccessPage).toContain('<Descriptions className="v2-access-protocol-descriptions"');
|
||||||
|
|||||||
@@ -11079,13 +11079,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.v2-access-table-v3 .semi-table-tbody > .semi-table-row > .semi-table-row-cell {
|
.v2-access-table-v3 .semi-table-tbody > .semi-table-row > .semi-table-row-cell {
|
||||||
height: 58px;
|
height: 54px;
|
||||||
padding-block: 6px;
|
padding-block: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v2-access-table-scroll-v3 table {
|
.v2-access-table-scroll-v3 table {
|
||||||
width: max(100%, 1005px) !important;
|
width: max(100%, 975px) !important;
|
||||||
min-width: 1005px;
|
min-width: 975px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v2-access-table-scroll-v3 {
|
.v2-access-table-scroll-v3 {
|
||||||
@@ -11109,7 +11109,7 @@
|
|||||||
|
|
||||||
.v2-access-semi-table .semi-table-body {
|
.v2-access-semi-table .semi-table-body {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
scroll-padding-inline-start: 305px;
|
scroll-padding-inline-start: 385px;
|
||||||
scrollbar-color: #b7c4d4 #f4f7fa;
|
scrollbar-color: #b7c4d4 #f4f7fa;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
@@ -11129,6 +11129,18 @@
|
|||||||
background: #f2f7ff;
|
background: #f2f7ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v2-access-semi-table .semi-table-tbody > .semi-table-row.is-state-healthy > .semi-table-row-cell:first-child {
|
||||||
|
box-shadow: inset 3px 0 0 #27a56f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-access-semi-table .semi-table-tbody > .semi-table-row:is(.is-state-degraded, .is-state-incomplete) > .semi-table-row-cell:first-child {
|
||||||
|
box-shadow: inset 3px 0 0 #e29a32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-access-semi-table .semi-table-tbody > .semi-table-row:is(.is-state-offline, .is-state-not_connected) > .semi-table-row-cell:first-child {
|
||||||
|
box-shadow: inset 3px 0 0 #db5753;
|
||||||
|
}
|
||||||
|
|
||||||
.v2-access-semi-table .semi-table-tbody > .semi-table-row.is-selected > .semi-table-row-cell:first-child {
|
.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;
|
box-shadow: inset 3px 0 0 #2f7de1;
|
||||||
}
|
}
|
||||||
@@ -11137,7 +11149,7 @@
|
|||||||
background: linear-gradient(90deg, #f2f7ff 0%, #f7faff 100%);
|
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 {
|
.v2-access-semi-table :is(.semi-table-row-head, .semi-table-row-cell).v2-access-difference-column {
|
||||||
box-shadow: 12px 0 18px -18px rgba(35, 55, 82, .72);
|
box-shadow: 12px 0 18px -18px rgba(35, 55, 82, .72);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11185,11 +11197,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.v2-access-table-scroll-v3 th:nth-child(1) { width: 165px; }
|
.v2-access-table-scroll-v3 th:nth-child(1) { width: 165px; }
|
||||||
.v2-access-table-scroll-v3 th:nth-child(2) { width: 140px; }
|
.v2-access-table-scroll-v3 th:nth-child(2) { width: 220px; }
|
||||||
.v2-access-table-scroll-v3 th:nth-child(3),
|
.v2-access-table-scroll-v3 th:nth-child(3),
|
||||||
.v2-access-table-scroll-v3 th:nth-child(4),
|
.v2-access-table-scroll-v3 th:nth-child(4),
|
||||||
.v2-access-table-scroll-v3 th:nth-child(5) { width: 160px; }
|
.v2-access-table-scroll-v3 th:nth-child(5) { width: 150px; }
|
||||||
.v2-access-table-scroll-v3 th:nth-child(6) { width: 220px; }
|
.v2-access-table-scroll-v3 th:nth-child(6) { width: 140px; }
|
||||||
|
|
||||||
.v2-access-connection {
|
.v2-access-connection {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -11614,10 +11626,41 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.v2-access-mobile-card.semi-card {
|
.v2-access-mobile-card.semi-card {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
contain-intrinsic-size: auto 126px;
|
contain-intrinsic-size: auto 126px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v2-access-mobile-card.semi-card::before {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 0;
|
||||||
|
width: 3px;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
background: #b8c3d0;
|
||||||
|
content: "";
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-access-mobile-card.semi-card.is-state-healthy::before {
|
||||||
|
background: #27a56f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-access-mobile-card.semi-card:is(.is-state-degraded, .is-state-incomplete)::before {
|
||||||
|
background: #e29a32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-access-mobile-card.semi-card:is(.is-state-offline, .is-state-not_connected)::before {
|
||||||
|
background: #db5753;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-access-mobile-card.semi-card.is-selected::before {
|
||||||
|
background: #2f7de1;
|
||||||
|
}
|
||||||
|
|
||||||
.v2-access-mobile-card-content {
|
.v2-access-mobile-card-content {
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 8px 9px;
|
padding: 8px 9px;
|
||||||
@@ -11668,17 +11711,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.v2-access-mobile-protocols {
|
.v2-access-mobile-protocols {
|
||||||
gap: 4px;
|
gap: 0;
|
||||||
padding-top: 5px;
|
overflow: hidden;
|
||||||
|
border: 1px solid #e5eaf1;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #f7f9fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v2-access-mobile-list .v2-access-protocol-cell {
|
.v2-access-mobile-list .v2-access-protocol-cell {
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
gap: 2px 4px;
|
gap: 2px 4px;
|
||||||
border-radius: 6px;
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
padding: 5px 6px;
|
padding: 5px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v2-access-mobile-list .v2-access-protocol-cell + .v2-access-protocol-cell {
|
||||||
|
border-left: 1px solid #e5eaf1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v2-access-mobile-list .v2-access-protocol-cell.is-online,
|
||||||
|
.v2-access-mobile-list .v2-access-protocol-cell.is-offline,
|
||||||
|
.v2-access-mobile-list .v2-access-protocol-cell.is-unknown {
|
||||||
|
border-color: #e5eaf1;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.v2-access-mobile-list .v2-access-protocol-label {
|
.v2-access-mobile-list .v2-access-protocol-label {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
Reference in New Issue
Block a user