2026-05-14 09:57:41 +08:00

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