feat: 网络双线图+告警联系人/联系组+Agent v2.0.0 + 一键报警阈值

- HostMonitor: 网络改为双线图 rx/tx 网卡带宽吞吐(双Area线), 移除单rx图
- Agent: 默认版本号从 dev 改为 v2.0.0, 重建二进制 5.2MB
- 告警中心: 新增报警联系人/报警联系组 Tab, ContactDialog/ContactGroupDialog
- 后端: AlertContact/AlertContactGroup 模型+CRUD, monitor_alert_contacts/groups 表
- 路由: /alerts/contacts + /alerts/contact-groups 8个REST端点
- 一键报警: 更新描述为6条规则(含TCP+进程), 切换到 /alerts/quick-setup
- 测试规则: 内存>10% 触发告警已创建, 飞书通知验证通过
This commit is contained in:
attacker committed 2026-06-24 08:47:40 +00:00
1 parent 5333f6f04a
commit e1cfbaf45e
11 files changed
+810 -16

No files matched your search

@@ -7,7 +7,7 @@ import {
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select';
@@ -15,13 +15,16 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Switch } from '@/components/ui/switch';
import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { alertCenterApi, agentApi } from '@/services/api';
import { alertCenterApi, agentApi, alertApi } from '@/services/api';
import {
type Alert, type AlertRule, mapAlert, getSeverityBadge, getStatusBadge,
} from './alert-center-helpers';
import { AlertDetailDialog, AlertSilenceDialog } from './AlertCenterDetail';
import { AlertChannelsTab } from './AlertChannelsTab';
import { AlertRuleDialog } from '@/components/monitor/AlertRuleDialog';
import { ContactDialog } from './ContactDialog';
import { ContactGroupDialog } from './ContactGroupDialog';
import type { Contact, ContactGroup } from './alert-center-helpers';
const AlertCenter: React.FC = () => {
const _confirm = useConfirm();
@@ -40,6 +43,13 @@ const AlertCenter: React.FC = () => {
const [ruleDialogOpen, setRuleDialogOpen] = useState(false);
const [editingRule, setEditingRule] = useState<any>(null);
const [quickSetupLoading, setQuickSetupLoading] = useState(false);
// 联系人 & 联系组
const [contacts, setContacts] = useState<Contact[]>([]);
const [contactGroups, setContactGroups] = useState<ContactGroup[]>([]);
const [contactDialogOpen, setContactDialogOpen] = useState(false);
const [editingContact, setEditingContact] = useState<Contact | null>(null);
const [groupDialogOpen, setGroupDialogOpen] = useState(false);
const [editingGroup, setEditingGroup] = useState<ContactGroup | null>(null);
// 分页
const [page, setPage] = useState(1);
const [pageSize] = useState(10);
@@ -48,9 +58,11 @@ const AlertCenter: React.FC = () => {
useEffect(() => {
fetchData();
if (activeTab === 'contacts') fetchContacts();
if (activeTab === 'groups') { fetchContacts(); fetchContactGroups(); }
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, [page, severityFilter, statusFilter]);
}, [page, severityFilter, statusFilter, activeTab]);
const fetchData = async () => {
await Promise.all([fetchAlerts(), fetchRules()]);
@@ -103,6 +115,20 @@ const AlertCenter: React.FC = () => {
}
};
const fetchContacts = async () => {
try {
const res: any = await alertApi.getContacts();
setContacts((res?.data?.list || res?.list || []) as Contact[]);
} catch { /* silent */ }
};
const fetchContactGroups = async () => {
try {
const res: any = await alertApi.getContactGroups();
setContactGroups((res?.data?.list || res?.list || []) as ContactGroup[]);
} catch { /* silent */ }
};
const handleAcknowledge = async (id: string) => {
try {
await alertCenterApi.acknowledge(id);
@@ -151,13 +177,13 @@ const AlertCenter: React.FC = () => {
const handleQuickSetup = async () => {
const ok = await _confirm({
title: '一键报警',
description: '将为 Agent 在线的全部主机创建 CPU使用率、内存使用率、磁盘使用率、网络带宽使用率 四条默认告警规则(阈值 80%,连续 5 次触发)。',
description: '为 Agent 在线主机创建 CPU(>80%)/内存(>80%)/磁盘(>90%)/TIME_WAIT(>15000)/CLOSE_WAIT(>200)/进程检测 6 条告警规则。',
confirmText: '确认创建',
});
if (!ok) return;
setQuickSetupLoading(true);
try {
const res = await agentApi.quickSetup('ecs');
const res = await alertCenterApi.quickSetup('ecs');
toast.success(typeof res === 'string' ? res : (res?.message || '默认告警规则创建成功'));
fetchRules();
} catch {
@@ -236,10 +262,12 @@ const AlertCenter: React.FC = () => {
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3 lg:w-[360px] h-9">
<TabsList className="grid w-full grid-cols-5 lg:w-[600px] h-9">
<TabsTrigger value="alerts" className="text-xs">告警列表</TabsTrigger>
<TabsTrigger value="rules" className="text-xs">告警规则</TabsTrigger>
<TabsTrigger value="channels" className="text-xs">通知渠道</TabsTrigger>
<TabsTrigger value="contacts" className="text-xs">报警联系人</TabsTrigger>
<TabsTrigger value="groups" className="text-xs">报警联系组</TabsTrigger>
</TabsList>
{/* 告警列表 Tab */}
@@ -478,6 +506,121 @@ const AlertCenter: React.FC = () => {
<TabsContent value="channels">
<AlertChannelsTab refreshKey={0} />
</TabsContent>
{/* 报警联系人 Tab */}
<TabsContent value="contacts" className="space-y-4">
<div className="flex justify-between items-center">
<Button variant="outline" onClick={fetchContacts}>
<RefreshCw className="w-4 h-4 mr-2" />
刷新
</Button>
<Button onClick={() => { setEditingContact(null); setContactDialogOpen(true); }}>
<Plus className="w-4 h-4 mr-2" />
添加联系人
</Button>
</div>
{contacts.length === 0 ? (
<div className="text-center py-16 text-muted-foreground border border-dashed rounded-lg">
暂无报警联系人,点击「添加联系人」创建
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{contacts.map((c) => (
<Card key={c.id} className="hover:border-primary/50 transition-colors">
<CardHeader>
<div className="flex items-start justify-between">
<CardTitle className="text-base">{c.name}</CardTitle>
</div>
</CardHeader>
<CardContent className="pt-0 space-y-1 text-sm text-muted-foreground">
{c.phone && <div>📱 {c.phone}</div>}
{c.email && <div>📧 {c.email}</div>}
{c.dingtalk && <div>🔷 钉钉已配置</div>}
{c.feishu && <div>🟢 飞书已配置</div>}
{c.wechat && <div>💚 企业微信已配置</div>}
<div className="flex gap-2 pt-2">
<Button variant="outline" size="sm" className="flex-1"
onClick={() => { setEditingContact(c); setContactDialogOpen(true); }}>
编辑
</Button>
<Button variant="outline" size="sm"
onClick={async () => {
const ok = await _confirm({ title: '删除联系人', description: `确定删除联系人「${c.name}」?`, confirmText: '删除', variant: 'destructive' });
if (!ok) return;
try { await alertApi.deleteContact(c.id); toast.success('已删除'); fetchContacts(); }
catch { toast.error('删除失败'); }
}}>
删除
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</TabsContent>
{/* 报警联系组 Tab */}
<TabsContent value="groups" className="space-y-4">
<div className="flex justify-between items-center">
<Button variant="outline" onClick={fetchContactGroups}>
<RefreshCw className="w-4 h-4 mr-2" />
刷新
</Button>
<Button onClick={() => { setEditingGroup(null); setGroupDialogOpen(true); }}>
<Plus className="w-4 h-4 mr-2" />
添加联系组
</Button>
</div>
{contactGroups.length === 0 ? (
<div className="text-center py-16 text-muted-foreground border border-dashed rounded-lg">
暂无报警联系组,点击「添加联系组」创建
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{contactGroups.map((g) => {
let cids: number[] = [];
try { cids = JSON.parse(g.contact_ids || '[]'); } catch { /* ignore */ }
const memberNames = contacts
.filter(c => cids.includes(c.id))
.map(c => c.name);
return (
<Card key={g.id} className="hover:border-primary/50 transition-colors">
<CardHeader>
<div className="flex items-start justify-between">
<CardTitle className="text-base">{g.group_name}</CardTitle>
<Badge variant="secondary">{cids.length} 人</Badge>
</div>
</CardHeader>
<CardContent className="pt-0 space-y-2">
{g.notes && <p className="text-sm text-muted-foreground">{g.notes}</p>}
<div className="flex flex-wrap gap-1">
{memberNames.map(name => (
<Badge key={name} variant="outline" className="text-xs">{name}</Badge>
))}
</div>
<div className="flex gap-2 pt-2">
<Button variant="outline" size="sm" className="flex-1"
onClick={() => { setEditingGroup(g); setGroupDialogOpen(true); }}>
编辑
</Button>
<Button variant="outline" size="sm"
onClick={async () => {
const ok = await _confirm({ title: '删除联系组', description: `确定删除联系组「${g.group_name}」?`, confirmText: '删除', variant: 'destructive' });
if (!ok) return;
try { await alertApi.deleteContactGroup(g.id); toast.success('已删除'); fetchContactGroups(); }
catch { toast.error('删除失败'); }
}}>
删除
</Button>
</div>
</CardContent>
</Card>
);
})}
</div>
)}
</TabsContent>
</Tabs>
<AlertDetailDialog
@@ -499,6 +642,19 @@ const AlertCenter: React.FC = () => {
notifyChannels={[]}
onSaved={() => { handleRuleDialogClose(); fetchRules(); }}
/>
<ContactDialog
open={contactDialogOpen}
onOpenChange={setContactDialogOpen}
editingContact={editingContact}
onSaved={fetchContacts}
/>
<ContactGroupDialog
open={groupDialogOpen}
onOpenChange={setGroupDialogOpen}
editingGroup={editingGroup}
contacts={contacts}
onSaved={() => { fetchContactGroups(); }}
/>
</div>
);
};
@@ -0,0 +1,143 @@
// 报警联系人创建/编辑对话框 // Logan.Li
import React, { useState, useEffect } from 'react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { toast } from 'sonner';
import { alertApi } from '@/services/api';
import type { Contact } from './alert-center-helpers';
export function ContactDialog({
open,
onOpenChange,
editingContact,
onSaved,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
editingContact: Contact | null;
onSaved: () => void;
}) {
const [form, setForm] = useState({
name: '', phone: '', email: '', dingtalk: '', feishu: '', wechat: '',
});
const [saving, setSaving] = useState(false);
useEffect(() => {
if (open) {
if (editingContact) {
setForm({
name: editingContact.name || '',
phone: editingContact.phone || '',
email: editingContact.email || '',
dingtalk: editingContact.dingtalk || '',
feishu: editingContact.feishu || '',
wechat: editingContact.wechat || '',
});
} else {
setForm({ name: '', phone: '', email: '', dingtalk: '', feishu: '', wechat: '' });
}
}
}, [open, editingContact]);
const handleSave = async () => {
if (!form.name.trim()) return;
setSaving(true);
try {
const payload = {
name: form.name.trim(),
phone: form.phone.trim() || undefined,
email: form.email.trim() || undefined,
dingtalk: form.dingtalk.trim() || undefined,
feishu: form.feishu.trim() || undefined,
wechat: form.wechat.trim() || undefined,
};
if (editingContact) {
await alertApi.updateContact(editingContact.id, payload);
toast.success('联系人已更新');
} else {
await alertApi.createContact(payload);
toast.success('联系人已添加');
}
onOpenChange(false);
onSaved();
} catch {
toast.error('保存失败');
} finally {
setSaving(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{editingContact ? '编辑报警联系人' : '添加报警联系人'}</DialogTitle>
<DialogDescription>配置报警联系人信息(姓名必填,渠道选填)</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>姓名 *</Label>
<Input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="如:张三"
/>
</div>
<div className="space-y-2">
<Label>手机号</Label>
<Input
value={form.phone}
onChange={(e) => setForm({ ...form, phone: e.target.value })}
placeholder="如:13800138000"
/>
</div>
<div className="space-y-2">
<Label>邮箱</Label>
<Input
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
placeholder="如:zhangsan@example.com"
/>
</div>
<div className="space-y-2">
<Label>钉钉(webhook URL)</Label>
<Input
value={form.dingtalk}
onChange={(e) => setForm({ ...form, dingtalk: e.target.value })}
placeholder="https://oapi.dingtalk.com/robot/send?access_token=..."
className="font-mono text-sm"
/>
</div>
<div className="space-y-2">
<Label>飞书(webhook URL)</Label>
<Input
value={form.feishu}
onChange={(e) => setForm({ ...form, feishu: e.target.value })}
placeholder="https://open.feishu.cn/open-apis/bot/v2/hook/..."
className="font-mono text-sm"
/>
</div>
<div className="space-y-2">
<Label>企业微信(webhook URL)</Label>
<Input
value={form.wechat}
onChange={(e) => setForm({ ...form, wechat: e.target.value })}
placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=..."
className="font-mono text-sm"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
<Button onClick={handleSave} disabled={!form.name.trim() || saving}>
{saving ? '保存中...' : editingContact ? '保存修改' : '添加联系人'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,141 @@
// 报警联系组创建/编辑对话框 // Logan.Li
import React, { useState, useEffect } from 'react';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner';
import { alertApi } from '@/services/api';
import type { Contact, ContactGroup } from './alert-center-helpers';
export function ContactGroupDialog({
open,
onOpenChange,
editingGroup,
contacts,
onSaved,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
editingGroup: ContactGroup | null;
contacts: Contact[];
onSaved: () => void;
}) {
const [form, setForm] = useState({ group_name: '', notes: '', contact_ids: [] as number[] });
const [saving, setSaving] = useState(false);
useEffect(() => {
if (open) {
if (editingGroup) {
let existingIDs: number[] = [];
try {
existingIDs = JSON.parse(editingGroup.contact_ids || '[]');
} catch { /* ignore */ }
setForm({
group_name: editingGroup.group_name || '',
notes: editingGroup.notes || '',
contact_ids: existingIDs,
});
} else {
setForm({ group_name: '', notes: '', contact_ids: [] });
}
}
}, [open, editingGroup]);
const toggleContact = (id: number) => {
setForm(prev => ({
...prev,
contact_ids: prev.contact_ids.includes(id)
? prev.contact_ids.filter(cid => cid !== id)
: [...prev.contact_ids, id],
}));
};
const handleSave = async () => {
if (!form.group_name.trim()) return;
setSaving(true);
try {
const payload = {
group_name: form.group_name.trim(),
notes: form.notes.trim() || undefined,
contact_ids: form.contact_ids,
};
if (editingGroup) {
await alertApi.updateContactGroup(editingGroup.id, payload);
toast.success('联系组已更新');
} else {
await alertApi.createContactGroup(payload);
toast.success('联系组已添加');
}
onOpenChange(false);
onSaved();
} catch {
toast.error('保存失败');
} finally {
setSaving(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>{editingGroup ? '编辑报警联系组' : '添加报警联系组'}</DialogTitle>
<DialogDescription>配置报警联系组,选择组内成员</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>组名称 *</Label>
<Input
value={form.group_name}
onChange={(e) => setForm({ ...form, group_name: e.target.value })}
placeholder="如:运维值班组"
/>
</div>
<div className="space-y-2">
<Label>备注</Label>
<Textarea
value={form.notes}
onChange={(e) => setForm({ ...form, notes: e.target.value })}
placeholder="如:周一至周五 9:00-21:00"
rows={2}
/>
</div>
<div className="space-y-2">
<Label>选择联系人</Label>
{contacts.length === 0 ? (
<p className="text-xs text-muted-foreground">暂无联系人,请先在「报警联系人」标签页添加</p>
) : (
<div className="flex flex-wrap gap-1.5 max-h-[180px] overflow-y-auto border rounded-md p-2">
{contacts.map(c => (
<Badge
key={c.id}
variant={form.contact_ids.includes(c.id) ? 'default' : 'outline'}
className="cursor-pointer text-xs"
onClick={() => toggleContact(c.id)}
>
{c.name}
{form.contact_ids.includes(c.id) && ' ✓'}
</Badge>
))}
</div>
)}
{form.contact_ids.length > 0 && (
<p className="text-xs text-muted-foreground">已选择 {form.contact_ids.length} 位联系人</p>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
<Button onClick={handleSave} disabled={!form.group_name.trim() || saving}>
{saving ? '保存中...' : editingGroup ? '保存修改' : '添加联系组'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -55,6 +55,27 @@ export interface NotificationChannel {
test_status?: string;
}
export interface Contact {
id: number;
name: string;
phone?: string;
email?: string;
dingtalk?: string;
feishu?: string;
wechat?: string;
created_at?: string;
updated_at?: string;
}
export interface ContactGroup {
id: number;
group_name: string;
notes?: string;
contact_ids?: string;
created_at?: string;
updated_at?: string;
}
// Logan.Li — 后端字段映射到前端 Alert 接口
export function mapAlert(a: any): Alert {
return {
+81 -8
View File
@@ -272,14 +272,22 @@ export function HostMonitor() {
</div>
</TabsContent>
{/* 网络监控 — 增加 TIME_WAIT + FD 图表 */}
<TabsContent value="network" className="mt-3 grid grid-cols-1 md:grid-cols-3 gap-3">
<MetricChartCard title="TCP 连接数(EST)" value={currentVal('tcp')} icon={<Network className="h-4 w-4 text-purple-400" />}
data={metrics.tcpEst} color="#8b5cf6" unit="个" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title="TCP TIME_WAIT" value={currentVal('tw')} icon={<Clock className="h-4 w-4 text-amber-400" />}
data={metrics.tcpTW} color="#f59e0b" unit="个" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title="网络接收(KB/s)" value={currentVal('rx')} icon={<Network className="h-4 w-4 text-blue-400" />}
data={metrics.rx} color="#3b82f6" unit="KB/s" domain={[0, 'auto']} aggType={aggType} />
{/* 网络监控 — 双线网卡带宽 + TIME_WAIT + TCP连接 */}
<TabsContent value="network" className="mt-3 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<MetricChartCard title="TCP 连接数(EST)" value={currentVal('tcp')} icon={<Network className="h-4 w-4 text-purple-400" />}
data={metrics.tcpEst} color="#8b5cf6" unit="个" domain={[0, 'auto']} aggType={aggType} />
<MetricChartCard title="TCP TIME_WAIT" value={currentVal('tw')} icon={<Clock className="h-4 w-4 text-amber-400" />}
data={metrics.tcpTW} color="#f59e0b" unit="个" domain={[0, 'auto']} aggType={aggType} />
</div>
{/* 网卡带宽吞吐 — 双线 rx/tx */}
<DualLineChart
title="网卡带宽吞吐"
currentRx={netInfo?.net_rx_bps != null ? formatBPS(netInfo.net_rx_bps) : '--'}
currentTx={netInfo?.net_tx_bps != null ? formatBPS(netInfo.net_tx_bps) : '--'}
rxData={metrics.rx} txData={metrics.tx}
aggType={aggType} unit="KB/s"
/>
</TabsContent>
{/* ═══ 进程监控 — v2.0 新增 ═══ */}
@@ -505,3 +513,68 @@ function MetricChartCard({
</Card>
);
}
// ═══════ 双线图表(网卡带宽 rx+tx)═══════
function DualLineChart({
title, currentRx, currentTx, rxData, txData, aggType, unit,
}: {
title: string; currentRx: string; currentTx: string;
rxData: BucketPoint[]; txData: BucketPoint[];
aggType: AggType; unit: string;
}) {
// 合并 rx/tx 为统一时间序列
const chartData = rxData.map((d, i) => ({
time: d.time,
timestamp: d.timestamp,
rx: d[aggType] ?? 0,
tx: txData[i]?.[aggType] ?? 0,
}));
const rxGradId = `grad-rx-${title.replace(/\s/g, '')}`;
const txGradId = `grad-tx-${title.replace(/\s/g, '')}`;
return (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-3">
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-1.5">
<Network className="h-4 w-4 text-blue-400" />
<span className="text-xs text-[var(--dg-text-primary)] font-medium">{title}</span>
</div>
<div className="flex items-center gap-3 text-xs">
<span><span className="inline-block w-2.5 h-2.5 rounded-sm bg-blue-400 mr-1 align-middle" />接收 <b className="text-blue-300">{currentRx}</b></span>
<span><span className="inline-block w-2.5 h-2.5 rounded-sm bg-emerald-400 mr-1 align-middle" />发送 <b className="text-emerald-300">{currentTx}</b></span>
</div>
</div>
<div className="h-56">
{chartData.length === 0 ? (
<div className="h-full flex items-center justify-center text-xs text-[var(--dg-text-muted)] border border-dashed border-[var(--dg-border)] rounded-md">暂无数据</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 2, right: 2, bottom: 0, left: -10 }}>
<defs>
<linearGradient id={rxGradId} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
</linearGradient>
<linearGradient id={txGradId} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" strokeOpacity={0.5} />
<XAxis dataKey="time" hide />
<YAxis hide domain={[0, 'auto']} />
<Tooltip contentStyle={{ backgroundColor: '#0f172a', border: '1px solid #1e293b', borderRadius: '4px', color: '#f8fafc', fontSize: '11px', padding: '4px 8px' }}
formatter={(v: number, name: string) => [`${v.toFixed(2)} ${unit}`, name === 'rx' ? '接收' : '发送']}
labelFormatter={(label) => `时间: ${label}`} />
<Area type="monotone" dataKey="rx" stroke="#3b82f6" strokeWidth={1.5} fill={`url(#${rxGradId})`} name="rx" />
<Area type="monotone" dataKey="tx" stroke="#10b981" strokeWidth={1.5} fill={`url(#${txGradId})`} name="tx" />
<Brush dataKey="time" height={24} stroke="#334155" fill="#0f172a"
startIndex={Math.max(0, chartData.length - Math.min(20, chartData.length))} />
</AreaChart>
</ResponsiveContainer>
)}
</div>
</CardContent>
</Card>
);
}
+26
View File
@@ -576,6 +576,32 @@ export const alertApi = {
testNotifyChannel: async (id: number | string) => {
return http.post(`/alerts/notify-channels/${id}/test`);
},
// 报警联系人
getContacts: async () => {
return http.get('/alerts/contacts');
},
createContact: async (data: any) => {
return http.post('/alerts/contacts', data);
},
updateContact: async (id: number | string, data: any) => {
return http.put(`/alerts/contacts/${id}`, data);
},
deleteContact: async (id: number | string) => {
return http.delete(`/alerts/contacts/${id}`);
},
// 报警联系组
getContactGroups: async () => {
return http.get('/alerts/contact-groups');
},
createContactGroup: async (data: any) => {
return http.post('/alerts/contact-groups', data);
},
updateContactGroup: async (id: number | string, data: any) => {
return http.put(`/alerts/contact-groups/${id}`, data);
},
deleteContactGroup: async (id: number | string) => {
return http.delete(`/alerts/contact-groups/${id}`);
},
};
// ==================== 流水线 API ====================