vue动态菜单及tag切换
刚刚接触项目的小伙伴 几乎都接触不到这一块的 因为入职 公司要么有骨干 要么是现有项目维护 所以 对于动态菜单 很好奇 今天带着小伙伴们一起来看看吧可能有些人接触过 只是看看别人写的代码 觉得都没有问题没有实际动手去做过 这就应对了那句 “看花容易绣花难” 其实我开始的时候 也是这种心理 直到我自己动手的时候 发现里面有很多坑 如果不涉及到tag的切换 估计看看element官网就知道了 关键就是
刚刚接触项目的小伙伴 几乎都接触不到这一块的 因为入职 公司要么有骨干 要么是现有项目维护 所以 对于动态菜单 很好奇 今天带着小伙伴们一起来看看吧
可能有些人接触过 只是看看别人写的代码 觉得都没有问题 没有实际动手去做过 这就应对了那句 “看花容易绣花难” 其实我开始的时候 也是这种心理 直到我自己动手的时候 发现里面有很多坑 如果不涉及到tag的切换 估计看看element官网就知道了 关键就是带有tag切换 导致很多小伙伴无从下手
现在很多公司的项目都是基于vue-element-admin二开的 虽然vue-element-admin是经典 但是毕竟时间太久了 里面也存在一些缺陷 比如项目过大 所以想弄明白的小伙伴还是老老实实的自己走一遍
先看下这个项目目录结构 因为就是个demo 所以也没有全部都建出来
首先先和小伙伴分析一下 所谓的动态路由 不完全都是动态的 其实是有两部分组成 一部分是静态的 我们称之为静态路由(constantRoutes)例如login、404等 还有一部分是根据权限后台返回的 我们才称为动态路由(asyncRoutes)
其次既然是跟路由有关的 我们肯定要用到状态存储器 vuex
先看下路由接口 我也没有模拟数据 就暂时先写死的
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
import Layout from "@/layout/index"
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/redirect',
component: Layout,
hidden: true,
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/redirect/index')
}
]
}
];
export const asyncRoutes =[
{
path: '/',
component: Layout,
redirect: 'dashboard',
children: [
{
path: '/dashboard',
component: () => import('@/views/dashboard/index'),
name: 'Dashboard',
meta: { title: '首页', icon: 'dashboard', affix: true }
}
]
},
{
path: '/card',
component: Layout,
children: [
{
path: '/card/index',
component: () => import('@/views/card/index'),
name: 'card',
meta: { title: '购物车', icon: 'icon', noCache: true }
}
]
},
{
path: '/phone',
component: Layout,
meta: {
title: '数码手机'
},
children: [
{
path: '/phone/apply',
component: () => import('@/views/phone/applyPhone/index'),
name: 'apply',
meta: { title: '苹果手机', icon: 'icon', noCache: true }
},
{
path: '/phone/hw',
component: () => import('@/views/phone/hwPhone/index'),
name: 'hw',
meta: { title: '华为手机', icon: 'icon', noCache: true }
}
]
}
]
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes: constantRoutes,
});
export default router;
main.js 其实我这不是完整的 因为没有调用接口 数据暂时写死的 所以在touter.beforeEach里面 没有做token的判断 不过不影响后续流程
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.config.productionTip = false;
Vue.use(ElementUI)
router.beforeEach(async(to, from, next) => {
if (to.path === '/login') {
next()
} else {
const hasRoutes = store.getters.routes && store.getters.routes.length > 0
if (hasRoutes) {
next()
} else {
try {
const accessRoutes = await store.dispatch('permission/generateRoutes', ["admin"])
router.addRoutes(accessRoutes)
next({ ...to, replace: true })
} catch (error) {
next(`/login?redirect=${to.path}`)
}
}
}
})
new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");
侧边栏
<template>
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
background-color="red"
text-color="black"
:unique-opened="false"
active-text-color="yellow"
:collapse-transition="false"
router
mode="vertical"
>
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</template>
<script>
import { mapGetters } from 'vuex'
import sidebarItem from "./sidebarItem.vue"
export default {
name: '',
components:{sidebarItem},
computed: {
...mapGetters([
'routes',
]),
activeMenu() {
const route = this.$route
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
isCollapse() {
return !this.sidebar.opened
}
},
mounted(){
}
}
</script>
<style lang='scss' scoped>
.sidebar{
height:100vh;
width:200px;
background: red;
}
</style>
侧边栏组件
<template>
<div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children,item)">
<el-menu-item :index="onlyOneChild.path">
<span slot="title">{{onlyOneChild.meta.title}}</span>
</el-menu-item>
</template>
<template v-else>
<el-submenu ref="subMenu" :index="item.path" popper-append-to-body>
<template slot="title">
<span>{{item.meta.title}}</span>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="child.path"
/>
</el-submenu>
</template>
</div>
</template>
<script>
export default {
name: 'sidebarItem',
props: {
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
data() {
this.onlyOneChild = null
return {}
},
mounted(){
},
methods: {
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
this.onlyOneChild = item
return true
}
})
if (showingChildren.length === 1) {
return true
}
if (showingChildren.length === 0) {
this.onlyOneChild = { ... parent}
return true
}
return false
}
}
}
</script>
tags
<template>
<div id="tags-view-container" class="tags-view-container">
<scroll-pane scroll-paneref="scrollPane" ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
<router-link
v-for="tag in visitedViews"
ref="tag"
:key="tag.path"
:class="isActive(tag)?'active':''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
class="tags-view-item"
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
@contextmenu.prevent.native="openMenu(tag,$event)"
>
{{ tag.title }}
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
</router-link>
</scroll-pane>
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)">Refresh</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">Close</li>
<li @click="closeOthersTags">Close Others</li>
<li @click="closeAllTags(selectedTag)">Close All</li>
</ul>
</div>
</template>
<script>
import scrollPane from "./ScrollPane.vue"
import path from "path"
export default {
name: '',
components:{scrollPane},
data(){
return{
visible:false,
top: 0,
left: 0,
selectedTag: {},
affixTags: []
}
},
watch:{
$route() {
console.log(987)
this.addTags()
this.moveToCurrentTag()
},
visible(value) {
if (value) {
document.body.addEventListener('click', this.closeMenu)
} else {
document.body.removeEventListener('click', this.closeMenu)
}
}
},
computed:{
visitedViews() {
return this.$store.state.tagsView.visitedViews
},
routes() {
return this.$store.state.permission.routes
}
},
methods:{
isActive(route) {
return route.path === this.$route.path
},
isAffix(tag) {
return tag.meta && tag.meta.affix
},
addTags() {
const { name } = this.$route
if (name) {
this.$store.dispatch('tagsView/addView', this.$route)
}
return false
},
filterAffixTags(routes,basePath = '/'){
let tags = []
routes.forEach(route => {
if (route.meta && route.meta.affix) {
const tagPath = path.resolve(basePath, route.path)
tags.push({
fullPath: tagPath,
path: tagPath,
name: route.name,
meta: { ...route.meta }
})
}
if (route.children) {
const tempTags = this.filterAffixTags(route.children, route.path)
if (tempTags.length >= 1) {
tags = [...tags, ...tempTags]
}
}
})
return tags
},
moveToCurrentTag() {
const tags = this.$refs.tag
this.$nextTick(() => {
for (const tag of tags) {
if (tag.to.path === this.$route.path) {
this.$refs.scrollPane.moveToTarget(tag)
if (tag.to.fullPath !== this.$route.fullPath) {
this.$store.dispatch('tagsView/updateVisitedView', this.$route)
}
break
}
}
})
},
refreshSelectedTag(view) {
this.$store.dispatch('tagsView/delCachedView', view).then(() => {
const { fullPath } = view
this.$nextTick(() => {
this.$router.replace({
path: '/redirect' + fullPath
})
})
})
},
closeSelectedTag(view) {
this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
if (this.isActive(view)) {
this.toLastView(visitedViews, view)
}
})
},
closeOthersTags() {
this.$router.push(this.selectedTag)
this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
this.moveToCurrentTag()
})
},
closeAllTags(view) {
this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
if (this.affixTags.some(tag => tag.path === view.path)) {
return
}
this.toLastView(visitedViews, view)
})
},
toLastView(visitedViews, view) {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
this.$router.push(latestView.fullPath)
} else {
if (view.name === 'Dashboard') {
this.$router.replace({ path: '/redirect' + view.fullPath })
} else {
this.$router.push('/')
}
}
},
openMenu(tag, e) {
const menuMinWidth = 105
const offsetLeft = this.$el.getBoundingClientRect().left
const offsetWidth = this.$el.offsetWidth
const maxLeft = offsetWidth - menuMinWidth
const left = e.clientX + 15
if (left > maxLeft) {
this.left = offsetLeft
} else {
this.left = left
}
this.top = e.clientY
this.visible = true
this.selectedTag = tag
},
closeMenu() {
this.visible = false
},
handleScroll() {
this.closeMenu()
},
initTags() {
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
for (const tag of affixTags) {
if (tag.name) {
this.$store.dispatch('tagsView/addVisitedView', tag)
}
}
},
},
mounted(){
this.initTags()
this.addTags()
}
}
</script>
<style lang='scss' scoped>
.tags-view-container {
height: 34px;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
.tags-view-wrapper {
.tags-view-item {
display: inline-block;
position: relative;
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
background-color: #42b983;
color: #fff;
border-color: #42b983;
&::before {
content: '';
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 2px;
}
}
}
}
.contextmenu {
margin: 0;
background: #fff;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
}
</style>
scrollPane组件
<template>
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
<slot />
</el-scrollbar>
</template>
<script>
const tagAndTagSpacing = 4 // tagAndTagSpacing
export default {
name: 'ScrollPane',
data() {
return {
left: 0
}
},
computed: {
scrollWrapper() {
return this.$refs.scrollContainer.$refs.wrap
}
},
mounted() {
this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
},
beforeDestroy() {
this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
},
methods: {
handleScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40
const $scrollWrapper = this.scrollWrapper
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
},
emitScroll() {
this.$emit('scroll')
},
moveToTarget(currentTag) {
const $container = this.$refs.scrollContainer.$el
const $containerWidth = $container.offsetWidth
const $scrollWrapper = this.scrollWrapper
const tagList = this.$parent.$refs.tag
let firstTag = null
let lastTag = null
if (tagList.length > 0) {
firstTag = tagList[0]
lastTag = tagList[tagList.length - 1]
}
if (firstTag === currentTag) {
$scrollWrapper.scrollLeft = 0
} else if (lastTag === currentTag) {
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
} else {
const currentIndex = tagList.findIndex(item => item === currentTag)
const prevTag = tagList[currentIndex - 1]
const nextTag = tagList[currentIndex + 1]
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
}
}
}
}
}
</script>
<style lang="scss" scoped>
.scroll-container {
white-space: nowrap;
position: relative;
overflow: hidden;
width: 100%;
::v-deep {
.el-scrollbar__bar {
bottom: 0px;
}
.el-scrollbar__wrap {
height: 49px;
}
}
}
</style>
很多方法都是从vue-element-admin拿过来用的 但是关键的vuex数据获取 存储 给简化了 具体就不展示了 想要代码的小伙伴 私信我我上传到gitBub后给地址 自己下载看吧 希望对小伙伴的提升有所帮助!
更多推荐
所有评论(0)