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
- 2. Uso de enrutamiento
- 3. Usa elemento-plus
- 4. Usa pinia
- 5. Usa axios
- 6. Usando nprogress
- 7. Introducir iconfont
- 8. Encapsular ELMessage
- 9. Función de inicio de sesión
- 10. Diseño de página de fondo
- 11. Menú
-
- Crear una ruta de menú estática
- Implementar menú de enrutamiento dinámico
- 12 Función de pantalla completa
- 13. Pan rallado
- 14. etiquetasVer
- 15. El comando vue controla la visualización del botón de permiso
- 16. Agrega efectos de transición
- 17. Cambiar para usar import.meta.glob para importar rutas
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
<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. .
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
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
- 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
- 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.
<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.
- Necesita obtener los datos de la barra de menú izquierda y luego recorrerlos recursivamente
- 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>
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
}
})
}
}
})
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
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
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)
}
})
},
}
})