From 26ccd2434a8d96a0455f8f9863436b0e7b792c0d Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 19 Jul 2026 08:48:32 +0800 Subject: [PATCH] feat: focus operations review queue --- .../web/src/v2/pages/OperationsPage.test.tsx | 12 +- .../web/src/v2/pages/ReconciliationCenter.tsx | 48 +++- .../apps/web/src/v2/productionEntry.test.ts | 9 +- .../v2/shared/WorkspaceMetricRail.test.tsx | 27 ++- .../web/src/v2/shared/WorkspaceMetricRail.tsx | 60 ++++- .../apps/web/src/v2/styles/workspace.css | 226 ++++++++++++++++++ 6 files changed, 354 insertions(+), 28 deletions(-) diff --git a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx index 3bee16fa..862568c6 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.test.tsx @@ -82,9 +82,14 @@ test('renders reconciliation queue, loads evidence on demand and records review expect(screen.queryByText('先选择一辆车')).not.toBeInTheDocument(); expect(screen.getByText('质量问题队列').closest('.semi-card')).toHaveClass('v2-reconcile-center'); expect(screen.getByText('质量问题队列').closest('.v2-workspace-panel-header')).toHaveClass('v2-reconcile-heading'); - expect(document.querySelector('.v2-reconcile-kpi-card small')?.textContent).toBe('待复核'); - expect(document.querySelectorAll('.v2-reconcile-kpi-card')).toHaveLength(4); - expect(screen.getByText('SLA 超时').closest('.semi-card')).toHaveClass('v2-reconcile-kpi-card', 'is-sla', 'is-overdue'); + const reconciliationMetrics = screen.getByRole('list', { name: '差异处置队列概览' }); + expect(reconciliationMetrics.closest('.semi-card')).toHaveClass('v2-workspace-metric-rail', 'v2-reconcile-metric-rail'); + expect(within(reconciliationMetrics).getAllByRole('listitem')).toHaveLength(4); + expect(within(reconciliationMetrics).getByText('待复核').closest('[role="listitem"]')).toHaveClass('is-primary'); + expect(within(reconciliationMetrics).getByText('SLA 超时').closest('[role="listitem"]')).toHaveClass('is-danger'); + expect(within(reconciliationMetrics).getByText('已自动恢复').closest('[role="listitem"]')).toHaveClass('is-secondary', 'is-success'); + expect(screen.getByText('当前筛选').closest('.v2-workspace-metric-context')).toHaveTextContent('1'); + expect(document.querySelectorAll('.v2-reconcile-kpi-card')).toHaveLength(0); expect(screen.queryByText('活跃问题', { exact: true })).not.toBeInTheDocument(); expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument(); const trendButton = screen.getByRole('button', { name: '30 天趋势' }); @@ -169,6 +174,7 @@ test('renders only the compact mobile reconciliation surface and defers secondar const toolbar = document.querySelector('.v2-reconcile-toolbar'); expect(toolbar).toHaveClass('is-mobile-collapsed'); + expect(screen.getByRole('button', { name: /修改筛选差异:活跃差异 · 1 条/ })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /修改筛选差异/ })); expect(toolbar).not.toHaveClass('is-mobile-collapsed'); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/ReconciliationCenter.tsx b/vehicle-data-platform/apps/web/src/v2/pages/ReconciliationCenter.tsx index 984fe177..a7ba271b 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/ReconciliationCenter.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/ReconciliationCenter.tsx @@ -8,6 +8,7 @@ import { InlineError } from '../shared/AsyncState'; import { MobileFilterToggle } from '../shared/MobileFilterToggle'; import { PlatformTime } from '../shared/PlatformTime'; import { TablePagination } from '../shared/TablePagination'; +import { WorkspaceMetricRail } from '../shared/WorkspaceMetricRail'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet'; import { detailTriggerRow } from '../shared/detailTriggerRow'; @@ -312,7 +313,7 @@ export default function ReconciliationCenter() { const totalPages = Math.max(1, Math.ceil((page?.total ?? 0) / pageSize)); const issueRows = page?.items ?? []; const activeFilterCount = Number(Boolean(deferredKeyword)) + Number(severity !== 'all') + Number(ruleCode !== 'all'); - const filterSummary = `${status === 'active' ? '活跃差异' : statusLabel(status)}${activeFilterCount ? ` · 另 ${activeFilterCount} 项` : ''}`; + const filterSummary = `${status === 'active' ? '活跃差异' : statusLabel(status)} · ${(page?.total ?? 0).toLocaleString('zh-CN')} 条${activeFilterCount ? ` · 另 ${activeFilterCount} 项` : ''}`; const selectedIssue = detail.data ?? issueRows.find((item) => item.id === selectedID); const detailPanel = selectedID && detail.isPending ?
@@ -350,14 +351,39 @@ export default function ReconciliationCenter() { } /> {summary.isError ? summary.refetch()} /> : null} -
-
- 待复核{data?.pending.toLocaleString('zh-CN') ?? '—'}活跃问题 {data?.active.toLocaleString('zh-CN') ?? '—'} - SLA 超时{data?.overSla.toLocaleString('zh-CN') ?? '—'}{data?.overSla ? '超过 24 小时,优先复核' : '当前没有超时差异'} - 已确认来源{data?.confirmed.toLocaleString('zh-CN') ?? '—'}保留为活跃差异 - 已自动恢复{data?.recovered.toLocaleString('zh-CN') ?? '—'}规则已不再命中 -
-
+ 当前筛选{(page?.total ?? 0).toLocaleString('zh-CN')}按严重程度、最近发现时间排序} + />
({ value: item.name, label: `${ruleLabel(item.name)} · ${item.count}` })) ?? []) ]} />
-
- {(page?.total ?? 0).toLocaleString('zh-CN')} 个符合条件的问题 - 按严重程度、最近发现时间排序 -
{issues.isError ? issues.refetch()} /> : null}
{ expect(workspaceMetricRailSource).toContain("import { Card, CardGroup } from '@douyinfe/semi-ui'"); expect(workspaceMetricRailSource).toContain(' { expect(reconciliationSource).toContain(' { expect(view.container.querySelectorAll('.test-card.semi-card')).toHaveLength(3); expect(screen.getByLabelText('车辆数:20;当前授权范围')).toHaveClass('is-vehicles'); }); + + it('renders a list-first queue variant inside one Semi card', () => { + const view = render(当前筛选 1,347 条} + />); + + const rail = view.container.querySelector('.v2-workspace-metric-rail'); + expect(rail).toHaveClass('semi-card', 'is-queue', 'queue-metrics'); + const list = screen.getByRole('list', { name: '处置队列概览' }); + expect(within(list).getAllByRole('listitem')).toHaveLength(4); + expect(within(list).getByText('待复核').closest('[role="listitem"]')).toHaveClass('is-primary'); + expect(within(list).getByText('SLA 超时').closest('[role="listitem"]')).toHaveClass('is-danger'); + expect(within(list).getByText('已恢复').closest('[role="listitem"]')).toHaveClass('is-secondary', 'is-success'); + expect(view.container.querySelector('.v2-workspace-metric-context')).toHaveTextContent('当前筛选 1,347 条'); + expect(view.container.querySelectorAll('.semi-card')).toHaveLength(1); + }); }); diff --git a/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceMetricRail.tsx b/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceMetricRail.tsx index bb91569f..d4d00525 100644 --- a/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceMetricRail.tsx +++ b/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceMetricRail.tsx @@ -8,15 +8,15 @@ export type WorkspaceMetricRailItem = { className?: string; }; -export function WorkspaceMetricRail({ - ariaLabel, - primary, - secondary, - className, - cardClassName, - secondaryClassName, - primaryValueClassName -}: { +export type WorkspaceQueueMetricRailItem = { + label: string; + value: ReactNode; + note?: ReactNode; + tone?: 'neutral' | 'primary' | 'success' | 'warning' | 'danger'; + emphasis?: 'primary' | 'secondary'; +}; + +type WorkspaceSummaryMetricRailProps = { ariaLabel: string; primary: WorkspaceMetricRailItem; secondary: WorkspaceMetricRailItem[]; @@ -24,7 +24,47 @@ export function WorkspaceMetricRail({ cardClassName: string; secondaryClassName: string; primaryValueClassName?: string; -}) { +}; + +type WorkspaceQueueMetricRailProps = { + variant: 'queue'; + ariaLabel: string; + className: string; + items: WorkspaceQueueMetricRailItem[]; + context?: ReactNode; +}; + +export function WorkspaceMetricRail(props: WorkspaceSummaryMetricRailProps | WorkspaceQueueMetricRailProps) { + if ('items' in props) { + return +
+ {props.items.map((item, index) => { + const toneClassName = `is-${item.tone ?? 'neutral'}`; + const emphasisClassName = `is-${item.emphasis ?? (index < 2 ? 'primary' : 'secondary')}`; + return + {item.label} + {item.value} + {item.note ? {item.note} : null} + ; + })} +
+ {props.context ?
{props.context}
: null} +
; + } + + const { + ariaLabel, + primary, + secondary, + className, + cardClassName, + secondaryClassName, + primaryValueClassName + } = props; const cardClass = (item: WorkspaceMetricRailItem, primaryCard = false) => [ cardClassName, primaryCard ? 'is-primary' : '', 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 9c508fa5..68ee6608 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -16180,6 +16180,207 @@ min-width: 908px; } +/* + * Shared Semi UI queue metrics. + * Keep operational evidence in one compact rail so the result list remains + * the visual and interaction focus. + */ +.v2-workspace-metric-rail.is-queue.semi-card { + overflow: hidden; + border: 0; + border-bottom: 1px solid #e1e8f0; + border-radius: 0; + background: #fff; + box-shadow: none; +} + +.v2-workspace-metric-rail.is-queue > .semi-card-body { + display: grid; + min-height: 58px; + grid-template-columns: minmax(0, 1fr) 218px; +} + +.v2-workspace-metric-list { + display: grid; + min-width: 0; + grid-template-columns: 1.15fr 1.15fr .9fr .9fr; +} + +.v2-workspace-metric-list > span { + display: grid; + min-width: 0; + min-height: 58px; + grid-template-columns: auto minmax(0, 1fr); + grid-template-rows: auto auto; + align-content: center; + column-gap: 10px; + padding: 7px 13px; +} + +.v2-workspace-metric-list > span + span { + border-left: 1px solid #e5ebf2; +} + +.v2-workspace-metric-list > span > small { + grid-column: 1; + color: #78879a; + font-size: 9px; + font-weight: 650; + line-height: 14px; + white-space: nowrap; +} + +.v2-workspace-metric-list > span > strong { + grid-column: 1; + color: #243950; + font-size: 21px; + font-variant-numeric: tabular-nums; + line-height: 24px; + white-space: nowrap; +} + +.v2-workspace-metric-list > span > em { + align-self: center; + grid-column: 2; + grid-row: 1 / 3; + overflow: hidden; + color: #8a97a8; + font-size: 9px; + font-style: normal; + line-height: 14px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.v2-workspace-metric-list > span.is-secondary > strong { + font-size: 17px; +} + +.v2-workspace-metric-list > span.is-primary { + background: linear-gradient(145deg, #edf5ff 0%, #f8fbff 100%); + box-shadow: inset 0 -3px #1f6feb; +} + +.v2-workspace-metric-list > span.is-primary > :is(small, strong) { + color: #155fc5; +} + +.v2-workspace-metric-list > span.is-danger { + background: linear-gradient(145deg, #fff 0%, #fff8f7 100%); + box-shadow: inset 0 -3px #e2554f; +} + +.v2-workspace-metric-list > span.is-danger > :is(small, strong) { + color: #c2413d; +} + +.v2-workspace-metric-list > span.is-success > :is(small, strong) { + color: #148459; +} + +.v2-workspace-metric-list > span.is-warning > :is(small, strong) { + color: #a76512; +} + +.v2-workspace-metric-context { + display: flex; + min-width: 0; + align-items: center; + border-left: 1px solid #e5ebf2; + background: #fbfcfe; + padding: 7px 13px; +} + +.v2-workspace-metric-context > span { + display: grid; + min-width: 0; + grid-template-columns: auto 1fr; + align-items: baseline; + gap: 1px 6px; +} + +.v2-workspace-metric-context small { + color: #8491a2; + font-size: 9px; +} + +.v2-workspace-metric-context strong { + color: #2f5f9b; + font-size: 18px; + font-variant-numeric: tabular-nums; +} + +.v2-workspace-metric-context em { + grid-column: 1 / -1; + overflow: hidden; + color: #929eac; + font-size: 8px; + font-style: normal; + text-overflow: ellipsis; + white-space: nowrap; +} + +.v2-reconcile-metric-rail { + flex: 0 0 auto; +} + +@media (min-width: 681px) { + .v2-reconcile-heading.v2-workspace-panel-header { + min-height: 62px; + padding-block: 9px; + } + + .v2-reconcile-toolbar { + padding-block: 8px; + } +} + +@media (max-width: 900px) { + .v2-workspace-metric-rail.is-queue > .semi-card-body { + grid-template-columns: minmax(0, 1fr) 180px; + } + + .v2-workspace-metric-list > span { + column-gap: 6px; + padding-inline: 9px; + } +} + +@media (max-width: 680px) { + .v2-workspace-metric-rail.is-queue > .semi-card-body { + min-height: 56px; + grid-template-columns: minmax(0, 1fr); + } + + .v2-workspace-metric-list { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .v2-workspace-metric-list > span { + min-height: 56px; + grid-template-columns: auto minmax(0, 1fr); + column-gap: 8px; + padding: 6px 12px; + } + + .v2-workspace-metric-list > span.is-secondary { + display: none; + } + + .v2-workspace-metric-list > span > strong { + font-size: 19px; + line-height: 22px; + } + + .v2-workspace-metric-list > span > em { + font-size: 8px; + } + + .v2-workspace-metric-context { + display: none; + } +} + .v2-reconcile-issue-cell > span { display: flex; min-width: 0; @@ -21899,10 +22100,35 @@ padding: 8px; } + .v2-reconcile-detail-sidesheet .v2-workspace-config-summary { + min-height: 104px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .v2-reconcile-detail-sidesheet .v2-workspace-config-summary > span:last-child { + min-height: 38px; + grid-column: 1 / -1; + grid-template-columns: auto auto minmax(0, 1fr); + align-items: baseline; + align-content: center; + column-gap: 8px; + padding-block: 6px; + } + + .v2-reconcile-detail-sidesheet .v2-workspace-config-summary > span:last-child :is(small, strong, em) { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .v2-reconcile-detail-sidesheet .v2-workspace-config-summary > span:first-child strong, .v2-reconcile-detail-sidesheet .v2-workspace-config-summary > span:nth-child(2) strong { font-size: 11px; } + + .v2-reconcile-detail-sidesheet .v2-workspace-config-summary > span:last-child strong { + font-size: 10px; + } } @media (max-width: 900px) and (max-height: 480px) and (orientation: landscape) {