All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 9s
- Billing, ChatLogs, ContentSafety, NotificationAdmin, SecurityEvents - ComplianceAdmin (deletions + exports) - MemberManagement (members + deletion requests) - ComplianceAdmin status labels also fixed Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
137 lines
8.7 KiB
TypeScript
137 lines
8.7 KiB
TypeScript
import { useState } from 'react'
|
|
import { Table, Button, Modal, Form, Input, Tag, 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 { complianceStatusLabels } from '@/constants/labels'
|
|
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'}>{complianceStatusLabels[s] || 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 , render: (_: any, r: any) => r.userName || r.userId },
|
|
{ title: '状态', dataIndex: 'status', width: 70, render: (s: string) => <Tag color={statusColors[s] || 'default'}>{complianceStatusLabels[s] || 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 , render: (_: any, r: any) => r.userName || r.userId },
|
|
{ title: '状态', dataIndex: 'status', width: 70, render: (s: string) => <Tag color={statusColors[s] || 'default'}>{complianceStatusLabels[s] || 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>
|
|
)
|
|
}
|