VUE實現Studio管理後台(七):樹形結構,文件樹,節點樹共用一套代碼NodeTree

本次介紹的內容,稍稍複雜了一點,用VUE實現樹形結構。目前這個屬性結構還沒有編輯功能,僅僅是展示。明天再開一篇文章,介紹如何增加編輯功能,標題都想好了。先看今天的展示效果:

構建樹必須用到遞歸,使用slot這種直觀明了的方式,已經行不通了。只能通過屬性參數,傳遞一個樹形的數據結構給組件,傳入的數據結構大致是這個樣子:

[          {            title:‘頁面 ’            selected:false,            opened:false,            isFolder:true,            children:[              {                title:'index.html',                selected:false,                opened:false,                icon:"far fa-file-code",              },              {                title:'product.html',                selected:false,                opened:false,                icon:"far fa-file-code",              },            ],          },          {            title:‘樣式’            selected:false,            opened:false,            isFolder:true,            children:[              {                title:'style.css',                selected:false,                opened:false,                icon:"far fa-file-code",              },            ],          },  ]

 

每個節點通過children嵌套子節點。需要注意的是,我們希望這顆樹是可以被編輯的,可以增加、刪除、編輯其節點,所以需要數據的雙向綁定,不能通過普通屬性props傳遞給組件,而是通過v-model傳遞。
RXEditor項目中,只有兩個地方用到了樹形結構,要製作的組件滿足這兩處需求就可以,因為不是構建一個通用類庫,就可以相對簡單些。這兩處地方一處用於展示並編輯文件目錄結構,一處是節點樹,純顯示,沒有編輯功能。文件樹只有葉子節點可以被選中,節點樹所有節點都可以被選中。都是單選,無複選需求。
給這個控件取個大氣的名字,叫NodeTree吧,先看如何使用NodeTree。
第一處調用:

<NodeTree v-model="files"  :openIcon="'fas fa-folder-open'"  :closeIcon="'fas fa-folder'" >  </NodeTree>

 

第二處調用:

<NodeTree v-model="nodes"  :openIcon="'fas fa-caret-down'"  :closeIcon="'fas fa-caret-right'"  :leafIcon="''"  :folderCanbeSelected = 'true'>  </NodeTree>

通過v-model傳遞樹形數據結構,openIcon是節點展開時的圖標,closeIcion是節點閉合時的圖標,leafIcon是沒有子節點時的圖標。這些圖標如果不設置,會有缺省值,是文件夾跟文件的樣子。為了增加可擴展性,樹形數據結構也可以放置圖標,數據結構里的圖標設置優先級高,可以覆蓋控件的設置。明白個原理,想做成什麼樣子,看自己的項目需求。folderCanbeSelected 參數是指含有子節點的節點(比如文件夾)是否可以被選中。

在src目錄下新建tree目錄,放兩個文件:

NodeTree是樹形控件,TreeNode是樹形控件內部的節點,名字稍微優點繞,但是是我喜歡的命名方式。

NodeTree.vue的代碼(省略CSS):

<template>    <div class="node-tree">      <TreeNode v-for = "(node, i) in inputValue"        :key = "i"        v-model = "inputValue[i]"        :openIcon = "openIcon"        :closeIcon = "closeIcon"        :leafIcon = "leafIcon"        :folderCanbeSelected = "folderCanbeSelected"        @nodeSelected = "nodeSelected"        ></TreeNode>    </div>  </template>    <script>  import TreeNode from "./TreeNode.vue"    export default {    name: 'FileTree',    props: {      value: { default: []},      openIcon:{ default: 'fas fa-folder-open'},      closeIcon:{ default: 'fas fa-folder'},      leafIcon:{ default: 'fas fa-file' },      folderCanbeSelected:{ default:false }    },    components:{      TreeNode    },    data() {      return {      };    },      computed:{      inputValue: {          get:function() {            return this.value;          },          set:function(val) {            this.$emit('input', val);          },      },    },      methods: {      nodeSelected(selectedNode){        this.inputValue.forEach(child=>{          this.resetSelected(selectedNode, child)        })        this.$emit('nodeSelected', selectedNode)      },        //遞歸充置選擇狀態      resetSelected(selectedNode, node){        node.selected = (node === selectedNode)        if(node.children){          node.children.forEach(child=>{            this.resetSelected(selectedNode, child)          })        }      }    },  }  </script>

 

這個代碼邏輯很簡單,就是接收外面參數,循環調用TreeNode。要自定義v-model的話,需要用到屬性(props)value,計算屬性inputValue用於修改value,具體原理,可以參考VUE官方文檔。
需要特殊注意的是nodeSelected事件,這個事件在子節點產生,通過冒泡的方式層層往父節點發送,最後到達NodeTree組件。NodeTree組件再通過$emit方法,分發到外層調用組件。
這次實現的控件是單選,排他的,需要遞歸調用resetSelected方法消除其它節點的選中狀態。

TreeNode組件的代碼如下(省略CSS,如需要,請到GIthub獲取):

<template>    <div class="tree-node" :class="inputValue.selected ? 'selected' :''"      >      <div class="node-title"        @click="click"        @contextmenu.prevent = 'onContextMenu'      >        <div  class="node-icon" @click="iconClick">          <i v-show="icon" :class="icon"></i>        </div>        {{inputValue.title}}      </div>      <div v-show="showChild" class="children-nodes">        <TreeNode v-for="(child, i) in inputValue.children"          :openIcon = "openIcon"          :closeIcon = "closeIcon"          :leafIcon = "leafIcon"          :key="i"          :folderCanbeSelected = "folderCanbeSelected"          v-model="inputValue.children[i]"          @nodeSelected = "nodeSelected"        ></TreeNode>      </div>    </div>  </template>    <script>  export default {    name: 'TreeNode',    props: {      value: { default: {}},      openIcon:{ default: 'fas fa-folder-open'},      closeIcon:{ default: 'fas fa-folder'},      leafIcon:{ default: 'fas fa-file' },      folderCanbeSelected:{default: false},    },    data() {      return {      }    },      computed:{      inputValue: {          get:function() {            return this.value;          },          set:function(val) {            this.$emit('input', val);          },      },        icon(){        if(this.hasChildren){          return this.inputValue.opened ? this.openIcon : this.closeIcon        }        return this.inputValue.icon !== undefined ? this.inputValue.icon : this.leafIcon      },        showChild(){        return this.hasChildren && this.inputValue.opened      },        hasChildren(){        return this.inputValue.children           &&this.inputValue.children.length > 0      },    },      methods: {      click(){        if((this.hasChildren && this.folderCanbeSelected) || !this.hasChildren){          this.inputValue.selected = true          this.$emit('nodeSelected', this.inputValue)        }        else {          this.inputValue.opened = !this.inputValue.opened        }      },        iconClick(event){        if(this.hasChildren && this.folderCanbeSelected){          event.stopPropagation()          this.inputValue.opened = !this.inputValue.opened        }      },        nodeSelected(node){        this.$emit('nodeSelected', node)      },        onContextMenu(event){        console.log(event)      }    },    }  </script>

父組件調用時通過v-mode,把整個節點的數據傳入該控件。該組件遞歸調用自身,從而形成樹形結構。三個狀態:opened(展開),closed(閉合),selected(選中)存於model數據中,這樣在控件外部,通過修改model,也可以控制節點狀態。

本功能介紹完畢,代碼請自行到github獲取相應歷史版本:
https://github.com/vularsoft/studio-ui