From 3b5a66b6879fc4530ea61d8646350df66df2e24f Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 19 Jul 2026 06:51:23 +0800 Subject: [PATCH] feat: unify governance evidence workbenches --- .../apps/web/src/v2/pages/AccessPage.test.tsx | 6 +- .../apps/web/src/v2/pages/AccessPage.tsx | 47 ++++-- .../web/src/v2/pages/OperationsPage.test.tsx | 38 +++-- .../web/src/v2/pages/ReconciliationCenter.tsx | 140 ++++++++++++------ .../apps/web/src/v2/productionEntry.test.ts | 6 +- .../apps/web/src/v2/styles/workspace.css | 91 ++++++++++++ 6 files changed, 249 insertions(+), 79 deletions(-) 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 09205501..0b023627 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 @@ -273,8 +273,10 @@ test('renders mobile access vehicles as selectable Semi cards', async () => { const governanceButton = screen.getByRole('button', { name: /接入治理/ }); fireEvent.click(governanceButton); const governanceDialog = await screen.findByRole('dialog', { name: '接入治理配置' }); - expect(document.querySelector('.v2-access-governance-sidesheet')).toHaveClass('semi-sidesheet-bottom'); - expect(document.querySelector('.v2-access-governance-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(86dvh, 760px)' }); + expect(document.querySelector('.v2-access-governance-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-config-sidesheet'); + expect(document.querySelector('.v2-access-governance-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(88dvh, 780px)' }); + expect(within(governanceDialog).getByText('接入治理工作台')).toBeInTheDocument(); + expect(within(governanceDialog).getByRole('list', { name: '接入治理配置摘要' })).toBeInTheDocument(); fireEvent.click(within(governanceDialog).getByRole('button', { name: '关闭接入治理配置' })); expect(governanceButton).toHaveAttribute('aria-expanded', 'false'); expect(document.body.style.overflow).not.toBe('hidden'); 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 8c7f3f2c..817585ed 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx @@ -1,5 +1,5 @@ import { IconChevronDown, IconChevronRight, IconClose, IconDownload, IconMore, IconRefresh, IconSave, IconSearch, IconSetting } from '@douyinfe/semi-icons'; -import { Button, Card, CardGroup, Collapse, Descriptions, Dropdown, Empty, Input, Select, SideSheet, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui'; +import { Button, Card, CardGroup, Collapse, Descriptions, Dropdown, Empty, Input, Select, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { FormEvent, KeyboardEvent, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { Link, useSearchParams } from 'react-router-dom'; @@ -15,6 +15,7 @@ import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; +import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet'; import { detailTriggerRow } from '../shared/detailTriggerRow'; import { usePlatformSession } from '../auth/AuthGate'; import { canAdminister } from '../auth/session'; @@ -356,7 +357,6 @@ export default function AccessPage() { }, [mobileLayout]); const rows = vehiclesQuery.data?.items ?? []; const selected = rows.find((row) => row.vin === selectedVIN); const mobileFiltersOpen = mobileLayout && !filtersCollapsed; - useSideSheetA11y(editable && governanceOpen, '.v2-access-governance-sidesheet', 'v2-access-governance', '接入治理配置', '关闭接入治理配置'); useSideSheetA11y(mobileFiltersOpen, '.v2-access-mobile-filter-sidesheet', 'v2-access-mobile-filters', '接入车辆筛选', '关闭接入车辆筛选'); const syncURL = (filters: Filters) => { const next = new URLSearchParams(); Object.entries(filters).forEach(([key, value]) => { if (value) next.set(key, value); }); setSearchParams(next, { replace: true }); }; const apply = (next: Filters) => { setDraft(next); setCriteria(next); setOffset(0); setSelectedVIN(''); syncURL(next); }; @@ -505,28 +505,49 @@ export default function AccessPage() { > {selected ? setSelectedVIN('')} sheet mobile={mobileLayout} /> : null} - {editable ? 接入治理集中处理待绑定来源与在线判定阈值} + height={mobileLayout ? 'min(88dvh, 780px)' : undefined} + title="接入治理工作台" + description="核对待绑定来源,统一维护协议在线判定阈值" + badge={(unresolvedQuery.data?.total ?? 0) > 0 ? `${unresolvedQuery.data?.total.toLocaleString('zh-CN')} 项待办` : '治理正常'} + badgeColor={(unresolvedQuery.data?.total ?? 0) > 0 ? 'orange' : 'green'} + summaryItems={[ + { + label: '待绑定来源', + value: (unresolvedQuery.data?.total ?? 0).toLocaleString('zh-CN'), + detail: (unresolvedQuery.data?.total ?? 0) > 0 ? '需要核对权威 VIN' : '当前没有身份待办', + tone: (unresolvedQuery.data?.total ?? 0) > 0 ? 'warning' : 'success' + }, + { + label: '阈值版本', + value: `v${thresholdQuery.data?.version ?? '—'}`, + detail: '在线与延迟统一口径', + tone: 'primary' + }, + { + label: '主车辆口径', + value: (summary?.totalVehicles ?? 0).toLocaleString('zh-CN'), + detail: '仅统计权威主车辆', + tone: 'neutral' + } + ]} + footerNote="阈值保存后即时生效;关闭工作台不会丢失已保存配置" + primaryAction={{ label: '完成', onClick: () => setGovernanceOpen(false) }} onCancel={() => setGovernanceOpen(false)} - footer={} > {governanceOpen ?
- -
0 ? 'is-attention' : ''}>待绑定来源{(unresolvedQuery.data?.total ?? 0).toLocaleString('zh-CN')}{(unresolvedQuery.data?.total ?? 0) > 0 ? '优先处理' : '暂无待办'}
-
阈值版本v{thresholdQuery.data?.version ?? '—'}
-
主车辆口径{(summary?.totalVehicles ?? 0).toLocaleString('zh-CN')}
-
{unresolvedQuery.isError ? unresolvedQuery.refetch()} /> : null} {thresholdQuery.isError ? thresholdQuery.refetch()} /> : null} thresholdDraft && updateThreshold.mutate(thresholdDraft)} />
: null} -
: null} + : null} ; } 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 d8c3e0b1..4e147d88 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 @@ -92,14 +92,19 @@ test('renders reconciliation queue, loads evidence on demand and records review expect(screen.getByRole('button', { name: '刷新结果' })).toHaveClass('v2-workspace-mobile-tool-button', 'is-muted'); expect(trendButton).toHaveAttribute('aria-expanded', 'false'); fireEvent.click(trendButton); - expect(screen.getByText('近 30 天趋势').closest('.semi-card')).toHaveClass('v2-reconcile-trend'); - expect(screen.getByText('近 30 天趋势').closest('.v2-workspace-panel-header')).toHaveClass('is-compact'); + const trendDialog = await screen.findByRole('dialog', { name: '30 天差异趋势' }); + expect(trendDialog.closest('.semi-sidesheet')).toHaveClass('v2-workspace-detail-sidesheet', 'v2-reconcile-trend-sidesheet'); + expect(within(trendDialog).getByText('趋势明细').closest('.semi-card')).toHaveClass('v2-reconcile-trend'); + expect(within(trendDialog).getByText('趋势明细').closest('.v2-workspace-panel-header')).toHaveClass('is-compact'); + expect(within(trendDialog).getByRole('list', { name: '30 天差异趋势摘要' })).toBeInTheDocument(); expect(trendButton).toHaveAttribute('aria-expanded', 'true'); + fireEvent.click(within(trendDialog).getByRole('button', { name: '关闭 30 天差异趋势' })); + expect(trendButton).toHaveAttribute('aria-expanded', 'false'); expect(document.querySelector('.v2-reconcile-table.semi-table-wrapper')).toBeInTheDocument(); expect(screen.getByLabelText('运维质量操作')).toHaveTextContent('上海时间'); expect(screen.getAllByText('2026-07-16 18:00:00').length).toBeGreaterThan(0); expect(document.querySelector('.v2-platform-time.v2-ops-time')).toBeInTheDocument(); - const issueTitles = await screen.findAllByText('多来源实时位置漂移'); + expect((await screen.findAllByText('多来源实时位置漂移')).length).toBeGreaterThan(0); expect(document.querySelector('.v2-reconcile-mobile-card.semi-card')).not.toBeInTheDocument(); expect(document.querySelector('.v2-reconcile-table-wrap > table')).not.toBeInTheDocument(); const reconcileRow = screen.getByTestId('reconcile-row-issue-1'); @@ -108,18 +113,21 @@ test('renders reconciliation queue, loads evidence on demand and records review fireEvent.keyDown(reconcileRow, { key: ' ' }); expect(await screen.findByText('规则证据')).toBeInTheDocument(); expect(reconcileRow).toHaveAttribute('aria-expanded', 'true'); + const detailDialog = screen.getByRole('dialog', { name: '差异证据与处置' }); + expect(detailDialog.closest('.semi-sidesheet')).toHaveClass('v2-workspace-detail-sidesheet', 'v2-reconcile-detail-sidesheet'); + expect(within(detailDialog).getByRole('list', { name: '差异证据与处置摘要' })).toBeInTheDocument(); expect(screen.getByText('规则证据').closest('.semi-card')).toHaveClass('v2-reconcile-evidence'); expect(screen.getByText('复核结论').closest('.semi-card')).toHaveClass('v2-reconcile-review'); expect(screen.getByText('处理履历').closest('.semi-card')).toHaveClass('v2-reconcile-actions'); expect(screen.getByText('规则证据').closest('.v2-workspace-panel-header')).toHaveClass('is-compact'); - expect(document.querySelector('.v2-reconcile-detail-heading.v2-workspace-panel-header')).toBeInTheDocument(); + expect(document.querySelector('.v2-reconcile-detail-heading.v2-workspace-panel-header')).not.toBeInTheDocument(); expect(screen.getByText('复核提示')).toBeInTheDocument(); expect(screen.getByText('坐标差距')).toBeInTheDocument(); expect(screen.getByText('distanceM')).toBeInTheDocument(); expect(screen.getByText('1,286 m')).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: '关闭差异详情' })); + fireEvent.click(within(detailDialog).getByRole('button', { name: '关闭差异证据与处置' })); expect(reconcileRow).toHaveAttribute('aria-expanded', 'false'); - fireEvent.click(issueTitles[0]); + fireEvent.click(reconcileRow); expect(await screen.findByText('规则证据')).toBeInTheDocument(); fireEvent.click(screen.getByRole('radio', { name: '采信 GB32960' })); fireEvent.change(screen.getByLabelText('说明(必填)'), { target: { value: '来源 A 原始报文可信' } }); @@ -152,10 +160,11 @@ test('renders only the compact mobile reconciliation surface and defers secondar expect(await screen.findByRole('dialog', { name: '差异证据与处置' })).toBeInTheDocument(); expect(document.querySelector('.v2-reconcile-detail-sidesheet .v2-reconcile-detail.is-sheet')).toBeInTheDocument(); expect(document.querySelector('.v2-reconcile-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom'); - expect(document.querySelector('.v2-reconcile-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(84dvh, 740px)' }); - expect(await screen.findByRole('button', { name: '关闭差异详情' })).toBeInTheDocument(); + expect(document.querySelector('.v2-reconcile-detail-sidesheet')).toHaveClass('v2-workspace-detail-sidesheet'); + expect(document.querySelector('.v2-reconcile-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(88dvh, 780px)' }); + expect(await screen.findByRole('button', { name: '关闭差异证据与处置' })).toBeInTheDocument(); expect(mobileIssueAction).toHaveAttribute('aria-expanded', 'true'); - fireEvent.click(screen.getByRole('button', { name: '关闭差异详情' })); + fireEvent.click(screen.getByRole('button', { name: '关闭差异证据与处置' })); expect(mobileIssueAction).toHaveAttribute('aria-expanded', 'false'); const toolbar = document.querySelector('.v2-reconcile-toolbar'); @@ -166,14 +175,13 @@ test('renders only the compact mobile reconciliation surface and defers secondar expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '30 天趋势' })); expect(await screen.findByRole('dialog', { name: '30 天差异趋势' })).toBeInTheDocument(); - expect(document.querySelector('.v2-reconcile-trend-sidesheet')).toHaveClass('semi-sidesheet-bottom'); - expect(document.querySelector('.v2-reconcile-trend-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(62dvh, 560px)' }); - const trend = screen.getByText('近 30 天趋势').closest('.v2-reconcile-trend'); + expect(document.querySelector('.v2-reconcile-trend-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-detail-sidesheet'); + expect(document.querySelector('.v2-reconcile-trend-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(72dvh, 620px)' }); + const trend = screen.getByText('趋势明细').closest('.v2-reconcile-trend'); expect(trend).toBeInTheDocument(); expect(screen.getByText(/最近运行/)).toBeInTheDocument(); - expect(document.querySelector('.v2-reconcile-trend-toggle')).toHaveAttribute('aria-expanded', 'true'); - fireEvent.click(screen.getByRole('button', { name: '收起趋势' })); - expect(screen.queryByText('近 30 天趋势')).not.toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '关闭 30 天差异趋势' })); + expect(screen.queryByText('趋势明细')).not.toBeInTheDocument(); }); test('reconciles service identities with bound and identity-required vehicles', async () => { 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 85c76677..984fe177 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/ReconciliationCenter.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/ReconciliationCenter.tsx @@ -1,5 +1,5 @@ import { IconAlertTriangle, IconChevronDown, IconChevronRight, IconChevronUp, IconClose, IconRefresh, IconSearch } from '@douyinfe/semi-icons'; -import { Button, Card, Empty, Input, RadioGroup, Select, SideSheet, Spin, Table, Tag, TextArea } from '@douyinfe/semi-ui'; +import { Button, Card, Empty, Input, RadioGroup, Select, Spin, Table, Tag, TextArea } from '@douyinfe/semi-ui'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useDeferredValue, useEffect, useMemo, useState } from 'react'; import { api } from '../../api/client'; @@ -9,10 +9,10 @@ import { MobileFilterToggle } from '../shared/MobileFilterToggle'; import { PlatformTime } from '../shared/PlatformTime'; import { TablePagination } from '../shared/TablePagination'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; +import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet'; import { detailTriggerRow } from '../shared/detailTriggerRow'; import { QUERY_MEMORY } from '../queryPolicy'; import { useMobileLayout } from '../hooks/useMobileLayout'; -import { useSideSheetA11y } from '../hooks/useSideSheetA11y'; const DESKTOP_PAGE_SIZE = 50; const MOBILE_PAGE_SIZE = 20; @@ -171,27 +171,16 @@ function reviewGuidance(issue: ReconciliationIssue) { }[issue.ruleCode] ?? '先核对原始证据、影响对象和时间范围,再选择结论并留下可追溯说明。'; } -function ReconciliationTrend({ data, maxTrend, onClose }: { +function ReconciliationTrend({ data, maxTrend }: { data?: ReconciliationSummary; maxTrend: number; - onClose: () => void; }) { return 最近运行 } - actions={} />
{data?.trend.slice(-14).map((item) =>
{item.active}
)}
{!data?.trend.length ?

完成首次每日检测后显示趋势。

: null} @@ -221,19 +210,19 @@ function ReconciliationDetail({ issue, onClose, sheet = false }: { issue: Reconc const evidenceEntries = Object.entries(issue.evidence ?? {}); const statusOptions = reviewStatusOptions(issue); return - } - actions={sheet ? undefined : } @@ -363,7 +358,7 @@ export default function ReconciliationCenter() { 已自动恢复{data?.recovered.toLocaleString('zh-CN') ?? '—'}规则已不再命中 -
+
setSelectedID(item.id) + onOpen: () => openDetail(item.id) }) : ({})} /> :
{issueRows.map((item) => -
setOffset((next - 1) * pageSize)} />
- {trendExpanded && !mobileLayout ?
setTrendExpanded(false)} />
: null} - {!mobileLayout ? detailPanel : null}
- 差异证据与处置{selectedIssue ? `${selectedIssue.plate || '平台级差异'} · ${ruleLabel(selectedIssue.ruleCode)}` : '加载规则证据与处置履历'}
} + variant="detail" + visible={Boolean(selectedID)} + placement={mobileLayout ? 'bottom' : 'right'} + width={mobileLayout ? undefined : 680} + height={mobileLayout ? 'min(88dvh, 780px)' : undefined} + ariaLabel="差异证据与处置" + closeLabel="关闭差异证据与处置" + title={selectedIssue?.title ?? '差异证据与处置'} + description={selectedIssue ? `${ruleLabel(selectedIssue.ruleCode)} · 原始证据、复核结论与处理履历` : '正在加载规则证据与处置履历'} + badge={selectedIssue ? `${severityLabel(selectedIssue.severity)} · ${statusLabel(selectedIssue.status)}` : '读取中'} + badgeColor={selectedIssue?.severity === 'critical' ? 'red' : selectedIssue?.severity === 'major' ? 'orange' : 'grey'} + summaryItems={selectedIssue ? [ + { + label: '车辆', + value: selectedIssue.plate || '平台级差异', + detail: selectedIssue.vin || '非单车口径', + tone: 'primary' + }, + { + label: '证据来源', + value: [selectedIssue.protocolA, selectedIssue.protocolB].filter(Boolean).join(' ↔ ') || '平台口径', + detail: `累计发现 ${selectedIssue.occurrenceCount.toLocaleString('zh-CN')} 次`, + tone: selectedIssue.severity === 'critical' ? 'danger' : 'warning' + }, + { + label: '最近发现', + value: , + detail: 首次 , + tone: 'neutral' + } + ] : []} onCancel={closeDetail} > - {mobileLayout ? detailPanel : null} - - + setTrendExpanded(false)} > - {mobileLayout && trendExpanded ?
setTrendExpanded(false)} />
: null} -
+ {trendExpanded ?
: null} + ; } 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 83b6f6ad..143208f5 100644 --- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts @@ -237,6 +237,8 @@ describe('V2 production entry', () => { expect(corePageSources.AccessPage).toContain(' { expect(reconciliationSource).toContain(' span:first-child strong, +.v2-reconcile-detail-sidesheet .v2-workspace-config-summary > span:nth-child(2) strong { + font-size: 13px; +} + +.v2-reconcile-detail-sidesheet .v2-workspace-config-summary > span:last-child strong, +.v2-reconcile-detail-sidesheet .v2-workspace-config-summary > span:last-child em { + overflow: visible; +} + +.v2-reconcile-detail-sidesheet .v2-reconcile-detail.is-sheet.semi-card { + min-height: 100%; + background: transparent; +} + +.v2-reconcile-detail-sidesheet .v2-reconcile-detail.is-sheet > .semi-card-body { + background: transparent; +} + +.v2-reconcile-detail-sidesheet .v2-reconcile-detail-body { + gap: 10px; + padding: 10px; +} + +.v2-reconcile-trend-sidesheet .v2-workspace-config-content { + padding: 10px; +} + +.v2-reconcile-trend-sidesheet .v2-workspace-config-content > #v2-reconcile-trend, +.v2-reconcile-trend-sidesheet .v2-reconcile-trend.semi-card { + min-height: 100%; +} + +.v2-reconcile-trend-sidesheet .v2-reconcile-trend.semi-card { + border: 1px solid #dce5ef; + border-radius: 11px; + background: #fff; + box-shadow: 0 7px 22px rgba(31, 53, 80, .05); +} + +@media (max-width: 680px) { + .v2-access-governance-sidesheet .v2-workspace-config-content, + .v2-reconcile-detail-sidesheet .v2-reconcile-detail-body, + .v2-reconcile-trend-sidesheet .v2-workspace-config-content { + padding: 8px; + } + + .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; + } +} + +@media (max-width: 900px) and (max-height: 480px) and (orientation: landscape) { + .v2-access-governance-sidesheet .v2-access-governance { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr) auto; + } + + .v2-access-governance-sidesheet .v2-access-identity-queue-v3, + .v2-access-governance-sidesheet .v2-access-settings { + grid-column: 1; + } +}