vue+vuex+vue-router+elementui实现无限标签页后台管理系统框架(动态路由)
效果图App主页面<template><div id="app"><router-view/></div></template><script>export default {name: 'App',created(){//禁止浏览器回退,只能回退一次...
·
效果图
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));
}
更多推荐
已为社区贡献5条内容
所有评论(0)