import { useCallback, useEffect, useMemo, useState } from "react"; import { Card, CardBody, CardHeader } from "@heroui/card"; import { Button } from "@heroui/button"; import { Input } from "@heroui/input"; import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, useDisclosure, } from "@heroui/modal"; import { Select, SelectItem } from "@heroui/select"; import { Table, TableBody, TableCell, TableColumn, TableHeader, TableRow, } from "@heroui/table"; import { Chip } from "@heroui/chip"; import { Spinner } from "@heroui/spinner"; import toast from "react-hot-toast"; import { assignGroupPermission, assignTunnelsToGroup, assignUsersToGroup, createTunnelGroup, createUserGroup, deleteTunnelGroup, deleteUserGroup, getAllUsers, getGroupPermissionList, getTunnelGroupList, getTunnelList, getUserGroupList, removeGroupPermission, updateTunnelGroup, updateUserGroup, } from "@/api"; interface TunnelItem { id: number; name: string; } interface UserItem { id: number; user: string; } interface TunnelGroup { id: number; name: string; status: number; tunnelIds: number[]; tunnelNames: string[]; createdTime: number; } interface UserGroup { id: number; name: string; status: number; userIds: number[]; userNames: string[]; createdTime: number; } interface GroupPermission { id: number; userGroupId: number; userGroupName: string; tunnelGroupId: number; tunnelGroupName: string; createdTime: number; } const formatDate = (timestamp?: number): string => { if (!timestamp) { return "-"; } return new Date(timestamp).toLocaleString(); }; const isAdminUser = () => { let adminFlag = localStorage.getItem("admin") === "true"; if (localStorage.getItem("admin") === null) { const roleId = parseInt(localStorage.getItem("role_id") || "1", 10); adminFlag = roleId === 0; localStorage.setItem("admin", adminFlag.toString()); } return adminFlag; }; export default function GroupPage() { const [loading, setLoading] = useState(true); const [isAdmin] = useState(isAdminUser()); const [tunnelGroups, setTunnelGroups] = useState([]); const [userGroups, setUserGroups] = useState([]); const [permissions, setPermissions] = useState([]); const [tunnels, setTunnels] = useState([]); const [users, setUsers] = useState([]); const [selectedUserGroupId, setSelectedUserGroupId] = useState( null, ); const [selectedTunnelGroupId, setSelectedTunnelGroupId] = useState< number | null >(null); const [savingPermission, setSavingPermission] = useState(false); const { isOpen: tunnelGroupModalOpen, onOpen: onTunnelGroupModalOpen, onClose: onTunnelGroupModalClose, onOpenChange: onTunnelGroupModalChange, } = useDisclosure(); const { isOpen: userGroupModalOpen, onOpen: onUserGroupModalOpen, onClose: onUserGroupModalClose, onOpenChange: onUserGroupModalChange, } = useDisclosure(); const { isOpen: tunnelAssignModalOpen, onOpen: onTunnelAssignModalOpen, onClose: onTunnelAssignModalClose, onOpenChange: onTunnelAssignModalChange, } = useDisclosure(); const { isOpen: userAssignModalOpen, onOpen: onUserAssignModalOpen, onClose: onUserAssignModalClose, onOpenChange: onUserAssignModalChange, } = useDisclosure(); const [editingTunnelGroup, setEditingTunnelGroup] = useState(null); const [editingUserGroup, setEditingUserGroup] = useState( null, ); const [groupName, setGroupName] = useState(""); const [groupStatus, setGroupStatus] = useState("1"); const [savingGroup, setSavingGroup] = useState(false); const [assignTunnelGroup, setAssignTunnelGroup] = useState(null); const [assignUserGroup, setAssignUserGroup] = useState( null, ); const [selectedTunnelKeys, setSelectedTunnelKeys] = useState>( new Set(), ); const [selectedUserKeys, setSelectedUserKeys] = useState>( new Set(), ); const [savingAssign, setSavingAssign] = useState(false); const tunnelNameMap = useMemo(() => { const map = new Map(); tunnels.forEach((item) => { map.set(item.id, item.name); }); return map; }, [tunnels]); const userNameMap = useMemo(() => { const map = new Map(); users.forEach((item) => { map.set(item.id, item.user); }); return map; }, [users]); const loadData = useCallback(async () => { setLoading(true); try { const [tunnelGroupRes, userGroupRes, permissionRes, tunnelRes, userRes] = await Promise.all([ getTunnelGroupList(), getUserGroupList(), getGroupPermissionList(), getTunnelList(), getAllUsers(), ]); if (tunnelGroupRes.code === 0) { setTunnelGroups(tunnelGroupRes.data || []); } if (userGroupRes.code === 0) { setUserGroups(userGroupRes.data || []); } if (permissionRes.code === 0) { setPermissions(permissionRes.data || []); } if (tunnelRes.code === 0) { setTunnels(tunnelRes.data || []); } if (userRes.code === 0) { setUsers(userRes.data || []); } if ( tunnelGroupRes.code !== 0 || userGroupRes.code !== 0 || permissionRes.code !== 0 ) { toast.error("部分分组数据加载失败"); } } catch { toast.error("分组数据加载失败"); } finally { setLoading(false); } }, []); useEffect(() => { loadData(); }, [loadData]); const openCreateTunnelGroup = () => { setEditingTunnelGroup(null); setGroupName(""); setGroupStatus("1"); onTunnelGroupModalOpen(); }; const openEditTunnelGroup = (group: TunnelGroup) => { setEditingTunnelGroup(group); setGroupName(group.name); setGroupStatus(String(group.status)); onTunnelGroupModalOpen(); }; const openCreateUserGroup = () => { setEditingUserGroup(null); setGroupName(""); setGroupStatus("1"); onUserGroupModalOpen(); }; const openEditUserGroup = (group: UserGroup) => { setEditingUserGroup(group); setGroupName(group.name); setGroupStatus(String(group.status)); onUserGroupModalOpen(); }; const saveTunnelGroup = async () => { if (!groupName.trim()) { toast.error("请输入分组名称"); return; } setSavingGroup(true); try { const payload = { name: groupName.trim(), status: Number(groupStatus) }; const res = editingTunnelGroup ? await updateTunnelGroup({ id: editingTunnelGroup.id, ...payload }) : await createTunnelGroup(payload); if (res.code === 0) { toast.success(editingTunnelGroup ? "更新成功" : "创建成功"); onTunnelGroupModalClose(); loadData(); } else { toast.error(res.msg || "保存失败"); } } catch { toast.error("保存失败"); } finally { setSavingGroup(false); } }; const saveUserGroup = async () => { if (!groupName.trim()) { toast.error("请输入分组名称"); return; } setSavingGroup(true); try { const payload = { name: groupName.trim(), status: Number(groupStatus) }; const res = editingUserGroup ? await updateUserGroup({ id: editingUserGroup.id, ...payload }) : await createUserGroup(payload); if (res.code === 0) { toast.success(editingUserGroup ? "更新成功" : "创建成功"); onUserGroupModalClose(); loadData(); } else { toast.error(res.msg || "保存失败"); } } catch { toast.error("保存失败"); } finally { setSavingGroup(false); } }; const handleDeleteTunnelGroup = async (id: number) => { try { const res = await deleteTunnelGroup(id); if (res.code === 0) { toast.success("删除成功"); loadData(); } else { toast.error(res.msg || "删除失败"); } } catch { toast.error("删除失败"); } }; const handleDeleteUserGroup = async (id: number) => { try { const res = await deleteUserGroup(id); if (res.code === 0) { toast.success("删除成功"); loadData(); } else { toast.error(res.msg || "删除失败"); } } catch { toast.error("删除失败"); } }; const openAssignTunnels = (group: TunnelGroup) => { setAssignTunnelGroup(group); setSelectedTunnelKeys(new Set(group.tunnelIds.map((id) => String(id)))); onTunnelAssignModalOpen(); }; const openAssignUsers = (group: UserGroup) => { setAssignUserGroup(group); setSelectedUserKeys(new Set(group.userIds.map((id) => String(id)))); onUserAssignModalOpen(); }; const saveAssignTunnels = async () => { if (!assignTunnelGroup) return; setSavingAssign(true); try { const tunnelIds = Array.from(selectedTunnelKeys).map((id) => Number(id)); const res = await assignTunnelsToGroup({ groupId: assignTunnelGroup.id, tunnelIds, }); if (res.code === 0) { toast.success("分配成功"); onTunnelAssignModalClose(); loadData(); } else { toast.error(res.msg || "分配失败"); } } catch { toast.error("分配失败"); } finally { setSavingAssign(false); } }; const saveAssignUsers = async () => { if (!assignUserGroup) return; setSavingAssign(true); try { const userIds = Array.from(selectedUserKeys).map((id) => Number(id)); const res = await assignUsersToGroup({ groupId: assignUserGroup.id, userIds, }); if (res.code === 0) { toast.success("分配成功"); onUserAssignModalClose(); loadData(); } else { toast.error(res.msg || "分配失败"); } } catch { toast.error("分配失败"); } finally { setSavingAssign(false); } }; const handleAssignPermission = async () => { if (!selectedUserGroupId || !selectedTunnelGroupId) { toast.error("请选择用户分组和隧道分组"); return; } setSavingPermission(true); try { const res = await assignGroupPermission({ userGroupId: selectedUserGroupId, tunnelGroupId: selectedTunnelGroupId, }); if (res.code === 0) { toast.success(res.msg || "权限分配成功"); loadData(); } else { toast.error(res.msg || "权限分配失败"); } } catch { toast.error("权限分配失败"); } finally { setSavingPermission(false); } }; const handleRemovePermission = async (id: number) => { try { const res = await removeGroupPermission(id); if (res.code === 0) { toast.success("权限回收成功"); loadData(); } else { toast.error(res.msg || "权限回收失败"); } } catch { toast.error("权限回收失败"); } }; if (!isAdmin) { return (

权限不足,只有管理员可以访问分组管理页面。

); } return (
{loading && (
)}

隧道分组

名称 隧道 状态 创建时间 操作 {(item) => ( {item.name} {item.tunnelNames.length > 0 ? item.tunnelNames.join("、") : "-"} {item.status === 1 ? "启用" : "停用"} {formatDate(item.createdTime)}
)}

用户分组

名称 用户 状态 创建时间 操作 {(item) => ( {item.name} {item.userNames.length > 0 ? item.userNames.join("、") : "-"} {item.status === 1 ? "启用" : "停用"} {formatDate(item.createdTime)}
)}

权限分配

ID 用户分组 隧道分组 创建时间 操作 {(item) => ( {item.id} {item.userGroupName || item.userGroupId} {item.tunnelGroupName || item.tunnelGroupId} {formatDate(item.createdTime)} )}
{editingTunnelGroup ? "编辑隧道分组" : "新建隧道分组"} setGroupName(e.target.value)} /> {editingUserGroup ? "编辑用户分组" : "新建用户分组"} setGroupName(e.target.value)} /> 分配隧道 - {assignTunnelGroup?.name}

当前已选: {Array.from(selectedTunnelKeys) .map((id) => tunnelNameMap.get(Number(id)) || id) .join("、") || "无"}

不选择任何隧道并保存将清空该分组成员。

分配用户 - {assignUserGroup?.name}

当前已选: {Array.from(selectedUserKeys) .map((id) => userNameMap.get(Number(id)) || id) .join("、") || "无"}

不选择任何用户并保存将清空该分组成员。

); }