先跟大家道个歉,距离上一篇写完居然隔了一个月。这一个月实在太多事情了,身体状态也不太好,每到周末都想着总该写一篇了吧,转念又想反正没人看,干嘛逼着这么累…直到前几天,看到有小伙伴留言催更,我才意识到既然写的是系列文章,就没有半途而废的道理,唯有一鼓作气写完。其实我也只是写技术文章的新手,上来就写系列文章,确实不是好的选择;好在本系列快完结了,后面几篇都是一些真实场景的演示,可以作为vue表单实践的一些参考。
代码地址:gitee.com/wyh-19/supe…
上篇代码分支:essay-8
本篇代码分支:essay-9
系列文章:
- vue+element大型表单解决方案(1)–概览
- vue+element大型表单解决方案(2)–表单拆分
- vue+element大型表单解决方案(3)–锚点组件(上)
- vue+element大型表单解决方案(4)–锚点组件(下)
- vue+element大型表单解决方案(5)–校验标识
- vue+element大型表单解决方案(6)–自动标识
- vue+element大型表单解决方案(7)–表单形态
- vue+element大型表单解决方案(8)–数据比对(上)
前言
上一篇实现了基本的数据比对,只是场景比较简单,比对的都是文本类控件的数据。这一篇将补充一些复杂控件的数据比对,比如select、radio、checkbox等,他们都有一个共同的特点,即value并不适宜直接展示,需要转换成相应的文字后才有比对价值。
准备工作
找到form1.vue文件,添加一些常见的复杂控件,代码如下:
<el-form-item label="学历" class="field-wrapper">
<el-select v-model="formData.education" v-compare:education="oldFormData">
<el-option v-for="item in educationList" :key="item.value" :value="item.value"
:label="item.label"></el-option>
</el-select>
</el-form-item>
<el-form-item label="性别" class="field-wrapper">
<el-radio-group v-model="formData.gender" v-compare:gender="oldFormData">
<el-radio :label="1">女</el-radio>
<el-radio :label="2">男</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="爱好" class="field-wrapper">
<el-checkbox-group v-model="formData.hobby" v-compare:hobby="oldFormData">
<el-checkbox v-for="item in hobbyList" :key="item.value" :label="item.value">
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
复制代码
相应的data中加上需要的响应式数据:
educationList: [
{
label: '研究生',
value: 1
},
{
label: '本科',
value: 2
},
{
label: '大专',
value: 3
}
],
hobbyList: [
{
label: '看书',
value: 1
},
{
label: '打游戏',
value: 2
},
{
label: '运动',
value: 3
}
],
复制代码
找到demo.js文件,增加测试数据返回,如下:
export function ajaxGetData() {
...省略
resolve(
{
name: 'wyh',
age: 30,
education: 1,
gender: 1,
hobby: [1, 3],
company: 'aaa'
}
)
...省略
}
export function ajaxGetOldData() {
...省略
resolve(
{
name: 'wyh19',
age: 30,
education: 2,
gender: 2,
hobby: [2, 3],
company: 'bbb'
}
)
...省略
}
复制代码
表单组装文件index.vue中,找到resolveDataToMap
方法,增加字段处理:
resolveDataToMap(data) {
const form1 = {
name: data.name,
age: data.age,
education: data.education,
gender: data.gender,
hobby: data.hobby
}
...省略
}
复制代码
到此准备工作完毕,进入比对页面,效果如下:
显然,这些value型的数据比对结果无法让人满意,这正是本篇要解决的问题,下面正式开始。
实现思路
通过新旧value的比较,可以区分出数据是否有变化,但是又不能像文本类的字段那样直接显示文本内容,需要根据value反推出文本,因此指令中需要增加文本解析的功能。之前v-compare:字段名="oldFormData"
的信息已经不够了,需要扩展下增加额外信息,比如这种形式v-compare:字段名.比对方式="{oldFormData,其他信息}"
。由于涉及到指令内部,因此一些字段需要固定成规范,比如当前例子中,select、radio都是需要将value映射成label进行显示,这里比对方式使用map这个单词,其他信息的字段也采用map作为字段名,形式为v-compare:字段名.map="{oldFormData,map:{...}}"
。
具体实现
radio的实现
radio相对来说是最简单的,先从这里入手实现上面的思路。修改性别这个字段的比对指令使用代码:
<el-radio-group v-model="formData.gender"
v-compare:gender.map="{oldFormData,map:{1:'女',2:'男'}}">
<el-radio :label="1">女</el-radio>
<el-radio :label="2">男</el-radio>
</el-radio-group>
复制代码
此时进入v-compare.js文件中,增加对map类型的处理。由于指令的使用形式和之前的不一样,我又不想改变原有的写法,因此需要在指令内部做了隔离,保留原来的逻辑不变。从binding参数中取出modifiers,根据其内容判断采用哪套逻辑,代码如下:
componentUpdated(el, binding, vnode) {
const { value, oldValue, arg, modifiers } = binding
if (modifiers.map) {
// map类型的逻辑在此实现
} else {
// ===原来文本类型比对逻辑保持写法用法不变====
// oldFormData从无数据到有数据时,才进行比对
// 避免数据更新过多无效的比对
if (!oldValue && value) {
// 进入此if判断时才真正有比对功能
// 最新的数据,即v-model里现在绑定的值
const lastModel = vnode.data.model.value
// 之前的数据,即oldFormData[arg]
const beforeModel = value[arg]
// 如果两个数据不相同,这里没有使用!==
if (lastModel !== beforeModel) {
// 打上标记
markDiffrent(el, beforeModel)
}
}
}
}
复制代码
在map的判断体内写上类似的比对逻辑代码,如下:
if (modifiers.map) {
// map类型的逻辑在此实现
// 拿到指令更新前后两次的oldFormData
const oldV = oldValue.oldFormData
const v = value.oldFormData
// 拿到map信息
const map = value.map
// 比较两次oldFormData,当从无到有时才比对,避免多余的无效比对
if (!oldV && v) {
const lastModel = vnode.data.model.value
const beforeModel = v[arg]
if (lastModel !== beforeModel) {
// 直接从map中映射成相应的文本
markDiffrent(el, map[beforeModel])
}
}
}
复制代码
此时看到radio已经实现了比对效果,如下图(样式问题不在此讨论,可自行根据需要调整):
select的实现
在这个例子中,本质上radio和select是一样的,唯一的区别是radio的选项是枚举出来的,而select的选项是遍历出来的,因此这里主要工作是如何得到map信息。在super-form-mixin.js文件中,写一个公共方法,专门处理转换工作,代码如下:
/**
* 转换select选项为比对指令需要的map类型
* 例如: [{value:1,label:'a'}] ===> {1:a}
*/
composeOptions(options = [], value = 'value', label = 'label') {
const map = {}
options.forEach(item => {
map[item[value]] = item[label]
})
return map
},
复制代码
此时修改select的比对指令为 v-compare:education.map="{oldFormData,map:composeOptions(educationList)}"
此时比对结果如下图:
checkbox的实现
与之前不同,checkbox不是单一值的映射,而是数组类型值的映射,因此在逻辑上有些区别,修饰符命名为arrayMap,map信息依然采用composeOptions函数转换,checkbox的比对指令使用代码为v-compare:hobby.arrayMap="{oldFormData,map:composeOptions(hobbyList)}
在modifiers.map判断后面增加新的判断分支,代码如下:
else if (modifiers.arrayMap) {
// arrayMap类型的逻辑在此实现
// 拿到指令更新前后两次的oldFormData
const oldV = oldValue.oldFormData
const v = value.oldFormData
// 拿到map信息
const map = value.map
// 比较两次oldFormData,当从无到有时才比对,避免多余的无效比对
if (!oldV && v) {
const lastModel = vnode.data.model.value
const beforeModel = v[arg]
if (!compareEasyArray(lastModel, beforeModel)) {
// 直接从map中映射成相应的文本
markDiffrent(el, getArrayMapResult(beforeModel, map))
}
}
}
复制代码
这里我为了书写方便,没有优化代码,大家可以根据自己习惯采用switch分支方式扩展以及优化掉重复的代码。
和前面的比对不同的地方在于:
- 不是简单的使用
lastModel !== beforeModel
比对两个值是否不同,而是使用了compareEasyArray
方法,这里的需求是判断数组不同不在于其顺序,而在于是否存在不同的项。 - 不是简单的
map[beforeModel]
得到文本,而是使用了getArrayMapResult
方法
下面实现这两个方法:
function compareEasyArray(arr1, arr2) {
// 数组的每一项都是简单类型,且不比较顺序
const arr1ToString = arr1.sort().join(',')
const arr2ToString = arr2.sort().join(',')
return arr1ToString === arr2ToString
}
function getArrayMapResult(arr, map) {
const result = arr.map(item => map[item])
return result.join(',')
}
复制代码
此时效果如下:
此时已基本实现我们的目标,如果想做的更完美些,只需要沿用当前思路加上自己的奇思妙想了。
拓展补充
实践中,远不止这些比对类型,下面的代码都不在demo中演示了,只是简单的记录一下,便于以后需要时快速查找。
- value型的字段,但是后端直接返回了相应的label,或者前端自己查出了laebl,不想在指令内部map,那么可以增加label比对方式
v-compare:字段名.label="{oldFormData,label:xxLabel}"
对应的指令解析办法:
if (modifiers.label) {
const oldV = oldValue.oldFormData
const v = value.oldFormData
const label = value.label
if (!oldV && v) {
const lastModel = vnode.data.model.value
const beforeModel = v[arg]
if (lastModel !== beforeModel) {
markDiffrent(el, label)
}
}
}
复制代码
- select-tree型控件数据,需要递归查找tree中的节点
<v-tree-select v-model="formData.respUnitId"
v-compare:respUnitId.tree="{oldFormData,tree:{options:$store.state.base.unitUserTreeData,id:'id',label:'name',children:'childList'}}"
:options="$store.state.base.unitUserTreeData"
no-results-text="不存在该部门"
:normalizer="(node)=>({id:node.id,label:node.name,children:node.childList})" />
复制代码
对应指令解析代码:
if (modifiers.tree) {
const oldV = oldValue.oldFormData
const v = value.oldFormData
const tree = value.tree
if (!oldV && v) {
const lastModel = vnode.data.model.value
const beforeModel = v[arg]
if (lastModel !== beforeModel) {
const { options, id, label, children } = tree
const beforeLabel = getLabelFromTree(beforeModel, options, id, label, children)
markDiffrent(el, beforeLabel)
}
}
}
复制代码
通过递归实现getLabelFromTree方法:
function getLabelFromTree(value, tree, idKey, labelKey, childrenKey) {
let result = ''
if (!value || !tree || !tree.length) {
return result
}
for (let i = 0; i < tree.length; i++) {
if (tree[i][idKey] === value) {
result = tree[i][labelKey]
} else {
result = getLabelFromTree(value, tree[i][childrenKey], idKey, labelKey, childrenKey)
}
if (result) {
break
}
}
return result
}
复制代码
通过上面例子,我想说明任何复杂的value型控件都能转换成可以显示的比对结果,如果遇到实在不好解决的情况,可以在外层主动获取label,通过label的形式传入显示。
到这里,普通表单的字段比对的功能已全部实现。后面将演示一些复杂类型的表单如何实现,以及跨表单之间如何联动通信。谢谢您的阅读,欢迎提出指正意见!