import React, { useEffect, useState } from 'react'; import { Button, Card, Checkbox, Form, Input, InputNumber, message, Modal, Popconfirm, Progress, Select, Space, Switch, Table, Tabs, Tag, Typography, } from 'antd'; import { UserOutlined, WalletOutlined, SearchOutlined, StopOutlined, CheckCircleOutlined, PlusOutlined, MenuOutlined, LockOutlined, SettingOutlined, SaveOutlined, DatabaseOutlined, TeamOutlined, PictureOutlined, SecurityScanOutlined, } from '@ant-design/icons'; import { adjustCredits, adminGetPrivatePortraitConfig, adminUpdatePrivatePortraitConfig, createUser, deleteUserResourceCapacity, getAdminUsers, getMenuConfigs, getTeamOptions, getSystemConfigs, getUserResourceCapacity, resetUserPassword, saveUserResourceCapacity, toggleUserStatus, updateFrontendUserKind, updateUserTeam, updateSystemConfig, updateUserMenus, updateUserAdminStatus, } from '../api'; import type { AdminTeamOption, AdminUser, AdminUserResourceCapacityOut, PrivatePortraitConfig, ResourceCapacityUnit, ResourceCapacityUsage, SystemConfig } from '../types'; import { formatDate } from '../utils/formatDate'; const TEAM_UNASSIGNED_VALUE = '__none__'; const capacityUnitOptions: { value: ResourceCapacityUnit; label: string }[] = [ { value: 'MB', label: 'MB(1024 × 1024 字节)' }, { value: 'GB', label: 'GB(1024 × 1024 × 1024 字节)' }, { value: 'TB', label: 'TB(1024 × 1024 × 1024 × 1024 字节)' }, ]; function formatBytes(bytes?: number | null): string { if (bytes === null || bytes === undefined) return '-'; const value = Number(bytes || 0); if (value < 1024) return `${value} B`; const units = ['KB', 'MB', 'GB', 'TB', 'PB']; let size = value; let unitIndex = -1; do { size /= 1024; unitIndex += 1; } while (size >= 1024 && unitIndex < units.length - 1); return `${size.toFixed(size >= 100 ? 0 : size >= 10 ? 1 : 2)} ${units[unitIndex]}`; } function capacitySourceLabel(capacity?: ResourceCapacityUsage | null): string { if (!capacity || !capacity.enabled) return '未开启'; if (capacity.source === 'user') return '个人'; if (capacity.source === 'global') return '全局'; return '未开启'; } const AdminUsers: React.FC = () => { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [activeTab, setActiveTab] = useState('frontend'); const [frontendKindFilter, setFrontendKindFilter] = useState(''); const [teamFilter, setTeamFilter] = useState(''); const [teamOptions, setTeamOptions] = useState([]); const [creditModal, setCreditModal] = useState<{ open: boolean; user: AdminUser | null }>({ open: false, user: null }); const [createModal, setCreateModal] = useState(false); const [createType, setCreateType] = useState('frontend'); const [menuModal, setMenuModal] = useState<{ open: boolean; user: AdminUser | null }>({ open: false, user: null }); const [allMenus, setAllMenus] = useState([]); const [checkedMenus, setCheckedMenus] = useState([]); const [resetPwdModal, setResetPwdModal] = useState<{ open: boolean; user: AdminUser | null }>({ open: false, user: null }); const [capacityModal, setCapacityModal] = useState<{ open: boolean; user: AdminUser | null; detail: AdminUserResourceCapacityOut | null }>({ open: false, user: null, detail: null }); const [teamModal, setTeamModal] = useState<{ open: boolean; user: AdminUser | null }>({ open: false, user: null }); const [portraitModal, setPortraitModal] = useState<{ open: boolean; user: AdminUser | null; config: PrivatePortraitConfig | null }>({ open: false, user: null, config: null }); const [capacityLoading, setCapacityLoading] = useState(false); const [capacitySaving, setCapacitySaving] = useState(false); const [teamSaving, setTeamSaving] = useState(false); const [portraitLoading, setPortraitLoading] = useState(false); const [portraitSaving, setPortraitSaving] = useState(false); const [form] = Form.useForm(); const [createForm] = Form.useForm(); const [resetPwdForm] = Form.useForm(); const [capacityForm] = Form.useForm(); const [teamForm] = Form.useForm(); const [portraitForm] = Form.useForm(); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [total, setTotal] = useState(0); const [creditConfigs, setCreditConfigs] = useState([]); const [configForm] = Form.useForm(); const [configSaving, setConfigSaving] = useState(false); const load = async () => { setLoading(true); try { const data = await getAdminUsers( page, pageSize, search || undefined, activeTab, activeTab === 'frontend' ? (frontendKindFilter || undefined) : undefined, activeTab === 'frontend' ? (teamFilter || undefined) : undefined, ); setUsers(data.items || []); setTotal(data.total || 0); } catch { /* auth error handled by client */ } setLoading(false); }; useEffect(() => { load(); }, [page, pageSize, search, activeTab, frontendKindFilter, teamFilter]); useEffect(() => { getTeamOptions(true).then(setTeamOptions).catch(() => {}); }, []); useEffect(() => { const loadCreditConfigs = async () => { try { const configs = await getSystemConfigs(); const credit = configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits')); setCreditConfigs(credit); const formValues: Record = {}; credit.forEach(c => { formValues[c.key] = c.value; }); configForm.setFieldsValue(formValues); } catch { /* auth error handled by client */ } }; loadCreditConfigs(); }, []); const handleSaveCreditConfigs = async () => { try { const values = await configForm.validateFields(); setConfigSaving(true); for (const config of creditConfigs) { const newVal = values[config.key]; if (newVal !== undefined && String(newVal) !== config.value) { await updateSystemConfig(config.id, String(newVal ?? '')); } } message.success('积分配置已保存'); const configs = await getSystemConfigs(); setCreditConfigs(configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits'))); } catch (e: any) { message.error(e?.message || '保存失败'); } finally { setConfigSaving(false); } }; const handlePageChange = (p: number, ps: number) => { setPage(p); setPageSize(ps); }; const handleSearch = () => load(); const handleAdjustCredits = async () => { try { const values = await form.validateFields(); const { user } = creditModal; if (!user) return; await adjustCredits(user.id, values.amount, values.description); message.success(`已${values.amount > 0 ? '增加' : '扣除'} ${Math.abs(values.amount)} 积分`); setCreditModal({ open: false, user: null }); form.resetFields(); load(); } catch { /* validation */ } }; const handleToggleStatus = async (user: AdminUser) => { await toggleUserStatus(user.id, !user.isActive); message.success(user.isActive ? '已禁用该用户' : '已启用该用户'); load(); }; const handleCreateUser = async () => { try { const values = await createForm.validateFields(); const userType = values.user_type || 'frontend'; await createUser({ username: userType === 'admin' ? values.username : undefined, password: values.password, email: values.email || undefined, phone: userType === 'frontend' ? values.phone : (values.phone || undefined), credits: values.credits || 0, user_type: userType, is_admin: userType === 'admin' ? (values.is_admin ?? false) : false, frontend_user_kind: values.frontend_user_kind || 'external', private_portrait_asset_limit: userType === 'frontend' ? Number(values.private_portrait_asset_limit ?? 5) : 0, }); message.success('用户创建成功'); setCreateModal(false); createForm.resetFields(); setCreateType('frontend'); load(); } catch { /* validation */ } }; const openMenuModal = async (user: AdminUser) => { try { const menus = await getMenuConfigs(); const isAdminUser = user.userType === 'admin'; setAllMenus(menus.filter((m: any) => { const target = m.menu_target ?? m.menuTarget ?? 'frontend'; return isAdminUser ? (target === 'admin' || target === 'both') : (target === 'frontend' || target === 'both'); })); setCheckedMenus(user.allowedMenus || []); setMenuModal({ open: true, user }); } catch { message.error('加载菜单失败'); } }; const openCapacityModal = async (user: AdminUser) => { setCapacityLoading(true); setCapacityModal({ open: true, user, detail: null }); try { const detail = await getUserResourceCapacity(user.id); const initialConfig = detail.userConfig; capacityForm.setFieldsValue({ enabled: initialConfig?.enabled ?? false, limitValue: initialConfig?.limitValue ?? detail.effective.limitValue ?? detail.globalConfig.limitValue ?? '1.000', limitUnit: initialConfig?.limitUnit ?? detail.effective.limitUnit ?? detail.globalConfig.limitUnit ?? 'GB', }); setCapacityModal({ open: true, user, detail }); } catch (e: any) { message.error(e?.message || '加载容量配置失败'); setCapacityModal({ open: false, user: null, detail: null }); } finally { setCapacityLoading(false); } }; const handleSaveCapacity = async () => { const { user } = capacityModal; if (!user) return; try { const values = await capacityForm.validateFields(); setCapacitySaving(true); await saveUserResourceCapacity(user.id, { enabled: !!values.enabled, limitValue: String(values.limitValue ?? '1.000'), limitUnit: values.limitUnit || 'GB', }); message.success('用户容量配置已保存'); setCapacityModal({ open: false, user: null, detail: null }); capacityForm.resetFields(); load(); } catch (e: any) { message.error(e?.message || '保存失败'); } finally { setCapacitySaving(false); } }; const handleRestoreGlobalCapacity = async () => { const { user } = capacityModal; if (!user) return; try { setCapacitySaving(true); await deleteUserResourceCapacity(user.id); message.success('已恢复为全局容量配置'); setCapacityModal({ open: false, user: null, detail: null }); capacityForm.resetFields(); load(); } catch (e: any) { message.error(e?.message || '恢复失败'); } finally { setCapacitySaving(false); } }; const openPortraitModal = async (user: AdminUser) => { setPortraitLoading(true); setPortraitModal({ open: true, user, config: null }); portraitForm.setFieldsValue({ privatePortraitAssetLimit: user.privatePortraitAssetLimit ?? 5 }); try { const config = await adminGetPrivatePortraitConfig(user.id); portraitForm.setFieldsValue({ privatePortraitAssetLimit: config.assetLimit }); setPortraitModal({ open: true, user, config }); } catch (e: any) { message.error(e?.message || '加载私域人像素材库配置失败'); setPortraitModal({ open: false, user: null, config: null }); } finally { setPortraitLoading(false); } }; const handleSavePortraitConfig = async () => { const { user } = portraitModal; if (!user) return; try { const values = await portraitForm.validateFields(); const limit = Number(values.privatePortraitAssetLimit ?? 0); setPortraitSaving(true); const config = await adminUpdatePrivatePortraitConfig(user.id, limit); message.success(limit > 0 ? `私域人像素材库已开启,限制 ${limit} 个` : '私域人像素材库已关闭'); setPortraitModal({ open: false, user: null, config }); portraitForm.resetFields(); load(); } catch (e: any) { if (e?.errorFields) return; message.error(e?.message || '保存私域人像素材库配置失败'); } finally { setPortraitSaving(false); } }; const openTeamModal = (user: AdminUser) => { teamForm.setFieldsValue({ teamId: user.teamId || '' }); setTeamModal({ open: true, user }); }; const handleSaveTeam = async () => { const { user } = teamModal; if (!user) return; try { const values = await teamForm.validateFields(); setTeamSaving(true); await updateUserTeam(user.id, values.teamId || null); message.success('用户团队已更新'); setTeamModal({ open: false, user: null }); teamForm.resetFields(); load(); } catch (e: any) { if (e?.errorFields) return; message.error(e?.message || '保存团队失败'); } finally { setTeamSaving(false); } }; const menuGroups = allMenus.filter((m: any) => (m.menu_type ?? m.menuType) === 'group'); const menuPages = allMenus.filter((m: any) => (m.menu_type ?? m.menuType) !== 'group'); const childMap: Record = {}; menuPages.filter((m: any) => m.parent_id ?? m.parentId).forEach((m: any) => { const pid = m.parent_id ?? m.parentId; if (!childMap[pid]) childMap[pid] = []; childMap[pid].push(m); }); const topLevelPages = menuPages.filter((m: any) => !(m.parent_id ?? m.parentId)); const handleSaveMenus = async () => { const { user } = menuModal; if (!user) return; try { await updateUserMenus(user.id, checkedMenus.length > 0 ? checkedMenus : null); message.success('菜单权限已更新'); setMenuModal({ open: false, user: null }); load(); } catch (e: any) { message.error(e?.message || '保存失败'); } }; const handleResetPassword = async () => { try { const values = await resetPwdForm.validateFields(); const { user } = resetPwdModal; if (!user) return; await resetUserPassword(user.id, values.newPassword); message.success(`已重置 ${user.username} 的密码`); setResetPwdModal({ open: false, user: null }); resetPwdForm.resetFields(); } catch { /* validation */ } }; const handleUpdateFrontendKind = async (user: AdminUser, kind: 'internal' | 'external') => { try { await updateFrontendUserKind(user.id, kind); message.success(kind === 'internal' ? '已设为前台内部用户' : '已设为前台外部用户'); load(); } catch (e: any) { message.error(e?.message || '设置失败'); } }; const handleToggleAdminStatus = async (user: AdminUser) => { try { await updateUserAdminStatus(user.id, !user.isAdmin); message.success(user.isAdmin ? '已取消超级管理员' : '已设为超级管理员'); load(); } catch (e: any) { message.error(e?.message || '设置失败'); } }; const isAdminTab = activeTab === 'admin'; const columns = [ { title: '用户', key: 'user', width: 200, render: (_: any, r: AdminUser) => (
{r.username.charAt(0).toUpperCase()}
{r.username} {r.isAdmin && 超级管理员}
{r.email}
), }, ...(!isAdminTab ? [{ title: '积分余额', dataIndex: 'credits', width: 120, sorter: (a: AdminUser, b: AdminUser) => a.credits - b.credits, render: (v: number) => ( 0 ? '#10b981' : '#ef4444', fontSize: 15 }}> {v.toLocaleString()} ), }] : []), { title: '手机号', dataIndex: 'phone', width: 130, render: (v: string) => {v || '-'}, }, ...(!isAdminTab ? [{ title: '前台归类', dataIndex: 'frontendUserKind', width: 110, render: (v: string) => {v === 'internal' ? '内部用户' : '外部用户'}, }] : []), ...(!isAdminTab ? [{ title: '团队', dataIndex: 'teamName', width: 140, render: (v: string | null | undefined) => v ? {v} : 未分配, }] : []), ...(!isAdminTab ? [{ title: '私域人像素材库', dataIndex: 'privatePortraitAssetLimit', width: 150, render: (v: number) => { const limit = Number(v || 0); return limit > 0 ? 开启:{limit} 个 : 未开启; }, }] : []), ...(!isAdminTab ? [{ title: '资源容量', dataIndex: 'resourceCapacity', width: 230, render: (capacity: ResourceCapacityUsage | null | undefined) => { const usedText = formatBytes(capacity?.usedBytes || 0); if (!capacity || !capacity.enabled) { return (
未开启 已用 {usedText}
); } const percent = Math.min(Number(capacity.usagePercent || 0), 100); return (
{capacitySourceLabel(capacity)} {capacity.exceeded && 已超额} {usedText} / {formatBytes(capacity.totalBytes)},可用 {formatBytes(capacity.availableBytes)}
); }, }] : []), { title: '状态', dataIndex: 'isActive', width: 80, render: (v: boolean) => ( {v ? '正常' : '禁用'} ), }, { title: '注册时间', dataIndex: 'createdAt', width: 120, render: (v: string) => {formatDate(v)}, }, { title: '最后登录', dataIndex: 'lastLoginAt', width: 140, render: (v: string) => {formatDate(v)}, }, { title: '操作', key: 'action', width: 560, fixed: 'right' as const, render: (_: any, r: AdminUser) => ( {!isAdminTab && ( )} {!isAdminTab && ( )} {!isAdminTab && ( )} {!isAdminTab && ( )} {!isAdminTab && r.frontendUserKind !== 'internal' && ( )} {!isAdminTab && r.frontendUserKind === 'internal' && ( )} {isAdminTab && ( handleToggleAdminStatus(r)} > )} handleToggleStatus(r)} > ), }, ]; return (
{/*
积分配置 设置用户注册和登录赠送的积分
注册赠送积分} extra="用户注册时赠送的初始积分" > 每日登录赠送积分} extra="用户每日首次登录赠送的积分" > 启用每日登录积分} extra="是否开启每日登录赠送积分功能" >
c.key === 'user_login_credits_enabled')?.value === 'true'} /> {creditConfigs.find(c => c.key === 'user_login_credits_enabled')?.value === 'true' ? '已启用' : '已禁用'}
*/}
} value={search} onChange={e => setSearch(e.target.value)} onPressEnter={handleSearch} style={{ width: 280, borderRadius: 8 }} allowClear /> {!isAdminTab && ( { setPage(1); setTeamFilter(v); }} style={{ width: 170 }} options={[ { value: '', label: '全部团队' }, { value: TEAM_UNASSIGNED_VALUE, label: '未分配团队' }, ...teamOptions.map(t => ({ value: t.id, label: t.status === 'disabled' ? `${t.name}(禁用)` : t.name })), ]} /> )}
{ setActiveTab(key); setFrontendKindFilter(''); setTeamFilter(''); setPage(1); }} items={[ { key: 'frontend', label: '前台用户' }, { key: 'admin', label: '后台用户' }, ]} /> `共 ${t} 个用户`, }} scroll={{ x: 1280 }} /> 调整积分 - {creditModal.user?.username}} open={creditModal.open} onOk={handleAdjustCredits} onCancel={() => { setCreditModal({ open: false, user: null }); form.resetFields(); }} okText="确认" cancelText="取消" width={440} >
当前积分: {creditModal.user?.credits.toLocaleString()}
`${v}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')} />
团队设置 - {teamModal.user?.username}} open={teamModal.open} confirmLoading={teamSaving} onOk={handleSaveTeam} onCancel={() => { setTeamModal({ open: false, user: null }); teamForm.resetFields(); }} okText="保存" cancelText="取消" width={460} >
恢复全局设置会删除该用户个人配置,让用户重新按全局规则判断。
私域人像素材库设置 - {portraitModal.user?.username}} open={portraitModal.open} confirmLoading={portraitSaving} onOk={handleSavePortraitConfig} onCancel={() => { setPortraitModal({ open: false, user: null, config: null }); portraitForm.resetFields(); }} okText="保存" cancelText="取消" width={520} > 当前状态:{portraitModal.config?.enabled ? 已开启 : 未开启} 已用:{portraitModal.config?.usedAssetCount ?? '-'} 个; 剩余:{portraitModal.config?.enabled ? portraitModal.config.remainingAssetCount : 0} 个
创建用户} open={createModal} onOk={handleCreateUser} onCancel={() => { setCreateModal(false); createForm.resetFields(); setCreateType('frontend'); }} okText="创建" cancelText="取消" width={480} >
{ if (changed.user_type) setCreateType(changed.user_type); }} > )} {createType === 'frontend' ? ( ) : ( )} {createType === 'frontend' && ( )} {createType === 'frontend' && ( )} {createType === 'admin' && ( )}
菜单权限 - {menuModal.user?.username} ({menuModal.user?.userType === 'admin' ? '后台菜单' : '前台菜单'})} open={menuModal.open} onOk={handleSaveMenus} onCancel={() => { setMenuModal({ open: false, user: null }); }} okText="保存" cancelText="取消" width={520} >
勾选该用户可访问的菜单,不勾选则显示全部菜单
setCheckedMenus(vals as string[])}> {topLevelPages.map((m: any) => ( {m.label} {m.path} ))} {menuGroups.map((g: any) => { const children = childMap[g.id] || []; if (children.length === 0) return null; return (
{g.label}
{children.map((c: any) => ( {c.label} {c.path} ))}
); })}
重置密码 - {resetPwdModal.user?.username}} open={resetPwdModal.open} onOk={handleResetPassword} onCancel={() => { setResetPwdModal({ open: false, user: null }); resetPwdForm.resetFields(); }} okText="确认重置" cancelText="取消" width={420} >
); }; export default AdminUsers;