天行健,君子以自强不息;地势坤,君子以厚德载物。
每个人都有惰性,但不断学习是好好生活的根本,共勉!
文章均为学习整理笔记,分享记录为主,如有错误请指正,共同学习进步。
黄鹤楼中吹玉笛,江城五月落梅花。
——《与史郎中钦听黄鹤楼上吹笛》
文章目录
- 前后端分离项目实战-通用管理系统搭建(前端Vue3+ElementPlus,后端Springboot+Mysql+Redis)第八篇:Tab标签页的实现
- 25. Tab标签页的实现
- 25.1 本地缓存代码更新(store/index.ts)
- 25.2 菜单数据的后端接口代码
- 25.3 后端接口返回的菜单数据
- 25.4 创建菜单组件
- 25.5 菜单栏代码更新(MenuBar.vue)
- 25.6 主页代码更新(HomeIndex.vue)
- 25.7 App.vue代码更新
- 25.8 页面效果展示
- 25.9 代码下载地址
Vue入门学习专栏
前后端分离项目实战-通用管理系统搭建(前端Vue3+ElementPlus,后端Springboot+Mysql+Redis)第八篇:Tab标签页的实现
25. Tab标签页的实现
本小节将实现一下几点功能:
- 左侧菜单栏点击后在tab栏显示对应的标签页
- tab标签页与菜单动态绑定
- tab标签页与路由地址动态绑定
- tab标签页关闭按钮
25.1 本地缓存代码更新(store/index.ts)
src/store.index.ts
// 引入, 用于存储全局的状态数据,可供其他地方调用
import { createStore } from "vuex";
// 引入工具方法
import utils from "@/utils/utils";
// 创建一个新的store实例
const store = createStore({
state() {
return{
// count: 0
// 当前登录的用户信息
userInfo: {},
// 当前登录的标识token
token: null,
}
},
getters: {
// 获取当前用户信息
getUserInfo(state:any){
return state.userInfo;
},
// 获取当前token
getToken(state:any){
return state.token;
},
// 判断当前是否登录
isLogin(state:any){
console.log("---",state.token, "===",state.userInfo)
return (state.token && state.userInfo) ? true : false;
}
},
mutations: {
// 登出,清除缓存中的数据
logout: function(state:any){
console.log("---111---")
state.userInfo = null;
utils.removeData("userInfo");
utils.removeData("token");
// utils.removeData("username");
// utils.saveData("username","");
// utils.removeData("saveUsername");
// utils.removeData("password");
// utils.removeData("savePassword");
},
// 存储用户信息
setUserInfo: function(state:any, userInfo:any){
state.userInfo = userInfo;
utils.saveData('userInfo', userInfo);
},
// 存储token
setToken: function(state:any, token:any){
state.token = token;
utils.saveData('token', token);
}
}
})
export default store;
25.2 菜单数据的后端接口代码
MenuController.java
package com.hslb.management.controller;
import com.alibaba.fastjson2.JSONObject;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
/**
* @ClassDescription: 菜单相关接口
* @JdkVersion: 1.8
* @Author: 李白
* @Created: 2024/8/19 14:19
*/
@RestController
@CrossOrigin
@RequestMapping(value = "/menu")
public class MenuController {
@GetMapping(value = "/getMenu")
public JSONObject getMenu(){
//menu
List<JSONObject> menuList = new ArrayList<>();
//工作台-----------------------------------------------------------------------
JSONObject workPlat = new JSONObject();
workPlat.put("name","工作台");
workPlat.put("path","/HomeIndex/WorkPlat");
workPlat.put("icon","Platform");
workPlat.put("component","/src/views/workPlat/WorkPlat.vue");
workPlat.put("requireAuth",true);
//是否需要鉴权
// JSONObject meta = new JSONObject();
// meta
List<JSONObject> workPlatList = new ArrayList<>();
// JSONObject workPlatChildrenJs = new JSONObject();
// workPlatChildrenJs.put("name","列表实例");
// workPlatChildrenJs.put("path","/index/workPlat/List");
// workPlatChildrenJs.put("icon","ScaleToOriginal");
// workPlatChildrenJs.put("components","WorkPlat");
// workPlatList.add(workPlatChildrenJs);
workPlat.put("children",workPlatList);
menuList.add(workPlat);
//业务菜单-----------------------------------------------------------------------
JSONObject businessMenu = new JSONObject();
businessMenu.put("name","业务菜单");
businessMenu.put("path","/HomeIndex/businessMenu");
businessMenu.put("icon","Menu");
// businessMenu.put("component","BusinessMenu");
// businessMenu.put("requireAuth",true);
List<JSONObject> businessMenuList = new ArrayList<>();
//列表示例
JSONObject businessMenuListExam = new JSONObject();
businessMenuListExam.put("name","列表示例");
businessMenuListExam.put("path","/HomeIndex/businessMenu/listExam");
businessMenuListExam.put("icon","Tickets");
businessMenuListExam.put("component","/src/views/businessMenu/ListExam.vue");
businessMenuListExam.put("requireAuth",true);
businessMenuList.add(businessMenuListExam);
//详情示例
JSONObject businessMenuDetailExam = new JSONObject();
businessMenuDetailExam.put("name","详情示例");
businessMenuDetailExam.put("path","/HomeIndex/businessMenu/detailExam");
businessMenuDetailExam.put("icon","DocumentRemove");
businessMenuDetailExam.put("component","/src/views/businessMenu/DetailExam.vue");
businessMenuDetailExam.put("requireAuth",true);
businessMenuList.add(businessMenuDetailExam);
//图表示例
JSONObject businessMenuChartExam = new JSONObject();
businessMenuChartExam.put("name","图表示例");
businessMenuChartExam.put("path","/HomeIndex/businessMenu/chartExam");
businessMenuChartExam.put("icon","Postcard");
businessMenuChartExam.put("component","/src/views/businessMenu/ChartExam.vue");
businessMenuChartExam.put("requireAuth",true);
businessMenuList.add(businessMenuChartExam);
//文件上传
JSONObject businessMenuFileUpload = new JSONObject();
businessMenuFileUpload.put("name","文件上传");
businessMenuFileUpload.put("path","/HomeIndex/businessMenu/fileUpload");
businessMenuFileUpload.put("icon","Files");
businessMenuFileUpload.put("component","/src/views/businessMenu/FileUpload.vue");
businessMenuFileUpload.put("requireAuth",true);
businessMenuList.add(businessMenuFileUpload);
//富文本示例
JSONObject businessMenuRichTextExam = new JSONObject();
businessMenuRichTextExam.put("name","富文本示例");
businessMenuRichTextExam.put("path","/HomeIndex/businessMenu/richTextExam");
businessMenuRichTextExam.put("icon","Document");
businessMenuRichTextExam.put("component","/src/views/businessMenu/RichTextExam.vue");
businessMenuRichTextExam.put("requireAuth",true);
businessMenuList.add(businessMenuRichTextExam);
businessMenu.put("children",businessMenuList);
menuList.add(businessMenu);
//基础数据-----------------------------------------------------------------------
JSONObject baseData = new JSONObject();
baseData.put("name","基础数据");
baseData.put("path","/HomeIndex/baseData");
baseData.put("icon","TrendCharts");
// baseData.put("component","BaseData");
// baseData.put("requireAuth",true);
List<JSONObject> baseDataList = new ArrayList<>();
//基础数据-消息数据
JSONObject baseDataMsgData = new JSONObject();
baseDataMsgData.put("name","消息数据");
baseDataMsgData.put("path","/HomeIndex/baseData/msgData");
baseDataMsgData.put("icon","Message");
baseDataMsgData.put("component","/src/views/baseData/MsgData.vue");
baseDataMsgData.put("requireAuth",true);
baseDataList.add(baseDataMsgData);
//基础数据-实体配置
JSONObject baseDataEntitySet = new JSONObject();
baseDataEntitySet.put("name","实体配置");
baseDataEntitySet.put("path","/HomeIndex/baseData/entityConfig");
baseDataEntitySet.put("icon","Operation");
baseDataEntitySet.put("component","/src/views/baseData/EntityConfig.vue");
baseDataEntitySet.put("requireAuth",true);
baseDataList.add(baseDataEntitySet);
//基础数据-验证码数据
JSONObject baseDataValidationCode = new JSONObject();
baseDataValidationCode.put("name","验证码数据");
baseDataValidationCode.put("path","/HomeIndex/baseData/validationCode");
baseDataValidationCode.put("icon","DocumentChecked");
baseDataValidationCode.put("component","/src/views/baseData/ValidationCode.vue");
baseDataValidationCode.put("requireAuth",true);
baseDataList.add(baseDataValidationCode);
baseData.put("children",baseDataList);
menuList.add(baseData);
//系统管理-----------------------------------------------------------------------
JSONObject systemManagement = new JSONObject();
systemManagement.put("name","系统管理");
systemManagement.put("path","/HomeIndex/systemManagement");
systemManagement.put("icon","Tools");
// systemManagement.put("component","System");
// systemManagement.put("requireAuth",true);
List<JSONObject> systemManagementList = new ArrayList<JSONObject>();
//系统管理-用户管理
JSONObject sysMngUser = new JSONObject();
sysMngUser.put("name","用户管理");
sysMngUser.put("path","/HomeIndex/systemManagement/userManagement");
sysMngUser.put("icon","User");
sysMngUser.put("component","/src/views/systemManagement/UserManagement.vue");
sysMngUser.put("requireAuth",true);
systemManagementList.add(sysMngUser);
//系统管理-角色管理
JSONObject sysMngRole = new JSONObject();
sysMngRole.put("name","角色管理");
sysMngRole.put("path","/HomeIndex/systemManagement/roleManagement");
sysMngRole.put("icon","Van");
sysMngRole.put("component","/src/views/systemManagement/RoleManagement.vue");
sysMngRole.put("requireAuth",true);
systemManagementList.add(sysMngRole);
//系统管理-菜单管理
JSONObject sysMngMenu = new JSONObject();
sysMngMenu.put("name","菜单管理");
sysMngMenu.put("path","/HomeIndex/systemManagement/menuManagement");
sysMngMenu.put("icon","Reading");
sysMngMenu.put("component","/src/views/systemManagement/MenuManagement.vue");
sysMngMenu.put("requireAuth",true);
systemManagementList.add(sysMngMenu);
//系统管理-日志管理
JSONObject sysMngLog = new JSONObject();
sysMngLog.put("name","日志管理");
sysMngLog.put("path","/HomeIndex/systemManagement/logManagement");
sysMngLog.put("icon","Memo");
sysMngLog.put("component","/src/views/systemManagement/LogManagement.vue");
sysMngLog.put("requireAuth",true);
systemManagementList.add(sysMngLog);
//系统管理-系统配置
JSONObject sysMngSet = new JSONObject();
sysMngSet.put("name","系统配置");
sysMngSet.put("path","/HomeIndex/systemManagement/systemConfig");
sysMngSet.put("icon","DataLine");
sysMngSet.put("component","/src/views/systemManagement/SystemConfig.vue");
sysMngSet.put("requireAuth",true);
systemManagementList.add(sysMngSet);
systemManagement.put("children",systemManagementList);
menuList.add(systemManagement);
JSONObject resultJson = new JSONObject();
resultJson.put("result", 200);
resultJson.put("data", menuList);
resultJson.put("msg", "左侧栏菜单数据获取");
System.out.println(resultJson);
return resultJson;
}
}
25.3 后端接口返回的菜单数据
menuData.json
// 引入, 用于存储全局的状态数据,可供其他地方调用
import { createStore } from "vuex";
// 引入工具方法
import utils from "@/utils/utils";
// 创建一个新的store实例
const store = createStore({
state() {
return{
// count: 0
// 当前登录的用户信息
userInfo: {},
// 当前登录的标识token
token: null,
}
},
getters: {
// 获取当前用户信息
getUserInfo(state:any){
return state.userInfo;
},
// 获取当前token
getToken(state:any){
return state.token;
},
// 判断当前是否登录
isLogin(state:any){
console.log("---",state.token, "===",state.userInfo)
return (state.token && state.userInfo) ? true : false;
}
},
mutations: {
// 登出,清除缓存中的数据
logout: function(state:any){
console.log("---111---")
state.userInfo = null;
utils.removeData("userInfo");
utils.removeData("token");
// utils.removeData("username");
// utils.saveData("username","");
// utils.removeData("saveUsername");
// utils.removeData("password");
// utils.removeData("savePassword");
},
// 存储用户信息
setUserInfo: function(state:any, userInfo:any){
state.userInfo = userInfo;
utils.saveData('userInfo', userInfo);
},
// 存储token
setToken: function(state:any, token:any){
state.token = token;
utils.saveData('token', token);
}
}
})
export default store;
25.4 创建菜单组件
根据后端接口定义的菜单数据,创建所有菜单组件,当然,此时只是组件,内容并未实现
组件模板如下
<script setup lang="ts">
</script>
<template>
组件名称
</template>
<script setup>
</script>
在src/views/包下创建主菜单及子菜单组件如下
这里的组件名称需与接口返回数据中的路径一致
25.5 菜单栏代码更新(MenuBar.vue)
src/views/index/components/MenuBar.vue
<script setup lang="ts">
import { onMounted, reactive, ref, } from 'vue'
// import {reactive, onMounted, onUnmounted } from 'vue'
import utils from '@/utils/utils';
import api from '@/api/api';
import { useRoute, useRouter } from 'vue-router';
// import MenuBar from './components/MenuBar.vue';
// import ToolBar from './components/ToolBar.vue';
import HomeIndex from '../HomeIndex.vue';
const router = useRouter();
// 左侧菜单栏展开收起的标识
// const isCollapse = ref(true)
// 默认false,左侧栏展开
const isCollapse = ref(false)
const collapseController = (value:boolean)=> {
// isCollapse.value = !isCollapse.value;
isCollapse.value = value;
// 将值传到事件中
emits('menuCollapse', value);
}
// 定义事件,传值,并在主页监听
const emits = defineEmits(['menuCollapse', 'select'])
// const handleOpen = (key: string, keyPath: string[]) => {
// console.log(key, keyPath)
// }
// const handleClose = (key: string, keyPath: string[]) => {
// console.log(key, keyPath)
// }
// 菜单数据
const menuData = reactive([]);
// let menuData:any = null;
// 这里定义一个默认展示的路由地址,展示对应的菜单页面
const curMenu = ref("");
// 外部参数,这里是从HomeIndex组件中传过来的
const option = defineProps({
fixedTab:{
type: String
}
})
onMounted (()=>{
loadMenuData();
});
const checkToken = ref(1);
checkToken.value = utils.getData("token");
// 加载菜单数据
const loadMenuData = () => {
utils.showLoadding("加载中");
api.get("/menu/getMenu").then((res)=>{
utils.showLoadding("加载中");
if(!res||res.status!=200){
if(res.data){
utils.showError("问题");
return;
}
// utils.showError("加载失败");
return;
}
if(res.data.result==200){
// utils.showSuccess("请求成功")
menuData.values = res.data;
// menuData = res.data;
console.log("111",res.data);
console.log("222",menuData.values);
// menuData.splice(0, menuData.length);
// menuData.push(res.data.path);
// 将菜单信息注册到路由中
let indexChildrens:any = [];
// 固定tab页对象
let fixedTabItem = null;
menuData.values.data.forEach((item:any)=>{
console.log("item: ",item)
let routerItem:any = {
path: item.path,
// 注意:这里为了能正常使用还未创建的vue组件,故意将component写成component,不然报错
component: item.components,
meta:{
requireAuth: item.requireAuth
},
children: []
};
// 如果传过来的参数与当前的item路径path一致,则将当前item赋值fixedTabItem
if(option && option.fixedTab && option.fixedTab == item.path){
fixedTabItem = item;
}
if(item.children && item.children.length>0){
item.children.forEach((subItem:any)=>{
// console.log("subItem: ",subItem)
let subRouterItem:any = {
path: subItem.path,
component: subItem.components,
meta:{
requireAuth: subItem.requireAuth
},
};
if(option && option.fixedTab && option.fixedTab == subItem.path){
fixedTabItem = subItem;
}
routerItem.children.push(subRouterItem);
});
}
indexChildrens.push(routerItem);
console.log("indexChildrens: ",indexChildrens)
})
router.addRoute({
// path: '/:HomeIndex+',
// path: '/HomeIndex/:path+',
path: '/HomeIndex',
component: HomeIndex,
meta: {
requireAuth: true
// requireAuth: false
},
children: indexChildrens
});
// router.addRoute('/HomeIndex/:path+', indexChildrens);
// router.addRoute('/HomeIndex', indexChildrens);
// router.addRoute('/', indexChildrens);
// 这里判断固定的标签页是否有值,有值则先保持该标签页不关闭
if(option && option.fixedTab && fixedTabItem){
selectMenu(fixedTabItem.path);
}
// 根据url中的路由信息自动选中对应的菜单
// curMenu.value = router.currentRoute.value.path;
// 选中菜单de事件触发 传入的值为当前组件的路由地址如,/HomeIndex/businessMenu/detail
selectMenu(router.currentRoute.value.path);
console.log("router.currentRoute.value.path: ",router.currentRoute.value.path)
}
}).catch((error)=>{
console.log("error: ",error)
utils.hideLoadding();
utils.showError("加载失败");
}).finally(()=>{
utils.hideLoadding();
});
}
// 选择当前菜事件触发的方法
const selectMenu = (value:any)=>{
const curMenuData = selectMenuByPath(value);
emits('select', curMenuData);
};
// 根据路径选中对应菜单
const selectMenuByPath = (value:any)=>{
// if(value == curMenu.value){
// return;
// }
if(value){
curMenu.value = value;
// 当前菜单路由
console.log("selectMenu-value: ",value);
}
let curMenuData = null;
// 遍历菜单所有路由列表
menuData.values.data.forEach((item:any)=>{
// console.log("selectMenu-item: ",item);
// 如果获取的菜单路由地址和当前地址一致
if(item.path == curMenu.value){
// 将数据获取
curMenuData = item;
console.log("selectMenu-curMenuData: ",curMenuData);
}
// 如果该菜单项的子菜单不为空且子菜单数量大于0,即该项为二级菜单
if(item.children && item.children.length>0){
// 遍历子菜单
item.children.forEach((subItem:any)=>{
// console.log("selectMenu-subItem: ",subItem);
// 如果子菜单路由和子项的值一致
if(subItem.path==curMenu.value){
// 获取子项数据
curMenuData = subItem;
console.log("selectMenu-sub-curMenuData: ",curMenuData);
}
});
}
});
console.log("--------->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>---: ", curMenuData.path);
if(curMenuData){
console.log("--------->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>---: ", curMenuData.path);
// router.push(curMenuData.path);
}
return curMenuData;
}
// 暴露选中菜单方法,可让外部调用该方法选中对应菜单
defineExpose({
selectMenuByPath
})
// 13-8.5=4.5
// 36-4.5=31.5
// 12.5+7.5=20
// 11.5
</script>
<template>
<div class="logo" >
<div class="logo-name" v-if="!isCollapse">
寒山李白通用系统
</div>
<!-- 动态绑定侧边栏展开收起的图标按钮,当收起时即isCollapse为真,将class值转为logo-collapse-ef并设置图标居中 -->
<div class="logo-collapse" :class="{'logo-collapse-ef': isCollapse}">
<!-- 展开按钮 如果isCollapse是真则展示按钮,触发事件传值为false -->
<el-icon v-if="isCollapse" @click="collapseController(false)">
<Expand />
</el-icon>
<!-- 收起按钮 如果isCollapse是假则展示按钮,触发事件,传值为true -->
<el-icon v-else @click="collapseController(true)">
<Fold />
</el-icon>
</div>
</div>
<!-- <el-radio-group v-model="isCollapse" style="margin-bottom: 20px">
<el-radio-button :value="false">expand</el-radio-button>
<el-radio-button :value="true">collapse</el-radio-button>
</el-radio-group> -->
<!-- default-active="4" 设置加载时的激活项,此为4 -->
<!-- :collapse-transition="false" 取消收起展开时的动画,展开收起更快 -->
<!-- router 启用vue-router模式 激活导航时 以index作为path进行路由跳转 使用 -->
<el-menu
:default-active="curMenu"
class="el-menu-vertical-collapse"
:collapse="isCollapse"
:collapse-transition="false"
router
@select="selectMenu"
>
<!-- @open="handleOpen"
@close="handleClose" -->
<!-- 请求接口返回数据-获取其中的菜单数据data,遍历菜单数据中的每一项 -->
<template v-for="item in menuData.values.data">
<!-- 如果该项中有子项,则为二级菜单,继续进行遍历 -->
<el-sub-menu class="menu" v-if="item.children && item.children.length>0" :index="item.path">
<!-- 该项的一级菜单图标和名称 -->
<template #title>
<!-- 该项的一级菜单图标 -->
<component class="menu-icon" :is="item.icon"></component>
<!-- 该项的一级菜单名称 -->
<span>{{ item.name }}</span>
</template>
<!-- 该项的二级菜单遍历 -->
<template v-for="subItem in item.children">
<el-menu-item class="menu" :index="subItem.path">
<component class="menu-icon" :is="subItem.icon"></component>
<span>{{ subItem.name }}</span>
</el-menu-item>
</template>
</el-sub-menu>
<!-- 如果该项中没有子项,则为一级菜单,直接展示即可 -->
<el-menu-item class="menu" v-else :index="item.path">
<component class="menu-icon" :is="item.icon"></component>
<span>{{ item.name }}</span>
</el-menu-item>
</template>
<!-- <el-sub-menu index="1">
<template #title>
<el-icon><location /></el-icon>
<span>Navigator One</span>
</template>
<el-menu-item-group>
<template #title><span>Group One1</span></template>
<el-menu-item index="1-1">item one</el-menu-item>
<el-menu-item index="1-2">item two</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group Two1">
<el-menu-item index="1-3">item three</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="1-4">
<template #title><span>item four</span></template>
<el-menu-item index="1-4-1">item one</el-menu-item>
</el-sub-menu>
</el-sub-menu> -->
<!-- <el-menu-item index="2">
<el-icon><icon-menu /></el-icon>
<template #title>Navigator Two</template>
</el-menu-item>
<el-menu-item index="3" disabled>
<el-icon><document /></el-icon>
<template #title>Navigator Three</template>
</el-menu-item>
<el-menu-item index="4">
<el-icon><setting /></el-icon>
<template #title>Navigator Four</template>
</el-menu-item> -->
<!-- <el-sub-menu index="1">
<template #title>
<el-icon><location /></el-icon>
<span>Navigator Five</span>
</template>
<el-menu-item-group>
<template #title><span>Group One1</span></template>
<el-menu-item index="1-1">item one</el-menu-item>
<el-menu-item index="1-2">item two</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group Two1">
<el-menu-item index="1-3">item three</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="1-4">
<template #title><span>item four</span></template>
<el-menu-item index="1-4-1">item one</el-menu-item>
</el-sub-menu>
</el-sub-menu>
<el-sub-menu index="1">
<template #title>
<el-icon><location /></el-icon>
<span>Navigator Six</span>
</template>
<el-menu-item-group>
<template #title><span>Group One1</span></template>
<el-menu-item index="1-1">item one</el-menu-item>
<el-menu-item index="1-2">item two</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group Two1">
<el-menu-item index="1-3">item three</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="1-4">
<template #title><span>item four</span></template>
<el-menu-item index="1-4-1">item one</el-menu-item>
</el-sub-menu>
</el-sub-menu> -->
</el-menu>
</template>
<style scoped>
/* .el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
} */
.logo{
display: flex;
background-color: var(--el-color-info-light-7)
/* height: 60px; */
}
.logo-name{
/* position: fixed; */
/* top: 0; */
/* left: 0; */
flex: 1;
text-align: center;
font-size: 20px;
font-weight: bold;
letter-spacing: 2px;
padding: 2%;
background-image: -webkit-linear-gradient(right, rgba(78, 224, 33, 0.795), #22fc2d, rgb(236, 126, 36));
/* background-image: -webkit-background-clip(bottom, red, #fd8403, yellow); */
/* -webkit-background-clip: text; */
background-clip: text;
-webkit-text-fill-color: transparent;
}
.logo-collapse{
width: 20px;
/* margin-top: 10px; */
padding-right: 10%;
padding-top: 1%;
/* height: 30px; */
text-align: center;
cursor: pointer;
font-size: 30px;
}
.logo-collapse:hover{
color: var(--el-color-primary)
}
/* 动态绑定侧边栏收起展开图标的样式 */
.logo-collapse-ef{
/* 图标宽度居中 */
width: 100%
}
.el-menu-vertical-collapse{
/* 剔除侧边栏菜单边框,收起时无边框 */
border: none;
height: calc(100% - 60px);
overflow-y: auto;
}
/* 设置滚动条样式 */
.el-menu-vertical-collapse::-webkit-scrollbar{
width: 10px;
}
/* 滚动槽 */
.el-menu-vertical-collapse::-webkit-scrollbar-track{
-webkit-box-shadow: inset 0 0 6px var(--el-border-color-dark);
border-radius: 8px;
}
/* 滚动条滑块 */
.el-menu-vertical-collapse::-webkit-scrollbar-thumb{
border-radius: 8px;
background: var(--el-border-color-darker);
/* -webkit-box-shadow: inset 0 0 6px var(--el-border-color-dark); */
}
/* 滚动条上下设置 */
/* .el-menu-vertical-collapse::-webkit-scrollbar-thumb{
background: var(--el-border-color-darker);
} */
.el-menu-vertical-collapse:deep(.menu-icon){
width: 20px;
margin: 10px;
color: var(--el-color-primary);
}
.el-menu-vertical-collapse .menu:hover{
color: var(--el-color-primary);
}
</style>
25.6 主页代码更新(HomeIndex.vue)
src/views/index/HomeIndex.vue
<script setup lang="ts">
import { defineAsyncComponent, reactive, ref, } from 'vue'
// import {reactive, onMounted, onUnmounted } from 'vue'
// import utils from '@/utils/utils';
// import { useRoute, useRouter } from 'vue-router';
import MenuBar from './components/MenuBar.vue';
import ToolBar from './components/ToolBar.vue';
// 左侧菜单栏宽度
const slideWidth = ref('250px');
// 从MenuBar组件中传过来的值
const menuCollapse = (value:boolean)=>{
if(value){
// 如果值为true则为收起状态,宽度设为60px
slideWidth.value = '60px';
}else{
// 如果为false则是展开状态,宽度设为250px
slideWidth.value = '250px'
}
}
// 当前选中的tab
const activeName = ref('');
// 当前打开的所有tab
const tabDatas = reactive([]);
// 通过路径动态加载对应的组件
const getComponentByPath = (path:any) => {
// 异步组件
// return defineAsyncComponent(()=> import(
// new URL(path,import.meta.url).href
// ));
return defineAsyncComponent(()=> {
return import(new URL(path,import.meta.url).href);
});
// return defineAsyncComponent(()=> {return import(path)});
}
// 选中菜单的事件处理
const selectMenu = (value:any)=>{
let tabExsisted = false;
// 遍历tabDatas(打开的tab页)中的每一项
tabDatas.forEach(item => {
console.log("item.path: ",item);
// 如果包含当前选中菜单相同的项
if(item.path == value.path){
// 将tabExsisted值改为true
tabExsisted = true;
}
});
// 如果tabDatas(打开的tab页)中不包含当前选中菜单
if(!tabExsisted){
// 则将当前选中的菜单添加到tab页中
tabDatas.push(value);
}
activeName.value = value.path;
}
// 固定不可关闭的tab页
const fixedTab = ref("/HomeIndex/WorkPlat");
// menuBar组件实例
const menuBar = ref(null);
// tab标签改变时触发的事件
const tabChange = (value:any) => {
menuBar.value.selectMenuByPath(value);
}
// tab标签移出时触发事件
const tabRemove = () => {
}
</script>
<template>
<!-- 后台主页 -->
<div class="index-layout">
<el-container>
<!-- 宽度以变量形式传入,打开关闭侧边菜单栏 -->
<el-aside class="layout-aside" :width="slideWidth">
<MenuBar ref="menuBar" @menuCollapse="menuCollapse" @select="selectMenu" :fixedTab="fixedTab"></MenuBar>
<!-- <MenuBar @menuCollapse="menuCollapse" ></MenuBar> -->
</el-aside>
<el-container>
<el-main class="layout-main">
<!-- tab标签页 -->
<!-- <el-tabs v-model="activeName" class="main-tabs" @tab-click="handleClick"> -->
<el-tabs class="main-tabs" v-model="activeName" @tab-change="tabChange" @tab-remove="tabRemove">
<!-- 单个tab -->
<!-- <el-tab-pane label="User" name="first">主界面</el-tab-pane> -->
<!-- 这里需要注意:tabDatas不需要使用.values获取值,直接遍历即可 -->
<!-- <el-tab-pane class="tabs-pane" v-for="item in tabDatas" :name="item.path" closable> -->
<el-tab-pane class="tabs-pane" v-for="item in tabDatas" :name="item.path" :closable="fixedTab != item.path">
<!-- <el-tab-pane class="tabs-pane" v-for="item in tabDatas.values"> -->
<!-- <el-tab-pane class="tabs-pane" > -->
<!-- 主界面 -->
<!-- <RouterView></RouterView> -->
<template #label>
<span class="pane-label">
<!-- <el-icon class="label-icon">
<calendar />
</el-icon> -->
<!-- <el-icon class="label-icon"><Menu /></el-icon> -->
<component class="label-icon" :is="item.icon"></component>
<!-- <span class="label-span">工作台</span> -->
<span class="label-span">{{ item.name }}</span>
</span>
</template>
<component class="label-icon" :is="item.component?getComponentByPath(item.component):null"></component>
</el-tab-pane>
</el-tabs>
</el-main>
<!-- ToolBar 头部工具栏 -->
<el-header class="layout-header">
<ToolBar></ToolBar>
</el-header>
</el-container>
</el-container>
</div>
</template>
<style scoped>
.index-layout{
/* height: 100%; */
/* width: 100%; */
font-size: 20px;
}
/* 侧边菜单栏样式 */
.layout-aside{
/* height: 100%; */
height: 100vh;
box-shadow: var(--el-box-shadow);
/* 左右侧栏之间的边框线 */
border-right: var(--el-border);
}
/* 菜单栏与右侧界面的边距设为0 */
.layout-main{
padding: 0;
margin: 0;
background: var(--el-bg-color-page);
}
/* header头工具栏相对于主界面的样式设置 */
.layout-main:deep(.el-tabs__header){
/* 让主界面与header头工具栏的距离归0 */
margin: 0;
/* 头部栏背景色设为白色 */
background-color: #fff;
/* 头部栏左侧边框距离 */
padding-left: 10px;
/* 头部栏右侧边框距离 */
padding-right: 10px;
}
/* 图标的位置调整,与文字上下和左右距离 */
.layout-main:deep(.pane-label .label-icon){
/* 图标右侧边距 */
margin-right: 4px;
/* 位置 */
/* position: relative; */
/* 图标上方距离 */
top: 2px;
}
/* 标签样式设置 */
.main-tabs:deep(.label-icon ){
width: 15px;
/* height: 15px; */
/* 位置-图标和文本的位置持平 */
position: relative;
}
/* tab关闭按钮样式设置 */
.main-tabs:deep(.is-icon-close ){
/* width: 15px; */
/* height: 15px; */
/* 位置-图标和文本的位置持平 */
/* position: relative; */
top: 1px;
}
.layout-header{
position: fixed;
top: 0;
right: 0;
width: 300px;
/* height: 60px; */
line-height: 35px;
}
</style>
25.7 App.vue代码更新
src/App.vue
<script setup lang="ts">
import { onMounted } from 'vue';
// import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import utils from './utils/utils';
import api from './api/api';
// // // 引入暗黑主题的动态切换
// import { useDark, useToggle } from '@vueuse/core'
// const isDark = useDark()
// // // 切换主题函数
// const toggleDark = useToggle(isDark)
// 状态存储
// let store = useStore();
// 路由使用
const router = useRouter();
// 路由守卫
router.beforeEach((to, from)=>{
console.log("to: ",to)
// 鉴权 在router.ts中设置的requireAuth参数,true则需要鉴权,false则不需要
if(to.meta.requireAuth){
console.log("开始鉴权,===>>>")
// 进入鉴权,通过缓存中的token与接口中的token进行校验
// let token = utils.getData("token");
let routerToken = utils.getData("token");
let userInfo = utils.getData("userInfo");
// 当前不是登录状态
// console.log("20240021 token ",token);
console.log("store.getters.isLogin: ",(routerToken&&userInfo)?true:false);
// if(!store.getters.isLogin){
// 是否登录过,校验缓存中token和userinfo数据是否存在
const loginCheck = (routerToken&&userInfo)?true:false;
const userLogin = router.currentRoute.value.path;
console.log("!loginCheck: ", !loginCheck);
console.log("userLogin==UserLogin: ", userLogin=="/UserLogin");
// if(!loginCheck&&userLogin!="/UserLogin"){
if(!loginCheck){
// router.push({
// path: "/UserLogin",
// query: {
// redirect: router.currentRoute.value.fullPath
// }
// })
router.push("/UserLogin");
return false;
}
// if(router.currentRoute.value.path=="/UserLogin"){
// console.log("鉴权成功,====》》》》",router.currentRoute.value.path)
// // router.push("/");
// // return true;
// }
console.log("鉴权成功,====》》》》")
return true;
}
});
onMounted(()=>{
let token = "";
// let nToken = getToken();
// 由于token可能返回undefined报错,需要进行报错处理
try {
console.log("=== 1 ===");
token = utils.getData("token");
const userinfo = utils.getData("userInfo");
console.log("token: ",token);
console.log("userinfo: ",userinfo);
} catch (error) {
console.log("=== 2 ===");
console.log("error: ",error)
error;
}
let userInfo = utils.getData('userInfo');
if(token && userInfo){
console.log("=== 3 ===");
// 登录成功,验证
utils.showLoadding("正在加载")
const username = utils.getData('username');
console.log("username-1-1-1-1-",username);
if(!username){
console.log("=== 4 ===");
// 登录失败,跳转到登录页
if(username===undefined){
console.log("=== 5 ===");
utils.saveData("username","");
}
// token验证失败
utils.showError("用户名过期-请重新登录");
router.push('/UserLogin');
utils.hideLoadding();
}else{
console.log("=== 6 ===");
console.log("username: ", username);
api.get('/login/tokenCheck',{
params:{username}
}).then((res)=>{
console.log("res.data.token: ",res.data);
// newToken = res.data.token;
utils.hideLoadding();
if(res.data.token==token){
// 登陆成功
// store.commit('setUserInfo', userInfo);
// store.commit('setToken', token);
// router.push('/');
// 验证成功后保持当前页面,即刷新页面时不再跳转
// router.push(router.currentRoute.value.path);
// 也可注释掉跳转功能,此处不做跳转处理,使用MenuBar.vue中的selectMenu方法进行保持当前选中菜单路由
// 如果当前在UserLogin界面则进行跳转操作
console.log("currentRoutePath: ",router.currentRoute.value.fullPath)
// if(router.currentRoute.value.path=="/UserLogin"){
// router.push("/");
// }
utils.showSuccess("登录状态验证成功app.vue");
}else{
// if(username===undefined){
// utils.saveData("username","");
// }
// 登录失败
utils.showError("Token已过期,请重新登录");
// 登录失败,跳转到登录页
router.push('/UserLogin');
return;
}
});
// utils.showError("未知错误!!!!!!");
utils.hideLoadding();
}
}else{
// 登录失败,跳转到登录页
utils.showError("用户登录缓存过期,请重新登录");
router.push('/UserLogin');
utils.hideLoadding();
}
});
</script>
<template>
<!-- 暗黑主题动态切换按钮实现 -->
<!-- <button @click="toggleDark()">
<i inline-block align-middle i="dark:carbon-moon carbon-sun"/>
<span class="ml-2">{{ isDark ? 'Dark' : 'Light' }}</span>
</button> -->
<RouterView></RouterView>
</template>
<style scoped>
/* @import url(./styles/default.css);
@import url(./styles/theme/default-theme.css); */
/* html,
body{
margin: 0;
} */
/* #app{
width: 100%;
height: 100%;
} */
</style>
25.8 页面效果展示
控制台一致保持不关闭
标签页与路由地址及左侧菜单栏绑定
刷新页面后工作台和选中的菜单对应的标签页保留
25.9 代码下载地址
此阶段代码已上传到CSDN
前段项目下载地址:前端 vue 前后端分离项目hslb-management-system 菜单栏功能实现0827
后端项目下载地址:java springboot 前后端分离项目hslb-management-system 后端接口 菜单数据的更新优化0827
感谢阅读,祝君暴富!