浏览代码

feat(router): Add dynamic routing

陈凯龙 3 年之前
父节点
当前提交
b218ccc9cc

+ 4 - 1
src/api/login/index.ts

@@ -4,5 +4,8 @@ import type { UserLoginType } from './types'
 const { request } = useAxios()
 
 export const loginApi = (data: UserLoginType) => {
-  return request({ url: '/user/login', method: 'post', data })
+  return request({ url: '/user/login', method: 'post', data } as AxiosConfig<
+    Recordable,
+    UserLoginType
+  >)
 }

+ 2 - 8
src/hooks/web/useAxios.ts

@@ -7,14 +7,8 @@ import { config } from '@/config/axios/config'
 const { default_headers } = config
 
 export function useAxios() {
-  function request({
-    url,
-    method,
-    params,
-    data,
-    headersType,
-    responseType
-  }: AxiosConfig): AxiosPromise {
+  function request(option: AxiosConfig): AxiosPromise {
+    const { url, method, params, data, headersType, responseType } = option
     return service({
       url: url,
       method,

+ 16 - 15
src/hooks/web/useNProgress.ts

@@ -1,4 +1,4 @@
-import { watch, ref, nextTick, unref } from 'vue'
+import { nextTick, unref } from 'vue'
 import type { NProgressOptions } from 'nprogress'
 import NProgress from 'nprogress'
 import 'nprogress/nprogress.css'
@@ -7,26 +7,27 @@ import { useCssVar } from '@vueuse/core'
 const primaryColor = useCssVar('--el-color-primary', document.documentElement)
 
 export function useNProgress() {
-  const isLoading = ref(false)
   NProgress.configure({ showSpinner: false } as NProgressOptions)
+  initColor()
 
-  watch(
-    () => isLoading.value,
-    async (loading: boolean) => {
-      loading ? NProgress.start() : NProgress.done()
-      await nextTick()
-      const bar = document.getElementById('nprogress')?.getElementsByClassName('bar')[0] as ElRef
-      if (bar) {
-        bar.style.background = unref(primaryColor.value)
-      }
+  async function initColor() {
+    await nextTick()
+    const bar = document.getElementById('nprogress')?.getElementsByClassName('bar')[0] as ElRef
+    if (bar) {
+      bar.style.background = unref(primaryColor.value)
     }
-  )
+  }
+
+  function start() {
+    NProgress.start()
+  }
 
-  function toggle() {
-    isLoading.value = !isLoading.value
+  function done() {
+    NProgress.done()
   }
 
   return {
-    toggle
+    start,
+    done
   }
 }

+ 13 - 6
src/layout/Layout.vue

@@ -1,9 +1,16 @@
-<script setup lang="ts"></script>
+<script setup lang="ts">
+// import { computed } from 'vue'
+// const getCaches = computed((): string[] => {
+//   return []
+// })
+</script>
 
 <template>
-  <section>
-    <router-view v-slot="{ Component, route }">
-      <component :is="Component" :key="route.fullPath" />
-    </router-view>
-  </section>
+  <RouterView>
+    <template #default="{ Component, route }">
+      <KeepAlive>
+        <Component :is="Component" :key="route.fullPath" />
+      </KeepAlive>
+    </template>
+  </RouterView>
 </template>

+ 9 - 2
src/locales/en.ts

@@ -20,12 +20,19 @@ export default {
     login: 'Sign in',
     otherLogin: 'Sign in with',
     remember: 'Remember me',
-    forgetPassword: 'Forget password'
+    forgetPassword: 'Forget password',
+    usernamePlaceholder: 'username is admin or test',
+    passwordPlaceholder: 'password is admin or test'
   },
   router: {
     login: 'Login',
     level: 'Multi level menu',
-    menu: 'Menu'
+    menu: 'Menu',
+    menu1: 'Menu1',
+    menu11: 'Menu1-1',
+    menu111: 'Menu1-1-1',
+    menu12: 'Menu1-2',
+    menu2: 'Menu2'
   },
   mock: {
     loginErr: 'Wrong account or password'

+ 9 - 2
src/locales/zh-CN.ts

@@ -20,12 +20,19 @@ export default {
     login: '登录',
     otherLogin: '其他登录方式',
     remember: '记住我',
-    forgetPassword: '忘记密码'
+    forgetPassword: '忘记密码',
+    usernamePlaceholder: '用户名为 admin 或者 test ',
+    passwordPlaceholder: '密码为 admin 或者 test '
   },
   router: {
     login: '登录',
     level: '多级菜单',
-    menu: '菜单'
+    menu: '菜单',
+    menu1: '菜单1',
+    menu11: '菜单1-1',
+    menu111: '菜单1-1-1',
+    menu12: '菜单1-2',
+    menu2: '菜单2'
   },
   mock: {
     loginErr: '账号或密码错误'

+ 22 - 22
src/permission.ts

@@ -1,40 +1,40 @@
 import router from './router'
 import { useAppStoreWithOut } from '@/store/modules/app'
 import { useCache } from '@/hooks/web/useCache'
-// import type { RouteRecordRaw } from 'vue-router'
+import type { RouteRecordRaw } from 'vue-router'
 import { useTitle } from '@/hooks/web/useTitle'
 import { useNProgress } from '@/hooks/web/useNProgress'
+import { usePermissionStoreWithOut } from '@/store/modules/permission'
+
+const permissionStore = usePermissionStoreWithOut()
 
 const appStore = useAppStoreWithOut()
 
 const { wsCache } = useCache()
 
-const { toggle } = useNProgress()
+const { start, done } = useNProgress()
 
 const whiteList = ['/login'] // 不重定向白名单
 
-router.beforeEach((to, from, next) => {
-  console.log(from)
-  toggle()
+router.beforeEach(async (to, from, next) => {
+  start()
   if (wsCache.get(appStore.getUserInfo)) {
     if (to.path === '/login') {
       next({ path: '/' })
     } else {
-      // if (permissionStore.getIsAddRouters) {
-      //   next()
-      //   return
-      // }
-      // permissionStore.generateRoutes().then(() => {
-      //   permissionStore.addRouters.forEach(async (route) => {
-      //     await router.addRoute(route as RouteRecordRaw) // 动态添加可访问路由表
-      //   })
-      //   const redirectPath = from.query.redirect || to.path
-      //   const redirect = decodeURIComponent(redirectPath as string)
-      //   const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
-      //   permissionStore.setIsAddRouters(true)
-      //   next(nextData)
-      // })
-      next()
+      if (permissionStore.getIsAddRouters) {
+        next()
+        return
+      }
+      await permissionStore.generateRoutes()
+      permissionStore.getAddRouters.forEach((route) => {
+        router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表
+      })
+      const redirectPath = from.query.redirect || to.path
+      const redirect = decodeURIComponent(redirectPath as string)
+      const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
+      permissionStore.setIsAddRouters(true)
+      next(nextData)
     }
   } else {
     if (whiteList.indexOf(to.path) !== -1) {
@@ -45,7 +45,7 @@ router.beforeEach((to, from, next) => {
   }
 })
 
-router.afterEach(async (to) => {
+router.afterEach((to) => {
   useTitle(to?.meta?.title as string)
-  toggle() // 结束Progress
+  done() // 结束Progress
 })

+ 0 - 27
src/router/helper.ts

@@ -1,27 +0,0 @@
-import ParentLayout from '@/components/ParentView/index.vue'
-import type { RouteLocationNormalized, RouteRecordNormalized } from 'vue-router'
-
-export const getParentLayout = (name: string) => {
-  return () =>
-    new Promise((resolve) => {
-      resolve({
-        ...ParentLayout,
-        name
-      })
-    })
-}
-
-export function getRoute(route: RouteLocationNormalized): RouteLocationNormalized {
-  if (!route) return route
-  const { matched, ...opt } = route
-  return {
-    ...opt,
-    matched: (matched
-      ? matched.map((item) => ({
-          meta: item.meta,
-          name: item.name,
-          path: item.path
-        }))
-      : undefined) as RouteRecordNormalized[]
-  }
-}

+ 63 - 62
src/router/index.ts

@@ -1,8 +1,9 @@
 import { createRouter, createWebHashHistory } from 'vue-router'
 import type { RouteRecordRaw } from 'vue-router'
 import type { App } from 'vue'
-// import { getParentLayout } from './helper'
+import { getParentLayout } from '@/utils/routerHelper'
 import { useI18n } from '@/hooks/web/useI18n'
+
 const { t } = useI18n()
 
 /* Layout */
@@ -15,7 +16,7 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
     name: 'Redirect',
     children: [
       {
-        path: '/redirect/:path*',
+        path: '/redirect/:path(.*)',
         name: 'Redirect',
         component: () => import('@/views/Redirect/Redirect.vue'),
         meta: {}
@@ -37,66 +38,66 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
   }
 ]
 
-// export const asyncRouterMap: AppRouteRecordRaw[] = [
-//   {
-//     path: '/level',
-//     component: Layout,
-//     redirect: '/level/menu1/menu1-1/menu1-1-1',
-//     name: 'Level',
-//     meta: {
-//       title: t('router.level')
-//     },
-//     children: [
-//       {
-//         path: 'menu1',
-//         name: 'Menu1',
-//         component: getParentLayout('Menu1'),
-//         redirect: '/level/menu1/menu1-1/menu1-1-1',
-//         meta: {
-//           title: `${t('router.menu')}1`
-//         },
-//         children: [
-//           {
-//             path: 'menu1-1',
-//             name: 'Menu11',
-//             component: getParentLayout('Menu11Demo'),
-//             redirect: '/level/menu1/menu1-1/menu1-1-1',
-//             meta: {
-//               title: `${t('router.menu')}1-1`,
-//               alwaysShow: true
-//             },
-//             children: [
-//               {
-//                 path: 'menu1-1-1',
-//                 name: 'Menu111',
-//                 component: () => import('@/views/Level/Menu111.vue'),
-//                 meta: {
-//                   title: `${t('router.menu')}1-1-1`
-//                 }
-//               }
-//             ]
-//           },
-//           {
-//             path: 'menu1-2',
-//             name: 'Menu12',
-//             component: () => import('@/views/Level/Menu12.vue'),
-//             meta: {
-//               title: `${t('router.menu')}1-2`
-//             }
-//           }
-//         ]
-//       },
-//       {
-//         path: 'menu2',
-//         name: 'Menu2Demo',
-//         component: () => import('@/views/Level/Menu2.vue'),
-//         meta: {
-//           title: `${t('router.menu')}2`
-//         }
-//       }
-//     ]
-//   }
-// ]
+export const asyncRouterMap: AppRouteRecordRaw[] = [
+  {
+    path: '/level',
+    component: Layout,
+    redirect: '/level/menu1/menu1-1/menu1-1-1',
+    name: 'Level',
+    meta: {
+      title: t('router.level')
+    },
+    children: [
+      {
+        path: 'menu1',
+        name: 'Menu1',
+        component: getParentLayout(),
+        redirect: '/level/menu1/menu1-1/menu1-1-1',
+        meta: {
+          title: t('router.menu1')
+        },
+        children: [
+          {
+            path: 'menu1-1',
+            name: 'Menu11',
+            component: getParentLayout(),
+            redirect: '/level/menu1/menu1-1/menu1-1-1',
+            meta: {
+              title: t('router.menu11'),
+              alwaysShow: true
+            },
+            children: [
+              {
+                path: 'menu1-1-1',
+                name: 'Menu111',
+                component: () => import('@/views/Level/Menu111.vue'),
+                meta: {
+                  title: t('router.menu111')
+                }
+              }
+            ]
+          },
+          {
+            path: 'menu1-2',
+            name: 'Menu12',
+            component: () => import('@/views/Level/Menu12.vue'),
+            meta: {
+              title: t('router.menu12')
+            }
+          }
+        ]
+      },
+      {
+        path: 'menu2',
+        name: 'Menu2Demo',
+        component: () => import('@/views/Level/Menu2.vue'),
+        meta: {
+          title: t('router.menu2')
+        }
+      }
+    ]
+  }
+]
 
 const router = createRouter({
   history: createWebHashHistory(),

+ 89 - 173
src/store/modules/permission.ts

@@ -1,180 +1,96 @@
-// import { defineStore } from 'pinia'
-// import { asyncRouterMap, constantRouterMap } from '@/router'
+import { defineStore } from 'pinia'
+import { asyncRouterMap, constantRouterMap } from '@/router'
 // import { useCache } from '@/hooks/web/useCache'
-// import { getParentLayout } from '@/router/helper'
-// import { store } from '../index'
+import { flatMultiLevelRoutes } from '@/utils/routerHelper'
+// import { generateRoutesFn1, generateRoutesFn2, flatMultiLevelRoutes } from '@/utils/routerHelper'
+import { store } from '../index'
 // import { useAppStoreWithOut } from '@/store/modules/app'
-// import { isUrl } from '@/utils/is'
-// import { deepClone } from '@/utils'
+import { cloneDeep } from 'lodash-es'
 
 // const { wsCache } = useCache()
 
 // const appStore = useAppStoreWithOut()
 
-// const modules = import.meta.glob('../../views/**/*.{vue,tsx}')
-
-// /* Layout */
-// const Layout = () => import('@/layout/index.vue')
-
-// export interface PermissionState {
-//   routers: AppRouteRecordRaw[]
-//   addRouters: AppRouteRecordRaw[]
-//   isAddRouters: boolean
-//   activeTab: string
-//   menuTabRouters: AppRouteRecordRaw[]
-// }
-
-// export const usePermissionStore = defineStore({
-//   id: 'permission',
-//   state: (): PermissionState => ({
-//     routers: [],
-//     addRouters: [],
-//     isAddRouters: false,
-//     menuTabRouters: [],
-//     activeTab: ''
-//   }),
-//   getters: {
-//     getRouters(): AppRouteRecordRaw[] {
-//       return this.routers
-//     },
-//     getAddRouters(): AppRouteRecordRaw[] {
-//       return this.addRouters
-//     },
-//     getIsAddRouters(): boolean {
-//       return this.isAddRouters
-//     },
-//     getActiveTab(): string {
-//       return this.activeTab
-//     },
-//     getMenuTabRouters(): AppRouteRecordRaw[] {
-//       return this.menuTabRouters
-//     }
-//   },
-//   actions: {
-//     generateRoutes(): Promise<unknown> {
-//       return new Promise<void>((resolve) => {
-//         // 路由权限控制
-//         let routerMap: AppRouteRecordRaw[] = []
-//         if (wsCache.get(appStore.getUserInfo).roleName === 'admin') {
-//           // 模拟前端控制权限
-//           routerMap = generateRoutesFn(deepClone(asyncRouterMap, ['component']))
-//         } else {
-//           // 模拟后端控制权限
-//           routerMap = getFilterRoutes(wsCache.get(appStore.getUserInfo).checkedNodes)
-//         }
-//         // const routerMap: AppRouteRecordRaw[] = generateRoutesFn(deepClone(asyncRouterMap, ['component']))
-//         // 动态路由,404一定要放到最后面
-//         this.addRouters = routerMap.concat([
-//           {
-//             path: '/:path(.*)*',
-//             redirect: '/404',
-//             name: '404',
-//             meta: {
-//               hidden: true,
-//               breadcrumb: false
-//             }
-//           }
-//         ])
-//         // 渲染菜单的所有路由
-//         this.routers = deepClone(constantRouterMap, ['component']).concat(routerMap)
-//         resolve()
-//       })
-//     },
-//     setIsAddRouters(state: boolean): void {
-//       this.isAddRouters = state
-//     },
-//     setMenuTabRouters(routers: AppRouteRecordRaw[]): void {
-//       this.menuTabRouters = routers
-//     },
-//     setAcitveTab(activeTab: string): void {
-//       this.activeTab = activeTab
-//     }
-//   }
-// })
-
-// // 路由过滤,主要用于权限控制
-// function generateRoutesFn(routes: AppRouteRecordRaw[], basePath = '/'): AppRouteRecordRaw[] {
-//   const res: AppRouteRecordRaw[] = []
-
-//   for (const route of routes) {
-//     // skip some route
-//     if (route.meta && route.meta.hidden && !route.meta.showMainRoute) {
-//       continue
-//     }
-
-//     let onlyOneChild: Nullable<string> = null
-
-//     if (route.children && route.children.length === 1 && !route.meta.alwaysShow) {
-//       onlyOneChild = (
-//         isUrl(route.children[0].path)
-//           ? route.children[0].path
-//           : path.resolve(path.resolve(basePath, route.path), route.children[0].path)
-//       ) as string
-//     }
-
-//     let data: Nullable<AppRouteRecordRaw> = null
-
-//     // 如不需要路由权限,可注释以下逻辑
-//     // 权限过滤,通过获取登录信息里面的角色权限,动态的渲染菜单。
-//     const list = wsCache.get(appStore.getUserInfo).checkedNodes
-//     // 开发者可以根据实际情况进行扩展
-//     for (const item of list) {
-//       // 通过路径去匹配
-//       if (isUrl(item.path) && (onlyOneChild === item.path || route.path === item.path)) {
-//         data = Object.assign({}, route)
-//       } else {
-//         const routePath = path.resolve(basePath, onlyOneChild || route.path)
-//         if (routePath === item.path || (route.meta && route.meta.followRoute === item.path)) {
-//           data = Object.assign({}, route)
-//         }
-//       }
-//     }
-//     // 如不需要路由权限,解注释下面一行
-//     // data = Object.assign({}, route)
-
-//     // recursive child routes
-//     if (route.children && data) {
-//       data.children = generateRoutesFn(route.children, path.resolve(basePath, data.path))
-//     }
-//     if (data) {
-//       res.push(data as AppRouteRecordRaw)
-//     }
-//   }
-//   return res
-// }
-
-// // 模拟后端过滤路由
-// function getFilterRoutes(routes: AppRouteRecordRaw[]): AppRouteRecordRaw[] {
-//   const res: AppRouteRecordRaw[] = []
-
-//   for (const route of routes) {
-//     const data: AppRouteRecordRaw = {
-//       path: route.path,
-//       name: route.name,
-//       redirect: route.redirect,
-//       meta: {}
-//     }
-//     data.meta = Object.assign({}, route.meta || {}, { title: route.meta.title })
-//     if (route.component) {
-//       // 动态加载路由文件,可根据实际情况进行自定义逻辑
-//       const component = route.component as string
-//       data.component = (
-//         component === '#'
-//           ? Layout
-//           : component.includes('##')
-//           ? getParentLayout(component.split('##')[1])
-//           : modules[`../../${route.component}.vue`] || modules[`../../${route.component}.tsx`]
-//       )
-//     }
-//     // recursive child routes
-//     if (route.children) {
-//       data.children = getFilterRoutes(route.children)
-//     }
-//     res.push(data as AppRouteRecordRaw)
-//   }
-//   return res
-// }
-
-// export function usePermissionStoreWithOut() {
-//   return usePermissionStore(store)
-// }
+export interface PermissionState {
+  routers: AppRouteRecordRaw[]
+  addRouters: AppRouteRecordRaw[]
+  isAddRouters: boolean
+  activeTab: string
+  menuTabRouters: AppRouteRecordRaw[]
+}
+
+export const usePermissionStore = defineStore({
+  id: 'permission',
+  state: (): PermissionState => ({
+    routers: [],
+    addRouters: [],
+    isAddRouters: false,
+    menuTabRouters: [],
+    activeTab: ''
+  }),
+  getters: {
+    getRouters(): AppRouteRecordRaw[] {
+      return this.routers
+    },
+    getAddRouters(): AppRouteRecordRaw[] {
+      return flatMultiLevelRoutes(cloneDeep(this.addRouters))
+    },
+    getIsAddRouters(): boolean {
+      return this.isAddRouters
+    },
+    getActiveTab(): string {
+      return this.activeTab
+    },
+    getMenuTabRouters(): AppRouteRecordRaw[] {
+      return this.menuTabRouters
+    }
+  },
+  actions: {
+    generateRoutes(): Promise<unknown> {
+      return new Promise<void>((resolve) => {
+        // 路由权限控制,如果不需要权限控制,请注释
+        // let routerMap: AppRouteRecordRaw[] = []
+        // if (wsCache.get(appStore.getUserInfo).username === 'admin') {
+        //   // 模拟前端控制权限
+        //   routerMap = generateRoutesFn1(cloneDeep(asyncRouterMap))
+        // } else {
+        //   // 模拟后端控制权限
+        //   routerMap = generateRoutesFn2(wsCache.get(appStore.getUserInfo).checkedNodes)
+        // }
+
+        // 不需要权限控制
+        const routerMap: AppRouteRecordRaw[] = cloneDeep(asyncRouterMap)
+
+        // 动态路由,404一定要放到最后面
+        this.addRouters = routerMap
+        // .concat([
+        //   {
+        //     path: '/:path(.*)*',
+        //     redirect: '/404',
+        //     name: '404',
+        //     meta: {
+        //       hidden: true,
+        //       breadcrumb: false
+        //     }
+        //   }
+        // ])
+        // 渲染菜单的所有路由
+        this.routers = cloneDeep(constantRouterMap).concat(routerMap)
+        resolve()
+      })
+    },
+    setIsAddRouters(state: boolean): void {
+      this.isAddRouters = state
+    },
+    setMenuTabRouters(routers: AppRouteRecordRaw[]): void {
+      this.menuTabRouters = routers
+    },
+    setAcitveTab(activeTab: string): void {
+      this.activeTab = activeTab
+    }
+  }
+})
+
+export function usePermissionStoreWithOut() {
+  return usePermissionStore(store)
+}

+ 0 - 0
src/store/modules/tagsView.ts


+ 192 - 0
src/utils/routerHelper.ts

@@ -0,0 +1,192 @@
+import { createRouter, createWebHashHistory } from 'vue-router'
+import type { Router, RouteLocationNormalized, RouteRecordNormalized } from 'vue-router'
+import { isUrl } from '@/utils/is'
+import { useCache } from '@/hooks/web/useCache'
+import { useAppStoreWithOut } from '@/store/modules/app'
+import { omit, cloneDeep } from 'lodash-es'
+
+const appStore = useAppStoreWithOut()
+
+const { wsCache } = useCache()
+
+const modules = import.meta.glob('../../views/**/*.{vue,tsx}')
+
+/* Layout */
+const Layout = () => import('@/layout/index.vue')
+
+export const getParentLayout = () => {
+  return () =>
+    new Promise((resolve) => {
+      resolve({
+        name: 'ParentLayout'
+      })
+    })
+}
+
+export function getRoute(route: RouteLocationNormalized): RouteLocationNormalized {
+  if (!route) return route
+  const { matched, ...opt } = route
+  return {
+    ...opt,
+    matched: (matched
+      ? matched.map((item) => ({
+          meta: item.meta,
+          name: item.name,
+          path: item.path
+        }))
+      : undefined) as RouteRecordNormalized[]
+  }
+}
+
+// 前端控制路由生成
+export function generateRoutesFn1(
+  routes: AppRouteRecordRaw[],
+  basePath = '/'
+): AppRouteRecordRaw[] {
+  const res: AppRouteRecordRaw[] = []
+
+  for (const route of routes) {
+    // skip some route
+    if (route.meta && route.meta.hidden && !route.meta.showMainRoute) {
+      continue
+    }
+
+    let data: Nullable<AppRouteRecordRaw> = null
+
+    let onlyOneChild: Nullable<string> = null
+
+    if (route.children && route.children.length === 1 && !route.meta.alwaysShow) {
+      onlyOneChild = (
+        isUrl(route.children[0].path)
+          ? route.children[0].path
+          : pathResolve(pathResolve(basePath, route.path), route.children[0].path)
+      ) as string
+    }
+
+    // 权限过滤,通过获取登录信息里面的角色权限,动态的渲染菜单。
+    const list = wsCache.get(appStore.getUserInfo).checkedNodes
+    // 开发者可以根据实际情况进行扩展
+    for (const item of list) {
+      // 通过路径去匹配
+      if (isUrl(item.path) && (onlyOneChild === item.path || route.path === item.path)) {
+        data = Object.assign({}, route)
+      } else {
+        const routePath = pathResolve(basePath, onlyOneChild || route.path)
+        if (routePath === item.path || (route.meta && route.meta.followRoute === item.path)) {
+          data = Object.assign({}, route)
+        }
+      }
+    }
+
+    // recursive child routes
+    if (route.children && data) {
+      data.children = generateRoutesFn1(route.children, pathResolve(basePath, data.path))
+    }
+    if (data) {
+      res.push(data as AppRouteRecordRaw)
+    }
+  }
+  return res
+}
+
+// 后端控制路由生成
+export function generateRoutesFn2(routes: AppRouteRecordRaw[]): AppRouteRecordRaw[] {
+  const res: AppRouteRecordRaw[] = []
+
+  for (const route of routes) {
+    const data: AppRouteRecordRaw = {
+      path: route.path,
+      name: route.name,
+      redirect: route.redirect,
+      meta: route.meta
+    }
+    if (route.component) {
+      // 动态加载路由文件,可根据实际情况进行自定义逻辑
+      const component = route.component as string
+      data.component =
+        component === '#'
+          ? Layout
+          : component.includes('##')
+          ? getParentLayout()
+          : modules[`../../${route.component}.vue`] || modules[`../../${route.component}.tsx`]
+    }
+    // recursive child routes
+    if (route.children) {
+      data.children = generateRoutesFn2(route.children)
+    }
+    res.push(data as AppRouteRecordRaw)
+  }
+  return res
+}
+
+export function pathResolve(parentPath: string, path: string) {
+  return `${parentPath}/${path}`
+}
+
+// 路由降级
+export function flatMultiLevelRoutes(routes: AppRouteRecordRaw[]) {
+  const modules: AppRouteRecordRaw[] = cloneDeep(routes)
+  for (let index = 0; index < modules.length; index++) {
+    const route = modules[index]
+    if (!isMultipleRoute(route)) {
+      continue
+    }
+    promoteRouteLevel(route)
+  }
+  return modules
+}
+
+// 层级是否大于2
+function isMultipleRoute(route: AppRouteRecordRaw) {
+  if (!route || !Reflect.has(route, 'children') || !route.children?.length) {
+    return false
+  }
+
+  const children = route.children
+
+  let flag = false
+  for (let index = 0; index < children.length; index++) {
+    const child = children[index]
+    if (child.children?.length) {
+      flag = true
+      break
+    }
+  }
+  return flag
+}
+
+// 路由降级
+function promoteRouteLevel(route: AppRouteRecordRaw) {
+  let router: Router | null = createRouter({
+    routes: [route as unknown as RouteRecordNormalized],
+    history: createWebHashHistory()
+  })
+
+  const routes = router.getRoutes()
+  addToChildren(routes, route.children || [], route)
+  router = null
+
+  route.children = route.children?.map((item) => omit(item, 'children'))
+}
+
+// 添加所有子菜单
+function addToChildren(
+  routes: RouteRecordNormalized[],
+  children: AppRouteRecordRaw[],
+  routeModule: AppRouteRecordRaw
+) {
+  for (let index = 0; index < children.length; index++) {
+    const child = children[index]
+    const route = routes.find((item) => item.name === child.name)
+    if (!route) {
+      continue
+    }
+    routeModule.children = routeModule.children || []
+    if (!routeModule.children.find((item) => item.name === route.name)) {
+      routeModule.children?.push(route as unknown as AppRouteRecordRaw)
+    }
+    if (child.children?.length) {
+      addToChildren(routes, child.children, routeModule)
+    }
+  }
+}

+ 1 - 1
src/views/Level/Menu111.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts"></script>
 
 <template>
-  <div>Menu11</div>
+  <div>Menu111</div>
 </template>

+ 59 - 5
src/views/Login/components/LoginForm.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { reactive, ref, unref } from 'vue'
+import { reactive, ref, unref, watch } from 'vue'
 import { Form } from '@/components/Form'
 import { useI18n } from '@/hooks/web/useI18n'
 import { ElButton, ElCheckbox, ElLink } from 'element-plus'
@@ -7,6 +7,17 @@ import { required } from '@/utils/formRules'
 import { useForm } from '@/hooks/web/useForm'
 import { loginApi } from '@/api/login'
 import type { UserLoginType } from '@/api/login/types'
+import { useCache } from '@/hooks/web/useCache'
+import { useAppStore } from '@/store/modules/app'
+import { usePermissionStore } from '@/store/modules/permission'
+import { useRouter } from 'vue-router'
+import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
+
+const appStore = useAppStore()
+
+const permissionStore = usePermissionStore()
+
+const { currentRoute, addRoute, push } = useRouter()
 
 const { t } = useI18n()
 
@@ -29,6 +40,9 @@ const schema = reactive<FormSchema[]>([
     component: 'Input',
     colProps: {
       span: 24
+    },
+    componentProps: {
+      placeholder: t('login.usernamePlaceholder')
     }
   },
   {
@@ -42,7 +56,8 @@ const schema = reactive<FormSchema[]>([
     componentProps: {
       style: {
         width: '100%'
-      }
+      },
+      placeholder: t('login.passwordPlaceholder')
     }
   },
   {
@@ -81,6 +96,20 @@ const { register, elFormRef, methods } = useForm()
 
 const loading = ref(false)
 
+const iconColor = '#999'
+
+const redirect = ref<string>('')
+
+watch(
+  () => currentRoute.value,
+  (route: RouteLocationNormalizedLoaded) => {
+    redirect.value = route?.query?.redirect as string
+  },
+  {
+    immediate: true
+  }
+)
+
 // 登录
 async function signIn() {
   const formRef = unref(elFormRef)
@@ -89,10 +118,23 @@ async function signIn() {
     loading.value = true
     const { getFormData } = methods
     const formData = (await getFormData()) as UserLoginType
+
     const res = await loginApi(formData)
       .catch(() => {})
       .finally(() => (loading.value = false))
-    console.log(res)
+
+    if (res) {
+      const { wsCache } = useCache()
+      wsCache.set(appStore.getUserInfo, res.data)
+      await permissionStore.generateRoutes().catch(() => {})
+
+      permissionStore.getAddRouters.forEach((route) => {
+        console.log(route)
+        addRoute(route as RouteRecordRaw) // 动态添加可访问路由表
+      })
+      permissionStore.setIsAddRouters(true)
+      push({ path: redirect.value || '/level' })
+    }
   }
 }
 </script>
@@ -125,16 +167,28 @@ async function signIn() {
 
     <template #otherIcon>
       <div class="flex justify-between w-[100%]">
-        <Icon icon="ant-design:github-filled" :size="iconSize" class="cursor-pointer anticon" />
-        <Icon icon="ant-design:wechat-filled" :size="iconSize" class="cursor-pointer anticon" />
+        <Icon
+          icon="ant-design:github-filled"
+          :size="iconSize"
+          class="cursor-pointer anticon"
+          :color="iconColor"
+        />
+        <Icon
+          icon="ant-design:wechat-filled"
+          :size="iconSize"
+          class="cursor-pointer anticon"
+          :color="iconColor"
+        />
         <Icon
           icon="ant-design:alipay-circle-filled"
           :size="iconSize"
+          :color="iconColor"
           class="cursor-pointer anticon"
         />
         <Icon
           icon="ant-design:weibo-circle-filled"
           :size="iconSize"
+          :color="iconColor"
           class="cursor-pointer anticon"
         />
       </div>

+ 3 - 3
types/global.d.ts

@@ -25,9 +25,9 @@ declare type AxiosMethod = 'get' | 'post' | 'delete' | 'put'
 
 declare type AxiosResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
 
-declare type AxiosConfig = {
-  params?: Recordable
-  data?: Recordable
+declare type AxiosConfig<T = Recordable, K = Recordable> = {
+  params?: T
+  data?: K
   url?: string
   method?: AxiosMethod
   headersType?: string