Vue3 通过作用域插槽实现树形菜单/嵌套组件

504 阅读1分钟

一、需求来源

工作中需要一种树形菜单组件,经过两天的构思最终通过作用域插槽实现: 此组件将每个节点(插槽名为 node)暴露出来。通过插槽的 attributes 向当前插槽节点传递子项 item(数据对象)和level(层深)参数,在保持组件内部极简的同时支持在数据模型中扩展性。基本达到比较好的封装颗粒度,大家可以在此基础上无限扩展封装具体的业务逻辑。

二、效果图

let list = reactive(
  [{ 
    name:'1 一级菜单',
    isExpand: true,//是否展开子项
    enabled: false,//是否可以响应事件
    child:[
      { name:'1.1 二级菜单',     
        isExpand: true,
        child:[
          { name:'1.1.1 三级菜单', isExpand: true, },
        ]
      },
      { name:'1.2 二级菜单', isExpand: true, },
    ]
  },
  { 
    name:'1.1 一级菜单',
    isExpand: true,
    child:[
      { name:'1.1.1 二级菜单', isExpand: true, },
      { name:'1.1.2 二级菜单', 
        isExpand: false, 
        child:[
          { name:'1.1.2.1 三级菜单', isExpand: true, },
        ]},
    ]
  },]
);

192.168.1.33_57103_(iPhone SE) (2).png

三、使用示例(VTreeNodeDemo.vue)

<template>
  <VTree 
    :list="list"
    :level="level"
  >
    <template #node="slotProps">
      <div class="tree-node">
        {{slotProps.item.name}}{{sufix(slotProps.item)}}
      </div>
    </template>
  </VTree>
</template>


<script setup>
import VTree from '@/components/VTree/VTree.vue';

import { ref, reactive, watch, onMounted, } from 'vue';

let list = reactive(
  [{ 
    name:'1 一级菜单',
    isExpand: true,//是否展开子项
    enabled: false,//是否可以响应事件
    child:[
      { name:'1.1 二级菜单',     
        isExpand: true,
        child:[
          { name:'1.1.1 三级菜单', isExpand: true, },
        ]
      },
      { name:'1.2 二级菜单', isExpand: true, },
    ]
  },
  { 
    name:'1.1 一级菜单',
    isExpand: true,
    child:[
      { name:'1.1.1 二级菜单', isExpand: true, },
      { name:'1.1.2 二级菜单', 
        isExpand: false, 
        child:[
          { name:'1.1.2.1 三级菜单', isExpand: true, },
        ]},
    ]
  },]
);

const sufix = (item) => {
  if (!Reflect.has(item, 'child')) {
    return '';
  }
  return ` (${item.child.length}子项)`;
};

</script>


<style scoped lang='scss'>
.tree-node{
  height: 45px;

  display: flex;
  justify-self: center;
  align-items: center;

  // background-color: green;

  border-bottom: 1px solid #e4e4e4;
}
</style>

四、源码(VTreeNode.vue):

<template>
  <div class="tree" v-for="(item,index) in list" :key="index">
    <slot name="node" :item="item" :level="levelRef">
      <div>{{ item.name }}</div>
    </slot>

    <div class="child" v-show="item.children && canExpand(item)" >
      <VTree :list="item.children">
        <template #node="slotProps">
          <slot name="node" :item="slotProps.item" :level="slotProps.level">
            <div>{{ slotProps.item.name }}</div>
          </slot>
        </template>
      </VTree>
    </div>
  </div>
</template>


<script setup>
import { ref, reactive, watch, computed, onMounted, } from 'vue';

const props = defineProps({
  list: {
    type: Array,
    default: () => [],
    validator: (val) => {
      return Array.isArray(val) && val.every(e => Reflect.has(e, 'name'));
    }
  },
  indent: {
    type: String,
    default: "30px",
  }
});

const canExpand = (item) => {
  return Reflect.has(item, 'isExpand') && item.isExpand;
};

</script>


<style scoped lang='scss'>
.tree {
  font-size: 20;
}

.child {
  padding-left: v-bind(indent);
}

</style>

VTree.vue

VTreeDemo.vue