对齐 Vue 后台惯例: - src/api/ 域 API(原 services/api) - src/utils/request.ts HTTP 封装(原 services/http.ts) - src/utils/export.ts 客户端导出工具 - src/utils/index.ts 工具统一出口 全局导入改为 @/api 与 @/utils/request,tsc + build 通过。
227 lines
8.5 KiB
TypeScript
227 lines
8.5 KiB
TypeScript
// 告警静默规则管理 Tab — 列表 + 创建/编辑/删除/启停 // Logan.Li
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { VolumeX, Plus, RefreshCw, Clock, Calendar, Repeat, Edit, Trash2 } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import { toast } from 'sonner';
|
|
import { useConfirm } from '@/components/ui/confirm-dialog';
|
|
import { alertCenterApi } from '@/api';
|
|
import { SilenceDialog } from './SilenceDialog';
|
|
|
|
const SILENCE_TYPE_LABELS: Record<string, string> = {
|
|
onetime: '一次性',
|
|
daily: '每天',
|
|
weekly: '每周',
|
|
};
|
|
|
|
const WEEKDAY_LABELS = ['日', '一', '二', '三', '四', '五', '六'];
|
|
|
|
function formatTime(t: string | Date): string {
|
|
if (!t) return '—';
|
|
const d = typeof t === 'string' ? new Date(t) : t;
|
|
if (isNaN(d.getTime())) return '—';
|
|
return d.toLocaleString('zh-CN', {
|
|
month: '2-digit', day: '2-digit',
|
|
hour: '2-digit', minute: '2-digit',
|
|
});
|
|
}
|
|
|
|
const SilenceRuleTab: React.FC = () => {
|
|
const confirm = useConfirm();
|
|
const [silences, setSilences] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [editing, setEditing] = useState<any>(null);
|
|
|
|
const fetchSilences = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res: Record<string, any> = await alertCenterApi.getSilences();
|
|
const list = res?.list || res?.data?.list || [];
|
|
setSilences(list);
|
|
} catch {
|
|
toast.error('获取静默规则失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetchSilences();
|
|
}, [fetchSilences]);
|
|
|
|
const handleAdd = () => {
|
|
setEditing(null);
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
const handleEdit = (silence: any) => {
|
|
setEditing(silence);
|
|
setDialogOpen(true);
|
|
};
|
|
|
|
const handleDelete = async (silence: any) => {
|
|
const ok = await confirm({
|
|
title: '删除静默规则',
|
|
description: `确定删除「${silence.name}」?删除后该规则匹配的告警将恢复通知。`,
|
|
confirmText: '删除',
|
|
variant: 'destructive',
|
|
});
|
|
if (!ok) return;
|
|
try {
|
|
await alertCenterApi.deleteSilence(silence.id);
|
|
toast.success('已删除');
|
|
fetchSilences();
|
|
} catch {
|
|
toast.error('删除失败');
|
|
}
|
|
};
|
|
|
|
const handleToggle = async (silence: any) => {
|
|
try {
|
|
await alertCenterApi.toggleSilence(silence.id);
|
|
toast.success(silence.status === 1 ? '已禁用' : '已启用');
|
|
fetchSilences();
|
|
} catch {
|
|
toast.error('操作失败');
|
|
}
|
|
};
|
|
|
|
const isCurrentlyActive = (s: any): boolean => {
|
|
if (s.is_active !== undefined) return s.is_active;
|
|
if (s.status !== 1) return false;
|
|
const now = new Date();
|
|
const st = new Date(s.start_time);
|
|
const et = new Date(s.end_time);
|
|
return now >= st && now <= et;
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* 操作栏 */}
|
|
<div className="flex justify-between items-center">
|
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
<VolumeX className="w-4 h-4" />
|
|
<span>共 {silences.length} 条规则</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button variant="outline" size="sm" onClick={fetchSilences} disabled={loading}>
|
|
<RefreshCw className="w-3.5 h-3.5 mr-1" />
|
|
刷新
|
|
</Button>
|
|
<Button size="sm" onClick={handleAdd}>
|
|
<Plus className="w-3.5 h-3.5 mr-1" />
|
|
添加静默规则
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 规则列表 */}
|
|
{loading && silences.length === 0 ? (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<p className="text-sm">加载中...</p>
|
|
</div>
|
|
) : silences.length === 0 ? (
|
|
<div className="text-center py-12 text-muted-foreground border border-dashed rounded-lg">
|
|
<VolumeX className="w-10 h-10 mx-auto mb-2 opacity-50" />
|
|
<p className="text-sm">暂无静默规则</p>
|
|
<p className="text-xs mt-1">点击「添加静默规则」创建维护窗口或免打扰时段</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 gap-3">
|
|
{silences.map((s) => {
|
|
const active = isCurrentlyActive(s);
|
|
const enabled = s.status === 1;
|
|
const sType = s.silence_type || 'onetime';
|
|
const weekdayList: number[] = s.weekday_list || (() => {
|
|
try { return JSON.parse(s.weekdays || '[]'); } catch { return []; }
|
|
})();
|
|
return (
|
|
<Card key={s.id} className={active ? 'border-primary/50' : ''}>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-start justify-between gap-3">
|
|
{/* 左侧信息 */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1.5 flex-wrap">
|
|
<span className="font-medium text-sm">{s.name}</span>
|
|
{/* 类型 Badge */}
|
|
<Badge variant="outline" className="text-xs">
|
|
{sType === 'onetime' && <Clock className="w-3 h-3 mr-1" />}
|
|
{sType === 'daily' && <Repeat className="w-3 h-3 mr-1" />}
|
|
{sType === 'weekly' && <Calendar className="w-3 h-3 mr-1" />}
|
|
{SILENCE_TYPE_LABELS[sType] || sType}
|
|
</Badge>
|
|
{/* 活跃状态 */}
|
|
{active && (
|
|
<Badge className="bg-green-500 text-xs">生效中</Badge>
|
|
)}
|
|
{!enabled && (
|
|
<Badge variant="secondary" className="text-xs">已禁用</Badge>
|
|
)}
|
|
</div>
|
|
|
|
{s.description && (
|
|
<p className="text-xs text-muted-foreground mb-1.5">{s.description}</p>
|
|
)}
|
|
|
|
{/* 时间窗口 */}
|
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
|
<span>
|
|
{sType === 'onetime'
|
|
? `${formatTime(s.start_time)} — ${formatTime(s.end_time)}`
|
|
: `每日 ${formatTime(s.start_time)} — ${formatTime(s.end_time)}`
|
|
}
|
|
</span>
|
|
{sType === 'weekly' && weekdayList.length > 0 && (
|
|
<span className="flex items-center gap-1">
|
|
{weekdayList.sort((a,b)=>a-b).map(d => (
|
|
<span key={d} className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-primary/10 text-primary text-[10px]">
|
|
{WEEKDAY_LABELS[d]}
|
|
</span>
|
|
))}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* 创建时间 */}
|
|
<div className="text-[10px] text-muted-foreground/60 mt-1">
|
|
创建于 {formatTime(s.created_at)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 右侧操作 */}
|
|
<div className="flex items-center gap-2 shrink-0">
|
|
<Switch
|
|
checked={enabled}
|
|
onCheckedChange={() => handleToggle(s)}
|
|
/>
|
|
<Button variant="ghost" size="sm" className="h-7 text-xs" onClick={() => handleEdit(s)}>
|
|
<Edit className="w-3 h-3 mr-1" />编辑
|
|
</Button>
|
|
<Button variant="ghost" size="sm" className="h-7 text-xs text-red-400" onClick={() => handleDelete(s)}>
|
|
<Trash2 className="w-3 h-3 mr-1" />删除
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* 创建/编辑弹框 */}
|
|
<SilenceDialog
|
|
open={dialogOpen}
|
|
onOpenChange={setDialogOpen}
|
|
editingSilence={editing}
|
|
onSaved={fetchSilences}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SilenceRuleTab;
|