@sjlit/rest-ui
v1.1.5
Published
Schema-driven UI components for REST projects
Downloads
1,904
Readme
@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 页面
- 全自动 CRUD:
SchemaViewer组件一行代码完成完整的增删改查页面 - 手动控制:
SchemaPage组件提供底层编排,外部控制数据流 - 响应式设计:
SchemaGrid自动适配移动端(折叠面板)和桌面端(表格) - 内置样式体系:基于 Element Plus CSS 变量的自适应阴影、间距系统和响应式断点
- 插件化配置:通过 Vue Plugin 全局注入 HTTP 客户端、权限、路由、国际化
- 类型安全:完整的 TypeScript 类型定义,与 REST Go 结构体对齐
- 高度可扩展:丰富的插槽系统支持自定义搜索表单、表格列、表单字段
安装
npm install @sjlit/rest-uiPeer 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-components 的 ElementPlusResolver 内部路径完全
一致,可被 vite / rollup / webpack 正确 treeshake。消费方无需任何
配置改动,升级版本即可生效(前提是消费方自己没额外 import ElementPlus
或 import '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 注入,所有字段均为可选,但 httpClient 是 SchemaViewer 必需的。
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| httpClient | { get, post, put, delete } | 是 | HTTP 客户端,用于 SchemaViewer 和 CRUD 类 |
| 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') // falseencode / 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')转换规则:
- encode:
datetime/date/timestamp/time格式的Date对象转为YYYY-MM-DD HH:mm:ss格式字符串 - decode:
integer类型的字符串转为整数;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
清除搜索模型中的空值(''、null、undefined)。
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 相同:searchform、gridview、crudform、headerleft、headerright。
使用示例
<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 库根据 module、table 和 scenario 自动构建 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.enable 为 true 时,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'),
},
},
})步骤:
- 在消费项目中安装本库的
peerDependencies:npm install vue@^3.3.0 element-plus@^2.12.0 @element-plus/icons-vue@^2.3.0 - 在
vite.config.ts添加上述alias配置(路径替换为你本地的实际绝对路径)。 - 启动消费项目,修改
rest-ui源码即可实时热更新。
注意:此方式要求消费项目具备编译
.vue单文件组件的能力(已安装@vitejs/plugin-vue)。
方式二:本地路径安装 + Watch 构建
如果你不方便修改消费项目的 Vite 配置,可以通过 file: 协议直接安装本地包:
# 在消费项目中执行
npm install /absolute/path/to/rest-uipackage.json 会自动添加:
"dependencies": {
"@sjlit/rest-ui": "file:/absolute/path/to/rest-ui"
}步骤:
- 在消费项目中执行上述
npm install命令。 - 确保消费项目已安装
peerDependencies(vue、element-plus、@element-plus/icons-vue)。 - 在
rest-ui目录启动监听构建:cd rest-ui npm run dev # vite build --watch,自动更新 dist/ - 消费项目刷新页面即可获得最新构建产物。
提示:
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 操作
- 响应式移动端适配
- 插件化配置系统
