250 lines
8.2 KiB
JavaScript
250 lines
8.2 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"], 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);
|
|
}
|