Sistema de gestión de fondo Vue3

Puede consultar lo siguiente más adelante: serie vue (3): le enseña cómo crear una plantilla básica de fondo de administración de vue3

El siguiente código de dirección de la casa rural del proyecto

Directorio de artículos

1. Inicializar el proyecto front-end

Inicializar el proyecto

Puede consultar: sitio web oficial de vite https://vitejs.cn/guide/#scaffolding-your-first-vite-project

npm init vite@latest mushan-vue3-admin

npm install

npm run dev

Agregar efecto de carga

En index.html, donde la identificación es la aplicación, escriba

<!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>

Configurar 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. Uso de enrutamiento

instalar enrutador

npm i vue-router@4 -S

configurar el enrutamiento

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;

Configurar @alias y saltar

ruta de instalación

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

En el mismo directorio que vite.config.js

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

Usar enrutamiento en 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. Usa elemento-plus

Instalar elment-plus

npm install element-plus --save

Usa elment-plus en 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. Usa pinia

Puede consultar: uso de pinia en Vue3 (edición de colección)

instalar pinia

npm install pinia --save

configurar pinia

Crear tienda/index.js

import {
    
     createPinia } from 'pinia'

const pinia = createPinia()

export default pinia

Crear tienda/contador.js

import {
    
     defineStore } from 'pinia'

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

    },
    actions: {
    
    
        
    }
})

Introducido en 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')

utilizado en el componente

<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. Usa axios

Puede consultar: Tutorial de configuración de Vue3 usando axios

instalar axios

npm install axios --save

Escribir solicitud.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

Escribir interfaz de solicitud de API

import request from '@/utils/request'

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

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

Usar axios en el componente

<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. Usando nprogress

instalar nprogreso

npm i nprogress -S

Paquete 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

Usar nprogress en el enrutamiento

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. Introducir iconfont

icono de descarga fuente

Descargue recursos relacionados con iconfont localmente y agréguelos al directorio assets/iconfont

Introducido en main.js

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

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

import App from './App.vue'

8. Encapsular ELMessage

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

9. Función de inicio de sesión

Configurar la ruta de inicio de sesión

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;

iniciar sesión.vue

inserte la descripción de la imagen aquí

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

tienda/usuario.js

Almacene el token obtenido al iniciar sesión en 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. Diseño de página de fondo

Después de un inicio de sesión exitoso, saltará a la página de inicio. El diseño de la página de inicio es más o menos el siguiente. Primero puede consultar vue3 + elment-plus para realizar el diseño de página estática del diseño de fondo y luego dividirlo en diferentes componentes. El intercambio de datos de diferentes componentes se gestiona a través de la tienda pinia. .

inserte la descripción de la imagen aquí

Configure la ruta después de un inicio de sesión exitoso

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;

dividir componentes

Crear diseño/index.vue

El componente de diseño presenta los componentes Sider y Main


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

Crear tienda/layout.js

Almacene los datos compartidos del componente en pinia

import {
    
     defineStore } from 'pinia'

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

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

Crear diseño/componentes/Sider.vue

isExpand son los datos almacenados en 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>

Crear diseño/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>

Crear diseño/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>

Crear diseño/Componente TagsView

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

Crear diseño/componentes/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. Menú

Crear una ruta de menú estática

En este paso obtendremos el siguiente efecto
inserte la descripción de la imagen aquí

Configurar el enrutamiento de inicio/usuario/rol/menú

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;

Crear un menú de barra lateral con el-menu

  1. el-menu es un ul, y el-subm-menu y el-menu-item son ambos un li, donde un div y un ul>li están anidados en el li de el-sub-menu, y el div dentro ( Use la ranura del título) se mostrará como un menú, y el interior de ul>li se utilizará como un menú de contracción
  2. Al reducir, se agregará un nombre de clase el-menu–collapse a la ul generada por el-menu (es decir, la ul más externa), que ocultará el texto del lapso en el menú, para que solo se muestre Icono arriba
<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>

Vistas del edificio/Home.vue

Home.vue se puede utilizar como plantilla para colocar otros componentes en la salida de la ruta del cuerpo principal del componente Main.vue, de modo que la barra de desplazamiento vertical no aparezca a la derecha como un todo (como se muestra en la figura a continuación), y otros componentes pueden personalizar el diseño.
inserte la descripción de la imagen aquí

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

Crear vistas/404/NotFound.vue

Aquí hay un retorno simple

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

Implementar menú de enrutamiento dinámico

Cuando diferentes usuarios inician sesión, deben mostrarse en la barra lateral de acuerdo con el menú propiedad del usuario actual, y las rutas se agregan dinámicamente al enrutador. En otras palabras, tan pronto como el usuario inicie sesión, debemos solicitar el fondo para obtener todos los menús que tiene el usuario, armar el menú de la barra lateral y agregar rutas dinámicamente.

Ajustar enrutamiento y menús

Necesitamos hacer lo siguiente, pero antes de hacer lo siguiente, primero ajustemos nuestro menú, asegurémonos de que esté disponible y luego accedamos a los datos de fondo.

  1. Necesita obtener los datos de la barra de menú izquierda y luego recorrerlos recursivamente
  2. Agregar la ruta al enrutador
Ajustar enrutamiento
  • Nos dimos cuenta de que si la ruta en vue comienza con / directamente, ignorará la ruta de la ruta principal y la emparejará directamente, y si no comienza con /, empalmará la ruta principal para que coincida. Empezar con /.
  • Consideramos todas las rutas como subrutas de diseño, por lo que las agregaremos directamente debajo de la ruta de diseño más adelante.
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;
menú de ajuste

Aquí solo necesitamos escribir la ruta del índice a la ruta. También se debe tener en cuenta que si el usuario ingresa directamente la ruta en la barra de direcciones para saltar, también debemos resaltar el menú correspondiente y solo podemos monitorear la ruta.

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

Menú de fondo y ejemplo de devolución de datos de enrutamiento

menú.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"
            }
          ]
        }

      ]
    }
  ]
}


enrutador.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"
    }
  ]
}

Modificar 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'
    })
}
Modificar request.js

Porque es necesario agregar el encabezado de la solicitud para acceder a la interfaz de enrutamiento del menú

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
Modificar enrutador/index.js

Elimine la ruta estática configurada originalmente, esta parte de la ruta es devuelta por el backend y agregue la lógica de protección frontal

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;
Modificar usuario.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()
        }
    }
})
Crear tienda/menu.js

Crear menu.js para almacenar los datos devueltos por el fondo

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

        })
       }
    }
})

Modificar el componente de la barra de menús 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>

Crear componente recursivo TreeMenu.vue

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

inserte la descripción de la imagen aquí

Resuelve el problema de actualizar la barra de direcciones

Cometí un error arriba, puse la ruta 404 como una ruta estática y la puse directamente en el enrutador, para que la ruta 404 esté al frente, no es una coincidencia exacta, así que cuando actualizo la página, salto directamente a la página 404. , así que cambie la ruta de 404 a después de obtener todos los datos de ruta del backend

Modificar enrutador/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;
Modificar 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 Función de pantalla completa

Instalar pantalla completa

npm i screenfull -S

usar pantalla completa

<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. Pan rallado

Necesitamos mostrar las migas de pan del menú de la ruta actual, primero acuerde los datos, el nombre de la ruta es único y corresponde al nombre en el menú y es único, de modo que cuando cambiamos a una ruta determinada, puede ingresar recursivamente al menú según el nombre Buscar todos los padres a los que corresponde.

datos

{
    
    
  "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"
            }
          ]
        }

      ]
    }
  ]
}

Modificar menús.js

De acuerdo con el menú devuelto por el fondo, obtenga recursivamente los títulos de menú jerárquicos correspondientes a todas las rutas y colóquelos en el meta de la ruta.

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

        })
       },

    }
})

Modificar Breadcrumb.vue

Escuche los cambios de enrutamiento y obtenga datos de migas de pan almacenados en caché del meta de enrutamiento

<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. etiquetasVer

Este paso es principalmente para implementar la función tagsView. TagsView registra los menús que el usuario ha visitado y puede cerrarlo según sea necesario, pero siempre se debe mantener la etiqueta de la página de inicio.
Almacenamos las etiquetas en pinia. El componente tagsView hace referencia a las etiquetas en pinia a través de la propiedad calculada y llama al método en pinia a través del método de activación del evento de escucha. Activa el evento de cierre
del icono i y activa el evento de clic del div, use @click.stop para enlazar

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

inserte la descripción de la imagen aquí

15. El comando vue controla la visualización del botón de permiso

A través del método de instrucciones directivas de vue, el botón solo se muestra cuando el usuario tiene el permiso especificado

Antecedentes devuelve datos de permisos

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

Crear archivo de directiva 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)
            }
        },
    }
}

Registrar la directiva en 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')

Agregar interfaz en loginApi.js

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

Modificar tienda/menu.js

Agregar la parte que obtiene permiso

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

        })
       },

    }
})

Usado en 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>

El siguiente efecto
inserte la descripción de la imagen aquí

16. Agrega efectos de transición

El proceso de cambio de migas de pan y enrutamiento parece particularmente contundente, necesitamos agregarles efectos de transición, como el siguiente
inserte la descripción de la imagen aquí

Efecto de transición de migas de pan

  • El siguiente código de efecto de transición es una copia directa del sitio web oficial, porque está atravesado por v-for, por lo que se usa el grupo de transición.
  • También se debe tener en cuenta que la clave vinculada al elemento debe ser única (el índice no se puede usar como clave), lo cual es un requisito de Vue; de ​​lo contrario, no habrá efecto de animación.
<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>

efectos de transición de enrutamiento

Modifique Main.vue, solo pegue el código modificado a continuación y omita las otras partes

<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. Cambiar para usar import.meta.glob para importar rutas

Referencia:
vue3 + vite enrutamiento de importación dinámica
import.meta.glob archivos de importación por lotes

Modificar tienda/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)
            }

        })
       },

    }
})

Modificar store/menu.js de nuevo

Dado que todo lo anterior es posible, no hay razón para usar @, así que modifíquelo nuevamente de la siguiente manera:

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

        })
       },

    }
})

Supongo que te gusta

Origin blog.csdn.net/qq_16992475/article/details/129591665
Recomendado
Clasificación