所谓动态菜单,就是菜单数据从后台加载,前端接收到的是一个JSON,前端代码解析后渲染相应的菜单信息,相应的路由也应该是动态加载的。
TreeDetail.vue
<template>
<!--<h3>-->
<div>
<list-content :resourceId="resourceId"
:resourceType="resourceType"
ref="listContent">
</list-content>
</div>
<!--</h3>-->
</template>
<script>
import listContent from '../contents/content.vue'
export default {
name: "TreeViewDetail",
data() {
return {
isLoadList: false,
currentRoute: this.$route.path,
resourceId: '',
resourceType: '' };
},
created() {
},
mounted() {
this.initList();
},
methods: {
// 初始化列表
initList() {
if (this.$route.path != '' && this.$route.path != null && this.$route.path != undefined) {
this.resourceId = this.$route.name;
this.resourceType = 'link';
}
}
},
watch: {
//监听路由,只要路由有变化(路径,参数等变化)都有执行下面的函数
$route: {
handler: function(val, oldVal) {
this.resourceId = val.name;
this.resourceType = 'link';
this.currentRoute = val.name;
},
deep: true
}
},
components : {
listContent
}
};
</script>
<style scoped>
h3 {
margin-top: 10px;
font-weight: normal;
}
</style>
TreeView.vue
<template>
<div class="tree-view-menu">
<Tree-view-item :menus='menus'></Tree-view-item>
</div>
</template>
<script>
import TreeViewItem from "./TreeViewItem.vue";
export default {
components: {
TreeViewItem
},
mounted() {
console.log(this.menus);
},
name: "TreeViewMenu",
data() {
return {
menus: this.$store.state.menusModule.menus
};
}
};
</script>
<style scoped>
.tree-view-menu {
background-color: #404655;
width: 100%;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
}
.tree-view-menu::-webkit-scrollbar {
height: 6px;
width: 6px;
}
.tree-view-menu::-webkit-scrollbar-trac {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
}
.tree-view-menu::-webkit-scrollbar-thumb {
background-color: #6e6e6e;
outline: 1px solid #333;
}
.tree-view-menu::-webkit-scrollbar {
height: 4px;
width: 4px;
}
.tree-view-menu::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
}
.tree-view-menu::-webkit-scrollbar-thumb {
background-color: #6e6e6e;
outline: 1px solid #708090;
}
</style>
TreeViewItem.vue
<template>
<div class="tree-view-item">
<div class="level" :class="'level-'+ menu.resourceLevel" v-for="menu in menus" :key="menu.id">
<div v-if="menu.resourceType === 'link'">
<router-link class="link" v-bind:to="menu.resourceUrl" @click.native="toggle(menu)">{{menu.resourceName}}</router-link>
</div>
<div v-if="menu.resourceType === 'button'">
<div class="button heading" :class="{selected: menu.selected,expand:menu.expanded}" @click="toggle(menu)">
<span class="ats-tree-node"
:title="handleTitleVisible(menu.resourceName)">{{menu.resourceName}}</span>
<!--{{menu.resourceName}}-->
<div class="icon">
<svg xmlns="http://www.w3.org/2000/svg" focusable="false" viewBox="0 0 24 24">
<path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z "></path>
</svg>
</div>
</div>
<transition name="fade">
<div class="heading-children" v-show="menu.expanded" v-if="menu.childQueues">
<Tree-view-item :menus='menu.childQueues'></Tree-view-item>
</div>
</transition>
</div>
</div>
</div>
</template>
<script>
export default {
name: "TreeViewItem",
props: ["menus"],
created() {
this.$store.commit("firstInit", { url: this.$route.path });
},
mounted() {
},
data() {
return {
treeData: [],
}
},
methods: {
toggle(menu) {
this.$store.commit("findParents", { menu });
},
//超长菜单名后面用...代替,并且加上title属性
handleTitleVisible(title){
let titleLen = title.replace(/[^\x00-\xff]/g, '..').length;
if(titleLen>10){
return title;
}else{
return '';
}
}
}
};
</script>
<style scoped>
a {
text-decoration: none;
color: #333;
}
.level-1 {
font-size: 20px;
font-weight: bold;
}
.level-2 {
font-size: 18px;
font-weight: lighter;
}
.level-3 {
font-size: 16px;
font-weight: lighter;
}
.link {
font-size: 14px;
font-weight: lighter;
}
.button {
/*选中最靠近标题的菜单时,避免背景色一致导致视觉上连在一起*/
margin-bottom: 2px;
}
.link,
.button {
color:#ffffff;
display: block;
padding: 10px 15px;
transition: background-color 0.2s ease-in-out 0s, color 0.3s ease-in-out 0.1s;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
-khtml-user-select: none;
user-select: none;
}
.button {
position: relative;
}
.link:hover,
.button:hover {
color: #1976d2;
/*color: #ffffff;*/
/*background-color: #eee;*/
cursor: pointer;
}
.icon {
position: absolute;
right: 0;
display: inline-block;
height: 24px;
width: 24px;
fill: currentColor;
transition: -webkit-transform 0.15s;
transition: transform 0.15s;
transition: transform 0.15s, -webkit-transform 0.15s;
transition-timing-function: ease-in-out;
}
.heading-children {
padding-left: 14px;
overflow: hidden;
}
.expand {
display: block;
}
.collapsed {
display: none;
}
.expand .icon {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.selected {
/*background-color: #0071ff;*/
color: #0071ff;
font-weight: bold;
}
.fade-enter-active {
transition: all 0.5s ease 0s;
}
.fade-enter {
opacity: 0;
}
.fade-enter-to {
opacity: 1;
}
.fade-leave-to {
height: 0;
}
.ats-tree-node{
max-width: 130px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
}
</style>
初始化菜单信息以及菜单点击相关逻辑menusModule.js
let menus = toArray();
function toArray() {
var obj = sessionStorage.getItem("menuJson");
return JSON.parse(obj);
}
let resourceLevelNum = 1;
let startExpand = []; // 保存刷新后当前要展开的菜单项
function setExpand(source, resourceUrl) {
let sourceItem = '';
for (let i = 0; i < source.length; i++) {
sourceItem = JSON.stringify(source[i]); // 把菜单项转为字符串
// 查找当前 resourceUrl 所对应的子菜单属于哪一个祖先菜单,并且初始路径不为'/',否则会把菜单第一个选然后选中并展开
if (sourceItem.indexOf(resourceUrl) > -1 && resourceUrl != '/') {
// debugger;
if (source[i].resourceType === 'button') { // 导航菜单为按钮
source[i].selected = true; // 设置选中高亮
source[i].expanded = true; // 设置为展开
startExpand.push(source[i]);
// 递归下一级菜单,以此类推
setExpand(source[i].childQueues, resourceUrl);
}
break;
}
}
}
const state = {
menus,
resourceLevelNum
};
const mutations = {
findParents(state, payload) {
if (payload.menu.resourceType === "button") {
payload.menu.expanded = !payload.menu.expanded;
} else if (payload.menu.resourceType === "link") {
if (startExpand.length > 0) {
for (let i = 0; i < startExpand.length; i++) {
startExpand[i].selected = false;
}
}
startExpand = []; // 清空展开菜单记录项
setExpand(state.menus, payload.menu.resourceUrl);
};
},
firstInit(state, payload) {
setExpand(state.menus, payload.url);
},
// 初始化state中的菜单信息
initStateMenus(state, payload) {
state.menus = payload.menus;
}
}
export default {
state,
mutations
};
路由index.js
// 只给初始默认路由
import Vue from 'vue';
import Router from 'vue-router';
import welcome from '../components/welcome.vue';
import error from '../components/error.vue';
Vue.use(Router)
export default new Router({
linkActiveClass: 'selected',
mode: 'history',
routes: [{
path: '/',
name: 'welcome',
component: welcome
},
{
path: '/error',
name: 'error',
component: error
}
]
})
Home.vue
<template>
<Layout>
<div class="home-wrapper layout">
<div class="content-wrapper">
<template v-if="showHome">
<div class="side-bar">
<Tree-view></Tree-view>
</div>
<div class="continer">
<router-view></router-view>
</div>
</template>
</div>
</div>
<!--<Footer class="layout-footer-center">2011-2016 © TalkingData</Footer>-->
</Layout>
</template>
<script>
import listContent from './contents/content.vue'
import TreeView from "./tree_menu/TreeView.vue";
import TreeDetail from '../components/tree_menu/TreeDetail.vue';
export default {
name: "Home",
data() {
return {
showHome: false,
menuArr:[], // 遍历菜单menus时的临时变量
dynamicRouters: [] // 动态路由数组
}
},
created() {
this.initMenuInfo();
},
mounted() {
},
methods: {
refreshList : function () {
this.$refs.listContent.refreshListPage(this.resourceId);
},
// 初始化菜单
initMenuInfo() {
// 如果本地已存在,则不去后台加载
if (sessionStorage.getItem('menuJson')) {
// 添加动态路由
this.addDynamicRouters(JSON.parse(sessionStorage.getItem('menuJson')));
this.showHome = true;
} else {
this.axios.get(this.GLOBAL.gatewayCsbSrc + '/listQueue/getAppMenu/1').then((response) => {
//本地缓存菜单信息字符串
sessionStorage.removeItem('menuJson');
sessionStorage.setItem('menuJson',JSON.stringify(response.data));
// 添加动态路由
this.addDynamicRouters(response.data);
// 初始化状态中的菜单信息,否则第一次加载menusModule#firstInit中的status.menus为null,会报错
// 只有通过store.commit才有效
this.$store.commit("initStateMenus", { menus: response.data});
// this.$router.push("/");
this.showHome = true;
}).catch((response) => {
console.log(response);
})
}
},
// 动态添加路由
addDynamicRouters(menuObj) {
// 组装需要渲染的动态路由
this.assemblyRoutersArr(menuObj, this.menuArr);
// 将404页面的路由加在最后,因为路由会从上往下匹配,上面的匹配不到最终会走到这里,匹配任意,进入404页面
let errorRouter = {
path:'*',
redirect: '/error'
};
// 添加动态路由
this.dynamicRouters.push(errorRouter);
this.$router.addRoutes(this.dynamicRouters);
},
// 递归遍历菜单json,将其中type未link的提取出来拼成需要渲染的路由数组
assemblyRoutersArr(json, arr) {
if (json == undefined) {
return;
}
for (var i = 0; i < json.length; i++) {
var sonList = json[i].childQueues;
// 资源类型未link的,添加到路由中
if (json[i].resourceType == 'link') {
// 防止重复添加动态路由
if (this.containsRouter(json[i].resourceId.toString())) {
continue;
}
let routerObj = {
path: json[i].resourceUrl,
name: json[i].resourceId.toString(),
component: TreeDetail
};
this.dynamicRouters.push(routerObj);
}
if (sonList.length == 0) {
arr.push(json[i]);
} else {
this.assemblyRoutersArr(sonList, arr);
}
}
},
containsRouter(resourceId) {
let routers = this.dynamicRouters;
if (routers.length == 0) {
return false;
}
for (var i = 0; i < routers.length; i++) {
if (routers.name == resourceId) {
return true;
}
}
return false;
}
},
components: {
TreeView,
listContent,
TreeDetail
}
};
</script>
<style scoped>
.side-bar {
width: 180px;
height: 100%;
font-size: 14px;
}
.continer {
margin-left: 10px;
width: calc(100% - 200px);
height:100%;
}
.home-wrapper {
display : flex;
flex-direction: column;
flex:1;
width:100%;
height: 100%;
}
.content-wrapper {
display : flex;
flex-direction: row;
width:100%;
/*height: calc(100% - 60px);*/
height:100%;
}
.layout{
border: 1px solid #d7dde4;
background: #ffffff;
/*background-color: red;*/
position: relative;
border-radius: 4px;
overflow: hidden;
height: 100%;
}
</style>
静态图效果:
参考了网上开源的一个tree组件,原文找不到了。
动态菜单需要考虑每次都去后台加载,都需要连接查询,所以后台做了优化,菜单json存到redis中。
前端也做了优化,菜单加载后使用sessionStorage存储到本地,优先从本地加载,加载不到再发起后台请求。
递归遍历菜单json后,同时将路由信息也渲染一下,this.$router.addRoutes(routerArr),参数是数组,不是对象。如果链接不在路由信息中跳到404页面,路由信息从上向下查找,所以404的通配路由信息要放在最后,也就是上面未匹配到,最后匹配进入通配路由中,所以在动态路由加载完后,单独再将该路由信息添加到最后。