vue+vuex+vue-router+elementui实现无限标签页后台管理系统框架(动态路由)

效果图

App主页面

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

<script>
export default {
  name: 'App',
  created(){
    //禁止浏览器回退,只能回退一次
    window.onload=function () {
      history.pushState(null, null, document.URL);
      window.addEventListener('popstate', function () {
        history.pushState(null, null, document.URL);
      });
    }
  }
}
</script>

<style>
#app {

}
</style>

vuex 主文件 index

import Vue from 'vue';
import Vuex from 'vuex';
import state from './state';
import * as getters from './getters' ;// 导入响应的模块,*相当于引入了这个组件下所有导出的事例
import * as actions from './actions';
import * as mutations from './mutations';
import createPersistedState from "vuex-persistedstate";//vuex持久化
Vue.use(Vuex);
// 注册上面引入的各大模块
const store = new Vuex.Store({
  plugins: [
    createPersistedState({
      storage: window.sessionStorage,
      reducer(data) {
        return {
          // 设置只储存state中的visitedviews
          visitedviews: data.visitedviews
        }
      }
    })
  ],//持久化vuex防止刷新
  state,    // 共同维护的一个状态,state里面可以是很多个全局状态
  getters,  // 获取数据并渲染
  actions,  // 数据的异步操作
  mutations,  // 处理数据的唯一途径,state的改变或赋值只能在这里
});

export default store  // 导出store并在 main.js中引用注册。

vuex state.js文件

const state={
  isCollapse: true,//面板展开收起
  visitedviews: [],//存放所有浏览过的且不重复的路由数据
  defaultActive:'/Home',//默认**菜单
};
export default state;

左侧导航菜单 Menu组件

<template>
  <div class="menu">
    <el-menu
      :default-active="defaultActive"
      router
      background-color="#242834"
      text-color="#fff"
      active-text-color="#fff"
      class="el-menu-vertical-demo"
      style="min-height:100vh;overflow-x: hidden"
      unique-opened
      :collapse="isCollapse">
      <div v-if='!isCollapse' class="title">
        <img src="../assets/img/logo.png" alt="" class="img">
      </div>
      <div v-else class="title">
        <el-tooltip class="item" effect="dark" content="会员管理系统" placement="right-start">
          <span class="el-icon-more-outline"></span>
        </el-tooltip>
      </div>
      <el-menu-item v-for="(item,index) in currentRouter[2]" :index="item.path" :key="index">
        <i :class="item.meta.icon"></i>
        <span slot="title">{{item.meta.title}}</span>
      </el-menu-item>
    </el-menu>
  </div>
</template>

<script>
  import {mapState,mapGetters} from 'vuex';

  export default {
    name: "Menu",
    data() {
      return {};
    },
    computed: {
      ...mapState(['defaultActive','isCollapse']),//获取**菜单路由和菜单是否展开
      ...mapGetters(['currentRouter'])//获取路由表所有定义为左侧菜单导航栏的路由
    },
  }
</script>

<style scoped>
  .el-menu-vertical-demo:not(.el-menu--collapse) {
    width: 200px;
    min-height:100vh;
    background-color:#242834 !important;
  }
  .is-active{
    background: #1989FB !important;
  }
  .title{
    color: white;
    height: 50px;
    line-height: 50px;
    text-align: center;
    font-size: 18px;
    margin-bottom: 10px;
  }
  .title>span{
    font-size: 16px;
    color: #909399;
  }
  .img{
    width: 150px;
    height: 45px;
    display: block;
    margin: 5px auto 0 auto;
    cursor: pointer;
  }
</style>
currentRouter对应的getter.js
import router from '../router';
//获取路由
export const currentRouter=()=>{
  const PATH=router.history.current;//当前路由
  const ROUTER_LIST=router.options.routes[0].children;//index所有的子级路由
  const NAVIGATION=[];//导航栏列表
  for (let i=0;i<ROUTER_LIST.length;i++){
    if (ROUTER_LIST[i].meta.bar){//如果bar为true,则是添加至导航栏
      NAVIGATION.push(ROUTER_LIST[i])
    }
  }
  const INIT=NAVIGATION[0];//初始路由
  return [PATH,INIT,NAVIGATION];
};
头部 Header 组件
<template>
  <div class="header">
    <div>
      <p>
        <span class="el-icon-s-fold fold" v-show="isCollapse" @click="COLLAPSE"></span>
        <span class="el-icon-s-unfold fold"  v-show="!isCollapse" @click="COLLAPSE"></span>
      </p>
    </div>
    <Tags class="tags"/>
  </div>
</template>

<script>
  import {mapState,mapMutations} from 'vuex';
  import Tags from './Tags';
  export default {
    name: "Header",
    computed:{
      ...mapState(['isCollapse'])
    },
    methods:{
      ...mapMutations(['COLLAPSE'])
    },
    components:{
      Tags
    },
  }
</script>

<style scoped>
  .header{
    display: flex;
    flex-direction: column;
    padding: 0 20px;
  }
  .fold{
    font-size: 26px;
    cursor: pointer;
    line-height: 40px;
  }
  .tags{
    /*border-top: 1px solid;*/
    /*border-bottom: 1px solid;*/
  }
</style>
COLLAPSE 对应的 mutations.js
//展开或收起左侧菜单
export const COLLAPSE=(state)=>{
  state.isCollapse=!state.isCollapse;
};

主页面 Index

<template>
    <div class="Home">
      <el-container>
        <el-aside class="aside">
          <Menu/>
        </el-aside>
        <el-container>
          <el-header class="header">
            <Header/>
          </el-header>
          <el-main class="main">
            <keep-alive>
              <router-view v-if="$route.meta.keepAlive">
                <!-- 这里是会被缓存的视图组件 -->
              </router-view>
            </keep-alive>
            <router-view v-if="!$route.meta.keepAlive">
              <!-- 这里是不被缓存的视图组件 -->
            </router-view>
          </el-main>
        </el-container>
      </el-container>
    </div>
</template>

<script>
  import Menu from '../components/Menu';
  import Header from '../components/Header';
  import {mapGetters} from 'vuex';
  export default {
    name: "Home",
    components:{
      Menu,Header
    },
    computed:{
      ...mapGetters(['currentRouter'])
    },
    created(){
      // 如果当前路由是默认路由 '/ '
      if (this.currentRouter[0].path==='/'){
        this.$router.push({path:this.currentRouter[1].path});//跳转到路由表的第一项
        this.$store.dispatch('changeVisitedViews', this.currentRouter[1]);//改变左侧菜单**项
      }
      else {
        this.$router.push({path:this.currentRouter[0].path});//跳转到当前路由
        this.$store.dispatch('changeVisitedViews', this.currentRouter[0]);//改变左侧菜单**项
      }
    },
  }
</script>

<style scoped>
  .aside{
    width: auto !important;
    min-height:100vh;
    background-color:#242834 !important;
  }
  .header{
    position: relative;
    height: 70px !important;
    padding: 0;
    box-shadow: 0 0 15px #e8e8e8;
  }
  .Home>>>.el-main,.el-main{
    padding: 0 !important;
  }
  .main{
    background: #F3F3F4
  }
</style>
changeVisitedViews 对应的 action.js
//页签切换路由时
export function changeVisitedViews({commit},view) {
  return commit('CHANGE_VISITED_VIEWS', view);//去触发 CHANGE_VISITED_VIEWS,并传入参数
}
mutations中的CHANGE_VISITED_VIEWS
//切换路由时,改变左侧菜单栏
export const CHANGE_VISITED_VIEWS=(state,view)=>{
  state.defaultActive=view.path;
};

Tags 组件

<template>
  <div class="tags-view-container">
    <!--Array.from()方法就是将一个类数组对象或者可遍历对象转换成一个真正的数组。-->
    <router-link v-for="tag in Array.from(visitedViews)" :to="tag.path" :key="tag.path">
      <el-tag
        size="small"
        :type="isActive(tag)?'primary':'info'"
        effect="plain"
        @click="viewTagChange(tag)"
        @close.prevent.stop="delSelectTag(tag)"
        :closable="tag.meta.closable">
        {{tag.meta.title}}
      </el-tag>
    </router-link>
  </div>
</template>

<script>
  import {mapGetters} from 'vuex';
  export default {
    name: "Tags",
    computed: {
      ...mapGetters(['visitedViews','currentRouter']),
    },
    methods: {
      isActive(route) {//判断页签的路由是否为当前路由
        return route.path === this.$route.path
      },
      viewTagChange(route){
        if (this.$route.name) {
          this.$store.dispatch('changeVisitedViews', route);
        }
      },
      addViewTags() {//路由改变时执行的方法
        if (this.$route.name) {
          const route = this.$route;
          this.$store.dispatch('addVisitedViews', route);
        }
      },
      delSelectTag(route) {//先提交删除数据的方法,数组删除出掉数据后,如果关闭的是当前打开的路由需要将路由改为数组最后一次push进去的路由
        this.$store.dispatch('delVisitedViews', route).then((views) => {
          // 此时的views是指的被删除后的 visitedViews 数组中存在的元素,即resolve的回调
          if (this.isActive(route)) {//当前关闭的标签是否是被选中的标签
           /* slice() 方法可从已有的数组中返回选定的元素。
            如果是负数,那么它规定从数组尾部开始算起的位置,-1 指最后一个元素.*/
            let lastView = views.slice(-1)[0];//选取路由数组中的最后一位
            if (lastView) {
              this.$router.push(lastView);
              // this.$store.dispatch('changeVisitedViews', lastView);//改变左侧菜单
            }
            else {
              this.$router.push(this.currentRouter[1].path);
              // this.$store.dispatch('changeVisitedViews', this.currentRouter[1]);//改变左侧菜单
            }
          }
        })
      },
    },
    watch: {
      $route(val) {//点击左侧菜单树,或者点击其他的tab页签,会导致路由的改变
        this.$store.dispatch('changeVisitedViews', val);//改变左侧菜单
        this.addViewTags();
      }
    },
  }
</script>

<style scoped>
  .tags-view-container{
    /*border-top: 1px solid #333;*/
  }
  a{
    text-decoration: none;
    color: #000;
    margin-left: 5px;
  }
  a:first-of-type{
    margin-left: 0;
  }
</style>
visitedViews对应的getter.js
//初始页签增加初始路由
export const visitedViews=(state) => {
  const INIT=currentRouter()[1];
  const flag=state.visitedviews.some(v=>v.path===INIT.path);//判断数组中是否已经存在该路由
  if (!flag){
    state.visitedviews.push({
      name: INIT.name,
      path: INIT.path,
      meta: INIT.meta
    });
  }
  return state.visitedviews;
};

增加和删除页签时,对应的action.js

//添加页签路由
export function addVisitedViews({commit},view) {
  return commit('ADD_VISITED_VIEWS', view);//去触发ADD_VISITED_VIEWS,并传入参数
}
//关闭页签--删除路由数据的方法
export function delVisitedViews({commit,state},view) {
  return new Promise((resolve) => {
    commit('DEL_VISITED_VIEWS', view);
    resolve([...state.visitedviews]);//resolve方法:成功后回调的方法,返回新的state.visitedviews
  })
}

增加和删除页签时,对应的mutations.js

//打开新页签--添加路由数据的方法
export const ADD_VISITED_VIEWS=(state,view)=>{
  /*  some() 方法用于检测数组中的元素是否满足指定条件,
    如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测。
    如果没有满足条件的元素,则返回false */
  const flag=state.visitedviews.some(v=>v.path===view.path);//判断数组中是否已经存在该路由
  // 如果存在当前路由,则返回false,否则添加进去
  if(flag){
    return false;
  }
  else {
    state.visitedviews.push({
      name: view.name,
      path: view.path,
      meta: view.meta
    })
  }
};
//关闭页签--删除路由数据的方法
export const DEL_VISITED_VIEWS=(state,view)=>{
  //entries() 方法返回一个数组的迭代对象,该对象包含数组的键值对 (key/value)。
  for (let [i, v] of state.visitedviews.entries()) {
    if (v.path === view.path) {//i代表索引,v代表对应的对象
      state.visitedviews.splice(i, 1);
      break
    }
  }
};

路由表

import Vue from 'vue'
import Router from 'vue-router';

Vue.use(Router);
const originalPush = Router.prototype.push;
Router.prototype.push = function push(location) {
  return originalPush.call(this, location).catch(err => err)
};//解决element-ui的子菜单项点击两下出现 NavigationDuplicated {_name: "NavigationDuplicated"}

/*keepAlive 页面缓存
  closable  页签是否关闭
  title     页面名称,
  icon      页面图标
  bar       是否是左侧菜单*/

const constantRouterMap = [
  {
    path: '/',
    name: 'Index',
    component: resolve => require(['@/views/Index'], resolve),
    children: [
      {
        path: '/Home',
        name: 'Home',
        component: resolve => require(['@/views/Home'], resolve),
        meta: {
          keepAlive: false,
          closable: false,
          bar:true,
          title: '首页',
          icon:'el-icon-s-home',
        },
      },
      {
        path: '/Test',
        name: 'Test',
        component: resolve => require(['@/views/Test'], resolve),
        meta: {
          keepAlive: false,
          closable: true,
          bar:false,
          title: '测试',
          icon:'el-icon-s-promotion',
        },
      },
    ]
  },
  //需要注意这里,404的路由一定要写在静态路由中,但捕获未定义路由配置一定要放在动态路由里
  {
    path: '/Error',
    name: 'Error',
    component: resolve => require(['@/views/Error'], resolve),
    meta: {
      keepAlive: false,
      title: '错误页面'
    },
  }
];
export default new Router({
  mode: 'history',
  routes: constantRouterMap
})

在main.js中引入 permission.js文件,进入页面时先执行

import './utils/permission';
import {postRequest} from './http';
import {GetLeftList,Du} from './api';
import Router from 'vue-router';
import router from '../router';

const params={
  org_id: 3,
  role_code: "1038"
};
let getRouter; //用来获取后台拿到的路由
//需要挂载的路由,捕获未定义的路由配置一定要挂载到动态路由上,否则页面刷新会直接跳转到404
let asyncRouterMap = {
  "children": [
    {
      "path": "/My",
      "name": "My",
      "component":routerPath("My"),
      "meta": {
        "keepAlive": false,
        "closable": true,
        "bar":true,
        "title": "个人中心",
        "icon":"el-icon-s-custom",
      },
    },
    {
      "path": "/Member",
      "name": 'Member',
      "component": routerPath("Member"),
      "meta": {
        "keepAlive": false,
        "closable": true,
        "bar":true,
        "title": "会员管理",
        "icon":"el-icon-s-management",
      },
    },
    {
      "path": "/Lesson",
      "name": "Lesson",
      "component":routerPath("Lesson"),
      "meta": {
        "keepAlive": false,
        "closable": true,
        "bar":true,
        "title": "课程管理",
        "icon":"el-icon-s-order",
      },
    },
  ],
  "error":{//捕获未定义的路由配置
    "path": "*",
    "redirect": "/Error",
    "hidden": true
  }
};

router.beforeEach((to, from, next) => {
  if (!getRouter) { //不加这个判断,路由会陷入死循环
    if (!getObjArr('router')) {
      // postRequest(GetLeftList,params,Du).then(res=>{
        getRouter = asyncRouterMap; //假装模拟后台请求得到的路由数据
        saveObjArr('router', getRouter); //存储路由到localStorage
        routerGo(to, next); //执行路由跳转方法
      // });
    }
    else { //从localStorage拿到了路由
      getRouter = getObjArr('router'); //拿到路由
      routerGo(to, next)
    }
  } else {
    next()
  }

});

function routerGo(to, next) {
  let routerList=router.options.routes;//获取所有路由
  let children=asyncRouterMap.children;
  for (let i=0;i<children.length;i++) {
    routerList[0].children.push(children[i]);//将后台获取的路由添加至子级路由
    matcher(routerList,to, next);
    /*  router.addRoutes(routerList);
   直接覆盖路由,会出现路由重复,原来的路由还存在
  导致报Duplicate named routes definition
  replace 一个布尔类型,默认为false。如果replace设置为true,
  那么导航不会留下history记录,
  点击浏览器回退按钮不会再回到这个路由。*/
  }
}

//重新注册,清空以前的,注册现在的路由
function matcher(params,to,next) {//重新注册
  let error=asyncRouterMap.error;//捕获未定义的路由
  const flag=params.some(v=>v.path===error.path);//判断路由中是否已经存在 error
  if (!flag) params.push(error);//如果不存在,将错误路由捕获添加进去
  router.matcher = new Router({mode: 'history'}).matcher;
  router.addRoutes(params);
  next({ ...to, replace: true })
}

//路由路径处理
function routerPath(file) {
  return resolve => require(['@/views/' + file + '.vue'], resolve)
}

//localStorage 存储数组对象的方法
function saveObjArr(name, data) { //localStorage 存储数组对象的方法
  localStorage.setItem(name, JSON.stringify(data))
}

//localStorage 获取数组对象的方法
function getObjArr(name) { //localStorage 获取数组对象的方法
  return JSON.parse(window.localStorage.getItem(name));
}

 

版权声明:本文为XinYe666666原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/XinYe666666/article/details/103260712

智能推荐

android问题记录

Error: Cannot fit requested classes in a single dex file (# methods: 80441 > 65536) 解决办法: gradle文件的defaultConfig默认配置里面增加...

ROS机器人Diego 1# 利用人工智能 风格迁移技术拍摄不同画风的视频

风格迁移,就是将一种图片的风格迁移到其他图片上,改变其他图片的风格,很好玩的一个人工自能模型,github上已经有很多实现的方法,本文参考https://github.com/hzy46/fast-neural-style-tensorflow 的算法,利用Diego1#的平台实现实时视频的风格转换,先上两张图看效果: 是不是很酷呢,其实实现方法和上篇博文中的原理是一样的,只是把人工智能的算法包装...

数据分析学习总结笔记17:文本分析入门案例实战

文章目录 1 数据准备 2 分词 3 统计词频 4 词云 5 提取特征 6 用sklearn进行训练 1 数据准备 数据样例如下, 数据总量为7.7万+: 本节通过一个实战的例子来展示文本分析的最简单流程。首先设定因变量为原始数据中的"评分"。自变量是"评价内容",这里根据评价内容提取TF-IDF特征。之后,通过评价内容的特征建模预测下整体评分。 以上只是最...

LeetCode 150. 逆波兰表达式求值

题目描述 根据逆波兰表示法,求表达式的值。 有效的运算符包括 +, -, *, / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。 整数除法只保留整数部分。 给定逆波兰表达式总是有效的。 换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。 示例 1: 输入:[“2”, “1”, “+”, “3&r...

猜你喜欢

并查集原理及应用

并查集 树形的数据结构,每个集合有其代表节点,代表节点相同的元素属于同一集合。 find:通过查找节点的代表节点,判断节点所属集合。 union:合并两集合,小集合合并到大集合,使用大集合的代表节点。 在find的递归过程中,让路过节点的父节点直接赋值为代表节点,节省下次查找时间,如图所示。 计算岛的个数 遍历二维数组,遇到1时就将所相连的1都改为2,看看遇到多少次1,就是岛的数量。改数时使用回溯...

linux nutch1.0安装配置

1,下载nutch1.0 下载地址:http://archive.apache.org/dist/nutch/,下载这个文件nutch-1.0.tar.gz   2,上传到服务器 上传位置:/home/www/,解压nutch-1.0.tar.gz #tar -xvf nutch-1.0.tar.gz 重命名 #mv nutch-1.0 nutch   3,修改配置文...

如何搭建自己的博客?附加美化

如何搭建自己的blog?附加美化 前言: 之前在腾讯云以学生优惠租了一年的服务器,还买了一年的域名,忽然觉得不能闲置着域名,所以搭建了个博客,过程也遇到了很多的问题,望在此阐述,予以他人帮助,祝好~ 准备工作:使用Xshell连接上Linux服务器,我的是centos系统,方便进行操作。使用Xftp连接上Linux服务器,方便传输文件。 安装apache服务器:yum install httpd ...

rabbitmq五种模式详解(含实现代码)

1.简单模式 当生产端发送消息到交换机,交换机根据消息属性发送到队列,消费者监听绑定队列实现消息的接收和消费逻辑编写.简单模式下,强调的一个队列queue只被一个消费者监听消费. 1.1 结构   生产者:生成消息,发送到交换机 交换机:根据消息属性,将消息发送给队列 消费者:监听这个队列,发现消息后,获取消息执行消费逻辑 1.2应用场景 常见的应用场景就是一发,一接的结构 例如: 手机...

AndroidStudio 常用配置

1. 设置主题&左侧导航栏字体 AndroidStudio->Preferences(下同) 2. 设置字体大小 3. 取消竖线 间距设置大些 4. 控制台字体大小 5. 修改LogCat颜色 LogCat 色值 Verbose BBBBBB Debug 48BB31 Info 0070BB Warn BBBB23 Error FF0006 Assert 8F0005 5. 修改变量...