diff --git a/vehicle-data-platform/apps/api/internal/platform/access.go b/vehicle-data-platform/apps/api/internal/platform/access.go index a1d2f4c0..1070fc8a 100644 --- a/vehicle-data-platform/apps/api/internal/platform/access.go +++ b/vehicle-data-platform/apps/api/internal/platform/access.go @@ -223,11 +223,12 @@ func buildAccessVehicleGroup(items []AccessEvidenceRow, config AccessThresholdCo OEM: strings.TrimSpace(base.OEM), Model: strings.TrimSpace(base.Model), Company: strings.TrimSpace(base.Company), - ExpectedProtocols: append([]string(nil), canonicalVehicleProtocols...), + ExpectedProtocols: []string{}, ActualProtocols: []string{}, MissingProtocols: []string{}, ProtocolStatuses: []AccessProtocolStatus{}, - ExpectationEvidence: "平台标准接入基线:GB32960 / JT808 / YUTONG_MQTT", + MasterDataIssues: []string{}, + ExpectationEvidence: "尚未接入业务系统的应接协议口径;当前只展示真实接入来源,不推断车辆必须具备三协议", ConnectionState: "not_connected", OnlineState: "never_reported", FirstSeenEvidence: "尚未形成任何协议接入快照", @@ -254,29 +255,29 @@ func buildAccessVehicleGroup(items []AccessEvidenceRow, config AccessThresholdCo } onlineCount := 0 unknownCount := 0 - connectedCount := 0 for _, protocol := range canonicalVehicleProtocols { item, connected := byProtocol[protocol] if !connected { threshold := accessProtocolThreshold(config, protocol) - row.MissingProtocols = append(row.MissingProtocols, protocol) row.ProtocolStatuses = append(row.ProtocolStatuses, AccessProtocolStatus{ - Protocol: protocol, Expected: true, Connected: false, OnlineState: "never_reported", ThresholdSec: threshold, - FirstSeenEvidence: "应接协议尚未形成实时快照", ReportIntervalEvidence: "尚无接收样本", + Protocol: protocol, Expected: false, Connected: false, OnlineState: "never_reported", ThresholdSec: threshold, + FirstSeenEvidence: "当前未发现该协议来源;在业务应接口径接入前不判定为缺失", ReportIntervalEvidence: "尚无接收样本", }) continue } source := buildAccessVehicleRow(item, config, now) - connectedCount++ row.ActualProtocols = append(row.ActualProtocols, protocol) if source.OnlineState == "online" { onlineCount++ } + if source.OnlineState == "unknown" { + unknownCount++ + } if source.DelayAbnormal { row.DelayAbnormal = true } row.ProtocolStatuses = append(row.ProtocolStatuses, AccessProtocolStatus{ - Protocol: protocol, Expected: true, Connected: true, Provider: source.Provider, + Protocol: protocol, Expected: false, Connected: true, Provider: source.Provider, FirstSeenAt: source.FirstSeenAt, LatestEventAt: source.LatestEventAt, LatestReceivedAt: source.LatestReceivedAt, ReportIntervalSec: source.ReportIntervalSec, DataDelaySec: source.DataDelaySec, FreshnessSec: source.FreshnessSec, OnlineState: source.OnlineState, ThresholdSec: source.ThresholdSec, DelayAbnormal: source.DelayAbnormal, @@ -292,10 +293,16 @@ func buildAccessVehicleGroup(items []AccessEvidenceRow, config AccessThresholdCo copyAccessPrimaryFields(&row, source) } } - for protocol, item := range byProtocol { + extraProtocols := make([]string, 0) + for protocol := range byProtocol { if containsString(canonicalVehicleProtocols, protocol) { continue } + extraProtocols = append(extraProtocols, protocol) + } + sort.Strings(extraProtocols) + for _, protocol := range extraProtocols { + item := byProtocol[protocol] source := buildAccessVehicleRow(item, config, now) row.ActualProtocols = append(row.ActualProtocols, protocol) if source.OnlineState == "online" { @@ -316,29 +323,47 @@ func buildAccessVehicleGroup(items []AccessEvidenceRow, config AccessThresholdCo copyAccessPrimaryFields(&row, source) } } + if accessMasterDataMissing(row.OEM) { + row.MasterDataIssues = append(row.MasterDataIssues, "车辆品牌未维护") + } + for _, status := range row.ProtocolStatuses { + if status.Connected && accessMasterDataMissing(status.Provider) { + row.MasterDataIssues = append(row.MasterDataIssues, status.Protocol+" 接入方未维护") + } + } + sourceCount := len(row.ActualProtocols) switch { - case connectedCount == 0 && len(row.ActualProtocols) == 0: + case sourceCount == 0: row.ConnectionState = "not_connected" row.OnlineState = "never_reported" - case connectedCount == len(canonicalVehicleProtocols) && onlineCount == connectedCount: - row.ConnectionState = "healthy" - row.OnlineState = "online" - case onlineCount == 0 && unknownCount > 0: - row.ConnectionState = "incomplete" + case onlineCount == 0 && unknownCount == sourceCount: + row.ConnectionState = "degraded" row.OnlineState = "unknown" case onlineCount == 0: row.ConnectionState = "offline" row.OnlineState = "offline" - case connectedCount < len(canonicalVehicleProtocols): + case onlineCount < sourceCount || row.DelayAbnormal: + row.ConnectionState = "degraded" + row.OnlineState = "online" + case len(row.MasterDataIssues) > 0: row.ConnectionState = "incomplete" row.OnlineState = "online" default: - row.ConnectionState = "degraded" + row.ConnectionState = "healthy" row.OnlineState = "online" } return row } +func accessMasterDataMissing(value string) bool { + switch strings.ToLower(strings.TrimSpace(value)) { + case "", "未维护", "待维护", "unknown", "未知": + return true + default: + return false + } +} + func accessProtocolThreshold(config AccessThresholdConfig, protocol string) int { for _, override := range config.Protocols { if strings.EqualFold(override.Protocol, protocol) { @@ -446,8 +471,14 @@ func keepAccessRow(row AccessVehicleRow, query AccessQuery) bool { if value := strings.TrimSpace(query.Protocol); value != "" && !containsString(row.ActualProtocols, value) { return false } - if value := strings.TrimSpace(query.OEM); value != "" && !strings.EqualFold(value, row.OEM) { - return false + if value := strings.TrimSpace(query.OEM); value != "" { + if value == "未维护" { + if !accessMasterDataMissing(row.OEM) { + return false + } + } else if !strings.EqualFold(value, row.OEM) { + return false + } } if value := strings.ToLower(strings.TrimSpace(query.Model)); value != "" && !strings.Contains(strings.ToLower(row.Model), value) { return false diff --git a/vehicle-data-platform/apps/api/internal/platform/access_store.go b/vehicle-data-platform/apps/api/internal/platform/access_store.go index 48b4bbd3..3b78b6a4 100644 --- a/vehicle-data-platform/apps/api/internal/platform/access_store.go +++ b/vehicle-data-platform/apps/api/internal/platform/access_store.go @@ -69,7 +69,7 @@ COALESCE(NULLIF(b.oem, ''), '') AS oem, COALESCE(p.model_name, '') AS model_name, COALESCE(p.company_name, '') AS company_name, COALESCE(s.protocol, '') AS protocol, -COALESCE(s.platform_name, '') AS provider, +COALESCE(NULLIF(TRIM(s.platform_name), ''), NULLIF(TRIM(ls.source_code), ''), '') AS provider, COALESCE(DATE_FORMAT(s.access_first_seen_at, '%Y-%m-%d %H:%i:%s.%f'), '') AS first_seen_at, COALESCE(s.access_first_seen_source, '') AS first_seen_source, COALESCE(DATE_FORMAT(s.event_time, '%Y-%m-%d %H:%i:%s.%f'), '') AS event_time, @@ -91,6 +91,7 @@ LEFT JOIN vehicle_identity_binding b ON b.vin = v.vin LEFT JOIN vehicle_profile p ON p.vin = v.vin LEFT JOIN vehicle_realtime_snapshot s ON s.vin = v.vin LEFT JOIN vehicle_realtime_location l ON l.vin = s.vin AND l.protocol = s.protocol +LEFT JOIN vehicle_realtime_location_source ls ON ls.vin = l.vin AND ls.protocol = l.protocol AND ls.source_key = l.source_key LEFT JOIN ( SELECT vin, protocol, MAX(daily_mileage_km) AS daily_mileage_km FROM vehicle_daily_mileage diff --git a/vehicle-data-platform/apps/api/internal/platform/access_test.go b/vehicle-data-platform/apps/api/internal/platform/access_test.go index 49ba3959..bff195a5 100644 --- a/vehicle-data-platform/apps/api/internal/platform/access_test.go +++ b/vehicle-data-platform/apps/api/internal/platform/access_test.go @@ -23,7 +23,7 @@ func TestAccessSummaryUsesDynamicFreshnessAndDelay(t *testing.T) { if summary.DelayAbnormal != 1 || summary.ThresholdVersion != 1 { t.Fatalf("summary must expose dynamic delay and threshold version: %+v", summary) } - if summary.HealthyVehicles != 0 || summary.IncompleteVehicles != 3 || summary.DegradedVehicles != 0 { + if summary.HealthyVehicles != 1 || summary.IncompleteVehicles != 0 || summary.DegradedVehicles != 2 { t.Fatalf("connection-state counters must be mutually exclusive: %+v", summary) } } @@ -47,8 +47,8 @@ func TestAccessVehiclesFiltersAndKeepsEvidenceGapsExplicit(t *testing.T) { } attention, err := service.AccessVehicles(context.Background(), AccessQuery{ConnectionState: "attention", Limit: 20}) - if err != nil || attention.Total != 6 { - t.Fatalf("attention filter should return every vehicle with an access difference: page=%+v err=%v", attention, err) + if err != nil || attention.Total != 5 { + t.Fatalf("attention filter should return every vehicle requiring attention: page=%+v err=%v", attention, err) } } @@ -116,7 +116,7 @@ func TestAccessVehiclesKeywordMatchesFleetAndModel(t *testing.T) { } } -func TestAccessVehicleGroupsCanonicalProtocolsByMasterVehicle(t *testing.T) { +func TestAccessVehicleShowsCanonicalSlotsWithoutInventingExpectedSources(t *testing.T) { store := NewMockStore() service := NewService(store) page, err := service.AccessVehicles(t.Context(), AccessQuery{Keyword: "LB9A32A24R0LS1426", Limit: 10}) @@ -124,19 +124,37 @@ func TestAccessVehicleGroupsCanonicalProtocolsByMasterVehicle(t *testing.T) { t.Fatalf("access vehicle query failed: page=%+v err=%v", page, err) } row := page.Items[0] - if len(row.ExpectedProtocols) != 3 || len(row.ProtocolStatuses) != 3 { - t.Fatalf("vehicle must expose three canonical protocol slots: %+v", row) + if len(row.ExpectedProtocols) != 0 || len(row.ProtocolStatuses) != 3 { + t.Fatalf("vehicle must expose comparison slots without inventing an expected-source baseline: %+v", row) } - if len(row.ActualProtocols) != 1 || row.ActualProtocols[0] != "JT808" || len(row.MissingProtocols) != 2 { - t.Fatalf("actual and missing protocols are incorrect: %+v", row) + if len(row.ActualProtocols) != 1 || row.ActualProtocols[0] != "JT808" || len(row.MissingProtocols) != 0 { + t.Fatalf("actual source must remain truthful and absent protocols must not be reported as missing: %+v", row) } - if row.ConnectionState != "incomplete" || row.OnlineState != "online" { - t.Fatalf("single online source should be an online but incomplete vehicle: %+v", row) + if row.ConnectionState != "healthy" || row.OnlineState != "online" { + t.Fatalf("a maintained single online source should be healthy: %+v", row) } for _, status := range row.ProtocolStatuses { if status.Protocol == "JT808" && (!status.Connected || status.LatestReceivedAt == "") { t.Fatalf("connected protocol lost evidence: %+v", status) } + if status.Expected { + t.Fatalf("business expectation must remain unknown until OneOS provides it: %+v", status) + } + } +} + +func TestAccessVehicleSeparatesMasterDataMaintenanceFromSourceAbsence(t *testing.T) { + now := time.Now() + row := buildAccessVehicleGroup([]AccessEvidenceRow{{ + VIN: "VIN-MASTER-DATA", Protocol: "JT808", OEM: "", Provider: "", + LatestEventAt: now.Add(-2 * time.Second).Format(time.RFC3339), + LatestReceivedAt: now.Add(-time.Second).Format(time.RFC3339), + }}, defaultAccessThresholds(now), now) + if row.ConnectionState != "incomplete" || len(row.MasterDataIssues) != 2 { + t.Fatalf("online source with missing brand/provider should enter the maintenance queue: %+v", row) + } + if len(row.MissingProtocols) != 0 || len(row.ExpectedProtocols) != 0 { + t.Fatalf("master-data maintenance must not invent absent protocol alarms: %+v", row) } } diff --git a/vehicle-data-platform/apps/api/internal/platform/model.go b/vehicle-data-platform/apps/api/internal/platform/model.go index 8310508e..98ae82a7 100644 --- a/vehicle-data-platform/apps/api/internal/platform/model.go +++ b/vehicle-data-platform/apps/api/internal/platform/model.go @@ -460,6 +460,7 @@ type AccessVehicleRow struct { ActualProtocols []string `json:"actualProtocols"` MissingProtocols []string `json:"missingProtocols"` ProtocolStatuses []AccessProtocolStatus `json:"protocolStatuses"` + MasterDataIssues []string `json:"masterDataIssues"` ConnectionState string `json:"connectionState"` ExpectationEvidence string `json:"expectationEvidence"` } diff --git a/vehicle-data-platform/apps/web/src/api/types.ts b/vehicle-data-platform/apps/web/src/api/types.ts index 5ca1ab4e..25940cc7 100644 --- a/vehicle-data-platform/apps/web/src/api/types.ts +++ b/vehicle-data-platform/apps/web/src/api/types.ts @@ -286,6 +286,7 @@ export interface AccessVehicleRow { thresholdSec: number; latestMessageType: string; latestEventId: string; latestError: string; delayAbnormal: boolean; firstSeenEvidence: string; firstSeenSource: string; reportIntervalEvidence: string; reportSampleCount: number; expectedProtocols: string[]; actualProtocols: string[]; missingProtocols: string[]; protocolStatuses: AccessProtocolStatus[]; + masterDataIssues: string[]; connectionState: 'healthy' | 'degraded' | 'incomplete' | 'offline' | 'not_connected'; expectationEvidence: string; } export interface AccessProtocolStatus { diff --git a/vehicle-data-platform/apps/web/src/v2/domain/access.test.ts b/vehicle-data-platform/apps/web/src/v2/domain/access.test.ts index c3f6a7ee..4e4b19ea 100644 --- a/vehicle-data-platform/apps/web/src/v2/domain/access.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/domain/access.test.ts @@ -17,9 +17,10 @@ describe('access domain helpers', () => { }); it('exports explicit state and evidence fields', () => { - const csv = accessRowsToCSV([{ vin: 'VIN1', plate: '粤A1', oem: '', model: '', company: '示范企业', protocol: 'JT808', provider: '', source: '', firstSeenAt: '', latestEventAt: '', latestReceivedAt: '', reportIntervalSec: null, dataDelaySec: 2, freshnessSec: 3, onlineState: 'online', thresholdSec: 60, latestMessageType: '位置,数据', latestEventId: '', latestError: '', delayAbnormal: false, firstSeenEvidence: '', firstSeenSource: '', reportIntervalEvidence: '', reportSampleCount: 2, expectedProtocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], actualProtocols: ['JT808'], missingProtocols: ['GB32960', 'YUTONG_MQTT'], protocolStatuses: [{ protocol: 'JT808', expected: true, connected: true, provider: 'G7', firstSeenAt: '2026-07-01T00:00:00+08:00', latestEventAt: '', latestReceivedAt: '2026-07-15T09:00:00+08:00', reportIntervalSec: 10, dataDelaySec: 2, freshnessSec: 3, onlineState: 'online', thresholdSec: 60, delayAbnormal: false, firstSeenEvidence: '网关首次观测', reportIntervalEvidence: '连续样本' }], connectionState: 'incomplete', expectationEvidence: '平台标准接入基线' }]); + const csv = accessRowsToCSV([{ vin: 'VIN1', plate: '粤A1', oem: '', model: '', company: '示范企业', protocol: 'JT808', provider: '', source: '', firstSeenAt: '', latestEventAt: '', latestReceivedAt: '', reportIntervalSec: null, dataDelaySec: 2, freshnessSec: 3, onlineState: 'online', thresholdSec: 60, latestMessageType: '位置,数据', latestEventId: '', latestError: '', delayAbnormal: false, firstSeenEvidence: '', firstSeenSource: '', reportIntervalEvidence: '', reportSampleCount: 2, expectedProtocols: [], actualProtocols: ['JT808'], missingProtocols: [], masterDataIssues: ['车辆品牌未维护'], protocolStatuses: [{ protocol: 'JT808', expected: false, connected: true, provider: 'G7', firstSeenAt: '2026-07-01T00:00:00+08:00', latestEventAt: '', latestReceivedAt: '2026-07-15T09:00:00+08:00', reportIntervalSec: 10, dataDelaySec: 2, freshnessSec: 3, onlineState: 'online', thresholdSec: 60, delayAbnormal: false, firstSeenEvidence: '网关首次观测', reportIntervalEvidence: '连续样本' }], connectionState: 'incomplete', expectationEvidence: '尚未接入业务应接口径' }]); expect(csv).toContain('在线'); - expect(csv).toContain('"GB32960/YUTONG_MQTT"'); + expect(csv).toContain('"车辆品牌未维护"'); + expect(csv).not.toContain('应接协议'); expect(csv).toContain('"2026-07-15T09:00:00+08:00"'); expect(csv).toContain('"示范企业"'); }); diff --git a/vehicle-data-platform/apps/web/src/v2/domain/access.ts b/vehicle-data-platform/apps/web/src/v2/domain/access.ts index 61ef5b55..350f402a 100644 --- a/vehicle-data-platform/apps/web/src/v2/domain/access.ts +++ b/vehicle-data-platform/apps/web/src/v2/domain/access.ts @@ -41,14 +41,14 @@ export function updateProtocolThreshold(items: AccessProtocolThreshold[], protoc export function accessRowsToCSV(rows: AccessVehicleRow[]) { const protocolColumns = ['GB32960', 'JT808', 'YUTONG_MQTT'].flatMap((protocol) => [`${protocol}接入状态`, `${protocol}接入厂家`, `${protocol}首次接入`, `${protocol}最新上报`, `${protocol}离线秒数`]); - const columns = ['综合状态', '车牌', 'VIN', '品牌', '车型', '企业', '应接协议', '实际接入协议', '缺失协议', ...protocolColumns]; + const columns = ['综合状态', '车牌', 'VIN', '品牌', '车型', '企业', '真实接入协议', '资料待维护', ...protocolColumns]; const quote = (value: unknown) => `"${String(value ?? '').replace(/"/g, '""')}"`; const lines = rows.map((row) => { const protocolValues = ['GB32960', 'JT808', 'YUTONG_MQTT'].flatMap((protocol) => { const status = row.protocolStatuses.find((item) => item.protocol === protocol); - return [status?.connected ? accessStateLabels[status.onlineState] : '未接入', status?.provider, status?.firstSeenAt, status?.latestReceivedAt, status?.freshnessSec]; + return [status?.connected ? accessStateLabels[status.onlineState] : '无来源', status?.provider, status?.firstSeenAt, status?.latestReceivedAt, status?.freshnessSec]; }); - return [row.connectionState, row.plate, row.vin, row.oem, row.model, row.company, row.expectedProtocols.join('/'), row.actualProtocols.join('/'), row.missingProtocols.join('/'), ...protocolValues].map(quote).join(','); + return [row.connectionState, row.plate, row.vin, row.oem, row.model, row.company, row.actualProtocols.join('/'), row.masterDataIssues.join('/'), ...protocolValues].map(quote).join(','); }); return `\uFEFF${columns.map(quote).join(',')}\n${lines.join('\n')}`; } 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 9eab27b5..13d19f9b 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 @@ -26,8 +26,8 @@ function accessRow(vin: string, plate: string): AccessVehicleRow { firstSeenAt: '2026-07-16T00:00:00Z', latestEventAt: '2026-07-16T04:00:00Z', latestReceivedAt: '2026-07-16T04:00:01Z', reportIntervalSec: 10, dataDelaySec: 1, freshnessSec: 3, onlineState: 'online', thresholdSec: 300, latestMessageType: 'location', latestEventId: `${vin}-event`, latestError: '', delayAbnormal: false, firstSeenEvidence: 'test first seen', firstSeenSource: 'test', reportIntervalEvidence: 'test interval', reportSampleCount: 10, - expectedProtocols: ['JT808'], actualProtocols: ['JT808'], missingProtocols: [], connectionState: 'healthy', expectationEvidence: 'test expectation', - protocolStatuses: [{ protocol: 'JT808', expected: true, connected: true, provider: '测试厂家', firstSeenAt: '2026-07-16T00:00:00Z', latestEventAt: '2026-07-16T04:00:00Z', latestReceivedAt: '2026-07-16T04:00:01Z', reportIntervalSec: 10, dataDelaySec: 1, freshnessSec: 3, onlineState: 'online', thresholdSec: 300, delayAbnormal: false, firstSeenEvidence: 'test first seen', reportIntervalEvidence: 'test interval' }] + expectedProtocols: [], actualProtocols: ['JT808'], missingProtocols: [], masterDataIssues: [], connectionState: 'healthy', expectationEvidence: '尚未接业务口径', + protocolStatuses: [{ protocol: 'JT808', expected: false, connected: true, provider: '测试厂家', firstSeenAt: '2026-07-16T00:00:00Z', latestEventAt: '2026-07-16T04:00:00Z', latestReceivedAt: '2026-07-16T04:00:01Z', reportIntervalSec: 10, dataDelaySec: 1, freshnessSec: 3, onlineState: 'online', thresholdSec: 300, delayAbnormal: false, firstSeenEvidence: 'test first seen', reportIntervalEvidence: 'test interval' }] }; } 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 c373fd2c..c2026658 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx @@ -18,7 +18,7 @@ const EMPTY_FILTERS = { keyword: '', protocol: '', oem: '', connectionState: '', type Filters = typeof EMPTY_FILTERS; const connectionLabels: Record = { - healthy: '三协议正常', degraded: '协议离线', incomplete: '接入不完整', offline: '全部离线', not_connected: '尚未接入' + healthy: '已接来源正常', degraded: '部分来源异常', incomplete: '资料待维护', offline: '已接来源离线', not_connected: '尚无来源' }; function statusByProtocol(row: AccessVehicleRow, protocol: string) { @@ -34,7 +34,7 @@ function compactTime(value: string) { function ProtocolState({ status, detailed = false }: { status?: AccessProtocolStatus; detailed?: boolean }) { const state = !status?.connected ? 'missing' : status.onlineState; - const label = state === 'missing' ? '未接入' : state === 'online' ? '在线' : state === 'offline' ? '离线' : state === 'unknown' ? '未知' : '从未上报'; + const label = state === 'missing' ? '无来源' : state === 'online' ? '在线' : state === 'offline' ? '离线' : state === 'unknown' ? '未知' : '从未上报'; if (detailed) { return
{status?.protocol}{label}
@@ -46,26 +46,25 @@ function ProtocolState({ status, detailed = false }: { status?: AccessProtocolSt
上报间隔
{formatSeconds(status?.reportIntervalSec)}
数据延迟
{formatSeconds(status?.dataDelaySec)}
-

{status?.firstSeenEvidence || '应接协议尚未形成实时快照'}

+

{status?.firstSeenEvidence || '当前未发现该协议来源'}

; } - return
+ return
{label} - {status?.connected ? compactTime(status.latestReceivedAt) : '等待接入'} - {status?.provider || (status?.connected ? '接入方未维护' : '无实时快照')} + {status?.connected ? compactTime(status.latestReceivedAt) : '—'} + {status?.provider || (status?.connected ? '接入方未维护' : '未发现来源')}
; } function ConnectionState({ row }: { row: AccessVehicleRow }) { - return
{connectionLabels[row.connectionState]}{row.actualProtocols.length} / {row.expectedProtocols.length} 已接入
; + return
{connectionLabels[row.connectionState]}{row.actualProtocols.length} 个真实来源
; } function ProtocolCoverage({ summary }: { summary?: AccessSummary }) { - return
+ return
{PROTOCOLS.map((protocol) => { const actual = summary?.protocols.find((item) => item.name === protocol); - const total = summary?.totalVehicles ?? 0; - return {protocol}{(actual?.total ?? 0).toLocaleString('zh-CN')} / {total.toLocaleString('zh-CN')}; + return {protocol}{(actual?.total ?? 0).toLocaleString('zh-CN')} 辆; })}
; } @@ -73,7 +72,7 @@ function ProtocolCoverage({ summary }: { summary?: AccessSummary }) { function VehicleInspector({ row, onClose }: { row: AccessVehicleRow; onClose: () => void }) { return ; @@ -119,15 +118,15 @@ export default function AccessPage() { const refresh = () => Promise.all([summaryQuery.refetch(), vehiclesQuery.refetch(), ...(editable ? [unresolvedQuery.refetch(), thresholdQuery.refetch()] : [])]); return
-

车辆接入管理

以主车辆为对象,对照应接协议、实际接入和各协议最新上报时间

数据时间 {summary?.asOf ? formatAccessTime(summary.asOf) : '—'}
+

车辆接入管理

只展示车辆真实存在的来源、在线健康和待维护资料;未接业务系统前不推断应接协议

数据时间 {summary?.asOf ? formatAccessTime(summary.asOf) : '—'}
-
+
{summaryQuery.isError ? summaryQuery.refetch()} /> : null}
{[ - ['主车辆', summary?.totalVehicles ?? 0, 'all', ''], ['有接入差异', Math.max(0, (summary?.totalVehicles ?? 0) - (summary?.healthyVehicles ?? 0)), 'attention', 'attention'], ['全部离线', summary?.offlineVehicles ?? 0, 'offline', 'offline'], ['尚未接入', summary?.neverReported ?? 0, 'never', 'not_connected'] + ['主车辆', summary?.totalVehicles ?? 0, 'all', ''], ['需关注', Math.max(0, (summary?.totalVehicles ?? 0) - (summary?.healthyVehicles ?? 0)), 'attention', 'attention'], ['资料待维护', summary?.incompleteVehicles ?? 0, 'incomplete', 'incomplete'], ['尚无来源', summary?.neverReported ?? 0, 'never', 'not_connected'] ].map(([label, value, tone, connectionState]) => )}
{vehiclesQuery.isError ? vehiclesQuery.refetch()} /> : null} -
车辆协议接入差异优先展示应接与实接差异;时间为各协议最后接收时间
{mobileLayout ?
{rows.map((row) => )}
: {PROTOCOLS.map((item) => )}{rows.map((row) => setSelectedVIN(row.vin)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') setSelectedVIN(row.vin); }}>{PROTOCOLS.map((protocol) => )})}
车辆品牌 / 车型应接协议{item}综合状态
{row.plate || '未绑定车牌'}{row.vin}{row.oem || '品牌未维护'}{row.model || row.company || '车型未维护'}{row.expectedProtocols.length} 项{row.expectedProtocols.join(' / ')}
}{vehiclesQuery.isFetching ?
正在更新车辆接入状态…
: null}{!vehiclesQuery.isFetching && !rows.length ?
当前筛选条件没有车辆
: null}
第 {page} / {totalPages} 页,共 {(vehiclesQuery.data?.total ?? 0).toLocaleString('zh-CN')} 辆主车辆
{selected ? setSelectedVIN('')} /> : null}
+
车辆真实接入来源缺席协议只表示“当前未发现”,不会被推断成应接缺失;时间为各来源最后接收时间
{mobileLayout ?
{rows.map((row) => )}
: {PROTOCOLS.map((item) => )}{rows.map((row) => setSelectedVIN(row.vin)} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') setSelectedVIN(row.vin); }}>{PROTOCOLS.map((protocol) => )})}
车辆品牌 / 车型真实来源{item}综合状态
{row.plate || '未绑定车牌'}{row.vin}{row.oem || '品牌未维护'}{row.model || row.company || '车型未维护'}{row.actualProtocols.length} 个{row.actualProtocols.join(' / ') || '尚无来源'}
}{vehiclesQuery.isFetching ?
正在更新车辆接入状态…
: null}{!vehiclesQuery.isFetching && !rows.length ?
当前筛选条件没有车辆
: null}
第 {page} / {totalPages} 页,共 {(vehiclesQuery.data?.total ?? 0).toLocaleString('zh-CN')} 辆主车辆
{selected ? setSelectedVIN('')} /> : null}
{editable && unresolvedQuery.isError ? unresolvedQuery.refetch()} /> : null} {editable ? : null} {editable && thresholdQuery.isError ? thresholdQuery.refetch()} /> : null} diff --git a/vehicle-data-platform/apps/web/src/v2/styles/v2.css b/vehicle-data-platform/apps/web/src/v2/styles/v2.css index c3d0e1fe..f97b47c0 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/v2.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/v2.css @@ -907,13 +907,13 @@ button, a { -webkit-tap-highlight-color: transparent; } .v2-access-table-scroll-v3 tbody tr { cursor: pointer; transition: background .14s ease; }.v2-access-table-scroll-v3 tbody tr:hover, .v2-access-table-scroll-v3 tbody tr.is-selected { background: #f2f7ff; }.v2-access-table-scroll-v3 tbody td { height: 67px; border-bottom: 1px solid #edf1f5; } .v2-access-table-scroll-v3 td > strong, .v2-access-table-scroll-v3 td > b { display: block; overflow: hidden; color: #314158; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }.v2-access-table-scroll-v3 td > span { display: block; overflow: hidden; margin-top: 5px; color: #8793a5; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } .v2-access-protocol-cell { display: grid; min-width: 0; gap: 4px; }.v2-access-protocol-cell > span { display: inline-flex; width: fit-content; align-items: center; gap: 5px; color: #6c788b; font-size: 12px; }.v2-access-protocol-cell > span i { width: 7px; height: 7px; border-radius: 50%; background: #9aa6b5; }.v2-access-protocol-cell > strong { color: #3d4d63; font-size: 12px; font-weight: 650; }.v2-access-protocol-cell > small { overflow: hidden; color: #8b97a7; font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } -.v2-access-protocol-cell.is-online > span { color: #16815f; }.v2-access-protocol-cell.is-online > span i { background: #19a974; box-shadow: 0 0 0 3px rgba(25,169,116,.11); }.v2-access-protocol-cell.is-offline > span { color: #b26512; }.v2-access-protocol-cell.is-offline > span i { background: #e6922e; }.v2-access-protocol-cell.is-missing > span { color: #c14b4b; }.v2-access-protocol-cell.is-missing > span i { border: 1px solid #dd7777; background: #fff; } +.v2-access-protocol-cell.is-online > span { color: #16815f; }.v2-access-protocol-cell.is-online > span i { background: #19a974; box-shadow: 0 0 0 3px rgba(25,169,116,.11); }.v2-access-protocol-cell.is-offline > span { color: #b26512; }.v2-access-protocol-cell.is-offline > span i { background: #e6922e; }.v2-access-protocol-cell.is-missing > span { color: #8290a3; }.v2-access-protocol-cell.is-missing > span i { border: 1px solid #aeb8c5; background: #fff; } .v2-access-connection { display: grid; gap: 5px; }.v2-access-connection strong { width: fit-content; border-radius: 12px; background: #eef2f6; padding: 4px 8px; color: #607086; font-size: 11px; }.v2-access-connection span { color: #8793a5; font-size: 11px; }.v2-access-connection.is-healthy strong { background: #eaf8f2; color: #15815e; }.v2-access-connection.is-incomplete strong, .v2-access-connection.is-degraded strong { background: #fff4e5; color: #b5660c; }.v2-access-connection.is-not_connected strong { background: #fff0f0; color: #bc4d4d; } .v2-access-table-v3 > footer { display: flex; min-height: 46px; flex: 0 0 auto; align-items: center; justify-content: space-between; gap: 12px; border-top: 1px solid var(--v2-border); padding: 0 12px; color: #788699; font-size: 12px; }.v2-access-table-v3 > footer div { display: flex; align-items: center; gap: 7px; }.v2-access-table-v3 > footer button, .v2-access-table-v3 > footer select { height: 30px; border: 1px solid #dbe3ed; border-radius: 6px; background: #fff; padding: 0 9px; color: #5d6b80; font-size: 12px; } .v2-access-inspector-v3 { min-width: 0; overflow: auto; border: 1px solid var(--v2-border); border-radius: 9px; background: #fff; box-shadow: var(--v2-shadow); } .v2-access-inspector-v3 > header { position: sticky; z-index: 2; top: 0; display: flex; min-height: 58px; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--v2-border); background: #fff; padding: 0 14px; }.v2-access-inspector-v3 > header > div { display: grid; gap: 4px; }.v2-access-inspector-v3 > header strong { color: #2f3f55; font-size: 16px; }.v2-access-inspector-v3 > header span { color: #8290a3; font-size: 11px; }.v2-access-inspector-v3 > header button { display: grid; width: 30px; height: 30px; place-items: center; border: 0; border-radius: 6px; background: #f2f5f8; color: #6d7a8e; cursor: pointer; } .v2-access-inspector-summary { display: grid; grid-template-columns: 1fr 1fr; border-bottom: 1px solid var(--v2-border); padding: 10px 14px; }.v2-access-inspector-summary > div { min-width: 0; padding: 7px 4px; }.v2-access-inspector-summary span { display: block; color: #8793a5; font-size: 11px; }.v2-access-inspector-summary strong { display: block; margin-top: 5px; color: #3f4e63; font-size: 12px; line-height: 1.45; overflow-wrap: anywhere; }.v2-access-inspector-summary .v2-access-connection { margin-top: 4px; } -.v2-access-protocol-details { display: grid; gap: 9px; padding: 12px; }.v2-access-protocol-detail { overflow: hidden; border: 1px solid #e1e7ef; border-radius: 8px; }.v2-access-protocol-detail > header { display: flex; height: 38px; align-items: center; justify-content: space-between; background: #f8fafc; padding: 0 10px; }.v2-access-protocol-detail > header strong { color: #3b4a60; font-size: 13px; }.v2-access-protocol-detail > header span { display: inline-flex; align-items: center; gap: 5px; color: #69778b; font-size: 11px; }.v2-access-protocol-detail > header i { width: 7px; height: 7px; border-radius: 50%; background: #9aa6b5; }.v2-access-protocol-detail.is-online > header i { background: var(--v2-green); }.v2-access-protocol-detail.is-offline > header i { background: var(--v2-orange); }.v2-access-protocol-detail.is-missing { border-color: #efd7d7; }.v2-access-protocol-detail.is-missing > header { background: #fff8f8; }.v2-access-protocol-detail.is-missing > header i { border: 1px solid #d96c6c; background: #fff; } +.v2-access-protocol-details { display: grid; gap: 9px; padding: 12px; }.v2-access-protocol-detail { overflow: hidden; border: 1px solid #e1e7ef; border-radius: 8px; }.v2-access-protocol-detail > header { display: flex; height: 38px; align-items: center; justify-content: space-between; background: #f8fafc; padding: 0 10px; }.v2-access-protocol-detail > header strong { color: #3b4a60; font-size: 13px; }.v2-access-protocol-detail > header span { display: inline-flex; align-items: center; gap: 5px; color: #69778b; font-size: 11px; }.v2-access-protocol-detail > header i { width: 7px; height: 7px; border-radius: 50%; background: #9aa6b5; }.v2-access-protocol-detail.is-online > header i { background: var(--v2-green); }.v2-access-protocol-detail.is-offline > header i { background: var(--v2-orange); }.v2-access-protocol-detail.is-missing { border-color: #e1e7ef; }.v2-access-protocol-detail.is-missing > header { background: #f8fafc; }.v2-access-protocol-detail.is-missing > header i { border: 1px solid #aeb8c5; background: #fff; } .v2-access-protocol-detail dl { display: grid; grid-template-columns: 1fr 1fr; margin: 0; padding: 7px 10px; }.v2-access-protocol-detail dl > div { min-width: 0; padding: 5px 3px; }.v2-access-protocol-detail dt { color: #8995a6; font-size: 11px; }.v2-access-protocol-detail dd { overflow: hidden; margin: 4px 0 0; color: #46556a; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }.v2-access-protocol-detail dd.is-danger { color: var(--v2-red); }.v2-access-protocol-detail p { margin: 0; border-top: 1px solid #edf1f5; padding: 7px 10px; color: #7c899b; font-size: 11px; line-height: 1.5; } .v2-access-inspector-v3 > footer { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; border-top: 1px solid var(--v2-border); padding: 10px 14px; }.v2-access-inspector-v3 > footer span { color: #8591a2; font-size: 11px; line-height: 1.5; }.v2-access-inspector-v3 > footer a { flex: 0 0 auto; color: var(--v2-blue); font-size: 12px; text-decoration: none; } .v2-access-identity-queue-v3, .v2-access-settings { flex: 0 0 auto; border: 1px solid var(--v2-border); border-radius: 8px; background: #fff; }.v2-access-identity-queue-v3 summary, .v2-access-settings summary { display: flex; min-height: 40px; align-items: center; gap: 10px; padding: 0 12px; color: #5c6b80; cursor: pointer; font-size: 11px; }.v2-access-identity-queue-v3 summary span { color: #8995a6; font-size: 10px; }.v2-access-identity-queue-v3 > div { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; border-top: 1px solid var(--v2-border); padding: 10px; }.v2-access-identity-queue-v3 article { display: grid; gap: 4px; border: 1px solid #e4e9f0; border-radius: 7px; padding: 9px; }.v2-access-identity-queue-v3 article b { font-size: 11px; }.v2-access-identity-queue-v3 article span, .v2-access-identity-queue-v3 article small { color: #7d8a9c; font-size: 9px; line-height: 1.5; }