前言
后端篇的时候已经对字典模块进行了设计,相应的接口也已经完成。在前后端未分离的情况下,因为页面是由服务端渲染的,所以一般都会自定义一个字典标签用于对字典数据的取值、渲染。该种情况下,服务端很方便地对字典做缓存处理。前后端分离后,前端与后端都是通过接口进行交互的,所以维护字典的方式也会有所区别。
字典组件设计
我们简单的分析一下字典组件应该具备的功能。
先分析使用场景
在后台管理中,常见的使用字典的场景有三个:
- 添加/修改表单,下拉选择
- 列表页,由值转为显示名称
- 搜索表单,下拉选择
使用场景不同,对于前端来说,其实就是呈现方式的不同。所以我们在做组件的时候,可以先默认按场景分三种布局。
关于缓存的思路
缓存的思路有很多种,这里简单讲一下:
-
系统登录后,一次性返回所有的字典数据,缓存在本地的cookies或vuex上;
优点:减轻服务器压力
缺点:一次性返回,字典量多的话,可能会影响体验
-
不进行缓存,每次都调用接口获取数据;
优点:无
缺点:频繁请求,页面中字典多的话,影响体验
-
使用vuex,基于dictKey进行缓存,保证在同一个vue实例下,同一个key,只调用一次接口。
方案三是本框架采用的方式,也不能说是最优的。但是相对而已,可能会比前两个方案会好一些。当然,除了这三个方案,肯定还有别的方案,这里就不讨论了。
组件参数说明
暂时定几个常用的参数,后续可能还会有追加
参数名 | 类型 | 默认值 | 说明 |
---|---|---|---|
dictKey | String | undefined | 字典唯一编码(表名_字段名) |
type | String | enum | 字典类型(enum->枚举类字典类型,db->数据库字典类型,local->本地字典类型) |
value | String, Number | undefined | 绑定的值 |
size | String | medium | 对应el-select的size,medium/small/mini |
mode | String | form | form->普通表单,list->列表页,searchForm->搜索表单 |
接口说明
先简单说一下后端提供的接口
请求地址:
{{api_base_url}}/sys/dict/getByDictKey
数据类型:
application/json
请求示例:
{
"dictKey": "sys_role_role_type",
"type": "enum"
}
响应示例:
{
"code": 0, // 返回状态码0,成功
"msg": "通过字典唯一编码查询成功", // 消息描述
"data": {
"name": "角色类型",
"dictKey": "sys_role_role_type", // 字典唯一编码
"items": [{
"name": "管理员",
"dictItemValue": 10
}, {
"name": "流程审核员",
"dictItemValue": 20
}]
}
}
开始编码
目录结构
├── src
├── components/m
├── Dict
└── index.vue
├── store
├── modules
└── dict.js
├── getters.js
└── index.js
├── views
├── dashboard
└── index.vue
└── main.js
文件详解
src/components/m/Dict/index.vue
字典组件
<template>
<div class="m-dict">
<!--表单布局模式-->
<slot v-if="mode==='form'" v-bind:dict="dict">
<el-select :size="size" v-model="mValue" v-if="dict.items" @change="handleChange">
<el-option
v-for="item in dict.items"
:key="item.dictItemValue"
:label="item.name"
:value="item.dictItemValue">
</el-option>
</el-select>
</slot>
<!--列表布局模式-->
<slot v-else-if="mode==='list'" v-bind:dict="dict">
<span v-for="item in dict.items" :key="item.dictItemValue">
<el-tag :type="type" size="mini" v-if="item.dictItemValue === value">{{ item.name }}</el-tag>
</span>
</slot>
<!--搜索表单布局模式-->
<slot v-else-if="mode==='searchForm'" v-bind:dict="dict">
<el-select :size="size" v-model="mValue" v-if="dict.items" @change="handleChange">
<el-option label="所有" :value="undefined"></el-option>
<el-option
v-for="item in dict.items"
:key="item.dictItemValue"
:label="item.name"
:value="item.dictItemValue">
</el-option>
</el-select>
</slot>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'MDict',
props: {
// 字典唯一编码(表名_字段名)
dictKey: {
type: String,
default: undefined
},
// 字典类型(enum->枚举类字典类型,db->数据库字典类型,local->本地字典类型)
// 不传的话,后端先查enum,再查db
type: {
type: String,
default: 'enum'
},
// 绑定的值
value: {
type: [String, Number],
default: undefined
},
size: { // medium/small/mini
type: String,
default: 'medium'
},
mode: { // form->普通表单,list->列表页,searchForm->搜索表单
type: String,
default: 'form'
}
},
data() {
return {
mValue: this.value
}
},
computed: {
...mapGetters([
'dictMap'
]),
// 当前字典
dict() {
return this.dictMap[this.dictKey] || {}
}
},
watch: {
value(n) { // 监听父组件值变动,子组件也要变动
this.mValue = n
}
},
created() {
if (!this.dictMap[this.dictKey]) {
// 这里调用store/modules/dict.js/action->getByDictKey
this.$store.dispatch('dict/getByDictKey', {
dictKey: this.dictKey,
type: this.type
})
}
},
methods: {
// 子组件值变化要通知父组件
handleChange(value) {
this.$emit('input', value)
}
}
}
</script>
src/store/modules/dict.js
import request from '@/utils/request'
const getDefaultState = () => {
return {
// 字典map
dictMap: {}
}
}
const state = getDefaultState()
const mutations = {
// 保存字典项
SAVE_DICT_ITEM: (state, data) => {
var obj = {}
obj[data.dictKey] = data
// 需要拷贝一份,要不然数据变动监听不到
state.dictMap = Object.assign({}, state.dictMap, obj)
},
// 移除字典项
DELETE_DICT_ITEM: (state, dictKey) => {
delete state.dictMap[dictKey]
}
}
const actions = {
// 获取字典的action
getByDictKey({ commit }, data) {
return new Promise((resolve, reject) => {
if (state.dictMap[data.dictKey]) {
resolve()
} else {
// 防止同一个key多次请求
commit('SAVE_DICT_ITEM', {
dictKey: data.dictKey,
items: []
})
// 这里暂不用api.service.js
request({
url: '/sys/dict/getByDictKey',
method: 'post',
data
}).then(res => {
if (res.code === 0 && res.data) {
commit('SAVE_DICT_ITEM', res.data)
} else {
commit('DELETE_DICT_ITEM', data.dictKey)
}
resolve()
}).catch(error => {
commit('DELETE_DICT_ITEM', data.dictKey)
reject(error)
})
}
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
src/store/getters.js
定义dictMap的get方法
const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
// 这里追加dictMap的get方法,可以使用mapGetters,详见src/components/m/Dict/index.vue
dictMap: state => state.dict.dictMap
}
export default getters
src/store/index.js
这里引入dict.js模块,充分利用了require.contex
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
// import app from './modules/app'
// import settings from './modules/settings'
// import user from './modules/user'
// 自动注册vuex模块
const files = require.context('./modules', true, /\.js$/)
var modules = {}
files.keys().forEach((routerPath) => {
const name = routerPath.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = files(routerPath)
const fileModule = value.default
modules[name] = fileModule
}, {})
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
...modules
},
getters
})
export default store
src/main.js
主入口全局注册自定义组件,这里也用了require.context,代码片段
import Vue from 'vue'
// 处理自定义组件全局注册
const files = require.context('./components/m', true, /\.vue$/)
files.keys().forEach((routerPath) => {
const componentName = routerPath.replace(/^\.\/(.*)\/index\.\w+$/, '$1')
const value = files(routerPath)
Vue.component('m-' + componentName.toLowerCase(), value.default)
}, {})
src/views/dashboard/index.vue
这里提供了使用样例:
自定义布局
<m-dict v-model="form.roleType" dict-key="sys_role_role_type">
<template v-slot:default="{ dict }">
<el-select v-model="form.roleType" v-if="dict.items">
<el-option
v-for="item in dict.items"
:key="item.dictItemValue"
:label="item.name"
:value="item.dictItemValue">
</el-option>
</el-select>
</template>
</m-dict>
表单布局模式
<m-dict mode="form" v-model="form.roleType" dict-key="sys_role_role_type"></m-dict>
列表布局模式
<m-dict mode="list" v-model="form.roleType" dict-key="sys_role_role_type"></m-dict>
搜索表单布局模式
<m-dict mode="searchForm" v-model="form.roleType" dict-key="sys_role_role_type"></m-dict>
效果图
小结
本文对字典组件的封装还是比较粗糙,不过也基本上满足平常使用,如后续场景需要,再考虑继续扩展。比如预留的local本地字典类型,如果本地存在字典配置,就可以不走接口请求。目前该参数也未实现。
项目源码地址
- 后端
- 前端