文章目录
前言
通过路由和vuex可以实现各模块跳转及菜单统一管理。每模块页面根据命名规范,实现分别管理。本文将假设三类模块:车辆、人员、物品来举例说明。
一、建立路由
index.js
import { createRouter, createWebHistory } from 'vue-router'
import car from './modules/car';
import person from './modules/person';
import things from './modules/things';
const routes = [
{
path: '/',
name: 'home',
component: () => import('../views/HomeView.vue'),
meta: {
title: '首页',
}
},
{
path: '/system',
name: 'system',
redirect: '/car/royce',
component: () => import('@/components/layout.vue'),
meta: {
title: '系统管理'
},
children: [
{
path: '/car',
name: 'car',
redirect: '/car/royce',
component: () => import('@/components/leftNav.vue'),
meta: {
title: '车辆管理'
},
children: [...car]
},
{
path: '/person',
name: 'person',
redirect: '/person/teacher',
component: () => import('@/components/leftNav.vue'),
meta: {
title: '人员管理'
},
children: [...person]
},
{
path: '/things',
name: 'things',
redirect: '/things/pen',
component: () => import('@/components/leftNav.vue'),
meta: {
title: '物品管理'
},
children: [...things]
},
{
path: '/map',
name: 'map',
component: () => import('../views/map.vue'),
meta: {
title: '地图管理'
}
},
]
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
router.beforeEach((to, from, next) => {
if (to.meta.title) {
document.title = to.meta.title
}
next()
});
router.afterEach((to, from) => {
if (to.meta.title) {
document.title = to.meta.title
}
})
export default router
car.js
export default [
{
path: '/car/royce',
name: 'car.royce',
component: () => import('@/views/Car/royce/index.vue'),
meta: {
title: 'royce'
}
},
{
path: '/car/bentley',
name: 'car.bentley',
component: () => import('@/views/Car/bentley/index.vue'),
meta: {
title: 'bentley'
}
},
{
path: '/car/ferrari',
name: 'car.ferrari',
component: () => import('@/views/Car/ferrari/index.vue'),
meta: {
title: 'ferrari'
}
},
{
path: '/car/lamborghini',
name: 'car.lamborghini',
component: () => import('@/views/Car/lamborghini/index.vue'),
meta: {
title: 'lamborghini'
}
},
{
path: '/car/bugatti',
name: 'car.bugatti',
component: () => import('@/views/Car/bugatti/index.vue'),
meta: {
title: 'bugatti'
}
},
{
path: '/car/maybach',
name: 'car.maybach',
component: () => import('@/views/Car/maybach/index.vue'),
meta: {
title: 'maybach'
}
},
{
path: '/car/maserati',
name: 'car.maserati',
component: () => import('@/views/Car/maserati/index.vue'),
meta: {
title: 'maserati'
}
},
]
person.js
export default [
{
path: '/person/teacher',
name: 'person.teacher',
component: () => import('@/views/Person/teacher/index.vue'),
meta: {
title: 'teacher'
}
},
{
path: '/person/doctor',
name: 'person.doctor',
component: () => import('@/views/Person/doctor/index.vue'),
meta: {
title: 'doctor'
}
},
{
path: '/person/engineer',
name: 'person.engineer',
component: () => import('@/views/Person/engineer/index.vue'),
meta: {
title: 'engineer'
}
},
{
path: '/person/designer',
name: 'person.designer',
component: () => import('@/views/Person/designer/index.vue'),
meta: {
title: 'designer'
}
},
{
path: '/person/writer',
name: 'person.writer',
component: () => import('@/views/Person/writer/index.vue'),
meta: {
title: 'writer'
}
},
]
things.js
export default [
{
path: '/things/pen',
name: 'things.pen',
component: () => import('@/views/Things/pen/index.vue'),
meta: {
title: 'pen'
}
},
{
path: '/things/book',
name: 'things.book',
component: () => import('@/views/Things/book/index.vue'),
meta: {
title: 'book'
}
},
{
path: '/things/table',
name: 'things.table',
component: () => import('@/views/Things/table/index.vue'),
meta: {
title: 'table'
}
},
{
path: '/things/umbrella',
name: 'things.umbrella',
component: () => import('@/views/Things/umbrella/index.vue'),
meta: {
title: 'umbrella'
}
},
{
path: '/things/glasses',
name: 'things.glasses',
component: () => import('@/views/Things/glasses/index.vue'),
meta: {
title: 'glasses'
}
},
{
path: '/things/headphones',
name: 'things.headphones',
component: () => import('@/views/Things/headphones/index.vue'),
meta: {
title: 'headphones'
}
}
]
二、tab菜单页面
1.一级菜单页面 layout.vue
<template>
<div class="main">
<div class="header">
<template v-for="item in menuList" :key="item.path">
<div class="menuItem" :class="{ active: isActive(item) }" @click="router.push({ path: item.path })">
{{ item.name }}
</div>
</template>
</div>
<router-view :key="route.fullPath" class="content"></router-view>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter, useRoute } from "vue-router";
import { useStore } from "vuex";
const store = useStore();
const route = useRoute();
const router = useRouter();
// 菜单
let menuList = reactive([
{ name: "车辆管理", path: "/car" },
{ name: "人员管理", path: "/person" },
{ name: "物品管理", path: "/things" },
{ name: "地图管理", path: "/map" },
]);
const isActive = (item) => route.path.includes(item.path);
</script>
<style lang="scss" scoped>
.main {
width: 100vw;
height: 100vh;
color: #fff;
.header {
width: 100%;
height: 3.75rem;
line-height: 3.75rem;
padding: 0 0.625rem;
display: flex;
justify-content: space-evenly;
background: linear-gradient(270deg, #3695fd 0%, #295acc 99%);
.menuItem {
height: 100%;
cursor: pointer;
padding: 0 1rem;
color: #fff;
font-size: 1rem;
&:hover {
background: rgba(0, 0, 0, 0.1);
border-bottom: 0.125rem solid #00d4ff;
}
&.active {
background: rgba(0, 0, 0, 0.1);
border-bottom: 0.125rem solid #00d4ff;
}
}
}
.content {
height: calc(100% - 3.75rem);
width: 100%;
display: flex;
}
}
</style>
2.二级菜单页面(左侧竖行) leftNav.vue
<template>
<div class="system_main">
<div class="menu_list">
<template v-for="item in menuList" :key="item.id">
<div class="menu_item" :class="{ active: isActive(item) }" @click="handleLink(item)">
<span>
<p>{{ item.name }}</p>
</span>
</div>
</template>
</div>
<div class="menu_main">
<HistoryMenu />
<div style="height:calc(100% - 2.5rem);overflow-y: auto;">
<router-view></router-view>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import HistoryMenu from './HistoryMenu.vue'
import { useStore } from 'vuex'
import car from '@/router/modules/car';
import person from '@/router/modules/person';
import things from '@/router/modules/things';
const router = useRouter();
const route = useRoute();
const store = useStore()
const tabHistory = computed(() => store.state.tabHistory)
const menuList = ref([])
const moduleMappings = {
'/car/': car,
'/person/': person,
'/things/': things,
};
const getModulesMenu = () => {
const currentPath = route.path;
for (const path in moduleMappings) {
if (currentPath.includes(path)) {
const dataSource = moduleMappings[path];
menuList.value = dataSource.reduce((acc, item, index) => {
const arr = item.name.split('.');
if (arr.length === 2) {
acc.push({
id: index,
name: item.meta.title,
path: item.path,
pathName: arr[1],
});
}
return acc;
}, []);
if (menuList.value.length > 0) {
handleTabHistory(route.path, menuList.value[0])
}
break;
}
}
};
const handleLink = row => {
router.push({ path: row.path });
handleTabHistory(row.path, row);
};
const handleTabHistory = (curPath, addTab) => {
const existsIndex = tabHistory.value.findIndex(tab => curPath.includes(tab.path));
if (existsIndex === -1) {
store.commit('addTabHistory', addTab)
}
}
const isActive = item => route.path.includes(item.path)
onMounted(() => {
getModulesMenu();
});
</script>
<style lang='scss' scoped>
.system_main {
overflow: hidden;
.menu_list {
width: 5.625rem;
height: 100%;
overflow-y: auto;
background: #122c52;
box-shadow: 0px 3px 8px 0px rgba(0, 0, 0, 0.16);
float: left;
.menu_item {
height: 6.25rem;
text-align: center;
cursor: pointer;
font-size: 0.875rem;
&:hover {
background: #1a427c;
}
span {
display: inline-block;
position: relative;
top: 3.125rem;
transform: translateY(-50%);
img {
width: 3.125rem;
height: 3.125rem;
}
p {
color: #fff;
margin-top: -0.625rem;
}
}
&.active {
background: #1a427c;
}
}
}
.menu_main {
width: calc(100% - 5.625rem);
height: 100%;
color: #000;
}
}
</style>
3.历史页面记录(内容上方可关闭的tab) HistoryMenu.vue
<template>
<div class="historyMenu flex justify-start items-center">
<template v-for="(item, index) in tabHistory" :key="index">
<div :class="['tabMenu', { 'activeTag': route.path.includes(item.path) }]" @click="router.push({ path: item.path })">
<div class="name">{{item.name}}</div>
<el-icon v-if="tabHistory.length !== 1" class="close" @click.stop="handleClose(item,index)">
<Close />
</el-icon>
</div>
</template>
<el-tooltip effect="dark" content="返回" placement="bottom">
<div @click="router.go(-1)" style="cursor: pointer;position: absolute;right: .3125rem;height: 2.1875rem">
<img src="@/assets/back.png" alt="" style="height: 100%;">
</div>
</el-tooltip>
</div>
</template>
<script setup>
import { useRoute, useRouter } from 'vue-router'
import { ref, computed } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
const route = useRoute()
const router = useRouter()
const tabHistory = computed(() => store.state.tabHistory)
const handleClose = (item, index) => {
store.commit('removeTabHistory', index)
// 删除当前选中菜单
if (route.path == item.path) {
if (index === tabHistory.value.length) { // 从后面删
router.push({ path: tabHistory.value[tabHistory.value.length - 1].path })
} else if (index === 0) { // 从前面删
router.push({ path: tabHistory.value[0].path })
} else { // 从中间删
router.push({ path: tabHistory.value[index ].path })
}
}
}
</script>
<style lang="scss" scoped>
.historyMenu {
height: 2.1875rem;
padding: 0;
background: rgba(245, 245, 245, 1);
border-bottom: 0.125rem solid #3a83fd;
border-radius: 0.3125rem 0.3125rem 0 0;
position: relative;
margin-bottom: 0.625rem;
.tabMenu {
height: 100%;
position: relative;
display: inline-block;
cursor: pointer;
font-size: 0.875rem;
font-weight: 400;
color: #213d64;
background: none;
&:before {
content: "";
position: absolute;
bottom: 0;
left: 0;
border-left: 0.625rem solid rgba(245, 245, 245, 1);
border-right: 0.625rem solid rgba(245, 245, 245, 1);
border-bottom: 2rem solid #fff;
border-top: none;
width: 100%;
height: 0;
}
.name {
position: relative;
padding: 0rem 1.375rem;
text-align: center;
z-index: 1;
line-height: 2.1875rem;
}
.close {
position: absolute;
top: 0.2rem;
right: 0.625rem;
width: 0.625rem;
height: 0.625rem;
line-height: 0.625rem;
text-align: center;
border-radius: 50%;
font-size: 0.625rem;
color: #409eff;
cursor: pointer;
z-index: 2;
&:hover {
background: #409eff;
color: #fff;
}
}
&.activeTag {
color: #fff;
&:before {
border-bottom-color: rgba(58, 131, 253, 1);
}
.close {
background: transparent;
color: #fff;
}
}
}
}
.rotate-scale-enter-active,
.rotate-scale-leave-active {
transition: all 0.3s ease;
}
.rotate-scale-enter,
.rotate-scale-leave-to {
transform: scaleX(0);
opacity: 0;
}
</style>
三、页面内容
1.App.vue
<template>
<router-view />
</template>
2.HomeView.vue
<template>
我是首页
<div @click="router.push({name:'system'})" class="btn"> 去系统管理</div>
</template>
<script setup>
import router from '@/router';
</script>
<style lang='scss' scoped>
.btn {
color: skyblue;
text-decoration: solid;
cursor: pointer;
}
</style>
3.map.vue
该页面为测试页面,主要用于测试只使用一级菜单,不使用二级菜单的情况。
4.其他页面
其他页面都是统一的代码。
<template>
{{route.meta.title}}
</template>
<script setup>
import { useRoute } from 'vue-router';
const route = useRoute()
</script>
四.vuex
index.js
import { createStore } from 'vuex'
import getters from "./getters";
import mutations from "./mutations";
import actions from "./actions";
import createPersistedState from 'vuex-persistedstate'
export default createStore({
state: {
tabHistory: [], // 历史tab
},
getters,
mutations,
actions,
modules: {},
plugins: [
createPersistedState({
storage: window.localstorage,
paths: ['tabHistory']
})
]
})
mutations.js
export default {
addTabHistory(state, tab) {
state.tabHistory.push(tab)
if (state.tabHistory.length > 12) {
state.tabHistory.splice(0, 1)
}
},
removeTabHistory(state, tabIndex) {
state.tabHistory.splice(tabIndex, 1)
},
}
五.在此基础上建立一个左侧有二级菜单的版本(结合elementplus的el-menu,可建立多级)
leftNav.vue
<el-menu router :collapse-transition="true" :default-active="route.path" class="el-menu-vertical-demo" unique-opened="true">
<template v-for="(item, index) in menuList">
<el-menu-item v-if="!item.children" :index="item.router" :key="item.id" @click="e=>getInfo(e,item)">
<img :src="require(`@/assets/img/layout/${item.icon}${isActive(item)? 'active':''}.png`)" alt="">
<span>{{ item.name }}</span>
</el-menu-item>
<el-sub-menu v-else :index="item.router" :key="item.id" class="sub-menu">
<template #title>
<div class="menu_item_left">
<img :src="require(`@/assets/img/layout/${item.icon}${isActive(item)? 'active':''}.png`)" alt="">
<span>{{ item.name }}</span>
</div>
</template>
<el-menu-item v-for="j in item.children" :index="j.router" :key="j.id" class="sub-menu-item" @click="e=>getInfo(e,item)">
<img :src="require(`@/assets/img/layout/${j.icon}.png`)" alt="">
<span>{{ j.name }}</span>
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
const getInfo = (e, item) => {
let obj = {}
if (item.children) {
obj = item.children.filter(i => i.router == e.index).map(j => ({ name: j.name, path: j.router }))[0]
} else {
obj = { name: item.name, path: e.index }
}
handleTabHistory(e.index, obj);
}
const handleTabHistory = (curPath, addTab) => {
const existsIndex = tabHistory.value.findIndex(tab => curPath.includes(tab.path));
if (existsIndex === -1) {
store.commit('addTabHistory', addTab)
}
}
<style lang='scss' scoped>
::v-deep {
.el-menu-vertical-demo {
margin-top: 4px;
}
.el-menu {
border-right: 0;
}
.el-sub-menu .el-sub-menu__icon-arrow {
position: relative;
top: 3px;
right: 0;
background: url("@/assets/img/layout/arrow.png") center center no-repeat;
&:before {
content: "";
visibility: hidden;
}
svg {
display: none;
}
}
.sub-menu .el-sub-menu__title {
display: flex;
align-items: center;
justify-content: space-between;
.menu_item_left {
display: flex;
align-items: center;
}
}
.el-menu.el-menu-vertical-demo .el-menu-item,
.el-sub-menu__title {
width: calc(100% - 8px);
margin: 0 4px;
padding: 0 10px 0 16px !important;
height: 40px;
line-height: 40px;
border-radius: 2px;
cursor: pointer;
font-size: 14px;
color: #656c83;
&.is-active {
background: #cee3ff;
color: #2487ff;
font-weight: 500;
}
img {
width: 14px;
height: 14px;
margin-right: 8px;
}
}
.sub-menu-item.el-menu-item {
font-weight: 350;
font-size: 14px;
color: #3b4e73;
margin: 8px 8px 8px 20px;
width: calc(100% - 28px);
height: 24px;
line-height: 24px;
cursor: pointer;
border-radius: 2px 2px 2px 2px;
&.active,
&:hover {
background: #f0f6ff;
color: #2487ff;
}
img {
margin: 0 4px 0 16px;
width: 12px;
height: 12px;
}
}
}
</style>