mx-permission.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import router, { constantRoutes } from '@/router'
  2. import { getRouters } from '@/api/menu'
  3. import Layout from '@/layout/index'
  4. import ParentView from '@/components/ParentView'
  5. import auth from '@/utils/auth'
  6. import { getFrontInitialRouters } from '@/api/system/user'
  7. const permission = {
  8. state: {
  9. // 路由缓存
  10. routeCache: {},
  11. // 全部路由 = 本地路由 + 后台路由
  12. routes: [],
  13. // 后台路由
  14. addRoutes: [],
  15. // 显示在最顶部的路由 = 后台路由中的一级路由
  16. topbarRouters: [],
  17. // 中部路由 = topRouters中选中的一级路由的二级子路由
  18. middlebarRouters: [],
  19. // 边部路由 = middleRouters中选中的二级路由的三级子路由
  20. sidebarRouters: [],
  21. // 当前路由
  22. currentRoute: null,
  23. activeTopPath: null,
  24. activeMiddlePath: null,
  25. activeSidePath: null,
  26. // 宽屏
  27. isWideScreen: false
  28. },
  29. mutations: {
  30. REMOVE_ROUTES: (state, token) => {
  31. state.routes = []
  32. state.addRoutes = []
  33. state.routeCache = {} // 全部清除
  34. },
  35. SET_ROUTES: (state, { routes, token }) => {
  36. state.addRoutes = routes
  37. state.routes = constantRoutes.concat(routes)
  38. state.routeCache[token] = routes
  39. },
  40. SET_TOPBAR_ROUTERS: (state, routes) => {
  41. state.topbarRouters = routes
  42. },
  43. SET_MIDDLEBAR_ROUTERS: (state, routes) => {
  44. state.middlebarRouters = routes
  45. },
  46. SET_SIDEBAR_ROUTERS: (state, routes) => {
  47. state.sidebarRouters = routes
  48. },
  49. SET_IS_WIDESCREEN: (state, trueOrFalse) => {
  50. state.isWideScreen = trueOrFalse
  51. },
  52. SET_CURRENT_ROUTE: (state, route) => {
  53. state.currentRoute = route
  54. },
  55. SET_ACTIVE_TOP_PATH: (state, path) => {
  56. state.activeTopPath = path
  57. },
  58. SET_ACTIVE_MIDDLE_PATH: (state, path) => {
  59. state.activeMiddlePath = path
  60. },
  61. SET_ACTIVE_SIDE_PATH: (state, path) => {
  62. state.activeSidePath = path
  63. }
  64. },
  65. actions: {
  66. // 生成路由
  67. CheckRoutes({ commit, state }, force) {
  68. const token = auth.getToken() || ''
  69. const cache = state.routeCache[token]
  70. let routeAdded = false
  71. if (cache && !force) return Promise.resolve(routeAdded) // 缓存不需要返回,已经在外部被调用过store.addRoutes
  72. return new Promise(resolve => {
  73. // 向后端请求路由数据
  74. const routeApi = token ? getRouters : getFrontInitialRouters
  75. routeApi().then(res => {
  76. const global404 = {
  77. path: '*',
  78. redirect: '/404',
  79. hidden: true
  80. }
  81. const topRoutes = res.data
  82. handleAsyncRouters(topRoutes)
  83. const addRoutes = topRoutes.concat([global404])
  84. // 目前只能确定全局路由与top路由,middle/side需要等路由跳转
  85. commit('SET_ROUTES', { routes: addRoutes, token })
  86. commit('SET_TOPBAR_ROUTERS', topRoutes)
  87. router.addRoutes(addRoutes)
  88. routeAdded = !!addRoutes.length
  89. resolve(routeAdded)
  90. })
  91. })
  92. },
  93. SetLocation({ commit, state }, { to, from }) {
  94. // 定位路由,显示合适的菜单
  95. // console.log('receive set-location request', to, from)
  96. commit('SET_CURRENT_ROUTE', to)
  97. commit('SET_IS_WIDESCREEN', to.meta?.isWideScreen)
  98. // 现在开始设置top/middle/side三级的当前路由
  99. // 有两项任务,1是要设置菜单集合,2是设置当前菜单
  100. const topRoutes = state.topbarRouters
  101. const ancestors = { fullMatched: false, matches: [] }
  102. let targetPath = to.path
  103. traceAncestorRoutesInMenus(topRoutes, targetPath, ancestors)
  104. if (!ancestors.fullMatched && to.meta?.parentPath) {
  105. targetPath = to.meta.parentPath
  106. ancestors.matches.length = 0 // clear for re-trace
  107. traceAncestorRoutesInMenus(topRoutes, targetPath, ancestors)
  108. }
  109. // 完全匹配,开始操作菜单
  110. if (ancestors.fullMatched) {
  111. const activeCommits = [
  112. {
  113. name: 'SET_ACTIVE_TOP_PATH',
  114. getPath: (routes, idx) => routes[idx]?.path || ''
  115. },
  116. {
  117. name: 'SET_ACTIVE_MIDDLE_PATH',
  118. getPath: (routes, idx) => routes[idx]?.path || ''
  119. },
  120. {
  121. name: 'SET_ACTIVE_SIDE_PATH',
  122. getPath: (routes, idx) => (routes[idx] && routes.last())?.path || ''
  123. }]
  124. // 设置高亮菜单
  125. activeCommits.forEach((config, idx) => {
  126. commit(config.name, config.getPath(ancestors.matches, idx))
  127. })
  128. // 设置middle/side菜单集合
  129. const topMatch = ancestors.matches[0]
  130. if (topMatch) {
  131. commit('SET_MIDDLEBAR_ROUTERS', topMatch.children || [])
  132. commit('SET_SIDEBAR_ROUTERS', []) // 先清空,可能立即又会被后续代码赋值
  133. } else {
  134. commit('SET_MIDDLEBAR_ROUTERS', [])
  135. commit('SET_SIDEBAR_ROUTERS', [])
  136. }
  137. const middleMatch = ancestors.matches[1]
  138. if (middleMatch &&
  139. ancestors.matches.length > 2 // NOTE: 有部分路由强制只匹配至2级菜单,如报告想全屏展示
  140. ) {
  141. commit('SET_SIDEBAR_ROUTERS', middleMatch.children || [])
  142. }
  143. }
  144. },
  145. RemoveRoutes({ commit }) {
  146. commit('REMOVE_ROUTES')
  147. },
  148. AccessDeepMenu({ commit }, route) {
  149. const path = route.meta?.firstLeafPath || route.path
  150. router.push(path)
  151. }
  152. }
  153. }
  154. function handleAsyncRouters(routes = [], parent = null, level = 0) {
  155. // NOTE: hht 22.8.29 解析思路,因为系统菜单全由后台配置,本地静态路由多是子页面等
  156. // 1 asyncRoutes尽可能提供多的便利强引用,以用来在匹配到路由之后,快速锁定top/middle/side三级菜单
  157. // 2 constantRoutes可以手动去挂载一个弱关联,表示它与后台配置的某个菜单是有关联的,这样在跳转后能正确定位菜单
  158. // 其中,1是必要处理步骤;2是充要处理步骤,没有关联即不更改菜单定位(效果不会比现在更差)
  159. routes.forEach(route => {
  160. // 递归处理所有的path, 保证每级路由的path都是绝对地址,方便跳转操作与路由path匹配
  161. handleAsyncRouterPath(route, parent)
  162. // 递归处理所有的redirect, 让每个非叶子结点路由能redirect到叶子结点
  163. handleAsyncRouterRedirect(route)
  164. // 递归处理所有的组件属性,从字符串转化为VUE组件
  165. handleAsyncRouterComponent(route)
  166. // 递归挂载本地强引用
  167. handleAsyncRouterLocalReference(route, parent, level)
  168. })
  169. }
  170. function handleAsyncRouterPath(route, parent) {
  171. if (!route.path) {
  172. console.log('handleAsyncRouterPath - route must have `path` value', route.name)
  173. return
  174. }
  175. if (!route.path.startsWith('/')) {
  176. route.path = (parent?.path || '') + '/' + route.path
  177. }
  178. if (route.children?.length) {
  179. route.children.forEach(child => handleAsyncRouterPath(child, route))
  180. } else {
  181. delete route.children
  182. delete route.redirect
  183. }
  184. }
  185. function handleAsyncRouterRedirect(route) {
  186. // 22.8.30 本来准备利用redirect属性来自动跳转,但后台路由可能会和本地路由重合
  187. // 比如`/user`, 后台路由会配置它跳'/user/info',再跳`/user/info/userinfo`
  188. // 但因为本地也配置了`/user`,本地静态路由优先级更高,它是没有配置redirect的,结果就是会404
  189. // 所以这里直接挂了`_firstLeafPath`, 提供了AccessDeepMenu方法来做跳转
  190. // 挂载两个后代path, 方便定制菜单
  191. const getFirstLeafPath = (routes, deep) => {
  192. const firstChild = routes?.first(c => !c.hidden)
  193. if (!firstChild) return ''
  194. if (!deep) return firstChild.path
  195. if (!firstChild.children?.length) return firstChild.path
  196. return getFirstLeafPath(firstChild.children, deep)
  197. }
  198. // 属性挂在meta上可以不受匹配方法等因素的影响,最大限度保留设置
  199. route.meta = route.meta || {}
  200. route.meta.firstChildPath = getFirstLeafPath(route.children, false)
  201. route.meta.firstLeafPath = getFirstLeafPath(route.children, true)
  202. route.children?.forEach(child => handleAsyncRouterRedirect(child))
  203. }
  204. function handleAsyncRouterComponent(route) {
  205. if (typeof route.component === 'string') {
  206. const sharedComponents = { Layout, ParentView }
  207. const sharedComponent = sharedComponents[route.component]
  208. route.component = sharedComponent || loadView(route.component)
  209. }
  210. route.children?.forEach(child => handleAsyncRouterComponent(child))
  211. }
  212. function handleAsyncRouterLocalReference(route, parent, level) {
  213. // 属性挂在meta上可以不受匹配方法等因素的影响,最大限度保留设置
  214. route.meta = route.meta || {}
  215. route.name = route.meta.routeName || route.name // overwrite route name by server meta config
  216. // 挂parent可逆向追溯, 不能挂反向引用vuex内部会深拷贝!
  217. // route.parent = parent
  218. route.meta.parentPath = parent?.path || ''
  219. // 挂level可直接定位top/middle/side
  220. route.meta.level = level
  221. route.children?.forEach(child => handleAsyncRouterLocalReference(child, route, level + 1))
  222. }
  223. function traceAncestorRoutesInMenus(routes, path, ancestors) {
  224. // 从routes中找到目标路径,才算完全成功
  225. const fullMath = routes.first(r => r.path == path)
  226. if (fullMath) {
  227. ancestors.fullMatched = true
  228. ancestors.matches.push(fullMath)
  229. return
  230. }
  231. const partialMath = routes.first(r => path.startsWith(r.path))
  232. if (partialMath) {
  233. ancestors.fullMatched = false
  234. ancestors.matches.push(partialMath)
  235. // 继续往下找
  236. if (partialMath.children?.length) {
  237. traceAncestorRoutesInMenus(partialMath.children, path, ancestors)
  238. }
  239. }
  240. }
  241. export const loadView = (view) => { // 路由懒加载
  242. return (resolve) => require([`@/views/${view}`], resolve)
  243. }
  244. export default permission