Selaa lähdekoodia

feat: Add dynamic route

kailong321200875 3 vuotta sitten
vanhempi
säilyke
9d926b2760

+ 475 - 0
mock/role/index.ts

@@ -0,0 +1,475 @@
+import { config } from '@/config/axios/config'
+import { MockMethod } from 'vite-plugin-mock'
+
+const { result_code } = config
+
+const timeout = 1000
+
+const adminList = [
+  {
+    path: '/dashboard',
+    component: '#',
+    redirect: '/dashboard/analysis',
+    name: 'Dashboard',
+    meta: {
+      title: 'router.dashboard',
+      icon: 'ant-design:dashboard-filled',
+      alwaysShow: true
+    },
+    children: [
+      {
+        path: 'analysis',
+        component: 'views/Dashboard/Analysis',
+        name: 'Analysis',
+        meta: {
+          title: 'router.analysis',
+          noCache: true
+        }
+      },
+      {
+        path: 'workplace',
+        component: 'views/Dashboard/Workplace',
+        name: 'Workplace',
+        meta: {
+          title: 'router.workplace',
+          noCache: true
+        }
+      }
+    ]
+  },
+  {
+    path: '/guide',
+    component: '#',
+    name: 'Guide',
+    meta: {},
+    children: [
+      {
+        path: 'index',
+        component: 'views/Guide/Guide',
+        name: 'GuideDemo',
+        meta: {
+          title: 'router.guide',
+          icon: 'cib:telegram-plane'
+        }
+      }
+    ]
+  },
+  {
+    path: '/components',
+    component: '#',
+    redirect: '/components/icon',
+    name: 'ComponentsDemo',
+    meta: {
+      title: 'router.component',
+      icon: 'bx:bxs-component',
+      alwaysShow: true
+    },
+    children: [
+      {
+        path: 'form',
+        component: '##',
+        name: 'Form',
+        meta: {
+          title: 'router.form',
+          alwaysShow: true
+        },
+        children: [
+          {
+            path: 'default-form',
+            component: 'views/Components/Form/DefaultForm',
+            name: 'DefaultForm',
+            meta: {
+              title: 'router.defaultForm'
+            }
+          },
+          {
+            path: 'use-form',
+            component: 'views/Components/Form/UseFormDemo',
+            name: 'UseForm',
+            meta: {
+              title: 'UseForm'
+            }
+          },
+          {
+            path: 'ref-form',
+            component: 'views/Components/Form/RefForm',
+            name: 'RefForm',
+            meta: {
+              title: 'RefForm'
+            }
+          }
+        ]
+      },
+      {
+        path: 'table',
+        component: '##',
+        name: 'TableDemo',
+        meta: {
+          title: 'router.table',
+          alwaysShow: true
+        },
+        children: [
+          {
+            path: 'default-table',
+            component: 'views/Components/Table/DefaultTable',
+            name: 'DefaultTable',
+            meta: {
+              title: 'router.defaultTable'
+            }
+          },
+          {
+            path: 'use-table',
+            component: 'views/Components/Table/UseTableDemo',
+            name: 'UseTable',
+            meta: {
+              title: 'UseTable'
+            }
+          },
+          {
+            path: 'ref-table',
+            component: 'views/Components/Table/RefTable',
+            name: 'RefTable',
+            meta: {
+              title: 'RefTable'
+            }
+          }
+        ]
+      },
+      {
+        path: 'editor-demo',
+        component: '##',
+        name: 'EditorDemo',
+        meta: {
+          title: 'router.editor',
+          alwaysShow: true
+        },
+        children: [
+          {
+            path: 'editor',
+            component: 'views/Components/Editor/Editor',
+            name: 'Editor',
+            meta: {
+              title: 'router.richText'
+            }
+          }
+        ]
+      },
+      {
+        path: 'search',
+        component: 'views/Components/Search',
+        name: 'Search',
+        meta: {
+          title: 'router.search'
+        }
+      },
+      {
+        path: 'descriptions',
+        component: 'views/Components/Descriptions',
+        name: 'Descriptions',
+        meta: {
+          title: 'router.descriptions'
+        }
+      },
+      {
+        path: 'image-viewer',
+        component: 'views/Components/ImageViewer',
+        name: 'ImageViewer',
+        meta: {
+          title: 'router.imageViewer'
+        }
+      },
+      {
+        path: 'dialog',
+        component: 'views/Components/Dialog',
+        name: 'Dialog',
+        meta: {
+          title: 'router.dialog'
+        }
+      },
+      {
+        path: 'icon',
+        component: 'views/Components/Icon',
+        name: 'Icon',
+        meta: {
+          title: 'router.icon'
+        }
+      },
+      {
+        path: 'echart',
+        component: 'views/Components/Echart',
+        name: 'Echart',
+        meta: {
+          title: 'router.echart'
+        }
+      },
+      {
+        path: 'count-to',
+        component: 'views/Components/CountTo',
+        name: 'CountTo',
+        meta: {
+          title: 'router.countTo'
+        }
+      },
+      {
+        path: 'watermark',
+        component: 'views/Components/Watermark',
+        name: 'Watermark',
+        meta: {
+          title: 'router.watermark'
+        }
+      },
+      {
+        path: 'qrcode',
+        component: 'views/Components/Qrcode',
+        name: 'Qrcode',
+        meta: {
+          title: 'router.qrcode'
+        }
+      },
+      {
+        path: 'highlight',
+        component: 'views/Components/Highlight',
+        name: 'Highlight',
+        meta: {
+          title: 'router.highlight'
+        }
+      },
+      {
+        path: 'infotip',
+        component: 'views/Components/Infotip',
+        name: 'Infotip',
+        meta: {
+          title: 'router.infotip'
+        }
+      }
+    ]
+  },
+  {
+    path: '/level',
+    component: '#',
+    redirect: '/level/menu1/menu1-1/menu1-1-1',
+    name: 'Level',
+    meta: {
+      title: 'router.level',
+      icon: 'carbon:skill-level-advanced'
+    },
+    children: [
+      {
+        path: 'menu1',
+        name: 'Menu1',
+        component: '##',
+        redirect: '/level/menu1/menu1-1/menu1-1-1',
+        meta: {
+          title: 'router.menu1'
+        },
+        children: [
+          {
+            path: 'menu1-1',
+            name: 'Menu11',
+            component: '##',
+            redirect: '/level/menu1/menu1-1/menu1-1-1',
+            meta: {
+              title: 'router.menu11',
+              alwaysShow: true
+            },
+            children: [
+              {
+                path: 'menu1-1-1',
+                name: 'Menu111',
+                component: 'views/Level/Menu111',
+                meta: {
+                  title: 'router.menu111'
+                }
+              }
+            ]
+          },
+          {
+            path: 'menu1-2',
+            name: 'Menu12',
+            component: 'views/Level/Menu12',
+            meta: {
+              title: 'router.menu12'
+            }
+          }
+        ]
+      },
+      {
+        path: 'menu2',
+        name: 'Menu2Demo',
+        component: 'views/Level/Menu2',
+        meta: {
+          title: 'router.menu2'
+        }
+      }
+    ]
+  },
+  {
+    path: '/example',
+    component: '#',
+    redirect: '/example/example-dialog',
+    name: 'Example',
+    meta: {
+      title: 'router.example',
+      icon: 'ep:management',
+      alwaysShow: true
+    },
+    children: [
+      {
+        path: 'example-dialog',
+        component: 'views/Example/Dialog/ExampleDialog',
+        name: 'ExampleDialog',
+        meta: {
+          title: 'router.exampleDialog'
+        }
+      },
+      {
+        path: 'example-page',
+        component: 'views/Example/Page/ExamplePage',
+        name: 'ExamplePage',
+        meta: {
+          title: 'router.examplePage'
+        }
+      },
+      {
+        path: 'example-add',
+        component: 'views/Example/Page/ExampleAdd',
+        name: 'ExampleAdd',
+        meta: {
+          title: 'router.exampleAdd',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          showMainRoute: true,
+          activeMenu: '/example/example-page'
+        }
+      },
+      {
+        path: 'example-edit',
+        component: 'views/Example/Page/ExampleEdit',
+        name: 'ExampleEdit',
+        meta: {
+          title: 'router.exampleEdit',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          showMainRoute: true,
+          activeMenu: '/example/example-page'
+        }
+      },
+      {
+        path: 'example-detail',
+        component: 'views/Example/Page/ExampleDetail',
+        name: 'ExampleDetail',
+        meta: {
+          title: 'router.exampleDetail',
+          noTagsView: true,
+          noCache: true,
+          hidden: true,
+          showMainRoute: true,
+          activeMenu: '/example/example-page'
+        }
+      }
+    ]
+  },
+  {
+    path: '/error',
+    component: '#',
+    redirect: '/error/404',
+    name: 'Error',
+    meta: {
+      title: 'router.errorPage',
+      icon: 'ci:error',
+      alwaysShow: true
+    },
+    children: [
+      {
+        path: '404-demo',
+        component: 'views/Error/404',
+        name: '404Demo',
+        meta: {
+          title: '404'
+        }
+      },
+      {
+        path: '403-demo',
+        component: 'views/Error/403',
+        name: '403Demo',
+        meta: {
+          title: '403'
+        }
+      },
+      {
+        path: '500-demo',
+        component: 'views/Error/500',
+        name: '500Demo',
+        meta: {
+          title: '500'
+        }
+      }
+    ]
+  }
+]
+
+const testList: string[] = [
+  '/dashboard',
+  '/dashboard/analysis',
+  '/dashboard/workplace',
+  '/guide',
+  '/guide/index',
+  '/components',
+  '/components/form',
+  '/components/form/default-form',
+  '/components/form/use-form',
+  '/components/form/ref-form',
+  '/components/table',
+  '/components/table/default-table',
+  '/components/table/use-table',
+  '/components/table/ref-table',
+  '/components/editor-demo',
+  '/components/editor-demo/editor',
+  '/components/search',
+  '/components/descriptions',
+  '/components/image-viewer',
+  '/components/dialog',
+  '/components/icon',
+  '/components/echart',
+  '/components/count-to',
+  '/components/watermark',
+  '/components/qrcode',
+  '/components/highlight',
+  '/components/infotip',
+  '/level',
+  '/level/menu1',
+  '/level/menu1/menu1-1',
+  '/level/menu1/menu1-1/menu1-1-1',
+  '/level/menu1/menu1-2',
+  '/level/menu2',
+  '/example',
+  '/example/example-dialog',
+  '/example/example-page',
+  '/example/example-add',
+  '/example/example-edit',
+  '/example/example-detail',
+  '/error',
+  '/error/404-demo',
+  '/error/403-demo',
+  '/error/500-demo'
+]
+
+export default [
+  // 列表接口
+  {
+    url: '/role/list',
+    method: 'get',
+    timeout,
+    response: ({ query }) => {
+      const { roleName } = query
+      return {
+        code: result_code,
+        data: {
+          list: roleName === 'admin' ? adminList : testList
+        }
+      }
+    }
+  }
+] as MockMethod[]

+ 24 - 0
mock/user/index.ts

@@ -26,6 +26,30 @@ const List: {
 ]
 
 export default [
+  // 列表接口
+  {
+    url: '/user/list',
+    method: 'get',
+    response: ({ query }) => {
+      const { username, pageIndex, pageSize } = query
+
+      const mockList = List.filter((item) => {
+        if (username && item.username.indexOf(username) < 0) return false
+        return true
+      })
+      const pageList = mockList.filter(
+        (_, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1)
+      )
+
+      return {
+        code: result_code,
+        data: {
+          total: mockList.length,
+          list: pageList
+        }
+      }
+    }
+  },
   // 登录接口
   {
     url: '/user/login',

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

@@ -1,5 +1,5 @@
 import { useAxios } from '@/hooks/web/useAxios'
-import type { UserLoginType } from './types'
+import type { UserLoginType, UserType } from './types'
 
 const { request } = useAxios()
 
@@ -13,3 +13,22 @@ export const loginApi = (data: UserLoginType) => {
 export const loginOutApi = () => {
   return request({ url: '/user/loginOut', method: 'get' })
 }
+
+export const getUserListApi = ({ params }: AxiosConfig) => {
+  return request<{
+    total: number
+    list: UserType[]
+  }>({ url: '/user/list', method: 'get', params })
+}
+
+export const getAdminRoleApi = ({ params }: AxiosConfig) => {
+  return request<{
+    list: AppCustomRouteRecordRaw[]
+  }>({ url: '/role/list', method: 'get', params })
+}
+
+export const getTestRoleApi = ({ params }: AxiosConfig) => {
+  return request<{
+    list: string[]
+  }>({ url: '/role/list', method: 'get', params })
+}

+ 7 - 0
src/api/login/types.ts

@@ -2,3 +2,10 @@ export type UserLoginType = {
   username: string
   password: string
 }
+
+export type UserType = {
+  username: string
+  password: string
+  role: string
+  roleId: string
+}

+ 17 - 1
src/locales/en.ts

@@ -126,7 +126,10 @@ export default {
     exampleAdd: 'Example page - add',
     exampleEdit: 'Example page - edit',
     exampleDetail: 'Example page - detail',
-    errorPage: 'Error page'
+    errorPage: 'Error page',
+    authorization: 'Authorization',
+    user: 'User management',
+    role: 'Role management'
   },
   analysis: {
     newUser: 'New user',
@@ -393,5 +396,18 @@ export default {
     content: 'Content',
     save: 'Save',
     detail: 'Detail'
+  },
+  userDemo: {
+    title: 'User management',
+    message:
+      'Because it is simulated data, only two accounts with different permissions are provided, which can be modified and combined by developers according to the actual situation.',
+    index: 'Index',
+    action: 'Action',
+    username: 'Username',
+    password: 'Password',
+    role: 'Role',
+    remark: 'Remark',
+    remarkMessage1: 'Back end control routing permission',
+    remarkMessage2: 'Front end control routing permission'
   }
 }

+ 16 - 1
src/locales/zh-CN.ts

@@ -126,7 +126,10 @@ export default {
     exampleAdd: '综合示例 - 新增',
     exampleEdit: '综合示例 - 编辑',
     exampleDetail: '综合示例 - 详情',
-    errorPage: '错误页面'
+    errorPage: '错误页面',
+    authorization: '权限管理',
+    user: '用户管理',
+    role: '角色管理'
   },
   analysis: {
     newUser: '新增用户',
@@ -390,5 +393,17 @@ export default {
     content: '内容',
     save: '保存',
     detail: '详情'
+  },
+  userDemo: {
+    title: '用户管理',
+    message: '由于是模拟数据,所以只提供了两种不同权限的帐号,开发者可根据实际情况自行改造结合。',
+    index: '序号',
+    action: '操作',
+    username: '用户名',
+    password: '密码',
+    role: '角色',
+    remark: '备注',
+    remarkMessage1: '后端控制路由权限',
+    remarkMessage2: '前端控制路由权限'
   }
 }

+ 9 - 1
src/permission.ts

@@ -30,7 +30,15 @@ router.beforeEach(async (to, from, next) => {
         to.path === '/' ? next({ path: permissionStore.addRouters[0]?.path as string }) : next()
         return
       }
-      await permissionStore.generateRoutes()
+
+      // 开发者可根据实际情况进行修改
+      const roleRouters = wsCache.get('roleRouters') || []
+      const userInfo = wsCache.get(appStore.getUserInfo)
+
+      userInfo.role === 'admin'
+        ? await permissionStore.generateRoutes('admin', roleRouters as AppCustomRouteRecordRaw[])
+        : await permissionStore.generateRoutes('test', roleRouters as string[])
+
       permissionStore.getAddRouters.forEach((route) => {
         router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表
       })

+ 36 - 7
src/router/index.ts

@@ -424,31 +424,60 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
     },
     children: [
       {
-        path: '404',
+        path: '404-demo',
         component: () => import('@/views/Error/404.vue'),
-        name: '404',
+        name: '404Demo',
         meta: {
           title: '404'
         }
       },
       {
-        path: '403',
+        path: '403-demo',
         component: () => import('@/views/Error/403.vue'),
-        name: '403',
+        name: '403Demo',
         meta: {
           title: '403'
         }
       },
       {
-        path: '500',
+        path: '500-demo',
         component: () => import('@/views/Error/500.vue'),
-        name: '500',
+        name: '500Demo',
         meta: {
           title: '500'
         }
       }
     ]
   }
+  // {
+  //   path: '/authorization',
+  //   component: Layout,
+  //   redirect: '/authorization/user',
+  //   name: 'Authorization',
+  //   meta: {
+  //     title: t('router.authorization'),
+  //     icon: 'eos-icons:role-binding',
+  //     alwaysShow: true
+  //   },
+  //   children: [
+  //     {
+  //       path: 'user',
+  //       component: () => import('@/views/Authorization/User.vue'),
+  //       name: 'User',
+  //       meta: {
+  //         title: t('router.user')
+  //       }
+  //     },
+  //     {
+  //       path: 'role',
+  //       component: () => import('@/views/Authorization/Role.vue'),
+  //       name: 'Role',
+  //       meta: {
+  //         title: t('router.role')
+  //       }
+  //     }
+  //   ]
+  // }
 ]
 
 const router = createRouter({
@@ -459,7 +488,7 @@ const router = createRouter({
 })
 
 export const resetRouter = (): void => {
-  const resetWhiteNameList = ['RedirectRoot', 'Redirect', 'Login', 'Root', 'Dashboard', 'Page404']
+  const resetWhiteNameList = ['Redirect', 'Login', 'NoFind']
   router.getRoutes().forEach((route) => {
     const { name } = route
     if (name && !resetWhiteNameList.includes(name as string)) {

+ 17 - 22
src/store/modules/permission.ts

@@ -1,16 +1,9 @@
 import { defineStore } from 'pinia'
 import { asyncRouterMap, constantRouterMap } from '@/router'
-// import { useCache } from '@/hooks/web/useCache'
-import { flatMultiLevelRoutes } from '@/utils/routerHelper'
-// import { generateRoutesFn1, generateRoutesFn2, flatMultiLevelRoutes } from '@/utils/routerHelper'
+import { generateRoutesFn1, generateRoutesFn2, flatMultiLevelRoutes } from '@/utils/routerHelper'
 import { store } from '../index'
-// import { useAppStoreWithOut } from '@/store/modules/app'
 import { cloneDeep } from 'lodash-es'
 
-// const { wsCache } = useCache()
-
-// const appStore = useAppStoreWithOut()
-
 export interface PermissionState {
   routers: AppRouteRecordRaw[]
   addRouters: AppRouteRecordRaw[]
@@ -44,21 +37,23 @@ export const usePermissionStore = defineStore({
     }
   },
   actions: {
-    generateRoutes(): Promise<unknown> {
+    generateRoutes(
+      type: 'admin' | 'test' | 'none',
+      routers?: AppCustomRouteRecordRaw[] | string[]
+    ): 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)
-
+        let routerMap: AppRouteRecordRaw[] = []
+        if (type === 'admin') {
+          // 模拟后端过滤菜单
+          routerMap = generateRoutesFn2(routers as AppCustomRouteRecordRaw[])
+        } else if (type === 'test') {
+          // 模拟前端过滤菜单
+          routerMap = generateRoutesFn1(cloneDeep(asyncRouterMap), routers as string[])
+        } else {
+          // 直接读取静态路由表
+          routerMap = cloneDeep(asyncRouterMap)
+        }
+        console.log(routerMap)
         // 动态路由,404一定要放到最后面
         this.addRouters = routerMap.concat([
           {

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

@@ -16,9 +16,6 @@ export const useTagsViewStore = defineStore({
     visitedViews: [],
     cachedViews: new Set()
   }),
-  persist: {
-    enabled: true
-  },
   getters: {
     getVisitedViews(): RouteLocationNormalizedLoaded[] {
       return this.visitedViews

+ 13 - 22
src/utils/routerHelper.ts

@@ -1,15 +1,9 @@
 import { createRouter, createWebHashHistory } from 'vue-router'
 import type { Router, RouteLocationNormalized, RouteRecordNormalized, RouteMeta } 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}')
+const modules = import.meta.glob('../views/**/*.{vue,tsx}')
 
 /* Layout */
 export const Layout = () => import('@/layout/Layout.vue')
@@ -41,6 +35,7 @@ export const getRawRoute = (route: RouteLocationNormalized): RouteLocationNormal
 // 前端控制路由生成
 export const generateRoutesFn1 = (
   routes: AppRouteRecordRaw[],
+  keys: string[],
   basePath = '/'
 ): AppRouteRecordRaw[] => {
   const res: AppRouteRecordRaw[] = []
@@ -55,7 +50,6 @@ export const generateRoutesFn1 = (
     let data: Nullable<AppRouteRecordRaw> = null
 
     let onlyOneChild: Nullable<string> = null
-
     if (route.children && route.children.length === 1 && !meta.alwaysShow) {
       onlyOneChild = (
         isUrl(route.children[0].path)
@@ -64,16 +58,14 @@ export const generateRoutesFn1 = (
       ) as string
     }
 
-    // 权限过滤,通过获取登录信息里面的角色权限,动态的渲染菜单。
-    const list = wsCache.get(appStore.getUserInfo).checkedNodes
     // 开发者可以根据实际情况进行扩展
-    for (const item of list) {
+    for (const item of keys) {
       // 通过路径去匹配
-      if (isUrl(item.path) && (onlyOneChild === item.path || route.path === item.path)) {
+      if (isUrl(item) && (onlyOneChild === item || route.path === item)) {
         data = Object.assign({}, route)
       } else {
         const routePath = pathResolve(basePath, onlyOneChild || route.path)
-        if (routePath === item.path || meta.followRoute === item.path) {
+        if (routePath === item || meta.followRoute === item) {
           data = Object.assign({}, route)
         }
       }
@@ -81,7 +73,7 @@ export const generateRoutesFn1 = (
 
     // recursive child routes
     if (route.children && data) {
-      data.children = generateRoutesFn1(route.children, pathResolve(basePath, data.path))
+      data.children = generateRoutesFn1(route.children, keys, pathResolve(basePath, data.path))
     }
     if (data) {
       res.push(data as AppRouteRecordRaw)
@@ -91,7 +83,7 @@ export const generateRoutesFn1 = (
 }
 
 // 后端控制路由生成
-export const generateRoutesFn2 = (routes: AppRouteRecordRaw[]): AppRouteRecordRaw[] => {
+export const generateRoutesFn2 = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => {
   const res: AppRouteRecordRaw[] = []
 
   for (const route of routes) {
@@ -102,15 +94,14 @@ export const generateRoutesFn2 = (routes: AppRouteRecordRaw[]): AppRouteRecordRa
       meta: route.meta
     }
     if (route.component) {
-      const comModule =
-        modules[`../../${route.component}.vue`] || modules[`../../${route.component}.tsx`]
-      if (comModule) {
+      const comModule = modules[`../${route.component}.vue`] || modules[`../${route.component}.tsx`]
+      const component = route.component as string
+      if (!comModule && !component.includes('#')) {
+        console.error(`未找到${route.component}.vue文件或${route.component}.tsx文件,请创建`)
+      } else {
         // 动态加载路由文件,可根据实际情况进行自定义逻辑
-        const component = route.component as string
         data.component =
           component === '#' ? Layout : component.includes('##') ? getParentLayout() : comModule
-      } else {
-        console.error(`未找到${route.component}.vue文件或${route.component}.tsx文件,请创建`)
       }
     }
     // recursive child routes
@@ -124,7 +115,7 @@ export const generateRoutesFn2 = (routes: AppRouteRecordRaw[]): AppRouteRecordRa
 
 export const pathResolve = (parentPath: string, path: string) => {
   const childPath = path.startsWith('/') || !path ? path : `/${path}`
-  return `${parentPath}${childPath}`
+  return `${parentPath}${childPath}`.replace(/\/\//g, '/')
 }
 
 // 路由降级

+ 88 - 0
src/views/Authorization/Role.vue

@@ -0,0 +1,88 @@
+<script setup lang="ts">
+import { ContentWrap } from '@/components/ContentWrap'
+import { useI18n } from '@/hooks/web/useI18n'
+import { Table } from '@/components/Table'
+import { getUserListApi } from '@/api/login'
+import { UserType } from '@/api/login/types'
+import { ref, h } from 'vue'
+import { ElButton } from 'element-plus'
+
+interface Params {
+  pageIndex?: number
+  pageSize?: number
+}
+
+const { t } = useI18n()
+
+const columns: TableColumn[] = [
+  {
+    field: 'index',
+    label: t('userDemo.index'),
+    type: 'index'
+  },
+  {
+    field: 'username',
+    label: t('userDemo.username')
+  },
+  {
+    field: 'password',
+    label: t('userDemo.password')
+  },
+  {
+    field: 'role',
+    label: t('userDemo.role')
+  },
+  {
+    field: 'remark',
+    label: t('userDemo.remark'),
+    formatter: (row: UserType) => {
+      return h(
+        'span',
+        row.username === 'admin' ? t('userDemo.remarkMessage1') : t('userDemo.remarkMessage2')
+      )
+    }
+  },
+  {
+    field: 'action',
+    label: t('userDemo.action')
+  }
+]
+
+const loading = ref(true)
+
+let tableDataList = ref<UserType[]>([])
+
+const getTableList = async (params?: Params) => {
+  const res = await getUserListApi({
+    params: params || {
+      pageIndex: 1,
+      pageSize: 10
+    }
+  })
+    .catch(() => {})
+    .finally(() => {
+      loading.value = false
+    })
+  if (res) {
+    tableDataList.value = res.data.list
+  }
+}
+
+getTableList()
+
+const acitonFn = (data: TableSlotDefault) => {
+  console.log(data)
+}
+</script>
+
+<template>
+  <ContentWrap :title="t('userDemo.title')" :message="t('userDemo.message')">
+    <Table :columns="columns" :data="tableDataList" :loading="loading" :selection="false">
+      <template #action="data">
+        <ElButton type="primary" @click="acitonFn(data as TableSlotDefault)">
+          {{ t('tableDemo.action') }}
+        </ElButton>
+      </template>
+    </Table>
+  </ContentWrap>
+</template>

+ 88 - 0
src/views/Authorization/User.vue

@@ -0,0 +1,88 @@
+<script setup lang="ts">
+import { ContentWrap } from '@/components/ContentWrap'
+import { useI18n } from '@/hooks/web/useI18n'
+import { Table } from '@/components/Table'
+import { getUserListApi } from '@/api/login'
+import { UserType } from '@/api/login/types'
+import { ref, h } from 'vue'
+import { ElButton } from 'element-plus'
+
+interface Params {
+  pageIndex?: number
+  pageSize?: number
+}
+
+const { t } = useI18n()
+
+const columns: TableColumn[] = [
+  {
+    field: 'index',
+    label: t('userDemo.index'),
+    type: 'index'
+  },
+  {
+    field: 'username',
+    label: t('userDemo.username')
+  },
+  {
+    field: 'password',
+    label: t('userDemo.password')
+  },
+  {
+    field: 'role',
+    label: t('userDemo.role')
+  },
+  {
+    field: 'remark',
+    label: t('userDemo.remark'),
+    formatter: (row: UserType) => {
+      return h(
+        'span',
+        row.username === 'admin' ? t('userDemo.remarkMessage1') : t('userDemo.remarkMessage2')
+      )
+    }
+  },
+  {
+    field: 'action',
+    label: t('userDemo.action')
+  }
+]
+
+const loading = ref(true)
+
+let tableDataList = ref<UserType[]>([])
+
+const getTableList = async (params?: Params) => {
+  const res = await getUserListApi({
+    params: params || {
+      pageIndex: 1,
+      pageSize: 10
+    }
+  })
+    .catch(() => {})
+    .finally(() => {
+      loading.value = false
+    })
+  if (res) {
+    tableDataList.value = res.data.list
+  }
+}
+
+getTableList()
+
+const acitonFn = (data: TableSlotDefault) => {
+  console.log(data)
+}
+</script>
+
+<template>
+  <ContentWrap :title="t('userDemo.title')" :message="t('userDemo.message')">
+    <Table :columns="columns" :data="tableDataList" :loading="loading" :selection="false">
+      <template #action="data">
+        <ElButton type="primary" @click="acitonFn(data as TableSlotDefault)">
+          {{ t('tableDemo.action') }}
+        </ElButton>
+      </template>
+    </Table>
+  </ContentWrap>
+</template>

+ 33 - 9
src/views/Login/components/LoginForm.vue

@@ -5,7 +5,7 @@ import { useI18n } from '@/hooks/web/useI18n'
 import { ElButton, ElCheckbox, ElLink } from 'element-plus'
 import { required } from '@/utils/formRules'
 import { useForm } from '@/hooks/web/useForm'
-import { loginApi } from '@/api/login'
+import { loginApi, getTestRoleApi, getAdminRoleApi } from '@/api/login'
 import type { UserLoginType } from '@/api/login/types'
 import { useCache } from '@/hooks/web/useCache'
 import { useAppStore } from '@/store/modules/app'
@@ -126,17 +126,41 @@ const signIn = async () => {
     if (res) {
       const { wsCache } = useCache()
       wsCache.set(appStore.getUserInfo, res.data)
-      await permissionStore.generateRoutes().catch(() => {})
-
-      permissionStore.getAddRouters.forEach((route) => {
-        addRoute(route as RouteRecordRaw) // 动态添加可访问路由表
-      })
-      permissionStore.setIsAddRouters(true)
-      // push({ path: redirect.value || permissionStore.addRouters[0].path })
-      push({ path: permissionStore.addRouters[0].path })
+
+      getRole()
     }
   }
 }
+
+// 获取角色信息
+const getRole = async () => {
+  const { getFormData } = methods
+  const formData = await getFormData<UserLoginType>()
+  const params = {
+    roleName: formData.username
+  }
+  // admin - 模拟后端过滤菜单
+  // test - 模拟前端过滤菜单
+  const res =
+    formData.username === 'admin'
+      ? await getAdminRoleApi({ params })
+      : await getTestRoleApi({ params })
+  if (res) {
+    const { wsCache } = useCache()
+    const routers = res.data.list || []
+    wsCache.set('roleRouters', routers)
+
+    formData.username === 'admin'
+      ? await permissionStore.generateRoutes('admin', routers).catch(() => {})
+      : await permissionStore.generateRoutes('test', routers).catch(() => {})
+
+    permissionStore.getAddRouters.forEach((route) => {
+      addRoute(route as RouteRecordRaw) // 动态添加可访问路由表
+    })
+    permissionStore.setIsAddRouters(true)
+    push({ path: redirect.value || permissionStore.addRouters[0].path })
+  }
+}
 </script>
 
 <template>

+ 9 - 0
types/router.d.ts

@@ -62,4 +62,13 @@ declare global {
     props?: Recordable
     fullPath?: string
   }
+
+  declare interface AppCustomRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
+    name: string
+    meta: RouteMeta
+    component: string
+    path: string
+    redirect: string
+    children?: AppCustomRouteRecordRaw[]
+  }
 }