vue遞歸實現樹形組件

本文實例為大傢分享瞭vue遞歸實現樹形組件的具體代碼,供大傢參考,具體內容如下

1. 先來看一下效果:

2. 代碼部分 (myTree.vue)

圖片可以自己引一下自己的圖片,或者使用iconfont的css引入。

<template>
    <div class="tree">
        <ul class="ul">
            <li v-for="(item,index) of treeMenu" :key="index">
                <div class="jiantou" @click="changeStatus(index)">
                    <img src="../../assets/right.png" v-if="!scopesDefault[index]===true && item.children">
                    <img src="../../assets/down.png" v-if="scopesDefault[index]===true && item.children ">
                </div>
                <input type="checkbox" @click="checkBox(item)" v-model="item.check">
                <span @click="changeStatus(index)">{{item.label}}</span>
                <div class="subtree">
                    <tree-menu :treeMenu='item.children' v-if="scopesDefault[index]" @selectnode = "selectnode"></tree-menu>
                </div>
            </li>  
        </ul> 
    </div> 
</template>
<script>
    export default{
    name:'treeMenu',
     props:{
        treeMenu:{
            type:Array,
            default:[]
        },
    },
    data(){
        return{
            scopesDefault: [],
            scopes: [], 
            node:[],
            flatTreeMenu:[],
            check:'',
        }
    },
    methods:{
        //展開
        scope() {
            this.treeMenu.forEach((item, index) => {
                this.scopesDefault[index] = false
                if ('children' in item) {
                    this.scopes[index] = true
                    //console.log(item, index)
                } else {
                    this.scopes[index] = false
                }
            })
        },
        changeStatus(index) {
            if (this.scopesDefault[index] == true) {
                this.$set(this.scopesDefault, index, false)
            } else {
                this.$set(this.scopesDefault, index, this.scopes[index])
            }
        },
        //nodelist 深度優先遞歸
        checkBox(node,nodelist=[]){
            //console.log("start:",node,nodelist)
            if(node!==null){
                nodelist.push(node);
                if(node.children){
                    let children=node.children;
                    for(let i=0;i<children.length;i++){
                        this.checkBox(children[i],nodelist)//遞歸調用
                        children[i].check = nodelist[0].check==false?true:false;//選中父節點,子節點全選,取消,子節點取消
                    }
                }
            }
            this.node=node;
            this.check=node.check
        },
        selectnode(node){
            this.$emit("selectnode",node);
        }
    },
    watch:{ 
        node:{
            handler(val){
                this.selectnode(val);
            },
            immediate: true
        },
        check:{
            handler(val){
                this.selectnode(this.node);
            },
            immediate: true
        }
    },
    mounted(){
        this.scope(); 
    }
}
</script>
<style lang = "scss" scoped>
.tree{
    .ul{
        margin: 5px 0 5px 0;
        >li{
            .jiantou{
                display: inline-block;
                width: 15px;
                >img{
                position: relative;
                top: 2.0px;
                left: 4px;
                }
            }
            .subtree{
                margin-left: 20px;
                margin-top: 8px;
                margin-bottom: 8px;
            }
        }
    }
}
input[type=checkbox]{
    visibility: hidden;
    cursor: pointer;
    position: relative;
    width: 15px;
    height: 15px;
    font-size: 14px;
    border: 1px solid #dcdfe6;
    background-color: #fff !important;
    &::after{
        position: absolute;
        top: 0;
        background-color: #fff;
        border: 1px solid #ddd;
        color: #000;
        width: 15px;
        height: 15px;
        display: inline-block;
        visibility: visible;
        padding-left: 0px;
        text-align: center;
        content: ' ';
        border-radius: 3px;
        transition: all linear .1s;
    }
    &:checked::after{
        content: "\2713";
        font-size: 12px;
        background-color: #409eff;
        border: 1px solid #409eff;
        transition: all linear .1s;
        color: #fff;
        font-weight: bold;
    }
}
.check{
    &:checked::after{
        content: "--" !important;
    }
}
</style>

講解:

1、調用組件:

我這用來一個global.js來控制組件的使用(這個js附在文章末尾瞭),在component文件夾中建立一個myTree文件夾,裡面放同名vue文件(myTree.vue),這樣無論在哪裡調用這個組件,都可以直接使用<my-tree></my-tree>的方式去調用。

2、組件的方法:

scope():會生成一個數組,裡面有根節點是否有子節點,如本代碼裡設定的數據,會有scopes=[true,true,true]這樣的結果。
changeStatus():每點擊標題或者箭頭,如果當前下標的節點有沒有子節點,再將結果動態賦值給scopesDefault[index],將這個值放於dom上控制開關,遞歸組件。
checkBox():在組件內部實現瞭點擊全選、點擊取消全選的功能,遞歸調用當前方法,將子元素的狀態隨父元素一起變化。
selectnode():將當前點擊的node的節點內容上傳到父組件。

3、監聽:

同時監聽:不同節點的切換、同一個節點的是否選中的切換,監聽得到的結果都傳到父組件中。

4、組件遞歸:調用時與父組件相同

3. 使用組件(useMyTree.vue)

<template>
    <div class = "loginModuel">
        <my-tree :treeMenu='tree' @selectnode="selectnode"></my-tree>
    </div>
</template>
<script>
export default{
    data(){
        return{
            msg:"這是登錄頁面",
            tree:[
                  {
                    id:1,
                    label:"1級目錄1",
                    check:false,
                    children:[
                        {
                            id:"1-1",
                            pid:1,
                            label:"1.1目錄",
                            check:false
                        },
                        {
                            id:"1-2",
                            pid:1,
                            label:"1.2目錄",
                            check:false
                        },
                        {
                            id:"1-3",
                            pid:1,
                            label:"1.3目錄",
                            check:false
                        },
                    ]
                },
                {
                  id:2,
                  label:"1級目錄2",
                  check:false,
                  children:[
                      {
                          id:"2-1",
                          label:"2.1目錄",
                          check:false,
                          pid:2,
                          children:[
                            {
                                id:"2-1-1",
                                pid:'2-1',
                                label:"2.1.1目錄",
                                check:false,
                                children:[
                                    {
                                        id:"2-1-1-1",
                                        pid:'2-1-1',
                                        label:"2.1.1.1目錄",
                                        check:false,
                                        children:[
                                            {
                                                id:"2-1-1-1-1",
                                                pid:'2-1-1-1',
                                                label:"2.1.1.1.1目錄",
                                                check:false,
                                            },
                                            {
                                                id:"2-1-1-1-2",
                                                pid:'2-1-1-1',
                                                label:"2.1.1.1.2目錄",
                                                check:false,
                                            },
                                        ]
                                    },
                                ]
                            },
                            {
                                id:"2-1-2",
                                pid:'2-1',
                                label:"2.1.2目錄",
                                check:false,
                            },
                            {
                                id:"2-1-3",
                                pid:'2-1',
                                label:"2.1.3目錄",
                                check:false,
                            },
                        ]
                      },
                      {
                          id:"2-2",
                          pid:2,
                          label:"2.2目錄",
                          check:false
                      }
                  ]
              },//在此繼續添加目錄
              {
                id:3,
                label:"1級目錄3",
                check:false,
                children:[
                    {
                        id:"3-1",
                        pid:3,
                        label:"3.1目錄",
                        check:false,
                        children:[
                            {
                                id:"3-1-1",
                                pid:"3-1",
                                label:"3.1.1目錄",
                                check:false,
                                children:[
                                    {
                                        id:"3-1-1-1",
                                        pid:"3-1-1",
                                        label:"3.1.1.1目錄",
                                        check:false,
                                        children:[
                                            {
                                                id:"3-1-1-1-1",
                                                pid:"3-1-1-1",
                                                label:"3.1.1.1.1目錄",
                                                check:false
                                            },
                                        ]
                                    },
                                ]
                            },
                        ]
                    }
                ]
            },
            ],
            plist:[],//此級以上所有父節點列表
            flatTree:[],//tree的平行數據
            node:'',//當前點擊的node,
        }
    },
    methods:{
        //將tree樹形數據轉換為平行數據
        transformData(tree){
            tree.forEach(item=>{
                this.flatTree.push(item);
                item.children && item.children.length>0 ? this.transformData(item.children) : ""
            })
        },
        //子組件傳遞過來的點擊的node的值
        selectnode(node){
            this.node=node;
            this.flatTree=[];
            this.transformData(this.tree);
            if(node.check==false){//這個節點已經被選中,正在點擊取消選中
                this.plist=[];//每次點擊一個新的節點都將原來plist的內容清空
                this.getParentnode(this.flatTree,node.pid)
            }else{//正在選中
                this.childAllToParent(node,this.flatTree,1);
            }
        },
        //子節點取消選中,拿到此子節點所有的父節點plist
        getParentnode(tree,pid){
            //this.plist=[]
            if(pid!==null){
                tree.forEach(item=>{
                    if(item.id==pid){
                        this.plist.push(item)
                        this.getParentnode(this.flatTree,item.pid)
                    }
                })
            } 
            if(!pid){
                this.plist.forEach(item=>{
                    this.updateParentCheck(this.tree,item)
                })
            }
        },
        //將原數據tree對應id的項的check值改為false
        updateParentCheck(tree,plistItem){
            //console.log("方法updateParentCheck接收的plistItem參數:",plistItem)
            tree.forEach(item=>{
                if(item.id==plistItem.id){
                    item.check=false;
                }
                if(item.id!==plistItem.id && item.children){
                    this.updateParentCheck(item.children,plistItem)
                }
            })
        },
        //子節點全部選中後父節點選中
        childAllToParent(node,flatTree,j){
            let fatherNode='';
            let brotherNode=[];
            this.flatTree.forEach(item=>{
                if(node.pid && node.pid==item.id){
                    fatherNode=item;//找到瞭父節點--用於改變check的值
                }
            })
            //判斷該結點所有的兄弟節點是否全部選中
            flatTree.forEach(item=>{
                if(item.pid && node.pid && item.pid==node.pid){
                    brotherNode.push(item)//找到所有的兄弟節點
                }
            })
            //i為被選中的兄弟節點的個數
            let i=0;
            this.flatTree.forEach(item=>{
                if(node.pid==item.pid && item.check==true){
                    i=i+1;
                }
            })
            //修改父節點的選中值
            if(i==brotherNode.length && fatherNode){
                fatherNode.check=true
            }
            // console.log(`第j次遞歸 j=${j}`)
            // console.log(`選中的bro=${i},brother的個數:${brotherNode.length}`)
            // console.log("父節點:",fatherNode,"兄弟節點",brotherNode)
            if(fatherNode.pid!==undefined){
                j=j+1;
                this.childAllToParent(fatherNode,this.flatTree,j)
            }
        }
    },
    mounted(){
        this.transformData(this.tree);//數據初始化:將tree樹形數據轉換為平行數據
        //console.log(this.flatTree)
    }
}
</script>
<style lang = "scss" scoped>
.loginModuel{
    margin-left: 400px;
    margin-top: 100px;
    .tree{
        .ul{
            >li{
                margin: 5px 0 5px 0;
                >img{
                    position: relative;
                    top: 2.4px;
                    left: 4px;
                }
            }
            .ul2{
                >li{
                    position: relative;
                    left: 20px;
                    margin: 5px 0 5px 0;
                    >img{
                        //transition: all ease-in-out 1s;
                        position: relative;
                        top: 2.4px;
                        left: 4px;
                    }
                }
            }
        }
    }
}

input[type=checkbox]{
    cursor: pointer;
    position: relative;
    width: 15px;
    height: 15px;
    font-size: 14px;
    border: 1px solid #dcdfe6;
    background-color: #fff !important;
    &::after{
        position: absolute;
        top: 0;
        background-color: #fff;
        border: 1px solid #ddd;
        color: #000;
        width: 15px;
        height: 15px;
        display: inline-block;
        visibility: visible;
        padding-left: 0px;
        text-align: center;
        content: ' ';
        border-radius: 3px;
        transition: all linear .1s;
    }
    &:checked::after{
        content: "✓";
        font-size: 12px;
        background-color: #409eff;
        border: 1px solid #409eff;
        transition: all linear .1s;
        color: #fff;
        font-weight: bold;
    }
}
</style>

子組件主要是實現全選和取消全選。由於遞歸組件的原因,子組件拿不到完整的數據,所以接下來的兩個功能:全選後某一個子節點取消選中則父節點取消選中、子節點全選後父節點自覺選中的功能就要在父組件中完成瞭。

講解:

1、設值:

樹形數據必須有pid屬性,用於向上遍歷。

2、方法:

transformData():將層級數據轉為平行數據,避免後期不停的遞歸調用消耗時間,平級數據使用一般的循環即可完成。
selectnode():由子組件傳遞過來的方法,大致分為兩個方向:選中、取消選中。選中時實現功能一:子節點全選後父節點自覺選中;取消選中實現功能二:全選後某一個子節點取消選中則父節點取消選中。
getParentnode():用於實現功能二。子節點取消選中後,根據pid,將在它上面級別的所有父節點列表拿到,再由方法updateParentCheck()將父節點的check值全部改為false
childAllToParent():用於實現功能一。遞歸調用該方法,將操作節點的父節點拿到,根據兄弟節點有相同的pid,拿到兄弟節點的個數,如果兄弟節點中被選中的個數等於兄弟節點的個數,則修改父節點的check值為true,直到到瞭根節點結束遞歸。

  • 這個組件實現起來不是很難,隻要是心細就能很好的完成。
  • 如果後期需要使用某些數據的話,直接掛到data裡就可以。
  • 如果有更好的方法或者存在某些疑問,歡迎小夥伴留言!

附: (global.js => 放於component文件夾下)

import Vue from 'vue';

function capitalizeFirstLetter(string){
    return string.charAt(0).toUpperCase() + string.slice(1);
}
const requireComponent = require.context(
    '.',true,/\.vue$/
    //找到components文件夾下以.vue命名的文件
)
requireComponent.keys().forEach(fileName => {
    const componetConfig = requireComponent(fileName);
    let a = fileName.lastIndexOf('/');
    fileName = '.' + fileName.slice(a);
    const componetName = capitalizeFirstLetter(
        fileName.replace(/^\.\//,'').replace(/\.\w+$/,'')
    )
    Vue.component(componetName,componetConfig.default || componetConfig)
})
  • 由此其實可以實現很多遞歸組件,如側邊欄。
  • 下面我放一個自己寫的側邊欄的動圖,方法比這個樹形組件要簡單些,畢竟不用考慮復選框的值。感興趣的小夥伴們可以試著實踐一下

以上就是本文的全部內容,希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: