admin-projects/src/pages/UserManagement.tsx
WangDL 4dad572731 feat: add admin layout, auth, user management, and routing
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 17:19:58 +08:00

242 lines
7.5 KiB
TypeScript

import { useRef, useState } from 'react'
import { ProTable } from '@ant-design/pro-components'
import type { ProColumns, ActionType } from '@ant-design/pro-components'
import { Button, Tag, message, Tooltip } from 'antd'
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import type { AdminUser } from '@/types/admin'
import { ADMIN_ROLE_LABELS, ADMIN_ROLE_COLORS } from '@/constants/roles'
import { getAdminUsers, deleteAdminUser } from '@/services/admin-api'
import DetailDrawer from '@/components/DetailDrawer'
import ConfirmDangerModal from '@/components/ConfirmDangerModal'
const statusLabelMap: Record<string, string> = {
ACTIVE: '正常',
DISABLED: '已禁用',
}
const statusColorMap: Record<string, string> = {
ACTIVE: 'green',
DISABLED: 'red',
}
export default function UserManagement() {
const actionRef = useRef<ActionType>(undefined)
const queryClient = useQueryClient()
const [detailOpen, setDetailOpen] = useState(false)
const [selectedUser, setSelectedUser] = useState<AdminUser | null>(null)
const [deleteTarget, setDeleteTarget] = useState<AdminUser | null>(null)
const { data, isLoading } = useQuery({
queryKey: ['admin-users'],
queryFn: () => getAdminUsers({ page: 1, limit: 20 }),
})
const deleteMutation = useMutation({
mutationFn: (id: string) => deleteAdminUser(id),
onSuccess: () => {
message.success('已删除')
queryClient.invalidateQueries({ queryKey: ['admin-users'] })
},
onError: () => message.error('删除失败'),
})
const columns: ProColumns<AdminUser>[] = [
{
title: '姓名',
dataIndex: 'displayName',
width: 130,
ellipsis: true,
},
{
title: '邮箱',
dataIndex: 'email',
width: 200,
ellipsis: true,
},
{
title: '角色',
dataIndex: 'role',
width: 120,
valueType: 'select',
valueEnum: {
SUPER_ADMIN: { text: '超级管理员' },
ADMIN: { text: '管理员' },
OPERATIONS: { text: '运营人员' },
DEVELOPER: { text: '开发者' },
READONLY: { text: '只读用户' },
},
render: (_, record) => (
<Tag color={ADMIN_ROLE_COLORS[record.role]}>{ADMIN_ROLE_LABELS[record.role]}</Tag>
),
},
{
title: '状态',
dataIndex: 'status',
width: 90,
valueType: 'select',
valueEnum: {
ACTIVE: { text: '正常' },
DISABLED: { text: '已禁用' },
},
render: (_, record) => (
<Tag color={statusColorMap[record.status]}>{statusLabelMap[record.status]}</Tag>
),
},
{
title: '最后登录',
dataIndex: 'lastLoginAt',
width: 160,
search: false,
render: (_, record) =>
record.lastLoginAt
? new Date(record.lastLoginAt).toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
})
: '-',
},
{
title: '创建时间',
dataIndex: 'createdAt',
width: 160,
search: false,
render: (_, record) =>
new Date(record.createdAt).toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}),
},
{
title: '操作',
valueType: 'option',
width: 100,
render: (_, record) => [
<a
key="view"
onClick={() => {
setSelectedUser(record)
setDetailOpen(true)
}}
>
</a>,
<Tooltip key="delete" title={record.role === 'SUPER_ADMIN' ? '不可删除超级管理员' : ''}>
<a
style={{ color: record.role === 'SUPER_ADMIN' ? '#ccc' : '#ff4d4f' }}
onClick={() => {
if (record.role !== 'SUPER_ADMIN') {
setDeleteTarget(record)
}
}}
>
</a>
</Tooltip>,
],
},
]
return (
<>
<ProTable<AdminUser>
actionRef={actionRef}
columns={columns}
dataSource={data?.items || []}
loading={isLoading}
rowKey="id"
search={false}
options={false}
pagination={
data
? {
current: data.page,
pageSize: data.limit,
total: data.total,
showSizeChanger: true,
showTotal: (total) => `${total}`,
}
: false
}
headerTitle="管理员列表"
toolbar={{
actions: [
<Button key="new" type="primary" icon={<PlusOutlined />}>
</Button>,
<Button
key="refresh"
icon={<ReloadOutlined />}
onClick={() => queryClient.invalidateQueries({ queryKey: ['admin-users'] })}
>
</Button>,
],
}}
/>
<DetailDrawer
open={detailOpen}
onClose={() => setDetailOpen(false)}
title="管理员详情"
>
{selectedUser && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<div style={{ fontSize: 12, color: '#999', marginBottom: 4 }}></div>
<div style={{ fontSize: 14 }}>{selectedUser.displayName}</div>
</div>
<div>
<div style={{ fontSize: 12, color: '#999', marginBottom: 4 }}></div>
<div style={{ fontSize: 14 }}>{selectedUser.email}</div>
</div>
<div>
<div style={{ fontSize: 12, color: '#999', marginBottom: 4 }}></div>
<Tag color={ADMIN_ROLE_COLORS[selectedUser.role]}>{ADMIN_ROLE_LABELS[selectedUser.role]}</Tag>
</div>
<div>
<div style={{ fontSize: 12, color: '#999', marginBottom: 4 }}></div>
<Tag color={statusColorMap[selectedUser.status]}>{statusLabelMap[selectedUser.status]}</Tag>
</div>
<div>
<div style={{ fontSize: 12, color: '#999', marginBottom: 4 }}></div>
<div style={{ fontSize: 14 }}>{selectedUser.twoFactorEnabled ? '已启用' : '未启用'}</div>
</div>
<div>
<div style={{ fontSize: 12, color: '#999', marginBottom: 4 }}></div>
<div style={{ fontSize: 14 }}>{selectedUser.lastLoginAt || '-'}</div>
</div>
<div>
<div style={{ fontSize: 12, color: '#999', marginBottom: 4 }}> IP</div>
<div style={{ fontSize: 14 }}>{selectedUser.lastLoginIp || '-'}</div>
</div>
<div>
<div style={{ fontSize: 12, color: '#999', marginBottom: 4 }}></div>
<div style={{ fontSize: 14 }}>{selectedUser.createdAt}</div>
</div>
</div>
)}
</DetailDrawer>
<ConfirmDangerModal
open={!!deleteTarget}
onCancel={() => setDeleteTarget(null)}
onConfirm={() => {
if (deleteTarget) {
deleteMutation.mutate(deleteTarget.id)
setDeleteTarget(null)
}
}}
title="删除管理员"
description="此操作不可撤销。请输入管理员邮箱确认删除:"
targetName={deleteTarget?.email || ''}
loading={deleteMutation.isPending}
/>
</>
)
}