/** * Shrimp 类型契约 — 前后端 wire protocol 定义(前端副本) * * 这是 shrimp-server/src/IShrimpType.ts 的逐字副本(除本注释块外)。 * 不要在这里改 wire protocol,先改 shrimp-server 那份升版本,再整份 cp 过来。 * * 本文件由 shrimp-server 的 postbuild 自动同步过来,手改会被下次 build 覆盖。 * 手动同步: npm run sync:contract 只想看漂没漂: npm run check:contract * 自动同步这件事存在的原因是:曾经靠人工复制,漂了一整代都没人发现—— * 副本停在旧的 json 类型、缺 object/array/fields/item/parse/refs, * 而前端对未注册类型静默回落到 string,所以漂移不会报错、只会悄悄渲染错。 * * 故意保留副本而不 import 'shrimp-server' 的原因: * - shrimp-server 默认入口会把 mongoose / express 牵进前端 bundle * - 前端项目不该为一份纯类型去装后端包 */ /** 业务错误码 */ export enum ShrimpCode { OK = 0, FAIL = 1, UNAUTHORIZED = 401, FORBIDDEN = 403, NOT_FOUND = 404, SERVER_ERROR = 500, } /** 业务错误 - tip 展示给用户 */ export class ShrimpError extends Error { code: ShrimpCode tip: ILabel constructor(tip: ILabel, code: ShrimpCode = ShrimpCode.FAIL) { super(extractILabelText(tip)) this.code = code this.tip = tip this.name = 'ShrimpError' } } /** 抽取 ILabel 的字符串形态用于 Error.message / 日志(非展示) */ function extractILabelText(v: ILabel): string { if (typeof v === 'string') return v if (v && typeof v === 'object') { const o = v as any if ('t' in o) return o.t if ('tc' in o) return o.tc if ('d' in o) return String(o.d) if ('n' in o) return String(o.n) } return '[ILabel]' } /** 统一响应格式 */ export interface IShrimpResponse { code: ShrimpCode data?: T tip?: ILabel // 给用户看的提示 log?: string // 给开发者看的日志 } /** 模块信息(结构元数据,扁平表示:子模块与主模块同列,通过 parent 关联) */ export interface IModuleInfo { moduleKey: string events: string[] label: ILabel fields: IFieldConfig[] icon?: string // 子模块跳转按钮图标(element-plus icon 名称) parent?: string // 父模块 moduleKey;存在即为子模块(不进菜单、路径嵌套) } /** 登录结果(仅 token) */ export interface ILoginResult { token: string } /** 当前用户信息 */ export interface IMeResult { nickname?: string moduleInfo: IModuleInfo[] menuTree: IMenuItem[] options?: Record } // ============== 通用类型 ============== /** * 通用国际化标签 - 所有前端可见文本字段统一类型 * * 形态: * - string 字面量,前端原样显示 * - { t: 'i18n.key', params?: ... } 翻译(vue-i18n t) * - { tc: 'i18n.key', count, params?: ... } 复数(vue-i18n tc) * - { d: Date|number, format? } 日期格式化(vue-i18n d) * - { n: number, format? } 数字/货币格式化(vue-i18n n) * - { zh: '提交', en: 'Submit' } 内联多语言 * * params 值本身可以是 ILabel,前端递归解析 */ export type ILabel = | string | { t: string; params?: Record } | { tc: string; count: number; params?: Record } | { d: number | Date; format?: string } | { n: number; format?: string } | Record /** 菜单项 */ export interface IMenuItem { name: string icon?: string label: ILabel // 菜单显示名称 path?: string moduleKey?: string children?: IMenuItem[] } // ============== Proto 协议定义 ============== /** 标准 Table CRUD 协议(前端可见) */ export interface ITableProto { list(query?: IPageQuery & any): Promise> get(id: string): Promise add(item: Omit, ownerId?: string): Promise edit(id: string, item: Partial): Promise delete(id: string): Promise } /** 完整 Table 协议(后端可用,含管理操作) */ export interface ITableCrudProto extends ITableProto { clear(query?: IPageQuery & any): Promise export(query?: IPageQuery & any): Promise import(data?: T[], ownerId?: string): Promise } // ============== 预制数据模型 ============== /** 用户 */ export interface IUser { username: string password: string nickname: string roleId: string[] active: boolean createdAt: number updatedAt: number } /** 角色 */ export interface IRole { name: string description: string isSuperAdmin: boolean powers: string[] createdAt: number updatedAt: number } /** 操作日志 */ export interface IOperationLog { operatorId: string | null // 操作者 userId,登录失败等场景为 null ip: string // 来源 IP method: string // RPC 路径,如 "user.edit"、"login" args: string // 参数(已脱敏 + JSON.stringify + 截断) success: boolean errorMsg: string // success=false 时填错误信息 createdAt: number } /** 为数据模型注入 _id 的泛型工具类型 */ export type WithId = T & { _id: string } /** 操作者契约 - 框架层通用操作者上下文,不依赖具体用户模型 */ export interface IShrimpOperator { userId: string roleId: string[] isSuperAdmin: boolean } /** 权限 */ export interface IShrimpPowerData { _id: string powerKey: string } // ============== 前端客户端接口 ============== /** 底层请求函数类型 */ export type ShrimpRequestFn = (path: string[], ...args: any[]) => Promise /** Shrimp 客户端接口(前端实现) */ export interface IShrimpClient { shrimpRequest: ShrimpRequestFn hasEvent(moduleKey: string, eventName: string): boolean } // ============== 字段配置 ============== // // 【功能说明 - 给前端】 // // IFieldConfig 是 Shrimp 框架的动态字段配置系统。 // 后端模块定义 fields 后,前端通过 GET /me 获取 moduleInfo[].fields, // 即可自动生成表格列、搜索栏、新增/编辑表单,无需硬编码页面结构。 // // 注意: _id 由框架自动注入(tableHide=true), 不需要在 fields 中配置, 数据返回但表格不显示。 // // 使用流程: // 1. 调用 GET /me → 拿到 moduleInfo 数组 // 2. 每个 moduleInfo 包含: moduleKey(模块名), events(可调用接口), fields(字段配置) // 3. 根据 fields 渲染: // - 表格列: 非 hidden 且非 tableHide 的字段 → 列标题=label, 数据=key // - 搜索栏: searchable=true 的字段 → 渲染对应类型的搜索输入 // - 表单: 非 hidden 且 editable≠"readonly" 的字段, 根据 editable 决定: // editable="always" 或缺省 → 新增/编辑都可编辑 // editable="readonly" → 新增/编辑都不可编辑(只读,如createdAt) // editable="addOnly" → 新增可编辑, 编辑时不可编辑(如username) // required=true 的字段标记必填, defaultValue 填入默认值 // - 排序: sortable=true 的字段 → 表头可点击排序 // // type 与前端控件映射建议: // string → Input 文本框 // number → InputNumber 数字框 // boolean → Switch 开关 // date → DatePicker 日期选择器 // select → Select 下拉框, 选项由 options 提供 // textarea → Input.TextArea 多行文本 // password → Input.Password 密码框 // relation → Select 下拉框, 选项从 moduleKey 指定的模块 list 接口获取, // 显示字段为 labelKey, 值为 _id // /** 字段类型(string 基础类型,前端可自由扩展自定义类型) */ export type ShrimpFieldType = string /** 框架内置字段类型常量(前端可直接使用或在此基础上扩展) */ export const ShrimpFieldTypes = { String: "string" as ShrimpFieldType, Number: "number" as ShrimpFieldType, Boolean: "boolean" as ShrimpFieldType, Date: "date" as ShrimpFieldType, Select: "select" as ShrimpFieldType, Textarea: "textarea" as ShrimpFieldType, Password: "password" as ShrimpFieldType, Relation: "relation" as ShrimpFieldType, Tab: "tab" as ShrimpFieldType, Object: "object" as ShrimpFieldType, Array: "array" as ShrimpFieldType, } /** 字段可编辑状态 */ export type FieldEditable = "always" | "readonly" | "addOnly" /** 字段类型处理器:定义每种 type 在请求/响应时的转换、验证、默认值 */ export interface IFieldTypeHandler { /** 请求过滤:前端提交 → 保存前转换(如密码哈希、trim) */ reqFilter?: (value: any, field: IFieldConfig, item: Record) => any | Promise /** 响应过滤:数据库取出 → 返回前端前转换(如密码脱敏为 ***、日期格式化) */ resFilter?: (value: any, field: IFieldConfig, item: Record) => any /** 验证:保存前校验,返回错误 ILabel 或 null(可直接返回字面字符串、{t}、{tc} 等) */ validate?: (value: any, field: IFieldConfig, item: Record) => ILabel | null | Promise /** 新增时的默认值(静态值或工厂函数) */ defaultValue?: any | (() => any) } /** 字段配置 */ export interface IFieldConfig { key: string // 字段名(对应数据对象的属性名) label: ILabel // 显示名称(表格列标题、表单标签) type: ShrimpFieldType // 字段类型(决定前端渲染哪种控件) required?: boolean // 是否必填(表单校验) hidden?: boolean // 数据不返回给前端 tableHide?: boolean // 表格中不显示此列,但数据仍返回(如 _id) editable?: FieldEditable // 可编辑状态: always(默认,始终可编辑) | readonly(始终只读,如createdAt) | addOnly(仅添加可编辑,编辑时只读,如username) searchable?: boolean // 是否可搜索(出现在搜索栏) sortable?: boolean // 是否可排序(表头可点击排序) filterable?: boolean // 是否可筛选(出现在筛选栏,用于 filter 精确匹配) options?: { label: ILabel; value: any }[] | string // select 类型的可选项,或全局 options key relation?: { // relation 类型的关联配置 moduleKey: string // 关联模块名(从该模块的 list 接口获取选项) labelKey: string // 关联模块中用作显示的字段名 } defaultValue?: any // 新增时的默认值(优先级高于 typeHandler.defaultValue) multiple?: boolean // 是否支持多选(relation、select 等) rules?: IFieldRule[] // 通用验证规则(前后端共用) /** tab 类型专用:每个 type 对应的子字段配置 */ tabs?: { key: any; label: ILabel; fields: IFieldConfig[] }[] /** object 类型专用:嵌套字段组 */ fields?: IFieldConfig[] /** array 类型专用:每个元素的字段配置(递归 IFieldConfig;元素无需 key,label 用作每项标题) */ item?: IFieldConfig /** tab 类型专用:import 反序列化(把字符串还原成对象,如 xlsx 导入),默认 JSON.parse */ parse?: (text: string) => any } /** 通用验证规则(前后端共用) */ export interface IFieldRule { required?: boolean min?: number max?: number pattern?: string message?: ILabel } // ============== 预制协议 ============== /** 分页查询参数 */ export interface IPageQuery { page?: number // 页码,从 1 开始 pageSize?: number // 每页数量,默认 ShrimpConfig.defaultPageSize keyword?: string // 关键词搜索,按 searchable 字段模糊匹配 sortBy?: string // 排序字段(对应 field.key) sortOrder?: 'asc' | 'desc' // 排序方向 filter?: Record // 字段精确匹配条件(AND 关系) ownerId?: string // 子表场景: 父记录 _id。后端按模块 ownerKey 配置翻译为 filter 注入(用户无法绕过) } /** Shrimp 全局配置 */ export class ShrimpConfig { static defaultPageSize: number = 100000 } /** 分页结果 */ export interface IPageResult { list: T[] // 当前页数据 total: number // 总记录数 page: number // 当前页码 pageSize: number // 每页数量 totalPages: number // 总页数 refs?: Record> // relation 显示名字典: { moduleKey: { id: 显示名 } },前端显示时查名,替代逐行 _label } // ============== 数据源查询协议 ============== // // IShrimpQuery 是数据源无关的查询协议。 // ShrimpTableModule 负责将前端参数(IPageQuery)翻译为 IShrimpQuery, // 各 ShrimpCrud 实现(MemoryCrud/MongoShrimpCrud)负责执行。 // // 查询语义: // WHERE (where 全部满足) AND (orWhere 至少一组满足) // ORDER BY order // LIMIT limit OFFSET offset // /** 查询条件运算符 */ export interface IShrimpCondition { eq?: any // 精确匹配 (=) in?: any[] // IN 查询 like?: string // 模糊匹配 (不区分大小写) gte?: any // 大于等于 (>=) lte?: any // 小于等于 (<=) } /** 判断值是否为 IShrimpCondition 对象 */ export function isShrimpCondition(val: any): val is IShrimpCondition { return val != null && typeof val === 'object' && !Array.isArray(val) && ('eq' in val || 'in' in val || 'like' in val || 'gte' in val || 'lte' in val) } /** 关联填充指令 */ export interface IShrimpPopulate { key: string // 当前模块的字段名 (如 roleId) moduleKey: string // 关联模块名 (如 role) labelKey: string // 关联模块中用作显示的字段 (如 name) } /** 通用查询协议 (数据源无关) */ export interface IShrimpQuery { where?: Record // AND 条件 (原始值=eq简写) orWhere?: Record[] // OR 条件组 (用于 keyword) order?: [string, 'asc' | 'desc'][] // 排序 offset?: number // 偏移量 limit?: number // 限制数量 populate?: IShrimpPopulate[] // 关联填充 (输出 {key}_label) } /** 权限项 */ export interface IPowerItem { key: string label: ILabel } /** 用户模块协议 */ export interface IUserProto extends ITableCrudProto> { } /** 角色模块协议 */ export interface IRoleProto extends ITableCrudProto> { allPower(): Promise }