feat: M4-09 — compliance admin page (policies, agreements, filings, data requests, security)
Some checks failed
Deploy Admin Frontend / build-and-deploy (push) Failing after 7s
Some checks failed
Deploy Admin Frontend / build-and-deploy (push) Failing after 7s
- ComplianceAdmin page with 6 tabs: privacy policies, user agreements, filings, data deletion, data export, security events Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
adfeeaa657
commit
050fb554f0
@ -34,6 +34,7 @@ const KnowledgeOpsPage = lazy(() => import('./pages/KnowledgeOps'))
|
||||
const ChatLogsPage = lazy(() => import('./pages/ChatLogs'))
|
||||
const HermesSettings = lazy(() => import('./pages/HermesSettings'))
|
||||
const ReleaseAdmin = lazy(() => import('./pages/ReleaseAdmin'))
|
||||
const ComplianceAdmin = lazy(() => import('./pages/ComplianceAdmin'))
|
||||
const TaskAssistant = lazy(() => import('./pages/TaskAssistant'))
|
||||
const Placeholder = lazy(() => import('./pages/Placeholder'))
|
||||
const ForbiddenPage = lazy(() => import('./pages/403'))
|
||||
@ -184,6 +185,7 @@ function App() {
|
||||
<Route path="backup" element={<Suspense fallback={<PageLoading />}><BackupAdmin /></Suspense>} />
|
||||
<Route path="reporting" element={<Suspense fallback={<PageLoading />}><ReportingAdmin /></Suspense>} />
|
||||
<Route path="release" element={<Suspense fallback={<PageLoading />}><ReleaseAdmin /></Suspense>} />
|
||||
<Route path="compliance" element={<Suspense fallback={<PageLoading />}><ComplianceAdmin /></Suspense>} />
|
||||
<Route path="notification-admin" element={<Suspense fallback={<PageLoading />}><NotificationAdmin /></Suspense>} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
|
||||
@ -29,6 +29,7 @@ export const adminMenuItems: AdminMenuItem[] = [
|
||||
{ path: '/imports', name: '文档导入', icon: <ImportOutlined /> },
|
||||
{ path: '/files', name: '文件与 COS', icon: <FileOutlined /> },
|
||||
{ path: '/reporting', name: '报表导出', icon: <FileOutlined />, requiredRole: 'ADMIN' },
|
||||
{ path: '/compliance', name: '合规安全', icon: <SafetyOutlined />, requiredRole: 'ADMIN' },
|
||||
{ path: '/audit', name: '审计日志', icon: <SafetyOutlined />, requiredRole: 'ADMIN' },
|
||||
{ path: '/billing', name: 'API 用量', icon: <DollarOutlined />, requiredRole: 'SUPER_ADMIN' },
|
||||
{ path: '/git', name: '项目中心', icon: <CodeOutlined /> },
|
||||
|
||||
134
src/pages/ComplianceAdmin.tsx
Normal file
134
src/pages/ComplianceAdmin.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import { Table, Button, Modal, Form, Input, Tag, Space, Typography, Tabs, DatePicker } from 'antd'
|
||||
import { PlusOutlined, EditOutlined, SafetyOutlined, CheckOutlined } from '@ant-design/icons'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { api } from '@/services/http-client'
|
||||
import { message } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const { Title } = Typography
|
||||
const { TextArea } = Input
|
||||
|
||||
const statusColors: Record<string, string> = { pending: 'blue', approved: 'green', rejected: 'red', PENDING: 'blue', APPROVED: 'green', REJECTED: 'red' }
|
||||
|
||||
export default function ComplianceAdmin() {
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [modalType, setModalType] = useState('')
|
||||
const [editing, setEditing] = useState<any>(null)
|
||||
const [form] = Form.useForm()
|
||||
const qc = useQueryClient()
|
||||
|
||||
const { data: policies, isLoading: pLoading } = useQuery({
|
||||
queryKey: ['compliance', 'policies'],
|
||||
queryFn: () => api.get<any[]>('/admin-api/compliance/privacy-policies').then(d => d ?? []),
|
||||
})
|
||||
|
||||
const { data: agreements, isLoading: aLoading } = useQuery({
|
||||
queryKey: ['compliance', 'agreements'],
|
||||
queryFn: () => api.get<any[]>('/admin-api/compliance/user-agreements').then(d => d ?? []),
|
||||
})
|
||||
|
||||
const { data: filings, isLoading: fLoading } = useQuery({
|
||||
queryKey: ['compliance', 'filings'],
|
||||
queryFn: () => api.get<any[]>('/admin-api/compliance/filings').then(d => d ?? []),
|
||||
})
|
||||
|
||||
const { data: deletions, isLoading: dLoading } = useQuery({
|
||||
queryKey: ['compliance', 'deletions'],
|
||||
queryFn: () => api.get<any[]>('/admin-api/compliance/deletion-requests').then(d => d ?? []),
|
||||
})
|
||||
|
||||
const { data: exports_, isLoading: eLoading } = useQuery({
|
||||
queryKey: ['compliance', 'exports'],
|
||||
queryFn: () => api.get<any[]>('/admin-api/compliance/export-requests').then(d => d ?? []),
|
||||
})
|
||||
|
||||
const { data: securityEvents, isLoading: sLoading } = useQuery({
|
||||
queryKey: ['compliance', 'security-events'],
|
||||
queryFn: () => api.get<any[]>('/admin-api/compliance/security-events').then(d => d ?? []),
|
||||
})
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (values: any) => {
|
||||
if (modalType === 'policy') return editing
|
||||
? api.patch(`/admin-api/compliance/privacy-policies/${editing.id}`, values)
|
||||
: api.post('/admin-api/compliance/privacy-policies', values)
|
||||
if (modalType === 'agreement') return editing
|
||||
? api.patch(`/admin-api/compliance/user-agreements/${editing.id}`, values)
|
||||
: api.post('/admin-api/compliance/user-agreements', values)
|
||||
return api.post('/admin-api/compliance/filings', values)
|
||||
},
|
||||
onSuccess: () => {
|
||||
message.success('已保存')
|
||||
qc.invalidateQueries({ queryKey: ['compliance'] })
|
||||
setModalOpen(false); setEditing(null)
|
||||
},
|
||||
})
|
||||
|
||||
const approveDeletion = useMutation({
|
||||
mutationFn: (id: string) => api.post(`/admin-api/compliance/deletion-requests/${id}/approve`),
|
||||
onSuccess: () => { message.success('已批准'); qc.invalidateQueries({ queryKey: ['compliance', 'deletions'] }) },
|
||||
})
|
||||
|
||||
const openCreate = (type: string) => { setModalType(type); setEditing(null); form.resetFields(); setModalOpen(true) }
|
||||
|
||||
const docColumns = [
|
||||
{ title: '版本', dataIndex: 'version', width: 80 },
|
||||
{ title: '标题', dataIndex: 'title', width: 200, ellipsis: true },
|
||||
{ title: '生效时间', dataIndex: 'effectiveAt', width: 110, render: (d: string) => d ? new Date(d).toLocaleDateString() : '-' },
|
||||
{ title: '状态', dataIndex: 'published', width: 60, render: (p: boolean) => <Tag color={p ? 'green' : 'orange'}>{p ? '已发布' : '草稿'}</Tag> },
|
||||
{ title: '操作', width: 60, render: (_: any, r: any) => <Button size="small" icon={<EditOutlined />} onClick={() => { setModalType(modalType); setEditing(r); form.setFieldsValue({ ...r, effectiveAt: r.effectiveAt ? dayjs(r.effectiveAt) : undefined }); setModalOpen(true) }} /> },
|
||||
]
|
||||
|
||||
const filingColumns = [
|
||||
{ title: '类型', dataIndex: 'type', width: 100 },
|
||||
{ title: '标题', dataIndex: 'title', width: 200, ellipsis: true },
|
||||
{ title: '状态', dataIndex: 'status', width: 70, render: (s: string) => <Tag color={statusColors[s] || 'default'}>{s}</Tag> },
|
||||
{ title: '备注', dataIndex: 'notes', width: 200, ellipsis: true, render: (n: string) => n || '-' },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', width: 110, render: (d: string) => new Date(d).toLocaleDateString() },
|
||||
]
|
||||
|
||||
const deletionColumns = [
|
||||
{ title: '用户', dataIndex: 'userId', width: 100, ellipsis: true },
|
||||
{ title: '状态', dataIndex: 'status', width: 70, render: (s: string) => <Tag color={statusColors[s] || 'default'}>{s}</Tag> },
|
||||
{ title: '申请时间', dataIndex: 'requestedAt', width: 110, render: (d: string) => new Date(d).toLocaleDateString() },
|
||||
{ title: '冷却结束', dataIndex: 'coolingEndsAt', width: 110, render: (d: string) => new Date(d).toLocaleDateString() },
|
||||
{ title: '操作', width: 60, render: (_: any, r: any) => r.status === 'pending' ? <Button size="small" type="primary" icon={<CheckOutlined />} onClick={() => approveDeletion.mutate(r.id)}>批准</Button> : null },
|
||||
]
|
||||
|
||||
const securityColumns = [
|
||||
{ title: '类型', dataIndex: 'eventType', width: 100 },
|
||||
{ title: '严重程度', dataIndex: 'severity', width: 80, render: (s: string) => <Tag color={s === 'high' ? 'red' : s === 'medium' ? 'orange' : 'blue'}>{s}</Tag> },
|
||||
{ title: '已处理', dataIndex: 'handled', width: 60, render: (h: boolean) => h ? '是' : '否' },
|
||||
{ title: '时间', dataIndex: 'createdAt', width: 110, render: (d: string) => d ? new Date(d).toLocaleString() : '-' },
|
||||
]
|
||||
|
||||
const tabs = [
|
||||
{ key: 'policies', label: '隐私政策', children: <div><Button type="primary" icon={<PlusOutlined />} onClick={() => openCreate('policy')} style={{ marginBottom: 16 }}>新建版本</Button><Table dataSource={policies || []} columns={docColumns} rowKey="id" loading={pLoading} size="small" /></div> },
|
||||
{ key: 'agreements', label: '用户协议', children: <div><Button type="primary" icon={<PlusOutlined />} onClick={() => openCreate('agreement')} style={{ marginBottom: 16 }}>新建版本</Button><Table dataSource={agreements || []} columns={docColumns} rowKey="id" loading={aLoading} size="small" /></div> },
|
||||
{ key: 'filings', label: '备案台账', children: <div><Button type="primary" icon={<PlusOutlined />} onClick={() => openCreate('filing')} style={{ marginBottom: 16 }}>新建备案</Button><Table dataSource={filings || []} columns={filingColumns} rowKey="id" loading={fLoading} size="small" /></div> },
|
||||
{ key: 'deletions', label: '数据删除', children: <Table dataSource={deletions || []} columns={deletionColumns} rowKey="id" loading={dLoading} size="small" /> },
|
||||
{ key: 'exports', label: '数据导出', children: <Table dataSource={exports_ || []} columns={[
|
||||
{ title: '用户', dataIndex: 'userId', width: 100, ellipsis: true },
|
||||
{ title: '状态', dataIndex: 'status', width: 70, render: (s: string) => <Tag color={statusColors[s] || 'default'}>{s}</Tag> },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', width: 110, render: (d: string) => new Date(d).toLocaleDateString() },
|
||||
]} rowKey="id" loading={eLoading} size="small" /> },
|
||||
{ key: 'security', label: '安全事件', children: <Table dataSource={securityEvents || []} columns={securityColumns} rowKey="id" loading={sLoading} size="small" /> },
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={4}><SafetyOutlined /> 合规与安全</Title>
|
||||
<Tabs items={tabs} />
|
||||
<Modal title={editing ? '编辑' : '新建'} open={modalOpen} onCancel={() => { setModalOpen(false); setEditing(null) }} onOk={() => form.submit()} confirmLoading={saveMutation.isPending}>
|
||||
<Form form={form} layout="vertical" onFinish={saveMutation.mutate}>
|
||||
<Form.Item name="version" label="版本" rules={[{ required: modalType !== 'filing' }]}><Input placeholder="1.0" /></Form.Item>
|
||||
<Form.Item name="title" label="标题" rules={[{ required: true }]}><Input placeholder="标题" /></Form.Item>
|
||||
{modalType === 'filing' && <Form.Item name="type" label="类型" rules={[{ required: true }]}><Input placeholder="ICP备案/公安备案/..." /></Form.Item>}
|
||||
{modalType !== 'filing' && <Form.Item name="content" label="内容"><TextArea rows={6} placeholder="文档内容" /></Form.Item>}
|
||||
{modalType !== 'filing' && <Form.Item name="effectiveAt" label="生效时间"><DatePicker style={{ width: '100%' }} /></Form.Item>}
|
||||
<Form.Item name="notes" label="备注"><TextArea rows={2} /></Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user