328 lines
10 KiB
JavaScript
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;
|
|
}
|