feat: Add dynamic route

This commit is contained in:
kailong321200875
2022-02-19 20:34:44 +08:00
parent d5b6e2a777
commit 9d926b2760
15 changed files with 851 additions and 66 deletions

View File

@@ -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 })
}

View File

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

View File

@@ -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'
}
}

View File

@@ -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: '前端控制路由权限'
}
}

View File

@@ -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) // 动态添加可访问路由表
})

View File

@@ -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)) {

View File

@@ -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([
{

View File

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

View File

@@ -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, '/')
}
// 路由降级

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>