第六次课-动态菜单实现

飞一样的编程
飞一样的编程
擅长邻域:Java,MySQL,Linux,nginx,springboot,mongodb,微信小程序,vue

分类: springboot 专栏: 新版在线教育项目 标签: 动态菜单

2024-04-10 11:32:51 911浏览

动态菜单实现,权限控制,导航守卫

后端接口

权限模块准备工作

我们模仿若依的菜单效果

http://vue.ruoyi.vip/

根据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展开评论

暂无评论,快来写一下吧

展开评论

您可能感兴趣的博客

客服QQ 1913284695