新增: - 书库导入导出(library_import/library_export)及测试 - 图片变体生成(image_variants)与响应式图片(responsiveImage) - 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件 - 小组件运行时(widgetRuntime)与静态检查(widgetLint) - 上传缓存中间件(upload_cache)与字体 CSS 提取脚本 其它: - 后端 handlers/services 全量调整 - 前端页面、组件、库函数与配置更新
238 lines
8.5 KiB
TypeScript
238 lines
8.5 KiB
TypeScript
// 角色与权限中枢:与后端 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<string, number> = {
|
||
[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<RoleKey, RoleMeta> = {
|
||
[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;
|
||
}
|