// ============================================================ // 账号管理 // ============================================================ import { useState, useEffect } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { Button, message, Tag, Modal, Form, Input, Select, Popconfirm, Space, Divider } from 'antd' import type { ProColumns } from '@ant-design/pro-components' import { ProTable } from '@ant-design/pro-components' import { accountService } from '@/services/accounts' import { industryService } from '@/services/industries' import { billingService } from '@/services/billing' import { PageHeader } from '@/components/PageHeader' import type { AccountPublic } from '@/types' const roleLabels: Record = { super_admin: '超级管理员', admin: '管理员', user: '用户', } const roleColors: Record = { super_admin: 'red', admin: 'blue', user: 'default', } const statusLabels: Record = { active: '正常', disabled: '已禁用', suspended: '已封禁', } const statusColors: Record = { 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(null) const [searchParams, setSearchParams] = useState>({}) const { data, isLoading } = useQuery({ queryKey: ['accounts', searchParams], queryFn: ({ signal }) => accountService.list(searchParams, signal), }) // 获取行业列表(用于下拉选择) const { data: industriesData } = useQuery({ queryKey: ['industries-all'], queryFn: ({ signal }) => industryService.list({ page: 1, page_size: 100, status: 'active' }, signal), }) // 获取当前编辑用户的行业授权 const { data: accountIndustries } = useQuery({ queryKey: ['account-industries', editingId], queryFn: ({ signal }) => industryService.getAccountIndustries(editingId!, signal), enabled: !!editingId, }) // 当账户行业数据加载完且正在编辑时,同步到表单 // Guard: only sync when editingId matches the query key useEffect(() => { if (accountIndustries && editingId) { const ids = accountIndustries.map((item) => item.industry_id) form.setFieldValue('industry_ids', ids) } }, [accountIndustries, editingId, form]) // 获取所有活跃计划(用于管理员切换) const { data: plansData } = useQuery({ queryKey: ['billing-plans'], queryFn: ({ signal }) => billingService.listPlans(signal), }) const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: Partial }) => accountService.update(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['accounts'] }) }, 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 setIndustriesMutation = useMutation({ mutationFn: ({ accountId, industries }: { accountId: string; industries: string[] }) => industryService.setAccountIndustries(accountId, { industries: industries.map((id, idx) => ({ industry_id: id, is_primary: idx === 0, })), }), onError: (err: Error) => message.error(err.message || '行业授权更新失败'), }) // 管理员切换用户计划 const switchPlanMutation = useMutation({ mutationFn: ({ accountId, planId }: { accountId: string; planId: string }) => billingService.adminSwitchPlan(accountId, planId), onSuccess: () => message.success('计划切换成功'), onError: (err: Error) => message.error(err.message || '计划切换失败'), }) const columns: ProColumns[] = [ { 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) => {roleLabels[record.role] || record.role}, }, { title: '状态', dataIndex: 'status', width: 100, valueType: 'select', valueEnum: { active: { text: '正常', status: 'Success' }, disabled: { text: '已禁用', status: 'Default' }, suspended: { text: '已封禁', status: 'Error' }, }, render: (_, record) => {statusLabels[record.status] || record.status}, }, { title: '2FA', dataIndex: 'totp_enabled', width: 80, hideInSearch: true, render: (_, record) => record.totp_enabled ? 已启用 : 未启用, }, { 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) => ( {record.status === 'active' ? ( statusMutation.mutate({ id: record.id, status: 'disabled' })}> ) : ( statusMutation.mutate({ id: record.id, status: 'active' })}> )} ), }, ] const handleSave = async () => { const values = await form.validateFields() if (!editingId) return try { // 更新基础信息 const { industry_ids, plan_id, ...accountData } = values await updateMutation.mutateAsync({ id: editingId, data: accountData }) // 更新行业授权(如果变更了) const newIndustryIds: string[] = industry_ids || [] const oldIndustryIds = accountIndustries?.map((i) => i.industry_id) || [] const changed = newIndustryIds.length !== oldIndustryIds.length || newIndustryIds.some((id) => !oldIndustryIds.includes(id)) if (changed) { await setIndustriesMutation.mutateAsync({ accountId: editingId, industries: newIndustryIds }) message.success('行业授权已更新') queryClient.invalidateQueries({ queryKey: ['account-industries'] }) } // 切换订阅计划(如果选择了新计划) if (plan_id) { await switchPlanMutation.mutateAsync({ accountId: editingId, planId: plan_id }) } handleClose() } catch { // Errors handled by mutation onError callbacks } } const handleClose = () => { setModalOpen(false) setEditingId(null) form.resetFields() } const industryOptions = (industriesData?.items || []).map((item) => ({ value: item.id, label: `${item.icon} ${item.name}`, })) const planOptions = (plansData || []).map((plan) => ({ value: plan.id, label: `${plan.display_name} (¥${(plan.price_cents / 100).toFixed(0)}/月)`, })) return (
columns={columns} dataSource={data?.items ?? []} loading={isLoading} rowKey="id" search={{}} toolBarRender={() => []} onSubmit={(values) => { const filtered: Record = {} for (const [k, v] of Object.entries(values)) { if (v !== undefined && v !== null && v !== '') { 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, }} /> 编辑账号} open={modalOpen} onOk={handleSave} onCancel={handleClose} confirmLoading={updateMutation.isPending || setIndustriesMutation.isPending || switchPlanMutation.isPending} width={560} >
订阅计划
) }