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启动项目…...

第19节 Node.js Express 框架
Express 是一个为Node.js设计的web开发框架,它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用,和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...

【入坑系列】TiDB 强制索引在不同库下不生效问题
文章目录 背景SQL 优化情况线上SQL运行情况分析怀疑1:执行计划绑定问题?尝试:SHOW WARNINGS 查看警告探索 TiDB 的 USE_INDEX 写法Hint 不生效问题排查解决参考背景 项目中使用 TiDB 数据库,并对 SQL 进行优化了,添加了强制索引。 UAT 环境已经生效,但 PROD 环境强制索…...

大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...

dedecms 织梦自定义表单留言增加ajax验证码功能
增加ajax功能模块,用户不点击提交按钮,只要输入框失去焦点,就会提前提示验证码是否正确。 一,模板上增加验证码 <input name"vdcode"id"vdcode" placeholder"请输入验证码" type"text&quo…...

相机从app启动流程
一、流程框架图 二、具体流程分析 1、得到cameralist和对应的静态信息 目录如下: 重点代码分析: 启动相机前,先要通过getCameraIdList获取camera的个数以及id,然后可以通过getCameraCharacteristics获取对应id camera的capabilities(静态信息)进行一些openCamera前的…...
聊一聊接口测试的意义有哪些?
目录 一、隔离性 & 早期测试 二、保障系统集成质量 三、验证业务逻辑的核心层 四、提升测试效率与覆盖度 五、系统稳定性的守护者 六、驱动团队协作与契约管理 七、性能与扩展性的前置评估 八、持续交付的核心支撑 接口测试的意义可以从四个维度展开,首…...

【Oracle】分区表
个人主页:Guiat 归属专栏:Oracle 文章目录 1. 分区表基础概述1.1 分区表的概念与优势1.2 分区类型概览1.3 分区表的工作原理 2. 范围分区 (RANGE Partitioning)2.1 基础范围分区2.1.1 按日期范围分区2.1.2 按数值范围分区 2.2 间隔分区 (INTERVAL Partit…...

AI病理诊断七剑下天山,医疗未来触手可及
一、病理诊断困局:刀尖上的医学艺术 1.1 金标准背后的隐痛 病理诊断被誉为"诊断的诊断",医生需通过显微镜观察组织切片,在细胞迷宫中捕捉癌变信号。某省病理质控报告显示,基层医院误诊率达12%-15%,专家会诊…...
《C++ 模板》
目录 函数模板 类模板 非类型模板参数 模板特化 函数模板特化 类模板的特化 模板,就像一个模具,里面可以将不同类型的材料做成一个形状,其分为函数模板和类模板。 函数模板 函数模板可以简化函数重载的代码。格式:templa…...