vue动态菜单及tag切换

2023-11-05

     刚刚接触项目的小伙伴 几乎都接触不到这一块的 因为入职 公司要么有骨干 要么是现有项目维护 所以 对于动态菜单 很好奇 今天带着小伙伴们一起来看看吧 

     可能有些人接触过 只是看看别人写的代码 觉得都没有问题  没有实际动手去做过 这就应对了那句 “看花容易绣花难” 其实我开始的时候 也是这种心理 直到我自己动手的时候 发现里面有很多坑 如果不涉及到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后给地址 自己下载看吧 希望对小伙伴的提升有所帮助!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

vue动态菜单及tag切换 的相关文章

随机推荐

  • 【Python】基于wxauto的超简单微信机器人

    前言 我是一个python初学者 一直想做一个微信版类似qq的群助手 我尝试去百度过 python微信机器人 之类的搜索 但得到的结果几乎都是使用 itchat wxpy 之类的库通过网页版微信去实现行为的 但腾讯在2019年7月份开始 彻
  • java.net.SocketException: Socket closed 解决方式

    问题背景 客户端连接服务器 发送一个请求 捕获响应信息 建立连接 Socket socket new Socket socket connect new InetSocketAddress InetAddress getLocalHost
  • 方差分析 球形检验_不等距重复测量方差分析

    作者 红豆牛奶 封面 自己想吧 在进行重复测量方差时 测量的间隔大多数的等距的 但有时测量的间隔却是不等距的 若用等距测量的方法分析 会使结果不准确 此时就需要手动编程一下 不要一提到编程就觉得很难哦 看完此篇文章 原来它竟如此简单 重复测
  • 以前写SpringMVC的时候,如果需要访问一个页面,必须要写Controller类,然后再写一个方法跳转到页面,感觉好麻烦,其实重写WebMvcConfigurerAdapter中的addViewC...

    以前写SpringMVC的时候 如果需要访问一个页面 必须要写Controller类 然后再写一个方法跳转到页面 感觉好麻烦 其实重写WebMvcConfigurerAdapter中的addViewControllers方法即可达到效果了
  • CSS的样式注释(部分)

    a link text decoration none color white a visited text decoration none color white a hover text decoration underline col
  • RT-Thread 断言:assertion failed at function:rt_mutex_take 等

    断言 断言是什么 https www cnblogs com thisway p 5558914 html ASSERT 是一个调试程序时经常使用的宏 在程序运行时它计算括号内的表达式 如果表达式为FALSE 0 程序将报告错误 并终止执行
  • 【ETL】常见的ETL工具(含开源及付费)一览和优劣势分析?

    一 Kettle Kettle 中文名称叫水壶 该项目的概念是把各种数据放到一个壶里 然后以一种指定的格式流出 Kettle是一款国外开源的ETL工具 纯java编写 可以在Window Linux Unix上运行 无需安装 数据抽取 高效
  • 【面试】GDB调试

    用GDB调试多进程程序 如果一个进程通过fork系统调用创建了子进程 gdb会继续调试原来的进程 子进程则正常运行 那么该如何调试子进程呢 单独调试子进程 子进程从本质上说也是一个进程 因此我们可以用通用的gdb调试方法来调试他 举例来说如
  • The origin server did not find a current representation for the target resource(4种解决方案)

    The origin server did not find a current representation for the target resource or is not willing to disclose that one e
  • UCOS 的延时函数OSTimeDlyHMSM()实现精确延时

    介绍UCOS的资料汗牛 但详细解说OSTimeDlyHMSM 函数的不多 经过本人仔细研究该函数代码并通过调试发现 要想实现精确延时的对代码进行相应的修正 本人实现的是UCOS在2812上的移植 在其它DSP型号上移植情况是一样的 相差不大
  • 104. Maximum Depth of Binary Tree

    Definition for a binary tree node struct TreeNode int val TreeNode left TreeNode right TreeNode int x val x left NULL ri
  • macOS下使用vscode+xdebug调试php

    手动安装xdebug 1 浏览器访问https xdebug org wizard 2 在本地终端输入php i 命令 将输出的内容复制到指南中的输入框中并提交分析 3 分析完后会给出分析概览 然后根据下面提示步骤进行手动安装即可 第5步中
  • MIT最新研究:多个AI协作有助提高大模型推理能力和准确性

    麻省理工学院计算机科学与人工智能实验室 CSAIL 研究团队发现 多个语言模型协同工作胜过单一模型 多个AI协作有助于提高大型语言模型的推理能力和事实准确性 每个语言模型都生成对给定问题的回答 然后整合来自其他代理的反馈 以更新自己的回应
  • 初识QT(十四)——Qt项目界面文件(.ui)及其作用(超详细)

    Qt 项目中 后缀为 ui 的文件是可视化设计的窗体的定义文件 如 widget ui 双击项目文件目录树中的文件 widget ui 会打开一个集成在 Qt Creator 中的 Qt Designer 对窗体进行可视化设计 如图 1 所
  • curl请求返回空白问题

    今天使用curl get请求阿里的接口出现了返回空白问题 但是curl是我之前封装的函数 使用很多次了都没有问题 然后网上也没找到解决方法 后面打印了head查看发现是提示 HTTP 1 1 505 HTTP Version Not Sup
  • 【STL详解】stack

    文章目录 前言 一 STL 二 stack 1 stack的创建 2 stack相关方法 3 如何对satck进行排序 前言 本篇文章将总结SLT stack 以及其常用方法 一 STL STL 是 Standard Template Li
  • 牛客练习赛69 C

    题意 给定 n n n点 m m m边 让你确定一个大小为 n n n的排列使得
  • Backtrader 基本使用教程 — 量化投资实战教程(1)

    都说Python可以用于量化投资 但是很多人都不知道该怎么做 甚至觉得是非常高深的知识 其实并非如此 任何人都可以在只有一点Python的基础上回测一个简单的策略 Backtrader是一个基于Python的自动化回溯测试框架 作者是德国人
  • CUDA samples系列 0.3 vectorAdd

    目录 CPU与GPU同步方法详解 源代码中的同步 同步方法扩展 代码解析 扩展一 vectorAdd nvrtc 扩展二 vectorAddDrv 这份代码非常的简单和基础 就把两个向量相加 CPU与GPU同步方法详解 源代码中的同步 代码
  • vue动态菜单及tag切换

    刚刚接触项目的小伙伴 几乎都接触不到这一块的 因为入职 公司要么有骨干 要么是现有项目维护 所以 对于动态菜单 很好奇 今天带着小伙伴们一起来看看吧 可能有些人接触过 只是看看别人写的代码 觉得都没有问题 没有实际动手去做过 这就应对了那句