refine Semi UI access evidence

This commit is contained in:
lingniu
2026-07-18 17:37:29 +08:00
parent 82d78926c6
commit 801a9851ea
3 changed files with 186 additions and 8 deletions

View File

@@ -57,6 +57,11 @@ test('removes old access rows immediately when the vehicle filter scope changes'
expect(view.container.querySelector(`.${className}.semi-card`)).toBeInTheDocument();
}
expect(screen.getByRole('navigation', { name: '接入差异筛选' })).toHaveClass('v2-access-status-tabs');
const protocolCoverage = screen.getByLabelText('真实协议来源概览');
expect(protocolCoverage).toHaveTextContent('真实来源覆盖');
expect(protocolCoverage).toHaveTextContent('32960');
expect(protocolCoverage).toHaveTextContent('808');
expect(protocolCoverage).toHaveTextContent('宇通');
expect(screen.getByRole('columnheader', { name: '差异摘要' })).toBeInTheDocument();
expect(screen.queryByRole('columnheader', { name: '真实来源' })).not.toBeInTheDocument();
expect(screen.getByText('已接来源状态正常')).toBeInTheDocument();
@@ -92,6 +97,12 @@ test('removes old access rows immediately when the vehicle filter scope changes'
expect(within(detailDialog).getByText('旧接入车牌 · OLDVIN')).toBeInTheDocument();
expect(within(detailDialog).getByText('接入结论')).toBeInTheDocument();
expect(within(detailDialog).getByText('已接来源状态正常')).toBeInTheDocument();
expect(within(detailDialog).getByText('处置建议 · 无需处理,持续监测来源上报')).toBeInTheDocument();
const healthOverview = within(detailDialog).getByRole('group', { name: '接入健康概览' });
expect(healthOverview).toHaveTextContent('已接入1 / 3');
expect(healthOverview).toHaveTextContent('当前在线1');
expect(healthOverview).toHaveTextContent('异常来源0');
expect(healthOverview).toHaveTextContent('资料状态已维护');
expect(detailDialog.querySelector('.v2-access-inspector-header')).not.toBeInTheDocument();
fireEvent.change(screen.getByRole('textbox', { name: '车辆' }), { target: { value: 'NEWVIN' } });
fireEvent.click(screen.getByRole('button', { name: '查询' }));
@@ -195,7 +206,8 @@ test('renders mobile access vehicles as selectable Semi cards', async () => {
expect(within(dialog).getByRole('button', { name: '关闭车辆接入详情' })).toBeInTheDocument();
expect(dialog.querySelector('.v2-access-inspector-v3.semi-card')).toBeInTheDocument();
expect(dialog.querySelector('.v2-access-inspector-focus.semi-card')).toBeInTheDocument();
expect(dialog.querySelector('.v2-access-inspector-summary-grid')).toHaveAttribute('aria-label', '车辆接入概览');
expect(within(dialog).getByRole('group', { name: '接入健康概览' })).toBeInTheDocument();
expect(dialog.querySelector('.v2-access-mobile-profile')).toHaveTextContent('车辆档案测试品牌 / 测试车型');
expect(within(dialog).getByText('协议证据')).toBeInTheDocument();
expect(within(dialog).getByText('1 / 3 已接入').closest('.semi-tag')).toBeInTheDocument();
expect(dialog.querySelector('.v2-access-protocol-details.semi-card-group')).toBeInTheDocument();

View File

@@ -157,13 +157,22 @@ const ConnectionState = memo(function ConnectionState({ row }: { row: AccessVehi
function ProtocolCoverage({ summary }: { summary?: AccessSummary }) {
return <div className="v2-access-protocol-coverage" aria-label="真实协议来源概览">
<small></small>
{PROTOCOLS.map((protocol) => {
const actual = summary?.protocols.find((item) => item.name === protocol);
return <span key={protocol}><b>{protocol}</b><em>{(actual?.total ?? 0).toLocaleString('zh-CN')} </em></span>;
return <span key={protocol}><b>{compactProtocolLabel(protocol)}</b><em>{(actual?.total ?? 0).toLocaleString('zh-CN')} </em></span>;
})}
</div>;
}
function accessRecommendation(row: AccessVehicleRow) {
if (row.connectionState === 'healthy') return '无需处理,持续监测来源上报';
if (row.connectionState === 'incomplete') return '补齐品牌、车型或来源标识';
if (row.connectionState === 'not_connected') return '核对车端设备与接入映射';
if (row.connectionState === 'offline') return '优先核对设备供电与接入链路';
return '优先核对离线或延迟异常来源';
}
const AccessVehicleTable = memo(function AccessVehicleTable({ rows, selectedVIN, onSelect }: { rows: AccessVehicleRow[]; selectedVIN: string; onSelect: (vin: string) => void }) {
const columns = useMemo(() => [
{
@@ -205,6 +214,14 @@ const AccessVehicleTable = memo(function AccessVehicleTable({ rows, selectedVIN,
function VehicleInspector({ row, onClose, sheet = false, mobile = false }: { row: AccessVehicleRow; onClose: () => void; sheet?: boolean; mobile?: boolean }) {
const protocols = useMemo(() => [...PROTOCOLS].sort((left, right) => protocolPriority(statusByProtocol(row, left)) - protocolPriority(statusByProtocol(row, right))), [row]);
const health = useMemo(() => {
const connected = row.protocolStatuses.filter((item) => item.connected);
return {
connected: connected.length,
online: connected.filter((item) => item.onlineState === 'online').length,
attention: connected.filter((item) => protocolNeedsAttention(item)).length
};
}, [row.protocolStatuses]);
const [expandedProtocol, setExpandedProtocol] = useState(() => protocols.find((protocol) => protocolNeedsAttention(statusByProtocol(row, protocol))) || '');
const toggleProtocol = useCallback((protocol: string) => {
setExpandedProtocol((current) => current === protocol ? '' : protocol);
@@ -213,14 +230,16 @@ function VehicleInspector({ row, onClose, sheet = false, mobile = false }: { row
return <Card className={`v2-access-inspector-v3${sheet ? ' is-sheet' : ''}`} bodyStyle={{ padding: 0 }}>
{sheet ? null : <WorkspacePanelHeader className="v2-access-inspector-header" title={row.plate || '未绑定车牌'} description={row.vin} actions={<Button theme="borderless" icon={<IconClose />} onClick={onClose} aria-label="关闭车辆接入详情" />} />}
<Card className={`v2-access-inspector-focus is-${row.connectionState}`} bodyStyle={{ padding: 0 }}>
<div><small>{row.connectionState === 'healthy' ? '接入结论' : '优先核对'}</small><strong>{accessIssueSummary(row)}</strong><span>{row.actualProtocols.length ? `已发现 ${row.actualProtocols.join(' / ')} ${row.actualProtocols.length} 个真实来源` : '当前没有可用的真实来源证据'}</span></div>
<div><small>{row.connectionState === 'healthy' ? '接入结论' : '优先核对'}</small><strong>{accessIssueSummary(row)}</strong><span>{row.actualProtocols.length ? `已发现 ${row.actualProtocols.join(' / ')} ${row.actualProtocols.length} 个真实来源` : '当前没有可用的真实来源证据'}</span><b> · {accessRecommendation(row)}</b></div>
<ConnectionTag row={row} />
</Card>
{mobile ? <div className="v2-access-inspector-summary-grid" role="group" aria-label="车辆接入概览">
<span><small> / </small><strong>{[row.oem, row.model].filter(Boolean).join(' / ') || '未维护'}</strong></span>
<span><small></small><strong>{row.actualProtocols.length ? row.actualProtocols.join(' / ') : '尚无来源'}</strong></span>
<span><small></small><strong>{row.masterDataIssues.length ? row.masterDataIssues.join('') : '已维护'}</strong></span>
</div> : <Descriptions className="v2-access-inspector-summary" align="left" size="small" data={[
<div className="v2-access-health-strip" role="group" aria-label="接入健康概览">
<span><small></small><strong>{health.connected} / {PROTOCOLS.length}</strong></span>
<span><small>线</small><strong className="is-good">{health.online}</strong></span>
<span><small></small><strong className={health.attention ? 'is-danger' : ''}>{health.attention}</strong></span>
<span><small></small><strong className={row.masterDataIssues.length ? 'is-warning' : ''}>{row.masterDataIssues.length ? `${row.masterDataIssues.length} 项待维护` : '已维护'}</strong></span>
</div>
{mobile ? <div className="v2-access-mobile-profile"><small></small><strong>{[row.oem, row.model].filter(Boolean).join(' / ') || '品牌与车型未维护'}</strong></div> : <Descriptions className="v2-access-inspector-summary" align="left" size="small" data={[
{ key: '品牌 / 车型', value: [row.oem, row.model].filter(Boolean).join(' / ') || '未维护' },
{ key: '真实接入来源', value: row.actualProtocols.length ? row.actualProtocols.join(' / ') : '尚无来源' },
{ key: '资料状态', value: row.masterDataIssues.length ? row.masterDataIssues.join('') : '已维护' }

View File

@@ -9200,6 +9200,44 @@
flex: 1 1 0;
}
.v2-access-protocol-coverage {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
}
.v2-access-protocol-coverage > small {
color: #8794a6;
font-size: 10px;
font-weight: 600;
white-space: nowrap;
}
.v2-access-protocol-coverage > span {
display: inline-flex;
min-height: 27px;
align-items: center;
gap: 5px;
border: 1px solid #e0e7f0;
border-radius: 7px;
background: #f8fafd;
padding: 4px 7px;
white-space: nowrap;
}
.v2-access-protocol-coverage b {
color: #50627a;
font-size: 10px;
}
.v2-access-protocol-coverage em {
color: #78879a;
font-size: 10px;
font-style: normal;
font-variant-numeric: tabular-nums;
}
.v2-access-table-v3.semi-card,
.v2-access-table-v3 > .semi-card-body {
height: 100%;
@@ -9452,10 +9490,101 @@
line-height: 1.45;
}
.v2-access-inspector-focus b {
margin-top: 3px;
color: #526c91;
font-size: 10px;
font-weight: 650;
line-height: 1.45;
}
.v2-access-inspector-focus.is-degraded b,
.v2-access-inspector-focus.is-incomplete b {
color: #9c640f;
}
.v2-access-inspector-focus.is-offline b,
.v2-access-inspector-focus.is-not_connected b {
color: #a45048;
}
.v2-access-inspector-focus .v2-access-connection-tag.semi-tag {
flex: 0 0 auto;
}
.v2-access-health-strip {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
margin: 10px 12px 0;
overflow: hidden;
border: 1px solid #e0e7ef;
border-radius: 10px;
background: #fff;
}
.v2-access-health-strip > span {
display: grid;
min-width: 0;
gap: 5px;
padding: 10px 11px;
}
.v2-access-health-strip > span + span {
border-left: 1px solid #e8edf3;
}
.v2-access-health-strip small {
color: #8794a6;
font-size: 9px;
white-space: nowrap;
}
.v2-access-health-strip strong {
overflow: hidden;
color: #34475f;
font-size: 15px;
line-height: 1;
text-overflow: ellipsis;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.v2-access-health-strip strong.is-good {
color: #169b62;
}
.v2-access-health-strip strong.is-danger {
color: #d1493f;
}
.v2-access-health-strip strong.is-warning {
color: #b86c0d;
}
.v2-access-mobile-profile {
display: flex;
min-height: 42px;
align-items: center;
gap: 10px;
border-bottom: 1px solid #e6ebf2;
background: #fff;
padding: 8px 12px;
}
.v2-access-mobile-profile small {
flex: 0 0 auto;
color: #8794a6;
font-size: 10px;
}
.v2-access-mobile-profile strong {
overflow: hidden;
color: #40536b;
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.v2-access-inspector-focus + .v2-access-inspector-summary.semi-descriptions {
padding-top: 8px;
}
@@ -9875,6 +10004,24 @@
font-size: 13px;
}
.v2-access-health-strip {
margin: 7px 9px 0;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.v2-access-health-strip > span {
gap: 4px;
padding: 8px 6px;
}
.v2-access-health-strip small {
font-size: 9px;
}
.v2-access-health-strip strong {
font-size: 12px;
}
.v2-ops-page {
height: auto;
min-height: 100%;