- 拷 shrimp-server/src/IShrimpType.ts 到 src/IShrimpType.ts,头改 @syncedFrom shrimp-server contract 1.0.0 - 全部内部 import 'shrimp-server/types' → 本地 './IShrimpType'(各文件按相对路径) - src/index.ts 加上类型契约的 re-export(ShrimpCode/ShrimpError/枚举/全部 interface),消费侧从 'shrimp-admin' 一处拿到 - 删 dependencies.shrimp-server,前端 bundle 不再牵 mongoose/express 为什么不直接 import 'shrimp-server': shrimp-server v0.6.0 收单入口后,入口含 mongoose/express,前端会被 vite 牵进 bundle。 契约副本 + 版本号对照是低频改动场景的更干净选择。
377 lines
14 KiB
TypeScript
377 lines
14 KiB
TypeScript
/**
|
||
* Shrimp 类型契约 — 前后端 wire protocol 定义(前端副本)
|
||
*
|
||
* @syncedFrom shrimp-server contract 1.0.0
|
||
*
|
||
* 这是 shrimp-server/src/IShrimpType.ts 的逐字副本。
|
||
* 不要在这里改 wire protocol,先改 shrimp-server 那份升版本,再 copy 过来更新 @syncedFrom。
|
||
*
|
||
* 故意保留副本而不 import 'shrimp-server' 的原因:
|
||
* - shrimp-server 入口会把 mongoose / express 牵进前端 bundle
|
||
* - 拆 './types' 子入口会绕开 vite optimize、可读性也差
|
||
* - 类型契约低频改动,版本号对照即可
|
||
*/
|
||
|
||
/** 业务错误码 */
|
||
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<T = any> {
|
||
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, any>
|
||
}
|
||
|
||
// ============== 通用类型 ==============
|
||
|
||
/**
|
||
* 通用国际化标签 - 所有前端可见文本字段统一类型
|
||
*
|
||
* 形态:
|
||
* - 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<string, ILabel | any> }
|
||
| { tc: string; count: number; params?: Record<string, ILabel | any> }
|
||
| { d: number | Date; format?: string }
|
||
| { n: number; format?: string }
|
||
| Record<string, string>
|
||
|
||
/** 菜单项 */
|
||
export interface IMenuItem {
|
||
name: string
|
||
icon?: string
|
||
label: ILabel // 菜单显示名称
|
||
path?: string
|
||
moduleKey?: string
|
||
children?: IMenuItem[]
|
||
}
|
||
|
||
// ============== Proto 协议定义 ==============
|
||
|
||
/** 标准 Table CRUD 协议(前端可见) */
|
||
export interface ITableProto<T> {
|
||
list(query?: IPageQuery & any): Promise<IPageResult<T>>
|
||
get(id: string): Promise<T | null>
|
||
add(item: Omit<T, '_id'>, ownerId?: string): Promise<T>
|
||
edit(id: string, item: Partial<T>): Promise<T | null>
|
||
delete(id: string): Promise<boolean>
|
||
}
|
||
|
||
/** 完整 Table 协议(后端可用,含管理操作) */
|
||
export interface ITableCrudProto<T> extends ITableProto<T> {
|
||
clear(query?: IPageQuery & any): Promise<boolean>
|
||
export(query?: IPageQuery & any): Promise<T[]>
|
||
import(data?: T[], ownerId?: string): Promise<void>
|
||
}
|
||
|
||
// ============== 预制数据模型 ==============
|
||
|
||
/** 用户 */
|
||
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> = 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<IShrimpResponse>
|
||
|
||
/** 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,
|
||
Json: "json" as ShrimpFieldType,
|
||
Tab: "tab" as ShrimpFieldType,
|
||
}
|
||
|
||
/** 字段可编辑状态 */
|
||
export type FieldEditable = "always" | "readonly" | "addOnly"
|
||
|
||
/** 字段类型处理器:定义每种 type 在请求/响应时的转换、验证、默认值 */
|
||
export interface IFieldTypeHandler {
|
||
/** 请求过滤:前端提交 → 保存前转换(如密码哈希、trim) */
|
||
reqFilter?: (value: any, field: IFieldConfig, item: Record<string, any>) => any | Promise<any>
|
||
/** 响应过滤:数据库取出 → 返回前端前转换(如密码脱敏为 ***、日期格式化) */
|
||
resFilter?: (value: any, field: IFieldConfig, item: Record<string, any>) => any
|
||
/** 验证:保存前校验,返回错误 ILabel 或 null(可直接返回字面字符串、{t}、{tc} 等) */
|
||
validate?: (value: any, field: IFieldConfig, item: Record<string, any>) => ILabel | null | Promise<ILabel | null>
|
||
/** 新增时的默认值(静态值或工厂函数) */
|
||
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[] }[]
|
||
}
|
||
|
||
/** 通用验证规则(前后端共用) */
|
||
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<string, any> // 字段精确匹配条件(AND 关系)
|
||
ownerId?: string // 子表场景: 父记录 _id。后端按模块 ownerKey 配置翻译为 filter 注入(用户无法绕过)
|
||
}
|
||
|
||
/** Shrimp 全局配置 */
|
||
export class ShrimpConfig {
|
||
static defaultPageSize: number = 100000
|
||
}
|
||
|
||
/** 分页结果 */
|
||
export interface IPageResult<T> {
|
||
list: T[] // 当前页数据
|
||
total: number // 总记录数
|
||
page: number // 当前页码
|
||
pageSize: number // 每页数量
|
||
totalPages: number // 总页数
|
||
}
|
||
|
||
// ============== 数据源查询协议 ==============
|
||
//
|
||
// 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<string, IShrimpCondition | any> // AND 条件 (原始值=eq简写)
|
||
orWhere?: Record<string, IShrimpCondition>[] // 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<WithId<IUser>> { }
|
||
|
||
/** 角色模块协议 */
|
||
export interface IRoleProto extends ITableCrudProto<WithId<IRole>> {
|
||
allPower(): Promise<IPowerItem[]>
|
||
}
|