refine Semi UI alert workspace

This commit is contained in:
lingniu
2026-07-18 03:13:34 +08:00
parent bb3820bfaa
commit 91db15ebde
4 changed files with 686 additions and 11 deletions

View File

@@ -123,9 +123,12 @@ test('clears old alert rows and inspector evidence when the event scope changes'
const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts?keyword=OLDVIN']}><AlertsPage /></MemoryRouter></QueryClientProvider>);
expect((await screen.findAllByText('旧告警车牌')).length).toBeGreaterThan(0);
for (const className of ['v2-alert-filter-card', 'v2-alert-kpis-card', 'v2-alert-table-card']) {
for (const className of ['v2-alert-filter-card', 'v2-alert-kpis-card', 'v2-alert-context-card', 'v2-alert-table-card']) {
expect(view.container.querySelector(`.${className}.semi-card`)).toBeInTheDocument();
}
expect(view.container.querySelector('.v2-alert-navigation')).toBeInTheDocument();
expect(screen.getByText('可处置事件')).toBeInTheDocument();
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);

View File

@@ -8,7 +8,6 @@ import type { AlertEvent, AlertNotification, AlertQuery, AlertRule, AlertRuleInp
import { actionLabels, alertValue, canAct, formatAlertTime, operatorLabels, ruleCondition, severityLabels, statusLabels, thresholdText } from '../domain/alert';
import { InlineError, PanelEmpty, PanelLoading } from '../shared/AsyncState';
import { MetricActionButton } from '../shared/MetricActionButton';
import { PageHeader } from '../shared/PageHeader';
import { SegmentedTabs } from '../shared/SegmentedTabs';
import { TablePagination } from '../shared/TablePagination';
import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader';
@@ -197,8 +196,8 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
</form>
</WorkspaceFilterPanel>
{summary.isError ? <InlineError message={summary.error instanceof Error ? summary.error.message : '告警汇总读取失败'} onRetry={() => summary.refetch()} /> : null}
<Card className="v2-alert-kpis-card" bodyStyle={{ padding: 0 }}>
<section className="v2-alert-kpis">{[
<Card className="v2-alert-kpis-card v2-alert-context-card" bodyStyle={{ padding: 0 }}>
<section className="v2-alert-kpis" aria-label="活跃告警概览">{[
{ label: '活跃告警', value: sums?.active, tone: 'active', status: '' },
{ label: '未处理', value: sums?.unprocessed, tone: 'unprocessed', status: 'unprocessed' },
{ label: '处理中', value: sums?.processing, tone: 'processing', status: 'processing' },
@@ -218,7 +217,12 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
{events.isError ? <InlineError message={events.error instanceof Error ? events.error.message : '告警事件读取失败'} onRetry={() => events.refetch()} /> : null}
<div className={`v2-alert-workspace${selectedID && !mobileLayout ? ' is-inspector-open' : ''}`}>
<Card className="v2-alert-table-card" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader title="告警事件" meta={`${(events.data?.total ?? 0).toLocaleString('zh-CN')}`} actions={<Button theme="borderless" icon={<IconRefresh />} onClick={() => Promise.all([events.refetch(), summary.refetch(), ...(selectedID ? [detail.refetch()] : [])])}></Button>} />
<WorkspacePanelHeader
title="告警事件"
description="点击事件查看触发证据、处理进度与处置动作"
meta={`${(events.data?.total ?? 0).toLocaleString('zh-CN')}`}
actions={<Button theme="borderless" icon={<IconRefresh />} loading={events.isFetching} onClick={() => Promise.all([events.refetch(), summary.refetch(), ...(selectedID ? [detail.refetch()] : [])])}></Button>}
/>
<div className="v2-alert-table-scroll">
{mobileLayout
? <div className="v2-alert-mobile-list">{rows.map((event) => <Card key={event.id} className={`v2-alert-mobile-card${selectedID === event.id ? ' is-selected' : ''}`} bodyStyle={{ padding: 0 }}><Button theme="borderless" type="tertiary" className="v2-alert-mobile-action" aria-pressed={selectedID === event.id} aria-expanded={selectedID === event.id} aria-label={`查看 ${event.plate || event.vin} ${event.ruleName} 告警详情`} onClick={() => setSelection({ scope: eventScope, id: event.id })}><span className="v2-alert-mobile-card-content"><header><strong>{event.plate || '未绑定车牌'}</strong><span><SeverityTag severity={event.severity} /><StatusTag status={event.status} /></span></header><p>{event.ruleName}</p><dl><div><dt></dt><dd>{formatAlertTime(event.triggeredAt)}</dd></div><div><dt></dt><dd>{event.protocol || '—'}</dd></div><div><dt></dt><dd>{alertValue(event)}</dd></div><div><dt></dt><dd>{thresholdText(event)}</dd></div></dl><footer><IconChevronRight /></footer></span></Button></Card>)}</div>
@@ -406,5 +410,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: <IconAlarm /> }, ...(admin ? [{ key: 'rules' as const, label: '规则配置' }] : []), { key: 'notifications' as const, label: '站内通知', icon: <IconBell />, count: unread || undefined }];
return <div className="v2-alert-page"><PageHeader title="告警中心" description="统一监控车辆运行异常,串联触发证据、处理进度与通知状态。" status={unread ? `${unread} 条未读` : '通知已读'} statusColor={unread ? 'orange' : 'green'} meta={<Typography.Text type="tertiary">{operator ? '可处置事件' : '只读查看'}</Typography.Text>} /><SegmentedTabs className="v2-alert-tabs" variant="filled" ariaLabel="告警中心分类" value={activeTab} items={tabs} onChange={setTab} />{activeTab !== 'notifications' && rules.isError ? <InlineError message={rules.error.message} onRetry={() => rules.refetch()} /> : null}{activeTab === 'rules' && metrics.isError ? <InlineError message={metrics.error.message} onRetry={() => metrics.refetch()} /> : null}{activeTab === 'events' ? <EventWorkspace filters={filters} draft={eventDraft} setDraft={setEventDraft} setFilters={setFilters} rules={rules.data ?? []} unread={unread} editable={operator} onTab={setTab} /> : activeTab === 'rules' ? <RulesWorkspace rules={rules.data ?? []} metrics={metrics.data?.metrics ?? []} /> : <NotificationsWorkspace editable={operator} notifications={notifications} />}</div>;
return <div className="v2-alert-page"><div className="v2-alert-navigation"><SegmentedTabs className="v2-alert-tabs" variant="filled" ariaLabel="告警中心分类" value={activeTab} items={tabs} onChange={setTab} /><div className="v2-alert-navigation-meta"><Typography.Text type="tertiary">{operator ? '可处置事件' : '只读查看'}</Typography.Text><Tag color={unread ? 'orange' : 'green'} type="light" size="small">{unread ? `${unread.toLocaleString('zh-CN')} 条未读` : '通知已读'}</Tag></div></div>{activeTab !== 'notifications' && rules.isError ? <InlineError message={rules.error.message} onRetry={() => rules.refetch()} /> : null}{activeTab === 'rules' && metrics.isError ? <InlineError message={metrics.error.message} onRetry={() => metrics.refetch()} /> : null}{activeTab === 'events' ? <EventWorkspace filters={filters} draft={eventDraft} setDraft={setEventDraft} setFilters={setFilters} rules={rules.data ?? []} unread={unread} editable={operator} onTab={setTab} /> : activeTab === 'rules' ? <RulesWorkspace rules={rules.data ?? []} metrics={metrics.data?.metrics ?? []} /> : <NotificationsWorkspace editable={operator} notifications={notifications} />}</div>;
}