vue框架 --- 树形列表的基础实现

1,951 阅读3分钟

树形列表: 可以无限次递归显示

设想: 递归组件 + 控制元素的显隐(dom操作/vue数据控制)

第一步:实现递归组件 伪代码

    // 引用组件的页面 v-if="item.children" 不成为死循环
        <tree-component :propdata='data'  v-if="item.children"></tree-component>
        ...
        // 递归数据
        data: [{
            name: '1级',
            url: '/url1',
            children: [{
                name: '2级',
                url: '/url2'
                children: [{
                    name: '3级',
                    url: '/url3'
                }]
            }]
        }]
    // 子组件
    <div class="container">
        <div v-for="item in propdata" class="item-div" @click="handleClick($event)">
            <p>{{item.name}}<p>
            // 递归核心
            <tree-component  v-if="item.children" :propdata="item.children"></tree-component>
        </div>
    </div>
    new Vue({
        name: 'treeComponent' // 当前组件定义名字属性,才能被递归引用
    })

第二步:控制元素的显隐

渲染出的页面大概

<div class="container">
     <div class="item-div"> 
        <p>上一级</p>
        <div class="container">
            <p> 子元素 </p>
            ...
        </div>
    </div>
</div>

1. dom元素直接隐藏 
-- 只要会js的display,就能写
-- 弃用:1.vue最好不直接操作dom 2.不好控制动画,子级内容显示隐藏应该有上下收缩

<div class="container">
 <div v-for="item in propdata" class="item-div" @click="handleClick($event)">
    <p>{{item.name}}<p>
    // 递归核心
    <tree-component v-if="item.children" :propdata="item.children($event, item)"></tree-component>
 </div>
</div>

js:
handleClick (event, item) {
    // item有子级就是展开、关闭功能,没有子级跳转到指定路由
    if (item.children && item.children.length > 0) {
        // event.currentTarget响应绑定事件的元素 event.target响应点击的元素
        // childNodes 是class = item-div元素
        let chidNodes = event.currentTarget.childNodes; 
        for(let index = 0; index < childNodes.length; index++){
            // 找到包含‘contaienr’的元素 = 当前点击的下一级
            let itemChild = childNodes[index];
            let className = itemChild.className;
            if(className && className.indexOf('contaienr') > -1){
                let dispaly = itemChild.style.display;
                if(display ==== 'none'){
                    itemChild.style.display = 'block'; 
                }else{
                    itemChild.style.display = 'none';
                }
            }
        }
        
    }else {
        this.$router.push(item.url)
    }
}
2.监听数据改变 -- 数据中每一级加一个变量visible,控制自身
// 父组件 -- 不直接改变data值
    dataForEach(data, boolean){ // boolean控制全部显示还是隐藏 true隐藏
        data.forEach((item, index) => {
            if (Array.isArray(item.children)) {
              item.visible = boolean
              // 只有两层, 为了全部隐藏或展开
              this.putDataChange(index, item)
              this.dataForEach(item.children, boolean)
            }
        })
    },
    // 改变数据
    putDataChange(index, val) {
      this.$set(this.data, index, val)
    }
    -------加上了visible
// component.js 递归组件
    <div class="container">
        <div v-for="item in propdata" class="item-div" @click="handleClick($event, item, index)">
            <p>{{item.name}}<p>
            <tree-component v-if="item.children&&item.visible" :propdata="item.children($event, item)"></tree-component>
        </div>
    </div>
    new Vue({
        props:['propdata'], // 不能直接使用,需要监听变化
        data () {
            return {
                items: []
            }
        },
        watch: {
            'propdata': function(val){
                this.items = val
            }
        },
        mounted () {
            this.items = this.propdata
        },
        methods: {
            handleClick (event, prop,index) {
                if (prop.hasOwnProperty('visible')) {
                prop.visible = !prop.visible
                // vue的数组更新检测,直接改变一个值不会检测变动
                // 导致只能递归一次 (~~~)
                this.$emit('putDataChange', index, prop) // 父元素改变数据
              } else {
                this.$router.push(prop.url)
              }
            }
        }
    })

额外问题: url地址输入对应的导航高亮

<div v-for="item in propdata" class="item-div" :class="{'active': item.url === activeUrl}"...>
js:
watch:{
    '$route': function(to, from){
        this.activeIndex = to.path
    }
},
mounted () {
    this.activeIndex = this.$route.path
}

目前为止出现两个问题: 1. 只能递归一次,硬伤 2.收缩展开的动画,检测height高度变化

  • 问题1: 只能递归一次
  • 根本原因 :在无法更新数组数据
  • 解决方案: 每次改变替换数组,深拷贝
  • 过程: 开始想用递归组件上绑定父子通信事件,以为事件也会每个递归直到最外层父组件,结果。。。考虑祖孙通信事件,递归组件不知道怎么绑定,哎。。。想到不同组件通信可以用bus事件总线,感觉应该行。因为项目有用vuex就选择了它,果然很好用。
    vuex-1.js
    state: {
        data: ...
    },
    CHANGEDATA(state, data) {
      state.data = data
   },
    actions: {
        changedata ({commit, state}) {
          // 考虑到与后台会有异步操作
          let data = []
          data = deepCopy(state.data) // deepCopy深拷贝
          commit('CHANGEDATA', data)
        },
    }
// 子组件
handleClick(item){ 
    item.visible = !item.visible
    this.$store.dispatch('changedata')
}

问题2:根据height收缩动画,开始dom操作在每次点击后对应display代码块中,后来用数组数据操作,还是想着先获取子级高度,再在用户点击的元素上去加动画。。。

    第一个失败尝试:前提是dom操作
    缺点: 1.操作dom,还加了个data-height,2.因为默认展开,第一次height=0,没有任何效果
    template:
        <div class="container" :data-height='propdata.length * 40 + 1'></div>
    js:
    handleClick(){
        .... 这里代码是dom操作的click
        if(display === 'none'){
            itemChild.style.display = 'block';
            // 纯dom操作
            itemChild.style.height = itemChild.getAttribute('data-height') + 'px' 
        }else{
            itemChild.style.display = 'none';
            itemChild.style.height = 0
        }
    }
第二个失败尝试: 前提是数组visible控制
template:
  // 亮(xia)点(yan):style绑定了height,还是用data-height记录高度。
  只有两级,每一个元素高度相同 40 -- 重点:只能有两级,不然高度完全不对
    .... <tree-component v-if="item.children"  
            :style="{height: !item.visible ? item.children.length * 40 + 1 + 'px' : 0}" 
            :data-height='item.children.length * 40 + 1'
            :propData="item.children" :activeclass="activeclass" ></tree-component>
js: 不变--毕竟只能控制两级

看着element-ui的实现,心中一阵凉意。于是学(bei)习(bi)了一把,还是要好好学习。哪怕现在我没什么用 看了有收缩效果的el-tree\el-collapse,都用了一个东西import ElCollapseTransition from 'element-ui/src/transitions/collapse-transition'; 看到了transition+scrollHeight 哇~~~我是渣渣

// 子组件
    template: vuex版
        <transition
            v-on:before-enter="beforeEnter"
            v-on:enter="enter"
            v-on:after-enter="afterEnter"
            v-on:before-leave="beforeLeave"
            v-on:leave="leave"
        >
            <tree-component>...</tree-componet>
        </transition>
    js: 
    --- 只是高度,源码中还有paddingTop..我没有考虑padding。。。
    beforeEnter (el) {
      el.style.height = '0';
    },
    enter (el) {
      if (el.scrollHeight !== 0) {
        el.style.height = el.scrollHeight + 'px';
      } else {
        el.style.height = '';
      }
      el.style.overflow = 'hidden';
    },
    afterEnter (el) {
      el.style.height = ''
    },
    beforeLeave (el) {
      el.style.height = el.scrollHeight + 'px'
      el.style.overflow = 'hidden';
    },
    leave (el) {
      if (el.scrollHeight !== 0) {
        el.style.height = 0
      }
    }

假装总结:树形列表: 递归组件 + bus/vuex通信改变数组数据 + height动画(transition+scrollHeight)

问题: 走了太多弯路,组件不完善,只是简单实现。没有懒加载

加后台数据

存在几个问题:

  • 1.store状态中的id值不能直接在mounted中获取, id来源是页面头组件中赋值
         computed: {
            deptId () {
              return this.$store.state.userInfo.id
            }
          },
          watch: {
            deptId: function (val) {
              this.$store.commit("CHANGEDATA", val) // must id
            }
        }
    
  • 2.删除按钮和删除弹窗中的确定不在同一组件,vuex
  • 3.展开点击后请求后台数据,添加 删除 移动 === 需要增加字段children(子级),level(间距left),visible(显示或隐藏) === 各个功能中添加后台接口