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

     
  • 相关阅读:
    使用yarn来替代npm
    React及Nextjs相关知识点小结
    appstore-react v2.0—redux-actions和redux-saga的应用
    开机SystemServer到ActivityManagerService启动过程分析
    java 读取气象专业格式NetCDF文件
    maven项目对于maven远程仓库没有资源的解决办法
    leaflet 使用kriging.js实现前端自定义插值
    leaflet 使用高德地图实例
    uni-app上使用leaflet地图的解决方案
    MySQL创建新用户并且赋予权限
  • 原文地址:https://www.cnblogs.com/idlewater/p/12433270.html
Copyright © 2011-2022 走看看