feat(web): refine Semi UI alert center

This commit is contained in:
lingniu
2026-07-20 00:01:47 +08:00
parent e20f320f6c
commit cd13a2952d
5 changed files with 149 additions and 76 deletions

View File

@@ -157,9 +157,12 @@ test('clears old alert rows and inspector evidence when the event scope changes'
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();
expect(screen.getByRole('heading', { level: 5, name: '当前事件队列' })).toBeInTheDocument();
expect(screen.getByText('1 条待处置 · 共 1 条')).toBeInTheDocument();
expect(screen.getByRole('columnheader', { name: '触发证据' })).toBeInTheDocument();
expect(screen.getByRole('columnheader', { name: '位置' })).toBeInTheDocument();
expect(view.container.querySelector('.v2-alert-event-table .v2-alert-protocol-tag.semi-tag')).toHaveTextContent('JT/T 808');
expect(view.container.querySelector('.v2-alert-event-table input[type="radio"]')).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-alert-table-scroll > table')).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-alert-mobile-list')).not.toBeInTheDocument();
expect(view.container.querySelector('time[datetime="2026-07-16T04:00:00Z"]')).toHaveTextContent('07-16 12:00:00');
@@ -172,7 +175,8 @@ test('clears old alert rows and inspector evidence when the event scope changes'
expect(view.container.querySelector('.v2-alert-inspector.semi-card')).toBeInTheDocument();
expect(view.container.querySelector('.v2-alert-workspace')).toHaveClass('is-inspector-open');
expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).toHaveClass('is-compact');
expect(screen.queryByRole('columnheader', { name: '触发 / 阈值' })).not.toBeInTheDocument();
expect(desktopAlertRow).toHaveClass('is-severity-major', 'is-selected');
expect(screen.queryByRole('columnheader', { name: '触发证据' })).not.toBeInTheDocument();
expect(screen.queryByRole('columnheader', { name: '位置' })).not.toBeInTheDocument();
expect(screen.getByText('已展开处置详情 · 点击其他事件快速切换')).toBeInTheDocument();
expect(desktopAlertRow).toHaveAttribute('aria-expanded', 'true');
@@ -194,7 +198,7 @@ test('clears old alert rows and inspector evidence when the event scope changes'
fireEvent.click(screen.getByRole('button', { name: '关闭告警详情' }));
expect(view.container.querySelector('.v2-alert-inspector')).not.toBeInTheDocument();
expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).not.toHaveClass('is-compact');
expect(screen.getByRole('columnheader', { name: '触发 / 阈值' })).toBeInTheDocument();
expect(screen.getByRole('columnheader', { name: '触发证据' })).toBeInTheDocument();
expect(screen.getByRole('columnheader', { name: '位置' })).toBeInTheDocument();
expect(desktopAlertRow).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(desktopAlertRow);

View File

@@ -1,5 +1,5 @@
import { IconAlarm, IconBell, IconChevronRight, IconClose, IconFilter, IconPlus, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons';
import { Button, Card, CardGroup, Collapse, Descriptions, Empty, Input, Radio, Select, Spin, Table, Tag, TextArea, Timeline, Typography } from '@douyinfe/semi-ui';
import { Button, Card, CardGroup, Collapse, Descriptions, Empty, Input, Select, Spin, Table, Tag, TextArea, Timeline, Typography } from '@douyinfe/semi-ui';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { FormEvent, memo, useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
@@ -32,13 +32,12 @@ const PROTOCOLS = ['GB32960', 'JT808', 'YUTONG_MQTT'];
const NUMERIC_OPERATORS = ['gt', 'gte', 'lt', 'lte', 'eq', 'neq', 'between', 'outside'];
const BOOLEAN_OPERATORS = ['eq', 'neq', 'changed'];
const COMPACT_ALERT_COLUMN_WIDTHS: Record<string, number> = {
selection: 40,
severity: 80,
plate: 132,
ruleName: 148,
protocol: 100,
triggeredAt: 120,
status: 78
severity: 70,
plate: 116,
ruleName: 124,
protocol: 86,
triggeredAt: 110,
status: 72
};
type RuleTemplate = { id: string; title: string; summary: string; draft: AlertRuleInput };
@@ -90,24 +89,20 @@ function AlertTime({ value, className = '' }: { value?: string; className?: stri
const AlertEventTable = memo(function AlertEventTable({ rows, selectedID, compact, onSelect }: { rows: AlertEvent[]; selectedID: string; compact: boolean; onSelect: (id: string) => void }) {
const columns = useMemo(() => {
const allColumns = [
{ title: '级别', dataIndex: 'severity', width: 78, render: (_: AlertEvent['severity'], event: AlertEvent) => <SeverityTag severity={event.severity} /> },
{
title: '', dataIndex: 'selection', width: 42,
render: (_: unknown, event: AlertEvent) => <Radio name="alert-event" checked={selectedID === event.id} onChange={() => onSelect(event.id)} aria-label={`选择 ${event.plate || event.vin} ${event.ruleName}`} />
},
{ title: '严重程度', dataIndex: 'severity', width: 90, render: (_: AlertEvent['severity'], event: AlertEvent) => <SeverityTag severity={event.severity} /> },
{
title: '车牌 / VIN', dataIndex: 'plate', width: 142,
title: '车牌 / VIN', dataIndex: 'plate', width: 132,
render: (_: string, event: AlertEvent) => <div className="v2-alert-event-vehicle"><strong>{event.plate || '未绑定车牌'}</strong><small>{event.vin}</small></div>
},
{ title: '规则', dataIndex: 'ruleName', width: 170, render: (value: string) => <span className="v2-alert-event-rule" title={value}>{value}</span> },
{ title: '协议', dataIndex: 'protocol', width: 112, render: (value: string) => <ProtocolTag className="v2-alert-protocol-tag" protocol={value} compact /> },
{ title: '触发时间', dataIndex: 'triggeredAt', width: 132, render: (value: string) => <AlertTime value={value} /> },
{ title: '状态', dataIndex: 'status', width: 90, render: (_: AlertEvent['status'], event: AlertEvent) => <StatusTag status={event.status} /> },
{ title: '规则', dataIndex: 'ruleName', width: 146, render: (value: string) => <span className="v2-alert-event-rule" title={value}>{value}</span> },
{ title: '协议', dataIndex: 'protocol', width: 98, render: (value: string) => <ProtocolTag className="v2-alert-protocol-tag" protocol={value} compact /> },
{ title: '触发时间', dataIndex: 'triggeredAt', width: 124, render: (value: string) => <AlertTime value={value} /> },
{ title: '状态', dataIndex: 'status', width: 78, render: (_: AlertEvent['status'], event: AlertEvent) => <StatusTag status={event.status} /> },
{
title: '触发 / 阈值', dataIndex: 'evidence', width: 164,
title: '触发证据', dataIndex: 'evidence', width: 150,
render: (_: unknown, event: AlertEvent) => <div className="v2-alert-event-evidence"><strong>{alertValue(event)}</strong><small>{thresholdText(event)}</small></div>
},
{ title: '位置', dataIndex: 'location', width: 180, render: (value: string) => <span className="v2-alert-event-location" title={value}>{value || '—'}</span> },
{ title: '位置', dataIndex: 'location', width: 164, render: (value: string) => <span className="v2-alert-event-location" title={value}>{value || '—'}</span> },
];
return compact
? allColumns
@@ -124,7 +119,7 @@ const AlertEventTable = memo(function AlertEventTable({ rows, selectedID, compac
pagination={false}
empty={null}
onRow={(event) => event ? detailTriggerRow({
className: selectedID === event.id ? 'is-selected' : '',
className: [`is-severity-${event.severity}`, selectedID === event.id ? 'is-selected' : ''].filter(Boolean).join(' '),
expanded: selectedID === event.id,
label: `查看 ${event.plate || event.vin} ${event.ruleName} 告警详情`,
testId: `alert-row-${event.id}`,
@@ -354,9 +349,9 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
<div className={`v2-alert-workspace${selectedID && !mobileLayout ? ' is-inspector-open' : ''}`}>
<Card className="v2-alert-table-card" bodyStyle={{ padding: 0 }}>
<WorkspacePanelHeader
title="告警事件"
title="当前事件队列"
description={selectedID && !mobileLayout ? '已展开处置详情 · 点击其他事件快速切换' : '点击事件查看触发证据、处理进度与处置动作'}
meta={`${(events.data?.total ?? 0).toLocaleString('zh-CN')}`}
meta={`${Number(sums?.unprocessed ?? 0).toLocaleString('zh-CN')} 条待处置 · ${(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

View File

@@ -198,7 +198,8 @@ describe('V2 production entry', () => {
expect(corePageSources.AlertsPage).toContain('<Input');
expect(corePageSources.AlertsPage).toContain('<Select');
expect(corePageSources.AlertsPage).toContain('<TextArea');
expect(corePageSources.AlertsPage).toContain('<Radio');
expect(corePageSources.AlertsPage).not.toContain('<Radio');
expect(corePageSources.AlertsPage).toContain('is-severity-${event.severity}');
expect(corePageSources.AlertsPage).toContain('<Card key={event.id} className={`v2-alert-mobile-card');
expect(corePageSources.AlertsPage).toContain('<WorkspaceMetricRail');
expect(corePageSources.AlertsPage).toContain('className="v2-alert-metric-rail"');

View File

@@ -8402,7 +8402,7 @@
* command rail and reserves the remaining height for the operational list.
*/
.v2-alert-page {
gap: 12px;
gap: 10px;
padding: 14px 18px 18px;
background: #f6f8fb;
}
@@ -8503,6 +8503,7 @@
.v2-alert-filter-card.semi-card {
overflow: visible;
border-color: #d8e1ec;
}
.v2-alert-filter-card > .semi-card-body > .v2-workspace-filter-heading {
@@ -8510,24 +8511,36 @@
}
.v2-alert-filter.v2-alert-filter-primary {
min-height: 70px;
min-height: 54px;
grid-template-columns: minmax(250px, 1.45fr) minmax(122px, .62fr) minmax(136px, .68fr) auto auto auto;
gap: 10px;
padding: 11px 13px;
align-items: center;
padding: 8px 10px;
}
.v2-alert-filter.v2-alert-filter-primary label {
position: relative;
gap: 5px;
color: #66778e;
font-size: 11px;
font-weight: 600;
}
.v2-alert-filter.v2-alert-filter-primary label > span {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
}
.v2-alert-filter.v2-alert-filter-primary label > div,
.v2-alert-filter.v2-alert-filter-primary .semi-select,
.v2-alert-filter.v2-alert-filter-primary .semi-input-wrapper,
.v2-alert-filter.v2-alert-filter-primary > .semi-button {
min-height: 38px;
min-height: 36px;
border-radius: 8px;
}
@@ -8588,7 +8601,7 @@
}
.v2-alert-workspace.is-inspector-open {
grid-template-columns: minmax(680px, 1fr) 390px;
grid-template-columns: minmax(0, 1fr) minmax(360px, 390px);
}
.v2-alert-table-card > .semi-card-body > .v2-workspace-panel-header {
@@ -8646,12 +8659,51 @@
font-size: 12px;
}
.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row {
position: relative;
transition: background-color .16s ease, box-shadow .16s ease;
}
.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row::before {
position: absolute;
z-index: 2;
top: 7px;
bottom: 7px;
left: 0;
width: 3px;
border-radius: 0 4px 4px 0;
background: #c2cad5;
content: "";
pointer-events: none;
}
.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row.is-severity-critical::before {
background: #e5484d;
}
.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row.is-severity-major::before {
background: #ed8d32;
}
.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row.is-severity-minor::before {
background: #d9a31d;
}
.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row:hover {
background: #f7faff;
}
.v2-alert-table-scroll .v2-alert-event-table .semi-table-tbody > .semi-table-row.is-selected {
background: #eef5ff;
box-shadow: inset 0 0 0 1px rgba(18, 104, 243, .16);
}
.v2-alert-event-table.semi-table-wrapper {
min-width: 1102px;
min-width: 970px;
}
.v2-alert-event-table.is-compact.semi-table-wrapper {
min-width: 758px;
min-width: 660px;
}
.v2-alert-event-evidence {
@@ -9529,10 +9581,15 @@
padding: 5px 8px;
}
.v2-alert-table-card .v2-workspace-panel-copy > .semi-typography {
.v2-alert-table-card .v2-workspace-panel-copy > .semi-typography:not(h5) {
display: none;
}
.v2-alert-table-card .v2-workspace-panel-copy > h5.semi-typography {
display: block;
font-size: 13px;
}
.v2-alert-table-scroll.is-mobile-scroll {
min-height: 0;
flex: 1 1 0;
@@ -25382,8 +25439,8 @@
}
.v2-alert-filter.v2-alert-filter-primary {
min-height: 66px;
padding: 9px 13px;
min-height: 52px;
padding: 7px 10px;
}
.v2-alert-filter.v2-alert-filter-primary label > div,
@@ -25417,13 +25474,18 @@
}
.v2-alert-table-scroll .v2-alert-event-table.semi-table-wrapper .semi-table-tbody > .semi-table-row > .semi-table-row-cell {
height: 56px;
padding: 8px 10px;
height: 52px;
padding: 7px 10px;
color: #405269;
font-size: 12.5px;
transition: background-color .16s ease;
}
.v2-alert-event-table .semi-table {
width: 100%;
table-layout: fixed;
}
.v2-alert-table-scroll .v2-alert-event-table.semi-table-wrapper .semi-table-tbody > .semi-table-row:hover > .semi-table-row-cell {
background: #f6f9fe;
}
@@ -25445,7 +25507,7 @@
}
.v2-alert-workspace.is-inspector-open {
grid-template-columns: minmax(650px, 1fr) 420px;
grid-template-columns: minmax(0, 1fr) minmax(360px, 390px);
}
.v2-alert-workspace.is-inspector-open .v2-alert-event-table.is-compact.semi-table-wrapper {