安全框架springSecurity+Jwt+Vue-1(vue环境搭建、动态路由、动态标签页)

一、安装vue环境,并新建Vue项目

①:安装node.js

官网(https://nodejs.org/zh-cn/)

2.安装完成之后检查下版本信息:

在这里插入图片描述

②:创建vue项目

1.接下来,我们安装vue的环境

# 安装淘宝npm
npm install -g cnpm --registry=https://registry.npm.taobao.org
# vue-cli 安装依赖包
cnpm install --g vue-cli
# 打开vue的可视化管理工具界面
vue ui

2.创建spring_security_vue项目 运行vue ui

在这里插入图片描述

3. 会为我们打开一个http://localhost:8001/dashboard的页面:

在这里插入图片描述

4.我们将在这个页面完成我们的前端Vue项目的新建。然后切换到【创建】,注意创建的目录最好是和你运行vue ui同一级。这样方便管理和切换

在这里插入图片描述

5.然后点击按钮【在此创建新项目】下一步中,项目文件夹中输入项目名称“sping_security_vue”

在这里插入图片描述

6.点击下一步,选择【手动】,再点击下一步,如图点击按钮,勾选上路由Router、状态管理Vuex,去掉js的校验。

在这里插入图片描述
在这里插入图片描述

7.下一步中,也选上【Use history mode for router】,点击创建项目,然后弹窗中选择按钮【创建项目,不保存预设】,就进入项目创建啦

稍等片刻之后,项目就初始化完成了。上面的步骤中,我们创建了一个vue项目,并且安装了Router、Vuex。这样我们后面就可以直接使用。

Router: WebApp的链接路径管理系统,简单就是建立起url和页面之间的映射关系
Vuex: 一个专为 Vue.js 应用程序开发的状态管理模式,简单来说就是为了方便数据的操作而建立的一个临时” 前端数据库“,用于各个组件间共享和检测数据变化。

ok,我们使用IDEA导入项目,看看创建好的项目长啥样子:
在这里插入图片描述

③:启动项目

1.然后我们在IDEA窗口的底部打开Terminal命令行窗口,输入yarn run serve
运行vue项目,我们就可以通过http://localhost:8080/打开我们的项目了。

在这里插入图片描述

2.效果如下,Hello Vue!

在这里插入图片描述

④:安装element-ui

接下来我们引入element-ui组件(https://element.eleme.cn),这样我们就可以获得好看的vue组件,开发好看的后台管理系统的界面啦。

在这里插入图片描述

1.命令安装

# 安装element-ui
yarn add element-ui --save

在这里插入图片描述

2.然后我们打开项目src目录下的main.js,引入element-ui依赖。

import Element from 'element-ui'
import "element-ui/lib/theme-chalk/index.css"
Vue.use(Element)

⑤: 安装axios、qs、mockjs

  • axios:一个基于 promise 的 HTTP 库,类ajax
  • qs:查询参数序列化和解析库
  • mockjs:为我们生成随机数据的工具库

1. 安装axios

接下来,我们来安装axios(http://www.axios-js.com/),axios是一个基于 promise 的 HTTP 库,这样我们进行前后端对接的时候,使用这个工具可以提高我们的开发效率。

1.安装命令

 yarn add axios --save

2.在main.js中全局引入axios

import axios from 'axios'
Vue.prototype.$axios = axios //

2.安装qs

我们安装一个qs,什么是qs?qs是一个流行的查询参数序列化和解析库。可以将一个普通的object序列化成一个查询字符串,或者反过来将一个查询字符串解析成一个object,帮助我们查询字符串解析和序列化字符串。

1.安装命令

 yarn add qs --save

3.安装mockjs

因为后台我们现在还没有搭建,无法与前端完成数据交互,因此我们这里需要mock数据,因此我们引入mockjs(http://mockjs.com/),方便后续我们提供api返回数据

1.安装命令

 yarn add mockjs --save-dev

2.然后我们在src目录下新建mock.js文件,用于编写随机数据的api,然后我们需要在main.js中引入这个文件

  • src/main.js
require("./mock") //引入mock数据,关闭则注释该行

后面我们mackjs会自动为我们拦截ajax,并自动匹配路径返回数据!

二、页面路由

Router:WebApp的链接路径管理系统,简单就是建立起url和页面之间的映射关系
所以我们要打开页面然后开发页面,我们需要先配置路由,然后再开发,这样我们可以试试看到效果。项目中,src\router\index.js就是用来配置路由的。

1.我们在views文件夹下定义几个页面:

  • Login.vue(登录页面)
  • Index.vue(首页)

2.配置url与vue页面的映射关系src\router\index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from "@/views/Login";
import Index from "@/views/Index";

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'login',
    component: Login
  },
  {
    path: '/index',
    name: 'index',
    component: Index
  },
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

3.运行yarn run serve打开http://localhost:8082/login查看效果

在这里插入图片描述

三、登陆界面开发

一开始的时候为了页面风格的统一,我们采用了Element Ui的组件库,所以这里我们就直接去element的官网。所以先找到Loyout布局然后再弄表单,然后我们涉及到的后台交互有2个:

  • 获取登录验证码
  • 提交登录表单完成登录

因为后台系统我们暂时还没有开发,所以这里我们需要自己mock数据完成交互。前面我们已经引入了mockjs,所以我们到mock.js文件中开发我们的api。

①:登录交互过程

1.交互流程

1.我们梳理一下交互流程:

  1. 浏览器打开登录页面

  2. 动态加载登录验证码,因为这是前后端分离的项目,我们不再使用session进行交互,所以后端我打算禁用session,那么验证码的验证就是问题了,所以后端设计上我打算生成验证码同时生成一个随机码,随机码作为key,验证码为value保存到redis中,然后把随机码和验证码图片的Base64字符串码发送到前端

  3. 前端提交用户名、密码、验证码还有随机码

  4. 后台验证验证码是否匹配以及密码是否正确
    在这里插入图片描述
    ok,这样我们就知道mock应该弄成什么样的api了。

2. mock.js定义需要的api

2.mock.js - 获取登录验证码

// 引入mock
let Mock = require('mockjs');
// 获取Mock.random对象
// 参考:https://github.com/nuysoft/Mock/wiki/Mock.Random
let random = Mock.Random;
let Result = {
    code: 200,
    msg: '操作成功!',
    data: null
}
/**
 * Mock.mock( url, post/get , function(options));
 * url 表示需要拦截的 URL,
 * post/get 需要拦截的 Ajax 请求类型
 *
 * 用于生成响应数据的函数
 */

Mock.mock('/captcha', 'post', ()=>{
    Result.data = {
        randomCode: random.string(32), // 获取一个32位的随机字符串
        captchaImg: random.dataImage('120x40', 'p7n5w') // //生成验证码为11111的base64图片编码
    }
    return Result;
})

mock生成数据还算简单,一般都是利用Mock.Random对象来生成一些随机数据,具体的用法可以参考https://github.com/nuysoft/Mock/wiki/Mock.Random。然后Result是为了统一返回结果,因为后台设计的时候,前后端交互,一般都有固定的返回格式,所以就有了Result。

3.mock.js - 登录接口

/*
    登录接口
 */

// 因为mock 不认识/login?username=xxx, 所以用了正则表达式
Mock.mock(RegExp('/login*'),'post',(config)=>{
    // 这里无法在header添加authorization,直接跳过
    console.log("mock----------------login")
    return Result
})

3.开发登录页面

1.Login.vue登录页面

<template>
    <el-row type="flex" class="row-bg" justify="center">
        <el-col class="el-col">
            <h3 style="color: white; font-weight: bold; font-size: 21px; margin: 0 0 20px 0;padding: 0">Spring security安全框架</h3>
            <el-form :model="form" :rules="rules" ref="ruleForm" class="demo-ruleForm">
                <el-form-item prop="username" style="width: 18rem;">
                    <el-input prefix-icon="el-icon-user" placeholder="用户名" v-model="form.username"></el-input>
                </el-form-item>
                <el-form-item prop="password" style="width: 18rem;">
                    <el-input prefix-icon="el-icon-lock" show-password placeholder="密码" v-model="form.password"></el-input>
                </el-form-item>
                <el-form-item prop="code" style="width: 18rem;">
                    <el-input prefix-icon="el-icon-picture-outline" v-model="form.code" placeholder="验证码"
                              :show-password="true" style="width: 10.8rem; float: left;" maxlength="5"></el-input>
                    <el-image class="captchaImg" :src="captchaImg" style="width: 6.7rem; float: left;"></el-image>
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" style="width: 18rem;" @click="submitForm('ruleForm')">登录</el-button>
                </el-form-item>
            </el-form>
        </el-col>
    </el-row>
</template>

<script>
export default {
    name: "Login",
    data() {
        return {
            form: {
                username: null, // 用户名
                password: null, // 密码
                code: null, // 验证码
                randomCode: null, // 随机码
            },
            captchaImg: null, //图片
            rules: {
                username: [
                    {required: true, message: '请输入用户名', trigger: 'blur'},
                ],
                password: [
                    {required: true, message: '请输入密码', trigger: 'blur'},
                    {min: 6, message: '密码长度至少 6 个字符', trigger: 'blur'}
                ],
                code: [
                    {required: true, message: '请输入验证码', trigger: 'blur'},
                    {min: 5, max: 5, message: '验证码长度为 5 个字符', trigger: 'blur'}
                ],
            }
        }
    },
    mounted() {
        this.getCaptchaImg();
    },
    methods: {
        // 获取验证码和随机码
        getCaptchaImg() {
            this.$axios.post('/captcha').then((res) => {
                if (res.data.code == 200){
                    this.form.randomCode = res.data.data.randomCode;
                    this.captchaImg = res.data.data.captchaImg;
                }else {
                    this.$message.error("验证码获取失败!")
                }
            })
        },

        // 登录
        toLogin() {
            this.$axios.post('/login', this.form).then((res) => {
                if (res.data.code == 200){
                    // todo 登录成功
                    const jwt = res.headers['authorization']
                    this.$store.commit('SET_TOKEN', jwt)
                    this.$router.push('/index')
                }else {
                    this.$message.error(res.data.msg)
                }
            })
        },


        submitForm(formName) {
            this.$refs[formName].validate((valid) => {
                if (valid) {
                    this.toLogin();
                } else {
                    console.log('error submit!!');
                    return false;
                }
            });
        },
    }
}
</script>

<style scoped>
.row-bg {
    background-image: url("/public/img/login_bk2.jpg");
    background-size: cover;
    background-repeat: no-repeat;
    /*background-color: #fafafa;*/
    height: 100vh;
    opacity: 0.9;
    filter: none;
}

.el-col {
    width: 22rem;
    margin: auto;
    /* 半透明黑色背景 */
    background-color: rgba(0, 0, 0, 0.30) !important;
    padding: 1rem 1.5rem 1rem 1.5rem;
    border-radius: 0.6rem;
    box-shadow: 0 0 10.8rem 0.2rem rgba(0, 0, 0, 0.1);
}

.demo-ruleForm {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
    margin-bottom: -10px;
}

.captchaImg {
    float: left;
    margin-left: 8px;
    border-radius: 4px;
}
</style>

2.效果

在这里插入图片描述

②:token的状态同步

再讲一下,submitForm方法中,提交表单之后做了几个动作,从Header中获取用户的authorization,也就是含有用户登录信息的jwt,然后提交到store中进行状态管理。

this.$store.commit(“SET_TOKEN”, jwt) 表示调用store中的SET_TOKEN方法,所以我们需要在store中编写方法

1.src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
    state: {
        token: null,
    },
    getters: {},
    mutations: {
        SET_TOKEN(state, token) {
            state.token = token;
            localStorage.setItem('token', token)
        }
    },
    actions: {},
    modules: {}
})

在这里插入图片描述
这样登录之后获取到的jwt就可以存储到应用的store以及localStorage中,方便使用直接从localStorage中获取即可! 这样用户登录成功之后就会跳转到/index页面this.$router.push(“/index”)

③:定义全局axios拦截器

这里有个问题,那么如果登录失败,我们是需要弹窗显示错误的,比如验证码错误,用户名或密码不正确等。不仅仅是这个登录接口,所有的接口调用都会有这个情况,所以我们想做个拦截器,对返回的结果进行分析,如果是异常就直接弹窗显示错误,这样我们就省得每个接口都写一遍了。

1.在src目录下创建一个文件axios.js(与main.js同级),定义axios的拦截:

// 引入所需的库和模块
import axios from "axios";
import router from "@/router"; // 假设这是指向路由模块的路径
import Element from "element-ui";

// 设置所有 Axios 请求的基础 URL
// axios.defaults.baseURL = "https://localhost:19005";

// 创建一个具有自定义设置的 Axios 实例
let request = axios.create({
    timeout: 5000, // 设置请求的超时时间为5000毫秒
    headers: {
        'Content-Type': 'application/json;charset=utf-8' // 设置请求数据的内容类型为 JSON
    }
});

// 在发送请求之前拦截请求
request.interceptors.request.use(config => {
    // 使用本地存储中的令牌设置请求的 'Authorization' 头部
    config.headers['Authorization'] = localStorage.getItem('token');
    return config;
});

// 在处理响应之前拦截响应
request.interceptors.response.use(response => {
    // 从响应中提取数据
    let res = response.data;

    // 检查响应代码是否为200(成功)
    if (res.code === 200) {
        return response; // 如果成功,则返回响应
    } else {
        // 如果响应代码不是200,则使用 Element UI 显示错误消息
        Element.Message.error(res.msg ? res.msg : '系统异常');
        return Promise.reject(res.msg); // 使用错误消息拒绝 Promise
    }
}, error => {
    console.log('error', error);

    // 处理特定的错误情况
    if (error.code === 401) {
        router.push('/login'); // 如果错误代码是401(未经授权),则重定向到登录页面
    }

    console.log(error.message);

    // 使用 Element UI 显示错误消息,持续时间为3000毫秒
    Element.Message.error(error.message, { duration: 3000 });
    return Promise.reject(error.message); // 使用错误消息拒绝 Promise
});

// 将配置好的 Axios 实例导出,以在应用程序的其他部分中使用
export default request;

前置拦截,其实可以统一为所有需要权限的请求装配上header的token信息,后置拦截中,判断status.code和error.response.status,如果是401未登录没权限的就调到登录页面,其他的就直接弹窗显示错误。

2.再main.js中导入自己创建axios.js

import axios from "@/axios";

Vue.prototype.$axios = axios

在这里插入图片描述

这样axios每次请求都会被前置拦截器和后置拦截器拦截了。

3.在mock.js中修改登录的接口

/*
    登录接口
 */

// 因为mock 不认识/login?username=xxx, 所以用了正则表达式
Mock.mock(RegExp('/login*'),'post',(config)=>{
    // 这里无法在header添加authorization,直接跳过
    Result.code = 400;
    Result.msg = '验证码错误!';
    return Result
})

4.登录异常弹窗效果如下:

  • 我们发现登录时 确实有验证码错误的弹出 但是同时界面会出现一个遮罩层提示Uncaught runtime errors
    在这里插入图片描述

  • 解决方法

5.打开vue.config.js

    devServer:{
        // 解决页面弹出红色报错遮罩层
        client: {
            //将overlay设置为false即可
            overlay: false
        }
    }

在这里插入图片描述

6.重新测试登录 正常

在这里插入图片描述

四、后台管理界面开发

ok,登录界面我们已经开发完毕,并且我们已经能够进入管理系统的首页了,接下来我们就来开发首页的页面。

一般来说,管理系统的页面我们都是头部是一个简单的信息展示系统名称和登录用户信息,然后中间的左边是菜单导航栏,右边是内容,对应到elementui的组件中,我们可以找到这个Container 布局容器用于布局,方便快速搭建页面的基本结构。

而我们采用这个布局:

在这里插入图片描述

而这个页面,一般来说Header和Aside都是不会变化的,只有Main部分会跟着链接变化而变化,所以我们可以提炼公共部分出来,放在Home.vue中,然后Main部分放在Index.vue中,

那么问题来了,我们如何才能做到点击左边的Aside,然后局部刷新Main中的内容呢?在Vue中,我们可以通过嵌套路由(子路由)的形式。也就是我们需要重新定义路由,一级路由是Home.vue,Index.vue是作为Home.vue页面的子路由,然后Home.vue中我们通过来展示Index.vue的内容即可。

1.创建 src/views/Home.vue

2.在router中,我们这样修改:

const routes = [
    {
        path: '/login',
        name: 'login',
        component: Login
    },
    {
        path: '/',
        name: 'home',
        redirect: '/index',
        component: Home,
        children: [{
            path: '/index',
            name: 'index',
            meta: {
                title: '首页'
            },
            component: Index
        }]
    },
]

可以看到原本的Index已经作为了Home的children,所以在链接到/index的时候我们会展示父级Home的内容,然后再显示Index内容。

3.src/views/Home.vue

<template>
    <div id="home">
        <el-container>
            <el-aside width="200px">菜单栏</el-aside>
            <el-container>
                <el-header>
                    <strong>Spring Security安全框架</strong>
                    <div class="header-right">
                        <el-avatar size="medium" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"></el-avatar>
                        <el-dropdown>
                            <span class="el-dropdown-link">
                                Admin<i class="el-icon-arrow-down el-icon--right"></i>
                            </span>
                            <el-dropdown-menu slot="dropdown">
                                <el-dropdown-item divided>个人中心</el-dropdown-item>
                                <el-dropdown-item divided>退出</el-dropdown-item>
                            </el-dropdown-menu>
                        </el-dropdown>
                        <el-link href="https://mp.csdn.net/mp_blog/manage/article?spm=1011.2124.3001.5298">CSDN笔记</el-link>
                        <el-link href="https://gitee.com/">Gitee仓库</el-link>
                    </div>
                </el-header>
                <el-main>
                    <router-view/>
                </el-main>
            </el-container>
        </el-container>
    </div>
</template>

<script>
export default {
    name: "Home"
}
</script>

<style lang="less" scoped>
.el-container {
    margin: 0;
    padding: 0;
    height: 100vh;

    .header-right {
        width: 260px;
        float: right;
        display: flex;
        justify-content: space-around;
        align-items: center;
        font-weight: bold;
    }
}

.el-header, .el-footer {
    background-color: #B3C0D1;
    color: #333;
    text-align: center;
    line-height: 60px;
}

.el-aside {
    background-color: #D3DCE6;
    color: #333;
    text-align: center;
    line-height: 200px;
}

.el-main {
    background-color: #E9EEF3;
    color: #333;
    text-align: center;
    line-height: 160px;
}

body > .el-container {
    margin-bottom: 40px;
}

.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
    line-height: 260px;
}

.el-container:nth-child(7) .el-aside {
    line-height: 320px;
}

.el-dropdown-link {
    cursor: pointer;
    color: #409EFF;
}
.el-icon-arrow-down {
    font-size: 12px;
}
</style>

4.src/views/Index.vue

<template>
    <div>
        <el-carousel :interval="4000" type="card" indicator-position="outside">
            <el-carousel-item v-for="url in urls" :key="url">
                <el-image :src="url"></el-image>
            </el-carousel-item>
        </el-carousel>
    </div>
</template>

<script>
export default {
    name: "Index",
    data() {
        return {
            urls: [
                'https://fuss10.elemecdn.com/a/3f/3302e58f9a181d2509f3dc0fa68b0jpeg.jpeg',
                'https://fuss10.elemecdn.com/1/34/19aa98b1fcb2781c4fba33d850549jpeg.jpeg',
                'https://fuss10.elemecdn.com/0/6f/e35ff375812e6b0020b6b4e8f9583jpeg.jpeg',
                'https://fuss10.elemecdn.com/9/bb/e27858e973f5d7d3904835f46abbdjpeg.jpeg',
                'https://fuss10.elemecdn.com/d/e6/c4d93a3805b3ce3f323f7974e6f78jpeg.jpeg',
                'https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg',
                'https://fuss10.elemecdn.com/2/11/6535bcfb26e4c79b48ddde44f4b6fjpeg.jpeg'
            ]
        }
    }
}
</script>

<style lang="less" scoped>
.el-carousel__item h3 {
    color: #475669;
    font-size: 14px;
    opacity: 0.75;
    line-height: 200px;
    margin: 0;
}

.el-carousel__item:nth-child(2n) {
    background-color: #99a9bf;
}

.el-carousel__item:nth-child(2n+1) {
    background-color: #d3dce6;
}
</style>

5.总体下来效果如下:

在这里插入图片描述

有点感觉了,然后左边的菜单栏我们也弄下,我们找到NavMenu 导航菜单组件,然后加到Home.vue中,因为考虑到后面我们需要做动态菜单,所以我想单独这个页面出来,因此我新建了个SideMenu.vue

6.SideMenu.vue

<template>
    <el-menu
        class="el-menu-vertical-demo"
        background-color="#545c64"
        text-color="#fff"
        active-text-color="#ffd04b"
    >
        <router-link to="/index">
            <el-menu-item index="Index">
                <template slot="title">
                    <i class="el-icon-s-home"></i>
                    <span slot="title">首页</span>
                </template>
            </el-menu-item>
        </router-link>
        <el-submenu index="1">
            <template slot="title">
                <i class="el-icon-s-operation"></i>
                <span>系统管理</span>
            </template>
            <el-menu-item index="1-1">
                <template slot="title">
                    <i class="el-icon-s-custom"></i>
                    <span slot="title">用户管理</span>
                </template>
            </el-menu-item>
            <el-menu-item index="1-2">
                <template slot="title">
                    <i class="el-icon-rank"></i>
                    <span slot="title">角色管理</span>
                </template>
            </el-menu-item>
            <el-menu-item index="1-3">
                <template slot="title">
                    <i class="el-icon-menu"></i>
                    <span slot="title">菜单管理</span>
                </template>
            </el-menu-item>
        </el-submenu>
        <el-submenu index="2">
            <template slot="title">
                <i class="el-icon-s-tools"></i>
                <span>系统工具</span>
            </template>
            <el-menu-item index="2-2">
                <template slot="title">
                    <i class="el-icon-s-order"></i>
                    <span slot="title">数字字典</span>
                </template>
            </el-menu-item>
        </el-submenu>
    </el-menu>
</template>

<script>
export default {
    name: "SideMenu"
}
</script>

<style lang="less" scoped>
.el-menu-vertical-demo{
    height: 100%;
}
</style>

SideMenu.vue作为一个组件添加到Home.vue中,我们首选需要导入,然后声明compoents,然后才能使用标签

7.在Home.vue中代码如下

<template>
    <div id="home">
        <el-container>
            <el-aside width="200px">
                <SideMenu></SideMenu>
            </el-aside>
            <el-container>
            ....
            </el-container>
        </el-container>
    </div>
</template>

<script>
import SideMenu from "@/views/SideMenu";
export default {
    name: "Home",
    components: {SideMenu}
}
</script>

在这里插入图片描述

8.最后效果如下:

在这里插入图片描述

我们先来新建几个页面,先在views下新建文件夹sys,然后再新建vue页面,具体看下面,这样我们就能把链接和页面可以连接起来。

  • src\views\sys
    • Dict.vue 数字字典
    • Menu.vue 菜单管理
    • Role.vue 角色管理
    • User.vue 用户管理
      在这里插入图片描述

虽然建立了页面,但是因为我们没有在router中注册链接与组件的关系,所以我们现在打开链接还是打开不了页面的。下面我们就要动态联系起来。

五、用户登录信息展示

管理界面的右上角的用户信息现在是写死的,因为我们现在已经登录成功,所以我们可以通过接口去请求获取到当前的用户信息了,这样我们就可以动态显示用户的信息,这个接口比较简单,然后退出登录的链接也一起完成,就请求接口同时把浏览器中的缓存删除就退出了哈。

1.src\views\Home.vue

<template>
    <div id="home">
        <el-container>
            <el-aside width="200px">
                <SideMenu></SideMenu>
            </el-aside>
            <el-container>
                <el-header>
                    <strong>Spring Security安全框架</strong>
                    <div class="header-right">
                        <el-avatar size="medium" :src="form.avatar"></el-avatar>
                        <el-dropdown>
                            <span class="el-dropdown-link">
                                {{ form.username }}<i class="el-icon-arrow-down el-icon--right"></i>
                            </span>
                            <el-dropdown-menu slot="dropdown">
                                <el-dropdown-item divided>个人中心</el-dropdown-item>
                                <el-dropdown-item @click.native="logout" divided>退出</el-dropdown-item>
                            </el-dropdown-menu>
                        </el-dropdown>
                        <el-link href="https://mp.csdn.net/mp_blog/manage/article?spm=1011.2124.3001.5298">CSDN笔记
                        </el-link>
                        <el-link href="https://gitee.com/">Gitee仓库</el-link>
                    </div>
                </el-header>
                <el-main>
                    <router-view/>
                </el-main>
            </el-container>
        </el-container>
    </div>
</template>

<script>
import SideMenu from "@/views/SideMenu";
import {getUserInfo, logout} from "@/api/login";

export default {
    name: "Home",
    components: {SideMenu},
    data() {
        return {
            form: {
                id: null,
                username: null, // 用户名
                avatar: null, // 头像
            }
        }
    },
    mounted() {
        this.getUserInfo();
    },
    methods: {
        getUserInfo(){
            getUserInfo().then(res =>{
                Object.assign(this.form, res.data.data);
            })
        },
        logout(){
            logout().then(res =>{
                console.log(res.data.data)
                this.$store.commit('RESET_STATE')
                this.$router.push('/login')
            })
        }
    },
}
</script>

<style lang="less" scoped>
.el-container {
    margin: 0;
    padding: 0;
    height: 100vh;

    .header-right {
        width: 260px;
        float: right;
        display: flex;
        justify-content: space-around;
        align-items: center;
        font-weight: bold;
    }
}

.el-header, .el-footer {
    background-color: #B3C0D1;
    color: #333;
    text-align: center;
    line-height: 60px;
}

.el-aside {
    background-color: #D3DCE6;
    color: #333;
    text-align: center;
    line-height: 200px;
}

.el-main {
    background-color: #E9EEF3;
    color: #333;
    text-align: center;
    line-height: 160px;
}

body > .el-container {
    margin-bottom: 40px;
}

.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
    line-height: 260px;
}

.el-container:nth-child(7) .el-aside {
    line-height: 320px;
}

.el-dropdown-link {
    cursor: pointer;
    color: #409EFF;
}

.el-icon-arrow-down {
    font-size: 12px;
}

</style>

2.由于我们将请求接口提取到js中了 所以在src下创建一个api文件夹

在这里插入图片描述

  • login.js
import axios from "@/axios";


// 获取验证码和随机码
export function getCaptchaImg(data) {
    return axios({
        url: '/captcha',
        method: 'post',
        data: data
    })
}

// 登录
export function toLogin(data) {
    return axios({
        url: '/login',
        method: 'post',
        data: data
    })
}

// 获取用户信息
export function getUserInfo(data) {
    return axios({
        url: '/userInfo',
        method: 'get',
        params: data
    })
}

// 登出
export function logout(data) {
    return axios({
        url: '/logout',
        method: 'post',
        data: data
    })
}

3.src/store/index.js

        RESET_STATE(state, token) {
            state.token = null;
            localStorage.clear();
            sessionStorage.clear();
        },

在这里插入图片描述

4.src/mock.js

/**
 获取用户信息
 */

Mock.mock(RegExp('/userInfo'),'get',(config)=>{
    // 这里无法在header添加authorization,直接跳过
    Result.data = {
        id: random.string(3), // 获取一个3位的随机字符串
        username:'Admin',
        avatar: 'https://tse4-mm.cn.bing.net/th/id/OIP-C.QiENtPtG3CIjC6yr0P-bMQHaFj?w=252&h=188&c=7&r=0&o=5&pid=1.7'
    }
    return Result
})

/**
 登出
 */

Mock.mock(RegExp('/logout'),'post',(config)=>{
    return Result
})

5.效果

在这里插入图片描述

六、动态菜单栏开发

①:动态菜单

上面代码中,左侧的菜单栏的数据是写死的,在实际场景中我们不可能这样做,因为菜单是需要根据登录用户的权限动态显示菜单的,也就是用户看到的菜单栏可能是不一样的,这些数据需要去后端访问获取。

首先我们先把写死的数据简化成一个json数组数据,然后for循环展示出来,代码如下

1./src/views/inc/SideMenu.vue

<template>
    <el-menu
        class="el-menu-vertical-demo"
        background-color="#545c64"
        text-color="#fff"
        active-text-color="#ffd04b"
    >
        <router-link to="/index">
            <el-menu-item index="Index">
                <template slot="title">
                    <i class="el-icon-s-home"></i>
                    <span slot="title">首页</span>
                </template>
            </el-menu-item>
        </router-link>
        <el-submenu :index="menu.name" v-for="menu in menuList" :key="menu.id">
            <template slot="title">
                <i :class="menu.icon"></i>
                <span>{{ menu.title }}</span>
            </template>
            <router-link :to="item.path" v-for="item in menu.children" :key="item.id">
                <el-menu-item :index="item.name">
                    <template slot="title">
                        <i :class="item.icon"></i>
                        <span slot="title">{{ item.title }}</span>
                    </template>
                </el-menu-item>
            </router-link>
        </el-submenu>
    </el-menu>
</template>

<script>
export default {
    // 导航菜单
    name: "SideMenu",
    data() {
        return {}
    },
    computed: {
        menuList: {
            get() {
                return this.$store.state.menus.menuList
            }
        }
    }

}
</script>

<style lang="less" scoped>
.el-menu-vertical-demo {
    height: 100%;
}
</style>

在这里插入图片描述

可以看到,用for循环显示数据,那么这样变动菜单栏时候只需要修改menuList即可。效果和之前的完全一样。 menuList的数据一般我们是要请求后端的,所以这里我们定义一个mock接口,因为是动态菜单,一般我们也要考虑到权限问题,所以我们请求数据的时候一般除了动态菜单,还要权限的数据,比如菜单的添加、删除是否有权限,是否能显示该按钮等,有了权限数据我们就定动态决定是否展示这些按钮了。

2.src/mock.js

/**
 获取用户菜单以及权限接口
 */

Mock.mock('/sys/menuAndAuth','get',(config)=>{
    let menu = [
        {
            id:1,
            name: 'SysManga',
            title: '系统管理',
            icon: 'el-icon-s-operation',
            component: '',
            path: '',
            children: [
                {
                    id:2,
                    name: 'SysUser',
                    title: '用户管理',
                    icon: 'el-icon-s-custom',
                    path: '/sys/user',
                    component: 'sys/User',
                    children: []
                },
                {
                    id:3,
                    name: 'SysRole',
                    title: '角色管理',
                    icon: 'el-icon-rank',
                    path: '/sys/role',
                    component: 'sys/Role',
                    children: []
                },
                {
                    id:4,
                    name: 'SysMenu',
                    title: '菜单管理',
                    icon: 'el-icon-menu',
                    path: '/sys/menu',
                    component: 'sys/Menu',
                    children: []
                }
            ]
        },
        {
            id:5,
            name: 'SysTools',
            title: '系统工具',
            icon: 'el-icon-s-tools',
            path: '',
            component: '',
            children: [
                {
                    id:6,
                    name: 'SysDict',
                    title: '数字字典',
                    icon: 'el-icon-s-order',
                    path: '/sys/dict',
                    component: 'sys/Dict',
                    children: []
                },
            ]
        }
    ]
    let  auth = ['sys:user:list', "sys:user:save", "sys:user:delete"]
    Result.data = {
        menus: menu,
        auths:auth
    }
    return Result
})

综上,我们把加载菜单数据这个动作放在router.js中。Router有个前缀拦截,就是在路由到页面之前我们可以做一些判断或者加载数据。

②:动态路由

1.创建src/store/modules/menus.js 模块来共享菜单相关的全局变量

在这里插入图片描述

2.在src/store/index.js中引刚刚创建的menus.js

import menus from "@/store/modules/menus";
   modules: {
        menus
    }

在这里插入图片描述

3.src/store/modules/menus.js中添加全局共享变量

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default{
    state: {
        hasRoutes: false, // 是否为第一次加载路由
        menuList: [],
        authList:[],
    },
    getters: {},
    mutations: {
        // 设置菜单列表
        SET_MENU_LIST(state, menuList) {
            state.menuList = menuList;
        },
        // 设置权限列表
        SET_AUTH_LIST(state, authList) {
            state.authList = authList;
        },
        // 设置路由已经加载过
        SET_HAS_ROUTES(state, hasRoutes) {
            state.hasRoutes = hasRoutes;
        },
    },
    actions: {}
}

4.src/router/index.js加载菜单数据

import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from "@/views/Login";
import Index from "@/views/Index";
import Home from "@/views/Home";
import store from "@/store";
import {getUserMenuAndAuth} from "@/api/login";

Vue.use(VueRouter)

const routes = [
    {
        path: '/login',
        name: 'login',
        component: Login
    },
    {
        path: '/',
        name: 'home',
        redirect: '/index',
        component: Home,
        children: [
            {
                path: '/index',
                name: 'Index',
                meta: {
                    title: '首页'
                },
                component: Index
            },
        ]
    },
]

const router = new VueRouter({
    mode: 'history',
    base: process.env.BASE_URL,
    routes
})

router.beforeEach((to, from, next) => {
    // 获取到是否为第一个加载路由
    let hasRoutes = store.state.menus.hasRoutes;
    // 获取token
    let token = localStorage.getItem('token');
    // 如果访问的是登录页面,直接放行
    if (to.path === '/login') next()
    // 如果token为空 没有登录 跳转到登录页面
    if (!token) next({path: '/login'})
    // 如果不是第一次动态加载路由(已经登录 并且加载过路由) 无需再次加载 直接放行
    if (hasRoutes) next();
    // 能够执行到这里(代表 已经 登录 并且是第一次加载路由)
    // 获取用户菜单以及权限接口(发送请求)
    getUserMenuAndAuth().then(res => {
        console.log('获取用户菜单以及权限接口', res.data.data);
        // 拿到用户菜单
        store.commit('SET_MENU_LIST', res.data.data.menus)
        // 拿到用户权限
        store.commit('SET_AUTH_LIST', res.data.data.auths)

        // 动态绑定路由
        // 获取当前的路由配置
        let newRoutes = router.options.routes;
        // 置空之前的动态配置
        newRoutes[1].children = []
        console.log('newRoutes前', newRoutes)
        res.data.data.menus.forEach(menu => {
            // 判断是否有子菜单 有子菜单转成路由
            if (menu.children) {
                menu.children.forEach(e => {
                    // 转成路由
                    let router = menuToRouter(e);
                    // 把路由添加到路由管理器  因为要添加到home路由下的children中 所有newRoutes[1].children
                    if (router) newRoutes[1].children.push(router)
                })
            }
        })
      
        // 将新生成的路由逐个添加到现有路由配置中
        newRoutes.forEach(route => {
            router.addRoute(route);
        });

        console.log('newRoutes后',newRoutes)
        // 设置路由是否已经加载过
        hasRoutes = true;
        store.commit('SET_HAS_ROUTES', hasRoutes)

        next({path: to.path});
    })
})


// 导航转成路由
function menuToRouter(menu) {
    // 如果 component为空 无需转换
    if (!menu.component) return null

    let route = {
        name: menu.name,
        path: menu.path,
        meta: {
            icon: menu.icon,
            title: menu.title
        },
    };
    route.component = () => import ('@/views/' + menu.component + '.vue')
    return route
}

export default router

可以看到,我们通过menuToRoute就是把menu(菜单)数据转换成路由对象,然后router.addRoute(route)动态添加路由对象。 同时上面的menu对象中,有个menu.component,这个就是连接对应的组件,我们需要添加上去,比如说**/sys/user**链接对应到 component(sys/User)

这样我们才能绑定添加到路由。所以我会修改mock中的nav的数据成这样:

在这里插入图片描述

同时上面router中我们还通过判断是否登录页面,是否有token等判断提前判断是否能加载菜单,同时还做了个开关hasRoute来动态判断是否已经加载过菜单。

还需要在store中定义几个方法用于存储数据,我们定义一个menu模块

这样我们菜单的数据就可以加载了,然后再SideMenu.vue中直接获取store中的menuList数据即可显示菜单出来了。

5.最后效果如下

七、 动态标签页开发

我看别的后台管理系统都有这个,效果是这样的:

在这里插入图片描述

element-ui中寻了一圈,发现Tab标签页组件挺符合我们要求的,可以动态增减标签页。

理想的动作是这样的:

  1. 当我们点击导航菜单,上方会添加一个对应的标签,注意不能重复添加,发现已存在标签直接切换到这标签即可
  2. 删除当前标签的时候会自动切换到前一个标签页
  3. 点击标签页的时候会调整到对应的内容页中
    综合Vue的思想,我们可以这样设计:在Store中统一存储:1、当前标签Tab,2、已存在的标签Tab列表,然后页面从Store中获取列表显示,并切换到当前Tab即可。删除时候我们循环当前Tab列表,剔除Tab,并切换到指定Tab。

我们先和左侧菜单一样单独定义一个组件Tabs.vue放在views/文件夹内:

1.src/views/Tabs.vue

<template>
    <el-tabs v-model="editableTabsValue" closable type="card" @tab-remove="removeTab" @tab-click="clickTab">
        <el-tab-pane v-for="item in editableTabs"
                     :key="item.name"
                     :label="item.title"
                     :name="item.name"></el-tab-pane>
    </el-tabs>
</template>

<script>
export default {
    name: "Tabs",
    data() {
        return {};
    },
    computed: {
        editableTabs: {
            get() {
                return this.$store.state.menus.editableTabs
            },
            set(val) {
                this.$store.state.menus.editableTabs = val
            }
        },
        editableTabsValue: {
            get() {
                return this.$store.state.menus.editableTabsValue
            },
            set(val) {
                this.$store.state.menus.editableTabsValue = val
            }
        },
    },
    methods: {
        removeTab(tabName) {
            let tabs = this.editableTabs;
            let tabValue = this.editableTabsValue;
            // 如果 关闭的时首页直接返回
            if (tabValue === 'Index') return
            // 如果关闭的是当前页面 则寻找下一个页面做为当前页
            if (tabName === tabValue) {
                tabs.forEach((tab, index) => {
                    if (tab.name === tabValue) {
                        // 找下一个 或者前一个页面
                        let nextTab = tabs[index + 1] || tabs[index - 1];
                        if (nextTab) tabValue = nextTab.name;
                    }
                })
            }
            // 替换 标签名
            this.editableTabsValue = tabValue;
            // 过滤出除了关闭的标签
            this.editableTabs = tabs.filter(tab => tab.name !== tabName)

            this.$router.push({name: tabValue})

        },
        clickTab(tab) {
            this.$router.push({name: tab.name})
        }
    }
}
</script>

<style scoped>
</style>

上面代码中,computed表示当其依赖的属性的值发生变化时,计算属性会重新计算,反之,则使用缓存中的属性值。这样我们就可以实时监测Tabs标签的动态变化实时显示(相当于实时get、set)。其他clickTab、removeTab的逻辑其实也还算简单,特别是removeTab注意考虑多种情况就可以。 然后我们来到store中的menu.js,我们添加 editableTabsValue和editableTabs,然后把首页作为默认显示的页面。

2.src/store/modules/menus.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default {
    state: {
        hasRoutes: false, // 是否为第一次加载路由
        menuList: [],
        authList: [],
        editableTabsValue: 'Index',
        editableTabs: [
            {
                title: '首页',
                name: 'Index',
            }
        ]
    },
    getters: {},
    mutations: {
        // 设置菜单列表
        SET_MENU_LIST(state, menuList) {
            state.menuList = menuList;
        },
        // 设置权限列表
        SET_AUTH_LIST(state, authList) {
            state.authList = authList;
        },
        // 设置路由已经加载过
        SET_HAS_ROUTES(state, hasRoutes) {
            state.hasRoutes = hasRoutes;
        },
        ADD_TAB(state, tab) {
            // 查看要添加的标签是否已经存在
            let index = state.editableTabs.findIndex(e => e.name === tab.name);
            console.log(tab.name)

            // 没有找打 不存在 则添加
            if (index === -1) {
                state.editableTabs.push({
                    title: tab.title,
                    name: tab.name,
                })
            }
            // 把标签名字改为刚添加的名字
            state.editableTabsValue = tab.name;
        },
        RESET_TAB_STATUS(state) {
            state.menuList = [];
            state.authList = [];
            state.hasRoutes = false;
            state.editableTabsValue = 'Index';
            state.editableTabs = [
                {
                    title: '首页',
                    name: 'Index',
                }
            ]
        }
    },
    actions: {}
}

ok,然后再Home.vue中引入我们Tabs.vue这个组件,添加代码的地方比较零散,所以我就写重要代码出来就好,自行添加到指定的地方哈。

3.src/views/Home.vue

  • 只需引入即可
    在这里插入图片描述

  • 退出登录时要重置标签的状态
    在这里插入图片描述

  • 注释掉居中的样式
    在这里插入图片描述

好了完成了第一步了,现在我们需要点击菜单导航,然后再tabs列表中添加tab标签页,那么我们来到SideMenu.vue,我们给el-menu-item每个菜单都添加一个点击事件:

4.src/views/inc/SideMenu.vue

<template>
    <el-menu
        :default-active="this.$store.state.menus.editableTabsValue"
        class="el-menu-vertical-demo"
        background-color="#545c64"
        text-color="#fff"
        active-text-color="#ffd04b"
    >
        <router-link to="/index">
            <el-menu-item index="Index" @click="addTab({name: 'Index', title: '首页'})">
                <template slot="title">
                    <i class="el-icon-s-home"></i>
                    <span slot="title">首页</span>
                </template>
            </el-menu-item>
        </router-link>
        <el-submenu :index="menu.name" v-for="menu in menuList" :key="menu.id">
            <template slot="title">
                <i :class="menu.icon"></i>
                <span>{{ menu.title }}</span>
            </template>
            <router-link :to="item.path" v-for="item in menu.children" :key="item.id">
                <el-menu-item :index="item.name" @click="addTab(item)">
                    <template slot="title">
                        <i :class="item.icon"></i>
                        <span slot="title">{{ item.title }}</span>
                    </template>
                </el-menu-item>
            </router-link>
        </el-submenu>
    </el-menu>
</template>

<script>
export default {
    // 导航菜单
    name: "SideMenu",
    data() {
        return {}
    },
    computed: {
        menuList: {
            get() {
                return this.$store.state.menus.menuList
            }
        }
    },
    methods: {
        addTab(tab){
            this.$store.commit('ADD_TAB', tab)
        }
    },

}
</script>

<style lang="less" scoped>
.el-menu-vertical-demo {
    height: 100%;
}
</style>

添加tab标签的时候注意需要激活指定当前标签,也就是设置editableTabsValue。然后我们也添加了setActiveTab方法,方便其他地方指定激活某个标签。

但是当我们刷新浏览器、或者直接通过输入链接打开页面时候就不会自动帮我们根据链接回显激活Tab。

刷新浏览器之后链接/sys/users不变,内容不变,但是Tab却不见了,所以我们需要修补一下,当用户是直接通过输入链接形式打开页面的时候我们也能根据链接自动添加激活指定的tab。那么在哪里添加这个回显的方法呢?router中?其实可以,只不过我们需要做判断,因为每次点击导航都会触发router。有没有更简便的方法?有的!因为刷新或者打开页面都是一次性的行为,所以我们可以在更高层的App.vue中做这个回显动作,具体如下:

5.src\App.vue

<template>
    <div id="app">
        <router-view/>
    </div>
</template>
<script>
export default {
    name: 'App',
    watch: {
        $route(to, from) {
            if (to.path !== '/login') {
                let object = {
                    name: to.name,
                    title: to.meta.title
                }
                this.$store.commit('ADD_TAB', object)
            }
        }
    }
}
</script>

上面代码可以看到,除了login页面,其他页面都会触发addTabs方法,这样我们就可以添加tab和激活tab了。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Mybatis学习笔记-映射文件,标签,插件

目录 概述 mybatis做了什么 原生JDBC存在什么问题 MyBatis组成部分 Mybatis工作原理 mybatis和hibernate区别 使用mybatis&#xff08;springboot&#xff09; mybatis核心-sql映射文件 基础标签说明 1.namespace&#xff0c;命名空间 2.select&#xff0c;insert&a…

TensorFlow:GPU的使用

**引言** TensorFlow 是一个由 Google 开发的开源机器学习框架&#xff0c;它提供了丰富的工具和库&#xff0c;支持开发者构建和训练各种深度学习模型。而 GPU 作为一种高性能并行计算设备&#xff0c;能够显著提升训练深度学习模型的速度&#xff0c;从而加快模型迭代和优化…

CorelDRAW2024最新版本的图形设计软件

CorelDRAW2024是Corel公司推出的最新版本的图形设计软件。CorelDRAW是一款功能强大的矢量图形编辑工具&#xff0c;被广泛用于图形设计、插图、页面布局、照片编辑和网页设计等领域。 1. 新增的设计工具&#xff1a;CorelDRAW 2024引入了一些全新的设计工具&#xff0c;使用户能…

Web(5)Burpsuite之文件上传漏洞

1.搭建网站&#xff1a;为网站设置没有用过的端口号 2.中国蚁剑软件的使用 通过一句话木马获得权限 3.形象的比喻&#xff08;风筝&#xff09; 4.实验操作 参考文章&#xff1a; 文件上传之黑名单绕过_文件上传黑名单绕过_pigzlfa的博客-CSDN博客 后端验证特性 与 Window…

再也不用担心忘记密码了!如何在Windows 10或11中重置被遗忘的密码

​如果你忘记了Windows电脑的密码,不要惊慌。Windows 10和Windows 11都允许你重置忘记的密码,无论你使用的是Microsoft帐户还是本地帐户。你所要做的就是回答你的安全问题以重置密码。另一种选择是创建一个密码重置盘,你可以在任何U盘上进行。 除了使用密码之外,你还应该启…

【MySQL】索引与事务

作者主页&#xff1a;paper jie_博客 本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 本文录入于《MySQL》专栏&#xff0c;本专栏是针对于大学生&#xff0c;编程小白精心打造的。笔者用重金(时间和精力)打造&a…

前端Vue拖拽功能

文章目录 安装使用 直接复制粘贴即可页面使用 直接复制粘贴即可小结&#xff08;带有效果图&#xff09; 安装 提示&#xff1a;首先您需要安装它&#xff0c;命令如下&#xff1a; npm install awe-dnd --save使用 直接复制粘贴即可 在mian.js文件中引入 //main.jsimport V…

【数据库】数据库连接池导致系统吞吐量上不去-复盘

在实际的开发中&#xff0c;我们会使用数据库连接池&#xff0c;但是如果不能很好的理解其中的含义&#xff0c;那么就可以出现生产事故。 HikariPool-1 - Connection is not available, request timed out after 30001ms.当系统的调用量上去&#xff0c;就出现大量这样的连接…

市级奖项+1,持安获「创业北京」创业创新大赛优秀奖!

2274个创业项目参赛 历经五个多月的激烈角逐 第六届“创业北京”创业创新大赛 终于圆满落下帷幕 持安科技在北京市总决赛中再创佳绩&#xff01; 荣获制造业赛道优秀奖 本次大赛由北京市人力资源和社会保障局、北京市发展和改革委员会等11家单位联合主办&#xff0c;以“创…

代码示例:基于JAX-WS和JAXB,其中http请求和响应的报文体都是xml数据

说明 基于JAX-WS编写了RESTful的web服务端点。 http请求和响应的报文体都是xml数据&#xff0c;服务端分别对应了用JAXB注解的请求和响应类。 只实现了服务端的代码示例 客户端使用了Postman 示例 要实现的目标&#xff1a;http请求和响应报文体的xml数据 http请求报文体的…

c语言免杀火绒

文章目录 前记c加载器补充知识 前记 pyinstaller pyinstaller目前已经被杀疯了&#xff0c;简单打包一个hello a"hello" print(a)#pyinstaller -F -w b.py -n HipsMain.exe考虑Nuitka pip uninstall nuitka pip install nuitka pip install nuitka1.8.5 这里最新…

【2】SM2验签工具和RSA验签工具

0X01 前言 最近看了好多验签工具&#xff0c;感觉不是很好用&#xff0c;就自己造了个。 0x02 工具功能介绍 对SM2算法进行验签和RSA算分进行验签&#xff0c;签名值可以是base64&#xff0c;也可以是十六进制。 兼容各种输入。 0x03 工具使用 RSA 验签 SM2 验签 0x04 工具…

架构师篇 DDD领域驱动设计篇

一 DDD领域驱动设计 1.1 领域驱动设计 领域驱动设计(英文&#xff1a;Domain-Driven Design&#xff0c;缩写DDD)是一种模型驱动设计的方法&#xff0c;领域驱动设计常以战略设计与战术设计来将整个领域展现的淋漓尽致&#xff0c;其作用范围既面向业务也面向技术。从战略角度…

『GitHub项目圈选02』一款可实现视频自动翻译配音为其他语言的开源项目

&#x1f525;&#x1f525;&#x1f525;本周GitHub项目圈选****: 主要包含视频翻译、正则填字游戏、敏感词检测、聊天机器人框架、AI 换脸、分布式数据集成平台等热点项目。 1、pyvideotrans pyvideotrans 是一个视频翻译工具&#xff0c;可将一种语言的视频翻译为另一种语…

完整版解答!2023年数维杯国际大学生数学建模挑战赛B题

B题完整版全部5问&#xff0c;问题解答、代码&#xff0c;完整论文、模型的建立和求解、各种图表代码已更新&#xff01; 大家好&#xff0c;目前已完成2023数维杯国际赛B题全部5问的代码和完整论文已更新&#xff0c;部分展示如下&#xff1a; 部分解答图表 问题分析 B题前三…

打不开github网页解决方法

问题&#xff1a; 1、composer更新包总是失败 2、github打不开&#xff0c;访问不了 解决方法&#xff1a;下载一个Watt Toolkit工具&#xff0c;勾选上&#xff0c;一键加速就可以打开了。 下载步骤&#xff1a; 1、打开网址&#xff1a; Watt Toolkit 2、点击【下载wind…

ROS 学习应用篇(八)ROS中的坐标变换管理之tf广播与监听的编程实现

偶吼吼胜利在望&#xff0c;冲冲冲 老规矩新建功能包 工作空间目录下/src下开启终端输入 catkin_create_pkg learning_tf roscpp rospy tf turtlesim 如何实现tf广播 引入库 c python …

基于猕猴感觉运动皮层Spike信号的运动解码分析不同运动参数对解码的影响

公开数据集中文版详细描述参考前文&#xff1a;https://editor.csdn.net/md/?not_checkout1&spm1011.2124.3001.6192神经元Spike信号分析参考前文&#xff1a;https://blog.csdn.net/qq_43811536/article/details/134359566?spm1001.2014.3001.5501神经元运动调制分析参考…

开源简历生成器OpenResume

什么是 OpenResume &#xff1f; OpenResume 是一个功能强大的开源简历生成器和简历解析器。OpenResume 的目标是为每个人提供免费的现代专业简历设计&#xff0c;让任何人都能充满信心地申请工作。 OpenResume 有 5 个核心特点&#xff1a; 特征描述1. 实时UI更新当您输入简历…

如何去掉图片上的水印?这三种去水印的方法帮你解决!

当我们从网上看到喜欢的图片&#xff0c;想要保存下来作为头像或者插入到工作汇报中时&#xff0c;却发现下载的图片带有水印。这不仅影响了图片的美观&#xff0c;还可能对图片的可用性造成影响。那么&#xff0c;如何去掉图片上的水印呢? 实际上&#xff0c;现在市面上的很多…
最新文章