You can refer to the following later: vue series (3) - teach you how to build a vue3 management background basic template
The following code project gitee address
Article directory
-
- 1. Initialize the front-end project
- 2. Using routing
- 3. Use elment-plus
- 4. Use pinia
- 5. Use axios
- 6. Using nprogress
- 7. Introduce iconfont
- 8. Encapsulate ELMessage
- 9. Login function
- 10. Background page layout
- 11. Menu
- 12. Full screen function
- 13. Breadcrumbs
- 14. tagsView
- 15. The vue command controls the display of the permission button
- 16. Add transition effects
- 17. Change to use import.meta.glob to import routes
1. Initialize the front-end project
Initialize the project
You can refer to: vite official website https://vitejs.cn/guide/#scaffolding-your-first-vite-project
npm init vite@latest mushan-vue3-admin
npm install
npm run dev
Add loading effect
In index.html, where the id is app, write
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
<style>
body {
padding: 0px;
margin: 0px;
}
.loading {
display: flex;
height: 100vh;
width: 100vw;
background: #92b1d7;
justify-content: center;
align-items: center;
}
.loading .content {
position: relative;
display: flex;
justify-content: space-around;
align-items: center;
margin: 15px;
border-radius: 4px;
padding: 10px;
}
.circle-3 {
width: 60px;
height: 60px;
border-radius: 50%;
display: inline-block;
position: relative;
border: 3px solid;
border-color: #fff #fff transparent transparent;
animation: rotation 1s linear infinite;
}
.circle-3::after,
.circle-3::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
border-radius: 50%;
border: 3px solid;
animation: rotation-back 0.5s linear infinite;
}
.circle-3::after {
border-color: transparent #f6b352 #f6b352 transparent;
width: 52px;
height: 52px;
}
.circle-3::before {
border-color: transparent transparent #fff #fff;
width: 44px;
height: 44px;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes rotation-back {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-360deg);
}
}
</style>
</head>
<body>
<div id="app">
<div class="loading">
<div class="content">
<div class="circle-3"></div>
</div>
</div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
Configure vite.config.js
import {
defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
hmr: true,
port: 5174,
},
resolve: {
alias: {
'@':path.resolve(__dirname,'./src')
}
}
})
2. Using routing
install router
npm i vue-router@4 -S
configure routing
import {
createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";
// 路由信息
const routes = [
{
path: '/',
name: 'home',
component: ()=>import('@/views/index.vue')
},
{
path: "/login",
name: "login",
component: () => import('@/views/login/index.vue'),
}
]
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach((to,from,next)=>{
nprogress.start()
next()
})
router.afterEach((to,from,next)=>{
nprogress.done()
})
// 导出路由
export default router;
Configure @alias and jump
installation path
npm i path
vite.config.js
import {
defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@':path.resolve(__dirname,'./src')
}
}
})
jsconfig.json
In the same directory as vite.config.js
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
],
}
},
"exclude": [
"node_modules",
"dist"
],
"include": [
"src/**/*"
]
}
Use routing in main.js
import {
createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from '@/router'
const app = createApp(App)
app.mount('#app')
app.use(router)
3. Use elment-plus
install elment-plus
npm install element-plus --save
Use elment-plus in main.js
import {
createApp } from 'vue'
import './style.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from '@/router'
const app = createApp(App)
app.mount('#app')
app.use(router)
app.use(ElementPlus)
4. Use pinia
You can refer to: pinia use in Vue3 (collection edition)
install pinia
npm install pinia --save
configure pinia
Create store/index.js
import {
createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
Create store/counter.js
import {
defineStore } from 'pinia'
export const useCounter = defineStore('counter',{
state: () => ({
count:99
}),
getters: {
},
actions: {
}
})
Introduced in main.js
import {
createApp } from 'vue'
import './style.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from '@/router'
import pinia from '@/store'
const app = createApp(App)
app.use(router)
app.use(pinia)
app.use(ElementPlus)
app.mount('#app')
used in the component
<template>
{
{ counterStore.count }}
<el-button @click="visitStore">你好</el-button>
</template>
<script setup>
import {
useCounter} from '@/store/counter'
const counterStore = useCounter()
function visitStore() {
console.log(counterStore.count);
}
</script>
<style lang="scss">
</style>
5. Use axios
You can refer to: Vue3 configuration tutorial using axios
install axios
npm install axios --save
Write request.js
import axios from 'axios'
import Messager from './messager'; // 在下面封装了
const instance = axios.create({
baseURL: 'http://127.0.0.1:8080/api',
timeout: 10000
})
instance.interceptors.request.use((config)=>{
return config;
})
instance.interceptors.response.use(response=>{
if(response.data.errno == 0) {
return Promise.resolve(response.data.data)
} else {
if(response.data.errno == 501) {
Messager.error('请重新登录')
window.location.href = '/login'
} else {
Messager.error(response.data.errmsg)
return Promise.reject(new Error(response.data.errmsg))
}
}
})
export default instance
Write API request interface
import request from '@/utils/request'
export function getCaptchaImage() {
return request({
url: 'captchaImage',
})
}
export function login(data) {
return request({
method:'post',
url: 'user/login',
data
})
}
Use axios in the component
<template>
<el-button @click="refreshCaptchaImage">验证码</el-button>
</template>
<script setup>
import {
getCaptchaImage} from '@/api/loginApi'
async function refreshCaptchaImage() {
let result = await getCaptchaImage()
console.log(result);
}
</script>
<style lang="scss">
</style>
6. Using nprogress
install nprogress
npm i nprogress -S
Package nprogress.js
import Nprogress from 'nprogress'
import 'nprogress/nprogress.css'
const nprogress = Nprogress.configure({
easing: 'ease', // 动画方式
speed: 1000, // 递增进度条的速度
showSpinner: false, // 是否显示加载ico
trickleSpeed: 200, // 自动递增间隔
minimum: 0.3, // 更改启动时使用的最小百分比
parent: 'body', //指定进度条的父容器
})
export default nprogress
Use nprogress in routing
import {
createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";
// 路由信息
const routes = [
{
path: '/',
name: 'home',
component: ()=>import('@/views/index.vue')
},
{
path: "/login",
name: "login",
component: () => import('@/views/login/index.vue'),
}
]
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach((to,from,next)=>{
nprogress.start()
next()
})
router.afterEach((to,from,next)=>{
nprogress.done()
})
// 导出路由
export default router;
7. Introduce iconfont
download iconfont
Download iconfont- related resources locally and add them to the assets/iconfont directory
Introduced in main.js
import {
createApp } from 'vue'
import './style.css'
import '@/assets/iconfont/iconfont.css' // 引入iconfont的css文件
import App from './App.vue'
8. Encapsulate ELMessage
import {
ElMessage } from "element-plus";
const Messager = {
ok(msg){
ElMessage.success(msg)
},
error(msg) {
ElMessage.error(msg)
}
}
export default Messager
9. Login function
Configure the login route
import {
createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";
// 路由信息
const routes = [
{
path: '/',
name: 'home',
component: ()=>import('@/views/index.vue')
},
{
path: "/login",
name: "login",
component: () => import('@/views/login/index.vue'),
}
]
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach((to,from,next)=>{
nprogress.start()
next()
})
router.afterEach((to,from,next)=>{
nprogress.done()
})
// 导出路由
export default router;
login.vue
<template>
<div class="login-page">
<div class="login-container">
<h1 class="login-title">登录</h1>
<el-form ref="loginFormRef" :model="loginFormData" :rules="loginFormRules" class="login-form">
<el-form-item prop="username">
<el-input v-model="loginFormData.username" prop="username">
<template #prefix>
<i class="iconfont icon-yonghu"></i>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginFormData.password">
<template #prefix>
<i class="iconfont icon-mima"></i>
</template>
</el-input>
</el-form-item>
<el-form-item prop="code">
<div class="login-code">
<el-input v-model="loginFormData.code" prop="password">
<template #prefix>
<i class="iconfont icon-yanzhengma"></i>
</template>
</el-input>
<div class="code-img">
<img :src="codeImg" @click="getCodeImg">
</div>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" style="width:100%;" @click="submitLoginForm">登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup>
import {
getCaptchaImage} from '@/api/loginApi'
import useUser from '@/store/user'
import {
ref, reactive,getCurrentInstance, onMounted } from 'vue'
import {
useRouter } from 'vue-router'
const {
proxy } = getCurrentInstance()
const userStore = useUser()
const router = useRouter()
const codeImg = ref('')
const loginFormData = reactive({
username: 'admin',
password: '123456',
uuid: '',
code: ''
})
const loginFormRules = {
username: [
{
required:true,message: '用户名不能为空',trigger: 'blur'}
],
password: [
{
required:true,message: '密码不能为空',trigger: 'blur'}
],
code: [
{
required:true,message: '验证码不能为空',trigger: 'blur'}
],
}
const loginFormRef = ref(null)
function submitLoginForm() {
loginFormRef.value.validate(async(valid,fields)=>{
if(!valid) {
proxy.Messager.error('请填写完整')
return
}
console.log(userStore);
let result = await userStore.doLogin(loginFormData)
router.replace('/')
})
}
function getCodeImg() {
getCaptchaImage().then(res=>{
codeImg.value = "data:image/gif;base64," + res.img
loginFormData.uuid = res.uuid
})
}
onMounted(()=>{
getCodeImg()
})
</script>
<style lang="scss" scoped>
.iconfont {
font-size: 16px;
}
.login-page {
height: 100vh;
background-image: url(@/assets/bg.jpg);
background-position: center;
background-size: cover;
display: flex;
justify-content: center;
align-items: center;
.login-container {
width: 350px;
padding: 20px;
background: rgba(255, 255, 255, 1);
border-radius: 5px;
.login-title {
font-size: 26px;
text-align: center;
margin-bottom: 15px;
}
.login-code {
display: flex;
.code-img {
height: 34px;
width: 180px;
margin-left: 10px;
border-radius: 5px;
cursor: pointer;
background-color: pink;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
transform: scale(1.2);
}
}
}
}
}
</style>
store/user.js
Store the token obtained by login into localStorage
import {
defineStore } from 'pinia'
import {
login } from '@/api/loginApi'
function retrieveLocalToken() {
return localStorage.getItem('token') || ''
}
export default defineStore('user',{
state: () => {
return {
token: retrieveLocalToken() // 当刷新页面时, 这个会加载一次
}
},
getters: {
},
actions: {
doLogin(data) {
return new Promise((resolve, reject) => {
login(data).then(res=>{
this.token = res // 同样先存入到pinia中
localStorage.setItem('token', res)
console.log('login',res);
resolve(data)
}).catch(err=>{
reject(err)
})
})
}
}
})
api/loginApi.js
import request from '@/utils/request'
export function getCaptchaImage() {
return request({
url: 'captchaImage',
})
}
export function login(data) {
return request({
method:'post',
url: 'user/login',
data
})
}
10. Background page layout
After successful login, it will jump to the homepage. The layout of the homepage is roughly as follows. You can first refer to vue3 + elment-plus to realize the static page layout of the background layout , and then divide it into different components. The data sharing of different components is managed through the pinia store. .
Configure the route after successful login
import {
createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";
// 路由信息
const routes = [
{
path: "/login",
name: "login",
component: () => import('@/views/login/index.vue'),
},
{
path: '/',
name: 'home',
component: ()=>import('@/layout/index.vue')
},
]
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach((to,from,next)=>{
nprogress.start()
next()
})
router.afterEach((to,from,next)=>{
nprogress.done()
})
// 导出路由
export default router;
split components
Create layout/index.vue
Layout component introduces Sider and Main components
<template>
<div class="layout">
<Sider/>
<Main></Main>
</div>
</template>
<script setup>
import Sider from './components/Sider.vue';
import Main from './components/Main.vue';
import {
ref, reactive } from 'vue'
</script>
<style lang="scss" scoped>
.layout {
display: flex;
}
</style>
Create store/layout.js
Store the component's shared data into pinia
import {
defineStore } from 'pinia'
export default defineStore('layout', {
state: ()=> {
return {
isExpand: true, // 侧边栏是否展开
}
},
getters: {
},
actions: {
// 切换侧边栏
toggleSider() {
console.log('切换侧边栏', this.isExpand);
this.isExpand = !this.isExpand
}
}
})
Create layout/components/Sider.vue
isExpand is the data stored in pinia
<template>
<div class="sider" :style="{ 'width': isExpand ? '220px' : '80px', transition: 'all 0.28s' }">
<div class="sider-top">
<h3 v-if="isExpand" class="site-title">PSCOOL管理系统</h3>
<i v-else class="iconfont icon-graphcool site-icon"></i>
</div>
<div class="sider-body">
<el-scrollbar>
<ul>
<li class="li-item">1</li>
<li class="li-item">2</li>
<li class="li-item">3</li>
<li class="li-item">4</li>
<li class="li-item">5</li>
<li class="li-item">6</li>
<li class="li-item">7</li>
<li class="li-item">8</li>
<li class="li-item">9</li>
<li class="li-item">9</li>
<li class="li-item">9</li>
<li class="li-item">9</li>
<li class="li-item">9</li>
</ul>
</el-scrollbar>
</div>
</div>
</template>
<script setup>
import useLayout from '@/store/layout'
import {
storeToRefs } from 'pinia'
const layoutStore = useLayout()
const {
isExpand } = storeToRefs(layoutStore) // 需要使用storeToRefs, 才能保持响应式
</script>
<style lang="scss">
.sider {
width: 220px;
height: 100vh;
background-color: #294256;
position: relative;
box-shadow: 2px 0 8px 0 rgba(0, 0, 0, 0.3);
flex-shrink: 0;
.sider-top {
height: 50px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.site-title {
white-space: nowrap;
font-weight: bold;
color: #fff;
}
.site-icon {
font-size: 20px;
color: #27ae60;
}
}
.sider-body {
position: absolute;
top: 50px;
left: 0;
right: 0;
bottom: 0;
background-color: #294256;
.li-item {
height: 50px;
margin: 10px;
background-color: #294256;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
}
}
</style>
Create layout/Main.vue
<template>
<div class="main">
<div class="main-header">
<div class="main-header-top">
<div class="main-header-top-left">
<div class="hamburger" @click="layoutStore.toggleSider">
<i :class="['iconfont', { 'icon-shousuocaidan': isExpand }, { 'icon-shousuocaidan-copy': !isExpand }]"></i>
</div>
<Breadcrumb />
</div>
<div class="main-header-top-right">
<div class="gitee mlr8 pointer">
<i class="iconfont icon-gitee"></i>
</div>
<div class="fullscreen mlr8">
<i :class="['iconfont', 'pointer', { 'icon-quanping_o': !isFullScreen, 'icon-quxiaoquanping_o': isFullScreen }]"></i>
</div>
<div class="theme-mode mlr8">
<el-switch inline-prompt :active-icon="Check" :inactive-icon="Close" />
</div>
<div class="avatar-box mlr8 pointer">
<el-dropdown>
<span class="el-dropdown-link">
<img class="avatar" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" alt="">
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item divided>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<TagsView/>
</div>
<div class="main-body">
<Demo/>
<!-- <router-view></router-view> -->
</div>
</div>
</template>
<script setup>
import Breadcrumb from './Breadcrumb.vue'
import useLayout from '@/store/layout'
import Demo from './Demo.vue'
import {
storeToRefs } from 'pinia'
import {
ref, reactive } from 'vue'
import TagsView from './TagsView.vue'
const isFullScreen = ref(false)
const layoutStore = useLayout()
const {
isExpand } = storeToRefs(layoutStore)
</script>
<style lang="scss">
.main {
flex: 1;
overflow: hidden;
position: relative;
.main-header {
border-bottom: 1px solid #ccc;
box-shadow: 0 3px 5px 0 rgb(0 0 0 / 10%);
.main-header-top {
height: 50px;
box-shadow: 0 3px 10px 0 rgb(0 0 0 / 6%);
background: #fff;
border-bottom: 1px solid rgba(0, 0, 0, .1);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
.main-header-top-left {
display: flex;
align-items: center;
.hamburger {
cursor: pointer;
padding: 8px;
margin: 5px;
i {
font-size: 1.2em;
}
}
}
.main-header-top-right {
display: flex;
align-items: center;
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
}
.gitee {
color: #c71d23;
}
}
}
}
.main-body {
position: absolute;
top: 83px;
left: 0;
right: 0;
bottom: 0;
}
}
i.iconfont {
font-size: 1.6em;
}
.mlr8 {
margin-left: 8px;
margin-right: 8px;
}
</style>
Create layout/Breadcrumb.vue
<template>
<el-breadcrumb separator="/" stsyle="color: #303133;">
<el-breadcrumb-item :to="{ path: '/' }">系统管理</el-breadcrumb-item>
<el-breadcrumb-item><a href="/">用户管理</a></el-breadcrumb-item>
<el-breadcrumb-item>添加用户</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup>
</script>
<style lang="scss">
</style>
Create layout/TagsView component
<template>
<div class="main-header-tags-wrapper">
<el-scrollbar>
<div class="main-header-tags">
<div class="tag-item">1</div>
<div class="tag-item">2</div>
<div class="tag-item">3</div>
<div class="tag-item">4</div>
<div class="tag-item">5</div>
<div class="tag-item">6</div>
<div class="tag-item">7</div>
<div class="tag-item">8</div>
<div class="tag-item">9</div>
</div>
</el-scrollbar>
</div>
</template>
<script setup>
</script>
<style lang="scss">
.main-header-tags-wrapper {
padding: 0 10px;
.main-header-tags {
height: 32px;
display: flex;
align-items: center;
.tag-item {
width: 160px;
height: 26px;
margin-right: 10px;
border: 1px solid #ccc;
background-color: #fff;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
}
}
</style>
Create layout/components/Demo.vue
<template>
<div class="main-content-wrapper">
<div class="content">
<el-scrollbar>
<el-timeline>
<el-timeline-item v-for="(activity, index) in activities" :key="index" :icon="activity.icon"
:type="activity.type" :color="activity.color" :size="activity.size" :hollow="activity.hollow"
:timestamp="activity.timestamp">
{
{ activity.content }}
</el-timeline-item>
</el-timeline>
</el-scrollbar>
</div>
</div>
</template>
<script setup>
import {
Expand, Fold, MoreFilled } from '@element-plus/icons-vue'
const activities = [
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
}, {
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
]
</script>
<style lang="scss"></style>
11. Menu
Build a static menu route
In this step, we will obtain the following effect
Configure home/user/role/menu routing
import {
createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";
// 路由信息
const routes = [
{
path: "/login",
name: "login",
component: () => import('@/views/login/index.vue'),
},
{
path: '/',
name: 'layout',
redirect:'/home',
component: ()=>import('@/layout/index.vue'),
children: [
{
path: 'home',
name: 'home',
component: ()=>import('@/views/Home.vue'),
},
{
path: 'user',
name: 'user',
component: ()=>import('@/views/sys/user.vue'),
},
{
path: 'role',
name: 'role',
component: ()=>import('@/views/sys/role.vue'),
},
{
path: 'menu',
name: 'menu',
component: ()=>import('@/views/sys/menu.vue'),
}
]
},
// 匹配404页面
{
path:'/:pathMatch(.*)*',
name: 'notFound',
component: ()=>import('@/views/404/NotFound.vue')
},
]
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach((to,from,next)=>{
nprogress.start()
next()
})
router.afterEach((to,from,next)=>{
nprogress.done()
})
// 导出路由
export default router;
Create a sidebar menu with el-menu
- el-menu is a ul, and el-subm-menu and el-menu-item are both a li, where a div and a ul>li are nested in the li of el-sub-menu, and the div inside ( Use the title slot) will be displayed as a menu, and the ul>li inside will be used as a contraction menu
- When shrinking, an el-menu–collapse class name will be added to the ul generated by el-menu (that is, the outermost ul), which will hide the text of the span in the menu, so that it will only be displayed Icon up
<template>
<div class="sider" :style="{ 'width': isExpand ? '220px' : '80px', transition: 'all 0.28s' }">
<div class="sider-top">
<h3 v-if="isExpand" class="site-title">PSCOOL管理系统</h3>
<i v-else class="iconfont icon-graphcool site-icon"></i>
</div>
<div class="sider-body">
<el-scrollbar>
<el-menu
:collapse="isCollapse"
router
collapse-transition text-color="#eee"
:default-openeds="['/sys']"
default-active="/home"
background-color="#294256" class="menu-bar">
<el-menu-item index="/home">
<i class="iconfont icon-home-line"></i>
<span>主页</span>
</el-menu-item>
<el-sub-menu index="/sys">
<template #title>
<i class="iconfont icon-shezhi"></i>
<span>系统管理</span>
</template>
<el-menu-item index="/user">
<i class="iconfont icon-yonghuguanli"></i>
<span>用户管理</span>
</el-menu-item>
<el-menu-item index="/role">
<i class="iconfont icon-jiaoseguanli"></i>
<span>角色管理</span>
</el-menu-item>
<el-menu-item index="/menu">
<i class="iconfont icon-icon_caidanguanli"></i>
<span>菜单管理</span>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="/test">
<template #title>
<i class="iconfont icon-graphcool"></i>
<span>多级菜单</span>
</template>
<el-menu-item index="/test-1">
<i class="iconfont icon-graphcool"></i>
<span>test-1</span>
</el-menu-item>
<el-sub-menu index="test-2" class="nested-sub-menu">
<template #title>
<i class="iconfont icon-graphcool"></i>
<span>test-2</span>
</template>
<el-menu-item index="/test-2-1">
<i class="iconfont icon-graphcool"></i>
<span>test-2-1</span>
</el-menu-item>
<el-menu-item index="/test-2-2">
<i class="iconfont icon-graphcool"></i>
<span>test-2-2</span>
</el-menu-item>
</el-sub-menu>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</div>
</div>
</template>
<script setup>
import useLayout from '@/store/layout'
import {
storeToRefs } from 'pinia'
import {
computed, watch } from 'vue'
const layoutStore = useLayout()
const {
isExpand } = storeToRefs(layoutStore) // 需要使用storeToRefs, 才能保持响应式
const isCollapse = computed({
get() {
return !isExpand.value
}
})
watch(isExpand, (newVal, oldVal) => {
// console.log('监听到变化');
})
</script>
<style lang="scss">
.sider {
width: 220px;
height: 100vh;
background-color: #294256;
position: relative;
box-shadow: 2px 0 8px 0 rgba(0, 0, 0, 0.3);
flex-shrink: 0;
.sider-top {
height: 50px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.site-title {
white-space: nowrap;
font-weight: bold;
color: #fff;
}
.site-icon {
font-size: 20px;
color: #27ae60;
}
}
.sider-body {
position: absolute;
top: 50px;
left: 0;
right: 0;
bottom: 0;
background-color: #294256;
.menu-bar {
.iconfont {
margin-right: 10px;
}
}
}
}
.el-menu {
border-right: none; // 修复边缘白边
}
ul.el-menu--inline, .nested-sub-menu div {
background-color: #1f2d3d !important; // 二级菜单深暗色
}
</style>
Building views/Home.vue
Home.vue can be used as a template for other components to be placed in the route exit of the main-body of the Main.vue component, so that the vertical scroll bar will not appear on the right as a whole (as shown in the figure below), and other components can customize the layout.
<template>
<div class="main-content-wrapper">
<div class="content">
<el-scrollbar>
<el-timeline>
<el-timeline-item v-for="(activity, index) in activities" :key="index" :icon="activity.icon"
:type="activity.type" :color="activity.color" :size="activity.size" :hollow="activity.hollow"
:timestamp="activity.timestamp">
{
{ activity.content }}
</el-timeline-item>
</el-timeline>
</el-scrollbar>
</div>
</div>
</template>
<script setup>
import {
Expand, Fold, MoreFilled } from '@element-plus/icons-vue'
const activities = [
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
{
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
}, {
content: 'Custom icon',
timestamp: '2018-04-12 20:46',
size: 'large',
type: 'primary',
icon: MoreFilled,
},
]
</script>
<style lang="scss">
.main-content-wrapper {
overflow: auto;
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 20px;
background-clip: content-box;
.content {
width: 100%;
height: 100%;
overflow: auto;
background-color: #fff;
border-radius: 8px;
padding: 10px 0 10px 10px;
box-sizing: border-box;
border: 1px solid red;
}
}
</style>
Create views/404/NotFound.vue
Here is a simple return
<template>
<div class="main-content-wrapper">
<div>
<h1>页面找丢了。。。</h1>
<el-button type="primary" @click="goBack">返回</el-button>
</div>
</div>
</template>
<script setup>
function goBack() {
window.history.go(-1)
}
</script>
<style lang="scss">
.main-content-wrapper {
overflow: auto;
box-sizing: border-box;
width: 100%;
height: 100%;
padding: 20px;
background-clip: content-box;
display: flex;
align-items: center;
justify-content: center;
}
</style>
Implement dynamic routing menu
When different users log in, they need to be displayed on the sidebar according to the menu owned by the current user, and routes are dynamically added to the router. In other words, as soon as the user logs in, we should request the background to get all the menus the user has, assemble the sidebar menu, and dynamically add routes.
Adjust routing and menus
We need to do the following things, but before doing the following things, let's adjust our menu first, make sure it is available, and then access the background data.
- Need to get the data of the left menu bar, and then traverse it recursively
- Add the route to the router
Adjust routing
- We noticed that if the route in vue starts with / directly, it will ignore the path of the parent route and directly match it, and if it does not start with /, it will splice the parent path to match. For convenience, just All start with /.
- We regard all routes as sub-routes of layout, so we will add them directly under the route of layout later.
import {
createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";
// 路由信息
const routes = [
{
path: "/login",
name: "login",
component: () => import('@/views/login/index.vue'),
},
{
path: '/',
name: 'layout',
component: ()=>import('@/layout/index.vue'),
children: [
{
path: '/home', // 最好以/开头, 如果不以/开头,那么路由到这个组件就需要拼接上父路径
name: 'home',
component: ()=>import('@/views/Home.vue'),
},
{
path: '/sys/user',
name: 'user',
component: ()=>import('@/views/sys/user.vue'),
},
{
path: '/sys/role',
name: 'role',
component: ()=>import('@/views/sys/role.vue'),
},
{
path: '/sys/menu',
name: 'menu',
component: ()=>import('@/views/sys/menu.vue'),
},
{
path: '/test/test_1',
name: 'test_1',
component: ()=>import('@/views/test/test_1.vue'),
},
{
path: '/test/test2/test_2_1',
name: 'test_2_1',
component: ()=>import('@/views/test/test2/test_2_1.vue'),
},
{
path: '/test/test2/test_2_2',
name: 'test_2_2',
component: ()=>import('@/views/test/test2/test_2_2.vue'),
},
]
},
{
path:'/:pathMatch(.*)*',
name: 'notFound',
component: ()=>import('@/views/404/NotFound.vue')
},
]
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach((to,from,next)=>{
nprogress.start()
next()
})
router.afterEach((to,from,next)=>{
nprogress.done()
})
// 导出路由
export default router;
adjustment menu
Here we only need to write the path of index to the route. It should also be noted that if the user directly enters the path in the address bar to jump, we also need to highlight the corresponding menu, and we can just monitor the route.
<template>
<div class="sider" :style="{ 'width': isExpand ? '220px' : '80px', transition: 'all 0.28s' }">
<div class="sider-top">
<h3 v-if="isExpand" class="site-title">PSCOOL管理系统</h3>
<i v-else class="iconfont icon-graphcool site-icon"></i>
</div>
<div class="sider-body">
<el-scrollbar>
<el-menu
:collapse="isCollapse"
router
collapse-transition text-color="#eee"
:default-openeds="['/sys']"
:default-active="activeMenu"
background-color="#294256" class="menu-bar">
<el-menu-item index="/home">
<i class="iconfont icon-home-line"></i>
<span>主页</span>
</el-menu-item>
<el-sub-menu index="/sys">
<template #title>
<i class="iconfont icon-shezhi"></i>
<span>系统管理</span>
</template>
<el-menu-item index="/sys/user">
<i class="iconfont icon-yonghuguanli"></i>
<span>用户管理</span>
</el-menu-item>
<el-menu-item index="/sys/role">
<i class="iconfont icon-jiaoseguanli"></i>
<span>角色管理</span>
</el-menu-item>
<el-menu-item index="/sys/menu">
<i class="iconfont icon-icon_caidanguanli"></i>
<span>菜单管理</span>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="/test">
<template #title>
<i class="iconfont icon-graphcool"></i>
<span>多级菜单</span>
</template>
<el-menu-item index="/test/test_1">
<i class="iconfont icon-graphcool"></i>
<span>test_1</span>
</el-menu-item>
<el-sub-menu index="/test/test2" class="nested-sub-menu">
<template #title>
<i class="iconfont icon-graphcool"></i>
<span>test_2</span>
</template>
<el-menu-item index="/test/test2/test_2_1">
<i class="iconfont icon-graphcool"></i>
<span>test_2_1</span>
</el-menu-item>
<el-menu-item index="/test/test2/test_2_2">
<i class="iconfont icon-graphcool"></i>
<span>test_2_2</span>
</el-menu-item>
</el-sub-menu>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</div>
</div>
</template>
<script setup>
import {
ref,reactive } from 'vue'
import useLayout from '@/store/layout'
import {
storeToRefs } from 'pinia'
import {
computed, watch } from 'vue'
import {
useRouter,useRoute } from 'vue-router'
const layoutStore = useLayout()
const {
isExpand } = storeToRefs(layoutStore) // 需要使用storeToRefs, 才能保持响应式
const isCollapse = computed({
get() {
return !isExpand.value
}
})
watch(isExpand, (newVal, oldVal) => {
// console.log('监听到变化');
})
const activeMenu = ref('/home') // 需要监听到当前的路由, 让它高亮, 因为用户有可能手动地址栏输入
const router = useRouter()
const route = useRoute()
watch(()=>route.fullPath, (newVal,oldVal)=>{
console.log('监听到当前的路由', newVal);
activeMenu.value = newVal;
}, {
immediate:true})
</script>
<style lang="scss">
.sider {
width: 220px;
height: 100vh;
background-color: #294256;
position: relative;
box-shadow: 2px 0 8px 0 rgba(0, 0, 0, 0.3);
flex-shrink: 0;
.sider-top {
height: 50px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.site-title {
white-space: nowrap;
font-weight: bold;
color: #fff;
}
.site-icon {
font-size: 20px;
color: #27ae60;
}
}
.sider-body {
position: absolute;
top: 50px;
left: 0;
right: 0;
bottom: 0;
background-color: #294256;
.menu-bar {
.iconfont {
margin-right: 10px;
font-size: 1.4em;
}
}
}
}
.el-menu {
border-right: none; // 修复边缘白边
}
ul.el-menu--inline, .nested-sub-menu div {
background-color: #1f2d3d !important; // 二级菜单深暗色
}
</style>
Background menu and routing data return example
menu.json
{
"errno": 0,
"errmsg": "成功",
"data": [
{
"id": 1,
"parentId": 0,
"title":"主页",
"icon":"iconfont icon-home-line",
"url":"/home",
"menuType": "C",
"component":"@/views/Home.vue"
},
{
"id": 2,
"parentId": 0,
"title":"系统设置",
"icon":"iconfont icon-shezhi",
"url":"/sys",
"menuType": "M",
"component":"",
"children": [
{
"id": 3,
"parentId": 2,
"title":"用户管理",
"icon":"iconfont icon-yonghuguanli",
"url":"/sys/user",
"menuType": "C",
"component":"@/views/sys/user.vue"
},
{
"id": 4,
"parentId": 2,
"title":"角色管理",
"icon":"iconfont icon-jiaoseguanli",
"url":"/sys/role",
"menuType":"C",
"component":"@/views/sys/role.vue"
},
{
"id": 5,
"parentId": 2,
"title":"菜单管理",
"icon":"iconfont icon-icon_caidanguanli",
"url":"/sys/menu",
"menuType":"C",
"component":"@/views/sys/menu.vue"
}
]
},
{
"id": 6,
"parentId": 0,
"title":"多级菜单",
"icon":"iconfont icon-graphcool",
"url":"/test",
"component":"",
"menuType":"M",
"children": [
{
"id": 7,
"parentId": 6,
"title":"test_1",
"icon":"iconfont icon-graphcool",
"url":"/test/test_1",
"menuType":"C",
"component":"@/views/test/test_1.vue"
},
{
"id": 8,
"parentId": 2,
"title":"test_2",
"icon":"iconfont icon-graphcool",
"url":"/test/test_2",
"menuType":"M",
"component":"",
"children":[
{
"id": 9,
"parentId": 8,
"title":"test_2_1",
"icon":"iconfont icon-graphcool",
"menuType":"C",
"url":"/test/test_2/test_2_1",
"component":"@/views/test/test_2_1.vue"
},
{
"id": 10,
"parentId": 8,
"title":"test_2_2",
"icon":"iconfont icon-graphcool",
"menuType":"C",
"url":"/test/test_2/test_2_2",
"component":"@/views/test/test_2_2.vue"
}
]
}
]
}
]
}
router.json
{
"errno": 0,
"errmsg": "成功",
"data": [
{
"path": "/home",
"name": "home",
"component": "@/views/Home.vue"
},
{
"path": "/sys/user",
"name": "user",
"component": "@/views/sys/user.vue"
},
{
"path": "/sys/role",
"name": "role",
"component": "@/views/sys/role.vue"
},
{
"path": "/sys/menu",
"name": "menu",
"component": "@/views/sys/menu.vue"
},
{
"path": "/test/test_1",
"name": "test_1",
"component": "@/views/test/test_1.vue"
},
{
"path": "/test/test_2/test_2_1",
"name": "test_2_1",
"component": "@/views/test/test2/test_2_1.vue"
},
{
"path": "/test/test_2/test_2_2",
"name": "test_2_2",
"component": "@/views/test/test2/test_2_2.vue"
}
]
}
Modify loginApi.js
import request from '@/utils/request'
export function getCaptchaImage() {
return request({
url: 'captchaImage',
})
}
export function login(data) {
return request({
method:'post',
url: 'user/login',
data
})
}
export function getMenus() {
// 获取菜单
return request({
method:'get',
url: 'test/getMenus'
})
}
export function getRoutes() {
// 获取路由
return request({
method:'get',
url: 'test/getRoutes'
})
}
Modify request.js
Because the request header needs to be added to access the menu routing interface
import axios from 'axios'
import Messager from './messager';
import pinia from '@/store'
import useUser from '@/store/user'
const instance = axios.create({
baseURL: 'http://127.0.0.1:8080/api',
timeout: 10000
})
instance.interceptors.request.use((config)=>{
// debugger
let userStore = useUser()
if(userStore.token) {
console.log('userStore.token',userStore.token);
config.headers['Authorization'] = userStore.token
}
return config;
})
instance.interceptors.response.use(response=>{
if(response.data.errno == 0) {
return Promise.resolve(response.data.data)
} else {
if(response.data.errno == 501) {
Messager.error('请重新登录')
window.location.href = '/login'
} else {
Messager.error(response.data.errmsg)
return Promise.reject(new Error(response.data.errmsg))
}
}
})
export default instance
Modify router/index.js
Delete the originally configured static route, this part of the route is returned by the backend, and add front guard logic
import {
createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";
import Messager from '@/utils/messager';
import useMenu from '@/store/menu'
import pinia from '@/store'
import useUser from '@/store/user'
const userStore = useUser(pinia) // 此处不能像在组件里使用userStore一样直接useUser()调用,而是要先引入pinia,再传入。参考: https://blog.csdn.net/qq_21473443/article/details/126405859
console.log('router->userStore',userStore);
const menuStore = useMenu(pinia)
// 路由信息
const routes = [
{
path: "/login",
name: "login",
component: () => import('@/views/login/index.vue'),
},
{
path: '/',
name: 'layout',
component: ()=>import('@/layout/index.vue'),
children: []
},
{
path:'/:pathMatch(.*)*',
name: 'notFound',
component: ()=>import('@/views/404/NotFound.vue')
},
]
const router = createRouter({
history: createWebHistory(),
routes
});
router.beforeEach((to,from,next)=>{
nprogress.start()
// debugger
let token = userStore.token
if(!token) {
if(to.path == '/login') {
next()
} else {
next('/login')
}
} else {
if(!menuStore.routesMenusLoaded) {
menuStore.loadRoutesMenus().then(res=>{
next()
}).catch(err=>{
// 加载出错,跳回到登录页去
userStore.clearUserInfo()
next('/login')
})
} else {
if(to.path == '/login') {
Messager.warn('你已登录!')
next('/home')
} else {
next()
}
}
}
})
router.afterEach((to,from,next)=>{
nprogress.done()
})
// 导出路由
export default router;
Modify user.js
import {
defineStore } from 'pinia'
import {
login } from '@/api/loginApi'
function retrieveLocalToken() {
console.log('read token');
return localStorage.getItem('token') || ''
}
function clearLocalToken() {
return localStorage.clear('token')
}
export default defineStore('user',{
state: () => {
return {
token: retrieveLocalToken() // 当刷新页面时, 这个会加载一次
}
},
getters: {
},
actions: {
doLogin(data) {
return new Promise((resolve, reject) => {
login(data).then(res=>{
this.token = res // 同样先存入到pinia中
localStorage.setItem('token', res)
resolve(data)
}).catch(err=>{
reject(err)
})
})
},
clearUserInfo() {
this.token = null
clearLocalToken()
}
}
})
Create store/menu.js
Create menu.js to store the data returned by the background
import {
defineStore } from 'pinia'
import {
getMenus,getRoutes} from '@/api/loginApi'
import router from '@/router'
export default defineStore('menu', {
state: ()=> {
return {
routesMenusLoaded: false, // 路由菜单是否已加载
menus: [], // 菜单
routes: [] // 路由
}
},
getters: {
},
actions: {
loadRoutesMenus() {
return new Promise(async (resolve,reject)=>{
try {
let menus = await getMenus()
let routes = await getRoutes()
// 保存路由
this.routes = routes
// 保存菜单
this.menus = menus
// 动态加载路由
routes.forEach(route=>{
router.addRoute(
'layout',
{
path: route.path,
name: route.name,
component: ()=>import(route.component.replace('@',"../")) // 好像直接以@开头,会报错,所以这干脆替换成相对路径吧
}
)
})
console.log(router.getRoutes(),'finished');
resolve()
} catch (err) {
reject(err)
}
})
}
}
})
Modify the menu bar component Sider.vue
<template>
<div class="sider" :style="{ 'width': isExpand ? '220px' : '80px', transition: 'all 0.28s' }">
<div class="sider-top">
<h3 v-if="isExpand" class="site-title">PSCOOL管理系统</h3>
<i v-else class="iconfont icon-graphcool site-icon"></i>
</div>
<div class="sider-body">
<el-scrollbar>
<el-menu
:collapse="isCollapse"
router
collapse-transition text-color="#eee"
:default-openeds="['/sys']"
:default-active="activeMenu"
background-color="#294256" class="menu-bar">
<TreeMenu v-for="menuData,index in menuList" :menu="menuData" :key="index"></TreeMenu>
</el-menu>
</el-scrollbar>
</div>
</div>
</template>
<script setup>
import TreeMenu from './TreeMenu.vue'
import {
ref,reactive } from 'vue'
import useLayout from '@/store/layout'
import {
storeToRefs } from 'pinia'
import {
computed, watch } from 'vue'
import {
useRouter,useRoute } from 'vue-router'
import useMenu from '@/store/menu'
const layoutStore = useLayout()
const {
isExpand } = storeToRefs(layoutStore) // 需要使用storeToRefs, 才能保持响应式
const isCollapse = computed({
get() {
return !isExpand.value
}
})
watch(isExpand, (newVal, oldVal) => {
// console.log('监听到变化');
})
const activeMenu = ref('/home') // 需要监听到当前的路由, 让它高亮, 因为用户有可能手动地址栏输入
const router = useRouter()
const route = useRoute()
watch(()=>route.fullPath, (newVal,oldVal)=>{
console.log('监听到当前的路由', newVal);
activeMenu.value = newVal;
}, {
immediate:true})
const menuStore = useMenu()
const menuList = computed({
get() {
return menuStore.menus
}
})
</script>
<style lang="scss">
.sider {
width: 220px;
height: 100vh;
background-color: #294256;
position: relative;
box-shadow: 2px 0 8px 0 rgba(0, 0, 0, 0.3);
flex-shrink: 0;
.sider-top {
height: 50px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.site-title {
white-space: nowrap;
font-weight: bold;
color: #fff;
}
.site-icon {
font-size: 20px;
color: #27ae60;
}
}
.sider-body {
position: absolute;
top: 50px;
left: 0;
right: 0;
bottom: 0;
background-color: #294256;
.menu-bar {
.iconfont {
margin-right: 10px;
font-size: 1.4em;
}
}
}
}
.el-menu {
border-right: none; // 修复边缘白边
}
ul.el-menu--inline, .nested-sub-menu div {
background-color: #1f2d3d !important; // 二级菜单深暗色
}
</style>
Create TreeMenu.vue recursive component
<template>
<template v-if="!menu.children && menu.menuType == 'C'">
<el-menu-item :index="menu.url">
<i :class="menu.icon"></i>
<span>{
{ menu.title }}</span>
</el-menu-item>
</template>
<template v-if="menu.children && menu.menuType == 'M'">
<el-sub-menu :index="menu.url" :class="{
'nested-sub-menu': menu.parentId != 0}">
<template #title>
<i :class="menu.icon"></i>
<span>{
{ menu.title }}</span>
</template>
<TreeMenu v-for="childMenu,index in menu.children" :menu="childMenu" :key="index"></TreeMenu>
</el-sub-menu>
</template>
</template>
<script setup>
defineProps({
menu: {
type: Object
}
})
</script>
<style lang="scss"></style>
Solve the problem of refreshing the address bar
I made a mistake above. I put the 404 route as a static route and put it directly in the router, so that the 404 route is in the front. It is not an exact match, so when I refresh the page, I jump directly to the 404 page. , so change the route of 404 to after obtaining all the route data of the backend
Modify router/index.js
import {
createRouter,createWebHistory} from "vue-router";
import nprogress from "@/utils/nprogress";
import Messager from '@/utils/messager';
import useMenu from '@/store/menu'
import pinia from '@/store'
import useUser from '@/store/user'
const userStore = useUser(pinia) // 此处不能像在组件里使用userStore一样直接useUser()调用,而是要先引入pinia,再传入。参考: https://blog.csdn.net/qq_21473443/article/details/126405859
console.log('router->userStore',userStore);
const menuStore = useMenu(pinia)
// 路由信息
const routes = [
{
path: "/login",
name: "login",
component: () => import('@/views/login/index.vue'),
},
{
path: '/',
name: 'layout',
component: ()=>import('@/layout/index.vue'),
children: []
}
]
const router = createRouter({
history: createWebHistory(),
routes
});
function existRoutePath(path) {
let routes = router.getRoutes()
let routePathArr = []
routes.forEach((route) => {
routePathArr.push(route.path)
})
return routePathArr.indexOf(path)
}
router.beforeEach((to,from,next)=>{
nprogress.start()
// console.log(router.getRoutes(),existRoutePath(to.path),'router hasRoute-3',to);
// debugger
let token = userStore.token
if(!token) {
if(to.path == '/login') {
next()
} else {
next('/login')
}
} else {
if(!menuStore.routesMenusLoaded) {
// console.log(router.getRoutes(),existRoutePath(to.name),'router hasRoute-1',to);
menuStore.loadRoutesMenus().then(res=>{
// console.log(router.getRoutes(),existRoutePath(to.name),'router hasRoute-2',to);
next({
...to})
}).catch(err=>{
// 加载出错,跳回到登录页去
userStore.clearUserInfo()
next('/login')
})
} else {
if(to.path == '/login') {
Messager.warn('你已登录!')
next('/home')
} else {
// console.log(router.getRoutes(),existRoutePath(to.name),'router hasRoute-4');
next()
}
}
}
})
router.afterEach((to,from,next)=>{
nprogress.done()
})
// 导出路由
export default router;
Modify menu.js
import {
defineStore } from 'pinia'
import {
getMenus,getRoutes} from '@/api/loginApi'
import router from '@/router'
export default defineStore('menu', {
state: ()=> {
return {
routesMenusLoaded: false, // 路由菜单是否已加载
menus: [], // 菜单
routes: [] // 路由
}
},
getters: {
},
actions: {
loadRoutesMenus() {
return new Promise(async (resolve,reject)=>{
try {
let menus = await getMenus()
let routes = await getRoutes()
// 保存路由
this.routes = routes
// 保存菜单
this.menus = menus
// 动态加载路由
routes.forEach(route=>{
router.addRoute(
'layout',
{
path: route.path,
name: route.name,
component: ()=>import(route.component.replace('@',"../")) // 好像直接以@开头,会报错,所以这干脆替换成相对路径吧
}
)
})
router.addRoute({
path:'/:pathMatch(.*)*',
name: 'notFound',
component: ()=>import('../views/404/NotFound.vue')
})
console.log(router.getRoutes(),'加载路由 finished');
this.routesMenusLoaded = true
resolve()
} catch (err) {
reject(err)
}
})
}
}
})
12. Full screen function
Install screenfull
npm i screenfull -S
use screenfull
<template>
<div class="fullscreen mlr8" @click="toggleFullScreen">
<i :class="['iconfont', 'pointer', { 'icon-quanping_o': !isFullScreen, 'icon-quxiaoquanping_o': isFullScreen }]"></i>
</div>
</template>
<script>
import {
ref} from 'vue'
const isFullScreen = ref(false)
function toggleFullScreen() {
screenfull.toggle()
isFullScreen.value = !isFullScreen.value
}
</script>
13. Breadcrumbs
We need to display the breadcrumbs of the menu of the current route, first agree on the data, the name of the route is unique and corresponds to the name in the menu and is unique, so that when we switch to a certain route, we can recursively enter the menu according to the name Find all the parents it corresponds to.
data
{
"errno": 0,
"errmsg": "成功",
"data": [
{
"path": "/home",
"name": "home",
"component": "@/views/Home.vue"
},
{
"path": "/sys/user",
"name": "user",
"component": "@/views/sys/user.vue"
},
{
"path": "/sys/role",
"name": "role",
"component": "@/views/sys/role.vue"
},
{
"path": "/sys/menu",
"name": "menu",
"component": "@/views/sys/menu.vue"
},
{
"path": "/test/test_1",
"name": "test_1",
"component": "@/views/test/test_1.vue"
},
{
"path": "/test/test_2/test_2_1",
"name": "test_2_1",
"component": "@/views/test/test2/test_2_1.vue"
},
{
"path": "/test/test_2/test_2_2",
"name": "test_2_2",
"component": "@/views/test/test2/test_2_2.vue"
}
]
}
{
"errno": 0,
"errmsg": "成功",
"data": [
{
"id": 1,
"parentId": 0,
"name": "home",
"title":"主页",
"icon":"iconfont icon-home-line",
"url":"/home",
"menuType": "C",
"component":"@/views/Home.vue"
},
{
"id": 2,
"parentId": 0,
"name": "sys",
"title":"系统设置",
"icon":"iconfont icon-shezhi",
"url":"/sys",
"menuType": "M",
"component":"",
"children": [
{
"id": 3,
"parentId": 2,
"name": "user",
"title":"用户管理",
"icon":"iconfont icon-yonghuguanli",
"url":"/sys/user",
"menuType": "C",
"component":"@/views/sys/user.vue"
},
{
"id": 4,
"parentId": 2,
"name": "role",
"title":"角色管理",
"icon":"iconfont icon-jiaoseguanli",
"url":"/sys/role",
"menuType":"C",
"component":"@/views/sys/role.vue"
},
{
"id": 5,
"parentId": 2,
"name": "menu",
"title":"菜单管理",
"icon":"iconfont icon-icon_caidanguanli",
"url":"/sys/menu",
"menuType":"C",
"component":"@/views/sys/menu.vue"
}
]
},
{
"id": 6,
"parentId": 0,
"name": "test",
"title":"多级菜单",
"icon":"iconfont icon-graphcool",
"url":"/test",
"component":"",
"menuType":"M",
"children": [
{
"id": 7,
"parentId": 6,
"name": "test_1",
"title":"test_1",
"icon":"iconfont icon-graphcool",
"url":"/test/test_1",
"menuType":"C",
"component":"@/views/test/test_1.vue"
},
{
"id": 8,
"parentId": 2,
"name": "test_2",
"title":"test_2",
"icon":"iconfont icon-graphcool",
"url":"/test/test_2",
"menuType":"M",
"component":"",
"children":[
{
"id": 9,
"parentId": 8,
"name": "test_2_1",
"title":"test_2_1",
"icon":"iconfont icon-graphcool",
"menuType":"C",
"url":"/test/test_2/test_2_1",
"component":"@/views/test/test_2_1.vue"
},
{
"id": 10,
"parentId": 8,
"name": "test_2_2",
"title":"test_2_2",
"icon":"iconfont icon-graphcool",
"menuType":"C",
"url":"/test/test_2/test_2_2",
"component":"@/views/test/test_2_2.vue"
}
]
}
]
}
]
}
Modify menus.js
According to the menu returned by the background, recursively get the hierarchical menu titles corresponding to all routes, and put them into the meta of the route
import {
defineStore } from 'pinia'
import {
getMenus,getRoutes} from '@/api/loginApi'
import router from '@/router'
import {
dropdownMenuProps } from 'element-plus'
function generateNameMap(menus) {
const nameMap = {
}
menus.forEach(menu => {
handleMenu(menu,nameMap,[])
})
return nameMap
}
function handleMenu(menu,nameMap,titleArr) {
titleArr.push(menu.title)
nameMap[menu.name] = JSON.parse(JSON.stringify(titleArr))
if(menu.children && menu.children.length > 0) {
menu.children.forEach(menu => {
let newTitleArr = JSON.parse(JSON.stringify(titleArr))
handleMenu(menu,nameMap,newTitleArr)
})
}
}
export default defineStore('menu', {
state: ()=> {
return {
routesMenusLoaded: false, // 路由菜单是否已加载
menus: [], // 菜单
routes: [] // 路由
}
},
getters: {
},
actions: {
loadRoutesMenus() {
return new Promise(async (resolve,reject)=>{
try {
let menus = await getMenus()
let routes = await getRoutes()
// 保存路由
this.routes = routes
// 保存菜单
this.menus = menus
// debugger
const nameMap = generateNameMap(menus)
// 动态加载路由
routes.forEach(route=>{
router.addRoute(
'layout',
{
path: route.path,
name: route.name,
meta: {
titleArr: nameMap[route.name]
},
// 好像直接以@开头,会报错,所以这干脆替换成相对路径吧
component: ()=>import(route.component.replace('@',"../"))
}
)
})
router.addRoute({
path:'/:pathMatch(.*)*',
name: 'notFound',
component: ()=>import('../views/404/NotFound.vue')
})
console.log(router.getRoutes(),'加载路由 finished');
this.routesMenusLoaded = true
resolve()
} catch (err) {
reject(err)
}
})
},
}
})
Modify Breadcrumb.vue
Listen for routing changes and get cached breadcrumb data from the routing meta
<template>
<el-breadcrumb separator="/" style="color: #303133;display: flex;white-space: nowrap;">
<el-breadcrumb-item v-for="(title,index) in titleArr" :key="index">{
{ title }}</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup>
import {
ref,reactive,watch } from 'vue'
import {
useRoute } from 'vue-router'
const route = useRoute()
const titleArr = ref([])
watch(()=>route, (newRoute,oldRoute)=>{
console.log('路由更新了',newRoute);
titleArr.value = newRoute.meta.titleArr
},{
immediate: true,deep:true})
</script>
<style lang="scss">
</style>
14. tagsView
This step is mainly to implement the tagsView function. The tagsView records the menus that the user has visited, and can close it as needed, but the tag of the home page should always be kept.
We store the tags in pinia. The tagsView component refers to the tags in pinia through the calculated property, and calls the method in pinia through the listening event trigger method
. Trigger the closing event of the i icon, and trigger the click event of the div, use @click.stop to bind
TagsView.vue
<template>
<div class="main-header-tags-wrapper">
<el-scrollbar>
<div class="main-header-tags" id="main-header-tags">
<div :class="['tag-item',{
'active':tag.isActive}]" @click="selectSpecifiedTag(tag)" v-for="(tag,index) in tags" :key="index">
<span>{
{ tag.title }}</span>
<i class="close-ico iconfont icon-guanbi" v-show="tag.name != 'home'" @click.stop="closeTag(tag)"></i>
</div>
</div>
</el-scrollbar>
</div>
</template>
<script setup>
import useTagsView from '@/store/tagsView'
import {
computed, watch } from 'vue'
import {
useRoute,useRouter } from 'vue-router'
const tagsViewStore = useTagsView()
const tags = computed({
get() {
return tagsViewStore.tags
}
})
const route = useRoute()
const router = useRouter()
watch(()=>route, (newRoute,oldRoute)=>{
tagsViewStore.doOnrouteChange(newRoute)
},{
immediate:true,deep:true})
function selectSpecifiedTag(tag) {
debugger
tagsViewStore.selectSpecifiedTag(tag)
router.push({
name:tag.name})
}
function closeTag(tag) {
// 关闭的是不是当前激活的tag, 如果是当前激活的tag的话,就选择最后一个tag;如果不是当前激活的tag的话,就关掉它就行了
let isCurrTagActiveClose = tag.isActive
tagsViewStore.closeSpecifiedTag(tag)
if(isCurrTagActiveClose) {
// 选择最后面的tag
debugger
console.log('选择最后面的tag', tagsViewStore.tags[tagsViewStore.tags.length - 1]);
selectSpecifiedTag(tagsViewStore.tags[tagsViewStore.tags.length - 1])
}
}
</script>
<style lang="scss">
.main-header-tags-wrapper {
padding: 0 10px;
.main-header-tags {
height: 32px;
display: flex;
align-items: center;
.tag-item {
height: 26px;
padding: 0 20px;
margin-right: 8px;
font-size: 13px;
cursor: pointer;
color: #495060;
border: 1px solid #ccc;
background-color: #fff;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
position: relative;
i.close-ico {
font-size: 12px;
position: absolute;
right: 2px;
top: 4.5px;
transform: scale(0.6);
cursor: pointer;
padding: 3px;
border-radius: 50%;
&:hover {
background: #b4bccc;
}
}
&.active {
background-color: #409eff;
border: #409eff;
color: #fff;
&::before {
content: '';
position: absolute;
width: 6px;
height: 6px;
background-color: #fff;
border-radius: 50%;
left: 8px;
top: 10.5px;
}
}
}
}
}
</style>
TagsView.js
import {
defineStore } from 'pinia'
export default defineStore('tagsView', {
state: ()=> {
return {
tags: [
{
title: '主页',
name: 'home',
path: '/home',
isActive: false
}
],
}
},
getters: {
},
actions: {
doOnrouteChange(route) {
debugger
console.log('doOnrouteChange->新路由', route.name);
let currRouteName = route.name
let tagNameArr = []
let flag = false
this.tags.forEach(tag=>{
tag.isActive = false
if(tag.name == currRouteName) {
flag = true
tag.isActive = true
}
})
if(!flag) {
console.log('原先没有这个路由,现在添加tag', route.name);
this.tags.push({
title: route.meta.title,
name: route.name,
path: route.path,
isActive: true
})
}
},
closeSpecifiedTag(tag){
debugger
let index = -1;
for(let i=0;i<this.tags.length;i++) {
if(this.tags[i].name === tag.name) {
index = i
break
}
}
if(index > -1) {
this.tags.splice(index,1)
}
},
selectSpecifiedTag(tag) {
debugger
this.tags.forEach(t=>{
t.isActive = false
if(t.name == tag.name) {
t.isActive = true
}
})
}
}
})
15. The vue command controls the display of the permission button
Through the directive instruction method of vue, the button is only displayed when the user has the specified permission
Background returns permission data
{
"errno": 0,
"errmsg": "成功",
"data": {
"perms": [
"user:list",
"user:add",
"user:remove",
"role:list",
"role:add",
"role:remove"
]
}
}
Create directive file perms.js
import useMenu from '@/store/menu'
import {
toRaw } from '@vue/reactivity'
export default {
hasPerms: {
mounted(el,binding) {
const menuStore = useMenu()
let perms1 = menuStore.perms
console.log(el,binding,perms1);
let perms2 = toRaw(perms1)
let perms3 = JSON.parse(JSON.stringify(perms1))
console.log(perms2.perms);
console.log(perms3.perms);
// 有任一指定的权限, 即可显示指定的dom, 否则移除
if(!perms2.perms.some(p=>binding.value.includes(p))) {
el.parentNode.removeChild(el)
}
},
}
}
Register the directive in main.js
import {
createApp } from 'vue'
import './style.css'
import '@/assets/iconfont/iconfont.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import Messager from '@/utils/messager'
import router from '@/router'
import pinia from '@/store'
import perm from '@/directive/perm'
const app = createApp(App)
app.config.globalProperties.Messager = Messager
app.use(pinia)
app.use(router)
app.use(ElementPlus)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// 注册指令
for(let key in perm) {
app.directive(key, perm[key])
}
app.mount('#app')
Add interface in loginApi.js
// ...省略
export function getPerms() {
return request({
method:'get',
url: 'test/getPerms'
})
}
Modify store/menu.js
Add the part that gets permission
import {
defineStore } from 'pinia'
import {
getMenus,getRoutes,getPerms} from '@/api/loginApi'
import router from '@/router'
import {
dropdownMenuProps } from 'element-plus'
function generateNameMap(menus) {
const nameMap = {
}
menus.forEach(menu => {
handleMenu(menu,nameMap,[])
})
return nameMap
}
function handleMenu(menu,nameMap,titleArr) {
titleArr.push(menu.title)
nameMap[menu.name] = JSON.parse(JSON.stringify(titleArr))
if(menu.children && menu.children.length > 0) {
menu.children.forEach(menu => {
let newTitleArr = JSON.parse(JSON.stringify(titleArr))
handleMenu(menu,nameMap,newTitleArr)
})
}
}
export default defineStore('menu', {
state: ()=> {
return {
routesMenusLoaded: false, // 路由菜单是否已加载
menus: [], // 菜单
routes: [], // 路由,
perms: [], // 权限
}
},
getters: {
},
actions: {
loadRoutesMenus() {
return new Promise(async (resolve,reject)=>{
try {
let menus = await getMenus()
let routes = await getRoutes()
let perms = await getPerms()
// 保存路由
this.routes = routes
// 保存菜单
this.menus = menus
// 保存权限
this.perms = perms
// debugger
const nameMap = generateNameMap(menus)
// 动态加载路由
routes.forEach(route=>{
router.addRoute(
'layout',
{
path: route.path,
name: route.name,
meta: {
titleArr: nameMap[route.name],
title: nameMap[route.name][nameMap[route.name].length-1]
},
// 好像直接以@开头,会报错,所以这干脆替换成相对路径吧
component: ()=>import(route.component.replace('@',"../"))
}
)
})
router.addRoute({
path:'/:pathMatch(.*)*',
name: 'notFound',
component: ()=>import('../views/404/NotFound.vue')
})
console.log(router.getRoutes(),'加载路由 finished');
this.routesMenusLoaded = true
resolve()
} catch (err) {
reject(err)
}
})
},
}
})
Used in User.vue
<template>
用户管理
<el-button type="danger" v-hasPerms="['user:list']">查看</el-button>
<el-button type="primary" v-hasPerms="['user:add']">添加</el-button>
<el-button type="primary" v-hasPerms="['user:update']">修改</el-button>
<el-button type="primary" v-hasPerms="['user:remove']">删除</el-button>
</template>
<script setup>
</script>
<style lang="scss">
</style>
The following effect
16. Add transition effects
The switching process of breadcrumbs and routing looks particularly blunt, we need to add transition effects to them, like the following
Breadcrumb transition effect
- The following transition effect code is a direct copy of the official website, because it is traversed by v-for, so transition-group is used.
- It should also be noted that the key bound to the element must be unique (the index cannot be used as the key), which is a requirement of Vue, otherwise there will be no animation effect
<template>
<el-breadcrumb separator="/" style="color: #303133;display: flex;white-space: nowrap;">
<transition-group name="list">
<el-breadcrumb-item v-for="title in titleArr" :key="title">{
{ title }}</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script setup>
import {
ref,reactive,watch } from 'vue'
import {
useRoute } from 'vue-router'
const route = useRoute()
const titleArr = ref([])
watch(()=>route, (newRoute,oldRoute)=>{
// console.log('路由更新了',newRoute);
titleArr.value = newRoute.meta.titleArr
},{
immediate: true,deep:true})
</script>
<style lang="scss">
/* breadcrumb transition */
.list-move, /* 对移动中的元素应用的过渡 */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* 确保将离开的元素从布局流中删除
以便能够正确地计算移动的动画。 */
.list-leave-active {
position: absolute;
}
</style>
routing transition effects
Modify Main.vue, only paste the modified code below, and omit the other parts
<template>
<div class="main-header">
...
</div>
<div class="main-body">
<router-view v-slot:="{Component,route}">
<transition name="slide-fade" mode="out-in">
<component :is="Component" :key="route.path"/>
</transition>
</router-view>
</div>
</template>
<style lang="scss" scoped>
/*
进入和离开动画可以使用不同
持续时间和速度曲线。
*/
.slide-fade-enter-active {
transition: all 0.5s ease-out;
}
.slide-fade-leave-active {
transition: all 0.5s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
}
...
</style>
17. Change to use import.meta.glob to import routes
Reference:
vue3 + vite dynamic import routing
import.meta.glob batch import files
Modify store/menu.js
import {
defineStore } from 'pinia'
import {
getMenus,getRoutes,getPerms} from '@/api/loginApi'
import router from '@/router'
import {
dropdownMenuProps } from 'element-plus'
function generateNameMap(menus) {
const nameMap = {
}
menus.forEach(menu => {
handleMenu(menu,nameMap,[])
})
return nameMap
}
function handleMenu(menu,nameMap,titleArr) {
titleArr.push(menu.title)
nameMap[menu.name] = JSON.parse(JSON.stringify(titleArr))
if(menu.children && menu.children.length > 0) {
menu.children.forEach(menu => {
let newTitleArr = JSON.parse(JSON.stringify(titleArr))
handleMenu(menu,nameMap,newTitleArr)
})
}
}
export default defineStore('menu', {
state: ()=> {
return {
routesMenusLoaded: false, // 路由菜单是否已加载
menus: [], // 菜单
routes: [], // 路由,
perms: [], // 权限
}
},
getters: {
},
actions: {
loadRoutesMenus() {
return new Promise(async (resolve,reject)=>{
try {
let menus = await getMenus()
let routes = await getRoutes()
let perms = await getPerms()
// 保存路由
this.routes = routes
// 保存菜单
this.menus = menus
// 保存权限
this.perms = perms
// debugger
const nameMap = generateNameMap(menus)
//定义一个函数,引入所有views下.vue文件
const modules = import.meta.glob(`../views/**/*.vue`);
console.log(modules,'modules');
/*
../views/404/NotFound.vue: () => import("/src/views/404/NotFound.vue")
../views/Home.vue: () => import("/src/views/Home.vue")
../views/login/index.vue: () => import("/src/views/login/index.vue?t=1679996129333")
../views/sys/Menu.vue: () => import("/src/views/sys/Menu.vue")
../views/sys/Role.vue: () => import("/src/views/sys/Role.vue")
../views/sys/User.vue: () => import("/src/views/sys/User.vue")
../views/test/test_1.vue: () => import("/src/views/test/test_1.vue")
modules
*/
// 动态加载路由
routes.forEach(route=>{
// debugger
// console.log(`../views/${route.component.substring(8)}`);
// console.log(`../views/${route.component.substring(8)}`,modules[`../views/${route.component.substring(8)}`]);
router.addRoute(
'layout',
{
path: route.path,
name: route.name,
meta: {
titleArr: nameMap[route.name],
title: nameMap[route.name][nameMap[route.name].length-1]
},
// 好像直接以@开头,会报错,所以这干脆替换成相对路径吧
// component: ()=>import(/* @vite-ignore */route.component.replace('@',"../"))
component: modules[`../views/${
route.component.substring(8)}`]
// @/views/Home.vue -> ../views/Home.vue
}
)
})
router.addRoute({
path:'/:pathMatch(.*)*',
name: 'notFound',
component: ()=>import('../views/404/NotFound.vue')
})
console.log(router.getRoutes(),'加载路由 finished');
this.routesMenusLoaded = true
resolve()
} catch (err) {
reject(err)
}
})
},
}
})
Modify store/menu.js again
Since all of the above are possible, there is no reason to use @, so modify it again as follows:
import {
defineStore } from 'pinia'
import {
getMenus,getRoutes,getPerms} from '@/api/loginApi'
import router from '@/router'
import {
dropdownMenuProps } from 'element-plus'
function generateNameMap(menus) {
const nameMap = {
}
menus.forEach(menu => {
handleMenu(menu,nameMap,[])
})
return nameMap
}
function handleMenu(menu,nameMap,titleArr) {
titleArr.push(menu.title)
nameMap[menu.name] = JSON.parse(JSON.stringify(titleArr))
if(menu.children && menu.children.length > 0) {
menu.children.forEach(menu => {
let newTitleArr = JSON.parse(JSON.stringify(titleArr))
handleMenu(menu,nameMap,newTitleArr)
})
}
}
export default defineStore('menu', {
state: ()=> {
return {
routesMenusLoaded: false, // 路由菜单是否已加载
menus: [], // 菜单
routes: [], // 路由,
perms: [], // 权限
}
},
getters: {
},
actions: {
loadRoutesMenus() {
return new Promise(async (resolve,reject)=>{
try {
let menus = await getMenus()
let routes = await getRoutes()
let perms = await getPerms()
// 保存路由
this.routes = routes
// 保存菜单
this.menus = menus
// 保存权限
this.perms = perms
// debugger
const nameMap = generateNameMap(menus)
//定义一个函数,引入所有views下.vue文件
const modules = import.meta.glob(`@/views/**/*.vue`);
console.log(modules,'modules');
/*
/src/views/404/NotFound.vue: () => import("/src/views/404/NotFound.vue")
/src/views/Home.vue: () => import("/src/views/Home.vue")
/src/views/login/index.vue: () => import("/src/views/login/index.vue?t=1679996129333")
/src/views/sys/Menu.vue: () => import("/src/views/sys/Menu.vue")
/src/views/sys/Role.vue: () => import("/src/views/sys/Role.vue")
/src/views/sys/User.vue: () => import("/src/views/sys/User.vue")
/src/views/test/test_1.vue: () => import("/src/views/test/test_1.vue")
modules
*/
// 动态加载路由
routes.forEach(route=>{
// debugger
console.log(route.component);
console.log(route.component, modules[route.component]);
router.addRoute(
'layout',
{
path: route.path,
name: route.name,
meta: {
titleArr: nameMap[route.name],
title: nameMap[route.name][nameMap[route.name].length-1]
},
// 好像直接以@开头,会报错,所以这干脆替换成相对路径吧
// component: ()=>import(/* @vite-ignore */route.component.replace('@',"../"))
// 改成使用import.meta.glob动态导入
component: modules[route.component.replace('@','/src')]
}
)
})
router.addRoute({
path:'/:pathMatch(.*)*',
name: 'notFound',
component: ()=>import('../views/404/NotFound.vue')
})
console.log(router.getRoutes(),'加载路由 finished');
this.routesMenusLoaded = true
resolve()
} catch (err) {
reject(err)
}
})
},
}
})