From 866dfdaf5f919812abacfb4e7d08f266c9e7116d Mon Sep 17 00:00:00 2001 From: lingniu Date: Sat, 18 Jul 2026 04:26:37 +0800 Subject: [PATCH] refine Semi UI alert and access workspaces --- .../apps/web/src/v2/pages/AccessPage.test.tsx | 7 +- .../apps/web/src/v2/pages/AccessPage.tsx | 11 +- .../apps/web/src/v2/pages/AlertsPage.test.tsx | 2 + .../apps/web/src/v2/pages/AlertsPage.tsx | 8 +- .../apps/web/src/v2/styles/workspace.css | 383 ++++++++++++++++-- 5 files changed, 375 insertions(+), 36 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 f4db634c..341a0d07 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 @@ -132,11 +132,16 @@ test('renders mobile access vehicles as selectable Semi cards', async () => { prepareBaseData(); mocks.accessVehicles.mockResolvedValue({ items: [accessRow('VIN001', '粤A00001')], total: 1, limit: 50, offset: 0 }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); - render(); + const view = render(); const action = await screen.findByRole('button', { name: '查看 粤A00001 接入详情' }); expect(action).toHaveClass('semi-button', 'v2-access-mobile-action'); expect(action.closest('.semi-card')).toHaveClass('v2-access-mobile-card'); + expect(view.container.querySelector('.v2-access-table-scroll-v3.is-mobile-scroll')).toHaveAttribute('tabindex', '0'); + expect(view.container.querySelector('.v2-access-table-scroll-v3.is-mobile-scroll')).toHaveAttribute('aria-label', '车辆接入列表,可上下滚动'); + expect(within(action).getByText('GB32960')).toBeInTheDocument(); + expect(within(action).getByText('JT808')).toBeInTheDocument(); + expect(within(action).getByText('YUTONG_MQTT')).toBeInTheDocument(); expect(action).toHaveAttribute('aria-pressed', 'false'); expect(action).toHaveAttribute('aria-expanded', 'false'); fireEvent.click(action); 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 3d288318..79c4efef 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AccessPage.tsx @@ -40,7 +40,7 @@ function compactTime(value: string) { return compactAccessTimeFormatter.format(parsed).replace(/\//g, '-'); } -function ProtocolState({ status, detailed = false }: { status?: AccessProtocolStatus; detailed?: boolean }) { +function ProtocolState({ status, detailed = false, protocolLabel }: { status?: AccessProtocolStatus; detailed?: boolean; protocolLabel?: string }) { const state = !status?.connected ? 'missing' : status.onlineState; const label = state === 'missing' ? '无来源' : state === 'online' ? '在线' : state === 'offline' ? '离线' : state === 'unknown' ? '未知' : '从未上报'; const color = state === 'online' ? 'green' : state === 'offline' || state === 'unknown' ? 'orange' : 'grey'; @@ -66,6 +66,7 @@ function ProtocolState({ status, detailed = false }: { status?: AccessProtocolSt ; } return
+ {protocolLabel ? {protocolLabel} : null} {label} {status?.connected ? compactTime(status.latestReceivedAt) : '—'} {status?.provider || (status?.connected ? '接入方未维护' : '未发现来源')} @@ -246,9 +247,13 @@ export default function AccessPage() { actionsClassName="v2-access-table-actions" actions={<>} /> -
+
{mobileLayout - ?
{rows.map((row) => )}
+ ?
{rows.map((row) => )}
: } {vehiclesQuery.isFetching ? : null} {!vehiclesQuery.isFetching && !rows.length ? : null} diff --git a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx index 503a363c..d13ac562 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.test.tsx @@ -188,6 +188,8 @@ test('renders only selectable Semi alert cards on mobile', async () => { const action = await screen.findByRole('button', { name: '查看 粤A移动01 粤A移动01速度告警 告警详情' }); expect(action).toHaveClass('semi-button', 'v2-alert-mobile-action'); expect(action.closest('.semi-card')).toHaveClass('v2-alert-mobile-card'); + expect(view.container.querySelector('.v2-alert-table-scroll.is-mobile-scroll')).toHaveAttribute('tabindex', '0'); + expect(view.container.querySelector('.v2-alert-table-scroll.is-mobile-scroll')).toHaveAttribute('aria-label', '告警事件列表,可上下滚动'); expect(view.container.querySelector('.v2-alert-event-table')).not.toBeInTheDocument(); expect(action).toHaveAttribute('aria-expanded', 'false'); fireEvent.click(action); 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 adce4c76..7339c010 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx @@ -223,7 +223,11 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e meta={`共 ${(events.data?.total ?? 0).toLocaleString('zh-CN')} 条`} actions={} /> -
+
{mobileLayout ?
{rows.map((event) => )}
: setSelection({ scope: eventScope, id })} />} @@ -410,5 +414,5 @@ export default function AlertsPage() { const setTab = (next: Tab) => { setTabState(next); const copy = new URLSearchParams(params); copy.set('tab', next); setParams(copy, { replace: true }); }; const setFilters = (next: Filters) => { setFilterState(next); const copy = new URLSearchParams(); if (tab !== 'events') copy.set('tab', tab); Object.entries(next).forEach(([key, value]) => { if (value) copy.set(key, value); }); setParams(copy, { replace: true }); }; const tabs = [{ key: 'events' as const, label: '告警事件', icon: }, ...(admin ? [{ key: 'rules' as const, label: '规则配置' }] : []), { key: 'notifications' as const, label: '站内通知', icon: , count: unread || undefined }]; - return
{operator ? '可处置事件' : '只读查看'}{unread ? `${unread.toLocaleString('zh-CN')} 条未读` : '通知已读'}
{activeTab !== 'notifications' && rules.isError ? rules.refetch()} /> : null}{activeTab === 'rules' && metrics.isError ? metrics.refetch()} /> : null}{activeTab === 'events' ? : activeTab === 'rules' ? : }
; + return
{operator ? '可处置事件' : '只读查看'}{unread ? `${unread.toLocaleString('zh-CN')} 条未读` : '通知已读'}
{activeTab !== 'notifications' && rules.isError ? rules.refetch()} /> : null}{activeTab === 'rules' && metrics.isError ? metrics.refetch()} /> : null}{activeTab === 'events' ? : activeTab === 'rules' ? : }
; } 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 0f533ee4..0d0abd89 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -6409,6 +6409,13 @@ padding: 8px 7px 16px; } + .v2-alert-page.is-events { + height: 100%; + min-height: 0; + overflow: hidden; + padding-bottom: 8px; + } + .v2-alert-navigation { position: sticky; z-index: 8; @@ -6460,7 +6467,7 @@ } .v2-alert-context-card.semi-card { - min-height: 0; + min-height: 94px; } .v2-alert-context-card > .semi-card-body { @@ -6468,78 +6475,208 @@ } .v2-alert-context-card .v2-alert-kpis { - min-height: 88px; + height: 58px; + min-height: 58px; grid-template-columns: repeat(4, minmax(0, 1fr)); } .v2-alert-context-card .v2-alert-kpis .v2-metric-action, .v2-alert-context-card .v2-alert-kpis .v2-metric-action-content { - min-height: 44px; + min-height: 58px; + } + + .v2-alert-context-card .v2-alert-kpis .v2-metric-action { + padding: 7px 5px; + } + + .v2-alert-context-card .v2-alert-kpis .v2-metric-action.is-notice { + grid-column: auto; + } + + .v2-alert-context-card .v2-alert-kpis .v2-metric-action-content { + display: grid; + align-content: center; + gap: 4px; + } + + .v2-alert-context-card .v2-alert-kpis small { + font-size: 8px; + } + + .v2-alert-context-card .v2-alert-kpis strong { + margin-top: 0; + font-size: 16px; } .v2-alert-context-card .v2-alert-secondary-states { + min-height: 34px; border-top: 1px solid #e4e9f0; border-left: 0; - padding: 4px; + padding: 3px 4px; } .v2-alert-context-card .v2-alert-secondary-states > .semi-button { - min-height: 40px; - padding: 3px 5px; + min-height: 30px; + padding: 2px 5px; + } + + .v2-alert-context-card .v2-alert-secondary-states span { + font-size: 8px; + } + + .v2-alert-context-card .v2-alert-secondary-states b { + font-size: 12px; + } + + .v2-alert-page.is-events .v2-alert-workspace { + min-height: 0; + flex: 1 1 0; + grid-template-columns: minmax(0, 1fr); + overflow: hidden; } .v2-alert-table-card { - min-height: 460px; + height: 100%; + min-height: 0; } .v2-alert-table-card > .semi-card-body > .v2-workspace-panel-header { - min-height: 58px; - align-items: flex-start; - padding: 8px; + min-height: 50px; + align-items: center; + padding: 6px 8px; } .v2-alert-table-card .v2-workspace-panel-copy > .semi-typography { display: none; } + .v2-alert-table-scroll.is-mobile-scroll { + min-height: 0; + flex: 1 1 0; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; + scrollbar-width: auto; + touch-action: pan-y; + -webkit-overflow-scrolling: touch; + } + + .v2-alert-table-scroll.is-mobile-scroll:focus-visible { + outline: 2px solid rgba(18, 104, 243, .34); + outline-offset: -2px; + } + + .v2-alert-table-scroll.is-mobile-scroll::-webkit-scrollbar { + width: 11px; + } + + .v2-alert-table-scroll.is-mobile-scroll::-webkit-scrollbar-thumb { + border: 3px solid transparent; + border-radius: 999px; + background: #9cacc0; + background-clip: padding-box; + } + .v2-alert-mobile-list { - gap: 7px; - padding: 7px; + gap: 6px; + padding: 6px; + } + + .v2-alert-mobile-list > .v2-alert-mobile-card.semi-card { + border-radius: 9px; + contain-intrinsic-size: auto 110px; } .v2-alert-mobile-card-content { - padding: 11px; + padding: 8px 9px; + } + + .v2-alert-mobile-card-content header > strong { + font-size: 13px; + } + + .v2-alert-mobile-card-content header > span { + gap: 3px; + } + + .v2-alert-mobile-card-content .v2-alert-severity.semi-tag, + .v2-alert-mobile-card-content .v2-alert-status.semi-tag { + height: 20px; + min-height: 20px; + border-radius: 5px; + padding-inline: 5px; + font-size: 8px; } .v2-alert-mobile-card-content p { - margin: 6px 0; + margin: 4px 0 5px; + overflow: hidden; + color: #405168; + font-size: 11px; + line-height: 14px; + text-overflow: ellipsis; + white-space: nowrap; } .v2-alert-mobile-card-content dl { - gap: 6px 10px; - padding-top: 7px; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0; + padding-top: 6px; } .v2-alert-mobile-card-content dl > div { - display: grid; + display: block; min-width: 0; - min-height: 24px; - grid-template-columns: auto minmax(0, 1fr); - align-items: center; - gap: 6px; + min-height: 28px; + padding: 0 5px; + } + + .v2-alert-mobile-card-content dl > div:first-child { + padding-left: 0; + } + + .v2-alert-mobile-card-content dl > div + div { + border-left: 1px solid #edf1f5; + } + + .v2-alert-mobile-card-content dt { + overflow: hidden; + font-size: 8px; + text-overflow: ellipsis; + white-space: nowrap; } .v2-alert-mobile-card-content dd { min-width: 0; - margin: 0; + margin: 3px 0 0; overflow: hidden; - text-align: right; + font-size: 9px; + line-height: 12px; + text-align: left; text-overflow: ellipsis; white-space: nowrap; } .v2-alert-mobile-card-content footer { - margin-top: 6px; + display: none; + } + + .v2-alert-table-card > .semi-card-body > footer:has(.v2-table-pagination-controls) { + min-height: 50px; + flex-direction: row; + align-items: center; + padding: 5px 6px; + } + + .v2-alert-table-card > .semi-card-body > footer .v2-table-pagination-info { + display: none; + } + + .v2-alert-table-card > .semi-card-body > footer .v2-table-pagination-button.semi-button, + .v2-alert-table-card > .semi-card-body > footer .v2-table-pagination-current, + .v2-alert-table-card > .semi-card-body > footer .v2-table-pagination-size.semi-select { + height: 34px; } .v2-alert-rules { @@ -7456,11 +7593,11 @@ @media (max-width: 680px) { .v2-access-page-v3 { - height: auto; - min-height: 100%; + height: 100%; + min-height: 0; gap: 7px; - overflow: auto; - padding: 7px 7px 14px; + overflow: hidden; + padding: 7px 7px 8px; } .v2-access-filter-card-v3 > .semi-card-body > .v2-workspace-filter-heading { @@ -7482,9 +7619,195 @@ display: block; } + .v2-access-kpis-card-v3.semi-card, + .v2-access-kpis-v3 { + min-height: 62px; + } + + .v2-access-kpis-v3 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + .v2-access-kpis-v3 .v2-metric-action { + min-height: 62px; + padding: 7px 6px; + } + + .v2-access-kpis-v3 .v2-metric-action-content { + display: grid; + align-content: center; + gap: 4px; + } + + .v2-access-kpis-v3 small { + overflow: hidden; + font-size: 8px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .v2-access-kpis-v3 strong { + margin-left: 0; + font-size: 16px; + } + + .v2-access-kpis-v3 em { + display: none; + } + .v2-access-workspace-v3 { - min-height: 540px; - flex: none; + min-height: 0; + flex: 1 1 0; + overflow: hidden; + } + + .v2-access-table-v3 > .semi-card-body > .v2-workspace-panel-header { + min-height: 50px; + align-items: center; + padding: 6px 8px; + } + + .v2-access-table-v3 .v2-workspace-panel-copy > .semi-typography { + display: none; + } + + .v2-access-table-scroll-v3.is-mobile-scroll { + min-height: 0; + flex: 1 1 0; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; + scrollbar-width: auto; + touch-action: pan-y; + -webkit-overflow-scrolling: touch; + } + + .v2-access-table-scroll-v3.is-mobile-scroll:focus-visible { + outline: 2px solid rgba(18, 104, 243, .34); + outline-offset: -2px; + } + + .v2-access-table-scroll-v3.is-mobile-scroll::-webkit-scrollbar { + width: 11px; + } + + .v2-access-table-scroll-v3.is-mobile-scroll::-webkit-scrollbar-thumb { + border: 3px solid transparent; + border-radius: 999px; + background: #9cacc0; + background-clip: padding-box; + } + + .v2-access-mobile-list { + gap: 6px; + padding: 6px; + } + + .v2-access-mobile-card.semi-card { + border-radius: 9px; + contain-intrinsic-size: auto 126px; + } + + .v2-access-mobile-card-content { + gap: 5px; + padding: 8px 9px; + } + + .v2-access-mobile-card-content > header { + align-items: center; + } + + .v2-access-mobile-card-content > header strong { + font-size: 13px; + } + + .v2-access-mobile-card-content > header small { + font-size: 8px; + } + + .v2-access-mobile-card-content > header .v2-access-connection { + gap: 0; + } + + .v2-access-mobile-card-content > header .v2-access-connection > span { + display: none; + } + + .v2-access-mobile-card-content > header .v2-access-connection-tag.semi-tag { + min-height: 20px; + border-radius: 5px; + padding-inline: 5px; + font-size: 8px; + } + + .v2-access-mobile-card-content > p { + overflow: hidden; + font-size: 9px; + line-height: 12px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .v2-access-mobile-protocols { + gap: 4px; + padding-top: 5px; + } + + .v2-access-mobile-list .v2-access-protocol-cell { + grid-template-columns: minmax(0, 1fr) auto; + gap: 2px 4px; + border-radius: 6px; + padding: 5px 6px; + } + + .v2-access-mobile-list .v2-access-protocol-label { + min-width: 0; + overflow: hidden; + color: #50627a; + font-size: 8px; + line-height: 11px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .v2-access-mobile-list .v2-access-protocol-cell > span { + justify-self: end; + gap: 3px; + font-size: 8px; + line-height: 11px; + } + + .v2-access-mobile-list .v2-access-protocol-cell > span i { + width: 5px; + height: 5px; + } + + .v2-access-mobile-list .v2-access-protocol-cell > strong { + grid-column: 1 / -1; + font-size: 8px; + line-height: 11px; + } + + .v2-access-mobile-card-content > footer { + display: none; + } + + .v2-access-table-v3 > .semi-card-body > footer:has(.v2-table-pagination-controls) { + min-height: 50px; + flex-direction: row; + align-items: center; + padding: 5px 6px; + } + + .v2-access-table-v3 > .semi-card-body > footer .v2-table-pagination-info { + display: none; + } + + .v2-access-table-v3 > .semi-card-body > footer .v2-table-pagination-button.semi-button, + .v2-access-table-v3 > .semi-card-body > footer .v2-table-pagination-current, + .v2-access-table-v3 > .semi-card-body > footer .v2-table-pagination-size.semi-select { + height: 34px; } .v2-ops-page {