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 b3585a28..845f3166 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 @@ -158,8 +158,10 @@ test('clears stale rows, charts, and evidence as soon as the history scope chang expect(desktopHistoryRow).toHaveAttribute('aria-expanded', 'true'); expect(await screen.findByText('OLDVIN-evidence')).toBeInTheDocument(); expect(view.container.querySelector('.v2-history-evidence-descriptions.semi-descriptions')).toBeInTheDocument(); + 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-workspace')).toHaveClass('is-inspector-open'); fireEvent.click(screen.getByRole('button', { name: '关闭数据详情' })); expect(desktopHistoryRow).toHaveAttribute('aria-expanded', 'false'); @@ -170,6 +172,8 @@ test('clears stale rows, charts, and evidence as soon as the history scope chang await waitFor(() => expect(mocks.historySeries).toHaveBeenCalledTimes(1)); expect(screen.getByRole('heading', { level: 5, name: '聚合趋势' })).toBeInTheDocument(); expect(view.container.querySelector('.v2-history-trend')).toHaveClass('is-expanded'); + expect((await screen.findByText('覆盖 100.0%')).closest('.semi-tag')).toBeInTheDocument(); + expect((await screen.findByText('时间窗完整')).closest('.semi-tag')).toBeInTheDocument(); fireEvent.change(screen.getByPlaceholderText('车牌 / VIN,多台用逗号分隔'), { target: { value: 'NEWVIN' } }); fireEvent.click(screen.getByRole('button', { name: '查询' })); @@ -206,6 +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(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 d07b609d..b2b1f4e9 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx @@ -44,6 +44,11 @@ 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; @@ -61,10 +66,14 @@ function HistoryTrend({ response, category, loading, error, hasMetrics, expanded : category === 'mileage' ? '日里程按自然日通过明细核对' : '按查询时间窗汇总连续指标'; + const coverageColor = coverage >= 95 ? 'green' : coverage >= 70 ? 'orange' : 'red'; const header = + 覆盖 {coverage.toFixed(1)}% + {formatSeriesGrain(summary.grainSeconds)}粒度 · {summary.rawPointCount.toLocaleString('zh-CN')} 原始点 + : undefined} actions={{panel.maximum.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}{panel.minimum.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}{formatAxisTime(panel.start)}{formatAxisTime(panel.end)} {panel.lines.flatMap((line) => line.paths.map((path, index) => {line.label}))} )} : } - {summary ? {summary.evidence} · 缺失 {summary.missingBucketCount.toLocaleString('zh-CN')} / {summary.expectedBucketCount.toLocaleString('zh-CN')} 桶 · 查询 {summary.queryDurationMs} ms : null} + {summary ? : null} ; } @@ -141,12 +155,22 @@ function ExportJobsPanel({ showHeader = true }: { showHeader?: boolean }) { function EvidencePanel({ row, metrics, onClose, showHeader = true }: { row?: HistoryDataRow; metrics: HistoryMetricDefinition[]; onClose: () => void; showHeader?: boolean }) { return {showHeader ? } onClick={onClose} aria-label="关闭数据详情" /> : null} /> : null} - {row ? <> + + 证据判读 + {historyQualityLabel(row.quality)} + {row.qualityReason || '平台未返回质量说明'} + + + + + + }, { key: '服务时间', value: }, - { key: '数据来源', value: {row.protocol} }, + { key: '数据来源', value: }, { key: '数据质量', value: }, { key: '质量原因', value: row.qualityReason || '平台未返回质量说明' } ]} /> @@ -202,7 +226,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: 100 }, + { title: '协议', dataIndex: 'protocol', width: 110, render: (value: string) => }, ...metrics.map((metric) => ({ title: `${metric.label}${metric.unit ? ` (${metric.unit})` : ''}`, dataIndex: metric.key, @@ -461,7 +485,7 @@ export default function HistoryPage() { onKeyDown={scrollHistoryTable} > {mobileLayout - ? {resultRows.map((row) => selectRow(row)}>{row.plate || '未绑定车牌'}{row.vin}{historyQualityLabel(row.quality)}{row.protocol}{isHealthyHistoryQuality(row.quality) ? null : {row.qualityReason || '平台未返回质量说明'}}{visibleMetrics.slice(0, 4).map((metric) => {metric.label}{formatHistoryValue(row.values[metric.key], metric)})})} + ? {resultRows.map((row) => selectRow(row)}>{row.plate || '未绑定车牌'}{row.vin}{historyQualityLabel(row.quality)}{isHealthyHistoryQuality(row.quality) ? null : {row.qualityReason || '平台未返回质量说明'}}{visibleMetrics.slice(0, 4).map((metric) => {metric.label}{formatHistoryValue(row.values[metric.key], metric)})})} : } {dataQuery.isPending && keywords.length ? diff --git a/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.test.tsx index 9791934e..09dc6c3f 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/UsersPage.test.tsx @@ -286,7 +286,7 @@ test('filters the customer directory by status and exposes the active result sco } ]); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); - render(); + const view = render(); expect(await screen.findByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /选择客户 西部客户/ })).toBeInTheDocument(); @@ -309,6 +309,14 @@ test('filters the customer directory by status and exposes the active result sco await waitFor(() => expect(screen.getByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument()); expect(screen.getByRole('button', { name: /选择客户 西部客户/ })).toBeInTheDocument(); + // Semi keeps the first Select popup mounted briefly while its close animation + // finishes. Remount before the second scope assertion so the test never clicks + // a stale portal when the full suite is under load. + view.unmount(); + document.querySelectorAll('.semi-portal').forEach((portal) => portal.remove()); + render(); + expect(await screen.findByRole('button', { name: /选择客户 华东客户/ })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('combobox', { name: '访问状态' })); const attentionOption = await waitFor(() => { const options = [...document.querySelectorAll('.semi-select-option')] 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 2ea17b86..d933899a 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -7358,7 +7358,8 @@ font-size: 9px; } - .v2-history-mobile-card-content p { + .v2-history-mobile-card-content p, + .v2-history-mobile-meta { margin: 5px 0; font-size: 9px; } @@ -14311,6 +14312,199 @@ font-weight: 650; } +/* + * Semi UI history evidence refinement. + * Protocol, coverage and quality are decision signals, so they use one shared + * tag language instead of competing with dense identifiers and raw numbers. + */ +.v2-history-protocol-tag.semi-tag { + min-height: 22px; + flex: 0 0 auto; + border-radius: 999px; + padding-inline: 8px; + font-size: 10px; + font-weight: 700; + letter-spacing: .01em; + white-space: nowrap; +} + +.v2-history-trend-meta { + display: inline-flex; + min-width: 0; + align-items: center; + gap: 8px; +} + +.v2-history-trend-meta > .semi-tag { + min-height: 22px; + flex: 0 0 auto; + border-radius: 999px; + padding-inline: 8px; + font-size: 10px; + font-weight: 720; +} + +.v2-history-trend-meta > span { + overflow: hidden; + color: #75859a; + font-size: 10px; + font-variant-numeric: tabular-nums; + text-overflow: ellipsis; + white-space: nowrap; +} + +.v2-history-trend-panels article { + display: grid; + grid-template-rows: 28px minmax(0, 1fr) 23px; + border-color: #dfe7f0; + border-radius: 9px; + background: #fff; +} + +.v2-history-trend-panels article > header { + height: auto; + padding-inline: 9px; + border-bottom: 1px solid #edf1f5; + background: #fbfcfe; +} + +.v2-history-trend-panels article > header strong { + color: #33475f; + font-size: 11px; +} + +.v2-history-trend-panels article > header span { + color: #8290a2; + font-size: 9px; + font-variant-numeric: tabular-nums; +} + +.v2-history-trend-panels svg { + height: 100%; + min-height: 0; +} + +.v2-history-trend-panels article > footer { + min-height: 23px; + border-top: 1px solid #f0f3f7; + padding-inline: 9px; + color: #65768b; + font-size: 9px; +} + +.v2-history-trend-panels article > footer i { + width: 12px; + height: 3px; + border-radius: 999px; +} + +.v2-chart-axis text { + fill: #6f7f93; + font-size: 9px; +} + +.v2-history-trend-evidence { + display: flex; + min-height: 31px; + flex: 0 0 auto; + align-items: center; + gap: 8px; + overflow: hidden; + border-top: 1px solid #e8edf3; + padding: 4px 10px; + color: #748397; + font-size: 9px; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.v2-history-trend-evidence > .semi-tag { + min-height: 21px; + flex: 0 0 auto; + border-radius: 999px; + padding-inline: 7px; + font-size: 9px; + font-weight: 700; +} + +.v2-history-trend-evidence > span:first-of-type { + min-width: 0; + flex: 1 1 auto; + overflow: hidden; + text-overflow: ellipsis; +} + +.v2-history-trend-evidence > strong { + flex: 0 0 auto; + color: #596b80; + font-size: inherit; + font-weight: 700; +} + +.v2-history-evidence-status { + display: grid; + min-height: 80px; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + border-bottom: 1px solid #dfe7f0; + padding: 11px 13px; + background: linear-gradient(135deg, #f7fafc 0%, #fff 100%); +} + +.v2-history-evidence-status.is-healthy { + box-shadow: inset 3px 0 #18a86b; +} + +.v2-history-evidence-status.is-attention { + box-shadow: inset 3px 0 #eda43a; +} + +.v2-history-evidence-status > div { + display: flex; + min-width: 0; + flex-direction: column; + gap: 3px; +} + +.v2-history-evidence-status > div:last-child { + align-items: flex-end; + gap: 7px; +} + +.v2-history-evidence-status span { + color: #8290a2; + font-size: 10px; + font-weight: 650; +} + +.v2-history-evidence-status strong { + color: #263b53; + font-size: 17px; + line-height: 1.2; +} + +.v2-history-evidence-status small { + overflow: hidden; + color: #68798e; + font-size: 10px; + line-height: 1.45; + text-overflow: ellipsis; + white-space: nowrap; +} + +.v2-history-evidence-status .v2-history-time { + color: #66788e; + font-size: 10px; + font-weight: 650; +} + +.v2-history-data-table .v2-history-protocol-tag.semi-tag { + min-height: 21px; + padding-inline: 7px; + font-size: 9px; +} + @media (max-width: 900px) and (min-width: 681px) { .v2-history-summary-rail > .semi-card-body { grid-template-columns: minmax(230px, .7fr) minmax(0, 1.6fr); @@ -14351,6 +14545,71 @@ font-weight: 650; } + .v2-history-mobile-meta { + display: flex; + align-items: center; + justify-content: space-between; + margin-block: 6px; + } + + .v2-history-mobile-meta .v2-history-protocol-tag.semi-tag { + min-height: 20px; + padding-inline: 7px; + font-size: 9px; + } + + .v2-history-mobile-card-content dt { + font-size: 9px; + } + + .v2-history-mobile-card-content dd { + font-size: 11px; + font-weight: 700; + } + + .v2-history-trend .v2-workspace-panel-meta { + display: flex; + } + + .v2-history-trend-meta > span { + display: none; + } + + .v2-history-trend-meta > .semi-tag { + min-height: 20px; + padding-inline: 7px; + font-size: 9px; + } + + .v2-history-trend-panels { + padding: 7px; + } + + .v2-history-trend-evidence { + min-height: 29px; + gap: 6px; + padding-inline: 8px; + } + + .v2-history-trend-evidence > span:first-of-type, + .v2-history-trend-evidence > span:last-child { + display: none; + } + + .v2-history-evidence-status { + min-height: 74px; + gap: 8px; + padding: 9px 11px; + } + + .v2-history-evidence-status strong { + font-size: 16px; + } + + .v2-history-evidence-status small { + max-width: 205px; + } + .v2-history-summary-rail.semi-card { min-height: 132px; overflow: visible;
{row.protocol}