refine Semi UI alert disposition flow

This commit is contained in:
lingniu
2026-07-18 18:38:04 +08:00
parent 23669d8190
commit db1079ae27
3 changed files with 124 additions and 6 deletions

View File

@@ -167,6 +167,10 @@ test('clears old alert rows and inspector evidence when the event scope changes'
expect(screen.getByLabelText('告警触发证据')).toHaveTextContent('触发值90 km/h阈值条件> 80 km/h持续 60 秒超限幅度+10 km/h');
expect(screen.getByText('1 条记录').closest('.semi-tag')).toBeInTheDocument();
expect(screen.getByText('可操作').closest('.semi-tag')).toBeInTheDocument();
const dispositionCard = view.container.querySelector('.v2-alert-disposition-card');
const progressCard = view.container.querySelector('.v2-alert-progress-card');
expect(dispositionCard).toHaveAttribute('aria-label', '告警处置操作');
expect(dispositionCard?.compareDocumentPosition(progressCard!)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(view.container.querySelector('.v2-alert-technical-collapse.semi-collapse')).toBeInTheDocument();
expect(screen.getByText('技术详情')).toBeInTheDocument();
expect(view.container.querySelector('.v2-alert-inspector-heading')).toHaveClass('v2-workspace-panel-header');
@@ -244,7 +248,12 @@ test('renders only selectable Semi alert cards on mobile', async () => {
expect(action).toHaveAttribute('aria-expanded', 'true');
expect(await screen.findByRole('dialog', { name: '告警事件详情' })).toBeInTheDocument();
expect(document.querySelector('.v2-alert-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom');
expect(document.querySelector('.v2-alert-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(86dvh, 760px)' });
expect(document.querySelector('.v2-alert-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(90dvh, 800px)' });
expect(document.querySelector('.v2-alert-detail-sidesheet .v2-alert-inspector-heading')).not.toBeInTheDocument();
const mobileDisposition = document.querySelector('.v2-alert-detail-sidesheet .v2-alert-disposition-card');
const mobileProgress = document.querySelector('.v2-alert-detail-sidesheet .v2-alert-progress-card');
expect(mobileDisposition).toHaveAttribute('aria-label', '告警处置操作');
expect(mobileDisposition?.compareDocumentPosition(mobileProgress!)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
expect(screen.getByLabelText('告警触发证据')).toHaveTextContent('超限幅度+10 km/h');
expect(await screen.findByText('mobile-event')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: '关闭告警详情' }));

View File

@@ -115,7 +115,8 @@ const AlertEventTable = memo(function AlertEventTable({ rows, selectedID, compac
function EventInspector({ event, note, acting, actionError, editable, onNote, onAction, onClose, sheet = false }: { event?: AlertEvent; note: string; acting: boolean; actionError?: string; editable: boolean; onNote: (value: string) => void; onAction: (action: 'acknowledge' | 'close' | 'ignore') => void; onClose: () => void; sheet?: boolean }) {
if (!event) return <Card className="v2-alert-inspector" bodyStyle={{ padding: 0 }}><Empty className="v2-alert-inspector-empty" image={<IconAlarm />} title="选择告警事件" description="查看触发证据、状态时间线和处置动作。" /></Card>;
const actionCount = event.actions?.length ?? 0;
return <Card className={`v2-alert-inspector${sheet ? ' is-sheet' : ''}`} bodyStyle={{ padding: 0 }}><WorkspacePanelHeader className="v2-alert-inspector-heading" title="事件处置" description={event.ruleName} actions={<><span className="v2-alert-inspector-status"><SeverityTag severity={event.severity} /><StatusTag status={event.status} /></span>{sheet ? null : <Button className="v2-alert-inspector-close" theme="borderless" icon={<IconClose />} aria-label="关闭告警详情" onClick={onClose} />}</>} />
return <Card className={`v2-alert-inspector${sheet ? ' is-sheet' : ''}`} bodyStyle={{ padding: 0 }}>
{!sheet ? <WorkspacePanelHeader className="v2-alert-inspector-heading" title="事件处置" description={event.ruleName} actions={<><span className="v2-alert-inspector-status"><SeverityTag severity={event.severity} /><StatusTag status={event.status} /></span><Button className="v2-alert-inspector-close" theme="borderless" icon={<IconClose />} aria-label="关闭告警详情" onClick={onClose} /></>} /> : null}
<div className="v2-alert-inspector-content">
<Card className="v2-alert-detail-card v2-alert-focus-card" bodyStyle={{ padding: 0 }} aria-label="告警处置摘要">
<header className="v2-alert-focus-identity">
@@ -134,12 +135,12 @@ function EventInspector({ event, note, acting, actionError, editable, onNote, on
</div>
<div className="v2-alert-focus-rule"><Tag color="grey" type="light" size="small"></Tag><span>{event.ruleName}</span></div>
</Card>
<Card className="v2-alert-detail-card v2-alert-disposition-card" title={<span className="v2-alert-detail-title"><strong></strong><Tag color={editable ? 'green' : 'grey'} type="light" size="small">{editable ? '可操作' : '只读'}</Tag></span>} headerLine aria-label="告警处置操作">
{editable ? <><TextArea maxCount={200} autosize={{ minRows: 2, maxRows: 5 }} placeholder="请输入处置说明(选填)" value={note} onChange={onNote} />{actionError ? <p className="v2-alert-action-error">{actionError}</p> : null}<div className="v2-alert-actions"><Button theme="solid" className="is-primary" disabled={acting || !canAct(event.status, 'acknowledge')} onClick={() => onAction('acknowledge')}></Button><Button theme="light" disabled={acting || !canAct(event.status, 'close')} onClick={() => onAction('close')}></Button><Button theme="borderless" disabled={acting || !canAct(event.status, 'ignore')} onClick={() => onAction('ignore')}></Button></div></> : <p className="v2-role-notice"></p>}
</Card>
<Card className="v2-alert-detail-card v2-alert-progress-card" title={<span className="v2-alert-detail-title"><strong></strong><Tag color={actionCount ? 'blue' : 'amber'} type="light" size="small">{actionCount ? `${actionCount} 条记录` : '待处置'}</Tag></span>} headerLine>
{event.actions?.length ? <Timeline className="v2-alert-timeline" aria-label="告警处理进度">{event.actions.map((item, index) => <Timeline.Item key={item.id} type={index === event.actions!.length - 1 ? 'ongoing' : 'default'} time={<span>{item.actor} · <AlertTime value={item.createdAt} /></span>}><strong>{actionLabels[item.action] ?? item.action}</strong>{item.note ? <p>{item.note}</p> : null}</Timeline.Item>)}</Timeline> : <Empty className="v2-alert-timeline-empty" title="暂无处理记录" description="事件被确认、关闭或忽略后,将在这里形成审计履历。" />}
</Card>
<Card className="v2-alert-detail-card v2-alert-disposition-card" title={<span className="v2-alert-detail-title"><strong></strong><Tag color={editable ? 'green' : 'grey'} type="light" size="small">{editable ? '可操作' : '只读'}</Tag></span>} headerLine>
{editable ? <><TextArea maxCount={200} autosize={{ minRows: 2, maxRows: 5 }} placeholder="请输入处置说明(选填)" value={note} onChange={onNote} />{actionError ? <p className="v2-alert-action-error">{actionError}</p> : null}<div className="v2-alert-actions"><Button theme="solid" className="is-primary" disabled={acting || !canAct(event.status, 'acknowledge')} onClick={() => onAction('acknowledge')}></Button><Button theme="light" disabled={acting || !canAct(event.status, 'close')} onClick={() => onAction('close')}></Button><Button theme="borderless" disabled={acting || !canAct(event.status, 'ignore')} onClick={() => onAction('ignore')}></Button></div></> : <p className="v2-role-notice"></p>}
</Card>
<Collapse className="v2-alert-technical-collapse" keepDOM>
<Collapse.Panel
itemKey="technical-evidence"
@@ -294,7 +295,7 @@ function EventWorkspace({ filters, draft, setDraft, setFilters, rules, unread, e
visible={mobileLayout && Boolean(selectedID)}
aria-label="告警事件详情"
placement="bottom"
height="min(86dvh, 760px)"
height="min(90dvh, 800px)"
title={<div className="v2-alert-sheet-title"><strong></strong><span>{selectedEvent ? `${selectedEvent.plate || selectedEvent.vin} · ${selectedEvent.ruleName}` : '证据、状态与处置履历'}</span></div>}
onCancel={closeDetail}
>

View File

@@ -1941,6 +1941,31 @@
padding-bottom: 14px;
}
.v2-alert-inspector.is-sheet .v2-alert-inspector-content {
gap: 8px;
padding: 8px 8px 14px;
}
.v2-alert-inspector.is-sheet .v2-alert-disposition-card > .semi-card-header {
min-height: 40px;
}
.v2-alert-inspector.is-sheet .v2-alert-disposition-card > .semi-card-body {
padding: 8px !important;
}
.v2-alert-inspector.is-sheet .v2-alert-disposition-card .semi-input-textarea {
min-height: 52px;
}
.v2-alert-inspector.is-sheet .v2-alert-disposition-card .v2-alert-actions {
margin-top: 7px;
}
.v2-alert-inspector.is-sheet .v2-alert-disposition-card .v2-alert-actions > .semi-button {
min-height: 38px;
}
.v2-access-governance-summary > .semi-card-body {
grid-template-columns: 1fr;
}
@@ -7630,6 +7655,89 @@
padding: 10px !important;
}
.v2-alert-disposition-card {
border-color: #cfe0f7 !important;
box-shadow: 0 6px 18px rgba(18, 104, 243, .06) !important;
}
.v2-alert-disposition-card > .semi-card-header {
background: linear-gradient(90deg, #f4f8ff 0%, #fff 72%);
}
.v2-alert-disposition-card .v2-alert-actions {
margin-top: 9px;
}
.v2-alert-disposition-card .v2-alert-actions > .semi-button {
min-height: 36px;
border-radius: 8px;
}
.v2-alert-inspector:not(.is-sheet) .v2-alert-inspector-content {
gap: 8px;
padding: 8px;
}
.v2-alert-inspector:not(.is-sheet) > .semi-card-body > .v2-alert-inspector-heading {
min-height: 64px;
padding: 8px 11px;
}
.v2-alert-inspector:not(.is-sheet) > .semi-card-body > .v2-alert-inspector-heading > .v2-workspace-panel-copy {
gap: 2px;
}
.v2-alert-inspector:not(.is-sheet) > .semi-card-body > .v2-alert-inspector-heading > .v2-workspace-panel-actions {
max-width: none;
align-items: center;
flex-direction: row;
gap: 6px;
}
.v2-alert-inspector:not(.is-sheet) .v2-alert-focus-identity {
min-height: 68px;
padding: 9px 11px;
}
.v2-alert-inspector:not(.is-sheet) .v2-alert-focus-facts > span {
min-height: 48px;
padding: 6px 9px;
}
.v2-alert-inspector:not(.is-sheet) .v2-alert-focus-card .v2-alert-evidence {
padding: 8px;
}
.v2-alert-inspector:not(.is-sheet) .v2-alert-focus-card .v2-alert-evidence > .v2-alert-evidence-value > .semi-card-body {
min-height: 62px;
padding-block: 6px;
}
.v2-alert-inspector:not(.is-sheet) .v2-alert-focus-rule {
min-height: 34px;
padding-block: 5px;
}
.v2-alert-inspector:not(.is-sheet) .v2-alert-disposition-card > .semi-card-header {
min-height: 40px;
}
.v2-alert-inspector:not(.is-sheet) .v2-alert-disposition-card > .semi-card-body {
padding: 8px !important;
}
.v2-alert-inspector:not(.is-sheet) .v2-alert-disposition-card .semi-input-textarea {
min-height: 48px;
}
.v2-alert-inspector:not(.is-sheet) .v2-alert-disposition-card .v2-alert-actions {
margin-top: 7px;
}
.v2-alert-inspector:not(.is-sheet) .v2-alert-disposition-card .v2-alert-actions > .semi-button {
min-height: 34px;
}
.v2-alert-inspector > .semi-card-body > .v2-alert-links {
min-height: 50px;
padding: 7px 10px;