refine Semi UI access evidence
This commit is contained in:
@@ -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(view.container.querySelector(`.${className}.semi-card`)).toBeInTheDocument();
|
||||||
}
|
}
|
||||||
expect(screen.getByRole('navigation', { name: '接入差异筛选' })).toHaveClass('v2-access-status-tabs');
|
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.getByRole('columnheader', { name: '差异摘要' })).toBeInTheDocument();
|
||||||
expect(screen.queryByRole('columnheader', { name: '真实来源' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('columnheader', { name: '真实来源' })).not.toBeInTheDocument();
|
||||||
expect(screen.getByText('已接来源状态正常')).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('旧接入车牌 · OLDVIN')).toBeInTheDocument();
|
||||||
expect(within(detailDialog).getByText('接入结论')).toBeInTheDocument();
|
expect(within(detailDialog).getByText('接入结论')).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();
|
expect(detailDialog.querySelector('.v2-access-inspector-header')).not.toBeInTheDocument();
|
||||||
fireEvent.change(screen.getByRole('textbox', { name: '车辆' }), { target: { value: 'NEWVIN' } });
|
fireEvent.change(screen.getByRole('textbox', { name: '车辆' }), { target: { value: 'NEWVIN' } });
|
||||||
fireEvent.click(screen.getByRole('button', { name: '查询' }));
|
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(within(dialog).getByRole('button', { name: '关闭车辆接入详情' })).toBeInTheDocument();
|
||||||
expect(dialog.querySelector('.v2-access-inspector-v3.semi-card')).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-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('协议证据')).toBeInTheDocument();
|
||||||
expect(within(dialog).getByText('1 / 3 已接入').closest('.semi-tag')).toBeInTheDocument();
|
expect(within(dialog).getByText('1 / 3 已接入').closest('.semi-tag')).toBeInTheDocument();
|
||||||
expect(dialog.querySelector('.v2-access-protocol-details.semi-card-group')).toBeInTheDocument();
|
expect(dialog.querySelector('.v2-access-protocol-details.semi-card-group')).toBeInTheDocument();
|
||||||
|
|||||||
@@ -157,13 +157,22 @@ const ConnectionState = memo(function ConnectionState({ row }: { row: AccessVehi
|
|||||||
|
|
||||||
function ProtocolCoverage({ summary }: { summary?: AccessSummary }) {
|
function ProtocolCoverage({ summary }: { summary?: AccessSummary }) {
|
||||||
return <div className="v2-access-protocol-coverage" aria-label="真实协议来源概览">
|
return <div className="v2-access-protocol-coverage" aria-label="真实协议来源概览">
|
||||||
|
<small>真实来源覆盖</small>
|
||||||
{PROTOCOLS.map((protocol) => {
|
{PROTOCOLS.map((protocol) => {
|
||||||
const actual = summary?.protocols.find((item) => item.name === 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>;
|
</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 AccessVehicleTable = memo(function AccessVehicleTable({ rows, selectedVIN, onSelect }: { rows: AccessVehicleRow[]; selectedVIN: string; onSelect: (vin: string) => void }) {
|
||||||
const columns = useMemo(() => [
|
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 }) {
|
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 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 [expandedProtocol, setExpandedProtocol] = useState(() => protocols.find((protocol) => protocolNeedsAttention(statusByProtocol(row, protocol))) || '');
|
||||||
const toggleProtocol = useCallback((protocol: string) => {
|
const toggleProtocol = useCallback((protocol: string) => {
|
||||||
setExpandedProtocol((current) => current === protocol ? '' : protocol);
|
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 }}>
|
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="关闭车辆接入详情" />} />}
|
{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 }}>
|
<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} />
|
<ConnectionTag row={row} />
|
||||||
</Card>
|
</Card>
|
||||||
{mobile ? <div className="v2-access-inspector-summary-grid" role="group" aria-label="车辆接入概览">
|
<div className="v2-access-health-strip" role="group" aria-label="接入健康概览">
|
||||||
<span><small>品牌 / 车型</small><strong>{[row.oem, row.model].filter(Boolean).join(' / ') || '未维护'}</strong></span>
|
<span><small>已接入</small><strong>{health.connected} / {PROTOCOLS.length}</strong></span>
|
||||||
<span><small>真实来源</small><strong>{row.actualProtocols.length ? row.actualProtocols.join(' / ') : '尚无来源'}</strong></span>
|
<span><small>当前在线</small><strong className="is-good">{health.online}</strong></span>
|
||||||
<span><small>资料状态</small><strong>{row.masterDataIssues.length ? row.masterDataIssues.join(';') : '已维护'}</strong></span>
|
<span><small>异常来源</small><strong className={health.attention ? 'is-danger' : ''}>{health.attention}</strong></span>
|
||||||
</div> : <Descriptions className="v2-access-inspector-summary" align="left" size="small" data={[
|
<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.oem, row.model].filter(Boolean).join(' / ') || '未维护' },
|
||||||
{ key: '真实接入来源', value: row.actualProtocols.length ? row.actualProtocols.join(' / ') : '尚无来源' },
|
{ key: '真实接入来源', value: row.actualProtocols.length ? row.actualProtocols.join(' / ') : '尚无来源' },
|
||||||
{ key: '资料状态', value: row.masterDataIssues.length ? row.masterDataIssues.join(';') : '已维护' }
|
{ key: '资料状态', value: row.masterDataIssues.length ? row.masterDataIssues.join(';') : '已维护' }
|
||||||
|
|||||||
@@ -9200,6 +9200,44 @@
|
|||||||
flex: 1 1 0;
|
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,
|
||||||
.v2-access-table-v3 > .semi-card-body {
|
.v2-access-table-v3 > .semi-card-body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -9452,10 +9490,101 @@
|
|||||||
line-height: 1.45;
|
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 {
|
.v2-access-inspector-focus .v2-access-connection-tag.semi-tag {
|
||||||
flex: 0 0 auto;
|
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 {
|
.v2-access-inspector-focus + .v2-access-inspector-summary.semi-descriptions {
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
@@ -9875,6 +10004,24 @@
|
|||||||
font-size: 13px;
|
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 {
|
.v2-ops-page {
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user