// 告警集合通知策略 — 四类监控绑定联系组 + 消息模版 // Logan.Li import { useState, useEffect, useCallback, useMemo } from 'react'; import { Server, Database, Globe, Network, Bell, Zap, Loader2, Save, FileText } from 'lucide-react'; import { SilenceRulesPanel } from './SilenceRulesPanel'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { PageLoading } from '@/components/layout/PageLoading'; import { alertApi } from '@/api'; import type { Contact, ContactGroup } from './alert-center-helpers'; export interface NotifyPolicy { id: number; category: 'host' | 'middleware' | 'site' | 'snmp'; label?: string; enabled: boolean; contact_group_ids?: string; contact_ids?: string; notify_mode?: string; description?: string; } import type { NotifyMessageSettings } from '@/api/monitor/alert'; type PolicyDraft = { enabled: boolean; groupIds: number[]; contactIds: number[] }; const CATEGORIES = [ { id: 'host' as const, label: '主机监控', desc: 'Agent 采集 CPU/内存/磁盘/离线等', icon: Server }, { id: 'middleware' as const, label: '中间件监控', desc: 'Redis/MySQL 等指标告警', icon: Database }, { id: 'site' as const, label: '站点拨测', desc: 'HTTP/TCP/DNS 拨测失败与延迟', icon: Globe }, { id: 'snmp' as const, label: 'SNMP 监控', desc: 'H3C/华为/Cisco/锐捷/Dell/浪潮', icon: Network }, ]; const DEFAULT_TITLE = '[{{severity_label}}] {{rule_name}} — {{hostname}} ({{metric}})'; const DEFAULT_FOOTER = '{{time}} | {{platform}}'; const DEFAULT_PLATFORM = 'AIOps 智能运维平台'; const TEMPLATE_VARS = [ '{{time}}', '{{platform}}', '{{rule_name}}', '{{hostname}}', '{{metric}}', '{{severity}}', '{{severity_label}}', ]; function parseIDs(raw?: string): number[] { if (!raw || raw === 'null') return []; try { const arr = JSON.parse(raw); return Array.isArray(arr) ? arr.map(Number).filter(n => !isNaN(n)) : []; } catch { return []; } } function draftKey(d: PolicyDraft) { return `${d.enabled}|${d.groupIds.join(',')}|${d.contactIds.join(',')}`; } export function NotifyPolicyTab() { const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(null); const [testCategory, setTestCategory] = useState('host'); const [contacts, setContacts] = useState([]); const [groups, setGroups] = useState([]); const [drafts, setDrafts] = useState>({}); const [savedDraftKeys, setSavedDraftKeys] = useState>({}); const [msgSettings, setMsgSettings] = useState({ title_template: DEFAULT_TITLE, footer_template: DEFAULT_FOOTER, platform_name: DEFAULT_PLATFORM, }); const [savedMsgSettings, setSavedMsgSettings] = useState({ title_template: DEFAULT_TITLE, footer_template: DEFAULT_FOOTER, platform_name: DEFAULT_PLATFORM, }); const load = useCallback(async () => { setLoading(true); try { const [policyRes, contactRes, groupRes, msgRes] = await Promise.all([ alertApi.getNotifyPolicies(), alertApi.getContacts(), alertApi.getContactGroups(), alertApi.getNotifyMessageSettings(), ]); const list = (policyRes as { list?: NotifyPolicy[] })?.list || []; setContacts((contactRes as { list?: Contact[] })?.list || []); setGroups((groupRes as { list?: ContactGroup[] })?.list || []); const next: Record = {}; const keys: Record = {}; for (const p of list) { const d = { enabled: p.enabled, groupIds: parseIDs(p.contact_group_ids), contactIds: parseIDs(p.contact_ids), }; next[p.category] = d; keys[p.category] = draftKey(d); } for (const c of CATEGORIES) { if (!next[c.id]) { const d = { enabled: false, groupIds: [], contactIds: [] }; next[c.id] = d; keys[c.id] = draftKey(d); } } setDrafts(next); setSavedDraftKeys(keys); const msg = (msgRes || {}) as NotifyMessageSettings; const merged = { title_template: msg.title_template || DEFAULT_TITLE, footer_template: msg.footer_template || DEFAULT_FOOTER, platform_name: msg.platform_name || DEFAULT_PLATFORM, }; setMsgSettings(merged); setSavedMsgSettings(merged); } catch { toast.error('加载通知策略失败'); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); const policiesDirty = useMemo(() => CATEGORIES.some(c => draftKey(drafts[c.id] || { enabled: false, groupIds: [], contactIds: [] }) !== savedDraftKeys[c.id]), [drafts, savedDraftKeys]); const templateDirty = useMemo(() => msgSettings.title_template !== savedMsgSettings.title_template || msgSettings.footer_template !== savedMsgSettings.footer_template || msgSettings.platform_name !== savedMsgSettings.platform_name, [msgSettings, savedMsgSettings]); const isDirty = policiesDirty || templateDirty; const footerPreview = useMemo(() => { const now = new Date().toLocaleString('zh-CN', { hour12: false }).replace(/\//g, '-'); return (msgSettings.footer_template || DEFAULT_FOOTER) .replace('{{time}}', now) .replace('{{platform}}', msgSettings.platform_name || DEFAULT_PLATFORM); }, [msgSettings]); const toggleGroup = (cat: string, gid: number) => { setDrafts(prev => { const d = prev[cat] || { enabled: false, groupIds: [], contactIds: [] }; const groupIds = d.groupIds.includes(gid) ? d.groupIds.filter(id => id !== gid) : [...d.groupIds, gid]; return { ...prev, [cat]: { ...d, groupIds } }; }); }; const toggleContact = (cat: string, cid: number) => { setDrafts(prev => { const d = prev[cat] || { enabled: false, groupIds: [], contactIds: [] }; const contactIds = d.contactIds.includes(cid) ? d.contactIds.filter(id => id !== cid) : [...d.contactIds, cid]; return { ...prev, [cat]: { ...d, contactIds } }; }); }; const handleSaveAll = async () => { setSaving(true); try { await Promise.all([ ...CATEGORIES.map(c => { const d = drafts[c.id]; if (!d) return Promise.resolve(); return alertApi.upsertNotifyPolicy(c.id, { enabled: d.enabled, contact_group_ids: d.groupIds, contact_ids: d.contactIds, }); }), alertApi.saveNotifyMessageSettings(msgSettings), ]); toast.success('通知配置已保存'); load(); } catch (e) { toast.error((e as { message?: string }).message || '保存失败'); } finally { setSaving(false); } }; const handleTest = async (category: string) => { setTesting(category); try { await alertApi.testNotifyPolicy(category); toast.success('测试通知已发送'); } catch (e) { toast.error((e as { message?: string }).message || '测试失败,请检查策略与联系人'); } finally { setTesting(null); } }; if (loading) return ; return (
{/* 顶栏:说明 + 统一保存 / 测试 */}

按监控类型配置默认通知对象;消息模版作用于飞书卡片标题与底部署名。

{groups.length === 0 && contacts.length === 0 && (
请先在「报警联系人」和「报警联系组」中创建通知对象。
)} {/* 消息模版 */} 消息通知模版
setMsgSettings(s => ({ ...s, title_template: e.target.value }))} placeholder={DEFAULT_TITLE} />
setMsgSettings(s => ({ ...s, platform_name: e.target.value }))} placeholder={DEFAULT_PLATFORM} />
setMsgSettings(s => ({ ...s, footer_template: e.target.value }))} placeholder={DEFAULT_FOOTER} />

预览:{footerPreview}

{TEMPLATE_VARS.map(v => ( {v} ))}
{/* 四类通知策略 */}
{CATEGORIES.map(cat => { const Icon = cat.icon; const d = drafts[cat.id] || { enabled: false, groupIds: [], contactIds: [] }; const memberCount = d.groupIds.length + d.contactIds.length; const catDirty = draftKey(d) !== savedDraftKeys[cat.id]; return (
{cat.label} {catDirty && 未保存}

{cat.desc}

setDrafts(prev => ({ ...prev, [cat.id]: { ...d, enabled: v }, }))} />
{groups.length === 0 ? (

暂无联系组

) : (
{groups.map(g => ( ))}
)}
{contacts.length === 0 ? (

暂无联系人

) : (
{contacts.map(c => ( ))}
)}
{memberCount > 0 && ( 已选 {d.groupIds.length} 组 + {d.contactIds.length} 人 )}
); })}
未配置策略时将回退到「通知渠道」中的全局飞书渠道。
); }