vue3后台管理系统
后面可参考下:vue系列(三)——手把手教你搭建一个vue3管理后台基础模板
以下代码项目gitee地址
文章目录
- 1. 初始化前端项目
- 初始化项目
- 添加加载效果
- 配置 vite.config.js
- 2. 使用路由
- 安装路由
- 配置路由
- 配置@别名和跳转
- 安装path
- vite.config.js
- jsconfig.json
- main.js中使用路由
- 3. 使用elment-plus
- 安装elment-plus
- main.js中使用elment-plus
- 4. 使用pinia
- 安装pinia
- 配置pinia
- 创建store/index.js
- 创建store/counter.js
- main.js中引入
- 组件中使用
- 5. 使用axios
- 安装axios
- 编写request.js
- 编写api请求接口
- 组件中使用axios
- 6. 使用nprogress
- 安装nprogress
- 封装nprogress.js
- 路由中使用nprogress
- 7. 引入iconfont
- 下载iconfont
- main.js中引入
- 8. 封装ELMessage
- 9. 登录功能
- 配置登录的路由
- login.vue
- store/user.js
- api/loginApi.js
- 10.后台页面布局
- 配置登录成功后的路由
- 拆分组件
- 创建layout/index.vue
- 创建store/layout.js
- 创建layout/components/Sider.vue
- 创建layout/Main.vue
- 创建layout/Breadcrumb.vue
- 创建layout/TagsView组件
- 创建layout/components/Demo.vue
- 11. 菜单
- 搭建静态菜单路由
- 配置主页/用户/角色/菜单路由
- 使用el-menu创建侧边栏菜单
- 创建views/Home.vue
- 创建views/404/NotFound.vue
- 实现动态路由菜单
- 调整路由和菜单
- 调整路由
- 调整菜单
- 后台菜单和路由数据返回示例
- menu.json
- router.json
- 修改loginApi.js
- 修改request.js
- 修改router/index.js
- 修改user.js
- 创建store/menu.js
- 修改菜单栏组件Sider.vue
- 创建TreeMenu.vue递归组件
- 解决地址栏刷新问题
- 修改router/index.js
- 修改menu.js
- 12.全屏功能
- 安装screenfull
- 使用screenfull
- 13. 面包屑
- 数据
- 修改menus.js
- 修改Breadcrumb.vue
- 14. tagsView
- TagsView.vue
- TagsView.js
- 15. vue指令控制权限按钮显示
- 后台返回权限数据
- 创建指令文件perms.js
- main.js中注册该指令
- loginApi.js中添加接口
- 修改store/menu.js
- User.vue中使用
- 16.添加过渡效果
- 面包屑过渡效果
- 路由过渡效果
1. 初始化前端项目
初始化项目
可参考:vite官网 https://vitejs.cn/guide/#scaffolding-your-first-vite-project
npm init vite@latest mushan-vue3-adminnpm installnpm run dev
添加加载效果
在index.html中的id为app中,写入
<!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>
配置 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. 使用路由
安装路由
npm i vue-router@4 -S
配置路由
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;
配置@别名和跳转
安装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
与vite.config.js在同一级目录下
{"compilerOptions": {"baseUrl": "./","paths": {"@/*": ["src/*"],}},"exclude": ["node_modules","dist"],"include": ["src/**/*"]
}
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. 使用elment-plus
安装elment-plus
npm install element-plus --save
main.js中使用elment-plus
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. 使用pinia
可参考:Vue3中的pinia使用(收藏版)
安装pinia
npm install pinia --save
配置pinia
创建store/index.js
import { createPinia } from 'pinia'const pinia = createPinia()export default pinia
创建store/counter.js
import { defineStore } from 'pinia'export const useCounter = defineStore('counter',{state: () => ({count:99}),getters: {},actions: {}
})
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')
组件中使用
<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. 使用axios
可参考:Vue3使用axios的配置教程
安装axios
npm install axios --save
编写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
编写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})
}
组件中使用axios
<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. 使用nprogress
安装nprogress
npm i nprogress -S
封装nprogress.js
import Nprogress from 'nprogress'
import 'nprogress/nprogress.css'const nprogress = Nprogress.configure({easing: 'ease', // 动画方式speed: 1000, // 递增进度条的速度showSpinner: false, // 是否显示加载icotrickleSpeed: 200, // 自动递增间隔minimum: 0.3, // 更改启动时使用的最小百分比parent: 'body', //指定进度条的父容器
})export default nprogress
路由中使用nprogress
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. 引入iconfont
下载iconfont
下载iconfont相关资源到本地,添加到assets/iconfont目录下
main.js中引入
import { createApp } from 'vue'
import './style.css'import '@/assets/iconfont/iconfont.css' // 引入iconfont的css文件import App from './App.vue'
8. 封装ELMessage
import { ElMessage } from "element-plus";
const Messager = {ok(msg){ElMessage.success(msg)},error(msg) {ElMessage.error(msg)}
}
export default Messager
9. 登录功能
配置登录的路由
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.imgloginFormData.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
将登录获取的token存入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.后台页面布局
登录成功之后,会跳到主页,主页大概如下布局,可以先参考vue3 + elment-plus实现后台布局的静态页面布局,然后把它划分成不同的组件,不同组件的数据共享通过pinia这个store来管理。
配置登录成功后的路由
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;
拆分组件
创建layout/index.vue
Layout组件引入Sider和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>
创建store/layout.js
将组件的共享数据存入pinia
import { defineStore } from 'pinia'export default defineStore('layout', {state: ()=> {return {isExpand: true, // 侧边栏是否展开}},getters: {},actions: {// 切换侧边栏toggleSider() {console.log('切换侧边栏', this.isExpand);this.isExpand = !this.isExpand}}
})
创建layout/components/Sider.vue
isExpand是存放在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>
创建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>
创建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>
创建layout/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>
创建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. 菜单
搭建静态菜单路由
这一步,我们将获得如下的效果
配置主页/用户/角色/菜单路由
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;
使用el-menu创建侧边栏菜单
- el-menu就是一个ul,而el-subm-menu和el-menu-item都是一个li,其中el-sub-menu这个li中里面会嵌套一个div和一个ul>li,里面的这个div(使用title插槽)会显示出来作为菜单,里面的ul>li会作为收缩菜单
- 当收缩的时候,会给el-menu生成的ul(也就是最外面的ul)加上一个el-menu–collapse的类名,它会把菜单中span的文字给隐藏掉,这样就只会显示图标了
<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>
创建views/Home.vue
Home.vue可作为其它组件放入到Main.vue组件的main-body中的路由出口的模板,这样就不会让右边整体出现垂直滚动条(如下图),其它组件可以自定义布局方式,
<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>
创建views/404/NotFound.vue
这里就展示简单的返回下
<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>
实现动态路由菜单
不同用户登录进来,需要根据当前用户拥有的菜单显示在侧边栏,并且动态添加路由到router中。也就是说,用户一登陆完成,我们就应该请求后台去拿到用户拥有的所有菜单,组装侧边栏菜单,并且要动态的添加路由。
调整路由和菜单
我们需要做如下的事情,但是在做下面的事情之前,我们先调整一下我们的菜单,确保这样是可用的,然后再接入后台数据。
- 需要获取左侧菜单栏的数据,然后递归遍历出来
- 将路由添加到router里面
调整路由
- 我们注意到,vue里面的路由,如果是以/直接开头,它就会忽略父路由的路径,而直接去匹配,而如果不是以/开头,则会拼接上父路径去匹配,为了方便,就全部以/开头。
- 我们把所有的路由都作为layout的子路由,所以后面我们就直接添加到layout的路由下面就行了
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;
调整菜单
这里我们只需要对着路由写index的路径即可。还有注意的是,如果用户是直接在地址栏输入的路径而跳转的话,我们也需要让对应的菜单高亮,我们监听路由即可。
<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>
后台菜单和路由数据返回示例
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"}]
}
修改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'})
}
修改request.js
因为需要添加请求头,才能访问获取菜单路由接口
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)=>{// debuggerlet 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
修改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: []},{path:'/:pathMatch(.*)*',name: 'notFound',component: ()=>import('@/views/404/NotFound.vue')},
]const router = createRouter({history: createWebHistory(),routes
});router.beforeEach((to,from,next)=>{nprogress.start()// debuggerlet token = userStore.tokenif(!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;
修改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 = nullclearLocalToken()}}
})
创建store/menu.js
创建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('@',"../")) // 好像直接以@开头,会报错,所以这干脆替换成相对路径吧})})console.log(router.getRoutes(),'finished');resolve()} catch (err) {reject(err)}})}}
})
修改菜单栏组件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>
创建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>
解决地址栏刷新问题
上面犯了一个错误,我把404路由作为静态路由,直接给放到了router里面了,这样404的路由就排在了前面,它不是精确匹配,导致我刷新页面的时候,直接就跳404页面了,所以把404的路由改到获取完后端的全部路由数据之后
修改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);// debuggerlet token = userStore.tokenif(!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;
修改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 = trueresolve()} catch (err) {reject(err)}})}}
})
12.全屏功能
安装screenfull
npm i screenfull -S
使用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. 面包屑
我们需要展示当前路由的菜单的面包屑,先约定下数据,路由的name唯一且对应到菜单里的name且唯一,这样当我们切换到某一个路由的时候,就可以根据name到菜单里面递归的找到它所对应的所有父级。
数据
{"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"}]}]}]
}
修改menus.js
根据后台返回的菜单,递归出所有路由对应的带层级的菜单标题,放入路由的meta中
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// debuggerconst 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 = trueresolve()} catch (err) {reject(err)}})},}
})
修改Breadcrumb.vue
监听路由变化,从路由的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
这一步主要实现tagsView功能,tagsView中记录用户访问过的菜单,并且能够根据需要关闭它,但是主页这个tag要一直保留。
我们把tag存放在pinia里面,tagsView组件通过计算属性引用pinia里面的tags,通过监听事件触发方法调用pinia里的方法
里面有个坑:点击关闭按钮的时候,需要阻止事件冒泡,否则,不仅会触发i这个icon的关闭的事件,又会触发div的点击事件,使用@click.stop去绑定
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) {debuggertagsViewStore.selectSpecifiedTag(tag)router.push({name:tag.name})}function closeTag(tag) {// 关闭的是不是当前激活的tag, 如果是当前激活的tag的话,就选择最后一个tag;如果不是当前激活的tag的话,就关掉它就行了let isCurrTagActiveClose = tag.isActivetagsViewStore.closeSpecifiedTag(tag)if(isCurrTagActiveClose) {// 选择最后面的tagdebuggerconsole.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) {debuggerconsole.log('doOnrouteChange->新路由', route.name);let currRouteName = route.namelet tagNameArr = []let flag = falsethis.tags.forEach(tag=>{tag.isActive = falseif(tag.name == currRouteName) {flag = truetag.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){debuggerlet index = -1;for(let i=0;i<this.tags.length;i++) {if(this.tags[i].name === tag.name) {index = ibreak}}if(index > -1) {this.tags.splice(index,1)}},selectSpecifiedTag(tag) {debuggerthis.tags.forEach(t=>{t.isActive = falseif(t.name == tag.name) {t.isActive = true}}) }}
})
15. vue指令控制权限按钮显示
通过vue的directive指令方式,当用户拥有指定的权限时,才显示按钮
后台返回权限数据
{"errno": 0,"errmsg": "成功","data": {"perms": ["user:list","user:add","user:remove","role:list","role:add","role:remove"]}
}
创建指令文件perms.js
import useMenu from '@/store/menu'
import { toRaw } from '@vue/reactivity'export default {hasPerms: {mounted(el,binding) {const menuStore = useMenu()let perms1 = menuStore.permsconsole.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)}},}
}
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 = Messagerapp.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')
loginApi.js中添加接口
// ...省略
export function getPerms() {return request({method:'get',url: 'test/getPerms'})
}
修改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// debuggerconst 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 = trueresolve()} catch (err) {reject(err)}})},}
})
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>
如下效果
16.添加过渡效果
面包屑和路由的切换过程,看上去特别的生硬,我们需要给它们添加过渡效果,就像下面这样
面包屑过渡效果
- 下面的过渡效果代码,是直接拷贝的官网,因为是用v-for遍历出来的,所以要用transition-group。
- 还要注意的是,元素绑定的key要必须唯一(不能使用索引当key哦),这是vue的要求,不然不会有动画效果的
<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>
路由过渡效果
修改Main.vue,以下仅把修改代码粘贴出来,其它部分省略了
<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>
相关文章:

vue3后台管理系统
后面可参考下:vue系列(三)——手把手教你搭建一个vue3管理后台基础模板 以下代码项目gitee地址 文章目录1. 初始化前端项目初始化项目添加加载效果配置 vite.config.js2. 使用路由安装路由配置路由配置别名和跳转安装pathvite.config.jsjsco…...
掷骰子式的乐趣:探究C语言生成随机数的奥秘
掷骰子式的乐趣:探究C语言生成随机数的奥秘一、引言二、C标准库的rand函数三、srand函数的使用四、基于时间的种子生成五、高质量随机数的应用一、引言 C语言中生成随机数是一项非常重要的功能,因为许多现代应用程序需要使用随机数。随机数可以用于密码…...

一线大厂软件测试常见面试题1500问,背完直接拿捏面试官,
三、测试理论 3.1 你们原来项目的测试流程是怎么样的? 我们的测试流程主要有三个阶段:需求了解分析、测试准备、测试执行。 1、需求了解分析阶段 我们的SE会把需求文档给我们自己先去了解一到两天这样,之后我们会有一个需求澄清会议, 我…...
小迪安全day12WEB漏洞-SQL注入之简要SQL注入
小迪安全day12WEB漏洞-SQL注入之简要SQL注入 注入产生原理详细分析 可控变量带入数据库查询变量未存在过滤或过滤不严谨 连接符区分 and是sql语句连接符,&是uel参数连接符 and 11是注入语句, &是添加一个新变量 数据库内容 数据库A 网站…...

自动化测试学习(七)-正则表达式,你真的会用吗?
目录 一、正则表达式在python中如何使用 二、用正则表达式匹配更多模式 三、常用字符分类的缩写代码 总结 所谓正则表达式(regex),就是一种模式匹配,学会用正则匹配,就可以达到事半功倍的效果。 一、正则表达式在…...
验证码——vue中后端返回的图片流如何显示
目录 前言 一、p调用接口获取验证码 canvas画布渲染? 二、后端返回图片(图片流),前端显示 1.blob 2.arraybuffer 总结 前言 登录界面经常会有验证码,验证码的实现方式也有很多,我目前做过以下两种&…...

聚观早报 | 拼多多驳斥Google的指控;80%美国人工作将被AI影响
今日要闻:拼多多驳斥Google“恶意软件”的指控;80%美国人工作将被AI影响;iPhone 15 Pro设计图上热搜;贾扬清离职阿里投身AI大模型创业;OPPO Find X6 系列发布拼多多驳斥Google“恶意软件”的指控 3 月 21 日࿰…...
define,typedef,inline 的区别
define 1.用于在代码中创建宏定义,将一个标识符替换为一个表达式或语句。例如: #define PI 3.14159 #define SQUARE(x) ((x) * (x))这样,程序中所有出现的 PI 都将被替换为 3.14159,SQUARE(x) 则被替换成了 (x) * (x)。 使用 #…...

百度文心一言正式亮相
OpenAI 刚发布了 GPT-4,百度预热已久的人工智能生成式对话产品也终于亮相了。昨天下午,文心一言 (ERNIE Bot)—— 百度全新一代知识增强大语言模型、文心大模型家族的新成员,正式在百度总部 “挥手点江山” 会议室里发布。 发布会一开场&…...

使用Android架构模板
使用Android架构模板 项目介绍 为了方便开发者引入最新的Android架构组建进行开发,Google官方给我们引入了一个架构模板,方便我们快速进入开发。 github地址: https://github.com/android/architecture-templates 该模板遵循官方架构指南 …...

2023年天津市逆向re2.exe解析-比较难(超详细)
2023年天津市逆向re2.exe解析(较难) 1.拖进IDA里进行分析2.动态调试3.编写EXP脚本获取FLAG4.获得FLAG1.拖进IDA里进行分析 进入主程序查看伪代码 发现一个循环,根据行为初步判定为遍历输入的字符并对其ascii^7进行加密 初步判断sub_1400ab4ec为比较输入和flag的函数 跟进u…...

springboot: mybatis动态拼接sql查询条件
目录 需求01: 根据不同类型 查询不同的订单名, 1. 书写订单 类型转换方法 2. 使用方式: 3.. 构建条件构造器并进行查询, 传递查询参数 4. Mapper 写法 5. 最核心位置 xml位置 6. sql执行效果: 需求01: 根据不同类型 查询不同的订单名, 条件也是不同的, 需要复用sql…...
最优化算法 - 动态规划算法
动态规划算法简介 动态规划(Dynamic programming)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 动态规划常常适用于有重叠子问题和最优子结构性质的问题…...

springCloud学习【3】之Docker(1)
文章目录一 Docker环境准备1.1 应用部署的环境问题1.2 Docker简介1.3 Docker解决操作系统环境差异1.4 Docker和虚拟机的区别1.5 Docker架构1.5.1 镜像和容器1.5.2 DockerHub1.5.3 Docker架构1.5.4 Docker工作流1.6 Docker的安装和启动1.7 安装步骤1.8 启动Docker1.9 配置镜像加…...

难以置信,已经有人用 ChatGPT 做 Excel 报表了?
要问2023年初科技领域什么最火,那自然是 ChatGPT。 ChatGPT 由人工智能研究实验室 OpenAI 于2022年11月30日推出。上线短短5天,用户数量已突破100万,在今年2月份,用户数量已经突破1亿。 ChatGPT 是一个超级智能聊天机器人&#…...
中断相关操作函数HAL_NVIC_SetPriority()、HAL_NVIC_EnableIRQ()
文章目录HAL_NVIC_SetPriority():设置中断优先级HAL_NVIC_EnableIRQ():使能中断结束HAL_NVIC_SetPriority():设置中断优先级 HAL_NVIC_SetPriority()函数是一个用于设置中断优先级的函数,其定义如下: void HAL_NVIC_…...

企业增长秘诀丨设立优质的帮助中心,加深用户产品使用深度,促进产品转化
客户的留存问题一直备受企业关注,留存率的高低反应了产品的真实状况,将直接影响企业后期的发展规划。下文将为大家剖析下产品中客户的转化流程,以及如何提高产品的使用深处与复购率。 产品中,从客户生命周期角度,可分…...

3.OSPF与BGP的联动
14.3实验3:OSPF与BGP联动配置 实验目的实验拓扑实验步骤 配置IP地址 AR1的配置 <Huawei>system-view Enter system view, return user view with CtrlZ. [Huawei]undo info-center enable Info: Information center is disabled. [Huawei]sysname AR1 …...

机器学习算法——决策树详解
文章目录前言:决策树的定义熵和信息熵的相关概念信息熵的简单理解经典的决策树算法ID3算法划分选择或划分标准——信息增益ID3算法的优缺点C4.5算法信息增益率划分选择或划分标准——Gini系数(CART算法)Gini系数计算举例CART算法的优缺点其他…...

配置Jenkins
目录 一.前言 1.1简介 1.2工作步骤图 二.配置jenkins部署项目 2.1项目新建 2.2jenkinsfile文件如下 三.jenkins工作台配置 3.1.点击新建item进入新建页面,输入任务名称,选择pipeline 3.2.选择第二个配置 3.4将ideal中jenkinsfile文件的路径粘入脚本路径中 3.5启动项目…...
浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)
✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义(Task Definition&…...

idea大量爆红问题解决
问题描述 在学习和工作中,idea是程序员不可缺少的一个工具,但是突然在有些时候就会出现大量爆红的问题,发现无法跳转,无论是关机重启或者是替换root都无法解决 就是如上所展示的问题,但是程序依然可以启动。 问题解决…...

C++_核心编程_多态案例二-制作饮品
#include <iostream> #include <string> using namespace std;/*制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料 利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶*//*基类*/ class AbstractDr…...
Leetcode 3577. Count the Number of Computer Unlocking Permutations
Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接:3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯,要想要能够将所有的电脑解锁&#x…...

学校招生小程序源码介绍
基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码,专为学校招生场景量身打造,功能实用且操作便捷。 从技术架构来看,ThinkPHP提供稳定可靠的后台服务,FastAdmin加速开发流程,UniApp则保障小程序在多端有良好的兼…...
质量体系的重要
质量体系是为确保产品、服务或过程质量满足规定要求,由相互关联的要素构成的有机整体。其核心内容可归纳为以下五个方面: 🏛️ 一、组织架构与职责 质量体系明确组织内各部门、岗位的职责与权限,形成层级清晰的管理网络…...
浅谈不同二分算法的查找情况
二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。 需要说明的是,以下二分算法都是基于有序序列为升序有序的情况…...

蓝桥杯3498 01串的熵
问题描述 对于一个长度为 23333333的 01 串, 如果其信息熵为 11625907.5798, 且 0 出现次数比 1 少, 那么这个 01 串中 0 出现了多少次? #include<iostream> #include<cmath> using namespace std;int n 23333333;int main() {//枚举 0 出现的次数//因…...
大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...

论文笔记——相干体技术在裂缝预测中的应用研究
目录 相关地震知识补充地震数据的认识地震几何属性 相干体算法定义基本原理第一代相干体技术:基于互相关的相干体技术(Correlation)第二代相干体技术:基于相似的相干体技术(Semblance)基于多道相似的相干体…...