路由支持外链

image.png
点击跳转百度
image.png

2-1 创建SidebarItemLink.组件

在这个组件里来判断 是否是带协议链接 如果是带协议链接 menu-item里,就用a标签渲染 否则用router-link渲染 (注意 我们要把el-menu路由模式关掉 去掉el-menu组件上router属性)

image.png
src/layout/components/Sidebar/SidebarItemLink.vue

用到了component动态组件 并以插槽形式包裹 SIdebarItem组件

  1. <template>
  2. <component :is="type" v-bind="linkProps">
  3. <slot />
  4. </component>
  5. </template>
  6. <script lang="ts">
  7. import { computed, defineComponent } from 'vue'
  8. import { isExternal } from '@/utils/validate'
  9. // 针对路径是外链 就渲染为a标签 如果是正常路由路径 就渲染为 router-link
  10. // el-menu组件的router属性去掉了 不开启路由模式
  11. export default defineComponent({
  12. name: 'SidebarItemLink',
  13. props: {
  14. to: {
  15. type: String,
  16. required: true
  17. }
  18. },
  19. setup(props) {
  20. // 判断接收的路径 是不是外链
  21. const isExt = computed(() => isExternal(props.to))
  22. const type = computed(() => {
  23. if (isExt.value) {
  24. return 'a'
  25. }
  26. return 'router-link'
  27. })
  28. const linkProps = computed(() => {
  29. if (isExt.value) {
  30. return { // a 标签的一些原生属性
  31. href: props.to,
  32. target: '_blank',
  33. rel: 'noopener'
  34. }
  35. }
  36. // router-link只需一个to props
  37. return {
  38. to: props.to
  39. }
  40. })
  41. return {
  42. type,
  43. linkProps
  44. }
  45. }
  46. })
  47. </script>

2-2 SidebarItem中使用SidebarItemLink组件

src/layout/components/Sidebar/SidebarItem.vue
image.png
image.png

  1. <template>
  2. <div class="sidebar-item-container">
  3. <!-- 只渲染一个路由 并且路由只有一个子路由时直接渲染这个子路由 -->
  4. <template
  5. v-if="theOnlyOneChildRoute && !theOnlyOneChildRoute.children"
  6. >
  7. <sidebar-item-link
  8. v-if="theOnlyOneChildRoute.meta"
  9. :to="resolvePath(theOnlyOneChildRoute.path)"
  10. >
  11. <el-menu-item
  12. :index="resolvePath(theOnlyOneChildRoute.path)"
  13. >
  14. <svg-icon
  15. v-if="icon"
  16. class="menu-icon"
  17. :icon-class="icon"
  18. ></svg-icon>
  19. <template #title>
  20. <span>{{ theOnlyOneChildRoute.meta.title }}</span>
  21. </template>
  22. </el-menu-item>
  23. </sidebar-item-link>
  24. </template>
  25. <!-- 有多个子路由时 -->
  26. <el-submenu
  27. v-else
  28. :index="resolvePath(item.path)"
  29. popper-append-to-body
  30. >
  31. <template #title>
  32. <svg-icon
  33. v-if="item.meta.icon"
  34. class="menu-icon"
  35. :icon-class="item.meta.icon"
  36. ></svg-icon>
  37. <span class="submenu-title">{{ item.meta.title }}</span>
  38. </template>
  39. <sidebar-item
  40. v-for="child in item.children"
  41. :key="child.path"
  42. :is-nest="true"
  43. :item="child"
  44. :base-path="resolvePath(child.path)"
  45. >
  46. </sidebar-item>
  47. </el-submenu>
  48. </div>
  49. </template>
  50. <script lang="ts">
  51. import path from 'path'
  52. import { defineComponent, PropType, computed, toRefs } from 'vue'
  53. import { RouteRecordRaw } from 'vue-router'
  54. import SidebarItemLink from './SidebarItemLink.vue'
  55. import { isExternal } from '@/utils/validate'
  56. export default defineComponent({
  57. name: 'SidebarItem',
  58. components: {
  59. SidebarItemLink
  60. },
  61. props: {
  62. item: {
  63. type: Object as PropType<RouteRecordRaw>,
  64. required: true
  65. },
  66. basePath: {
  67. type: String,
  68. required: true
  69. }
  70. },
  71. setup (props) {
  72. const { item } = toRefs(props)
  73. // 子路由数量
  74. const showingChildNumber = computed(() => {
  75. const children = (props.item.children || []).filter(child => {
  76. if (child.meta && child.meta.hidden) return false
  77. return true
  78. })
  79. return children.length
  80. })
  81. // 只有一个可渲染的子路由直接渲染这个子路由 (由于我们有的路由 layout布局组件是一级路由 二级路由才是我们要渲染成菜单)
  82. const theOnlyOneChildRoute = computed(() => {
  83. // 多个children
  84. if (showingChildNumber.value > 1) {
  85. return null
  86. }
  87. // 子路由只有一个时 并且做个hidden筛选
  88. if (item.value.children) {
  89. for (const child of item.value.children) {
  90. if (!child.meta || !child.meta.hidden) { // hidden属性控制路由是否渲染成菜单
  91. return child
  92. }
  93. }
  94. }
  95. // showingChildNumber === 0
  96. // 没有可渲染chiildren时 把当前路由item作为仅有的子路由渲染
  97. return {
  98. ...props.item,
  99. path: '' // resolvePath避免resolve拼接时 拼接重复
  100. }
  101. })
  102. // menu icon
  103. const icon = computed(() => {
  104. // 子路由 如果没有icon就用父路由的
  105. return theOnlyOneChildRoute.value?.meta?.icon || (props.item.meta && props.item.meta.icon)
  106. })
  107. // 拼接路径 父路径+子路径(相对路径)
  108. const resolvePath = (childPath: string) => {
  109. // 如果是带协议外链 直接返回
  110. if (isExternal(childPath)) {
  111. return childPath
  112. }
  113. // 如果不是外链 需要和basePath拼接
  114. return path.resolve(props.basePath, childPath)
  115. }
  116. return {
  117. theOnlyOneChildRoute,
  118. icon,
  119. resolvePath
  120. }
  121. }
  122. })
  123. </script>
  124. <style lang="scss">
  125. .sidebar-item-container {
  126. .menu-icon {
  127. margin-right: 16px;
  128. margin-left: 5px;
  129. vertical-align: middle;
  130. }
  131. }
  132. </style>

2-3 关掉el-menu路由模式

image.png

2-4 添加外链路由

image.png

src/router/index.ts

  1. import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
  2. import Layout from '@/layout/index.vue'
  3. // 看作是异步获取路由
  4. export const asyncRoutes: Array<RouteRecordRaw> = [
  5. {
  6. path: '/documentation',
  7. component: Layout, // 布局组件作为一级路由
  8. redirect: '/documentation/index',
  9. children: [
  10. {
  11. path: 'index',
  12. name: 'Documentation',
  13. component: () => import(/* webpackChunkName: "documentation" */ '@/views/documentation/index.vue'),
  14. meta: {
  15. title: 'Documentation',
  16. icon: 'documentation'
  17. }
  18. }
  19. ]
  20. },
  21. {
  22. path: '/guide',
  23. component: Layout,
  24. redirect: '/guide/index',
  25. children: [
  26. {
  27. path: 'index',
  28. name: 'Guide',
  29. component: () => import(/* webpackChunkName: "guide" */ '@/views/guide/index.vue'),
  30. meta: {
  31. title: 'Guide',
  32. icon: 'guide'
  33. }
  34. }
  35. ]
  36. },
  37. {
  38. path: '/system',
  39. component: Layout,
  40. redirect: '/system/user',
  41. meta: {
  42. title: 'System',
  43. icon: 'lock'
  44. },
  45. children: [
  46. {
  47. path: 'menu',
  48. component: () => import(/* webpackChunkName: "menu" */ '@/views/system/menu.vue'),
  49. meta: {
  50. title: 'Menu Management'
  51. }
  52. },
  53. {
  54. path: 'role',
  55. component: () => import(/* webpackChunkName: "role" */ '@/views/system/role.vue'),
  56. meta: {
  57. title: 'Role Management'
  58. }
  59. },
  60. {
  61. path: 'user',
  62. component: () => import(/* webpackChunkName: "user" */ '@/views/system/user.vue'),
  63. meta: {
  64. title: 'User Management'
  65. }
  66. }
  67. ]
  68. },
  69. { // 外链路由
  70. path: '/external-link',
  71. component: Layout,
  72. children: [
  73. {
  74. path: 'https://www.baidu.com/',
  75. redirect: '/',
  76. meta: {
  77. title: 'External Link',
  78. icon: 'link'
  79. }
  80. }
  81. ]
  82. }
  83. ]
  84. export const constantRoutes: Array<RouteRecordRaw> = [
  85. {
  86. path: '/',
  87. component: Layout,
  88. redirect: '/dashboard',
  89. children: [
  90. {
  91. path: 'dashboard',
  92. name: 'Dashboard',
  93. component: () => import(/* webpackChunkName: "dashboard" */ '@/views/dashboard/index.vue'),
  94. meta: {
  95. title: 'Dashboard',
  96. icon: 'dashboard'
  97. }
  98. }
  99. ]
  100. }
  101. ]
  102. export const routes = [
  103. ...constantRoutes,
  104. ...asyncRoutes
  105. ]
  106. const router = createRouter({
  107. history: createWebHashHistory(),
  108. routes
  109. })
  110. export default router

2-5 去掉a标签默认样式

image.png
src/styles/index.scss

  1. @import './variables.scss';
  2. @import './sidebar.scss';
  3. html {
  4. height: 100%;
  5. box-sizing: border-box;
  6. }
  7. body {
  8. height: 100%;
  9. -moz-osx-font-smoothing: grayscale;
  10. -webkit-font-smoothing: antialiased;
  11. text-rendering: optimizeLegibility;
  12. font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
  13. }
  14. #app {
  15. height: 100%;
  16. }
  17. // a标签默认样式调整
  18. a:focus,
  19. a:active {
  20. outline: none;
  21. }
  22. a,
  23. a:focus,
  24. a:hover {
  25. cursor: pointer;
  26. color: inherit;
  27. text-decoration: none;
  28. }

本节参考源码

https://gitee.com/brolly/vue3-element-admin/commit/501926e86ea911b5d5d8c0744131a6c9bc245962