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 544302df..f4db634c 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 @@ -50,6 +50,8 @@ test('removes old access rows immediately when the vehicle filter scope changes' const view = render(); expect(await screen.findByText('旧接入车牌')).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: '真实来源与接入健康', level: 5 })).toBeInTheDocument(); + expect(screen.getByLabelText('接入管理操作')).toHaveClass('v2-workspace-command-bar', 'v2-access-command-bar'); for (const className of ['v2-access-filter-card-v3', 'v2-access-kpis-card-v3', 'v2-access-table-v3']) { expect(view.container.querySelector(`.${className}.semi-card`)).toBeInTheDocument(); } 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 f5febb25..3d288318 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx @@ -8,8 +8,8 @@ import type { AccessProtocolStatus, AccessQuery, AccessSummary, AccessThresholdC import { accessRowsToCSV, formatAccessTime, formatSeconds, updateProtocolThreshold } from '../domain/access'; import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState'; import { MetricActionButton } from '../shared/MetricActionButton'; -import { PageHeader } from '../shared/PageHeader'; import { TablePagination } from '../shared/TablePagination'; +import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { detailTriggerRow } from '../shared/detailTriggerRow'; @@ -201,8 +201,10 @@ export default function AccessPage() { const refresh = () => Promise.all([summaryQuery.refetch(), vehiclesQuery.refetch(), ...(editable ? [unresolvedQuery.refetch(), thresholdQuery.refetch()] : [])]); return
- 数据时间 {summary?.asOf ? formatAccessTime(summary.asOf) : '—'}} 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 680beb62..5ef067d3 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 @@ -59,6 +59,8 @@ test('renders reconciliation queue, loads evidence on demand and records review render(); expect(await screen.findByText('数据差异中心')).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: '运行与数据质量', level: 5 })).toBeInTheDocument(); + expect(screen.getByLabelText('运维质量操作')).toHaveClass('v2-workspace-command-bar', 'v2-ops-command-bar'); expect(screen.getByRole('tab', { name: '差异处置', selected: true })).toHaveClass('semi-button'); expect(screen.getByRole('tabpanel', { name: '差异处置' })).toBeInTheDocument(); expect(screen.queryByText('先选择一辆车')).not.toBeInTheDocument(); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx index 2d0288b4..cb90c6b9 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/OperationsPage.tsx @@ -5,9 +5,9 @@ import { FormEvent, useDeferredValue, useEffect, useState } from 'react'; import { api } from '../../api/client'; import type { VehicleCoverageRow, VehicleLocationSourceEvidence, VehicleSourceDiagnostic } from '../../api/types'; import { InlineError } from '../shared/AsyncState'; -import { PageHeader } from '../shared/PageHeader'; import { SegmentedTabs } from '../shared/SegmentedTabs'; import { VehicleCandidateList } from '../shared/VehicleCandidateList'; +import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { LIVE_QUERY_POLICY, QUERY_MEMORY } from '../queryPolicy'; @@ -340,8 +340,10 @@ export default function OperationsPage() { const healthIssueCount = linkIssueCount + runtimeIssueCount + capacityIssueCount + Number((data?.kafkaLag ?? 0) > 0); const overallStatus = !data ? 'unknown' : healthIssueCount > 0 ? 'warning' : 'ok'; return
- -
-
- 客户访问治理 - 菜单与车辆按最小权限开放,变更最多 30 秒生效 -
-
- {customers.length} 个客户账号 - 本地账号 · 可扩展外部身份源 - -
-
+ 本地账号 · 可扩展外部身份源} + actions={} + /> } />} />); - expect(screen.getByRole('heading', { name: '车辆查询', level: 2 })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: '车辆快速定位', level: 5 })).toBeInTheDocument(); + expect(screen.getByLabelText('查车操作')).toHaveClass('v2-workspace-command-bar', 'v2-vehicle-command-bar'); expect(screen.getByRole('heading', { name: '车辆定位', level: 5 }).closest('.semi-card')).toHaveClass('v2-vehicle-query-panel', 'v2-workspace-filter-panel'); const input = screen.getByRole('textbox', { name: '搜索车辆' }); expect(input).toHaveAttribute('aria-expanded', 'false'); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx index 406935da..8ae54fef 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/VehiclePage.tsx @@ -17,11 +17,11 @@ import { isValidAMapCoordinate } from '../../integrations/amap'; import { FleetMap } from '../map/FleetMap'; import { InlineError, PageLoading } from '../shared/AsyncState'; import { MonitorReturnBar } from '../shared/MonitorReturnBar'; -import { PageHeader } from '../shared/PageHeader'; import { SegmentedTabs } from '../shared/SegmentedTabs'; import { VehicleCandidateList } from '../shared/VehicleCandidateList'; import { mergeVehicleCandidates } from '../shared/vehicleCandidates'; import { VehicleSourceEvidencePanel } from '../shared/VehicleSourceEvidencePanel'; +import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { monitorReturnFromParams, withMonitorReturn } from '../routing/monitorContext'; @@ -105,9 +105,11 @@ function VehicleSearch() { }; const vehicleScope = candidates.data ? `${candidates.data.total.toLocaleString('zh-CN')} 辆授权` : '授权范围'; return
- setSyncOpen((value) => !value)}>{syncOpen ? '收起主档同步' : '批量同步主档'} : null} @@ -115,7 +117,7 @@ function VehicleSearch() { { expect(source).toContain("from '@douyinfe/semi-ui'"); expect(source).toContain('['color']; - meta?: ReactNode; - actions?: ReactNode; -}) { - return ( -
-
-
- {title} - {status ? {status} : null} -
- {description} -
- {meta || actions ?
{meta}{actions}
: null} -
- ); -} diff --git a/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceCommandBar.test.tsx b/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceCommandBar.test.tsx new file mode 100644 index 00000000..a8b27f7a --- /dev/null +++ b/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceCommandBar.test.tsx @@ -0,0 +1,22 @@ +import { render, screen } from '@testing-library/react'; +import { Button, Typography } from '@douyinfe/semi-ui'; +import { expect, test } from 'vitest'; +import { WorkspaceCommandBar } from './WorkspaceCommandBar'; + +test('renders a compact Semi workspace command hierarchy', () => { + const view = render(刚刚更新} + actions={} + />); + + expect(screen.getByRole('heading', { name: '测试工作区', level: 5 })).toBeInTheDocument(); + expect(screen.getByLabelText('测试工作区操作')).toHaveClass('v2-workspace-command-bar', 'custom-command'); + expect(screen.getByText('12 条').closest('.semi-tag')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '执行' })).toHaveClass('semi-button'); + expect(view.container.querySelector('.v2-workspace-command-copy')).toHaveTextContent('说明当前任务和数据范围'); +}); diff --git a/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceCommandBar.tsx b/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceCommandBar.tsx new file mode 100644 index 00000000..43cc6210 --- /dev/null +++ b/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceCommandBar.tsx @@ -0,0 +1,40 @@ +import { Tag, Typography } from '@douyinfe/semi-ui'; +import type { ReactNode } from 'react'; + +const { Text, Title } = Typography; + +export function WorkspaceCommandBar({ + title, + description, + status, + statusColor = 'blue', + meta, + actions, + className = '', + ariaLabel +}: { + title: ReactNode; + description: ReactNode; + status?: string; + statusColor?: React.ComponentProps['color']; + meta?: ReactNode; + actions?: ReactNode; + className?: string; + ariaLabel?: string; +}) { + const rootClassName = ['v2-workspace-command-bar', className].filter(Boolean).join(' '); + + return ( +
+
+ {title} + {description} +
+ {status || meta || actions ?
+ {status ? {status} : null} + {meta ? {meta} : null} + {actions} +
: null} +
+ ); +} 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 f0655139..25734dc3 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -1877,6 +1877,7 @@ } } + @media (max-width: 680px) { .v2-assigned-vins { max-height: min(360px, 42dvh); @@ -6521,7 +6522,7 @@ padding: 12px 14px 14px; } -.v2-user-command-bar { +.v2-workspace-command-bar { display: flex; width: 100%; max-width: 1480px; @@ -6538,35 +6539,37 @@ box-shadow: 0 6px 20px rgba(31, 50, 76, .035); } -.v2-user-command-copy { +.v2-workspace-command-copy { display: grid; min-width: 0; gap: 2px; } -.v2-user-command-copy strong { +.v2-workspace-command-copy .semi-typography { color: #263b53; font-size: 14px; font-weight: 700; letter-spacing: -.012em; } -.v2-user-command-copy span { +.v2-workspace-command-copy > .semi-typography:last-child { overflow: hidden; color: #7d8b9d; font-size: 10px; + font-weight: 400; + letter-spacing: 0; text-overflow: ellipsis; white-space: nowrap; } -.v2-user-command-meta { +.v2-workspace-command-actions { display: flex; min-width: 0; align-items: center; gap: 10px; } -.v2-user-command-meta > .semi-tag { +.v2-workspace-command-actions > .semi-tag { min-height: 24px; flex: 0 0 auto; border-radius: 999px; @@ -6575,7 +6578,13 @@ font-weight: 650; } -.v2-user-command-meta > .semi-typography { +.v2-workspace-command-meta { + display: flex; + min-width: 0; + align-items: center; +} + +.v2-workspace-command-meta > .semi-typography { overflow: hidden; max-width: 220px; font-size: 10px; @@ -6583,7 +6592,7 @@ white-space: nowrap; } -.v2-user-command-meta > .semi-button { +.v2-workspace-command-actions > .semi-button { min-height: 36px; flex: 0 0 auto; border-radius: 8px; @@ -6903,7 +6912,7 @@ } @media (max-width: 980px) and (min-width: 681px) { - .v2-user-command-meta > .semi-typography { + .v2-workspace-command-meta > .semi-typography { display: none; } @@ -6929,35 +6938,50 @@ padding: 7px 7px 10px; } - .v2-user-command-bar { + .v2-workspace-command-bar { min-height: 48px; gap: 8px; border-radius: 9px; padding: 5px 6px 5px 10px; } - .v2-user-command-copy strong { + .v2-workspace-command-copy > .semi-typography:first-child { font-size: 12px; } - .v2-user-command-copy span, - .v2-user-command-meta > .semi-tag, - .v2-user-command-meta > .semi-typography { + .v2-workspace-command-copy > .semi-typography:last-child, + .v2-workspace-command-actions > .semi-tag, + .v2-workspace-command-meta { display: none; } - .v2-user-command-meta > .semi-button { + .v2-workspace-command-actions { + gap: 5px; + } + + .v2-workspace-command-actions > .semi-button { + min-height: 38px; + padding-inline: 9px; + font-size: 10px; + } + + .v2-user-command-bar .v2-workspace-command-actions > .semi-button, + .v2-access-command-bar .v2-workspace-command-actions > .semi-button, + .v2-ops-command-bar .v2-workspace-command-actions > .semi-button { width: 40px; min-width: 40px; - min-height: 40px; padding: 0; } - .v2-user-command-meta > .semi-button .semi-button-content { + .v2-user-command-bar .v2-workspace-command-actions > .semi-button .semi-button-content, + .v2-access-command-bar .v2-workspace-command-actions > .semi-button .semi-button-content, + .v2-ops-command-bar .v2-workspace-command-actions > .semi-button .semi-button-content { font-size: 0; } - .v2-user-command-meta > .semi-button .semi-icon { + .v2-user-command-bar .v2-workspace-command-actions > .semi-button .semi-icon, + .v2-access-command-bar .v2-workspace-command-actions > .semi-button .semi-icon, + .v2-ops-command-bar .v2-workspace-command-actions > .semi-button .semi-icon { margin-right: 0; font-size: 16px; } @@ -7158,3 +7182,233 @@ grid-template-columns: minmax(0, 1fr); } } + +/* Final cascade layer for the shared command hierarchy. */ +.v2-workspace-command-bar { + box-sizing: border-box; + height: 52px; + min-height: 52px; + padding-block: 5px; +} + +.v2-workspace-command-copy { + gap: 0; +} + +.v2-workspace-command-copy > .semi-typography:first-child { + margin: 0; + line-height: 18px; +} + +.v2-workspace-command-copy > .semi-typography:last-child { + margin: 0; + line-height: 14px; +} + +.v2-vehicle-query-panel > .semi-card-body > .v2-workspace-filter-heading { + display: none; +} + +.v2-vehicle-query-panel .v2-vehicle-search-form { + display: flex; + height: 54px; + min-height: 54px; + align-items: center; + gap: 8px; + border-top: 0; + padding: 6px 8px; +} + +.v2-vehicle-query-panel .v2-vehicle-search-form > .v2-vehicle-search-picker { + width: auto; + min-height: 40px; + flex: 1 1 0; +} + +.v2-vehicle-query-panel .v2-vehicle-search-form > .semi-button { + height: 40px; + min-height: 40px; + flex: 0 0 auto; +} + +.v2-ops-tabs { + box-sizing: border-box; + height: 44px; + min-height: 44px; + flex: 0 0 44px; + overflow: hidden; + padding: 3px; +} + +.v2-ops-tabs > .semi-button { + height: 36px; + min-height: 36px; +} + +@media (max-width: 680px) { + .v2-workspace-command-bar { + height: 48px; + min-height: 48px; + } + + .v2-vehicle-query-panel > .semi-card-body > .v2-workspace-filter-heading { + display: flex; + } + + .v2-vehicle-query-panel .v2-vehicle-search-form { + height: auto; + min-height: 0; + padding: 8px; + } +} + +.v2-vehicle-search-page > .v2-workspace-command-bar { + width: min(1280px, 100%); +} + +.v2-access-page-v3 { + width: 100%; + height: 100%; + min-height: 0; + gap: 8px; + overflow: hidden; + padding: 12px 14px 14px; +} + +.v2-access-page-v3 > .v2-workspace-command-bar, +.v2-ops-command-bar { + max-width: none; +} + +.v2-access-filter-card-v3 > .semi-card-body { + display: block; +} + +.v2-access-filter-card-v3 > .semi-card-body > .v2-workspace-filter-heading { + display: none; +} + +.v2-access-filter-v3 { + min-height: 54px; + grid-template-columns: minmax(220px, 1.35fr) repeat(3, minmax(128px, .7fr)) auto auto; + align-items: center; + gap: 8px; + padding: 7px 9px; +} + +.v2-access-filter-v3 > label { + display: contents; +} + +.v2-access-filter-v3 > label > span { + display: none; +} + +.v2-access-filter-v3 label > :is(.semi-input-wrapper, .semi-select), +.v2-access-filter-v3 > .semi-button { + width: 100%; + height: 38px; + min-height: 38px; + align-self: center; + border-radius: 8px; +} + +.v2-access-kpis-card-v3.semi-card, +.v2-access-kpis-v3 { + min-height: 60px; +} + +.v2-access-kpis-v3 .v2-metric-action { + min-height: 58px; + padding: 8px 14px; +} + +.v2-access-kpis-v3 strong { + font-size: 20px; +} + +.v2-access-workspace-v3 { + min-height: 0; + flex: 1 1 0; +} + +.v2-access-table-v3.semi-card, +.v2-access-table-v3 > .semi-card-body { + height: 100%; + min-height: 0; +} + +.v2-ops-page { + width: 100%; + height: 100%; + min-height: 0; + gap: 8px; + overflow: hidden; + padding: 12px 14px 14px; +} + +.v2-ops-workspace { + min-height: 0; + flex: 1 1 0; + overflow: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; +} + +@media (max-width: 1120px) and (min-width: 681px) { + .v2-access-filter-v3 { + grid-template-columns: minmax(200px, 1fr) repeat(3, minmax(110px, .65fr)) auto auto; + } + + .v2-access-command-bar .v2-workspace-command-meta { + display: none; + } +} + +@media (max-width: 680px) { + .v2-access-page-v3 { + height: auto; + min-height: 100%; + gap: 7px; + overflow: auto; + padding: 7px 7px 14px; + } + + .v2-access-filter-card-v3 > .semi-card-body > .v2-workspace-filter-heading { + display: flex; + } + + .v2-access-filter-v3 { + min-height: 0; + grid-template-columns: minmax(0, 1fr); + gap: 8px; + padding: 8px; + } + + .v2-access-filter-v3 > label { + display: grid; + } + + .v2-access-filter-v3 > label > span { + display: block; + } + + .v2-access-workspace-v3 { + min-height: 540px; + flex: none; + } + + .v2-ops-page { + height: auto; + min-height: 100%; + gap: 7px; + overflow: auto; + padding: 7px 7px 16px; + } + + .v2-ops-workspace { + min-height: 0; + flex: none; + overflow: visible; + } +}