前后端分离项目实战-通用管理系统搭建(前端Vue3+ElementPlus,后端Springboot+Mysql+Redis)第八篇:Tab标签页的实现

本文主要是介绍前后端分离项目实战-通用管理系统搭建(前端Vue3+ElementPlus,后端Springboot+Mysql+Redis)第八篇:Tab标签页的实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!


天行健,君子以自强不息;地势坤,君子以厚德载物。


每个人都有惰性,但不断学习是好好生活的根本,共勉!


文章均为学习整理笔记,分享记录为主,如有错误请指正,共同学习进步。


黄鹤楼中吹玉笛,江城五月落梅花。
——《与史郎中钦听黄鹤楼上吹笛》


文章目录

  • 前后端分离项目实战-通用管理系统搭建(前端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: {},// 当前登录的标识tokentoken: null,}},getters: {// 获取当前用户信息getUserInfo(state:any){return state.userInfo;},// 获取当前tokengetToken(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);},// 存储tokensetToken: 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(){//menuList<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();
//        metaList<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: {},// 当前登录的标识tokentoken: null,}},getters: {// 获取当前用户信息getUserInfo(state:any){return state.userInfo;},// 获取当前tokengetToken(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);},// 存储tokensetToken: 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赋值fixedTabItemif(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/detailselectMenu(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则为收起状态,宽度设为60pxslideWidth.value = '60px';}else{// 如果为false则是展开状态,宽度设为250pxslideWidth.value = '250px'}}// 当前选中的tabconst activeName = ref('');// 当前打开的所有tabconst 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值改为truetabExsisted = 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


感谢阅读,祝君暴富!


这篇关于前后端分离项目实战-通用管理系统搭建(前端Vue3+ElementPlus,后端Springboot+Mysql+Redis)第八篇:Tab标签页的实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1117139

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

SQL中的外键约束

外键约束用于表示两张表中的指标连接关系。外键约束的作用主要有以下三点: 1.确保子表中的某个字段(外键)只能引用父表中的有效记录2.主表中的列被删除时,子表中的关联列也会被删除3.主表中的列更新时,子表中的关联元素也会被更新 子表中的元素指向主表 以下是一个外键约束的实例展示