From ce00b58c4acdc8e74e7d6cc261a0252336826a9c Mon Sep 17 00:00:00 2001 From: WangDL Date: Sun, 24 May 2026 11:19:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20M2-01=20member=20management=20=E2=80=94?= =?UTF-8?q?=20membership=20+=20deletion=20review=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- src/App.tsx | 3 +- src/pages/MemberManagement.tsx | 90 ++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/pages/MemberManagement.tsx diff --git a/src/App.tsx b/src/App.tsx index e550018..16ad91c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,6 +28,7 @@ const ServersPage = lazy(() => import("./pages/Servers")) const AuditLogPage = lazy(() => import("./pages/AuditLog")) const Dashboard = lazy(() => import('./pages/Dashboard')) const UserManagement = lazy(() => import('./pages/UserManagement')) +const MemberManagement = lazy(() => import('./pages/MemberManagement')) const HermesSettings = lazy(() => import('./pages/HermesSettings')) const TaskAssistant = lazy(() => import('./pages/TaskAssistant')) const Placeholder = lazy(() => import('./pages/Placeholder')) @@ -75,7 +76,7 @@ function App() { } /> - } /> + }>} /> }>} diff --git a/src/pages/MemberManagement.tsx b/src/pages/MemberManagement.tsx new file mode 100644 index 0000000..7d1edbf --- /dev/null +++ b/src/pages/MemberManagement.tsx @@ -0,0 +1,90 @@ +import { useState } from 'react' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { Table, Button, Typography, App, Tabs, Tag, Space, Modal, Form, Input, Select, DatePicker } from 'antd' +import { ReloadOutlined, UserOutlined, PlusOutlined } from '@ant-design/icons' +import { api } from '@/services/http-client' +import dayjs from 'dayjs' + +const { Title, Text } = Typography + +export default function MemberManagement() { + const { message, modal } = App.useApp() + const qc = useQueryClient() + const [addOpen, setAddOpen] = useState(false) + const [form] = Form.useForm() + + const { data: memberships } = useQuery({ queryKey: ['users', 'memberships'], queryFn: (): Promise => api.get('/admin-api/users/memberships') }) + const { data: deletions } = useQuery({ queryKey: ['users', 'deletions'], queryFn: (): Promise => api.get('/admin-api/users/deletion-requests') }) + + const assignMembership = async (v: any) => { + await api.post('/admin-api/users/memberships', { userId: v.userId, planId: v.planId, expiresAt: v.expiresAt?.toISOString() }) + message.success('已分配'); setAddOpen(false); form.resetFields(); qc.invalidateQueries({ queryKey: ['users'] }) + } + + const handleDeletion = async (id: string, action: 'approve' | 'reject') => { + modal.confirm({ + title: action === 'approve' ? '确认批准注销' : '确认驳回注销', + content: action === 'approve' ? '批准后将立即执行账号注销' : '驳回后用户可正常使用', + okType: action === 'approve' ? 'danger' : undefined, + onOk: async () => { + await api.post(`/admin-api/users/deletion-requests/${id}/${action}`) + message.success(action === 'approve' ? '已批准注销' : '已驳回') + qc.invalidateQueries({ queryKey: ['users'] }) + }, + }) + } + + const membershipCols = [ + { title: '用户ID', dataIndex: 'userId', width: 160, ellipsis: true }, + { title: '计划', dataIndex: ['plan', 'name'], width: 120, render: (_: any, r: any) => r.plan?.name || '-' }, + { title: '开始', dataIndex: 'startedAt', width: 120, render: (d: string) => d ? dayjs(d).format('YYYY-MM-DD') : '-' }, + { title: '到期', dataIndex: 'expiresAt', width: 120, render: (d: string) => d ? dayjs(d).format('YYYY-MM-DD') : '永久' }, + { title: '状态', dataIndex: 'active', width: 70, render: (v: boolean) => v ? 生效 : 失效 }, + ] + + const deletionCols = [ + { title: '用户ID', dataIndex: 'userId', width: 160, ellipsis: true }, + { title: '状态', dataIndex: 'status', width: 80, render: (v: string) => v === 'pending' ? 待处理 : v === 'completed' ? 已注销 : 已取消 }, + { title: '申请时间', dataIndex: 'requestedAt', width: 130, render: (d: string) => dayjs(d).format('MM-DD HH:mm') }, + { title: '冷静期截止', dataIndex: 'coolingEndsAt', width: 130, render: (d: string) => dayjs(d).format('MM-DD HH:mm') }, + { title: '操作', width: 160, render: (_: any, r: any) => r.status === 'pending' ? ( + + + + + ) : - }, + ] + + return ( +
+
+ <UserOutlined /> 普通用户 + + + + +
+ + , + }, + { + key: 'deletions', label: '注销审核', + children: , + }, + ]} /> + + form.submit()} onCancel={() => setAddOpen(false)}> +
+ + +