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

news2024/12/23 3:05:19

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


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


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


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


文章目录

  • 前后端分离项目实战-通用管理系统搭建(前端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


感谢阅读,祝君暴富!


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2083842.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Selenium】UI自动化实践——输入验证码登录

文章目录 实战题目解题方案 实战题目 使用pythonselenium实现输入验证码的UI自动化。登录页面如图&#xff1a; 解题方案 验证码登录需要导入相关模块和库&#xff0c;本文使用的是opencv和ddddocr模块组合&#xff0c;导入方式采用pip3 install opencv-python、pip3 insta…

JMeter 工具安装以及简单使用

一、安装以及汉化 傻瓜式JMeter下载和环境配置及永久汉化-CSDN博客https://blog.csdn.net/weixin_45608163/article/details/136528719 二、发送GET请求 配置请求头: 配置该线程组的请求: 放在线程组统计,下面请求则共享配置

深度强化学习算法(三)(附带MATLAB程序)

深度强化学习&#xff08;Deep Reinforcement Learning, DRL&#xff09;结合了深度学习和强化学习的优点&#xff0c;能够处理具有高维状态和动作空间的复杂任务。它的核心思想是利用深度神经网络来逼近强化学习中的策略函数和价值函数&#xff0c;从而提高学习能力和决策效率…

光性能 -- 光功率平坦度

什么是光功率平坦度? 光功率平坦度指的是&#xff0c;光放各单波功率值与所有波平均值的功率差。 通过MCA&#xff08;多通道光谱分析单元&#xff09;扫描OMS&#xff08;光复用段&#xff09;上的所有单波光功率&#xff0c;计算经过光放的所有波长的功率平均值&#xff0…

OHIF viewers

OHIF Viewer 是一个开源的 DICOM&#xff08;数字成像和通信医学&#xff09;图像查看器&#xff0c;旨在为医疗影像学提供一个灵活且功能强大的解决方案。以下是 OHIF Viewer 的详细介绍&#xff0c;包括发展史、特点、优势、应用及目的等方面的信息。 1. 介绍 OHIF Viewer 是…

一个初始化的服务器,需要配置的相关软件以及环境(cuda、torch、conda)

文章目录 一个刚初始化的服务器需要下载的应用google chromeghelp 解压安装包解压大型zip文件 更新nvidia的驱动pycharm设置conda相关下载condaconda换源 torch相关安装torch包&#xff0c;浏览器下载包安装pytorch常用包安装 导包的方法 一个刚初始化的服务器需要下载的应用 …

【AI】:探索在图像领域的无限可能

欢迎来到 破晓的历程的 博客 ⛺️不负时光&#xff0c;不负己✈️ 文章目录 图像识别与分类的飞跃图像生成与创造的艺术图像增强与修复的神奇图像搜索与理解的智能图像分析与挖掘的洞察图形生成技术1. 生成对抗网络&#xff08;GANs&#xff09;2. 卷积神经网络&#xff08;CN…

Jenkins+Docker | K8S虚拟化实现网站自动部署 简单流程 未完待续,,

目录 大纲 1.Jenkins 的设置与 Docker、Kubernetes 集成指南 1. 创建新的Pipeline项目或Freestyle项目 1.1 创建Pipeline项目 1.2 创建Freestyle项目 2. 配置源代码管理 2.1 配置Git作为源代码管理工具 3. 配置构建触发器 4. 配置构建步骤 4.1 对于Pipeline项目 4.2…

Threadlocal+拦截器+JWT实现登录

很多数据库表都会有创建时间和修改时间&#xff0c;这个可以用mp的自动填充来实现。 也有修改人和更新人的字段&#xff0c;用户登录进来后&#xff0c;修改数据如何拿到修改人呢&#xff1f;每次操作不能把操作人的信息都携带者&#xff0c;那么如何拿到修改人的数据&#xf…

数学建模赛前备赛——模拟退火算法

一.什么是智能优化算法 智能优化算法本质上是一个优化算法,它通过不断优化模型的参数,使得系统表现达到最优&#xff0c;常见的只能优化算法有很多&#xff0c;比如说蚁群算法,遗传算法以及我们今天的主角——模拟退火算法。 二.模拟算法的前身——爬山算法 爬山算法是一种简…

【Python入门】第1节 基础语法

&#x1f4d6;第1节 基础语法 ✅字面量✅注释✅变量✅数据类型&#x1f9ca;数据类型转换 ✅标识符✅运算符✅字符串扩展&#x1f9ca;字符串的三种定义方式&#x1f9ca;字符串拼接&#x1f9ca;字符串格式化&#x1f9ca;格式化的精度控制&#x1f9ca;字符串格式化方式2&…

equals与== 区别,全面总结如何使用(Java)

先理解JVM内存模型 虚拟机栈&#xff1a;JVM 运行过程中存储当前线程运行方法所需的数据&#xff0c; 指令、 返回地址本地方法栈&#xff1a;Java程序自动调用底层C/C函数库程序计数器&#xff1a;当前线程执行的字节码的行号指示器堆&#xff1a;存放我们申请的对象&#xff…

【Python 千题 —— 基础篇】入门异常处理

Python 千题持续更新中 …… 脑图地址 👉:⭐https://twilight-fanyi.gitee.io/mind-map/Python千题.html⭐ 题目描述 题目描述 编写一个程序,要求在处理用户输入时捕获各种异常情况,并为每种异常提供相应的处理方式。具体要求如下: 定义一个函数 divide_numbers(),它接…

php mail函数配置SMTP服务器发邮件的指南!

php mail函数安全性考虑&#xff1f;PHP mail()函数漏洞利用技巧&#xff1f; 在使用PHP进行开发时&#xff0c;发送邮件是一个常见的需求。使用php mail函数配置SMTP服务器发邮件&#xff0c;则是实现这一需求的有效途径。AokSend将详细探讨如何通过php mail函数来配置SMTP服…

Density-invariant Features for Distant Point Cloud Registration 论文解读

目录 一、导言 二、先导知识 1、FCGF 三、相关工作 1、深度学习的点云配准 2、对抗密度变化的方法 3、对比学习 四、GCL方法 1、U型曲线假设 一、导言 该论文来自于ICCV2023&#xff0c;上海交通大学提出的基于组对比学习的方案&#xff0c;来提取密度不变的几何特征&…

【终端IDPS】开源安全平台Wazuh之Wazuh Server

引言 Wazuh是一个开源的、免费的企业级安全监控解决方案&#xff0c;专注于威胁检测、完整性监控、事件响应和合规性。它由部署在受监控系统的端点安全代理和管理服务器组成&#xff0c;服务器收集并分析代理收集的数据。Wazuh支持多平台&#xff0c;包括Windows、Linux、macOS…

Linux学习笔记4 重点!网络排障命令

网络排障命令 命令行下载工具wget wget https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.20.17.tar.gz wget https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.20.17.tar.gz 限速下载 wget --limit-rate1M https://mirrors.edge.kernel.or…

【已解决】Vue Duplicate keys detected: ‘[object Object]’

【已解决】Vue Duplicate keys detected: ‘[object Object]’ 在Vue项目开发过程中&#xff0c;我们可能会遇到这样的报错&#xff1a;“Duplicate keys detected: ‘[object Object]’. This may cause an update error.”。这个错误通常发生在Vue的虚拟DOM进行渲染更新时&a…

上书房信息咨询:医疗满意度调研

随着人们生活水平的不断提高&#xff0c;医疗服务的需求日益增长。近期&#xff0c;上书房信息咨询受托完成了某市医疗市场的满意度调研&#xff0c;旨在深入了解市民对医疗服务的评价和需求&#xff0c;为提升医疗服务质量提供有力支持。 近年来&#xff0c;某市致力于推进医…

鸿蒙ArkTS语言学习(五):扩展(函数)@Extend@Styles@Builder

如何实现结构、样式复用呢&#xff1f; Extend&#xff1a;扩展组件&#xff08;样式、事件&#xff09; 作用&#xff1a;将相同组件复用的属性结构抽取封装&#xff0c;将不同的结构通过传入参数进行修改。 1. 定义语法 Extend(组件名) function 函数名{ ... } 2. 调用 组件…