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 d60868aa..878d59c1 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 @@ -141,7 +141,7 @@ test('clears old alert rows and inspector evidence when the event scope changes' const view = render(); expect((await screen.findAllByText('旧告警车牌')).length).toBeGreaterThan(0); - for (const className of ['v2-alert-filter-card', 'v2-alert-kpis-card', 'v2-alert-context-card', 'v2-alert-table-card']) { + for (const className of ['v2-alert-filter-card', 'v2-alert-metric-rail', 'v2-alert-table-card']) { expect(view.container.querySelector(`.${className}.semi-card`)).toBeInTheDocument(); } expect(view.container.querySelector('.v2-alert-navigation')).toBeInTheDocument(); @@ -152,9 +152,9 @@ test('clears old alert rows and inspector evidence when the event scope changes' expect(screen.queryByRole('heading', { level: 2, name: '告警中心' })).not.toBeInTheDocument(); expect(view.container.querySelector('.v2-alert-inspector')).not.toBeInTheDocument(); expect(view.container.querySelector('.v2-alert-workspace')).not.toHaveClass('is-inspector-open'); - expect(view.container.querySelectorAll('.v2-alert-kpis .v2-metric-action')).toHaveLength(4); - expect(view.container.querySelectorAll('.v2-alert-secondary-states .semi-button')).toHaveLength(3); - expect(screen.getByRole('button', { name: '查看未读通知,共 0 条' })).toHaveClass('is-notice'); + expect(view.container.querySelectorAll('.v2-alert-metric-rail .v2-workspace-metric-action.semi-button')).toHaveLength(4); + expect(view.container.querySelectorAll('.v2-alert-metric-context-actions .semi-button')).toHaveLength(3); + expect(screen.getByRole('button', { name: '查看未读通知,共 0 条' }).closest('[role="listitem"]')).toHaveClass('is-warning', 'is-secondary'); expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).toBeInTheDocument(); expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).not.toHaveClass('is-compact'); expect(screen.getByRole('columnheader', { name: '触发 / 阈值' })).toBeInTheDocument(); @@ -254,11 +254,11 @@ test('renders only selectable Semi alert cards on mobile', async () => { 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(); - const mobileMetrics = Array.from(view.container.querySelectorAll('.v2-alert-kpis .v2-metric-action')); - expect(mobileMetrics).toHaveLength(3); - expect(mobileMetrics.map((metric) => metric.textContent)).toEqual(['待处置1', '处理中0', '全部事件1']); + const mobileMetrics = Array.from(view.container.querySelectorAll('.v2-alert-metric-rail .v2-workspace-metric-action')); + expect(mobileMetrics).toHaveLength(2); + expect(mobileMetrics.map((metric) => metric.textContent)).toEqual(['待处置1需要核验', '全部事件1当前范围']); expect(screen.getByRole('button', { name: '筛选待处置,共 1 条' })).toHaveAttribute('aria-pressed', 'false'); - expect(view.container.querySelector('.v2-alert-secondary-states')).not.toBeInTheDocument(); + expect(view.container.querySelector('.v2-alert-metric-context-actions')).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /查看未读通知/ })).not.toBeInTheDocument(); expect(screen.getByRole('tab', { name: /站内通知/ })).toBeInTheDocument(); expect(action).toHaveAttribute('aria-expanded', 'false'); 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 5e334e62..08df83dc 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx @@ -7,7 +7,6 @@ import { api } from '../../api/client'; import type { AlertEvent, AlertQuery, AlertRule, AlertRuleInput, AlertStatus, MetricDefinition, Page } from '../../api/types'; import { actionLabels, alertDeltaText, alertValue, canAct, formatAlertTime, operatorLabels, ruleCondition, severityLabels, statusLabels, thresholdText } from '../domain/alert'; import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState'; -import { MetricActionButton } from '../shared/MetricActionButton'; import { MobileFilterToggle } from '../shared/MobileFilterToggle'; import { MobileFilterSheet, MobileFilterSheetSection } from '../shared/MobileFilterSheet'; import { ProtocolTag } from '../shared/ProtocolTag'; @@ -17,6 +16,7 @@ import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; +import { WorkspaceMetricRail, type WorkspaceQueueMetricRailItem } from '../shared/WorkspaceMetricRail'; import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet'; import { detailTriggerRow } from '../shared/detailTriggerRow'; import { usePlatformSession } from '../auth/AuthGate'; @@ -229,17 +229,33 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e ; - const primaryMetrics = mobileLayout + const metricItem = ( + label: string, + value: number, + status: string, + options: Pick + ): WorkspaceQueueMetricRailItem => { + const formattedValue = value.toLocaleString('zh-CN'); + const notice = status === 'notice'; + return { + label, + value: formattedValue, + ...options, + active: !notice && filters.status === status, + ariaLabel: notice ? `查看未读通知,共 ${formattedValue} 条` : `筛选${label},共 ${formattedValue} 条`, + onClick: () => notice ? onTab('notifications') : quickStatus(status) + }; + }; + const primaryMetrics: WorkspaceQueueMetricRailItem[] = mobileLayout ? [ - { label: '待处置', value: sums?.unprocessed, tone: 'unprocessed', status: 'unprocessed' }, - { label: '处理中', value: sums?.processing, tone: 'processing', status: 'processing' }, - { label: '全部事件', value: allEventCount, tone: 'active', status: '' } + metricItem('待处置', Number(sums?.unprocessed ?? 0), 'unprocessed', { note: '需要核验', tone: 'danger', emphasis: 'primary' }), + metricItem('全部事件', allEventCount, '', { note: '当前范围', tone: 'neutral', emphasis: 'primary' }) ] : [ - { label: '全部事件', value: allEventCount, tone: 'active', status: '' }, - { label: '未处理', value: sums?.unprocessed, tone: 'unprocessed', status: 'unprocessed' }, - { label: '处理中', value: sums?.processing, tone: 'processing', status: 'processing' }, - { label: '未读通知', value: unread, tone: 'notice', status: 'notice' } + metricItem('未处理', Number(sums?.unprocessed ?? 0), 'unprocessed', { note: '需要核验与处置', tone: 'danger', emphasis: 'primary' }), + metricItem('全部事件', allEventCount, '', { note: '当前查询范围', tone: 'neutral', emphasis: 'primary' }), + metricItem('处理中', Number(sums?.processing ?? 0), 'processing', { note: '处置进行中', tone: 'primary', emphasis: 'secondary' }), + metricItem('未读通知', unread, 'notice', { note: '进入通知队列', tone: 'warning', emphasis: 'secondary' }) ]; return <> {mobileLayout ?
@@ -292,19 +308,19 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e } {summary.isError ? summary.refetch()} /> : null} - -
{primaryMetrics.map((item) => { - const value = Number(item.value ?? 0).toLocaleString('zh-CN'); - return item.status === 'notice' ? onTab('notifications') : quickStatus(item.status)} />; - })}
- {!mobileLayout ? : null} -
+ ].map((item) => ) + } : undefined} + /> {events.isError ? events.refetch()} /> : 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 51531f59..81341381 100644 --- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts @@ -188,7 +188,9 @@ describe('V2 production entry', () => { expect(corePageSources.AlertsPage).toContain(' { expect(corePageSources.StatisticsPage).toContain(' { expect(source).toContain('重新加载地图'); } - for (const name of ['AlertsPage', 'AccessPage']) { - expect(corePageSources[name]).toContain("from '../shared/MetricActionButton'"); - expect(corePageSources[name]).toContain(' { @@ -48,4 +48,33 @@ describe('WorkspaceMetricRail', () => { expect(view.container.querySelector('.v2-workspace-metric-context')).toHaveTextContent('当前筛选 1,347 条'); expect(view.container.querySelectorAll('.semi-card')).toHaveLength(1); }); + + it('keeps interactive queue metrics inside accessible Semi buttons', () => { + const onOpen = vi.fn(); + render(); + + const list = screen.getByRole('list', { name: '告警队列概览' }); + const action = within(list).getByRole('button', { name: '筛选未处理,共 687 条' }); + expect(action).toHaveClass('semi-button', 'v2-workspace-metric-action'); + expect(action).toHaveAttribute('aria-pressed', 'true'); + expect(action.closest('[role="listitem"]')).toHaveClass('is-action', 'is-active', 'is-danger'); + fireEvent.click(action); + expect(onOpen).toHaveBeenCalledTimes(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 d4d00525..cccc1c5a 100644 --- a/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceMetricRail.tsx +++ b/vehicle-data-platform/apps/web/src/v2/shared/WorkspaceMetricRail.tsx @@ -1,4 +1,4 @@ -import { Card, CardGroup } from '@douyinfe/semi-ui'; +import { Button, Card, CardGroup } from '@douyinfe/semi-ui'; import type { ReactNode } from 'react'; export type WorkspaceMetricRailItem = { @@ -14,6 +14,9 @@ export type WorkspaceQueueMetricRailItem = { note?: ReactNode; tone?: 'neutral' | 'primary' | 'success' | 'warning' | 'danger'; emphasis?: 'primary' | 'secondary'; + active?: boolean; + ariaLabel?: string; + onClick?: () => void; }; type WorkspaceSummaryMetricRailProps = { @@ -41,14 +44,31 @@ export function WorkspaceMetricRail(props: WorkspaceSummaryMetricRailProps | Wor {props.items.map((item, index) => { const toneClassName = `is-${item.tone ?? 'neutral'}`; const emphasisClassName = `is-${item.emphasis ?? (index < 2 ? 'primary' : 'secondary')}`; - return + const classes = [ + toneClassName, + emphasisClassName !== toneClassName ? emphasisClassName : '', + item.onClick ? 'is-action' : '', + item.active ? 'is-active' : '' + ].filter(Boolean).join(' '); + const content = <> {item.label} {item.value} {item.note ? {item.note} : null} + ; + return + {item.onClick ? : content} ; })}
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 68ee6608..ea6ea9da 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/workspace.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/workspace.css @@ -16217,11 +16217,47 @@ padding: 7px 13px; } +.v2-workspace-metric-list > span.is-action { + padding: 0; +} + .v2-workspace-metric-list > span + span { border-left: 1px solid #e5ebf2; } -.v2-workspace-metric-list > span > small { +.v2-workspace-metric-action.semi-button { + width: 100%; + height: 100%; + min-height: 58px; + border-radius: 0; + padding: 0; + color: inherit; +} + +.v2-workspace-metric-action.semi-button:hover { + background: rgba(31, 111, 235, .045); +} + +.v2-workspace-metric-action.semi-button:focus-visible { + outline: 2px solid rgba(31, 111, 235, .42); + outline-offset: -3px; +} + +.v2-workspace-metric-action .semi-button-content { + display: grid; + width: 100%; + 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; + text-align: left; +} + +.v2-workspace-metric-list > span > small, +.v2-workspace-metric-action .semi-button-content > small { grid-column: 1; color: #78879a; font-size: 9px; @@ -16230,7 +16266,8 @@ white-space: nowrap; } -.v2-workspace-metric-list > span > strong { +.v2-workspace-metric-list > span > strong, +.v2-workspace-metric-action .semi-button-content > strong { grid-column: 1; color: #243950; font-size: 21px; @@ -16239,7 +16276,8 @@ white-space: nowrap; } -.v2-workspace-metric-list > span > em { +.v2-workspace-metric-list > span > em, +.v2-workspace-metric-action .semi-button-content > em { align-self: center; grid-column: 2; grid-row: 1 / 3; @@ -16252,7 +16290,8 @@ white-space: nowrap; } -.v2-workspace-metric-list > span.is-secondary > strong { +.v2-workspace-metric-list > span.is-secondary > strong, +.v2-workspace-metric-list > span.is-secondary .v2-workspace-metric-action strong { font-size: 17px; } @@ -16261,7 +16300,8 @@ box-shadow: inset 0 -3px #1f6feb; } -.v2-workspace-metric-list > span.is-primary > :is(small, strong) { +.v2-workspace-metric-list > span.is-primary > :is(small, strong), +.v2-workspace-metric-list > span.is-primary .v2-workspace-metric-action :is(small, strong) { color: #155fc5; } @@ -16270,18 +16310,44 @@ box-shadow: inset 0 -3px #e2554f; } -.v2-workspace-metric-list > span.is-danger > :is(small, strong) { +.v2-workspace-metric-list > span.is-danger > :is(small, strong), +.v2-workspace-metric-list > span.is-danger .v2-workspace-metric-action :is(small, strong) { color: #c2413d; } -.v2-workspace-metric-list > span.is-success > :is(small, strong) { +.v2-workspace-metric-list > span.is-success > :is(small, strong), +.v2-workspace-metric-list > span.is-success .v2-workspace-metric-action :is(small, strong) { color: #148459; } -.v2-workspace-metric-list > span.is-warning > :is(small, strong) { +.v2-workspace-metric-list > span.is-warning > :is(small, strong), +.v2-workspace-metric-list > span.is-warning .v2-workspace-metric-action :is(small, strong) { color: #a76512; } +.v2-workspace-metric-list > span.is-action { + background: #fff; + box-shadow: none; +} + +.v2-workspace-metric-list > span.is-action.is-danger { + background: linear-gradient(145deg, #fff 0%, #fff8f7 100%); +} + +.v2-workspace-metric-list > span.is-action.is-warning { + background: linear-gradient(145deg, #fff 0%, #fffcf5 100%); +} + +.v2-workspace-metric-list > span.is-action.is-active { + background: linear-gradient(145deg, #edf5ff 0%, #f8fbff 100%); + box-shadow: inset 0 -3px #1f6feb; +} + +.v2-workspace-metric-list > span.is-action.is-active.is-danger { + background: linear-gradient(145deg, #fff1ef 0%, #fff8f7 100%); + box-shadow: inset 0 -3px #e2554f; +} + .v2-workspace-metric-context { display: flex; min-width: 0; @@ -16320,6 +16386,68 @@ white-space: nowrap; } +.v2-alert-metric-rail.is-queue.semi-card { + flex: 0 0 auto; + border: 1px solid #dce4ed; + border-radius: 10px; + box-shadow: 0 4px 12px rgba(38, 56, 82, .035); +} + +.v2-alert-metric-rail.is-queue > .semi-card-body { + grid-template-columns: minmax(0, 1fr) 286px; +} + +.v2-alert-metric-rail .v2-workspace-metric-context { + padding: 5px 6px; +} + +.v2-alert-metric-context-actions { + display: grid; + width: 100%; + min-width: 0; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 4px; +} + +.v2-alert-metric-context-actions > .semi-button { + height: 46px; + min-width: 0; + border: 1px solid transparent; + border-radius: 7px; + padding: 4px 5px; + color: #718096; +} + +.v2-alert-metric-context-actions > .semi-button:hover, +.v2-alert-metric-context-actions > .semi-button[aria-pressed="true"] { + border-color: #d5e3f8; + background: #eef5ff; + color: #155fc5; +} + +.v2-alert-metric-context-actions .semi-button-content { + display: grid; + justify-items: center; + gap: 2px; +} + +.v2-alert-metric-context-actions span { + font-size: 9px; + line-height: 12px; + white-space: nowrap; +} + +.v2-alert-metric-context-actions strong { + color: #34455c; + font-size: 14px; + font-variant-numeric: tabular-nums; + line-height: 17px; +} + +.v2-alert-metric-context-actions > .semi-button[aria-pressed="true"] strong { + color: inherit; +} + .v2-reconcile-metric-rail { flex: 0 0 auto; } @@ -16344,6 +16472,11 @@ column-gap: 6px; padding-inline: 9px; } + + .v2-workspace-metric-action .semi-button-content { + column-gap: 6px; + padding-inline: 9px; + } } @media (max-width: 680px) { @@ -16363,22 +16496,44 @@ padding: 6px 12px; } + .v2-workspace-metric-action.semi-button, + .v2-workspace-metric-action .semi-button-content { + min-height: 56px; + } + + .v2-workspace-metric-action .semi-button-content { + column-gap: 8px; + padding: 6px 12px; + } + .v2-workspace-metric-list > span.is-secondary { display: none; } - .v2-workspace-metric-list > span > strong { + .v2-workspace-metric-list > span > strong, + .v2-workspace-metric-action .semi-button-content > strong { font-size: 19px; line-height: 22px; } - .v2-workspace-metric-list > span > em { + .v2-workspace-metric-list > span > em, + .v2-workspace-metric-action .semi-button-content > em { font-size: 8px; } .v2-workspace-metric-context { display: none; } + + .v2-alert-metric-rail.is-queue.semi-card { + border-radius: 10px; + } +} + +@media (min-width: 681px) and (max-width: 900px) { + .v2-alert-metric-rail.is-queue > .semi-card-body { + grid-template-columns: minmax(0, 1fr) 230px; + } } .v2-reconcile-issue-cell > span {