前言
在开发各类后台管理系统时,我们不可避免地会涉及到大量表单和表格的处理。这些表单和表格内部通常包含各种基础或类型数据选项,如性别、状态和进度等。为了方便存储,在后端我们会使用数字或字符串来代替这些选项(例如男性为1,女性为0,未知性别为2)。因此,在传输和接收数据时,我们需要使用数字或字符串。但是在页面中展示时,我们又需要将这些数字或字符串转换为汉字,这通常需要大量且重复的代码。一旦这个数据用在两个甚至两个以上的地方,并且需要进行修改的话,我们必须一个个地方的去修改,将极其痛苦,你甚至都不知道这个数据都用在了哪。为了解决这个问题,我逐步在使用字典方面不断进化和提升,下面是我的进化历程。
字典的史前阶段
史前阶段我们将字典数据嵌入页面代码区域。例如,在用户管理页面,我们会直接在data里面定义用户状态。 问题是:如果后端的对应发生了变化(虽然很少有这种情况,但是偶尔会出现),就需要一个个修改所有包含这个字典的代码。
字典的初次进化
为了解决史前阶段的痛点,我们引入了数据字典。将所有的字典数据放在一个单独的js文件中,现在,我们只需要在数据字典中定义所有可能用到的数据,然后在页面代码中引用即可。如果后端对应发生了变化,我们只需要修改数据字典中的内容即可自动更新所有引用该数据字典的页面。这样不仅可以减少重复的代码编写,还可以提高代码的维护性和可读性,非常方便。
问题我们解决了四处修改字典的问题,改为了在一处地方维护处处生效的方式。目前还有以下问题:
* 系统庞大的时候维护的成本会逐步上升,需要增删改查都将变得困难
* 上线后如果需要增加一个字典的枚举值,那么依旧需要进行前端代码的改动,适应性差
* 字典改动的时候需要前后端一起改动
字典的终级进化
首先确定一点,数据放在前端进行维护肯定是不合适的。我们需要跟后端进行配合,将字典的数据放在数据库中进行维护。我们可以通过请求接口的形式获取到数据库的数据,并将数据缓存到前端本地,每次读的时候去缓存读取,如果未读到值,那么进行接口请求数据。具体实现可以通过Axios + Vuex + localstroge进行。这种的方式在各大开源后台管理系统中都是这么实现的(ruoyi、element-admin等);
使用层:
<!-- form表单中 -->
<el-select v-model="queryParams.status" placeholder="部门状态" clearable>
<el-option
v-for="dict in dict.type.sys_normal_disable"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
<!-- table表格中 -->
<el-table-column prop="status" label="状态" width="100">
<template slot-scope="scope">
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/>
</template>
</el-table-column>
总结:现在我们解决了初次进化遗留下来的三个问题。我们看到上方使用层的代码,需要不停的遍历着去写,或者像table中进行一个组件的封装可以减少一部分代码的编写。当我们新开一个项目的时候,我们需要重新勤劳的搬砖,一点点的搬动过去。风险性高且适应性低。所以我们基于终极解决方案研究出来了究极的解决方案,请往下看。
究极解决方案hc-basic
在我们使用字典值的事情,95%以上的情况字典都是在table、form、desc中进行使用。所以我们封装table、form、desc可以将字典组件结合进去。将请求和数据缓存等处理全部内置于组件库内存。
下面是组件库实现之后代码开发效果。我们只需要在form-item或者table组件配置项中添加对应的code字段,code值为字典的枚举,这样值的请求、回显、缓存等处理在组件内部已实现。
form-item:
formConfig: [
{ itemType: 'select', label: '性别', prop: 'sex', code: 'sexCode' }
]
table:
tableColumn: [
{ label: '状态', prop: 'status', code: 'status_code' }
]
遇到的第一个问题:组件内部封装字典需要axios参与。 我们在组件库内部引入axios。众所周知,项目开发axios封装时,我们通常会封装请求拦截器和响应拦截器。在拦截器内部进行请求的请求的公共处理。我们可以将拦截器通过hc-options进行传递。 我们拿form-item组件为例进行说明:
- 我们先对config参数进行监听处理,初始化或者变化后,进行code字典处理
watch: {
config: {
handler(val) {
val.forEach(e => {
e.code && muta.getListData(this, e.code);
});
},
immediate: true
}
},
- 然后结合传入的参数和vue.obserable进行字典枚举的请求和缓存 我们先来看看都需要传入那些参数:
tCode: {
path: '/dict/data/list', // tcode字典接口请求地址
key: 'dictCode', // tcode接口入参字段
type: 'get', // 接口请求方式get or post
cache: true, // 是否缓存处理
dataType: 'dictType', // 标明数据类型的字段
isNumValue: 0, // '当数据类型字段的值为0时,该字典数据是数字类型'
func: res => res.data.data // 接口请求的参数处理,需要返回list
},
拿到以上参数后,根据参数我们进行以下处理,最终将数据存储到state.tCodeData和localStroge的tCodeData字段里面去
import Vue from 'vue';
const state = Vue.observable({
tCodeData: {},
});
const muta = {
getListData(vm, value) {
// 获取浏览器localStroge缓存数据tcodeListData
const tCodeData = JSON.parse(window && window.localStorage.getItem('tCodeData')) || {};
const tCode = vm.$Config.tCode;
// 默认入参
let params = { [tCode.key]: value };
// 如果设置了缓存,那么判断state里面有没有该数据,如果没有那么进行数据请求
if ((tCode.cache && !state.tCodeData[value]) || !tCode.cache) {
// 设置get方式请求入参
tCode.type === 'get' && (params = { params });
vm.$hcHttp[tCode.type](vm.$Config.axios.ipUrl + tCode.path, params).then(res => {
const result = vm.$Func.filterOptionArray(tCode.func(res) || [], 'id');
// 后台返回字典值取数字或者字符串数据
state.tCodeData[value] = result.map(e => {
let value = e[vm.$Config.Components.formItem.value];
if (tCode.dataType && e[tCode.dataType] === tCode.isNumValue) {
value = Number(value || '0') || 0;
}
return { ...e, [vm.$Config.Components.formItem.value]: value };
});
state.tCodeData = JSON.parse(JSON.stringify({ ...tCodeData, ...state.tCodeData }));
vm.$Func.addStorgeValue('tCodeData', JSON.stringify(state.tCodeData));
});
}
}
};
export { state, muta };
3.form-item子组件:select组件开发
<template>
<el-select v-model="values[keys]" v-bind="$attrs" :placeholder="placeholder" style="width: 100%" @change="($attrs.change && $attrs.change(form)) || ''">
<el-option
v-for="(item, index) in list"
:key="'select' + index"
:label="item[$attrs.labelKey || $Config.Components.formItem.label]"
:value="item[$attrs.valueKey || $Config.Components.formItem.value]"
:disabled="item.disabled"
/>
</el-select>
</template>
<script>
import { state, muta } from '../../../tCode';
export default {
name: 'ItemSelect',
inheritAttrs: false,
props: {
form: { type: Object, default: () => ({}) }
},
computed: {
list() {
return (this.$attrs.code && state.tCodeData[this.$attrs.code]) || this.$attrs.list || [];
},
placeholder() {
return this.$attrs.placeholder || (this.$attrs.noLabel && this.$attrs.label) || '请选择';
},
values() {
if (!this.$attrs.prop) {
throw new Error(this.$attrs.label + ':prop未定义');
}
const props = this.$attrs.prop.split('.');
return props[1] ? this.form[props[0]] : this.form;
},
keys() {
const props = this.$attrs.prop.split('.');
return props[1] || props[0];
}
}
};
</script>
我们看到组件层computed下面的list,如果传入的配置参数中包含有code字段,那我们通过code字段去store.tCodeData中取值。然后在HTML中通过v-for进行遍历渲染
4.table组件封装的处理 在el-table组件中有一个参数fomatter,可以对每一个参数进行数据处理,处理方式如下:
<el-table-column
v-for="(item, index) in column"
:key="'otable' + index"
:formatter="item.code ? setFormatData(item) : item.formatter"
>
setFormatData(data) {
const that = this;
return function (row) {
const result = (state.tCodeData[data.code] || []).find(e => row[data.prop] === e[that.$Config.Components.formItem.value]) || {};
return result[that.$Config.Components.formItem.label] || row[data.prop];
};
},
总结:我们通过以上方式,就可以将tcode融合到组件库内,达到了以下简单code配置字典的功能。
**form-item**:
formConfig: [
{ itemType: 'select', label: '性别', prop: 'sex', code: 'sexCode' }
]
**table**:
tableColumn: [
{ label: '状态', prop: 'status', code: 'status_code' }
]