feat: M2-01 member management — membership + deletion review page
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 8s
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:
parent
92653385ea
commit
ce00b58c4a
@ -28,6 +28,7 @@ const ServersPage = lazy(() => import("./pages/Servers"))
|
|||||||
const AuditLogPage = lazy(() => import("./pages/AuditLog"))
|
const AuditLogPage = lazy(() => import("./pages/AuditLog"))
|
||||||
const Dashboard = lazy(() => import('./pages/Dashboard'))
|
const Dashboard = lazy(() => import('./pages/Dashboard'))
|
||||||
const UserManagement = lazy(() => import('./pages/UserManagement'))
|
const UserManagement = lazy(() => import('./pages/UserManagement'))
|
||||||
|
const MemberManagement = lazy(() => import('./pages/MemberManagement'))
|
||||||
const HermesSettings = lazy(() => import('./pages/HermesSettings'))
|
const HermesSettings = lazy(() => import('./pages/HermesSettings'))
|
||||||
const TaskAssistant = lazy(() => import('./pages/TaskAssistant'))
|
const TaskAssistant = lazy(() => import('./pages/TaskAssistant'))
|
||||||
const Placeholder = lazy(() => import('./pages/Placeholder'))
|
const Placeholder = lazy(() => import('./pages/Placeholder'))
|
||||||
@ -75,7 +76,7 @@ function App() {
|
|||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route path="users/members" element={<Placeholder title="普通用户" />} />
|
<Route path="users/members" element={<Suspense fallback={<PageLoading />}><MemberManagement /></Suspense>} />
|
||||||
<Route
|
<Route
|
||||||
path="throttle"
|
path="throttle"
|
||||||
element={<PermissionGuard requiredRole="SUPER_ADMIN"><Suspense fallback={<PageLoading />}><ThrottlePage /></Suspense></PermissionGuard>}
|
element={<PermissionGuard requiredRole="SUPER_ADMIN"><Suspense fallback={<PageLoading />}><ThrottlePage /></Suspense></PermissionGuard>}
|
||||||
|
|||||||
90
src/pages/MemberManagement.tsx
Normal file
90
src/pages/MemberManagement.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user