Files
shrimp-admin/src/IShrimpType.ts
T
wc24 f210ce4582 v0.2.0: 解耦 shrimp-server,内置 IShrimpType 契约副本
- 拷 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。
  契约副本 + 版本号对照是低频改动场景的更干净选择。
2026-05-23 22:29:42 +08:00

377 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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[]>
}