admin-projects/src/pages/ContentSafety.tsx
wangdl 91698a2f3b
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 13s
refactor: TypeScript 类型体系重构 + 状态管理 + 环境配置
## 类型安全
- tsconfig: 开启 strict: true(豁免 noImplicitAny + strictNullChecks,待后续收紧)
- 新建 15 个领域类型文件 (src/types/),共 ~80 个类型定义
- 统一 barrel export (src/types/index.ts)
- 消除 9 个 [key: string]: any 接口
- 消除 22 个 API data: any 参数 → 替换为具体 DTO
- 消除 36 个 Promise<any> → 使用具体泛型
- 消除重复 PaginatedResponse,统一使用 PaginatedResult
- API 内联类型全部迁至 types/

## 状态管理
- 引入 Zustand (zustand + persist)
- 新建 src/stores/ui-store.ts — 侧边栏折叠状态持久化
- AdminLayout 接入 useUIStore

## 环境配置
- 新建 .env.example — 环境变量模板
- 新建 .env.production — 生产架构文档
- 新建 src/vite-env.d.ts — IDE 类型声明

## 通用 UI 类型
- 新建 src/types/ui.ts — TypedColumn<T>, SortState, FilterState 等

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-04 11:02:52 +08:00

125 lines
7.8 KiB
TypeScript

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 { contentSafetyAPI } from '@/api'
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: () => contentSafetyAPI.words() })
const { data: checks } = useQuery({ queryKey: ['safety', 'checks'], queryFn: () => contentSafetyAPI.checks() })
const { data: reports } = useQuery({ queryKey: ['safety', 'reports'], queryFn: () => contentSafetyAPI.reports() })
const { data: violations } = useQuery({ queryKey: ['safety', 'violations'], queryFn: () => contentSafetyAPI.violations() })
const addWord = async () => {
await contentSafetyAPI.addWord({ 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 contentSafetyAPI.deleteWord(id); qc.invalidateQueries({ queryKey: ['safety'] }) },
})
const handleReport = (id: string, action: string) => modal.confirm({
title: action === 'confirmed' ? '确认违规' : '驳回举报',
content: action === 'confirmed' ? '确认后将创建违规记录' : '驳回后将关闭此举报',
onOk: async () => {
await contentSafetyAPI.handleReport(id, { action, note: `Admin ${action}` })
message.success('已处理'); qc.invalidateQueries({ queryKey: ['safety'] })
},
})
const applyPenalty = (id: string, penalty: string) => modal.confirm({
title: '应用处罚',
content: `确认对违规记录应用「${penalty}」处罚?`,
onOk: async () => {
await contentSafetyAPI.penaltyViolation(id, { penalty })
message.success('处罚已应用'); 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> },
]
const reportCols = [
{ title: '时间', dataIndex: 'createdAt', width: 130, render: (d: string) => dayjs(d).format('MM-DD HH:mm') },
{ title: '类型', dataIndex: 'targetType', width: 110 },
{ title: '目标ID', dataIndex: 'targetId', width: 120, ellipsis: true },
{ title: '原因', dataIndex: 'reason', ellipsis: true },
{ title: '状态', dataIndex: 'status', width: 80, render: (v: string) => v === 'pending' ? <Tag color="gold"></Tag> : v === 'confirmed' ? <Tag color="red"></Tag> : <Tag></Tag> },
{ title: '操作', width: 160, render: (_: any, r: any) => r.status === 'pending' ? (
<Space size="small">
<Button type="link" size="small" onClick={() => handleReport(r.id, 'confirmed')}></Button>
<Button type="link" size="small" danger onClick={() => handleReport(r.id, 'dismissed')}></Button>
</Space>
) : <Text type="secondary">-</Text> },
]
const violationCols = [
{ title: '时间', dataIndex: 'createdAt', width: 130, render: (d: string) => dayjs(d).format('MM-DD HH:mm') },
{ title: '用户', dataIndex: 'userId', width: 140, ellipsis: true , render: (_: any, r: any) => r.userName || r.userId },
{ title: '类型', dataIndex: 'contentType', width: 100 },
{ title: '内容', dataIndex: 'content', ellipsis: true, render: (v: string) => <Text style={{ fontSize: 12 }}>{v?.slice(0, 60)}</Text> },
{ title: '风险', dataIndex: 'riskLevel', width: 70, render: (v: string) => <Tag color={v==='critical'?'red':v==='high'?'orange':'blue'}>{v}</Tag> },
{ title: '处罚', dataIndex: 'penalty', width: 80, render: (v: string) => v === 'none' ? <Tag></Tag> : <Tag color="red">{v}</Tag> },
{ title: '操作', width: 120, render: (_: any, r: any) => r.penalty === 'none' ? (
<Space size="small">
<Button type="link" size="small" onClick={() => applyPenalty(r.id, 'warning')}></Button>
<Button type="link" size="small" danger onClick={() => applyPenalty(r.id, 'restrict')}></Button>
</Space>
) : <Text type="secondary">{r.penalty}</Text> },
]
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" /> },
{ key: 'reports', label: '举报管理', children: <Table dataSource={reports || []} columns={reportCols} rowKey="id" pagination={{ pageSize: 20 }} size="small" /> },
{ key: 'violations', label: '违规记录', children: <Table dataSource={violations || []} columns={violationCols} 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