前后端分离项目实战-通用管理系统搭建(前端Vue3+ElementPlus,后端Springboot+Mysql+Redis)第七篇:菜单和路由动态绑定


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


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


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


绿竹入幽径,青萝拂行衣。

------《下终南山过斛斯山人宿置酒》


文章目录

    • [24. 菜单和路由动态绑定](#24. 菜单和路由动态绑定)
      • [24.1 后端接口代码修改(MenuController.java)](#24.1 后端接口代码修改(MenuController.java))
      • [24.2 接口返回数据](#24.2 接口返回数据)
      • [24.3 路由代码修改(src/router/index.ts)](#24.3 路由代码修改(src/router/index.ts))
      • [24.4 缓存代码修改(src/store/index.ts)](#24.4 缓存代码修改(src/store/index.ts))
      • [24.5 主页代码修改(src/views/index/HomeIndex.vue)](#24.5 主页代码修改(src/views/index/HomeIndex.vue))
      • [24.6 菜单栏组件代码修改(src/views/index/components/MenuBar.vue)](#24.6 菜单栏组件代码修改(src/views/index/components/MenuBar.vue))
      • [24.7 工具栏组件代码修改(src/views/index/components/ToolBar.vue)](#24.7 工具栏组件代码修改(src/views/index/components/ToolBar.vue))
      • [24.8 新增组件-工作台(src/views/index/components/WorkPlat.vue)](#24.8 新增组件-工作台(src/views/index/components/WorkPlat.vue))
      • [24.9 手机验证码登录组件代码修改(src/views/login/components/PhoneCodeForm.vue)](#24.9 手机验证码登录组件代码修改(src/views/login/components/PhoneCodeForm.vue))
      • [24.10 二维码登录组件代码修改(src/views/login/components/QcodeForm.vue)](#24.10 二维码登录组件代码修改(src/views/login/components/QcodeForm.vue))
      • [24.11 账号密码登录组件代码修改(src/views/login/components/UsernameForm.vue)](#24.11 账号密码登录组件代码修改(src/views/login/components/UsernameForm.vue))
      • [24.12 App.vue组件代码修改(src/App.vue)](#24.12 App.vue组件代码修改(src/App.vue))
      • [24.13 页面效果展示](#24.13 页面效果展示)

Vue入门学习专栏


24. 菜单和路由动态绑定

将菜单和路由地址动态绑定,实现点击菜单跳转对应路由,且刷新页面依旧保持当前选中菜单界面

24.1 后端接口代码修改(MenuController.java)

这里需要先修改后端接口代码中的数据结构,修改后代码如下

MenuController.java

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","WorkPlat");
        //是否需要鉴权
//        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");
        List<JSONObject>  businessMenuList = new ArrayList<>();
        //列表示例
        JSONObject businessMenuListExam = new JSONObject();
        businessMenuListExam.put("name","列表示例");
        businessMenuListExam.put("path","/HomeIndex/businessMenu/list");
        businessMenuListExam.put("icon","Tickets");
        businessMenuListExam.put("component","BusinessMenu");
        businessMenuList.add(businessMenuListExam);
        //详情示例
        JSONObject businessMenuDetailExam = new JSONObject();
        businessMenuDetailExam.put("name","详情示例");
        businessMenuDetailExam.put("path","/HomeIndex/businessMenu/detail");
        businessMenuDetailExam.put("icon","DocumentRemove");
        businessMenuDetailExam.put("component","BusinessMenu");
        businessMenuList.add(businessMenuDetailExam);
        //图表示例
        JSONObject businessMenuChartExam = new JSONObject();
        businessMenuChartExam.put("name","图表示例");
        businessMenuChartExam.put("path","/HomeIndex/businessMenu/chart");
        businessMenuChartExam.put("icon","Postcard");
        businessMenuChartExam.put("component","BusinessMenu");
        businessMenuList.add(businessMenuChartExam);
        //文件上传
        JSONObject businessMenuFileUpload = new JSONObject();
        businessMenuFileUpload.put("name","文件上传");
        businessMenuFileUpload.put("path","/HomeIndex/businessMenu/fileUpload");
        businessMenuFileUpload.put("icon","Files");
        businessMenuFileUpload.put("component","BusinessMenu");
        businessMenuList.add(businessMenuFileUpload);
        //富文本示例
        JSONObject businessMenuRichTextExam = new JSONObject();
        businessMenuRichTextExam.put("name","富文本示例");
        businessMenuRichTextExam.put("path","/HomeIndex/businessMenu/richText");
        businessMenuRichTextExam.put("icon","Document");
        businessMenuRichTextExam.put("component","BusinessMenu");
        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");
        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","BaseData");
        baseDataList.add(baseDataMsgData);
        //基础数据-实体配置
        JSONObject baseDataEntitySet = new JSONObject();
        baseDataEntitySet.put("name","实体配置");
        baseDataEntitySet.put("path","/HomeIndex/baseData/entitySet");
        baseDataEntitySet.put("icon","Operation");
        baseDataEntitySet.put("component","BaseData");
        baseDataList.add(baseDataEntitySet);
        //基础数据-验证码数据
        JSONObject baseDataValidationCode = new JSONObject();
        baseDataValidationCode.put("name","验证码数据");
        baseDataValidationCode.put("path","/HomeIndex/baseData/validationCode");
        baseDataValidationCode.put("icon","DocumentChecked");
        baseDataValidationCode.put("component","BaseData");
        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");
        List<JSONObject>  systemManagementList = new ArrayList<JSONObject>();

        //系统管理-用户管理
        JSONObject sysMngUser = new JSONObject();
        sysMngUser.put("name","用户管理");
        sysMngUser.put("path","/HomeIndex/SystemManagement/user");
        sysMngUser.put("icon","User");
        sysMngUser.put("component","System");
        systemManagementList.add(sysMngUser);
        //系统管理-角色管理
        JSONObject sysMngRole = new JSONObject();
        sysMngRole.put("name","角色管理");
        sysMngRole.put("path","/HomeIndex/SystemManagement/role");
        sysMngRole.put("icon","Van");
        sysMngRole.put("component","System");
        systemManagementList.add(sysMngRole);
        //系统管理-菜单管理
        JSONObject sysMngMenu = new JSONObject();
        sysMngMenu.put("name","菜单管理");
        sysMngMenu.put("path","/HomeIndex/SystemManagement/menu");
        sysMngMenu.put("icon","Reading");
        sysMngMenu.put("component","System");
        systemManagementList.add(sysMngMenu);
        //系统管理-日志管理
        JSONObject sysMngLog = new JSONObject();
        sysMngLog.put("name","日志管理");
        sysMngLog.put("path","/HomeIndex/SystemManagement/log");
        sysMngLog.put("icon","Memo");
        sysMngLog.put("component","System");
        systemManagementList.add(sysMngLog);
        //系统管理-系统配置
        JSONObject sysMngSet = new JSONObject();
        sysMngSet.put("name","系统配置");
        sysMngSet.put("path","/HomeIndex/SystemManagement/set");
        sysMngSet.put("icon","DataLine");
        sysMngSet.put("component","System");
        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;
    }

}

24.2 接口返回数据

接口返回的数据如下

json 复制代码
{
  "result": 200,
  "data": [
    {
      "name": "工作台",
      "path": "/HomeIndex/WorkPlat",
      "icon": "Platform",
      "component": "WorkPlat",
      "children": []
    },
    {
      "name": "业务菜单",
      "path": "/HomeIndex/businessMenu",
      "icon": "Menu",
      "component": "BusinessMenu",
      "children": [
        {
          "name": "列表示例",
          "path": "/HomeIndex/businessMenu/list",
          "icon": "Tickets",
          "component": "BusinessMenu"
        },
        {
          "name": "详情示例",
          "path": "/HomeIndex/businessMenu/detail",
          "icon": "DocumentRemove",
          "component": "BusinessMenu"
        },
        {
          "name": "图表示例",
          "path": "/HomeIndex/businessMenu/chart",
          "icon": "Postcard",
          "component": "BusinessMenu"
        },
        {
          "name": "文件上传",
          "path": "/HomeIndex/businessMenu/fileUpload",
          "icon": "Files",
          "component": "BusinessMenu"
        },
        {
          "name": "富文本示例",
          "path": "/HomeIndex/businessMenu/richText",
          "icon": "Document",
          "component": "BusinessMenu"
        }
      ]
    },
    {
      "name": "基础数据",
      "path": "/HomeIndex/baseData",
      "icon": "TrendCharts",
      "component": "BaseData",
      "children": [
        {
          "name": "消息数据",
          "path": "/HomeIndex/baseData/msgData",
          "icon": "Message",
          "component": "BaseData"
        },
        {
          "name": "实体配置",
          "path": "/HomeIndex/baseData/entitySet",
          "icon": "Operation",
          "component": "BaseData"
        },
        {
          "name": "验证码数据",
          "path": "/HomeIndex/baseData/validationCode",
          "icon": "DocumentChecked",
          "component": "BaseData"
        }
      ]
    },
    {
      "name": "系统管理",
      "path": "/HomeIndex/SystemManagement",
      "icon": "Tools",
      "component": "System",
      "children": [
        {
          "name": "用户管理",
          "path": "/HomeIndex/SystemManagement/user",
          "icon": "User",
          "component": "System"
        },
        {
          "name": "角色管理",
          "path": "/HomeIndex/SystemManagement/role",
          "icon": "Van",
          "component": "System"
        },
        {
          "name": "菜单管理",
          "path": "/HomeIndex/SystemManagement/menu",
          "icon": "Reading",
          "component": "System"
        },
        {
          "name": "日志管理",
          "path": "/HomeIndex/SystemManagement/log",
          "icon": "Memo",
          "component": "System"
        },
        {
          "name": "系统配置",
          "path": "/HomeIndex/SystemManagement/set",
          "icon": "DataLine",
          "component": "System"
        }
      ]
    }
  ],
  "msg": "左侧栏菜单数据获取"
}

24.3 路由代码修改(src/router/index.ts)

修改路由代码新增meta参数,其中的requireAuth参数会在后续使用导航守卫时根据此参数进行路由跳转时的校验

应用了router的路由匹配机制使用:path+来匹配一级到多级的路由

src/router/index.ts

ts 复制代码
import { createRouter, createWebHistory } from 'vue-router'

// 自定义路由组件或从其他文件导入,这里选择从其他文件导入
import UserLogin from "../views/login/UserLogin.vue";
// import UserLogin from '@/views/login/UserLogin.vue';
import HomeIndex from '@/views/index/HomeIndex.vue';
import WorkPlat from '@/views/index/components/WorkPlat.vue';
import ResetPwd from '@/views/resetPassword/ResetPwd.vue';
// import ResetPwd from '../views/resetPassword/ResetPwd.vue';

// 定义一些路由,每个路由都需要映射到一个组件,
const routes = [
  {
    path: '/',
    // component: HomeIndex,
    // component: WorkPlat,
    // redirect: "/HomeIndex",
    redirect: "/HomeIndex/WorkPlat",
    meta: {
      requireAuth: true
      // requireAuth: false
    }
  },
  {
    path: '/UserLogin',
    component: UserLogin,
    meta: {
      requireAuth: false
    }
  },
  {
    // 匹配具有多个部分的路由 如/111/222/333
    // :path+表示匹配一级或多级
    // path: '/:HomeIndex+',
    path: '/HomeIndex/:path+',
    // path: '/HomeIndex',
    component: HomeIndex,
    meta: {
      requireAuth: true
      // requireAuth: false
    }
  },
  {
    path: '/ResetPwd',
    component: ResetPwd,
    meta: {
      requireAuth: false
    }
  }
]

// 创建路由实例并传递'routes'配置 你可以在这里输入更多的配置
const router = createRouter({
  history: createWebHistory(),
  // routes:routes可以简写成routes,不会报错
  // routes:[]
  routes,
  // : [
  //   {
  //     path:"/HomeIndex",
  //     component: HomeIndex
  //   }
  // ]
})

export default router

24.4 缓存代码修改(src/store/index.ts)

加了一些注释,并未有大的改动

src/store/index.ts

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;

24.5 主页代码修改(src/views/index/HomeIndex.vue)

无大修改,简单优化

src/views/index/HomeIndex.vue

ts 复制代码
<script setup lang="ts">
    import { 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'
        }
    }


</script>

<template>

    <!-- 后台主页 -->
    <div class="index-layout">
        <el-container>
            <!-- 宽度以变量形式传入,打开关闭侧边菜单栏 -->
            <el-aside class="layout-aside" :width="slideWidth">
                <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" >
                        <!-- <el-tab-pane label="User" name="first">主界面</el-tab-pane> -->
                        <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>
                                    <span class="label-span">工作台</span>
                                    </span>
                                </template>

                        </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;

    }

    .layout-header{
        position: fixed;
        top: 0;
        right: 0;
        width: 300px;
        /* height: 60px; */
        line-height: 35px;
    }

</style>

24.6 菜单栏组件代码修改(src/views/index/components/MenuBar.vue)

新增菜单栏数据获取,菜单栏路由及组件的遍历,事件的监听,选中菜单栏的事件触发等

src/views/index/components/MenuBar.vue

ts 复制代码
<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("");

    onMounted (()=>{
        loadMenuData();
    });

    // 加载菜单数据
    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 = [];
                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: []
                    };

                    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
                                },
                            };
                            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
                });

            }

            // 根据url中的路由信息自动选中对应的菜单
            // curMenu.value = router.currentRoute.value.path;

            // 选中菜单de事件触发 传入的值为当前组件的路由地址如,/HomeIndex/businessMenu/detail
            selectMenu(router.currentRoute.value.path);
            
        }).catch((error)=>{
            console.log("error,",error)
            utils.hideLoadding();
            utils.showError("加载失败");
        }).finally(()=>{
            utils.hideLoadding();
        });
    }


    // 选择当前菜事件触发的方法
    const selectMenu = (value:any)=>{
        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);
                    }
                });
            }
        })

        emits('select', curMenuData);
    };

    // 暴露选中菜单方法,可让外部调用该方法选中对应菜单
    defineExpose({
        selectMenu
    })

    

    // 13-8.5=4.5
    // 36-4.5=31.5
    // 12+10=22
    // 9.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>

24.7 工具栏组件代码修改(src/views/index/components/ToolBar.vue)

新增了一个消息图标和未定义的函数

src/views/index/components/ToolBar.vue

ts 复制代码
<script setup lang="ts">


    const openMsg = ()=>{
        
    }

</script>

<template>
    <div class="toolbar">
        <div class="toolbar-icon">
            <el-icon>
                <Search />
            </el-icon>
        </div>
        <div class="toolbar-icon">
            <el-icon>
                <FullScreen />
            </el-icon>
        </div>
        <div class="toolbar-icon">
            <el-icon>
                <TurnOff />
            </el-icon>
        </div>
        <div class="toolbar-icon" @click="openMsg">
            <el-icon>
                <BellFilled />
            </el-icon>
        </div>
        <div class="toolbar-icon">
            <el-dropdown>
                <span class="icon-dropdown-link">
                    <el-icon class="link-icon--left" style="top: 2px;">
                        <UserFilled />
                    </el-icon>
                    <span>管理员</span>
                    <el-icon class="link-icon--right">
                        <arrow-down />
                    </el-icon>
                </span>
                <template #dropdown>
                    <el-dropdown-menu>
                        <el-dropdown-item>个人信息</el-dropdown-item>
                        <el-dropdown-item>修改密码</el-dropdown-item>
                        <el-dropdown-item>退出系统</el-dropdown-item>
                    </el-dropdown-menu>
                </template>
            </el-dropdown>

        </div>
    </div>
</template>

<style scoped>

    .toolbar{
        display: flex;
    }
    
    .toolbar-icon{
        flex: 1;
        text-align: center;
        cursor: pointer;
        /* height: 20px; */
        
    }

    .toolbar-icon:hover{
        color: var(--el-color-primary);
    }

    .toolbar-icon:deep(.icon-dropdown-link){
        /* width: 100%; */
        line-height: 30px;
        width: 80px;
    }

    .icon-dropdown-link:hover{
        color: var(--el-color-primary);
    }

</style>

24.8 新增组件-工作台(src/views/index/components/WorkPlat.vue)

src/views/index/components/WorkPlat.vue

ts 复制代码
<script setup lang="ts">
    
</script>

<template>
    工作台界面
</template>

<style scoped>

</style>

24.9 手机验证码登录组件代码修改(src/views/login/components/PhoneCodeForm.vue)

仅修改了登录成功后跳转的路由地址

src/views/login/components/PhoneCodeForm.vue

ts 复制代码
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'

// 引入状态存储工具store
import {useStore} from 'vuex'

// 引入工具方法
import utils from '../../../utils/utils'
import api from '../../../api/api'
// 路由引入
import { useRoute, useRouter } from 'vue-router';

// 登录表单的实例
// let loginFormRef = ref(null);
let loginFormRef = ref()
// 登录表单的数据
const loginForm = reactive({
  // 用户名
  username: '',
  // 手机验证码
  smscode: '',
  // 图片验证
  imgcode: '',
  // 记住用户名,默认否
  saveUsername: false
})

// 登录验证规则
const rules = {
  username: [
    {
      required: true,
      message: '请输入用户名',
      trigger: 'blur'
    }
  ],
  smscode: [
    {
      required: true,
      message: '请输入短信验证码',
      trigger: 'blur'
    }
  ],
  imgcode: [
    {
      required: true,
      message: '请输入图片验证码',
      trigger: 'blur'
    }
  ]
}

const formSize = "";

// 图片验证码路径
let imgCodeSrc = new URL('../../../assets/code.png', import.meta.url).href
// const imgCodeSrc = '../../../assets/code.png';

// 刷新图片验证码
const getImgCode = () => {
  // 后续改为从服务器上获取动态图片
  imgCodeSrc = new URL('../../../assets/code.png', import.meta.url).href
}

// 定时器
let timer: any = null
// 获取短信验证码的间隔时间
let curTime = 0
// 获取短信验证码按钮的文本显示内容
let smsCodeBtnText = ref('获取验证码')

// 获取短信验证码
const getSmsCode = () => {
  // 当点击获取短信验证码时,如果其他信息没填则提示输入
  if (!loginForm.username) {
    utils.showError('请输入用户名')
    return
  }
  // if(!loginForm.smscode){
  //     utils.showError('请输入短信验证码');
  //     return;
  // }


  // TODO 从后台获取短信验证码

  // 调用接口生成短信验证码

     // 1 直接使用axios请求后端完整地址请求
//   axios({
//     method: 'post',
//     url: 'http://127.0.0.1:8888/login/redis/setMessageCode',
//     // url: 'login/redis/setMessageCode',
//     // 这里需要注意,不管请求方式是什么,这里是根据后端传参方式来定的,如果后端使用@RequestParam则这里使用params作为key
//     params: {
//       username: loginForm.username
//     }
//   });


  // 2 使用axios实例传参请求后端接口地址的用法  
  api({
    method: 'post',
    url: '/login/redis/setMessageCode',
    params: {
        username: loginForm.username
    }
  })

  curTime = 60
  timer = setInterval(() => {
    curTime--;
    smsCodeBtnText.value = curTime + '秒后重新获取';
    if (curTime <= 0) {
      smsCodeBtnText.value = '获取验证码'
      clearInterval(timer)
      
      // 清除时,值为空,防止重复点击触发多次
      timer = null
    }
  }, 1000)
}

// 状态存储的store
const store = useStore();
// 路由,转到指定页面,使用push
// const route = useRoute();
const router = useRouter();

// 登录提交事件
const onSubmit = () => {
  // form表单中的值,校验,
  loginFormRef.value.validate((valid: string, fileds: any) => {
    // 如果valid值为假,则遍历输出报错
    if (!valid) {
      for (let key in fileds) {
        // 获取报错信息中的字段对应的key的索引为0的信息
        utils.showError(fileds[key][0].message)
        return;
      }
      return
    }
    // 登录表单的记住用户名如果被勾选
    if (loginForm.saveUsername==true) {
      console.log("短信验证登录1:",loginForm.saveUsername);
      // 保存输入的用户名
      utils.saveData('username', loginForm.username)
      // 保存被勾选的操作
      utils.saveData('saveUsername', loginForm.saveUsername)
    } else {
      console.log("短信验证登录2:",loginForm.saveUsername);
      // 如果记住用户名的勾选取消,则移除这两个存储的内容
      utils.removeData('username')
      utils.removeData('saveUsername')
    }

    // TODO 调用接口登录

    // 因为太快了,所以可能看不到效果,可以将下方的hideLoading方法注掉,可以看到效果
    utils.showLoadding('正在加载中')

    api({
      method: 'get',
      url: '/login/redis/getMessageCode',
      params: {
        username: loginForm.username,
        smscode: loginForm.smscode
        // imgcode: loginForm.imgcode
      }
    })
      .then((res) => {
        utils.hideLoadding()
        console.log(res)
        // console.log(res.status)
        // if (!res || res.status != 200 || !res.data || res.data.result != 200 || !res.data.data) {
        if (res.status != 200 || res.data.result != 200 || !res.data.msgCode) {
          utils.showError('登录失败-请求数据返回有误');
          return;
        }
        // console.log(res.data.data, loginForm.smscode);
        if(res.data.msgCode == loginForm.smscode){
          utils.showSuccess('登陆成功')
          // 存储用户token信息并转到主页
          // let userInfo = res.data.data
          let userInfo = res.data
          let token = res.data.token
          // 状态数据存储
          store.commit('setUserInfo', userInfo);
          store.commit('setToken', token);
          // 登录成功后将页面转到主页
          // router.push('/HomeIndex')
          router.push('/')

        }else if(res.data.msgCode != loginForm.smscode){
          utils.showError('登录失败-验证码错误');
          return;
        }

        // utils.showError('登录失败')

      })
      .catch((error) => {
        // utils.hideLoadding();
        console.log(error);
        utils.showError('登录失败-出现异常')
      })

    // api.post("/api/login/code",{
    //     username: loginForm.username,
    //     smscode: loginForm.smscode,
    //     imgcode: loginForm.imgcode
    // }).then((res)=>{
    //     utils.hideLoadding();
    //     console.log(res);
    //     console.log(res.status);
    //     if(!res || res.status != 200 || !res.data || res.data.code != 8888 || !res.data.data){
    //         if(res.data.message){
    //             utils.showError(res.data.message);
    //             return;
    //         }
    //         utils.showError('登录失败');

    //         return;
    //     }
    //     // 存储用户token信息并转到主页
    //     let userInfo = res.data.data;
    //     let token = res.data.token;
    //     utils.showSuccess('登陆成功');

    // }).catch((error)=>{
    //     // utils.hideLoadding();
    //     utils.showError('登录失败');
    // });

    // 登录成功信息提示
    // utils.showSuccess("登录成功");
  })
}

// 挂载
onMounted(() => {
  // 获取记住用户名的值
  loginForm.saveUsername = utils.getData('saveUsername')
  // 如果记住用户名被勾选,则获取用户名显示(saveUsername可能会是undefined,当为undefined时也是真,故这里不能直接使用saveUsername,而是要判断是否为true)
  if (loginForm.saveUsername==true) {
    loginForm.username = utils.getData('username')
  }
})

// 清空定时器
onUnmounted(() => {
  timer && clearInterval(timer)
})
</script>

<template>
  <!-- 手机验证码登录 -->
  <div class="phoneCodeLoginBox">
    <el-form
      ref="loginFormRef"
      style="max-width: 600px"
      :model="loginForm"
      :rules="rules"
      label-width="0"
      class="loginFrom"
      :size="formSize"
      status-icon
    >
      <!-- 用户名 -->
      <el-form-item prop="username">
        <!-- 图标设置,动态绑定username,提示信息,设置输入框大小 -->
        <el-input
          prefix-icon="UserFilled"
          v-model="loginForm.username"
          placeholder="请输入用户名"
          size="large"
        />
      </el-form-item>
      <!-- 短信验证 -->
      <el-form-item prop="smscode">
        <!-- 使用两个div块来左右布局验证码输入和获取验证码按钮的实现 -->
        <div class="flex loginLine">
          <div class="flexItem">
            <el-input
              prefix-icon="Iphone"
              v-model="loginForm.smscode"
              placeholder="请输入验证码"
              size="large"
            />
          </div>
          <div class="codeBtn">
            <el-button type="primary" size="large" @click="getSmsCode" :disabled="curTime > 0">{{
              smsCodeBtnText
            }}</el-button>
          </div>
        </div>
      </el-form-item>
      <!-- 图片验证 -->
      <el-form-item prop="imgcode">
        <!-- 使用两个div块来左右布局验证码输入和获取验证码按钮的实现 -->
        <div class="flex loginLine">
          <div class="flexItem">
            <el-input
              prefix-icon="Picture"
              v-model="loginForm.imgcode"
              placeholder="请输入图片验证码"
              size="large"
            />
            <!-- <el-input prefix-icon="Iphone" v-model="loginForm.smscode" placeholder="请输入验证码" size='large' /> -->
          </div>
          <div class="codeBtn">
            <el-image :src="imgCodeSrc" size="large" @click="getImgCode"></el-image>
            <!-- <el-button type="primary" size="large" @click="getSmsCode" class="" >获取验证码</el-button> -->
          </div>
        </div>
      </el-form-item>
      <!-- 记住用户名 -->
      <el-form-item prop="saveUsername">
        <el-checkbox v-model="loginForm.saveUsername">记住用户名</el-checkbox>
      </el-form-item>
      <!-- 登录按钮 -->
      <el-form-item>
        <el-button class="loginBtn" type="danger" size="large" @click="onSubmit">登录</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<style scoped>
/* 按钮宽度设为最大 */
.loginBtn {
  width: 100%;
  /* 登录按钮圆角边框 */
  border-radius: 20px;
}

/* 验证码按钮样式配置 */
.codeBtn {
  width: 100px;
  margin-left: 10px;
}
/* 按钮和图片宽度100px */
.codeBtn:deep(.el-button),
.codeBtn:deep(img) {
  width: 100px;
  /* height: 40px; */
}
/* 验证码图片高度 */
.codeBtn:deep(img) {
  height: 40px;
  /* 鼠标移上去会变成手型 */
  cursor: pointer;
}

/* 这一行宽度占满 */
.loginLine {
  width: 100%;
}
</style>

24.10 二维码登录组件代码修改(src/views/login/components/QcodeForm.vue)

仅修改了登录成功后跳转的路由地址

src/views/login/components/QcodeForm.vue

ts 复制代码
<script setup lang="ts">

import { ref,reactive, onMounted, onUnmounted } from 'vue'

// 引入工具方法
import utils from '../../../utils/utils'
import api from '../../../api/api'

// 引入store
import {useStore} from 'vuex'
// 引入router
import {useRouter} from 'vue-router'

    const store = useStore();
    const router = useRouter();


    // 二维码
    // let qcodePath:any = null;
        
    // 二维码对应的token, 用于判断当前二维码是否已经被扫码登录
    let qrToken:string = "";

    // 第一次获取验证码
    // api({
    //     method: 'post',
    //     url: 'login/qr/generateQrCodeAsFile'
    // }).then((res)=>{
        // if(res.data.result != 200){
            // utils.showError("登录失败");
        // }
        // utils.showSuccess("登录成功");
    //     qcodePath = res.data.data
    //     qrToken = res.data.token
    // });

    // qcodePath = 'E:\\WORKPROJECTS\\MySelfPro\\hslb-general-management-system\\src\\main\resources\\login_qr_pngs\\QRCode.png';

    // 二维码
    let qcodeSrc = new URL("../../../assets/hslb-qcode.png", import.meta.url).href;
    // let qcodeSrc = new URL(qcodePath, import.meta.url).href;
    // let qcodeSrc = qcodePath;


    const qcodeToken = ref('');

    // 当前定时器事件
    const curTime = ref(0);
    let timer:any = null;

    let username:string = utils.getData("username");
    const qrString = "100100100222";
    
    // 后台更新获取二维码
    const loadQcode = () => {
        // 后续改为从服务器上获取动态图片

        // const qrString = "100100100222";
        console.log("9999999====== "+qrString);

        // let username:string = utils.getData("username");

        api({
            method: 'post',
            url: 'login/qr/generateQrCodeAsFile',
            params: {
                username: username,
                qrContent: qrString
            }
        }).then((res)=>{
            // if(res.data.result != 200){
                // utils.showError("登录失败");
            // }
            // utils.showSuccess("登录成功");
            // qcodePath = res.data.data
            qrToken = res.data.token
        });

        qcodeSrc = new URL("../../../assets/hslb-qcode.png", import.meta.url).href;
        // qcodeSrc = new URL(qcodePath, import.meta.url).href;
        // qcodeSrc = qcodePath;

        // 初始化token的值
        qcodeToken.value = qrToken;
        // 设定定时时间
        // curTime.value = 60;
        // 为了让二维码失效的效果及时,这里暂时设置10秒,后续改回60秒即可
        curTime.value = 10;
        // 定义定时器,倒计时
        timer = setInterval(() => {
            curTime.value--;

            // 这里获取toekn,校验是否已经被登陆过
            checkLogin();

            if(curTime.value<=0){
                // 事件为0则清空定时器
                clearInterval(timer);
                timer = null;
            }
        }, 1000);

    };





    // 登录提交事件
    // const onSubmit = () => {
    // };

    // 挂载
    onMounted(() => {
        // 获取二维码
        loadQcode();

    });


    // 清空计时器
    onUnmounted(()=>{
        timer && clearInterval(timer);
    });

    // 使用qcodeToken判断当前二维码是否已经被扫码登录
    const checkLogin = () => {
        // TODO
        api({
            method: 'post',
            url: 'login/qr/generateQrCodeAsFile',
            params: {
                username: username,
                qrContent: qrString
            }
        }).then((res)=>{
            if(res.data.token){
                utils.showSuccess("登录成功");
                store.commit('setUserInfo',res.data);
                store.commit('setToken',res.data.token);
                // router.push('/HomeIndex');
                router.push('/');
            }

            // res.data.token;
        }).catch((error)=>{
            console.log(error);
            // utils.showError("登录失败")
        });

    }

</script>

<template>
    <!-- 扫码登录 -->
    <div class="qcodeLoginBox">
        <div class="qcodeBox" >
            <img class="qcodeImg" :class="{'endImg':curTime<=0}" :src="qcodeSrc" alt="无法获取二维码,请联系客服解决">
            <div v-if="curTime<=0" class="endBox" @click="loadQcode" >
                当前二维码失效,点击重新加载{{ curTime }}秒
            </div>
        </div>
        <div class="tipInfo" >
            使用微信或移动端扫码登录 此二维码将在{{ curTime }}秒后刷新
        </div>
    </div>

</template>

<style scoped>

    /* 二维码窗口样式 */
    .qcodeBox{
        width: 80%;
        height: 80%;
        position: relative;
        /* 边框自动 */
        margin: 0 auto;   
    }

    /* 二维码图片样式 */
    .qcodeBox .qcodeImg{
        width: 100%;
        height: 100%;
    }

    .qcodeBox .endBox{
        width: 100%;
        height: 100%;
        /* 悬浮显示 */
        position: absolute;
        /* 靠左 */
        /* left: 0%; */
        /* 靠上 */
        top: 0;
        /* 居中 */
        /* text-align: center; */
        /* 字体大小 */
        font-size: 14px;
        /* 字体颜色 */
        color: red;
        display: flex;
        /* 上下居中 */
        align-items: center;
        /* justify-items: center; */
        /* 左右居中 */
        justify-content: center;
        /* 背景色为灰色 */
        background-color: #00000055;
    }

    /* .endImg{
        filter: brightness(10%);
    } */

    /* 提示信息样式 */
    .tipInfo{

        /* 行高 */
        line-height: 30px;
        /* 字体大小 */
        font-size: 14px;
        /* 居中 */
        text-align: center;
        /* 颜色 */
        color: var(--el-text-color-placeholder);
    }


</style>

24.11 账号密码登录组件代码修改(src/views/login/components/UsernameForm.vue)

仅修改了登录成功后跳转的路由地址

src/views/login/components/UsernameForm.vue

ts 复制代码
<script setup lang="ts">

import { ref,reactive, onMounted } from 'vue'
// 引入状态存储store
import { useStore } from 'vuex'
// 引入路由工具router
import { useRouter } from 'vue-router'


// 引入工具方法
import utils from '../../../utils/utils'
import api from '../../../api/api'


    // 登录表单的实例
    // let loginFormRef = ref(null);
    let loginFormRef = ref();
    // 登录表单的数据
    const loginForm = reactive({
        // 用户名
        username: '',
        // 密码
        password: '',
        // 图片验证
        imgcode: '',
        // 记住用户名,默认否
        saveUsername: false,
        // 记住用户名,默认否
        savePassword: false
    });

    // 登录验证规则
    const rules = ({
        username:[{
            required: true,
            message: '请输入用户名',
            trigger: 'blur'
        }],
        password:[{
            required: true,
            message: '请输入密码',
            trigger: 'blur'
        }],
        imgcode:[{
            required: true,
            message: '请输入图片验证码',
            trigger: 'blur'
        }]
    });

    const formSize = "";

    // 图片验证码路径
    let imgCodeSrc = new URL("../../../assets/code.png", import.meta.url).href;
    // const imgCodeSrc = '../../../assets/code.png';

    
    // 刷新图片验证码
    const getImgCode = () => {
        // 后续改为从服务器上获取动态图片
        imgCodeSrc = new URL("../../../assets/code.png", import.meta.url).href;
    };

    // 全局状态存储
    const store = useStore();
    // 路由调用
    const router = useRouter();

    // 登录提交事件
    const onSubmit = () => {
        // form表单中的值,校验
        loginFormRef.value.validate((valid:string, fileds:any)=>{
            // 如果valid值为假,则遍历输出报错
            if(!valid){
                for(let key in fileds){
                    // 获取报错信息中的字段对应的key的索引为0的信息
                    utils.showError(fileds[key][0].message);
                }
                return;
            }
            // 登录表单的记住用户名如果被勾选
            if(loginForm.saveUsername){
                // 保存输入的用户名
                utils.saveData('username', loginForm.username);
                // 保存被勾选的操作
                utils.saveData('saveUsername', loginForm.saveUsername);
            }else{
                // 如果记住用户名的勾选取消,则移除这两个存储的内容
                utils.removeData('username');
                utils.removeData('saveUsername');
            }

            // 登录表单的记住用户名如果被勾选
            if(loginForm.savePassword){
                // 保存输入的用户名
                utils.saveData('password', loginForm.password);
                // 保存被勾选的操作
                utils.saveData('savePassword', loginForm.savePassword);
            }else{
                // 如果记住用户名的勾选取消,则移除这两个存储的内容
                utils.removeData('password');
                utils.removeData('savePassword');
            }

            // TODO 调用接口登录
            utils.showLoadding("正在加载中");
            api({
                method: 'get',
                url: '/login/login',
                params: {
                    username: loginForm.username,
                    password: loginForm.password
                }
            }).then((res)=>{
                utils.hideLoadding();
                if(res.status != 200 || res.data.result != 200){
                    utils.showError("登录失败-请求数据返回有误");
                    return;
                }

                if(res.data.login == 1){
                    utils.showSuccess("登录成功");
                    // 存储用户信息
                    // let userInfoLogin = res.data.login;
                    let userInfoLogin = res.data;
                    let token = res.data.token;
                    console.log("usernamelogin:", token);
                    store.commit('setUserInfo', userInfoLogin);
                    store.commit('setToken', token);
                    console.log("----------------token: ", token);
                    // 登录成功后跳转主页
                    // router.push('/HomeIndex');
                    router.push('/');
                }else if(res.data.login == 0){
                    utils.showError("登录失败-用户不存在");
                    return;
                }else if(res.data.login == 2){
                    utils.showError("登录失败-密码错误");
                    return;
                }
                

                // utils.showError("登录失败-返回数据错误")

            }).catch((error)=>{
                console.log(error);
                utils.showError("登录失败-发生异常");
            });

            // 登录成功提示
            // utils.showSuccess("登录成功");
        });
    };

    // 挂载
    onMounted(() => {
        // 获取记住用户名的值
        loginForm.saveUsername = utils.getData('saveUsername');
        // 如果记住用户名被勾选,则获取用户名显示
        if(loginForm.saveUsername==true){
            loginForm.username = utils.getData('username');
        }

        // 获取记住密码的值
        loginForm.savePassword = utils.getData('savePassword');
        // 如果记住密码被勾选,则获取密码
        if(loginForm.savePassword==true){
            loginForm.password = utils.getData('password');
        }
    });

</script>

<template>
    <!-- 用户密码登录 -->
    <div class="usernameLoginBox">
        <el-form 
            ref="loginFormRef"
            style="max-width: 600px"
            :model="loginForm"
            :rules="rules"
            label-width="0"
            class="loginFrom"
            :size="formSize" status-icon>
            <!-- 用户名 -->
            <el-form-item prop="username">
                <!-- 图标设置,动态绑定username,提示信息,设置输入框大小 -->
                <el-input prefix-icon="UserFilled" v-model="loginForm.username" placeholder="请输入用户名" size='large' />
            </el-form-item>
            <!-- 密码 -->
            <el-form-item prop="password">
                <!-- 密码 -->
                <div class="flexItem" >
                    <!-- show-password 属性表示是否显示切换显示密码的图标,true为显示 -->
                    <!-- <el-input prefix-icon="Lock" show-password="off" type="password" v-model="loginForm.password" placeholder="请输入密码" size='large' /> -->
                    <el-input prefix-icon="Lock" show-password type="password" v-model="loginForm.password" placeholder="请输入密码" size='large' />
                </div>
            </el-form-item>
            <!-- 图片验证 -->
            <el-form-item prop="imgcode">
                <!-- 使用两个div块来左右布局验证码输入和获取验证码按钮的实现 -->
                <div class="flex loginLine">
                    <div class="flexItem" >
                        <el-input prefix-icon="Picture" v-model="loginForm.imgcode" placeholder="请输入图片验证码" size='large' />
                        <!-- <el-input prefix-icon="Iphone" v-model="loginForm.smscode" placeholder="请输入验证码" size='large' /> -->
                    </div>
                    <div class="codeBtn" >
                        <el-image  :src="imgCodeSrc" size='large' @click="getImgCode" ></el-image>
                        <!-- <el-button type="primary" size="large" @click="getSmsCode" class="" >获取验证码</el-button> -->
                    </div>
                </div>
            </el-form-item>

            <!-- <el-form-item prop="saveUsername"> -->
            <el-form-item >
                <!-- 记住账号密码的勾选 -->
                <div class="flex loginLine" >
                    <!-- 记住用户名 -->
                    <div class="flexItem" >
                        <el-form-item prop="saveUsername">
                            <el-checkbox v-model="loginForm.saveUsername">记住用户名</el-checkbox>
                        </el-form-item>
                    </div>
                    <!-- 记住密码 -->
                    <div class="flexItem" >
                        <el-form-item prop="savePassword">
                            <el-checkbox v-model="loginForm.savePassword">记住密码</el-checkbox>
                        </el-form-item>
                    </div>
                    <div class="flexItem" >
                        <!-- <el-form-item prop="savePassword"> -->
                            <router-link to="/ResetPwd">忘记密码</router-link>
                        <!-- </el-form-item> -->
                    </div>
                </div>
            </el-form-item>
            <!-- <el-form-item prop="savePassword"> -->
            <!-- </el-form-item> -->

            <!-- 登录按钮 -->
            <el-form-item>
                <el-button class="loginBtn" type="danger" size='large' @click="onSubmit">登录</el-button>
            </el-form-item>

        </el-form>
    </div>

</template>

<style scoped>

    /* 按钮宽度设为最大 */
    .loginBtn{
        width: 100%;
        /* 登录按钮圆角边框 */
        border-radius: 20px;
    }

    /* 验证码按钮样式配置 */
    .codeBtn{
        width: 100px;
        margin-left: 10px;
    }
    /* 按钮和图片宽度100px */
    .codeBtn:deep(.el-button),
    .codeBtn:deep(img){
        width: 100px;
        /* height: 40px; */

    }
    /* 验证码图片高度 */
    .codeBtn:deep(img){
        height: 40px;
        /* 鼠标移上去会变成手型 */
        cursor: pointer;
    }

    /* 这一行宽度占满 */
    .loginLine{
        width: 100%
    }

</style>

24.12 App.vue组件代码修改(src/App.vue)

新增路由守卫,以及注释了部分多于代码

src/App.vue

ts 复制代码
<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)=>{
  console.log("to: ",to)
  // 鉴权 在router.ts中设置的requireAuth参数,true则需要鉴权,false则不需要
  if(to.meta.requireAuth){
    console.log("开始鉴权,===>>>")
    // 进入鉴权,通过缓存中的token与接口中的token进行校验
    let token = utils.getData("token");
    // let userInfo = utils.getData("userInfo");
    // 当前不是登录状态
    console.log("20240021 token",token);
    let username = utils.getData("username");
    if(username==undefined){
      username = "";
    }
    let newToken = "";
    api.get('/login/tokenCheck',{
        params:{username}
      }).then((res)=>{
        console.log("data: ",res.data.token);
        newToken = res.data.token;

        if(token!=newToken){
          console.log("鉴权失败,返回登录界面===>>>")
          // 路由跳转登录
          router.push("/UserLogin")
          // 以下方法跳转失败
          // return { 
          //   path:"/UserLogin",
          //   query: {
          //     redirect: router.currentRoute.value.path
          //   }
          // }
        }
        console.log("---->",newToken)

      });

    // console.log("---->",newToken)
    // 如果token不一致,则进行路由跳转,进行重新登陆
    // if(token!=newToken&&to.name!=="UserLogin"){
    // if(token!=newToken){
      // console.log("===>>>")
      // 路由跳转登录
      // router.push("/UserLogin")
      // router.push({
      //   path: "/UserLogin",
      //   query: {
      //     redirect: router.currentRoute.value.path
      //   }
      // })
      // return {path: "/UserLogin"}
    // }
    console.log("鉴权成功,====》》》》")
  }
});

onMounted(()=>{

  // let tt = localStorage.getItem("token");
  // console.log("tt: ",tt);

  // console.log("=== ===");

  

  let token = "";
  // 由于token可能返回undefined报错,需要进行报错处理
  try {
    token = utils.getData("token");
  } catch (error) {
    error;
  }

  // console.log("store-token",token);
  let userInfo = utils.getData('userInfo');
  // console.log("userInfoL: "+userInfo)
  // console.log("userInfoL: "+token&&userInfo)
  if(token && userInfo){

    // console.log("token userInfo :",token," -- ", userInfo);
    // 登录成功,验证
    utils.showLoadding("正在加载")
    const username = utils.getData('username');
    
    if(!username){
      // 登录失败,跳转到登录页
      if(username===undefined){
        utils.saveData("username","");
      }
      // token验证失败
      utils.showError("用户名过期-请重新登录");

      router.push('/UserLogin');
      utils.hideLoadding();
    }else{
      // 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方法进行保持当前选中菜单路由
        
          utils.showSuccess("登录成功");
        }else{
          // if(username===undefined){
          //   utils.saveData("username","");
          // }
          // 登录失败
          utils.showError("Token已过期,请重新登录");
          // 登录失败,跳转到登录页
          router.push('/UserLogin');
        }
        
      });
      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>

24.13 页面效果展示

登录成功后的界面

选择菜单后的路由地址界面(详情示例)

刷新页面后依旧是这个菜单被选中,路由地址不变


感谢阅读,祝君暴富!


相关推荐
齐 飞5 分钟前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
狂放不羁霸13 分钟前
idea | 搭建 SpringBoot 项目之配置 Maven
spring boot·maven·intellij-idea
LunarCod22 分钟前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
神仙别闹22 分钟前
基于tensorflow和flask的本地图片库web图片搜索引擎
前端·flask·tensorflow
计算机学长felix41 分钟前
基于SpringBoot的“校园交友网站”的设计与实现(源码+数据库+文档+PPT)
数据库·spring boot·毕业设计·交友
码农派大星。1 小时前
Spring Boot 配置文件
java·spring boot·后端
江深竹静,一苇以航1 小时前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
GIS程序媛—椰子1 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_0011 小时前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端1 小时前
Content Security Policy (CSP)
前端·javascript·面试