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。
  契约副本 + 版本号对照是低频改动场景的更干净选择。
This commit is contained in:
2026-05-23 22:29:42 +08:00
parent 4cb3bb8d75
commit f210ce4582
12 changed files with 427 additions and 13 deletions
+1 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "shrimp-admin", "name": "shrimp-admin",
"version": "0.1.0", "version": "0.2.0",
"private": true, "private": true,
"description": "Shrimp 框架前端 — 消费 ShrimpServer + ShrimpTable 契约的经典 Vue3 + Element Plus 实现", "description": "Shrimp 框架前端 — 消费 ShrimpServer + ShrimpTable 契约的经典 Vue3 + Element Plus 实现",
"main": "src/index.ts", "main": "src/index.ts",
@@ -17,9 +17,6 @@
"src", "src",
"README.md" "README.md"
], ],
"dependencies": {
"shrimp-server": "git+http://git.zeromvc.com/wc24/shrimp-server.git#v0.5.0"
},
"peerDependencies": { "peerDependencies": {
"vue": "^3.3.0", "vue": "^3.3.0",
"vue-router": "^4.0.0", "vue-router": "^4.0.0",
+376
View File
@@ -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<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[]>
}
+1 -1
View File
@@ -2,7 +2,7 @@
* ShrimpClient — Shrimp 框架前端基类 * 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,实现层负责翻译后展示 */ /** tip 展示接口(由框架层实现,如 Element Plus)。msg 是 ILabel,实现层负责翻译后展示 */
export interface IShrimpTipper { export interface IShrimpTipper {
+1 -1
View File
@@ -7,7 +7,7 @@ import { createRouter, createWebHashHistory, Router } from 'vue-router'
import zh_cn from './locales/zh-cn.json' import zh_cn from './locales/zh-cn.json'
import en from './locales/en.json' import en from './locales/en.json'
import { createI18n, I18n } from 'vue-i18n' 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 { ShrimpClient, handleShrimpResponse, type IShrimpTipper, type IRouteInfo } from './ShrimpClient'
import FieldString from './components/fields/FieldString.vue' import FieldString from './components/fields/FieldString.vue'
+1 -1
View File
@@ -8,7 +8,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, watch, nextTick } from 'vue' import { ref, computed, watch, nextTick } from 'vue'
import { vueAdmin } from '../VueAdmin' import { vueAdmin } from '../VueAdmin'
import type { IMenuItem, ILabel } from 'shrimp-server/types' import type { IMenuItem, ILabel } from '../IShrimpType'
interface IPowerItem { interface IPowerItem {
key: string key: string
+1 -1
View File
@@ -22,7 +22,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { provide, computed, ref } from 'vue' import { provide, computed, ref } from 'vue'
import { IFieldConfig, IFieldRule } from 'shrimp-server/types' import { IFieldConfig, IFieldRule } from '../IShrimpType'
import { vueAdmin } from '../VueAdmin' import { vueAdmin } from '../VueAdmin'
import type { FormInstance, FormItemRule } from 'element-plus' import type { FormInstance, FormItemRule } from 'element-plus'
+1 -1
View File
@@ -21,7 +21,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { IFieldConfig } from 'shrimp-server/types' import { IFieldConfig } from '../IShrimpType'
import { vueAdmin } from '../VueAdmin' import { vueAdmin } from '../VueAdmin'
interface Props { interface Props {
+1 -1
View File
@@ -79,7 +79,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, useSlots, onMounted, type Component } from 'vue' import { ref, computed, useSlots, onMounted, type Component } from 'vue'
import { ITableCrudProto, IFieldConfig, IModuleInfo, IPageQuery } from 'shrimp-server/types' import { ITableCrudProto, IFieldConfig, IModuleInfo, IPageQuery } from '../IShrimpType'
import { vueAdmin } from '../VueAdmin' import { vueAdmin } from '../VueAdmin'
import { Plus, Delete as DeleteIcon, Download, Upload, Edit, VideoPlay, VideoPause, Operation } from '@element-plus/icons-vue' import { Plus, Delete as DeleteIcon, Download, Upload, Edit, VideoPlay, VideoPause, Operation } from '@element-plus/icons-vue'
import * as ElIcons from '@element-plus/icons-vue' import * as ElIcons from '@element-plus/icons-vue'
+1 -1
View File
@@ -20,7 +20,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { vueAdmin } from '../../VueAdmin' import { vueAdmin } from '../../VueAdmin'
import { ITableCrudProto } from 'shrimp-server/types' import { ITableCrudProto } from '../../IShrimpType'
const props = defineProps<{ const props = defineProps<{
field: any field: any
+41
View File
@@ -2,6 +2,47 @@
// //
// 消费侧用法见 README.md。本文件只做 re-export。 // 消费侧用法见 README.md。本文件只做 re-export。
// ===== 类型契约(IShrimpType 本地副本,@syncedFrom shrimp-server contract 1.0.0)=====
export {
ShrimpCode,
ShrimpError,
ShrimpFieldTypes,
ShrimpConfig,
isShrimpCondition,
} from './IShrimpType'
export type {
ILabel,
IShrimpResponse,
IModuleInfo,
ILoginResult,
IMeResult,
IMenuItem,
ITableProto,
ITableCrudProto,
IUser,
IRole,
IOperationLog,
WithId,
IShrimpOperator,
IShrimpPowerData,
ShrimpRequestFn,
IShrimpClient,
ShrimpFieldType,
FieldEditable,
IFieldTypeHandler,
IFieldConfig,
IFieldRule,
IPageQuery,
IPageResult,
IShrimpCondition,
IShrimpPopulate,
IShrimpQuery,
IPowerItem,
IUserProto,
IRoleProto,
} from './IShrimpType'
export { export {
VueAdmin, VueAdmin,
vueAdmin, vueAdmin,
+1 -1
View File
@@ -51,7 +51,7 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { vueAdmin } from '../VueAdmin' import { vueAdmin } from '../VueAdmin'
import { IRole, IRoleProto, WithId, ILabel } from 'shrimp-server/types' import { IRole, IRoleProto, WithId, ILabel } from '../IShrimpType'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import PowerTree from '../components/PowerTree.vue' import PowerTree from '../components/PowerTree.vue'
+1 -1
View File
@@ -49,7 +49,7 @@ import navMenu from "./navMenu.vue";
import Topbar from "./topbar.vue"; import Topbar from "./topbar.vue";
import Userbar from "./userbar.vue"; import Userbar from "./userbar.vue";
import { vueAdmin } from "../VueAdmin"; import { vueAdmin } from "../VueAdmin";
import { type IMenuItem } from "shrimp-server/types"; import { type IMenuItem } from "../IShrimpType";
import Logo from "./logo.vue"; import Logo from "./logo.vue";
// 初始化选中菜单 // 初始化选中菜单