feat: M0-06+M0-07 admin web — Content Safety + Metrics pages
Some checks failed
Deploy Admin Frontend / build-and-deploy (push) Failing after 5s
Some checks failed
Deploy Admin Frontend / build-and-deploy (push) Failing after 5s
This commit is contained in:
parent
e28f962147
commit
2413cdf561
@ -105,6 +105,14 @@ function App() {
|
||||
path="config"
|
||||
element={<PermissionGuard requiredRole="SUPER_ADMIN"><Suspense fallback={<PageLoading />}><ConfigPage /></Suspense></PermissionGuard>}
|
||||
/>
|
||||
<Route
|
||||
path="metrics"
|
||||
element={<PermissionGuard requiredRole="SUPER_ADMIN"><Suspense fallback={<PageLoading />}><MetricsPage /></Suspense></PermissionGuard>}
|
||||
/>
|
||||
<Route
|
||||
path="safety"
|
||||
element={<PermissionGuard requiredRole="SUPER_ADMIN"><Suspense fallback={<PageLoading />}><CSPage /></Suspense></PermissionGuard>}
|
||||
/>
|
||||
<Route
|
||||
path="events"
|
||||
element={<PermissionGuard requiredRole="SUPER_ADMIN"><Suspense fallback={<PageLoading />}><EventsPage /></Suspense></PermissionGuard>}
|
||||
|
||||
@ -30,7 +30,11 @@ export const adminMenuItems: AdminMenuItem[] = [
|
||||
{ path: '/billing', name: 'API 用量', icon: <DollarOutlined />, requiredRole: 'SUPER_ADMIN' },
|
||||
{ path: '/git', name: '代码仓库', icon: <CodeOutlined /> },
|
||||
{ path: '/ops', name: '系统运维', icon: <CloudServerOutlined />, 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: '事件队列' },
|
||||
]},
|
||||
|
||||
@ -25,6 +25,8 @@ const breadcrumbMap: Record<string, string> = {
|
||||
'/git': '代码仓库',
|
||||
'/servers': '服务器运维',
|
||||
'/config': '配置管理',
|
||||
'/metrics': '接口监控',
|
||||
'/safety': '内容安全',
|
||||
'/events': '事件队列',
|
||||
'/audit': '审计日志',
|
||||
}
|
||||
|
||||
73
src/pages/ContentSafety.tsx
Normal file
73
src/pages/ContentSafety.tsx
Normal file
@ -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) => <Tag color={v==='critical'?'red':v==='high'?'orange':v==='medium'?'blue':'default'}>{v}</Tag> },
|
||||
{ title: '状态', dataIndex: 'enabled', width: 70, render: (v: boolean) => v ? <Tag color="green">启用</Tag> : <Tag>禁用</Tag> },
|
||||
{ title: '添加时间', dataIndex: 'createdAt', width: 140, render: (d: string) => dayjs(d).format('MM-DD HH:mm') },
|
||||
{ title: '操作', width: 80, render: (_: any, r: any) => <Button type="link" size="small" danger icon={<DeleteOutlined />} onClick={() => removeWord(r.id)} /> },
|
||||
]
|
||||
|
||||
const checkCols = [
|
||||
{ title: '时间', dataIndex: 'createdAt', width: 130, render: (d: string) => dayjs(d).format('MM-DD HH:mm:ss') },
|
||||
{ title: '类型', dataIndex: 'contentType', width: 100 },
|
||||
{ title: '内容', dataIndex: 'content', ellipsis: true, render: (v: string) => <Text style={{ fontSize: 12 }}>{v?.slice(0, 80)}</Text> },
|
||||
{ title: '风险', dataIndex: 'riskLevel', width: 70, render: (v: string) => <Tag color={v==='critical'?'red':v==='high'?'orange':'default'}>{v}</Tag> },
|
||||
{ title: '结果', dataIndex: 'result', width: 80, render: (v: string) => <Tag color={v==='passed'?'green':v==='blocked'?'red':'gold'}>{v}</Tag> },
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<Title level={5} style={{ margin: 0 }}><SafetyOutlined /> 内容安全</Title>
|
||||
<Space>
|
||||
<Button icon={<PlusOutlined />} type="primary" onClick={() => { setNewWord(''); setAddOpen(true) }}>新增敏感词</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => qc.invalidateQueries({ queryKey: ['safety'] })}>刷新</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Tabs items={[
|
||||
{ key: 'words', label: '敏感词库', children: <Table dataSource={words || []} columns={wordCols} rowKey="id" pagination={false} size="small" /> },
|
||||
{ key: 'checks', label: '审核记录', children: <Table dataSource={checks || []} columns={checkCols} rowKey="id" pagination={{ pageSize: 20 }} size="small" /> },
|
||||
]} />
|
||||
|
||||
<Modal title="新增敏感词" open={addOpen} onOk={addWord} onCancel={() => setAddOpen(false)} okText="添加">
|
||||
<Input placeholder="词汇" value={newWord} onChange={e => setNewWord(e.target.value)} style={{ marginBottom: 12 }} />
|
||||
<Select value={category} onChange={setCategory} style={{ width: '100%', marginBottom: 12 }} options={[{ label: '通用', value: 'general' }, { label: '政治', value: 'political' }, { label: '色情', value: 'adult' }, { label: '暴力', value: 'violence' }]} />
|
||||
<Select value={risk} onChange={setRisk} style={{ width: '100%' }} options={[{ label: '低', value: 'low' }, { label: '中', value: 'medium' }, { label: '高', value: 'high' }, { label: '严重', value: 'critical' }]} />
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CSPage
|
||||
57
src/pages/Metrics.tsx
Normal file
57
src/pages/Metrics.tsx
Normal file
@ -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) => <span style={{ color: v > 1000 ? '#ff4d4f' : v > 500 ? '#faad14' : '#52c41a' }}>{v}ms</span> },
|
||||
]
|
||||
|
||||
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) => <span style={{ color: v >= 400 ? '#ff4d4f' : '#52c41a' }}>{v}</span> },
|
||||
{ title: '耗时', dataIndex: 'duration', width: 70, align: 'center' as const, render: (v: number) => `${v}ms` },
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<Title level={5} style={{ margin: 0 }}><DashboardOutlined /> 接口监控</Title>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => qc.invalidateQueries({ queryKey: ['metrics'] })}>刷新</Button>
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
<Col span={6}><Card size="small"><Statistic title="今日调用" value={overview?.todayCalls || 0} /></Card></Col>
|
||||
<Col span={6}><Card size="small"><Statistic title="平均耗时" value={overview?.avgDuration || 0} suffix="ms" valueStyle={{ color: (overview?.avgDuration || 0) > 500 ? '#ff4d4f' : '#52c41a' }} /></Card></Col>
|
||||
<Col span={6}><Card size="small"><Statistic title="错误数" value={overview?.errorCount || 0} valueStyle={{ color: (overview?.errorCount || 0) > 0 ? '#ff4d4f' : '#52c41a' }} /></Card></Col>
|
||||
<Col span={6}><Card size="small"><Statistic title="总调用" value={overview?.total || 0} /></Card></Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Card size="small" title="耗时排行"><Table dataSource={top || []} columns={topCols} rowKey="path" pagination={false} size="small" /></Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card size="small" title="最近请求"><Table dataSource={recent || []} columns={recentCols} rowKey="id" pagination={false} size="small" /></Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MetricsPage
|
||||
Loading…
x
Reference in New Issue
Block a user