diff --git a/package.json b/package.json index 4508585..971de77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shrimp-admin", - "version": "0.1.0", + "version": "0.2.0", "private": true, "description": "Shrimp 框架前端 — 消费 ShrimpServer + ShrimpTable 契约的经典 Vue3 + Element Plus 实现", "main": "src/index.ts", @@ -17,9 +17,6 @@ "src", "README.md" ], - "dependencies": { - "shrimp-server": "git+http://git.zeromvc.com/wc24/shrimp-server.git#v0.5.0" - }, "peerDependencies": { "vue": "^3.3.0", "vue-router": "^4.0.0", diff --git a/src/IShrimpType.ts b/src/IShrimpType.ts new file mode 100644 index 0000000..eb3ddb7 --- /dev/null +++ b/src/IShrimpType.ts @@ -0,0 +1,376 @@ +/** + * 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 { + 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, + 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) => 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[] }[] +} + +/** 通用验证规则(前后端共用) */ +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 // 总页数 +} + +// ============== 数据源查询协议 ============== +// +// 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 +} diff --git a/src/ShrimpClient.ts b/src/ShrimpClient.ts index 11ab58c..db71288 100644 --- a/src/ShrimpClient.ts +++ b/src/ShrimpClient.ts @@ -2,7 +2,7 @@ * ShrimpClient — Shrimp 框架前端基类 * 框架无关层,包含协议通信、模块管理、菜单树操作 */ -import { ShrimpCode, ShrimpRequestFn, IShrimpResponse, IModuleInfo, IMenuItem, ILoginResult, IMeResult, IShrimpClient, ILabel } from 'shrimp-server/types' +import { ShrimpCode, ShrimpRequestFn, IShrimpResponse, IModuleInfo, IMenuItem, ILoginResult, IMeResult, IShrimpClient, ILabel } from './IShrimpType' /** tip 展示接口(由框架层实现,如 Element Plus)。msg 是 ILabel,实现层负责翻译后展示 */ export interface IShrimpTipper { diff --git a/src/VueAdmin.ts b/src/VueAdmin.ts index 4b681cc..dd36041 100644 --- a/src/VueAdmin.ts +++ b/src/VueAdmin.ts @@ -7,7 +7,7 @@ import { createRouter, createWebHashHistory, Router } from 'vue-router' import zh_cn from './locales/zh-cn.json' import en from './locales/en.json' import { createI18n, I18n } from 'vue-i18n' -import { IShrimpResponse, ILabel } from 'shrimp-server/types' +import { IShrimpResponse, ILabel } from './IShrimpType' import { ShrimpClient, handleShrimpResponse, type IShrimpTipper, type IRouteInfo } from './ShrimpClient' import FieldString from './components/fields/FieldString.vue' diff --git a/src/components/PowerTree.vue b/src/components/PowerTree.vue index 64ebe59..08cbe61 100644 --- a/src/components/PowerTree.vue +++ b/src/components/PowerTree.vue @@ -8,7 +8,7 @@