Vue3 background management system

You can refer to the following later: vue series (3) - teach you how to build a vue3 management background basic template

The following code project gitee address

Article directory

1. Initialize the front-end project

Initialize the project

You can refer to: vite official website https://vitejs.cn/guide/#scaffolding-your-first-vite-project

npm init vite@latest mushan-vue3-admin

npm install

npm run dev

Add loading effect

In index.html, where the id is app, write

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Vite + Vue</title>
  <style>
    body {
      
      
      padding: 0px;
      margin: 0px;
    }

    .loading {
      
      
      display: flex;
      height: 100vh;
      width: 100vw;
      background: #92b1d7;
      justify-content: center;
      align-items: center;
    }

    .loading .content {
      
      
      position: relative;
      display: flex;
      justify-content: space-around;
      align-items: center;
      margin: 15px;
      border-radius: 4px;
      padding: 10px;
    }

    .circle-3 {
      
      
      width: 60px;
      height: 60px;
      border-radius: 50%;
      display: inline-block;
      position: relative;
      border: 3px solid;
      border-color: #fff #fff transparent transparent;
      animation: rotation 1s linear infinite;
    }

    .circle-3::after,
    .circle-3::before {
      
      
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
      margin: auto;
      border-radius: 50%;
      border: 3px solid;
      animation: rotation-back 0.5s linear infinite;
    }

    .circle-3::after {
      
      
      border-color: transparent #f6b352 #f6b352 transparent;
      width: 52px;
      height: 52px;
    }

    .circle-3::before {
      
      
      border-color: transparent transparent #fff #fff;
      width: 44px;
      height: 44px;
    }

    @keyframes rotation {
      
      
      0% {
      
      
        transform: rotate(0deg);
      }

      100% {
      
      
        transform: rotate(360deg);
      }
    }

    @keyframes rotation-back {
      
      
      0% {
      
      
        transform: rotate(0deg);
      }

      100% {
      
      
        transform: rotate(-360deg);
      }
    }
  </style>
</head>

<body>
  <div id="app">
    <div class="loading">
      <div class="content">
        <div class="circle-3"></div>
      </div>
    </div>
  </div>
  <script type="module" src="/src/main.js"></script>
</body>

</html>

Configure vite.config.js

import {
    
     defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
    
    
  plugins: [vue()],
  server: {
    
    
    hmr: true,
    port: 5174,
  },
  resolve: {
    
    
    alias: {
    
    
      '@':path.resolve(__dirname,'./src')
    }
  }
})

2. Using routing

install router

npm i vue-router@4 -S

configure routing

import {
    
     createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";


// 路由信息
const routes = [
    {
    
    
        path: '/',
        name: 'home',
        component: ()=>import('@/views/index.vue')
    },
    {
    
    
        path: "/login",
        name: "login",
        component:  () => import('@/views/login/index.vue'),
    }
]

const router = createRouter({
    
    
    history: createWebHistory(),
    routes
});

router.beforeEach((to,from,next)=>{
    
    
    nprogress.start()
    next()
})

router.afterEach((to,from,next)=>{
    
    
    nprogress.done()
})

// 导出路由
export default router;

Configure @alias and jump

installation path

npm i path

vite.config.js

import {
    
     defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
    
    
  plugins: [vue()],
  resolve: {
    
    
    alias: {
    
    
      '@':path.resolve(__dirname,'./src')
    }
  }
})

jsconfig.json

In the same directory as vite.config.js

{
    
    
    "compilerOptions": {
    
    
        "baseUrl": "./",
        "paths": {
    
    
            "@/*": [
                "src/*"
            ],
        }
    },
    "exclude": [
        "node_modules",
        "dist"
    ],
    "include": [
        "src/**/*"
    ]
}

Use routing in main.js

import {
    
     createApp } from 'vue'
import './style.css'
import App from './App.vue'

import router from '@/router'

const app = createApp(App)
app.mount('#app')
app.use(router)

3. Use elment-plus

install elment-plus

npm install element-plus --save

Use elment-plus in main.js

import {
    
     createApp } from 'vue'
import './style.css'
import App from './App.vue'

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

import router from '@/router'

const app = createApp(App)
app.mount('#app')
app.use(router)
app.use(ElementPlus)

4. Use pinia

You can refer to: pinia use in Vue3 (collection edition)

install pinia

npm install pinia --save

configure pinia

Create store/index.js

import {
    
     createPinia } from 'pinia'

const pinia = createPinia()

export default pinia

Create store/counter.js

import {
    
     defineStore } from 'pinia'

export const useCounter =  defineStore('counter',{
    
    
    state: () => ({
    
    
		count:99
	}),
    getters: {
    
    

    },
    actions: {
    
    
        
    }
})

Introduced in main.js

import {
    
     createApp } from 'vue'
import './style.css'
import App from './App.vue'

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

import router from '@/router'
import pinia from '@/store'



const app = createApp(App)

app.use(router)
app.use(pinia)
app.use(ElementPlus)

app.mount('#app')

used in the component

<template>
    {
   
   { counterStore.count }}
    <el-button @click="visitStore">你好</el-button>
</template>

<script setup>
    import {
      
      useCounter}  from '@/store/counter'
    const counterStore = useCounter()

    function visitStore() {
      
      
        console.log(counterStore.count);
    }
</script>

<style lang="scss">

</style>

5. Use axios

You can refer to: Vue3 configuration tutorial using axios

install axios

npm install axios --save

Write request.js

import axios from 'axios'
import Messager from './messager'; // 在下面封装了

const instance = axios.create({
    
    
    baseURL: 'http://127.0.0.1:8080/api',
    timeout: 10000
})

instance.interceptors.request.use((config)=>{
    
    
    return config;
})

instance.interceptors.response.use(response=>{
    
    
    if(response.data.errno == 0) {
    
    
        return Promise.resolve(response.data.data)
    } else {
    
    
        if(response.data.errno == 501) {
    
    
            Messager.error('请重新登录')
            window.location.href = '/login'
        } else {
    
    
            Messager.error(response.data.errmsg)
            return Promise.reject(new Error(response.data.errmsg))
        }
    }
})

export default instance

Write API request interface

import request from '@/utils/request'

export function getCaptchaImage()  {
    
    
    return request({
    
    
        url: 'captchaImage',
    })
}

export function login(data)  {
    
    
    return request({
    
    
        method:'post',
        url: 'user/login',
        data
    })
}

Use axios in the component

<template>
    <el-button @click="refreshCaptchaImage">验证码</el-button>
</template>

<script setup>
    import {
    
    getCaptchaImage} from '@/api/loginApi'

    async function refreshCaptchaImage() {
    
    
        let result = await getCaptchaImage()
        console.log(result);
    }
</script>

<style lang="scss">

</style>

6. Using nprogress

install nprogress

npm i nprogress -S

Package nprogress.js

import Nprogress from 'nprogress'
import 'nprogress/nprogress.css'

const nprogress = Nprogress.configure({
    
    
    easing: 'ease', // 动画方式
    speed: 1000, // 递增进度条的速度
    showSpinner: false, // 是否显示加载ico
    trickleSpeed: 200, // 自动递增间隔
    minimum: 0.3, // 更改启动时使用的最小百分比
    parent: 'body', //指定进度条的父容器
})

export default nprogress

Use nprogress in routing

import {
    
     createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";


// 路由信息
const routes = [
    {
    
    
        path: '/',
        name: 'home',
        component: ()=>import('@/views/index.vue')
    },
    {
    
    
        path: "/login",
        name: "login",
        component:  () => import('@/views/login/index.vue'),
    }
]

const router = createRouter({
    
    
    history: createWebHistory(),
    routes
});

router.beforeEach((to,from,next)=>{
    
    
    nprogress.start()
    next()
})

router.afterEach((to,from,next)=>{
    
    
    nprogress.done()
})

// 导出路由
export default router;

7. Introduce iconfont

download iconfont

Download iconfont- related resources locally and add them to the assets/iconfont directory

Introduced in main.js

import {
    
     createApp } from 'vue'
import './style.css'

import '@/assets/iconfont/iconfont.css' // 引入iconfont的css文件

import App from './App.vue'

8. Encapsulate ELMessage

import {
    
     ElMessage } from "element-plus";
const Messager = {
    
    
    ok(msg){
    
    
        ElMessage.success(msg)
    },
    error(msg) {
    
    
        ElMessage.error(msg)
    }
}
export default Messager

9. Login function

Configure the login route

import {
    
     createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";


// 路由信息
const routes = [
    {
    
    
        path: '/',
        name: 'home',
        component: ()=>import('@/views/index.vue')
    },
    {
    
    
        path: "/login",
        name: "login",
        component:  () => import('@/views/login/index.vue'),
    }
]

const router = createRouter({
    
    
    history: createWebHistory(),
    routes
});

router.beforeEach((to,from,next)=>{
    
    
    nprogress.start()
    next()
})

router.afterEach((to,from,next)=>{
    
    
    nprogress.done()
})

// 导出路由
export default router;

login.vue

insert image description here

<template>
    <div class="login-page">
        <div class="login-container">
            <h1 class="login-title">登录</h1>
            <el-form ref="loginFormRef" :model="loginFormData" :rules="loginFormRules" class="login-form">
                <el-form-item prop="username">
                    <el-input v-model="loginFormData.username" prop="username">
                        <template #prefix>
                            <i class="iconfont icon-yonghu"></i>
                        </template>
                    </el-input>
                </el-form-item>

                <el-form-item prop="password">
                    <el-input v-model="loginFormData.password">
                        <template #prefix>
                            <i class="iconfont icon-mima"></i>
                        </template>
                    </el-input>
                </el-form-item>
                <el-form-item prop="code">
                    <div class="login-code">
                        <el-input v-model="loginFormData.code" prop="password">
                            <template #prefix>
                                <i class="iconfont icon-yanzhengma"></i>
                            </template>
                        </el-input>
                        <div class="code-img">
                            <img :src="codeImg" @click="getCodeImg">
                        </div>
                    </div>
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" style="width:100%;" @click="submitLoginForm">登录</el-button>
                </el-form-item>
            </el-form>
        </div>
    </div>
</template>

<script setup>
import {
      
      getCaptchaImage} from '@/api/loginApi'

import useUser from '@/store/user'
import {
      
       ref, reactive,getCurrentInstance, onMounted } from 'vue'
import {
      
       useRouter } from 'vue-router'

const {
      
       proxy } = getCurrentInstance()
const userStore = useUser()
const router = useRouter()


const codeImg = ref('')

const loginFormData = reactive({
      
      
    username: 'admin',
    password: '123456',
    uuid: '',
    code: ''
})
const loginFormRules = {
      
      
    username: [
        {
      
      required:true,message: '用户名不能为空',trigger: 'blur'}
    ],
    password: [
        {
      
      required:true,message: '密码不能为空',trigger: 'blur'}
    ],
    code: [
        {
      
      required:true,message: '验证码不能为空',trigger: 'blur'}
    ],
}

const loginFormRef = ref(null)
function submitLoginForm() {
      
      
    loginFormRef.value.validate(async(valid,fields)=>{
      
      

        if(!valid) {
      
      
            proxy.Messager.error('请填写完整')
            return
        }

        console.log(userStore);
        let result  = await userStore.doLogin(loginFormData)
        router.replace('/')

    })
}

function getCodeImg() {
      
      
    getCaptchaImage().then(res=>{
      
      
        codeImg.value = "data:image/gif;base64," + res.img
        loginFormData.uuid = res.uuid
    })
}

onMounted(()=>{
      
      
    getCodeImg()
})

</script>

<style lang="scss" scoped>
    .iconfont {
      
      
        font-size: 16px;
    }
    .login-page {
      
      
        height: 100vh;
        background-image: url(@/assets/bg.jpg);
        background-position: center;
        background-size: cover;
        display: flex;
        justify-content: center;
        align-items: center;
        .login-container {
      
      
            width: 350px;
            padding: 20px;
            background: rgba(255, 255, 255, 1);
            border-radius: 5px;
            .login-title {
      
      
                font-size: 26px;
                text-align: center;
                margin-bottom: 15px;
            }

            .login-code {
      
      
                display: flex;
                .code-img {
      
      
                    height: 34px;
                    width: 180px;
                    margin-left: 10px;
                    border-radius: 5px;
                    cursor: pointer;
                    background-color: pink;
                    overflow: hidden;
                    img {
      
      
                        width: 100%;
                        height: 100%;
                        object-fit: cover;
                        transform: scale(1.2);
                    }
                }
            }
        }
    }
</style>

store/user.js

Store the token obtained by login into localStorage

import {
    
     defineStore } from 'pinia'

import {
    
     login } from '@/api/loginApi'

function retrieveLocalToken() {
    
    
    return localStorage.getItem('token') || ''
}

export default defineStore('user',{
    
    
    state: () => {
    
    
        return {
    
    
            token: retrieveLocalToken() // 当刷新页面时, 这个会加载一次
        }
    },
    getters: {
    
    

    },
    actions: {
    
    
        doLogin(data) {
    
    
            return new Promise((resolve, reject) => {
    
    
                login(data).then(res=>{
    
    
                    this.token = res // 同样先存入到pinia中
                    localStorage.setItem('token', res)
                    console.log('login',res);
                    resolve(data)
                }).catch(err=>{
    
    
                    reject(err)
                })
            })
        }
    }
})

api/loginApi.js

import request from '@/utils/request'

export function getCaptchaImage()  {
    
    
    return request({
    
    
        url: 'captchaImage',
    })
}

export function login(data)  {
    
    
    return request({
    
    
        method:'post',
        url: 'user/login',
        data
    })
}

10. Background page layout

After successful login, it will jump to the homepage. The layout of the homepage is roughly as follows. You can first refer to vue3 + elment-plus to realize the static page layout of the background layout , and then divide it into different components. The data sharing of different components is managed through the pinia store. .

insert image description here

Configure the route after successful login

import {
    
     createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";


// 路由信息
const routes = [
    {
    
    
        path: "/login",
        name: "login",
        component:  () => import('@/views/login/index.vue'),
    },
    {
    
    
        path: '/',
        name: 'home',
        component: ()=>import('@/layout/index.vue')
    },
]

const router = createRouter({
    
    
    history: createWebHistory(),
    routes
});

router.beforeEach((to,from,next)=>{
    
    
    nprogress.start()
    next()
})

router.afterEach((to,from,next)=>{
    
    
    nprogress.done()
})

// 导出路由
export default router;

split components

Create layout/index.vue

Layout component introduces Sider and Main components


<template>
    <div class="layout">
        <Sider/>
        <Main></Main>
    </div>
</template>

<script setup>
import Sider from './components/Sider.vue';
import Main from './components/Main.vue';
import {
      
       ref, reactive } from 'vue'

</script>

<style lang="scss" scoped>
.layout {
      
      
    display: flex;
}

</style>

Create store/layout.js

Store the component's shared data into pinia

import {
    
     defineStore } from 'pinia'

export default defineStore('layout', {
    
    
    state: ()=> {
    
    
        return {
    
    
            isExpand: true, // 侧边栏是否展开
        }
    },
    getters: {
    
    

    },
    actions: {
    
    
        // 切换侧边栏
        toggleSider() {
    
    
            console.log('切换侧边栏', this.isExpand);
            this.isExpand = !this.isExpand
        }
    }
})

Create layout/components/Sider.vue

isExpand is the data stored in pinia

<template>
    <div class="sider" :style="{ 'width': isExpand ? '220px' : '80px', transition: 'all 0.28s' }">

        <div class="sider-top">
            <h3 v-if="isExpand" class="site-title">PSCOOL管理系统</h3>
            <i v-else class="iconfont icon-graphcool site-icon"></i>
        </div>
        <div class="sider-body">
            <el-scrollbar>
                <ul>
                    <li class="li-item">1</li>
                    <li class="li-item">2</li>
                    <li class="li-item">3</li>
                    <li class="li-item">4</li>
                    <li class="li-item">5</li>
                    <li class="li-item">6</li>
                    <li class="li-item">7</li>
                    <li class="li-item">8</li>
                    <li class="li-item">9</li>
                    <li class="li-item">9</li>
                    <li class="li-item">9</li>
                    <li class="li-item">9</li>
                    <li class="li-item">9</li>
                </ul>
            </el-scrollbar>
        </div>
    </div>
</template>

<script setup>
import useLayout from '@/store/layout'
import {
      
       storeToRefs } from 'pinia'

const layoutStore = useLayout()
const {
      
       isExpand } = storeToRefs(layoutStore) // 需要使用storeToRefs, 才能保持响应式

</script>

<style lang="scss">
.sider {
      
      
    width: 220px;
    height: 100vh;
    background-color: #294256;
    position: relative;

    box-shadow: 2px 0 8px 0 rgba(0, 0, 0, 0.3);

    flex-shrink: 0;

    .sider-top {
      
      
        height: 50px;

        display: flex;
        align-items: center;
        justify-content: center;

        overflow: hidden;

        .site-title {
      
      
            white-space: nowrap;
            font-weight: bold;
            color: #fff;
        }

        .site-icon {
      
      
            font-size: 20px;
            color: #27ae60;
        }
    }

    .sider-body {
      
      
        position: absolute;
        top: 50px;
        left: 0;
        right: 0;
        bottom: 0;

        background-color: #294256;

        .li-item {
      
      
            height: 50px;
            margin: 10px;
            background-color: #294256;
            color: #fff;

            display: flex;
            align-items: center;
            justify-content: center;
        }
    }

}
</style>

Create layout/Main.vue

<template>
  <div class="main">

        <div class="main-header">
            <div class="main-header-top">
                <div class="main-header-top-left">
                    <div class="hamburger" @click="layoutStore.toggleSider">
                        <i :class="['iconfont', { 'icon-shousuocaidan': isExpand }, { 'icon-shousuocaidan-copy': !isExpand }]"></i>
                    </div>
                    <Breadcrumb />
                </div>
                <div class="main-header-top-right">
                    <div class="gitee mlr8 pointer">
                        <i class="iconfont icon-gitee"></i>
                    </div>
                    <div class="fullscreen mlr8">
                        <i :class="['iconfont', 'pointer', { 'icon-quanping_o': !isFullScreen, 'icon-quxiaoquanping_o': isFullScreen }]"></i>
                    </div>
                    <div class="theme-mode mlr8">
                        <el-switch inline-prompt :active-icon="Check" :inactive-icon="Close" />
                    </div>
                    <div class="avatar-box mlr8 pointer">
                        <el-dropdown>
                            <span class="el-dropdown-link">
                                <img class="avatar" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" alt="">
                            </span>
                            <template #dropdown>
                                <el-dropdown-menu>
                                    <el-dropdown-item>个人中心</el-dropdown-item>
                                    <el-dropdown-item divided>退出登录</el-dropdown-item>
                                </el-dropdown-menu>
                            </template>
                        </el-dropdown>
                    </div>
                </div>
            </div>
            <TagsView/>
        </div>
        <div class="main-body">
            <Demo/>
            <!-- <router-view></router-view> -->
        </div>
    </div>
</template>

<script setup>
import Breadcrumb from './Breadcrumb.vue'
import useLayout from '@/store/layout'
import Demo from './Demo.vue'
import {
      
       storeToRefs } from 'pinia'
import {
      
       ref, reactive } from 'vue'
import TagsView from './TagsView.vue'

const isFullScreen = ref(false)
const layoutStore = useLayout()
const {
      
       isExpand } = storeToRefs(layoutStore)


</script>

<style lang="scss">
.main {
      
      
    flex: 1;
    overflow: hidden;

    position: relative;

    .main-header {
      
      
        border-bottom: 1px solid #ccc;
        box-shadow: 0 3px 5px 0 rgb(0 0 0 / 10%);

        .main-header-top {
      
      
            height: 50px;
            box-shadow: 0 3px 10px 0 rgb(0 0 0 / 6%);
            background: #fff;
            border-bottom: 1px solid rgba(0, 0, 0, .1);

            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 0 10px;

            .main-header-top-left {
      
      
                display: flex;
                align-items: center;

                .hamburger {
      
      
                    cursor: pointer;
                    padding: 8px;
                    margin: 5px;

                    i {
      
      
                        font-size: 1.2em;
                    }

                }
            }

            .main-header-top-right {
      
      
                display: flex;
                align-items: center;
                .avatar {
      
      
                    width: 40px;
                    height: 40px;
                    border-radius: 50%;
                }
                .gitee {
      
      
                    color: #c71d23;
                }
            }


        }

        
    }

    .main-body {
      
      
        position: absolute;
        top: 83px;
        left: 0;
        right: 0;
        bottom: 0;
    }

}

i.iconfont {
      
      
    font-size: 1.6em;
}

.mlr8 {
      
      
    margin-left: 8px;
    margin-right: 8px;
}
</style>

Create layout/Breadcrumb.vue

<template>
    <el-breadcrumb separator="/" stsyle="color: #303133;">
        <el-breadcrumb-item :to="{ path: '/' }">系统管理</el-breadcrumb-item>
        <el-breadcrumb-item><a href="/">用户管理</a></el-breadcrumb-item>
        <el-breadcrumb-item>添加用户</el-breadcrumb-item>
    </el-breadcrumb>
</template>

<script setup>

</script>

<style lang="scss">

</style>

Create layout/TagsView component

<template>
    <div class="main-header-tags-wrapper">
        <el-scrollbar>
            <div class="main-header-tags">
                <div class="tag-item">1</div>
                <div class="tag-item">2</div>
                <div class="tag-item">3</div>
                <div class="tag-item">4</div>
                <div class="tag-item">5</div>
                <div class="tag-item">6</div>
                <div class="tag-item">7</div>
                <div class="tag-item">8</div>
                <div class="tag-item">9</div>
            </div>
        </el-scrollbar>
    </div>
</template>

<script setup>

</script>

<style lang="scss">
.main-header-tags-wrapper {
      
      

padding: 0 10px;


.main-header-tags {
      
      
    height: 32px;

    display: flex;
    align-items: center;

    .tag-item {
      
      
        width: 160px;
        height: 26px;
        margin-right: 10px;

        border: 1px solid #ccc;

        background-color: #fff;

        flex-shrink: 0;

        display: flex;
        align-items: center;
        justify-content: center;
    }
}
}
</style>

Create layout/components/Demo.vue

<template>
    <div class="main-content-wrapper">
        <div class="content">
            <el-scrollbar>
                <el-timeline>
                    <el-timeline-item v-for="(activity, index) in activities" :key="index" :icon="activity.icon"
                        :type="activity.type" :color="activity.color" :size="activity.size" :hollow="activity.hollow"
                        :timestamp="activity.timestamp">
                        {
   
   { activity.content }}
                    </el-timeline-item>
                </el-timeline>
            </el-scrollbar>
        </div>
    </div>
</template>

<script setup>
import {
      
       Expand, Fold, MoreFilled } from '@element-plus/icons-vue'

const activities = [
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    }, {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
]
</script>

<style lang="scss"></style>

11. Menu

Build a static menu route

In this step, we will obtain the following effect
insert image description here

Configure home/user/role/menu routing

import {
    
     createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";


// 路由信息
const routes = [
    {
    
    
        path: "/login",
        name: "login",
        component:  () => import('@/views/login/index.vue'),
    },
    {
    
    
        path: '/',
        name: 'layout',
        redirect:'/home',
        component: ()=>import('@/layout/index.vue'),
        children: [
            {
    
    
                path: 'home',
                name: 'home',
                component: ()=>import('@/views/Home.vue'),
            },
            {
    
    
                path: 'user',
                name: 'user',
                component: ()=>import('@/views/sys/user.vue'),
            },
            {
    
    
                path: 'role',
                name: 'role',
                component: ()=>import('@/views/sys/role.vue'),
            },
            {
    
    
                path: 'menu',
                name: 'menu',
                component: ()=>import('@/views/sys/menu.vue'),
            }
        ]
    },
	// 匹配404页面
    {
    
    
        path:'/:pathMatch(.*)*',
        name: 'notFound',
        component: ()=>import('@/views/404/NotFound.vue')
    },
]

const router = createRouter({
    
    
    history: createWebHistory(),
    routes
});

router.beforeEach((to,from,next)=>{
    
    
    nprogress.start()
    next()
})

router.afterEach((to,from,next)=>{
    
    
    nprogress.done()
})

// 导出路由
export default router;

Create a sidebar menu with el-menu

  1. el-menu is a ul, and el-subm-menu and el-menu-item are both a li, where a div and a ul>li are nested in the li of el-sub-menu, and the div inside ( Use the title slot) will be displayed as a menu, and the ul>li inside will be used as a contraction menu
  2. When shrinking, an el-menu–collapse class name will be added to the ul generated by el-menu (that is, the outermost ul), which will hide the text of the span in the menu, so that it will only be displayed Icon up
<template>
    <div class="sider" :style="{ 'width': isExpand ? '220px' : '80px', transition: 'all 0.28s' }">

        <div class="sider-top">
            <h3 v-if="isExpand" class="site-title">PSCOOL管理系统</h3>
            <i v-else class="iconfont icon-graphcool site-icon"></i>
        </div>
        <div class="sider-body">
            <el-scrollbar>

                <el-menu 
                    :collapse="isCollapse" 
                    router 
                    collapse-transition text-color="#eee" 
                    :default-openeds="['/sys']" 
                    default-active="/home"
                    background-color="#294256" class="menu-bar">

                    <el-menu-item index="/home">
                        <i class="iconfont icon-home-line"></i>
                        <span>主页</span>
                    </el-menu-item>

                    <el-sub-menu index="/sys">

                        <template #title>
                            <i class="iconfont icon-shezhi"></i>
                            <span>系统管理</span>
                        </template>

                        <el-menu-item index="/user">
                            <i class="iconfont icon-yonghuguanli"></i>
                            <span>用户管理</span>
                        </el-menu-item>

                        <el-menu-item index="/role">
                            <i class="iconfont icon-jiaoseguanli"></i>
                            <span>角色管理</span>
                        </el-menu-item>

                        <el-menu-item index="/menu">
                            <i class="iconfont icon-icon_caidanguanli"></i>
                            <span>菜单管理</span>
                        </el-menu-item>

                    </el-sub-menu>

                    <el-sub-menu index="/test">

                        <template #title>
                            <i class="iconfont icon-graphcool"></i>
                            <span>多级菜单</span>
                        </template>

                        <el-menu-item index="/test-1">
                            <i class="iconfont icon-graphcool"></i>
                            <span>test-1</span>
                        </el-menu-item>

                        <el-sub-menu index="test-2" class="nested-sub-menu">

                            <template #title>
                                <i class="iconfont icon-graphcool"></i>
                                <span>test-2</span>
                            </template>

                            <el-menu-item index="/test-2-1">
                                <i class="iconfont icon-graphcool"></i>
                                <span>test-2-1</span>
                            </el-menu-item>
                            <el-menu-item index="/test-2-2">
                                <i class="iconfont icon-graphcool"></i>
                                <span>test-2-2</span>
                            </el-menu-item>

                        </el-sub-menu>

                    </el-sub-menu>

                </el-menu>
            </el-scrollbar>
        </div>
    </div>
</template>

<script setup>
import useLayout from '@/store/layout'
import {
      
       storeToRefs } from 'pinia'
import {
      
       computed, watch } from 'vue'

const layoutStore = useLayout()
const {
      
       isExpand } = storeToRefs(layoutStore) // 需要使用storeToRefs, 才能保持响应式

const isCollapse = computed({
      
      
    get() {
      
      
        return !isExpand.value
    }
})
watch(isExpand, (newVal, oldVal) => {
      
      
    // console.log('监听到变化');
})

</script>

<style lang="scss">
.sider {
      
      
    width: 220px;
    height: 100vh;
    background-color: #294256;
    position: relative;

    box-shadow: 2px 0 8px 0 rgba(0, 0, 0, 0.3);

    flex-shrink: 0;

    .sider-top {
      
      
        height: 50px;

        display: flex;
        align-items: center;
        justify-content: center;

        overflow: hidden;

        .site-title {
      
      
            white-space: nowrap;
            font-weight: bold;
            color: #fff;
        }

        .site-icon {
      
      
            font-size: 20px;
            color: #27ae60;
        }
    }

    .sider-body {
      
      
        position: absolute;
        top: 50px;
        left: 0;
        right: 0;
        bottom: 0;

        background-color: #294256;

        .menu-bar {
      
      
            .iconfont {
      
      
                margin-right: 10px;
            }
        }
    }

}

.el-menu {
      
      
    border-right: none; // 修复边缘白边
}

ul.el-menu--inline, .nested-sub-menu div  {
      
      
    background-color: #1f2d3d !important; // 二级菜单深暗色
}
</style>

Building views/Home.vue

Home.vue can be used as a template for other components to be placed in the route exit of the main-body of the Main.vue component, so that the vertical scroll bar will not appear on the right as a whole (as shown in the figure below), and other components can customize the layout.
insert image description here

<template>
    <div class="main-content-wrapper">
        <div class="content">
            <el-scrollbar>
                <el-timeline>
                    <el-timeline-item v-for="(activity, index) in activities" :key="index" :icon="activity.icon"
                        :type="activity.type" :color="activity.color" :size="activity.size" :hollow="activity.hollow"
                        :timestamp="activity.timestamp">
                        {
   
   { activity.content }}
                    </el-timeline-item>
                </el-timeline>
            </el-scrollbar>
        </div>
    </div>
</template>

<script setup>
import {
      
       Expand, Fold, MoreFilled } from '@element-plus/icons-vue'

const activities = [
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
    {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    }, {
      
      
        content: 'Custom icon',
        timestamp: '2018-04-12 20:46',
        size: 'large',
        type: 'primary',
        icon: MoreFilled,
    },
]
</script>

<style lang="scss">
.main-content-wrapper {
      
      
    overflow: auto;
    box-sizing: border-box;
    width: 100%;
    height: 100%;
    padding: 20px;
    background-clip: content-box;

    .content {
      
      
        width: 100%;
        height: 100%;
        overflow: auto;
        background-color: #fff;
        border-radius: 8px;

        padding: 10px 0 10px 10px;
        box-sizing: border-box;

        border: 1px solid red;
    }
}
</style>

Create views/404/NotFound.vue

Here is a simple return

<template>
    <div class="main-content-wrapper">
        <div>
            <h1>页面找丢了。。。</h1>
            <el-button type="primary" @click="goBack">返回</el-button>
        </div>
    </div>
</template>

<script setup>
    function goBack() {
      
      
        window.history.go(-1)
    }
</script>

<style lang="scss">
.main-content-wrapper {
      
      
    overflow: auto;
    box-sizing: border-box;
    width: 100%;
    height: 100%;
    padding: 20px;
    background-clip: content-box;

    display: flex;
    align-items: center;
    justify-content: center;
}
</style>

Implement dynamic routing menu

When different users log in, they need to be displayed on the sidebar according to the menu owned by the current user, and routes are dynamically added to the router. In other words, as soon as the user logs in, we should request the background to get all the menus the user has, assemble the sidebar menu, and dynamically add routes.

Adjust routing and menus

We need to do the following things, but before doing the following things, let's adjust our menu first, make sure it is available, and then access the background data.

  1. Need to get the data of the left menu bar, and then traverse it recursively
  2. Add the route to the router
Adjust routing
  • We noticed that if the route in vue starts with / directly, it will ignore the path of the parent route and directly match it, and if it does not start with /, it will splice the parent path to match. For convenience, just All start with /.
  • We regard all routes as sub-routes of layout, so we will add them directly under the route of layout later.
import {
    
     createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";


// 路由信息
const routes = [
    {
    
    
        path: "/login",
        name: "login",
        component:  () => import('@/views/login/index.vue'),
    },
    {
    
    
        path: '/',
        name: 'layout',
        component: ()=>import('@/layout/index.vue'),
        children: [
            {
    
    
                path: '/home', // 最好以/开头, 如果不以/开头,那么路由到这个组件就需要拼接上父路径
                name: 'home',
                component: ()=>import('@/views/Home.vue'),
            },
            {
    
    
                path: '/sys/user',
                name: 'user',
                component: ()=>import('@/views/sys/user.vue'),
            },
            {
    
    
                path: '/sys/role',
                name: 'role',
                component: ()=>import('@/views/sys/role.vue'),
            },
            {
    
    
                path: '/sys/menu',
                name: 'menu',
                component: ()=>import('@/views/sys/menu.vue'),
            },
            {
    
    
                path: '/test/test_1',
                name: 'test_1',
                component: ()=>import('@/views/test/test_1.vue'),
            },
            {
    
    
                path: '/test/test2/test_2_1',
                name: 'test_2_1',
                component: ()=>import('@/views/test/test2/test_2_1.vue'),
            },
            {
    
    
                path: '/test/test2/test_2_2',
                name: 'test_2_2',
                component: ()=>import('@/views/test/test2/test_2_2.vue'),
            },
        ]
    },
    {
    
    
        path:'/:pathMatch(.*)*',
        name: 'notFound',
        component: ()=>import('@/views/404/NotFound.vue')
    },
]

const router = createRouter({
    
    
    history: createWebHistory(),
    routes
});

router.beforeEach((to,from,next)=>{
    
    
    nprogress.start()
    next()
})

router.afterEach((to,from,next)=>{
    
    
    nprogress.done()
})

// 导出路由
export default router;
adjustment menu

Here we only need to write the path of index to the route. It should also be noted that if the user directly enters the path in the address bar to jump, we also need to highlight the corresponding menu, and we can just monitor the route.

<template>
    <div class="sider" :style="{ 'width': isExpand ? '220px' : '80px', transition: 'all 0.28s' }">

        <div class="sider-top">
            <h3 v-if="isExpand" class="site-title">PSCOOL管理系统</h3>
            <i v-else class="iconfont icon-graphcool site-icon"></i>
        </div>
        <div class="sider-body">
            <el-scrollbar>

                <el-menu 
                    :collapse="isCollapse" 
                    router 
                    collapse-transition text-color="#eee" 
                    :default-openeds="['/sys']" 
                    :default-active="activeMenu"
                    background-color="#294256" class="menu-bar">

                    <el-menu-item index="/home">
                        <i class="iconfont icon-home-line"></i>
                        <span>主页</span>
                    </el-menu-item>

                    <el-sub-menu index="/sys">

                        <template #title>
                            <i class="iconfont icon-shezhi"></i>
                            <span>系统管理</span>
                        </template>

                        <el-menu-item index="/sys/user">
                            <i class="iconfont icon-yonghuguanli"></i>
                            <span>用户管理</span>
                        </el-menu-item>

                        <el-menu-item index="/sys/role">
                            <i class="iconfont icon-jiaoseguanli"></i>
                            <span>角色管理</span>
                        </el-menu-item>

                        <el-menu-item index="/sys/menu">
                            <i class="iconfont icon-icon_caidanguanli"></i>
                            <span>菜单管理</span>
                        </el-menu-item>

                    </el-sub-menu>

                    <el-sub-menu index="/test">

                        <template #title>
                            <i class="iconfont icon-graphcool"></i>
                            <span>多级菜单</span>
                        </template>

                        <el-menu-item index="/test/test_1">
                            <i class="iconfont icon-graphcool"></i>
                            <span>test_1</span>
                        </el-menu-item>

                        <el-sub-menu index="/test/test2" class="nested-sub-menu">

                            <template #title>
                                <i class="iconfont icon-graphcool"></i>
                                <span>test_2</span>
                            </template>

                            <el-menu-item index="/test/test2/test_2_1">
                                <i class="iconfont icon-graphcool"></i>
                                <span>test_2_1</span>
                            </el-menu-item>
                            <el-menu-item index="/test/test2/test_2_2">
                                <i class="iconfont icon-graphcool"></i>
                                <span>test_2_2</span>
                            </el-menu-item>

                        </el-sub-menu>

                    </el-sub-menu>

                </el-menu>
            </el-scrollbar>
        </div>
    </div>
</template>

<script setup>
import {
      
       ref,reactive } from 'vue'
import useLayout from '@/store/layout'
import {
      
       storeToRefs } from 'pinia'
import {
      
       computed, watch } from 'vue'
import {
      
       useRouter,useRoute } from 'vue-router'

const layoutStore = useLayout()
const {
      
       isExpand } = storeToRefs(layoutStore) // 需要使用storeToRefs, 才能保持响应式

const isCollapse = computed({
      
      
    get() {
      
      
        return !isExpand.value
    }
})
watch(isExpand, (newVal, oldVal) => {
      
      
    // console.log('监听到变化');
})

const activeMenu = ref('/home') // 需要监听到当前的路由, 让它高亮, 因为用户有可能手动地址栏输入
const router = useRouter()
const route = useRoute()

watch(()=>route.fullPath, (newVal,oldVal)=>{
      
      
    console.log('监听到当前的路由', newVal);
    activeMenu.value = newVal;
}, {
      
      immediate:true})


</script>

<style lang="scss">
.sider {
      
      
    width: 220px;
    height: 100vh;
    background-color: #294256;
    position: relative;

    box-shadow: 2px 0 8px 0 rgba(0, 0, 0, 0.3);

    flex-shrink: 0;

    .sider-top {
      
      
        height: 50px;

        display: flex;
        align-items: center;
        justify-content: center;

        overflow: hidden;

        .site-title {
      
      
            white-space: nowrap;
            font-weight: bold;
            color: #fff;
        }

        .site-icon {
      
      
            font-size: 20px;
            color: #27ae60;
        }
    }

    .sider-body {
      
      
        position: absolute;
        top: 50px;
        left: 0;
        right: 0;
        bottom: 0;

        background-color: #294256;

        .menu-bar {
      
      
            .iconfont {
      
      
                margin-right: 10px;
                font-size: 1.4em;
            }
        
        }
    }

}

.el-menu {
      
      
    border-right: none; // 修复边缘白边
}

ul.el-menu--inline, .nested-sub-menu div  {
      
      
    background-color: #1f2d3d !important; // 二级菜单深暗色
}
</style>

Background menu and routing data return example

menu.json
{
    
    
  "errno": 0,
  "errmsg": "成功",
  "data": [
    {
    
    
      "id": 1,
      "parentId": 0,
      "title":"主页",
      "icon":"iconfont icon-home-line",
      "url":"/home",
      "menuType": "C",
      "component":"@/views/Home.vue"
    },
    {
    
    
      "id": 2,
      "parentId": 0,
      "title":"系统设置",
      "icon":"iconfont icon-shezhi",
      "url":"/sys",
      "menuType": "M",
      "component":"",
      "children": [
        {
    
    
          "id": 3,
          "parentId": 2,
          "title":"用户管理",
          "icon":"iconfont icon-yonghuguanli",
          "url":"/sys/user",
          "menuType": "C",
          "component":"@/views/sys/user.vue"
        },
        {
    
    
          "id": 4,
          "parentId": 2,
          "title":"角色管理",
          "icon":"iconfont icon-jiaoseguanli",
          "url":"/sys/role",
          "menuType":"C",
          "component":"@/views/sys/role.vue"
        },
        {
    
    
          "id": 5,
          "parentId": 2,
          "title":"菜单管理",
          "icon":"iconfont icon-icon_caidanguanli",
          "url":"/sys/menu",
          "menuType":"C",
          "component":"@/views/sys/menu.vue"
        }
      ]
    },
    {
    
    
      "id": 6,
      "parentId": 0,
      "title":"多级菜单",
      "icon":"iconfont icon-graphcool",
      "url":"/test",
      "component":"",
      "menuType":"M",
      "children": [
        {
    
    
          "id": 7,
          "parentId": 6,
          "title":"test_1",
          "icon":"iconfont icon-graphcool",
          "url":"/test/test_1",
          "menuType":"C",
          "component":"@/views/test/test_1.vue"
        },
        {
    
    
          "id": 8,
          "parentId": 2,
          "title":"test_2",
          "icon":"iconfont icon-graphcool",
          "url":"/test/test_2",
          "menuType":"M",
          "component":"",
          "children":[
            {
    
    
              "id": 9,
              "parentId": 8,
              "title":"test_2_1",
              "icon":"iconfont icon-graphcool",
              "menuType":"C",
              "url":"/test/test_2/test_2_1",
              "component":"@/views/test/test_2_1.vue"
            },
            {
    
    
              "id": 10,
              "parentId": 8,
              "title":"test_2_2",
              "icon":"iconfont icon-graphcool",
              "menuType":"C",
              "url":"/test/test_2/test_2_2",
              "component":"@/views/test/test_2_2.vue"
            }
          ]
        }

      ]
    }
  ]
}


router.json
{
    
    
  "errno": 0,
  "errmsg": "成功",
  "data": [
    {
    
    
      "path": "/home",
      "name": "home",
      "component": "@/views/Home.vue"
    },
    {
    
    
      "path": "/sys/user",
      "name": "user",
      "component": "@/views/sys/user.vue"
    },
    {
    
    
      "path": "/sys/role",
      "name": "role",
      "component": "@/views/sys/role.vue"
    },
    {
    
    
      "path": "/sys/menu",
      "name": "menu",
      "component": "@/views/sys/menu.vue"
    },
    {
    
    
      "path": "/test/test_1",
      "name": "test_1",
      "component": "@/views/test/test_1.vue"
    },
    {
    
    
      "path": "/test/test_2/test_2_1",
      "name": "test_2_1",
      "component": "@/views/test/test2/test_2_1.vue"
    },
    {
    
    
      "path": "/test/test_2/test_2_2",
      "name": "test_2_2",
      "component": "@/views/test/test2/test_2_2.vue"
    }
  ]
}

Modify loginApi.js
import request from '@/utils/request'

export function getCaptchaImage()  {
    
    
    return request({
    
    
        url: 'captchaImage',
    })
}

export function login(data)  {
    
    
    return request({
    
    
        method:'post',
        url: 'user/login',
        data
    })
}

export function getMenus()  {
    
     // 获取菜单
    return request({
    
    
        method:'get',
        url: 'test/getMenus'
    })
}

export function getRoutes()  {
    
     // 获取路由
    return request({
    
    
        method:'get',
        url: 'test/getRoutes'
    })
}
Modify request.js

Because the request header needs to be added to access the menu routing interface

import axios from 'axios'
import Messager from './messager';
import pinia from '@/store'
import useUser from '@/store/user'



const instance = axios.create({
    
    
    baseURL: 'http://127.0.0.1:8080/api',
    timeout: 10000
})

instance.interceptors.request.use((config)=>{
    
    
    // debugger
    let userStore = useUser()
    if(userStore.token) {
    
    
        console.log('userStore.token',userStore.token);
        config.headers['Authorization'] = userStore.token
    }
    return config;
})

instance.interceptors.response.use(response=>{
    
    
    if(response.data.errno == 0) {
    
    
        return Promise.resolve(response.data.data)
    } else {
    
    
        if(response.data.errno == 501) {
    
    
            Messager.error('请重新登录')
            window.location.href = '/login'
        } else {
    
    
            Messager.error(response.data.errmsg)
            return Promise.reject(new Error(response.data.errmsg))
        }
    }
})

export default instance
Modify router/index.js

Delete the originally configured static route, this part of the route is returned by the backend, and add front guard logic

import {
    
     createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";
import Messager from '@/utils/messager';

import useMenu from '@/store/menu'

import pinia from '@/store'
import useUser from '@/store/user'
const userStore = useUser(pinia) // 此处不能像在组件里使用userStore一样直接useUser()调用,而是要先引入pinia,再传入。参考: https://blog.csdn.net/qq_21473443/article/details/126405859
console.log('router->userStore',userStore);

const menuStore = useMenu(pinia)


// 路由信息
const routes = [
    {
    
    
        path: "/login",
        name: "login",
        component:  () => import('@/views/login/index.vue'),
    },
    {
    
    
        path: '/',
        name: 'layout',
        component: ()=>import('@/layout/index.vue'),
        children: []
    },
    {
    
    
        path:'/:pathMatch(.*)*',
        name: 'notFound',
        component: ()=>import('@/views/404/NotFound.vue')
    },
]

const router = createRouter({
    
    
    history: createWebHistory(),
    routes
});

router.beforeEach((to,from,next)=>{
    
    
    nprogress.start()
    // debugger
    let token = userStore.token
    if(!token) {
    
    
        if(to.path == '/login') {
    
    
            next()
        } else {
    
    
            next('/login')
        }
    } else {
    
    
        if(!menuStore.routesMenusLoaded) {
    
    
            menuStore.loadRoutesMenus().then(res=>{
    
    
                next()
            }).catch(err=>{
    
    
                // 加载出错,跳回到登录页去
                userStore.clearUserInfo()
                next('/login')
            })
        } else {
    
    
            if(to.path == '/login') {
    
    
                Messager.warn('你已登录!')
                next('/home')
            } else {
    
    
                next()
            }
        }
    }

})

router.afterEach((to,from,next)=>{
    
    
    nprogress.done()
})

// 导出路由
export default router;
Modify user.js
import {
    
     defineStore } from 'pinia'

import {
    
     login } from '@/api/loginApi'

function retrieveLocalToken() {
    
    
    console.log('read token'); 
    return localStorage.getItem('token') || '' 
}
function clearLocalToken() {
    
    
    return localStorage.clear('token')
}

export default defineStore('user',{
    
    
    state: () => {
    
    
        return {
    
    
            token: retrieveLocalToken() // 当刷新页面时, 这个会加载一次
        }
    },
    getters: {
    
    

    },
    actions: {
    
    
        doLogin(data) {
    
    
            return new Promise((resolve, reject) => {
    
    
                login(data).then(res=>{
    
    
                    this.token = res // 同样先存入到pinia中
                    localStorage.setItem('token', res)
                    resolve(data)
                }).catch(err=>{
    
    
                    reject(err)
                })
            })
        },
        clearUserInfo() {
    
    
            this.token = null
            clearLocalToken()
        }
    }
})
Create store/menu.js

Create menu.js to store the data returned by the background

import {
    
     defineStore } from 'pinia'
import {
    
    getMenus,getRoutes} from '@/api/loginApi'
import router from '@/router'

export default defineStore('menu', {
    
    
    state: ()=> {
    
    
        return {
    
    
            routesMenusLoaded: false, // 路由菜单是否已加载
            menus: [], // 菜单
            routes: [] // 路由
        }
    },
    getters: {
    
    

    },
    actions: {
    
    
       loadRoutesMenus() {
    
    
        return new Promise(async (resolve,reject)=>{
    
    
            try {
    
    
                let menus = await getMenus()
                let routes = await getRoutes()

                // 保存路由
                this.routes = routes

                // 保存菜单
                this.menus = menus
                
                // 动态加载路由
                routes.forEach(route=>{
    
    
                    router.addRoute(
                        'layout', 
                        {
    
    
                            path: route.path,
                            name: route.name,
                            component: ()=>import(route.component.replace('@',"../")) // 好像直接以@开头,会报错,所以这干脆替换成相对路径吧
                        }
                    )
                })

                console.log(router.getRoutes(),'finished');

                resolve()
            
            } catch (err) {
    
    
                reject(err)
            }

        })
       }
    }
})

Modify the menu bar component Sider.vue

<template>
    <div class="sider" :style="{ 'width': isExpand ? '220px' : '80px', transition: 'all 0.28s' }">

        <div class="sider-top">
            <h3 v-if="isExpand" class="site-title">PSCOOL管理系统</h3>
            <i v-else class="iconfont icon-graphcool site-icon"></i>
        </div>
        <div class="sider-body">
            <el-scrollbar>

                <el-menu 
                    :collapse="isCollapse" 
                    router 
                    collapse-transition text-color="#eee" 
                    :default-openeds="['/sys']" 
                    :default-active="activeMenu"
                    background-color="#294256" class="menu-bar">

                    <TreeMenu v-for="menuData,index in menuList" :menu="menuData" :key="index"></TreeMenu>

                </el-menu>
            </el-scrollbar>
        </div>
    </div>
</template>

<script setup>
import TreeMenu from './TreeMenu.vue'
import {
      
       ref,reactive } from 'vue'
import useLayout from '@/store/layout'
import {
      
       storeToRefs } from 'pinia'
import {
      
       computed, watch } from 'vue'
import {
      
       useRouter,useRoute } from 'vue-router'
import useMenu from '@/store/menu'

const layoutStore = useLayout()
const {
      
       isExpand } = storeToRefs(layoutStore) // 需要使用storeToRefs, 才能保持响应式

const isCollapse = computed({
      
      
    get() {
      
      
        return !isExpand.value
    }
})
watch(isExpand, (newVal, oldVal) => {
      
      
    // console.log('监听到变化');
})

const activeMenu = ref('/home') // 需要监听到当前的路由, 让它高亮, 因为用户有可能手动地址栏输入
const router = useRouter()
const route = useRoute()

watch(()=>route.fullPath, (newVal,oldVal)=>{
      
      
    console.log('监听到当前的路由', newVal);
    activeMenu.value = newVal;
}, {
      
      immediate:true})

const menuStore = useMenu()
const menuList = computed({
      
      
    get() {
      
      
        return menuStore.menus
    }
})


</script>

<style lang="scss">
.sider {
      
      
    width: 220px;
    height: 100vh;
    background-color: #294256;
    position: relative;

    box-shadow: 2px 0 8px 0 rgba(0, 0, 0, 0.3);

    flex-shrink: 0;

    .sider-top {
      
      
        height: 50px;

        display: flex;
        align-items: center;
        justify-content: center;

        overflow: hidden;

        .site-title {
      
      
            white-space: nowrap;
            font-weight: bold;
            color: #fff;
        }

        .site-icon {
      
      
            font-size: 20px;
            color: #27ae60;
        }
    }

    .sider-body {
      
      
        position: absolute;
        top: 50px;
        left: 0;
        right: 0;
        bottom: 0;

        background-color: #294256;

        .menu-bar {
      
      
            .iconfont {
      
      
                margin-right: 10px;
                font-size: 1.4em;
            }
        
        }
    }

}

.el-menu {
      
      
    border-right: none; // 修复边缘白边
}

ul.el-menu--inline, .nested-sub-menu div  {
      
      
    background-color: #1f2d3d !important; // 二级菜单深暗色
}
</style>

Create TreeMenu.vue recursive component

<template>
    <template v-if="!menu.children && menu.menuType == 'C'">
        <el-menu-item :index="menu.url">
            <i :class="menu.icon"></i>
            <span>{
   
   { menu.title }}</span>
        </el-menu-item>
    </template>
    <template v-if="menu.children && menu.menuType == 'M'">
        <el-sub-menu :index="menu.url" :class="{
     
     'nested-sub-menu': menu.parentId != 0}">
            <template #title>
                <i :class="menu.icon"></i>
                <span>{
   
   { menu.title }}</span>
            </template>
            <TreeMenu v-for="childMenu,index in menu.children" :menu="childMenu" :key="index"></TreeMenu>
        </el-sub-menu>
    </template>
</template>

<script setup>
defineProps({
      
      
    menu: {
      
      
        type: Object
    }
})
</script>

<style lang="scss"></style>

insert image description here

Solve the problem of refreshing the address bar

I made a mistake above. I put the 404 route as a static route and put it directly in the router, so that the 404 route is in the front. It is not an exact match, so when I refresh the page, I jump directly to the 404 page. , so change the route of 404 to after obtaining all the route data of the backend

Modify router/index.js
import {
    
     createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";
import Messager from '@/utils/messager';

import useMenu from '@/store/menu'

import pinia from '@/store'
import useUser from '@/store/user'
const userStore = useUser(pinia) // 此处不能像在组件里使用userStore一样直接useUser()调用,而是要先引入pinia,再传入。参考: https://blog.csdn.net/qq_21473443/article/details/126405859
console.log('router->userStore',userStore);

const menuStore = useMenu(pinia)


// 路由信息
const routes = [
    {
    
    
        path: "/login",
        name: "login",
        component:  () => import('@/views/login/index.vue'),
    },
    {
    
    
        path: '/',
        name: 'layout',
        component: ()=>import('@/layout/index.vue'),
        children: []
    }
]

const router = createRouter({
    
    
    history: createWebHistory(),
    routes
});

function existRoutePath(path) {
    
    
    let routes = router.getRoutes()
    let routePathArr = []
    routes.forEach((route) => {
    
    
        routePathArr.push(route.path)
    })
    return routePathArr.indexOf(path)
}

router.beforeEach((to,from,next)=>{
    
    
    nprogress.start()
    // console.log(router.getRoutes(),existRoutePath(to.path),'router hasRoute-3',to);
    // debugger
    let token = userStore.token
    if(!token) {
    
    
        if(to.path == '/login') {
    
    
            next()
        } else {
    
    
            next('/login')
        }
    } else {
    
    
        if(!menuStore.routesMenusLoaded) {
    
    
            // console.log(router.getRoutes(),existRoutePath(to.name),'router hasRoute-1',to);
            menuStore.loadRoutesMenus().then(res=>{
    
    
                // console.log(router.getRoutes(),existRoutePath(to.name),'router hasRoute-2',to);
                next({
    
    ...to})
            }).catch(err=>{
    
    
                // 加载出错,跳回到登录页去
                userStore.clearUserInfo()
                next('/login')
            })
        } else {
    
    
            if(to.path == '/login') {
    
    
                Messager.warn('你已登录!')
                next('/home')
            } else {
    
    
                // console.log(router.getRoutes(),existRoutePath(to.name),'router hasRoute-4');
                next()
            }
        }
    }

})

router.afterEach((to,from,next)=>{
    
    
    nprogress.done()
})

// 导出路由
export default router;
Modify menu.js
import {
    
     defineStore } from 'pinia'
import {
    
    getMenus,getRoutes} from '@/api/loginApi'
import router from '@/router'

export default defineStore('menu', {
    
    
    state: ()=> {
    
    
        return {
    
    
            routesMenusLoaded: false, // 路由菜单是否已加载
            menus: [], // 菜单
            routes: [] // 路由
        }
    },
    getters: {
    
    

    },
    actions: {
    
    
       loadRoutesMenus() {
    
    
        return new Promise(async (resolve,reject)=>{
    
    
            try {
    
    
                let menus = await getMenus()
                let routes = await getRoutes()

                // 保存路由
                this.routes = routes

                // 保存菜单
                this.menus = menus
                
                // 动态加载路由
                routes.forEach(route=>{
    
    
                    router.addRoute(
                        'layout', 
                        {
    
    
                            path: route.path,
                            name: route.name,
                            component: ()=>import(route.component.replace('@',"../")) // 好像直接以@开头,会报错,所以这干脆替换成相对路径吧
                        }
                    )
                })

                router.addRoute({
    
    
                    path:'/:pathMatch(.*)*',
                    name: 'notFound',
                    component: ()=>import('../views/404/NotFound.vue')
                })
                

                console.log(router.getRoutes(),'加载路由 finished');
                this.routesMenusLoaded = true

                resolve()
            
            } catch (err) {
    
    
                reject(err)
            }

        })
       }
    }
})

12. Full screen function

Install screenfull

npm i screenfull -S

use screenfull

<template>
	<div class="fullscreen mlr8" @click="toggleFullScreen">
	     <i :class="['iconfont', 'pointer', { 'icon-quanping_o': !isFullScreen, 'icon-quxiaoquanping_o': isFullScreen }]"></i>
	 </div>
</template>

<script>
	import {
      
       ref} from 'vue'
	const isFullScreen = ref(false)
	function toggleFullScreen() {
      
      
	    screenfull.toggle()
	    isFullScreen.value = !isFullScreen.value
	} 
</script>

13. Breadcrumbs

We need to display the breadcrumbs of the menu of the current route, first agree on the data, the name of the route is unique and corresponds to the name in the menu and is unique, so that when we switch to a certain route, we can recursively enter the menu according to the name Find all the parents it corresponds to.

data

{
    
    
  "errno": 0,
  "errmsg": "成功",
  "data": [
    {
    
    
      "path": "/home",
      "name": "home",
      "component": "@/views/Home.vue"
    },
    {
    
    
      "path": "/sys/user",
      "name": "user",
      "component": "@/views/sys/user.vue"
    },
    {
    
    
      "path": "/sys/role",
      "name": "role",
      "component": "@/views/sys/role.vue"
    },
    {
    
    
      "path": "/sys/menu",
      "name": "menu",
      "component": "@/views/sys/menu.vue"
    },
    {
    
    
      "path": "/test/test_1",
      "name": "test_1",
      "component": "@/views/test/test_1.vue"
    },
    {
    
    
      "path": "/test/test_2/test_2_1",
      "name": "test_2_1",
      "component": "@/views/test/test2/test_2_1.vue"
    },
    {
    
    
      "path": "/test/test_2/test_2_2",
      "name": "test_2_2",
      "component": "@/views/test/test2/test_2_2.vue"
    }
  ]
}

{
    
    
  "errno": 0,
  "errmsg": "成功",
  "data": [
    {
    
    
      "id": 1,
      "parentId": 0,
      "name": "home",
      "title":"主页",
      "icon":"iconfont icon-home-line",
      "url":"/home",
      "menuType": "C",
      "component":"@/views/Home.vue"
    },
    {
    
    
      "id": 2,
      "parentId": 0,
      "name": "sys",
      "title":"系统设置",
      "icon":"iconfont icon-shezhi",
      "url":"/sys",
      "menuType": "M",
      "component":"",
      "children": [
        {
    
    
          "id": 3,
          "parentId": 2,
          "name": "user",
          "title":"用户管理",
          "icon":"iconfont icon-yonghuguanli",
          "url":"/sys/user",
          "menuType": "C",
          "component":"@/views/sys/user.vue"
        },
        {
    
    
          "id": 4,
          "parentId": 2,
          "name": "role",
          "title":"角色管理",
          "icon":"iconfont icon-jiaoseguanli",
          "url":"/sys/role",
          "menuType":"C",
          "component":"@/views/sys/role.vue"
        },
        {
    
    
          "id": 5,
          "parentId": 2,
          "name": "menu",
          "title":"菜单管理",
          "icon":"iconfont icon-icon_caidanguanli",
          "url":"/sys/menu",
          "menuType":"C",
          "component":"@/views/sys/menu.vue"
        }
      ]
    },
    {
    
    
      "id": 6,
      "parentId": 0,
      "name": "test",
      "title":"多级菜单",
      "icon":"iconfont icon-graphcool",
      "url":"/test",
      "component":"",
      "menuType":"M",
      "children": [
        {
    
    
          "id": 7,
          "parentId": 6,
          "name": "test_1",
          "title":"test_1",
          "icon":"iconfont icon-graphcool",
          "url":"/test/test_1",
          "menuType":"C",
          "component":"@/views/test/test_1.vue"
        },
        {
    
    
          "id": 8,
          "parentId": 2,
          "name": "test_2",
          "title":"test_2",
          "icon":"iconfont icon-graphcool",
          "url":"/test/test_2",
          "menuType":"M",
          "component":"",
          "children":[
            {
    
    
              "id": 9,
              "parentId": 8,
              "name": "test_2_1",
              "title":"test_2_1",
              "icon":"iconfont icon-graphcool",
              "menuType":"C",
              "url":"/test/test_2/test_2_1",
              "component":"@/views/test/test_2_1.vue"
            },
            {
    
    
              "id": 10,
              "parentId": 8,
              "name": "test_2_2",
              "title":"test_2_2",
              "icon":"iconfont icon-graphcool",
              "menuType":"C",
              "url":"/test/test_2/test_2_2",
              "component":"@/views/test/test_2_2.vue"
            }
          ]
        }

      ]
    }
  ]
}

Modify menus.js

According to the menu returned by the background, recursively get the hierarchical menu titles corresponding to all routes, and put them into the meta of the route

import {
    
     defineStore } from 'pinia'
import {
    
    getMenus,getRoutes} from '@/api/loginApi'
import router from '@/router'
import {
    
     dropdownMenuProps } from 'element-plus'

function generateNameMap(menus) {
    
    
    const nameMap = {
    
    }
    menus.forEach(menu => {
    
    
        handleMenu(menu,nameMap,[])
    })
    return nameMap
}

function handleMenu(menu,nameMap,titleArr) {
    
    
    titleArr.push(menu.title)
    nameMap[menu.name] = JSON.parse(JSON.stringify(titleArr))
    if(menu.children && menu.children.length > 0) {
    
    
        menu.children.forEach(menu => {
    
    
            let newTitleArr = JSON.parse(JSON.stringify(titleArr))
            handleMenu(menu,nameMap,newTitleArr)
        })
    }
}
    
export default defineStore('menu', {
    
    
    state: ()=> {
    
    
        return {
    
    
            routesMenusLoaded: false, // 路由菜单是否已加载
            menus: [], // 菜单
            routes: [] // 路由
        }
    },
    getters: {
    
    

    },
    actions: {
    
    
       loadRoutesMenus() {
    
    
        return new Promise(async (resolve,reject)=>{
    
    
            try {
    
    
                let menus = await getMenus()
                let routes = await getRoutes()

                // 保存路由
                this.routes = routes

                // 保存菜单
                this.menus = menus
                // debugger
                const nameMap = generateNameMap(menus)
                
                // 动态加载路由
                routes.forEach(route=>{
    
    
                    router.addRoute(
                        'layout', 
                        {
    
    
                            path: route.path,
                            name: route.name,
                            meta: {
    
    
                                titleArr: nameMap[route.name]
                            },
                            // 好像直接以@开头,会报错,所以这干脆替换成相对路径吧
                            component: ()=>import(route.component.replace('@',"../")) 
                        }
                    )
                })

                router.addRoute({
    
    
                    path:'/:pathMatch(.*)*',
                    name: 'notFound',
                    component: ()=>import('../views/404/NotFound.vue')
                })
                

                console.log(router.getRoutes(),'加载路由 finished');
                this.routesMenusLoaded = true

                resolve()
            
            } catch (err) {
    
    
                reject(err)
            }

        })
       },

    }
})

Modify Breadcrumb.vue

Listen for routing changes and get cached breadcrumb data from the routing meta

<template>
    <el-breadcrumb separator="/" style="color: #303133;display: flex;white-space: nowrap;">
        <el-breadcrumb-item v-for="(title,index) in titleArr" :key="index">{
   
   { title }}</el-breadcrumb-item>
    </el-breadcrumb>
</template>

<script setup>
    import {
      
       ref,reactive,watch } from 'vue'
    import {
      
       useRoute } from 'vue-router'

    const route = useRoute()
    const titleArr = ref([])

    watch(()=>route, (newRoute,oldRoute)=>{
      
      
        console.log('路由更新了',newRoute);
        titleArr.value = newRoute.meta.titleArr
    },{
      
      immediate: true,deep:true})
   

</script>

<style lang="scss">

</style>

14. tagsView

This step is mainly to implement the tagsView function. The tagsView records the menus that the user has visited, and can close it as needed, but the tag of the home page should always be kept.
We store the tags in pinia. The tagsView component refers to the tags in pinia through the calculated property, and calls the method in pinia through the listening event trigger method
. Trigger the closing event of the i icon, and trigger the click event of the div, use @click.stop to bind

TagsView.vue

<template>
    <div class="main-header-tags-wrapper">
        <el-scrollbar>
            <div class="main-header-tags" id="main-header-tags">
                <div :class="['tag-item',{
     
     'active':tag.isActive}]" @click="selectSpecifiedTag(tag)" v-for="(tag,index) in tags" :key="index">
                    <span>{
   
   { tag.title }}</span>
                    <i class="close-ico iconfont icon-guanbi" v-show="tag.name != 'home'" @click.stop="closeTag(tag)"></i>
                </div>
            </div>
        </el-scrollbar>
    </div>
</template>

<script setup>
    import useTagsView from '@/store/tagsView'
    import {
      
       computed, watch } from 'vue'
    import {
      
       useRoute,useRouter } from 'vue-router'
    const tagsViewStore = useTagsView()

    const tags = computed({
      
      
        get() {
      
      
            return tagsViewStore.tags
        }
    })

    const route = useRoute()
    const router = useRouter()
    watch(()=>route, (newRoute,oldRoute)=>{
      
      
        tagsViewStore.doOnrouteChange(newRoute)
    },{
      
      immediate:true,deep:true})
    
    function selectSpecifiedTag(tag) {
      
      
        debugger
         tagsViewStore.selectSpecifiedTag(tag)
         router.push({
      
      name:tag.name})
    }

    function closeTag(tag) {
      
      
        // 关闭的是不是当前激活的tag, 如果是当前激活的tag的话,就选择最后一个tag;如果不是当前激活的tag的话,就关掉它就行了
        let isCurrTagActiveClose = tag.isActive
        tagsViewStore.closeSpecifiedTag(tag)
        if(isCurrTagActiveClose) {
      
      
            // 选择最后面的tag
            debugger
            console.log('选择最后面的tag', tagsViewStore.tags[tagsViewStore.tags.length - 1]);
            selectSpecifiedTag(tagsViewStore.tags[tagsViewStore.tags.length - 1])
        }
        
    }

    
    
</script>

<style lang="scss">
.main-header-tags-wrapper {
      
      

padding: 0 10px;


.main-header-tags {
      
      
    height: 32px;

    display: flex;
    align-items: center;

    .tag-item {
      
      
        height: 26px;
        padding: 0 20px;
        margin-right: 8px;
        font-size: 13px;
        cursor: pointer;

        color: #495060;

        border: 1px solid #ccc;

        background-color: #fff;

        flex-shrink: 0;

        display: flex;
        align-items: center;
        justify-content: center;

        position: relative;

        i.close-ico {
      
      
            font-size: 12px;
            position: absolute;
            right: 2px;
            top: 4.5px;
            transform: scale(0.6);
            cursor: pointer;
            padding: 3px;
            border-radius: 50%;
            &:hover {
      
      
                background: #b4bccc;
            }
        }

        &.active {
      
      
            background-color: #409eff;
            border: #409eff;
            color: #fff;
            &::before {
      
      
                content: '';
                position: absolute;
                width: 6px;
                height: 6px;
                background-color: #fff;
                border-radius: 50%;
                left: 8px;
                top: 10.5px;
            }
        }
    }
}
}
</style>

TagsView.js

import {
    
     defineStore } from 'pinia'

export default defineStore('tagsView', {
    
    
    state: ()=> {
    
    
        return {
    
    
            tags: [
                {
    
    
                    title: '主页',
                    name: 'home',
                    path: '/home',
                    isActive: false
                }
            ],
        }
    },
    getters: {
    
    

    },
    actions: {
    
    
        doOnrouteChange(route) {
    
    
            debugger
            console.log('doOnrouteChange->新路由', route.name);
            let currRouteName = route.name
            let tagNameArr = []
            let flag = false
            this.tags.forEach(tag=>{
    
    
                tag.isActive = false
                if(tag.name == currRouteName) {
    
    
                    flag = true
                    tag.isActive = true
                }
            })       
            if(!flag) {
    
    
                console.log('原先没有这个路由,现在添加tag', route.name);
                this.tags.push({
    
    
                    title: route.meta.title,
                    name: route.name,
                    path: route.path,
                    isActive: true
                })
            }     
        },
        closeSpecifiedTag(tag){
    
    
            debugger
            let index = -1;
            for(let i=0;i<this.tags.length;i++) {
    
    
                if(this.tags[i].name === tag.name) {
    
    
                    index = i
                    break
                }
            }
            if(index > -1) {
    
    
                this.tags.splice(index,1)
            }
        },
        selectSpecifiedTag(tag) {
    
    
            debugger
            this.tags.forEach(t=>{
    
    
                t.isActive = false
                if(t.name == tag.name) {
    
    
                    t.isActive = true
                }
            })  
        }
    }
})

insert image description here

15. The vue command controls the display of the permission button

Through the directive instruction method of vue, the button is only displayed when the user has the specified permission

Background returns permission data

{
    
    
  "errno": 0,
  "errmsg": "成功",
  "data": {
    
    
    "perms": [
      "user:list",
      "user:add",
      "user:remove",
      "role:list",
      "role:add",
      "role:remove"
    ]
  }
}

Create directive file perms.js

import useMenu from '@/store/menu'
import {
    
     toRaw } from '@vue/reactivity'


export default {
    
    
    hasPerms: {
    
    
        mounted(el,binding) {
    
    
            const menuStore = useMenu()
            let perms1 = menuStore.perms

            console.log(el,binding,perms1);

            let perms2 = toRaw(perms1)
            let perms3 = JSON.parse(JSON.stringify(perms1))
            console.log(perms2.perms);
            console.log(perms3.perms);

            // 有任一指定的权限, 即可显示指定的dom, 否则移除
            if(!perms2.perms.some(p=>binding.value.includes(p))) {
    
    
                el.parentNode.removeChild(el)
            }
        },
    }
}

Register the directive in main.js

import {
    
     createApp } from 'vue'
import './style.css'
import '@/assets/iconfont/iconfont.css'
import App from './App.vue'

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'

import * as ElementPlusIconsVue from '@element-plus/icons-vue'

import Messager from '@/utils/messager'


import router from '@/router'
import pinia from '@/store'

import perm from '@/directive/perm'


const app = createApp(App)

app.config.globalProperties.Messager = Messager

app.use(pinia)
app.use(router)
app.use(ElementPlus)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    
    
    app.component(key, component)
}

// 注册指令
for(let key in perm) {
    
    
    app.directive(key, perm[key])
}

app.mount('#app')

Add interface in loginApi.js

// ...省略
export function getPerms()  {
    
    
    return request({
    
    
        method:'get',
        url: 'test/getPerms'
    })
}

Modify store/menu.js

Add the part that gets permission

import {
    
     defineStore } from 'pinia'
import {
    
    getMenus,getRoutes,getPerms} from '@/api/loginApi'
import router from '@/router'
import {
    
     dropdownMenuProps } from 'element-plus'

function generateNameMap(menus) {
    
    
    const nameMap = {
    
    }
    menus.forEach(menu => {
    
    
        handleMenu(menu,nameMap,[])
    })
    return nameMap
}

function handleMenu(menu,nameMap,titleArr) {
    
    
    titleArr.push(menu.title)
    nameMap[menu.name] = JSON.parse(JSON.stringify(titleArr))
    if(menu.children && menu.children.length > 0) {
    
    
        menu.children.forEach(menu => {
    
    
            let newTitleArr = JSON.parse(JSON.stringify(titleArr))
            handleMenu(menu,nameMap,newTitleArr)
        })
    }
}
    
export default defineStore('menu', {
    
    
    state: ()=> {
    
    
        return {
    
    
            routesMenusLoaded: false, // 路由菜单是否已加载
            menus: [], // 菜单
            routes: [], // 路由,
            perms: [], // 权限
        }
    },
    getters: {
    
    

    },
    actions: {
    
    
       loadRoutesMenus() {
    
    
        return new Promise(async (resolve,reject)=>{
    
    
            try {
    
    
                let menus = await getMenus()
                let routes = await getRoutes()
                let perms = await getPerms()

                // 保存路由
                this.routes = routes

                // 保存菜单
                this.menus = menus

                // 保存权限
                this.perms = perms
                
                // debugger
                const nameMap = generateNameMap(menus)
                
                // 动态加载路由
                routes.forEach(route=>{
    
    
                    router.addRoute(
                        'layout', 
                        {
    
    
                            path: route.path,
                            name: route.name,
                            meta: {
    
    
                                titleArr: nameMap[route.name],
                                title: nameMap[route.name][nameMap[route.name].length-1]
                            },
                            // 好像直接以@开头,会报错,所以这干脆替换成相对路径吧
                            component: ()=>import(route.component.replace('@',"../")) 
                        }
                    )
                })

                router.addRoute({
    
    
                    path:'/:pathMatch(.*)*',
                    name: 'notFound',
                    component: ()=>import('../views/404/NotFound.vue')
                })
                

                console.log(router.getRoutes(),'加载路由 finished');
                this.routesMenusLoaded = true

                resolve()
            
            } catch (err) {
    
    
                reject(err)
            }

        })
       },

    }
})

Used in User.vue

<template>
    用户管理
    <el-button type="danger" v-hasPerms="['user:list']">查看</el-button>
    <el-button type="primary" v-hasPerms="['user:add']">添加</el-button>
    <el-button type="primary" v-hasPerms="['user:update']">修改</el-button>
    <el-button type="primary" v-hasPerms="['user:remove']">删除</el-button>
</template>

<script setup>

</script>

<style lang="scss">

</style>

The following effect
insert image description here

16. Add transition effects

The switching process of breadcrumbs and routing looks particularly blunt, we need to add transition effects to them, like the following
insert image description here

Breadcrumb transition effect

  • The following transition effect code is a direct copy of the official website, because it is traversed by v-for, so transition-group is used.
  • It should also be noted that the key bound to the element must be unique (the index cannot be used as the key), which is a requirement of Vue, otherwise there will be no animation effect
<template>
    <el-breadcrumb separator="/" style="color: #303133;display: flex;white-space: nowrap;">
      <transition-group name="list">
        <el-breadcrumb-item v-for="title in titleArr" :key="title">{
   
   { title }}</el-breadcrumb-item>
      </transition-group>
    </el-breadcrumb>
</template>

<script setup>
    import {
      
       ref,reactive,watch } from 'vue'
    import {
      
       useRoute } from 'vue-router'

    const route = useRoute()
    const titleArr = ref([])

    watch(()=>route, (newRoute,oldRoute)=>{
      
      
        // console.log('路由更新了',newRoute);
        titleArr.value = newRoute.meta.titleArr
    },{
      
      immediate: true,deep:true})
   

</script>

<style lang="scss">
/* breadcrumb transition */
.list-move, /* 对移动中的元素应用的过渡 */
.list-enter-active,
.list-leave-active {
      
      
  transition: all 0.5s ease;
}

.list-enter-from,
.list-leave-to {
      
      
  opacity: 0;
  transform: translateX(30px);
}

/* 确保将离开的元素从布局流中删除
  以便能够正确地计算移动的动画。 */
.list-leave-active {
      
      
  position: absolute;
}
</style>

routing transition effects

Modify Main.vue, only paste the modified code below, and omit the other parts

<template>
	<div class="main-header">
		...
	</div>
	<div class="main-body">
	    <router-view v-slot:="{Component,route}">
	        <transition name="slide-fade" mode="out-in">
	            <component :is="Component" :key="route.path"/>
	        </transition>
	    </router-view>
	</div>
</template>

<style lang="scss" scoped>

	/*
	  进入和离开动画可以使用不同
	  持续时间和速度曲线。
	*/
	.slide-fade-enter-active {
      
      
	  transition: all 0.5s ease-out;
	}
	
	.slide-fade-leave-active {
      
      
	  transition: all 0.5s cubic-bezier(1, 0.5, 0.8, 1);
	}
	
	.slide-fade-enter-from,
	.slide-fade-leave-to {
      
      
	  transform: translateX(20px);
	  opacity: 0;
	}

...
</style>

17. Change to use import.meta.glob to import routes

Reference:
vue3 + vite dynamic import routing
import.meta.glob batch import files

Modify store/menu.js

import {
    
     defineStore } from 'pinia'
import {
    
    getMenus,getRoutes,getPerms} from '@/api/loginApi'
import router from '@/router'
import {
    
     dropdownMenuProps } from 'element-plus'

function generateNameMap(menus) {
    
    
    const nameMap = {
    
    }
    menus.forEach(menu => {
    
    
        handleMenu(menu,nameMap,[])
    })
    return nameMap
}

function handleMenu(menu,nameMap,titleArr) {
    
    
    titleArr.push(menu.title)
    nameMap[menu.name] = JSON.parse(JSON.stringify(titleArr))
    if(menu.children && menu.children.length > 0) {
    
    
        menu.children.forEach(menu => {
    
    
            let newTitleArr = JSON.parse(JSON.stringify(titleArr))
            handleMenu(menu,nameMap,newTitleArr)
        })
    }
}
    
export default defineStore('menu', {
    
    
    state: ()=> {
    
    
        return {
    
    
            routesMenusLoaded: false, // 路由菜单是否已加载
            menus: [], // 菜单
            routes: [], // 路由,
            perms: [], // 权限
        }
    },
    getters: {
    
    

    },
    actions: {
    
    
       loadRoutesMenus() {
    
    
        return new Promise(async (resolve,reject)=>{
    
    
            try {
    
    
                let menus = await getMenus()
                let routes = await getRoutes()
                let perms = await getPerms()

                // 保存路由
                this.routes = routes

                // 保存菜单
                this.menus = menus

                // 保存权限
                this.perms = perms

                // debugger
                const nameMap = generateNameMap(menus)

                //定义一个函数,引入所有views下.vue文件 
                const modules = import.meta.glob(`../views/**/*.vue`);
                console.log(modules,'modules');
                /* 
                ../views/404/NotFound.vue: () => import("/src/views/404/NotFound.vue")
                ../views/Home.vue: () => import("/src/views/Home.vue")
                ../views/login/index.vue: () => import("/src/views/login/index.vue?t=1679996129333")
                ../views/sys/Menu.vue: () => import("/src/views/sys/Menu.vue")
                ../views/sys/Role.vue: () => import("/src/views/sys/Role.vue")
                ../views/sys/User.vue: () => import("/src/views/sys/User.vue")
                ../views/test/test_1.vue: () => import("/src/views/test/test_1.vue")
                modules
                */
                
                // 动态加载路由
                routes.forEach(route=>{
    
    
                    // debugger
                    // console.log(`../views/${route.component.substring(8)}`);
                    // console.log(`../views/${route.component.substring(8)}`,modules[`../views/${route.component.substring(8)}`]);
                    router.addRoute(
                        'layout', 
                        {
    
    
                            path: route.path,
                            name: route.name,
                            meta: {
    
    
                                titleArr: nameMap[route.name],
                                title: nameMap[route.name][nameMap[route.name].length-1]
                            },
                            // 好像直接以@开头,会报错,所以这干脆替换成相对路径吧
                            // component: ()=>import(/* @vite-ignore */route.component.replace('@',"../")) 
                            
                            component: modules[`../views/${
      
      route.component.substring(8)}`]
                            // @/views/Home.vue -> ../views/Home.vue
                        }
                    )
                })

                router.addRoute({
    
    
                    path:'/:pathMatch(.*)*',
                    name: 'notFound',
                    component: ()=>import('../views/404/NotFound.vue')
                })
                

                console.log(router.getRoutes(),'加载路由 finished');
                this.routesMenusLoaded = true

                resolve()
            
            } catch (err) {
    
    
                reject(err)
            }

        })
       },

    }
})

Modify store/menu.js again

Since all of the above are possible, there is no reason to use @, so modify it again as follows:

import {
    
     defineStore } from 'pinia'
import {
    
    getMenus,getRoutes,getPerms} from '@/api/loginApi'
import router from '@/router'
import {
    
     dropdownMenuProps } from 'element-plus'

function generateNameMap(menus) {
    
    
    const nameMap = {
    
    }
    menus.forEach(menu => {
    
    
        handleMenu(menu,nameMap,[])
    })
    return nameMap
}

function handleMenu(menu,nameMap,titleArr) {
    
    
    titleArr.push(menu.title)
    nameMap[menu.name] = JSON.parse(JSON.stringify(titleArr))
    if(menu.children && menu.children.length > 0) {
    
    
        menu.children.forEach(menu => {
    
    
            let newTitleArr = JSON.parse(JSON.stringify(titleArr))
            handleMenu(menu,nameMap,newTitleArr)
        })
    }
}
    
export default defineStore('menu', {
    
    
    state: ()=> {
    
    
        return {
    
    
            routesMenusLoaded: false, // 路由菜单是否已加载
            menus: [], // 菜单
            routes: [], // 路由,
            perms: [], // 权限
        }
    },
    getters: {
    
    

    },
    actions: {
    
    
       loadRoutesMenus() {
    
    
        return new Promise(async (resolve,reject)=>{
    
    
            try {
    
    
                let menus = await getMenus()
                let routes = await getRoutes()
                let perms = await getPerms()

                // 保存路由
                this.routes = routes

                // 保存菜单
                this.menus = menus

                // 保存权限
                this.perms = perms

                // debugger
                const nameMap = generateNameMap(menus)

                //定义一个函数,引入所有views下.vue文件 
                const modules = import.meta.glob(`@/views/**/*.vue`);
                console.log(modules,'modules');
                /* 
                /src/views/404/NotFound.vue: () => import("/src/views/404/NotFound.vue")
                /src/views/Home.vue: () => import("/src/views/Home.vue")
                /src/views/login/index.vue: () => import("/src/views/login/index.vue?t=1679996129333")
                /src/views/sys/Menu.vue: () => import("/src/views/sys/Menu.vue")
                /src/views/sys/Role.vue: () => import("/src/views/sys/Role.vue")
                /src/views/sys/User.vue: () => import("/src/views/sys/User.vue")
                /src/views/test/test_1.vue: () => import("/src/views/test/test_1.vue")
                modules
                */
                
                // 动态加载路由
                routes.forEach(route=>{
    
    
                    // debugger
                    console.log(route.component);
                    console.log(route.component, modules[route.component]);
                    router.addRoute(
                        'layout', 
                        {
    
    
                            path: route.path,
                            name: route.name,
                            meta: {
    
    
                                titleArr: nameMap[route.name],
                                title: nameMap[route.name][nameMap[route.name].length-1]
                            },
                            // 好像直接以@开头,会报错,所以这干脆替换成相对路径吧
                            // component: ()=>import(/* @vite-ignore */route.component.replace('@',"../")) 
                            
                            // 改成使用import.meta.glob动态导入
                                                        component: modules[route.component.replace('@','/src')]

                        }
                    )
                })

                router.addRoute({
    
    
                    path:'/:pathMatch(.*)*',
                    name: 'notFound',
                    component: ()=>import('../views/404/NotFound.vue')
                })
                

                console.log(router.getRoutes(),'加载路由 finished');
                this.routesMenusLoaded = true

                resolve()
            
            } catch (err) {
    
    
                reject(err)
            }

        })
       },

    }
})

Guess you like

Origin blog.csdn.net/qq_16992475/article/details/129591665