2026-06-11 17:11:04 +08:00

328 lines
10 KiB
JavaScript

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;
}