/** * ShrimpClient — Shrimp 框架前端基类 * 框架无关层,包含协议通信、模块管理、菜单树操作 */ import { ShrimpCode, ShrimpRequestFn, IShrimpResponse, IModuleInfo, IMenuItem, ILoginResult, IMeResult, IShrimpClient, ILabel } from './IShrimpType' /** tip 展示接口(由框架层实现,如 Element Plus)。msg 是 ILabel,实现层负责翻译后展示 */ export interface IShrimpTipper { success(msg: ILabel): void error(msg: ILabel): void alert(msg: ILabel): Promise } /** 路由注册信息,子类 addRoute 的参数 */ export interface IRouteInfo { moduleKey: string name: string path?: string label: ILabel events: string[] /** 组件相对 module 目录的路径(不含 .vue),不传则默认 moduleKey。用于子模块嵌套目录定位 */ componentPath?: string /** 子模块跳转按钮图标(element-plus icon 名,仅 sub 模块有意义) */ icon?: string } class ShrimpResponseError extends Error { constructor(public data: IShrimpResponse) { super(data.code.toString()) this.name = 'ShrimpResponseError' } } /** * 处理 Shrimp 响应中的 tip 和 log * @returns 处理后的响应数据,或抛出错误 */ export function handleShrimpResponse( body: IShrimpResponse, tipper: IShrimpTipper, onUnauthorized?: () => void, ): any { if (body.log) { console.warn('[Shrimp]', body.log) } if (body.code === ShrimpCode.OK) { if (body.tip) tipper.success(body.tip) return body.data } // 业务错误:根据 code 决定展示方式 if (body.tip) { if (body.code === ShrimpCode.UNAUTHORIZED) { tipper.alert(body.tip).then(() => onUnauthorized?.()) } else { tipper.error(body.tip) } } else { tipper.alert("").then(() => onUnauthorized?.()) // tipper.error(t('base.fail')) } const err = new ShrimpResponseError(body) as any err.code = body.code err.handled = true throw err } /** * 根据协议类型生成类型安全的 API 调用器 * 用法: * const userApi = getCaller(request, "user") * const subApi = getCaller(request, ["parent", "sub"]) */ export function getCaller

>(request: ShrimpRequestFn, moduleKey: string | string[]): P { const segments = Array.isArray(moduleKey) ? moduleKey : [moduleKey] return new Proxy({} as any, { get: (target, prop, receiver) => { if (typeof prop === 'symbol' || prop === 'valueOf' || prop === 'toString') { return Reflect.get(target, prop, receiver) } return (...args: any[]) => request([...segments, prop as string], ...args) } }) } /** * ShrimpClient 基类 * 封装与 Shrimp 协议相关的公共方法,框架无关 * 业务层(如 HandAdmin)继承此类并实现底层请求 */ export abstract class ShrimpClient implements IShrimpClient { /** 当前用户的模块权限列表 */ moduleInfo: IModuleInfo[] = [] /** 当前用户的菜单树(含层级结构) */ menuTree: IMenuItem[] = [] /** 全局选项(/me 返回的 options) */ globalOptions: Record = {} private callers: Map = new Map() /** 子类必须实现:底层 HTTP 请求 */ abstract rawRequest(method: string, url: string, data?: any): Promise /** 子类必须实现:路由注册(框架特定,如 Vue Router) */ abstract registerRoute(info: IRouteInfo): void // ---- Shrimp RPC ---- /** Shrimp RPC 请求(POST /moduleKey/eventName) */ async doRequest(path: string[], data?: any[]): Promise { return this.rawRequest('POST', '/' + path.join('/'), data) } /** 标准 Shrimp 请求函数(过滤 undefined 参数),供 getCaller 使用 */ async shrimpRequest(path: string[], ...args: any[]): Promise { const filteredArgs = args.filter(arg => arg !== undefined) return await this.doRequest(path, filteredArgs.length > 0 ? filteredArgs : undefined) } // ---- REST 接口 ---- /** 获取当前用户信息(权限 + 菜单) */ async fetchMe(): Promise { return this.rawRequest('GET', '/me') } /** 用户登录 */ async fetchLogin(data: { username: string; password: string }): Promise { return this.rawRequest('POST', '/login', data) } /** 获取后台配置 */ async fetchConfig(): Promise> { return this.rawRequest>('GET', '/config') } // ---- 用户信息 ---- /** 获取当前用户信息(moduleInfo + menuTree)并注册路由 */ async loadUserInfo(): Promise { const data = await this.fetchMe() this.moduleInfo = data.moduleInfo this.menuTree = data.menuTree this.globalOptions = data.options || {} if (this.menuTree.length > 0) { this.registerRoutesFromMenuTree(this.menuTree) } else { this.registerRoutesFromModuleInfo(this.moduleInfo) } // 子模块永远走 moduleInfo 递归注册(不进 menu) this.registerSubModuleRoutes(this.moduleInfo) return data } // ---- 路由注册 ---- /** * 根据 moduleInfo 扁平注册路由 * 适用于无层级菜单的场景:moduleKey 即路由名,path 为 /moduleKey * 子模块(带 parent)不会作为顶层路由,由 registerSubModuleRoutes 处理 */ registerRoutesFromModuleInfo(moduleInfo: IModuleInfo[]) { moduleInfo.forEach(info => { if (info.parent) return this.registerRoute({ moduleKey: info.moduleKey, name: info.moduleKey, label: info.label, path: '/' + info.moduleKey, events: info.events, }) }) } /** * 根据 menuTree 层级注册路由 * 遍历菜单树,对每个带 moduleKey 的项调用 addRoute */ registerRoutesFromMenuTree(menuTree: IMenuItem[]) { const walk = (items: IMenuItem[]) => { items.forEach(item => { if (item.moduleKey) { this.registerRoute({ moduleKey: item.moduleKey, name: item.name, path: item.path, label: item.label, events: this.getEvents(item.moduleKey), }) } if (item.children) { walk(item.children) } }) } walk(menuTree) } /** * 注册子模块路由(扁平 moduleInfo,通过 parent 链接) * 子模块路径 = //,组件文件 = @/module//.vue */ registerSubModuleRoutes(moduleInfo: IModuleInfo[]) { const byKey = new Map(moduleInfo.map(m => [m.moduleKey, m])) const buildSegments = (info: IModuleInfo): string[] => { const segs = [info.moduleKey] let p = info.parent while (p) { segs.unshift(p) p = byKey.get(p)?.parent } return segs } moduleInfo.forEach(info => { if (!info.parent) return const segments = buildSegments(info) this.registerRoute({ moduleKey: info.moduleKey, name: segments.join('.'), path: '/' + segments.join('/'), label: info.label, events: info.events, componentPath: segments.join('/'), icon: info.icon, }) }) } // ---- 菜单树操作 ---- /** 获取第一个菜单路径(用于默认跳转) */ getFirstMenuPath(): string { const findFirst = (items: IMenuItem[]): string | null => { for (const item of items) { if (item.path) return item.path if (item.moduleKey) return '/' + item.moduleKey if (item.children) { const found = findFirst(item.children) if (found) return found } } return null } const path = findFirst(this.menuTree) if (path) return path const top = this.moduleInfo.find(m => !m.parent) return '/' + (top?.moduleKey || '') } /** 根据 path 获取所在顶级菜单的索引 */ getMenuIndex(path: string): number { const findIndex = (items: IMenuItem[], targetPath: string): number => { for (let i = 0; i < items.length; i++) { const item = items[i] if (item.path === targetPath || '/' + item.name === targetPath) return i if (item.children) { const childIndex = findIndex(item.children, targetPath) if (childIndex !== -1) return i } } return -1 } return findIndex(this.menuTree, path) } /** 判断是否为分组菜单(有 children) */ isGroupMenu(item?: IMenuItem): boolean { return item != null && item.children != null && item.children.length > 0 } /** 根据 path 生成面包屑路径(支持子模块: 走 moduleInfo.parent 链) */ getBreadcrumb(path: string): { key: string; label: ILabel; path?: string }[] { const result: { key: string; label: ILabel; path?: string }[] = [] const findInMenu = (items: IMenuItem[], targetPath: string, out: typeof result): boolean => { for (const item of items) { const itemPath = item.path || '/' + item.name if (itemPath === targetPath) { out.unshift({ key: item.name, label: item.label, path: itemPath }) return true } if (item.children) { if (findInMenu(item.children, targetPath, out)) { out.unshift({ key: item.name, label: item.label, path: item.path }) return true } } } return false } if (findInMenu(this.menuTree, path, result)) return result // 子模块 fallback: 沿 moduleInfo.parent 链构建,带累积路径 const segments = path.split('/').filter(Boolean) if (segments.length === 0) return result const byKey = new Map(this.moduleInfo.map(m => [m.moduleKey, m])) const chain: IModuleInfo[] = [] let info = byKey.get(segments[segments.length - 1]) while (info) { chain.unshift(info) info = info.parent ? byKey.get(info.parent) : undefined } if (chain.length === 0) return result // 顶层模块如果在 menu 分组下, 先把分组上下文放进来 findInMenu(this.menuTree, '/' + chain[0].moduleKey, result) const lastKey = result.length > 0 ? result[result.length - 1].key : '' let acc = '' chain.forEach(m => { acc += '/' + m.moduleKey if (m.moduleKey === lastKey) return // 已被 menu 添加 result.push({ key: m.moduleKey, label: m.label, path: acc }) }) return result } // ---- 模块权限 ---- /** 获取模块的权限事件列表 */ getEvents(moduleKey: string): string[] { const mod = this.moduleInfo.find(m => m.moduleKey === moduleKey) return mod?.events || [] } /** 判断是否有某个事件的权限 */ hasEvent(moduleKey: string, eventName: string): boolean { return this.getEvents(moduleKey).includes(eventName) } /** 获取绑定到指定模块的权限检查器:const can = client.can('shopAdmin'); can('apply') */ can(moduleKey: string): (event: string) => boolean { return (event: string) => this.hasEvent(moduleKey, event) } // ---- 模块 API ---- /** 获取类型安全的模块 API(带缓存,自动沿 parent 链解析嵌套模块路径) */ getModule

>(moduleKey: string): P { let caller = this.callers.get(moduleKey) if (!caller) { const segments = this.resolveModulePath(moduleKey) caller = getCaller

(this.shrimpRequest.bind(this), segments) this.callers.set(moduleKey, caller) } return caller } /** 从 moduleInfo 沿 parent 链回溯出完整路径(根→自身);找不到时退化为单段 */ private resolveModulePath(moduleKey: string): string[] { const byKey = new Map(this.moduleInfo.map(m => [m.moduleKey, m])) const info = byKey.get(moduleKey) if (!info) return [moduleKey] const segs = [info.moduleKey] let p = info.parent while (p) { segs.unshift(p) p = byKey.get(p)?.parent } return segs } /** 清除模块 API 缓存 */ clearModuleCache(moduleKey?: string) { if (moduleKey) { this.callers.delete(moduleKey) } else { this.callers.clear() } } }