From 2413cdf561903f45ceb3f75f7da2e5ea885d6040 Mon Sep 17 00:00:00 2001 From: WangDL Date: Fri, 22 May 2026 23:21:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20M0-06+M0-07=20admin=20web=20=E2=80=94?= =?UTF-8?q?=20Content=20Safety=20+=20Metrics=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 8 ++++ src/config/menu.tsx | 4 ++ src/layouts/AdminLayout.tsx | 2 + src/pages/ContentSafety.tsx | 73 +++++++++++++++++++++++++++++++++++++ src/pages/Metrics.tsx | 57 +++++++++++++++++++++++++++++ 5 files changed, 144 insertions(+) create mode 100644 src/pages/ContentSafety.tsx create mode 100644 src/pages/Metrics.tsx diff --git a/src/App.tsx b/src/App.tsx index 78bf746..9363188 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -105,6 +105,14 @@ function App() { path="config" element={}>} /> + }>} + /> + }>} + /> }>} diff --git a/src/config/menu.tsx b/src/config/menu.tsx index 1903989..4f00b27 100644 --- a/src/config/menu.tsx +++ b/src/config/menu.tsx @@ -30,7 +30,11 @@ export const adminMenuItems: AdminMenuItem[] = [ { path: '/billing', name: 'API 用量', icon: , requiredRole: 'SUPER_ADMIN' }, { path: '/git', name: '代码仓库', icon: }, { path: '/ops', name: '系统运维', icon: , requiredRole: 'SUPER_ADMIN', children: [ + { path: '/metrics', name: '接口监控' }, + { path: '/servers', name: '服务器' }, + { path: '/events', name: '事件队列' }, { path: '/config', name: '配置管理' }, + { path: '/safety', name: '内容安全' }, { path: '/servers', name: '服务器' }, { path: '/events', name: '事件队列' }, ]}, diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx index 045b247..a9ec455 100644 --- a/src/layouts/AdminLayout.tsx +++ b/src/layouts/AdminLayout.tsx @@ -25,6 +25,8 @@ const breadcrumbMap: Record = { '/git': '代码仓库', '/servers': '服务器运维', '/config': '配置管理', + '/metrics': '接口监控', + '/safety': '内容安全', '/events': '事件队列', '/audit': '审计日志', } diff --git a/src/pages/ContentSafety.tsx b/src/pages/ContentSafety.tsx new file mode 100644 index 0000000..38b646f --- /dev/null +++ b/src/pages/ContentSafety.tsx @@ -0,0 +1,73 @@ +import { useState } from 'react' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { Table, Button, Typography, App, Modal, Input, Select, Tag, Space, Tabs } from 'antd' +import { ReloadOutlined, PlusOutlined, DeleteOutlined, SafetyOutlined } from '@ant-design/icons' +import { api } from '@/services/http-client' +import dayjs from 'dayjs' + +const { Title, Text } = Typography + +function CSPage() { + const { modal, message } = App.useApp() + const qc = useQueryClient() + const [addOpen, setAddOpen] = useState(false) + const [newWord, setNewWord] = useState('') + const [category, setCategory] = useState('general') + const [risk, setRisk] = useState('medium') + + const { data: words } = useQuery({ queryKey: ['safety', 'words'], queryFn: () => api.get('/admin-api/content-safety/words') }) + const { data: checks } = useQuery({ queryKey: ['safety', 'checks'], queryFn: () => api.get('/admin-api/content-safety/checks') }) + + const addWord = async () => { + await api.post('/admin-api/content-safety/words', { word: newWord, category, riskLevel: risk }) + message.success('已添加'); setAddOpen(false); setNewWord('') + qc.invalidateQueries({ queryKey: ['safety'] }) + } + + const removeWord = (id: string) => modal.confirm({ + title: '删除敏感词', okType: 'danger', + onOk: async () => { await api.delete(`/admin-api/content-safety/words/${id}`); qc.invalidateQueries({ queryKey: ['safety'] }) }, + }) + + const wordCols = [ + { title: '词汇', dataIndex: 'word', width: 150 }, + { title: '分类', dataIndex: 'category', width: 100 }, + { title: '等级', dataIndex: 'riskLevel', width: 80, render: (v: string) => {v} }, + { title: '状态', dataIndex: 'enabled', width: 70, render: (v: boolean) => v ? 启用 : 禁用 }, + { title: '添加时间', dataIndex: 'createdAt', width: 140, render: (d: string) => dayjs(d).format('MM-DD HH:mm') }, + { title: '操作', width: 80, render: (_: any, r: any) => + + + + + }, + { key: 'checks', label: '审核记录', children: }, + ]} /> + + setAddOpen(false)} okText="添加"> + setNewWord(e.target.value)} style={{ marginBottom: 12 }} /> + + + + ) +} + +export default CSPage diff --git a/src/pages/Metrics.tsx b/src/pages/Metrics.tsx new file mode 100644 index 0000000..02f3ef2 --- /dev/null +++ b/src/pages/Metrics.tsx @@ -0,0 +1,57 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { Table, Card, Row, Col, Statistic, Button, Typography, App } from 'antd' +import { ReloadOutlined, DashboardOutlined } from '@ant-design/icons' +import { api } from '@/services/http-client' +import dayjs from 'dayjs' + +const { Title } = Typography + +function MetricsPage() { + const qc = useQueryClient() + + const { data: overview } = useQuery({ queryKey: ['metrics', 'overview'], queryFn: () => api.get('/admin-api/metrics/overview'), staleTime: 10_000 }) + const { data: top } = useQuery({ queryKey: ['metrics', 'top'], queryFn: () => api.get('/admin-api/metrics/top?limit=15'), staleTime: 10_000 }) + const { data: recent } = useQuery({ queryKey: ['metrics', 'recent'], queryFn: () => api.get('/admin-api/metrics/recent?limit=30'), staleTime: 5_000 }) + + const topCols = [ + { title: '接口', dataIndex: 'path', width: 300, ellipsis: true }, + { title: '方法', dataIndex: 'method', width: 70 }, + { title: '调用', dataIndex: 'calls', width: 70, align: 'center' as const }, + { title: '平均耗时', dataIndex: 'avgDuration', width: 100, align: 'center' as const, render: (v: number) => 1000 ? '#ff4d4f' : v > 500 ? '#faad14' : '#52c41a' }}>{v}ms }, + ] + + const recentCols = [ + { title: '时间', dataIndex: 'createdAt', width: 140, render: (d: string) => dayjs(d).format('HH:mm:ss') }, + { title: '接口', dataIndex: 'path', ellipsis: true }, + { title: '方法', dataIndex: 'method', width: 60 }, + { title: '状态', dataIndex: 'statusCode', width: 60, render: (v: number) => = 400 ? '#ff4d4f' : '#52c41a' }}>{v} }, + { title: '耗时', dataIndex: 'duration', width: 70, align: 'center' as const, render: (v: number) => `${v}ms` }, + ] + + return ( +
+
+ <DashboardOutlined /> 接口监控 + +
+ + +
+ 500 ? '#ff4d4f' : '#52c41a' }} /> + 0 ? '#ff4d4f' : '#52c41a' }} /> + + + + + +
+ + +
+ + + + ) +} + +export default MetricsPage