/** * Shrimp * 后台管理系统快速开发框架 */ import { ShrimpCode, IShrimpResponse, ITableCrudProto, IModuleInfo, IUser, IRole, WithId, IRoleProto, IPageQuery, IPageResult, IFieldConfig, IFieldRule, IFieldTypeHandler, IShrimpQuery, IShrimpCondition, IShrimpPopulate, ShrimpConfig, IShrimpOperator, IMenuItem, ShrimpError, ShrimpFieldTypes, ILabel } from "./IShrimpType" import { AsyncLocalStorage } from 'async_hooks' // 懒加载 bcrypt: 只有走到 Password 字段 hash 或 verifyPassword 才 import, // 让纯核心(MemoryShrimpCrud + 业务模块)的消费方可以不装 bcrypt async function loadBcrypt(): Promise { return await import("bcrypt") } export class Shrimp { root: Shrimp = this modules: Map = new Map() /** 全局 CRUD 注册表,用于 MongoShrimpCrud 自动解析 relation */ crudRegistry: Map> = new Map() /** 请求级操作者上下文,通过 AsyncLocalStorage 避免并发覆盖 */ static asyncLocalStorage = new AsyncLocalStorage() get operator(): IShrimpOperator | null { return Shrimp.asyncLocalStorage.getStore() ?? null } async call(path: string[], ...args: any[]): Promise { let module = this.modules.get(path[0]) if (module == null) { throw new Error(`Module "${path[0]}" not found`) } return await module.call(path.slice(1), ...args) } checkEvent(powers: Set, path: string[]): boolean { let module = this.modules.get(path[0]) if (module == null) return false return module.checkEvent(powers, path.slice(1)) } getModuleInfo(powers: Set): IModuleInfo[] { const list: IModuleInfo[] = [] this.collectModuleInfo(this.modules, powers, undefined, list) return list } private collectModuleInfo(modules: Map, powers: Set, parent: string | undefined, out: IModuleInfo[]) { modules.forEach(module => { if (!powers.has(module.power.key)) return const info: IModuleInfo = { moduleKey: module.moduleKey, events: module.getEventInfo(powers), label: module.label, fields: module instanceof ShrimpTableModule ? module.getFields() : [], } if (module.icon) info.icon = module.icon if (parent) info.parent = parent out.push(info) if (module.modules.size > 0) { this.collectModuleInfo(module.modules, powers, module.moduleKey, out) } }) } /** 成功响应 */ success(data: T, tip?: ILabel, log?: string): IShrimpResponse { const r: IShrimpResponse = { code: ShrimpCode.OK, data } if (tip) r.tip = tip if (log) r.log = log return r } /** 失败响应 */ fail(code: ShrimpCode, tip?: ILabel, log?: string): IShrimpResponse { const r: IShrimpResponse = { code } if (tip) r.tip = tip if (log) r.log = log return r } async allPower(): Promise<{ key: string; label: ILabel }[]> { const powers: { key: string; label: ILabel }[] = [] const collect = (module: any) => { // 模块权限 powers.push({ key: module.power.key, label: module.power.label }) // 事件权限(children 已有 key 和 label) powers.push(...module.power.children) // 递归子模块 module.modules.forEach(collect) } this.root.modules.forEach(collect) // 去重 const map = new Map() powers.forEach(p => map.set(p.key, p.label)) return Array.from(map.entries()).map(([key, label]) => ({ key, label })) } /** 菜单树,默认平铺所有模块(label 用模块注册时声明的 label),子类可覆盖为分组结构 */ getMenuTree(): IMenuItem[] { const items: IMenuItem[] = [] this.modules.forEach(module => { items.push({ name: module.moduleKey, label: module.label, moduleKey: module.moduleKey }) }) return items } /** 根据权限过滤菜单树(递归处理 children;children 全部被过滤后,分组节点也一并剔除) */ filterMenuTree(fullMenuTree: IMenuItem[], rolePower: string[]): IMenuItem[] { const allowedModules = new Set(rolePower.map(p => p.split('.')[0])) const filter = (nodes: IMenuItem[]): IMenuItem[] => { const result: IMenuItem[] = [] for (const node of nodes) { if (node.moduleKey) { if (allowedModules.has(node.moduleKey)) { result.push(node) } continue } if (!node.children || node.children.length === 0) { continue } const filteredChildren = filter(node.children) if (filteredChildren.length === 0) continue result.push({ ...node, children: filteredChildren }) } return result } return filter(fullMenuTree) } } type ShrimpEventHandler = (...args: any[]) => Promise export class ShrimpEvent { constructor(public name: string, public handler: ShrimpEventHandler, public power: RootShrimpPower) { } } export interface IShrimpModuleOptions { icon?: string // 前端子模块跳转按钮图标(element-plus icon 名) ownerKey?: string // 子表→父记录归属字段名(子模块专用,语义: 主人/上级/归属)。前端据此自动过滤和回填默认值 } export class ShrimpModule

= Record> { power: RootShrimpPower root: Shrimp modules: Map = new Map() events: Map = new Map() icon?: string ownerKey?: string constructor(public parent: Shrimp | ShrimpModule, public moduleKey: string, public label: ILabel, power?: RootShrimpPower, options?: IShrimpModuleOptions) { if (power == null) { if (parent instanceof Shrimp) { power = new RootShrimpPower(moduleKey, label) } else { power = new ShrimpPower(parent.power, moduleKey, label) } } this.power = power this.icon = options?.icon this.ownerKey = options?.ownerKey parent.modules.set(this.moduleKey, this) this.root = parent.root } addEvent(name: K, label: ILabel): this addEvent(name: K, power: RootShrimpPower): this addEvent(name: K, powerOrLabel: RootShrimpPower | ILabel) { const handler = (this as any)[name] if (typeof handler !== 'function') { throw new Error(`Method "${name}" not found on ${this.moduleKey}`) } let power: RootShrimpPower if (powerOrLabel instanceof RootShrimpPower) { power = powerOrLabel } else { power = new ShrimpPower(this.power, name, powerOrLabel) } this.events.set(name, new ShrimpEvent(name, handler.bind(this), power)) return this } async call(path: string[], ...args: any[]): Promise { if (path.length > 1) { let module = this.modules.get(path[0]) if (module == null) { throw new Error(`Module "${path[0]}" not found`) } return await module.call(path.slice(1), ...args) } else { const event = this.events.get(path[0]) if (event == null) { throw new Error(`Event "${path[0]}" not found`) } return await event.handler(...args) } } getEventInfo(powers: Set): string[] { let events: string[] = [] this.events.forEach(event => { if (powers.has(event.power.key)) { events.push(event.name) } }) return events } checkEvent(powers: Set, path: string[]): boolean { if (path.length > 1) { let module = this.modules.get(path[0]) if (module == null) return false return module.checkEvent(powers, path.slice(1)) } else { const event = this.events.get(path[0]) if (event == null) return false return powers.has(event.power.key) } } } export class RootShrimpPower { children: ShrimpPower[] = [] constructor(public powerKey: string, public label: ILabel) { } get key(): string { return this.powerKey } } export class ShrimpPower extends RootShrimpPower { constructor(public parent: RootShrimpPower, public powerKey: string, public label: ILabel) { super(powerKey, label) parent.children.push(this) } get key(): string { return this.parent.key + "." + this.powerKey } } // CRUD 接口 export interface ShrimpCrud { findOne(query?: any): Promise list(query?: IShrimpQuery): Promise> get(id: string): Promise add(item: Omit): Promise edit(id: string, item: Partial): Promise delete(id: string): Promise clear(): Promise /** 为记录填充关联 _label 字段 */ populateItems(items: any[], populates: IShrimpPopulate[], crudRegistry?: Map>): Promise } /** 内置字段类型处理器 */ export const defaultTypeHandlers: Record = { [ShrimpFieldTypes.Password]: { reqFilter: async (value) => { if (typeof value === 'string' && value.length > 0) { const bcrypt = await loadBcrypt() return bcrypt.hash(value, 10) } return value }, resFilter: () => "***", }, [ShrimpFieldTypes.Date]: { defaultValue: () => Date.now(), }, [ShrimpFieldTypes.Number]: { defaultValue: 0, }, [ShrimpFieldTypes.Boolean]: { defaultValue: false, }, [ShrimpFieldTypes.Json]: { reqFilter: (value) => { if (value == null || value === "") return undefined if (typeof value === "string") { try { return JSON.parse(value) } catch { throw new ShrimpError({ t: 'shrimp.invalidJson' }) } } return value }, resFilter: (value) => { if (value == null) return value if (typeof value === "object") { return JSON.stringify(value) } return value }, }, [ShrimpFieldTypes.Tab]: { reqFilter: (value) => { if (value == null || value === "") return undefined if (typeof value === "string") { try { return JSON.parse(value) } catch { throw new ShrimpError({ t: 'shrimp.invalidJson' }) } } return value }, resFilter: (value) => { if (value == null) return value if (typeof value === "object") { return JSON.stringify(value) } return value }, }, } export class ShrimpTableModule = ITableCrudProto> extends ShrimpModule

implements ITableCrudProto { fields: IFieldConfig[] = [] constructor(public parent: Shrimp | ShrimpModule, moduleKey: string, public dataSource: ShrimpCrud, fields?: IFieldConfig[], label?: ILabel, power?: RootShrimpPower, options?: IShrimpModuleOptions) { super(parent, moduleKey, label || { t: moduleKey }, power, options) if (fields) { this.fields = fields } // 自动注册 CRUD 到 root.crudRegistry,populateItems 据此解析 relation this.root.crudRegistry.set(moduleKey, dataSource) } getFields(): IFieldConfig[] { return this.fields } static typeHandlers: Map = new Map(Object.entries(defaultTypeHandlers)) static registerTypeHandler(type: string, handler: IFieldTypeHandler) { this.typeHandlers.set(type, handler) } private getTypeHandler(field: IFieldConfig): IFieldTypeHandler | undefined { return ShrimpTableModule.typeHandlers.get(field.type) } /** 前置验证:基于 IFieldConfig 的通用校验 */ private async validateField(field: IFieldConfig, value: any, item: Record): Promise { const isEmpty = value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0) // 1. required if (field.required && isEmpty) { return field.rules?.find(r => r.required)?.message || { t: 'shrimp.fieldRequired', params: { field: field.label } } } if (isEmpty) return null // 2. options 校验 if (Array.isArray(field.options)) { const validValues = new Set(field.options.map(o => o.value)) const values = field.multiple && Array.isArray(value) ? value : [value] for (const v of values) { if (!validValues.has(v)) { return field.rules?.find(r => r.message)?.message || { t: 'shrimp.fieldInvalid', params: { field: field.label } } } } } // 3. type 基础校验 switch (field.type) { case ShrimpFieldTypes.String: if (typeof value !== 'string') return { t: 'shrimp.fieldTypeError', params: { field: field.label } } break case ShrimpFieldTypes.Number: if (typeof value !== 'number' || Number.isNaN(value)) return { t: 'shrimp.fieldTypeError', params: { field: field.label } } break case ShrimpFieldTypes.Boolean: if (typeof value !== 'boolean') return { t: 'shrimp.fieldTypeError', params: { field: field.label } } break case ShrimpFieldTypes.Date: if (typeof value !== 'number' && !(value instanceof Date) && isNaN(Date.parse(value))) { return { t: 'shrimp.fieldTypeError', params: { field: field.label } } } break } // 4. rules 校验 if (field.rules) { for (const rule of field.rules) { const msg = rule.message || { t: 'shrimp.fieldInvalid', params: { field: field.label } } if (rule.min !== undefined) { if (typeof value === 'number' && value < rule.min) return { t: 'shrimp.fieldMin', params: { field: field.label, min: rule.min } } if (typeof value === 'string' && value.length < rule.min) return { t: 'shrimp.fieldMinLength', params: { field: field.label, min: rule.min } } if (Array.isArray(value) && value.length < rule.min) return { t: 'shrimp.fieldMinLength', params: { field: field.label, min: rule.min } } } if (rule.max !== undefined) { if (typeof value === 'number' && value > rule.max) return { t: 'shrimp.fieldMax', params: { field: field.label, max: rule.max } } if (typeof value === 'string' && value.length > rule.max) return { t: 'shrimp.fieldMaxLength', params: { field: field.label, max: rule.max } } if (Array.isArray(value) && value.length > rule.max) return { t: 'shrimp.fieldMaxLength', params: { field: field.label, max: rule.max } } } if (rule.pattern) { const regex = new RegExp(rule.pattern) if (!regex.test(String(value))) return msg } } } return null } /** 准备新增数据:过滤 readonly → 默认值 → reqFilter → validate */ private async prepareAddItem(item: Record): Promise> { const result = { ...item } for (const field of this.fields) { if (field.editable === "readonly") { delete result[field.key] continue } const handler = this.getTypeHandler(field) if (result[field.key] === undefined) { const dv = field.defaultValue !== undefined ? field.defaultValue : handler?.defaultValue if (dv !== undefined) { result[field.key] = typeof dv === 'function' ? (dv as () => any)() : dv } } if (handler?.reqFilter && result[field.key] !== undefined) { result[field.key] = await handler.reqFilter(result[field.key], field, result) } const fieldErr = await this.validateField(field, result[field.key], result) if (fieldErr) throw new ShrimpError(fieldErr) if (handler?.validate && result[field.key] !== undefined) { const err = await handler.validate(result[field.key], field, result) if (err) throw new ShrimpError(err) } } return result } /** 准备编辑数据:过滤 readonly/addOnly → reqFilter → validate */ private async prepareEditItem(item: Record): Promise> { const result = { ...item } for (const field of this.fields) { if (field.editable === "readonly" || field.editable === "addOnly") { delete result[field.key] continue } // 编辑时 undefined 表示未修改,跳过校验并从结果中移除 if (result[field.key] === undefined) { delete result[field.key] continue } const handler = this.getTypeHandler(field) if (handler?.reqFilter) { result[field.key] = await handler.reqFilter(result[field.key], field, result) } const fieldErr = await this.validateField(field, result[field.key], result) if (fieldErr) throw new ShrimpError(fieldErr) if (handler?.validate) { const err = await handler.validate(result[field.key], field, result) if (err) throw new ShrimpError(err) } } return result } /** 处理响应数据:resFilter */ private async processResItem(item: Record): Promise> { const result = { ...item } for (const field of this.fields) { const handler = this.getTypeHandler(field) if (handler?.resFilter && result[field.key] !== undefined) { result[field.key] = await handler.resFilter(result[field.key], field, result) } } return result } /** 填充关联 _label 并应用 resFilter(多条) */ private async prepareResponseItems(items: any[]): Promise { if (items.length === 0) return items const populates = this.getRelationPopulates() if (populates.length > 0 && this.dataSource.populateItems) { await this.dataSource.populateItems(items, populates, this.root.crudRegistry) } return Promise.all(items.map(item => this.processResItem(item))) } /** 填充关联 _label 并应用 resFilter(单条) */ private async prepareResponseItem(item: any): Promise { if (!item) return item const populates = this.getRelationPopulates() if (populates.length > 0 && this.dataSource.populateItems) { await this.dataSource.populateItems([item], populates, this.root.crudRegistry) } return this.processResItem(item) } private excludeKeys(item: any, keys: string[]): any { if (keys.length === 0) return item const result = { ...item } for (const key of keys) { delete result[key] } return result } private filterHidden(item: any): any { return this.excludeKeys(item, this.fields.filter(f => f.hidden).map(f => f.key)) } private getRelationFields(): IFieldConfig[] { return this.fields.filter(f => f.type === "relation" && f.relation) } private getRelationPopulates(): IShrimpPopulate[] { return this.getRelationFields().map(f => ({ key: f.key, moduleKey: f.relation!.moduleKey, labelKey: f.relation!.labelKey, })) } /** 将前端 IPageQuery 翻译为数据源 IShrimpQuery */ private buildQuery(query?: IPageQuery & any): IShrimpQuery { const { keyword, page, pageSize, sortBy, sortOrder, filter, ownerId } = query || {} const p = page || 1 const ps = pageSize || ShrimpConfig.defaultPageSize const shrimpQuery: IShrimpQuery = { offset: (p - 1) * ps, limit: ps, } // filter → where if (filter && typeof filter === 'object') { const filterableFields = new Map(this.fields.filter(f => f.filterable).map(f => [f.key, f])) const stringLikeTypes = new Set(['string', 'textarea']) const where: Record = {} for (const [key, value] of Object.entries(filter)) { const field = filterableFields.get(key) if (!field) continue if (value == null) continue if (Array.isArray(value) && value.length === 0) continue if (typeof value === 'string' && value === '') continue const obj = value as any if (typeof obj === 'object' && !Array.isArray(obj) && obj.min === undefined && obj.max === undefined) continue if (Array.isArray(value)) { where[key] = { in: value } } else if (typeof value === 'object') { const cond: IShrimpCondition = {} if (obj.min !== undefined) cond.gte = obj.min if (obj.max !== undefined) cond.lte = obj.max where[key] = cond } else if (typeof value === 'boolean') { where[key] = { eq: value } } else if (typeof value === 'string' && stringLikeTypes.has(field.type)) { where[key] = { like: value } } else { where[key] = { eq: value } } } if (Object.keys(where).length > 0) shrimpQuery.where = where } // 子表场景: ownerId + 模块 ownerKey 配置 → 强制注入过滤(用户无法绕过) if (ownerId != null && ownerId !== '' && this.ownerKey) { shrimpQuery.where = shrimpQuery.where || {} shrimpQuery.where[this.ownerKey] = { eq: ownerId } } // keyword → orWhere(仅对字符串类型字段做模糊匹配) if (keyword) { const stringLikeTypes = new Set(['string', 'textarea']) const searchFields = this.fields .filter(f => f.searchable && stringLikeTypes.has(f.type)) .map(f => f.key) if (searchFields.length > 0) { shrimpQuery.orWhere = searchFields.map(key => ({ [key]: { like: keyword } })) } } // 排序 if (sortBy) { shrimpQuery.order = [[sortBy, sortOrder === 'desc' ? 'desc' : 'asc']] } return shrimpQuery } async list(query?: IPageQuery & any): Promise> { const shrimpQuery = this.buildQuery(query) const result = await this.dataSource.list(shrimpQuery) result.list = await this.prepareResponseItems(result.list) result.list = result.list.map(item => this.filterHidden(item)) return result } async get(id: string): Promise { const item = await this.dataSource.get(id) if (item) { const processed = await this.prepareResponseItem(item) return this.filterHidden(processed) as T } return item } async add(item: Omit, ownerId?: string): Promise { if (ownerId && this.ownerKey) { (item as any)[this.ownerKey] = ownerId } const prepared = await this.prepareAddItem(item as any) const result = await this.dataSource.add(prepared as Omit) const processed = await this.prepareResponseItem(result) return this.filterHidden(processed) as T } async edit(id: string, item: Partial): Promise { const prepared = await this.prepareEditItem(item as any) const result = await this.dataSource.edit(id, prepared as Partial) if (result) { const processed = await this.prepareResponseItem(result) return this.filterHidden(processed) as T } return result } async delete(id: string): Promise { return await this.dataSource.delete(id) } async clear(query?: IPageQuery & any): Promise { const hasFilter = query && ( (query.keyword) || (query.filter && typeof query.filter === 'object' && Object.keys(query.filter).length > 0) ) if (hasFilter) { const shrimpQuery = this.buildQuery(query) delete shrimpQuery.offset delete shrimpQuery.limit const result = await this.dataSource.list(shrimpQuery) for (const item of result.list as any[]) { await this.dataSource.delete(item._id) } return true } return await this.dataSource.clear() } async export(query?: IPageQuery & any): Promise { const shrimpQuery = this.buildQuery(query) delete shrimpQuery.offset delete shrimpQuery.limit const result = await this.dataSource.list(shrimpQuery) result.list = await this.prepareResponseItems(result.list) return result.list.map(item => this.filterHidden(item)) } async import(data?: T[], ownerId?: string): Promise { if (!data || data.length === 0) return for (const raw of data) { const item = { ...raw } as Record if (ownerId && this.ownerKey) item[this.ownerKey] = ownerId const id = item._id ? String(item._id) : undefined if (id) { // 有 _id 且数据库里已存在 → 编辑覆盖 const existing = await this.dataSource.get(id) if (existing) { const prepared = await this.prepareEditItem(item) await this.dataSource.edit(id, prepared as Partial) continue } } // 无 _id,或 _id 在库里找不到 → 当作新增 const prepared = await this.prepareAddItem(item) await this.dataSource.add(prepared as Omit) } } initCrud() { return this .addEvent("list", this.power) .addEvent("get", this.power) .addEvent("add", { t: "power.add" }) .addEvent("edit", { t: "power.edit" }) .addEvent("delete", { t: "power.delete" }) } } /** 用户模块扩展 - 添加登录和获取用户信息功能 */ const userFields: IFieldConfig[] = [ { key: "username", label: { t: "user.username" }, type: "string", required: true, searchable: true, editable: "addOnly" }, { key: "password", label: { t: "user.password" }, type: "password", tableHide: true }, { key: "nickname", label: { t: "user.nickname" }, type: "string", searchable: true }, { key: "roleId", label: { t: "user.roleId" }, type: "relation", relation: { moduleKey: "role", labelKey: "name" }, filterable: true, multiple: true, defaultValue: [] }, // { key: "status", label: { t: "user.status" }, type: "select", options: [{ label: { t: "base.disabled" }, value: 0 }, { label: { t: "base.enabled" }, value: 1 }], filterable: true, defaultValue: 1 }, { key: "active", label: { t: "user.active" }, type: "boolean", filterable: true, defaultValue: true }, { key: "createdAt", label: { t: "user.createdAt" }, type: "date", editable: "readonly", sortable: true, filterable: true }, { key: "updatedAt", label: { t: "user.updatedAt" }, type: "date", editable: "readonly", sortable: true }, ] export class UserModule extends ShrimpTableModule> { constructor(public parent: Shrimp | ShrimpModule, dataSource: ShrimpCrud>, fields?: IFieldConfig[]) { super(parent, "user", dataSource, fields || userFields, { t: "user.menuManage" }) this.initCrud() } /** 校验密码,子类可覆盖 */ protected async verifyPassword(storedPassword: string, plainPassword: string): Promise { const bcrypt = await loadBcrypt() return bcrypt.compare(plainPassword, storedPassword) } /** 从用户对象提取角色 ID 列表,子类可覆盖 */ protected getUserRoleIds(user: WithId): string[] { return Array.isArray(user.roleId) ? user.roleId.map(String) : user.roleId ? [String(user.roleId)] : [] } /** 用户登录 */ async getUser(username: string, password: string): Promise> { const user = await this.dataSource.findOne({ username, active: true }) if (!user || !(await this.verifyPassword(user.password, password))) { throw new ShrimpError({ t: 'shrimp.loginFailed' }) } return user } /** 获取当前用户信息 */ async getModule(userId: string, roleModule: RoleModule): Promise { const user = await this.dataSource.get(userId) if (!user) { throw new ShrimpError({ t: 'shrimp.userNotFound' }) } const roleIds = this.getUserRoleIds(user) const roles = await Promise.all(roleIds.map(id => roleModule.dataSource.get(id))) const isSuperAdmin = roles.some(r => r?.isSuperAdmin) const powerKeys = isSuperAdmin ? (await this.root.allPower()).map(p => p.key) : roles.flatMap(r => r?.powers || []) return this.root.getModuleInfo(new Set(powerKeys)) } /** 覆盖编辑 - 禁止禁用自己、禁止修改自己的角色 */ async edit(id: string, item: Partial): Promise | null> { const op = this.root.operator if (op && String(op.userId) === String(id)) { if (item.active === false) { throw new ShrimpError({ t: 'shrimp.cannotDisableSelf' }) } if (item.roleId != null) { const newRoleIds = Array.isArray(item.roleId) ? item.roleId.map(String) : [String(item.roleId)] const oldUser = await this.dataSource.get(id) const oldRoleIds = oldUser ? this.getUserRoleIds(oldUser) : [] if (JSON.stringify([...newRoleIds].sort()) !== JSON.stringify([...oldRoleIds].sort())) { throw new ShrimpError({ t: 'shrimp.cannotChangeOwnRole' }) } } } return super.edit(id, item) } /** 覆盖删除 - 禁止删除自己 */ async delete(id: string): Promise { const op = this.root.operator if (op && String(op.userId) === String(id)) { throw new ShrimpError({ t: 'shrimp.cannotDeleteSelf' }) } return super.delete(id) } /** 覆盖清空 - 禁止清空所有用户 */ async clear(): Promise { if (this.root.operator) { throw new ShrimpError({ t: 'shrimp.cannotClearAllUsers' }) } return super.clear() } } /** 角色模块 */ const roleFields: IFieldConfig[] = [ { key: "name", label: { t: "role.name" }, type: "string", required: true, searchable: true }, { key: "description", label: { t: "role.description" }, type: "textarea" }, { key: "isSuperAdmin", label: { t: "role.isSuperAdmin" }, type: "boolean" }, { key: "powers", label: { t: "role.powers" }, type: "string" }, { key: "createdAt", label: { t: "role.createdAt" }, type: "date", editable: "readonly", sortable: true }, { key: "updatedAt", label: { t: "role.updatedAt" }, type: "date", editable: "readonly", sortable: true }, ] export class RoleModule extends ShrimpTableModule, IRoleProto> { constructor(public parent: Shrimp | ShrimpModule, dataSource: ShrimpCrud>, fields?: IFieldConfig[]) { super(parent, "role", dataSource, fields || roleFields, { t: "role.menuManage" }) this.initCrud() this.addEvent("clear", { t: "power.clear" }) this.addEvent("export", { t: "power.export" }) this.addEvent("import", { t: "power.import" }) this.addEvent("allPower", this.power) } async allPower(): Promise<{ key: string; label: ILabel }[]> { return this.root.allPower() } /** 从操作者上下文提取角色 ID 列表,子类可覆盖 */ protected getOperatorRoleIds(op: IShrimpOperator): string[] { return Array.isArray(op.roleId) ? op.roleId.map(String) : op.roleId ? [String(op.roleId)] : [] } /** 覆盖编辑 - 禁止编辑自己的任一角色(防止锁死自己) */ async edit(id: string, item: Partial): Promise | null> { const op = this.root.operator if (op) { const opRoleIds = this.getOperatorRoleIds(op) if (opRoleIds.includes(String(id))) { throw new ShrimpError({ t: 'shrimp.cannotEditOwnRole' }) } } return super.edit(id, item) } /** 填充超级管理员权限(展示用) */ private async fillSuperAdminPowers(role: WithId | null): Promise | null> { if (role && role.isSuperAdmin) { const allPowers = await this.allPower() role.powers = allPowers.map(p => p.key) } return role } async list(query?: IPageQuery & any): Promise>> { const result = await super.list(query) for (const item of result.list) { if (item.isSuperAdmin) { const allPowers = await this.allPower() item.powers = allPowers.map(p => p.key) } } return result } async get(id: string): Promise | null> { const result = await super.get(id) return this.fillSuperAdminPowers(result) } /** 覆盖删除 - 禁止删除自己的任一角色 */ async delete(id: string): Promise { const op = this.root.operator if (op) { const opRoleIds = this.getOperatorRoleIds(op) if (opRoleIds.includes(String(id))) { throw new ShrimpError({ t: 'shrimp.cannotDeleteOwnRole' }) } } return super.delete(id) } /** 覆盖清空 - 禁止清空所有角色 */ async clear(): Promise { if (this.root.operator) { throw new ShrimpError({ t: 'shrimp.cannotClearAllRoles' }) } return super.clear() } }