feat: M2-01 member management — membership + deletion review page
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 8s

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
WangDL 2026-05-24 11:19:18 +08:00
parent 92653385ea
commit ce00b58c4a
2 changed files with 92 additions and 1 deletions

View File

@ -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() {
</PermissionGuard>
}
/>
<Route path="users/members" element={<Placeholder title="普通用户" />} />
<Route path="users/members" element={<Suspense fallback={<PageLoading />}><MemberManagement /></Suspense>} />
<Route
path="throttle"
element={<PermissionGuard requiredRole="SUPER_ADMIN"><Suspense fallback={<PageLoading />}><ThrottlePage /></Suspense></PermissionGuard>}

View File

@ -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<any> => api.get('/admin-api/users/memberships') })
const { data: deletions } = useQuery({ queryKey: ['users', 'deletions'], queryFn: (): Promise<any> => 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 ? <Tag color="green"></Tag> : <Tag></Tag> },
]
const deletionCols = [
{ title: '用户ID', dataIndex: 'userId', width: 160, ellipsis: true },
{ title: '状态', dataIndex: 'status', width: 80, render: (v: string) => v === 'pending' ? <Tag color="gold"></Tag> : v === 'completed' ? <Tag color="red"></Tag> : <Tag></Tag> },
{ 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' ? (
<Space size="small">
<Button type="link" size="small" danger onClick={() => handleDeletion(r.id, 'approve')}></Button>
<Button type="link" size="small" onClick={() => handleDeletion(r.id, 'reject')}></Button>
</Space>
) : <Text type="secondary">-</Text> },
]
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Title level={5} style={{ margin: 0 }}><UserOutlined /> </Title>
<Space>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setAddOpen(true)}></Button>
<Button icon={<ReloadOutlined />} onClick={() => qc.invalidateQueries({ queryKey: ['users'] })}></Button>
</Space>
</div>
<Tabs items={[
{
key: 'membership', label: '会员管理',
children: <Table dataSource={memberships || []} columns={membershipCols} rowKey="id" pagination={{ pageSize: 20 }} size="small" />,
},
{
key: 'deletions', label: '注销审核',
children: <Table dataSource={deletions || []} columns={deletionCols} rowKey="id" pagination={{ pageSize: 20 }} size="small" />,
},
]} />
<Modal title="分配会员" open={addOpen} onOk={() => form.submit()} onCancel={() => setAddOpen(false)}>
<Form form={form} layout="vertical" onFinish={assignMembership}>
<Form.Item name="userId" label="用户ID" rules={[{ required: true }]}><Input placeholder="用户ID" /></Form.Item>
<Form.Item name="planId" label="会员计划" rules={[{ required: true }]}>
<Select options={[{ label: '免费', value: 'plan-free' }, { label: '月度', value: 'plan-monthly' }, { label: '年度', value: 'plan-yearly' }]} />
</Form.Item>
<Form.Item name="expiresAt" label="到期时间"><DatePicker style={{ width: '100%' }} /></Form.Item>
</Form>
</Modal>
</div>
)
}