Vue 2.x折腾记 - (19) 基于Antd Design Vue 封装一个符合业务的树形组件

简介: 仔细翻了下对应的文档(antd vue),发现有那么一个树形控件,但是没有上面部分全局控制的功能。那么只能自己动手改造出一个符合业务的了,有兴趣的看官可以瞅瞅。


前言


原型上有个权限分配的功能;



仔细翻了下对应的文档(antd vue),发现有那么一个树形控件,但是没有上面部分全局控制的功能。


那么只能自己动手改造出一个符合业务的了,有兴趣的看官可以瞅瞅。


效果图



实现的思路


首先先梳理下要实现的功能点


  • 要考虑默认值的传递以及产生的联动
  • 全局开关对树控件产生的影响
  • 子项操作要反馈给全局实现联动;
  • 最后避免太多服务器资源(若是勾选一次触发一次有点大),回调改由按钮触发提交到外部


确定了功能点就可以开始搞起了,为此我实现过三个版本;


第一版是switch开关控件来控制, 为此树组件和开关组是抽离出两个独立组件,发现很难避免一些极端的操作行为;


第二版用的.sync+ switch来实现,发现一样难避免一些极端的操作行为;


第三版是改由按钮组去实现,发现可以很好的解决极端的情况,可以分解成三种情况去实现。


代码实现


TreePanel.vue


<template>
  <div :bordered="false" :bodyStyle="{ padding: 0 }">
    <a-row type="flex" justify="space-between" align="middle">
      <a-col :span="20">
        <a-radio-group :size="size" @change="onCheckedBtnGroupChange" v-model="allChecked" buttonStyle="solid">
          <a-radio-button value="2" :disabled="true">局部选中</a-radio-button>
          <a-radio-button value="1">全选</a-radio-button>
          <a-radio-button value="0">全不选</a-radio-button>
        </a-radio-group>
        <a-divider type="vertical" />
        <a-radio-group :size="size" @change="onExpandedBtnGroupChange" v-model="allExpanded" buttonStyle="solid">
          <a-radio-button value="2" :disabled="true">局部展开</a-radio-button>
          <a-radio-button value="1">展开所有</a-radio-button>
          <a-radio-button value="0">折叠所有</a-radio-button>
        </a-radio-group>
      </a-col>
      <a-col>
        <a-button type="primary" :size="size" href="javascript:;" @click="callBackData">
          <span>提交改动</span>
        </a-button>
      </a-col>
    </a-row>
    <a-tree
      :expandedKeys="innerExpandedKeys"
      :treeData="treeData"
      checkable
      :checkedKeys="innerCheckedKeys"
      @expand="onExpand"
      @check="onCheck"
    />
  </div>
</template>
<script>
import { getTreeKey } from './utils';
export default {
  props: {
    size: {
      // 控件规格
      type: String,
      default: 'small'
    },
    checkedKeys: {
      // 传递选中的key
      type: Array,
      default: function() {
        return [];
      }
    },
    expandedKeys: {
      // 传递需要展开的key
      type: Array,
      default: function() {
        return [];
      }
    },
    treeData: {
      type: Array,
      default: function() {
        return [
          {
            title: '0-0',
            key: '0-0',
            children: [
              {
                title: '0-0-0',
                key: '0-0-0',
                children: [
                  { title: '0-0-0-0', key: '0-0-0-0' },
                  { title: '0-0-0-1', key: '0-0-0-1' },
                  { title: '0-0-0-2', key: '0-0-0-2' }
                ]
              },
              {
                title: '0-0-1',
                key: '0-0-1',
                children: [
                  { title: '0-0-1-0', key: '0-0-1-0' },
                  { title: '0-0-1-1', key: '0-0-1-1' },
                  { title: '0-0-1-2', key: '0-0-1-2' }
                ]
              },
              {
                title: '0-0-2',
                key: '0-0-2'
              }
            ]
          },
          {
            title: '0-1',
            key: '0-1',
            children: [
              { title: '0-1-0-0', key: '0-1-0-0' },
              { title: '0-1-0-1', key: '0-1-0-1' },
              { title: '0-1-0-2', key: '0-1-0-2' }
            ]
          },
          {
            title: '0-2',
            key: '0-2'
          }
        ];
      }
    }
  },
  data() {
    return {
      allChecked: '', // 全选按钮组
      allExpanded: '', // 全部展开按钮组
      innerCheckedKeys: [], // 选中的值
      innerExpandedKeys: [] //  展开的值
    };
  },
  watch: {
    checkedKeys: {
      // 复制props
      immediate: true,
      deep: true,
      handler(newValue, oldValue) {
        if (newValue) {
          if (newValue.length === this.getTreeAllKey.length) {
            this.allChecked = '1';
          } else if (newValue.length === 0) {
            this.allChecked = '0';
          } else {
            this.allChecked = '2';
          }
          this.innerCheckedKeys = newValue;
        }
      }
    },
    expandedKeys: {
      // 复制props
      immediate: true,
      deep: true,
      handler(newValue, oldValue) {
        if (newValue) {
          if (newValue.length === this.getTreeAllGroupKey.length) {
            this.allExpanded = '1';
          } else if (newValue.length === 0) {
            this.allExpanded = '0';
          } else {
            this.allExpanded = '2';
          }
          this.innerExpandedKeys = newValue;
        }
      }
    }
  },
  computed: {
    switchDataSource() {
      // 勾选数据源
      return [
        {
          type: 'ALL_CHECKED',
          labelText: this.isAllChecked ? '全不选' : '全选',
          checked: this.isAllChecked
        },
        {
          type: 'ALL_EXPAND',
          labelText: this.isAllExpanded ? '折叠所有' : '展开所有',
          checked: this.isAllExpanded
        }
      ];
    },
    cacheEmitValue() {
      // 缓存响应的值
      return {
        checkedKeys: this.innerCheckedKeys,
        expandedKeys: this.innerExpandedKeys
      };
    },
    getTreeAllKey() {
      // 获取树的所有key
      return getTreeKey(this.treeData);
    },
    getTreeAllGroupKey() {
      // 获取树的所有组key
      return getTreeKey(this.treeData, true);
    },
    isAllChecked() {
      // 是否全部勾选
      return this.innerCheckedKeys.length === this.getTreeAllKey.length;
    },
    isAllExpanded() {
      // 是否全部展开
      return this.innerExpandedKeys.length === this.getTreeAllGroupKey.length;
    }
  },
  methods: {
    onExpandedBtnGroupChange({ target: { value: expanedBtnGroupValue } }) {
      console.log('expanedBtnGroupValue: ', expanedBtnGroupValue);
      switch (expanedBtnGroupValue) {
        case '0':
          this.innerExpandedKeys = [];
          break;
        case '1':
          this.innerExpandedKeys = this.getTreeAllGroupKey;
          break;
        default:
          break;
      }
    },
    onCheckedBtnGroupChange({ target: { value: checkedBtnGroupValue } }) {
      console.log('checkedBtnGroupValue: ', checkedBtnGroupValue);
      switch (checkedBtnGroupValue) {
        case '0':
          this.innerCheckedKeys = [];
          break;
        case '1':
          this.innerCheckedKeys = this.getTreeAllKey;
          break;
        default:
          break;
      }
    },
    callBackData(emit) {
      // 响应改动的值
      if (emit) {
        this.$emit('change', this.cacheEmitValue);
      }
      return false;
    },
    onExpand(expandedKeys) {
      if (expandedKeys.length === this.getTreeAllGroupKey.length) {
        this.allExpanded = '1';
      } else {
        this.allExpanded = '2';
      }
      this.innerExpandedKeys = expandedKeys;
    },
    onCheck(checkedKeys) {
      if (checkedKeys.length === this.getTreeAllKey.length) {
        this.allChecked = '1';
      } else {
        this.allChecked = '2';
      }
      this.innerCheckedKeys = checkedKeys;
    }
  }
};
</script>


utils.js:递归获取需要的数据


/**
 * @param arr 数组对象
 * @param parent 是否只获取父一层的属性
 * @description 递归获取需要的数据
 */
export function getTreeKey(arr, parent = false) {
  const dataList = [];
  const generateList = data => {
    for (let i = 0; i < data.length; i++) {
      const { key, title, children } = data[i];
      if (!parent) dataList.push({ key, title });
      if (Array.isArray(children) && children.length > 0) {
        if (parent) dataList.push({ key, title });
        generateList(children);
      }
    }
  };
  generateList(arr);
  return dataList.map(item => item.key);
}


用法


<tree-panel
          :treeData="treeData"
          :expandedKeys="treeExpandedKeys"
          :checkedKeys="treeCheckedKeys"
          @change="onTreePanelChange"
/>


props 类型 介绍
treeData 数组对象 整个树的数据
expandedKeys 数组 展开的数组key
checkedKeys 数组 选中的数组key
@change 自定义事件 拿到返回值的回调函数
目录
相关文章
|
6天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
6天前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
18天前
|
缓存 JavaScript UED
Vue 中实现组件的懒加载
【10月更文挑战第23天】组件的懒加载是 Vue 应用中提高性能的重要手段之一。通过合理运用动态导入、路由配置等方式,可以实现组件的按需加载,减少资源浪费,提高应用的响应速度和用户体验。在实际应用中,需要根据具体情况选择合适的懒加载方式,并结合性能优化的其他措施,以打造更高效、更优质的 Vue 应用。
|
21天前
|
JavaScript 前端开发 测试技术
组件化开发:创建可重用的Vue组件
【10月更文挑战第21天】组件化开发:创建可重用的Vue组件
24 1
|
5天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
5天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
5天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
5天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
4天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
4天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉