import { useState, useEffect } from 'react'; import { Button } from "@heroui/button"; import { Card, CardBody, CardHeader } from "@heroui/card"; import { Input } from "@heroui/input"; import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell } from "@heroui/table"; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/modal"; import { Chip } from "@heroui/chip"; import { Select, SelectItem } from "@heroui/select"; import { RadioGroup, Radio } from "@heroui/radio"; import { DatePicker } from "@heroui/date-picker"; import { Spinner } from "@heroui/spinner"; import { Progress } from "@heroui/progress"; import toast from 'react-hot-toast'; import { User, UserForm, UserTunnel, UserTunnelForm, TunnelAssignItem, Tunnel, SpeedLimit, Pagination as PaginationType } from '@/types'; import { getAllUsers, createUser, updateUser, deleteUser, getTunnelList, assignUserTunnel, batchAssignUserTunnel, getUserTunnelList, removeUserTunnel, updateUserTunnel, getSpeedLimitList, resetUserFlow } from '@/api'; import { SearchIcon, EditIcon, DeleteIcon, UserIcon, SettingsIcon } from '@/components/icons'; import { parseDate } from "@internationalized/date"; // 工具函数 const formatFlow = (value: number, unit: string = 'bytes'): string => { if (unit === 'gb') { return `${value} GB`; } else { if (value === 0) return '0 B'; if (value < 1024) return `${value} B`; if (value < 1024 * 1024) return `${(value / 1024).toFixed(2)} KB`; if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(2)} MB`; return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GB`; } }; const formatDate = (timestamp: number): string => { return new Date(timestamp).toLocaleString(); }; const getExpireStatus = (expTime: number) => { const now = Date.now(); if (expTime < now) { return { color: 'danger' as const, text: '已过期' }; } const diffDays = Math.ceil((expTime - now) / (1000 * 60 * 60 * 24)); if (diffDays <= 7) { return { color: 'warning' as const, text: `${diffDays}天后过期` }; } return { color: 'success' as const, text: '正常' }; }; // 获取用户状态(根据status字段) const getUserStatus = (user: User) => { if (user.status === 1) { return { color: 'success' as const, text: '正常' }; } else { return { color: 'danger' as const, text: '禁用' }; } }; const calculateUserTotalUsedFlow = (user: User): number => { return (user.inFlow || 0) + (user.outFlow || 0); }; const calculateTunnelUsedFlow = (tunnel: UserTunnel): number => { const inFlow = tunnel.inFlow || 0; const outFlow = tunnel.outFlow || 0; // 后端已按计费类型处理流量,前端直接使用入站+出站总和 return inFlow + outFlow; }; export default function UserPage() { // 状态管理 const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); const [searchKeyword, setSearchKeyword] = useState(''); const [pagination, setPagination] = useState({ current: 1, size: 10, total: 0 }); // 用户表单相关状态 const { isOpen: isUserModalOpen, onOpen: onUserModalOpen, onClose: onUserModalClose } = useDisclosure(); const [isEdit, setIsEdit] = useState(false); const [userForm, setUserForm] = useState({ user: '', pwd: '', status: 1, flow: 100, num: 10, expTime: null, flowResetTime: 0 }); const [userFormLoading, setUserFormLoading] = useState(false); // 隧道权限管理相关状态 const { isOpen: isTunnelModalOpen, onOpen: onTunnelModalOpen, onClose: onTunnelModalClose } = useDisclosure(); const [currentUser, setCurrentUser] = useState(null); const [userTunnels, setUserTunnels] = useState([]); const [tunnelListLoading, setTunnelListLoading] = useState(false); // 分配新隧道权限相关状态 const [tunnelForm, setTunnelForm] = useState({ tunnelId: null, flow: 100, num: 10, expTime: null, flowResetTime: 0, speedId: null }); const [assignLoading, setAssignLoading] = useState(false); const [batchTunnelSelections, setBatchTunnelSelections] = useState>(new Map()); // 编辑隧道权限相关状态 const { isOpen: isEditTunnelModalOpen, onOpen: onEditTunnelModalOpen, onClose: onEditTunnelModalClose } = useDisclosure(); const [editTunnelForm, setEditTunnelForm] = useState(null); const [editTunnelLoading, setEditTunnelLoading] = useState(false); // 删除确认相关状态 const { isOpen: isDeleteModalOpen, onOpen: onDeleteModalOpen, onClose: onDeleteModalClose } = useDisclosure(); const [userToDelete, setUserToDelete] = useState(null); // 删除隧道权限确认相关状态 const { isOpen: isDeleteTunnelModalOpen, onOpen: onDeleteTunnelModalOpen, onClose: onDeleteTunnelModalClose } = useDisclosure(); const [tunnelToDelete, setTunnelToDelete] = useState(null); // 重置流量确认相关状态 const { isOpen: isResetFlowModalOpen, onOpen: onResetFlowModalOpen, onClose: onResetFlowModalClose } = useDisclosure(); const [userToReset, setUserToReset] = useState(null); const [resetFlowLoading, setResetFlowLoading] = useState(false); // 重置隧道流量确认相关状态 const { isOpen: isResetTunnelFlowModalOpen, onOpen: onResetTunnelFlowModalOpen, onClose: onResetTunnelFlowModalClose } = useDisclosure(); const [tunnelToReset, setTunnelToReset] = useState(null); const [resetTunnelFlowLoading, setResetTunnelFlowLoading] = useState(false); // 其他数据 const [tunnels, setTunnels] = useState([]); const [speedLimits, setSpeedLimits] = useState([]); // 生命周期 useEffect(() => { loadUsers(); loadTunnels(); loadSpeedLimits(); }, [pagination.current, pagination.size, searchKeyword]); // 数据加载函数 const loadUsers = async () => { setLoading(true); try { const response = await getAllUsers({ current: pagination.current, size: pagination.size, keyword: searchKeyword }); if (response.code === 0) { const data = response.data || {}; setUsers(data || []); } else { toast.error(response.msg || '获取用户列表失败'); } } catch (error) { toast.error('获取用户列表失败'); } finally { setLoading(false); } }; const loadTunnels = async () => { try { const response = await getTunnelList(); if (response.code === 0) { setTunnels(response.data || []); } } catch (error) { console.error('获取隧道列表失败:', error); } }; const loadSpeedLimits = async () => { try { const response = await getSpeedLimitList(); if (response.code === 0) { setSpeedLimits(response.data || []); } } catch (error) { console.error('获取限速规则列表失败:', error); } }; const loadUserTunnels = async (userId: number) => { setTunnelListLoading(true); try { const response = await getUserTunnelList({ userId }); if (response.code === 0) { setUserTunnels(response.data || []); } else { toast.error(response.msg || '获取隧道权限列表失败'); } } catch (error) { toast.error('获取隧道权限列表失败'); } finally { setTunnelListLoading(false); } }; // 用户管理操作 const handleSearch = () => { setPagination(prev => ({ ...prev, current: 1 })); loadUsers(); }; const handleAdd = () => { setIsEdit(false); setUserForm({ user: '', pwd: '', status: 1, flow: 100, num: 10, expTime: null, flowResetTime: 0 }); onUserModalOpen(); }; const handleEdit = (user: User) => { setIsEdit(true); setUserForm({ id: user.id, name: user.name, user: user.user, pwd: '', status: user.status, flow: user.flow, num: user.num, expTime: user.expTime ? new Date(user.expTime) : null, flowResetTime: user.flowResetTime ?? 0 }); onUserModalOpen(); }; const handleDelete = (user: User) => { setUserToDelete(user); onDeleteModalOpen(); }; const handleConfirmDelete = async () => { if (!userToDelete) return; try { const response = await deleteUser(userToDelete.id); if (response.code === 0) { toast.success('删除成功'); loadUsers(); onDeleteModalClose(); setUserToDelete(null); } else { toast.error(response.msg || '删除失败'); } } catch (error) { toast.error('删除失败'); } }; const handleSubmitUser = async () => { if (!userForm.user || (!userForm.pwd && !isEdit) || !userForm.expTime) { toast.error('请填写完整信息'); return; } setUserFormLoading(true); try { const submitData: any = { ...userForm, expTime: userForm.expTime.getTime() }; if (isEdit && !submitData.pwd) { delete submitData.pwd; } const response = isEdit ? await updateUser(submitData) : await createUser(submitData); if (response.code === 0) { toast.success(isEdit ? '更新成功' : '创建成功'); onUserModalClose(); loadUsers(); } else { toast.error(response.msg || (isEdit ? '更新失败' : '创建失败')); } } catch (error) { toast.error(isEdit ? '更新失败' : '创建失败'); } finally { setUserFormLoading(false); } }; // 隧道权限管理操作 const handleManageTunnels = (user: User) => { setCurrentUser(user); setTunnelForm({ tunnelId: null, flow: 100, num: 10, expTime: null, flowResetTime: 0, speedId: null }); setBatchTunnelSelections(new Map()); onTunnelModalOpen(); loadUserTunnels(user.id); }; const handleAssignTunnel = async () => { if (!tunnelForm.tunnelId || !tunnelForm.expTime || !currentUser) { toast.error('请填写完整信息'); return; } setAssignLoading(true); try { const response = await assignUserTunnel({ userId: currentUser.id, tunnelId: tunnelForm.tunnelId, flow: tunnelForm.flow, num: tunnelForm.num, expTime: tunnelForm.expTime.getTime(), flowResetTime: tunnelForm.flowResetTime, speedId: tunnelForm.speedId }); if (response.code === 0) { toast.success('分配成功'); setTunnelForm({ tunnelId: null, flow: 100, num: 10, expTime: null, flowResetTime: 0, speedId: null }); loadUserTunnels(currentUser.id); } else { toast.error(response.msg || '分配失败'); } } catch (error) { toast.error('分配失败'); } finally { setAssignLoading(false); } }; const handleBatchAssignTunnel = async () => { if (batchTunnelSelections.size === 0 || !currentUser) { toast.error('请选择至少一个隧道'); return; } setAssignLoading(true); try { const tunnelsToAssign: TunnelAssignItem[] = Array.from(batchTunnelSelections.entries()).map( ([tunnelId, speedId]) => ({ tunnelId, speedId }) ); const response = await batchAssignUserTunnel({ userId: currentUser.id, tunnels: tunnelsToAssign }); if (response.code === 0) { toast.success(response.msg || '分配成功'); setBatchTunnelSelections(new Map()); loadUserTunnels(currentUser.id); } else { toast.error(response.msg || '分配失败'); } } catch (error) { toast.error('分配失败'); } finally { setAssignLoading(false); } }; const handleEditTunnel = (userTunnel: UserTunnel) => { setEditTunnelForm({ ...userTunnel, expTime: userTunnel.expTime }); onEditTunnelModalOpen(); }; const handleUpdateTunnel = async () => { if (!editTunnelForm) return; setEditTunnelLoading(true); try { const response = await updateUserTunnel({ id: editTunnelForm.id, flow: editTunnelForm.flow, num: editTunnelForm.num, expTime: editTunnelForm.expTime, flowResetTime: editTunnelForm.flowResetTime, speedId: editTunnelForm.speedId, status: editTunnelForm.status }); if (response.code === 0) { toast.success('更新成功'); onEditTunnelModalClose(); if (currentUser) { loadUserTunnels(currentUser.id); } } else { toast.error(response.msg || '更新失败'); } } catch (error) { toast.error('更新失败'); } finally { setEditTunnelLoading(false); } }; const handleRemoveTunnel = (userTunnel: UserTunnel) => { setTunnelToDelete(userTunnel); onDeleteTunnelModalOpen(); }; const handleConfirmRemoveTunnel = async () => { if (!tunnelToDelete) return; try { const response = await removeUserTunnel({ id: tunnelToDelete.id }); if (response.code === 0) { toast.success('删除成功'); if (currentUser) { loadUserTunnels(currentUser.id); } onDeleteTunnelModalClose(); setTunnelToDelete(null); } else { toast.error(response.msg || '删除失败'); } } catch (error) { toast.error('删除失败'); } }; // 重置流量相关函数 const handleResetFlow = (user: User) => { setUserToReset(user); onResetFlowModalOpen(); }; const handleConfirmResetFlow = async () => { if (!userToReset) return; setResetFlowLoading(true); try { const response = await resetUserFlow({ id: userToReset.id, type: 1 // 1表示重置用户流量 }); if (response.code === 0) { toast.success('流量重置成功'); onResetFlowModalClose(); setUserToReset(null); loadUsers(); // 重新加载用户列表 } else { toast.error(response.msg || '重置失败'); } } catch (error) { toast.error('重置失败'); } finally { setResetFlowLoading(false); } }; // 隧道流量重置相关函数 const handleResetTunnelFlow = (userTunnel: UserTunnel) => { setTunnelToReset(userTunnel); onResetTunnelFlowModalOpen(); }; const handleConfirmResetTunnelFlow = async () => { if (!tunnelToReset) return; setResetTunnelFlowLoading(true); try { const response = await resetUserFlow({ id: tunnelToReset.id, type: 2 // 2表示重置隧道流量 }); if (response.code === 0) { toast.success('隧道流量重置成功'); onResetTunnelFlowModalClose(); setTunnelToReset(null); if (currentUser) { loadUserTunnels(currentUser.id); // 重新加载隧道权限列表 } } else { toast.error(response.msg || '重置失败'); } } catch (error) { toast.error('重置失败'); } finally { setResetTunnelFlowLoading(false); } }; // 过滤数据 const availableTunnels = tunnels.filter( tunnel => !userTunnels.some(ut => ut.tunnelId === tunnel.id) ); const availableSpeedLimits = speedLimits.filter( speedLimit => speedLimit.tunnelId === tunnelForm.tunnelId ); const editAvailableSpeedLimits = speedLimits.filter( speedLimit => speedLimit.tunnelId === editTunnelForm?.tunnelId ); const getSpeedLimitsForTunnel = (tunnelId: number) => { return speedLimits.filter(sl => sl.tunnelId === tunnelId); }; const toggleTunnelSelection = (tunnelId: number) => { setBatchTunnelSelections(prev => { const newMap = new Map(prev); if (newMap.has(tunnelId)) { newMap.delete(tunnelId); } else { newMap.set(tunnelId, null); } return newMap; }); }; const updateTunnelSpeedLimit = (tunnelId: number, speedId: number | null) => { setBatchTunnelSelections(prev => { const newMap = new Map(prev); newMap.set(tunnelId, speedId); return newMap; }); }; const isTunnelAssigned = (tunnelId: number) => { return userTunnels.some(ut => ut.tunnelId === tunnelId); }; return (
{/* 页面头部 */}
setSearchKeyword(e.target.value)} placeholder="搜索用户名" startContent={} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} className="flex-1" classNames={{ base: "bg-default-100", input: "bg-transparent", inputWrapper: "bg-default-100 border-2 border-default-200 hover:border-default-300 focus-within:border-primary data-[hover=true]:border-default-300" }} />
{/* 用户列表 */} {loading ? (
正在加载...
) : users.length === 0 ? (

暂无用户数据

还没有创建任何用户,点击上方按钮开始创建

) : (
{users.map((user) => { const userStatus = getUserStatus(user); const expStatus = user.expTime ? getExpireStatus(user.expTime) : null; const usedFlow = calculateUserTotalUsedFlow(user); const flowPercent = user.flow > 0 ? Math.min((usedFlow / (user.flow * 1024 * 1024 * 1024)) * 100, 100) : 0; return (

{user.name || user.user}

@{user.user}

{userStatus.text}
{/* 流量信息 */}
流量限制 {formatFlow(user.flow, 'gb')}
已使用 {formatFlow(usedFlow)}
90 ? 'danger' : flowPercent > 70 ? 'warning' : 'success'} className="mt-1" aria-label={`流量使用 ${flowPercent.toFixed(1)}%`} />
{/* 其他信息 */}
转发数量 {user.num}
重置日期 {user.flowResetTime === 0 ? '不重置' : `每月${user.flowResetTime}号`}
{user.expTime && (
过期时间
{expStatus && expStatus.color === 'success' ? (
{formatDate(user.expTime)}
) : ( {expStatus?.text || '未知状态'} )}
)}
{/* 第一行:编辑和重置 */}
{/* 第二行:权限和删除 */}
); })}
)} {/* 用户表单模态框 */} {isEdit ? '编辑用户' : '新增用户'}
setUserForm(prev => ({ ...prev, user: e.target.value }))} isRequired /> setUserForm(prev => ({ ...prev, pwd: e.target.value }))} placeholder={isEdit ? '留空则不修改密码' : '请输入密码'} isRequired={!isEdit} /> { const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999); setUserForm(prev => ({ ...prev, flow: value })); }} min="1" max="99999" isRequired /> { const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999); setUserForm(prev => ({ ...prev, num: value })); }} min="1" max="99999" isRequired /> { if (date) { const jsDate = new Date(date.year, date.month - 1, date.day, 23, 59, 59); setUserForm(prev => ({ ...prev, expTime: jsDate })); } else { setUserForm(prev => ({ ...prev, expTime: null })); } }} isRequired showMonthAndYearPickers className="cursor-pointer" />
setUserForm(prev => ({ ...prev, status: Number(value) }))} orientation="horizontal" > 正常 禁用
{/* 隧道权限管理模态框 */} 用户 {currentUser?.user} 的隧道权限管理
{/* 分配新权限部分 */}

分配新权限

流量限制、转发数量、到期时间、流量重置时间将自动继承用户设置
{tunnels.map(tunnel => { const isAssigned = isTunnelAssigned(tunnel.id); const isSelected = batchTunnelSelections.has(tunnel.id); const tunnelSpeedLimits = getSpeedLimitsForTunnel(tunnel.id); return (
toggleTunnelSelection(tunnel.id)} className="w-4 h-4 rounded border-gray-300 text-primary focus:ring-primary disabled:opacity-50" /> {tunnel.name} {isAssigned && ( 已分配 )}
{isSelected && !isAssigned && ( )}
); })} {tunnels.length === 0 && (
暂无可用隧道
)}
{/* 已有权限部分 */}

已有权限

隧道名称 流量统计 转发数量 状态 限速规则 重置时间 到期时间 操作 } emptyContent="暂无隧道权限" > {(userTunnel) => ( {userTunnel.tunnelName}
限制: {formatFlow(userTunnel.flow, 'gb')}
已用: {formatFlow(calculateTunnelUsedFlow(userTunnel))}
{userTunnel.num} {userTunnel.status === 1 ? '正常' : '禁用'} {userTunnel.speedLimitName || '不限速'} {userTunnel.flowResetTime === 0 ? '不重置' : `每月${userTunnel.flowResetTime}号`} {formatDate(userTunnel.expTime)}
)}
{/* 编辑隧道权限模态框 */} 编辑隧道权限 - {editTunnelForm?.tunnelName} {editTunnelForm && ( <>
{ const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999); setEditTunnelForm(prev => prev ? { ...prev, flow: value } : null); }} min="1" max="99999" /> { const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999); setEditTunnelForm(prev => prev ? { ...prev, num: value } : null); }} min="1" max="99999" /> { if (date) { const jsDate = new Date(date.year, date.month - 1, date.day, 23, 59, 59); setEditTunnelForm(prev => prev ? { ...prev, expTime: jsDate.getTime() } : null); } else { setEditTunnelForm(prev => prev ? { ...prev, expTime: Date.now() } : null); } }} showMonthAndYearPickers className="cursor-pointer" isRequired />
setEditTunnelForm(prev => prev ? { ...prev, status: Number(value) } : null)} orientation="horizontal" > 正常 禁用 )}
{/* 删除确认对话框 */} 确认删除用户

确定要删除用户 "{userToDelete?.user}" 吗?

此操作不可撤销,用户的所有数据将被永久删除。

{/* 删除隧道权限确认对话框 */} 确认删除隧道权限

确定要删除用户 {currentUser?.user} 对隧道 "{tunnelToDelete?.tunnelName}" 的权限吗?

删除后该用户将无法使用此隧道创建转发,此操作不可撤销。

{/* 重置流量确认对话框 */} 确认重置流量

确定要重置用户 "{userToReset?.user}" 的流量吗?

该操作只会重置账号流量不会重置隧道权限流量,重置后该用户的上下行流量将归零,此操作不可撤销。

当前流量使用情况:
上行流量: {userToReset ? formatFlow(userToReset.inFlow || 0) : '-'}
下行流量: {userToReset ? formatFlow(userToReset.outFlow || 0) : '-'}
总计: {userToReset ? formatFlow(calculateUserTotalUsedFlow(userToReset)) : '-'}
{/* 重置隧道流量确认对话框 */} 确认重置隧道流量

确定要重置用户 {currentUser?.user} 对隧道 "{tunnelToReset?.tunnelName}" 的流量吗?

该操作只会重置隧道权限流量不会重置账号流量,重置后该隧道权限的上下行流量将归零,此操作不可撤销。

当前流量使用情况:
上行流量: {tunnelToReset ? formatFlow(tunnelToReset.inFlow || 0) : '-'}
下行流量: {tunnelToReset ? formatFlow(tunnelToReset.outFlow || 0) : '-'}
总计: {tunnelToReset ? formatFlow(calculateTunnelUsedFlow(tunnelToReset)) : '-'}
); }