- Notifications
You must be signed in to change notification settings - Fork3
Admin CRUD frontend meta framework for Vue 2/3, extremely concise without losing flexibility.
License
cloydlau/admate
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
中后台 CRUD 前端元框架,极致简洁的基础上不向灵活性妥协,
致力于攻克「页面重复度高,提取公共代码却难以兼顾定制化需求」的痛点。
- Vue 2.6/2.7/3 一体通用
- 符合直觉的行为抽象
- 贴心却不武断的数据与状态管理
- 保姆级收尾工作,无后顾之忧
中后台模板如vue-pure-admin、vue-vben-admin、yudao-ui-admin-vue3 等的定位是中后台一整套解决方案,跟 UI 框架、CSS 框架高度耦合。
视图层面的技术百家争鸣、日新月异,但 CRUD 逻辑是亘古不变的,这正是 Admate 的发力点。
Admate 专注逻辑层面,轻量化、侵入性低,可独立使用,也支持跟任意中后台模板搭配使用。
npm i admate
- vue
@vue/composition-api:仅 Vue 2.6 或更早版本需要
建立全局适配层
示例:src/utils/useAdmateAdapter.js
- 量身打造生命周期行为
- 列表筛选参数重置 & 参数校验
- 支持 URL 传参指定筛选项默认值
- 支持动态生成筛选项默认值,使用场景举例:日期/时间类的参数,如果其默认值为当前最新时刻,重置筛选项时会重置到已过期的时刻
在页面中导入使用
按模块拆分适配层,每个模块拥有自己的适配层
比如:
- src/views/system/useAdmateAdapter.js
- src/views/infra/useAdmateAdapter.js
在页面中导入对应模块的适配层
const{ list, listFilterRef, form, faFormDialogRef}=useAdmateAdapter({// ...Admate 配置},{// ...Admate 适配层配置})
示例:接口级请求配置
useAdmate({// Axios 或 Axios 实例// 用于调用接口 axios,})
useAdmate({// Axios 配置axiosConfig:{// 各接口的 URL 前缀urlPrefix:`${import.meta.env.VITE_BASE_URL}/module`,// 列表相关接口list:{// 读取列表read:{},},// 表单相关接口form:{// 新增create:{},// 读取read:{},// 编辑update:{},// 删除delete:{},// 切换状态switch:{},},},})
// 示例: URL 前缀不统一useAdmate({axiosConfig:{urlPrefix:'module1',list:{read:{// 如果某个接口的前缀不是 'somepage',可以在 URL 前面加斜线,即可忽略该前缀url:'/module2/selectOne',}}}})
// src/http/index.jsconstaxiosInstance=axios.create({headers:{xxx:'xxx',},})
constheaders={xxx:'xxx',}const{ list, listFilterRef, form, faFormDialogRef}=useAdmateAdapter({axiosConfig:{ urlPrefix,list:{read:{url:'page', headers,},},form:{create:{url:'create', headers,},read:{url:'get', headers,},update:{url:'update', headers,},delete:{url:'delete', headers,},},},})
const{ list, listFilterRef, form, faFormDialogRef}=useAdmateAdapter({axiosConfig:{ urlPrefix,form:{create:{url:'create',headers:{xxx:'xxx',},},},},})
如果接口地址需要进行动态拼接
// 配置const{ list, form}=useAdmate({axiosConfig:{urlPrefix:`${import.meta.env.VITE_BASE_URL}/module`,form:{read:({ id})=>({method:'GET',url:id,}),update:({ id})=>({method:'PUT',url:id,}),delete:({ id})=>({method:'DELETE',url:id,}),switch:({ id})=>({method:'PUT',url:id,}),},}})// 使用form.open({id:1},'config')form.read({id:1},'config')form.update({id:1},'config')form.delete({id:1},'config')form.switch({id:1},'config')
Axios
的 data 默认以application/json
作为 MIME type,如果你需要使用multipart/form-data
:
- 全局配置
给你的Axios
配置transformRequest
、headers['Content-Type']
- 局部配置
list.read
、list.search
、list.reset
、form.open
、form.delete
、form.switch
、form.submit
的参数 1 均支持 FormData 类型
<!-- 示例: 局部配置 --><script setup>import useAdmateAdapter from '@/utils/useAdmateAdapter'// 过滤 list.value.filter 并转换为 FormData 格式FormData.from = (json) => { const formData = new FormData() for (const k in json) { if (![Number.NaN, null, undefined].includes(json[k])) { formData.append(k, json[k]) } } return formData}useAdmateAdapter({ list: { proxy: { read(readList, trigger) { readList(FormData.from(list.value.filter)) }, } }})const FormData = window.FormData</script><template> <el-table> <el-table-column label="操作"> <template #default="{ row: { id } }"> <el-button @click="form.read(FormData.from({ id }))"> 查看 </el-button> <el-button @click="form.update(FormData.from({ id }))"> 编辑 </el-button> <el-button @click="form.delete(FormData.from({ id }))"> 删除 </el-button> </template> </el-table-column> </el-table> <el-dialog> <template #footer> <el-button @click="() => form.submit(FormData.from(form.data))"> 确 定 </el-button> </template> </el-dialog></template>
list.filter
useAdmate({list:{// 页码的参数名称或路径,必填// 支持属性名,如 `'pageNo'`// 支持属性路径,如 `'page.pageNo'`pageNumberAt:undefined,// 可以在这里提供筛选参数的默认值filter:{'pageNumberAt 生成的页码参数名称':1},},})
- 点击专用的读取按钮触发
- ✗ 操作相对繁琐。
- ✗ 列表数据与筛选条件可能是无关的。可能产生 “当前的列表数据是否基于筛选项?” 的顾虑,导致徒增点击读取按钮的次数。
- ✓ 想同时设置多个筛选条件时,只调用一次接口,不浪费服务器资源。
useAdmate({list:{watchFilter:false,}})
- 改变筛选条件后即时触发
- ✓ 操作相对简便。
- ✓ 列表数据与筛选条件即时绑定。
- ✓
想同时设置多个筛选条件时,接口会被多次调用,浪费服务器资源(Admate 已优化)。
useAdmate({list:{watchFilter:true,// 默认值// 防抖间隔,单位毫秒// 如果筛选参数不含 input 类型,可以设置为 0,即不防抖// 翻页不会触发防抖// watchFilter 开启时有效debounce:300,// 默认值}})
list.data
useAdmate({list:{// 列表数据data:[],// 指定接口返回值中列表数据的位置// 支持属性名,如 `'data'`// 支持属性路径,如 `'data[0].records'`// 支持 symbol 类型的属性名// 支持 Function,如 `response => response.data`dataAt:undefined,// 指定接口返回值中记录总数的位置// 支持属性名,如 `'total'`// 支持属性路径,如 `'data[0].total'`// 支持 symbol 类型的属性名// 支持 Function,如 `response => response.total`totalAt:undefined,}})
读取列表,在首次进入页面、列表筛选参数改变、单条记录增删查改后会被调用
const{ list}=useAdmate()/** * PS: 以下为原始函数签名,如果你配置了 list.proxy.read ,则以 list.proxy.read 为准 * *@param {any} [payload = list.filter] *@param {'data'|'params'|'config'} [payloadAs] 指定 payload 的用途 *@returns {Promise<any>} 接口返回值 */list.read()// 手动读取
重置页码后执行list.read
,用于筛选条件改变后检索列表
const{ list}=useAdmate()/** * PS: 以下为原始函数签名,如果你配置了 list.proxy.read ,则以 list.proxy.read 为准 * *@param {any} [payload = list.filter] *@param {'data'|'params'|'config'} [payloadAs] 指定 payload 的用途 *@returns {Promise<any>} 接口返回值 */list.search()// 手动检索
重置筛选条件后执行list.read
const{ list}=useAdmate()/** * PS: 以下为原始函数签名,如果你配置了 list.proxy.reset ,则以 list.proxy.reset 为准 * *@param {any} [payload = list.filter] *@param {'data'|'params'|'config'} [payloadAs] 指定 payload 的用途 *@returns {Promise<any>} 接口返回值 */list.reset()// 手动重置
你可以使用list.proxy.read
来代理list.read
,以便在list.read
前后执行一些操作,或改变list.read
的行为
useAdmate({list:{proxy:{/** *@param {Function} readList 被代理的原始 readList *@param {string} trigger 调用动机 可能的值: * 'immediate': 初始化立即查询列表 * 'pageNumberChange': 页码改变触发查询列表 * 'filterChange': 筛选项(含分页大小)改变触发查询列表 * 'create': 表单新增触发查询列表 * 'update': 表单编辑触发查询列表 * 'delete': 表单删除触发查询列表 * 'switch': 表单状态变更触发查询列表 */read(readList,trigger){},},},})
// 示例: 读取列表之前,校验参数useAdmate({list:{proxy:{read(readList,trigger){if(trigger==='filterChange'){listFilterRef.value.validate().then(()=>{readList()})}else{readList()}},}}})
// 示例: 单条记录操作成功后,弹出提示useAdmate({list:{proxy:{read(readList,trigger){readList()if(['create','upadte','delete','switch'].includes(trigger)){currentInstance.value.$message.success('操作成功')}},}}})
// 示例: 读取列表后,修改列表数据const{ list}=useAdmate({list:{proxy:{read(readList,trigger){readList().then((response)=>{// response 为 axiosConfig.list.read 的接口返回值list.data=response.data?.filter(v=>!v.disabled)})},}}})
你可以使用list.proxy.reset
来代理list.reset
,以便在list.reset
前后执行一些操作,或改变list.reset
的行为
useAdmate({list:{proxy:{/** *@param {Function} resetList 被代理的原始 resetList */reset(resetList){},},},})
// 示例: 使用 UI 组件库的表单重置函数来重置筛选条件useAdmate({list:{proxy:{reset(resetList){listFilterElFormRef.value.resetFields()// 如果分页组件不归属于表单,则表单重置时页码不会被重置,需调用 list.searchif(!list.watchFilter){list.search()}},}}})
list.loading
axiosConfig.list.read
被调用时值为true
,否则为false
<!-- 示例 --><script setup>import useAdmate from 'admate'import { getCurrentInstance } from 'vue'const { proxy } = getCurrentInstance()const { list } = useAdmate()function handleTable() { list.value.loading = true proxy.$POST('').finally(() => { list.value.loading = false })}</script><template> <el-table v-loading="list.loading" /></template>
表单默认是对话框/抽屉的风格,但也支持独立页面的风格
对比
- 对话框/抽屉:体验好,割裂感低,表单的开闭不影响父页面状态
- 独立页面:体验较差,从表单返回父页面时,父页面的状态会丢失,比如列表筛选状态
form.show: boolean
Tip
表单关闭时,表单数据自动恢复至初始状态(不是直接清空)
form.data
useAdmate({form:{// 可以在这里提供表单数据的默认值data:{},// 在查看、编辑表单时,可能需要调用接口(axiosConfig.form.read)回显表单的数据// dataAt 用于指定接口返回值中表单数据的位置// 支持属性名,如 `'detail'`// 支持属性路径,如 `'data[0].detail'`// 支持 symbol 类型的属性名// 支持 Function,如 `response => response.detail`dataAt:undefined,// 接口(axiosConfig.form.read)返回值与 form.data 合并的方式mergeData:'deep',},})
mergeData:
'deep'
:深合并 (默认)'shallow'
:浅合并(newFormData: any) => any
:自定义合并方式false
:不合并,直接替换
为什么默认是深合并?
在 Vue 2 中,template 不支持?.
语法,要在 template 中判空,代码写起来会非常冗余,通常的做法是在 data 中声明空对象
比如给 form.data 提供默认值:
<script setup>importuseAdmatefrom'admate'const {form }=useAdmate({ form: { data: { a: { b: {} } } }})</script><template> {{ form.data.a.b.c }}</template>
如果 axiosConfig.form.read 的返回值为:{ a: {} }
如果与默认值浅合并后将得到:{ a: {} }
—— 默认值中的对象 b 丢失了,引发空指针异常。
如果与默认值深合并后将得到:{ a: { b: {} } }
—— 代码正常工作。
// 示例: 自定义合并方式import{mergeWith}from'lodash'functiondefaultFormData(){return{a:{b:{}}}}const{ form}=useAdmate({form:{data:defaultFormData(),// 接口返回值中嵌套的对象可能为 null,会覆盖默认值中的空对象mergeData(// 接口返回值在通过 form.dataAt 计算过后的值newFormData){// Vue 3 中不需要赋值,mergeWith 的改动是响应式的form.data=mergeWith(defaultFormData(),newFormData,(oldObj,newObj)=>[undefined,null].includes(newObj) ?oldObj :undefined)},},})
form.status: 'create' | 'read' | 'update'
form.title: string
// 示例: 根据表态形态生成对应的标题import{computed}from'vue'const{ form}=useAdmate({form:{title:computed(()=>({create:'新增',read:'查看',update:'编辑'}[form.status])),},})
打开表单,提交时会调用axiosConfig.form.create
const{ form}=useAdmate()form.create()// 等价于:将表单形态设置为“新增”,然后打开表单form.status='create'form.open()
表单的初始数据不是空白,而是复制一条已有的记录
- 打开表单时,和查看/编辑一样,需要调接口回显
- 提交表单时调用的是新增的接口
const{ form}=useAdmate()form.create(row)// 等价于:将表单形态设置为“新增”,然后打开表单并传参form.status='create'form.open(row)
打开表单,并调用axiosConfig.form.read
回显表单内容
const{ form}=useAdmate()form.read()// 等价于:将表单形态设置为“查看”,然后打开表单form.status='read'/** * PS: 以下为原始 openForm 的函数签名,如果你配置了 form.proxy.open ,则以 form.proxy.open 为准 * *@param {any} [payload] 如果 payload 不为空,则会调用 axiosConfig.form.read *@param {'data'|'params'|'config'|'cache'} [payloadAs] 指定 payload 的用途 * 'data': 将 payload 用作请求配置的 `data` 参数(请求方式为 POST / PATCH / PUT / DELETE 时默认) * 'params': 将 payload 用作请求配置的 `params` 参数(请求方式为 GET / HEAD 时默认) * 'config': 将 payload 仅用于构建请求配置(详见 RESTful 章节) * 'cache': 将 payload 直接用作表单数据(不调用读取单条记录的接口) *@returns {Promise<any>} axiosConfig.form.read 的返回值 */form.open()
打开表单,并调用axiosConfig.form.read
回显表单内容,提交时会调用axiosConfig.form.update
const{ form}=useAdmate()form.update()// 等价于:将表单形态设置为“编辑”,然后打开表单form.status='update'/** * PS: 以下为原始 form.open 的函数签名,如果你配置了 form.proxy.open ,则以 form.proxy.open 为准 * *@param {any} [payload] 如果 payload 不为空,则会调用 axiosConfig.form.read *@param {'data'|'params'|'config'|'cache'} [payloadAs] 指定 payload 的用途 * 'data': 将 payload 用作请求配置的 `data` 参数(请求方式为 POST / PATCH / PUT / DELETE 时默认) * 'params': 将 payload 用作请求配置的 `params` 参数(请求方式为 GET / HEAD 时默认) * 'config': 将 payload 仅用于构建请求配置(详见 RESTful 章节) * 'cache': 将 payload 直接用作表单数据(不调用读取单条记录的接口) *@returns {Promise<any>} axiosConfig.form.read 的返回值 */form.open()
const{ form}=useAdmate()/** *@param {any} [payload] *@param {'data'|'params'|'config'} [payloadAs] 指定 payload 的用途 * 'data': 将 payload 用作请求配置的 `data` 参数(请求方式为 POST / PATCH / PUT / DELETE 时默认) * 'params': 将 payload 用作请求配置的 `params` 参数(请求方式为 GET / HEAD 时默认) * 'config': 将 payload 仅用于构建请求配置(详见 RESTful 章节) *@returns {Promise<any>} axiosConfig.form.delete 的返回值 */form.delete()
Tip
删除非首页最后一条记录时,自动切换至上一页
状态变更有三种方式:
- 后端提供一个统一的接口,传参指定新的状态
<script setup>importuseAdmatefrom'admate'const {form }=useAdmate()/** *@param{any}[payload] *@param{'data'|'params'|'config'}[payloadAs] 指定 payload 的用途 * 'data': 将 payload 用作请求配置的 `data` 参数(请求方式为 POST / PATCH / PUT / DELETE 时默认) * 'params': 将 payload 用作请求配置的 `params` 参数(请求方式为 GET / HEAD 时默认) * 'config': 将 payload 仅用于构建请求配置(详见 RESTful 章节) *@returns{Promise<any>} axiosConfig.form.switch 的返回值*/form.switch()</script><template> <el-table> <el-table-columnlabel="操作"align="center" > <template #default="{ row: {id,status } }"> <el-switch@change="form.switch({ id, status: status ^ 1 })" /> </template> </el-table-column> </el-table></template>
- 后端提供启用和停用两个接口
<script setup>importuseAdmatefrom'admate'const {form }=useAdmate({ axiosConfig: { form: {switch: ({ id, status })=> ({ method:'PUT', url:`${status===1?'enable':`disable`}/${id}`, }), } },})/** *@param{any}[payload] *@param{'data'|'params'|'config'}[payloadAs] 指定 payload 的用途 * 'data': 将 payload 用作请求配置的 `data` 参数(请求方式为 POST / PATCH / PUT / DELETE 时默认) * 'params': 将 payload 用作请求配置的 `params` 参数(请求方式为 GET / HEAD 时默认) * 'config': 将 payload 仅用于构建请求配置(详见 RESTful 章节) *@returns{Promise<any>} axiosConfig.form.switch 的返回值*/form.switch()</script><template> <el-table> <el-table-columnlabel="操作"align="center" > <template #default="{ row: {id,status } }"> <el-switch@change="form.switch({ id, status: status ^ 1 }, 'config')" /> </template> </el-table-column> </el-table></template>
- 后端未提供独立的接口,使用编辑接口改变状态
<script setup>importuseAdmatefrom'admate'const {form }=useAdmate({ axiosConfig: { form: { update: {// ... },switch: {// 按编辑接口进行配置 }, } },})/** *@param{any}[payload] *@param{'data'|'params'|'config'}[payloadAs] 指定 payload 的用途 * 'data': 将 payload 用作请求配置的 `data` 参数(请求方式为 POST / PATCH / PUT / DELETE 时默 认) * 'params': 将 payload 用作请求配置的 `params` 参数(请求方式为 GET / HEAD 时默认) * 'config': 将 payload 仅用于构建请求配置(详见 RESTful 章节) *@returns{Promise<any>} axiosConfig.form.switch 的返回值*/form.switch()</script><template> <el-table> <el-table-columnlabel="操作"align="center" > <template #default="{row }"> <el-switch@change="form.switch({ ...row, status: row.status ^ 1 })" /> </template> </el-table-column> </el-table></template>
打开表单,函数签名要分情况:
你可以使用form.proxy.open
来代理form.open
,以便在form.open
前后执行一些操作,或改变form.open
的行为
useAdmate({form:{proxy:{/** *@param {Function} openForm 被代理的原始 openForm *@returns {Promise<object> | object | void} object 为打开表单后 form 的终态 */open(openForm){},}}})
// 示例: 回显表单后,修改表单数据const{ form}=useAdmate({form:{proxy:{open(openForm){// 新增时 openForm 没有返回值returnnewPromise((resolve,reject)=>{openForm()?.then((response)=>{// response 为 axiosConfig.r 的接口返回值// 修改表单数据form.data.status=1resolve()}).catch((e)=>{reject(e)})})},}}})
// 示例: 回显表单后,清除校验useAdmate({form:{proxy:{open(openForm){returnnewPromise((resolve,reject)=>{openForm()?.finally(()=>{formRef.value.clearValidate()}).then(()=>{resolve()}).catch((e)=>{reject(e)})})},}}})
// 示例: 回显表单后,自定义表单的开闭和读取状态useAdmate({form:{proxy:{open(openForm){returnnewPromise((resolve,reject)=>{// 可以在 finally 中 resolveopenForm().then(()=>{// 回显成功后,默认停止加载resolve({loading:false,})}).catch(()=>{// 回显失败后,默认关闭表单并停止加载resolve({show:false,loading:false,})})})}}}})// 也可以返回一个对象(如果没有异步操作)useAdmate({form:{proxy:{open(openForm){return{loading:false}}}}})
form.loading
axiosConfig.form.read
被调用时值为true
,否则为false
不能将该值当作表单回显结束的标志,因为复用列表数据时不会调用 axiosConfig.r
<!-- 示例 --><script setup>import useAdmate from 'admate'const { form } = useAdmate()</script><template> <el-dialog> <el-form v-loading="form.loading" /> </el-dialog></template>
提交表单,新增时调用axiosConfig.form.create
,编辑时调用axiosConfig.form.update
const{ form}=useAdmate()/** * PS: 以下为原始 form.submit 的函数签名,如果你配置了 form.proxy.submit ,则以 form.proxy.submit 为准 * *@param {any} [payload = form.data] *@param {'data'|'params'|'config'} [payloadAs] 指定 payload 的用途 *@returns {Promise<any>} 接口返回值 */form.submit()
你可以使用form.proxy.submit
来代理form.submit
,以便在form.submit
前后执行一些操作,或改变form.submit
的行为
useAdmate({form:{proxy:{/** *@param {Function} submitForm 被代理的原始 submitForm *@returns {Promise<object> | object | void} object 为提交表单后 form 的终态 */submit(submitForm){}}}})
// 示例: 指定提交参数form.submit({ ...form.data,status:1,})// form.submit 被代理时useAdmate({form:{proxy:{submit(submitForm){returnnewPromise((resolve,reject)=>{submitForm({ ...form.data,status:1,}).then(()=>{resolve()}).catch((e)=>{reject(e)})})}}}})
// 示例: 提交前校验表单useAdmate({form:{proxy:{submit(submitForm){returnnewPromise((resolve,reject)=>{formRef.value.validate().then(()=>{submitForm().then(()=>{resolve()}).catch((e)=>{reject(e)})})})}}}})
// 示例: 提交表单后,自定义表单的开闭和提交状态// 返回一个 promiseuseAdmate({form:{proxy:{submit(submitForm){returnnewPromise((resolve,reject)=>{formRef.value.validate().then(()=>{submitForm().then(()=>{// 提交成功后,默认关闭表单,并停止加载resolve({show:false,submitting:false,})}).catch(()=>{// 提交失败后,默认仅停止加载resolve({show:true,submitting:false,})})})})}}}})// 也可以返回一个对象(如果没有异步操作)useAdmate({form:{proxy:{submit(submitForm){return{show:false,submitting:false,}}}}})
form.submitting
axiosConfig.form.create
或axiosConfig.form.update
被调用时值为true
,否则为false
<!-- 示例 --><script setup>import useAdmate from 'admate'const { form } = useAdmate()</script><template> <el-dialog> <template #footer> <el-button :loading="form.submitting"> 确 定 </el-button> </template> </el-dialog></template>
useAdmateAdapter({},{onListRead(res,trigger){// res 为接口返回值,trigger 为调用动机// 可访问 this(组件实例)}})
- 读取表单前
watch(()=>form.value.show,(n)=>{if(n){// 打开表单}})
- 读取表单后
// 示例: 适配层提供 onFormOpeneduseAdmateAdapter({},{onFormOpened(res){// res 为接口返回值(新增时为空)// 可访问 this(组件实例)}})
- 读取表单后 (不含新增)
// 示例: 适配层提供 onFormReaduseAdmateAdapter({},{onFormRead(res){// res 为接口返回值// 可访问 this(组件实例)}})
- 提交表单前
useAdmateAdapter({},{onFormSubmit(form){// 可访问 this(组件实例)}})
- 提交表单后
useAdmateAdapter({},{onFormSubmitted(res){// res 为接口返回值// 可访问 this(组件实例)}})
watch(()=>form.value.show,(n)=>{if(!n){// 关闭表单}})
将表单抽离为子组件
操作单条记录时,跳转到专用的表单页面,操作完毕后返回
表单默认打开,且无法关闭,通常用于列表中只有一条数据,故列表被省略的场景
当前页面的对话框也使用 Admate
各版本详细改动请参考release notes
About
Admin CRUD frontend meta framework for Vue 2/3, extremely concise without losing flexibility.