feat(web): refine Semi UI access workspace
This commit is contained in:
@@ -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>);
|
||||
|
||||
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(view.container.querySelector('.v2-access-discovery-shell')).toBeInTheDocument();
|
||||
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('808');
|
||||
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.getByText('已接来源状态正常')).toBeInTheDocument();
|
||||
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' });
|
||||
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');
|
||||
expect(screen.getByRole('columnheader', { name: '品牌 / 车型' })).toHaveClass('v2-access-model-column');
|
||||
expect(screen.getByRole('columnheader', { name: '品牌 / 车型' })).not.toHaveClass('semi-table-cell-fixed-left');
|
||||
const protocolHeaders = [
|
||||
{ label: 'GB/T 32960', 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();
|
||||
}
|
||||
const desktopRow = screen.getByTestId('access-row-OLDVIN');
|
||||
expect(desktopRow).toHaveClass('is-state-healthy');
|
||||
expect(desktopRow).toHaveAttribute('role', 'button');
|
||||
expect(desktopRow).toHaveAttribute('aria-expanded', 'false');
|
||||
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>
|
||||
},
|
||||
{
|
||||
title: '品牌 / 车型', dataIndex: 'oem', width: 140, fixed: 'left' as const, className: 'v2-access-model-column',
|
||||
render: (_: string, row: AccessVehicleRow) => <div className="v2-access-primary-cell"><strong>{row.oem || '品牌未维护'}</strong><span>{row.model || row.company || '车型未维护'}</span></div>
|
||||
title: '差异结论', dataIndex: 'connectionState', width: 220, fixed: 'left' as const, className: 'v2-access-difference-column',
|
||||
render: (_: AccessVehicleRow['connectionState'], row: AccessVehicleRow) => <ConnectionState row={row} />
|
||||
},
|
||||
...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)} />
|
||||
})),
|
||||
{
|
||||
title: '差异摘要', dataIndex: 'connectionState', width: 220,
|
||||
render: (_: AccessVehicleRow['connectionState'], row: AccessVehicleRow) => <ConnectionState row={row} />
|
||||
title: '品牌 / 车型', dataIndex: 'oem', width: 140, className: 'v2-access-model-column',
|
||||
render: (_: string, row: AccessVehicleRow) => <div className="v2-access-primary-cell"><strong>{row.oem || '品牌未维护'}</strong><span>{row.model || row.company || '车型未维护'}</span></div>
|
||||
}
|
||||
], []);
|
||||
|
||||
return <Table
|
||||
className="v2-access-semi-table"
|
||||
style={{ width: '100%' }}
|
||||
scroll={{ x: 1005 }}
|
||||
scroll={{ x: 975 }}
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
rowKey="vin"
|
||||
pagination={false}
|
||||
empty={null}
|
||||
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,
|
||||
label: `查看 ${row.plate || row.vin} 接入详情`,
|
||||
testId: `access-row-${row.vin}`,
|
||||
@@ -433,10 +433,10 @@ export default function AccessPage() {
|
||||
<WorkspaceCommandBar
|
||||
className="v2-access-command-bar"
|
||||
ariaLabel="接入管理操作"
|
||||
title="真实来源与接入健康"
|
||||
title="接入健康工作台"
|
||||
description={mobileLayout
|
||||
? summary ? `截至 ${compactTime(summary.asOf)} · ${summary.totalVehicles.toLocaleString('zh-CN')} 辆主车辆` : '正在读取最新接入状态'
|
||||
: '真实来源、在线健康与资料差异一处核对'}
|
||||
: '核对真实来源、在线状态与车辆档案差异'}
|
||||
status={summary ? `${summary.totalVehicles.toLocaleString('zh-CN')} 辆主车辆` : '正在读取车辆'}
|
||||
meta={<Typography.Text type="tertiary">数据时间 <AccessTime value={summary?.asOf} /></Typography.Text>}
|
||||
actions={mobileLayout ? <>
|
||||
@@ -509,8 +509,8 @@ export default function AccessPage() {
|
||||
<div className="v2-access-workspace-v3">
|
||||
<Card className="v2-access-table-v3" bodyStyle={{ padding: 0 }}>
|
||||
<WorkspacePanelHeader
|
||||
title={scopeLabels[criteria.connectionState] || '车辆接入清单'}
|
||||
description="缺席协议仅表示当前未发现;优先核对异常、离线与资料差异"
|
||||
title="接入差异清单"
|
||||
description={`当前范围:${scopeLabels[criteria.connectionState] || '全部主车辆'} · 缺席协议仅表示当前未发现`}
|
||||
actionsClassName="v2-access-table-actions"
|
||||
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}
|
||||
>
|
||||
{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} />}
|
||||
{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}
|
||||
|
||||
@@ -266,7 +266,7 @@ describe('V2 production entry', () => {
|
||||
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 className={`v2-access-inspector-focus');
|
||||
expect(corePageSources.AccessPage).toContain("title: '差异摘要'");
|
||||
expect(corePageSources.AccessPage).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-protocol-descriptions"');
|
||||
|
||||
@@ -11079,13 +11079,13 @@
|
||||
}
|
||||
|
||||
.v2-access-table-v3 .semi-table-tbody > .semi-table-row > .semi-table-row-cell {
|
||||
height: 58px;
|
||||
height: 54px;
|
||||
padding-block: 6px;
|
||||
}
|
||||
|
||||
.v2-access-table-scroll-v3 table {
|
||||
width: max(100%, 1005px) !important;
|
||||
min-width: 1005px;
|
||||
width: max(100%, 975px) !important;
|
||||
min-width: 975px;
|
||||
}
|
||||
|
||||
.v2-access-table-scroll-v3 {
|
||||
@@ -11109,7 +11109,7 @@
|
||||
|
||||
.v2-access-semi-table .semi-table-body {
|
||||
overflow-x: auto;
|
||||
scroll-padding-inline-start: 305px;
|
||||
scroll-padding-inline-start: 385px;
|
||||
scrollbar-color: #b7c4d4 #f4f7fa;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
@@ -11129,6 +11129,18 @@
|
||||
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 {
|
||||
box-shadow: inset 3px 0 0 #2f7de1;
|
||||
}
|
||||
@@ -11137,7 +11149,7 @@
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -11185,11 +11197,11 @@
|
||||
}
|
||||
|
||||
.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(4),
|
||||
.v2-access-table-scroll-v3 th:nth-child(5) { width: 160px; }
|
||||
.v2-access-table-scroll-v3 th:nth-child(6) { width: 220px; }
|
||||
.v2-access-table-scroll-v3 th:nth-child(5) { width: 150px; }
|
||||
.v2-access-table-scroll-v3 th:nth-child(6) { width: 140px; }
|
||||
|
||||
.v2-access-connection {
|
||||
min-width: 0;
|
||||
@@ -11614,10 +11626,41 @@
|
||||
}
|
||||
|
||||
.v2-access-mobile-card.semi-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 9px;
|
||||
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 {
|
||||
gap: 5px;
|
||||
padding: 8px 9px;
|
||||
@@ -11668,17 +11711,33 @@
|
||||
}
|
||||
|
||||
.v2-access-mobile-protocols {
|
||||
gap: 4px;
|
||||
padding-top: 5px;
|
||||
gap: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5eaf1;
|
||||
border-radius: 7px;
|
||||
background: #f7f9fc;
|
||||
}
|
||||
|
||||
.v2-access-mobile-list .v2-access-protocol-cell {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 2px 4px;
|
||||
border-radius: 6px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
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 {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
Reference in New Issue
Block a user