diff --git a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx
index e48d9399..bb74642f 100644
--- a/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx
+++ b/vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx
@@ -46,6 +46,12 @@ function formatSeconds(value?: number) {
return `${Math.round(value)} 秒`;
}
+function sourceServiceRole(source: VehicleSourceStatus, primaryProtocol?: string) {
+ if (source.protocol === primaryProtocol) return { label: '主来源', color: 'blue' as const };
+ if (source.online) return { label: '在线证据', color: 'green' as const };
+ return { label: '离线证据', color: 'grey' as const };
+}
+
async function copyVehicleServiceURL() {
try {
await navigator.clipboard.writeText(`${window.location.origin}${window.location.pathname}${window.location.hash}`);
@@ -305,57 +311,72 @@ export function VehicleDetail({
-
- {detail.sourceStatus.map((source) => (
-
-
-
- {source.protocol}
-
-
- {source.lastSeen || '无上报'}
-
-
- {sourceCapabilities.map((item) => (
-
- {item.label}{source[item.key] ? '有' : '无'}
-
- ))}
-
-
+
(
+ {row.protocol}
+ )
+ },
+ {
+ title: '车辆服务角色',
+ width: 130,
+ render: (_: unknown, row: VehicleSourceStatus) => {
+ const role = sourceServiceRole(row, summary?.primaryProtocol);
+ return {role.label};
+ }
+ },
+ { title: '在线', width: 100, render: (_: unknown, row: VehicleSourceStatus) => },
+ { title: '最后时间', dataIndex: 'lastSeen', width: 190, render: (value?: string) => value || '无上报' },
+ ...sourceCapabilities.map((item) => ({
+ title: item.label,
+ width: 90,
+ render: (_: unknown, row: VehicleSourceStatus) => {row[item.key] ? '有' : '无'}
+ })),
+ {
+ title: '操作',
+ width: 330,
+ render: (_: unknown, row: VehicleSourceStatus) => (
-
-
- ))}
-
+ )
+ }
+ ]}
+ />
>
) : (
暂未查询到该车辆的数据来源覆盖。
@@ -377,24 +398,6 @@ export function VehicleDetail({
]}
/>
-
-
- },
- { title: '最后时间', dataIndex: 'lastSeen', width: 190 },
- { title: '实时', width: 90, render: (_: unknown, row: VehicleSourceStatus) => {row.hasRealtime ? '有' : '无'} },
- { title: '历史', width: 90, render: (_: unknown, row: VehicleSourceStatus) => {row.hasHistory ? '有' : '无'} },
- { title: 'RAW', width: 90, render: (_: unknown, row: VehicleSourceStatus) => {row.hasRaw ? '有' : '无'} },
- { title: '里程', width: 90, render: (_: unknown, row: VehicleSourceStatus) => {row.hasMileage ? '有' : '无'} }
- ]}
- />
-
>
) : null}
diff --git a/vehicle-data-platform/apps/web/src/styles/global.css b/vehicle-data-platform/apps/web/src/styles/global.css
index a98ba7f7..f12a60a8 100644
--- a/vehicle-data-platform/apps/web/src/styles/global.css
+++ b/vehicle-data-platform/apps/web/src/styles/global.css
@@ -318,41 +318,10 @@ body {
word-break: break-all;
}
-.vp-source-grid {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 12px;
-}
-
.vp-source-toolbar {
margin-bottom: 12px;
}
-.vp-source-card {
- min-height: 86px;
- padding: 12px;
- border: 1px solid var(--vp-border);
- border-radius: var(--vp-radius);
- background: #fbfcff;
-}
-
-.vp-source-card-active {
- border-color: rgba(22, 100, 255, 0.5);
- background: rgba(22, 100, 255, 0.06);
-}
-
-.vp-source-card-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 12px;
-}
-
-.vp-source-card-actions {
- margin-top: 12px;
-}
-
.vp-realtime-strip {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
diff --git a/vehicle-data-platform/apps/web/src/test/App.test.tsx b/vehicle-data-platform/apps/web/src/test/App.test.tsx
index fc9857b5..c523f0fc 100644
--- a/vehicle-data-platform/apps/web/src/test/App.test.tsx
+++ b/vehicle-data-platform/apps/web/src/test/App.test.tsx
@@ -1865,6 +1865,10 @@ test('switches vehicle detail to a source from the coverage cards', async () =>
render();
expect(await screen.findByText('VIN001')).toBeInTheDocument();
+ expect(screen.getByText('车辆服务角色')).toBeInTheDocument();
+ expect(screen.getByText('主来源')).toBeInTheDocument();
+ expect(screen.getByText('在线证据')).toBeInTheDocument();
+ expect(screen.getByText('离线证据')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '仅看 JT808' }));
await waitFor(() => {