vue+element大型表单解决方案(9)–数据比对(下)

先跟大家道个歉,距离上一篇写完居然隔了一个月。这一个月实在太多事情了,身体状态也不太好,每到周末都想着总该写一篇了吧,转念又想反正没人看,干嘛逼着这么累…直到前几天,看到有小伙伴留言催更,我才意识到既然写的是系列文章,就没有半途而废的道理,唯有一鼓作气写完。其实我也只是写技术文章的新手,上来就写系列文章,确实不是好的选择;好在本系列快完结了,后面几篇都是一些真实场景的演示,可以作为vue表单实践的一些参考。

代码地址:gitee.com/wyh-19/supe…
上篇代码分支:essay-8
本篇代码分支:essay-9

系列文章:

前言

上一篇实现了基本的数据比对,只是场景比较简单,比对的都是文本类控件的数据。这一篇将补充一些复杂控件的数据比对,比如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
  }
  ...省略
}
复制代码

到此准备工作完毕,进入比对页面,效果如下:

image.png
显然,这些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已经实现了比对效果,如下图(样式问题不在此讨论,可自行根据需要调整):

image.png

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)}"
此时比对结果如下图:

image.png

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分支方式扩展以及优化掉重复的代码。
和前面的比对不同的地方在于:

  1. 不是简单的使用lastModel !== beforeModel比对两个值是否不同,而是使用了compareEasyArray方法,这里的需求是判断数组不同不在于其顺序,而在于是否存在不同的项。
  2. 不是简单的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(',')
}
复制代码

此时效果如下:

image.png
此时已基本实现我们的目标,如果想做的更完美些,只需要沿用当前思路加上自己的奇思妙想了。

拓展补充

实践中,远不止这些比对类型,下面的代码都不在demo中演示了,只是简单的记录一下,便于以后需要时快速查找。

  1. 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)
    }
  }
}
复制代码
  1. 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的形式传入显示。

到这里,普通表单的字段比对的功能已全部实现。后面将演示一些复杂类型的表单如何实现,以及跨表单之间如何联动通信。谢谢您的阅读,欢迎提出指正意见!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享