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 75ee49d6..d60868aa 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 @@ -357,10 +357,13 @@ test('keeps mobile rule selection focused and opens editing in a Semi bottom Sid fireEvent.click(ruleItem); const editor = await screen.findByRole('dialog', { name: '告警规则编辑' }); - expect(document.querySelector('.v2-alert-rule-editor-sidesheet')).toHaveClass('semi-sidesheet-bottom'); + expect(document.querySelector('.v2-alert-rule-editor-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-editor-sidesheet'); expect(document.querySelector('.v2-alert-rule-editor-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(92dvh, 820px)' }); expect(within(editor).getByDisplayValue('测试超速规则')).toBeInTheDocument(); expect(within(editor).getByRole('button', { name: '保存规则' })).toBeInTheDocument(); + expect(within(editor).getByRole('heading', { name: '触发条件' })).toBeInTheDocument(); + expect(within(editor).getByRole('heading', { name: '恢复与提醒' })).toBeInTheDocument(); + expect(within(editor).getByRole('heading', { name: '适用范围与说明' })).toBeInTheDocument(); expect(within(editor).queryByRole('region', { name: '告警规则模板' })).not.toBeInTheDocument(); expect(ruleItem).toHaveAttribute('aria-expanded', 'true'); @@ -397,6 +400,9 @@ test('creates auditable drafts from simple offline and hydrogen rule templates', expect(view.container.querySelector('.v2-alert-rule-editor')).toHaveClass('semi-card'); expect(view.container.querySelector('.v2-alert-rule-list-heading')).toHaveClass('v2-workspace-panel-header'); expect(view.container.querySelector('.v2-alert-rule-editor-heading')).toHaveClass('v2-workspace-panel-header'); + expect(screen.getByRole('heading', { name: '触发条件' }).closest('.v2-rule-form-section')).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: '恢复与提醒' }).closest('.v2-rule-form-section')).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: '适用范围与说明' }).closest('.v2-rule-form-section')).toBeInTheDocument(); expect(screen.getByRole('region', { name: '告警规则列表,可左右滚动选择' })).toHaveAttribute('tabindex', '0'); expect(ruleItem).toHaveClass('semi-button', 'v2-alert-rule-item'); await waitFor(() => expect(ruleItem).toHaveAttribute('aria-pressed', 'true')); 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 ae852366..a7c7277c 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx @@ -1,7 +1,7 @@ import { IconAlarm, IconBell, IconChevronRight, IconClose, IconFilter, IconPlus, IconRefresh, IconSearch, IconSetting } from '@douyinfe/semi-icons'; import { Button, Card, CardGroup, Collapse, Descriptions, Empty, Input, Radio, Select, SideSheet, 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 } from 'react'; +import { FormEvent, memo, useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'; import { Link, useSearchParams } from 'react-router-dom'; import { api } from '../../api/client'; import type { AlertEvent, AlertQuery, AlertRule, AlertRuleInput, AlertStatus, MetricDefinition, Page } from '../../api/types'; @@ -17,6 +17,7 @@ import { WorkspaceCommandBar } from '../shared/WorkspaceCommandBar'; import { WorkspaceDetailSideSheet } from '../shared/WorkspaceDetailSideSheet'; import { WorkspacePanelHeader } from '../shared/WorkspacePanelHeader'; import { WorkspaceFilterPanel } from '../shared/WorkspaceFilterPanel'; +import { WorkspaceSideSheet } from '../shared/WorkspaceSideSheet'; import { detailTriggerRow } from '../shared/detailTriggerRow'; import { usePlatformSession } from '../auth/AuthGate'; import { canAdminister, canOperate } from '../auth/session'; @@ -369,6 +370,13 @@ function ruleDraft(rule: AlertRule): AlertRuleInput { }; } +function RuleFormSection({ id, title, description, children }: { id: string; title: string; description: string; children: ReactNode }) { + return
+
{title}{description}
+
{children}
+
; +} + function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: MetricDefinition[] }) { const queryClient = useQueryClient(); const mobileLayout = useMobileLayout(); @@ -378,7 +386,6 @@ function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: Metri useEffect(() => { if (!selectedID && rules[0]) { setSelectedID(rules[0].id); setDraft(ruleDraft(rules[0])); } }, [rules, selectedID]); const save = useMutation({ mutationFn: api.saveAlertRuleV2, onSuccess: async (rule) => { setSelectedID(rule.id); setDraft(ruleDraft(rule)); await queryClient.invalidateQueries({ queryKey: ['alert-rules-v2'] }); } }); const toggle = useMutation({ mutationFn: (rule: AlertRule) => api.setAlertRuleEnabledV2(rule.id, { version: rule.version, enabled: !rule.enabled }), onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['alert-rules-v2'] }); } }); - useSideSheetA11y(mobileLayout && editorOpen, '.v2-alert-rule-editor-sidesheet', 'v2-alert-rule-editor', '告警规则编辑', '关闭告警规则编辑'); const mutationError = save.error ?? toggle.error; const operators = draft.valueType === 'boolean' ? BOOLEAN_OPERATORS : NUMERIC_OPERATORS; const availableMetrics = metrics.filter((metric) => metric.alertable && metric.valueType === draft.valueType); @@ -417,7 +424,6 @@ function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: Metri actions={toggleButton} /> :
- {draft.version ? draft.enabled ? '已启用' : '已停用' : '新规则'} {draft.version ? `v${draft.version} · 配置变更写入版本审计` : '保存后生成首个可审计版本'} {toggleButton} @@ -440,24 +446,30 @@ function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: Metri ; })} : null} -
- - - - {draft.operator === 'changed' ? : draft.valueType === 'boolean' ? } - {draft.operator === 'between' || draft.operator === 'outside' ? : null} - - - - - - - - -