771 lines
25 KiB
TypeScript
771 lines
25 KiB
TypeScript
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<CreateUserForm>()
|
|
const [creating, setCreating] = useState(false)
|
|
const [actionLoadingId, setActionLoadingId] = useState<number | null>(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<DisplayNameForm>()
|
|
|
|
modal.confirm({
|
|
title: '修改姓名',
|
|
content: (
|
|
<Form
|
|
layout="vertical"
|
|
preserve={false}
|
|
initialValues={{ displayName: item.displayName || item.username }}
|
|
ref={form.bind}
|
|
className="modal-form"
|
|
>
|
|
<Form.Item
|
|
name="displayName"
|
|
label="姓名(展示用,可输入中文)"
|
|
rules={[{ max: 32, message: '姓名最多 32 个字符' }]}
|
|
extra="留空时回退显示登录账号"
|
|
>
|
|
<Input placeholder="例如:张三" maxLength={32} showCount />
|
|
</Form.Item>
|
|
</Form>
|
|
),
|
|
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: (
|
|
<Form
|
|
layout="vertical"
|
|
preserve={false}
|
|
initialValues={{ password: '' }}
|
|
ref={form.bind}
|
|
className="modal-form"
|
|
>
|
|
<Form.Item
|
|
name="password"
|
|
label={`请输入 ${displayNameOf(item)} 的新密码`}
|
|
rules={[
|
|
{ required: true, message: '请输入新密码' },
|
|
{ min: 8, message: '密码至少 8 位' },
|
|
]}
|
|
>
|
|
<Input.Password placeholder="至少 8 位" />
|
|
</Form.Item>
|
|
</Form>
|
|
),
|
|
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: (
|
|
<Form
|
|
layout="vertical"
|
|
preserve={false}
|
|
initialValues={{ phone: item.phone || '' }}
|
|
ref={form.bind}
|
|
className="modal-form"
|
|
>
|
|
<Form.Item
|
|
name="phone"
|
|
label={`${displayNameOf(item)} 的登录手机号`}
|
|
rules={[
|
|
{ required: true, message: '请输入手机号' },
|
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的 11 位手机号' },
|
|
]}
|
|
extra="绑定后该账号必须使用账号密码 + 短信验证码 + 本机 DCAuth 登录"
|
|
>
|
|
<Input placeholder="11 位手机号" maxLength={11} />
|
|
</Form.Item>
|
|
</Form>
|
|
),
|
|
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 (
|
|
<section className="page-stack">
|
|
<PageHeader title="后台用户" description="管理员可维护后台账号、角色和启停状态。" />
|
|
<Result status="warning" title="仅管理员可以访问用户管理。" />
|
|
</section>
|
|
)
|
|
}
|
|
|
|
const columns: TableColumnsType<AdminUserListItem> = [
|
|
{
|
|
title: 'ID',
|
|
dataIndex: 'userId',
|
|
width: 72,
|
|
align: 'center',
|
|
},
|
|
{
|
|
title: '姓名',
|
|
dataIndex: 'displayName',
|
|
minWidth: 160,
|
|
render: (value: string, row) => (
|
|
<Space>
|
|
<Typography.Text strong>{value || row.username}</Typography.Text>
|
|
{row.userId === currentUserId ? <Tag>当前账号</Tag> : null}
|
|
</Space>
|
|
),
|
|
},
|
|
{
|
|
title: '账号',
|
|
dataIndex: 'username',
|
|
minWidth: 140,
|
|
render: (value: string) => <Typography.Text type="secondary">{value}</Typography.Text>,
|
|
},
|
|
{
|
|
title: '手机号',
|
|
dataIndex: 'phone',
|
|
width: 150,
|
|
render: (value: string) =>
|
|
value ? <Typography.Text>{value}</Typography.Text> : <Tag color="orange">未绑定</Tag>,
|
|
},
|
|
{
|
|
title: '登录设备',
|
|
dataIndex: 'deviceBound',
|
|
width: 210,
|
|
render: (_: boolean, row) =>
|
|
row.deviceBound ? (
|
|
<Space direction="vertical" size={0}>
|
|
<Typography.Text>{row.deviceIdMasked}</Typography.Text>
|
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
|
绑定于 {formatAdminDateTime(row.deviceBoundAt || '')}
|
|
</Typography.Text>
|
|
</Space>
|
|
) : (
|
|
<Tag>未绑定</Tag>
|
|
),
|
|
},
|
|
{
|
|
title: '角色',
|
|
dataIndex: 'role',
|
|
width: 120,
|
|
render: (value: AdminRole) => formatRoleLabel(value),
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
width: 170,
|
|
render: (value, row) => (
|
|
<Space wrap>
|
|
<StatusTag value={value} />
|
|
{row.testAccount ? <Tag color="purple">技术测试</Tag> : null}
|
|
</Space>
|
|
),
|
|
},
|
|
{
|
|
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 (
|
|
<Space wrap>
|
|
<Button
|
|
loading={actionLoadingId === row.userId}
|
|
onClick={() => openEditDisplayNameModal(row)}
|
|
>
|
|
改名
|
|
</Button>
|
|
<Button
|
|
loading={actionLoadingId === row.userId}
|
|
onClick={() => openBindPhoneModal(row)}
|
|
>
|
|
绑定手机号
|
|
</Button>
|
|
<Button
|
|
disabled={!row.deviceBound}
|
|
danger
|
|
loading={actionLoadingId === row.userId}
|
|
onClick={() => confirmResetDevice(row)}
|
|
>
|
|
重置设备
|
|
</Button>
|
|
<Button
|
|
loading={actionLoadingId === row.userId}
|
|
onClick={() => confirmToggleTestAccount(row)}
|
|
>
|
|
{row.testAccount ? '取消测试账号' : '设为测试账号'}
|
|
</Button>
|
|
<Dropdown
|
|
disabled={isCurrent}
|
|
menu={{
|
|
items: roleMenuItems,
|
|
onClick: ({ key }) => confirmUpdateRole(row, key as AdminRole),
|
|
}}
|
|
>
|
|
<Button disabled={isCurrent} loading={actionLoadingId === row.userId}>
|
|
改角色 <DownOutlined />
|
|
</Button>
|
|
</Dropdown>
|
|
<Button
|
|
disabled={isCurrent}
|
|
loading={actionLoadingId === row.userId}
|
|
onClick={() => confirmToggleStatus(row)}
|
|
>
|
|
{row.status === 'active' ? '停用' : '启用'}
|
|
</Button>
|
|
<Button
|
|
type="primary"
|
|
loading={actionLoadingId === row.userId}
|
|
onClick={() => openResetPasswordModal(row)}
|
|
>
|
|
重置密码
|
|
</Button>
|
|
</Space>
|
|
)
|
|
},
|
|
},
|
|
]
|
|
|
|
return (
|
|
<section className="page-stack">
|
|
<PageHeader
|
|
title="后台用户"
|
|
description="管理员可维护后台账号、姓名、手机号、测试账号、角色、设备绑定和启停状态。"
|
|
/>
|
|
|
|
<Card className="users-action-card">
|
|
<div className="users-action-row">
|
|
<section className="users-action-section">
|
|
<div className="users-action-title">筛选用户</div>
|
|
<Form<UserFilterForm>
|
|
layout="inline"
|
|
initialValues={filters}
|
|
onFinish={applyFilters}
|
|
className="filter-form users-action-form"
|
|
>
|
|
<Form.Item name="username">
|
|
<Input allowClear placeholder="姓名/账号筛选" className="users-input-md" />
|
|
</Form.Item>
|
|
<Form.Item name="role">
|
|
<Select
|
|
allowClear
|
|
placeholder="角色"
|
|
options={adminUserRoleOptions}
|
|
className="users-select-sm"
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item name="status">
|
|
<Select
|
|
allowClear
|
|
placeholder="状态"
|
|
options={adminUserStatusOptions}
|
|
className="users-select-sm"
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item>
|
|
<Space>
|
|
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">
|
|
查询
|
|
</Button>
|
|
<Button icon={<ReloadOutlined />} onClick={resetFilters}>
|
|
重置
|
|
</Button>
|
|
</Space>
|
|
</Form.Item>
|
|
</Form>
|
|
</section>
|
|
|
|
<section className="users-action-section">
|
|
<div className="users-action-title">新增用户</div>
|
|
<Form<CreateUserForm>
|
|
form={createForm}
|
|
layout="inline"
|
|
initialValues={{ role: 'operator', testAccount: false }}
|
|
onFinish={submitCreate}
|
|
className="filter-form users-action-form"
|
|
>
|
|
<Form.Item name="displayName" rules={[{ max: 32, message: '姓名最多 32 个字符' }]}>
|
|
<Input placeholder="姓名(可中文)" className="users-input-md" maxLength={32} />
|
|
</Form.Item>
|
|
<Form.Item name="username" rules={[{ required: true, message: '请输入新账号' }]}>
|
|
<Input placeholder="登录账号(中文/英文/数字)" className="users-input-md" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="password"
|
|
rules={[
|
|
{ required: true, message: '请输入新密码' },
|
|
{ min: 8, message: '密码至少 8 位' },
|
|
]}
|
|
>
|
|
<Input.Password placeholder="新密码,至少 8 位" className="users-input-lg" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
name="phone"
|
|
rules={[
|
|
{
|
|
pattern: /^1[3-9]\d{9}$/,
|
|
message: '请输入正确的 11 位手机号',
|
|
},
|
|
]}
|
|
>
|
|
<Input placeholder="绑定手机号(可选)" className="users-input-md" maxLength={11} />
|
|
</Form.Item>
|
|
<Form.Item name="role" rules={[{ required: true, message: '请选择角色' }]}>
|
|
<Select options={USER_ROLE_OPTIONS} className="users-select-sm" />
|
|
</Form.Item>
|
|
<Form.Item name="testAccount" valuePropName="checked">
|
|
<Switch checkedChildren="技术测试" unCheckedChildren="正式账号" />
|
|
</Form.Item>
|
|
<Form.Item>
|
|
<Button type="primary" htmlType="submit" icon={<PlusOutlined />} loading={creating}>
|
|
新增用户
|
|
</Button>
|
|
</Form.Item>
|
|
</Form>
|
|
</section>
|
|
</div>
|
|
</Card>
|
|
|
|
{query.error ? (
|
|
<Alert
|
|
type="error"
|
|
showIcon
|
|
message={query.error instanceof Error ? query.error.message : '读取后台用户失败'}
|
|
/>
|
|
) : null}
|
|
|
|
<Card
|
|
title="用户列表"
|
|
extra={
|
|
<Typography.Text type="secondary">
|
|
共 {data?.pagination.total || 0} 个用户
|
|
</Typography.Text>
|
|
}
|
|
>
|
|
<Table<AdminUserListItem>
|
|
rowKey="userId"
|
|
loading={query.isLoading || query.isFetching}
|
|
columns={columns}
|
|
dataSource={data?.items || []}
|
|
locale={{ emptyText: <Empty description="暂无用户" /> }}
|
|
scroll={{ x: 1680 }}
|
|
pagination={buildAdminTablePagination({
|
|
current: data?.pagination.page || page,
|
|
pageSize: data?.pagination.pageSize || pageSize,
|
|
total: data?.pagination.total || 0,
|
|
onChange: (nextPage, nextPageSize) => {
|
|
setSearchParams(cleanParams({ ...filters, page: nextPage, pageSize: nextPageSize }))
|
|
},
|
|
})}
|
|
/>
|
|
</Card>
|
|
|
|
<Card title="角色权限">
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
{(rolePermissionsQuery.data?.data.roles || []).map((role) => (
|
|
<div key={role.roleKey}>
|
|
<Typography.Text strong>{role.roleName}</Typography.Text>
|
|
<div style={{ marginTop: 8 }}>
|
|
<Space wrap>
|
|
{role.permissions.map((permission) => (
|
|
<Button
|
|
key={permission.permissionKey}
|
|
size="small"
|
|
type={permission.enabled ? 'primary' : 'default'}
|
|
disabled={role.roleKey === 'admin'}
|
|
onClick={() =>
|
|
void toggleRolePermission(
|
|
role.roleKey,
|
|
permission.permissionKey,
|
|
!permission.enabled,
|
|
)
|
|
}
|
|
>
|
|
{permission.permissionName}
|
|
</Button>
|
|
))}
|
|
</Space>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</Space>
|
|
</Card>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function formatRoleLabel(role: AdminRole) {
|
|
if (role === 'admin') return '管理员'
|
|
if (role === 'support') return '客服'
|
|
if (role === 'finance') return '财务'
|
|
return '普通运营'
|
|
}
|
|
|
|
function displayNameOf(user: AdminUserListItem) {
|
|
return user.displayName || user.username
|
|
}
|
|
|
|
function cleanParams(params: Record<string, unknown>) {
|
|
const next = new URLSearchParams()
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
const normalized = String(value ?? '').trim()
|
|
if (normalized) {
|
|
next.set(key, normalized)
|
|
}
|
|
})
|
|
return next
|
|
}
|
|
|
|
class ModalFormStore<T extends object> {
|
|
private form: ReturnType<typeof Form.useForm<T>>[0] | null = null
|
|
|
|
static create<T extends object>() {
|
|
return new ModalFormStore<T>()
|
|
}
|
|
|
|
bind = (instance: unknown) => {
|
|
this.form = instance as ReturnType<typeof Form.useForm<T>>[0] | null
|
|
}
|
|
|
|
async validate(): Promise<T> {
|
|
if (!this.form) {
|
|
return {} as T
|
|
}
|
|
|
|
return this.form.validateFields()
|
|
}
|
|
}
|