diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx index 01ed7cd1..acd43638 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx @@ -159,6 +159,7 @@ test('clears old alert rows and inspector evidence when the event scope changes' expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).not.toHaveClass('is-compact'); expect(screen.getByRole('columnheader', { name: '触发 / 阈值' })).toBeInTheDocument(); expect(screen.getByRole('columnheader', { name: '位置' })).toBeInTheDocument(); + expect(view.container.querySelector('.v2-alert-event-table .v2-alert-protocol-tag.semi-tag')).toHaveTextContent('JT/T 808'); expect(view.container.querySelector('.v2-alert-table-scroll > table')).not.toBeInTheDocument(); expect(view.container.querySelector('.v2-alert-mobile-list')).not.toBeInTheDocument(); expect(view.container.querySelector('time[datetime="2026-07-16T04:00:00Z"]')).toHaveTextContent('07-16 12:00:00'); @@ -176,7 +177,7 @@ test('clears old alert rows and inspector evidence when the event scope changes' expect(screen.getByText('已展开处置详情 · 点击其他事件快速切换')).toBeInTheDocument(); expect(desktopAlertRow).toHaveAttribute('aria-expanded', 'true'); expect(view.container.querySelector('.v2-alert-focus-card[aria-label="告警处置摘要"]')).toHaveClass('v2-alert-detail-card'); - expect(screen.getByLabelText('告警事件上下文')).toHaveTextContent('数据来源JT808触发时间07-16 12:00:00处理记录1 条'); + expect(screen.getByLabelText('告警事件上下文')).toHaveTextContent('数据来源JT/T 808触发时间07-16 12:00:00处理记录1 条'); expect(screen.getByLabelText('告警触发证据')).toHaveTextContent('触发值90 km/h阈值条件> 80 km/h,持续 60 秒超限幅度+10 km/h'); expect(screen.getByText('1 条记录').closest('.semi-tag')).toBeInTheDocument(); expect(screen.getByText('可操作').closest('.semi-tag')).toBeInTheDocument(); @@ -255,6 +256,7 @@ test('renders only selectable Semi alert cards on mobile', async () => { expect(view.container.querySelector('.v2-alert-event-table')).not.toBeInTheDocument(); expect(action).toHaveAttribute('aria-expanded', 'false'); expect(action).toHaveTextContent('超限幅度+10 km/h'); + expect(action.querySelector('.v2-alert-protocol-tag.semi-tag')).toHaveTextContent('JT/T 808'); expect(action).toHaveTextContent('核验并处置'); fireEvent.click(action); expect(action).toHaveAttribute('aria-pressed', 'true'); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx index f2f4112a..4be28515 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx @@ -8,6 +8,7 @@ import type { AlertEvent, AlertQuery, AlertRule, AlertRuleInput, AlertStatus, Me import { actionLabels, alertDeltaText, alertValue, canAct, formatAlertTime, operatorLabels, ruleCondition, severityLabels, statusLabels, thresholdText } from '../domain/alert'; import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState'; import { MetricActionButton } from '../shared/MetricActionButton'; +import { ProtocolTag } from '../shared/ProtocolTag'; import { SegmentedTabs } from '../shared/SegmentedTabs'; import { TablePagination } from '../shared/TablePagination'; import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; @@ -83,7 +84,7 @@ const AlertEventTable = memo(function AlertEventTable({ rows, selectedID, compac render: (_: string, event: AlertEvent) =>
{event.plate || '未绑定车牌'}{event.vin}
}, { title: '规则', dataIndex: 'ruleName', width: 170, render: (value: string) => {value} }, - { title: '协议', dataIndex: 'protocol', width: 92, render: (value: string) => value || '—' }, + { title: '协议', dataIndex: 'protocol', width: 112, render: (value: string) => }, { title: '触发时间', dataIndex: 'triggeredAt', width: 132, render: (value: string) => }, { title: '状态', dataIndex: 'status', width: 90, render: (_: AlertEvent['status'], event: AlertEvent) => }, { @@ -116,7 +117,7 @@ function EventInspector({ event, note, acting, actionError, editable, onNote, on if (!event) return } title="选择告警事件" description="查看触发证据、状态时间线和处置动作。" />; const actionCount = event.actions?.length ?? 0; return - {!sheet ? )} + ?
{rows.map((event) => )}
: } {events.isFetching ? : null} {!events.isFetching && !rows.length ? : null} diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx index 845f3166..71a43287 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.test.tsx @@ -161,7 +161,7 @@ test('clears stale rows, charts, and evidence as soon as the history scope chang expect(view.container.querySelector('.v2-history-evidence-status')).toHaveTextContent('证据判读正常'); expect(view.container.querySelector('.v2-evidence-values.semi-descriptions')).toBeInTheDocument(); expect(view.container.querySelector('.v2-history-quality-tag.semi-tag')).toHaveTextContent('正常'); - expect(view.container.querySelector('.v2-history-protocol-tag.semi-tag')).toHaveTextContent('JT808'); + expect(view.container.querySelector('.v2-history-protocol-tag.semi-tag')).toHaveTextContent('JT/T 808'); expect(view.container.querySelector('.v2-history-workspace')).toHaveClass('is-inspector-open'); fireEvent.click(screen.getByRole('button', { name: '关闭数据详情' })); expect(desktopHistoryRow).toHaveAttribute('aria-expanded', 'false'); @@ -210,7 +210,7 @@ test('renders only selectable Semi history cards on mobile', async () => { expect(action).toHaveAttribute('aria-expanded', 'false'); expect(view.container.querySelector('.v2-history-table-scroll.is-mobile-scroll')).toHaveAttribute('aria-label', '历史明细列表,可上下滚动'); expect(view.container.querySelector('.v2-history-table-scroll.is-mobile-scroll')).toHaveAttribute('tabindex', '0'); - expect(action.closest('.v2-history-mobile-card')?.querySelector('.v2-history-protocol-tag.semi-tag')).toHaveTextContent('JT808'); + expect(action.closest('.v2-history-mobile-card')?.querySelector('.v2-history-protocol-tag.semi-tag')).toHaveTextContent('JT/T 808'); expect(view.container.querySelector('.v2-history-metric-scroll')).not.toBeInTheDocument(); expect(view.container.querySelector('.v2-history-summary-rail')).toBeInTheDocument(); expect(view.container.querySelector('.v2-history-result-actions')).toHaveClass('is-mobile-actions'); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx index b2b1f4e9..0683ef1a 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx @@ -11,6 +11,7 @@ import { formatShanghaiDateTime } from '../domain/formatters'; import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState'; import { MonitorReturnBar } from '../shared/MonitorReturnBar'; import { PlatformTime } from '../shared/PlatformTime'; +import { ProtocolTag } from '../shared/ProtocolTag'; import { TablePagination } from '../shared/TablePagination'; import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; @@ -44,11 +45,6 @@ function HistoryQualityTag({ quality }: { quality: string }) { return {historyQualityLabel(quality)}; } -function HistoryProtocolTag({ protocol }: { protocol: string }) { - const color = protocol === 'GB32960' ? 'blue' : protocol === 'JT808' ? 'cyan' : protocol === 'YUTONG_MQTT' ? 'violet' : 'grey'; - return {protocol || '未知来源'}; -} - function HistoryTrend({ response, category, loading, error, hasMetrics, expanded, onToggle }: { response?: HistorySeriesResponse; category: string; @@ -162,7 +158,7 @@ function EvidencePanel({ row, metrics, onClose, showHeader = true }: { row?: His {row.qualityReason || '平台未返回质量说明'}
- +
}, { key: '服务时间', value: }, - { key: '数据来源', value: }, + { key: '数据来源', value: }, { key: '数据质量', value: }, { key: '质量原因', value: row.qualityReason || '平台未返回质量说明' } ]} /> @@ -226,7 +222,7 @@ function HistoryDataTable({ rows, metrics, selectedRowID, onSelect }: { rows: Hi { title: '车牌', dataIndex: 'plate', width: 110, fixed: 'left' as const, className: 'v2-history-plate-column', render: (value: string) => value || '—' }, { title: '服务时间', dataIndex: 'serverTime', width: 150, render: (value: string) => }, { title: 'VIN', dataIndex: 'vin', width: 160, render: (value: string) => {value} }, - { title: '协议', dataIndex: 'protocol', width: 110, render: (value: string) => }, + { title: '协议', dataIndex: 'protocol', width: 118, render: (value: string) => }, ...metrics.map((metric) => ({ title: `${metric.label}${metric.unit ? ` (${metric.unit})` : ''}`, dataIndex: metric.key, @@ -485,7 +481,7 @@ export default function HistoryPage() { onKeyDown={scrollHistoryTable} > {mobileLayout - ?
{resultRows.map((row) => )}
+ ?
{resultRows.map((row) => )}
: } {dataQuery.isPending && keywords.length ? diff --git a/vehicle-data-platform/apps/web/src/v2/shared/ProtocolTag.test.tsx b/vehicle-data-platform/apps/web/src/v2/shared/ProtocolTag.test.tsx new file mode 100644 index 00000000..06e092e4 --- /dev/null +++ b/vehicle-data-platform/apps/web/src/v2/shared/ProtocolTag.test.tsx @@ -0,0 +1,22 @@ +import { render, screen } from '@testing-library/react'; +import { expect, test } from 'vitest'; +import { ProtocolTag } from './ProtocolTag'; + +test.each([ + ['GB32960', 'GB/T 32960', 'semi-tag-blue-light'], + ['JT808', 'JT/T 808', 'semi-tag-cyan-light'], + ['YUTONG_MQTT', '宇通 MQTT', 'semi-tag-violet-light'] +])('renders %s with its shared display label and source color', (protocol, label, colorClass) => { + const view = render(); + const tag = view.container.querySelector('.v2-protocol-tag.semi-tag'); + + expect(screen.getByText(label)).toBeInTheDocument(); + expect(tag).toHaveClass(colorClass); +}); + +test('renders a neutral fallback for a missing protocol', () => { + const view = render(); + + expect(screen.getByText('未知来源')).toBeInTheDocument(); + expect(view.container.querySelector('.v2-protocol-tag.semi-tag')).toHaveClass('semi-tag-grey-light'); +}); diff --git a/vehicle-data-platform/apps/web/src/v2/shared/ProtocolTag.tsx b/vehicle-data-platform/apps/web/src/v2/shared/ProtocolTag.tsx new file mode 100644 index 00000000..891a82ca --- /dev/null +++ b/vehicle-data-platform/apps/web/src/v2/shared/ProtocolTag.tsx @@ -0,0 +1,34 @@ +import { Tag } from '@douyinfe/semi-ui'; +import type { ComponentProps } from 'react'; +import { protocolDisplayLabel } from '../domain/protocols'; + +const PROTOCOL_COLORS: Record['color']> = { + GB32960: 'blue', + JT808: 'cyan', + YUTONG_MQTT: 'violet' +}; + +export function ProtocolTag({ + protocol, + className = '', + compact = false, + unknownLabel = '未知来源' +}: { + protocol?: string; + className?: string; + compact?: boolean; + unknownLabel?: string; +}) { + const normalized = protocol?.trim() ?? ''; + const classes = ['v2-protocol-tag', compact ? 'is-compact' : '', className].filter(Boolean).join(' '); + const label = normalized ? protocolDisplayLabel(normalized) : unknownLabel; + + return + {label} + ; +} diff --git a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css index d933899a..c98221c4 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -16576,3 +16576,83 @@ font-size: 8px; } } + +/* + * Shared protocol identity. + * Every evidence surface now presents GB/T 32960, JT/T 808 and 宇通 MQTT + * with the same label, source color, spacing and compact behavior. + */ +.v2-protocol-tag.semi-tag { + width: max-content; + max-width: 100%; + min-height: 22px; + flex: 0 0 auto; + justify-content: center; + overflow: hidden; + border-radius: 999px; + padding-inline: 8px; + font-size: 10px; + font-weight: 700; + letter-spacing: .01em; + text-overflow: ellipsis; + white-space: nowrap; +} + +.v2-protocol-tag.is-compact.semi-tag { + min-height: 20px; + padding-inline: 7px; + font-size: 9px; +} + +/* + * Semi UI alert evidence refinement. + * Notifications remain accessible but no longer compete with actionable event + * counts; protocol evidence stays legible in the table, cards and inspector. + */ +.v2-alert-context-card .v2-metric-action.is-notice { + background: linear-gradient(135deg, #fbfcfe 0%, #f7f8fc 100%); +} + +.v2-alert-context-card .v2-metric-action.is-notice strong { + color: #697a91; + font-size: 18px; +} + +.v2-alert-event-table .v2-alert-protocol-tag.semi-tag { + max-width: 98px; +} + +.v2-alert-focus-facts .v2-alert-protocol-tag.semi-tag { + max-width: 110px; +} + +.v2-alert-mobile-card-content dd:has(.v2-alert-protocol-tag) { + overflow: visible; +} + +.v2-alert-mobile-card-content .v2-alert-protocol-tag.semi-tag { + max-width: 100%; +} + +@media (max-width: 680px) { + .v2-alert-context-card .v2-metric-action.is-notice strong { + font-size: 14px; + } + + .v2-alert-mobile-card-content dl { + grid-template-columns: .95fr .9fr 1.15fr; + } + + .v2-alert-inspector.is-sheet .v2-alert-disposition-card .v2-alert-actions { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .v2-alert-inspector.is-sheet .v2-alert-disposition-card .v2-alert-actions > .semi-button { + width: 100%; + min-height: 40px; + } + + .v2-alert-inspector.is-sheet .v2-alert-disposition-card .v2-alert-actions > .semi-button.is-primary { + grid-column: 1 / -1; + } +}