From 63b50732cb7811c9a9c7f8a672c6009a6ecf90c7 Mon Sep 17 00:00:00 2001 From: lingniu Date: Sun, 19 Jul 2026 05:38:17 +0800 Subject: [PATCH] feat: unify detail side sheets --- .../apps/web/src/v2/domain/formatters.ts | 8 ++ .../apps/web/src/v2/pages/AccessPage.test.tsx | 3 +- .../apps/web/src/v2/pages/AccessPage.tsx | 20 ++-- .../apps/web/src/v2/pages/AlertsPage.test.tsx | 3 +- .../apps/web/src/v2/pages/AlertsPage.tsx | 21 ++-- .../web/src/v2/pages/HistoryPage.test.tsx | 3 +- .../apps/web/src/v2/pages/HistoryPage.tsx | 13 ++- .../apps/web/src/v2/pages/VehiclePage.tsx | 7 +- .../apps/web/src/v2/productionEntry.test.ts | 12 +- .../VehicleSourceEvidencePanel.test.tsx | 3 +- .../v2/shared/VehicleSourceEvidencePanel.tsx | 19 +-- .../shared/WorkspaceDetailSideSheet.test.tsx | 29 +++++ .../v2/shared/WorkspaceDetailSideSheet.tsx | 55 +++++++++ .../web/src/v2/shared/WorkspaceSideSheet.tsx | 10 +- .../apps/web/src/v2/styles/workspace.css | 109 ++++++++++++++++++ 15 files changed, 273 insertions(+), 42 deletions(-) create mode 100644 vehicle-data-platform/apps/web/src/v2/shared/WorkspaceDetailSideSheet.test.tsx create mode 100644 vehicle-data-platform/apps/web/src/v2/shared/WorkspaceDetailSideSheet.tsx diff --git a/vehicle-data-platform/apps/web/src/v2/domain/formatters.ts b/vehicle-data-platform/apps/web/src/v2/domain/formatters.ts index 32689c31..f071fe2f 100644 --- a/vehicle-data-platform/apps/web/src/v2/domain/formatters.ts +++ b/vehicle-data-platform/apps/web/src/v2/domain/formatters.ts @@ -12,6 +12,14 @@ const shanghaiDateTimeFormatter = new Intl.DateTimeFormat('sv-SE', { export type DateTimeSourceZone = 'preserve' | 'utc'; +export const vehicleOperationStatusLabels = { + unknown: '待维护', + active: '运营中', + inactive: '停运', + maintenance: '维保中', + retired: '已退役' +} as const; + export function formatZhNumber(value: number, maximumFractionDigits = 0) { let formatter = zhNumberFormatters.get(maximumFractionDigits); if (!formatter) { 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 0eb6906c..09205501 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 @@ -262,8 +262,9 @@ test('renders mobile access vehicles as selectable Semi cards', async () => { expect(within(dialog).getByRole('button', { name: '收起 JT808 完整证据' })).toHaveAttribute('aria-expanded', 'true'); expect(mocks.accessVehicles).toHaveBeenCalledTimes(vehicleRequestsBeforeEvidenceToggle); expect(mocks.accessSummary).toHaveBeenCalledTimes(summaryRequestsBeforeEvidenceToggle); - expect(document.querySelector('.v2-access-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom'); + expect(document.querySelector('.v2-access-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-detail-sidesheet'); expect(document.querySelector('.v2-access-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(84dvh, 740px)' }); + expect(document.querySelector('.v2-access-detail-sidesheet .v2-workspace-config-title > .semi-tag')).toHaveTextContent('部分来源异常'); fireEvent.click(within(dialog).getByRole('button', { name: '关闭车辆接入详情' })); expect(action).toHaveAttribute('aria-pressed', 'false'); expect(action).toHaveAttribute('aria-expanded', 'false'); 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 5e7d1ee9..8c7f3f2c 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx @@ -12,6 +12,7 @@ import { MobileFilterToggle } from '../shared/MobileFilterToggle'; import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet'; import { TablePagination } from '../shared/TablePagination'; import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; +import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { detailTriggerRow } from '../shared/detailTriggerRow'; @@ -149,8 +150,11 @@ const ProtocolState = memo(function ProtocolState({ }); function ConnectionTag({ row }: { row: AccessVehicleRow }) { - const color = row.connectionState === 'healthy' ? 'green' : row.connectionState === 'not_connected' || row.connectionState === 'offline' ? 'red' : 'orange'; - return {connectionLabels[row.connectionState]}; + return {connectionLabels[row.connectionState]}; +} + +function connectionTagColor(state: AccessVehicleRow['connectionState']) { + return state === 'healthy' ? 'green' as const : state === 'not_connected' || state === 'offline' ? 'red' as const : 'orange' as const; } const ConnectionState = memo(function ConnectionState({ row }: { row: AccessVehicleRow }) { @@ -352,7 +356,6 @@ export default function AccessPage() { }, [mobileLayout]); const rows = vehiclesQuery.data?.items ?? []; const selected = rows.find((row) => row.vin === selectedVIN); const mobileFiltersOpen = mobileLayout && !filtersCollapsed; - useSideSheetA11y(Boolean(selected), '.v2-access-detail-sidesheet', 'v2-access-detail', '车辆接入详情', '关闭车辆接入详情'); 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 }); }; @@ -487,18 +490,21 @@ export default function AccessPage() { - 车辆接入详情{selected ? `${selected.plate || '未绑定车牌'} · ${selected.vin}` : '来源、在线状态与接入证据'}} + title="车辆接入详情" + description={selected ? `${selected.plate || '未绑定车牌'} · ${selected.vin}` : '来源、在线状态与接入证据'} + badge={selected ? connectionLabels[selected.connectionState] : undefined} + badgeColor={selected ? connectionTagColor(selected.connectionState) : 'grey'} onCancel={() => setSelectedVIN('')} > {selected ? setSelectedVIN('')} sheet mobile={mobileLayout} /> : null} - + {editable ? { expect(action).toHaveAttribute('aria-pressed', 'true'); expect(action).toHaveAttribute('aria-expanded', 'true'); expect(await screen.findByRole('dialog', { name: '告警事件详情' })).toBeInTheDocument(); - expect(document.querySelector('.v2-alert-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom'); + expect(document.querySelector('.v2-alert-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-detail-sidesheet'); expect(document.querySelector('.v2-alert-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(90dvh, 800px)' }); + expect(document.querySelector('.v2-alert-detail-sidesheet .v2-workspace-config-title > .semi-tag')).toHaveTextContent('未处理'); expect(document.querySelector('.v2-alert-detail-sidesheet .v2-alert-inspector-heading')).not.toBeInTheDocument(); const mobileDisposition = document.querySelector('.v2-alert-detail-sidesheet .v2-alert-disposition-card'); const mobileProgress = document.querySelector('.v2-alert-detail-sidesheet .v2-alert-progress-card'); 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 e9216e02..ae852366 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx @@ -14,6 +14,7 @@ import { ProtocolTag } from '../shared/ProtocolTag'; import { SegmentedTabs } from '../shared/SegmentedTabs'; import { TablePagination } from '../shared/TablePagination'; import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; +import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; import { detailTriggerRow } from '../shared/detailTriggerRow'; @@ -63,8 +64,11 @@ function SeverityTag({ severity }: Pick) { return {severityLabels[severity]}; } function StatusTag({ status }: Pick) { - const color = status === 'unprocessed' ? 'red' : status === 'processing' ? 'blue' : status === 'recovered' ? 'green' : 'grey'; - return {statusLabels[status]}; + return {statusLabels[status]}; +} + +function alertStatusColor(status: AlertStatus) { + return status === 'unprocessed' ? 'red' as const : status === 'processing' ? 'blue' as const : status === 'recovered' ? 'green' as const : 'grey' as const; } function AlertTime({ value, className = '' }: { value?: string; className?: string }) { @@ -193,7 +197,6 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e const mobileFiltersOpen = mobileLayout && !filtersCollapsed; useSideSheetA11y(!mobileLayout && advancedOpen, '.v2-alert-filter-sidesheet', 'v2-alert-advanced-filters', '告警高级筛选', '关闭告警高级筛选'); useSideSheetA11y(mobileFiltersOpen, '.v2-alert-mobile-filter-sidesheet', 'v2-alert-mobile-filters', '告警事件筛选', '关闭告警事件筛选'); - useSideSheetA11y(mobileLayout && Boolean(selectedID), '.v2-alert-detail-sidesheet', 'v2-alert-detail', '告警事件详情', '关闭告警详情'); const applyDraft = () => { setFilters(draft); setOffset(0); @@ -337,17 +340,21 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
{advancedFilterFields}
: null} - 告警详情{selectedEvent ? `${selectedEvent.plate || selectedEvent.vin} · ${selectedEvent.ruleName}` : '证据、状态与处置履历'}} + title="告警详情" + description={selectedEvent ? `${selectedEvent.plate || selectedEvent.vin} · ${selectedEvent.ruleName}` : '证据、状态与处置履历'} + badge={selectedEvent ? statusLabels[selectedEvent.status] : undefined} + badgeColor={selectedEvent ? alertStatusColor(selectedEvent.status) : 'grey'} onCancel={closeDetail} > {mobileLayout && selectedID ? { const event = detail.data; if (event) action.mutate({ name, event }); }} onClose={closeDetail} sheet /> : 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 f1f40437..8fa381a9 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 @@ -267,8 +267,9 @@ test('renders only selectable Semi history cards on mobile', async () => { expect(action).toHaveAttribute('aria-pressed', 'true'); expect(action).toHaveAttribute('aria-expanded', 'true'); expect(await screen.findByRole('dialog', { name: '历史数据详情' })).toBeInTheDocument(); - expect(document.querySelector('.v2-history-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom'); + expect(document.querySelector('.v2-history-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-detail-sidesheet'); expect(document.querySelector('.v2-history-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(82dvh, 720px)' }); + expect(document.querySelector('.v2-history-detail-sidesheet .v2-workspace-config-title > .semi-tag')).toHaveTextContent('正常'); expect(await screen.findByText('MOBILEVIN-evidence')).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: '关闭历史数据详情' })); expect(action).toHaveAttribute('aria-pressed', 'false'); 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 b4d49b52..9dd3b85a 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/HistoryPage.tsx @@ -16,6 +16,7 @@ import { PlatformTime } from '../shared/PlatformTime'; import { ProtocolTag } from '../shared/ProtocolTag'; import { TablePagination } from '../shared/TablePagination'; import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; +import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; import { WorkspaceMetricRail } from '../shared/WorkspaceMetricRail'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; @@ -356,7 +357,6 @@ export default function HistoryPage() { setSelectedRowRef({ scope: dataScope, id: '' }); }, [dataScope]); const mobileFiltersOpen = mobileLayout && !filtersCollapsed; - useSideSheetA11y(mobileLayout && Boolean(selectedRow), '.v2-history-detail-sidesheet', 'v2-history-detail', '历史数据详情', '关闭历史数据详情'); useSideSheetA11y(exportAllowed && exportJobsOpen, '.v2-history-export-sidesheet', 'v2-history-export-jobs', '历史数据导出任务', '关闭历史数据导出任务'); useSideSheetA11y(mobileFiltersOpen, '.v2-history-filter-sidesheet', 'v2-history-filter-sheet', '历史查询范围', '关闭历史查询范围'); @@ -548,17 +548,20 @@ export default function HistoryPage() { {!mobileLayout && selectedRow ? : null} - 数据详情{selectedRow ? `${selectedRow.plate || '未绑定车牌'} · ${selectedRow.protocol}` : '来源、质量与原始证据'}} + title="数据详情" + description={selectedRow ? `${selectedRow.plate || '未绑定车牌'} · ${selectedRow.protocol}` : '来源、质量与原始证据'} + badge={selectedRow ? historyQualityLabel(selectedRow.quality) : undefined} + badgeColor={selectedRow && isHealthyHistoryQuality(selectedRow.quality) ? 'green' : selectedRow?.quality === 'error' || selectedRow?.quality === 'critical' ? 'red' : 'orange'} onCancel={closeSelectedRow} > {mobileLayout && selectedRow ? : null} - + import('./VehicleSearchWorkspace')); const VehicleActions = lazy(() => import('./VehicleActions')); @@ -101,7 +100,7 @@ function Archive({ detail, editable, onUpdated }: { detail: VehicleDetail; edita - @@ -110,7 +109,7 @@ function Archive({ detail, editable, onUpdated }: { detail: VehicleDetail; edita { key: '车辆品牌', value: fmt(profile?.brandName) }, { key: '车型 / 类型', value: [profile?.modelName, profile?.vehicleType].filter(Boolean).join(' / ') || '—' }, { key: '所属企业', value: fmt(profile?.companyName) }, - { key: '运营状态', value: {operationStatusLabels[profile?.operationStatus ?? 'unknown']} }, + { key: '运营状态', value: {vehicleOperationStatusLabels[profile?.operationStatus ?? 'unknown']} }, { key: '接入服务商', value: fmt(profile?.accessProvider) }, { key: '首次接入', value: fmt(profile?.firstAccessAt) }, { key: '累计运行', value: durationHours(profile?.runtimeSeconds) } 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 2a646748..32b74096 100644 --- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts @@ -22,6 +22,7 @@ const passwordInputSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/P const workspaceFilterPanelSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceFilterPanel.tsx'), 'utf8'); const workspaceMetricRailSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceMetricRail.tsx'), 'utf8'); const workspaceSideSheetSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceSideSheet.tsx'), 'utf8'); +const workspaceDetailSideSheetSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/WorkspaceDetailSideSheet.tsx'), 'utf8'); const segmentedTabsSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/SegmentedTabs.tsx'), 'utf8'); const metricActionSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/MetricActionButton.tsx'), 'utf8'); const tablePaginationSource = readFileSync(resolve(process.cwd(), 'src/v2/shared/TablePagination.tsx'), 'utf8'); @@ -123,16 +124,21 @@ describe('V2 production entry', () => { expect(corePageSources.MonitorPage).toContain('车辆'); - expect(sourceEvidenceSource).toContain("import { Button, Card, Empty, Input, SideSheet, Spin, Tag } from '@douyinfe/semi-ui'"); + expect(sourceEvidenceSource).toContain("import { Button, Card, Empty, Input, Spin, Tag } from '@douyinfe/semi-ui'"); expect(sourceEvidenceSource).toContain(' { expect(corePageSources.AlertsPage).toContain(' .semi-card-body > .v2-source-evidence-body')).not.toBeInTheDocument(); expect(await screen.findByText('北斗平台')).toBeInTheDocument(); - expect(document.querySelector('.v2-source-evidence-sidesheet')).toBeInTheDocument(); + expect(document.querySelector('.v2-source-evidence-sidesheet')).toHaveClass('v2-workspace-detail-sidesheet'); + expect(document.querySelector('.v2-source-evidence-sidesheet .v2-workspace-config-title > .semi-tag')).toHaveTextContent('3 个来源'); expect(document.querySelectorAll('.v2-source-evidence-sidesheet .v2-source-evidence-card')).toHaveLength(3); fireEvent.click(screen.getByRole('button', { name: '关闭来源证据' })); diff --git a/vehicle-data-platform/apps/web/src/v2/shared/VehicleSourceEvidencePanel.tsx b/vehicle-data-platform/apps/web/src/v2/shared/VehicleSourceEvidencePanel.tsx index a1e378c8..87ebd0aa 100644 --- a/vehicle-data-platform/apps/web/src/v2/shared/VehicleSourceEvidencePanel.tsx +++ b/vehicle-data-platform/apps/web/src/v2/shared/VehicleSourceEvidencePanel.tsx @@ -1,12 +1,12 @@ import { useQuery } from '@tanstack/react-query'; -import { Button, Card, Empty, Input, SideSheet, Spin, Tag } from '@douyinfe/semi-ui'; +import { Button, Card, Empty, Input, Spin, Tag } from '@douyinfe/semi-ui'; import { useMemo, useState } from 'react'; import { api } from '../../api/client'; import type { VehicleLocationSourceEvidence, VehicleMileageSourceEvidence } from '../../api/types'; import { protocolDisplayLabel, protocolSourceLabel } from '../domain/protocols'; import { useMobileLayout } from '../hooks/useMobileLayout'; -import { useSideSheetA11y } from '../hooks/useSideSheetA11y'; import { QUERY_MEMORY } from '../queryPolicy'; +import { WorkspaceDetailSideSheet } from './WorkspaceDetailSideSheet'; function today() { const now = new Date(); @@ -110,8 +110,6 @@ export function VehicleSourceEvidencePanel({ if (sourceCount <= 2) return '当前车辆来源较少,仅展示实际存在的证据'; return `已读取 ${query.data.locationSources.length} 个位置来源、${query.data.mileageSources.length} 个里程来源`; }, [query.data, sourceCount]); - useSideSheetA11y(mobileLayout && expanded, '.v2-source-evidence-sidesheet', 'v2-source-evidence-sheet', '位置与里程来源证据', '关闭来源证据'); - const evidenceBody =
@@ -140,17 +138,20 @@ export function VehicleSourceEvidencePanel({ {expanded && !mobileLayout ? evidenceBody : null} - {mobileLayout ? 位置与里程来源{description}
} - footer={null} + ariaLabel="位置与里程来源证据" + closeLabel="关闭来源证据" + title="位置与里程来源" + description={description} + badge={sourceCount ? `${sourceCount} 个来源` : '按需读取'} + badgeColor={sourceCount ? 'blue' : 'grey'} onCancel={() => setExpanded(false)} >
{evidenceBody}
- : null} + : null} ; } diff --git a/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceDetailSideSheet.test.tsx b/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceDetailSideSheet.test.tsx new file mode 100644 index 00000000..76580f99 --- /dev/null +++ b/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceDetailSideSheet.test.tsx @@ -0,0 +1,29 @@ +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import { afterEach, expect, test, vi } from 'vitest'; +import { WorkspaceDetailSideSheet } from './WorkspaceDetailSideSheet'; + +afterEach(cleanup); + +test('renders one accessible Semi detail sheet with shared identity and status', () => { + const onCancel = vi.fn(); + render( +

详情内容

+
); + + const dialog = screen.getByRole('dialog', { name: '车辆证据详情' }); + expect(dialog.closest('.semi-sidesheet')).toHaveClass('v2-workspace-detail-sidesheet', 'test-detail-sheet'); + expect(screen.getByText('粤A00001 · VIN-001')).toBeInTheDocument(); + expect(screen.getByText('状态正常').closest('.semi-tag')).toHaveClass('semi-tag-green-light'); + fireEvent.click(screen.getByRole('button', { name: '关闭证据详情' })); + expect(onCancel).toHaveBeenCalledTimes(1); +}); diff --git a/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceDetailSideSheet.tsx b/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceDetailSideSheet.tsx new file mode 100644 index 00000000..94c95bcf --- /dev/null +++ b/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceDetailSideSheet.tsx @@ -0,0 +1,55 @@ +import type { ReactNode } from 'react'; +import { + WorkspaceSideSheet, + type WorkspaceSideSheetBadgeColor +} from './WorkspaceSideSheet'; + +type WorkspaceDetailSideSheetProps = { + className?: string; + visible: boolean; + ariaLabel: string; + closeLabel?: string; + title: ReactNode; + description: ReactNode; + badge?: ReactNode; + badgeColor?: WorkspaceSideSheetBadgeColor; + placement?: 'left' | 'right' | 'top' | 'bottom'; + width?: string | number; + height?: string | number; + onCancel: () => void; + children: ReactNode; +}; + +export function WorkspaceDetailSideSheet({ + className, + visible, + ariaLabel, + closeLabel, + title, + description, + badge, + badgeColor, + placement, + width, + height, + onCancel, + children +}: WorkspaceDetailSideSheetProps) { + return + {children} + ; +} diff --git a/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceSideSheet.tsx b/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceSideSheet.tsx index 394a7bad..30165ff4 100644 --- a/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceSideSheet.tsx +++ b/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceSideSheet.tsx @@ -17,15 +17,18 @@ export type WorkspaceSideSheetSummaryItem = { tone?: 'neutral' | 'primary' | 'success' | 'warning' | 'danger'; }; -type WorkspaceSideSheetProps = { +export type WorkspaceSideSheetBadgeColor = 'blue' | 'green' | 'orange' | 'red' | 'grey'; + +export type WorkspaceSideSheetProps = { className?: string; + variant?: 'config' | 'detail'; visible: boolean; ariaLabel: string; closeLabel?: string; title: ReactNode; description: ReactNode; badge?: ReactNode; - badgeColor?: 'blue' | 'green' | 'orange' | 'red' | 'grey'; + badgeColor?: WorkspaceSideSheetBadgeColor; placement?: 'left' | 'right' | 'top' | 'bottom'; width?: string | number; height?: string | number; @@ -39,6 +42,7 @@ type WorkspaceSideSheetProps = { export function WorkspaceSideSheet({ className = '', + variant = 'config', visible, ariaLabel, closeLabel = `关闭${ariaLabel}`, @@ -57,7 +61,7 @@ export function WorkspaceSideSheet({ children }: WorkspaceSideSheetProps) { const sheetId = useId(); - const sheetClassName = ['v2-workspace-config-sidesheet', className].filter(Boolean).join(' '); + const sheetClassName = [`v2-workspace-${variant}-sidesheet`, className].filter(Boolean).join(' '); const hasFooter = Boolean(footerNote || secondaryActions.length || primaryAction); useLayoutEffect(() => { 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 000965a4..b258dbbb 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -20806,6 +20806,72 @@ box-shadow: 0 7px 18px rgba(18, 104, 243, .18); } +/* + * Shared Semi UI detail SideSheet: preserve each page's evidence content while + * keeping object identity, status, surface depth and scrolling consistent. + */ +.v2-workspace-detail-sidesheet .semi-sidesheet-inner { + overflow: hidden; + border-left: 1px solid #d9e3ef; + background: #f4f7fb; + box-shadow: -22px 0 64px rgba(24, 43, 68, .16); +} + +.v2-workspace-detail-sidesheet .semi-sidesheet-header { + min-height: 78px; + border-bottom: 1px solid #dfe7f0; + background: + radial-gradient(circle at 10% 0%, rgba(18, 104, 243, .075), transparent 36%), + linear-gradient(180deg, #fff 0%, #fbfcfe 100%); + padding: 13px 18px; +} + +.v2-workspace-detail-sidesheet .semi-sidesheet-title { + min-width: 0; +} + +.v2-workspace-detail-sidesheet .v2-workspace-config-title strong { + font-size: 18px; +} + +.v2-workspace-detail-sidesheet .semi-sidesheet-close { + width: 36px; + height: 36px; + border-radius: 9px; +} + +.v2-workspace-detail-sidesheet .semi-sidesheet-body { + min-height: 0; + overflow: hidden; + background: #f4f7fb; + padding: 0; +} + +.v2-workspace-detail-sidesheet .v2-workspace-config-content { + padding: 9px; + scrollbar-gutter: stable; +} + +.v2-workspace-detail-sidesheet .v2-workspace-config-content > .semi-card { + width: 100%; + min-height: 100%; + border-radius: 11px; +} + +.v2-alert-detail-sidesheet .v2-workspace-config-content, +.v2-source-evidence-sidesheet .v2-workspace-config-content { + padding: 0; +} + +.v2-history-detail-sidesheet .v2-workspace-config-content, +.v2-access-detail-sidesheet .v2-workspace-config-content { + padding: 8px; +} + +.v2-source-evidence-sidesheet .v2-source-evidence-sheet-body > .v2-source-evidence-body { + min-height: 100%; +} + .v2-history-column-sidesheet .v2-workspace-config-content { overflow: hidden; padding: 0; @@ -20872,6 +20938,49 @@ } @media (max-width: 680px) { + .v2-workspace-detail-sidesheet.semi-sidesheet-bottom .semi-sidesheet-inner { + width: 100% !important; + max-width: 100%; + border: 1px solid #d8e2ed; + border-bottom: 0; + border-radius: 18px 18px 0 0; + box-shadow: 0 -18px 48px rgba(23, 43, 68, .17); + padding-bottom: env(safe-area-inset-bottom); + } + + .v2-workspace-detail-sidesheet.semi-sidesheet-bottom .semi-sidesheet-header { + min-height: 70px; + padding: 10px 12px 10px 14px; + } + + .v2-workspace-detail-sidesheet .v2-workspace-config-title { + gap: 8px; + } + + .v2-workspace-detail-sidesheet .v2-workspace-config-title strong { + font-size: 16px; + } + + .v2-workspace-detail-sidesheet .v2-workspace-config-title small { + max-width: 250px; + font-size: 9px; + } + + .v2-workspace-detail-sidesheet .v2-workspace-config-title > .semi-tag { + min-height: 23px; + padding-inline: 8px; + font-size: 8px; + } + + .v2-workspace-detail-sidesheet .v2-workspace-config-content { + padding: 8px; + } + + .v2-alert-detail-sidesheet .v2-workspace-config-content, + .v2-source-evidence-sidesheet .v2-workspace-config-content { + padding: 0; + } + .v2-workspace-config-sidesheet.semi-sidesheet-bottom .semi-sidesheet-inner { border: 1px solid #d8e2ed; border-bottom: 0;