import { useCallback, useMemo, useState } from "react"; import { parseForm } from "@/shared/forms/validation"; import { useAdminQuery } from "@/shared/hooks/useAdminQuery.js"; import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { createRole, deleteRole, listPermissions, listRoles, replaceRolePermissions, updateRole, } from "@/features/roles/api"; import { useRoleAbilities } from "@/features/roles/permissions.js"; import { roleFormSchema } from "@/features/roles/schema"; const emptyRole = { code: "", description: "", name: "", permissionIds: [] }; const emptyData = { permissions: [], roles: [] }; const permissionGroupDefinitions = [ { key: "overview", prefixes: ["overview"], title: "总览" }, { key: "users", prefixes: ["user"], title: "用户管理" }, { key: "roles", prefixes: ["role"], title: "角色配置" }, { key: "menus", prefixes: ["menu", "permission"], title: "菜单权限" }, { key: "app-users", prefixes: ["app-user"], title: "App 用户" }, { key: "rooms", prefixes: ["room"], title: "房间管理" }, { key: "countries", prefixes: ["country"], title: "国家管理" }, { key: "regions", prefixes: ["region"], title: "区域管理" }, { key: "agencies", prefixes: ["agency"], title: "Agency 列表" }, { key: "bds", prefixes: ["bd"], title: "BD 列表" }, { key: "hosts", prefixes: ["host"], title: "主播列表" }, { key: "coin-sellers", prefixes: ["coin-seller"], title: "币商列表" }, { key: "logs", prefixes: ["log"], title: "日志审计" }, { key: "jobs", prefixes: ["job", "export"], title: "任务中心" }, { key: "common", prefixes: ["upload"], title: "通用能力" }, ]; const fallbackPermissionGroup = { key: "other", title: "其他权限" }; const permissionKindOrder = { menu: 1, button: 2, api: 3 }; export function useRolesPage() { const [query, setQuery] = useState(""); const [drawerOpen, setDrawerOpen] = useState(false); const [editingRole, setEditingRole] = useState(null); const [form, setForm] = useState(emptyRole); const abilities = useRoleAbilities(); const confirm = useConfirm(); const { showToast } = useToast(); const queryFn = useCallback(async () => { const roleData = await listRoles(); const permissionData = abilities.canLoadPermissions ? await listPermissions() : []; return { permissions: permissionData || [], roles: roleData || [] }; }, [abilities.canLoadPermissions]); const { data, error, loading, reload } = useAdminQuery(queryFn, { errorMessage: "加载角色失败", initialData: emptyData, keepPreviousData: true, queryKey: ["roles", abilities.canLoadPermissions], }); const permissionGroups = useMemo(() => { return groupPermissionsByMenu(data.permissions || []); }, [data.permissions]); const roles = useMemo(() => { const keyword = query.trim().toLowerCase(); const items = data.roles || []; if (!keyword) { return items; } return items.filter((role) => { return [role.name, role.code, role.description].some((value) => String(value || "") .toLowerCase() .includes(keyword), ); }); }, [data.roles, query]); const changeQuery = (value) => { setQuery(value); }; const resetFilters = () => { setQuery(""); }; const openCreate = () => { setEditingRole(null); setForm(emptyRole); setDrawerOpen(true); }; const openEdit = (role) => { setEditingRole(role); setForm({ code: role.code, description: role.description || "", name: role.name, permissionIds: role.permissions?.map((permission) => permission.id) || [], }); setDrawerOpen(true); }; const closeDrawer = () => { setDrawerOpen(false); }; const submit = async (event) => { event.preventDefault(); try { const payload = parseForm(roleFormSchema, form); if (editingRole) { if (abilities.canUpdate) { await updateRole(editingRole.id, { code: payload.code, description: payload.description, name: payload.name, }); } if (abilities.canGrant) { await replaceRolePermissions(editingRole.id, payload.permissionIds); } showToast("角色已更新", "success"); } else { await createRole({ code: payload.code, description: payload.description, name: payload.name, permissionIds: abilities.canGrant ? payload.permissionIds : [], }); showToast("角色已创建", "success"); } setDrawerOpen(false); reload(); } catch (err) { showToast(err.message || "保存角色失败", "error"); } }; const remove = async (role) => { const ok = await confirm({ confirmText: "删除", message: `${role.name} 删除前需要先解绑用户。`, title: "删除角色", tone: "danger", }); if (!ok) { return; } try { await deleteRole(role.id); showToast("角色已删除", "success"); reload(); } catch (err) { showToast(err.message || "删除角色失败", "error"); } }; const togglePermission = (permissionId, checked) => { if (!abilities.canGrant) { return; } setForm((current) => ({ ...current, permissionIds: checked ? Array.from(new Set([...current.permissionIds, permissionId])) : current.permissionIds.filter((id) => id !== permissionId), })); }; const togglePermissionGroup = (permissionIds, checked) => { if (!abilities.canGrant) { return; } setForm((current) => { const targetIds = new Set(permissionIds); return { ...current, permissionIds: checked ? Array.from(new Set([...current.permissionIds, ...permissionIds])) : current.permissionIds.filter((id) => !targetIds.has(id)), }; }); }; return { abilities, changeQuery, closeDrawer, drawerOpen, editingRole, error, form, loading, openCreate, openEdit, permissionGroups, permissions: data.permissions || [], query, reload, remove, resetFilters, roles, setForm, submit, togglePermission, togglePermissionGroup, }; } function groupPermissionsByMenu(permissions) { const groups = new Map( permissionGroupDefinitions.map((definition) => [definition.key, { ...definition, items: [] }]), ); const prefixToGroup = new Map( permissionGroupDefinitions.flatMap((definition) => definition.prefixes.map((prefix) => [prefix, definition.key]), ), ); const fallbackGroup = { ...fallbackPermissionGroup, items: [] }; permissions.forEach((permission) => { const prefix = getPermissionPrefix(permission.code); const groupKey = prefixToGroup.get(prefix); const group = groupKey ? groups.get(groupKey) : fallbackGroup; group.items.push(permission); }); return [...groups.values(), fallbackGroup] .filter((group) => group.items.length > 0) .map((group) => ({ ...group, items: [...group.items].sort(comparePermissions), })); } function getPermissionPrefix(code = "") { return code.split(":")[0] || "other"; } function comparePermissions(left, right) { const kindDiff = (permissionKindOrder[left.kind] || 9) - (permissionKindOrder[right.kind] || 9); if (kindDiff !== 0) { return kindDiff; } return left.code.localeCompare(right.code); }