mx-permission.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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._parentPath) {
  105. targetPath = to._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. commit('SET_SIDEBAR_ROUTERS', middleMatch.children || [])
  140. }
  141. }
  142. },
  143. RemoveRoutes({ commit }) {
  144. commit('REMOVE_ROUTES')
  145. },
  146. AccessDeepMenu({ commit }, route) {
  147. const path = route._firstLeafPath || route.path
  148. router.push(path)
  149. }
  150. }
  151. }
  152. function handleAsyncRouters(routes = [], parent = null, level = 0) {
  153. // NOTE: hht 22.8.29 解析思路,因为系统菜单全由后台配置,本地静态路由多是子页面等
  154. // 1 asyncRoutes尽可能提供多的便利强引用,以用来在匹配到路由之后,快速锁定top/middle/side三级菜单
  155. // 2 constantRoutes可以手动去挂载一个弱关联,表示它与后台配置的某个菜单是有关联的,这样在跳转后能正确定位菜单
  156. // 其中,1是必要处理步骤;2是充要处理步骤,没有关联即不更改菜单定位(效果不会比现在更差)
  157. routes.forEach(route => {
  158. // 递归处理所有的path, 保证每级路由的path都是绝对地址,方便跳转操作与路由path匹配
  159. handleAsyncRouterPath(route, parent)
  160. // 递归处理所有的redirect, 让每个非叶子结点路由能redirect到叶子结点
  161. handleAsyncRouterRedirect(route)
  162. // 递归处理所有的组件属性,从字符串转化为VUE组件
  163. handleAsyncRouterComponent(route)
  164. // 递归挂载本地强引用
  165. handleAsyncRouterLocalReference(route, parent, level)
  166. })
  167. }
  168. function handleAsyncRouterPath(route, parent) {
  169. if (!route.path) {
  170. console.log('handleAsyncRouterPath - route must have `path` value', route.name)
  171. return
  172. }
  173. if (!route.path.startsWith('/')) {
  174. route.path = (parent?.path || '') + '/' + route.path
  175. }
  176. if (route.children?.length) {
  177. route.children.forEach(child => handleAsyncRouterPath(child, route))
  178. } else {
  179. delete route.children
  180. delete route.redirect
  181. }
  182. }
  183. function handleAsyncRouterRedirect(route) {
  184. // 22.8.30 本来准备利用redirect属性来自动跳转,但后台路由可能会和本地路由重合
  185. // 比如`/user`, 后台路由会配置它跳'/user/info',再跳`/user/info/userinfo`
  186. // 但因为本地也配置了`/user`,本地静态路由优先级更高,它是没有配置redirect的,结果就是会404
  187. // 所以这里直接挂了`_firstLeafPath`, 提供了AccessDeepMenu方法来做跳转
  188. // 挂载两个后代path, 方便定制菜单
  189. const getFirstLeafPath = (routes, deep) => {
  190. const firstChild = routes?.first(c => !c.hidden)
  191. if (!firstChild) return ''
  192. if (!deep) return firstChild.path
  193. if (!firstChild.children?.length) return firstChild.path
  194. return getFirstLeafPath(firstChild.children, deep)
  195. }
  196. route._firstChildPath = getFirstLeafPath(route.children, false)
  197. route._firstLeafPath = getFirstLeafPath(route.children, true)
  198. route.children?.forEach(child => handleAsyncRouterRedirect(child))
  199. }
  200. function handleAsyncRouterComponent(route) {
  201. if (typeof route.component === 'string') {
  202. const sharedComponents = { Layout, ParentView }
  203. const sharedComponent = sharedComponents[route.component]
  204. route.component = sharedComponent || loadView(route.component)
  205. }
  206. route.children?.forEach(child => handleAsyncRouterComponent(child))
  207. }
  208. function handleAsyncRouterLocalReference(route, parent, level) {
  209. // 初始化meta.activeMenu
  210. if (route.meta && !route.meta?.hasOwnProperty('activeMenu')) route.meta.activeMenu = false
  211. // 挂parent可逆向追溯, 不能挂反向引用vuex内部会深拷贝!
  212. route._parentPath = parent?.path || ''
  213. // 挂level可直接定位top/middle/side
  214. route._level = level
  215. route.children?.forEach(child => handleAsyncRouterLocalReference(child, route, level + 1))
  216. }
  217. function traceAncestorRoutesInMenus(routes, path, ancestors) {
  218. // 从routes中找到目标路径,才算完全成功
  219. const fullMath = routes.first(r => r.path == path)
  220. if (fullMath) {
  221. ancestors.fullMatched = true
  222. ancestors.matches.push(fullMath)
  223. return
  224. }
  225. const partialMath = routes.first(r => path.startsWith(r.path))
  226. if (partialMath) {
  227. ancestors.fullMatched = false
  228. ancestors.matches.push(partialMath)
  229. // 继续往下找
  230. if (partialMath.children?.length) {
  231. traceAncestorRoutesInMenus(partialMath.children, path, ancestors)
  232. }
  233. }
  234. }
  235. export const loadView = (view) => { // 路由懒加载
  236. return (resolve) => require([`@/views/${view}`], resolve)
  237. }
  238. export default permission