From bba6a621aa67a6ea9d0c2c33b6c7c327ce6455cb Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 18 Jul 2026 10:39:00 +0800 Subject: [PATCH] refine Semi UI history workspace --- .../web/src/v2/pages/HistoryPage.test.tsx | 12 ++- .../apps/web/src/v2/pages/HistoryPage.tsx | 77 ++++++++++++----- .../apps/web/src/v2/productionEntry.test.ts | 2 + .../apps/web/src/v2/styles/workspace.css | 84 +++++++++++++++++++ 4 files changed, 151 insertions(+), 24 deletions(-) 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 2021eebb..21dbb3aa 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 @@ -5,7 +5,7 @@ import { MemoryRouter } from 'react-router-dom'; import type { HistoryDataResponse, HistorySeriesResponse } from '../../api/types'; import { buildMonitorPath, withMonitorReturn } from '../routing/monitorContext'; import { ROUTER_FUTURE } from '../routing/routerConfig'; -import HistoryPage from './HistoryPage'; +import HistoryPage, { formatHistoryDateTime } from './HistoryPage'; const mocks = vi.hoisted(() => ({ historyMetricCatalog: vi.fn(), @@ -54,6 +54,12 @@ function renderPage(initialEntry = '/history?keywords=OLDVIN&dateFrom=2026-07-16 return { ...view, client }; } +test('renders protocol timestamps in Asia/Shanghai while preserving local values', () => { + expect(formatHistoryDateTime('2026-07-18T02:33:00Z')).toBe('2026-07-18 10:33:00'); + expect(formatHistoryDateTime('2026-07-18T02:33:00+08:00')).toBe('2026-07-18 02:33:00'); + expect(formatHistoryDateTime('2026-07-18 02:33:00')).toBe('2026-07-18 02:33:00'); +}); + test('preserves the exact monitor return after changing history filters', async () => { mocks.historyMetricCatalog.mockResolvedValue({ categories: [{ key: 'location', label: '位置数据' }], metrics: [metric] }); mocks.historyData.mockResolvedValue(historyData('OLDVIN', '旧车牌', 'old-as-of')); @@ -113,6 +119,9 @@ test('clears stale rows, charts, and evidence as soon as the history scope chang for (const className of ['v2-history-filter-card', 'v2-history-summary-rail', 'v2-history-table-card']) { expect(view.container.querySelector(`.${className}.semi-card`)).toBeInTheDocument(); } + expect(view.container.querySelector('.v2-history-discovery-shell')).toContainElement(screen.getByLabelText('历史数据查询操作')); + expect(screen.getByLabelText('历史数据查询操作')).toHaveTextContent('遥测证据检索'); + expect(screen.getByLabelText('历史数据查询操作')).toHaveTextContent('上海时区'); expect(view.container.querySelector('.v2-history-summary-rail')).toHaveAttribute('aria-label', '历史数据查询统计信息'); expect(view.container.querySelector('.v2-history-summary-secondary.semi-card-group')).toBeInTheDocument(); expect(view.container.querySelectorAll('.v2-history-summary-card.semi-card')).toHaveLength(4); @@ -240,6 +249,7 @@ test('opens a working column visibility panel and preserves non-hideable identit const view = renderPage(); expect(await screen.findByRole('columnheader', { name: '速度 (km/h)' })).toBeInTheDocument(); + expect(view.container.querySelector('.v2-history-time')).toHaveTextContent('2026-07-16 04:00:00'); expect(screen.queryByRole('columnheader', { name: 'SOC (%)' })).not.toBeInTheDocument(); expect(view.container.querySelector('.v2-history-metric-scroll')).not.toBeInTheDocument(); expect(screen.getByText('1 / 2 字段').closest('.semi-tag')).toBeInTheDocument(); 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 6055d03b..a8dc8656 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx @@ -10,6 +10,7 @@ import { downloadBlob } from '../domain/download'; import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState'; import { MonitorReturnBar } from '../shared/MonitorReturnBar'; import { TablePagination } from '../shared/TablePagination'; +import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; import { WorkspaceMetricRail } from '../shared/WorkspaceMetricRail'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; @@ -24,6 +25,16 @@ import { useSideSheetA11y } from '../hooks/useSideSheetA11y'; const DESKTOP_HISTORY_PAGE_SIZE = 50; const MOBILE_HISTORY_PAGE_SIZE = 20; const historyAxisTimeFormatter = new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }); +const historyDateTimeFormatter = new Intl.DateTimeFormat('sv-SE', { + timeZone: 'Asia/Shanghai', + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false +}); function historyQualityLabel(quality: string) { if (quality === 'normal' || quality === 'good') return '正常'; @@ -86,6 +97,15 @@ export function formatAxisTime(value: string) { if (!Number.isFinite(parsed.getTime())) return value.replace('T', ' ').slice(5, 16); return historyAxisTimeFormatter.format(parsed).replace('/', '-'); } + +export function formatHistoryDateTime(value: string) { + const fallback = value.replace('T', ' ').replace(/\.\d{1,9}/, '').replace(/Z$/, '').slice(0, 19); + if (!/(?:Z|[+-]\d{2}:?\d{2})$/i.test(value)) return fallback; + const parsed = new Date(value); + if (!Number.isFinite(parsed.getTime())) return fallback; + return historyDateTimeFormatter.format(parsed).replace('T', ' '); +} + function currentHistoryWindow() { const now = new Date(); const pad = (value: number) => String(value).padStart(2, '0'); const day = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`; @@ -132,8 +152,8 @@ function EvidencePanel({ row, metrics, onClose, showHeader = true }: { row?: His {row ? <>{formatHistoryDateTime(row.deviceTime)} }, + { key: '服务时间', value: }, { key: '数据来源', value: {row.protocol} }, { key: '数据质量', value: }, { key: '质量原因', value: row.qualityReason || '平台未返回质量说明' } @@ -183,8 +203,8 @@ function HistoryDataTable({ rows, metrics, selectedRowID, onSelect }: { rows: Hi title: '', dataIndex: 'selection', width: 42, render: (_: unknown, row: HistoryDataRow) => onSelect(row)} aria-label={`选择 ${row.plate || row.vin} ${row.deviceTime}`} /> }, - { title: '设备时间', dataIndex: 'deviceTime', width: 150 }, - { title: '服务时间', dataIndex: 'serverTime', width: 150 }, + { title: '设备时间', dataIndex: 'deviceTime', width: 150, render: (value: string) => }, + { title: '服务时间', dataIndex: 'serverTime', width: 150, render: (value: string) => }, { title: '车牌', dataIndex: 'plate', width: 110, render: (value: string) => value || '—' }, { title: 'VIN', dataIndex: 'vin', width: 160, render: (value: string) => {value} }, { title: '协议', dataIndex: 'protocol', width: 100 }, @@ -357,24 +377,35 @@ export default function HistoryPage() { return
- setFiltersCollapsed((value) => !value)} - > -
- -
时间范围
setDraft((current) => ({ ...current, dateFrom: value }))} /> setDraft((current) => ({ ...current, dateTo: value }))} />
- -
{exportAllowed ? : 只读}
-
-
+
+ 上海时区 · 最多 5 台车辆} + /> + setFiltersCollapsed((value) => !value)} + > +
+ +
时间范围
setDraft((current) => ({ ...current, dateFrom: value }))} /> setDraft((current) => ({ ...current, dateTo: value }))} />
+ +
{exportAllowed ? : 只读}
+
+
+
{keywords.length ? {mobileLayout - ?
{resultRows.map((row) => )}
+ ?
{resultRows.map((row) => )}
: } {dataQuery.isPending && keywords.length ? diff --git a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts index 05c68122..e559de28 100644 --- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts @@ -136,6 +136,8 @@ describe('V2 production entry', () => { expect(corePageSources.HistoryPage).not.toContain('v2-history-context-card'); expect(corePageSources.HistoryPage).not.toContain('v2-history-metric-scroll'); expect(corePageSources.HistoryPage).toContain(' .v2-history-command-bar { + width: 100%; + min-height: 50px; + border: 0; + border-bottom: 1px solid #e6ecf3; + border-radius: 14px 14px 0 0; + box-shadow: none; +} + +.v2-history-discovery-shell > .v2-history-filter-card.v2-workspace-filter-panel.semi-card { + width: 100%; + overflow: visible; + border: 0; + border-radius: 0 0 14px 14px; + box-shadow: none; +} + +.v2-history-discovery-shell > .v2-history-filter-card > .semi-card-body > .v2-workspace-filter-heading { + display: none; +} + +.v2-history-discovery-shell .v2-history-toolbar { + border-radius: 0 0 14px 14px; + background: #fbfcfe; +} + +.v2-history-time { + color: inherit; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.v2-history-data-table .v2-history-time { + color: #40536a; + font-size: 11px; + font-weight: 560; +} + +.v2-history-evidence-descriptions .v2-history-time { + color: #33485f; + font-weight: 650; +} + @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); @@ -11449,6 +11504,35 @@ } @media (max-width: 680px) { + .v2-history-discovery-shell { + display: contents; + } + + .v2-history-discovery-shell > .v2-history-command-bar { + display: none; + } + + .v2-history-discovery-shell > .v2-history-filter-card.v2-workspace-filter-panel.semi-card { + border: 1px solid #dce6f2; + border-radius: 10px; + box-shadow: var(--v2-shadow); + } + + .v2-history-discovery-shell > .v2-history-filter-card > .semi-card-body > .v2-workspace-filter-heading { + display: flex; + } + + .v2-history-discovery-shell .v2-history-toolbar { + border-radius: 0 0 10px 10px; + background: #fff; + } + + .v2-history-mobile-card-content .v2-history-time { + color: #617288; + font-size: 10px; + font-weight: 650; + } + .v2-history-summary-rail.semi-card { min-height: 132px; overflow: visible;