- Add searchParams state connected to useQuery queryKey/queryFn - Enable role and status columns as searchable select dropdowns - Map username search field to backend 'search' param - Add onSubmit/onReset callbacks on ProTable
223 lines
7.3 KiB
TypeScript
223 lines
7.3 KiB
TypeScript
// ============================================================
|
|
// 账号管理
|
|
// ============================================================
|
|
|
|
import { useState } from 'react'
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { Button, message, Tag, Modal, Form, Input, Select, Popconfirm, Space } from 'antd'
|
|
import type { ProColumns } from '@ant-design/pro-components'
|
|
import { ProTable } from '@ant-design/pro-components'
|
|
import { accountService } from '@/services/accounts'
|
|
import { PageHeader } from '@/components/PageHeader'
|
|
import type { AccountPublic } from '@/types'
|
|
|
|
const roleLabels: Record<string, string> = {
|
|
super_admin: '超级管理员',
|
|
admin: '管理员',
|
|
user: '用户',
|
|
}
|
|
|
|
const roleColors: Record<string, string> = {
|
|
super_admin: 'red',
|
|
admin: 'blue',
|
|
user: 'default',
|
|
}
|
|
|
|
const statusLabels: Record<string, string> = {
|
|
active: '正常',
|
|
disabled: '已禁用',
|
|
suspended: '已封禁',
|
|
}
|
|
|
|
const statusColors: Record<string, string> = {
|
|
active: 'green',
|
|
disabled: 'default',
|
|
suspended: 'red',
|
|
}
|
|
|
|
export default function Accounts() {
|
|
const queryClient = useQueryClient()
|
|
const [form] = Form.useForm()
|
|
const [modalOpen, setModalOpen] = useState(false)
|
|
const [editingId, setEditingId] = useState<string | null>(null)
|
|
const [searchParams, setSearchParams] = useState<Record<string, string>>({})
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ['accounts', searchParams],
|
|
queryFn: ({ signal }) => accountService.list(searchParams, signal),
|
|
})
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: ({ id, data }: { id: string; data: Partial<AccountPublic> }) =>
|
|
accountService.update(id, data),
|
|
onSuccess: () => {
|
|
message.success('更新成功')
|
|
queryClient.invalidateQueries({ queryKey: ['accounts'] })
|
|
setModalOpen(false)
|
|
},
|
|
onError: (err: Error) => message.error(err.message || '更新失败'),
|
|
})
|
|
|
|
const statusMutation = useMutation({
|
|
mutationFn: ({ id, status }: { id: string; status: AccountPublic['status'] }) =>
|
|
accountService.updateStatus(id, { status }),
|
|
onSuccess: () => {
|
|
message.success('状态更新成功')
|
|
queryClient.invalidateQueries({ queryKey: ['accounts'] })
|
|
},
|
|
onError: (err: Error) => message.error(err.message || '状态更新失败'),
|
|
})
|
|
|
|
const columns: ProColumns<AccountPublic>[] = [
|
|
{ title: '用户名', dataIndex: 'username', width: 120, tooltip: '搜索用户名、邮箱或显示名' },
|
|
{ title: '显示名', dataIndex: 'display_name', width: 120, hideInSearch: true },
|
|
{ title: '邮箱', dataIndex: 'email', width: 180 },
|
|
{
|
|
title: '角色',
|
|
dataIndex: 'role',
|
|
width: 120,
|
|
valueType: 'select',
|
|
valueEnum: {
|
|
super_admin: { text: '超级管理员' },
|
|
admin: { text: '管理员' },
|
|
user: { text: '用户' },
|
|
},
|
|
render: (_, record) => <Tag color={roleColors[record.role]}>{roleLabels[record.role] || record.role}</Tag>,
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
width: 100,
|
|
valueType: 'select',
|
|
valueEnum: {
|
|
active: { text: '正常', status: 'Success' },
|
|
disabled: { text: '已禁用', status: 'Default' },
|
|
suspended: { text: '已封禁', status: 'Error' },
|
|
},
|
|
render: (_, record) => <Tag color={statusColors[record.status]}>{statusLabels[record.status] || record.status}</Tag>,
|
|
},
|
|
{
|
|
title: '2FA',
|
|
dataIndex: 'totp_enabled',
|
|
width: 80,
|
|
hideInSearch: true,
|
|
render: (_, record) => record.totp_enabled ? <Tag color="green">已启用</Tag> : <Tag>未启用</Tag>,
|
|
},
|
|
{
|
|
title: 'LLM 路由',
|
|
dataIndex: 'llm_routing',
|
|
width: 120,
|
|
hideInSearch: true,
|
|
valueType: 'select',
|
|
valueEnum: {
|
|
relay: { text: 'SaaS 中转', status: 'Success' },
|
|
local: { text: '本地直连', status: 'Default' },
|
|
},
|
|
},
|
|
{
|
|
title: '最后登录',
|
|
dataIndex: 'last_login_at',
|
|
width: 180,
|
|
hideInSearch: true,
|
|
render: (_, record) => record.last_login_at ? new Date(record.last_login_at).toLocaleString('zh-CN') : '-',
|
|
},
|
|
{
|
|
title: '操作',
|
|
width: 200,
|
|
hideInSearch: true,
|
|
render: (_, record) => (
|
|
<Space>
|
|
<Button
|
|
size="small"
|
|
onClick={() => { setEditingId(record.id); form.setFieldsValue(record); setModalOpen(true) }}
|
|
>
|
|
编辑
|
|
</Button>
|
|
{record.status === 'active' ? (
|
|
<Popconfirm title="确定禁用此账号?" onConfirm={() => statusMutation.mutate({ id: record.id, status: 'disabled' })}>
|
|
<Button size="small" danger>禁用</Button>
|
|
</Popconfirm>
|
|
) : (
|
|
<Popconfirm title="确定启用此账号?" onConfirm={() => statusMutation.mutate({ id: record.id, status: 'active' })}>
|
|
<Button size="small" type="primary">启用</Button>
|
|
</Popconfirm>
|
|
)}
|
|
</Space>
|
|
),
|
|
},
|
|
]
|
|
|
|
const handleSave = async () => {
|
|
const values = await form.validateFields()
|
|
if (editingId) {
|
|
updateMutation.mutate({ id: editingId, data: values })
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<PageHeader title="账号管理" description="管理系统用户账号、角色与权限" />
|
|
|
|
<ProTable<AccountPublic>
|
|
columns={columns}
|
|
dataSource={data?.items ?? []}
|
|
loading={isLoading}
|
|
rowKey="id"
|
|
search={{}}
|
|
toolBarRender={() => []}
|
|
onSubmit={(values) => {
|
|
const filtered: Record<string, string> = {}
|
|
for (const [k, v] of Object.entries(values)) {
|
|
if (v !== undefined && v !== null && v !== '') {
|
|
// Map 'username' search field to backend 'search' param
|
|
if (k === 'username') {
|
|
filtered.search = String(v)
|
|
} else {
|
|
filtered[k] = String(v)
|
|
}
|
|
}
|
|
}
|
|
setSearchParams(filtered)
|
|
}}
|
|
onReset={() => setSearchParams({})}
|
|
pagination={{
|
|
total: data?.total ?? 0,
|
|
pageSize: data?.page_size ?? 20,
|
|
current: data?.page ?? 1,
|
|
showSizeChanger: false,
|
|
}}
|
|
/>
|
|
|
|
<Modal
|
|
title={<span className="text-base font-semibold">编辑账号</span>}
|
|
open={modalOpen}
|
|
onOk={handleSave}
|
|
onCancel={() => { setModalOpen(false); setEditingId(null); form.resetFields() }}
|
|
confirmLoading={updateMutation.isPending}
|
|
>
|
|
<Form form={form} layout="vertical" className="mt-4">
|
|
<Form.Item name="display_name" label="显示名">
|
|
<Input />
|
|
</Form.Item>
|
|
<Form.Item name="email" label="邮箱">
|
|
<Input type="email" />
|
|
</Form.Item>
|
|
<Form.Item name="role" label="角色">
|
|
<Select options={[
|
|
{ value: 'super_admin', label: '超级管理员' },
|
|
{ value: 'admin', label: '管理员' },
|
|
{ value: 'user', label: '用户' },
|
|
]} />
|
|
</Form.Item>
|
|
<Form.Item name="llm_routing" label="LLM 路由模式">
|
|
<Select options={[
|
|
{ value: 'local', label: '本地直连' },
|
|
{ value: 'relay', label: 'SaaS 中转 (Token 池)' },
|
|
]} />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</div>
|
|
)
|
|
}
|