Browse Source

fix: 切换账户登录后 可以重定向到上次账户的路由(实际当前账户没有权限) 问题
perf: 菜单 store 优化 异步菜单路由 独立children 优化 菜单配置简化
fix: 调整 le-iconfont 资源
feat: 暗黑主题切换快捷方式

lanceJiang 1 year ago
parent
commit
4f55da9e4a

+ 3 - 3
src/api/system/menu.ts

@@ -1,6 +1,6 @@
 import request from '@/utils/request'
 import { AxiosPromise } from 'axios'
-import { local_permissionsRoutes } from '@/router'
+import { local_permissionsMenuList } from '@/router'
 
 /**
  * 获取路由列表
@@ -13,7 +13,7 @@ export function getMenuList(): AxiosPromise {
 		// @ts-ignore
 		return Promise.resolve({
 			// 路由菜单
-			menu: local_permissionsRoutes,
+			menu: local_permissionsMenuList,
 			// 权限
 			permissions: ['']
 		})
@@ -28,7 +28,7 @@ export function getMenuList(): AxiosPromise {
 		return res
 		// return Promise.resolve({
 		// 	// 路由菜单
-		// 	menu: local_permissionsRoutes,
+		// 	menu: local_permissionsMenuList,
 		// 	// 权限
 		// 	permissions: ['']
 		// })

+ 1 - 0
src/lang/lance-element/en.ts

@@ -23,6 +23,7 @@ export default {
 		fullscreen: 'Full Screen',
 		exitFullscreen: 'Exit Full Screen',
 		menuSearch: 'Search Menu',
+		menuDarkTheme: 'Switch Theme',
 		copy: 'Copy',
 		column: 'Column',
 		// 筛选相关

+ 1 - 0
src/lang/lance-element/zh-cn.ts

@@ -23,6 +23,7 @@ export default {
 		fullscreen: '全屏',
 		exitFullscreen: '退出全屏',
 		menuSearch: '搜索菜单',
+		menuDarkTheme: '切换主题',
 		copy: '复制',
 		column: '视图',
 		// 筛选相关

+ 0 - 1
src/layout/LayoutLeft/index.vue

@@ -30,7 +30,6 @@ import { computed } from 'vue'
 import { useRoute } from 'vue-router'
 // import { useAuthStore } from '@/stores/modules/auth'
 // import { useGlobalStore } from '@/stores/modules/global'
-// import Main from '@/layouts/components/Main/index.vue'
 import ToolBarLeft from '@/layout/components/Header/ToolBarLeft.vue'
 import ToolBarRight from '@/layout/components/Header/ToolBarRight.vue'
 import SubMenu from '@/layout/components/Menu/SubMenu.vue'

+ 0 - 1
src/layout/LayoutLeftMix/index.vue

@@ -50,7 +50,6 @@ import { ref, computed, watch } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 // import { useAuthStore } from '@/stores/modules/auth'
 // import { useGlobalStore } from '@/stores/modules/global'
-// import Main from '@/layouts/components/Main/index.vue'
 import AppMain from '@/layout/components/AppMain.vue'
 import ToolBarLeft from '@/layout/components/Header/ToolBarLeft.vue'
 import ToolBarRight from '@/layout/components/Header/ToolBarRight.vue'

+ 0 - 1
src/layout/LayoutTop/index.vue

@@ -49,7 +49,6 @@
 import { computed } from 'vue'
 // import { useAuthStore } from '@/stores/modules/auth'
 import { useRoute, useRouter } from 'vue-router'
-// import Main from '@/layouts/components/Main/index.vue'
 import AppMain from '@/layout/components/AppMain.vue'
 import ToolBarRight from '@/layout/components/Header/ToolBarRight.vue'
 import SubMenu from '@/layout/components/Menu/SubMenu.vue'

+ 2 - 0
src/layout/components/Header/ToolBarRight.vue

@@ -3,6 +3,7 @@
 		<div class="header-icon">
 			<SearchMenu />
 			<ScreenFull />
+			<SwitchDarkTheme />
 			<SizeSelect />
 			<Language />
 			<Message />
@@ -18,6 +19,7 @@ import SizeSelect from '@/components/SizeSelect/index.vue'
 import SearchMenu from './components/SearchMenu.vue'
 import Message from './components/Message.vue'
 import Avatar from './components/Avatar.vue'
+import SwitchDarkTheme from './components/SwitchDarkTheme.vue'
 </script>
 
 <style lang="scss">

+ 9 - 123
src/layout/components/Header/components/Avatar.vue

@@ -1,6 +1,6 @@
 <template>
-	<el-dropdown class="avatar-container menu-item le-hover-effect--bg" trigger="click" size="default">
-		<div class="avatar-wrapper">
+	<el-dropdown class="menu--avatar-wrap" trigger="click">
+		<div class="menu--avatar menu-item le-hover-effect--bg">
 			<span class="nickname">{{ userInfo.username || '' }}</span>
 			<el-avatar v-if="userInfo.avatar" :size="30" :src="userInfo.avatar" class="user-avatar" />
 			<ArrowDown style="width: 0.6em; height: 0.6em; margin-left: 5px; font-size: 24px" />
@@ -11,7 +11,10 @@
 				<!--						<router-link to="/">
           <el-dropdown-item>{{ $t('navbar.dashboard') }}</el-dropdown-item>
         </router-link>-->
-				<el-dropdown-item @click="logout">
+				<router-link to="/profile">
+					<el-dropdown-item>{{ $t('route.profile') }}</el-dropdown-item>
+				</router-link>
+				<el-dropdown-item divided @click="logout">
 					{{ $t('navbar.logout') }}
 				</el-dropdown-item>
 			</el-dropdown-menu>
@@ -47,128 +50,11 @@ function logout() {
 }
 </script>
 
-<!--
-<template>
-	<el-dropdown trigger="click">
-		<div class="avatar">
-			<img src="@/assets/images/avatar.gif" alt="avatar" />
-		</div>
-		<template #dropdown>
-			<el-dropdown-menu>
-				<el-dropdown-item @click="openDialog('infoRef')">
-					<el-icon><User /></el-icon>{{ $t('header.personalData') }}
-				</el-dropdown-item>
-				<el-dropdown-item @click="openDialog('passwordRef')">
-					<el-icon><Edit /></el-icon>{{ $t('header.changePassword') }}
-				</el-dropdown-item>
-				<el-dropdown-item divided @click="logout">
-					<el-icon><SwitchButton /></el-icon>{{ $t('header.logout') }}
-				</el-dropdown-item>
-			</el-dropdown-menu>
-		</template>
-	</el-dropdown>
-	&lt;!&ndash; infoDialog &ndash;&gt;
-	<InfoDialog ref="infoRef"></InfoDialog>
-	&lt;!&ndash; passwordDialog &ndash;&gt;
-	<PasswordDialog ref="passwordRef"></PasswordDialog>
-</template>
-
-<script setup lang="ts">
-import { ref } from 'vue'
-import { LOGIN_URL } from '@/config'
-import { useRouter } from 'vue-router'
-import { logoutApi } from '@/api/modules/login'
-import { useUserStore } from '@/stores/modules/user'
-import { ElMessageBox, ElMessage } from 'element-plus'
-import InfoDialog from './InfoDialog.vue'
-import PasswordDialog from './PasswordDialog.vue'
-
-const router = useRouter()
-const userStore = useUserStore()
-
-// 退出登录
-const logout = () => {
-	ElMessageBox.confirm('您是否确认退出登录?', '温馨提示', {
-		confirmButtonText: '确定',
-		cancelButtonText: '取消',
-		type: 'warning'
-	}).then(async () => {
-		// 1.执行退出登录接口
-		await logoutApi()
-
-		// 2.清除 Token
-		userStore.setToken('')
-
-		// 3.重定向到登陆页
-		router.replace(LOGIN_URL)
-		ElMessage.success('退出登录成功!')
-	})
-}
-
-// 打开修改密码和个人信息弹窗
-const infoRef = ref<InstanceType<typeof InfoDialog> | null>(null)
-const passwordRef = ref<InstanceType<typeof PasswordDialog> | null>(null)
-const openDialog = (ref: string) => {
-	if (ref == 'infoRef') infoRef.value?.openDialog()
-	if (ref == 'passwordRef') passwordRef.value?.openDialog()
-}
-</script>
-
-<style scoped lang="scss">
-.avatar {
-	width: 40px;
-	height: 40px;
-	overflow: hidden;
-	cursor: pointer;
-	border-radius: 50%;
-	img {
-		width: 100%;
-		height: 100%;
-	}
-}
-</style>
--->
 <style lang="scss" scoped>
-.right-menu-item {
-	padding: 0 8px;
+.menu--avatar-wrap {
 	height: 100%;
-	font-size: 18px;
-	//color: #5a5e66;
-	//color: var(--el-header-text-color);
-	color: var(--el-color-info);
-
-	&.hover-effect {
-		cursor: pointer;
-		transition: background 0.3s;
-		&::before {
-			z-index: auto;
-			content: '';
-			background-color: #0000;
-			opacity: 0.08;
-			position: absolute;
-			//left: 8px;
-			//right: 8px;
-			left: 0;
-			right: 0;
-			top: 0;
-			bottom: 0;
-			pointer-events: none;
-			border-radius: 3px;
-			transition: background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
-		}
-
-		&:hover {
-			//background: rgba(0, 0, 0, 0.025);
-			//background: var(--el-fill-color);
-			&::before {
-				background-color: var(--el-color-primary);
-			}
-		}
-	}
-}
-
-.avatar-container {
-	.avatar-wrapper {
+	.menu--avatar {
+		//height: 100%;
 		display: flex;
 		align-items: center;
 		white-space: nowrap;

+ 21 - 0
src/layout/components/Header/components/SwitchDarkTheme.vue

@@ -0,0 +1,21 @@
+<template>
+	<el-tooltip placement="top" :content="$t('le.menuDarkTheme')">
+		<div class="menu--theme menu-item le-hover-effect--bg" @click="changeDark">
+			<el-icon>
+				<Moon v-if="setting.isDark" />
+				<Sunny v-else />
+			</el-icon>
+		</div>
+	</el-tooltip>
+</template>
+
+<script setup lang="ts">
+import useStore from '@/store'
+import { useTheme } from '@/hooks/useTheme'
+const { switchDark } = useTheme()
+const { setting } = useStore()
+const changeDark = () => {
+	setting.isDark = !setting.isDark
+	switchDark()
+}
+</script>

+ 6 - 7
src/layout/components/TagsView/index.vue

@@ -64,7 +64,7 @@ const router = useRouter()
 const route = useRoute()
 
 const visitedViews = computed<any>(() => tagsView.visitedViews)
-const routes = computed<any>(() => permission.routes)
+const menuList = computed<any>(() => permission.menuList)
 
 const affixTags = ref([])
 const visible = ref(false)
@@ -86,10 +86,10 @@ watch(visible, value => {
 	}
 })
 
-function filterAffixTags(routes: RouteRecordRaw[], basePath = '/') {
+function filterAffixTags(menuList: RouteRecordRaw[], basePath = '/') {
 	let tags: TagView[] = []
 
-	routes.forEach(route => {
+	menuList.forEach(route => {
 		// affix 固定钉子
 		if (route.meta && route.meta.affix) {
 			const tagPath = path.resolve(basePath, route.path)
@@ -112,7 +112,7 @@ function filterAffixTags(routes: RouteRecordRaw[], basePath = '/') {
 }
 
 function initTags() {
-	const res = filterAffixTags(routes.value) as []
+	const res = filterAffixTags(menuList.value) as []
 	affixTags.value = res
 	for (const tag of res) {
 		// Must have tag name
@@ -246,7 +246,6 @@ function openMenu(tag: TagView, e: MouseEvent) {
 	} else {
 		left.value = l
 	}
-
 	top.value = e.clientY - 40
 	visible.value = true
 	selectedTag.value = tag
@@ -329,9 +328,9 @@ onMounted(() => {
 					//color: #fff;
 					border-color: var(--el-color-primary);
 					/*background-color: var(--el-button-hover-bg-color);
-					color: var(--el-button-hover-text-color);*/
+          color: var(--el-button-hover-text-color);*/
 					/*background-color: var(--el-border-color-lighter);
-					color: var(--el-text-color-primary);*/
+          color: var(--el-text-color-primary);*/
 				}
 			}
 		}

+ 1 - 1
src/main.ts

@@ -23,7 +23,7 @@ import 'virtual:svg-icons-register'
 	const existIconVersion = false
 	if (!existIconVersion) {
 		/** update 最新 le-iconfont(.css && .js) */
-		const origin_prefix = '//at.alicdn.com/t/c/font_4091949_0v9i1byqy04'
+		const origin_prefix = '//at.alicdn.com/t/c/font_4091949_h5ex5dw89e'
 		const link = d.createElement('link')
 		link.rel = 'stylesheet'
 		link.type = 'text/css'

+ 6 - 1
src/permission.ts

@@ -38,7 +38,12 @@ router.beforeEach(async (to, from, next) => {
 				const accessRoutes: any = await permission.queryMenuList([])
 				// 单独处理 菜单实体路径
 				accessRoutes.forEach((route: any) => {
-					router.addRoute(route)
+					// console.error(route, 'route....', route.meta?.parentName)
+					if (route.meta?.parentName) {
+						router.addRoute(route.meta.parentName, route)
+					} else {
+						router.addRoute(route)
+					}
 				})
 				/*router.addRoute('testLayout', {
 					// 管理员管理

+ 114 - 140
src/router/index.ts

@@ -4,39 +4,42 @@ import useStore from '@/store'
 
 export const Layout = () => import('@/layout/index.vue')
 
-// 参数说明: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
-// 静态路由
-type RouteMeta = {
-	// 标题
-	title?: string
-	// 图标
-	icon?: string
-	// 是否固定
-	affix?: true
-	// 是否在菜单列表进行隐藏
-	hidden?: boolean
-	// // 类型
-	// type: string;
-}
+const HOME_URL = '/dashboard'
 // 是否展示示例相关菜单
 const showDemoRoutes = !!import.meta.env.DEV
-export const constantRoutes: Array<AppRouteRecordRaw> = [
+// 参数说明: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
+// 静态路由
+export const sysStaticRouter: Array<AppRouteRecordRaw> = [
 	{
-		path: '/redirect',
+		path: '/login',
+		component: () => import('@/views/login/index.vue'),
+		meta: { hidden: true }
+	},
+	// 主入口
+	{
+		path: '/',
+		name: 'mainLayout',
 		component: Layout,
-		meta: { hidden: true },
+		redirect: HOME_URL,
+		// redirect: '/dashboard',
 		children: [
+			// 重定向
 			{
 				path: '/redirect/:path(.*)',
+				meta: { hidden: true },
 				component: () => import('@/views/redirect/index.vue')
+			},
+			{
+				path: 'dashboard',
+				component: () => import('@/views/dashboard/index.vue'),
+				name: 'dashboard',
+				meta: { title: 'dashboard', icon: 'icon-homepage', affix: true }
 			}
 		]
-	},
-	{
-		path: '/login',
-		component: () => import('@/views/login/index.vue'),
-		meta: { hidden: true }
-	},
+	}
+]
+
+export const sysErrorRoutes = [
 	{
 		path: '/404',
 		component: () => import('@/views/error-page/404.vue'),
@@ -47,40 +50,34 @@ export const constantRoutes: Array<AppRouteRecordRaw> = [
 		component: () => import('@/views/error-page/401.vue'),
 		meta: { hidden: true }
 	},
-	// 用户
 	{
-		path: '/profile',
-		component: Layout,
-		name: 'profile',
-		meta: { hidden: true, title: '用户' },
-		redirect: '/profile/index',
-		children: [
-			{
-				path: 'index',
-				component: () => import('@/views/profile/index.vue'),
-				name: 'profileIndex',
-				meta: {
-					hidden: true,
-					title: 'profile'
-				}
-			}
-		]
+		// redirect: '/404',
+		path: '/:pathMatch(.*)',
+		component: () => import('@/views/error-page/404.vue'),
+		meta: { hidden: true }
+	}
+]
+
+export const constantRoutes: AppRouteRecordRaw[] = [
+	// 首页
+	{
+		// path: '/dashboard',
+		path: HOME_URL,
+		component: () => import('@/views/dashboard/index.vue'),
+		name: 'dashboard',
+		meta: { title: 'dashboard', icon: 'icon-homepage', affix: true, parentName: 'mainLayout' }
 	},
-	// 首页 	// 主入口
+	/*// 外部链接
 	{
-		path: '/',
-		name: 'layout',
+		path: '/external-link',
 		component: Layout,
-		redirect: '/dashboard',
 		children: [
 			{
-				path: 'dashboard',
-				component: () => import('@/views/dashboard/index.vue'),
-				name: 'dashboard',
-				meta: { title: 'dashboard', icon: 'icon-homepage', affix: true }
+				path: 'https://github.com/LanceJiang/Lance-Element-Admin',
+				meta: { title: '外部链接', icon: 'icon-link' }
 			}
 		]
-	},
+	},*/
 	...(showDemoRoutes
 		? [
 				// 组件示例
@@ -101,7 +98,7 @@ export const constantRoutes: Array<AppRouteRecordRaw> = [
 				{
 					path: '/form',
 					component: Layout,
-					// meta: { title: 'Form', icon: 'icon-guide' },
+					// meta: { title: 'Form', icon: 'guide' },
 					redirect: '/default',
 					children: [
 						{
@@ -116,7 +113,7 @@ export const constantRoutes: Array<AppRouteRecordRaw> = [
 				{
 					path: '/table',
 					component: Layout,
-					redirect: '/default',
+					redirect: 'default',
 					meta: { title: 'table', icon: 'icon-table' },
 					children: [
 						{
@@ -166,112 +163,88 @@ export const constantRoutes: Array<AppRouteRecordRaw> = [
 				{
 					// demo演示
 					path: '/demo',
-					component: Layout,
+					component: 'Layout',
 					redirect: '/demo/adminManage',
 					meta: { title: 'demo', icon: 'icon-peoples' },
 					children: [
 						{
+							// path: '/demo/pageConfig',
 							path: 'pageConfig',
-							component: () => import('@/views/demo/pageConfig/index'),
-							// component: 'demo/pageConfig/index',
+							// component: () => import('@/views/demo/pageConfig/index'),
+							component: 'demo/pageConfig/index',
 							name: 'pageConfig',
 							meta: { title: 'demo_pageConfig', icon: 'le-fangda1' }
 						},
 						{
 							// 管理员管理
+							// path: '/demo/adminManage',
 							path: 'adminManage',
 							name: 'adminManage',
-							component: () => import('@/views/demo/adminManage/index'),
-							// component: 'demo/adminManage/index',
+							component: 'demo/adminManage/index',
 							meta: { title: 'demo_adminManage', icon: 'Setting' }
 						}
 					]
 				}
 		  ]
-		: [])
-	// 仅用于研发测试 START
-	/*{
-        path: '/test',
-        component: Layout,
-        // meta: {hidden: true, title: 'test', icon: 'system'},
-        meta: { title: 'test', icon: 'system' },
-        redirect: '/test/testSetup',
-        children: [
-            {
-                path: 'testSetup',
-                component: () => import('@/views/test/testSetup.vue'),
-                name: 'testSetup',
-                meta: { title: 'testSetup' }
-            },
-            {
-                path: 'componentCommunication',
-                component: () => import('@/views/test/componentCommunication/index.vue'),
-                name: 'componentCommunication',
-                meta: { title: '组件通信方式' }
-            }
-        ]
-    },*/
-	// 仅用于研发测试 END
-	// 外部链接
-	/*{
-        path: '/external-link',
-        component: Layout,
-        children: [
-            {
-                path: 'https://github.com/LanceJiang/vue3_element_admin',
-                meta: { title: '外部链接', icon: 'icon-link' }
-            }
-        ]
-    }*/
-]
-const getFlatMenuList_1children = (menuList: AppRouteRecordRaw[]) => {
-	return menuList.reduce((res, v) => {
-		// 过滤掉隐藏
-		if (v.meta?.hidden) return res
-		const children = v.children
-		if (Array.isArray(children) && children.length) {
-			if (children.length === 1) {
-				const child0 = children[0]
-				// delete v.children
-				res.push({
-					...child0,
-					path: /\/.*/.test(child0.path) ? child0.path : v.name !== 'layout' ? `${v.path}/${child0.path}` : `/${child0.path}`
-				})
-			} else {
-				res.push({
-					...v,
-					children: getFlatMenuList_1children(v.children as AppRouteRecordRaw[])
-				})
+		: []),
+	// 用户
+	{
+		path: '/profile',
+		component: Layout,
+		name: 'profile',
+		meta: { hidden: true, title: '用户' },
+		redirect: '/profile/index',
+		children: [
+			{
+				path: 'index',
+				component: () => import('@/views/profile/index.vue'),
+				name: 'profileIndex',
+				meta: {
+					hidden: true,
+					title: 'profile',
+					icon: 'le-account'
+					// parentName: 'mainLayout'
+				}
 			}
-		} else {
-			res.push(v)
-		}
-		return res
-	}, [] as AppRouteRecordRaw[])
-}
-export const constantMenuList: Array<AppRouteRecordRaw> = getFlatMenuList_1children(constantRoutes)
-export const noFoundRouters = [
+		]
+	}
+	/*// 仅用于研发测试 START
 	{
-		// redirect: '/404',
-		path: '/:pathMatch(.*)',
-		component: () => import('@/views/error-page/404.vue'),
-		meta: { hidden: true }
+		path: '/test',
+		component: Layout,
+		// meta: {hidden: true, title: 'test', icon: 'system'},
+		meta: { title: 'test', icon: 'system' },
+		redirect: '/test/testSetup',
+		children: [
+			{
+				path: 'testSetup',
+				component: () => import('@/views/test/testSetup.vue'),
+				name: 'testSetup',
+				meta: { title: 'testSetup' }
+			},
+			{
+				path: 'componentCommunication',
+				component: () => import('@/views/test/componentCommunication/index.vue'),
+				name: 'componentCommunication',
+				meta: { title: '组件通信方式' }
+			}
+		]
 	}
+	// 仅用于研发测试 END*/
 ]
 
 /**
- * local_permissionsRoutes: 本地存储带权限路由 每次有新的路由配置 请做好标注!!!
+ * local_permissionsMenuList: 每次有新的路由配置 请做好标注!!!
  * 本地 dev 调试 默认使用本地路由数据
  * (若想要调试 接口数据 请在 env.development.local 修改 VITE_APP_USE_LOCAL_ROUTES 不为 1即可)
  */
-export const local_permissionsRoutes: Array<AppRouteRecordRaw> = [
-	// todo 请添加相关新路由描述
+export const local_permissionsMenuList: Array<AppRouteRecordRaw> = [
+	...constantRoutes,
 	// 设置 权限
 	{
 		path: '/setting',
-		// component: Layout,
 		component: 'Layout',
-		meta: { title: '设置', icon: 'icon-guide' },
+		meta: { title: '设置', icon: 'icon-swagger' },
 		redirect: '/setting/user',
 		children: [
 			{
@@ -279,7 +252,14 @@ export const local_permissionsRoutes: Array<AppRouteRecordRaw> = [
 				// component: () => import('@/views/setting/user/index.vue'),
 				component: 'setting/user/index',
 				name: 'user',
-				meta: { title: '用户管理', icon: '' }
+				meta: { title: '用户管理', icon: 'icon-logo' } // 本地icon
+			},
+			{
+				path: 'role',
+				// component: () => import('@/views/setting/role/index.vue'),
+				component: 'setting/role/index',
+				name: 'role',
+				meta: { title: '角色管理', icon: 'le-amazon' } // le-iconfont
 			},
 			{
 				path: 'post',
@@ -288,13 +268,6 @@ export const local_permissionsRoutes: Array<AppRouteRecordRaw> = [
 				name: 'post',
 				meta: { title: '岗位管理', icon: '' }
 			},
-			{
-				path: 'role',
-				// component: () => import('@/views/setting/role/index.vue'),
-				component: 'setting/role/index',
-				name: 'role',
-				meta: { title: '角色管理', icon: '' }
-			},
 			{
 				path: 'app',
 				// component: () => import('@/views/setting/app/index.vue'),
@@ -333,10 +306,10 @@ export const local_permissionsRoutes: Array<AppRouteRecordRaw> = [
 			},
 			{
 				path: 'menu',
-				// component: () => import('@/views/setting/menu/index.vue'),
-				component: 'setting/menu/index',
+				component: () => import('@/views/setting/menu/index.vue'),
+				// component: 'setting/menu/index',
 				name: 'menu',
-				meta: { title: '菜单管理', icon: '' }
+				meta: { title: '菜单管理', icon: 'PriceTag' } // element icons
 			}
 		]
 	},
@@ -493,12 +466,13 @@ export const local_permissionsRoutes: Array<AppRouteRecordRaw> = [
 			}
 		]
 	}
+	// todo 请添加相关新路由描述
 ]
 
 // 创建路由
 const router = createRouter({
 	history: createWebHashHistory(),
-	routes: constantRoutes.concat(noFoundRouters) as RouteRecordRaw[],
+	routes: sysStaticRouter.concat(sysErrorRoutes) as RouteRecordRaw[],
 	// 刷新时,滚动条位置还原
 	scrollBehavior: () => ({ left: 0, top: 0 })
 })
@@ -506,7 +480,7 @@ const router = createRouter({
 // 重置路由
 export function resetRouter() {
 	const { permission } = useStore()
-	permission.routes.forEach(route => {
+	permission.showMenuListFlat.forEach(route => {
 		const name = route.name
 		if (name && router.hasRoute(name)) {
 			router.removeRoute(name)

+ 8 - 0
src/router/types.ts

@@ -8,6 +8,12 @@ export interface MetaProps {
 	// 标题
 	title?: string
 	// 图标
+	/**
+	 * 关于icon 描述:
+	 * 	// 1.来自本地src/assets/icons 的svg: 'icon-[dir]-[name]'
+	 * 	// 2.le-iconfont svg 链接: 'le-[name]'
+	 * 	// 3. 匹配不到icon- & le- 默认element
+	 */
 	icon?: string
 	// 隐藏菜单
 	hidden?: boolean
@@ -15,6 +21,8 @@ export interface MetaProps {
 	affix?: boolean
 	// 不缓存路由
 	noCache?: boolean
+	// 当前路由配置添加到 哪个父级: router.addRoute([parentName], route)
+	parentName?: string
 
 	// todo???
 	activeMenu?: string

+ 56 - 20
src/store/modules/permission.ts

@@ -2,14 +2,13 @@ import { PermissionState } from '@/types'
 import { AppRouteRecordRaw } from '@/router/types'
 // import { RouteRecordRaw } from 'vue-router'
 import { defineStore } from 'pinia'
-import { constantRoutes, noFoundRouters, constantMenuList } from '@/router'
+// import { sysStaticRouter, noFoundRouters, constantMenuList } from '@/router'
 import { getMenuList } from '@/api/system/menu'
 
 const modules = import.meta.glob('@/views/**/*.vue')
 export const Layout = () => import('@/layout/index.vue') // todo...
 // export const Layout = () => import('@/layout/index_old.vue.vue')
-// export const RouteView = () => import('@/layout/RouteView.vue')
-// export const Layout = () => import('@/layouts/index.vue')
+export const RouteView = () => import('@/layout/RouteView.vue')
 
 // const hasPermission = (roles: string[], route: AppRouteRecordRaw) => {
 // 	if (route.meta && route.meta.roles) {
@@ -25,10 +24,16 @@ export const Layout = () => import('@/layout/index.vue') // todo...
 // 	return false
 // }
 
-export const filterAsyncRoutes = (routes: AppRouteRecordRaw[], roles: string[]) => {
+export const filterAsyncRoutes = (routes: AppRouteRecordRaw[], roles: string[], parentPath = '') => {
 	const res: AppRouteRecordRaw[] = []
 	routes.forEach(route => {
-		const tmp = { ...route } as any
+		const tmp = { ...route } as AppRouteRecordRaw
+		tmp.meta = tmp.meta ? tmp.meta : {}
+		// 默认icon
+		tmp.meta.icon = tmp.meta.icon || 'menu'
+		// path全链 重组
+		// tmp.path = /\/.*/.test(tmp.path) ? tmp.path : `${parentPath}/${tmp.path}`
+		tmp.path = /\/.+/.test(tmp.path) ? tmp.path : `${parentPath}/${tmp.path}`
 		// if (hasPermission(roles, tmp)) {
 		// todo be delete
 		// }
@@ -38,7 +43,7 @@ export const filterAsyncRoutes = (routes: AppRouteRecordRaw[], roles: string[])
 			tmp.component = RouteView
 		} else*/ if (!tmp.component || tmp.component === 'Layout') {
 			tmp.component = Layout
-		} else {
+		} else if (typeof tmp.component === 'string') {
 			const component = modules[`/src/views/${tmp.component}.vue`] as any
 			console.error(component, 'component')
 			if (component) {
@@ -51,23 +56,50 @@ export const filterAsyncRoutes = (routes: AppRouteRecordRaw[], roles: string[])
 
 		// 递归
 		if (tmp.children) {
-			tmp.children = filterAsyncRoutes(tmp.children, roles)
+			tmp.children = filterAsyncRoutes(tmp.children, roles, tmp.path)
 		}
 	})
 	return res
 }
 
+/**
+ * getFlatMenuList_1children 单child 菜单扁平优化
+ * 针对:children 只有一个child 的 进行菜单优化
+ */
+const getFlatMenuList_1children = (menuList: AppRouteRecordRaw[]) => {
+	return menuList.reduce((res, v) => {
+		// 过滤掉隐藏
+		if (v.meta?.hidden) return res
+		const children = v.children
+		if (Array.isArray(children) && children.length) {
+			if (children.length === 1) {
+				const child0 = children[0]
+				// delete v.children
+				res.push({
+					...child0,
+					path: /\/.*/.test(child0.path) ? child0.path : v.name !== 'mainLayout' ? `${v.path}/${child0.path}` : `/${child0.path}`
+				})
+			} else {
+				res.push({
+					...v,
+					children: getFlatMenuList_1children(v.children as AppRouteRecordRaw[])
+				})
+			}
+		} else {
+			res.push(v)
+		}
+		return res
+	}, [] as AppRouteRecordRaw[])
+}
+
 // 过滤隐藏
-const getShowMenuList = (menuList: AppRouteRecordRaw[], parentPath = '') => {
+// const getShowMenuList = (menuList: AppRouteRecordRaw[], parentPath = '') => {
+const getShowMenuList = (menuList: AppRouteRecordRaw[]) => {
 	return menuList.filter(item => {
 		// console.error(item, 'item')
-		item.meta = item.meta ? item.meta : {}
-		// 默认icon
-		item.meta.icon = item.meta.icon || 'menu'
-		// path全链 重组
-		item.path = /\/.*/.test(item.path) ? item.path : `${parentPath}/${item.path}`
 		if (!item.meta.hidden) {
-			item.children?.length && (item.children = getShowMenuList(item.children, item.path))
+			// item.children?.length && (item.children = getShowMenuList(item.children, item.path))
+			item.children?.length && (item.children = getShowMenuList(item.children))
 			return true
 		}
 		return false
@@ -85,13 +117,14 @@ const getMenuListFlat = (menuList: AppRouteRecordRaw[]) => {
 const usePermissionStore = defineStore({
 	id: 'permission',
 	state: (): PermissionState => ({
-		routes: [],
+		// routes: [],
 		// 动态菜单
 		menuList: []
 	}),
 	getters: {
 		// 有效的 菜单列表
-		showMenuList: state => getShowMenuList(JSON.parse(JSON.stringify([...constantMenuList, ...state.menuList]))),
+		// showMenuList: state => getShowMenuList(JSON.parse(JSON.stringify([...constantMenuList, ...state.menuList]))),
+		showMenuList: state => getShowMenuList(JSON.parse(JSON.stringify(state.menuList))),
 		/*getShowMenuList([
 				...JSON.parse(JSON.stringify(state.menuList)),
 				// 测试
@@ -106,14 +139,17 @@ const usePermissionStore = defineStore({
 		// showMenuList: state => getShowMenuList(JSON.parse(JSON.stringify(state.routes)))
 		// 菜单权限列表 ==> 扁平化之后的一维数组菜单,主要用来添加动态路由
 		showMenuListFlat() {
+			// console.error(state, 'test...', this)
 			return getMenuListFlat(this.showMenuList)
 		}
 	},
 	actions: {
 		setRoutes(menuList: AppRouteRecordRaw[]) {
-			// 授权后的菜单列表
-			this.menuList = menuList
-			this.routes = menuList /*constantRoutes.concat(
+			// 授权后的菜单列表 对单个的 children 菜单减级优化
+			this.menuList = getFlatMenuList_1children(menuList)
+			// menuList
+			/*this.routes = menuList
+			sysStaticRouter.concat(
 				menuList,
 				noFoundRouters /!*, {
 				// 管理员管理
@@ -133,7 +169,7 @@ const usePermissionStore = defineStore({
 			return getMenuList().then((data: any) => {
 				console.error(data, 'menuList')
 				const menuList = filterAsyncRoutes(
-					data!.menu.filter((v: any) => /\/.*/.test(v.path)),
+					data!.menu, // .filter((v: any) => /\/.*/.test(v.path)),
 					roles
 				)
 				this.setRoutes(menuList)

+ 0 - 1
src/types/store.d.ts

@@ -18,7 +18,6 @@ export interface AppState {
  * 权限类型声明
  */
 export interface PermissionState {
-	routes: AppRouteRecordRaw[]
 	menuList: AppRouteRecordRaw[]
 }
 

+ 4 - 3
src/views/error-page/404.vue

@@ -25,9 +25,9 @@ const { app } = useStore()
 export default {
 	name: 'Page404',
 	data() {
-		// return {
-		//   locale: 'cn' // en
-		// }
+		return {
+			// locale: 'cn' // en
+		}
 	},
 	computed: {
 		// language() {
@@ -204,6 +204,7 @@ export default {
 		&__headline {
 			font-size: 20px;
 			line-height: 24px;
+			//color: #222;
 			color: var(--el-text-color-primary);
 			font-weight: bold;
 			opacity: 0;