VUE 實現 Studio 管理後臺(七):樹形結構,檔案樹,節點樹共用一套程式碼 NodeTree

idlewater發表於2020-03-07

本次介紹的內容,稍稍複雜了一點,用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

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章