前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue 多级菜单的实现

Vue 多级菜单的实现

作者头像
Innei
发布2021-12-28 11:40:26
1.7K0
发布2021-12-28 11:40:26
举报
文章被收录于专栏:静之森

最近开发后台,因为不想使用 ElementUI 和其他现成的 UI 框架,于是决定自己做。

碰到的第一个难题就是多级菜单。

因为之前没做过,第一次做起来还是有点难的,最后实现的效果是这样。注意看地址栏。

https://cdn.jsdelivr.net/gh/innei/img-bed@master/20191106204459.gif
https://cdn.jsdelivr.net/gh/innei/img-bed@master/20191106204459.gif

难题一 CSS 的实现

多级菜单的收缩,展开都是使用 CSS 控制,所以要配合 Vue 传值判断是否 active

在父组件加入 activeItem 告诉子组件哪个索引是活跃的。

菜单由于考虑是多级的,所以我们需要封装成一个组件,并且需要使用组件的递归调用自身已实现多级。

父组件

在父组件中,我们可以使用这种形式来记录菜单数据。

js

代码语言:javascript
复制
1data () {
2  return {
3items: [{
4        title: 'Dashboard', // 标题
5        icon: ['fas', 'tachometer-alt'], // fontawesome icon
6        path: '/dashboard' // route path
7      },
8      {
9        title: 'Moment',
10        icon: ['far', 'clock'],
11        path: '/moments'
12      }, {
13        title: '菜单测试',
14        icon: ['fas', 'vial'],
15        path: '/moments1',
16        subItems: [{
17          title: '菜单测试 1',
18          icon: ['fas', 'vial'],
19          path: '/moments',
20          subItems: [{
21            title: '菜单测试 1 - 1',
22            icon: ['fas', 'vial'],
23            path: '/moments',
24            subItems: [{
25              title: '菜单测试 1 - 1 - 1',
26              icon: ['fas', 'vial'],
27              path: '/moments',
28              subItems: [{
29                title: '菜单测试 1 - 1 - 1 - 1',
30                icon: ['fas', 'vial'],
31                path: '/moments',
32              }]
33            }]
34          }]
35        },
36        {
37          title: '菜单测试 2',
38          icon: ['fas', 'vial'],
39          path: '/moments2',
40        }]
41      }
42      ],
43      activeItems: 0
44    }
45}

COPY

封装组件 Item

Item 是一个菜单的每一个小项。他接受来自父组件的 items 数组,然后使用 v-for 渲染每一个子菜单(不是一级菜单,是多级菜单的递归渲染)。在父组件中,也通过 v-for 渲染一级菜单。

js

代码语言:javascript
复制
1// item.vue
2
3<template>
4  <div class="row-item" :class="{active: active}" ref="row-item">
5    <div class="item" @click="handleClick">
6      <div class="icon">
7        <font-awesome-icon :icon="item.icon" />
8      </div>
9      <div class="title">{{item.title}}</div>
10      <div class="down" v-if="hasChild">
11        <font-awesome-icon :icon="['fas','chevron-down']" />
12      </div>
13    </div>
14    <!-- 这里是子菜单 如果存在子菜单才会递归自身渲染 ->
15    <div
16      class="insider"
17      :style="active ? 'max-height: '+ height : ''"
18      ref="insider"
19      v-if="hasChild"
20    >
21      <item
22        :active="activeItems === index ? true : false"
23        :item="item"
24        :index="index"
25        v-for="(item, index) in item.subItems"
26        :key="index"
27        ref="item"
28      />
29    </div>
30  </div>
31</template>
32
33
34export default {
35  name: 'item', // 用于调用自身
36  props: {
37    active: Boolean,
38    item: {
39      type: Object,
40      required: true,
41      validator (val) {
42        return typeof (val.title) === "string"
43          && val.icon instanceof Array
44          && val.icon.length !== 0
45      }
46    },
47    index: Number
48  },
49  data () {
50    return {
51      height: 0,
52      activeItems: 0,
53
54    }
55  },
56}

COPY

子菜单中判断是否活跃一样是通过上级的 activeItem 是否等于 this.index

js

代码语言:javascript
复制
1// methods
2handleClick () {
3      this.$parent.activeItems = this.index
4      if (this.$parent.activeItems === this.index) {
5
6        this.$refs['row-item'].classList.toggle('hide') // 每次点击当前活跃的菜单 如有子菜单 则切换展开和收缩
7      }
8     
9    },

COPY

父组件调用组件

js

代码语言:javascript
复制
1//import item from '@/components/Admin/sidebar/item.vue'
2
3// components: {
4//   item
5//  },
6
7 <item
8            :active="activeItems === index ? true : false"
9            :item="item"
10            :index="index"
11            v-for="(item, index) in items"
12            :key="index"
13/>

COPY

CSS 样式

以上步骤已经实现了对菜单加入和取消 CSS类 activehide

接下来就只要写这两个样式就行了。

这里就不说了,菜单的收缩可以使用 max-height 属性。

难点二 路由

到这,我已经查了很多文章,也想了很久,可能是我比较笨吧,一直没想出来。

最后,我想到了点击菜单时,先判断是不是尾菜单,就是不含子菜单的菜单,不可再下拉。

如果是,就合并上一级菜单的 path,(注意看前面的 path

那么只要在 handleClick 的时候加一层判断和跳转就行了。

js

代码语言:javascript
复制
1// item.vue
2// handleClick(){
3 this.$parent.activeItems = this.index
4      if (this.$parent.activeItems === this.index) {
5
6        this.$refs['row-item'].classList.toggle('hide')
7      }
8      if (!this.hasChild) {
9        let path = this.item.path
10        let item = this.$parent
11        for (; ;) {
12          // path += item.path
13          if (item.item && item.item.path) {
14            path = item.item.path + path
15            item = item.$parent
16          } else break
17        }
18        // console.log(path);
19        path = this.$root.$data.route + path
20        if (path === this.$route.fullPath) {
21          return
22        }
23        this.$router.push(path)
24      }
25}

COPY

最后贴一张想了很久画了很久的手稿,字丑勿喷。

https://raw.githubusercontent.com/Innei/img-bed/master/20191106211233.png
https://raw.githubusercontent.com/Innei/img-bed/master/20191106211233.png
https://raw.githubusercontent.com/Innei/img-bed/master/20191106211326.png
https://raw.githubusercontent.com/Innei/img-bed/master/20191106211326.png

完整代码

vue

代码语言:javascript
复制
1// index.vue
2<template>
3  <div class="bg">
4    <div class="wrap">
5      <div class="side-bar">
6        <div class="title">Moment</div>
7        <div class="items">
8          <item
9            :active="activeItems === index ? true : false"
10            :item="item"
11            :index="index"
12            v-for="(item, index) in items"
13            :key="index"
14          />
15        </div>
16        <div class="user">
17          <div class="block">
18            <img :src="user.avatar" />
19            <div class="username" style="transform: translateY(5px)">{{user.username}}</div>
20            <div class="dot">.</div>
21          </div>
22        </div>
23      </div>
24      <div class="content">
25        <router-view></router-view>
26      </div>
27    </div>
28  </div>
29</template>
30
31<script>
32import { mapGetters } from 'vuex'
33
34import item from '@/components/Admin/sidebar/item.vue'
35export default {
36  name: 'admin',
37  computed: {
38    ...mapGetters(['user']),
39  },
40  components: {
41    item
42  },
43  created () {
44    this.$root.$data.route = '/master'
45  },
46  beforeDestroy () {
47    this.$root.$data.route = null
48    delete this.$root.$data.route
49  },
50  data () {
51    return {
52      path: '/',
53      items: [{
54        title: 'Dashboard',
55        icon: ['fas', 'tachometer-alt'],
56        path: '/dashboard'
57      },
58      {
59        title: 'Moment',
60        icon: ['far', 'clock'],
61        path: '/moments'
62      }, {
63        title: '菜单测试',
64        icon: ['fas', 'vial'],
65        path: '/moments1',
66        subItems: [{
67          title: '菜单测试 1',
68          icon: ['fas', 'vial'],
69          path: '/moments',
70          subItems: [{
71            title: '菜单测试 1 - 1',
72            icon: ['fas', 'vial'],
73            path: '/moments',
74            subItems: [{
75              title: '菜单测试 1 - 1 - 1',
76              icon: ['fas', 'vial'],
77              path: '/moments',
78              subItems: [{
79                title: '菜单测试 1 - 1 - 1 - 1',
80                icon: ['fas', 'vial'],
81                path: '/moments',
82              }]
83            }]
84          }]
85        },
86        {
87          title: '菜单测试 2',
88          icon: ['fas', 'vial'],
89          path: '/moments2',
90        }]
91      }
92      ],
93      activeItems: 0
94    }
95  },
96}
97</script>
98
99<style lang="scss" scoped>
100@import url(https://fonts.googleapis.com/css?family=McLaren&display=swap);
101$deepBg: #1681e1;
102$shallowbg: #1a9cf3;
103.bg {
104  position: fixed;
105  top: 0;
106  left: 0;
107  bottom: 0;
108  right: 0;
109  background-color: $deepBg;
110}
111
112.wrap {
113  position: fixed;
114  top: 0;
115  left: 0;
116  right: 0;
117  bottom: 0;
118  margin: 5rem;
119  background: linear-gradient(to bottom, #1188e8, #16aae7);
120  border-radius: 24px;
121  display: grid;
122  grid-template-columns: 17% auto;
123  box-shadow: 5px 24px 133px rgba(0, 0, 0, 0.3);
124
125  .side-bar {
126    $left-margin: 1.5rem;
127    color: #fff;
128    display: grid;
129    grid-template-rows: 6rem auto 6rem;
130    overflow: hidden;
131    > .title {
132      display: flex;
133      font-family: 'Josefin Sans', sans-serif;
134      justify-content: center;
135      align-items: center;
136      font-size: 1.4rem;
137      user-select: none;
138    }
139
140    .items {
141      margin-left: $left-margin;
142      box-sizing: border-box;
143      overflow: scroll;
144    }
145
146    .user {
147      margin: $left-margin;
148      background: #13afea;
149
150      // background-clip: content-box;
151      border-radius: 12px;
152      position: relative;
153      .block {
154        max-height: 100%;
155        display: grid;
156        grid-template-columns: 50px auto 20px;
157        margin: 0.5rem;
158        user-select: none;
159        * {
160          display: flex;
161          align-items: center;
162          justify-content: center;
163        }
164        .username {
165          font-family: 'Josefin Sans', sans-serif;
166        }
167
168        img {
169          max-width: 30px;
170          border-radius: 50%;
171        }
172      }
173    }
174  }
175  .content {
176    background-color: #fff !important;
177    border-radius: 0 24px 24px 0;
178  }
179}
180</style>

COPY

vue

代码语言:javascript
复制
1// item.vue
2<template>
3  <div class="row-item" :class="{active: active}" ref="row-item">
4    <div class="item" @click="handleClick">
5      <div class="icon">
6        <font-awesome-icon :icon="item.icon" />
7      </div>
8      <div class="title">{{item.title}}</div>
9      <div class="down" v-if="hasChild">
10        <font-awesome-icon :icon="['fas','chevron-down']" />
11      </div>
12    </div>
13    <div
14      class="insider"
15      :style="active ? 'max-height: '+ height : ''"
16      ref="insider"
17      v-if="hasChild"
18    >
19      <item
20        :active="activeItems === index ? true : false"
21        :item="item"
22        :index="index"
23        v-for="(item, index) in item.subItems"
24        :key="index"
25        ref="item"
26      />
27    </div>
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019-11-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 难题一 CSS 的实现
    • 父组件
      • 封装组件 Item
        • 父组件调用组件
          • CSS 样式
          • 难点二 路由
          • 完整代码
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档