import { useCallback, useEffect, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { Breadcrumb, Button, Input, Select, Table, Tabs, Tag, type TableColumn } from '@/components/ui'; import { MonitorRuntimeOverview } from './MonitorRuntimeOverview'; import { MonitorRulesModal, MonitorScopePicker, MonitorTargetsModal } from './sending-monitor/MonitorConfiguration'; import { MonitorAlerts, MonitorHistory, MonitorPager, Rate } from './sending-monitor/MonitorDetails'; import { monitorApi, names, ruleSource, states, time, title, type MonitorType, type Snapshot, } from './sending-monitor/monitorApi'; import './AdminMonitorPage.css'; export function AdminMonitorPage() { const [params, setParams] = useSearchParams(); const tab = params.get('tab') ?? 'industry'; const type: MonitorType = tab === 'overall' || tab === 'verification' ? tab : 'industry'; const [data, setData] = useState> | null>(null); const [summary, setSummary] = useState> | null>(null); const [error, setError] = useState(''), [loading, setLoading] = useState(false), [dialog, setDialog] = useState<'rules' | 'targets' | null>(null), [detail, setDetail] = useState(null); const lastLoad = useRef(0), busy = useRef(false); const page = Math.max(1, Number(params.get('page')) || 1), queryKey = params.toString(); const update = (values: Record) => { const next = new URLSearchParams(params); Object.entries(values).forEach(([key, value]) => (value ? next.set(key, value) : next.delete(key))); setParams(next); }; const load = useCallback( async (force = false, signal?: AbortSignal) => { if ( tab === 'alerts' || tab === 'runtime' || document.hidden || (busy.current && !force) || (!force && Date.now() - lastLoad.current < 1500) ) return; busy.current = true; lastLoad.current = Date.now(); setLoading(true); const q = new URLSearchParams(queryKey); try { const [rows, overview] = await Promise.all([ monitorApi.rows( { type, page: q.get('page') ?? '1', status: q.get('status') ?? '', keyword: q.get('keyword') ?? '', tenantId: q.get('tenantId') ?? '', applicationId: q.get('applicationId') ?? '', signatureId: q.get('signatureId') ?? '', }, signal, ), monitorApi.overview(type), ]); if (!signal?.aborted) { setData(rows); setSummary(overview); setError(''); } } catch (e) { if (!signal?.aborted) setError(e instanceof Error ? e.message : '监控加载失败'); } finally { if (!signal?.aborted) { setLoading(false); busy.current = false; } } }, [queryKey, tab, type], ); useEffect(() => { const controller = new AbortController(); void load(true, controller.signal); const refresh = () => void load(false, controller.signal); const timer = setInterval(refresh, 30000); window.addEventListener('focus', refresh); document.addEventListener('visibilitychange', refresh); return () => { controller.abort(); clearInterval(timer); window.removeEventListener('focus', refresh); document.removeEventListener('visibilitychange', refresh); }; }, [load]); const health = data?.health?.data; const stale = !health || !health.complete || Date.now() - new Date(health.checkedAt).getTime() > 30000; const rows = data?.items ?? []; const columns: TableColumn[] = [ { key: 'name', title: type === 'industry' ? '通道 / 运营商' : '企业 / 应用 / 签名', width: '260px', render: (row) => (
{title(row.dimensions)}

{row.dimensions.channelId ?? row.dimensions.signatureId}

), }, { key: 'total', title: '窗口提交量', width: '110px', render: (row) => row.metrics.total.toLocaleString() }, ...(type === 'overall' ? [60, 300, 1200] : [5, 20, 60]).map((seconds, i): TableColumn => ({ key: `rate${seconds}`, title: `${seconds < 60 ? `${seconds}秒` : `${seconds / 60}分钟`}到达率`, width: '165px', render: (row) => , })), { key: 'state', title: '状态 / 规则', width: '140px', render: (row) => ( <> {stale ? '数据延迟' : states[row.status]}

{ruleSource(row.rule)} {row.rule ? ` v${row.rule.version}` : ''}

), }, { key: 'detail', title: '操作', width: '110px', render: (row) => ( ), }, ]; const counts = summary?.rows ?? []; const cards = [ ['监控维度', counts.reduce((n, r) => n + r.dimensions, 0)], ['异常维度', counts.filter((r) => r.status === 'abnormal').reduce((n, r) => n + r.dimensions, 0)], [ '样本不足', counts .filter((r) => ['sample_insufficient', 'unassessable'].includes(r.status)) .reduce((n, r) => n + r.dimensions, 0), ], ['窗口提交量', counts.reduce((n, r) => n + Number(r.total), 0)], ]; const content = (
{cards.map(([label, value]) => (
{label} {data ? Number(value).toLocaleString() : '—'} {type === 'industry' ? '按通道发送尝试' : '按唯一业务短信'}
))}
评估时刻:{time(counts[0]?.evaluationAt)}

每{type === 'overall' ? '10' : '5'}分钟评估,提交窗口最近{type === 'overall' ? '30' : '5'}分钟

采集检查:{time(health?.checkedAt)} {stale ? ' · 数据延迟或等待首次计算,暂停告警判断' : ' · 已完成采集'}
update({ keyword: e.target.value, page: '1' })} />