vue-admin-element项目分析与难题解决

移植vue-admin-element 到普通的项目中
遇到的问题
这个是将layout目录移植过来
vue-admin-element项目分析与难题解决_第1张图片
将styles中的scss移植过来
vue-admin-element项目分析与难题解决_第2张图片
将项目中的store移植过来
vue-admin-element项目分析与难题解决_第3张图片
src目录中的setting和permission这个路由权限移植过来
vue-admin-element项目分析与难题解决_第4张图片
这个是main.js 我是使用sdn引入vue和vue-router 和axios和vuex
就不要再import 引入vue和vue-router 要不然会报错 会有重复引入的问题

// import Vue from 'vue'

// 全局样式  重置css样式表
import './styles/normalize.css'
// vue-admin-element 样式表
import './styles/index.scss'

import App from './App.vue'
import router from './router'
import store from './store'
import  './permission'




Vue.config.productionTip = false
// 禁用用户通过插件查看vuex数据
Vue.config.devtools = false
// 这个图片后期要换
const loadimage = require('./assets/common/loading.png')
const errorimage = require('./assets/common/logo.png')
// 图片懒加载
Vue.use(VueLazyload, {
  preLoad: 1.3,
  error: errorimage,
  loading: loadimage,
  attempt: 1
})

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

这个是permission.js 路由权限文件 和vue-admin里的权限路由一样


import router from "./router/index.js";
import store from "./store/index.js";
const WHITE_LIST = ['/login','/404']
import { getToken } from '@/utils/auth' // get token from cookie

// 使用cdn引入elementui 在js中使用message
// ELEMENT.Message.error('错误');

router.beforeEach(async (to,from,next)=>{
    // 是否有token?
    const hasToken = getToken()
    // 有token
    if(hasToken){
    // 是否去登入页
       if(to.path === '/login'){
       // 放行
        next()
       }else{
       // 判断是否有角色
        const hasRoles = store.getters.roles && store.getters.roles.length > 0
       if(hasRoles){
       // 放行
         next()
       }else{
       // 没有角色
        try {
        // 获取角色信息
          const { roles } = await store.dispatch('user/getInfo')
          // 将角色信息传入这个方法 去返回符合角色的路由信息
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          // 将路由信息加入路由
          // addRoutes 这个是路由3.0 版本的 后面的版本废弃了这个方法 
          // 我本来用的是3.5的路由 用addRoute 传入 路由
          // 在router.push() 跳转路由的时候会报错 要在push后面添加.catch((res)=>{})去捕获错误才行
          // 所以我就将路由降到了3.0版本 也是无奈啊!!!
          router.addRoutes(accessRoutes)
          // 我百度后 说是添加路由后 要等新的路由添加到路由生效后 再跳转 
          // 确保addRoutes()时动态添加的路由已经被完全加载上去
          // next({ ...to, replace: true })中的replace: true只是一个设置信息,告诉VUE本次操作后,不能通过浏览器后退按钮,返回前一个路由。
          next({ ...to, replace: true })
            
        } catch (error) {
            await store.dispatch('user/resetToken')
            next('/login')
        }
       }
       }
    }else{
        if(WHITE_LIST.indexOf(to.path) !== -1){
             next()
        }else{
            next('/login')
        }

    }

})

router.afterEach((to,from)=>{
    // to: 去的路由
    // from: 来的路由
}) 
export default WHITE_LIST
app.vue 最外的vue
<template>
  <div id="#app">
    <router-view></router-view>
  </div>
</template>

<script>
export default {
}
</script>
<style lang="scss">
</style>

这是vue.config.js 文件 打包时 去掉了vue vue-router等

const {
	defineConfig
} = require('@vue/cli-service')
// const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')
// 需要排除的包,对象
let externals = {}

// 判断是否是生产环境
const isProduction = process.env.NODE_ENV === 'production'
// 如果是生产环境,需要执行以下逻辑
if (isProduction) {
	externals = {
		/**
		 * externals对象属性分析:
		 * '包名''在项目中引入的名字'**/
		'axios': 'axios',
		'element-ui': 'ELEMENT',
		'vue': 'Vue',
		'vue-router': 'VueRouter',
		'vuex': 'Vuex'
	}

}



module.exports = defineConfig({
	publicPath: isProduction ? './' : '/',
	transpileDependencies: true,
	configureWebpack: {
		// 打包瘦身
		// 当为开发环境时,externals为空对象
		// 当为生产环境时,externals对象值为排除第三方包,达到瘦身目的
		externals: externals,
		resolve: {
			fallback: {
			  path: require.resolve('path-browserify')
			}
		  }		
	},
	css:{
		loaderOptions: {
			scss: {
				prependData: `
				@import "@/styles/common.scss";
			  `
			}
		  }
	},
	// configureWebpack: config => {
	// 	const plugins = []
	// 	plugins.push(new NodePolyfillPlugin())
	// }	
	// devServer: {
	//   // 设置代理请求
	//   proxy: {
	//     // 以 api开头
	//     '^/api': {
	//       // 设置代理地址 把基地址禁用
	//       target: ''
	//     }
	//   }
	// }
})

index.html文件 cdn 引入文件

<!DOCTYPE html>
<html lang="">

<head>
	<meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width,initial-scale=1.0">
	<link rel="icon" href="<%= BASE_URL %>favicon.ico">
	<title>xxx</title>

	<link rel="stylesheet" href="https://unpkg.com/[email protected]/lib/theme-chalk/index.css">
    <script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
    <script src="https://unpkg.com/[email protected]/dist/vue-router.js"></script>
    <script src="https://unpkg.com/[email protected]/dist/vuex.js"></script>
	<script src="https://unpkg.com/[email protected]/dist/axios.min.js"></script>
	<script src="https://unpkg.com/[email protected]/lib/index.js"></script>
</head>

<body>
	<noscript>
		<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript
				enabled. Please enable it to continue.</strong>
	</noscript>
	<div id="app"></div>
	<!-- built files will be auto injected -->

</body>

</html>

scss中的文件
我这个项目中不知道为啥 #app{} 这个设置不上去 不生效
所以在sidebar.scss 中的把#app 这一层注释掉 就可以了
vue-admin-element项目分析与难题解决_第5张图片
app这个尾巴的括号 别忘 去掉

vue-admin-element项目分析与难题解决_第6张图片
:exprot{
xx:xx
}
这个是sass 导出变量 在vue页面中可以引入 就可以获取到变量
要抛出变量的话,文件名要改,要不然获取不到变量 文件名中间要加上module 如上图片

我用的是vue-cli4 创建的项目 用的sass 版本是
在这里插入图片描述

sass版本太高会报错 所以降低sass版本

项目里有的页面会用到path 例如 SidebarItem 页面
import path from ‘path’
vue-admin-element项目分析与难题解决_第7张图片

需要下载 “path-browserify”: “^1.0.1”,
vue-admin-element项目分析与难题解决_第8张图片

layout中的index.vue

<template>
  <div :class="classObj" class="app-wrapper">
    <!-- 左侧导航栏 -->
    <sidebar class="sidebar-container" />
    <div :class="{hasTagsView:needTagsView}" class="main-container">
      <div :class="{'fixed-header':fixedHeader}">
        <!-- 顶部导航 -->
        <navbar />
        <!-- tag栏 -->
        <tags-view v-if="needTagsView" />
      </div>
      <app-main />

    </div>
  </div>
</template>

<script>
// 由components中的index.js 统一引入四个组件 
import { AppMain, Navbar,  Sidebar, TagsView } from './components'
import { mapState } from 'vuex'

export default {
  name: 'Layout',
  components: {
    AppMain,
    Navbar,
    Sidebar,
    TagsView
  },
  computed: {
    ...mapState({
      sidebar: state => state.app.sidebar,
      device: state => state.app.device,
      needTagsView: state => state.settings.tagsView,
      fixedHeader: state => state.settings.fixedHeader
    }),
    classObj() {
      return {
        hideSidebar: !this.sidebar.opened,
        openSidebar: this.sidebar.opened,
        withoutAnimation: this.sidebar.withoutAnimation,
      }
    }
  },
  methods: {
    handleClickOutside() {
      this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
    }
  }
}
</script>

<style lang="scss" scoped>
  @import "~@/styles/mixin.scss";
  @import "~@/styles/variables.module.scss";
// 因为#app 这个css样式不生效 所以把这个主体的高度调成100vh 要不然高度塌陷不够一屏
  .app-wrapper {
    @include clearfix;
    position: relative;
    height: 100vh;
    width: 100%;

    &.mobile.openSidebar {
      position: fixed;
      top: 0;
    }
  }

  .drawer-bg {
    background: #000;
    opacity: 0.3;
    width: 100%;
    top: 0;
    height: 100%;
    position: absolute;
    z-index: 999;
  }

  .fixed-header {
    position: fixed;
    top: 0;
    right: 0;
    z-index: 9;
    width: calc(100% - #{$sideBarWidth});
    transition: width 0.28s;
  }

  .hideSidebar .fixed-header {
    width: calc(100% - 54px)
  }

  .mobile .fixed-header {
    width: 100%;
  }
</style>

这个就是 引入主体的四个组件的js  
default as AppMain  意思是 引入后重命名 为AppMain  这样在解构的时候 就可以获取到这个重命名的组件
export { default as AppMain } from './AppMain'
export { default as Navbar } from './Navbar'
export { default as Sidebar } from './Sidebar/index.vue'
export { default as TagsView } from './TagsView/index.vue'

navbar.vue

<template>
  <div class="navbar">
    <!-- 汉堡图片的组件 -->
    <hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
    <!-- 面包屑导航 -->
    <breadcrumb id="breadcrumb-container" class="breadcrumb-container" />

    <div class="right-menu">

      <!-- 顶部navbar -->
      <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
        <!-- 右边用户名和用户头像 -->
        <div class="avatar-wrapper">
          <!-- <img :src="avatar+'?imageView2/1/w/80/h/80'" class="user-avatar"> -->
          <span>用户名</span>
          <i class="el-icon-caret-bottom" />
        </div>
        <!-- 下拉框 -->
        <el-dropdown-menu slot="dropdown">
         <!-- 退出 -->
          <el-dropdown-item divided @click.native="logout">
            <span style="display:block;">退出</span>
          </el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'


export default {
  components: {
    Breadcrumb,
    Hamburger,

  },
  computed: {
    ...mapGetters([
      'sidebar',
      'avatar',
      'device'
    ])
  },
  methods: {
    toggleSideBar() {
      this.$store.dispatch('app/toggleSideBar')
    },
    async logout() {
      await this.$store.dispatch('user/logout')
      this.$router.push(`/login?redirect=${this.$route.fullPath}`)
    }
  }
}
</script>

<style lang="scss" scoped>
.navbar {
  height: 50px;
  overflow: hidden;
  position: relative;
  background: #fff;
  box-shadow: 0 1px 4px rgba(0,21,41,.08);

  .hamburger-container {
    line-height: 46px;
    height: 100%;
    float: left;
    cursor: pointer;
    transition: background .3s;
    -webkit-tap-highlight-color:transparent;

    &:hover {
      background: rgba(0, 0, 0, .025)
    }
  }

  .breadcrumb-container {
    float: left;
  }

  .errLog-container {
    display: inline-block;
    vertical-align: top;
  }

  .right-menu {
    float: right;
    height: 100%;
    line-height: 50px;

    &:focus {
      outline: none;
    }

    .right-menu-item {
      display: inline-block;
      padding: 0 8px;
      height: 100%;
      font-size: 18px;
      color: #5a5e66;
      vertical-align: text-bottom;

      &.hover-effect {
        cursor: pointer;
        transition: background .3s;

        &:hover {
          background: rgba(0, 0, 0, .025)
        }
      }
    }

    .avatar-container {
      margin-right: 30px;

      .avatar-wrapper {
        margin-top: 5px;
        position: relative;

        .user-avatar {
          cursor: pointer;
          width: 40px;
          height: 40px;
          border-radius: 10px;
        }

        .el-icon-caret-bottom {
          cursor: pointer;
          position: absolute;
          right: -20px;
          top: 25px;
          font-size: 12px;
        }
      }
    }
  }
}
</style>

// 汉堡图片的组件没什么好说的 只有svg 要换的话 就直接改成图片

// 面包屑组件

<template>
  <el-breadcrumb class="app-breadcrumb" separator="/">
    <transition-group name="breadcrumb">
      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
        <!-- 判断是否是最后一项index==levelList.length-1 -->
        <!-- 最后一项不做跳转 -->
        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
      </el-breadcrumb-item>
    </transition-group>
  </el-breadcrumb>
</template>

<script>
import pathToRegexp from 'path-to-regexp'

export default {
  data() {
    return {
      levelList: null
    }
  },
  watch: {
    $route() {
      this.getBreadcrumb()
    }
  },
  created() {
    this.getBreadcrumb()
  },
  methods: {
    getBreadcrumb() {
      // only show routes with meta.title
      // 过滤出当前路由的层级 只有两层 第一层就是父级 第二层就是当前的选中的子级
      let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
      // 选出第一项
      const first = matched[0]
      // 判断是否是首页   如果第一个值没有name值 进入这个判断
      if (!this.isDashboard(first)) {
        // [] 里被我删掉了 路由级数就变为了当前父级和当前子级 首页这个被我删了
        matched = [].concat(matched)
      }
      // 过滤出有meta 有title breadcrumb不为false的数据
      this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
      // console.log(this.levelList);
    },
    // 当前路由的name 是否等于Dashboard 如果相等 就是首页
    isDashboard(route) {
      const name = route && route.name
      if (!name) {
        return false
      }
      return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
    },
    pathCompile(path) {
      // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
      const { params } = this.$route
      //字符串转为正则表达式 快速填充 url 字符串的参数值
      // 这是百度的答案 我还是没明白是啥用处
      var toPath = pathToRegexp.compile(path)
      return toPath(params)
    },
    handleLink(item) {
      const { redirect, path } = item
      // 判断是否有重定向的值 有的话跳转重定向的地址 redirect
      if (redirect) {
        this.$router.push(redirect)
        return
      }
      this.$router.push(this.pathCompile(path))
    }
  }
}
</script>

<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
  display: inline-block;
  font-size: 14px;
  line-height: 50px;
  margin-left: 8px;

  .no-redirect {
    color: #97a8be;
    cursor: text;
  }
}
</style>

AppMain.vue

<template>
  <section class="app-main">
    <transition name="fade-transform" mode="out-in">
      <!-- 是否缓存路由页面  include-->
      <keep-alive :include="cachedViews">
        <!-- Vue会复用相同组件,对于路由有多个子路由来说,
          当在子路由来回切换时,会导致页面不刷新的问题,
          因为不再执行created和mounted这些钩子函数。
          <router-view :key='key'/>中的key即可解决这个问题 -->
        <router-view :key="key" />
      </keep-alive>
    </transition>
  </section>
</template>

<script>
export default {
  name: 'AppMain',
  computed: {
    // 判断是否保存路由数据的数组
    cachedViews() {
      return this.$store.state.tagsView.cachedViews
    },
    // 当前的路由
    key() {
      return this.$route.path
    }
  }
}
</script>

<style lang="scss" scoped>
.app-main {
  /* 50= navbar  50  */
  min-height: calc(100vh - 50px);
  width: 100%;
  position: relative;
  overflow: hidden;
}

.fixed-header+.app-main {
  padding-top: 50px;
}

.hasTagsView {
  .app-main {
    /* 84 = navbar + tags-view = 50 + 34 */
    min-height: calc(100vh - 84px);
  }

  .fixed-header+.app-main {
    padding-top: 84px;
  }
}
</style>

<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
  .fixed-header {
    padding-right: 15px;
  }
}
</style>

ScrollPane.vue

<template>
  <!-- vertical:false 横向滚动  @wheel.native.prevent 鼠标滚动-->
  <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
    <slot />
  </el-scrollbar>
</template>

<script>
const tagAndTagSpacing = 4 // tagAndTagSpacing 每个tag之间的空隙

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) {
       // wheelDelta:获取滚轮滚动方向,向上120,向下-120,但为常量,与滚轮速率无关
      // deltaY:垂直滚动幅度,正值向下滚动。电脑鼠标滚轮垂直行数默认值是3
      // wheelDelta只有部分浏览器支持,deltaY几乎所有浏览器都支持
      // -e.deltaY * 40 这个 *40 不知道是啥意思
      const eventDelta = e.wheelDelta || -e.deltaY * 40
      const $scrollWrapper = this.scrollWrapper
       // 0到scrollLeft为滚动区域隐藏部分
      //  eventDelta / 4  这个/4 也不知道是什么意思
      $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
    },
    emitScroll() {
      this.$emit('scroll')
    },
    // 移动到目标tag
    moveToTarget(currentTag) {
      // 获取滚动的元素
      const $container = this.$refs.scrollContainer.$el
      // 获取元素的宽度 包括padding border 的值
      const $containerWidth = $container.offsetWidth
      // 获取 wapper 组件
      const $scrollWrapper = this.scrollWrapper
      // 获取tag 组件
      const tagList = this.$parent.$refs.tag
      // 第一个tag 和最后一个tag
      let firstTag = null
      let lastTag = null

      // 找到第一个tag 和最后一个tag 
      if (tagList.length > 0) {
        firstTag = tagList[0]
        lastTag = tagList[tagList.length - 1]
      }
      // 当前点击的tag 是否是第一个tag 
      if (firstTag === currentTag) {
        // 如果是第一个tag 就滚动到0
        $scrollWrapper.scrollLeft = 0
        // 如果是最后一个tag
      } else if (lastTag === currentTag) {
        // 他的结构是tags-view-container  > el-scrollbar__wrap
        // 里面的wrap的宽度超出container 用里面的宽度 减去 外面的宽度 
        // 就是要向左滑动的宽度
        // 滑动的距离是 el-scrollbar__wrap的总宽度 减去tags-view-container 元素的宽度
        $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
      } else {
        // 找到上一个tag和下一个tag
        // 找到当前tag的索引值
        const currentIndex = tagList.findIndex(item => item === currentTag)
        const prevTag = tagList[currentIndex - 1]
        const nextTag = tagList[currentIndex + 1]

        // 下一个tag距离左边的距离 = 下一个tag距离左边的距离 + 下一个tag自身的距离 + tag之间空隙的距离
        const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing

        // 上一个tag距离左边的距离 = 上一个tag距离左边的距离 - tag之间空隙的距离
        const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing

         // 如果下一个tag距离左边的距离 大于 需要往左滑动的距离 + tags-view-containe 元素的宽度
        //  是否点击的是右边边缘的tag 会滑动到下一个tag的距离
        // 说明这个tag的下一个tag 已经超出了tags-view-containe 可见范围
        //  所以会滑动到下一个tag的左边的距离
        // 简单点说就是最右边的下一个tag是否在可见范围内 是否被遮挡
        if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
          // 往左滑动的距离 = 下一个tag距离左边的距离 - tags-view-containe 元素的宽度
          $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth

        // 是否点击的是左边边缘的tag  会滑动到上一个tag的距离
        // 上一个tag距离左边的距离是否 小于 往右滑动的距离 
        // 就会滑动到上一个tag距离左边的的距离
        } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
          // 往左滑的距离 = 上一个tag距离左边的距离
          $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>

tag 的index页面

<template>
  <div id="tags-view-container" class="tags-view-container">
    <scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
      <!-- 
        @click.middle.native:监听鼠标中键 判断是否是固定tag 不是的话就删除当前tag
        @contextmenu.prevent.native 监听鼠标右键 打开当前tag的菜单
      -->
      <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 }}
        <!-- 不是固定tag就显示删除按钮 -->
        <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)">刷新</li>
      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li>
      <li @click="closeOthersTags">关闭其他</li>
      <li @click="closeAllTags(selectedTag)">关闭所有</li>
    </ul>
  </div>
</template>

<script>
import ScrollPane from './ScrollPane'
import path from 'path'

export default {
  components: { ScrollPane },
  data() {
    return {
      visible: false,
      top: 0,
      left: 0,
      selectedTag: {}, // 在右键打开菜单时赋值的
      affixTags: []
    }
  },
  computed: {
    visitedViews() { // 获取所有的tags
      return this.$store.state.tagsView.visitedViews
    },
    routes() { // 获取所有的路由信息
      return this.$store.state.permission.routes
    }
  },
  watch: {
    // 触发添加tag 条件就是是否触发了路由 触发了路由 就添加路由 并且前往当前的tag
    $route() {
      this.addTags()
      this.moveToCurrentTag()
    },
    visible(value) {
      if (value) {
        document.body.addEventListener('click', this.closeMenu)
      } else {
        document.body.removeEventListener('click', this.closeMenu)
      }
    }
  },
  mounted() {
    this.initTags()
    this.addTags()
  },
  methods: {
    // 是否是当前激活的状态
    isActive(route) {
      return route.path === this.$route.path
    },
    // 是否是固定tag
    isAffix(tag) {
      return tag.meta && tag.meta.affix
    },
    // 过滤出固定tag
    filterAffixTags(routes, basePath = '/') {
      let tags = []
      // 循环 和 递归 找出固定tag的值
      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
    },
    initTags() {
      // 获取固定tag的数量
      const affixTags = this.affixTags = this.filterAffixTags(this.routes)
      // 循环添加到tag里
      for (const tag of affixTags) {
        // tag必须有name值
        if (tag.name) {
          // 这个是只添加tag  没有添加tag的缓存
          this.$store.dispatch('tagsView/addVisitedView', tag)
        }
      }
    },
    addTags() {
      const { name } = this.$route
      if (name) {
        // 这个是tag 和缓存都添加 上下两个add的方法就差别在这
        this.$store.dispatch('tagsView/addView', this.$route)
      }
      return false
    },
    moveToCurrentTag() {
      // 获取当前所有的tag的值
      const tags = this.$refs.tag
      this.$nextTick(() => {
        // 循环tag
        for (const tag of tags) {
          // 如果tag的path地址和当前的路由地址一致
          if (tag.to.path === this.$route.path) {
            // 就滚动到这个匹配到的path的tag位置
            this.$refs.scrollPane.moveToTarget(tag)
            // when query is different then update
            // 当查询不同时,则更新 就是参数不同的话就会更新当前的路由信息
            // pullPach 是全地址,携带路由参数  path 不带参数
            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
          })
        })
      })
    },
    // 删除选中的tag
    closeSelectedTag(view) {
      this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
        // 判断是否是当前激活的tag页面
        if (this.isActive(view)) {
          // 如果是当前选中的tag 删除后 跳到最后一个tag页面
          // 传入tag数组和当前的tag值
          this.toLastView(visitedViews, view)
        }
      })
    },
    // 关闭其他tag
    closeOthersTags() {
      // 先前往当前路由地址 当路由发生变化 就会触发addtag方法添加当前的tag信息
      this.$router.push(this.selectedTag)

      this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
        // 移动到当前的tag页面
        this.moveToCurrentTag()
      })
    },
    // 关闭所有的除了固定的tag和缓存
    closeAllTags(view) {
      this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
        // 如果是固定的tag 就直接return
        if (this.affixTags.some(tag => tag.path === view.path)) {
          return
        }
        // 如果不是固定的tag 就去最后一个tag
        this.toLastView(visitedViews, view)
      })
    },
    // 去往最后一个tag页面
    toLastView(visitedViews, view) {
      const latestView = visitedViews.slice(-1)[0]
      if (latestView) {
        this.$router.push(latestView.fullPath)
      } else {
        // now the default is to redirect to the home page if there is no tags-view,
        // you can adjust it according to your needs.
        // 我加了一个首页固定的tag标签 就不会走到这里
        if (view.name === 'Dashboard') {
          // to reload home page
          this.$router.replace({ path: '/redirect' + view.fullPath })
        } else {
          this.$router.push({ path: '/teacher' })
        }
      }
    },
    openMenu(tag, e) {
      const menuMinWidth = 105
      const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
      const offsetWidth = this.$el.offsetWidth // container width
      const maxLeft = offsetWidth - menuMinWidth // left boundary
      const left = e.clientX - offsetLeft + 15 // 15: margin right

      if (left > maxLeft) {
        this.left = maxLeft
      } else {
        this.left = left
      }

      this.top = e.clientY
      this.visible = true
      this.selectedTag = tag
    },
    // 在滚动的时候 关闭tag菜单
    closeMenu() {
      this.visible = false
    },
    // 滚动的监听事件
    handleScroll() {
      this.closeMenu()
    }
  }
}
</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>

<style lang="scss">
//reset element css of el-icon-close
.tags-view-wrapper {
  .tags-view-item {
    .el-icon-close {
      width: 16px;
      height: 16px;
      vertical-align: 2px;
      border-radius: 50%;
      text-align: center;
      transition: all .3s cubic-bezier(.645, .045, .355, 1);
      transform-origin: 100% 50%;
      &:before {
        transform: scale(.6);
        display: inline-block;
        vertical-align: -3px;
      }
      &:hover {
        background-color: #b4bccc;
        color: #fff;
      }
    }
  }
}
</style>

tag的vuex中的页面

const state = {
  visitedViews: [], // tag的值
  cachedViews: []  // 缓存起来的tag
}

const mutations = {
  ADD_VISITED_VIEW: (state, view) => {
    if (state.visitedViews.some(v => v.path === view.path)) return
    state.visitedViews.push(
      Object.assign({}, view, {
        title: view.meta.title || 'no-name'
      })
    )
  },
  ADD_CACHED_VIEW: (state, view) => {
    if (state.cachedViews.includes(view.name)) return
    if (!view.meta.noCache) {
      state.cachedViews.push(view.name)
    }
  },

  DEL_VISITED_VIEW: (state, view) => {
    for (const [i, v] of state.visitedViews.entries()) {
      if (v.path === view.path) {
        state.visitedViews.splice(i, 1)
        break
      }
    }
  },
  // 从cachedViews删除当前tag的值
  DEL_CACHED_VIEW: (state, view) => {
    // 这个tag的值是否在cachedViews数组中 获取下标
    const index = state.cachedViews.indexOf(view.name)
    // 在这个数组中 并且根据下标删除当前项
    index > -1 && state.cachedViews.splice(index, 1)
  },
  // 新的tag页面 = tag数组中过滤出 有固定tag的值 和当前选中的值
  DEL_OTHERS_VISITED_VIEWS: (state, view) => {
    state.visitedViews = state.visitedViews.filter(v => {
      return v.meta.affix || v.path === view.path
    })
  },
  // tag缓存页面 = 
  DEL_OTHERS_CACHED_VIEWS: (state, view) => {
    // 找到当前选中tag的下标
    const index = state.cachedViews.indexOf(view.name)
    if (index > -1) {
      // 找到下标 tag缓存数组 = slice截取出当前tag的唯一值
      state.cachedViews = state.cachedViews.slice(index, index + 1)
    } else {
      // if index = -1, there is no cached tags
      // 如果没找到下标 tag缓存数组就为空
      state.cachedViews = []
    }
  },
  //  过滤出固定tag
  DEL_ALL_VISITED_VIEWS: state => {
    // keep affix tags
    const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
    state.visitedViews = affixTags
  },
  // 清空缓存
  DEL_ALL_CACHED_VIEWS: state => {
    state.cachedViews = []
  },
  // 循环tag数组 如果path路由地址相同 
  // 就将传进来的当前tag 深拷贝一份 重新赋值给 匹配到的v这一项的内容
  UPDATE_VISITED_VIEW: (state, view) => {
    for (let v of state.visitedViews) {
      if (v.path === view.path) {
        v = Object.assign(v, view)
        break
      }
    }
  }
}

const actions = {
  // 添加缓存和tag
  addView({ dispatch }, view) {
    dispatch('addVisitedView', view)
    dispatch('addCachedView', view)
  },
  // 只添加tag
  addVisitedView({ commit }, view) {
    commit('ADD_VISITED_VIEW', view)
  },
  // 只添加缓存
  addCachedView({ commit }, view) {
    commit('ADD_CACHED_VIEW', view)
  },
  // 将当前缓存页面的tag从缓存数组和显示tag标签的数组都删除
  delView({ dispatch, state }, view) {
    return new Promise(resolve => {
      dispatch('delVisitedView', view)
      dispatch('delCachedView', view)
      resolve({
        visitedViews: [...state.visitedViews],
        cachedViews: [...state.cachedViews]
      })
    })
  },
  delVisitedView({ commit, state }, view) {
    return new Promise(resolve => {
      commit('DEL_VISITED_VIEW', view)
      resolve([...state.visitedViews])
    })
  },
  // 删除当前缓存的tag从cachedViews删除
  // 在返回删除后剩余的tag的值
  delCachedView({ commit, state }, view) {
    return new Promise(resolve => {
      commit('DEL_CACHED_VIEW', view)
      resolve([...state.cachedViews])
    })
  },
  // 删除其他的值
  delOthersViews({ dispatch, state }, view) {
    return new Promise(resolve => {
      // 删除缓存和tag
      dispatch('delOthersVisitedViews', view)
      dispatch('delOthersCachedViews', view)
      resolve({
        visitedViews: [...state.visitedViews],
        cachedViews: [...state.cachedViews]
      })
    })
  },
  // 删除除了当前tag信息的其他值的tag
  delOthersVisitedViews({ commit, state }, view) {
    return new Promise(resolve => {
      commit('DEL_OTHERS_VISITED_VIEWS', view)
      resolve([...state.visitedViews])
    })
  },
  // 删除除了当前tag信息的缓存里其他的值
  delOthersCachedViews({ commit, state }, view) {
    return new Promise(resolve => {
      commit('DEL_OTHERS_CACHED_VIEWS', view)
      resolve([...state.cachedViews])
    })
  },
  // 删除所有的缓存和tag
  delAllViews({ dispatch, state }, view) {
    return new Promise(resolve => {
      dispatch('delAllVisitedViews', view)
      dispatch('delAllCachedViews', view)
      resolve({
        visitedViews: [...state.visitedViews],
        cachedViews: [...state.cachedViews]
      })
    })
  },
  delAllVisitedViews({ commit, state }) {
    return new Promise(resolve => {
      commit('DEL_ALL_VISITED_VIEWS')
      resolve([...state.visitedViews])
    })
  },
  delAllCachedViews({ commit, state }) {
    return new Promise(resolve => {
      commit('DEL_ALL_CACHED_VIEWS')
      resolve([...state.cachedViews])
    })
  },
  // 更新tags 传入当前路由信息
  updateVisitedView({ commit }, view) {
    commit('UPDATE_VISITED_VIEW', view)
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

你可能感兴趣的:(vue,vue)