vue、iview动态菜单(可折叠)

vue

  vue项目与iview3实现可折叠动态菜单。

  菜单实现一下效果:

  1. 动态获取项目路由生成动态三级菜单导航
  2. 可折叠展开
  3. 根据路由name默认打开子目录,选中当前项
  4. 自动过滤需要隐藏的路由(例:登陆)
  5. 在手机端首次进入自动收起全部的导航栏,pc端进入导航栏展开

  争议之处:当一级菜单项只有一个子元素时,只会显示一级菜单项不会展开下拉列表,设置子元素的显示(hidden)将无效。例如:主页

  demo效果如图显示,

  菜单使用iview3实现,菜单组件sider.vue的代码如下:

<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()两个函数将不需要显示的路由过滤隐藏。

  在layout.vue整体布局文件中引用sider组件,内容如下:

<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值判断收起时的宽度。

  store中使用modules中app.js,代码如下:

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。

  导航菜单是由路由动态生成,以下是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的一个很大的特点是组件化,上面关于展开和收起的菜单我写在了一个组件中。也可以将它们拆成组件,这样不显的累赘。

  多有考虑不到之处,欢迎多多指教。

以上是 vue、iview动态菜单(可折叠) 的全部内容, 来源链接: utcz.com/z/376182.html

回到顶部