import { DownOutlined, PlusOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons' import { Alert, App, Button, Card, Dropdown, Empty, Form, Input, Result, Select, Space, Switch, Table, Tag, Typography, } from 'antd' import type { MenuProps, TableColumnsType } from 'antd' import { useQuery, useQueryClient } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { useSearchParams } from 'react-router' import PageHeader from '@/components/admin/PageHeader' import StatusTag from '@/components/admin/StatusTag' import { createAdminUser, fetchAdminRolePermissions, fetchAdminUsers, resetAdminUserPassword, resetAdminUserDevice, updateAdminUserDisplayName, updateAdminUserPhone, updateAdminUserTestAccount, updateAdminUserRole, updateAdminUserStatus, updateAdminRolePermissions, } from '@/services/admin' import type { AdminRole, AdminUserListItem } from '@/types/admin' import { getAdminUserId, hasAdminRole, setAdminDisplayName } from '@/utils/admin-auth' import { adminUserRoleOptions, adminUserStatusOptions } from '@/utils/admin-options' import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination, resolveAdminPageSize, } from '@/utils/admin-pagination' import { formatAdminDateTime } from '@/utils/admin-time' type UserFilterForm = { username?: string role?: string status?: string } type CreateUserForm = { displayName?: string username: string password: string role: AdminRole phone?: string testAccount?: boolean } type DisplayNameForm = { displayName: string } const USER_ROLE_OPTIONS = [ { label: '普通运营', value: 'operator' }, { label: '客服', value: 'support' }, { label: '财务', value: 'finance' }, { label: '管理员', value: 'admin' }, ] export default function AdminUsersPage() { const isAdmin = hasAdminRole('admin') const currentUserId = getAdminUserId() const queryClient = useQueryClient() const { message, modal } = App.useApp() const [searchParams, setSearchParams] = useSearchParams() const [createForm] = Form.useForm() const [creating, setCreating] = useState(false) const [actionLoadingId, setActionLoadingId] = useState(null) const page = Number(searchParams.get('page') || 1) || 1 const pageSize = resolveAdminPageSize(searchParams.get('pageSize'), ADMIN_DEFAULT_PAGE_SIZE) const filters = { username: searchParams.get('username') || '', role: searchParams.get('role') || '', status: searchParams.get('status') || '', } const queryParams = useMemo( () => ({ page, pageSize, ...filters }), [filters.role, filters.status, filters.username, page, pageSize], ) const query = useQuery({ queryKey: ['admin-users', queryParams], enabled: isAdmin, queryFn: () => fetchAdminUsers(queryParams), }) const data = query.data?.data const rolePermissionsQuery = useQuery({ queryKey: ['admin-role-permissions'], enabled: isAdmin, queryFn: () => fetchAdminRolePermissions(), }) async function reloadUsers() { await queryClient.invalidateQueries({ queryKey: ['admin-users'] }) } async function toggleRolePermission(roleKey: string, permissionKey: string, enabled: boolean) { const role = rolePermissionsQuery.data?.data.roles.find((item) => item.roleKey === roleKey) if (!role || roleKey === 'admin') return const permissions = role.permissions .filter((item) => item.enabled) .map((item) => item.permissionKey) .filter((item) => item !== permissionKey) if (enabled) permissions.push(permissionKey) try { await updateAdminRolePermissions(roleKey, permissions) await queryClient.invalidateQueries({ queryKey: ['admin-role-permissions'] }) message.success('角色权限已更新') } catch (error) { message.error(error instanceof Error ? error.message : '更新角色权限失败') } } async function submitCreate(values: CreateUserForm) { setCreating(true) try { await createAdminUser({ username: values.username.trim(), displayName: values.displayName?.trim() || '', password: values.password.trim(), role: values.role, status: 'active', phone: values.phone?.trim() || '', testAccount: values.testAccount === true, }) message.success('后台用户已创建') createForm.resetFields() setSearchParams(cleanParams({ ...filters, page: 1, pageSize })) await reloadUsers() } catch (error) { message.error(error instanceof Error ? error.message : '创建后台用户失败') } finally { setCreating(false) } } function confirmUpdateRole(item: AdminUserListItem, nextRole: AdminRole) { if (item.role === nextRole) return modal.confirm({ title: '确认操作', content: `确认将 ${displayNameOf(item)} 调整为${formatRoleLabel(nextRole)}吗?`, okText: '继续执行', cancelText: '取消', centered: true, onOk: async () => { setActionLoadingId(item.userId) try { await updateAdminUserRole(item.userId, { role: nextRole }) message.success('用户角色已更新') await reloadUsers() } catch (error) { message.error(error instanceof Error ? error.message : '更新用户角色失败') } finally { setActionLoadingId(null) } }, }) } function confirmToggleStatus(item: AdminUserListItem) { const nextStatus = item.status === 'active' ? 'disabled' : 'active' modal.confirm({ title: '确认操作', content: `确认将 ${displayNameOf(item)}${nextStatus === 'active' ? '启用' : '停用'}吗?`, okText: '继续执行', cancelText: '取消', centered: true, onOk: async () => { setActionLoadingId(item.userId) try { await updateAdminUserStatus(item.userId, { status: nextStatus }) message.success('用户状态已更新') await reloadUsers() } catch (error) { message.error(error instanceof Error ? error.message : '更新用户状态失败') } finally { setActionLoadingId(null) } }, }) } function openEditDisplayNameModal(item: AdminUserListItem) { const form = ModalFormStore.create() modal.confirm({ title: '修改姓名', content: (
), okText: '保存', cancelText: '取消', centered: true, onOk: async () => { const values = await form.validate() setActionLoadingId(item.userId) try { const response = await updateAdminUserDisplayName(item.userId, { displayName: values.displayName?.trim() || '', }) if (item.userId === currentUserId) { setAdminDisplayName(response.data.user.displayName) } message.success('用户姓名已更新') await Promise.all([ queryClient.invalidateQueries({ queryKey: ['admin-session'] }), queryClient.invalidateQueries({ queryKey: ['admin-users'] }), queryClient.invalidateQueries({ queryKey: ['admin-audit-logs'] }), queryClient.invalidateQueries({ queryKey: ['admin-login-logs'] }), queryClient.invalidateQueries({ queryKey: ['admin-fund-overview'] }), queryClient.invalidateQueries({ queryKey: ['admin-fund-accounts'] }), queryClient.invalidateQueries({ queryKey: ['admin-fund-expenses'] }), queryClient.invalidateQueries({ queryKey: ['admin-fund-reconciliations'] }), queryClient.invalidateQueries({ queryKey: ['admin-fund-account-detail'] }), queryClient.invalidateQueries({ queryKey: ['admin-fund-options'] }), queryClient.invalidateQueries({ queryKey: ['admin-fund-my'] }), ]) await reloadUsers() } catch (error) { message.error(error instanceof Error ? error.message : '更新用户姓名失败') } finally { setActionLoadingId(null) } }, }) } function openResetPasswordModal(item: AdminUserListItem) { const form = ModalFormStore.create<{ password: string }>() modal.confirm({ title: '重置密码', content: (
), okText: '提交', cancelText: '取消', centered: true, onOk: async () => { const values = await form.validate() setActionLoadingId(item.userId) try { await resetAdminUserPassword(item.userId, { password: values.password.trim() }) message.success('用户密码已重置') await reloadUsers() } catch (error) { message.error(error instanceof Error ? error.message : '重置密码失败') } finally { setActionLoadingId(null) } }, }) } function openBindPhoneModal(item: AdminUserListItem) { const form = ModalFormStore.create<{ phone: string }>() modal.confirm({ title: '绑定手机号', content: (
), okText: '保存', cancelText: '取消', centered: true, onOk: async () => { const values = await form.validate() setActionLoadingId(item.userId) try { await updateAdminUserPhone(item.userId, { phone: values.phone.trim() }) message.success('手机号已绑定') await reloadUsers() } catch (error) { message.error(error instanceof Error ? error.message : '绑定手机号失败') } finally { setActionLoadingId(null) } }, }) } function confirmResetDevice(item: AdminUserListItem) { if (!item.deviceBound) return modal.confirm({ title: '重置设备绑定', content: `确认解除 ${displayNameOf(item)} 的当前设备吗?解绑后该账号需要重新完成首次设备绑定。`, okText: '确认解绑', cancelText: '取消', okButtonProps: { danger: true }, centered: true, onOk: async () => { setActionLoadingId(item.userId) try { await resetAdminUserDevice(item.userId) message.success('设备绑定已重置') await reloadUsers() } catch (error) { message.error(error instanceof Error ? error.message : '重置设备失败') } finally { setActionLoadingId(null) } }, }) } function confirmToggleTestAccount(item: AdminUserListItem) { const next = !item.testAccount modal.confirm({ title: next ? '设为技术测试账号' : '取消技术测试账号', content: next ? `确认让 ${displayNameOf(item)} 跳过短信验证码和 DCAuth 设备验证,仅使用账号密码登录吗?仅建议用于技术测试。` : `确认恢复 ${displayNameOf(item)} 的完整安全登录流程吗?`, okText: next ? '设为测试账号' : '恢复正式账号', cancelText: '取消', okButtonProps: { danger: next }, centered: true, onOk: async () => { setActionLoadingId(item.userId) try { await updateAdminUserTestAccount(item.userId, { testAccount: next }) message.success(next ? '已设为技术测试账号' : '已恢复正式账号') await reloadUsers() } catch (error) { message.error(error instanceof Error ? error.message : '更新测试账号设置失败') } finally { setActionLoadingId(null) } }, }) } function applyFilters(values: UserFilterForm) { setSearchParams(cleanParams({ ...values, page: 1, pageSize })) } function resetFilters() { setSearchParams(cleanParams({ page: 1, pageSize })) } if (!isAdmin) { return (
) } const columns: TableColumnsType = [ { title: 'ID', dataIndex: 'userId', width: 72, align: 'center', }, { title: '姓名', dataIndex: 'displayName', minWidth: 160, render: (value: string, row) => ( {value || row.username} {row.userId === currentUserId ? 当前账号 : null} ), }, { title: '账号', dataIndex: 'username', minWidth: 140, render: (value: string) => {value}, }, { title: '手机号', dataIndex: 'phone', width: 150, render: (value: string) => value ? {value} : 未绑定, }, { title: '登录设备', dataIndex: 'deviceBound', width: 210, render: (_: boolean, row) => row.deviceBound ? ( {row.deviceIdMasked} 绑定于 {formatAdminDateTime(row.deviceBoundAt || '')} ) : ( 未绑定 ), }, { title: '角色', dataIndex: 'role', width: 120, render: (value: AdminRole) => formatRoleLabel(value), }, { title: '状态', dataIndex: 'status', width: 170, render: (value, row) => ( {row.testAccount ? 技术测试 : null} ), }, { title: '创建时间', dataIndex: 'createdAt', width: 170, render: (value) => formatAdminDateTime(value), }, { title: '更新时间', dataIndex: 'updatedAt', width: 170, render: (value) => formatAdminDateTime(value), }, { title: '操作', width: 760, render: (_, row) => { const isCurrent = row.userId === currentUserId const roleMenuItems: MenuProps['items'] = USER_ROLE_OPTIONS.map((item) => ({ key: item.value, label: `设为${item.label}`, disabled: row.role === item.value, })) return ( confirmUpdateRole(row, key as AdminRole), }} > ) }, }, ] return (
筛选用户
layout="inline" initialValues={filters} onFinish={applyFilters} className="filter-form users-action-form" >
新增用户
form={createForm} layout="inline" initialValues={{ role: 'operator', testAccount: false }} onFinish={submitCreate} className="filter-form users-action-form" >