refactor(ui): refine semi access workspace
This commit is contained in:
@@ -91,6 +91,16 @@ test('removes old access rows immediately when the vehicle filter scope changes'
|
||||
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');
|
||||
const protocolHeaders = [
|
||||
{ label: 'GB/T 32960', description: '国标远程服务' },
|
||||
{ label: 'JT/T 808', description: '道路运输终端' },
|
||||
{ label: '宇通 MQTT', description: '宇通车辆平台' }
|
||||
];
|
||||
for (const protocol of protocolHeaders) {
|
||||
const header = screen.getByRole('columnheader', { name: new RegExp(protocol.label.replace('/', '\\/')) });
|
||||
expect(within(header).getByText(protocol.label).closest('.semi-tag')).toHaveClass('v2-protocol-tag', 'is-compact');
|
||||
expect(within(header).getByText(protocol.description)).toBeInTheDocument();
|
||||
}
|
||||
const desktopRow = screen.getByTestId('access-row-OLDVIN');
|
||||
expect(desktopRow).toHaveAttribute('role', 'button');
|
||||
expect(desktopRow).toHaveAttribute('aria-expanded', 'false');
|
||||
@@ -108,6 +118,10 @@ test('removes old access rows immediately when the vehicle filter scope changes'
|
||||
expect(healthOverview).toHaveTextContent('当前在线1');
|
||||
expect(healthOverview).toHaveTextContent('异常来源0');
|
||||
expect(healthOverview).toHaveTextContent('资料状态已维护');
|
||||
expect(detailDialog.querySelectorAll('.v2-access-protocol-title .v2-protocol-tag')).toHaveLength(3);
|
||||
expect(within(detailDialog).getByText('GB/T 32960')).toBeInTheDocument();
|
||||
expect(within(detailDialog).getByText('JT/T 808')).toBeInTheDocument();
|
||||
expect(within(detailDialog).getByText('宇通 MQTT')).toBeInTheDocument();
|
||||
expect(detailDialog.querySelector('.v2-access-inspector-header')).not.toBeInTheDocument();
|
||||
fireEvent.change(screen.getByRole('textbox', { name: '车辆' }), { target: { value: 'NEWVIN' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '查询' }));
|
||||
|
||||
@@ -25,7 +25,8 @@ import { downloadBlob } from '../domain/download';
|
||||
import { useMobileLayout } from '../hooks/useMobileLayout';
|
||||
|
||||
const PROTOCOLS = ['GB32960', 'JT808', 'YUTONG_MQTT'] as const;
|
||||
const protocolThresholdMeta: Record<(typeof PROTOCOLS)[number], { label: string; description: string }> = {
|
||||
type AccessProtocol = (typeof PROTOCOLS)[number];
|
||||
const protocolThresholdMeta: Record<AccessProtocol, { label: string; description: string }> = {
|
||||
GB32960: { label: 'GB32960', description: '国标远程服务' },
|
||||
JT808: { label: 'JT808', description: '道路运输终端' },
|
||||
YUTONG_MQTT: { label: 'YUTONG MQTT', description: '宇通车辆平台' }
|
||||
@@ -82,14 +83,23 @@ function protocolPriority(status?: AccessProtocolStatus) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
function AccessProtocolTitle({ protocol, compact = false }: { protocol: AccessProtocol; compact?: boolean }) {
|
||||
return <span className={`v2-access-protocol-title${compact ? ' is-compact' : ''}`}>
|
||||
<ProtocolTag protocol={protocol} compact={compact} />
|
||||
<small>{protocolThresholdMeta[protocol].description}</small>
|
||||
</span>;
|
||||
}
|
||||
|
||||
const ProtocolState = memo(function ProtocolState({
|
||||
status,
|
||||
protocol,
|
||||
detailed = false,
|
||||
protocolLabel,
|
||||
expanded = false,
|
||||
onToggle
|
||||
}: {
|
||||
status?: AccessProtocolStatus;
|
||||
protocol: AccessProtocol;
|
||||
detailed?: boolean;
|
||||
protocolLabel?: string;
|
||||
expanded?: boolean;
|
||||
@@ -99,12 +109,11 @@ const ProtocolState = memo(function ProtocolState({
|
||||
const label = state === 'missing' ? '无来源' : state === 'online' ? '在线' : state === 'offline' ? '离线' : state === 'unknown' ? '未知' : '从未上报';
|
||||
const color = state === 'online' ? 'green' : state === 'offline' || state === 'unknown' ? 'orange' : 'grey';
|
||||
if (detailed) {
|
||||
const protocol = status?.protocol || '未知协议';
|
||||
const evidenceId = `access-protocol-${protocol.toLowerCase().replace(/[^a-z0-9]+/g, '-')}-evidence`;
|
||||
const evidence = status?.firstSeenEvidence || '当前未发现该协议来源';
|
||||
return <Card
|
||||
className={`v2-access-protocol-detail is-${state}${expanded ? ' is-expanded' : ''}`}
|
||||
title={<strong>{protocol}</strong>}
|
||||
title={<AccessProtocolTitle protocol={protocol} />}
|
||||
headerExtraContent={<Tag className={`v2-access-protocol-tag is-${state}`} color={color} type="light" size="small"><i />{label}</Tag>}
|
||||
headerLine
|
||||
bodyStyle={{ padding: 0 }}
|
||||
@@ -196,8 +205,8 @@ const AccessVehicleTable = memo(function AccessVehicleTable({ rows, selectedVIN,
|
||||
render: (_: string, row: AccessVehicleRow) => <div className="v2-access-primary-cell"><strong>{row.oem || '品牌未维护'}</strong><span>{row.model || row.company || '车型未维护'}</span></div>
|
||||
},
|
||||
...PROTOCOLS.map((protocol) => ({
|
||||
title: protocol, dataIndex: protocol, width: 160,
|
||||
render: (_: unknown, row: AccessVehicleRow) => <ProtocolState status={statusByProtocol(row, protocol)} />
|
||||
title: <AccessProtocolTitle protocol={protocol} compact />, dataIndex: protocol, width: 160,
|
||||
render: (_: unknown, row: AccessVehicleRow) => <ProtocolState protocol={protocol} status={statusByProtocol(row, protocol)} />
|
||||
})),
|
||||
{
|
||||
title: '差异摘要', dataIndex: 'connectionState', width: 220,
|
||||
@@ -262,6 +271,7 @@ function VehicleInspector({ row, onClose, sheet = false, mobile = false }: { row
|
||||
</div>
|
||||
<CardGroup className="v2-access-protocol-details" type="grid" spacing={0}>{protocols.map((protocol) => <ProtocolState
|
||||
key={protocol}
|
||||
protocol={protocol}
|
||||
status={statusByProtocol(row, protocol)}
|
||||
detailed
|
||||
expanded={expandedProtocol === protocol}
|
||||
@@ -520,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} 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${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}
|
||||
|
||||
@@ -10960,6 +10960,39 @@
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.v2-access-protocol-title {
|
||||
display: inline-grid;
|
||||
min-width: 0;
|
||||
justify-items: start;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.v2-access-protocol-title > small {
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
color: #8491a4;
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
line-height: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.v2-access-protocol-title.is-compact {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.v2-access-protocol-title.is-compact .v2-protocol-tag.semi-tag {
|
||||
min-height: 19px;
|
||||
padding-inline: 6px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.v2-access-protocol-title.is-compact > small {
|
||||
font-size: 8px;
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
.v2-access-table-v3 .semi-table-tbody > .semi-table-row > .semi-table-row-cell {
|
||||
height: 58px;
|
||||
padding-block: 6px;
|
||||
@@ -11011,10 +11044,45 @@
|
||||
background: #f2f7ff;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.v2-access-semi-table .semi-table-tbody > .semi-table-row.is-selected > .semi-table-row-cell:not(.semi-table-cell-fixed-left) {
|
||||
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 {
|
||||
box-shadow: 12px 0 18px -18px rgba(35, 55, 82, .72);
|
||||
}
|
||||
|
||||
.v2-access-semi-table .v2-access-protocol-cell {
|
||||
gap: 2px;
|
||||
border: 1px solid #e6ebf2;
|
||||
border-radius: 7px;
|
||||
background: #fafbfd;
|
||||
padding: 4px 7px;
|
||||
}
|
||||
|
||||
.v2-access-semi-table .v2-access-protocol-cell.is-online {
|
||||
border-color: #d9ece5;
|
||||
background: #f7fcfa;
|
||||
}
|
||||
|
||||
.v2-access-semi-table .v2-access-protocol-cell.is-offline,
|
||||
.v2-access-semi-table .v2-access-protocol-cell.is-unknown {
|
||||
border-color: #f1e2ca;
|
||||
background: #fffbf5;
|
||||
}
|
||||
|
||||
.v2-access-semi-table .v2-access-protocol-cell > span {
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.v2-access-semi-table .v2-access-protocol-cell > strong {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.v2-access-semi-table .semi-table-body::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
@@ -11106,6 +11174,16 @@
|
||||
box-shadow: 0 7px 20px rgba(42, 72, 110, .055);
|
||||
}
|
||||
|
||||
.v2-access-protocol-detail > .semi-card-header .v2-access-protocol-title {
|
||||
padding-block: 5px;
|
||||
}
|
||||
|
||||
.v2-access-protocol-detail > .semi-card-header .v2-access-protocol-title .v2-protocol-tag.semi-tag {
|
||||
min-height: 20px;
|
||||
padding-inline: 7px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.v2-access-inspector-focus.is-degraded.semi-card,
|
||||
.v2-access-inspector-focus.is-incomplete.semi-card {
|
||||
border-color: #f0d9b7;
|
||||
|
||||
Reference in New Issue
Block a user