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 f3dcbf42..9c4776f1 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 @@ -284,6 +284,46 @@ test('uses the shared Semi bottom SideSheet pattern for mobile advanced filters' expect(document.querySelector('.v2-alert-filter-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(72dvh, 620px)' }); }); +test('keeps mobile rule selection focused and opens editing in a Semi bottom SideSheet', async () => { + layout.mobile = true; + mocks.alertRulesV2.mockResolvedValue([alertRule()]); + mocks.metricCatalog.mockResolvedValue({ + metrics: [{ key: 'speed_kmh', label: '速度', unit: 'km/h', category: 'driving', valueType: 'numeric', protocols: ['JT808'], sourceFields: {}, searchable: true, chartable: true, alertable: true }], + asOf: '' + }); + mocks.alertNotificationsV2.mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 }); + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + render(); + + const ruleItem = await screen.findByRole('button', { name: /测试超速规则/ }); + await waitFor(() => expect(ruleItem).toHaveAttribute('aria-pressed', 'true')); + expect(screen.getByRole('region', { name: '告警规则列表' })).toBeInTheDocument(); + expect(ruleItem).toHaveAttribute('aria-haspopup', 'dialog'); + expect(ruleItem).toHaveAttribute('aria-expanded', 'false'); + expect(document.querySelector('.v2-alert-rule-editor')).not.toBeInTheDocument(); + + 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 .semi-sidesheet-inner')).toHaveStyle({ height: 'min(92dvh, 820px)' }); + expect(within(editor).getByDisplayValue('测试超速规则')).toBeInTheDocument(); + expect(within(editor).getByRole('button', { name: '保存规则' })).toBeInTheDocument(); + expect(ruleItem).toHaveAttribute('aria-expanded', 'true'); + + fireEvent.click(within(editor).getByRole('button', { name: '关闭告警规则编辑' })); + await waitFor(() => expect(document.querySelector('.v2-alert-rule-editor')).not.toBeInTheDocument()); + expect(document.querySelector('.v2-alert-rule-editor-sidesheet .semi-sidesheet-inner')).toHaveClass('semi-sidesheet-animation-content_hide_bottom'); + expect(ruleItem).toHaveAttribute('aria-expanded', 'false'); + + const newRule = screen.getByRole('button', { name: /新建规则/ }); + expect(newRule).toHaveAttribute('aria-haspopup', 'dialog'); + fireEvent.click(newRule); + await waitFor(() => expect(document.querySelector('.v2-alert-rule-editor')).toBeInTheDocument()); + const newEditor = screen.getByRole('dialog', { name: '告警规则编辑' }); + expect(within(newEditor).getByText('新建规则')).toBeInTheDocument(); + expect(newRule).toHaveAttribute('aria-expanded', 'true'); +}); + test('creates auditable drafts from simple offline and hydrogen rule templates', async () => { mocks.alertRulesV2.mockResolvedValue([alertRule()]); mocks.metricCatalog.mockResolvedValue({ 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 eea84c32..6522a486 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/AlertsPage.tsx @@ -309,37 +309,115 @@ function ruleDraft(rule: AlertRule): AlertRuleInput { function RulesWorkspace({ rules, metrics }: { rules: AlertRule[]; metrics: MetricDefinition[] }) { const queryClient = useQueryClient(); + const mobileLayout = useMobileLayout(); const [selectedID, setSelectedID] = useState(''); const [draft, setDraft] = useState(emptyRule()); + const [editorOpen, setEditorOpen] = useState(false); 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); const catalogLabels = Object.fromEntries(metrics.map((metric) => [metric.key, metric.label])); const setList = (key: 'scopeProtocols' | 'scopeVins' | 'scopeOems' | 'scopeModels' | 'scopeCompanies', value: string) => setDraft({ ...draft, [key]: value.split(',').map((item) => item.trim()).filter(Boolean) }); + const openNewRule = () => { + setSelectedID('__new__'); + setDraft(emptyRule()); + if (mobileLayout) setEditorOpen(true); + }; + const openRule = (rule: AlertRule) => { + setSelectedID(rule.id); + setDraft(ruleDraft(rule)); + if (mobileLayout) setEditorOpen(true); + }; const applyTemplate = (template: RuleTemplate) => { setSelectedID('__new__'); setDraft({ ...template.draft, scopeProtocols: [...template.draft.scopeProtocols], notificationChannels: [...template.draft.notificationChannels] }); }; const severityColor = (severity: AlertRule['severity']) => severity === 'critical' ? 'red' : severity === 'major' ? 'orange' : 'amber'; - return
+ const toggleButton = draft.version ? : null; + const editor = +
{ event.preventDefault(); save.mutate(draft); }}> + {!mobileLayout ? :
+ + {draft.version ? draft.enabled ? '已启用' : '已停用' : '新规则'} + {draft.version ? `v${draft.version} · 配置变更写入版本审计` : '保存后生成首个可审计版本'} + + {toggleButton} +
} +
+
快速创建从常用业务场景开始,再按车辆、车型或企业收窄范围。
+ +
+
+ + + + {draft.operator === 'changed' ? : draft.valueType === 'boolean' ? } + {draft.operator === 'between' || draft.operator === 'outside' ? : null} + + + + + + + + +