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", "level-config", "region-block"], title: "APP 用户" }, { key: "rooms", prefixes: ["room", "room-pin", "room-config"], title: "房间管理" }, { key: "app-config", prefixes: ["app-config", "app-version"], title: "APP 配置" }, { key: "resources", prefixes: ["resource", "resource-shop", "resource-group", "resource-grant", "gift", "emoji-pack"], title: "资源管理", }, { key: "operations", prefixes: ["coin-ledger", "coin-seller-ledger", "coin-adjustment", "report", "gift-diamond"], title: "运营管理", }, { key: "payment", prefixes: ["payment-bill", "payment-product", "payment-third-party"], title: "支付管理" }, { key: "activities", prefixes: [ "achievement", "cp-config", "cumulative-recharge-reward", "daily-task", "first-recharge-reward", "invite-activity-reward", "lucky-gift", "red-packet", "registration-reward", "room-rocket", "room-turnover-reward", "seven-day-checkin", "user-leaderboard", "vip-config", "weekly-star", ], title: "活动管理", }, { key: "games", prefixes: ["game"], title: "游戏管理" }, { key: "countries", prefixes: ["country"], title: "国家管理" }, { key: "regions", prefixes: ["region"], title: "区域管理" }, { key: "host-org", prefixes: [ "agency", "bd", "coin-seller", "host", "host-agency-policy", "host-salary-settlement", "team-salary-policy", ], title: "团队管理", }, { key: "logs", prefixes: ["log"], title: "日志审计" }, { key: "common", prefixes: ["upload"], title: "通用能力" }, ]; const fallbackPermissionGroup = { key: "other", title: "其他权限" }; const permissionKindOrder = { menu: 1, button: 2, api: 3 }; const permissionActionOrder = { view: 1, create: 2, update: 3, status: 4, publish: 5, grant: 6, permission: 7, "data-scope": 8, sort: 9, visible: 10, "reset-password": 11, password: 12, export: 13, sync: 14, settle: 15, retry: 16, cancel: 17, "stock-credit": 18, "exchange-rate": 19, delete: 20, manage: 21, }; 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; } const prefixDiff = getPermissionPrefix(left.code).localeCompare(getPermissionPrefix(right.code)); if (prefixDiff !== 0) { return prefixDiff; } const actionDiff = getPermissionActionOrder(left.code) - getPermissionActionOrder(right.code); if (actionDiff !== 0) { return actionDiff; } return left.code.localeCompare(right.code); } function getPermissionActionOrder(code = "") { const action = code.split(":")[1] || ""; return permissionActionOrder[action] || 99; }