vue、iview動態菜單(可摺疊)

  vue項目與iview3實現可摺疊動態菜單。javascript

  菜單實現一下效果:css

  1. 動態獲取項目路由生成動態三級菜單導航
  2. 可摺疊展開
  3. 根據路由name默認打開子目錄,選中當前項
  4. 自動過濾須要隱藏的路由(例:登錄)
  5. 在手機端首次進入自動收起所有的導航欄,pc端進入導航欄展開

  爭議之處:當一級菜單項只有一個子元素時,只會顯示一級菜單項不會展開下拉列表,設置子元素的顯示(hidden)將無效。例如:主頁html

  demo效果如圖顯示,vue

  菜單使用iview3實現,菜單組件sider.vue的代碼以下:java

<template>
    <Menu ref="asideMenu" width="100%" accordion :theme="theme1" :open-names="openItem" :active-name="activeName">
        <!-- 動態菜單 -->
        <div v-for="(item, index) in menuItems" :key="index" v-if="!app.isCollapsed && !item.meta.hidden">
            <Submenu v-if="item.children && item.children.length>1 && !app.isCollapsed" :name="item.name">
                <template slot="title">
                    <Icon :size="item.size" :type="item.type"/>
                    <span>{{item.text}}</span>
                </template>
                <div v-for="(subItem, i) in item.children" :key="index + i">
                    <Submenu v-if="subItem.children" :name="subItem.name">
                        <template slot="title">
                            <Icon :size="subItem.size" :type="subItem.type"/>
                            <span class="text-over">{{subItem.text}}</span>
                        </template>
                        <MenuItem class="menu-level-3" v-for="(threeItem, k) in subItem.children" :name="threeItem.name" :to="item.path+ '/' + subItem.path+ '/' + threeItem.path" :key="index + i + k">
                            <Icon :size="threeItem.size" :type="threeItem.type"/>
                            <span>{{threeItem.text}}</span>
                        </MenuItem>
                    </Submenu>
                    <MenuItem v-else :name="subItem.name" :to="item.path+ '/' + subItem.path">
                        <Icon :size="subItem.size" :type="subItem.type"/>
                        <span class="text-over">{{subItem.text}}</span>
                    </MenuItem>
                </div>
            </Submenu>
            <MenuItem v-else :name="getName(item)" :to="item.path">
                <Icon :size="item.size" :type="item.type" />
                <span>{{item.text}}</span>
            </MenuItem>
        </div>

    <!-- 摺疊菜單 -->
        <div class="center-right" v-if="app.isCollapsed">
            <div v-for="(item,index) in menuItems" :key = "index">
                <Tooltip :content="item.text"  placement="right" theme="light">
                    <Dropdown style="margin-left: 20px" trigger="click" placement="right-end" @on-click="toRoute">
                        <div class="collapsed-icon" @click="goRoute(item)"><Icon :type="item.type" size="18"></Icon></div>
                            <DropdownMenu slot="list" class="" v-if="item.children && item.children.length>1">
                                <div v-for="(secItem,i) in item.children" :key="i">
                                    <Dropdown placement="right-start" v-if="secItem.children&& secItem.children.length>0">
                                        <DropdownItem name=''>
                                            {{secItem.text}}
                                            <Icon type="ios-arrow-forward"></Icon>
                                        </DropdownItem>
                                        <DropdownMenu slot="list">
                                            <DropdownItem v-for="(tt, t) in secItem.children" :key="t" :name="tt.name" >{{tt.text}}</DropdownItem>
                                        </DropdownMenu>
                                    </Dropdown>
                                <DropdownItem v-else :name="secItem.name">{{secItem.text}}</DropdownItem>
                                </div>
                            </DropdownMenu>
                    </Dropdown>
                </Tooltip>
            </div>
        </div>
    </Menu>
</template>
<script>
import { mapState, mapGetters, mapMutations } from 'vuex'
export default {
    data() {
        return {
            isShowAsideTitle: true,
            theme1: 'dark',
            openItem: [],
            activeName: ''
        }
    },
    computed: {
        ...mapState(['app', 'user']),
        menuItems(state) {
            return this.showMemuList(this.$router.options.routes)
        },
        // 獲取的store中isCollapsed
        getIsCollapsed() {
            return this.$store.state.app.isCollapsed
        }
    },
    watch: {
        // 監聽isCollapsed,當菜單展開時,默認當前打開,選中
        getIsCollapsed() {
            if (!this.app.isCollapsed) {
                this.$nextTick(() => {
                    this.$refs.asideMenu.updateOpened()
                    this.$refs.asideMenu.updateActiveName()
                })
            }
        },
        // 監聽路由,展開子目錄,更新當前選擇項
        $route() {
            this.openSideList()
            this.activeName = this.$route.name
        },
        // 監聽展開的子目錄,更新
        openItem() {
            this.$nextTick(() => {
                this.$refs.asideMenu.updateOpened()
            })
        }
    },
    methods: {
        // 篩選須要顯示的列表
        showMemuList(list) {
            let newArr = [];
            list.map((item, index, arr) => {
                // console.log(item, index, arr)
                if (!item.meta.hidden) {  // 顯示
                    this.filterObj(item)
                    newArr.push(item)
                }
            })
            return newArr
        },
        filterObj(obj) {
            if (obj.children && obj.children.length > 1) {
                obj.children = this.showMemuList(obj.children)
            }
        },
        toRoute(name) {
            if (name !== '') {
                this.$router.push({ name: name })
            }

        },
        goRoute(item) {
            if (item.children && item.children.length == 1) {
                this.$router.push({ path: item.path })
            }

        },
        getName(item) {
            return item.children[0].name
        },
        // 獲取到展開的子目錄  例:['/component']
        openSideList() {
            this.openItem = []
            if (this.$route.matched.length > 2) {
                this.openItem.push(this.$route.matched[0].name)
                this.openItem.push(this.$route.matched[1].name)
            } else
                this.openItem.push(this.$route.matched[0].name)
        }
    },
    mounted() {
        this.openSideList()
        this.activeName = this.$route.name
    }
}
</script>
<style>
.center-right {
  float: right;
}
.collapsed-icon {
  width: 78px;
  height: 78px;
  text-align: center;
  line-height: 78px;
}
.text-over {
  display: inline-block;
  width: 90px;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
  vertical-align: middle;
}
/* sider */
.layout {
  border: 1px solid #d7dde4;
  background: #f5f7f9;
  position: relative;
  border-radius: 4px;
  overflow: hidden;
}
.layout-header-bar {
  background: #fff;
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
}
.layout-logo-left {
  width: 90%;
  height: 30px;
  background: #5b6270;
  border-radius: 3px;
  margin: 15px auto;
}
.menu-icon {
  transition: all 0.3s;
}
.rotate-icon {
  transform: rotate(-90deg);
}
.ivu-menu {
  white-space: nowrap;
}
.ivu-menu-item span {
  display: inline-block;
  overflow: hidden;
  width: 69px;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
  transition: width 0.2s ease 0.2s;
}
.ivu-menu-item i {
  transform: translateX(0px);
  transition: font-size 0.2s ease, transform 0.2s ease;
  vertical-align: middle;
  font-size: 16px;
}
.ivu-layout-sider-collapsed .ivu-menu-submenu-title span,
.ivu-layout-sider-collapsed .ivu-menu-item span {
  display: inline-block;
  width: 0px;
  overflow: hidden;
  transition: width 0.2s ease;
}
.ivu-layout-sider-collapsed
  .ivu-menu-submenu-title
  .ivu-menu-submenu-title-icon {
  display: none;
}
.ivu-layout-sider-collapsed .ivu-menu-submenu-title i,
.ivu-layout-sider-collapsed .ivu-menu-item i {
  transform: translateX(5px) translateY(-15px) scale(1.4);
  transition: font-size 0.2s ease 0.2s, transform 0.2s ease 0.2s;
  vertical-align: middle;
  font-size: 22px;
}
</style>

  

  在sider.vue中,展開的菜單與摺疊起來的菜單是分開寫的,而後根據store中的狀態判斷是否展開收起。經過showMemuList()和filterObj()兩個函數將不須要顯示的路由過濾隱藏。android

  在layout.vue總體佈局文件中引用sider組件,內容以下:ios

<style lang="scss">
.height100 {
  height: 100%;
}
.main-bg {
  width: 100%;
  height: 100%;
}
.main-header-bg {
  overflow: hidden;
  position: relative;
  .header-theme {
    position: absolute;
    z-index: 9;
    height: 40px;
    line-height: 40px;
    top: 12px;
    right: 30px;
    display: flex;
    justify-content: flex-end;
    .theme {
      width: 40px;
      height: 40px;
      text-align: center;
      // @include bg_color($background_color_theme);
    }
  }
}
</style>
<template>
    <div class="layout height100">
        <Layout class="height100">
            <Sider ref="side1" hide-trigger collapsible :collapsed-width="width" v-model="app.isCollapsed">
                <!-- 引入菜單組件 -->
                <SideMenu></SideMenu>
            </Sider>
            <Layout>
                <Header :style="{padding: 0}" class="layout-header-bar">
                    <Row class="main-header-bg">
                        <Icon @click.native="collapsedSider" :class="rotateIcon" :style="{margin: '0 20px'}" type="md-menu" size="24"></Icon>
                        <i-col :xs="3" class="header-theme">
                            <div class="theme" @click="toRoute('theme',{})">theme</div>
                        </i-col>
                    </Row>
                </Header>
                <Content :style="{margin: '20px', background: '#fff', minHeight: '360px',overflow:'auto'}">
                    <div class="main-bg">
                        <router-view></router-view>
                    </div>
                </Content>
                <Footer>博客園地址:https://i.cnblogs.com/posts</Footer>
            </Layout>
        </Layout>
    </div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
import SideMenu from '@/components/layout/sider'
export default {
    data() {
        return {
            
        }
    },
    components: {
        SideMenu
    },
    computed: {
        ...mapState(['app']),
        ...mapGetters(['rotateIcon', 'menuitemClasses']),
        width(state) {
            console.log(state.app.pc ? 78 : 0, state.app.pc)
            return state.app.pc ? 78 : 0
        }
    },
    methods: {
        ...mapMutations(['collapsed']),
        collapsedSider() {
            this.collapsed()
        }
    },
    mounted(){
        
    }
}
</script>

  在layout.vue文件中,重要的部分是組件引入部分以及收起展開的邏輯部分。導航菜單的展開和收起經過操做。動畫效果經過store中getters實現,因此在layout.vue中引入了輔助函數。width()方法經過獲得store中的pc值判斷收起時的寬度。web

  store中使用modules中app.js,代碼以下:vuex

const moduleApp = {
    state: { 
        isCollapsed: false,  // 側邊欄是否摺疊,默認不折疊
        pc:true  // 是否pc端打開
    },
    mutations: {
      collapsed (state) {
        // 這裏的 `state` 對象是模塊的局部狀態
        state.isCollapsed = !state.isCollapsed
      },
      // 判斷是否pc端,若不是pc端,將自動收起菜單
      isPC(state,boo){
          state.pc = boo;
          if(!boo + '' == 'true'){
              state.isCollapsed = true
          }
      }
    },
    getters: {
        rotateIcon (state, getters, rootState) {
            return [
                'menu-icon',
                state.isCollapsed ? 'rotate-icon' : ''
            ];
        },
        menuitemClasses (state, getters, rootState) {
            return [
                'menu-item',
                state.isCollapsed ? 'collapsed-menu' : ''
            ]
        }
    },
    actions:{
        // 測試actions
        incrementIfOddOnRootSum ({ state, commit, rootState },param) {
            console.log(state,rootState,param)
              commit('increment',param.num)
        }
    }
  }
export default moduleApp

  在建立vuex實例時,經過模塊化引入app.js爲app。windows

  導航菜單是由路由動態生成,如下是router.js路由文件的代碼:

import layout from '@/pages/layout'
import home from '@/pages/home'

let routes = [
    {
      path: '/',
      name: 'layout',
      size:18, // 圖標大小
      type: 'md-home', // icon類型
      text: '主頁', // 文本內容
      component: layout,
      redirect: '/page1',
      meta:{
        hidden:false
      },
      children: [
        {
          path: 'page1',
          name: 'page1',
          size:18,
          type: 'ios-paper',
          text:'首頁',
          meta: {
            hidden:true
          },
          component: () => import('@/components/HelloWorld.vue')
        }
      ]
    },
    {
      path:'/login',
      name:'login',
      meta:{
        hidden:true
      },
      component:()=>import('@/components/HelloWorld.vue')
    },
    {
      path: '/component',
      name: 'component',
      size:18, // 圖標大小
      type: 'md-cube', // icon類型
      text: '組件', // 文本內容
      component: layout,
      meta: {
        hidden:false
      },
      children: [
        {
          path: 'other',
          name: 'other',
          // size:18, // 圖標大小
          type: 'ios-aperture', // icon類型
          text: '二級菜單', // 文本內容
          component: home,
          meta: {
            hidden:false
          },
          children: [
            {
              path: 'theme',
              name: 'theme',
              // size:18, // 圖標大小
              type: 'ios-brush', // icon類型
              text: 'theme', // 文本內容
              meta: {
                hidden:false
              },
              component: () => import('@/components/theme.vue')
            },
          ]
        },
        {
          path: 'page2',
          name: 'page2',
          // size:18, // 圖標大小
          type: 'md-cafe', // icon類型
          text: '單選框自定義樣式', // 文本內容
          meta: {
            hidden:false
          },
          component: () => import('@/components/input.vue')
        }
      ]
    }
  ]
export default routes

  某個路由是否在導航菜單中顯示,經過meta中hidden控制,true表示隱藏,false表示不隱藏。layout和hone爲模板頁,layout是上面layout.vue文件,layout是一級、二級菜單的模板頁,home是三級菜單的模板。引入通常的顯示頁面經過路由懶加載的方式,當要打開對應頁面時,在加載頁面。當一級菜單項只有一個子元素時,只會顯示一級菜單項不會展開下拉列表,設置子元素的顯示(hidden)將無效。例如:主頁的children只有一個子元素,這時,設置這個子元素的hidden爲false,頁面中也不會出現子菜單顯示此項。由於判斷時一級菜單項的子元素長度須要大於一纔會出現子菜單。

   另外,我在app.vue中判斷是否pc端,而後修改store中的值。

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<script>
import * as util from '@/libs/util'
import { mapMutations } from 'vuex'
export default {
  name: 'App',
  methods: {
    ...mapMutations(['isPC']),
    browserRedirect(state) {
      var sUserAgent = navigator.userAgent.toLowerCase();
      var bIsIpad = sUserAgent.match(/ipad/i) == "ipad";
      var bIsIphoneOs = sUserAgent.match(/iphone os/i) == "iphone os";
      var bIsMidp = sUserAgent.match(/midp/i) == "midp";
      var bIsUc7 = sUserAgent.match(/rv:1.2.3.4/i) == "rv:1.2.3.4";
      var bIsUc = sUserAgent.match(/ucweb/i) == "ucweb";
      var bIsAndroid = sUserAgent.match(/android/i) == "android";
      var bIsCE = sUserAgent.match(/windows ce/i) == "windows ce";
      var bIsWM = sUserAgent.match(/windows mobile/i) == "windows mobile";
      if (bIsIpad || bIsIphoneOs || bIsMidp || bIsUc7 || bIsUc || bIsAndroid || bIsCE || bIsWM) {
        this.isPC(false)
      } else {
        this.isPC(true)
      }
    }
  },
  mounted() {
    this.browserRedirect()
  }
}
</script>

<style lang='scss'>
html,
body {
  width: 100%;
  height: 100%;
}
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  height: 100%;
}
</style>

  固然,還可添加其餘操做,例如添加權限,動態從後端獲取路由等。

  • 總結

  其中在實現導航菜單展開收起(isCollapsed)操做時,我選擇將isCollapsed的值存儲在store中。目的是,若是之後要進入某一個頁面須要將菜單收起來時,能夠直接操做store中的值實現。若是不會有這種需求或者對store還不太熟悉的話,能夠直接將isCollapsed當作參數傳進菜單組件。

  vue的一個很大的特色是組件化,上面關於展開和收起的菜單我寫在了一個組件中。也能夠將它們拆成組件,這樣不顯的累贅。

  多有考慮不到之處,歡迎多多指教。

相關文章
相關標籤/搜索