Tree组件_基于elementUI的二次封装及&自己原生开发的轮子


因为害怕自己并非明珠而不敢刻苦琢磨,又因为有几分相信自己是明珠,而不敢庸庸碌碌,与石砾为伍


在这里插入图片描述


一. 需求

**加粗样式**

1. 左侧文件夹 / 部门 列表

  • 节点有部门文件夹
  • 文件夹下只有文件夹
  • 部门下 有部门和文件夹
  • 文件夹节点有勾选框, 部门节点无勾选框, 有部门图标

2. 右侧 文件列表(常规列表, 这里不做表述)

原来需求要求:
勾选父节点, 子节点全选; 子节点全选, 父节点不能全选; 勾选左侧文件夹, 右侧文件不能被勾选; 右侧文件被勾选, 左侧文件夹勾选框置灰;
这个版本只实现 每个文件夹独立被勾选, 左侧文件夹勾选, 右侧文件列表置灰;


二. 思路 & 疑难点


需求拆分:

  1. 数据层级渲染
  2. 部分节点不显示勾选框(不可勾选), 显示图标
  3. 每个文件夹独立, 脱离父子关系(子节点全选, 父节点不被全选)
  4. 支持数据回显(加载后部分文件默认已被勾选)

关联问题:

  1. 点击左侧文件夹, 右侧文件勾选框置灰
  2. 节点失焦后(点击区域外), 保留上一个节点的背景色(记录当前选中项)
  3. 点击三角箭头, 除了折叠还具有点击效果(获取右侧文件)

难点:

  1. 数据层级渲染(elementUI需要对数据做处理 / 造轮子需要用到递归)
  2. 失焦 / 样式选中 等样式(样式穿透)
  3. 节点父子关系(造轮子时候选中多层节点 / 以及回显多层节点) - 暂时有bug

三. elementUI


整体Code:

                  <el-tree
                    class="depart-tree"
                    ref="treeDepartment"
                    show-checkbox
                    :check-strictly="true"
                    :data="departmentList"
                    node-key="itemID"
                    :props="defaultPropsDepartment"
                    :default-expanded-keys="defaultItemID"
                    :default-checked-keys="checkedFoldersList"
                    @node-click="nodeClick"
                    @node-expand="nodeClick"
                    @node-collapse="nodeClick"
                    @check="checkClick"
                  >
                    <span
                      :title="node.label"
                      class="custom-tree-node"
                      slot-scope="{ node }"
                      style="color: #333333; font-size: 14px"
                    >
                      <span class="textOverflow2">
                        <!-- 部门图标 -->
                        <i v-if="node.data.isDepart == 1" class="treenode-depart"></i>
                        {{ node.label }}
                      </span>
                    </span>
                  </el-tree>
data() {
	return {
		// tree 部门文件
        departmentList: [],
        defaultItemID: [],
        defaultPropsDepartment: {
        	children: 'children',
        	label: 'name'
        },
        checkedFoldersList: [],
	}
},
methods() {
	nodeClick(data) { // 节点被点击时触发
		// 获取右侧文件列表
		this.getFileList()
		// 强制选中当前项
		this.$nextTick(() => {
          this.$refs.treeDepartment.setCurrentKey(data.itemCode) // 强制选中当前项
        })
	},
	checkClick() { // 复选框点击时触发
		// 获取选中文件夹列表
		let department = JSON.parse(JSON.stringify(this.$refs.treeDepartment.getCheckedNodes()))
	},
}

```css
/* tree */
.el-tree >>> .el-tree-node__content {
  height: 40px;
}
.el-tree >>> .el-tree-node__expand-icon {
  margin-left: 25px;
}
.el-tree >>> .el-tree-node__content:hover {
  background: #e8f0ff;
}
/* 改变被点击节点背景颜色,字体颜色 */
.el-tree >>> .el-tree-node:focus > .el-tree-node__content {
  background-color: #e8f0ff !important;
}
/*节点失焦时的背景颜色*/
.el-tree >>> .el-tree-node.is-current > .el-tree-node__content {
  background-color: #e8f0ff !important;
}
/* !!! tree 禁用的复选框隐藏 */
/deep/.el-checkbox__input.is-disabled {
  display: none;
}

/* 所有父节点均不显示复选框 */
/* .el-tree >>> .el-tree-node .is-leaf + .el-checkbox .el-checkbox__inner{display: inline-block;}
.el-tree >>> .el-tree-node .el-checkbox .el-checkbox__inner{display: none;} */
/* 一级节点无复选框 */
/* >>> .el-tree > .el-tree-node > .el-tree-node__content .el-checkbox {
  display: none;
} */

数据处理:

因为后端返回的数据并不是常规 departmentList: [ {0: name: name, children: Arr}, {1: name: name, children: Arr} ]
而是 departList: [ {0: departName: name, departChildren: } ] & folderList: [ { 0: folderName: name, children: Arr } ]
所以需要对数据处理, 两个数组合并

递归:

	// 递归
    recursion(arr = [], childName, childName2, newChildName, changeNameFunc, func) { 
      arr.forEach(item => {
        item[childName] = item[childName] || []
        item[childName2] = item[childName2] || []
        item[newChildName] = [...item[childName], ...item[childName2]]
        changeNameFunc(item)
        if (item[newChildName] && item[newChildName].length) {
          this.recursion(item[newChildName], childName, childName2, newChildName, changeNameFunc, func)
        } else {
          func(item)
        }
      })
      return arr
    },
    
    // 接口请求数据
    getFolderList() {
        this.$DataAccess.GetFolderByRoot().then(res => {
          // 将 folderName 替换为 name
          this.departmentList = JSON.parse(JSON.stringify(res.data.data.departmentList).replaceAll(/folderName/g, 'name'))
          // 使用递归遍历数据 
          // childItems / folderChild 均改成 children
          this.departmentList = this.recursion(this.departmentList, 'childItems', 'folderChild', 'children', (item) => {
            if (item.isDepart == 1) { // 部门节点 禁用(增加类名, 在样式中隐藏复选框)
              item.disabled = true
            }
            // name / folderName 均改成 name
            item.name = item.name || item.folderName
          }, (item) => { })
        })
	}

四. 原生的轮子


<template>
  <div>
    <li v-for="(item, index) in deps" :key="index" class="treenode-root">
      <!-- 父节点 -->
      <div
        class="treenode-node-item"
        @click="clickTag(item)"
        :class="item.clicked ? 'treenode-node-select' : ''"
      >
        <!-- 层级样式 -->
        <span style="margin-left: 20px;" v-for="(item, index) in level" :key="index"></span>
        <!-- 展开图标 -->
        <i
          v-if="!item.expand && item.children.length"
          class="treenode-switcher treenode-switcher-close"
          style="padding: 5px 7px 5px 0"
          @click="openTag(item)"
        ></i>
        <!-- 折叠图标 -->
        <i
          v-if="item.expand && item.children.length"
          class="treenode-switcher treenode-switcher-open"
          style="padding: 5px 7px 5px 0"
          @click="openTag(item)"
        ></i>
        <!-- 无折叠图标 占位符 -->
        <i v-if="!item.children.length" style="margin-right: 20px"></i>
        <span :title="item.name" class="treenode-title" style="color: #333333; font-size: 14px">
          <span class="textOverflow2">
            <!-- 部门图标 -->
            <i v-if="item.isDepart == 1" style="margin-right: 3px" class="treenode-depart"></i>
            <!-- 勾选框 -->
            <!-- {{ item.clicked }} -->
            <i
              v-if="item.isDepart == 0"
              @click="checkTag(item)"
              style="margin: 0 3px 0 4px"
              class="check02 ico-check02"
              :class="item.checked ? 'ico-checked02' : ''"
            ></i>
            {{ item.name }}
          </span>
        </span>
      </div>
      <!-- 子节点 -->
      <ul class="treenode" v-if="item.children && item.expand">
        <s-tree
          :data="item.children"
          class="treenode"
          :level="localLevel"
          @tag-click="clickTag"
          @tag-check="tagCheck"
          :showFloderList="showFloderList"
        />
      </ul>
    </li>
  </div>
</template>

<script>
import '@/css/css.css'
import '@/css/reset.css'
import '@/css/style.css'
import '@/css/public.paging.css'
export default {
  props: {
    // 父组件传值 - 需要被渲染的tree 
    data: {
      type: Array,
      required: false,
      default() {
        return [];
      }
    },
    // 层级 父组件传1
    level: {
      type: Number,
      default: 0
    },
    // 递归调用(该组件调用该组件时的传值)
    showFloderList: {
      type: Array,
      default: () => []
    }
  },
  name: 'sTree',
  data() {
    return {
      // 层级
      dataLevel: 1,
      deps: [],
      // 文件夹
      floderList: []
    }
  },
  created() {
    this.addLevel()
  },
  mounted() {
    this.showEchoFolder()
  },
  watch: {
    data: { // 监听 tree的值
      handler(newValue) {
        this.deps = JSON.parse(JSON.stringify(newValue))
        this.echoFolder(this.deps, this.floderList) // 监听 项目文件夹
      },
      deep: true,
    }
  },
  methods: {
    showEchoFolder() { // 文件夹回显
      // this.treeFloderList = this.showFloderList
      this.floderList = this.showFloderList.map(e => e.itemID)
      this.deps = JSON.parse(JSON.stringify(this.data))
      this.echoFolder(this.deps, this.floderList) // 监听 部门文件夹
    },
    clickTag(item) { // 子节点被点击时 触发
      this.$emit('tag-click', item)
    },
    tagCheck(item) { // 子节点被勾选时 触发
      this.$emit('tag-check', item)
    },
    checkTag(item) { // 节点被勾选 触发
      item.checked = !item.checked
      this.parentCheck(this.deps, item)
      this.$emit('tag-check', item)
    },
    openTag(item) { // 节点展开/折叠 触发
      item.expand = !item.expand
      this.$forceUpdate()
    },
    addLevel() { // 增加子项层级(样式)
      this.localLevel = this.level + 1
    },
    checkedChildren(list) {
      if (list.children && list.children.length) {
        item.children[j].checked = true
        this.checkedChildren(list.children)
      }
    },
    parentCheck(list, ids) { // 父节点勾选 字节点会全选(存在的bug - 只能勾选两级)
      if (ids.checked) {
        for (let i = 0; i < list.length; i++) {
          let item = list[i]
          if (item.itemID === ids.itemID) {
            if (item.isDepart != 1) { // 部门不勾选
              if (item.children && item.children.length) {
                for (let j = 0; j < item.children.length; j++) {
                  item.children[j].checked = true
                }
              }
              break;
            }
          } else {
            if (item.children) {
              this.parentCheck(item.children, ids)
            }
          }
        }
      } else {
        for (let i = 0; i < list.length; i++) {
          let item = list[i]
          if (item.itemID === ids.itemID) {
            if (item.isDepart != 1) { // 部门不勾选
              if (item.children && item.children.length) {
                for (let j = 0; j < item.children.length; j++) {
                  item.children[j].checked = false
                  if (item.children[j] && item.children[j].length) {
                    for (let h = 0; h < item.children[j].length; h++) {
                      item.children[j][h].checked = false
                    }
                  }
                }
              }
              break;
            }
          } else {
            if (item.children) {
              this.parentCheck(item.children, ids)
            }
          }
        }
      }
    },
    echoFolder(list, ids) { // 勾选的文件夹回显
      for (let i = 0; i < list.length; i++) {
        let item = list[i]
        if (ids.includes(item.itemID)) {
          if (item.isDepart != 1) { // 部门不勾选
            item.checked = true
            if (item.children.length) {
              for (let j = 0; j < item.children.length; j++) {
                let item2 = item.children[j]
                item2.checked = true
              }
            }
          }
        }
      }
    },
    bgColor(list) {
      for (let i = 0; i < list.length; i++) {
        let item = list[i]
        item.clicked = false
        if(item.children.length != 0) {
          this.bgColor(item)
        }
      }
    }
  }
}
</script>

<style scoped>
</style>



五. 拓展场景

场景一: 拖拽


1. 层级拖拽

<el-tree
                    ref="tree"
                    :data="data"
                    :props="props"
                    node-key="id"
                    :allow-drop="collapse"
                    @check-change="onTreeChang"
                    show-checkbox
                    draggable
                    default-expand-all
                    :indent="30"
                >
                </el-tree>

method事件
// 权限拖拽控制
        collapse(moveNode, inNode, type) {

            // return type == 'next';
            // 一级拖动到一级
            if (moveNode.level == 1 && inNode.level == 1) {
                // 四种情况
                if (moveNode.nextSibling == undefined) {
                    return type == 'prev';
                } else if (inNode.nextSibling == undefined) {
                    return type == 'next';
                } else if (moveNode.nextSibling.id !== inNode.id) {
                    return type == 'prev';
                } else {
                    return type == 'next';
                }
            }
            //是否为同级下的子节点
            // 二级拖动到二级
            if (
                moveNode.level == 2 &&
                inNode.level == 2 &&
                moveNode.parent.id == inNode.parent.id
            ) {
                // 四种情况
                if (moveNode.nextSibling == undefined) {
                    return type == 'prev';
                } else if (inNode.nextSibling == undefined) {
                    return type == 'next';
                } else if (moveNode.nextSibling.id !== inNode.id) {
                    return type == 'prev';
                } else {
                    return type == 'next';
                }
            }

            // 二级拖动到一级  或者一级拖住啊到二级
            if (
                (moveNode.level == 2 && inNode.level == 1) ||
                (moveNode.level == 1 && inNode.level == 2)
            ) {
                // 四种情况
                if (moveNode.nextSibling == undefined) {
                    return type == 'prev';
                } else if (inNode.nextSibling == undefined) {
                    return type == 'next';
                } else if (moveNode.nextSibling.id !== inNode.id) {
                    return type == 'prev';
                } else {
                    return type == 'next';
                }
            }
        }


2. 默认选中上次选中的节点, 定位滚动到该节点

                  <el-tree
                    class="depart-tree"
                    ref="treeDepartment"
                    id="filterTree"
                    show-checkbox
                    :default-expanded-keys="defaultItemID"
                    :check-strictly="true"
                    :data="departmentList"
                    node-key="itemID"
                    :props="defaultPropsDepartment"
                    :default-checked-keys="checkedFoldersList"
                    @node-click="nodeClick"
                    @node-expand="nodeClick"
                    @node-collapse="nodeClick"
                    @check="checkClick"
                  >
                </el-tree>
    defaultItem() { // 默认定位上一次点击的文件夹
      let defaultData = JSON.parse(localStorage.getItem("defaultData"))
      if (defaultData == 1) {
        this.switchTree(0)
        return
      }
      if (defaultData == 2) {
        this.switchTree(1)
        return
      }
      this.nodeClick(defaultData)
      this.$nextTick(() => {
        this.defaultItemID = [defaultData.itemID]
        this.$refs.treeDepartment.setCurrentKey(defaultData.itemID) // 强制选中当前项
      })

        // 获得父容器的高度(clientHeight) 
        let parentNodeClientHeight = document.querySelector('#filterTree').clientHeight
        this.$nextTick(() => {
          this.$nextTick(() => {
            this.$nextTick(() => {
              // 获取选中节点距离父元素的顶部距离(offsetTop)
              let nodeOffsetTop = document.querySelector('.is-current').offsetTop
                  setTimeout(() => {
                    document.getElementById('leftTree').scrollTop = (nodeOffsetTop - (parentNodeClientHeight / 2))
                  })
               })
            })
      })
    },

场景二: 鼠标悬停


1. 悬停控制右侧按钮显影


ElementUI 的 自定义节点内容为例:

在这里插入图片描述

期望: 不要一次性展示这么多按钮,鼠标悬停到某一项时,才展示这一项的功能按钮(新增、删除)

<!--ElementUI 为例 -->
	<el-tree
      :data="data"
      show-checkbox
      node-key="id"
      default-expand-all
      :expand-on-click-node="false">
      <!--自带的class属性 绑定鼠标悬停效果 -->
      <span class="custom-tree-node" slot-scope="{ node, data }">
        <span>{{ node.label }}</span>
        <!--默认隐藏右侧按钮 -->
        <span style="display:none">
          <el-button type="text" size="mini" @click="() => append(data)"> Append </el-button>
          <el-button type="text" size="mini" @click="() => remove(node, data)"> Delete </el-button>
        </span>
      </span>
    </el-tree>
<style lang="scss" scoped>
	/* 鼠标悬停至 tree-item 项时,控制 其子元素(右侧按钮)显示 */
	.custom-tree-node:hover :nth-child(2) {
	  display: inline-block !important;
	}
</style>




如果有人做过类似的轮子可以分享一下0.o…

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

后海 0_o

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值