手写一个级联菜单

1,785 阅读2分钟

现在实现一个级联菜单功能,不想用element实现,因为怕后面要加功能,基于element可能后面改起来比较麻烦,比较喜欢从头到尾由自己控制的感觉。

要考虑的几个事情:

  1. 填充到级联菜单的数据结构怎么设计(参考element的tree)
  2. 怎么实现级联这个效果(递归调用自己这个组件)
  3. 每个菜单怎么知道自己在第几级中的第几个(props一级一级传进去)
  4. 怎么把点击了这个菜单的信息传递出去?(通过路由)
  5. 怎么实现下一级相对于上一级的缩进(css)

废话不多说,直接上代码:

首先是要传入级联菜单的数据结构:

//这里path与后面的组件名字和路由的name对应,name为菜单中每一条要显示的名字,children为子级菜单,内容与父级一样
menuDetail:[{
  path:'...',
  name:'...',
  children:[...]
},{...}]

总体设计思路就是级联菜单总体创建一个组件cascade-menu,这个相当于总体管理,菜单中的每一条创建一个组件cascade-item(确定后面这点也是来回折腾了一小会),cascade-item设计的思路关键是每个item都要知道他自己在第几级的第几个,否则没办法做到激活和数据传递。通过menu管理一个记录激活的item的数组,这个数组的第n项代表第n层级,每项的数字代表激活第几个item,点击菜单时item除了要改变css,还要向menu传递信息改变这个数组,表示激活的菜单变了,改变了后因为props,item也知道了。

使用方法就是在别的组件中使用自己设计的级联菜单,传入填充的数据,很简单

<cascade-menu :dataContent="menuDetail"></cascade-menu>

先来看看总体组件的设计

//偷懒,有的东西显而易见的我就没写了
<template>
  <div>
    <cascade-item ...这里省略了很多传入的props和响应
                  v-for="(item,index) of dataContent">
    </cascade-item>
  </div>
</template>

<script>
import CascadeItem from './CascadeItem';
export default{
  props:['dataContent'],
  data(){
    return{
      //记录级联菜单的每一层的激活index,默认激活index为-1
      //采用数组的形式,数组中第n项对应级联菜单的第n层
      activeIndexArray:[-1],
    }
  },
  methods:{
    //把子组件传上来的激活的索引数组赋给activeIndexArray
    onChangeActiveIndex(payload){
      ...
    },
    /* 递归查找函数,查找在对象中path为要找的内容的那一项
    content 待查找的内容
    item 要查找的项
    index 当前顺序索引
    track 经过的路径数组
    return 查找到的路径数组,如果没找到则返回空数组 */
    findItem(content,item,index,track){
      ...
    },
    //去dataConten里找当前路由name对应的项,并赋给activeIndexArray
    changeActiveIndexArray(itemToFind){
      ...
    }
  },
  mounted(){
    //根据路由,在从别的页面进入或者刷新时对应的菜单项展开激活
  },
  watch:{
    //监控路由,如果变化则把变化后的路由name传入this.changeActiveIndexArray运行
  }
}
</script>

其次是级联菜单单个item的设计

//偷懒,有的东西显而易见的我就没写了
<template>
  <div ref="menu" class="menu-name">
    <!--自身名字-->
    <div @click="onClickMenu" ref="name"
       :class="{...}">
                {{dataFilling.name}}
                <i ref="arrow" class="el-icon-arrow-down zm-arrow" v-if="dataFilling.children"></i>
    </div>
    <!--子节点-->
    <div ref="children" v-if="dataFilling.children">
      <cas-cad-item ...这里省略了很多传入的props和响应
              v-for="(item1,index1) of dataFilling.children">
      </cas-cad-item>
    </div>
  </div>
</template>

<script>
export default{
  /* 
  说明:
  dataFilling:本身填充数据
  levelCount:本身在第几层
  index:本身顺序
  parentIndex:记录从最高级到本层级的顺序数组,用于级联菜单改变颜色
  activeIndexArray:记录级联菜单的每一层的激活index
  */
  props:['dataFilling','levelCount','index','parentIndex','activeIndexArray'],
  computed:{
    level:{
      //返回levelCount
    },
    //是否选中
    activeSelf:function () {
      //这里有小坑,一个是数组不能直接比较相等,因为其名字相当于指针,其实是不等的
      //第二点是要把levelArray(长度为n)和总的记录激活的前n项形成的数组比较相等,
      //要不然会出现只有最子级激活,父级不激活的情况
      return this.levelArray.toString()===this.activeIndexArray.slice(0,this.levelArray.length).toString();
    },
    //记录层级的数组
    levelArray:function () {
      //把this.index加入this.parentIndex数组
    }
  },
  data(){
    return{
      //是否展开子菜单
      spreadChildren:false
    }
  },
  methods:{
    onChangeActiveIndex(payload){
      //往上传递
      this.$emit('changeActiveIndex',payload);
    },
    //修改箭头和子菜单打开收起的css
    modifyChildrenCSS(){
     /*  这里不能用document.querySelector('i')
      要用$refs引用dom
      主要方法就是通过控制添加或删除一个active类名来实现箭头的旋转和子菜单的收放 */
    },
    //菜单点击相应函数
    onClickMenu(){
      /* 存在子级则将spreadChildren取反
      没有子级则说明需要改路由
      this.$emit('changeActiveIndex',payload);
      this.modifyChildrenCSS(); */
    }
  },
  mounted(){
    let name=this.$refs.name;
    name.style.paddingLeft=`${(this.level+1)*20}px`;
    
    //这里要实现在从别的页面进入或者刷新时对应的菜单项展开激活的功能
    //这里为什么要加延时
    //因为item组件先于menu组件渲染完,item组件渲染完的时候menu的activeIndexArray还没有来得及改变过来
    setTimeout(()=>{
      if(this.levelArray.toString()===this.activeIndexArray.slice(0,this.levelArray.length).toString()){
        this.spreadChildren=true;
        this.modifyChildrenCSS();
      }
    },0)
  }
}
</script>

<style scoped>
/* 这里本来是想写
.menu-name{
  padding-left:20px;
}
这样确实可以实现每个子级都比上一级缩进20px,但是背景色也跟着缩进了
因为其div是跟着缩进的,也尝试了很多方法,最后是放在mounted里了,
其实不想把css和js搅和到一起,但是没办法 */
</style>

总的就这么多,后续如果有新的需求就再改进,其实这里已经有一个缺陷了,就是应该设计和element的树结构一样,填充数据的key是可以自己指定的,我这里是限定死了。