zoukankan      html  css  js  c++  java
  • 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

     
  • 相关阅读:
    HDU1879 kruscal 继续畅通工程
    poj1094 拓扑 Sorting It All Out
    (转)搞ACM的你伤不起
    (转)女生应该找一个玩ACM的男生
    poj3259 bellman——ford Wormholes解绝负权问题
    poj2253 最短路 floyd Frogger
    Leetcode 42. Trapping Rain Water
    Leetcode 41. First Missing Positive
    Leetcode 4. Median of Two Sorted Arrays(二分)
    Codeforces:Good Bye 2018(题解)
  • 原文地址:https://www.cnblogs.com/idlewater/p/12433270.html
Copyright © 2011-2022 走看看