@sjlit/rest-ui

Schema-driven UI components for REST projects


License
MIT
Install
npm install @sjlit/rest-ui@1.1.6

Documentation

@sjlit/rest-ui

基于 REST Schema 定义的 Vue 3 + Element Plus 组件库,提供 schema 驱动的自动 CRUD 页面渲染能力。

⚠️ 升级提示:如果消费方构建后 vendor chunk 体积超过 1 MB, 请升级到 ≥ 1.1.1 版本。原因和原理详见 引入 Element Plus / 体积对比

设计理念

本库以 REST 项目的 schema 模块定义为唯一类型标准,通过后端返回的 schema 元数据自动渲染搜索表单、数据表格、创建/编辑对话框等 UI 元素,实现"零代码"或"低代码"的 CRUD 页面开发。

三层架构

core/      -- 纯逻辑层(类型定义、常量、工具函数)
runtime/   -- Vue 运行时层(配置注入、CRUD 状态管理)
ui/        -- UI 组件层(Element Plus 组件封装)
  • core 层不依赖 Vue 或 Element Plus,可在任何 JS 环境使用
  • runtime 层依赖 Vue 的 provide/inject,提供全局配置和 CRUD 状态管理
  • ui 层依赖 Element Plus,提供可复用的 schema 驱动组件

特性

  • Schema 驱动:基于后端 schema 定义自动渲染表单、表格、CRUD 页面
  • 全自动 CRUDSchemaViewer 组件一行代码完成完整的增删改查页面
  • 手动控制SchemaPage 组件提供底层编排,外部控制数据流
  • 响应式设计SchemaGrid 自动适配移动端(折叠面板)和桌面端(表格)
  • 内置样式体系:基于 Element Plus CSS 变量的自适应阴影、间距系统和响应式断点
  • 插件化配置:通过 Vue Plugin 全局注入 HTTP 客户端、权限、路由、国际化
  • 类型安全:完整的 TypeScript 类型定义,与 REST Go 结构体对齐
  • 高度可扩展:丰富的插槽系统支持自定义搜索表单、表格列、表单字段

安装

npm install @sjlit/rest-ui

Peer Dependencies

npm install vue@^3.3.0 element-plus@^2.12.0 @element-plus/icons-vue@^2.3.0

快速开始

1. 注册插件

在应用入口文件中注册 SchemaUIPlugin,传入全局配置:

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import { SchemaUIPlugin } from '@sjlit/rest-ui'
import axios from 'axios'
import App from './App.vue'

const app = createApp(App)

app.use(ElementPlus)
app.use(SchemaUIPlugin, {
  // 必填:HTTP 客户端
  httpClient: {
    get: (url, config) => axios.get(url, config),
    post: (url, data, config) => axios.post(url, data, config),
    put: (url, data, config) => axios.put(url, data, config),
    delete: (url, data) => axios.delete(url, { data }),
  },
  // 可选:权限检查
  hasPermission: (permission) => {
    const userPermissions = JSON.parse(localStorage.getItem('permissions') || '[]')
    return userPermissions.includes(permission)
  },
  // 可选:路由跳转
  router: {
    push: (to) => app.config.globalProperties.$router.push(to),
  },
  // 可选:国际化
  i18n: {
    t: (key, ...args) => {
      const messages: Record<string, string> = {
        'validation.required': `${args[0]}不能为空`,
        'validation.max': `${args[0]}不能超过${args[1]}个字符`,
        'validation.pattern': `${args[0]}格式不正确`,
      }
      return messages[key] || key
    },
  },
  // 可选:API 前缀
  apiPrefix: 'rest',
})

app.mount('#app')

2. 使用 SchemaViewer(全自动 CRUD)

<template>
  <SchemaViewer module="user" table="admin" title="管理员管理" />
</template>

<script setup lang="ts">
import { SchemaViewer } from '@sjlit/rest-ui'
</script>

3. 使用 SchemaPage(手动控制数据流)

<template>
  <SchemaPage
    :schemas="schemas"
    :models="models"
    :pagination="pagination"
    title="手动控制示例"
    @search="handleSearch"
    @pageChange="handlePageChange"
    @formSubmit="handleFormSubmit"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { SchemaPage } from '@sjlit/rest-ui'
import type { Schema, Model, Pagination } from '@sjlit/rest-ui'

const schemas = ref<Schema[]>([])
const models = ref<Model[]>([])
const pagination = ref<Pagination>({ index: 1, size: 15, totalCount: 0 })

async function loadSchemas() {
  // 从后端加载 schema 定义
  const res = await fetch('/rest/schema/user/admin')
  schemas.value = await res.json()
}

async function handleSearch(model: Model) {
  const res = await fetch('/rest/user/admins?' + new URLSearchParams(model))
  const data = await res.json()
  models.value = data.data
  pagination.value = {
    index: parseInt(data.page),
    size: parseInt(data.page_size),
    totalCount: parseInt(data.total_count),
  }
}

function handlePageChange(index: number) {
  pagination.value.index = index
  handleSearch({})
}

function handleFormSubmit(model: Model, scenario: string) {
  if (scenario === 'create') {
    fetch('/rest/user/admin', { method: 'POST', body: JSON.stringify(model) })
  } else {
    fetch(`/rest/user/admin/${model.id}`, { method: 'PUT', body: JSON.stringify(model) })
  }
}

loadSchemas()
</script>

引入 Element Plus

rest-ui 内部已经显式 import 了所有用到的 Element Plus 组件 (参见 src/ui/SchemaPage.vue / SchemaGrid.vue / SchemaForm.vue / parts/Action.vue / parts/Cell.vue / parts/FormItem.vue 顶部的 import { ... } from 'element-plus'),不需要依赖消费方的 app.use 或构建插件解析模板,任何引入方式都能正常工作

SchemaViewer.vue 里的命令式 API ElMessageBox.confirm 也已经 显式 import。下面分三种使用方式说明。

重要:上面说的"任何方式"指消费方只要保证 element-plus 已经被 加载到运行时即可。消费方页面里如果自己又写了 <el-xxx> 标签 (例如 <el-button><el-input> 放在自己的 <template> 里), 那么消费方要么走方式一(app.use(ElementPlus))、要么走方式二 (unplugin-vue-components + ElementPlusResolver),不然模板 编译时会报 Failed to resolve component: el-xxx。rest-ui 自带的 ElementPlusResolver 解析覆盖了它用到的 27 个 EP 组件,所以方式二 完全够用。

方式一:全量 app.use(ElementPlus)(最省事)

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import { SchemaUIPlugin } from '@sjlit/rest-ui'
import App from './App.vue'

const app = createApp(App)
app.use(ElementPlus)
app.use(SchemaUIPlugin, { httpClient: /* ... */ })
app.mount('#app')

rest-ui 内部对每个组件用的是局部 import(Vue 3 <script setup> 会优先匹配局部),不会和全局注册产生冲突,也不会出现重复注册告警。

方式二:按需引入(C 端项目强烈推荐

通过 unplugin-vue-components 让消费方自己写的 <el-xxx> 标签也能 自动 import;rest-ui 内部已经 import 的组件会由打包器自动 tree-shake 去掉重复 import,无需额外配置

npm install -D unplugin-vue-components
// vite.config.ts
import { defineConfig } from 'vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    Components({
      // 关键: 必须是 'css',让 resolver 自动按需注入组件对应的 CSS,
      // 而不是要求消费方手动 import 'element-plus/dist/index.css'(那会回到全量样式)。
      resolvers: [ElementPlusResolver({ importStyle: 'css' })],
    }),
  ],
})

采用按需引入后,不再需要 app.use(ElementPlus)import 'element-plus/dist/index.css'

import { createApp } from 'vue'
import { SchemaUIPlugin } from '@sjlit/rest-ui'
import App from './App.vue'

const app = createApp(App)
app.use(SchemaUIPlugin, { httpClient: /* ... */ })
app.mount('#app')

关于 CSS 变量

rest-ui 的样式里大量使用 var(--el-color-primary, #409eff) 等 Element Plus 全局变量。采用按需引入时,每个被加载的组件都会把它 对应的 CSS 变量注入到 :root,因此变量值会自动可用。请勿额外 import 'element-plus/dist/index.css',否则会回到全量样式, 抵消按需引入的收益。

Webpack / Vue CLI 用户

// vue.config.js 或 webpack.config.js
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
const Components = require('unplugin-vue-components/webpack').default

module.exports = {
  // ...
  plugins: [
    Components({
      resolvers: [ElementPlusResolver({ importStyle: 'css' })],
    }),
  ],
}

哪种方式选哪个?

场景 推荐
内部管理系统、原型验证、不在意体积 方式一(最省事)
面向 C 端、bundle 体积敏感 方式二(按需)
已有 unplugin-vue-components 配置 方式二(无需为 rest-ui 特殊处理)
单元测试 / Storybook / 脱离 app.use 方式一 or 方式二都可以,库内已经包含全部依赖

体积对比(实测,Vite 5 + element-plus 2.14)

下面数字来自同一个最小消费方 demo(仅引入 @sjlit/rest-ui 加上用到的 几个 el-* 组件),可见按需引入对最终 bundle 体积的影响:

引入方式 打包后 CSS gzip 后 CSS 打包后 JS gzip 后 JS
方式一(app.use(ElementPlus) + index.css 363.65 kB 49.15 kB 1 MB+ 280 kB+
方式二(ElementPlusResolver({ importStyle: 'css' }) 81.28 kB 11.44 kB ~200 kB ~60 kB
本次修复(≥1.1.1,库改深层路径 import) 6.53 kB 1.75 kB ~670 kB ~220 kB

按需引入对 CSS 的收益最大,节省约 78%rest-ui 自带的 dist/style.css(约 8 KB,仅 .schema-* 前缀的覆写样式)无论如何 都会进入产物,因为它依赖 --el-color-primary 等 CSS 变量来适配主题。 如果消费方已经全量引入了 EP 样式,这 8 KB 可以省掉。

为什么 1.1.0 → 1.1.1 升级 vendor chunk 就能瘦下来?

1.1.0 之前库内部写的是 import { ElButton } from 'element-plus'。这个 入口是聚合 barrel(element-plus/es/index.mjs),它内部 import 了全部 60+ 个组件 + 全部 hooks + 全部 constants。即使消费方只用了 ElButton 这一个具名,rollup treeshake 也会失败 —— 因为 SASS 主题样式 + 共享 hooks(如 useFormSize)在多组件间形成传递依赖,barrel 文件的执行链 无法被剥离,结果消费方 vendor chunk 拉进全量 EP(1 MB+)。

1.1.1 把库内 7 个 .vue 文件全部改成 深层路径 import,例如 import ElButton from 'element-plus/es/components/button/index.mjs'。 这与 unplugin-vue-componentsElementPlusResolver 内部路径完全 一致,可被 vite / rollup / webpack 正确 treeshake。消费方无需任何 配置改动,升级版本即可生效(前提是消费方自己没额外 import ElementPlusimport 'element-plus/dist/index.css')。

关于 @element-plus/icons-vue:如果消费方也按需引入图标 (import { Edit } from '@element-plus/icons-vue'),rollup 能正确 treeshake 整个包;如果消费方 import * as Icons from '@element-plus/icons-vue' 这种写法会拉全量图标(约 600+ 个 SVG),需要避免。


配置系统

SchemaUIConfig

全局配置通过 SchemaUIPlugin 注入,所有字段均为可选,但 httpClientSchemaViewer 必需的。

字段 类型 必填 说明
httpClient { get, post, put, delete } HTTP 客户端,用于 SchemaViewerCRUD
hasPermission (permission: string) => boolean 权限检查函数
router { push: (to: any) => void } 路由对象,用于页面跳转
i18n { t: (key: string, ...args: any[]) => string } 国际化翻译函数
apiPrefix string API 路由前缀(默认 'rest'

useSchemaUI

在组件或 composable 中获取全局配置:

import { useSchemaUI } from '@sjlit/rest-ui'

const config = useSchemaUI()
// config.httpClient, config.i18n, config.hasPermission, ...

注意useSchemaUI() 在插件未注册时会抛出错误。如果需要在不注册插件的情况下使用 SchemaPage 等纯展示组件,请避免调用此方法。


核心类型

Schema

后端 schema 定义的核心接口,与 REST 项目的 Go 结构体对齐。

interface Schema {
  id?: number
  created_at?: number
  updated_at?: number
  tenant_id?: string
  module_name: string
  table_name: string
  enable: number
  column: string        // 字段名
  label: string         // 显示标签
  type: string          // 数据类型:integer | float | boolean | string
  format: string        // 显示格式:text | dropdown | datetime | date | time | password | ...
  native: number
  primary_key: number   // 1 = 主键,0 = 非主键
  expression: string
  scenarios: string[]   // 适用场景:create | update | delete | search | list | detail | export
  rules: SchemaRule
  attributes: SchemaAttribute
  relations: Relation
  position: number
}

SchemaRule

字段验证规则。

interface SchemaRule {
  min: number
  max: number
  type: string
  unique: boolean
  required: string[]    // 必填场景列表
  regular?: string      // 正则表达式
  safe?: boolean
}

SchemaAttribute

字段扩展属性。

interface SchemaAttribute {
  match: string
  tag?: string
  default_value: string
  readonly: string[]    // 只读场景列表
  disable: string[]     // 禁用场景列表
  visible: VisibleCondition[]  // 可见性条件
  invisible: boolean    // 是否完全隐藏
  end_of_now: boolean
  time_search_range: string
  values?: EnumValue[]  // 枚举值列表(下拉选项)
  live: LiveValue       // 动态加载配置
  upload_url?: string   // 文件上传地址
  icon?: string
  sort: boolean         // 是否支持排序
  suffix?: string       // 输入框后缀
  tooltip?: string      // 提示文本
  dropdown?: DropdownOptions  // 下拉框配置
  description?: string
}

Action

操作按钮定义,用于表格行操作、表单操作、搜索操作等。

interface Action {
  name: string
  label: string
  type?: ButtonType     // Element Plus 按钮类型:primary | success | danger | warning | info | default
  icon?: string
  round?: boolean
  size?: ComponentSize  // Element Plus 尺寸:large | default | small
  permission?: string   // 所需权限标识
  selection?: boolean   // 预留字段,当前未生效
  hidden?: boolean | ((model: Model) => boolean | Promise<boolean>)
  callback?: (model: Model, schemas?: Schema[], loading?: any) => void
  asyncCallback?: (model: Model, schemas?: Schema[], action?: Action) => Promise<void>
}

ButtonType / ComponentSize 是从 element-plus 透传的类型联合,可从 @sjlit/rest-ui 一处取到,无须再单独 import element-plus 拿类型。

Model

数据模型,任意键值对。

interface Model {
  [key: string]: any
}

Pagination

分页信息。

interface Pagination {
  index: number         // 当前页码
  size: number          // 每页条数
  totalCount: number    // 总条数
}

CRUDOptions

CRUD 类构造选项。

interface CRUDOptions {
  module?: string
  table?: string
  apiPrefix?: string
  schemas?: Schema[] | Record<string, Schema>
  httpClient: SchemaUIConfig['httpClient'] // 必须,用于 HTTP 请求
}

核心常量

数据类型常量

TypeInteger   = 'integer'
TypeFloat     = 'float'
TypeBoolean   = 'boolean'
TypeString    = 'string'

格式常量

FormatInteger    = 'integer'
FormatFloat      = 'float'
FormatBoolean    = 'boolean'
FormatString     = 'string'
FormatText       = 'text'
FormatDropdown   = 'dropdown'
FormatDatetime   = 'datetime'
FormatDate       = 'date'
FormatTime       = 'time'
FormatTimestamp  = 'timestamp'
FormatPassword   = 'password'

场景常量

ScenarioCreate  = 'create'
ScenarioUpdate  = 'update'
ScenarioDelete  = 'delete'
ScenarioSearch  = 'search'
ScenarioExport  = 'export'
ScenarioList    = 'list'
ScenarioDetail  = 'detail'

匹配模式

MatchExactly  = 'exactly'
MatchFuzzy    = 'fuzzy'

Live 类型

LiveTypeDropdown   = 'dropdown'
LiveTypeCascader   = 'cascader'

工具函数

Scenarios

场景数组的扩展类,提供便捷的 has() 方法。

import { Scenarios } from '@sjlit/rest-ui'

const scenarios = Scenarios.from('create;update;list')
scenarios.has('create')  // true
scenarios.has('delete')  // false

encode / decode

模型值的编码/解码函数,用于表单提交前后的数据转换。

import { encode, decode } from '@sjlit/rest-ui'

// encode: Date -> 格式字符串(YYYY-MM-DD HH:mm:ss)
const submitModel = encode(model, schemas, 'create')

// decode: string -> number/boolean
const formModel = decode(rawModel, schemas, 'create')

转换规则:

  • encodedatetime/date/timestamp/time 格式的 Date 对象转为 YYYY-MM-DD HH:mm:ss 格式字符串
  • decodeinteger 类型的字符串转为整数;float/double/decimal 转为浮点数;boolean 格式转为布尔值

getModelValue / getModelLabel

import { getModelValue, getModelLabel } from '@sjlit/rest-ui'

const value = getModelValue(model, 'status')      // 获取原始值
const label = getModelLabel(model, 'status')      // 获取显示标签(支持枚举映射)

generateSchemaRule

根据 schema 生成 Element Plus 表单验证规则。

import { generateSchemaRule } from '@sjlit/rest-ui'

const rules = generateSchemaRule(
  (key, args) => `${args[0]}不能为空`,  // 翻译函数
  schema,
  'create'  // 场景
)
// 返回 Element Plus 的 rules 数组

checkSchemaVisible

检查字段在指定模型下是否可见(根据 visible 条件)。

import { checkSchemaVisible } from '@sjlit/rest-ui'

const isVisible = checkSchemaVisible(schema, model)

clearSearchModel

清除搜索模型中的空值(''nullundefined)。

import { clearSearchModel } from '@sjlit/rest-ui'

const cleanQuery = clearSearchModel(searchModel, schemas)

组件 API

SchemaViewer

全自动 CRUD 组件,内部管理所有 HTTP 请求,一行代码完成完整页面。

Props

Prop 类型 默认值 说明
module string - 模块名
table string - 表名
title string '' 页面标题
apiPrefix string '' API 前缀,默认使用全局配置
config Partial<CRUDOptions> {} CRUD 配置覆盖
size string - 预留字段,当前未生效
formMode 'drawer' | 'dialog' 'dialog' 表单弹窗模式
showHeader boolean true 是否显示头部
showSearch boolean true 是否显示搜索栏
showToolbar boolean true 是否显示工具栏
showPagination boolean true 是否显示分页
readonly boolean false 只读模式(隐藏编辑/删除/创建按钮)
autoFetch boolean true 初始化时是否自动加载数据
rowActions Action[] [] 自定义行操作按钮
batchActions Action[] [] 自定义批量操作按钮
formActions Action[] [] 自定义表单操作按钮
searchActions Action[] [] 自定义搜索操作按钮
defaultSort string '' 默认排序字段,前缀 - 表示降序
presetQuery Record<string, any> {} 预设查询参数
gridProps Record<string, any> {} 传递给 SchemaGrid 的额外属性
formProps Record<string, any> {} 传递给 SchemaForm 的额外属性

Events

Event 参数 说明
ready (crud: CRUD) CRUD 实例初始化完成

Slots

Slot 参数 说明
searchform { model, schema } 自定义搜索表单字段
gridview { model, schema } 自定义表格列内容
crudform { model, schema } 自定义表单字段
headerleft - 自定义头部左侧
headerright - 自定义头部右侧

使用示例

<template>
  <SchemaViewer
    module="order"
    table="order"
    title="订单管理"
    :readonly="false"
    :autoFetch="true"
    defaultSort="-created_at"
    :presetQuery="{ status: 'pending' }"
    :rowActions="customRowActions"
    @ready="onReady"
  >
    <template #gridview="{ model, schema }">
      <span v-if="schema.column === 'status'" :class="`status-${model.status}`">
        {{ statusMap[model.status] }}
      </span>
    </template>
  </SchemaViewer>
</template>

<script setup lang="ts">
import { SchemaViewer } from '@sjlit/rest-ui'
import type { CRUD, Action } from '@sjlit/rest-ui'

const customRowActions: Action[] = [
  {
    name: 'detail',
    label: '详情',
    type: 'primary',
    callback: (model) => {
      console.log('View detail:', model)
    },
  },
]

function onReady(crud: CRUD) {
  console.log('CRUD ready:', crud.getSchemas())
}
</script>

SchemaPage

底层编排组件,组合搜索栏 + 表格 + 分页 + 弹窗/抽屉,不发送 HTTP 请求,所有数据流通过 props/events 控制。

Props

Prop 类型 默认值 说明
schemas Schema[] - Schema 定义数组
models Model[] - 数据列表
pagination Pagination { index: 1, size: 15, totalCount: 0 } 分页信息
loading boolean false 加载状态
size string - 预留字段,当前未生效
title string - 页面标题
formMode 'drawer' | 'dialog' 'dialog' 表单弹窗模式
showHeader boolean true 是否显示头部
showSearch boolean true 是否显示搜索栏
showToolbar boolean true 是否显示工具栏
showPagination boolean true 是否显示分页
readonly boolean false 只读模式
searchActions Action[] [] 搜索操作按钮
rowActions Action[] [] 行操作按钮
batchActions Action[] [] 批量操作按钮
formActions Action[] [] 表单操作按钮
gridProps Record<string, any> {} 传递给 SchemaGrid 的属性
formProps Record<string, any> {} 传递给 SchemaForm 的属性

Events

Event 参数 说明
search (model: Model) 搜索提交
create - 点击创建按钮
edit (model: Model) 点击编辑按钮
delete (model: Model) 点击删除按钮
pageChange (index: number) 分页切换
sortChange { column, order } 排序变化
selectionChange (selection: any[]) 选中项变化
formSubmit (model: Model, scenario: string) 表单提交

Slots

SchemaViewer 相同:searchformgridviewcrudformheaderleftheaderright

使用示例

<template>
  <SchemaPage
    :schemas="schemas"
    :models="models"
    :pagination="pagination"
    :loading="loading"
    title="手动数据流示例"
    @search="handleSearch"
    @pageChange="handlePageChange"
    @formSubmit="handleFormSubmit"
    @delete="handleDelete"
  />
</template>

SchemaGrid

响应式数据表格组件,桌面端使用 el-table,移动端(< 768px)自动切换为折叠面板视图。

Props

Prop 类型 默认值 说明
schemas Schema[] - Schema 定义数组
models Model[] - 数据列表
scenario string 'list' 场景
size ComponentSize - el-table 尺寸:large | default | small
selection boolean true 是否显示多选列
actions Action[] [] 行操作按钮
gridProps Record<string, any> {} 传递给 el-table 的属性
responsive boolean true 是否启用响应式
loading boolean false 加载状态

Events

Event 参数 说明
selection (selection: any[]) 多选变化
sort { column, order } 排序变化

Slots

Slot 参数 说明
default { model, schema } 自定义单元格内容

SchemaForm

Schema 驱动的表单组件,支持网格布局、行内布局、响应式断点和场景过滤。

Props

Prop 类型 默认值 说明
schemas Schema[] - Schema 定义数组
model Model undefined 初始模型数据
scenario string 'create' 场景
size string - 预留字段,当前未生效
labelWidth string '' 标签宽度
inline boolean false 行内表单模式
grid boolean false 网格布局模式
gridCols number 0 网格列数(0 表示自动)
actions Action[] [] 表单操作按钮
autoSubmit boolean false 挂载后自动提交

Events

Event 参数 说明
submit (model: Model, schemas: Schema[]) 表单验证通过并提交

Slots

Slot 参数 说明
container { model, schemas } 自定义整个表单容器
default { model, schema } 自定义单个表单字段

Expose

方法 返回 说明
submit Promise<Model> 手动触发提交和验证

CRUD 类

CRUD 类封装了完整的 RESTful HTTP 操作,包括 schema 加载、数据搜索、增删改查、导出等功能。

构造函数

const crud = new CRUD({
  module: 'user',
  table: 'admin',
  apiPrefix: 'rest',
  httpClient: axiosInstance,
  schemas: preloadedSchemas,  // 可选:预加载 schema
})

属性

属性 类型 说明
primaryKey string 主键字段名
schemas Schema[] 当前 schema 列表
models Model[] 当前数据列表
sortable Sortable | null 当前排序
queryParams Record<string, any> 查询参数
fixedQuery Record<string, any> 固定查询参数(每次请求自动附加)
pagination Pagination 分页信息
fieldErrors Record<string, string> 字段错误信息

方法

方法 参数 返回 说明
initialize() - Promise<Schema[]> 初始化:加载 schema、识别主键、拉取 live 数据
getSchemas() - Schema[] 获取 schema 列表
getModels() - Model[] 获取数据列表
setColumnError(column, error) (string, string) void 设置字段错误
resetError() - void 重置所有字段错误
getFieldErrors() - Record<string, string> 获取字段错误映射
setPaginationIndex(index) number this 设置当前页
getPaginationIndex() - number 获取当前页
setPaginationSize(size) number this 设置每页条数
getPaginationSize() - number 获取每页条数
getPaginationCount() - number 获取总条数
resetPagination() - this 重置到第一页
setSortable(column, order) (string, 'ascending' | 'descending') this 设置排序
addQueryParams(k, v) (string, any) void 添加查询参数
setQueryParams(qs) Record<string, any> this 设置查询参数
setFixedQuery(qs) Record<string, any> this 设置固定查询参数
findModelPrimaryKey(model) Model any 获取模型主键值
createModel(model) Model Promise<Model> 创建记录
updateModel(model) Model Promise<Model> 更新记录
deleteModel(model) Model | string Promise<any> 删除记录
getModel(qs) Record<string, any> | string Promise<Model> 获取单条记录详情
searchModel() - Promise<Model[]> 搜索记录
deleteModels(data) any[] Promise<{ total, success, responses }> 批量删除
exportModels() - Promise<void> 导出数据(自动下载 CSV)

URI 构建规则

CRUD 类使用 pluralize 库根据 moduletablescenario 自动构建 RESTful URI:

Scenario URI 示例(module=user, table=admin)
create /rest/user/admin
update /rest/user/admin/123
delete /rest/user/admin/123
get /rest/user/admin/detail/123
search /rest/user/admins
export /rest/user/admin/export

Schema 加载 URI:

  • 有 module:GET /rest/schema/{module}/{table}
  • 无 module:GET /rest/schema/{table}

进阶示例

自定义搜索表单

<template>
  <SchemaViewer module="order" table="order">
    <template #searchform="{ model, schema }">
      <el-date-picker
        v-if="schema.column === 'date_range'"
        v-model="model.date_range"
        type="daterange"
      />
    </template>
  </SchemaViewer>
</template>

自定义表格列

<template>
  <SchemaViewer module="user" table="user">
    <template #gridview="{ model, schema }">
      <el-avatar v-if="schema.column === 'avatar'" :src="model.avatar" />
      <el-tag v-else-if="schema.column === 'role'" :type="roleType(model.role)">
        {{ model.role }}
      </el-tag>
    </template>
  </SchemaViewer>
</template>

自定义表单字段

<template>
  <SchemaViewer module="article" table="article">
    <template #crudform="{ model, schema }">
      <RichEditor
        v-if="schema.column === 'content'"
        v-model="model.content"
      />
    </template>
  </SchemaViewer>
</template>

批量操作

<script setup lang="ts">
import { SchemaViewer } from '@sjlit/rest-ui'
import type { Action } from '@sjlit/rest-ui'

const batchActions: Action[] = [
  {
    name: 'batchDelete',
    label: '批量删除',
    type: 'danger',
    callback: (selections) => {
      console.log('Batch delete:', selections)
    },
  },
  {
    name: 'batchExport',
    label: '批量导出',
    callback: (selections) => {
      console.log('Batch export:', selections)
    },
  },
]
</script>

<template>
  <SchemaViewer module="user" table="user" :batchActions="batchActions" />
</template>

权限控制

<script setup lang="ts">
const rowActions: Action[] = [
  {
    name: 'edit',
    label: '编辑',
    type: 'success',
    permission: 'user:edit',
    callback: (model) => { /* ... */ },
  },
  {
    name: 'delete',
    label: '删除',
    type: 'danger',
    permission: 'user:delete',
    hidden: (model) => model.status === 'locked',
    callback: (model) => { /* ... */ },
  },
]
</script>

手动数据流(完全自定义)

<template>
  <SchemaPage
    :schemas="schemas"
    :models="models"
    :pagination="pagination"
    :loading="loading"
    :rowActions="rowActions"
    @search="onSearch"
    @pageChange="onPageChange"
    @formSubmit="onFormSubmit"
    @delete="onDelete"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { SchemaPage, CRUD } from '@sjlit/rest-ui'
import type { Schema, Model, Pagination, Action } from '@sjlit/rest-ui'

const schemas = ref<Schema[]>([])
const models = ref<Model[]>([])
const pagination = ref<Pagination>({ index: 1, size: 15, totalCount: 0 })
const loading = ref(false)

// 使用 CRUD 类辅助 URI 构建
const crud = new CRUD({
  module: 'custom',
  table: 'data',
  httpClient: { /* ... */ },
})

async function loadData() {
  loading.value = true
  await crud.initialize()
  const result = await crud.searchModel()
  models.value = result
  pagination.value = { ...crud.pagination }
  loading.value = false
}

function onSearch(model: Model) {
  crud.setQueryParams(model).resetPagination()
  loadData()
}

function onPageChange(index: number) {
  crud.setPaginationIndex(index)
  loadData()
}

function onFormSubmit(model: Model, scenario: string) {
  if (scenario === 'create') {
    crud.createModel(model).then(loadData)
  } else {
    crud.updateModel(model).then(loadData)
  }
}

function onDelete(model: Model) {
  crud.deleteModel(model).then(loadData)
}

const rowActions: Action[] = [
  { name: 'detail', label: '详情', callback: (model) => openDetail(model) },
]
</script>

固定查询条件

<script setup lang="ts">
import { SchemaViewer } from '@sjlit/rest-ui'
import { onMounted, ref } from 'vue'
import type { CRUD } from '@sjlit/rest-ui'

const viewerRef = ref<InstanceType<typeof SchemaViewer> | null>(null)

function onReady(crud: CRUD) {
  // 设置固定查询条件:只显示未删除的数据
  crud.setFixedQuery({ deleted_at: null })
  crud.searchModel()
}
</script>

<template>
  <SchemaViewer
    module="article"
    table="article"
    :autoFetch="false"
    @ready="onReady"
  />
</template>

动态切换 module/table

<template>
  <SchemaViewer :module="currentModule" :table="currentTable" />
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'
import { SchemaViewer } from '@sjlit/rest-ui'

const currentModule = ref('user')
const currentTable = ref('admin')

// 切换时会自动重新初始化
function switchTable(module: string, table: string) {
  currentModule.value = module
  currentTable.value = table
}
</script>

与 REST 后端对接

Schema 接口约定

后端需要提供以下接口返回 schema 定义:

GET /{apiPrefix}/schema/{module}/{table}
GET /{apiPrefix}/schema/{table}

响应格式:

[
  {
    "id": 1,
    "module_name": "user",
    "table_name": "admin",
    "column": "username",
    "label": "用户名",
    "type": "string",
    "format": "text",
    "primary_key": 0,
    "scenarios": ["create", "update", "search", "list"],
    "rules": {
      "min": 0,
      "max": 50,
      "type": "string",
      "unique": true,
      "required": ["create", "update"]
    },
    "attributes": {
      "match": "fuzzy",
      "readonly": [],
      "disable": [],
      "visible": [],
      "invisible": false,
      "sort": true,
      "values": [],
      "live": { "enable": false, "type": "" }
    }
  }
]

搜索接口约定

GET /{apiPrefix}/{module}/{pluralTable}?page=1&pagesize=15&sort=-created_at&__format=both

响应格式:

{
  "page": "1",
  "page_size": "15",
  "total_count": "100",
  "data": [{ ... }]
}

Live 数据加载

当 schema 的 attributes.live.enabletrue 时,CRUD 类会自动请求 live.url 加载下拉选项:

{
  "attributes": {
    "live": {
      "enable": true,
      "type": "dropdown",
      "url": "/rest/user/roles",
      "method": "GET"
    }
  }
}

开发指南

本地开发

cd rest-ui
npm install
npm run dev    # 监听模式构建
npm test       # 运行单元测试(node:test,无额外依赖)

在独立 Vue 项目中本地引用

如果你有一个独立的 Vue 前端项目想要在开发时引用本库,推荐以下两种方式:

方式一:Vite Alias 指向源码(推荐,支持热更新)

在消费项目的 vite.config.ts 中配置路径别名,直接指向 @sjlit/rest-ui 的源码入口:

import { defineConfig } from 'vite'
import { resolve } from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@sjlit/rest-ui': resolve(__dirname, '/absolute/path/to/rest-ui/src/index.ts'),
    },
  },
})

步骤

  1. 在消费项目中安装本库的 peerDependencies
    npm install vue@^3.3.0 element-plus@^2.12.0 @element-plus/icons-vue@^2.3.0
  2. vite.config.ts 添加上述 alias 配置(路径替换为你本地的实际绝对路径)。
  3. 启动消费项目,修改 rest-ui 源码即可实时热更新。

注意:此方式要求消费项目具备编译 .vue 单文件组件的能力(已安装 @vitejs/plugin-vue)。

方式二:本地路径安装 + Watch 构建

如果你不方便修改消费项目的 Vite 配置,可以通过 file: 协议直接安装本地包:

# 在消费项目中执行
npm install /absolute/path/to/rest-ui

package.json 会自动添加:

"dependencies": {
  "@sjlit/rest-ui": "file:/absolute/path/to/rest-ui"
}

步骤

  1. 在消费项目中执行上述 npm install 命令。
  2. 确保消费项目已安装 peerDependenciesvueelement-plus@element-plus/icons-vue)。
  3. rest-ui 目录启动监听构建:
    cd rest-ui
    npm run dev   # vite build --watch,自动更新 dist/
  4. 消费项目刷新页面即可获得最新构建产物。

提示npm run dev 会监听源码变化并自动重新打包到 dist/。由于 file: 链接指向包的根目录,引用的是 dist/ 下的构建产物,因此必须保持 watch 构建处于运行状态。

构建

npm run build  # 输出到 dist/ 目录

输出文件:

  • dist/rest-ui.es.js — ESM 格式
  • dist/rest-ui.cjs — CJS 格式
  • dist/style.css — 组件样式(自动随 JS 注入,也可单独引用)
  • dist/index.d.ts — TypeScript 类型声明

单独引用样式(可选;组件样式默认已随 JS 自动注入):

import '@sjlit/rest-ui/style.css'
// 或
import '@sjlit/rest-ui/dist/style.css'

类型检查

npm run typecheck  # 使用 vue-tsc

发布流程

本仓库通过 GitHub Actions 在打 tag 时自动发布到 npm。配置要求:

  • 仓库 Secret NPM_TOKEN(拥有 @sjlit 组织 publish 权限的 npm automation token)

正式版本

npm run release:patch   # 1.0.1 → 1.0.2
# 或 release:minor / release:major
npm run push:tags       # 推送代码 + tag,自动触发发布

预发布版本

npm run release:beta    # 1.0.2 → 1.0.3-beta.0
npm run push:tags       # 推送后自动以 --tag beta 发布

完整流水线(install → typecheck → test → build → pack --dry-run → publish)在 .github/workflows/publish.yml 中定义,任何步骤失败都会阻止破损版本发布。 也可以在 GitHub Actions 页面手动触发发版。


类型声明

本库使用 TypeScript 编写,提供完整的类型声明。主要导出类型:

import type {
  Schema,
  SchemaRule,
  SchemaAttribute,
  EnumValue,
  VisibleCondition,
  LiveValue,
  DropdownOptions,
  Relation,
  Model,
  Action,
  Pagination,
  Sortable,
  CRUDOptions,
  SchemaUIConfig,
  // 从 element-plus 透传的联合类型
  ButtonType,
  ComponentSize,
} from '@sjlit/rest-ui'

更新日志

1.0.0

  • 初始版本发布
  • Schema 驱动的 CRUD 组件库
  • Vue 3 + Element Plus + TypeScript 支持
  • 全自动 CRUD(SchemaViewer)和手动编排(SchemaPage)两种模式
  • CRUD 类封装完整的 RESTful HTTP 操作
  • 响应式移动端适配
  • 插件化配置系统