import { useState, useEffect, useMemo, useCallback, useRef } from "react"; import toast from "react-hot-toast"; import { parseDate } from "@internationalized/date"; import { AnimatedPage, StaggerList, StaggerItem, } from "@/components/animated-page"; import { Button } from "@/shadcn-bridge/heroui/button"; import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; import { Input } from "@/shadcn-bridge/heroui/input"; import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, } from "@/shadcn-bridge/heroui/table"; import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure, } from "@/shadcn-bridge/heroui/modal"; import { Chip } from "@/shadcn-bridge/heroui/chip"; import { Select, SelectItem } from "@/shadcn-bridge/heroui/select"; import { RadioGroup, Radio } from "@/shadcn-bridge/heroui/radio"; import { Checkbox } from "@/shadcn-bridge/heroui/checkbox"; import { Switch } from "@/shadcn-bridge/heroui/switch"; import { DatePicker } from "@/shadcn-bridge/heroui/date-picker"; import { Spinner } from "@/shadcn-bridge/heroui/spinner"; import { Progress } from "@/shadcn-bridge/heroui/progress"; import { User, UserForm, UserGroup, UserTunnel, TunnelAssignItem, Tunnel, SpeedLimit, Pagination as PaginationType, } from "@/types"; import { getAllUsers, createUser, updateUser, deleteUser, getTunnelList, batchAssignUserTunnel, getUserTunnelList, removeUserTunnel, updateUserTunnel, getSpeedLimitList, resetUserFlow, resetUserQuota, getUserGroupList, getUserGroups, getMonitorPermissionList, assignMonitorPermission, removeMonitorPermission, } from "@/api"; import { EditIcon, DeleteIcon, SettingsIcon, SearchIcon, } from "@/components/icons"; import { PageLoadingState } from "@/components/page-state"; import { useLocalStorageState } from "@/hooks/use-local-storage-state"; import { removeItemsById, replaceItemById } from "@/utils/list-state"; // 工具函数 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 formatQuotaLimit = (value?: number): string => { const limit = Number(value ?? 0); if (!Number.isFinite(limit) || limit <= 0) { return "不限"; } return `${limit} 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; }; const USER_SEARCH_DEBOUNCE_MS = 250; const normalizeUserItem = (item: Partial): User => { return { id: Number(item.id ?? 0), name: item.name, user: String(item.user ?? ""), status: Number(item.status ?? 0), flow: Number(item.flow ?? 0), num: Number(item.num ?? 0), expTime: item.expTime, flowResetTime: item.flowResetTime ?? 0, createdTime: item.createdTime, inFlow: Number(item.inFlow ?? 0), outFlow: Number(item.outFlow ?? 0), dailyQuotaGB: Number(item.dailyQuotaGB ?? 0), monthlyQuotaGB: Number(item.monthlyQuotaGB ?? 0), dailyUsedBytes: Number(item.dailyUsedBytes ?? 0), monthlyUsedBytes: Number(item.monthlyUsedBytes ?? 0), disabledByQuota: Number(item.disabledByQuota ?? 0), quotaDisabledAt: Number(item.quotaDisabledAt ?? 0), }; }; const normalizeUserTunnelItem = (item: Partial): UserTunnel => { return { id: Number(item.id ?? 0), userId: Number(item.userId ?? 0), tunnelId: Number(item.tunnelId ?? 0), tunnelName: String(item.tunnelName ?? ""), status: Number(item.status ?? 0), flow: Number(item.flow ?? 0), num: Number(item.num ?? 0), expTime: Number(item.expTime ?? 0), flowResetTime: Number(item.flowResetTime ?? 0), speedId: item.speedId ?? null, speedLimitName: item.speedLimitName, inFlow: Number(item.inFlow ?? 0), outFlow: Number(item.outFlow ?? 0), tunnelFlow: item.tunnelFlow, }; }; export default function UserPage() { // 状态管理 const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); const [searchKeyword, setSearchKeyword] = useLocalStorageState( "user-search-keyword", "", ); const [isSearchVisible, setIsSearchVisible] = useState(false); const [pagination, setPagination] = useState({ current: 1, size: 10, total: 0, }); const searchDebounceRef = useRef | null>(null); // 用户表单相关状态 const { isOpen: isUserModalOpen, onOpen: onUserModalOpen, onClose: onUserModalClose, } = useDisclosure(); const [isEdit, setIsEdit] = useState(false); const [userForm, setUserForm] = useState({ user: "", pwd: "", status: 1, flow: 1000, dailyQuotaGB: 0, monthlyQuotaGB: 0, num: 10, expTime: null, flowResetTime: 0, }); const [userFormLoading, setUserFormLoading] = useState(false); const [quotaResetLoading, setQuotaResetLoading] = useState(false); const editingUser = useMemo( () => userForm.id ? users.find((item) => item.id === userForm.id) || null : null, [userForm.id, users], ); // 隧道权限管理相关状态 const { isOpen: isTunnelModalOpen, onOpen: onTunnelModalOpen, onClose: onTunnelModalClose, } = useDisclosure(); const [currentUser, setCurrentUser] = useState(null); const [monitorPermissionUserIds, setMonitorPermissionUserIds] = useState< Set >(new Set()); const [monitorPermissionLoading, setMonitorPermissionLoading] = useState(false); const [monitorPermissionMutatingUserId, setMonitorPermissionMutatingUserId] = useState(null); const [userTunnels, setUserTunnels] = useState([]); const [tunnelListLoading, setTunnelListLoading] = useState(false); // 分配新隧道权限相关状态 const [assignLoading, setAssignLoading] = useState(false); const [batchTunnelSelections, setBatchTunnelSelections] = useState< Map >(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([]); const [userGroups, setUserGroups] = useState([]); const noLimitSpeedLimitIds = useMemo(() => { return new Set( speedLimits .filter((speedLimit) => speedLimit.name.trim() === "不限速") .map((speedLimit) => speedLimit.id), ); }, [speedLimits]); const speedLimitIds = useMemo(() => { return new Set(speedLimits.map((speedLimit) => speedLimit.id)); }, [speedLimits]); const normalizeSpeedId = (speedId?: number | null): number | null => { if (speedId === null || speedId === undefined) { return null; } if (noLimitSpeedLimitIds.has(speedId)) { return null; } if (speedLimits.length > 0 && !speedLimitIds.has(speedId)) { return null; } return speedId; }; const isMissingSpeedLimit = (speedId?: number | null): boolean => { if (speedId === null || speedId === undefined) { return false; } if (speedLimits.length === 0 || noLimitSpeedLimitIds.has(speedId)) { return false; } return !speedLimitIds.has(speedId); }; // 数据加载函数 const loadUsers = useCallback( async (keywordOverride?: string) => { setLoading(true); try { const keyword = keywordOverride ?? searchKeyword; const response = await getAllUsers({ current: pagination.current, size: pagination.size, keyword, }); if (response.code === 0) { const nextUsers = Array.isArray(response.data) ? response.data.map((item) => normalizeUserItem(item)) : []; setUsers(nextUsers); setPagination((prev) => ({ ...prev, total: nextUsers.length })); } else { toast.error(response.msg || "获取用户列表失败"); } } catch { toast.error("获取用户列表失败"); } finally { setLoading(false); } }, [pagination.current, pagination.size], ); const loadTunnels = useCallback(async () => { try { const response = await getTunnelList(); if (response.code === 0) { setTunnels(Array.isArray(response.data) ? response.data : []); } } catch {} }, []); const loadSpeedLimits = useCallback(async () => { try { const response = await getSpeedLimitList(); if (response.code === 0) { const speedLimitList = Array.isArray(response.data) ? response.data.map((item) => ({ ...item, uploadSpeed: item.uploadSpeed ?? item.speed ?? 0, downloadSpeed: item.downloadSpeed ?? item.speed ?? 0, })) : []; setSpeedLimits(speedLimitList); } } catch {} }, []); const loadUserGroups = useCallback(async () => { try { const response = await getUserGroupList(); if (response.code === 0) { setUserGroups(Array.isArray(response.data) ? response.data : []); } } catch {} }, []); const loadMonitorPermissions = useCallback(async () => { setMonitorPermissionLoading(true); try { const response = await getMonitorPermissionList(); if (response.code === 0) { const ids = new Set(); if (Array.isArray(response.data)) { response.data.forEach((item: any) => { const id = Number(item?.userId ?? 0); if (id > 0) ids.add(id); }); } setMonitorPermissionUserIds(ids); } else if (response.code !== 403) { toast.error(response.msg || "获取监控权限失败"); } } catch { // ignore } finally { setMonitorPermissionLoading(false); } }, []); const loadUserTunnels = useCallback(async (userId: number) => { setTunnelListLoading(true); try { const response = await getUserTunnelList({ userId }); if (response.code === 0) { setUserTunnels( Array.isArray(response.data) ? response.data.map((item) => normalizeUserTunnelItem(item)) : [], ); } else { toast.error(response.msg || "获取隧道权限列表失败"); } } catch { toast.error("获取隧道权限列表失败"); } finally { setTunnelListLoading(false); } }, []); // 生命周期 useEffect(() => { void loadTunnels(); void loadSpeedLimits(); void loadUserGroups(); void loadMonitorPermissions(); }, [loadMonitorPermissions, loadSpeedLimits, loadTunnels, loadUserGroups]); useEffect(() => { void loadUsers(); }, [loadUsers]); useEffect(() => { if (searchDebounceRef.current) { clearTimeout(searchDebounceRef.current); } searchDebounceRef.current = setTimeout(() => { setPagination((prev) => { if (prev.current === 1) { void loadUsers(searchKeyword); return prev; } return { ...prev, current: 1 }; }); }, USER_SEARCH_DEBOUNCE_MS); return () => { if (searchDebounceRef.current) { clearTimeout(searchDebounceRef.current); searchDebounceRef.current = null; } }; }, [loadUsers, searchKeyword]); // 用户管理操作 const handleSearch = () => { if (searchDebounceRef.current) { clearTimeout(searchDebounceRef.current); searchDebounceRef.current = null; } setPagination((prev) => ({ ...prev, current: 1 })); void loadUsers(searchKeyword); }; const handleAdd = () => { setIsEdit(false); setUserForm({ user: "", pwd: "", status: 1, flow: 1000, dailyQuotaGB: 0, monthlyQuotaGB: 0, num: 10, expTime: null, flowResetTime: 0, groupIds: [], }); onUserModalOpen(); }; const handleEdit = async (user: User) => { setIsEdit(true); let currentGroupIds: number[] = []; try { const groupRes = await getUserGroups(user.id); if (groupRes.code === 0) { currentGroupIds = groupRes.data || []; } } catch {} setUserForm({ id: user.id, name: user.name, user: user.user, pwd: "", status: user.status, flow: user.flow, dailyQuotaGB: user.dailyQuotaGB ?? 0, monthlyQuotaGB: user.monthlyQuotaGB ?? 0, num: user.num, expTime: user.expTime ? new Date(user.expTime) : null, flowResetTime: user.flowResetTime ?? 0, groupIds: currentGroupIds, }); 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("删除成功"); onDeleteModalClose(); setUsers((prev) => removeItemsById(prev, [userToDelete.id])); setPagination((prev) => ({ ...prev, total: Math.max(prev.total - 1, 0), })); setUserToDelete(null); if (currentUser?.id === userToDelete.id) { setCurrentUser(null); setUserTunnels([]); } } else { toast.error(response.msg || "删除失败"); } } catch { 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(), groupIds: userForm.groupIds ?? [], }; if (isEdit && !submitData.pwd) { delete submitData.pwd; } const response = isEdit ? await updateUser(submitData) : await createUser(submitData); if (response.code === 0) { toast.success(isEdit ? "更新成功" : "创建成功"); onUserModalClose(); const responseUser = normalizeUserItem((response as any).data || {}); if ( isEdit && responseUser.id > 0 && pagination.current === 1 && !searchKeyword.trim() ) { setUsers((prev) => replaceItemById(prev, responseUser)); } else if ( !isEdit && responseUser.id > 0 && pagination.current === 1 && !searchKeyword.trim() ) { setUsers((prev) => [responseUser, ...prev].slice(0, pagination.size)); setPagination((prev) => ({ ...prev, total: prev.total + 1 })); } else { await loadUsers(); } } else { toast.error(response.msg || (isEdit ? "更新失败" : "创建失败")); } } catch { toast.error(isEdit ? "更新失败" : "创建失败"); } finally { setUserFormLoading(false); } }; const setUserMonitorPermission = useCallback( async (userId: number, enabled: boolean) => { if (userId <= 0) return; if (monitorPermissionMutatingUserId === userId) return; const prevEnabled = monitorPermissionUserIds.has(userId); if (prevEnabled === enabled) return; setMonitorPermissionMutatingUserId(userId); // Optimistic update for better UX. setMonitorPermissionUserIds((prev) => { const next = new Set(prev); if (enabled) { next.add(userId); } else { next.delete(userId); } return next; }); try { const response = enabled ? await assignMonitorPermission(userId) : await removeMonitorPermission(userId); if (response.code === 0) { toast.success(enabled ? "已授权监控" : "已撤销监控"); return; } toast.error(response.msg || "操作失败"); throw new Error("mutation failed"); } catch { // Revert optimistic update on failure. setMonitorPermissionUserIds((prev) => { const next = new Set(prev); if (prevEnabled) { next.add(userId); } else { next.delete(userId); } return next; }); } finally { setMonitorPermissionMutatingUserId(null); } }, [monitorPermissionMutatingUserId, monitorPermissionUserIds], ); // 隧道权限管理操作 const handleManageTunnels = (user: User) => { setCurrentUser(user); setBatchTunnelSelections(new Map()); onTunnelModalOpen(); loadUserTunnels(user.id); }; const handleBatchAssignTunnel = async () => { if (batchTunnelSelections.size === 0 || !currentUser) { toast.error("请选择至少一个隧道"); return; } setAssignLoading(true); try { let speedLimitAutoCleared = false; const tunnelsToAssign: TunnelAssignItem[] = Array.from( batchTunnelSelections.entries(), ).map(([tunnelId, speedId]) => ({ tunnelId, speedId: (() => { const cleared = normalizeSpeedId(speedId); if (isMissingSpeedLimit(speedId)) { speedLimitAutoCleared = true; } return cleared; })(), })); const response = await batchAssignUserTunnel({ userId: currentUser.id, tunnels: tunnelsToAssign, }); if (response.code === 0) { if (speedLimitAutoCleared) { toast("所选限速规则不存在,已自动清除为不限速", { icon: "⚠️", duration: 5000, }); } toast.success(response.msg || "分配成功"); setBatchTunnelSelections(new Map()); await loadUserTunnels(currentUser.id); } else { toast.error(response.msg || "分配失败"); } } catch { toast.error("分配失败"); } finally { setAssignLoading(false); } }; const handleEditTunnel = (userTunnel: UserTunnel) => { setEditTunnelForm({ ...userTunnel, speedId: normalizeSpeedId(userTunnel.speedId), expTime: userTunnel.expTime, }); onEditTunnelModalOpen(); }; const handleUpdateTunnel = async () => { if (!editTunnelForm) return; setEditTunnelLoading(true); try { const speedLimitAutoCleared = isMissingSpeedLimit(editTunnelForm.speedId); const response = await updateUserTunnel({ id: editTunnelForm.id, flow: editTunnelForm.flow, num: editTunnelForm.num, expTime: editTunnelForm.expTime, flowResetTime: editTunnelForm.flowResetTime, speedId: normalizeSpeedId(editTunnelForm.speedId), status: editTunnelForm.status, }); if (response.code === 0) { if (speedLimitAutoCleared) { toast("所选限速规则不存在,已自动清除为不限速", { icon: "⚠️", duration: 5000, }); } toast.success("更新成功"); onEditTunnelModalClose(); if (currentUser) { const nextTunnel = normalizeUserTunnelItem({ ...editTunnelForm, speedId: normalizeSpeedId(editTunnelForm.speedId), speedLimitName: normalizeSpeedId(editTunnelForm.speedId) !== null ? speedLimits.find( (speedLimit) => speedLimit.id === normalizeSpeedId(editTunnelForm.speedId), )?.name : undefined, }); setUserTunnels((prev) => replaceItemById(prev, nextTunnel)); } } else { toast.error(response.msg || "更新失败"); } } catch { 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) { setUserTunnels((prev) => removeItemsById(prev, [tunnelToDelete.id])); } onDeleteTunnelModalClose(); setTunnelToDelete(null); } else { toast.error(response.msg || "删除失败"); } } catch { 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(); const targetUserId = userToReset.id; setUsers((prev) => prev.map((user) => user.id === targetUserId ? { ...user, inFlow: 0, outFlow: 0 } : user, ), ); setUserToReset(null); } else { toast.error(response.msg || "重置失败"); } } catch { toast.error("重置失败"); } finally { setResetFlowLoading(false); } }; const handleQuotaReset = async (scope: "daily" | "monthly" | "all") => { const userId = userForm.id; if (!userId) { return; } setQuotaResetLoading(true); try { const response = await resetUserQuota({ userId, scope }); if (response.code === 0) { toast.success("用户配额已重置"); await loadUsers(searchKeyword); } else { toast.error(response.msg || "重置用户配额失败"); } } catch { toast.error("重置用户配额失败"); } finally { setQuotaResetLoading(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(); const targetTunnelId = tunnelToReset.id; setUserTunnels((prev) => prev.map((userTunnel) => userTunnel.id === targetTunnelId ? { ...userTunnel, inFlow: 0, outFlow: 0 } : userTunnel, ), ); setTunnelToReset(null); } else { toast.error(response.msg || "重置失败"); } } catch { toast.error("重置失败"); } finally { setResetTunnelFlowLoading(false); } }; const editAvailableSpeedLimits = speedLimits.filter( (speedLimit) => !noLimitSpeedLimitIds.has(speedLimit.id), ); const getSpeedLimitsForTunnel = (_tunnelId: number) => { return speedLimits.filter( (speedLimit) => !noLimitSpeedLimitIds.has(speedLimit.id), ); }; const editTunnelSelectedSpeedId = normalizeSpeedId(editTunnelForm?.speedId); 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 ( {/* 页面头部 */}
{!isSearchVisible ? ( ) : (
setSearchKeyword(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSearch()} />
)}
{/* 用户列表 */} {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} {user.disabledByQuota ? ( 配额超额 ) : null}
{/* 流量信息 */}
流量限制 {formatFlow(user.flow, "gb")}
已使用 {formatFlow(usedFlow)}
90 ? "danger" : flowPercent > 70 ? "warning" : "success" } size="sm" value={flowPercent} />
{/* 其他信息 */}
{(user.dailyQuotaGB ?? 0) > 0 || (user.monthlyQuotaGB ?? 0) > 0 || (user.disabledByQuota ?? 0) > 0 ? ( <>
每日配额 {formatFlow(Number(user.dailyUsedBytes ?? 0))} /{" "} {formatQuotaLimit(user.dailyQuotaGB)}
每月配额 {formatFlow(Number(user.monthlyUsedBytes ?? 0))}{" "} / {formatQuotaLimit(user.monthlyQuotaGB)}
) : null}
规则数量 {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 })) } /> setUserForm((prev) => ({ ...prev, pwd: e.target.value })) } /> { const value = Math.min( Math.max(Number(e.target.value) || 0, 1), 99999, ); setUserForm((prev) => ({ ...prev, flow: value })); }} /> { const value = Math.min( Math.max(Number(e.target.value) || 0, 1), 99999, ); setUserForm((prev) => ({ ...prev, num: value })); }} /> { 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 })); } }} />
{isEdit && editingUser && ((editingUser.dailyQuotaGB ?? 0) > 0 || (editingUser.monthlyQuotaGB ?? 0) > 0 || (editingUser.disabledByQuota ?? 0) > 0) && (

当前配额状态

配额超额后会自动暂停该用户的转发,重置后可恢复

{editingUser.disabledByQuota ? ( 配额已触发禁用 ) : ( 配额正常 )}
每日用量
{formatFlow(Number(editingUser.dailyUsedBytes ?? 0))} /{" "} {formatQuotaLimit(editingUser.dailyQuotaGB)}
每月用量
{formatFlow(Number(editingUser.monthlyUsedBytes ?? 0))}{" "} / {formatQuotaLimit(editingUser.monthlyQuotaGB)}
)} setUserForm((prev) => ({ ...prev, status: Number(value) })) } > 正常 禁用 {userGroups.length > 0 && ( )}
{/* 隧道权限管理模态框 */} 用户 {currentUser?.user} 的权限管理
{/* 监控权限部分 */}

监控权限

允许访问监控功能
授予后,该用户可以访问监控页面并管理服务监控(TCP/ICMP)。
{monitorPermissionLoading ? : null} currentUser && void setUserMonitorPermission(currentUser.id, v) } />
{/* 分配新权限部分 */}

分配新权限

流量限制、规则数量、到期时间、流量重置时间将自动继承用户设置
{tunnels.map((tunnel) => { const isAssigned = isTunnelAssigned(tunnel.id); const isSelected = batchTunnelSelections.has(tunnel.id); const tunnelSpeedLimits = getSpeedLimitsForTunnel( tunnel.id, ); return (
!isAssigned && toggleTunnelSelection(tunnel.id) } onKeyDown={(event) => { if (isAssigned) { return; } if (event.key === "Enter" || event.key === " ") { event.preventDefault(); toggleTunnelSelection(tunnel.id); } }} >
event.stopPropagation()} onKeyDown={(event) => event.stopPropagation()} onValueChange={() => toggleTunnelSelection(tunnel.id) } /> {tunnel.name} {isAssigned && ( 已分配 )}
{isSelected && !isAssigned && (
)}
); })} {tunnels.length === 0 && (
暂无可用隧道
)}
{batchTunnelSelections.size > 0 && ( 已选 {batchTunnelSelections.size} 个隧道 )}
{/* 已有权限部分 */}

已有权限

隧道名称 流量统计 规则数量 状态 限速规则 重置时间 到期时间 操作 } > {(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, ); }} /> { const value = Math.min( Math.max(Number(e.target.value) || 0, 1), 99999, ); setEditTunnelForm((prev) => prev ? { ...prev, num: value } : null, ); }} /> { 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, ); } }} />
setEditTunnelForm((prev) => prev ? { ...prev, status: Number(value) } : null, ) } > 正常 禁用 )}
{/* 删除确认对话框 */} 确认删除用户

确定要删除用户{" "} "{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)) : "-"}
); }