feat: 实现发送质量监控与报备状态消息通知
CSS quality / css-quality (push) Has been cancelled

This commit is contained in:
hectorzhao
2026-09-06 19:22:49 +08:00
parent 69e3d7368d
commit 457319e627
66 changed files with 6992 additions and 489 deletions
+297 -71
View File
@@ -1,82 +1,308 @@
import { useEffect, useMemo, useState } from 'react';
import { Activity } from 'lucide-react';
import { Breadcrumb, Button, CarrierTag, Table, Tag, type TableColumn } from '@/components/ui';
import { adminApi, type AdminChannel } from '@/api/adminApi';
const columns: Array<TableColumn<AdminChannel>> = [
{ key: 'id', title: '通道编号', render: (record) => record.id },
{ key: 'name', title: '通道名称', render: (record) => record.name },
{ key: 'carrier', title: '运营商', render: (record) => {
const carriers = record.carriers?.length ? record.carriers : record.carrier === 'all' ? ['mobile', 'unicom', 'telecom'] : record.carrier ? [record.carrier] : [];
return carriers.length ? <span className="ui-carrier-tags">{carriers.map((carrier) => <CarrierTag carrier={carrier} key={carrier} />)}</span> : '-';
} },
{ key: 'gatewayHost', title: '网关地址', render: (record) => `${record.gatewayHost}:${record.gatewayPort}` },
{ key: 'rateLimitPerSecond', title: '限速', render: (record) => `${record.rateLimitPerSecond} 条/秒` },
{
key: 'status',
title: '状态',
render: (record) => <Tag tone={record.status === 'active' ? 'success' : 'danger'}>{record.status === 'active' ? '运行中' : '已停用'}</Tag>,
},
];
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 [channels, setChannels] = useState<AdminChannel[]>([]);
const [monitor, setMonitor] = useState<Record<string, unknown>>({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
function loadData() {
setLoading(true);
Promise.all([adminApi.listChannels(), adminApi.listMonitor()])
.then(([channelItems, monitorData]) => {
setChannels(channelItems.filter((item) => item.status !== 'deleted'));
setMonitor(monitorData);
setError('');
})
.catch((reason: Error) => setError(reason.message || '监控数据加载失败'))
.finally(() => setLoading(false));
}
const [params, setParams] = useSearchParams();
const tab = params.get('tab') ?? 'industry';
const type: MonitorType = tab === 'overall' || tab === 'verification' ? tab : 'industry';
const [data, setData] = useState<Awaited<ReturnType<typeof monitorApi.rows>> | null>(null);
const [summary, setSummary] = useState<Awaited<ReturnType<typeof monitorApi.overview>> | null>(null);
const [error, setError] = useState(''),
[loading, setLoading] = useState(false),
[dialog, setDialog] = useState<'rules' | 'targets' | null>(null),
[detail, setDetail] = useState<Snapshot | null>(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<string, string>) => {
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(() => {
loadData();
}, []);
const enabledChannels = channels.filter((item) => item.status === 'active').length;
const statusGroups = Array.isArray(monitor.byStatus) ? monitor.byStatus as Array<{ status: string; _count: { _all: number } }> : [];
const totalMessages = useMemo(() => statusGroups.reduce((sum, item) => sum + (item._count?._all ?? 0), 0), [statusGroups]);
const deliveredMessages = useMemo(() => statusGroups.filter((item) => item.status === 'delivered').reduce((sum, item) => sum + (item._count?._all ?? 0), 0), [statusGroups]);
const successRate = totalMessages > 0 ? ((deliveredMessages / totalMessages) * 100).toFixed(1) : '0.0';
return (
<section className="page-stack">
<div className="page-heading">
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<Snapshot>[] = [
{
key: 'name',
title: type === 'industry' ? '通道 / 运营商' : '企业 / 应用 / 签名',
width: '260px',
render: (row) => (
<div className="ui-table__long-text">
<strong>{title(row.dimensions)}</strong>
<p>{row.dimensions.channelId ?? row.dimensions.signatureId}</p>
</div>
),
},
{ key: 'total', title: '窗口提交量', width: '110px', render: (row) => row.metrics.total.toLocaleString() },
...(type === 'overall' ? [60, 300, 1200] : [5, 20, 60]).map((seconds, i): TableColumn<Snapshot> => ({
key: `rate${seconds}`,
title: `${seconds < 60 ? `${seconds}` : `${seconds / 60}分钟`}到达率`,
width: '165px',
render: (row) => <Rate value={row.metrics.metrics[i]} />,
})),
{
key: 'state',
title: '状态 / 规则',
width: '140px',
render: (row) => (
<>
<Tag tone={!stale && row.status === 'abnormal' ? 'danger' : 'neutral'}>
{stale ? '数据延迟' : states[row.status]}
</Tag>
<p>
{ruleSource(row.rule)}
{row.rule ? ` v${row.rule.version}` : ''}
</p>
</>
),
},
{
key: 'detail',
title: '操作',
width: '110px',
render: (row) => (
<Button variant="ghost" size="sm" onClick={() => setDetail(row)}>
/
</Button>
),
},
];
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 = (
<div className="page-stack">
<div className="sending-monitor__summary">
{cards.map(([label, value]) => (
<div className="surface" key={label}>
<span>{label}</span>
<strong>{data ? Number(value).toLocaleString() : '—'}</strong>
<small>{type === 'industry' ? '按通道发送尝试' : '按唯一业务短信'}</small>
</div>
))}
</div>
<div className="surface sending-monitor__filters">
<div>
<Breadcrumb items={['发送监控']} />
<strong>{time(counts[0]?.evaluationAt)}</strong>
<p>
{type === 'overall' ? '10' : '5'}{type === 'overall' ? '30' : '5'}
</p>
<small>
{time(health?.checkedAt)}
{stale ? ' · 数据延迟或等待首次计算,暂停告警判断' : ' · 已完成采集'}
</small>
</div>
<Button icon={<Activity size={16} />} onClick={loadData} variant="ghost"></Button>
<Input
aria-label="搜索监控对象"
placeholder="搜索通道、企业、应用或签名"
value={params.get('keyword') ?? ''}
onChange={(e) => update({ keyword: e.target.value, page: '1' })}
/>
<Select
label="状态"
value={params.get('status') ?? ''}
options={[
{ value: '', label: '全部' },
...['abnormal', 'normal', 'sample_insufficient', 'stale', 'unassessable', 'unconfigured', 'no_data'].map(
(value) => ({ value, label: states[value] }),
),
]}
onChange={(e) => update({ status: e.target.value, page: '1' })}
/>
</div>
{loading ? <p className="muted">...</p> : null}
{error ? <p className="form-error">{error}</p> : null}
<div className="dashboard-grid">
<div className="surface metric-card">
<span></span>
<strong>{enabledChannels}</strong>
<small> {channels.length} </small>
</div>
<div className="surface metric-card">
<span></span>
<strong>{successRate}%</strong>
<small></small>
</div>
<div className="surface metric-card">
<span></span>
<strong>{totalMessages.toLocaleString('zh-CN')}</strong>
<small></small>
{type !== 'industry' && (
<details className="surface">
<summary></summary>
<MonitorScopePicker
scope={{
tenantId: params.get('tenantId') ?? undefined,
applicationId: params.get('applicationId') ?? undefined,
signatureId: params.get('signatureId') ?? undefined,
}}
onChange={(scope) =>
update({
tenantId: scope.tenantId ?? '',
applicationId: scope.applicationId ?? '',
signatureId: scope.signatureId ?? '',
page: '1',
})
}
/>
</details>
)}
{loading && <p role="status"></p>}
{error && (
<p role="alert" className="form-error">
{error}
</p>
)}
<div className="surface sending-monitor__desktop">
<Table columns={columns} data={rows} rowKey="id" />
</div>
<div className="sending-monitor__mobile">
{rows.map((row) => (
<article className="surface" key={row.id}>
<h3>{title(row.dimensions)}</h3>
<Tag tone="neutral">{stale ? '数据延迟' : states[row.status]}</Tag>
<p>
{row.metrics.total} · {ruleSource(row.rule)}
</p>
{row.metrics.metrics.map((m) => (
<div className="sending-monitor__mobile-metric" key={m.seconds}>
<span>{m.seconds}</span>
<Rate value={m} />
</div>
))}
<Button variant="ghost" onClick={() => setDetail(row)}>
/
</Button>
</article>
))}
</div>
{!loading && !rows.length && !error && (
<p className="surface">
</p>
)}
<MonitorPager page={page} total={data?.total ?? 0} onChange={(p) => update({ page: String(p) })} />
</div>
);
return (
<section className="sending-monitor page-stack">
<div className="page-heading">
<Breadcrumb items={['运营概览', '发送监控']} />
<div className="sending-monitor__actions">
<Button variant="secondary" disabled={loading} onClick={() => void load()}>
</Button>
{!['alerts', 'runtime'].includes(tab) && (
<Button variant="secondary" onClick={() => setDialog('rules')}>
</Button>
)}
{tab === 'industry' && <Button onClick={() => setDialog('targets')}></Button>}
</div>
</div>
<div className="surface">
<Table columns={columns} data={channels} rowKey="id" />
</div>
<p className="sending-monitor__notice">
/
</p>
<Tabs
value={tab}
onChange={(value) => {
setData(null);
setSummary(null);
setParams({ tab: value });
}}
items={[
...Object.entries(names).map(([value, label]) => ({ value, label, content })),
{ value: 'alerts', label: '告警记录', content: <MonitorAlerts /> },
{ value: 'runtime', label: '运行概况', content: <MonitorRuntimeOverview /> },
]}
/>
{dialog === 'rules' && (
<MonitorRulesModal
type={type}
onClose={() => {
setDialog(null);
void load(true);
}}
/>
)}
{dialog === 'targets' && (
<MonitorTargetsModal
onClose={() => {
setDialog(null);
void load(true);
}}
/>
)}
{detail && <MonitorHistory row={detail} onClose={() => setDetail(null)} />}
</section>
);
}