diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx
index 8bdd08c7..cf295813 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.test.tsx
@@ -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();
diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx
index 8eb538bf..aa266f00 100644
--- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx
+++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx
@@ -157,13 +157,22 @@ const ConnectionState = memo(function ConnectionState({ row }: { row: AccessVehi
function ProtocolCoverage({ summary }: { summary?: AccessSummary }) {
return
+ 真实来源覆盖
{PROTOCOLS.map((protocol) => {
const actual = summary?.protocols.find((item) => item.name === protocol);
- return {protocol}{(actual?.total ?? 0).toLocaleString('zh-CN')} 辆;
+ return {compactProtocolLabel(protocol)}{(actual?.total ?? 0).toLocaleString('zh-CN')} 辆;
})}
;
}
+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
{sheet ? null : } onClick={onClose} aria-label="关闭车辆接入详情" />} />}
- {row.connectionState === 'healthy' ? '接入结论' : '优先核对'}{accessIssueSummary(row)}{row.actualProtocols.length ? `已发现 ${row.actualProtocols.join(' / ')} ${row.actualProtocols.length} 个真实来源` : '当前没有可用的真实来源证据'}
+ {row.connectionState === 'healthy' ? '接入结论' : '优先核对'}{accessIssueSummary(row)}{row.actualProtocols.length ? `已发现 ${row.actualProtocols.join(' / ')} ${row.actualProtocols.length} 个真实来源` : '当前没有可用的真实来源证据'}处置建议 · {accessRecommendation(row)}
- {mobile ?
- 品牌 / 车型{[row.oem, row.model].filter(Boolean).join(' / ') || '未维护'}
- 真实来源{row.actualProtocols.length ? row.actualProtocols.join(' / ') : '尚无来源'}
- 资料状态{row.masterDataIssues.length ? row.masterDataIssues.join(';') : '已维护'}
-
:
+ 已接入{health.connected} / {PROTOCOLS.length}
+ 当前在线{health.online}
+ 异常来源{health.attention}
+ 资料状态{row.masterDataIssues.length ? `${row.masterDataIssues.length} 项待维护` : '已维护'}
+
+ {mobile ? 车辆档案{[row.oem, row.model].filter(Boolean).join(' / ') || '品牌与车型未维护'}
: 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%;