// 站点拨测 — 任务列表 + 详情页可观测 // Logan.Li import { useState, useEffect, useCallback, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Plus, Trash2, Pencil, Loader2, RefreshCw, Activity, ShieldAlert, LineChart, } from 'lucide-react'; import { toast } from 'sonner'; import { siteApi } from '@/api'; import { useConfirm } from '@/components/ui/confirm-dialog'; import type { JsonData } from '@/types/common'; import { SiteProtocolPieChart } from './site-monitor/SiteProtocolPieChart'; import { SiteTaskDetailPage } from './site-monitor/SiteTaskDetailPage'; import { PROBE_TYPE_OPTIONS, type ProbeType, type SiteStats, type SiteTask, probeTypeMeta, probeFrequencyHint, defaultProbeFrequency, MIN_PROBE_FREQUENCY, fmtAvailResponse, fmtFrequency, isSslProbeTask, isSslExpiringSoon, normalizeProbeType, } from './site-monitor/helpers'; interface CreateTaskForm { name: string; url: string; type: ProbeType; frequency_sec: number; timeout_sec: number; expected_status_code: number; } const emptyForm: CreateTaskForm = { name: '', url: '', type: 'HTTP', frequency_sec: defaultProbeFrequency('HTTP'), timeout_sec: 10, expected_status_code: 200, }; const FILTER_TYPES: Array = ['ALL', ...PROBE_TYPE_OPTIONS]; function alertStatusBadge(status?: string) { if (status === 'alerting') { return 告警中; } return 正常; } export function SiteMonitor() { const [searchParams, setSearchParams] = useSearchParams(); const taskIdParam = searchParams.get('task'); const [stats, setStats] = useState(null); const [tasks, setTasks] = useState([]); const [initialLoading, setInitialLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [formOpen, setFormOpen] = useState(false); const [editingTaskId, setEditingTaskId] = useState(null); const [form, setForm] = useState({ ...emptyForm }); const [saving, setSaving] = useState(false); const [selectedType, setSelectedType] = useState('ALL'); const _confirm = useConfirm(); const detailTask = useMemo(() => { if (!taskIdParam) return null; const id = Number(taskIdParam); if (!Number.isFinite(id)) return null; return tasks.find((t) => t.id === id) || null; }, [taskIdParam, tasks]); const loadStats = useCallback(async () => { try { setStats(await siteApi.getStats() as SiteStats); } catch { /* ignore */ } }, []); const loadTasks = useCallback(async () => { setRefreshing(true); try { const data = await siteApi.getTasks(); const list = (data as JsonData)?.list as SiteTask[] || []; setTasks(list); } catch { /* ignore */ } finally { setRefreshing(false); setInitialLoading(false); } }, []); useEffect(() => { loadStats(); loadTasks(); }, [loadStats, loadTasks]); // 30s 静默刷新 // Logan.Li useEffect(() => { if (initialLoading && tasks.length === 0) return; const timer = setInterval(() => { loadStats(); loadTasks(); }, 30_000); return () => clearInterval(timer); }, [loadStats, loadTasks, initialLoading, tasks.length]); const typeCounts = useMemo(() => { const counts: Record = {}; for (const t of PROBE_TYPE_OPTIONS) counts[t] = 0; for (const t of tasks) { const key = normalizeProbeType(t.type); counts[key]++; } return counts; }, [tasks]); const filteredTasks = selectedType === 'ALL' ? tasks : tasks.filter((t) => normalizeProbeType(t.type) === selectedType); const openCreate = (type?: ProbeType) => { setEditingTaskId(null); const t = type || (selectedType !== 'ALL' ? selectedType : 'HTTP'); setForm({ ...emptyForm, type: t, frequency_sec: defaultProbeFrequency(t) }); setFormOpen(true); }; const openEdit = (task: SiteTask) => { setEditingTaskId(task.id); setForm({ name: task.name, url: task.url, type: normalizeProbeType(task.type), frequency_sec: task.frequency_sec || 60, timeout_sec: task.timeout_sec || 10, expected_status_code: task.expected_status_code || 200, }); setFormOpen(true); }; const openDetail = (task: SiteTask) => { setSearchParams({ tab: 'site', task: String(task.id) }); }; const closeDetail = () => { setSearchParams({ tab: 'site' }); }; const handleSave = async () => { if (!form.name.trim() || !form.url.trim()) { toast.error('名称和地址不能为空'); return; } setSaving(true); const payload = { name: form.name.trim(), url: form.url.trim(), type: form.type.toLowerCase(), frequency_sec: Number(form.frequency_sec) || 60, timeout_sec: Number(form.timeout_sec) || 10, expected_status_code: Number(form.expected_status_code) || 200, }; try { if (editingTaskId) { await siteApi.updateTask(editingTaskId, payload); toast.success('任务已更新'); } else { await siteApi.createTask(payload); toast.success('拨测任务已创建'); } setFormOpen(false); setEditingTaskId(null); loadTasks(); loadStats(); } catch (e) { toast.error((e as JsonData).message || (editingTaskId ? '更新失败' : '创建失败')); } finally { setSaving(false); } }; const handleDelete = async (task: SiteTask) => { const ok = await _confirm({ title: '删除拨测任务', description: `确定删除「${task.name}」吗?`, variant: 'destructive' }); if (!ok) return; try { await siteApi.deleteTask(task.id); if (taskIdParam === String(task.id)) { closeDetail(); } toast.success('任务已删除'); loadTasks(); loadStats(); } catch (e) { toast.error((e as JsonData).message || '删除失败'); } }; const handleToggle = async (task: SiteTask) => { try { await siteApi.toggleTask(task.id, !task.enabled); toast.success(task.enabled ? '已停用' : '已启用'); loadTasks(); loadStats(); } catch (e) { toast.error((e as JsonData).message || '操作失败'); } }; const targetMeta = probeTypeMeta[form.type]; const availStr = stats?.availability != null ? `${(stats.availability * 100).toFixed(1)}%` : '--'; const probeCountStr = stats?.probe_count != null ? String(stats.probe_count) : '--'; if (initialLoading) { return (
); } if (taskIdParam) { if (!detailTask) { return (

拨测任务不存在或已删除

); } return ( { loadTasks(); loadStats(); }} /> ); } return (

站点拨测

HTTP Web 拨测 · SSL 证书检测 · TCP / DNS / PING

{[ { label: '总任务', value: stats?.total_tasks ?? '--' }, { label: '正常', value: stats?.up_count ?? '--', color: 'dg-text-green' }, { label: '异常', value: stats?.down_count ?? '--', color: 'dg-text-red' }, { label: '可用率(24h)', value: availStr, color: 'dg-text-blue' }, { label: '探测次数', value: probeCountStr }, ].map((item) => (

{item.label}

{item.value}

))}

拨测任务

点击可观测进入详情页
{FILTER_TYPES.map((type) => { const count = type === 'ALL' ? tasks.length : typeCounts[type] || 0; const active = selectedType === type; return ( ); })}
{filteredTasks.length === 0 ? (
{tasks.length === 0 ? '暂无拨测任务' : '当前筛选无任务'}
) : (
名称 类型 频率 报警状态 可用率/响应时间 启用 {filteredTasks.map((task) => (
{task.name} {isSslProbeTask(task) && isSslExpiringSoon(task.ssl_days_remaining) && ( 证书 )}

{task.url}

{normalizeProbeType(task.type)} {fmtFrequency(task.frequency_sec)} {alertStatusBadge(task.alert_status)} {fmtAvailResponse(task)} e.stopPropagation()}> handleToggle(task)} />
))}
)}
{ setFormOpen(open); if (!open) setEditingTaskId(null); }}> {editingTaskId ? '编辑拨测任务' : '添加拨测任务'} 按协议类型配置探活任务,系统将按间隔自动拨测
setForm({ ...form, name: e.target.value })} />

{targetMeta.hint}

setForm({ ...form, url: e.target.value })} />
setForm({ ...form, frequency_sec: Number(e.target.value) || defaultProbeFrequency(form.type) })} />

{probeFrequencyHint(form.type)}

setForm({ ...form, timeout_sec: Number(e.target.value) || 10 })} />
{form.type === 'HTTP' && (
setForm({ ...form, expected_status_code: Number(e.target.value) || 200 })} />
)}
); }