router.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. import { ElNotification } from 'element-plus'
  2. import { compact, isEmpty, reverse } from 'lodash-es'
  3. import type { RouteLocationRaw, RouteRecordRaw } from 'vue-router'
  4. import { isNavigationFailure, NavigationFailureType } from 'vue-router'
  5. import { i18n } from '/@/lang/index'
  6. import router from '/@/router/index'
  7. import adminBaseRoute from '/@/router/static/adminBase'
  8. import memberCenterBaseRoute from '/@/router/static/memberCenterBase'
  9. import { useConfig } from '/@/stores/config'
  10. import { useMemberCenter } from '/@/stores/memberCenter'
  11. import { useNavTabs } from '/@/stores/navTabs'
  12. import { useSiteConfig } from '/@/stores/siteConfig'
  13. import { isAdminApp } from '/@/utils/common'
  14. import { closeShade } from '/@/utils/pageShade'
  15. /**
  16. * 导航失败有错误消息的路由push
  17. * @param to — 导航位置,同 router.push
  18. */
  19. export const routePush = async (to: RouteLocationRaw) => {
  20. try {
  21. const failure = await router.push(to)
  22. if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
  23. ElNotification({
  24. message: i18n.global.t('utils.Navigation failed, navigation guard intercepted!'),
  25. type: 'error',
  26. })
  27. } else if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
  28. ElNotification({
  29. message: i18n.global.t('utils.Navigation failed, it is at the navigation target position!'),
  30. type: 'warning',
  31. })
  32. }
  33. } catch (error) {
  34. ElNotification({
  35. message: i18n.global.t('utils.Navigation failed, invalid route!'),
  36. type: 'error',
  37. })
  38. console.error(error)
  39. }
  40. }
  41. /**
  42. * 获取第一个菜单
  43. */
  44. export const getFirstRoute = (routes: RouteRecordRaw[]): false | RouteRecordRaw => {
  45. const routerPaths: string[] = []
  46. const routers = router.getRoutes()
  47. routers.forEach((item) => {
  48. if (item.path) routerPaths.push(item.path)
  49. })
  50. let find: boolean | RouteRecordRaw = false
  51. for (const key in routes) {
  52. if (routes[key].meta?.type == 'menu' && routerPaths.indexOf(routes[key].path) !== -1) {
  53. return routes[key]
  54. } else if (routes[key].children && routes[key].children?.length) {
  55. find = getFirstRoute(routes[key].children!)
  56. if (find) return find
  57. }
  58. }
  59. return find
  60. }
  61. /**
  62. * 打开侧边菜单
  63. * @param menu 菜单数据
  64. */
  65. export const onClickMenu = (menu: RouteRecordRaw) => {
  66. switch (menu.meta?.menu_type) {
  67. case 'iframe':
  68. case 'tab':
  69. routePush(menu.path)
  70. break
  71. case 'link':
  72. window.open(menu.path, '_blank')
  73. break
  74. default:
  75. ElNotification({
  76. message: i18n.global.t('utils.Navigation failed, the menu type is unrecognized!'),
  77. type: 'error',
  78. })
  79. break
  80. }
  81. const config = useConfig()
  82. if (config.layout.shrink) {
  83. closeShade(() => {
  84. config.setLayout('menuCollapse', true)
  85. })
  86. }
  87. }
  88. /**
  89. * 处理前台的路由
  90. * @param routes 路由规则
  91. * @param menus 会员中心菜单路由规则
  92. */
  93. export const handleFrontendRoute = (routes: any, menus: any) => {
  94. const siteConfig = useSiteConfig()
  95. const memberCenter = useMemberCenter()
  96. const viewsComponent = import.meta.glob('/src/views/frontend/**/*.vue')
  97. // 后端返回可能为 undefined/null,统一转成空数组避免长度读取报错
  98. const frontendRoutes = Array.isArray(routes) ? routes : []
  99. const memberMenus = Array.isArray(menus) ? menus : []
  100. if (frontendRoutes.length) {
  101. addRouteAll(viewsComponent, frontendRoutes, '', true)
  102. memberCenter.mergeAuthNode(handleAuthNode(frontendRoutes, '/'))
  103. siteConfig.setHeadNav(handleMenuRule(frontendRoutes, '/', ['nav']))
  104. memberCenter.mergeNavUserMenus(handleMenuRule(frontendRoutes, '/', ['nav_user_menu']))
  105. }
  106. if (memberMenus.length && isEmpty(memberCenter.state.viewRoutes)) {
  107. addRouteAll(viewsComponent, memberMenus, memberCenterBaseRoute.name as string)
  108. const menuMemberCenterBaseRoute = (memberCenterBaseRoute.path as string) + '/'
  109. memberCenter.mergeAuthNode(handleAuthNode(memberMenus, menuMemberCenterBaseRoute))
  110. memberCenter.mergeNavUserMenus(handleMenuRule(memberMenus, '/', ['nav_user_menu']))
  111. memberCenter.setShowHeadline(memberMenus.length > 1)
  112. memberCenter.setViewRoutes(handleMenuRule(memberMenus, menuMemberCenterBaseRoute))
  113. }
  114. }
  115. /**
  116. * 处理后台的路由
  117. */
  118. export const handleAdminRoute = (routes: any) => {
  119. const viewsComponent = import.meta.glob('/src/views/backend/**/*.vue')
  120. addRouteAll(viewsComponent, routes, adminBaseRoute.name as string)
  121. const menuAdminBaseRoute = (adminBaseRoute.path as string) + '/'
  122. // 更新stores中的路由菜单数据
  123. const navTabs = useNavTabs()
  124. navTabs.setTabsViewRoutes(handleMenuRule(routes, menuAdminBaseRoute))
  125. navTabs.fillAuthNode(handleAuthNode(routes, menuAdminBaseRoute))
  126. }
  127. /**
  128. * 获取菜单的paths
  129. */
  130. export const getMenuPaths = (menus: RouteRecordRaw[]): string[] => {
  131. let menuPaths: string[] = []
  132. menus.forEach((item) => {
  133. menuPaths.push(item.path)
  134. if (item.children && item.children.length > 0) {
  135. menuPaths = menuPaths.concat(getMenuPaths(item.children))
  136. }
  137. })
  138. return menuPaths
  139. }
  140. /**
  141. * 获取菜单唯一标识
  142. * @param menu 菜单数据
  143. * @param prefix 前缀
  144. */
  145. export const getMenuKey = (menu: RouteRecordRaw, prefix = '') => {
  146. if (prefix === '') {
  147. prefix = menu.path
  148. }
  149. return `${prefix}-${menu.name as string}-${menu.meta && menu.meta.id ? menu.meta.id : ''}`
  150. }
  151. /**
  152. * 会员中心和后台的菜单处理
  153. */
  154. const handleMenuRule = (routes: any, pathPrefix = '/', type = ['menu', 'menu_dir']) => {
  155. const menuRule: RouteRecordRaw[] = []
  156. for (const key in routes) {
  157. if (routes[key].extend == 'add_rules_only') {
  158. continue
  159. }
  160. if (!type.includes(routes[key].type)) {
  161. continue
  162. }
  163. if (routes[key].type == 'menu_dir' && routes[key].children && !routes[key].children.length) {
  164. continue
  165. }
  166. if (
  167. ['route', 'menu', 'nav_user_menu', 'nav'].includes(routes[key].type) &&
  168. ((routes[key].menu_type == 'tab' && !routes[key].component) || (['link', 'iframe'].includes(routes[key].menu_type) && !routes[key].url))
  169. ) {
  170. continue
  171. }
  172. const currentPath = ['link', 'iframe'].includes(routes[key].menu_type) ? routes[key].url : pathPrefix + routes[key].path
  173. let children: RouteRecordRaw[] = []
  174. if (routes[key].children && routes[key].children.length > 0) {
  175. children = handleMenuRule(routes[key].children, pathPrefix, type)
  176. }
  177. menuRule.push({
  178. path: currentPath,
  179. name: routes[key].name,
  180. component: routes[key].component,
  181. meta: {
  182. id: routes[key].id,
  183. title: routes[key].title,
  184. icon: routes[key].icon,
  185. keepalive: routes[key].keepalive,
  186. menu_type: routes[key].menu_type,
  187. type: routes[key].type,
  188. },
  189. children: children,
  190. })
  191. }
  192. return menuRule
  193. }
  194. /**
  195. * 处理权限节点
  196. * @param routes 路由数据
  197. * @param prefix 节点前缀
  198. * @returns 组装好的权限节点
  199. */
  200. const handleAuthNode = (routes: any, prefix = '/') => {
  201. const authNode: Map<string, string[]> = new Map([])
  202. assembleAuthNode(routes, authNode, prefix, prefix)
  203. return authNode
  204. }
  205. const assembleAuthNode = (routes: any, authNode: Map<string, string[]>, prefix = '/', parent = '/') => {
  206. const authNodeTemp = []
  207. for (const key in routes) {
  208. if (routes[key].type == 'button') authNodeTemp.push(prefix + routes[key].name)
  209. if (routes[key].children && routes[key].children.length > 0) {
  210. assembleAuthNode(routes[key].children, authNode, prefix, prefix + routes[key].name)
  211. }
  212. }
  213. if (authNodeTemp && authNodeTemp.length > 0) {
  214. authNode.set(parent, authNodeTemp)
  215. }
  216. }
  217. /**
  218. * 动态添加路由-带子路由
  219. * @param viewsComponent
  220. * @param routes
  221. * @param parentName
  222. * @param analyticRelation 根据 name 从已注册路由分析父级路由
  223. */
  224. export const addRouteAll = (viewsComponent: Record<string, any>, routes: any, parentName: string, analyticRelation = false) => {
  225. for (const idx in routes) {
  226. if (routes[idx].extend == 'add_menu_only') {
  227. continue
  228. }
  229. if ((routes[idx].menu_type == 'tab' && viewsComponent[routes[idx].component]) || routes[idx].menu_type == 'iframe') {
  230. addRouteItem(viewsComponent, routes[idx], parentName, analyticRelation)
  231. }
  232. if (routes[idx].children && routes[idx].children.length > 0) {
  233. addRouteAll(viewsComponent, routes[idx].children, parentName, analyticRelation)
  234. }
  235. }
  236. }
  237. /**
  238. * 动态添加路由
  239. * @param viewsComponent
  240. * @param route
  241. * @param parentName
  242. * @param analyticRelation 根据 name 从已注册路由分析父级路由
  243. */
  244. export const addRouteItem = (viewsComponent: Record<string, any>, route: any, parentName: string, analyticRelation: boolean) => {
  245. let path = '',
  246. component
  247. if (route.menu_type == 'iframe') {
  248. path = (isAdminApp() ? adminBaseRoute.path : memberCenterBaseRoute.path) + '/iframe/' + encodeURIComponent(route.url)
  249. component = () => import('/@/layouts/common/router-view/iframe.vue')
  250. } else {
  251. path = parentName ? route.path : '/' + route.path
  252. component = viewsComponent[route.component]
  253. }
  254. if (route.menu_type == 'tab' && analyticRelation) {
  255. const parentNames = getParentNames(route.name)
  256. if (parentNames.length) {
  257. for (const key in parentNames) {
  258. if (router.hasRoute(parentNames[key])) {
  259. parentName = parentNames[key]
  260. break
  261. }
  262. }
  263. }
  264. }
  265. const routeBaseInfo: RouteRecordRaw = {
  266. path: path,
  267. name: route.name,
  268. component: component,
  269. meta: {
  270. title: route.title,
  271. extend: route.extend,
  272. icon: route.icon,
  273. keepalive: route.keepalive,
  274. menu_type: route.menu_type,
  275. type: route.type,
  276. url: route.url,
  277. addtab: true,
  278. },
  279. }
  280. if (parentName) {
  281. router.addRoute(parentName, routeBaseInfo)
  282. } else {
  283. router.addRoute(routeBaseInfo)
  284. }
  285. }
  286. /**
  287. * 根据name字符串,获取父级name组合的数组
  288. * @param name
  289. */
  290. const getParentNames = (name: string) => {
  291. const names = compact(name.split('/'))
  292. const tempNames = []
  293. const parentNames = []
  294. for (const key in names) {
  295. tempNames.push(names[key])
  296. if (parseInt(key) != names.length - 1) {
  297. parentNames.push(tempNames.join('/'))
  298. }
  299. }
  300. return reverse(parentNames)
  301. }