第六次课-动态菜单实现
分类: springboot 专栏: 新版在线教育项目 标签: 动态菜单
2024-04-10 11:32:51 911浏览
后端接口
权限模块准备工作
我们模仿若依的菜单效果
根据token获取用户菜单
这个是用户登录的时候使用
封装菜单接口
用递归查询的方式封装
@Data
public class MenuVo implements Serializable {
private String id;
private String pid;
private String path;
private Boolean hidden;
private String component;
private String name;
private String redirect;
private MetaVo meta;
private List<MenuVo> children;
}
@Data
public class MetaVo {
private String title;
private String icon;
}
采用递归的方式返回菜单
可以考虑把查询菜单的放进redis缓存,避免每次登陆都要查一遍数据库。
前端页面
参考:https://juejin.cn/post/7002874867167019045
修改路由文件
位置:src\router\index.js
export const constantRoutes = [
{ path: '/login', component: () => import('@/views/login/index'), hidden: true },
{ path: '/404', component: () => import('@/views/404'), hidden: true },
{
path: '/',
component: Layout,
redirect: '/dashboard',
name: '首页',
hidden: true,
children: [{
path: 'dashboard',
component: () => import('@/views/dashboard/index')
}]
},
]
修改src\store\modules\user.js
const user = {
state: {
token: getToken(),
name: '',
avatar: '',
menus: [] // 菜单权限
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_MENUS: (state, menus) => {
state.menus = menus // 菜单权限
}
},
const actions = {
// user login
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
commit('SET_TOKEN', data)
setToken(data)
resolve()
}).catch(error => {
reject(error)
})
})
},
// get user info
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response
if (!data) {
return reject('Verification failed, please Login again.')
}
const { name, avatar,menus } = data
menus.push( {
'path': '*',
'redirect': '/404',
'hidden': 'true'
})
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
commit('SET_MENUS', menus)
resolve(data)
}).catch(error => {
reject(error)
})
})
},
const actions = {
// user login
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
commit('SET_TOKEN', data)
setToken(data)
resolve()
}).catch(error => {
reject(error)
})
})
},
// get user info
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response
if (!data) {
return reject('Verification failed, please Login again.')
}
const { name, avatar,menus } = data
menus.push( {
'path': '*',
'redirect': '/404',
'hidden': 'true'
})
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
commit('SET_MENUS', menus)
resolve(data)
}).catch(error => {
reject(error)
})
})
},
}
}
主要改的地方:
修改getters.js存储信息
位置:src\store\getters.js
const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
roles: state => state.user.roles,// 角色权限控制按钮
menus: state => state.user.menus, // 菜单权限
}
export default getters
修改permission.js的vuex导航守卫
自学:https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
位置:src\permission.js
注意:引入Layout
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
import Layout from '@/layout/index'// 引入Layout
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login'] // no redirect whitelist
router.beforeEach(async(to, from, next) => {
// start progress bar
NProgress.start()
// set page title
document.title = getPageTitle(to.meta.title)
// determine whether the user has logged in
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done()
} else {
const hasGetUserInfo = store.getters.name
if (hasGetUserInfo) {
next()
} else {
try {
// get user info
await store.dispatch('user/getInfo')
//就可以拿到当前登录者的菜单
// **在这里做动态路由**
if (store.getters.menus.length < 1) {
global.antRouter = []
next()
}
//store.getters.menus是长啥样???
const menus = filterAsyncRouter(store.getters.menus) // 过滤路由
router.addRoutes(menus) // 动态添加路由
global.antRouter = menus // 将路由数据传递给全局变量,做侧边栏菜单渲染工作
next({ ...to, replace: true })
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
// other pages that do not have permission to access are redirected to the login page.
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
function filterAsyncRouter(asyncRouterMap) { // 遍历后台传来的路由字符串,转换为组件对象
try {
const accessedRouters = asyncRouterMap.filter(route => {
if (route.component) {
if (route.component === 'Layout') { // Layout组件特殊处理
route.component = Layout
} else {
const component = route.component
route.component = resolve => {
require(['@/views' + component + '.vue'], resolve)
}
}
}
if (route.children && route.children.length) {
route.children = filterAsyncRouter(route.children)
}
return true
})
return accessedRouters
} catch (e) {
console.log(e)
}
}
替换成动态路由
位置:src\views\layout\components\Sidebar\index.vue
routes() {
return this.$router.options.routes.concat(global.antRouter) // 新路由连接
},
补充说明
上传组件设置请求头
项目加了安全框架以后,由于上传图片的请求是由<el-upload>组件直接发送到服务端的,没有经过request.js添加请求头 ‘Authorization’,所以会被登录过滤器拦截,显示未登录,这个时候可以在 <el-upload>组件中设置请求头
发布课程-课程分类二级联动
递归查询出的分类已经是把二级挂在了一级上了,所以没联动的时候(选中一级后找二级的时候),没必要再调接口查一遍。
<el-form-item label="课程分类">
<el-select
v-model="courseInfo.subjectParentId"
placeholder="一级分类" @change="subjectLevelOneChanged">
<el-option
v-for="subject in subjectOneList"
:key="subject.id"
:label="subject.title"
:value="subject.id"/>
</el-select>
<!-- 二级分类 -->
<el-select v-model="courseInfo.subjectId" placeholder="二级分类">
<el-option
v-for="subject in subjectTwoList"
:key="subject.id"
:label="subject.title"
:value="subject.id"/>
</el-select>
</el-form-item>
//点击某个一级分类,触发change,显示对应二级分类
subjectLevelOneChanged(value) {
//把二级分类id值清空
this.courseInfo.subjectId = ''
//value就是一级分类id值
//遍历所有的分类,包含一级和二级
for(var i=0;i<this.subjectOneList.length;i++) {
//每个一级分类
var oneSubject = this.subjectOneList[i]
//判断:所有一级分类id 和 点击一级分类id是否一样
if(value === oneSubject.id) {
//从一级分类获取里面所有的二级分类
this.subjectTwoList = oneSubject.children
}
}
},
//查询所有的一级分类
getOneSubject() {
subject.getSubjectList()
.then(response => {
this.subjectOneList = response.data.list
})
},
//根据课程id查询
getInfo() {
course.getCourseInfoId(this.courseId)
.then(response => {
//在courseInfo课程基本信息,包含 一级分类id 和 二级分类id
this.courseInfo = response.data.courseInfoVo
//1 查询所有的分类,包含一级和二级
subject.getSubjectList()
.then(response => {
//2 获取所有一级分类
this.subjectOneList = response.data.list
//3 把所有的一级分类数组进行遍历,
for(var i=0;i<this.subjectOneList.length;i++) {
//获取每个一级分类
var oneSubject = this.subjectOneList[i]
//比较当前courseInfo里面一级分类id和所有的一级分类id
if(this.courseInfo.subjectParentId == oneSubject.id) {
//获取一级分类所有的二级分类
this.subjectTwoList = oneSubject.children
}
}
})
//初始化所有讲师
this.getListTeacher()
})
},
springboot文件上传
- 上传controller
@RestController
@RequestMapping("/upload")
@CrossOrigin
public class UploadFileController {
@PostMapping("/avatar")
public ResultDto uploadAvatar(MultipartFile file){
SimpleDateFormat format= new SimpleDateFormat("yyyy/MM/dd");
String filePath = format.format(new Date());
try {
File directory=new File("D://avatar/"+filePath);
if (!directory.exists()) {
directory.mkdirs();
}
//文件名最好重命名
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = UUID.randomUUID().toString().replace("-", "");
File saveFile= new File("D://avatar/"+filePath+"/"+fileName+suffix);
file.transferTo(saveFile);
return ResultDto.success("上传成功","/avatar/"+filePath+"/"+fileName+suffix);
} catch (IOException e) {
e.printStackTrace();
throw new JfException(50000,"头像上传失败");
}
}
}
- 配置文件虚拟路径
web:
upload-path: D:/upload/
spring:
mvc:
static-path-pattern: /**
web:
resources:
static-locations:
file:${web.upload-path},
classpath:/META-INF/resources/,
classpath:/resources/,
classpath:/static/,
classpath:/public/
- springsecurity设置白名单
web.ignoring().requestMatchers("/avatar/**")
vue的js中打开一个新窗口
const { href } = this.$router.resolve({
path: '/'
});
window.open(href, '_blank');
video视频播放
<video height="600px" width="100%" src="http://localhost/teacher/avatar/2023/05/19/test.mp4" controls="controls"></video>
好博客就要一起分享哦!分享海报
此处可发布评论
评论(0)展开评论
展开评论
您可能感兴趣的博客