// 角色与权限中枢:与后端 model.Role / service.Actor 保持单一对应关系。 // 前端只做"是否展示入口/按钮"的判定,所有最终权限以后端校验为准。 export const ROLES = { USER: "user", BOARD_ADMIN: "board_admin", ADMIN: "admin", SUPER_ADMIN: "super_admin", OWNER: "owner", } as const; export type RoleKey = (typeof ROLES)[keyof typeof ROLES]; // 角色等级(与后端 model.RoleLevel 一致),未知角色按普通用户处理 export const ROLE_LEVEL: Record = { [ROLES.USER]: 0, [ROLES.BOARD_ADMIN]: 30, [ROLES.ADMIN]: 50, [ROLES.SUPER_ADMIN]: 80, [ROLES.OWNER]: 100, }; export function roleLevel(role?: string | null): number { return ROLE_LEVEL[role ?? ""] ?? 0; } /** 管理团队成员(板块管理员及以上) */ export function isStaff(role?: string | null): boolean { return roleLevel(role) >= ROLE_LEVEL[ROLES.BOARD_ADMIN]; } /** 管理员及以上(全站内容管理:置顶/加精/公告) */ export function isAdminOrAbove(role?: string | null): boolean { return roleLevel(role) >= ROLE_LEVEL[ROLES.ADMIN]; } /** 超级管理员及以上(用户与权限、站点设置) */ export function isSuperOrOwner(role?: string | null): boolean { return roleLevel(role) >= ROLE_LEVEL[ROLES.SUPER_ADMIN]; } /** 站长(站点所有者) */ export function isOwner(role?: string | null): boolean { return role === ROLES.OWNER; } // 后台功能点权限码(与后端 service.Perm* 一致)。 // 渠道类权限可由超管/站长账号级授予任意账号;/me 下发的 perms 为 // 角色默认 ∪ 账号授予的生效集合,前端只做显隐判定,最终权限以后端为准。 export const PERMS = { ANNOUNCEMENTS: "announcements", PAGES: "pages", LIBRARY: "library", ADS: "ads", MESSAGES: "messages", } as const; export type PermKey = (typeof PERMS)[keyof typeof PERMS]; /** 可账号级授予的渠道权限(与后端 GrantablePerms 一致) */ export const CHANNEL_PERMS = [ PERMS.ANNOUNCEMENTS, PERMS.PAGES, PERMS.LIBRARY, PERMS.ADS, ] as const; /** 渠道权限展示元信息(角色与账号弹窗/后台导航共用) */ export const PERM_META: Record< (typeof CHANNEL_PERMS)[number], { label: string; desc: string } > = { [PERMS.ANNOUNCEMENTS]: { label: "公告管理", desc: "发布、编辑、置顶与删除站点公告" }, [PERMS.PAGES]: { label: "单页管理", desc: "创建与维护站点单页(/p/:slug)" }, [PERMS.LIBRARY]: { label: "书库管理", desc: "管理书库条目、章节与文件" }, [PERMS.ADS]: { label: "广告与赞助", desc: "管理广告位与赞助配置、审批投放" }, }; /** 站长/超管:群聊监管旁路(可见全部群含私聊、撤回任意消息) */ export function canOverseeChat(role?: string | null): boolean { return role === ROLES.OWNER || role === ROLES.SUPER_ADMIN; } /** 彻底删除帖/评:仅站长 */ export function canPurgeContent(role?: string | null): boolean { return role === ROLES.OWNER; } /** 携带板块授权与消息权限的最小用户形状 */ export interface RoleUserLike { role?: string | null; board_ids?: number[] | null; can_manage_messages?: boolean | null; /** 生效权限码(/me 下发;角色默认 ∪ 账号授予) */ perms?: string[] | null; /** 是否至少管理一个群(/me 可带;后台入口用) */ chat_admin_room_count?: number | null; } /** 是否拥有某后台功能点(依据 /me 下发的生效权限码;最终以后端校验为准) */ export function hasPerm(user: RoleUserLike | null | undefined, perm: string): boolean { if (!user) return false; return (user.perms ?? []).includes(perm); } /** 是否拥有任一渠道权限(后台准入判定用) */ export function hasAnyChannelPerm(user: RoleUserLike | null | undefined): boolean { return CHANNEL_PERMS.some((p) => hasPerm(user, p)); } /** * 能否管理指定板块的内容(帖子/评论的编辑删除审核): * 管理员及以上不限板块;板块管理员仅限被授权板块 */ export function canModerateBoard( user: RoleUserLike | null | undefined, boardId: number | null | undefined ): boolean { if (!user) return false; if (isAdminOrAbove(user.role)) return true; if (user.role !== ROLES.BOARD_ADMIN || !boardId) return false; return (user.board_ids ?? []).includes(boardId); } /** * 板管不可软删管理员及以上作者的内容;作者本人不受此限(由调用方先判断自删) */ export function canModerateAuthor( actor: RoleUserLike | null | undefined, authorRole?: string | null ): boolean { if (!actor) return false; if (actor.role === ROLES.BOARD_ADMIN) { return roleLevel(authorRole) < ROLE_LEVEL[ROLES.ADMIN]; } return true; } /** 是否可软删他人内容(板块范围 + 作者角色保护) */ export function canStaffSoftDelete( actor: RoleUserLike | null | undefined, boardId: number | null | undefined, authorRole?: string | null ): boolean { return canModerateBoard(actor, boardId) && canModerateAuthor(actor, authorRole); } /** 是否拥有任意板块的内容审核权(后台「内容管理」入口) */ export function canModerateAny(user: RoleUserLike | null | undefined): boolean { if (!user) return false; if (isAdminOrAbove(user.role)) return true; return user.role === ROLES.BOARD_ADMIN && (user.board_ids?.length ?? 0) > 0; } /** 站点级消息管理(站长/超管固有,或 can_manage_messages) */ export function hasSiteMessagePerm(user: RoleUserLike | null | undefined): boolean { if (!user) return false; if (canOverseeChat(user.role)) return true; return !!user.can_manage_messages; } /** 后台「消息管理」入口:站点消息权或至少是一个群的管理员 */ export function canAccessAdminMessages(user: RoleUserLike | null | undefined): boolean { if (!user) return false; if (hasSiteMessagePerm(user)) return true; return (user.chat_admin_room_count ?? 0) > 0; } /** 角色图标 key(RoleBadge 负责映射到具体图标,集中配置便于替换) */ export type RoleIconKey = "crown" | "shield-star" | "shield" | "shield-board" | "user"; export interface RoleMeta { key: RoleKey; label: string; icon: RoleIconKey; /** 主色使用的 CSS token 名(RoleBadge 取 var(--token)) */ colorToken: string; softToken: string; /** * 授权弹窗用的职责说明(站长不出现在可授权列表)。 * 只描述该角色「有什么」,最终权限以后端为准。 */ grantDesc?: string; } // 各管理角色的图标与配色集中配置;站长固定使用皇冠站长标识 export const ROLE_META: Record = { [ROLES.OWNER]: { key: ROLES.OWNER, label: "站长", icon: "crown", colorToken: "--gold", softToken: "--gold-soft", grantDesc: "站点所有者:全部后台能力、群聊监管、用户内容档案、彻底删除内容;身份不可授予或降级。", }, [ROLES.SUPER_ADMIN]: { key: ROLES.SUPER_ADMIN, label: "超级管理员", icon: "shield-star", colorToken: "--accent", softToken: "--accent-soft", grantDesc: "用户与角色管理、封禁、外观主题、公告;全站内容软删与置顶/加精;群聊监管(可见全部会话、可撤回任意消息)。不可彻底删除内容,不可动站长账号,亦无站长专属的用户内容档案。", }, [ROLES.ADMIN]: { key: ROLES.ADMIN, label: "管理员", icon: "shield", colorToken: "--accent", softToken: "--accent-soft", grantDesc: "公告管理;全站帖子/评论审核与软删;前台置顶与加精。不含用户权限、外观设置、彻底删除与默认的群聊监管(可由站长另授消息管理或任命群管理员)。", }, [ROLES.BOARD_ADMIN]: { key: ROLES.BOARD_ADMIN, label: "板块管理员", icon: "shield-board", colorToken: "--accent", softToken: "--accent-soft", grantDesc: "仅审核并软删被授权板块内普通用户的帖子与评论;不可处置管理员/站长发布的内容,无消息管理、公告、置顶/加精、用户管理与外观设置;须至少勾选一个板块。", }, [ROLES.USER]: { key: ROLES.USER, label: "普通用户", icon: "user", colorToken: "--ink-3", softToken: "--panel-2", grantDesc: "无任何后台入口。可发帖、评论、私信等;新帖与评论按站点规则进入审核。降级为普通用户时会清空板块授权。", }, }; export function roleMeta(role?: string | null): RoleMeta { return ROLE_META[(role as RoleKey)] ?? ROLE_META.user; }