使用Vue手动封装树形控件组件

2023-10-27

提示:

       如果当下认为一件事很难做,那么不用犹豫直接做就OK,巨大的提升往往出现在反复的挣扎过后。跳出舒适圈,不断挑战自我,毕竟还年轻~


        因为已经完成了,索性就用上帝视角一次性把需要注意的东西都列出来。不怎么会循序渐进,原谅俺~

1.需求整理

                  

         如上图所示,树形结构有两种情况,可选不可选,不可选的话只需要考虑如何递归即可,可选则需要考虑更多的东西,

        简单举几个例子:

        1.当前选中节点,它父级的所有子节点是否全部选中,是的话父级也该选中,父级的父级也应该进行一个判断。

        2.当前节点选中,其是否还有子节点,如果有则全部都需要选中,子节点的子节点仍旧如此

        3.节点选中和点击的事件

节点除了选择之外还需要有拖拽查询过滤的功能,首先来看一下一个成熟的树形控件需要有哪些属性和事件...

    <WWQTree
      canselect
      canSearch
      draggable
      ref="treeSelect"
      :data="TreeData"
      @node-click="nodeclick"
      @select="select"
      :defaultCheck="defaultCheck"
      :default-expanded-keys="defaultExpandedKeys"
    >
    </WWQTree>

    /*
      canselect   是否可选择
      canSearch    是否可搜索
      draggable    是否拖拽
      ref="treeSelect"    绑定节点
      :data="TreeData"    核心数据源
      @node-click="nodeclick"    点击事件
      @select="select"    选择事件
      :defaultCheck="defaultCheck"    默认选中
      :default-expanded-keys="defaultExpandedKeys"    默认展开
    */

核心数据长这个样子        

  TreeData: [
        {
          label: "一级 1",
          children: [
            {
              label: "二级 1-1",

              children: [
                {
                  label: "三级 1-1-1",
                },
              ],
            },
          ],
        },
        {
          label: "一级 2",
          children: [
            {
              label: "二级 2-1",
            },
 
          ],
        },
      ],

 一个树形组件需要的基本属性基本如上

2.拿到核心数据我们需要做什么?

      /*  
      用于过滤数据,递归遍历将数据增强
      params: 原数据,目标容器
      */
    transData(this.data, null);
      /*  
      用于过滤默认展开的节点
      当前节点包括其父级节点全部展开
      */
    !this.defaultExpandedAll&&this.filterExpand();

    /*
      过滤默认选中的节点,选中后需要判断父级子元素是否全部选中
      ,并且需要使节点父级全部展开
    */   
    filterCheck();

        给数据变形,添加 选中状态,展开状态,父节点是哪个,查询时候是否高亮等增强属性

/*
    第一次进入时父节点为null,之后进行递归,
    将当前节点当参数,作为下一级节点的父级
*/
 transData(data, parent) {
      let trans = (item, parent) => {
        item = {
          ...item,
          checked: false,
          parent,
          expands: this.defaultExpandedAll?true:false,
          htmlText: '<span class="tree-title">' + item.label + "</span>",
        };
        if (item.children) {
          let arr = [];
          item.children.forEach((i) => {
            arr.push(trans(i, item));
          });
          item.children = arr;
        }
        return item;
      };
      data.forEach((item) => {
        this.StrongData.push(trans(item, parent));
      });

        组件还接受了两个数组,分别是默认选中以及默认展开的节点,也就是数据增强之后需要找出默认选中和默认展开的数据修改部分属性

        

  filterCheck() {
      let findNode = (list, a) => {
        list.forEach((item) => {
          if (item.label == a) {
            item.checked = true;
            findParent(item);
            haveParent(item);
            sonNodeSelect(item,true);
          }
          item.children && findNode(item.children, a);
        });
      };
      this.defaultCheck &&
        this.defaultCheck.forEach((a) => {
          findNode(this.StrongData, a);
        });
    },



    filterExpand() {
      let findNode = (list, a) => {
        list.forEach((item) => {
          if (item.label == a) {
            findParent(item);
          }
          item.children && findNode(item.children, a);
        });
      };
      this.defaultExpandedKeys &&
        this.defaultExpandedKeys.forEach((a) => {
          findNode(this.StrongData, a);
        });
    },

上面出现的辅助函数如下:

        

let haveParent = (item) => {
  let isAll = true;
  if (item.parent) {
    item.parent.children.forEach((item) => {
      if (item.checked != true) {
        isAll = false;
      }
    });
    item.parent.checked = isAll ? true : false;
    haveParent(item.parent);
  }
};

let sonNodeSelect = (item, bool) => {
  item.children &&
    item.children.forEach((i) => {
      i.checked = bool;
      i.children && sonNodeSelect(i,bool);
    });
};

let findParent = (item) => {
  item.expands = true;
  item.parent && findParent(item.parent);
};

3.渲染节点

        组件设计时共新建了两个组件,一个用于处理数据,向上级传递事件,另一个用于递归渲染树形结构

        tree.vue

//可以拖拽的话显示
 <div class="wwq-tree" v-if="draggable">
    //如果可以搜索
    <div class="tree-input" v-if="canSearch">
      <WWQInput placeholder="请输入内容..." inputType="text" v-model="value">
      </WWQInput>
    </div>
    <vuedraggable v-model="StrongData" class="one-ul">
      <div
        class="wwq-tree-item"
        v-for="(item, index) in StrongData"
        :key="'tree' + index"
      >
        <Item
          :item="item"
          :level="level"
          :canselect="canselect"
          :draggable="draggable"
          @select="select"
          @expand="expand"
          @node-click="nodeclick"
        >
        </Item>
      </div>
    </vuedraggable>
  </div>
//无需拖拽功能显示下面的结构
  <div class="wwq-tree" v-else>
      /*
        这里和上面结构一样,只是遍历时没有 vuedraggable    组件的包裹
        */
  </div>

注意:StrongData即增强后数据的容器

上面组件对是否可拖拽和是否搜索进行了判断,从而决定是否使用第三方 vuedraggable 和 输入框

        item.vue

 <div
    class="wwq-treeitem"
    :style="{
      left: 10 * level + 'px',  //层级越深,位置越靠右
    }"
  >
    <div class="wwq-item-label" @click="expands">
      //展开收起按钮
      <span
        v-if="item.children"
        class="icon-expand"
        :style="{
          transform: item.expands ? 'rotate(90deg)' : 'rotate(0deg)',
        }"
        >▶</span
      >
      <input
        v-if="canselect"
        type="checkbox"
        class="tree-check"
        :class="{ inputDisabled: item.disabled }"
        @change="checkTree"
        :checked="item.checked"
        :disabled="item.disabled"
      />
      <span v-html="item.htmlText"></span>
    </div>
    <transition name="task">
      <ul class="wwq-tree-ul" v-if="item.expands">
        //这里对是否拖拽再次进行了判断
        <template v-if="draggable">
          <vuedraggable v-model="item.children" class="one-ul">
            <li v-for="v in item.children" :key="v.label">
              <wwqTreeTtem
                :item="v"
                :level="num"
                :canselect="canselect"
                :parent="item"
                :draggable="draggable"
                @select="select"
                @node-click="nodeclick"
                @expand="expand"
              ></wwqTreeTtem>
            </li>
          </vuedraggable>
        </template>
        <template v-else>
          /*
            这里和上面结构一样,只是遍历时没有 vuedraggable    组件的包裹
            */
        </template>
      </ul>
    </transition>
  </div>

该组件进行了递归调用,递归组件时要注意option中的name即是调用自身时的组件名

 name: "wwqTreeTtem",

4.选中节点

 select(val) {
      let go = (data) => {
        data.forEach((item) => {
          if (item.label == val.label) {
            item.checked = !item.checked;
            haveParent(item);
            sonNodeSelect(item,item.checked);
          }
          item.children && go(item.children);
        });
      };
      go(this.StrongData);
      this.$emit("select", val);
    },

上面分析过一遍,选中时需要考虑的事情,这里只给出代码...

5.节点过滤且高亮功能的实现

在上面对数据进行增强的时候我添加了一条属性

 htmlText: '<span class="tree-title">' + item.label + "</span>",

在递归item的时候用到了他

        

  <span v-html="item.htmlText"></span>

这里本应该使用 双括号来渲染label的,毕竟组件的props接受了整个item,但是这样虽然完成了渲染,节点的高亮却无法实现

于是我选择了v-html,这样如果需要高亮,直接更改htmlText属性即可。

        首先,我们需要监听输入框绑定值的变化,从而执行相对的方法

 watch: {
    value: {
      handler(val) {
        this.findFilterNode(val, this.StrongData);
      },
    },
  },

        

/*
    如果输入框为空所有节点展开状态为false

    每次输入时将上一次的展开状态更改并恢复节点label的html
    符合条件的节点重新设置展开状态并且重新设置高亮状态
    如果有子节点,递归调用方法进行查询
*/
 findFilterNode(val, data) {
      if (val.length == 0) {
        setExpands(this.StrongData);
      } else {
        data.forEach((item) => {
          item.expands = false;
          item.htmlText = '<span class="tree-title">' + item.label + "</span>";
          if (item.label.includes(val)) {
            this.justFindParent(item);
            item.htmlText = this.filterReal(item, val);
          }
          item.children && this.findFilterNode(val, item.children);
        });
      }
    },

辅助函数如下:

/*
    如果节点符合过滤条件,将父级展开让其显示出来即可
*/ 
  justFindParent(item) {
      if (item.parent) {
        item.parent.expands = true;
        this.justFindParent(item.parent);
      }
   },

/*
   这里改变节点文本部分内容的样式
*/ 
   filterReal(value, key) {
      let label = value.label;
      const ind = label.indexOf(key);
      if (label.includes(key))
        return (
          label.split("").slice(0, ind).join("") +
          '<span class="tree-filter-text">' +
          key +
          "</span>" +
          label
            .split("")
            .slice(ind + key.length)
            .join("")
        );
    },


let setExpands = (arr) => {
  arr.forEach((item) => {
    item.expands = false;
    item.htmlText = '<span class="tree-title">' + item.label + "</span>";
    if (item.children) {
      setExpands(item.children);
    }
  });
};

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用Vue手动封装树形控件组件 的相关文章

随机推荐