瀏覽代碼

前端框架与官方仓库同步升级v2.4.0(破坏性修改,改成setup模式)

zuohuaijun 3 年之前
父節點
當前提交
79b3a1b82b
共有 100 個文件被更改,包括 5096 次插入 和 4657 次删除
  1. 4 4
      Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj
  2. 14 1
      Web/.eslintrc.js
  3. 14 0
      Web/CHANGELOG.md
  4. 10 9
      Web/README.md
  5. 187 183
      Web/package-lock.json
  6. 16 13
      Web/package.json
  7. 0 6
      Web/plugins.d.ts
  8. 0 13
      Web/shim.d.ts
  9. 0 6
      Web/source.d.ts
  10. 60 69
      Web/src/App.vue
  11. 2 10
      Web/src/api/login/index.ts
  12. 8 14
      Web/src/api/menu/index.ts
  13. 16 20
      Web/src/components/auth/auth.vue
  14. 16 20
      Web/src/components/auth/authAll.vue
  15. 21 25
      Web/src/components/auth/auths.vue
  16. 62 62
      Web/src/components/cropper/index.vue
  17. 77 74
      Web/src/components/editor/index.vue
  18. 184 190
      Web/src/components/iconSelector/index.vue
  19. 127 131
      Web/src/components/noticeBar/index.vue
  20. 48 58
      Web/src/components/svgIcon/index.vue
  21. 259 0
      Web/src/components/table/index.vue
  22. 40 0
      Web/src/directive/authDirective.ts
  23. 178 0
      Web/src/directive/customDirective.ts
  24. 18 0
      Web/src/directive/index.ts
  25. 1 0
      Web/src/i18n/lang/en.ts
  26. 1 0
      Web/src/i18n/lang/zh-cn.ts
  27. 1 0
      Web/src/i18n/lang/zh-tw.ts
  28. 134 145
      Web/src/layout/component/aside.vue
  29. 139 166
      Web/src/layout/component/columnsAside.vue
  30. 8 15
      Web/src/layout/component/header.vue
  31. 44 43
      Web/src/layout/component/main.vue
  32. 2 6
      Web/src/layout/footer/index.vue
  33. 38 42
      Web/src/layout/index.vue
  34. 135 161
      Web/src/layout/lockScreen/index.vue
  35. 20 32
      Web/src/layout/logo/index.vue
  36. 52 58
      Web/src/layout/main/classic.vue
  37. 52 59
      Web/src/layout/main/columns.vue
  38. 52 59
      Web/src/layout/main/defaults.vue
  39. 45 51
      Web/src/layout/main/transverse.vue
  40. 72 89
      Web/src/layout/navBars/breadcrumb/breadcrumb.vue
  41. 9 17
      Web/src/layout/navBars/breadcrumb/closeFull.vue
  42. 79 92
      Web/src/layout/navBars/breadcrumb/index.vue
  43. 63 82
      Web/src/layout/navBars/breadcrumb/search.vue
  44. 237 262
      Web/src/layout/navBars/breadcrumb/setings.vue
  45. 140 157
      Web/src/layout/navBars/breadcrumb/user.vue
  46. 2 2
      Web/src/layout/navBars/breadcrumb/userNews.vue
  47. 14 20
      Web/src/layout/navBars/index.vue
  48. 88 95
      Web/src/layout/navBars/tagsView/contextmenu.vue
  49. 507 570
      Web/src/layout/navBars/tagsView/tagsView.vue
  50. 99 110
      Web/src/layout/navMenu/horizontal.vue
  51. 19 29
      Web/src/layout/navMenu/subItem.vue
  52. 65 82
      Web/src/layout/navMenu/vertical.vue
  53. 72 75
      Web/src/layout/routerView/iframes.vue
  54. 26 46
      Web/src/layout/routerView/link.vue
  55. 79 97
      Web/src/layout/routerView/parent.vue
  56. 1 1
      Web/src/main.ts
  57. 6 7
      Web/src/router/backEnd.ts
  58. 4 4
      Web/src/router/frontEnd.ts
  59. 16 0
      Web/src/router/route.ts
  60. 0 101
      Web/src/stores/interface/index.ts
  61. 0 1
      Web/src/stores/keepAliveNames.ts
  62. 0 1
      Web/src/stores/requestOldRoutes.ts
  63. 0 1
      Web/src/stores/routesList.ts
  64. 0 1
      Web/src/stores/tagsViewRoutes.ts
  65. 2 3
      Web/src/stores/themeConfig.ts
  66. 3 5
      Web/src/stores/userInfo.ts
  67. 7 0
      Web/src/theme/dark.scss
  68. 14 1
      Web/src/theme/element.scss
  69. 4 0
      Web/src/theme/media/layout.scss
  70. 5 5
      Web/src/theme/media/pagination.scss
  71. 25 0
      Web/src/theme/tableTool.scss
  72. 13 0
      Web/src/types/axios.d.ts
  73. 112 0
      Web/src/types/global.d.ts
  74. 59 0
      Web/src/types/layout.d.ts
  75. 46 0
      Web/src/types/mitt.d.ts
  76. 0 22
      Web/src/types/mitt.ts
  77. 97 0
      Web/src/types/pinia.d.ts
  78. 322 0
      Web/src/types/views.d.ts
  79. 6 7
      Web/src/utils/arrayOperation.ts
  80. 11 10
      Web/src/utils/commonFunction.ts
  81. 0 1
      Web/src/utils/mitt.ts
  82. 31 13
      Web/src/utils/other.ts
  83. 24 163
      Web/src/utils/request.ts
  84. 1 1
      Web/src/utils/setIconfont.ts
  85. 2 2
      Web/src/utils/storage.ts
  86. 58 54
      Web/src/utils/theme.ts
  87. 2 2
      Web/src/utils/wartermark.ts
  88. 24 30
      Web/src/views/chart/head.vue
  89. 258 268
      Web/src/views/chart/index.vue
  90. 8 17
      Web/src/views/error/401.vue
  91. 7 14
      Web/src/views/error/404.vue
  92. 10 20
      Web/src/views/fun/clipboard/index.vue
  93. 61 68
      Web/src/views/fun/countup/index.vue
  94. 15 23
      Web/src/views/fun/cropper/index.vue
  95. 100 106
      Web/src/views/fun/echartsMap/index.vue
  96. 15 22
      Web/src/views/fun/gridLayout/index.vue
  97. 15 24
      Web/src/views/fun/printJs/index.vue
  98. 23 34
      Web/src/views/fun/qrcode/index.vue
  99. 4 14
      Web/src/views/fun/splitpanes/index.vue
  100. 29 41
      Web/src/views/fun/tagsView/index.vue

+ 4 - 4
Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj

@@ -16,9 +16,9 @@
     <PackageReference Include="AngleSharp" Version="0.17.1" />
     <PackageReference Include="AspNetCoreRateLimit" Version="4.0.2" />
     <PackageReference Include="DotNetCore.Natasha.CSharp" Version="5.0.2" />
-    <PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.8.2.2" />
-    <PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.8.2.2" />
-    <PackageReference Include="Furion.Pure" Version="4.8.2.2" />
+    <PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.8.2.3" />
+    <PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.8.2.3" />
+    <PackageReference Include="Furion.Pure" Version="4.8.2.3" />
     <PackageReference Include="Lazy.Captcha.Core" Version="1.1.6" />
     <PackageReference Include="Magicodes.IE.Excel" Version="2.7.0" />
     <PackageReference Include="Magicodes.IE.Pdf" Version="2.7.0" />
@@ -29,7 +29,7 @@
     <PackageReference Include="OnceMi.AspNetCore.OSS" Version="1.1.9" />
     <PackageReference Include="SKIT.FlurlHttpClient.Wechat.Api" Version="2.20.2" />
     <PackageReference Include="SKIT.FlurlHttpClient.Wechat.TenpayV3" Version="2.15.0" />
-    <PackageReference Include="SqlSugarCore" Version="5.1.3.34" />
+    <PackageReference Include="SqlSugarCore" Version="5.1.3.35" />
     <PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.23" />
     <PackageReference Include="UAParser" Version="3.1.47" />
     <PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />

+ 14 - 1
Web/.eslintrc.js

@@ -13,9 +13,18 @@ module.exports = {
 	},
 	extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'],
 	plugins: ['vue', '@typescript-eslint'],
+	overrides: [
+		{
+			files: ['*.ts', '*.tsx', '*.vue'],
+			rules: {
+				'no-undef': 'off',
+			},
+		},
+	],
 	rules: {
 		// http://eslint.cn/docs/rules/
 		// https://eslint.vuejs.org/rules/
+		// https://typescript-eslint.io/rules/no-unused-vars/
 		'@typescript-eslint/ban-ts-ignore': 'off',
 		'@typescript-eslint/explicit-function-return-type': 'off',
 		'@typescript-eslint/no-explicit-any': 'off',
@@ -26,6 +35,9 @@ module.exports = {
 		'@typescript-eslint/ban-types': 'off',
 		'@typescript-eslint/no-non-null-assertion': 'off',
 		'@typescript-eslint/explicit-module-boundary-types': 'off',
+		'@typescript-eslint/no-redeclare': 'error',
+		'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
+		'@typescript-eslint/no-unused-vars': [2],
 		'vue/custom-event-name-casing': 'off',
 		'vue/attributes-order': 'off',
 		'vue/one-component-per-file': 'off',
@@ -58,6 +70,7 @@ module.exports = {
 		'no-unused-vars': 'error',
 		'no-v-model-argument': 'off',
 		'no-case-declarations': 'off',
-		'no-console': 'off',
+		'no-console': 'error',
+		'no-redeclare': 'off',
 	},
 };

+ 14 - 0
Web/CHANGELOG.md

@@ -2,6 +2,20 @@
 
 🎉🎉🔥 `vue-next-admin` 基于 vue3.x 、Typescript、vite、Element plus 等,适配手机、平板、pc 的后台开源免费模板库(vue2.x 请切换 vue-prev-admin 分支)
 
+## 2.4.0
+
+`2022.11.29`
+
+⚡⚡⚡ 此版为破坏性更新,应群友建议 `script lang="ts"` 改 `script lang="ts" setup 语法糖`。
+
+- 🌟 更新 依赖更新最新版本
+- 🎉 新增 表格封装演示,路径:`组件封装 -> 表格封装演示`
+- 🎉 新增 master 分支 script lang="ts" 改成 script lang="ts" setup 语法糖,将同步基础分支
+- 🐞 修复 [v2.3.0 版本报错问题处理](https://gitee.com/lyt-top/vue-next-admin/issues/I623RP)
+- 🐞 修复 [el-backtop 滚动高度不触发(固定了 header)](https://gitee.com/lyt-top/vue-next-admin/issues/I63N0D),感谢[@dejavuuuuu](https://gitee.com/zc19951010)
+- 🎯 优化 完善 ts 类型,删除根目录 `plugins.d.ts、shim.d.ts、source.d.ts`,移入到 `/src/types/global.d.ts`
+- 🎯 优化 代码 `watch` 移动到 `生命周期钩子` 最后,文字注释等
+
 ## 2.3.0
 
 `2022.11.16`

+ 10 - 9
Web/README.md

@@ -22,7 +22,7 @@
 
 #### 🌈 介绍
 
-基于 vue3.x + CompositionAPI + typescript + vite + element plus + vue-router-next + next.vuex,适配手机、平板、pc 的后台开源免费模板,希望减少工作量,帮助大家实现快速开发。
+基于 vue3.x + CompositionAPI setup 语法糖 + typescript + vite + element plus + vue-router-next + pinia 技术,适配手机、平板、pc 的后台开源免费模板,希望减少工作量,帮助大家实现快速开发。
 
 #### ⛱️ 线上预览
 
@@ -43,7 +43,7 @@
 
 | Edge      | Firefox      | Chrome      | Safari      |
 | --------- | ------------ | ----------- | ----------- |
-| Edge ≥ 79 | Firefox ≥ 78 | Chrome ≥ 64 | Safari ≥ 12 |
+| Edge ≥ 79 | Firefox ≥ 78 | Chrome ≥ 72 | Safari ≥ 12 |
 
 > 由于 Vue3 不再支持 IE11,故而 ElementPlus 也不支持 IE11 及之前版本。
 
@@ -74,12 +74,12 @@ cnpm run build
 
 #### 💯 学习交流加 QQ 群
 
-- 若加群了没同意(一般秒过),那就是群满了(500 人群),请换一个群试试。群会定期清理半年(6 个月)未发言的群友,资源有限,请谅解。建议勿加多群,可能会误伤!微信群由于只有 `7天有效` 就不放这里了。
-- 群号码:
-  1 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=RdUY97Vx0T0vZ_1OOu-X1yFNkWgDwbjC&jump_from=webapi">665452019</a>
-  2 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=zVfy3gNy7pNWVK3kMduDzwU369PZg2fw&jump_from=webapi">766356862</a>
-  3 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=02EWb5P2JkP-8iwzaDadgFdxA0HSHPpn&jump_from=webapi">795345435</a>
-  4 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=0gTFO04WwkeZZ6R4lju6gucbeXHK-wNd&jump_from=webapi">736626228</a>
+> 若加群了没同意(一般秒过),那就是群满了(500 人群),请换一个群试试。群会定期清理半年(6 个月)未发言的群友,资源有限,请谅解。建议勿加多群,可能会误伤!微信群由于只有 `7天有效` 就不放这里了。
+
+- 1 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=RdUY97Vx0T0vZ_1OOu-X1yFNkWgDwbjC&jump_from=webapi">665452019</a>
+- 2 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=zVfy3gNy7pNWVK3kMduDzwU369PZg2fw&jump_from=webapi">766356862</a>
+- 3 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=02EWb5P2JkP-8iwzaDadgFdxA0HSHPpn&jump_from=webapi">795345435</a>
+- 4 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=0gTFO04WwkeZZ6R4lju6gucbeXHK-wNd&jump_from=webapi">736626228</a>
 
   <a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=RdUY97Vx0T0vZ_1OOu-X1yFNkWgDwbjC&jump_from=webapi">
   	<img src="https://img-blog.csdnimg.cn/35e00f12a3fe4820892ec630ca72f15f.png" width="220" height="220" alt="vue-next-admin 讨论群1" title="vue-next-admin 讨论群1"/>
@@ -113,7 +113,7 @@ cnpm run build
 - <a href="https://github.com/ElemeFE/element" target="_blank">element-ui</a>
 - <a href="https://github.com/element-plus/element-plus" target="_blank">element-plus</a>
 - <a href="https://github.com/vuejs/vue-router-next" target="_blank">vue-router-next</a>
-- <a href="https://github.com/vuejs/vuex" target="_blank">vuex</a>
+- <a href="https://github.com/vuejs/pinia" target="_blank">pinia</a>
 - <a href="https://github.com/apache/echarts" target="_blank">echarts</a>
 - <a href="https://github.com/axios/axios" target="_blank">axios</a>
 - <a href="https://github.com/zenorocha/clipboard.js" target="_blank">clipboard</a>
@@ -132,6 +132,7 @@ cnpm run build
 - <a href="https://github.com/jbaysolutions/vue-grid-layout" target="_blank">vue-grid-layout</a>
 - <a href="https://github.com/antoniandre/splitpanes" target="_blank">splitpanes</a>
 - <a href="https://github.com/jsplumb/jsplumb" target="_blank">jsplumb</a>
+- <a href="https://github.com/hxj9102/table2excel" target="_blank">js-table2excel</a>
 
 #### 💕 特别感谢
 

File diff suppressed because it is too large
+ 187 - 183
Web/package-lock.json


+ 16 - 13
Web/package.json

@@ -1,6 +1,6 @@
 {
 	"name": "vue-next-admin",
-	"version": "2.3.0",
+	"version": "2.4.0",
 	"description": "vue3 vite next admin template",
 	"author": "lyt_20201208",
 	"license": "MIT",
@@ -11,20 +11,22 @@
 	},
 	"dependencies": {
 		"@element-plus/icons-vue": "^2.0.10",
-		"@wangeditor/editor-for-vue": "^5.1.11",
+		"@wangeditor/editor": "^5.1.23",
+		"@wangeditor/editor-for-vue": "^5.1.12",
 		"@microsoft/signalr": "^6.0.10",
-		"axios": "^1.1.3",
+		"axios": "^1.2.0",
 		"countup.js": "^2.3.2",
-		"cropperjs": "^1.5.12",
+		"cropperjs": "^1.5.13",
 		"echarts": "^5.4.0",
 		"echarts-gl": "^2.0.9",
-		"echarts-wordcloud": "^2.0.0",
-		"element-plus": "^2.2.21",
+		"echarts-wordcloud": "^2.1.0",
+		"element-plus": "^2.2.25",
 		"js-cookie": "^3.0.1",
+		"js-table2excel": "^1.0.3",
 		"jsplumb": "^2.15.6",
 		"mitt": "^3.0.0",
 		"nprogress": "^0.2.0",
-		"pinia": "^2.0.23",
+		"pinia": "^2.0.27",
 		"print-js": "^1.6.0",
 		"qrcodejs2-fixes": "^0.0.2",
 		"screenfull": "^6.0.2",
@@ -36,7 +38,7 @@
 		"vue-grid-layout": "^3.0.0-beta1",
 		"vue-i18n": "^9.2.2",
 		"vue-router": "^4.1.6",
-		"vue-json-pretty": "^2.2.2",
+		"vue-json-pretty": "^2.2.3",
 		"vue-signature-pad": "^3.0.2",
 		"vue3-tree-org": "^4.1.1",
 		"animate.css": "^4.1.1",
@@ -46,18 +48,19 @@
 		"@types/node": "^18.11.9",
 		"@types/nprogress": "^0.2.0",
 		"@types/sortablejs": "^1.15.0",
-		"@typescript-eslint/eslint-plugin": "^5.43.0",
-		"@typescript-eslint/parser": "^5.43.0",
+		"@typescript-eslint/eslint-plugin": "^5.45.0",
+		"@typescript-eslint/parser": "^5.45.0",
 		"@vitejs/plugin-vue": "^3.2.0",
 		"@vue/compiler-sfc": "^3.2.45",
 		"dotenv": "^16.0.3",
-		"eslint": "^8.27.0",
-		"eslint-plugin-vue": "^9.7.0",
-		"prettier": "^2.7.1",
+		"eslint": "^8.28.0",
+		"eslint-plugin-vue": "^9.8.0",
+		"prettier": "^2.8.0",
 		"sass": "^1.56.1",
 		"sass-loader": "^13.2.0",
 		"typescript": "^4.9.3",
 		"vite": "^3.2.4",
+		"vite-plugin-vue-setup-extend": "^0.4.0",
 		"vue-eslint-parser": "^9.1.0"
 	},
 	"browserslist": [

+ 0 - 6
Web/plugins.d.ts

@@ -1,6 +0,0 @@
-declare module 'vue-grid-layout';
-declare module 'qrcodejs2-fixes';
-declare module 'splitpanes';
-declare module 'js-cookie';
-declare module 'vform3-builds'
-declare module 'vue-signature-pad'

+ 0 - 13
Web/shim.d.ts

@@ -1,13 +0,0 @@
-/* eslint-disable */
-
-// 声明文件,*.vue 后缀的文件交给 vue 模块来处理
-declare module '*.vue' {
-	import type { DefineComponent } from 'vue';
-	const component: DefineComponent<{}, {}, any>;
-	export default component;
-}
-
-// 声明文件,定义全局变量。其它 app.config.globalProperties.xxx,使用 getCurrentInstance() 来获取
-interface Window {
-	nextLoading: boolean;
-}

+ 0 - 6
Web/source.d.ts

@@ -1,6 +0,0 @@
-declare module '*.json';
-declare module '*.png';
-declare module '*.jpg';
-declare module '*.scss';
-declare module '*.ts';
-declare module '*.js';

+ 60 - 69
Web/src/App.vue

@@ -7,8 +7,8 @@
 	</el-config-provider>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, defineComponent, watch } from 'vue';
+<script setup lang="ts" name="app">
+import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch } from 'vue';
 import { useRoute } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import { storeToRefs } from 'pinia';
@@ -19,73 +19,64 @@ import { Local, Session } from '/@/utils/storage';
 import mittBus from '/@/utils/mitt';
 import setIntroduction from '/@/utils/setIconfont';
 
-export default defineComponent({
-	name: 'app',
-	components: {
-		LockScreen: defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue')),
-		Setings: defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/setings.vue')),
-		CloseFull: defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue')),
-	},
-	setup() {
-		const { messages, locale } = useI18n();
-		const setingsRef = ref();
-		const route = useRoute();
-		const stores = useTagsViewRoutes();
-		const storesThemeConfig = useThemeConfig();
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		// 获取全局组件大小
-		const getGlobalComponentSize = computed(() => {
-			return other.globalComponentSize();
-		});
-		// 获取全局 i18n
-		const getGlobalI18n = computed(() => {
-			return messages.value[locale.value];
-		});
-		// 设置初始化,防止刷新时恢复默认
-		onBeforeMount(() => {
-			// 设置批量第三方 icon 图标
-			setIntroduction.cssCdn();
-			// 设置批量第三方 js
-			setIntroduction.jsCdn();
-		});
-		// 页面加载时
-		onMounted(() => {
-			nextTick(() => {
-				// 监听布局配置弹窗点击打开
-				mittBus.on('openSetingsDrawer', () => {
-					setingsRef.value.openDrawer();
-				});
-				// 获取缓存中的布局配置
-				if (Local.get('themeConfig')) {
-					storesThemeConfig.setThemeConfig(Local.get('themeConfig'));
-					document.documentElement.style.cssText = Local.get('themeConfigStyle');
-				}
-				// 获取缓存中的全屏配置
-				if (Session.get('isTagsViewCurrenFull')) {
-					stores.setCurrenFullscreen(Session.get('isTagsViewCurrenFull'));
-				}
-			});
-		});
-		// 页面销毁时,关闭监听布局配置/i18n监听
-		onUnmounted(() => {
-			mittBus.off('openSetingsDrawer', () => {});
+// 引入组件
+const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue'));
+const Setings = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/setings.vue'));
+const CloseFull = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/closeFull.vue'));
+
+// 定义变量内容
+const { messages, locale } = useI18n();
+const setingsRef = ref();
+const route = useRoute();
+const stores = useTagsViewRoutes();
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+
+// 获取全局组件大小
+const getGlobalComponentSize = computed(() => {
+	return other.globalComponentSize();
+});
+// 获取全局 i18n
+const getGlobalI18n = computed(() => {
+	return messages.value[locale.value];
+});
+// 设置初始化,防止刷新时恢复默认
+onBeforeMount(() => {
+	// 设置批量第三方 icon 图标
+	setIntroduction.cssCdn();
+	// 设置批量第三方 js
+	setIntroduction.jsCdn();
+});
+// 页面加载时
+onMounted(() => {
+	nextTick(() => {
+		// 监听布局配'置弹窗点击打开
+		mittBus.on('openSetingsDrawer', () => {
+			setingsRef.value.openDrawer();
 		});
-		// 监听路由的变化,设置网站标题
-		watch(
-			() => route.path,
-			() => {
-				other.useTitle();
-			},
-			{
-				deep: true,
-			}
-		);
-		return {
-			themeConfig,
-			setingsRef,
-			getGlobalComponentSize,
-			getGlobalI18n,
-		};
-	},
+		// 获取缓存中的布局配置
+		if (Local.get('themeConfig')) {
+			storesThemeConfig.setThemeConfig({ themeConfig: Local.get('themeConfig') });
+			document.documentElement.style.cssText = Local.get('themeConfigStyle');
+		}
+		// 获取缓存中的全屏配置
+		if (Session.get('isTagsViewCurrenFull')) {
+			stores.setCurrenFullscreen(Session.get('isTagsViewCurrenFull'));
+		}
+	});
+});
+// 页面销毁时,关闭监听布局配置/i18n监听
+onUnmounted(() => {
+	mittBus.off('openSetingsDrawer', () => {});
 });
+// 监听路由的变化,设置网站标题
+watch(
+	() => route.path,
+	() => {
+		other.useTitle();
+	},
+	{
+		deep: true,
+	}
+);
 </script>

+ 2 - 10
Web/src/api/login/index.ts

@@ -8,18 +8,10 @@ import request from '/@/utils/request';
 export function useLoginApi() {
 	return {
 		signIn: (params: object) => {
-			return request({
-				url: '/user/signIn',
-				method: 'post',
-				data: params,
-			});
+			return request.post('/user/signIn', params);
 		},
 		signOut: (params: object) => {
-			return request({
-				url: '/user/signOut',
-				method: 'post',
-				data: params,
-			});
+			return request.post('/user/signOut', params);
 		},
 	};
 }

+ 8 - 14
Web/src/api/menu/index.ts

@@ -1,26 +1,20 @@
 import request from '/@/utils/request';
 
 /**
+ * 以下为模拟接口地址,gitee 的不通,就换自己的真实接口地址
+ *
  * 后端控制菜单模拟json,路径在 https://gitee.com/lyt-top/vue-next-admin-images/tree/master/menu
  * 后端控制路由,isRequestRoutes 为 true,则开启后端控制路由
- * @method getMenuAdmin 获取后端动态路由菜单(admin)
- * @method getMenuTest 获取后端动态路由菜单(test)
+ * @method getAdminMenu 获取后端动态路由菜单(admin)
+ * @method getTestMenu 获取后端动态路由菜单(test)
  */
 export function useMenuApi() {
 	return {
-		getMenuAdmin: (params?: object) => {
-			return request({
-				url: '/gitee/lyt-top/vue-next-admin-images/raw/master/menu/adminMenu.json',
-				method: 'get',
-				params,
-			});
+		getAdminMenu: (params?: object) => {
+			return request.get('/gitee/lyt-top/vue-next-admin-images/raw/master/menu/adminMenu.json', params);
 		},
-		getMenuTest: (params?: object) => {
-			return request({
-				url: '/gitee/lyt-top/vue-next-admin-images/raw/master/menu/testMenu.json',
-				method: 'get',
-				params,
-			});
+		getTestMenu: (params?: object) => {
+			return request.get('/gitee/lyt-top/vue-next-admin-images/raw/master/menu/testMenu.json', params);
 		},
 	};
 }

+ 16 - 20
Web/src/components/auth/auth.vue

@@ -2,29 +2,25 @@
 	<slot v-if="getUserAuthBtnList" />
 </template>
 
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
+<script setup lang="ts" name="auth">
+import { computed } from 'vue';
 import { storeToRefs } from 'pinia';
 import { useUserInfo } from '/@/stores/userInfo';
 
-export default defineComponent({
-	name: 'auth',
-	props: {
-		value: {
-			type: String,
-			default: () => '',
-		},
-	},
-	setup(props) {
-		const stores = useUserInfo();
-		const { userInfos } = storeToRefs(stores);
-		// 获取 vuex 中的用户权限
-		const getUserAuthBtnList = computed(() => {
-			return userInfos.value.authBtnList.some((v: string) => v === props.value);
-		});
-		return {
-			getUserAuthBtnList,
-		};
+// 定义父组件传过来的值
+const props = defineProps({
+	value: {
+		type: String,
+		default: () => '',
 	},
 });
+
+// 定义变量内容
+const stores = useUserInfo();
+const { userInfos } = storeToRefs(stores);
+
+// 获取 pinia 中的用户权限
+const getUserAuthBtnList = computed(() => {
+	return userInfos.value.authBtnList.some((v: string) => v === props.value);
+});
 </script>

+ 16 - 20
Web/src/components/auth/authAll.vue

@@ -2,30 +2,26 @@
 	<slot v-if="getUserAuthBtnList" />
 </template>
 
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
+<script setup lang="ts" name="authAll">
+import { computed } from 'vue';
 import { storeToRefs } from 'pinia';
 import { useUserInfo } from '/@/stores/userInfo';
 import { judementSameArr } from '/@/utils/arrayOperation';
 
-export default defineComponent({
-	name: 'authAll',
-	props: {
-		value: {
-			type: Array,
-			default: () => [],
-		},
-	},
-	setup(props) {
-		const stores = useUserInfo();
-		const { userInfos } = storeToRefs(stores);
-		// 获取 pinia 中的用户权限
-		const getUserAuthBtnList = computed(() => {
-			return judementSameArr(props.value, userInfos.value.authBtnList);
-		});
-		return {
-			getUserAuthBtnList,
-		};
+// 定义父组件传过来的值
+const props = defineProps({
+	value: {
+		type: Array,
+		default: () => [],
 	},
 });
+
+// 定义变量内容
+const stores = useUserInfo();
+const { userInfos } = storeToRefs(stores);
+
+// 获取 pinia 中的用户权限
+const getUserAuthBtnList = computed(() => {
+	return judementSameArr(props.value, userInfos.value.authBtnList);
+});
 </script>

+ 21 - 25
Web/src/components/auth/auths.vue

@@ -2,35 +2,31 @@
 	<slot v-if="getUserAuthBtnList" />
 </template>
 
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
+<script setup lang="ts" name="auths">
+import { computed } from 'vue';
 import { storeToRefs } from 'pinia';
 import { useUserInfo } from '/@/stores/userInfo';
 
-export default defineComponent({
-	name: 'auths',
-	props: {
-		value: {
-			type: Array,
-			default: () => [],
-		},
+// 定义父组件传过来的值
+const props = defineProps({
+	value: {
+		type: Array,
+		default: () => [],
 	},
-	setup(props) {
-		const stores = useUserInfo();
-		const { userInfos } = storeToRefs(stores);
-		// 获取 vuex 中的用户权限
-		const getUserAuthBtnList = computed(() => {
-			let flag = false;
-			userInfos.value.authBtnList.map((val: string) => {
-				props.value.map((v) => {
-					if (val === v) flag = true;
-				});
-			});
-			return flag;
+});
+
+// 定义变量内容
+const stores = useUserInfo();
+const { userInfos } = storeToRefs(stores);
+
+// 获取 pinia 中的用户权限
+const getUserAuthBtnList = computed(() => {
+	let flag = false;
+	userInfos.value.authBtnList.map((val: string) => {
+		props.value.map((v) => {
+			if (val === v) flag = true;
 		});
-		return {
-			getUserAuthBtnList,
-		};
-	},
+	});
+	return flag;
 });
 </script>

+ 62 - 62
Web/src/components/cropper/index.vue

@@ -1,21 +1,27 @@
 <template>
 	<div>
-		<el-dialog title="更换头像" v-model="isShowDialog" width="769px">
+		<el-dialog v-model="state.isShowDialog" draggable :close-on-click-modal="false" width="769px">
+			<template #header>
+				<div style="color: #fff">
+					<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-Edit /> </el-icon>
+					<span>更换头像</span>
+				</div>
+			</template>
 			<div class="cropper-warp">
 				<div class="cropper-warp-left">
-					<img :src="cropperImg" class="cropper-warp-left-img" />
+					<img :src="state.cropperImg" class="cropper-warp-left-img" />
 				</div>
 				<div class="cropper-warp-right">
 					<div class="cropper-warp-right-title">预览</div>
 					<div class="cropper-warp-right-item">
 						<div class="cropper-warp-right-value">
-							<img :src="cropperImgBase64" class="cropper-warp-right-value-img" />
+							<img :src="state.cropperImgBase64" class="cropper-warp-right-value-img" />
 						</div>
 						<div class="cropper-warp-right-label">100 x 100</div>
 					</div>
 					<div class="cropper-warp-right-item">
 						<div class="cropper-warp-right-value">
-							<img :src="cropperImgBase64" class="cropper-warp-right-value-img cropper-size" />
+							<img :src="state.cropperImgBase64" class="cropper-warp-right-value-img cropper-size" />
 						</div>
 						<div class="cropper-warp-right-label">50 x 50</div>
 					</div>
@@ -24,73 +30,67 @@
 			<template #footer>
 				<span class="dialog-footer">
 					<el-button @click="onCancel" size="default">取 消</el-button>
-					<el-button type="primary" @click="onSubmit" size="default">更 换</el-button>
+					<el-button type="primary" @click="onSubmit" size="default">确 定</el-button>
 				</span>
 			</template>
 		</el-dialog>
 	</div>
 </template>
 
-<script lang="ts">
-import { reactive, toRefs, nextTick, defineComponent } from 'vue';
+<script setup lang="ts" name="cropper">
+import { reactive, nextTick } from 'vue';
 import Cropper from 'cropperjs';
 import 'cropperjs/dist/cropper.css';
 
-export default defineComponent({
-	name: 'cropperIndex',
-	setup() {
-		const state = reactive({
-			isShowDialog: false,
-			cropperImg: '',
-			cropperImgBase64: '',
-			cropper: null,
-		});
-		// 打开弹窗
-		const openDialog = (imgs: any) => {
-			state.cropperImg = imgs;
-			state.isShowDialog = true;
-			nextTick(() => {
-				initCropper();
-			});
-		};
-		// 关闭弹窗
-		const closeDialog = () => {
-			state.isShowDialog = false;
-		};
-		// 取消
-		const onCancel = () => {
-			closeDialog();
-		};
-		// 更换
-		const onSubmit = () => {
-			// state.cropperImgBase64 = state.cropper.getCroppedCanvas().toDataURL('image/jpeg');
-		};
-		// 初始化cropperjs图片裁剪
-		const initCropper = () => {
-			const letImg: any = document.querySelector('.cropper-warp-left-img');
-			(<any>state.cropper) = new Cropper(letImg, {
-				viewMode: 1,
-				dragMode: 'none',
-				initialAspectRatio: 1,
-				aspectRatio: 1,
-				preview: '.before',
-				background: false,
-				autoCropArea: 0.6,
-				zoomOnWheel: false,
-				crop: () => {
-					state.cropperImgBase64 = (<any>state.cropper).getCroppedCanvas().toDataURL('image/jpeg');
-				},
-			});
-		};
-		return {
-			openDialog,
-			closeDialog,
-			onCancel,
-			onSubmit,
-			initCropper,
-			...toRefs(state),
-		};
-	},
+// 定义变量内容
+const state = reactive({
+	isShowDialog: false,
+	cropperImg: '',
+	cropperImgBase64: '',
+	cropper: '' as RefType,
+});
+
+// 打开弹窗
+const openDialog = (imgs: string) => {
+	state.cropperImg = imgs;
+	state.isShowDialog = true;
+	nextTick(() => {
+		initCropper();
+	});
+};
+// 关闭弹窗
+const closeDialog = () => {
+	state.isShowDialog = false;
+};
+// 取消
+const onCancel = () => {
+	closeDialog();
+};
+// 更换
+const onSubmit = () => {
+	// state.cropperImgBase64 = state.cropper.getCroppedCanvas().toDataURL('image/jpeg');
+};
+// 初始化cropperjs图片裁剪
+const initCropper = () => {
+	const letImg = <HTMLImageElement>document.querySelector('.cropper-warp-left-img');
+	state.cropper = new Cropper(letImg, {
+		viewMode: 1,
+		dragMode: 'none',
+		initialAspectRatio: 1,
+		aspectRatio: 1,
+		preview: '.before',
+		background: true,
+		autoCropArea: 0.6,
+		zoomOnWheel: false,
+		crop: () => {
+			state.cropperImgBase64 = state.cropper.getCroppedCanvas().toDataURL('image/jpeg');
+		},
+	});
+};
+
+// 导出变量
+defineExpose({
+	openDialog,
 });
 </script>
 

+ 77 - 74
Web/src/components/editor/index.vue

@@ -1,88 +1,91 @@
 <template>
 	<div class="editor-container">
 		<Toolbar :editor="editorRef" :mode="mode" />
-		<Editor :mode="mode" :defaultConfig="editorConfig" :style="{ height }" v-model="editorVal" @onCreated="handleCreated" @onChange="handleChange" />
+		<Editor
+			:mode="mode"
+			:defaultConfig="state.editorConfig"
+			:style="{ height }"
+			v-model="state.editorVal"
+			@onCreated="handleCreated"
+			@onChange="handleChange"
+		/>
 	</div>
 </template>
 
-<script lang="ts">
+<script setup lang="ts" name="wngEditor">
 // https://www.wangeditor.com/v5/for-frame.html#vue3
 import '@wangeditor/editor/dist/css/style.css';
-import { defineComponent, reactive, toRefs, shallowRef, watch, onBeforeUnmount } from 'vue';
+import { reactive, shallowRef, watch, onBeforeUnmount } from 'vue';
+import { IDomEditor } from '@wangeditor/editor';
 import { Toolbar, Editor } from '@wangeditor/editor-for-vue';
 
-export default defineComponent({
-	name: 'wngEditor',
-	components: { Toolbar, Editor },
-	props: {
-		// 是否禁用
-		disable: {
-			type: Boolean,
-			default: () => true,
-		},
-		// 内容框默认 placeholder
-		placeholder: {
-			type: String,
-			default: () => '请输入内容...',
-		},
-		// 双向绑定:双向绑定值,字段名为固定,改了之后将不生效
-		// 参考:https://v3.cn.vuejs.org/guide/migration/v-model.html#%E8%BF%81%E7%A7%BB%E7%AD%96%E7%95%A5
-		modelValue: String,
-		// https://www.wangeditor.com/v5/getting-started.html#mode-%E6%A8%A1%E5%BC%8F
-		// 模式,可选 <default|simple>,默认 default
-		mode: {
-			type: String,
-			default: () => 'default',
-		},
-		// 高度
-		height: {
-			type: String,
-			default: () => '310px',
-		},
+// 定义父组件传过来的值
+const props = defineProps({
+	// 是否禁用
+	disable: {
+		type: Boolean,
+		default: () => false,
 	},
-	setup(props, { emit }) {
-		const editorRef = shallowRef();
-		const state = reactive({
-			editorConfig: {
-				placeholder: props.placeholder,
-			},
-			editorVal: props.modelValue,
-		});
-		// 编辑器回调函数
-		const handleCreated = (editor: any) => {
-			editorRef.value = editor;
-		};
-		// 编辑器内容改变时
-		const handleChange = (editor: any) => {
-			// console.log(editor.getText());
-			// console.log(editor.getHtml());
-			emit('update:modelValue', editor.getHtml());
-		};
-		// 监听是否禁用改变
-		// https://gitee.com/lyt-top/vue-next-admin/issues/I4LM7I
-		watch(
-			() => props.disable,
-			(bool) => {
-				const editor = editorRef.value;
-				if (editor == null) return;
-				bool ? editor.disable() : editor.enable();
-			},
-			{
-				deep: true,
-			}
-		);
-		// 页面销毁时
-		onBeforeUnmount(() => {
-			const editor = editorRef.value;
-			if (editor == null) return;
-			editor.destroy();
-		});
-		return {
-			editorRef,
-			handleCreated,
-			handleChange,
-			...toRefs(state),
-		};
+	// 内容框默认 placeholder
+	placeholder: {
+		type: String,
+		default: () => '请输入内容...',
 	},
+	// https://www.wangeditor.com/v5/getting-started.html#mode-%E6%A8%A1%E5%BC%8F
+	// 模式,可选 <default|simple>,默认 default
+	mode: {
+		type: String,
+		default: () => 'default',
+	},
+	// 高度
+	height: {
+		type: String,
+		default: () => '310px',
+	},
+	// 双向绑定,用于获取 editor.getHtml()
+	getHtml: String,
+	// 双向绑定,用于获取 editor.getText()
+	getText: String,
 });
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['update:getHtml', 'update:getText']);
+
+// 定义变量内容
+const editorRef = shallowRef();
+const state = reactive({
+	editorConfig: {
+		placeholder: props.placeholder,
+	},
+	editorVal: props.getHtml,
+});
+
+// 编辑器回调函数
+const handleCreated = (editor: IDomEditor) => {
+	editorRef.value = editor;
+};
+// 编辑器内容改变时
+const handleChange = (editor: IDomEditor) => {
+	emit('update:getHtml', editor.getHtml());
+	emit('update:getText', editor.getText());
+};
+// 页面销毁时
+onBeforeUnmount(() => {
+	const editor = editorRef.value;
+	if (editor == null) return;
+	editor.destroy();
+});
+// 监听是否禁用改变
+// https://gitee.com/lyt-top/vue-next-admin/issues/I4LM7I
+watch(
+	() => props.disable,
+	(bool) => {
+		const editor = editorRef.value;
+		if (editor == null) return;
+		bool ? editor.disable() : editor.enable();
+	},
+	{
+		deep: true,
+	}
+);
 </script>

+ 184 - 190
Web/src/components/iconSelector/index.vue

@@ -2,7 +2,7 @@
 	<div class="icon-selector w100 h100">
 		<el-popover
 			placement="bottom"
-			:width="fontIconWidth"
+			:width="state.fontIconWidth"
 			trigger="click"
 			transition="el-zoom-in-top"
 			popper-class="icon-selector-popper"
@@ -10,8 +10,8 @@
 		>
 			<template #reference>
 				<el-input
-					v-model="fontIconSearch"
-					:placeholder="fontIconPlaceholder"
+					v-model="state.fontIconSearch"
+					:placeholder="state.fontIconPlaceholder"
 					:clearable="clearable"
 					:disabled="disabled"
 					:size="size"
@@ -22,11 +22,11 @@
 				>
 					<template #prepend>
 						<SvgIcon
-							:name="fontIconPrefix === '' ? prepend : fontIconPrefix"
+							:name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix"
 							class="font14"
-							v-if="fontIconPrefix === '' ? prepend?.indexOf('ele-') > -1 : fontIconPrefix?.indexOf('ele-') > -1"
+							v-if="state.fontIconPrefix === '' ? prepend?.indexOf('ele-') > -1 : state.fontIconPrefix?.indexOf('ele-') > -1"
 						/>
-						<i v-else :class="fontIconPrefix === '' ? prepend : fontIconPrefix" class="font14"></i>
+						<i v-else :class="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix" class="font14"></i>
 					</template>
 				</el-input>
 			</template>
@@ -35,16 +35,22 @@
 					<div class="icon-selector-warp-title flex">
 						<div class="flex-auto">{{ title }}</div>
 						<div class="icon-selector-warp-title-tab" v-if="type === 'all'">
-							<span :class="{ 'span-active': fontIconType === 'ali' }" @click="onIconChange('ali')" class="ml10" title="iconfont 图标">ali</span>
-							<span :class="{ 'span-active': fontIconType === 'ele' }" @click="onIconChange('ele')" class="ml10" title="elementPlus 图标">ele</span>
-							<span :class="{ 'span-active': fontIconType === 'awe' }" @click="onIconChange('awe')" class="ml10" title="fontawesome 图标">awe</span>
+							<span :class="{ 'span-active': state.fontIconType === 'ali' }" @click="onIconChange('ali')" class="ml10" title="iconfont 图标">
+								ali
+							</span>
+							<span :class="{ 'span-active': state.fontIconType === 'ele' }" @click="onIconChange('ele')" class="ml10" title="elementPlus 图标">
+								ele
+							</span>
+							<span :class="{ 'span-active': state.fontIconType === 'awe' }" @click="onIconChange('awe')" class="ml10" title="fontawesome 图标">
+								awe
+							</span>
 						</div>
 					</div>
 					<div class="icon-selector-warp-row">
 						<el-scrollbar ref="selectorScrollbarRef">
 							<el-row :gutter="10" v-if="fontIconSheetsFilterList.length > 0">
 								<el-col :xs="6" :sm="4" :md="4" :lg="4" :xl="4" @click="onColClick(v)" v-for="(v, k) in fontIconSheetsFilterList" :key="k">
-									<div class="icon-selector-warp-item" :class="{ 'icon-selector-active': fontIconPrefix === v }">
+									<div class="icon-selector-warp-item" :class="{ 'icon-selector-active': state.fontIconPrefix === v }">
 										<div class="flex-margin">
 											<div class="icon-selector-warp-item-value">
 												<SvgIcon :name="v" />
@@ -62,191 +68,179 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { ref, toRefs, reactive, onMounted, nextTick, computed, watch, defineComponent } from 'vue';
+<script setup lang="ts" name="iconSelector">
+import { ref, reactive, onMounted, nextTick, computed, watch } from 'vue';
 import initIconfont from '/@/utils/getStyleSheets';
 
-export default defineComponent({
-	name: 'iconSelector',
-	emits: ['update:modelValue', 'get', 'clear'],
-	props: {
-		// 输入框前置内容
-		prepend: {
-			type: String,
-			default: () => 'ele-Pointer',
-		},
-		// 输入框占位文本
-		placeholder: {
-			type: String,
-			default: () => '请输入内容搜索图标或者选择图标',
-		},
-		// 输入框占位文本
-		size: {
-			type: String,
-			default: () => 'default',
-		},
-		// 弹窗标题
-		title: {
-			type: String,
-			default: () => '请选择图标',
-		},
-		// icon 图标类型
-		type: {
-			type: String,
-			default: () => 'ele',
-		},
-		// 禁用
-		disabled: {
-			type: Boolean,
-			default: () => false,
-		},
-		// 是否可清空
-		clearable: {
-			type: Boolean,
-			default: () => true,
-		},
-		// 自定义空状态描述文字
-		emptyDescription: {
-			type: String,
-			default: () => '无相关图标',
-		},
-		// 双向绑定值,默认为 modelValue,
-		// 参考:https://v3.cn.vuejs.org/guide/migration/v-model.html#%E8%BF%81%E7%A7%BB%E7%AD%96%E7%95%A5
-		// 参考:https://v3.cn.vuejs.org/guide/component-custom-events.html#%E5%A4%9A%E4%B8%AA-v-model-%E7%BB%91%E5%AE%9A
-		modelValue: String,
+// 定义父组件传过来的值
+const props = defineProps({
+	// 输入框前置内容
+	prepend: {
+		type: String,
+		default: () => 'ele-Pointer',
 	},
-	setup(props, { emit }) {
-		const inputWidthRef = ref();
-		const selectorScrollbarRef = ref();
-		const state = reactive({
-			fontIconPrefix: '',
-			fontIconWidth: 0,
-			fontIconSearch: '',
-			fontIconTabsIndex: 0,
-			fontIconSheetsList: [],
-			fontIconPlaceholder: '',
-			fontIconType: 'ali',
-			fontIconShow: true,
+	// 输入框占位文本
+	placeholder: {
+		type: String,
+		default: () => '请输入内容搜索图标或者选择图标',
+	},
+	// 输入框占位文本
+	size: {
+		type: String,
+		default: () => 'default',
+	},
+	// 弹窗标题
+	title: {
+		type: String,
+		default: () => '请选择图标',
+	},
+	// icon 图标类型
+	type: {
+		type: String,
+		default: () => 'ele',
+	},
+	// 禁用
+	disabled: {
+		type: Boolean,
+		default: () => false,
+	},
+	// 是否可清空
+	clearable: {
+		type: Boolean,
+		default: () => true,
+	},
+	// 自定义空状态描述文字
+	emptyDescription: {
+		type: String,
+		default: () => '无相关图标',
+	},
+	// 双向绑定值,默认为 modelValue,
+	// 参考:https://v3.cn.vuejs.org/guide/migration/v-model.html#%E8%BF%81%E7%A7%BB%E7%AD%96%E7%95%A5
+	// 参考:https://v3.cn.vuejs.org/guide/component-custom-events.html#%E5%A4%9A%E4%B8%AA-v-model-%E7%BB%91%E5%AE%9A
+	modelValue: String,
+});
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['update:modelValue', 'get', 'clear']);
+
+// 定义变量内容
+const inputWidthRef = ref();
+const selectorScrollbarRef = ref();
+const state = reactive({
+	fontIconPrefix: '',
+	fontIconWidth: 0,
+	fontIconSearch: '',
+	fontIconTabsIndex: 0,
+	fontIconSheetsList: [],
+	fontIconPlaceholder: '',
+	fontIconType: 'ali',
+	fontIconShow: true,
+});
+
+// 处理 input 获取焦点时,modelValue 有值时,改变 input 的 placeholder 值
+const onIconFocus = () => {
+	if (!props.modelValue) return false;
+	state.fontIconSearch = '';
+	state.fontIconPlaceholder = props.modelValue;
+};
+// 处理 input 失去焦点时,为空将清空 input 值,为点击选中图标时,将取原先值
+const onIconBlur = () => {
+	setTimeout(() => {
+		const icon = state.fontIconSheetsList.filter((icon: string) => icon === state.fontIconSearch);
+		if (icon.length <= 0) state.fontIconSearch = '';
+	}, 300);
+};
+// 处理 icon 双向绑定数值回显
+const initModeValueEcho = () => {
+	if (props.modelValue === '') return ((<string | undefined>state.fontIconPlaceholder) = props.placeholder);
+	(<string | undefined>state.fontIconPlaceholder) = props.modelValue;
+	(<string | undefined>state.fontIconPrefix) = props.modelValue;
+};
+// 处理 icon type 类型为 all 时,类型 ali、ele、awe 回显问题
+const initFontIconTypeEcho = () => {
+	if (props.modelValue!.indexOf('iconfont') > -1) onIconChange('ali');
+	else if (props.modelValue!.indexOf('ele-') > -1) onIconChange('ele');
+	else if (props.modelValue!.indexOf('fa') > -1) onIconChange('awe');
+	else onIconChange('ali');
+};
+// 图标搜索及图标数据显示
+const fontIconSheetsFilterList = computed(() => {
+	if (!state.fontIconSearch) return state.fontIconSheetsList;
+	let search = state.fontIconSearch.trim().toLowerCase();
+	return state.fontIconSheetsList.filter((item: string) => {
+		if (item.toLowerCase().indexOf(search) !== -1) return item;
+	});
+});
+// 获取 input 的宽度
+const getInputWidth = () => {
+	nextTick(() => {
+		state.fontIconWidth = inputWidthRef.value.$el.offsetWidth;
+	});
+};
+// 监听页面宽度改变
+const initResize = () => {
+	window.addEventListener('resize', () => {
+		getInputWidth();
+	});
+};
+// 初始化数据
+const initFontIconData = async (type: string) => {
+	state.fontIconSheetsList = [];
+	if (type === 'ali') {
+		await initIconfont.ali().then((res: any) => {
+			// 阿里字体图标使用 `iconfont xxx`
+			state.fontIconSheetsList = res.map((i: string) => `iconfont ${i}`);
 		});
-		// 处理 input 获取焦点时,modelValue 有值时,改变 input 的 placeholder 值
-		const onIconFocus = () => {
-			if (!props.modelValue) return false;
-			state.fontIconSearch = '';
-			state.fontIconPlaceholder = props.modelValue;
-		};
-		// 处理 input 失去焦点时,为空将清空 input 值,为点击选中图标时,将取原先值
-		const onIconBlur = () => {
-			setTimeout(() => {
-				const icon = state.fontIconSheetsList.filter((icon: string) => icon === state.fontIconSearch);
-				if (icon.length <= 0) state.fontIconSearch = '';
-			}, 300);
-		};
-		// 处理 icon 双向绑定数值回显
-		const initModeValueEcho = () => {
-			if (props.modelValue === '') return ((<string | undefined>state.fontIconPlaceholder) = props.placeholder);
-			(<string | undefined>state.fontIconPlaceholder) = props.modelValue;
-			(<string | undefined>state.fontIconPrefix) = props.modelValue;
-		};
-		// 处理 icon type 类型为 all 时,类型 ali、ele、awe 回显问题
-		const initFontIconTypeEcho = () => {
-			if ((<any>props.modelValue)?.indexOf('iconfont') > -1) onIconChange('ali');
-			else if ((<any>props.modelValue)?.indexOf('ele-') > -1) onIconChange('ele');
-			else if ((<any>props.modelValue)?.indexOf('fa') > -1) onIconChange('awe');
-			else onIconChange('ali');
-		};
-		// 图标搜索及图标数据显示
-		const fontIconSheetsFilterList = computed(() => {
-			if (!state.fontIconSearch) return state.fontIconSheetsList;
-			let search = state.fontIconSearch.trim().toLowerCase();
-			return state.fontIconSheetsList.filter((item: any) => {
-				if (item.toLowerCase().indexOf(search) !== -1) return item;
-			});
+	} else if (type === 'ele') {
+		await initIconfont.ele().then((res: any) => {
+			state.fontIconSheetsList = res;
 		});
-		// 获取 input 的宽度
-		const getInputWidth = () => {
-			nextTick(() => {
-				state.fontIconWidth = inputWidthRef.value?.$el.offsetWidth;
-			});
-		};
-		// 监听页面宽度改变
-		const initResize = () => {
-			window.addEventListener('resize', () => {
-				getInputWidth();
-			});
-		};
-		// 初始化数据
-		const initFontIconData = async (type: string) => {
-			state.fontIconSheetsList = [];
-			if (type === 'ali') {
-				await initIconfont.ali().then((res: any) => {
-					// 阿里字体图标使用 `iconfont xxx`
-					state.fontIconSheetsList = res.map((i: string) => `iconfont ${i}`);
-				});
-			} else if (type === 'ele') {
-				await initIconfont.ele().then((res: any) => {
-					state.fontIconSheetsList = res;
-				});
-			} else if (type === 'awe') {
-				await initIconfont.awe().then((res: any) => {
-					// fontawesome字体图标使用 `fa xxx`
-					state.fontIconSheetsList = res.map((i: string) => `fa ${i}`);
-				});
-			}
-			// 初始化 input 的 placeholder
-			// 参考(单项数据流):https://cn.vuejs.org/v2/guide/components-props.html?#%E5%8D%95%E5%90%91%E6%95%B0%E6%8D%AE%E6%B5%81
-			state.fontIconPlaceholder = props.placeholder;
-			// 初始化双向绑定回显
-			initModeValueEcho();
-		};
-		// 图标点击切换
-		const onIconChange = (type: string) => {
-			state.fontIconType = type;
-			initFontIconData(type);
-		};
-		// 获取当前点击的 icon 图标
-		const onColClick = (v: any) => {
-			state.fontIconPlaceholder = v;
-			state.fontIconPrefix = v;
-			emit('get', state.fontIconPrefix);
-			emit('update:modelValue', state.fontIconPrefix);
-		};
-		// 清空当前点击的 icon 图标
-		const onClearFontIcon = () => {
-			state.fontIconPrefix = '';
-			emit('clear', state.fontIconPrefix);
-			emit('update:modelValue', state.fontIconPrefix);
-		};
-		// 监听 Popover 打开,用于双向绑定值回显
-		const onPopoverShow = () => {
-			initModeValueEcho();
-			initFontIconTypeEcho();
-		};
-		// 页面加载时
-		onMounted(() => {
-			initModeValueEcho();
-			initResize();
-			getInputWidth();
+	} else if (type === 'awe') {
+		await initIconfont.awe().then((res: any) => {
+			// fontawesome字体图标使用 `fa xxx`
+			state.fontIconSheetsList = res.map((i: string) => `fa ${i}`);
 		});
-
-		// 监听双向绑定 modelValue 的变化
-		watch(
-			() => props.modelValue,
-			() => {
-				initModeValueEcho();
-			}
-		);
-		return {
-			inputWidthRef,
-			selectorScrollbarRef,
-			fontIconSheetsFilterList,
-			onColClick,
-			onIconChange,
-			onClearFontIcon,
-			onIconFocus,
-			onIconBlur,
-			onPopoverShow,
-			...toRefs(state),
-		};
-	},
+	}
+	// 初始化 input 的 placeholder
+	// 参考(单项数据流):https://cn.vuejs.org/v2/guide/components-props.html?#%E5%8D%95%E5%90%91%E6%95%B0%E6%8D%AE%E6%B5%81
+	state.fontIconPlaceholder = props.placeholder;
+	// 初始化双向绑定回显
+	initModeValueEcho();
+};
+// 图标点击切换
+const onIconChange = (type: string) => {
+	state.fontIconType = type;
+	initFontIconData(type);
+};
+// 获取当前点击的 icon 图标
+const onColClick = (v: string) => {
+	state.fontIconPlaceholder = v;
+	state.fontIconPrefix = v;
+	emit('get', state.fontIconPrefix);
+	emit('update:modelValue', state.fontIconPrefix);
+};
+// 清空当前点击的 icon 图标
+const onClearFontIcon = () => {
+	state.fontIconPrefix = '';
+	emit('clear', state.fontIconPrefix);
+	emit('update:modelValue', state.fontIconPrefix);
+};
+// 监听 Popover 打开,用于双向绑定值回显
+const onPopoverShow = () => {
+	initModeValueEcho();
+	initFontIconTypeEcho();
+};
+// 页面加载时
+onMounted(() => {
+	initModeValueEcho();
+	initResize();
+	getInputWidth();
 });
+// 监听双向绑定 modelValue 的变化
+watch(
+	() => props.modelValue,
+	() => {
+		initModeValueEcho();
+	}
+);
 </script>

+ 127 - 131
Web/src/components/noticeBar/index.vue

@@ -1,5 +1,5 @@
 <template>
-	<div class="notice-bar" :style="{ background, height: `${height}px` }" v-show="!isMode">
+	<div class="notice-bar" :style="{ background, height: `${height}px` }" v-show="!state.isMode">
 		<div class="notice-bar-warp" :style="{ color, fontSize: `${size}px` }">
 			<i v-if="leftIcon" class="notice-bar-warp-left-icon" :class="leftIcon"></i>
 			<div class="notice-bar-warp-text-box" ref="noticeBarWarpRef">
@@ -11,139 +11,135 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { toRefs, reactive, defineComponent, ref, onMounted, nextTick } from 'vue';
+<script setup lang="ts" name="noticeBar">
+import { reactive, ref, onMounted, nextTick } from 'vue';
 
-export default defineComponent({
-	name: 'noticeBar',
-	props: {
-		// 通知栏模式,可选值为 closeable link
-		mode: {
-			type: String,
-			default: () => '',
-		},
-		// 通知文本内容
-		text: {
-			type: String,
-			default: () => '',
-		},
-		// 通知文本颜色
-		color: {
-			type: String,
-			default: () => 'var(--el-color-warning)',
-		},
-		// 通知背景色
-		background: {
-			type: String,
-			default: () => 'var(--el-color-warning-light-9)',
-		},
-		// 字体大小,单位px
-		size: {
-			type: [Number, String],
-			default: () => 14,
-		},
-		// 通知栏高度,单位px
-		height: {
-			type: Number,
-			default: () => 40,
-		},
-		// 动画延迟时间 (s)
-		delay: {
-			type: Number,
-			default: () => 1,
-		},
-		// 滚动速率 (px/s)
-		speed: {
-			type: Number,
-			default: () => 100,
-		},
-		// 是否开启垂直滚动
-		scrollable: {
-			type: Boolean,
-			default: () => false,
-		},
-		// 自定义左侧图标
-		leftIcon: {
-			type: String,
-			default: () => '',
-		},
-		// 自定义右侧图标
-		rightIcon: {
-			type: String,
-			default: () => '',
-		},
+// 定义父组件传过来的值
+const props = defineProps({
+	// 通知栏模式,可选值为 closeable link
+	mode: {
+		type: String,
+		default: () => '',
 	},
-	setup(props, { emit }) {
-		const noticeBarWarpRef = ref();
-		const noticeBarTextRef = ref();
-		const state = reactive({
-			order: 1,
-			oneTime: 0,
-			twoTime: 0,
-			warpOWidth: 0,
-			textOWidth: 0,
-			isMode: false,
-		});
-		// 初始化 animation 各项参数
-		const initAnimation = () => {
-			nextTick(() => {
-				state.warpOWidth = noticeBarWarpRef.value.offsetWidth;
-				state.textOWidth = noticeBarTextRef.value.offsetWidth;
-				document.styleSheets[0].insertRule(`@keyframes oneAnimation {0% {left: 0px;} 100% {left: -${state.textOWidth}px;}}`);
-				document.styleSheets[0].insertRule(`@keyframes twoAnimation {0% {left: ${state.warpOWidth}px;} 100% {left: -${state.textOWidth}px;}}`);
-				computeAnimationTime();
-				setTimeout(() => {
-					changeAnimation();
-				}, props.delay * 1000);
-			});
-		};
-		// 计算 animation 滚动时长
-		const computeAnimationTime = () => {
-			state.oneTime = state.textOWidth / props.speed;
-			state.twoTime = (state.textOWidth + state.warpOWidth) / props.speed;
-		};
-		// 改变 animation 动画调用
-		const changeAnimation = () => {
-			if (state.order === 1) {
-				noticeBarTextRef.value.style.cssText = `animation: oneAnimation ${state.oneTime}s linear; opactity: 1;}`;
-				state.order = 2;
-			} else {
-				noticeBarTextRef.value.style.cssText = `animation: twoAnimation ${state.twoTime}s linear infinite; opacity: 1;`;
-			}
-		};
-		// 监听 animation 动画的结束
-		const listenerAnimationend = () => {
-			noticeBarTextRef.value.addEventListener(
-				'animationend',
-				() => {
-					changeAnimation();
-				},
-				false
-			);
-		};
-		// 右侧 icon 图标点击
-		const onRightIconClick = () => {
-			if (!props.mode) return false;
-			if (props.mode === 'closeable') {
-				state.isMode = true;
-				emit('close');
-			} else if (props.mode === 'link') {
-				emit('link');
-			}
-		};
-		// 页面加载时
-		onMounted(() => {
-			if (props.scrollable) return false;
-			initAnimation();
-			listenerAnimationend();
-		});
-		return {
-			noticeBarWarpRef,
-			noticeBarTextRef,
-			onRightIconClick,
-			...toRefs(state),
-		};
+	// 通知文本内容
+	text: {
+		type: String,
+		default: () => '',
+	},
+	// 通知文本颜色
+	color: {
+		type: String,
+		default: () => 'var(--el-color-warning)',
+	},
+	// 通知背景色
+	background: {
+		type: String,
+		default: () => 'var(--el-color-warning-light-9)',
 	},
+	// 字体大小,单位px
+	size: {
+		type: [Number, String],
+		default: () => 14,
+	},
+	// 通知栏高度,单位px
+	height: {
+		type: Number,
+		default: () => 40,
+	},
+	// 动画延迟时间 (s)
+	delay: {
+		type: Number,
+		default: () => 1,
+	},
+	// 滚动速率 (px/s)
+	speed: {
+		type: Number,
+		default: () => 100,
+	},
+	// 是否开启垂直滚动
+	scrollable: {
+		type: Boolean,
+		default: () => false,
+	},
+	// 自定义左侧图标
+	leftIcon: {
+		type: String,
+		default: () => '',
+	},
+	// 自定义右侧图标
+	rightIcon: {
+		type: String,
+		default: () => '',
+	},
+});
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['close', 'link']);
+
+// 定义变量内容
+const noticeBarWarpRef = ref();
+const noticeBarTextRef = ref();
+const state = reactive({
+	order: 1,
+	oneTime: 0,
+	twoTime: 0,
+	warpOWidth: 0,
+	textOWidth: 0,
+	isMode: false,
+});
+
+// 初始化 animation 各项参数
+const initAnimation = () => {
+	nextTick(() => {
+		state.warpOWidth = noticeBarWarpRef.value.offsetWidth;
+		state.textOWidth = noticeBarTextRef.value.offsetWidth;
+		document.styleSheets[0].insertRule(`@keyframes oneAnimation {0% {left: 0px;} 100% {left: -${state.textOWidth}px;}}`);
+		document.styleSheets[0].insertRule(`@keyframes twoAnimation {0% {left: ${state.warpOWidth}px;} 100% {left: -${state.textOWidth}px;}}`);
+		computeAnimationTime();
+		setTimeout(() => {
+			changeAnimation();
+		}, props.delay * 1000);
+	});
+};
+// 计算 animation 滚动时长
+const computeAnimationTime = () => {
+	state.oneTime = state.textOWidth / props.speed;
+	state.twoTime = (state.textOWidth + state.warpOWidth) / props.speed;
+};
+// 改变 animation 动画调用
+const changeAnimation = () => {
+	if (state.order === 1) {
+		noticeBarTextRef.value.style.cssText = `animation: oneAnimation ${state.oneTime}s linear; opactity: 1;}`;
+		state.order = 2;
+	} else {
+		noticeBarTextRef.value.style.cssText = `animation: twoAnimation ${state.twoTime}s linear infinite; opacity: 1;`;
+	}
+};
+// 监听 animation 动画的结束
+const listenerAnimationend = () => {
+	noticeBarTextRef.value.addEventListener(
+		'animationend',
+		() => {
+			changeAnimation();
+		},
+		false
+	);
+};
+// 右侧 icon 图标点击
+const onRightIconClick = () => {
+	if (!props.mode) return false;
+	if (props.mode === 'closeable') {
+		state.isMode = true;
+		emit('close');
+	} else if (props.mode === 'link') {
+		emit('link');
+	}
+};
+// 页面加载时
+onMounted(() => {
+	if (props.scrollable) return false;
+	initAnimation();
+	listenerAnimationend();
 });
 </script>
 

+ 48 - 58
Web/src/components/svgIcon/index.vue

@@ -8,66 +8,56 @@
 	<i v-else :class="getIconName" :style="setIconSvgStyle" />
 </template>
 
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
+<script setup lang="ts" name="svgIcon">
+import { computed } from 'vue';
 
-export default defineComponent({
-	name: 'svgIcon',
-	props: {
-		// svg 图标组件名字
-		name: {
-			type: String,
-		},
-		// svg 大小
-		size: {
-			type: Number,
-			default: () => 14,
-		},
-		// svg 颜色
-		color: {
-			type: String,
-		},
+// 定义父组件传过来的值
+const props = defineProps({
+	// svg 图标组件名字
+	name: {
+		type: String,
 	},
-	setup(props) {
-		// 在线链接、本地引入地址前缀
-		const linesString = ['https', 'http', '/src', '/assets', import.meta.env.VITE_PUBLIC_PATH];
-
-		// 获取 icon 图标名称
-		const getIconName = computed(() => {
-			return props?.name;
-		});
-		// 用于判断 element plus 自带 svg 图标的显示、隐藏
-		const isShowIconSvg = computed(() => {
-			return props?.name?.startsWith('ele-');
-		});
-		// 用于判断在线链接、本地引入等图标显示、隐藏
-		const isShowIconImg = computed(() => {
-			return linesString.find((str) => props.name?.startsWith(str));
-		});
-		// 设置图标样式
-		const setIconSvgStyle = computed(() => {
-			return `font-size: ${props.size}px;color: ${props.color};`;
-		});
-		// 设置图片样式
-		const setIconImgOutStyle = computed(() => {
-			return `width: ${props.size}px;height: ${props.size}px;display: inline-block;overflow: hidden;`;
-		});
-		// 设置图片样式
-		// https://gitee.com/lyt-top/vue-next-admin/issues/I59ND0
-		const setIconSvgInsStyle = computed(() => {
-			const filterStyle: string[] = [];
-			const compatibles: string[] = ['-webkit', '-ms', '-o', '-moz'];
-			compatibles.forEach((j) => filterStyle.push(`${j}-filter: drop-shadow(${props.color} 30px 0);`));
-			return `width: ${props.size}px;height: ${props.size}px;position: relative;left: -${props.size}px;${filterStyle.join('')}`;
-		});
-		return {
-			getIconName,
-			isShowIconSvg,
-			isShowIconImg,
-			setIconSvgStyle,
-			setIconImgOutStyle,
-			setIconSvgInsStyle,
-		};
+	// svg 大小
+	size: {
+		type: Number,
+		default: () => 14,
+	},
+	// svg 颜色
+	color: {
+		type: String,
 	},
 });
+
+// 在线链接、本地引入地址前缀
+// https://gitee.com/lyt-top/vue-next-admin/issues/I62OVL
+const linesString = ['https', 'http', '/src', '/assets', 'data:image', import.meta.env.VITE_PUBLIC_PATH];
+
+// 获取 icon 图标名称
+const getIconName = computed(() => {
+	return props?.name;
+});
+// 用于判断 element plus 自带 svg 图标的显示、隐藏
+const isShowIconSvg = computed(() => {
+	return props?.name?.startsWith('ele-');
+});
+// 用于判断在线链接、本地引入等图标显示、隐藏
+const isShowIconImg = computed(() => {
+	return linesString.find((str) => props.name?.startsWith(str));
+});
+// 设置图标样式
+const setIconSvgStyle = computed(() => {
+	return `font-size: ${props.size}px;color: ${props.color};`;
+});
+// 设置图片样式
+const setIconImgOutStyle = computed(() => {
+	return `width: ${props.size}px;height: ${props.size}px;display: inline-block;overflow: hidden;`;
+});
+// 设置图片样式
+// https://gitee.com/lyt-top/vue-next-admin/issues/I59ND0
+const setIconSvgInsStyle = computed(() => {
+	const filterStyle: string[] = [];
+	const compatibles: string[] = ['-webkit', '-ms', '-o', '-moz'];
+	compatibles.forEach((j) => filterStyle.push(`${j}-filter: drop-shadow(${props.color} 30px 0);`));
+	return `width: ${props.size}px;height: ${props.size}px;position: relative;left: -${props.size}px;${filterStyle.join('')}`;
+});
 </script>

+ 259 - 0
Web/src/components/table/index.vue

@@ -0,0 +1,259 @@
+<template>
+	<div class="table-container">
+		<el-table
+			:data="data"
+			:border="setBorder"
+			v-bind="$attrs"
+			row-key="id"
+			stripe
+			style="width: 100%"
+			v-loading="config.loading"
+			@selection-change="onSelectionChange"
+		>
+			<el-table-column type="selection" :reserve-selection="true" width="30" v-if="config.isSelection" />
+			<el-table-column type="index" label="序号" width="60" v-if="config.isSerialNo" />
+			<el-table-column
+				v-for="(item, index) in setHeader"
+				:key="index"
+				show-overflow-tooltip
+				:prop="item.key"
+				:width="item.colWidth"
+				:label="item.title"
+			>
+				<template v-slot="scope">
+					<template v-if="item.key === 'image'">
+						<img :src="scope.row[item.key]" :width="item.width" :height="item.height" />
+					</template>
+					<template v-else>
+						{{ scope.row[item.key] }}
+					</template>
+				</template>
+			</el-table-column>
+			<el-table-column label="操作" width="100" v-if="config.isOperate">
+				<template v-slot="scope">
+					<el-popconfirm title="确定删除吗?" @confirm="onDelRow(scope.row)">
+						<template #reference>
+							<el-button text type="primary">删除</el-button>
+						</template>
+					</el-popconfirm>
+				</template>
+			</el-table-column>
+			<template #empty>
+				<el-empty description="暂无数据" />
+			</template>
+		</el-table>
+		<div class="table-footer mt15">
+			<el-pagination
+				v-model:current-page="state.page.pageNum"
+				v-model:page-size="state.page.pageSize"
+				:pager-count="5"
+				:page-sizes="[10, 20, 30]"
+				:total="config.total"
+				layout="total, sizes, prev, pager, next, jumper"
+				background
+				@size-change="onHandleSizeChange"
+				@current-change="onHandleCurrentChange"
+			>
+			</el-pagination>
+			<div class="table-footer-tool">
+				<SvgIcon name="iconfont icon-yunxiazai_o" :size="22" title="导出" @click="onImportTable" />
+				<SvgIcon name="iconfont icon-shuaxin" :size="22" title="刷新" @click="onRefreshTable" />
+				<el-popover
+					placement="top-end"
+					trigger="click"
+					transition="el-zoom-in-top"
+					popper-class="table-tool-popper"
+					:width="300"
+					:persistent="false"
+					@show="onSetTable"
+				>
+					<template #reference>
+						<SvgIcon name="iconfont icon-quanjushezhi_o" :size="22" title="设置" />
+					</template>
+					<template #default>
+						<div ref="toolSetRef">
+							<div class="tool-box">
+								<el-tooltip content="拖动进行排序" placement="top-start">
+									<SvgIcon name="fa fa-question-circle-o" :size="17" class="ml11" color="#909399" />
+								</el-tooltip>
+								<el-checkbox
+									v-model="state.checkListAll"
+									:indeterminate="state.checkListIndeterminate"
+									class="ml10 mr1"
+									label="列显示"
+									@change="onCheckAllChange"
+								/>
+								<el-checkbox v-model="getConfig.isSerialNo" class="ml12 mr1" label="序号" />
+								<el-checkbox v-model="getConfig.isSelection" class="ml12 mr1" label="多选" />
+							</div>
+							<div class="tool-item" v-for="v in header" :key="v.key" :data-key="v.key">
+								<i class="fa fa-arrows-alt handle cursor-pointer"></i>
+								<el-checkbox v-model="v.isCheck" class="ml12 mr8" :label="v.title" @change="onCheckChange" />
+							</div>
+						</div>
+					</template>
+				</el-popover>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts" name="netxTable">
+import { reactive, computed, nextTick, ref } from 'vue';
+import { ElMessage } from 'element-plus';
+import table2excel from 'js-table2excel';
+import Sortable from 'sortablejs';
+import { storeToRefs } from 'pinia';
+import { useThemeConfig } from '/@/stores/themeConfig';
+import '/@/theme/tableTool.scss';
+
+// 定义父组件传过来的值
+const props = defineProps({
+	// 列表内容
+	data: {
+		type: Array<EmptyObjectType>,
+		default: () => [],
+	},
+	// 表头内容
+	header: {
+		type: Array<EmptyObjectType>,
+		default: () => [],
+	},
+	// 搜索参数
+	param: {
+		type: Object,
+		default: () => {},
+	},
+	// 配置项
+	config: {
+		type: Object,
+		default: () => {},
+	},
+});
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['delRow', 'pageChange', 'sortHeader']);
+
+// 定义变量内容
+const toolSetRef = ref();
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const state = reactive({
+	page: {
+		pageNum: 1,
+		pageSize: 10,
+	},
+	selectlist: [] as EmptyObjectType[],
+	checkListAll: true,
+	checkListIndeterminate: false,
+});
+
+// 设置边框显示/隐藏
+const setBorder = computed(() => {
+	return props.config.isBorder ? true : false;
+});
+// 获取父组件 配置项(必传)
+const getConfig = computed(() => {
+	return props.config;
+});
+// 设置 tool header 数据
+const setHeader = computed(() => {
+	return props.header.filter((v) => v.isCheck);
+});
+// tool 列显示全选改变时
+const onCheckAllChange = (val: boolean) => {
+	if (val) props.header.forEach((v) => (v.isCheck = true));
+	else props.header.forEach((v) => (v.isCheck = false));
+	state.checkListIndeterminate = false;
+};
+// tool 列显示当前项改变时
+const onCheckChange = () => {
+	const headers = props.header.filter((v) => v.isCheck).length;
+	state.checkListAll = headers === props.header.length;
+	state.checkListIndeterminate = headers > 0 && headers < props.header.length;
+};
+// 表格多选改变时,用于导出
+const onSelectionChange = (val: EmptyObjectType[]) => {
+	state.selectlist = val;
+};
+// 删除当前项
+const onDelRow = (row: EmptyObjectType) => {
+	emit('delRow', row);
+};
+// 分页改变
+const onHandleSizeChange = (val: number) => {
+	state.page.pageSize = val;
+	emit('pageChange', state.page);
+};
+// 分页改变
+const onHandleCurrentChange = (val: number) => {
+	state.page.pageNum = val;
+	emit('pageChange', state.page);
+};
+// 搜索时,分页还原成默认
+const pageReset = () => {
+	state.page.pageNum = 1;
+	state.page.pageSize = 10;
+	emit('pageChange', state.page);
+};
+// 导出
+const onImportTable = () => {
+	if (state.selectlist.length <= 0) return ElMessage.warning('请先选择要导出的数据');
+	table2excel(props.header, state.selectlist, `${themeConfig.value.globalTitle} ${new Date().toLocaleString()}`);
+};
+// 刷新
+const onRefreshTable = () => {
+	emit('pageChange', state.page);
+};
+// 设置
+const onSetTable = () => {
+	nextTick(() => {
+		const sortable = Sortable.create(toolSetRef.value, {
+			handle: '.handle',
+			dataIdAttr: 'data-key',
+			animation: 150,
+			onEnd: () => {
+				const headerList: EmptyObjectType[] = [];
+				sortable.toArray().forEach((val) => {
+					props.header.forEach((v) => {
+						if (v.key === val) headerList.push({ ...v });
+					});
+				});
+				emit('sortHeader', headerList);
+			},
+		});
+	});
+};
+
+// 暴露变量
+defineExpose({
+	pageReset,
+});
+</script>
+
+<style scoped lang="scss">
+.table-container {
+	display: flex;
+	flex-direction: column;
+	.el-table {
+		flex: 1;
+	}
+	.table-footer {
+		display: flex;
+		.table-footer-tool {
+			flex: 1;
+			display: flex;
+			align-items: center;
+			justify-content: flex-end;
+			i {
+				margin-right: 10px;
+				cursor: pointer;
+				color: var(--el-text-color-regular);
+				&:last-of-type {
+					margin-right: 0;
+				}
+			}
+		}
+	}
+}
+</style>

+ 40 - 0
Web/src/directive/authDirective.ts

@@ -0,0 +1,40 @@
+import type { App } from 'vue';
+import { useUserInfo } from '/@/stores/userInfo';
+import { judementSameArr } from '/@/utils/arrayOperation';
+
+/**
+ * 用户权限指令
+ * @directive 单个权限验证(v-auth="xxx")
+ * @directive 多个权限验证,满足一个则显示(v-auths="[xxx,xxx]")
+ * @directive 多个权限验证,全部满足则显示(v-auth-all="[xxx,xxx]")
+ */
+export function authDirective(app: App) {
+	// 单个权限验证(v-auth="xxx")
+	app.directive('auth', {
+		mounted(el, binding) {
+			const stores = useUserInfo();
+			if (!stores.userInfos.authBtnList.some((v: string) => v === binding.value)) el.parentNode.removeChild(el);
+		},
+	});
+	// 多个权限验证,满足一个则显示(v-auths="[xxx,xxx]")
+	app.directive('auths', {
+		mounted(el, binding) {
+			let flag = false;
+			const stores = useUserInfo();
+			stores.userInfos.authBtnList.map((val: string) => {
+				binding.value.map((v: string) => {
+					if (val === v) flag = true;
+				});
+			});
+			if (!flag) el.parentNode.removeChild(el);
+		},
+	});
+	// 多个权限验证,全部满足则显示(v-auth-all="[xxx,xxx]")
+	app.directive('auth-all', {
+		mounted(el, binding) {
+			const stores = useUserInfo();
+			const flag = judementSameArr(binding.value, stores.userInfos.authBtnList);
+			if (!flag) el.parentNode.removeChild(el);
+		},
+	});
+}

+ 178 - 0
Web/src/directive/customDirective.ts

@@ -0,0 +1,178 @@
+import type { App } from 'vue';
+
+/**
+ * 按钮波浪指令
+ * @directive 默认方式:v-waves,如 `<div v-waves></div>`
+ * @directive 参数方式:v-waves=" |light|red|orange|purple|green|teal",如 `<div v-waves="'light'"></div>`
+ */
+export function wavesDirective(app: App) {
+	app.directive('waves', {
+		mounted(el, binding) {
+			el.classList.add('waves-effect');
+			binding.value && el.classList.add(`waves-${binding.value}`);
+			function setConvertStyle(obj: { [key: string]: unknown }) {
+				let style: string = '';
+				for (let i in obj) {
+					if (obj.hasOwnProperty(i)) style += `${i}:${obj[i]};`;
+				}
+				return style;
+			}
+			function onCurrentClick(e: { [key: string]: unknown }) {
+				let elDiv = document.createElement('div');
+				elDiv.classList.add('waves-ripple');
+				el.appendChild(elDiv);
+				let styles = {
+					left: `${e.layerX}px`,
+					top: `${e.layerY}px`,
+					opacity: 1,
+					transform: `scale(${(el.clientWidth / 100) * 10})`,
+					'transition-duration': `750ms`,
+					'transition-timing-function': `cubic-bezier(0.250, 0.460, 0.450, 0.940)`,
+				};
+				elDiv.setAttribute('style', setConvertStyle(styles));
+				setTimeout(() => {
+					elDiv.setAttribute(
+						'style',
+						setConvertStyle({
+							opacity: 0,
+							transform: styles.transform,
+							left: styles.left,
+							top: styles.top,
+						})
+					);
+					setTimeout(() => {
+						elDiv && el.removeChild(elDiv);
+					}, 750);
+				}, 450);
+			}
+			el.addEventListener('mousedown', onCurrentClick, false);
+		},
+		unmounted(el) {
+			el.addEventListener('mousedown', () => {});
+		},
+	});
+}
+
+/**
+ * 自定义拖动指令
+ * @description  使用方式:v-drag="[dragDom,dragHeader]",如 `<div v-drag="['.drag-container .el-dialog', '.drag-container .el-dialog__header']"></div>`
+ * @description dragDom 要拖动的元素,dragHeader 要拖动的 Header 位置
+ * @link 注意:https://github.com/element-plus/element-plus/issues/522
+ * @lick 参考:https://blog.csdn.net/weixin_46391323/article/details/105228020?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-10&spm=1001.2101.3001.4242
+ */
+export function dragDirective(app: App) {
+	app.directive('drag', {
+		mounted(el, binding) {
+			if (!binding.value) return false;
+
+			const dragDom = document.querySelector(binding.value[0]) as HTMLElement;
+			const dragHeader = document.querySelector(binding.value[1]) as HTMLElement;
+
+			dragHeader.onmouseover = () => (dragHeader.style.cursor = `move`);
+
+			function down(e: any, type: string) {
+				// 鼠标按下,计算当前元素距离可视区的距离
+				const disX = type === 'pc' ? e.clientX - dragHeader.offsetLeft : e.touches[0].clientX - dragHeader.offsetLeft;
+				const disY = type === 'pc' ? e.clientY - dragHeader.offsetTop : e.touches[0].clientY - dragHeader.offsetTop;
+
+				// body当前宽度
+				const screenWidth = document.body.clientWidth;
+				// 可见区域高度(应为body高度,可某些环境下无法获取)
+				const screenHeight = document.documentElement.clientHeight;
+
+				// 对话框宽度
+				const dragDomWidth = dragDom.offsetWidth;
+				// 对话框高度
+				const dragDomheight = dragDom.offsetHeight;
+
+				const minDragDomLeft = dragDom.offsetLeft;
+				const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth;
+
+				const minDragDomTop = dragDom.offsetTop;
+				const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomheight;
+
+				// 获取到的值带px 正则匹配替换
+				let styL: any = getComputedStyle(dragDom).left;
+				let styT: any = getComputedStyle(dragDom).top;
+
+				// 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
+				if (styL.includes('%')) {
+					styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100);
+					styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100);
+				} else {
+					styL = +styL.replace(/\px/g, '');
+					styT = +styT.replace(/\px/g, '');
+				}
+
+				return {
+					disX,
+					disY,
+					minDragDomLeft,
+					maxDragDomLeft,
+					minDragDomTop,
+					maxDragDomTop,
+					styL,
+					styT,
+				};
+			}
+
+			function move(e: any, type: string, obj: any) {
+				let { disX, disY, minDragDomLeft, maxDragDomLeft, minDragDomTop, maxDragDomTop, styL, styT } = obj;
+
+				// 通过事件委托,计算移动的距离
+				let left = type === 'pc' ? e.clientX - disX : e.touches[0].clientX - disX;
+				let top = type === 'pc' ? e.clientY - disY : e.touches[0].clientY - disY;
+
+				// 边界处理
+				if (-left > minDragDomLeft) {
+					left = -minDragDomLeft;
+				} else if (left > maxDragDomLeft) {
+					left = maxDragDomLeft;
+				}
+
+				if (-top > minDragDomTop) {
+					top = -minDragDomTop;
+				} else if (top > maxDragDomTop) {
+					top = maxDragDomTop;
+				}
+
+				// 移动当前元素
+				dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`;
+			}
+
+			/**
+			 * pc端
+			 * onmousedown 鼠标按下触发事件
+			 * onmousemove 鼠标按下时持续触发事件
+			 * onmouseup 鼠标抬起触发事件
+			 */
+			dragHeader.onmousedown = (e) => {
+				const obj = down(e, 'pc');
+				document.onmousemove = (e) => {
+					move(e, 'pc', obj);
+				};
+				document.onmouseup = () => {
+					document.onmousemove = null;
+					document.onmouseup = null;
+				};
+			};
+
+			/**
+			 * 移动端
+			 * ontouchstart 当按下手指时,触发ontouchstart
+			 * ontouchmove 当移动手指时,触发ontouchmove
+			 * ontouchend 当移走手指时,触发ontouchend
+			 */
+			dragHeader.ontouchstart = (e) => {
+				const obj = down(e, 'app');
+				document.ontouchmove = (e) => {
+					move(e, 'app', obj);
+				};
+				document.ontouchend = () => {
+					document.ontouchmove = null;
+					document.ontouchend = null;
+				};
+			};
+		},
+	});
+}

+ 18 - 0
Web/src/directive/index.ts

@@ -0,0 +1,18 @@
+import type { App } from 'vue';
+import { authDirective } from '/@/directive/authDirective';
+import { wavesDirective, dragDirective } from '/@/directive/customDirective';
+
+/**
+ * 导出指令方法:v-xxx
+ * @methods authDirective 用户权限指令,用法:v-auth
+ * @methods wavesDirective 按钮波浪指令,用法:v-waves
+ * @methods dragDirective 自定义拖动指令,用法:v-drag
+ */
+export function directive(app: App) {
+	// 用户权限指令
+	authDirective(app);
+	// 按钮波浪指令
+	wavesDirective(app);
+	// 自定义拖动指令
+	dragDirective(app);
+}

+ 1 - 0
Web/src/i18n/lang/en.ts

@@ -59,6 +59,7 @@ export default {
 		makeSelector: 'Icon selector',
 		makeNoticeBar: 'notification bar',
 		makeSvgDemo: 'Svgicon demo',
+		makeTableDemo: 'table demo',
 		paramsIndex: 'Routing parameters',
 		paramsCommon: 'General routing',
 		paramsDynamic: 'Dynamic routing',

+ 1 - 0
Web/src/i18n/lang/zh-cn.ts

@@ -59,6 +59,7 @@ export default {
 		makeSelector: '图标选择器',
 		makeNoticeBar: '滚动通知栏',
 		makeSvgDemo: 'svgIcon 演示',
+		makeTableDemo: '表格封装演示',
 		paramsIndex: '路由参数',
 		paramsCommon: '普通路由',
 		paramsDynamic: '动态路由',

+ 1 - 0
Web/src/i18n/lang/zh-tw.ts

@@ -59,6 +59,7 @@ export default {
 		makeSelector: '圖標選擇器',
 		makeNoticeBar: '滾動通知欄',
 		makeSvgDemo: 'svgIcon 演示',
+		makeTableDemo: '表格封裝演示',
 		paramsIndex: '路由參數',
 		paramsCommon: '普通路由',
 		paramsDynamic: '動態路由',

+ 134 - 145
Web/src/layout/component/aside.vue

@@ -3,14 +3,14 @@
 		<el-aside class="layout-aside" :class="setCollapseStyle">
 			<Logo v-if="setShowLogo" />
 			<el-scrollbar class="flex-auto" ref="layoutAsideScrollbarRef" @mouseenter="onAsideEnterLeave(true)" @mouseleave="onAsideEnterLeave(false)">
-				<Vertical :menuList="menuList" />
+				<Vertical :menuList="state.menuList" />
 			</el-scrollbar>
 		</el-aside>
 	</div>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, toRefs, reactive, computed, watch, onBeforeMount, defineComponent, ref } from 'vue';
+<script setup lang="ts" name="layoutAside">
+import { defineAsyncComponent, reactive, computed, watch, onBeforeMount, ref } from 'vue';
 import { storeToRefs } from 'pinia';
 import pinia from '/@/stores/index';
 import { useRoutesList } from '/@/stores/routesList';
@@ -18,148 +18,137 @@ import { useThemeConfig } from '/@/stores/themeConfig';
 import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
 import mittBus from '/@/utils/mitt';
 
-export default defineComponent({
-	name: 'layoutAside',
-	components: {
-		Logo: defineAsyncComponent(() => import('/@/layout/logo/index.vue')),
-		Vertical: defineAsyncComponent(() => import('/@/layout/navMenu/vertical.vue')),
-	},
-	setup() {
-		const layoutAsideScrollbarRef = ref();
-		const stores = useRoutesList();
-		const storesThemeConfig = useThemeConfig();
-		const storesTagsViewRoutes = useTagsViewRoutes();
-		const { routesList } = storeToRefs(stores);
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
-		const state = reactive({
-			menuList: [],
-			clientWidth: 0,
-		});
-		// 设置菜单展开/收起时的宽度
-		const setCollapseStyle = computed(() => {
-			const { layout, isCollapse, menuBar } = themeConfig.value;
-			const asideBrTheme = ['#FFFFFF', '#FFF', '#fff', '#ffffff'];
-			const asideBrColor = asideBrTheme.includes(menuBar) ? 'layout-el-aside-br-color' : '';
-			// 判断是否是手机端
-			if (state.clientWidth <= 1000) {
-				if (isCollapse) {
-					document.body.setAttribute('class', 'el-popup-parent--hidden');
-					const asideEle = document.querySelector('.layout-container') as HTMLElement;
-					const modeDivs = document.createElement('div');
-					modeDivs.setAttribute('class', 'layout-aside-mobile-mode');
-					asideEle.appendChild(modeDivs);
-					modeDivs.addEventListener('click', closeLayoutAsideMobileMode);
-					return [asideBrColor, 'layout-aside-mobile', 'layout-aside-mobile-open'];
-				} else {
-					// 关闭弹窗
-					closeLayoutAsideMobileMode();
-					return [asideBrColor, 'layout-aside-mobile', 'layout-aside-mobile-close'];
-				}
-			} else {
-				if (layout === 'columns') {
-					// 分栏布局,菜单收起时宽度给 1px
-					if (isCollapse) return [asideBrColor, 'layout-aside-pc-1'];
-					else return [asideBrColor, 'layout-aside-pc-220'];
-				} else {
-					// 其它布局给 64px
-					if (isCollapse) return [asideBrColor, 'layout-aside-pc-64'];
-					else return [asideBrColor, 'layout-aside-pc-220'];
-				}
-			}
-		});
-		// 关闭移动端蒙版
-		const closeLayoutAsideMobileMode = () => {
-			const el = document.querySelector('.layout-aside-mobile-mode');
-			el?.setAttribute('style', 'animation: error-img-two 0.3s');
-			setTimeout(() => {
-				el?.parentNode?.removeChild(el);
-			}, 300);
-			const clientWidth = document.body.clientWidth;
-			if (clientWidth < 1000) themeConfig.value.isCollapse = false;
-			document.body.setAttribute('class', '');
-		};
-		// 设置显示/隐藏 logo
-		const setShowLogo = computed(() => {
-			let { layout, isShowLogo } = themeConfig.value;
-			return (isShowLogo && layout === 'defaults') || (isShowLogo && layout === 'columns');
-		});
-		// 设置/过滤路由(非静态路由/是否显示在菜单中)
-		const setFilterRoutes = () => {
-			if (themeConfig.value.layout === 'columns') return false;
-			(state.menuList as any) = filterRoutesFun(routesList.value);
-		};
-		// 路由过滤递归函数
-		const filterRoutesFun = (arr: Array<string>) => {
-			return arr
-				.filter((item: any) => !item.meta.isHide)
-				.map((item: any) => {
-					item = Object.assign({}, item);
-					if (item.children) item.children = filterRoutesFun(item.children);
-					return item;
-				});
-		};
-		// 设置菜单导航是否固定(移动端)
-		const initMenuFixed = (clientWidth: number) => {
-			state.clientWidth = clientWidth;
-		};
-		// 鼠标移入、移出
-		const onAsideEnterLeave = (bool: Boolean) => {
-			let { layout } = themeConfig.value;
-			if (layout !== 'columns') return false;
-			if (!bool) mittBus.emit('restoreDefault');
-			stores.setColumnsMenuHover(bool);
-		};
-		// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
-		watch(themeConfig.value, (val) => {
-			if (val.isShowLogoChange !== val.isShowLogo) {
-				if (layoutAsideScrollbarRef.value) layoutAsideScrollbarRef.value.update();
-			}
-		});
-		// 监听vuex值的变化,动态赋值给菜单中
-		watch(
-			pinia.state,
-			(val) => {
-				let { layout, isClassicSplitMenu } = val.themeConfig.themeConfig;
-				if (layout === 'classic' && isClassicSplitMenu) return false;
-				setFilterRoutes();
-			},
-			{
-				deep: true,
-			}
-		);
-		// 页面加载前
-		onBeforeMount(() => {
-			initMenuFixed(document.body.clientWidth);
-			setFilterRoutes();
-			// 此界面不需要取消监听(mittBus.off('setSendColumnsChildren))
-			// 因为切换布局时有的监听需要使用,取消了监听,某些操作将不生效
-			mittBus.on('setSendColumnsChildren', (res: any) => {
-				state.menuList = res.children;
-			});
-			mittBus.on('setSendClassicChildren', (res: any) => {
-				let { layout, isClassicSplitMenu } = themeConfig.value;
-				if (layout === 'classic' && isClassicSplitMenu) {
-					state.menuList = [];
-					state.menuList = res.children;
-				}
-			});
-			mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
-				setFilterRoutes();
-			});
-			mittBus.on('layoutMobileResize', (res: any) => {
-				initMenuFixed(res.clientWidth);
-				closeLayoutAsideMobileMode();
-			});
+// 引入组件
+const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue'));
+const Vertical = defineAsyncComponent(() => import('/@/layout/navMenu/vertical.vue'));
+
+// 定义变量内容
+const layoutAsideScrollbarRef = ref();
+const stores = useRoutesList();
+const storesThemeConfig = useThemeConfig();
+const storesTagsViewRoutes = useTagsViewRoutes();
+const { routesList } = storeToRefs(stores);
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
+const state = reactive<AsideState>({
+	menuList: [],
+	clientWidth: 0,
+});
+
+// 设置菜单展开/收起时的宽度
+const setCollapseStyle = computed(() => {
+	const { layout, isCollapse, menuBar } = themeConfig.value;
+	const asideBrTheme = ['#FFFFFF', '#FFF', '#fff', '#ffffff'];
+	const asideBrColor = asideBrTheme.includes(menuBar) ? 'layout-el-aside-br-color' : '';
+	// 判断是否是手机端
+	if (state.clientWidth <= 1000) {
+		if (isCollapse) {
+			document.body.setAttribute('class', 'el-popup-parent--hidden');
+			const asideEle = document.querySelector('.layout-container') as HTMLElement;
+			const modeDivs = document.createElement('div');
+			modeDivs.setAttribute('class', 'layout-aside-mobile-mode');
+			asideEle.appendChild(modeDivs);
+			modeDivs.addEventListener('click', closeLayoutAsideMobileMode);
+			return [asideBrColor, 'layout-aside-mobile', 'layout-aside-mobile-open'];
+		} else {
+			// 关闭弹窗
+			closeLayoutAsideMobileMode();
+			return [asideBrColor, 'layout-aside-mobile', 'layout-aside-mobile-close'];
+		}
+	} else {
+		if (layout === 'columns') {
+			// 分栏布局,菜单收起时宽度给 1px
+			if (isCollapse) return [asideBrColor, 'layout-aside-pc-1'];
+			else return [asideBrColor, 'layout-aside-pc-220'];
+		} else {
+			// 其它布局给 64px
+			if (isCollapse) return [asideBrColor, 'layout-aside-pc-64'];
+			else return [asideBrColor, 'layout-aside-pc-220'];
+		}
+	}
+});
+// 设置显示/隐藏 logo
+const setShowLogo = computed(() => {
+	let { layout, isShowLogo } = themeConfig.value;
+	return (isShowLogo && layout === 'defaults') || (isShowLogo && layout === 'columns');
+});
+// 关闭移动端蒙版
+const closeLayoutAsideMobileMode = () => {
+	const el = document.querySelector('.layout-aside-mobile-mode');
+	el?.setAttribute('style', 'animation: error-img-two 0.3s');
+	setTimeout(() => {
+		el?.parentNode?.removeChild(el);
+	}, 300);
+	const clientWidth = document.body.clientWidth;
+	if (clientWidth < 1000) themeConfig.value.isCollapse = false;
+	document.body.setAttribute('class', '');
+};
+// 设置/过滤路由(非静态路由/是否显示在菜单中)
+const setFilterRoutes = () => {
+	if (themeConfig.value.layout === 'columns') return false;
+	state.menuList = filterRoutesFun(routesList.value);
+};
+// 路由过滤递归函数
+const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
+	return arr
+		.filter((item: T) => !item.meta?.isHide)
+		.map((item: T) => {
+			item = Object.assign({}, item);
+			if (item.children) item.children = filterRoutesFun(item.children);
+			return item;
 		});
-		return {
-			layoutAsideScrollbarRef,
-			setCollapseStyle,
-			setShowLogo,
-			isTagsViewCurrenFull,
-			onAsideEnterLeave,
-			...toRefs(state),
-		};
-	},
+};
+// 设置菜单导航是否固定(移动端)
+const initMenuFixed = (clientWidth: number) => {
+	state.clientWidth = clientWidth;
+};
+// 鼠标移入、移出
+const onAsideEnterLeave = (bool: Boolean) => {
+	let { layout } = themeConfig.value;
+	if (layout !== 'columns') return false;
+	if (!bool) mittBus.emit('restoreDefault');
+	stores.setColumnsMenuHover(bool);
+};
+// 页面加载前
+onBeforeMount(() => {
+	initMenuFixed(document.body.clientWidth);
+	setFilterRoutes();
+	// 此界面不需要取消监听(mittBus.off('setSendColumnsChildren))
+	// 因为切换布局时有的监听需要使用,取消了监听,某些操作将不生效
+	mittBus.on('setSendColumnsChildren', (res: MittMenu) => {
+		state.menuList = res.children;
+	});
+	mittBus.on('setSendClassicChildren', (res: MittMenu) => {
+		let { layout, isClassicSplitMenu } = themeConfig.value;
+		if (layout === 'classic' && isClassicSplitMenu) {
+			state.menuList = [];
+			state.menuList = res.children;
+		}
+	});
+	mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
+		setFilterRoutes();
+	});
+	mittBus.on('layoutMobileResize', (res: LayoutMobileResize) => {
+		initMenuFixed(res.clientWidth);
+		closeLayoutAsideMobileMode();
+	});
+});
+// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
+watch(themeConfig.value, (val) => {
+	if (val.isShowLogoChange !== val.isShowLogo) {
+		if (layoutAsideScrollbarRef.value) layoutAsideScrollbarRef.value.update();
+	}
 });
+// 监听 pinia 值的变化,动态赋值给菜单中
+watch(
+	pinia.state,
+	(val) => {
+		let { layout, isClassicSplitMenu } = val.themeConfig.themeConfig;
+		if (layout === 'classic' && isClassicSplitMenu) return false;
+		setFilterRoutes();
+	},
+	{
+		deep: true,
+	}
+);
 </script>

+ 139 - 166
Web/src/layout/component/columnsAside.vue

@@ -1,12 +1,10 @@
 <template>
 	<div class="layout-columns-aside">
-		<div class="layout-logo">
-			<img :src="logoMini" class="layout-logo-medium-img" />
-		</div>
+		<div class="layout-logo"><img :src="logoMini" class="layout-logo-medium-img" /></div>
 		<el-scrollbar>
 			<ul @mouseleave="onColumnsAsideMenuMouseleave()">
 				<li
-					v-for="(v, k) in columnsAsideList"
+					v-for="(v, k) in state.columnsAsideList"
 					:key="k"
 					@click="onColumnsAsideMenuClick(v, k)"
 					@mouseenter="onColumnsAsideMenuMouseenter(v, k)"
@@ -15,7 +13,7 @@
 							if (el) columnsAsideOffsetTopRefs[k] = el;
 						}
 					"
-					:class="{ 'layout-columns-active': liIndex === k, 'layout-columns-hover': liHoverIndex === k }"
+					:class="{ 'layout-columns-active': state.liIndex === k, 'layout-columns-hover': state.liHoverIndex === k }"
 					:title="$t(v.meta.title)"
 				>
 					<div :class="themeConfig.columnsAsideLayout" v-if="!v.meta.isLink || (v.meta.isLink && v.meta.isIframe)">
@@ -39,178 +37,152 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { reactive, toRefs, ref, onMounted, nextTick, watch, onUnmounted, defineComponent } from 'vue';
+<script setup lang="ts" name="layoutColumnsAside">
+import { reactive, ref, onMounted, nextTick, watch, onUnmounted } from 'vue';
 import { useRoute, useRouter, onBeforeRouteUpdate, RouteRecordRaw } from 'vue-router';
 import { storeToRefs } from 'pinia';
 import pinia from '/@/stores/index';
 import { useRoutesList } from '/@/stores/routesList';
 import { useThemeConfig } from '/@/stores/themeConfig';
 import mittBus from '/@/utils/mitt';
-
 import logoMini from '/@/assets/logo-mini.svg';
 
-// 定义接口来定义对象的类型
-interface ColumnsAsideState {
-	columnsAsideList: any[];
-	liIndex: number;
-	liOldIndex: null | number;
-	liHoverIndex: null | number;
-	liOldPath: null | string;
-	difference: number;
-	routeSplit: string[];
-}
+// 定义变量内容
+const columnsAsideOffsetTopRefs = ref<RefType>([]);
+const columnsAsideActiveRef = ref();
+const stores = useRoutesList();
+const storesThemeConfig = useThemeConfig();
+const { routesList, isColumnsMenuHover, isColumnsNavHover } = storeToRefs(stores);
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const route = useRoute();
+const router = useRouter();
+const state = reactive<ColumnsAsideState>({
+	columnsAsideList: [],
+	liIndex: 0,
+	liOldIndex: null,
+	liHoverIndex: null,
+	liOldPath: null,
+	difference: 0,
+	routeSplit: [],
+});
 
-export default defineComponent({
-	name: 'layoutColumnsAside',
-	setup() {
-		const columnsAsideOffsetTopRefs: any = ref([]);
-		const columnsAsideActiveRef = ref();
-		const stores = useRoutesList();
-		const storesThemeConfig = useThemeConfig();
-		const { routesList, isColumnsMenuHover, isColumnsNavHover } = storeToRefs(stores);
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		const route = useRoute();
-		const router = useRouter();
-		const state = reactive<ColumnsAsideState>({
-			columnsAsideList: [],
-			liIndex: 0,
-			liOldIndex: null,
-			liHoverIndex: null,
-			liOldPath: null,
-			difference: 0,
-			routeSplit: [],
-		});
-		// 设置菜单高亮位置移动
-		const setColumnsAsideMove = (k: number) => {
-			state.liIndex = k;
-			columnsAsideActiveRef.value.style.top = `${columnsAsideOffsetTopRefs.value[k].offsetTop + state.difference}px`;
-		};
-		// 菜单高亮点击事件
-		const onColumnsAsideMenuClick = (v: Object, k: number) => {
-			setColumnsAsideMove(k);
-			let { path, redirect } = v as any;
-			if (redirect) router.push(redirect);
-			else router.push(path);
-		};
-		// 鼠标移入时,显示当前的子级菜单
-		const onColumnsAsideMenuMouseenter = (v: RouteRecordRaw, k: number) => {
-			if (!themeConfig.value.isColumnsMenuHoverPreload) return false;
-			let { path } = v;
-			state.liOldPath = path;
-			state.liOldIndex = k;
-			state.liHoverIndex = k;
-			mittBus.emit('setSendColumnsChildren', setSendChildren(path));
-			stores.setColumnsMenuHover(false);
-			stores.setColumnsNavHover(true);
-		};
-		// 鼠标移走时,显示原来的子级菜单
-		const onColumnsAsideMenuMouseleave = async () => {
-			await stores.setColumnsNavHover(false);
-			// 添加延时器,防止拿到的 store.state.routesList 值不是最新的
-			setTimeout(() => {
-				if (!isColumnsMenuHover && !isColumnsNavHover) mittBus.emit('restoreDefault');
-			}, 100);
-		};
-		// 设置高亮动态位置
-		const onColumnsAsideDown = (k: number) => {
-			nextTick(() => {
-				setColumnsAsideMove(k);
-			});
-		};
-		// 设置/过滤路由(非静态路由/是否显示在菜单中)
-		const setFilterRoutes = () => {
-			state.columnsAsideList = filterRoutesFun(routesList.value);
-			const resData: any = setSendChildren(route.path);
-			if (Object.keys(resData).length <= 0) return false;
-			onColumnsAsideDown(resData.item[0].k);
-			mittBus.emit('setSendColumnsChildren', resData);
-		};
-		// 传送当前子级数据到菜单中
-		const setSendChildren = (path: string) => {
-			const currentPathSplit = path.split('/');
-			let currentData: any = {};
-			state.columnsAsideList.map((v: any, k: number) => {
-				if (v.path === `/${currentPathSplit[1]}`) {
-					v['k'] = k;
-					currentData['item'] = [{ ...v }];
-					currentData['children'] = [{ ...v }];
-					if (v.children) currentData['children'] = v.children;
-				}
-			});
-			return currentData;
-		};
-		// 路由过滤递归函数
-		const filterRoutesFun = (arr: Array<string>) => {
-			return arr
-				.filter((item: any) => !item.meta.isHide)
-				.map((item: any) => {
-					item = Object.assign({}, item);
-					if (item.children) item.children = filterRoutesFun(item.children);
-					return item;
-				});
-		};
-		// tagsView 点击时,根据路由查找下标 columnsAsideList,实现左侧菜单高亮
-		const setColumnsMenuHighlight = (path: string) => {
-			state.routeSplit = path.split('/');
-			state.routeSplit.shift();
-			const routeFirst = `/${state.routeSplit[0]}`;
-			const currentSplitRoute = state.columnsAsideList.find((v: any) => v.path === routeFirst);
-			if (!currentSplitRoute) return false;
-			// 延迟拿值,防止取不到
-			setTimeout(() => {
-				onColumnsAsideDown((<any>currentSplitRoute).k);
-			}, 0);
-		};
-		// 监听布局配置信息的变化,动态增加菜单高亮位置移动像素
-		watch(
-			pinia.state,
-			(val) => {
-				val.themeConfig.themeConfig.columnsAsideStyle === 'columnsRound' ? (state.difference = 3) : (state.difference = 0);
-				if (!val.routesList.isColumnsMenuHover && !val.routesList.isColumnsNavHover) {
-					state.liHoverIndex = null;
-					mittBus.emit('setSendColumnsChildren', setSendChildren(route.path));
-				} else {
-					state.liHoverIndex = state.liOldIndex;
-					if (!state.liOldPath) return false;
-					mittBus.emit('setSendColumnsChildren', setSendChildren(state.liOldPath));
-				}
-			},
-			{
-				deep: true,
-			}
-		);
-		// 页面加载时
-		onMounted(() => {
-			setFilterRoutes();
-			// 销毁变量,防止鼠标再次移入时,保留了上次的记录
-			mittBus.on('restoreDefault', () => {
-				state.liOldIndex = null;
-				state.liOldPath = null;
-			});
-		});
-		// 页面卸载时
-		onUnmounted(() => {
-			mittBus.off('restoreDefault', () => {});
-		});
-		// 路由更新时
-		onBeforeRouteUpdate((to) => {
-			setColumnsMenuHighlight(to.path);
-			mittBus.emit('setSendColumnsChildren', setSendChildren(to.path));
+// 设置菜单高亮位置移动
+const setColumnsAsideMove = (k: number) => {
+	state.liIndex = k;
+	columnsAsideActiveRef.value.style.top = `${columnsAsideOffsetTopRefs.value[k].offsetTop + state.difference}px`;
+};
+// 菜单高亮点击事件
+const onColumnsAsideMenuClick = (v: RouteItem, k: number) => {
+	setColumnsAsideMove(k);
+	let { path, redirect } = v;
+	if (redirect) router.push(redirect);
+	else router.push(path);
+};
+// 鼠标移入时,显示当前的子级菜单
+const onColumnsAsideMenuMouseenter = (v: RouteRecordRaw, k: number) => {
+	if (!themeConfig.value.isColumnsMenuHoverPreload) return false;
+	let { path } = v;
+	state.liOldPath = path;
+	state.liOldIndex = k;
+	state.liHoverIndex = k;
+	mittBus.emit('setSendColumnsChildren', setSendChildren(path));
+	stores.setColumnsMenuHover(false);
+	stores.setColumnsNavHover(true);
+};
+// 鼠标移走时,显示原来的子级菜单
+const onColumnsAsideMenuMouseleave = async () => {
+	await stores.setColumnsNavHover(false);
+	// 添加延时器,防止拿到的 store.state.routesList 值不是最新的
+	setTimeout(() => {
+		if (!isColumnsMenuHover && !isColumnsNavHover) mittBus.emit('restoreDefault');
+	}, 100);
+};
+// 设置高亮动态位置
+const onColumnsAsideDown = (k: number) => {
+	nextTick(() => {
+		setColumnsAsideMove(k);
+	});
+};
+// 设置/过滤路由(非静态路由/是否显示在菜单中)
+const setFilterRoutes = () => {
+	state.columnsAsideList = filterRoutesFun(routesList.value);
+	const resData: MittMenu = setSendChildren(route.path);
+	if (Object.keys(resData).length <= 0) return false;
+	onColumnsAsideDown(resData.item?.k);
+	mittBus.emit('setSendColumnsChildren', resData);
+};
+// 传送当前子级数据到菜单中
+const setSendChildren = (path: string) => {
+	const currentPathSplit = path.split('/');
+	let currentData: MittMenu = { children: [] };
+	state.columnsAsideList.map((v: RouteItem, k: number) => {
+		if (v.path === `/${currentPathSplit[1]}`) {
+			v['k'] = k;
+			currentData['item'] = { ...v };
+			currentData['children'] = [{ ...v }];
+			if (v.children) currentData['children'] = v.children;
+		}
+	});
+	return currentData;
+};
+// 路由过滤递归函数
+const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
+	return arr
+		.filter((item: T) => !item.meta?.isHide)
+		.map((item: T) => {
+			item = Object.assign({}, item);
+			if (item.children) item.children = filterRoutesFun(item.children);
+			return item;
 		});
-		return {
-			logoMini,
-			themeConfig,
-			columnsAsideOffsetTopRefs,
-			columnsAsideActiveRef,
-			onColumnsAsideDown,
-			onColumnsAsideMenuClick,
-			onColumnsAsideMenuMouseenter,
-			onColumnsAsideMenuMouseleave,
-			...toRefs(state),
-		};
-	},
+};
+// tagsView 点击时,根据路由查找下标 columnsAsideList,实现左侧菜单高亮
+const setColumnsMenuHighlight = (path: string) => {
+	state.routeSplit = path.split('/');
+	state.routeSplit.shift();
+	const routeFirst = `/${state.routeSplit[0]}`;
+	const currentSplitRoute = state.columnsAsideList.find((v: RouteItem) => v.path === routeFirst);
+	if (!currentSplitRoute) return false;
+	// 延迟拿值,防止取不到
+	setTimeout(() => {
+		onColumnsAsideDown(currentSplitRoute.k);
+	}, 0);
+};
+// 页面加载时
+onMounted(() => {
+	setFilterRoutes();
+	// 销毁变量,防止鼠标再次移入时,保留了上次的记录
+	mittBus.on('restoreDefault', () => {
+		state.liOldIndex = null;
+		state.liOldPath = null;
+	});
+});
+// 页面卸载时
+onUnmounted(() => {
+	mittBus.off('restoreDefault', () => {});
+});
+// 路由更新时
+onBeforeRouteUpdate((to) => {
+	setColumnsMenuHighlight(to.path);
+	mittBus.emit('setSendColumnsChildren', setSendChildren(to.path));
 });
+// 监听布局配置信息的变化,动态增加菜单高亮位置移动像素
+watch(
+	pinia.state,
+	(val) => {
+		val.themeConfig.themeConfig.columnsAsideStyle === 'columnsRound' ? (state.difference = 3) : (state.difference = 0);
+		if (!val.routesList.isColumnsMenuHover && !val.routesList.isColumnsNavHover) {
+			state.liHoverIndex = null;
+			mittBus.emit('setSendColumnsChildren', setSendChildren(route.path));
+		} else {
+			state.liHoverIndex = state.liOldIndex;
+			if (!state.liOldPath) return false;
+			mittBus.emit('setSendColumnsChildren', setSendChildren(state.liOldPath));
+		}
+	},
+	{
+		deep: true,
+	}
+);
 </script>
 
 <style scoped lang="scss">
@@ -291,6 +263,7 @@ export default defineComponent({
 		}
 	}
 }
+
 .layout-logo {
 	height: 50px;
 	display: flex;

+ 8 - 15
Web/src/layout/component/header.vue

@@ -4,22 +4,15 @@
 	</el-header>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, defineComponent } from 'vue';
+<script setup lang="ts" name="layoutHeader">
+import { defineAsyncComponent } from 'vue';
 import { storeToRefs } from 'pinia';
 import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
 
-export default defineComponent({
-	name: 'layoutHeader',
-	components: {
-		NavBarsIndex: defineAsyncComponent(() => import('/@/layout/navBars/index.vue')),
-	},
-	setup() {
-		const storesTagsViewRoutes = useTagsViewRoutes();
-		const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
-		return {
-			isTagsViewCurrenFull,
-		};
-	},
-});
+// 引入组件
+const NavBarsIndex = defineAsyncComponent(() => import('/@/layout/navBars/index.vue'));
+
+// 定义变量内容
+const storesTagsViewRoutes = useTagsViewRoutes();
+const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
 </script>

+ 44 - 43
Web/src/layout/component/main.vue

@@ -1,59 +1,60 @@
 <template>
 	<el-main class="layout-main" :style="isFixedHeader ? `height: calc(100% - ${setMainHeight})` : `minHeight: calc(100% - ${setMainHeight})`">
-		<el-scrollbar ref="layoutMainScrollbarRef" class="layout-main-scroll" wrap-class="layout-main-scroll" view-class="layout-main-scroll">
+		<el-scrollbar ref="layoutMainScrollbarRef" class="layout-main-scroll layout-backtop-header-fixed" wrap-class="layout-main-scroll" view-class="layout-main-scroll">
 			<LayoutParentView />
 			<LayoutFooter v-if="isFooter" />
 		</el-scrollbar>
-		<el-backtop target=".layout-backtop .el-scrollbar__wrap" />
+		<el-backtop :target="setBacktopClass" />
 	</el-main>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, defineComponent, onMounted, computed, ref } from 'vue';
+<script setup lang="ts" name="layoutMain">
+import { defineAsyncComponent, onMounted, computed, ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { storeToRefs } from 'pinia';
 import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
 import { useThemeConfig } from '/@/stores/themeConfig';
 import { NextLoading } from '/@/utils/loading';
 
-export default defineComponent({
-	name: 'layoutMain',
-	components: {
-		LayoutParentView: defineAsyncComponent(() => import('/@/layout/routerView/parent.vue')),
-		LayoutFooter: defineAsyncComponent(() => import('/@/layout/footer/index.vue')),
-	},
-	setup() {
-		const layoutMainScrollbarRef = ref('');
-		const route = useRoute();
-		const storesTagsViewRoutes = useTagsViewRoutes();
-		const storesThemeConfig = useThemeConfig();
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
-		// 设置 footer 显示/隐藏
-		const isFooter = computed(() => {
-			return themeConfig.value.isFooter && !route.meta.isIframe;
-		});
-		// 设置 header 固定
-		const isFixedHeader = computed(() => {
-			return themeConfig.value.isFixedHeader;
-		});
-		// 设置主内容区的高度
-		const setMainHeight = computed(() => {
-			if (isTagsViewCurrenFull.value) return '0px';
-			const { isTagsview, layout } = themeConfig.value;
-			if (isTagsview && layout !== 'classic') return '85px';
-			else return '51px';
-		});
-		// 页面加载前
-		onMounted(() => {
-			NextLoading.done(600);
-		});
-		return {
-			layoutMainScrollbarRef,
-			isFooter,
-			isFixedHeader,
-			setMainHeight,
-		};
-	},
+// 引入组件
+const LayoutParentView = defineAsyncComponent(() => import('/@/layout/routerView/parent.vue'));
+const LayoutFooter = defineAsyncComponent(() => import('/@/layout/footer/index.vue'));
+
+// 定义变量内容
+const layoutMainScrollbarRef = ref();
+const route = useRoute();
+const storesTagsViewRoutes = useTagsViewRoutes();
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
+
+// 设置 footer 显示/隐藏
+const isFooter = computed(() => {
+	return themeConfig.value.isFooter && !route.meta.isIframe;
+});
+// 设置 header 固定
+const isFixedHeader = computed(() => {
+	return themeConfig.value.isFixedHeader;
+});
+// 设置 Backtop 回到顶部
+const setBacktopClass = computed(() => {
+	if (themeConfig.value.isFixedHeader) return `.layout-backtop-header-fixed .el-scrollbar__wrap`;
+	else return `.layout-backtop .el-scrollbar__wrap`;
+});
+// 设置主内容区的高度
+const setMainHeight = computed(() => {
+	if (isTagsViewCurrenFull.value) return '0px';
+	const { isTagsview, layout } = themeConfig.value;
+	if (isTagsview && layout !== 'classic') return '85px';
+	else return '51px';
+});
+// 页面加载前
+onMounted(() => {
+	NextLoading.done(600);
+});
+
+// 暴露变量
+defineExpose({
+	layoutMainScrollbarRef,
 });
 </script>

+ 2 - 6
Web/src/layout/footer/index.vue

@@ -7,12 +7,8 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
-
-export default defineComponent({
-	name: 'layoutFooter',
-});
+<script setup lang="ts" name="layoutFooter">
+// 此处需有内容(注释也得),否则缓存将失败
 </script>
 
 <style scoped lang="scss">

+ 38 - 42
Web/src/layout/index.vue

@@ -1,54 +1,50 @@
 <template>
-	<component :is="themeConfig.layout" />
+	<component :is="layouts[themeConfig.layout]" />
 </template>
 
-<script lang="ts">
-import { onBeforeMount, onUnmounted, defineComponent, defineAsyncComponent } from 'vue';
+<script setup lang="ts" name="layout">
+import { onBeforeMount, onUnmounted, defineAsyncComponent } from 'vue';
 import { storeToRefs } from 'pinia';
 import { useThemeConfig } from '/@/stores/themeConfig';
 import { Local } from '/@/utils/storage';
 import mittBus from '/@/utils/mitt';
 
-export default defineComponent({
-	name: 'layout',
-	components: {
-		defaults: defineAsyncComponent(() => import('/@/layout/main/defaults.vue')),
-		classic: defineAsyncComponent(() => import('/@/layout/main/classic.vue')),
-		transverse: defineAsyncComponent(() => import('/@/layout/main/transverse.vue')),
-		columns: defineAsyncComponent(() => import('/@/layout/main/columns.vue')),
-	},
-	setup() {
-		const storesThemeConfig = useThemeConfig();
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		// 窗口大小改变时(适配移动端)
-		const onLayoutResize = () => {
-			if (!Local.get('oldLayout')) Local.set('oldLayout', themeConfig.value.layout);
-			const clientWidth = document.body.clientWidth;
-			if (clientWidth < 1000) {
-				themeConfig.value.isCollapse = false;
-				mittBus.emit('layoutMobileResize', {
-					layout: 'defaults',
-					clientWidth,
-				});
-			} else {
-				mittBus.emit('layoutMobileResize', {
-					layout: Local.get('oldLayout') ? Local.get('oldLayout') : themeConfig.value.layout,
-					clientWidth,
-				});
-			}
-		};
-		// 页面加载前
-		onBeforeMount(() => {
-			onLayoutResize();
-			window.addEventListener('resize', onLayoutResize);
+// 引入组件
+const layouts: any = {
+	defaults: defineAsyncComponent(() => import('/@/layout/main/defaults.vue')),
+	classic: defineAsyncComponent(() => import('/@/layout/main/classic.vue')),
+	transverse: defineAsyncComponent(() => import('/@/layout/main/transverse.vue')),
+	columns: defineAsyncComponent(() => import('/@/layout/main/columns.vue')),
+};
+
+// 定义变量内容
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+
+// 窗口大小改变时(适配移动端)
+const onLayoutResize = () => {
+	if (!Local.get('oldLayout')) Local.set('oldLayout', themeConfig.value.layout);
+	const clientWidth = document.body.clientWidth;
+	if (clientWidth < 1000) {
+		themeConfig.value.isCollapse = false;
+		mittBus.emit('layoutMobileResize', {
+			layout: 'defaults',
+			clientWidth,
 		});
-		// 页面卸载时
-		onUnmounted(() => {
-			window.removeEventListener('resize', onLayoutResize);
+	} else {
+		mittBus.emit('layoutMobileResize', {
+			layout: Local.get('oldLayout') ? Local.get('oldLayout') : themeConfig.value.layout,
+			clientWidth,
 		});
-		return {
-			themeConfig,
-		};
-	},
+	}
+};
+// 页面加载前
+onBeforeMount(() => {
+	onLayoutResize();
+	window.addEventListener('resize', onLayoutResize);
+});
+// 页面卸载时
+onUnmounted(() => {
+	window.removeEventListener('resize', onLayoutResize);
 });
 </script>

+ 135 - 161
Web/src/layout/lockScreen/index.vue

@@ -1,23 +1,23 @@
 <template>
-	<div v-show="isShowLockScreen">
+	<div v-show="state.isShowLockScreen">
 		<div class="layout-lock-screen-mask"></div>
-		<div class="layout-lock-screen-img" :class="{ 'layout-lock-screen-filter': isShowLoockLogin }"></div>
+		<div class="layout-lock-screen-img" :class="{ 'layout-lock-screen-filter': state.isShowLoockLogin }"></div>
 		<div class="layout-lock-screen">
 			<div
 				class="layout-lock-screen-date"
 				ref="layoutLockScreenDateRef"
-				@mousedown="onDown"
-				@mousemove="onMove"
+				@mousedown="onDownPc"
+				@mousemove="onMovePc"
 				@mouseup="onEnd"
-				@touchstart.stop="onDown"
-				@touchmove.stop="onMove"
+				@touchstart.stop="onDownApp"
+				@touchmove.stop="onMoveApp"
 				@touchend.stop="onEnd"
 			>
 				<div class="layout-lock-screen-date-box">
 					<div class="layout-lock-screen-date-box-time">
-						{{ time.hm }}<span class="layout-lock-screen-date-box-minutes">{{ time.s }}</span>
+						{{ state.time.hm }}<span class="layout-lock-screen-date-box-minutes">{{ state.time.s }}</span>
 					</div>
-					<div class="layout-lock-screen-date-box-info">{{ time.mdq }}</div>
+					<div class="layout-lock-screen-date-box-info">{{ state.time.mdq }}</div>
 				</div>
 				<div class="layout-lock-screen-date-top">
 					<SvgIcon name="ele-Top" />
@@ -25,19 +25,14 @@
 				</div>
 			</div>
 			<transition name="el-zoom-in-center">
-				<div v-show="isShowLoockLogin" class="layout-lock-screen-login">
+				<div v-show="state.isShowLoockLogin" class="layout-lock-screen-login">
 					<div class="layout-lock-screen-login-box">
 						<div class="layout-lock-screen-login-box-img">
 							<img src="https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500" />
 						</div>
 						<div class="layout-lock-screen-login-box-name">Administrator</div>
 						<div class="layout-lock-screen-login-box-value">
-							<el-input
-								placeholder="请输入密码"
-								ref="layoutLockScreenInputRef"
-								v-model="lockScreenPassword"
-								@keyup.enter.native.stop="onLockScreenSubmit()"
-							>
+							<el-input placeholder="请输入密码" ref="layoutLockScreenInputRef" v-model="state.lockScreenPassword" @keyup.enter.native.stop="onLockScreenSubmit()">
 								<template #append>
 									<el-button @click="onLockScreenSubmit">
 										<el-icon class="el-input__icon">
@@ -59,160 +54,139 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { nextTick, onMounted, reactive, toRefs, ref, onUnmounted, defineComponent } from 'vue';
+<script setup lang="ts" name="layoutLockScreen">
+import { nextTick, onMounted, reactive, ref, onUnmounted } from 'vue';
 import { formatDate } from '/@/utils/formatTime';
 import { Local } from '/@/utils/storage';
 import { storeToRefs } from 'pinia';
 import { useThemeConfig } from '/@/stores/themeConfig';
 
-// 定义接口来定义对象的类型
-interface LockScreenState {
-	transparency: number;
-	downClientY: number;
-	moveDifference: number;
-	isShowLoockLogin: boolean;
-	isFlags: boolean;
-	querySelectorEl: HTMLElement | string;
+// 定义变量内容
+const layoutLockScreenDateRef = ref<HtmlType>();
+const layoutLockScreenInputRef = ref();
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const state = reactive({
+	transparency: 1,
+	downClientY: 0,
+	moveDifference: 0,
+	isShowLoockLogin: false,
+	isFlags: false,
+	querySelectorEl: '' as HtmlType,
 	time: {
-		hm: string;
-		s: string;
-		mdq: string;
-	};
-	setIntervalTime: number;
-	isShowLockScreen: boolean;
-	isShowLockScreenIntervalTime: number;
-	lockScreenPassword: string;
-}
+		hm: '',
+		s: '',
+		mdq: '',
+	},
+	setIntervalTime: 0,
+	isShowLockScreen: false,
+	isShowLockScreenIntervalTime: 0,
+	lockScreenPassword: '',
+});
 
-export default defineComponent({
-	name: 'layoutLockScreen',
-	setup() {
-		const layoutLockScreenDateRef = ref();
-		const layoutLockScreenInputRef = ref();
-		const storesThemeConfig = useThemeConfig();
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		const state = reactive<LockScreenState>({
-			transparency: 1,
-			downClientY: 0,
-			moveDifference: 0,
-			isShowLoockLogin: false,
-			isFlags: false,
-			querySelectorEl: '',
-			time: {
-				hm: '',
-				s: '',
-				mdq: '',
-			},
-			setIntervalTime: 0,
-			isShowLockScreen: false,
-			isShowLockScreenIntervalTime: 0,
-			lockScreenPassword: '',
-		});
-		// 鼠标按下
-		const onDown = (down: any) => {
-			state.isFlags = true;
-			state.downClientY = down.touches ? down.touches[0].clientY : down.clientY;
-		};
-		// 鼠标移动
-		const onMove = (move: any) => {
-			if (state.isFlags) {
-				const el = <HTMLElement>state.querySelectorEl;
-				const opacitys = (state.transparency -= 1 / 200);
-				if (move.touches) {
-					state.moveDifference = move.touches[0].clientY - state.downClientY;
-				} else {
-					state.moveDifference = move.clientY - state.downClientY;
-				}
-				if (state.moveDifference >= 0) return false;
-				el.setAttribute('style', `top:${state.moveDifference}px;cursor:pointer;opacity:${opacitys};`);
-				if (state.moveDifference < -400) {
-					el.setAttribute('style', `top:${-el.clientHeight}px;cursor:pointer;transition:all 0.3s ease;`);
-					state.moveDifference = -el.clientHeight;
-					setTimeout(() => {
-						el && el.parentNode?.removeChild(el);
-					}, 300);
-				}
-				if (state.moveDifference === -el.clientHeight) {
-					state.isShowLoockLogin = true;
-					layoutLockScreenInputRef.value.focus();
-				}
-			}
-		};
-		// 鼠标松开
-		const onEnd = () => {
-			state.isFlags = false;
-			state.transparency = 1;
-			if (state.moveDifference >= -400) {
-				(<HTMLElement>state.querySelectorEl).setAttribute('style', `top:0px;opacity:1;transition:all 0.3s ease;`);
-			}
-		};
-		// 获取要拖拽的初始元素
-		const initGetElement = () => {
-			nextTick(() => {
-				state.querySelectorEl = layoutLockScreenDateRef.value;
-			});
-		};
-		// 时间初始化
-		const initTime = () => {
-			state.time.hm = formatDate(new Date(), 'HH:MM');
-			state.time.s = formatDate(new Date(), 'SS');
-			state.time.mdq = formatDate(new Date(), 'mm月dd日,WWW');
-		};
-		// 时间初始化定时器
-		const initSetTime = () => {
-			initTime();
-			state.setIntervalTime = window.setInterval(() => {
-				initTime();
-			}, 1000);
-		};
-		// 锁屏时间定时器
-		const initLockScreen = () => {
-			if (themeConfig.value.isLockScreen) {
-				state.isShowLockScreenIntervalTime = window.setInterval(() => {
-					if (themeConfig.value.lockScreenTime <= 1) {
-						state.isShowLockScreen = true;
-						setLocalThemeConfig();
-						return false;
-					}
-					themeConfig.value.lockScreenTime--;
-				}, 1000);
-			} else {
-				clearInterval(state.isShowLockScreenIntervalTime);
+// 鼠标按下 pc
+const onDownPc = (down: MouseEvent) => {
+	state.isFlags = true;
+	state.downClientY = down.clientY;
+};
+// 鼠标按下 app
+const onDownApp = (down: TouchEvent) => {
+	state.isFlags = true;
+	state.downClientY = down.touches[0].clientY;
+};
+// 鼠标移动 pc
+const onMovePc = (move: MouseEvent) => {
+	state.moveDifference = move.clientY - state.downClientY;
+	onMove();
+};
+// 鼠标移动 app
+const onMoveApp = (move: TouchEvent) => {
+	state.moveDifference = move.touches[0].clientY - state.downClientY;
+	onMove();
+};
+// 鼠标移动事件
+const onMove = () => {
+	if (state.isFlags) {
+		const el = <HTMLElement>state.querySelectorEl;
+		const opacitys = (state.transparency -= 1 / 200);
+		if (state.moveDifference >= 0) return false;
+		el.setAttribute('style', `top:${state.moveDifference}px;cursor:pointer;opacity:${opacitys};`);
+		if (state.moveDifference < -400) {
+			el.setAttribute('style', `top:${-el.clientHeight}px;cursor:pointer;transition:all 0.3s ease;`);
+			state.moveDifference = -el.clientHeight;
+			setTimeout(() => {
+				el && el.parentNode?.removeChild(el);
+			}, 300);
+		}
+		if (state.moveDifference === -el.clientHeight) {
+			state.isShowLoockLogin = true;
+			layoutLockScreenInputRef.value.focus();
+		}
+	}
+};
+// 鼠标松开
+const onEnd = () => {
+	state.isFlags = false;
+	state.transparency = 1;
+	if (state.moveDifference >= -400) {
+		(<HTMLElement>state.querySelectorEl).setAttribute('style', `top:0px;opacity:1;transition:all 0.3s ease;`);
+	}
+};
+// 获取要拖拽的初始元素
+const initGetElement = () => {
+	nextTick(() => {
+		state.querySelectorEl = layoutLockScreenDateRef.value;
+	});
+};
+// 时间初始化
+const initTime = () => {
+	state.time.hm = formatDate(new Date(), 'HH:MM');
+	state.time.s = formatDate(new Date(), 'SS');
+	state.time.mdq = formatDate(new Date(), 'mm月dd日,WWW');
+};
+// 时间初始化定时器
+const initSetTime = () => {
+	initTime();
+	state.setIntervalTime = window.setInterval(() => {
+		initTime();
+	}, 1000);
+};
+// 锁屏时间定时器
+const initLockScreen = () => {
+	if (themeConfig.value.isLockScreen) {
+		state.isShowLockScreenIntervalTime = window.setInterval(() => {
+			if (themeConfig.value.lockScreenTime <= 1) {
+				state.isShowLockScreen = true;
+				setLocalThemeConfig();
+				return false;
 			}
-		};
-		// 存储布局配置
-		const setLocalThemeConfig = () => {
-			themeConfig.value.isDrawer = false;
-			Local.set('themeConfig', themeConfig.value);
-		};
-		// 密码输入点击事件
-		const onLockScreenSubmit = () => {
-			themeConfig.value.isLockScreen = false;
-			themeConfig.value.lockScreenTime = 30;
-			setLocalThemeConfig();
-		};
-		// 页面加载时
-		onMounted(() => {
-			initGetElement();
-			initSetTime();
-			initLockScreen();
-		});
-		// 页面卸载时
-		onUnmounted(() => {
-			window.clearInterval(state.setIntervalTime);
-			window.clearInterval(state.isShowLockScreenIntervalTime);
-		});
-		return {
-			layoutLockScreenDateRef,
-			layoutLockScreenInputRef,
-			onDown,
-			onMove,
-			onEnd,
-			onLockScreenSubmit,
-			...toRefs(state),
-		};
-	},
+			themeConfig.value.lockScreenTime--;
+		}, 1000);
+	} else {
+		clearInterval(state.isShowLockScreenIntervalTime);
+	}
+};
+// 存储布局配置
+const setLocalThemeConfig = () => {
+	themeConfig.value.isDrawer = false;
+	Local.set('themeConfig', themeConfig.value);
+};
+// 密码输入点击事件
+const onLockScreenSubmit = () => {
+	themeConfig.value.isLockScreen = false;
+	themeConfig.value.lockScreenTime = 30;
+	setLocalThemeConfig();
+};
+// 页面加载时
+onMounted(() => {
+	initGetElement();
+	initSetTime();
+	initLockScreen();
+});
+// 页面卸载时
+onUnmounted(() => {
+	window.clearInterval(state.setIntervalTime);
+	window.clearInterval(state.isShowLockScreenIntervalTime);
 });
 </script>
 

+ 20 - 32
Web/src/layout/logo/index.vue

@@ -1,6 +1,6 @@
 <template>
 	<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
-		<img :src="logoMini" class="layout-logo-medium-img" v-if="showLogo"/>
+		<img :src="logoMini" class="layout-logo-medium-img" v-if="showLogo" />
 		<span>{{ themeConfig.globalTitle }}</span>
 	</div>
 	<div class="layout-logo-size" v-else @click="onThemeConfigChange">
@@ -8,42 +8,30 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
+<script setup lang="ts" name="layoutLogo">
+import { computed } from 'vue';
 import { storeToRefs } from 'pinia';
 import { useThemeConfig } from '/@/stores/themeConfig';
-
 import logoMini from '/@/assets/logo-mini.svg';
 
-export default defineComponent({
-	name: 'layoutLogo',
-	setup() {
-		const storesThemeConfig = useThemeConfig();
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		// 设置 logo 的显示。classic 经典布局默认显示 logo
-		const setShowLogo = computed(() => {
-			let { isCollapse, layout } = themeConfig.value;
-			return !isCollapse || layout === 'classic' || document.body.clientWidth < 1000;
-		});
-		// 分类模式不显示图标
-		const showLogo = computed(() => {
-			let { layout } = themeConfig.value;
-			return layout !== 'columns';
-		});
-		// logo 点击实现菜单展开/收起
-		const onThemeConfigChange = () => {
-			if (themeConfig.value.layout === 'transverse') return false;
-			themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
-		};
-		return {
-			logoMini,
-			setShowLogo,
-			showLogo,
-			themeConfig,
-			onThemeConfigChange,
-		};
-	},
+// 定义变量内容
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+
+// 设置 logo 的显示。classic 经典布局默认显示 logo
+const setShowLogo = computed(() => {
+	let { isCollapse, layout } = themeConfig.value;
+	return !isCollapse || layout === 'classic' || document.body.clientWidth < 1000;
+});
+const showLogo = computed(() => {
+	let { layout } = themeConfig.value;
+	return layout !== 'columns';
 });
+// logo 点击实现菜单展开/收起
+const onThemeConfigChange = () => {
+	if (themeConfig.value.layout === 'transverse') return false;
+	themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
+};
 </script>
 
 <style scoped lang="scss">

+ 52 - 58
Web/src/layout/main/classic.vue

@@ -11,67 +11,61 @@
 	</el-container>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, defineComponent, computed, ref, watch, nextTick, onMounted } from 'vue';
+<script setup lang="ts" name="layoutClassic">
+import { defineAsyncComponent, computed, ref, watch, nextTick, onMounted } from 'vue';
 import { useRoute } from 'vue-router';
 import { storeToRefs } from 'pinia';
 import { useThemeConfig } from '/@/stores/themeConfig';
 
-export default defineComponent({
-	name: 'layoutClassic',
-	components: {
-		LayoutAside: defineAsyncComponent(() => import('/@/layout/component/aside.vue')),
-		LayoutHeader: defineAsyncComponent(() => import('/@/layout/component/header.vue')),
-		LayoutMain: defineAsyncComponent(() => import('/@/layout/component/main.vue')),
-		LayoutTagsView: defineAsyncComponent(() => import('/@/layout/navBars/tagsView/tagsView.vue')),
-	},
-	setup() {
-		const layoutMainRef = ref<any>('');
-		const route = useRoute();
-		const storesThemeConfig = useThemeConfig();
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		// 判断是否显示 tasgview
-		const isTagsview = computed(() => {
-			return themeConfig.value.isTagsview;
-		});
-		// 重置滚动条高度,更新子级 scrollbar
-		const updateScrollbar = () => {
-			layoutMainRef.value.layoutMainScrollbarRef.update();
-		};
-		// 重置滚动条高度,由于组件是异步引入的
-		const initScrollBarHeight = () => {
-			nextTick(() => {
-				setTimeout(() => {
-					updateScrollbar();
-					layoutMainRef.value.layoutMainScrollbarRef.wrap$.scrollTop = 0;
-				}, 500);
-			});
-		};
-		// 监听路由的变化,切换界面时,滚动条置顶
-		watch(
-			() => route.path,
-			() => {
-				initScrollBarHeight();
-			}
-		);
-		// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
-		watch(
-			themeConfig,
-			() => {
-				updateScrollbar();
-			},
-			{
-				deep: true,
-			}
-		);
-		// 页面加载时
-		onMounted(() => {
-			initScrollBarHeight();
-		});
-		return {
-			layoutMainRef,
-			isTagsview,
-		};
-	},
+// 引入组件
+const LayoutAside = defineAsyncComponent(() => import('/@/layout/component/aside.vue'));
+const LayoutHeader = defineAsyncComponent(() => import('/@/layout/component/header.vue'));
+const LayoutMain = defineAsyncComponent(() => import('/@/layout/component/main.vue'));
+const LayoutTagsView = defineAsyncComponent(() => import('/@/layout/navBars/tagsView/tagsView.vue'));
+
+// 定义变量内容
+const layoutMainRef = ref<InstanceType<typeof LayoutMain>>();
+const route = useRoute();
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+
+// 判断是否显示 tasgview
+const isTagsview = computed(() => {
+	return themeConfig.value.isTagsview;
+});
+// 重置滚动条高度,更新子级 scrollbar
+const updateScrollbar = () => {
+	layoutMainRef.value?.layoutMainScrollbarRef.update();
+};
+// 重置滚动条高度,由于组件是异步引入的
+const initScrollBarHeight = () => {
+	nextTick(() => {
+		setTimeout(() => {
+			updateScrollbar();
+			// '!' not null 断言操作符,不执行运行时检查
+			layoutMainRef.value!.layoutMainScrollbarRef.wrapRef.scrollTop = 0;
+		}, 500);
+	});
+};
+// 页面加载时
+onMounted(() => {
+	initScrollBarHeight();
 });
+// 监听路由的变化,切换界面时,滚动条置顶
+watch(
+	() => route.path,
+	() => {
+		initScrollBarHeight();
+	}
+);
+// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
+watch(
+	themeConfig,
+	() => {
+		updateScrollbar();
+	},
+	{
+		deep: true,
+	}
+);
 </script>

+ 52 - 59
Web/src/layout/main/columns.vue

@@ -11,68 +11,61 @@
 	</el-container>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, watch, defineComponent, onMounted, nextTick, ref } from 'vue';
+<script setup lang="ts" name="layoutColumns">
+import { defineAsyncComponent, watch, onMounted, nextTick, ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { storeToRefs } from 'pinia';
 import { useThemeConfig } from '/@/stores/themeConfig';
 
-export default defineComponent({
-	name: 'layoutColumns',
-	components: {
-		LayoutAside: defineAsyncComponent(() => import('/@/layout/component/aside.vue')),
-		LayoutHeader: defineAsyncComponent(() => import('/@/layout/component/header.vue')),
-		LayoutMain: defineAsyncComponent(() => import('/@/layout/component/main.vue')),
-		ColumnsAside: defineAsyncComponent(() => import('/@/layout/component/columnsAside.vue')),
-	},
-	setup() {
-		const layoutScrollbarRef = ref<any>('');
-		const layoutMainRef = ref<any>('');
-		const route = useRoute();
-		const storesThemeConfig = useThemeConfig();
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		// 重置滚动条高度
-		const updateScrollbar = () => {
-			// 更新父级 scrollbar
-			layoutScrollbarRef.value.update();
-			// 更新子级 scrollbar
-			layoutMainRef.value.layoutMainScrollbarRef.update();
-		};
-		// 重置滚动条高度,由于组件是异步引入的
-		const initScrollBarHeight = () => {
-			nextTick(() => {
-				setTimeout(() => {
-					updateScrollbar();
-					layoutScrollbarRef.value.wrap$.scrollTop = 0;
-					layoutMainRef.value.layoutMainScrollbarRef.wrap$.scrollTop = 0;
-				}, 500);
-			});
-		};
-		// 监听路由的变化,切换界面时,滚动条置顶
-		watch(
-			() => route.path,
-			() => {
-				initScrollBarHeight();
-			}
-		);
-		// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
-		watch(
-			themeConfig,
-			() => {
-				updateScrollbar();
-			},
-			{
-				deep: true,
-			}
-		);
-		// 页面加载时
-		onMounted(() => {
-			initScrollBarHeight();
-		});
-		return {
-			layoutScrollbarRef,
-			layoutMainRef,
-		};
-	},
+// 引入组件
+const LayoutAside = defineAsyncComponent(() => import('/@/layout/component/aside.vue'));
+const LayoutHeader = defineAsyncComponent(() => import('/@/layout/component/header.vue'));
+const LayoutMain = defineAsyncComponent(() => import('/@/layout/component/main.vue'));
+const ColumnsAside = defineAsyncComponent(() => import('/@/layout/component/columnsAside.vue'));
+
+// 定义变量内容
+const layoutScrollbarRef = ref<RefType>('');
+const layoutMainRef = ref<InstanceType<typeof LayoutMain>>();
+const route = useRoute();
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+
+// 重置滚动条高度
+const updateScrollbar = () => {
+	// 更新父级 scrollbar
+	layoutScrollbarRef.value.update();
+	// 更新子级 scrollbar
+	layoutMainRef.value!.layoutMainScrollbarRef.update();
+};
+// 重置滚动条高度,由于组件是异步引入的
+const initScrollBarHeight = () => {
+	nextTick(() => {
+		setTimeout(() => {
+			updateScrollbar();
+			layoutScrollbarRef.value.wrapRef.scrollTop = 0;
+			layoutMainRef.value!.layoutMainScrollbarRef.wrapRef.scrollTop = 0;
+		}, 500);
+	});
+};
+// 页面加载时
+onMounted(() => {
+	initScrollBarHeight();
 });
+// 监听路由的变化,切换界面时,滚动条置顶
+watch(
+	() => route.path,
+	() => {
+		initScrollBarHeight();
+	}
+);
+// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
+watch(
+	themeConfig,
+	() => {
+		updateScrollbar();
+	},
+	{
+		deep: true,
+	}
+);
 </script>

+ 52 - 59
Web/src/layout/main/defaults.vue

@@ -10,69 +10,62 @@
 	</el-container>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, watch, defineComponent, onMounted, nextTick, ref } from 'vue';
+<script setup lang="ts" name="layoutDefaults">
+import { defineAsyncComponent, watch, onMounted, nextTick, ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { storeToRefs } from 'pinia';
 import { useThemeConfig } from '/@/stores/themeConfig';
 import { NextLoading } from '/@/utils/loading';
 
-export default defineComponent({
-	name: 'layoutDefaults',
-	components: {
-		LayoutAside: defineAsyncComponent(() => import('/@/layout/component/aside.vue')),
-		LayoutHeader: defineAsyncComponent(() => import('/@/layout/component/header.vue')),
-		LayoutMain: defineAsyncComponent(() => import('/@/layout/component/main.vue')),
-	},
-	setup() {
-		const layoutScrollbarRef = ref<any>('');
-		const layoutMainRef = ref<any>('');
-		const route = useRoute();
-		const storesThemeConfig = useThemeConfig();
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		// 重置滚动条高度
-		const updateScrollbar = () => {
-			// 更新父级 scrollbar
-			layoutScrollbarRef.value.update();
-			// 更新子级 scrollbar
-			layoutMainRef.value.layoutMainScrollbarRef.update();
-		};
-		// 重置滚动条高度,由于组件是异步引入的
-		const initScrollBarHeight = () => {
-			nextTick(() => {
-				setTimeout(() => {
-					updateScrollbar();
-					layoutScrollbarRef.value.wrap$.scrollTop = 0;
-					layoutMainRef.value.layoutMainScrollbarRef.wrap$.scrollTop = 0;
-				}, 500);
-			});
-		};
-		// 监听路由的变化,切换界面时,滚动条置顶
-		watch(
-			() => route.path,
-			() => {
-				initScrollBarHeight();
-			}
-		);
-		// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
-		watch(
-			themeConfig,
-			() => {
-				updateScrollbar();
-			},
-			{
-				deep: true,
-			}
-		);
-		// 页面加载时
-		onMounted(() => {
-			initScrollBarHeight();
-			NextLoading.done(600);
-		});
-		return {
-			layoutScrollbarRef,
-			layoutMainRef,
-		};
-	},
+// 引入组件
+const LayoutAside = defineAsyncComponent(() => import('/@/layout/component/aside.vue'));
+const LayoutHeader = defineAsyncComponent(() => import('/@/layout/component/header.vue'));
+const LayoutMain = defineAsyncComponent(() => import('/@/layout/component/main.vue'));
+
+// 定义变量内容
+const layoutScrollbarRef = ref<RefType>('');
+const layoutMainRef = ref<InstanceType<typeof LayoutMain>>();
+const route = useRoute();
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+
+// 重置滚动条高度
+const updateScrollbar = () => {
+	// 更新父级 scrollbar
+	layoutScrollbarRef.value.update();
+	// 更新子级 scrollbar
+	layoutMainRef.value!.layoutMainScrollbarRef.update();
+};
+// 重置滚动条高度,由于组件是异步引入的
+const initScrollBarHeight = () => {
+	nextTick(() => {
+		setTimeout(() => {
+			updateScrollbar();
+			layoutScrollbarRef.value.wrapRef.scrollTop = 0;
+			layoutMainRef.value!.layoutMainScrollbarRef.wrapRef.scrollTop = 0;
+		}, 500);
+	});
+};
+// 页面加载时
+onMounted(() => {
+	initScrollBarHeight();
+	NextLoading.done(600);
 });
+// 监听路由的变化,切换界面时,滚动条置顶
+watch(
+	() => route.path,
+	() => {
+		initScrollBarHeight();
+	}
+);
+// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
+watch(
+	themeConfig,
+	() => {
+		updateScrollbar();
+	},
+	{
+		deep: true,
+	}
+);
 </script>

+ 45 - 51
Web/src/layout/main/transverse.vue

@@ -5,60 +5,54 @@
 	</el-container>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, defineComponent, ref, watch, nextTick, onMounted } from 'vue';
+<script setup lang="ts" name="layoutTransverse">
+import { defineAsyncComponent, ref, watch, nextTick, onMounted } from 'vue';
 import { useRoute } from 'vue-router';
 import { storeToRefs } from 'pinia';
 import { useThemeConfig } from '/@/stores/themeConfig';
 
-export default defineComponent({
-	name: 'layoutTransverse',
-	components: {
-		LayoutHeader: defineAsyncComponent(() => import('/@/layout/component/header.vue')),
-		LayoutMain: defineAsyncComponent(() => import('/@/layout/component/main.vue')),
-	},
-	setup() {
-		const layoutMainRef = ref<any>('');
-		const storesThemeConfig = useThemeConfig();
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		const route = useRoute();
-		// 重置滚动条高度,更新子级 scrollbar
-		const updateScrollbar = () => {
-			layoutMainRef.value.layoutMainScrollbarRef.update();
-		};
-		// 重置滚动条高度,由于组件是异步引入的
-		const initScrollBarHeight = () => {
-			nextTick(() => {
-				setTimeout(() => {
-					updateScrollbar();
-					layoutMainRef.value.layoutMainScrollbarRef.wrap$.scrollTop = 0;
-				}, 500);
-			});
-		};
-		// 监听路由的变化,切换界面时,滚动条置顶
-		watch(
-			() => route.path,
-			() => {
-				initScrollBarHeight();
-			}
-		);
-		// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
-		watch(
-			themeConfig,
-			() => {
-				updateScrollbar();
-			},
-			{
-				deep: true,
-			}
-		);
-		// 页面加载时
-		onMounted(() => {
-			initScrollBarHeight();
-		});
-		return {
-			layoutMainRef,
-		};
-	},
+// 引入组件
+const LayoutHeader = defineAsyncComponent(() => import('/@/layout/component/header.vue'));
+const LayoutMain = defineAsyncComponent(() => import('/@/layout/component/main.vue'));
+
+// 定义变量内容
+const layoutMainRef = ref<InstanceType<typeof LayoutMain>>();
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const route = useRoute();
+
+// 重置滚动条高度,更新子级 scrollbar
+const updateScrollbar = () => {
+	layoutMainRef.value!.layoutMainScrollbarRef.update();
+};
+// 重置滚动条高度,由于组件是异步引入的
+const initScrollBarHeight = () => {
+	nextTick(() => {
+		setTimeout(() => {
+			updateScrollbar();
+			layoutMainRef.value!.layoutMainScrollbarRef.wrapRef.scrollTop = 0;
+		}, 500);
+	});
+};
+// 页面加载时
+onMounted(() => {
+	initScrollBarHeight();
 });
+// 监听路由的变化,切换界面时,滚动条置顶
+watch(
+	() => route.path,
+	() => {
+		initScrollBarHeight();
+	}
+);
+// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
+watch(
+	themeConfig,
+	() => {
+		updateScrollbar();
+	},
+	{
+		deep: true,
+	}
+);
 </script>

+ 72 - 89
Web/src/layout/navBars/breadcrumb/breadcrumb.vue

@@ -8,8 +8,8 @@
 		/>
 		<el-breadcrumb class="layout-navbars-breadcrumb-hide">
 			<transition-group name="breadcrumb">
-				<el-breadcrumb-item v-for="(v, k) in breadcrumbList" :key="!v.meta.tagsViewName ? v.meta.title : v.meta.tagsViewName">
-					<span v-if="k === breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
+				<el-breadcrumb-item v-for="(v, k) in state.breadcrumbList" :key="!v.meta.tagsViewName ? v.meta.title : v.meta.tagsViewName">
+					<span v-if="k === state.breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
 						<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
 						<div v-if="!v.meta.tagsViewName">{{ $t(v.meta.title) }}</div>
 						<div v-else>{{ v.meta.tagsViewName }}</div>
@@ -23,8 +23,8 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { toRefs, reactive, computed, onMounted, defineComponent } from 'vue';
+<script setup lang="ts" name="layoutBreadcrumb">
+import { reactive, computed, onMounted } from 'vue';
 import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
 import { Local } from '/@/utils/storage';
 import other from '/@/utils/other';
@@ -32,93 +32,76 @@ import { storeToRefs } from 'pinia';
 import { useThemeConfig } from '/@/stores/themeConfig';
 import { useRoutesList } from '/@/stores/routesList';
 
-// 定义接口来定义对象的类型
-interface BreadcrumbState {
-	breadcrumbList: Array<any>;
-	routeSplit: Array<string>;
-	routeSplitFirst: string;
-	routeSplitIndex: number;
-}
+// 定义变量内容
+const stores = useRoutesList();
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const { routesList } = storeToRefs(stores);
+const route = useRoute();
+const router = useRouter();
+const state = reactive<BreadcrumbState>({
+	breadcrumbList: [],
+	routeSplit: [],
+	routeSplitFirst: '',
+	routeSplitIndex: 1,
+});
 
-export default defineComponent({
-	name: 'layoutBreadcrumb',
-	setup() {
-		const stores = useRoutesList();
-		const storesThemeConfig = useThemeConfig();
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		const { routesList } = storeToRefs(stores);
-		const route = useRoute();
-		const router = useRouter();
-		const state = reactive<BreadcrumbState>({
-			breadcrumbList: [],
-			routeSplit: [],
-			routeSplitFirst: '',
-			routeSplitIndex: 1,
-		});
-		// 动态设置经典、横向布局不显示
-		const isShowBreadcrumb = computed(() => {
-			initRouteSplit(route.path);
-			const { layout, isBreadcrumb } = themeConfig.value;
-			if (layout === 'classic' || layout === 'transverse') return false;
-			else return isBreadcrumb ? true : false;
-		});
-		// 面包屑点击时
-		const onBreadcrumbClick = (v: any) => {
-			const { redirect, path } = v;
-			if (redirect) router.push(redirect);
-			else router.push(path);
-		};
-		// 展开/收起左侧菜单点击
-		const onThemeConfigChange = () => {
-			themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
-			setLocalThemeConfig();
-		};
-		// 存储布局配置
-		const setLocalThemeConfig = () => {
-			Local.remove('themeConfig');
-			Local.set('themeConfig', themeConfig.value);
-		};
-		// 处理面包屑数据
-		const getBreadcrumbList = (arr: Array<string>) => {
-			arr.forEach((item: any) => {
-				state.routeSplit.forEach((v: any, k: number, arrs: any) => {
-					if (state.routeSplitFirst === item.path) {
-						state.routeSplitFirst += `/${arrs[state.routeSplitIndex]}`;
-						state.breadcrumbList.push(item);
-						state.routeSplitIndex++;
-						if (item.children) getBreadcrumbList(item.children);
-					}
-				});
-			});
-		};
-		// 当前路由字符串切割成数组,并删除第一项空内容
-		const initRouteSplit = (path: string) => {
-			if (!themeConfig.value.isBreadcrumb) return false;
-			state.breadcrumbList = [routesList.value[0]];
-			state.routeSplit = path.split('/');
-			state.routeSplit.shift();
-			state.routeSplitFirst = `/${state.routeSplit[0]}`;
-			state.routeSplitIndex = 1;
-			getBreadcrumbList(routesList.value);
-			if (route.name === 'home' || (route.name === 'notFound' && state.breadcrumbList.length > 1)) state.breadcrumbList.shift();
-			if (state.breadcrumbList.length > 0) state.breadcrumbList[state.breadcrumbList.length - 1].meta.tagsViewName = other.setTagsViewNameI18n(route);
-		};
-		// 页面加载时
-		onMounted(() => {
-			initRouteSplit(route.path);
-		});
-		// 路由更新时
-		onBeforeRouteUpdate((to) => {
-			initRouteSplit(to.path);
+// 动态设置经典、横向布局不显示
+const isShowBreadcrumb = computed(() => {
+	initRouteSplit(route.path);
+	const { layout, isBreadcrumb } = themeConfig.value;
+	if (layout === 'classic' || layout === 'transverse') return false;
+	else return isBreadcrumb ? true : false;
+});
+// 面包屑点击时
+const onBreadcrumbClick = (v: RouteItem) => {
+	const { redirect, path } = v;
+	if (redirect) router.push(redirect);
+	else router.push(path);
+};
+// 展开/收起左侧菜单点击
+const onThemeConfigChange = () => {
+	themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
+	setLocalThemeConfig();
+};
+// 存储布局配置
+const setLocalThemeConfig = () => {
+	Local.remove('themeConfig');
+	Local.set('themeConfig', themeConfig.value);
+};
+// 处理面包屑数据
+const getBreadcrumbList = (arr: RouteItems) => {
+	arr.forEach((item: RouteItem) => {
+		state.routeSplit.forEach((v: string, k: number, arrs: string[]) => {
+			if (state.routeSplitFirst === item.path) {
+				state.routeSplitFirst += `/${arrs[state.routeSplitIndex]}`;
+				state.breadcrumbList.push(item);
+				state.routeSplitIndex++;
+				if (item.children) getBreadcrumbList(item.children);
+			}
 		});
-		return {
-			onThemeConfigChange,
-			isShowBreadcrumb,
-			themeConfig,
-			onBreadcrumbClick,
-			...toRefs(state),
-		};
-	},
+	});
+};
+// 当前路由字符串切割成数组,并删除第一项空内容
+const initRouteSplit = (path: string) => {
+	if (!themeConfig.value.isBreadcrumb) return false;
+	state.breadcrumbList = [routesList.value[0]];
+	state.routeSplit = path.split('/');
+	state.routeSplit.shift();
+	state.routeSplitFirst = `/${state.routeSplit[0]}`;
+	state.routeSplitIndex = 1;
+	getBreadcrumbList(routesList.value);
+	if (route.name === 'home' || (route.name === 'notFound' && state.breadcrumbList.length > 1)) state.breadcrumbList.shift();
+	if (state.breadcrumbList.length > 0)
+		state.breadcrumbList[state.breadcrumbList.length - 1].meta.tagsViewName = other.setTagsViewNameI18n(<RouteToFrom>route);
+};
+// 页面加载时
+onMounted(() => {
+	initRouteSplit(route.path);
+});
+// 路由更新时
+onBeforeRouteUpdate((to) => {
+	initRouteSplit(to.path);
 });
 </script>
 

+ 9 - 17
Web/src/layout/navBars/breadcrumb/closeFull.vue

@@ -6,26 +6,18 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script setup lang="ts" name="layoutCloseFull">
 import { storeToRefs } from 'pinia';
 import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
 
-export default defineComponent({
-	name: 'layoutCloseFull',
-	setup() {
-		const stores = useTagsViewRoutes();
-		const { isTagsViewCurrenFull } = storeToRefs(stores);
-		// 关闭当前全屏
-		const onCloseFullscreen = () => {
-			stores.setCurrenFullscreen(false);
-		};
-		return {
-			isTagsViewCurrenFull,
-			onCloseFullscreen,
-		};
-	},
-});
+// 定义变量内容
+const stores = useTagsViewRoutes();
+const { isTagsViewCurrenFull } = storeToRefs(stores);
+
+// 关闭当前全屏
+const onCloseFullscreen = () => {
+	stores.setCurrenFullscreen(false);
+};
 </script>
 
 <style scoped lang="scss">

+ 79 - 92
Web/src/layout/navBars/breadcrumb/index.vue

@@ -2,110 +2,97 @@
 	<div class="layout-navbars-breadcrumb-index">
 		<Logo v-if="setIsShowLogo" />
 		<Breadcrumb />
-		<Horizontal :menuList="menuList" v-if="isLayoutTransverse" />
+		<Horizontal :menuList="state.menuList" v-if="isLayoutTransverse" />
 		<User />
 	</div>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, computed, reactive, toRefs, onMounted, onUnmounted, defineComponent } from 'vue';
+<script setup lang="ts" name="layoutBreadcrumbIndex">
+import { defineAsyncComponent, computed, reactive, onMounted, onUnmounted } from 'vue';
 import { useRoute } from 'vue-router';
 import { storeToRefs } from 'pinia';
 import { useRoutesList } from '/@/stores/routesList';
 import { useThemeConfig } from '/@/stores/themeConfig';
 import mittBus from '/@/utils/mitt';
 
-// 定义接口来定义对象的类型
-interface IndexState {
-	menuList: object[];
-}
+// 引入组件
+const Breadcrumb = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/breadcrumb.vue'));
+const User = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/user.vue'));
+const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue'));
+const Horizontal = defineAsyncComponent(() => import('/@/layout/navMenu/horizontal.vue'));
 
-export default defineComponent({
-	name: 'layoutBreadcrumbIndex',
-	components: {
-		Breadcrumb: defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/breadcrumb.vue')),
-		User: defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/user.vue')),
-		Logo: defineAsyncComponent(() => import('/@/layout/logo/index.vue')),
-		Horizontal: defineAsyncComponent(() => import('/@/layout/navMenu/horizontal.vue')),
-	},
-	setup() {
-		const stores = useRoutesList();
-		const storesThemeConfig = useThemeConfig();
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		const { routesList } = storeToRefs(stores);
-		const route = useRoute();
-		const state = reactive<IndexState>({
-			menuList: [],
-		});
-		// 设置 logo 显示/隐藏
-		const setIsShowLogo = computed(() => {
-			let { isShowLogo, layout } = themeConfig.value;
-			return (isShowLogo && layout === 'classic') || (isShowLogo && layout === 'transverse');
-		});
-		// 设置是否显示横向导航菜单
-		const isLayoutTransverse = computed(() => {
-			let { layout, isClassicSplitMenu } = themeConfig.value;
-			return layout === 'transverse' || (isClassicSplitMenu && layout === 'classic');
-		});
-		// 设置/过滤路由(非静态路由/是否显示在菜单中)
-		const setFilterRoutes = () => {
-			let { layout, isClassicSplitMenu } = themeConfig.value;
-			if (layout === 'classic' && isClassicSplitMenu) {
-				state.menuList = delClassicChildren(filterRoutesFun(routesList.value));
-				const resData = setSendClassicChildren(route.path);
-				mittBus.emit('setSendClassicChildren', resData);
-			} else {
-				state.menuList = filterRoutesFun(routesList.value);
-			}
-		};
-		// 设置了分割菜单时,删除底下 children
-		const delClassicChildren = (arr: Array<object>) => {
-			arr.map((v: any) => {
-				if (v.children) delete v.children;
-			});
-			return arr;
-		};
-		// 路由过滤递归函数
-		const filterRoutesFun = (arr: Array<string>) => {
-			return arr
-				.filter((item: any) => !item.meta.isHide)
-				.map((item: any) => {
-					item = Object.assign({}, item);
-					if (item.children) item.children = filterRoutesFun(item.children);
-					return item;
-				});
-		};
-		// 传送当前子级数据到菜单中
-		const setSendClassicChildren = (path: string) => {
-			const currentPathSplit = path.split('/');
-			let currentData: any = {};
-			filterRoutesFun(routesList.value).map((v, k) => {
-				if (v.path === `/${currentPathSplit[1]}`) {
-					v['k'] = k;
-					currentData['item'] = [{ ...v }];
-					currentData['children'] = [{ ...v }];
-					if (v.children) currentData['children'] = v.children;
-				}
-			});
-			return currentData;
-		};
-		// 页面加载时
-		onMounted(() => {
-			setFilterRoutes();
-			mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
-				setFilterRoutes();
-			});
-		});
-		// 页面卸载时
-		onUnmounted(() => {
-			mittBus.off('getBreadcrumbIndexSetFilterRoutes', () => {});
+// 定义变量内容
+const stores = useRoutesList();
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const { routesList } = storeToRefs(stores);
+const route = useRoute();
+const state = reactive({
+	menuList: [] as RouteItems,
+});
+
+// 设置 logo 显示/隐藏
+const setIsShowLogo = computed(() => {
+	let { isShowLogo, layout } = themeConfig.value;
+	return (isShowLogo && layout === 'classic') || (isShowLogo && layout === 'transverse');
+});
+// 设置是否显示横向导航菜单
+const isLayoutTransverse = computed(() => {
+	let { layout, isClassicSplitMenu } = themeConfig.value;
+	return layout === 'transverse' || (isClassicSplitMenu && layout === 'classic');
+});
+// 设置/过滤路由(非静态路由/是否显示在菜单中)
+const setFilterRoutes = () => {
+	let { layout, isClassicSplitMenu } = themeConfig.value;
+	if (layout === 'classic' && isClassicSplitMenu) {
+		state.menuList = delClassicChildren(filterRoutesFun(routesList.value));
+		const resData = setSendClassicChildren(route.path);
+		mittBus.emit('setSendClassicChildren', resData);
+	} else {
+		state.menuList = filterRoutesFun(routesList.value);
+	}
+};
+// 设置了分割菜单时,删除底下 children
+const delClassicChildren = <T extends ChilType>(arr: T[]): T[] => {
+	arr.map((v: T) => {
+		if (v.children) delete v.children;
+	});
+	return arr;
+};
+// 路由过滤递归函数
+const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
+	return arr
+		.filter((item: T) => !item.meta?.isHide)
+		.map((item: T) => {
+			item = Object.assign({}, item);
+			if (item.children) item.children = filterRoutesFun(item.children);
+			return item;
 		});
-		return {
-			setIsShowLogo,
-			isLayoutTransverse,
-			...toRefs(state),
-		};
-	},
+};
+// 传送当前子级数据到菜单中
+const setSendClassicChildren = (path: string) => {
+	const currentPathSplit = path.split('/');
+	let currentData: MittMenu = { children: [] };
+	filterRoutesFun(routesList.value).map((v: RouteItem, k: number) => {
+		if (v.path === `/${currentPathSplit[1]}`) {
+			v['k'] = k;
+			currentData['item'] = { ...v };
+			currentData['children'] = [{ ...v }];
+			if (v.children) currentData['children'] = v.children;
+		}
+	});
+	return currentData;
+};
+// 页面加载时
+onMounted(() => {
+	setFilterRoutes();
+	mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
+		setFilterRoutes();
+	});
+});
+// 页面卸载时
+onUnmounted(() => {
+	mittBus.off('getBreadcrumbIndexSetFilterRoutes', () => {});
 });
 </script>
 

+ 63 - 82
Web/src/layout/navBars/breadcrumb/search.vue

@@ -1,9 +1,9 @@
 <template>
 	<div class="layout-search-dialog">
-		<el-dialog v-model="isShowSearch" destroy-on-close :show-close="false">
+		<el-dialog v-model="state.isShowSearch" destroy-on-close :show-close="false">
 			<template #footer>
 				<el-autocomplete
-					v-model="menuQuery"
+					v-model="state.menuQuery"
 					:fetch-suggestions="menuSearch"
 					:placeholder="$t('message.user.searchPlaceholder')"
 					ref="layoutMenuAutocompleteRef"
@@ -27,93 +27,74 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { reactive, toRefs, defineComponent, ref, nextTick } from 'vue';
+<script setup lang="ts" name="layoutBreadcrumbSearch">
+import { reactive, ref, nextTick } from 'vue';
 import { useRouter } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 import { storeToRefs } from 'pinia';
 import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
 
-// 定义接口来定义对象的类型
-interface SearchState {
-	isShowSearch: boolean;
-	menuQuery: string;
-	tagsViewList: object[];
-}
-interface Restaurant {
-	path: string;
-	meta: {
-		title: string;
-	};
-}
+// 定义变量内容
+const storesTagsViewRoutes = useTagsViewRoutes();
+const { tagsViewRoutes } = storeToRefs(storesTagsViewRoutes);
+const layoutMenuAutocompleteRef = ref();
+const { t } = useI18n();
+const router = useRouter();
+const state = reactive<SearchState>({
+	isShowSearch: false,
+	menuQuery: '',
+	tagsViewList: [],
+});
 
-export default defineComponent({
-	name: 'layoutBreadcrumbSearch',
-	setup() {
-		const storesTagsViewRoutes = useTagsViewRoutes();
-		const { tagsViewRoutes } = storeToRefs(storesTagsViewRoutes);
-		const layoutMenuAutocompleteRef = ref();
-		const { t } = useI18n();
-		const router = useRouter();
-		const state = reactive<SearchState>({
-			isShowSearch: false,
-			menuQuery: '',
-			tagsViewList: [],
+// 搜索弹窗打开
+const openSearch = () => {
+	state.menuQuery = '';
+	state.isShowSearch = true;
+	initTageView();
+	nextTick(() => {
+		setTimeout(() => {
+			layoutMenuAutocompleteRef.value.focus();
 		});
-		// 搜索弹窗打开
-		const openSearch = () => {
-			state.menuQuery = '';
-			state.isShowSearch = true;
-			initTageView();
-			nextTick(() => {
-				setTimeout(() => {
-					layoutMenuAutocompleteRef.value.focus();
-				});
-			});
-		};
-		// 搜索弹窗关闭
-		const closeSearch = () => {
-			state.isShowSearch = false;
-		};
-		// 菜单搜索数据过滤
-		const menuSearch = (queryString: string, cb: Function) => {
-			let results = queryString ? state.tagsViewList.filter(createFilter(queryString)) : state.tagsViewList;
-			cb(results);
-		};
-		// 菜单搜索过滤
-		const createFilter: any = (queryString: string) => {
-			return (restaurant: Restaurant) => {
-				return (
-					restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
-					restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
-					t(restaurant.meta.title).indexOf(queryString.toLowerCase()) > -1
-				);
-			};
-		};
-		// 初始化菜单数据
-		const initTageView = () => {
-			if (state.tagsViewList.length > 0) return false;
-			tagsViewRoutes.value.map((v: any) => {
-				if (!v.meta.isHide) state.tagsViewList.push({ ...v });
-			});
-		};
-		// 当前菜单选中时
-		const onHandleSelect = (item: any) => {
-			let { path, redirect } = item;
-			if (item.meta.isLink && !item.meta.isIframe) window.open(item.meta.isLink);
-			else if (redirect) router.push(redirect);
-			else router.push(path);
-			closeSearch();
-		};
-		return {
-			layoutMenuAutocompleteRef,
-			openSearch,
-			closeSearch,
-			menuSearch,
-			onHandleSelect,
-			...toRefs(state),
-		};
-	},
+	});
+};
+// 搜索弹窗关闭
+const closeSearch = () => {
+	state.isShowSearch = false;
+};
+// 菜单搜索数据过滤
+const menuSearch = (queryString: string, cb: Function) => {
+	let results = queryString ? state.tagsViewList.filter(createFilter(queryString)) : state.tagsViewList;
+	cb(results);
+};
+// 菜单搜索过滤
+const createFilter = (queryString: string) => {
+	return (restaurant: RouteItem) => {
+		return (
+			restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
+			restaurant.meta!.title!.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
+			t(restaurant.meta!.title!).indexOf(queryString.toLowerCase()) > -1
+		);
+	};
+};
+// 初始化菜单数据
+const initTageView = () => {
+	if (state.tagsViewList.length > 0) return false;
+	tagsViewRoutes.value.map((v: RouteItem) => {
+		if (!v.meta?.isHide) state.tagsViewList.push({ ...v });
+	});
+};
+// 当前菜单选中时
+const onHandleSelect = (item: RouteItem) => {
+	let { path, redirect } = item;
+	if (item.meta?.isLink && !item.meta?.isIframe) window.open(item.meta?.isLink);
+	else if (redirect) router.push(redirect);
+	else router.push(path);
+	closeSearch();
+};
+
+// 暴露变量
+defineExpose({
+	openSearch,
 });
 </script>
 

+ 237 - 262
Web/src/layout/navBars/breadcrumb/setings.vue

@@ -1,6 +1,6 @@
 <template>
 	<div class="layout-breadcrumb-seting">
-		<el-drawer :title="$t('message.layout.configTitle')" v-model="getThemeConfig.isDrawer" direction="rtl" destroy-on-close size="300px" @close="onDrawerClose">
+		<el-drawer :title="$t('message.layout.configTitle')" v-model="getThemeConfig.isDrawer" direction="rtl" destroy-on-close size="260px" @close="onDrawerClose">
 			<el-scrollbar class="layout-breadcrumb-seting-bar">
 				<!-- 全局主题 -->
 				<el-divider content-position="left">{{ $t('message.layout.oneTitle') }}</el-divider>
@@ -170,10 +170,10 @@
 						<el-switch v-model="getThemeConfig.isCacheTagsView" size="small" @change="setLocalThemeConfig"></el-switch>
 					</div>
 				</div>
-				<div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: isMobile ? 0.5 : 1 }">
+				<div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: state.isMobile ? 0.5 : 1 }">
 					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsSortableTagsView') }}</div>
 					<div class="layout-breadcrumb-seting-bar-flex-value">
-						<el-switch v-model="getThemeConfig.isSortableTagsView" :disabled="isMobile ? true : false" size="small" @change="onSortableTagsViewChange"></el-switch>
+						<el-switch v-model="getThemeConfig.isSortableTagsView" :disabled="state.isMobile ? true : false" size="small" @change="onSortableTagsViewChange"></el-switch>
 					</div>
 				</div>
 				<div class="layout-breadcrumb-seting-bar-flex mt15">
@@ -209,7 +209,7 @@
 				<div class="layout-breadcrumb-seting-bar-flex mt14">
 					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourWartermarkText') }}</div>
 					<div class="layout-breadcrumb-seting-bar-flex-value">
-						<el-input v-model="getThemeConfig.wartermarkText" size="default" style="width: 90px" @input="onWartermarkTextInput($event)"></el-input>
+						<el-input v-model="getThemeConfig.wartermarkText" size="default" style="width: 90px" @input="onWartermarkTextInput"></el-input>
 					</div>
 				</div>
 
@@ -377,13 +377,13 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { nextTick, onUnmounted, onMounted, defineComponent, computed, reactive, toRefs } from 'vue';
+<script setup lang="ts" name="layoutBreadcrumbSeting">
+import { nextTick, onUnmounted, onMounted, computed, reactive } from 'vue';
 import { ElMessage } from 'element-plus';
 import { useI18n } from 'vue-i18n';
 import { storeToRefs } from 'pinia';
 import { useThemeConfig } from '/@/stores/themeConfig';
-import { getLightColor, getDarkColor } from '/@/utils/theme';
+import { useChangeColor } from '/@/utils/theme';
 import { verifyAndSpace } from '/@/utils/toolsValidate';
 import { Local } from '/@/utils/storage';
 import Watermark from '/@/utils/wartermark';
@@ -391,264 +391,239 @@ import commonFunction from '/@/utils/commonFunction';
 import other from '/@/utils/other';
 import mittBus from '/@/utils/mitt';
 
-export default defineComponent({
-	name: 'layoutBreadcrumbSeting',
-	setup() {
-		const { locale } = useI18n();
-		const storesThemeConfig = useThemeConfig();
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		const { copyText } = commonFunction();
-		const state = reactive({
-			isMobile: false,
-		});
-		// 获取布局配置信息
-		const getThemeConfig = computed(() => {
-			return themeConfig.value;
-		});
-		// 1、全局主题
-		const onColorPickerChange = () => {
-			if (!getThemeConfig.value.primary) return ElMessage.warning('全局主题 primary 颜色值不能为空');
-			// 颜色加深
-			document.documentElement.style.setProperty('--el-color-primary-dark-2', `${getDarkColor(getThemeConfig.value.primary, 0.1)}`);
-			document.documentElement.style.setProperty('--el-color-primary', getThemeConfig.value.primary);
-			// 颜色变浅
-			for (let i = 1; i <= 9; i++) {
-				document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(getThemeConfig.value.primary, i / 10)}`);
-			}
-			setDispatchThemeConfig();
-		};
-		// 2、菜单 / 顶栏
-		const onBgColorPickerChange = (bg: string) => {
-			document.documentElement.style.setProperty(`--next-bg-${bg}`, (<any>getThemeConfig.value)[bg]);
-			if (bg === 'menuBar') {
-				document.documentElement.style.setProperty(`--next-bg-menuBar-light-1`, <any>getLightColor(getThemeConfig.value.menuBar, 0.05));
-			}
-			onTopBarGradualChange();
-			onMenuBarGradualChange();
-			onColumnsMenuBarGradualChange();
-			setDispatchThemeConfig();
-		};
-		// 2、菜单 / 顶栏 --> 顶栏背景渐变
-		const onTopBarGradualChange = () => {
-			setGraduaFun('.layout-navbars-breadcrumb-index', getThemeConfig.value.isTopBarColorGradual, getThemeConfig.value.topBar);
-		};
-		// 2、菜单 / 顶栏 --> 菜单背景渐变
-		const onMenuBarGradualChange = () => {
-			setGraduaFun('.layout-container .el-aside', getThemeConfig.value.isMenuBarColorGradual, getThemeConfig.value.menuBar);
-		};
-		// 2、菜单 / 顶栏 --> 分栏菜单背景渐变
-		const onColumnsMenuBarGradualChange = () => {
-			setGraduaFun('.layout-container .layout-columns-aside', getThemeConfig.value.isColumnsMenuBarColorGradual, getThemeConfig.value.columnsMenuBar);
-		};
-		// 2、菜单 / 顶栏 --> 背景渐变函数
-		const setGraduaFun = (el: string, bool: boolean, color: string) => {
-			setTimeout(() => {
-				let els = document.querySelector(el);
-				if (!els) return false;
-				document.documentElement.style.setProperty('--el-menu-bg-color', document.documentElement.style.getPropertyValue('--next-bg-menuBar'));
-				if (bool) els.setAttribute('style', `background:linear-gradient(to bottom left , ${color}, ${getLightColor(color, 0.6)}) !important;`);
-				else els.setAttribute('style', ``);
-				setLocalThemeConfig();
-			}, 200);
-		};
-		// 2、分栏设置 ->
-		const onColumnsMenuHoverPreloadChange = () => {
-			mittBus.emit('setHoverPreload');
-			setLocalThemeConfig();
-		};
-		// 3、界面设置 --> 菜单水平折叠
-		const onThemeConfigChange = () => {
-			setDispatchThemeConfig();
-		};
-		// 3、界面设置 --> 固定 Header
-		const onIsFixedHeaderChange = () => {
-			getThemeConfig.value.isFixedHeaderChange = getThemeConfig.value.isFixedHeader ? false : true;
-			setLocalThemeConfig();
-		};
-		// 3、界面设置 --> 经典布局分割菜单
-		const onClassicSplitMenuChange = () => {
-			getThemeConfig.value.isBreadcrumb = false;
-			setLocalThemeConfig();
-			mittBus.emit('getBreadcrumbIndexSetFilterRoutes');
-		};
-		// 4、界面显示 --> 侧边栏 Logo
-		const onIsShowLogoChange = () => {
-			getThemeConfig.value.isShowLogoChange = getThemeConfig.value.isShowLogo ? false : true;
-			setLocalThemeConfig();
-		};
-		// 4、界面显示 --> 面包屑 Breadcrumb
-		const onIsBreadcrumbChange = () => {
-			if (getThemeConfig.value.layout === 'classic') {
-				getThemeConfig.value.isClassicSplitMenu = false;
-			}
-			setLocalThemeConfig();
-		};
-		// 4、界面显示 --> 开启 TagsView 拖拽
-		const onSortableTagsViewChange = () => {
-			mittBus.emit('openOrCloseSortable');
-			setLocalThemeConfig();
-		};
-		// 4、界面显示 --> 开启 TagsView 共用
-		const onShareTagsViewChange = () => {
-			mittBus.emit('openShareTagsView');
-			setLocalThemeConfig();
-		};
-		// 4、界面显示 --> 灰色模式/色弱模式
-		const onAddFilterChange = (attr: string) => {
-			if (attr === 'grayscale') {
-				if (getThemeConfig.value.isGrayscale) getThemeConfig.value.isInvert = false;
-			} else {
-				if (getThemeConfig.value.isInvert) getThemeConfig.value.isGrayscale = false;
-			}
-			const cssAttr = attr === 'grayscale' ? `grayscale(${getThemeConfig.value.isGrayscale ? 1 : 0})` : `invert(${getThemeConfig.value.isInvert ? '80%' : '0%'})`;
-			const appEle: any = document.body;
-			appEle.setAttribute('style', `filter: ${cssAttr}`);
-			setLocalThemeConfig();
-		};
-		// 4、界面显示 --> 深色模式
-		const onAddDarkChange = () => {
-			const body = document.documentElement as HTMLElement;
-			if (getThemeConfig.value.isIsDark) body.setAttribute('data-theme', 'dark');
-			else body.setAttribute('data-theme', '');
-		};
-		// 4、界面显示 --> 开启水印
-		const onWartermarkChange = () => {
-			getThemeConfig.value.isWartermark ? Watermark.set(getThemeConfig.value.wartermarkText) : Watermark.del();
-			setLocalThemeConfig();
-		};
-		// 4、界面显示 --> 水印文案
-		const onWartermarkTextInput = (val: any) => {
-			getThemeConfig.value.wartermarkText = verifyAndSpace(val);
-			if (getThemeConfig.value.wartermarkText === '') return false;
-			if (getThemeConfig.value.isWartermark) Watermark.set(getThemeConfig.value.wartermarkText);
-			setLocalThemeConfig();
-		};
-		// 5、布局切换
-		const onSetLayout = (layout: string) => {
-			Local.set('oldLayout', layout);
-			if (getThemeConfig.value.layout === layout) return false;
-			if (layout === 'transverse') getThemeConfig.value.isCollapse = false;
-			getThemeConfig.value.layout = layout;
+// 定义变量内容
+const { locale } = useI18n();
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const { copyText } = commonFunction();
+const { getLightColor, getDarkColor } = useChangeColor();
+const state = reactive({
+	isMobile: false,
+});
+
+// 获取布局配置信息
+const getThemeConfig = computed(() => {
+	return themeConfig.value;
+});
+// 1、全局主题
+const onColorPickerChange = () => {
+	if (!getThemeConfig.value.primary) return ElMessage.warning('全局主题 primary 颜色值不能为空');
+	// 颜色加深
+	document.documentElement.style.setProperty('--el-color-primary-dark-2', `${getDarkColor(getThemeConfig.value.primary, 0.1)}`);
+	document.documentElement.style.setProperty('--el-color-primary', getThemeConfig.value.primary);
+	// 颜色变浅
+	for (let i = 1; i <= 9; i++) {
+		document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(getThemeConfig.value.primary, i / 10)}`);
+	}
+	setDispatchThemeConfig();
+};
+// 2、菜单 / 顶栏
+const onBgColorPickerChange = (bg: string) => {
+	document.documentElement.style.setProperty(`--next-bg-${bg}`, themeConfig.value[bg]);
+	if (bg === 'menuBar') {
+		document.documentElement.style.setProperty(`--next-bg-menuBar-light-1`, getLightColor(getThemeConfig.value.menuBar, 0.05));
+	}
+	onTopBarGradualChange();
+	onMenuBarGradualChange();
+	onColumnsMenuBarGradualChange();
+	setDispatchThemeConfig();
+};
+// 2、菜单 / 顶栏 --> 顶栏背景渐变
+const onTopBarGradualChange = () => {
+	setGraduaFun('.layout-navbars-breadcrumb-index', getThemeConfig.value.isTopBarColorGradual, getThemeConfig.value.topBar);
+};
+// 2、菜单 / 顶栏 --> 菜单背景渐变
+const onMenuBarGradualChange = () => {
+	setGraduaFun('.layout-container .el-aside', getThemeConfig.value.isMenuBarColorGradual, getThemeConfig.value.menuBar);
+};
+// 2、菜单 / 顶栏 --> 分栏菜单背景渐变
+const onColumnsMenuBarGradualChange = () => {
+	setGraduaFun('.layout-container .layout-columns-aside', getThemeConfig.value.isColumnsMenuBarColorGradual, getThemeConfig.value.columnsMenuBar);
+};
+// 2、菜单 / 顶栏 --> 背景渐变函数
+const setGraduaFun = (el: string, bool: boolean, color: string) => {
+	setTimeout(() => {
+		let els = document.querySelector(el);
+		if (!els) return false;
+		document.documentElement.style.setProperty('--el-menu-bg-color', document.documentElement.style.getPropertyValue('--next-bg-menuBar'));
+		if (bool) els.setAttribute('style', `background:linear-gradient(to bottom left , ${color}, ${getLightColor(color, 0.6)}) !important;`);
+		else els.setAttribute('style', ``);
+		setLocalThemeConfig();
+	}, 200);
+};
+// 2、分栏设置 ->
+const onColumnsMenuHoverPreloadChange = () => {
+	setLocalThemeConfig();
+};
+// 3、界面设置 --> 菜单水平折叠
+const onThemeConfigChange = () => {
+	setDispatchThemeConfig();
+};
+// 3、界面设置 --> 固定 Header
+const onIsFixedHeaderChange = () => {
+	getThemeConfig.value.isFixedHeaderChange = getThemeConfig.value.isFixedHeader ? false : true;
+	setLocalThemeConfig();
+};
+// 3、界面设置 --> 经典布局分割菜单
+const onClassicSplitMenuChange = () => {
+	getThemeConfig.value.isBreadcrumb = false;
+	setLocalThemeConfig();
+	mittBus.emit('getBreadcrumbIndexSetFilterRoutes');
+};
+// 4、界面显示 --> 侧边栏 Logo
+const onIsShowLogoChange = () => {
+	getThemeConfig.value.isShowLogoChange = getThemeConfig.value.isShowLogo ? false : true;
+	setLocalThemeConfig();
+};
+// 4、界面显示 --> 面包屑 Breadcrumb
+const onIsBreadcrumbChange = () => {
+	if (getThemeConfig.value.layout === 'classic') {
+		getThemeConfig.value.isClassicSplitMenu = false;
+	}
+	setLocalThemeConfig();
+};
+// 4、界面显示 --> 开启 TagsView 拖拽
+const onSortableTagsViewChange = () => {
+	mittBus.emit('openOrCloseSortable');
+	setLocalThemeConfig();
+};
+// 4、界面显示 --> 开启 TagsView 共用
+const onShareTagsViewChange = () => {
+	mittBus.emit('openShareTagsView');
+	setLocalThemeConfig();
+};
+// 4、界面显示 --> 灰色模式/色弱模式
+const onAddFilterChange = (attr: string) => {
+	if (attr === 'grayscale') {
+		if (getThemeConfig.value.isGrayscale) getThemeConfig.value.isInvert = false;
+	} else {
+		if (getThemeConfig.value.isInvert) getThemeConfig.value.isGrayscale = false;
+	}
+	const cssAttr = attr === 'grayscale' ? `grayscale(${getThemeConfig.value.isGrayscale ? 1 : 0})` : `invert(${getThemeConfig.value.isInvert ? '80%' : '0%'})`;
+	const appEle = document.body;
+	appEle.setAttribute('style', `filter: ${cssAttr}`);
+	setLocalThemeConfig();
+};
+// 4、界面显示 --> 深色模式
+const onAddDarkChange = () => {
+	const body = document.documentElement as HTMLElement;
+	if (getThemeConfig.value.isIsDark) body.setAttribute('data-theme', 'dark');
+	else body.setAttribute('data-theme', '');
+};
+// 4、界面显示 --> 开启水印
+const onWartermarkChange = () => {
+	getThemeConfig.value.isWartermark ? Watermark.set(getThemeConfig.value.wartermarkText) : Watermark.del();
+	setLocalThemeConfig();
+};
+// 4、界面显示 --> 水印文案
+const onWartermarkTextInput = (val: string) => {
+	getThemeConfig.value.wartermarkText = verifyAndSpace(val);
+	if (getThemeConfig.value.wartermarkText === '') return false;
+	if (getThemeConfig.value.isWartermark) Watermark.set(getThemeConfig.value.wartermarkText);
+	setLocalThemeConfig();
+};
+// 5、布局切换
+const onSetLayout = (layout: string) => {
+	Local.set('oldLayout', layout);
+	if (getThemeConfig.value.layout === layout) return false;
+	if (layout === 'transverse') getThemeConfig.value.isCollapse = false;
+	getThemeConfig.value.layout = layout;
+	getThemeConfig.value.isDrawer = false;
+	initLayoutChangeFun();
+};
+// 设置布局切换函数
+const initLayoutChangeFun = () => {
+	onBgColorPickerChange('menuBar');
+	onBgColorPickerChange('menuBarColor');
+	onBgColorPickerChange('topBar');
+	onBgColorPickerChange('topBarColor');
+	onBgColorPickerChange('columnsMenuBar');
+	onBgColorPickerChange('columnsMenuBarColor');
+};
+// 关闭弹窗时,初始化变量。变量用于处理 layoutScrollbarRef.value.update() 更新滚动条高度
+const onDrawerClose = () => {
+	getThemeConfig.value.isFixedHeaderChange = false;
+	getThemeConfig.value.isShowLogoChange = false;
+	getThemeConfig.value.isDrawer = false;
+	setLocalThemeConfig();
+};
+// 布局配置弹窗打开
+const openDrawer = () => {
+	getThemeConfig.value.isDrawer = true;
+};
+// 触发 store 布局配置更新
+const setDispatchThemeConfig = () => {
+	setLocalThemeConfig();
+	setLocalThemeConfigStyle();
+};
+// 存储布局配置
+const setLocalThemeConfig = () => {
+	Local.remove('themeConfig');
+	Local.set('themeConfig', getThemeConfig.value);
+};
+// 存储布局配置全局主题样式(html根标签)
+const setLocalThemeConfigStyle = () => {
+	Local.set('themeConfigStyle', document.documentElement.style.cssText);
+};
+// 一键复制配置
+const onCopyConfigClick = () => {
+	let copyThemeConfig = Local.get('themeConfig');
+	copyThemeConfig.isDrawer = false;
+	copyText(JSON.stringify(copyThemeConfig)).then(() => {
+		getThemeConfig.value.isDrawer = false;
+	});
+};
+// 一键恢复默认
+const onResetConfigClick = () => {
+	Local.clear();
+	window.location.reload();
+};
+// 初始化菜单样式等
+const initSetStyle = () => {
+	// 2、菜单 / 顶栏 --> 顶栏背景渐变
+	onTopBarGradualChange();
+	// 2、菜单 / 顶栏 --> 菜单背景渐变
+	onMenuBarGradualChange();
+	// 2、菜单 / 顶栏 --> 分栏菜单背景渐变
+	onColumnsMenuBarGradualChange();
+};
+onMounted(() => {
+	nextTick(() => {
+		// 判断当前布局是否不相同,不相同则初始化当前布局的样式,防止监听窗口大小改变时,布局配置logo、菜单背景等部分布局失效问题
+		if (!Local.get('frequency')) initLayoutChangeFun();
+		Local.set('frequency', 1);
+		// 监听窗口大小改变,非默认布局,设置成默认布局(适配移动端)
+		mittBus.on('layoutMobileResize', (res: LayoutMobileResize) => {
+			getThemeConfig.value.layout = res.layout;
 			getThemeConfig.value.isDrawer = false;
 			initLayoutChangeFun();
-		};
-		// 设置布局切换函数
-		const initLayoutChangeFun = () => {
-			onBgColorPickerChange('menuBar');
-			onBgColorPickerChange('menuBarColor');
-			onBgColorPickerChange('topBar');
-			onBgColorPickerChange('topBarColor');
-			onBgColorPickerChange('columnsMenuBar');
-			onBgColorPickerChange('columnsMenuBarColor');
-		};
-		// 关闭弹窗时,初始化变量。变量用于处理 layoutScrollbarRef.value.update() 更新滚动条高度
-		const onDrawerClose = () => {
-			getThemeConfig.value.isFixedHeaderChange = false;
-			getThemeConfig.value.isShowLogoChange = false;
-			getThemeConfig.value.isDrawer = false;
-			setLocalThemeConfig();
-		};
-		// 布局配置弹窗打开
-		const openDrawer = () => {
-			getThemeConfig.value.isDrawer = true;
-		};
-		// 触发 store 布局配置更新
-		const setDispatchThemeConfig = () => {
-			setLocalThemeConfig();
-			setLocalThemeConfigStyle();
-		};
-		// 存储布局配置
-		const setLocalThemeConfig = () => {
-			Local.remove('themeConfig');
-			Local.set('themeConfig', getThemeConfig.value);
-		};
-		// 存储布局配置全局主题样式(html根标签)
-		const setLocalThemeConfigStyle = () => {
-			Local.set('themeConfigStyle', document.documentElement.style.cssText);
-		};
-		// 一键复制配置
-		const onCopyConfigClick = () => {
-			let copyThemeConfig = Local.get('themeConfig');
-			copyThemeConfig.isDrawer = false;
-			copyText(JSON.stringify(copyThemeConfig)).then(() => {
-				getThemeConfig.value.isDrawer = false;
-			});
-		};
-		// 一键恢复默认
-		const onResetConfigClick = () => {
-			Local.clear();
-			window.location.reload();
-		};
-		// 初始化菜单样式等
-		const initSetStyle = () => {
-			// 2、菜单 / 顶栏 --> 顶栏背景渐变
-			onTopBarGradualChange();
-			// 2、菜单 / 顶栏 --> 菜单背景渐变
-			onMenuBarGradualChange();
-			// 2、菜单 / 顶栏 --> 分栏菜单背景渐变
-			onColumnsMenuBarGradualChange();
-		};
-		onMounted(() => {
-			nextTick(() => {
-				// 判断当前布局是否不相同,不相同则初始化当前布局的样式,防止监听窗口大小改变时,布局配置logo、菜单背景等部分布局失效问题
-				if (!Local.get('frequency')) initLayoutChangeFun();
-				Local.set('frequency', 1);
-				// 监听窗口大小改变,非默认布局,设置成默认布局(适配移动端)
-				mittBus.on('layoutMobileResize', (res: any) => {
-					getThemeConfig.value.layout = res.layout;
-					getThemeConfig.value.isDrawer = false;
-					initLayoutChangeFun();
-					state.isMobile = other.isMobile();
-				});
-				setTimeout(() => {
-					// 默认样式
-					onColorPickerChange();
-					// 灰色模式
-					if (getThemeConfig.value.isGrayscale) onAddFilterChange('grayscale');
-					// 色弱模式
-					if (getThemeConfig.value.isInvert) onAddFilterChange('invert');
-					// 深色模式
-					if (getThemeConfig.value.isIsDark) onAddDarkChange();
-					// 开启水印
-					onWartermarkChange();
-					// 语言国际化
-					if (Local.get('themeConfig')) locale.value = Local.get('themeConfig').globalI18n;
-					// 初始化菜单样式等
-					initSetStyle();
-				}, 100);
-			});
+			state.isMobile = other.isMobile();
 		});
-		onUnmounted(() => {
-			mittBus.off('layoutMobileResize', () => {});
-		});
-		return {
-			openDrawer,
-			onColorPickerChange,
-			onBgColorPickerChange,
-			onTopBarGradualChange,
-			onMenuBarGradualChange,
-			onColumnsMenuBarGradualChange,
-			onColumnsMenuHoverPreloadChange,
-			onThemeConfigChange,
-			onIsFixedHeaderChange,
-			onIsShowLogoChange,
-			getThemeConfig,
-			onDrawerClose,
-			onAddFilterChange,
-			onAddDarkChange,
-			onWartermarkChange,
-			onWartermarkTextInput,
-			onSetLayout,
-			setLocalThemeConfig,
-			onClassicSplitMenuChange,
-			onIsBreadcrumbChange,
-			onSortableTagsViewChange,
-			onShareTagsViewChange,
-			onCopyConfigClick,
-			onResetConfigClick,
-			...toRefs(state),
-		};
-	},
+		setTimeout(() => {
+			// 默认样式
+			onColorPickerChange();
+			// 灰色模式
+			if (getThemeConfig.value.isGrayscale) onAddFilterChange('grayscale');
+			// 色弱模式
+			if (getThemeConfig.value.isInvert) onAddFilterChange('invert');
+			// 深色模式
+			if (getThemeConfig.value.isIsDark) onAddDarkChange();
+			// 开启水印
+			onWartermarkChange();
+			// 语言国际化
+			if (Local.get('themeConfig')) locale.value = Local.get('themeConfig').globalI18n;
+			// 初始化菜单样式等
+			initSetStyle();
+		}, 100);
+	});
+});
+onUnmounted(() => {
+	mittBus.off('layoutMobileResize', () => {});
+});
+
+// 暴露变量
+defineExpose({
+	openDrawer,
 });
 </script>
 

+ 140 - 157
Web/src/layout/navBars/breadcrumb/user.vue

@@ -6,21 +6,21 @@
 			</div>
 			<template #dropdown>
 				<el-dropdown-menu>
-					<el-dropdown-item command="large" :disabled="disabledSize === 'large'">{{ $t('message.user.dropdownLarge') }}</el-dropdown-item>
-					<el-dropdown-item command="default" :disabled="disabledSize === 'default'">{{ $t('message.user.dropdownDefault') }}</el-dropdown-item>
-					<el-dropdown-item command="small" :disabled="disabledSize === 'small'">{{ $t('message.user.dropdownSmall') }}</el-dropdown-item>
+					<el-dropdown-item command="large" :disabled="state.disabledSize === 'large'">{{ $t('message.user.dropdownLarge') }}</el-dropdown-item>
+					<el-dropdown-item command="default" :disabled="state.disabledSize === 'default'">{{ $t('message.user.dropdownDefault') }}</el-dropdown-item>
+					<el-dropdown-item command="small" :disabled="state.disabledSize === 'small'">{{ $t('message.user.dropdownSmall') }}</el-dropdown-item>
 				</el-dropdown-menu>
 			</template>
 		</el-dropdown>
 		<el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onLanguageChange">
 			<div class="layout-navbars-breadcrumb-user-icon">
-				<i class="iconfont" :class="disabledI18n === 'en' ? 'icon-fuhao-yingwen' : 'icon-fuhao-zhongwen'" :title="$t('message.user.title1')"></i>
+				<i class="iconfont" :class="state.disabledI18n === 'en' ? 'icon-fuhao-yingwen' : 'icon-fuhao-zhongwen'" :title="$t('message.user.title1')"></i>
 			</div>
 			<template #dropdown>
 				<el-dropdown-menu>
-					<el-dropdown-item command="zh-cn" :disabled="disabledI18n === 'zh-cn'">简体中文</el-dropdown-item>
-					<el-dropdown-item command="en" :disabled="disabledI18n === 'en'">English</el-dropdown-item>
-					<el-dropdown-item command="zh-tw" :disabled="disabledI18n === 'zh-tw'">繁體中文</el-dropdown-item>
+					<el-dropdown-item command="zh-cn" :disabled="state.disabledI18n === 'zh-cn'">简体中文</el-dropdown-item>
+					<el-dropdown-item command="en" :disabled="state.disabledI18n === 'en'">English</el-dropdown-item>
+					<el-dropdown-item command="zh-tw" :disabled="state.disabledI18n === 'zh-tw'">繁體中文</el-dropdown-item>
 				</el-dropdown-menu>
 			</template>
 		</el-dropdown>
@@ -41,11 +41,11 @@
 						</el-icon>
 					</el-badge>
 				</template>
-				<UserNews :noticeList="noticeList" />
+				<UserNews :noticeList="state.noticeList" />
 			</el-popover>
 		</div>
 		<div class="layout-navbars-breadcrumb-user-icon" @click="onScreenfullClick">
-			<i class="iconfont" :title="isScreenfull ? $t('message.user.title6') : $t('message.user.title5')" :class="!isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'"></i>
+			<i class="iconfont" :title="state.isScreenfull ? $t('message.user.title6') : $t('message.user.title5')" :class="!state.isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'"></i>
 		</div>
 		<div class="layout-navbars-breadcrumb-user-icon mr10" @click="onOnlineUserClick">
 			<el-icon title="在线用户">
@@ -73,8 +73,8 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { ref, computed, reactive, toRefs, onMounted, defineComponent } from 'vue';
+<script setup lang="ts" name="layoutBreadcrumbUser">
+import { defineAsyncComponent, ref, computed, reactive, onMounted } from 'vue';
 import { useRouter } from 'vue-router';
 import { ElMessageBox, ElMessage, ElNotification } from 'element-plus';
 import screenfull from 'screenfull';
@@ -85,162 +85,145 @@ import { useThemeConfig } from '/@/stores/themeConfig';
 import other from '/@/utils/other';
 import mittBus from '/@/utils/mitt';
 import { Local } from '/@/utils/storage';
-import UserNews from '/@/layout/navBars/breadcrumb/userNews.vue';
-import Search from '/@/layout/navBars/breadcrumb/search.vue';
 
-import OnlineUser from '/@/views/system/onlineUser/index.vue';
 import { clearAccessTokens, getAPI } from '/@/utils/axios-utils';
 import { SysAuthApi, SysNoticeApi } from '/@/api-services/api';
 
 import { signalR } from '/@/views/system/onlineUser/signalR';
 
-export default defineComponent({
-	name: 'layoutBreadcrumbUser',
-	components: { UserNews, Search, OnlineUser },
-	setup() {
-		const { locale, t } = useI18n();
-		const router = useRouter();
-		const stores = useUserInfo();
-		const storesThemeConfig = useThemeConfig();
-		const { userInfos } = storeToRefs(stores);
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		const searchRef = ref();
-		const onlineUserRef = ref();
-		const state = reactive({
-			isScreenfull: false,
-			disabledI18n: 'zh-cn',
-			disabledSize: 'large',
-			noticeList: [] as any, // 站内信列表
-		});
-		// 设置分割样式
-		const layoutUserFlexNum = computed(() => {
-			let num: string | number = '';
-			const { layout, isClassicSplitMenu } = themeConfig.value;
-			const layoutArr: string[] = ['defaults', 'columns'];
-			if (layoutArr.includes(layout) || (layout === 'classic' && !isClassicSplitMenu)) num = '1';
-			else num = '';
-			return num;
-		});
-		// 全屏点击时
-		const onScreenfullClick = () => {
-			if (!screenfull.isEnabled) {
-				ElMessage.warning('暂不不支持全屏');
-				return false;
-			}
-			screenfull.toggle();
-			screenfull.on('change', () => {
-				if (screenfull.isFullscreen) state.isScreenfull = true;
-				else state.isScreenfull = false;
-			});
-		};
-		// 布局配置 icon 点击时
-		const onLayoutSetingClick = () => {
-			mittBus.emit('openSetingsDrawer');
-		};
-		// 下拉菜单点击时
-		const onHandleCommandClick = (path: string) => {
-			if (path === 'logOut') {
-				ElMessageBox({
-					closeOnClickModal: false,
-					closeOnPressEscape: false,
-					title: t('message.user.logOutTitle'),
-					message: t('message.user.logOutMessage'),
-					type: 'warning',
-					showCancelButton: true,
-					confirmButtonText: t('message.user.logOutConfirm'),
-					cancelButtonText: t('message.user.logOutCancel'),
-					buttonSize: 'default',
-					beforeClose: async (action, instance, done) => {
-						if (action === 'confirm') {
-							instance.confirmButtonLoading = true;
-							instance.confirmButtonText = t('message.user.logOutExit');
-							await getAPI(SysAuthApi).logoutPost();
-							instance.confirmButtonLoading = false;
-							done();
-						} else {
-							done();
-						}
-					},
-				})
-					.then(async () => {
-						clearAccessTokens();
-					})
-					.catch(() => {});
-			} else if (path === 'wareHouse') {
-				window.open('https://gitee.com/zuohuaijun/Admin.NET');
-			} else {
-				router.push(path);
-			}
-		};
-		// 菜单搜索点击
-		const onSearchClick = () => {
-			searchRef.value.openSearch();
-		};
-		// 在线用户列表
-		const onOnlineUserClick = () => {
-			onlineUserRef.value.openDrawer();
-		};
-		// 组件大小改变
-		const onComponentSizeChange = (size: string) => {
-			Local.remove('themeConfig');
-			themeConfig.value.globalComponentSize = size;
-			Local.set('themeConfig', themeConfig.value);
-			initI18nOrSize('globalComponentSize', 'disabledSize');
-			window.location.reload();
-		};
-		// 语言切换
-		const onLanguageChange = (lang: string) => {
-			Local.remove('themeConfig');
-			themeConfig.value.globalI18n = lang;
-			Local.set('themeConfig', themeConfig.value);
-			locale.value = lang;
-			other.useTitle();
-			initI18nOrSize('globalI18n', 'disabledI18n');
-		};
-		// 初始化组件大小/i18n
-		const initI18nOrSize = (value: string, attr: string) => {
-			(<any>state)[attr] = Local.get('themeConfig')[value];
-		};
-		// 页面加载时
-		onMounted(async () => {
-			if (Local.get('themeConfig')) {
-				initI18nOrSize('globalComponentSize', 'disabledSize');
-				initI18nOrSize('globalI18n', 'disabledI18n');
-			}
+// 引入组件
+const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/userNews.vue'));
+const Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue'));
+const OnlineUser = defineAsyncComponent(() => import('/@/views/system/onlineUser/index.vue'));
 
-			// 加载未读的站内信
-			var res = await getAPI(SysNoticeApi).sysNoticeUnReadListGet();
-			state.noticeList = res.data.result ?? [];
+// 定义变量内容
+const { locale, t } = useI18n();
+const router = useRouter();
+const stores = useUserInfo();
+const storesThemeConfig = useThemeConfig();
+const { userInfos } = storeToRefs(stores);
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const searchRef = ref();
+const onlineUserRef = ref();
+const state = reactive({
+	isScreenfull: false,
+	disabledI18n: 'zh-cn',
+	disabledSize: 'large',
+	noticeList: [] as any, // 站内信列表
+});
+// 设置分割样式
+const layoutUserFlexNum = computed(() => {
+	let num: string | number = '';
+	const { layout, isClassicSplitMenu } = themeConfig.value;
+	const layoutArr: string[] = ['defaults', 'columns'];
+	if (layoutArr.includes(layout) || (layout === 'classic' && !isClassicSplitMenu)) num = '1';
+	else num = '';
+	return num;
+});
+// 全屏点击时
+const onScreenfullClick = () => {
+	if (!screenfull.isEnabled) {
+		ElMessage.warning('暂不不支持全屏');
+		return false;
+	}
+	screenfull.toggle();
+	screenfull.on('change', () => {
+		if (screenfull.isFullscreen) state.isScreenfull = true;
+		else state.isScreenfull = false;
+	});
+};
+// 布局配置 icon 点击时
+const onLayoutSetingClick = () => {
+	mittBus.emit('openSetingsDrawer');
+};
+// 下拉菜单点击时
+const onHandleCommandClick = (path: string) => {
+	if (path === 'logOut') {
+		ElMessageBox({
+			closeOnClickModal: false,
+			closeOnPressEscape: false,
+			title: t('message.user.logOutTitle'),
+			message: t('message.user.logOutMessage'),
+			type: 'warning',
+			showCancelButton: true,
+			confirmButtonText: t('message.user.logOutConfirm'),
+			cancelButtonText: t('message.user.logOutCancel'),
+			buttonSize: 'default',
+			beforeClose: async (action, instance, done) => {
+				if (action === 'confirm') {
+					instance.confirmButtonLoading = true;
+					instance.confirmButtonText = t('message.user.logOutExit');
+					await getAPI(SysAuthApi).logoutPost();
+					instance.confirmButtonLoading = false;
+					done();
+				} else {
+					done();
+				}
+			},
+		})
+			.then(async () => {
+				clearAccessTokens();
+			})
+			.catch(() => {});
+	} else if (path === 'wareHouse') {
+		window.open('https://gitee.com/zuohuaijun/Admin.NET');
+	} else {
+		router.push(path);
+	}
+};
+// 菜单搜索点击
+const onSearchClick = () => {
+	searchRef.value.openSearch();
+};
+// 在线用户列表
+const onOnlineUserClick = () => {
+	onlineUserRef.value.openDrawer();
+};
+// 组件大小改变
+const onComponentSizeChange = (size: string) => {
+	Local.remove('themeConfig');
+	themeConfig.value.globalComponentSize = size;
+	Local.set('themeConfig', themeConfig.value);
+	initI18nOrSize('globalComponentSize', 'disabledSize');
+	window.location.reload();
+};
+// 语言切换
+const onLanguageChange = (lang: string) => {
+	Local.remove('themeConfig');
+	themeConfig.value.globalI18n = lang;
+	Local.set('themeConfig', themeConfig.value);
+	locale.value = lang;
+	other.useTitle();
+	initI18nOrSize('globalI18n', 'disabledI18n');
+};
+// 初始化组件大小/i18n
+const initI18nOrSize = (value: string, attr: string) => {
+	(<any>state)[attr] = Local.get('themeConfig')[value];
+};
+// 页面加载时
+onMounted(async () => {
+	if (Local.get('themeConfig')) {
+		initI18nOrSize('globalComponentSize', 'disabledSize');
+		initI18nOrSize('globalI18n', 'disabledI18n');
+	}
 
-			// 接收站内信
-			signalR.on('PublicNotice', reciveNotice);
-		});
-		const reciveNotice = (msg: any) => {
-			state.noticeList.unshift(msg);
+	// 加载未读的站内信
+	var res = await getAPI(SysNoticeApi).sysNoticeUnReadListGet();
+	state.noticeList = res.data.result ?? [];
 
-			ElNotification({
-				title: '提示',
-				message: '您有一条新消息...',
-				type: 'info',
-				position: 'bottom-right',
-			});
-		};
-		return {
-			userInfos,
-			onLayoutSetingClick,
-			onHandleCommandClick,
-			onScreenfullClick,
-			onSearchClick,
-			onOnlineUserClick,
-			onComponentSizeChange,
-			onLanguageChange,
-			searchRef,
-			onlineUserRef,
-			layoutUserFlexNum,
-			...toRefs(state),
-		};
-	},
+	// 接收站内信
+	signalR.on('PublicNotice', reciveNotice);
 });
+const reciveNotice = (msg: any) => {
+	state.noticeList.unshift(msg);
+
+	ElNotification({
+		title: '提示',
+		message: '您有一条新消息...',
+		type: 'info',
+		position: 'bottom-right',
+	});
+};
 </script>
 
 <style scoped lang="scss">

+ 2 - 2
Web/src/layout/navBars/breadcrumb/userNews.vue

@@ -40,13 +40,13 @@
 	</div>
 </template>
 
-<script lang="ts" setup>
+<script setup lang="ts" name="layoutBreadcrumbUserNews">
 import { reactive } from 'vue';
-import { SysNoticeApi } from '/@/api-services/api';
 import router from '/@/router';
 import commonFunction from '/@/utils/commonFunction';
 
 import { getAPI } from '/@/utils/axios-utils';
+import { SysNoticeApi } from '/@/api-services/api';
 
 defineProps({
 	noticeList: Array as any,

+ 14 - 20
Web/src/layout/navBars/index.vue

@@ -5,29 +5,23 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, computed, defineComponent } from 'vue';
+<script setup lang="ts" name="layoutNavBars">
+import { defineAsyncComponent, computed } from 'vue';
 import { storeToRefs } from 'pinia';
 import { useThemeConfig } from '/@/stores/themeConfig';
 
-export default defineComponent({
-	name: 'layoutNavBars',
-	components: {
-		BreadcrumbIndex: defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/index.vue')),
-		TagsView: defineAsyncComponent(() => import('/@/layout/navBars/tagsView/tagsView.vue')),
-	},
-	setup() {
-		const storesThemeConfig = useThemeConfig();
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		// 是否显示 tagsView
-		const setShowTagsView = computed(() => {
-			let { layout, isTagsview } = themeConfig.value;
-			return layout !== 'classic' && isTagsview;
-		});
-		return {
-			setShowTagsView,
-		};
-	},
+// 引入组件
+const BreadcrumbIndex = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/index.vue'));
+const TagsView = defineAsyncComponent(() => import('/@/layout/navBars/tagsView/tagsView.vue'));
+
+// 定义变量内容
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+
+// 是否显示 tagsView
+const setShowTagsView = computed(() => {
+	let { layout, isTagsview } = themeConfig.value;
+	return layout !== 'classic' && isTagsview;
 });
 </script>
 

+ 88 - 95
Web/src/layout/navBars/tagsView/contextmenu.vue

@@ -7,118 +7,111 @@
 			data-popper-placement="bottom"
 			:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
 			:key="Math.random()"
-			v-show="isShow"
+			v-show="state.isShow"
 		>
 			<ul class="el-dropdown-menu">
-				<template v-for="(v, k) in dropdownList">
-					<li
-						class="el-dropdown-menu__item"
-						aria-disabled="false"
-						tabindex="-1"
-						:key="k"
-						v-if="!v.affix"
-						@click="onCurrentContextmenuClick(v.contextMenuClickId)"
-					>
+				<template v-for="(v, k) in state.dropdownList">
+					<li class="el-dropdown-menu__item" aria-disabled="false" tabindex="-1" :key="k" v-if="!v.affix" @click="onCurrentContextmenuClick(v.contextMenuClickId)">
 						<SvgIcon :name="v.icon" />
 						<span>{{ $t(v.txt) }}</span>
 					</li>
 				</template>
 			</ul>
-			<div class="el-popper__arrow" :style="{ left: `${arrowLeft}px` }"></div>
+			<div class="el-popper__arrow" :style="{ left: `${state.arrowLeft}px` }"></div>
 		</div>
 	</transition>
 </template>
 
-<script lang="ts">
-import { computed, defineComponent, reactive, toRefs, onMounted, onUnmounted, watch } from 'vue';
+<script setup lang="ts" name="layoutTagsViewContextmenu">
+import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
 
-export default defineComponent({
-	name: 'layoutTagsViewContextmenu',
-	props: {
-		dropdown: {
-			type: Object,
-			default: () => {
-				return {
-					x: 0,
-					y: 0,
-				};
-			},
+// 定义父组件传过来的值
+const props = defineProps({
+	dropdown: {
+		type: Object,
+		default: () => {
+			return {
+				x: 0,
+				y: 0,
+			};
 		},
 	},
-	setup(props, { emit }) {
-		const state = reactive({
-			isShow: false,
-			dropdownList: [
-				{ contextMenuClickId: 0, txt: 'message.tagsView.refresh', affix: false, icon: 'ele-RefreshRight' },
-				{ contextMenuClickId: 1, txt: 'message.tagsView.close', affix: false, icon: 'ele-Close' },
-				{ contextMenuClickId: 2, txt: 'message.tagsView.closeOther', affix: false, icon: 'ele-CircleClose' },
-				{ contextMenuClickId: 3, txt: 'message.tagsView.closeAll', affix: false, icon: 'ele-FolderDelete' },
-				{
-					contextMenuClickId: 4,
-					txt: 'message.tagsView.fullscreen',
-					affix: false,
-					icon: 'iconfont icon-fullscreen',
-				},
-			],
-			item: {},
-			arrowLeft: 10,
-		});
-		// 父级传过来的坐标 x,y 值
-		const dropdowns = computed(() => {
-			// 117 为 `Dropdown 下拉菜单` 的宽度
-			if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
-				return {
-					x: document.documentElement.clientWidth - 117 - 5,
-					y: props.dropdown.y,
-				};
-			} else {
-				return props.dropdown;
-			}
-		});
-		// 当前项菜单点击
-		const onCurrentContextmenuClick = (contextMenuClickId: number) => {
-			emit('currentContextmenuClick', Object.assign({}, { contextMenuClickId }, state.item));
-		};
-		// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
-		const openContextmenu = (item: any) => {
-			state.item = item;
-			item.meta.isAffix ? (state.dropdownList[1].affix = true) : (state.dropdownList[1].affix = false);
-			closeContextmenu();
-			setTimeout(() => {
-				state.isShow = true;
-			}, 10);
-		};
-		// 关闭右键菜单
-		const closeContextmenu = () => {
-			state.isShow = false;
-		};
-		// 监听页面监听进行右键菜单的关闭
-		onMounted(() => {
-			document.body.addEventListener('click', closeContextmenu);
-		});
-		// 页面卸载时,移除右键菜单监听事件
-		onUnmounted(() => {
-			document.body.removeEventListener('click', closeContextmenu);
-		});
-		// 监听下拉菜单位置
-		watch(
-			() => props.dropdown,
-			({ x }) => {
-				if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
-				else state.arrowLeft = 10;
-			},
-			{
-				deep: true,
-			}
-		);
+});
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['currentContextmenuClick']);
+
+// 定义变量内容
+const state = reactive({
+	isShow: false,
+	dropdownList: [
+		{ contextMenuClickId: 0, txt: 'message.tagsView.refresh', affix: false, icon: 'ele-RefreshRight' },
+		{ contextMenuClickId: 1, txt: 'message.tagsView.close', affix: false, icon: 'ele-Close' },
+		{ contextMenuClickId: 2, txt: 'message.tagsView.closeOther', affix: false, icon: 'ele-CircleClose' },
+		{ contextMenuClickId: 3, txt: 'message.tagsView.closeAll', affix: false, icon: 'ele-FolderDelete' },
+		{
+			contextMenuClickId: 4,
+			txt: 'message.tagsView.fullscreen',
+			affix: false,
+			icon: 'iconfont icon-fullscreen',
+		},
+	],
+	item: {},
+	arrowLeft: 10,
+});
+
+// 父级传过来的坐标 x,y 值
+const dropdowns = computed(() => {
+	// 117 为 `Dropdown 下拉菜单` 的宽度
+	if (props.dropdown.x + 117 > document.documentElement.clientWidth) {
 		return {
-			dropdowns,
-			openContextmenu,
-			closeContextmenu,
-			onCurrentContextmenuClick,
-			...toRefs(state),
+			x: document.documentElement.clientWidth - 117 - 5,
+			y: props.dropdown.y,
 		};
+	} else {
+		return props.dropdown;
+	}
+});
+// 当前项菜单点击
+const onCurrentContextmenuClick = (contextMenuClickId: number) => {
+	emit('currentContextmenuClick', Object.assign({}, { contextMenuClickId }, state.item));
+};
+// 打开右键菜单:判断是否固定,固定则不显示关闭按钮
+const openContextmenu = (item: RouteItem) => {
+	state.item = item;
+	item.meta?.isAffix ? (state.dropdownList[1].affix = true) : (state.dropdownList[1].affix = false);
+	closeContextmenu();
+	setTimeout(() => {
+		state.isShow = true;
+	}, 10);
+};
+// 关闭右键菜单
+const closeContextmenu = () => {
+	state.isShow = false;
+};
+// 监听页面监听进行右键菜单的关闭
+onMounted(() => {
+	document.body.addEventListener('click', closeContextmenu);
+});
+// 页面卸载时,移除右键菜单监听事件
+onUnmounted(() => {
+	document.body.removeEventListener('click', closeContextmenu);
+});
+// 监听下拉菜单位置
+watch(
+	() => props.dropdown,
+	({ x }) => {
+		if (x + 117 > document.documentElement.clientWidth) state.arrowLeft = 117 - (document.documentElement.clientWidth - x);
+		else state.arrowLeft = 10;
 	},
+	{
+		deep: true,
+	}
+);
+
+// 暴露变量
+defineExpose({
+	openContextmenu,
 });
 </script>
 

+ 507 - 570
Web/src/layout/navBars/tagsView/tagsView.vue

@@ -3,7 +3,7 @@
 		<el-scrollbar ref="scrollbarRef" @wheel.prevent="onHandleScroll">
 			<ul class="layout-navbars-tagsview-ul" :class="setTagsStyle" ref="tagsUlRef">
 				<li
-					v-for="(v, k) in tagsViewList"
+					v-for="(v, k) in state.tagsViewList"
 					:key="k"
 					class="layout-navbars-tagsview-ul-li"
 					:data-url="v.url"
@@ -21,11 +21,7 @@
 					<SvgIcon :name="v.meta.icon" v-if="!isActive(v) && getThemeConfig.isTagsviewIcon" class="pr5" />
 					<span>{{ setTagsViewNameI18n(v) }}</span>
 					<template v-if="isActive(v)">
-						<SvgIcon
-							name="ele-RefreshRight"
-							class="ml5 layout-navbars-tagsview-ul-li-refresh"
-							@click.stop="refreshCurrentTagsView($route.fullPath)"
-						/>
+						<SvgIcon name="ele-RefreshRight" class="ml5 layout-navbars-tagsview-ul-li-refresh" @click.stop="refreshCurrentTagsView($route.fullPath)" />
 						<SvgIcon
 							name="ele-Close"
 							class="layout-navbars-tagsview-ul-li-icon layout-icon-active"
@@ -42,25 +38,12 @@
 				</li>
 			</ul>
 		</el-scrollbar>
-		<Contextmenu :dropdown="dropdown" ref="contextmenuRef" @currentContextmenuClick="onCurrentContextmenuClick" />
+		<Contextmenu :dropdown="state.dropdown" ref="contextmenuRef" @currentContextmenuClick="onCurrentContextmenuClick" />
 	</div>
 </template>
 
-<script lang="ts">
-import {
-	defineAsyncComponent,
-	toRefs,
-	reactive,
-	onMounted,
-	computed,
-	ref,
-	nextTick,
-	onBeforeUpdate,
-	onBeforeMount,
-	onUnmounted,
-	watch,
-	defineComponent,
-} from 'vue';
+<script setup lang="ts" name="layoutTagsView">
+import { defineAsyncComponent, reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, onBeforeMount, onUnmounted, watch } from 'vue';
 import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
 import Sortable from 'sortablejs';
 import { ElMessage } from 'element-plus';
@@ -74,572 +57,526 @@ import { isObjectValueEqual } from '/@/utils/arrayOperation';
 import other from '/@/utils/other';
 import mittBus from '/@/utils/mitt';
 
-// 定义接口来定义对象的类型
-interface TagsViewState {
-	routeActive: string;
-	routePath: string | unknown;
-	dropdown: {
-		x: string | number;
-		y: string | number;
-	};
-	sortable: any;
-	tagsRefsIndex: number;
-	tagsViewList: any[];
-	tagsViewRoutesList: any[];
-}
-interface RouteParams {
-	path: string;
-	url: string;
-	query: object;
-	params: object;
-}
-interface CurrentContextmenu {
-	meta: {
-		isDynamic: boolean;
-	};
-	params: any;
-	query: any;
-	path: string;
-	contextMenuClickId: string | number;
-	commonUrl: string;
-}
+// 引入组件
+const Contextmenu = defineAsyncComponent(() => import('/@/layout/navBars/tagsView/contextmenu.vue'));
 
-export default defineComponent({
-	name: 'layoutTagsView',
-	components: {
-		Contextmenu: defineAsyncComponent(() => import('/@/layout/navBars/tagsView/contextmenu.vue')),
-	},
-	setup() {
-		const tagsRefs = ref<any[]>([]);
-		const scrollbarRef = ref();
-		const contextmenuRef = ref();
-		const tagsUlRef = ref();
-		const stores = useTagsViewRoutes();
-		const storesThemeConfig = useThemeConfig();
-		const storesTagsViewRoutes = useTagsViewRoutes();
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		const { tagsViewRoutes } = storeToRefs(storesTagsViewRoutes);
-		const storesKeepALiveNames = useKeepALiveNames();
-		const route = useRoute();
-		const router = useRouter();
-		const state = reactive<TagsViewState>({
-			routeActive: '',
-			routePath: route.path,
-			dropdown: { x: '', y: '' },
-			sortable: '',
-			tagsRefsIndex: 0,
-			tagsViewList: [],
-			tagsViewRoutesList: [],
-		});
-		// 动态设置 tagsView 风格样式
-		const setTagsStyle = computed(() => {
-			return themeConfig.value.tagsStyle;
-		});
-		// 获取布局配置信息
-		const getThemeConfig = computed(() => {
-			return themeConfig.value;
-		});
-		// 设置 自定义 tagsView 名称、 自定义 tagsView 名称国际化
-		const setTagsViewNameI18n = computed(() => {
-			return (v: any) => {
-				return other.setTagsViewNameI18n(v);
-			};
-		});
-		// 设置 tagsView 高亮
-		const isActive = (v: RouteParams) => {
-			if (getThemeConfig.value.isShareTagsView) {
-				return v.path === state.routePath;
-			} else {
-				if ((v.query && Object.keys(v.query).length) || (v.params && Object.keys(v.params).length)) {
-					// 普通传参
-					return v.url ? v.url === state.routeActive : v.path === state.routeActive;
-				} else {
-					// 通过 name 传参,params 取值,刷新页面参数消失
-					// https://gitee.com/lyt-top/vue-next-admin/issues/I51RS9
-					return v.path === state.routePath;
-				}
-			}
-		};
-		// 存储 tagsViewList 到浏览器临时缓存中,页面刷新时,保留记录
-		const addBrowserSetSession = (tagsViewList: Array<object>) => {
-			Session.set('tagsViewList', tagsViewList);
-		};
-		// 获取 vuex 中的 tagsViewRoutes 列表
-		const getTagsViewRoutes = async () => {
-			state.routeActive = await setTagsViewHighlight(route);
-			state.routePath = (await route.meta.isDynamic) ? route.meta.isDynamicPath : route.path;
-			state.tagsViewList = [];
-			state.tagsViewRoutesList = tagsViewRoutes.value;
-			initTagsView();
-		};
-		// vuex 中获取路由信息:如果是设置了固定的(isAffix),进行初始化显示
-		const initTagsView = async () => {
-			if (Session.get('tagsViewList') && getThemeConfig.value.isCacheTagsView) {
-				state.tagsViewList = await Session.get('tagsViewList');
-			} else {
-				await state.tagsViewRoutesList.map((v: any) => {
-					if (v.meta.isAffix && !v.meta.isHide) {
-						v.url = setTagsViewHighlight(v);
-						state.tagsViewList.push({ ...v });
-						storesKeepALiveNames.addCachedView(v);
-					}
-				});
-				await addTagsView(route.path, route);
-			}
-			// 初始化当前元素(li)的下标
-			getTagsRefsIndex(getThemeConfig.value.isShareTagsView ? state.routePath : state.routeActive);
-		};
-		// 处理可开启多标签详情,单标签详情(动态路由(xxx/:id/:name"),普通路由处理)
-		const solveAddTagsView = async (path: string, to?: any) => {
-			let isDynamicPath = to.meta.isDynamic ? to.meta.isDynamicPath : path;
-			let current = state.tagsViewList.filter(
-				(v: any) =>
-					v.path === isDynamicPath &&
-					isObjectValueEqual(
-						to.meta.isDynamic ? (v.params ? v.params : null) : v.query ? v.query : null,
-						to.meta.isDynamic ? (to?.params ? to?.params : null) : to?.query ? to?.query : null
-					)
-			);
-			if (current.length <= 0) {
-				// 防止:Avoid app logic that relies on enumerating keys on a component instance. The keys will be empty in production mode to avoid performance overhead.
-				let findItem = state.tagsViewRoutesList.find((v: any) => v.path === isDynamicPath);
-				if (!findItem) return false;
-				if (findItem.meta.isAffix) return false;
-				if (findItem.meta.isLink && !findItem.meta.isIframe) return false;
-				to.meta.isDynamic ? (findItem.params = to.params) : (findItem.query = to.query);
-				findItem.url = setTagsViewHighlight(findItem);
-				state.tagsViewList.push({ ...findItem });
-				await storesKeepALiveNames.addCachedView(findItem);
-				addBrowserSetSession(state.tagsViewList);
+// 定义变量内容
+const tagsRefs = ref<RefType>([]);
+const scrollbarRef = ref();
+const contextmenuRef = ref();
+const tagsUlRef = ref();
+const stores = useTagsViewRoutes();
+const storesThemeConfig = useThemeConfig();
+const storesTagsViewRoutes = useTagsViewRoutes();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const { tagsViewRoutes } = storeToRefs(storesTagsViewRoutes);
+const storesKeepALiveNames = useKeepALiveNames();
+const route = useRoute();
+const router = useRouter();
+const state = reactive<TagsViewState>({
+	routeActive: '',
+	routePath: route.path,
+	dropdown: { x: '', y: '' },
+	sortable: '',
+	tagsRefsIndex: 0,
+	tagsViewList: [],
+	tagsViewRoutesList: [],
+});
+
+// 动态设置 tagsView 风格样式
+const setTagsStyle = computed(() => {
+	return themeConfig.value.tagsStyle;
+});
+// 获取布局配置信息
+const getThemeConfig = computed(() => {
+	return themeConfig.value;
+});
+// 设置 自定义 tagsView 名称、 自定义 tagsView 名称国际化
+const setTagsViewNameI18n = computed(() => {
+	return (v: RouteItem) => {
+		return other.setTagsViewNameI18n(v);
+	};
+});
+// 设置 tagsView 高亮
+const isActive = (v: RouteItem) => {
+	if (getThemeConfig.value.isShareTagsView) {
+		return v.path === state.routePath;
+	} else {
+		if ((v.query && Object.keys(v.query).length) || (v.params && Object.keys(v.params).length)) {
+			// 普通传参
+			return v.url ? v.url === state.routeActive : v.path === state.routeActive;
+		} else {
+			// 通过 name 传参,params 取值,刷新页面参数消失
+			// https://gitee.com/lyt-top/vue-next-admin/issues/I51RS9
+			return v.path === state.routePath;
+		}
+	}
+};
+// 存储 tagsViewList 到浏览器临时缓存中,页面刷新时,保留记录
+const addBrowserSetSession = (tagsViewList: Array<object>) => {
+	Session.set('tagsViewList', tagsViewList);
+};
+// 获取 pinia 中的 tagsViewRoutes 列表
+const getTagsViewRoutes = async () => {
+	state.routeActive = await setTagsViewHighlight(route);
+	state.routePath = (await route.meta.isDynamic) ? route.meta.isDynamicPath : route.path;
+	state.tagsViewList = [];
+	state.tagsViewRoutesList = tagsViewRoutes.value;
+	initTagsView();
+};
+// pinia 中获取路由信息:如果是设置了固定的(isAffix),进行初始化显示
+const initTagsView = async () => {
+	if (Session.get('tagsViewList') && getThemeConfig.value.isCacheTagsView) {
+		state.tagsViewList = await Session.get('tagsViewList');
+	} else {
+		await state.tagsViewRoutesList.map((v: RouteItem) => {
+			if (v.meta?.isAffix && !v.meta.isHide) {
+				v.url = setTagsViewHighlight(v);
+				state.tagsViewList.push({ ...v });
+				storesKeepALiveNames.addCachedView(v);
 			}
-		};
-		// 处理单标签时,第二次的值未覆盖第一次的 tagsViewList 值(Session Storage)
-		const singleAddTagsView = (path: string, to?: any) => {
-			let isDynamicPath = to.meta.isDynamic ? to.meta.isDynamicPath : path;
-			state.tagsViewList.forEach((v) => {
-				if (
-					v.path === isDynamicPath &&
-					!isObjectValueEqual(
-						to.meta.isDynamic ? (v.params ? v.params : null) : v.query ? v.query : null,
-						to.meta.isDynamic ? (to?.params ? to?.params : null) : to?.query ? to?.query : null
-					)
-				) {
-					to.meta.isDynamic ? (v.params = to.params) : (v.query = to.query);
-					v.url = setTagsViewHighlight(v);
-					addBrowserSetSession(state.tagsViewList);
-				}
-			});
-		};
-		// 1、添加 tagsView:未设置隐藏(isHide)也添加到在 tagsView 中(可开启多标签详情,单标签详情)
-		const addTagsView = (path: string, to?: any) => {
-			// 防止拿取不到路由信息
-			nextTick(async () => {
-				// 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
-				let item: any = '';
-				if (to && to.meta.isDynamic) {
-					// 动态路由(xxx/:id/:name"):参数不同,开启多个 tagsview
-					if (!getThemeConfig.value.isShareTagsView) await solveAddTagsView(path, to);
-					else await singleAddTagsView(path, to);
-					if (state.tagsViewList.some((v: any) => v.path === to.meta.isDynamicPath)) {
-						// 防止首次进入界面时(登录进入) tagsViewList 不存浏览器中
-						addBrowserSetSession(state.tagsViewList);
-						return false;
-					}
-					item = state.tagsViewRoutesList.find((v: any) => v.path === to.meta.isDynamicPath);
-				} else {
-					// 普通路由:参数不同,开启多个 tagsview
-					if (!getThemeConfig.value.isShareTagsView) await solveAddTagsView(path, to);
-					else await singleAddTagsView(path, to);
-					if (state.tagsViewList.some((v: any) => v.path === path)) {
-						// 防止首次进入界面时(登录进入) tagsViewList 不存浏览器中
-						addBrowserSetSession(state.tagsViewList);
-						return false;
-					}
-					item = state.tagsViewRoutesList.find((v: any) => v.path === path);
-				}
-				if (!item) return false;
-				if (item.meta.isLink && !item.meta.isIframe) return false;
-				if (to && to.meta.isDynamic) item.params = to?.params ? to?.params : route.params;
-				else item.query = to?.query ? to?.query : route.query;
-				item.url = setTagsViewHighlight(item);
-				await storesKeepALiveNames.addCachedView(item);
-				await state.tagsViewList.push({ ...item });
-				await addBrowserSetSession(state.tagsViewList);
-			});
-		};
-		// 2、刷新当前 tagsView:
-		const refreshCurrentTagsView = async (fullPath: string) => {
-			const decodeURIPath = decodeURI(fullPath);
-			let item: any = {};
-			state.tagsViewList.forEach((v: any) => {
-				v.transUrl = transUrlParams(v);
-				if (v.transUrl) {
-					if (v.transUrl === transUrlParams(v)) item = v;
-				} else {
-					if (v.path === decodeURIPath) item = v;
-				}
-			});
-			if (!item) return false;
-			await storesKeepALiveNames.delCachedView(item);
-			mittBus.emit('onTagsViewRefreshRouterView', fullPath);
-			if (item.meta.isKeepAlive) storesKeepALiveNames.addCachedView(item);
-		};
-		// 3、关闭当前 tagsView:如果是设置了固定的(isAffix),不可以关闭
-		const closeCurrentTagsView = (path: string) => {
-			state.tagsViewList.map((v: any, k: number, arr: any) => {
-				if (!v.meta.isAffix) {
-					if (getThemeConfig.value.isShareTagsView ? v.path === path : v.url === path) {
-						storesKeepALiveNames.delCachedView(v);
-						state.tagsViewList.splice(k, 1);
-						setTimeout(() => {
-							if (state.tagsViewList.length === k && getThemeConfig.value.isShareTagsView ? state.routePath === path : state.routeActive === path) {
-								// 最后一个且高亮时
-								if (arr[arr.length - 1].meta.isDynamic) {
-									// 动态路由(xxx/:id/:name")
-									if (k !== arr.length) router.push({ name: arr[k].name, params: arr[k].params });
-									else router.push({ name: arr[arr.length - 1].name, params: arr[arr.length - 1].params });
-								} else {
-									// 普通路由
-									if (k !== arr.length) router.push({ path: arr[k].path, query: arr[k].query });
-									else router.push({ path: arr[arr.length - 1].path, query: arr[arr.length - 1].query });
-								}
-							} else {
-								// 非最后一个且高亮时,跳转到下一个
-								if (state.tagsViewList.length !== k && getThemeConfig.value.isShareTagsView ? state.routePath === path : state.routeActive === path) {
-									if (arr[k].meta.isDynamic) {
-										// 动态路由(xxx/:id/:name")
-										router.push({ name: arr[k].name, params: arr[k].params });
-									} else {
-										// 普通路由
-										router.push({ path: arr[k].path, query: arr[k].query });
-									}
-								}
-							}
-						}, 0);
-					}
-				}
-			});
+		});
+		await addTagsView(route.path, <RouteToFrom>route);
+	}
+	// 初始化当前元素(li)的下标
+	getTagsRefsIndex(getThemeConfig.value.isShareTagsView ? state.routePath : state.routeActive);
+};
+// 处理可开启多标签详情,单标签详情(动态路由(xxx/:id/:name"),普通路由处理)
+const solveAddTagsView = async (path: string, to?: RouteToFrom) => {
+	let isDynamicPath = to?.meta?.isDynamic ? to.meta.isDynamicPath : path;
+	let current = state.tagsViewList.filter(
+		(v: RouteItem) =>
+			v.path === isDynamicPath &&
+			isObjectValueEqual(to?.meta?.isDynamic ? (v.params ? v.params : null) : v.query ? v.query : null, to?.meta?.isDynamic ? (to?.params ? to?.params : null) : to?.query ? to?.query : null)
+	);
+	if (current.length <= 0) {
+		// 防止:Avoid app logic that relies on enumerating keys on a component instance. The keys will be empty in production mode to avoid performance overhead.
+		let findItem = state.tagsViewRoutesList.find((v: RouteItem) => v.path === isDynamicPath);
+		if (!findItem) return false;
+		if (findItem.meta.isAffix) return false;
+		if (findItem.meta.isLink && !findItem.meta.isIframe) return false;
+		to?.meta?.isDynamic ? (findItem.params = to.params) : (findItem.query = to?.query);
+		findItem.url = setTagsViewHighlight(findItem);
+		state.tagsViewList.push({ ...findItem });
+		await storesKeepALiveNames.addCachedView(findItem);
+		addBrowserSetSession(state.tagsViewList);
+	}
+};
+// 处理单标签时,第二次的值未覆盖第一次的 tagsViewList 值(Session Storage)
+const singleAddTagsView = (path: string, to?: RouteToFrom) => {
+	let isDynamicPath = to?.meta?.isDynamic ? to.meta.isDynamicPath : path;
+	state.tagsViewList.forEach((v) => {
+		if (
+			v.path === isDynamicPath &&
+			!isObjectValueEqual(to?.meta?.isDynamic ? (v.params ? v.params : null) : v.query ? v.query : null, to?.meta?.isDynamic ? (to?.params ? to?.params : null) : to?.query ? to?.query : null)
+		) {
+			to?.meta?.isDynamic ? (v.params = to.params) : (v.query = to?.query);
+			v.url = setTagsViewHighlight(v);
 			addBrowserSetSession(state.tagsViewList);
-		};
-		// 4、关闭其它 tagsView:如果是设置了固定的(isAffix),不进行关闭
-		const closeOtherTagsView = (path: string) => {
-			if (Session.get('tagsViewList')) {
-				state.tagsViewList = [];
-				Session.get('tagsViewList').map((v: any) => {
-					if (v.meta.isAffix && !v.meta.isHide) {
-						v.url = setTagsViewHighlight(v);
-						storesKeepALiveNames.delOthersCachedViews(v);
-						state.tagsViewList.push({ ...v });
-					}
-				});
-				addTagsView(path, route);
+		}
+	});
+};
+// 1、添加 tagsView:未设置隐藏(isHide)也添加到在 tagsView 中(可开启多标签详情,单标签详情)
+const addTagsView = (path: string, to?: RouteToFrom) => {
+	// 防止拿取不到路由信息
+	nextTick(async () => {
+		// 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
+		let item: RouteItem;
+		if (to?.meta?.isDynamic) {
+			// 动态路由(xxx/:id/:name"):参数不同,开启多个 tagsview
+			if (!getThemeConfig.value.isShareTagsView) await solveAddTagsView(path, to);
+			else await singleAddTagsView(path, to);
+			if (state.tagsViewList.some((v: RouteItem) => v.path === to?.meta?.isDynamicPath)) {
+				// 防止首次进入界面时(登录进入) tagsViewList 不存浏览器中
 				addBrowserSetSession(state.tagsViewList);
+				return false;
 			}
-		};
-		// 5、关闭全部 tagsView:如果是设置了固定的(isAffix),不进行关闭
-		const closeAllTagsView = () => {
-			if (Session.get('tagsViewList')) {
-				storesKeepALiveNames.delAllCachedViews();
-				state.tagsViewList = [];
-				Session.get('tagsViewList').map((v: any) => {
-					if (v.meta.isAffix && !v.meta.isHide) {
-						v.url = setTagsViewHighlight(v);
-						state.tagsViewList.push({ ...v });
-						router.push({ path: state.tagsViewList[state.tagsViewList.length - 1].path });
-					}
-				});
+			item = state.tagsViewRoutesList.find((v: RouteItem) => v.path === to?.meta?.isDynamicPath);
+		} else {
+			// 普通路由:参数不同,开启多个 tagsview
+			if (!getThemeConfig.value.isShareTagsView) await solveAddTagsView(path, to);
+			else await singleAddTagsView(path, to);
+			if (state.tagsViewList.some((v: RouteItem) => v.path === path)) {
+				// 防止首次进入界面时(登录进入) tagsViewList 不存浏览器中
 				addBrowserSetSession(state.tagsViewList);
+				return false;
 			}
-		};
-		// 6、开启当前页面全屏
-		const openCurrenFullscreen = async (path: string) => {
-			const item = state.tagsViewList.find((v: any) => (getThemeConfig.value.isShareTagsView ? v.path === path : v.url === path));
-			if (item.meta.isDynamic) await router.push({ name: item.name, params: item.params });
-			else await router.push({ name: item.name, query: item.query });
-			stores.setCurrenFullscreen(true);
-		};
-		// 当前项右键菜单点击,拿 `当前点击的路由路径` 对比 `tagsView 路由数组`,取当前点击项的详细路由信息
-		// 防止 tagsView 非当前页演示时,操作异常
-		// https://gitee.com/lyt-top/vue-next-admin/issues/I61VS9
-		const getCurrentRouteItem = (item: any) => {
-			let resItem: any = {};
-			state.tagsViewList.forEach((v: any) => {
-				v.transUrl = transUrlParams(v);
-				if (v.transUrl) {
-					if (v.meta.isDynamic) {
-						// 路由带参数:动态路由(xxx/:id/:name")isDynamic
-						if (v.transUrl === transUrlParams(v) && v.transUrl === decodeURI(item.path)) resItem = v;
+			item = state.tagsViewRoutesList.find((v: RouteItem) => v.path === path);
+		}
+		if (!item) return false;
+		if (item?.meta?.isLink && !item.meta.isIframe) return false;
+		if (to?.meta?.isDynamic) item.params = to?.params ? to?.params : route.params;
+		else item.query = to?.query ? to?.query : route.query;
+		item.url = setTagsViewHighlight(item);
+		await storesKeepALiveNames.addCachedView(item);
+		await state.tagsViewList.push({ ...item });
+		await addBrowserSetSession(state.tagsViewList);
+	});
+};
+// 2、刷新当前 tagsView:
+const refreshCurrentTagsView = async (fullPath: string) => {
+	const decodeURIPath = decodeURI(fullPath);
+	let item: RouteToFrom = {};
+	state.tagsViewList.forEach((v: RouteItem) => {
+		v.transUrl = transUrlParams(v);
+		if (v.transUrl) {
+			if (v.transUrl === transUrlParams(v)) item = v;
+		} else {
+			if (v.path === decodeURIPath) item = v;
+		}
+	});
+	if (!item) return false;
+	await storesKeepALiveNames.delCachedView(item);
+	mittBus.emit('onTagsViewRefreshRouterView', fullPath);
+	if (item.meta?.isKeepAlive) storesKeepALiveNames.addCachedView(item);
+};
+// 3、关闭当前 tagsView:如果是设置了固定的(isAffix),不可以关闭
+const closeCurrentTagsView = (path: string) => {
+	state.tagsViewList.map((v: RouteItem, k: number, arr: RouteItems) => {
+		if (!v.meta?.isAffix) {
+			if (getThemeConfig.value.isShareTagsView ? v.path === path : v.url === path) {
+				storesKeepALiveNames.delCachedView(v);
+				state.tagsViewList.splice(k, 1);
+				setTimeout(() => {
+					if (state.tagsViewList.length === k && getThemeConfig.value.isShareTagsView ? state.routePath === path : state.routeActive === path) {
+						// 最后一个且高亮时
+						if (arr[arr.length - 1].meta.isDynamic) {
+							// 动态路由(xxx/:id/:name")
+							if (k !== arr.length) router.push({ name: arr[k].name, params: arr[k].params });
+							else router.push({ name: arr[arr.length - 1].name, params: arr[arr.length - 1].params });
+						} else {
+							// 普通路由
+							if (k !== arr.length) router.push({ path: arr[k].path, query: arr[k].query });
+							else router.push({ path: arr[arr.length - 1].path, query: arr[arr.length - 1].query });
+						}
 					} else {
-						// 路由带参数:普通路由,因为有 tagsView 共用/不共用
-						if (v.transUrl === transUrlParams(v) && v.transUrl === item.commonUrl) resItem = v;
+						// 非最后一个且高亮时,跳转到下一个
+						if (state.tagsViewList.length !== k && getThemeConfig.value.isShareTagsView ? state.routePath === path : state.routeActive === path) {
+							if (arr[k].meta.isDynamic) {
+								// 动态路由(xxx/:id/:name")
+								router.push({ name: arr[k].name, params: arr[k].params });
+							} else {
+								// 普通路由
+								router.push({ path: arr[k].path, query: arr[k].query });
+							}
+						}
 					}
-				} else {
-					// 路由不带参数
-					if (v.path === decodeURI(item.path)) resItem = v;
-				}
-			});
-			if (!resItem) return null;
-			else return resItem;
-		};
-		// 当前项右键菜单点击
-		const onCurrentContextmenuClick = async (item: CurrentContextmenu) => {
-			item.commonUrl = transUrlParams(item);
-			if (!getCurrentRouteItem(item)) return ElMessage({ type: 'warning', message: '请正确输入路径及完整参数(query、params)' });
-			const { path, name, params, query, meta, url } = getCurrentRouteItem(item);
-			switch (item.contextMenuClickId) {
-				case 0:
-					// 刷新当前
-					if (meta.isDynamic) await router.push({ name, params });
-					else await router.push({ path, query });
-					refreshCurrentTagsView(route.fullPath);
-					break;
-				case 1:
-					// 关闭当前
-					closeCurrentTagsView(getThemeConfig.value.isShareTagsView ? path : url);
-					break;
-				case 2:
-					// 关闭其它
-					if (meta.isDynamic) await router.push({ name, params });
-					else await router.push({ path, query });
-					closeOtherTagsView(path);
-					break;
-				case 3:
-					// 关闭全部
-					closeAllTagsView();
-					break;
-				case 4:
-					// 开启当前页面全屏
-					openCurrenFullscreen(getThemeConfig.value.isShareTagsView ? path : url);
-					break;
+				}, 0);
 			}
-		};
-		// 右键点击时:传 x,y 坐标值到子组件中(props)
-		const onContextmenu = (v: any, e: any) => {
-			const { clientX, clientY } = e;
-			state.dropdown.x = clientX;
-			state.dropdown.y = clientY;
-			contextmenuRef.value.openContextmenu(v);
-		};
-		// 鼠标按下时,判断是鼠标中键就关闭当前 tasgview
-		const onMousedownMenu = (v: any, e: any) => {
-			if (!v.meta.isAffix && e.which === 2) {
-				const item = Object.assign({}, { contextMenuClickId: 1, ...v });
-				onCurrentContextmenuClick(item);
+		}
+	});
+	addBrowserSetSession(state.tagsViewList);
+};
+// 4、关闭其它 tagsView:如果是设置了固定的(isAffix),不进行关闭
+const closeOtherTagsView = (path: string) => {
+	if (Session.get('tagsViewList')) {
+		state.tagsViewList = [];
+		Session.get('tagsViewList').map((v: RouteItem) => {
+			if (v.meta?.isAffix && !v.meta.isHide) {
+				v.url = setTagsViewHighlight(v);
+				storesKeepALiveNames.delOthersCachedViews(v);
+				state.tagsViewList.push({ ...v });
 			}
-		};
-		// 当前的 tagsView 项点击时
-		const onTagsClick = (v: any, k: number) => {
-			state.tagsRefsIndex = k;
-			router.push(v);
-		};
-		// 处理 url,地址栏链接有参数时,tagsview 右键菜单刷新功能失效问题
-		// https://gitee.com/lyt-top/vue-next-admin/issues/I5K3YO
-		const transUrlParams = (v: any) => {
-			let params = v.query && Object.keys(v.query).length > 0 ? v.query : v.params;
-			if (!params) return '';
-			let path = '';
-			for (let [key, value] of Object.entries(params)) {
-				if (v.meta.isDynamic) path += `/${value}`;
-				else path += `&${key}=${value}`;
+		});
+		addTagsView(path, <RouteToFrom>route);
+		addBrowserSetSession(state.tagsViewList);
+	}
+};
+// 5、关闭全部 tagsView:如果是设置了固定的(isAffix),不进行关闭
+const closeAllTagsView = () => {
+	if (Session.get('tagsViewList')) {
+		storesKeepALiveNames.delAllCachedViews();
+		state.tagsViewList = [];
+		Session.get('tagsViewList').map((v: RouteItem) => {
+			if (v.meta?.isAffix && !v.meta.isHide) {
+				v.url = setTagsViewHighlight(v);
+				state.tagsViewList.push({ ...v });
+				router.push({ path: state.tagsViewList[state.tagsViewList.length - 1].path });
 			}
-			// 判断是否是动态路由(xxx/:id/:name")isDynamic
-			return v.meta.isDynamic ? `${v.path.split(':')[0]}${path.replace(/^\//, '')}` : `${v.path}${path.replace(/^&/, '?')}`;
-		};
-		// 处理 tagsView 高亮(多标签详情时使用,单标签详情未使用)
-		const setTagsViewHighlight = (v: any) => {
-			let params = v.query && Object.keys(v.query).length > 0 ? v.query : v.params;
-			if (!params || Object.keys(params).length <= 0) return v.path;
-			let path = '';
-			for (let i in params) {
-				path += params[i];
+		});
+		addBrowserSetSession(state.tagsViewList);
+	}
+};
+// 6、开启当前页面全屏
+const openCurrenFullscreen = async (path: string) => {
+	const item = state.tagsViewList.find((v: RouteItem) => (getThemeConfig.value.isShareTagsView ? v.path === path : v.url === path));
+	if (item.meta.isDynamic) await router.push({ name: item.name, params: item.params });
+	else await router.push({ name: item.name, query: item.query });
+	stores.setCurrenFullscreen(true);
+};
+// 当前项右键菜单点击,拿 `当前点击的路由路径` 对比 `tagsView 路由数组`,取当前点击项的详细路由信息
+// 防止 tagsView 非当前页演示时,操作异常
+// https://gitee.com/lyt-top/vue-next-admin/issues/I61VS9
+const getCurrentRouteItem = (item: RouteItem): any => {
+	let resItem: RouteToFrom = {};
+	state.tagsViewList.forEach((v: RouteItem) => {
+		v.transUrl = transUrlParams(v);
+		if (v.transUrl) {
+			// 动态路由、普通路由带参数
+			if (v.transUrl === transUrlParams(v) && v.transUrl === item.commonUrl) resItem = v;
+		} else {
+			// 路由不带参数
+			if (v.path === decodeURI(item.path)) resItem = v;
+		}
+	});
+	if (!resItem) return null;
+	else return resItem;
+};
+// 当前项右键菜单点击
+const onCurrentContextmenuClick = async (item: RouteItem) => {
+	item.commonUrl = transUrlParams(item);
+	if (!getCurrentRouteItem(item)) return ElMessage({ type: 'warning', message: '请正确输入路径及完整参数(query、params)' });
+	const { path, name, params, query, meta, url } = getCurrentRouteItem(item);
+	switch (item.contextMenuClickId) {
+		case 0:
+			// 刷新当前
+			if (meta.isDynamic) await router.push({ name, params });
+			else await router.push({ path, query });
+			refreshCurrentTagsView(route.fullPath);
+			break;
+		case 1:
+			// 关闭当前
+			closeCurrentTagsView(getThemeConfig.value.isShareTagsView ? path : url);
+			break;
+		case 2:
+			// 关闭其它
+			if (meta.isDynamic) await router.push({ name, params });
+			else await router.push({ path, query });
+			closeOtherTagsView(path);
+			break;
+		case 3:
+			// 关闭全部
+			closeAllTagsView();
+			break;
+		case 4:
+			// 开启当前页面全屏
+			openCurrenFullscreen(getThemeConfig.value.isShareTagsView ? path : url);
+			break;
+	}
+};
+// 右键点击时:传 x,y 坐标值到子组件中(props)
+const onContextmenu = (v: RouteItem, e: MouseEvent) => {
+	const { clientX, clientY } = e;
+	state.dropdown.x = clientX;
+	state.dropdown.y = clientY;
+	contextmenuRef.value.openContextmenu(v);
+};
+// 鼠标按下时,判断是鼠标中键就关闭当前 tasgview
+const onMousedownMenu = (v: RouteItem, e: MouseEvent) => {
+	if (!v.meta?.isAffix && e.button === 1) {
+		const item = Object.assign({}, { contextMenuClickId: 1, ...v });
+		onCurrentContextmenuClick(item);
+	}
+};
+// 当前的 tagsView 项点击时
+const onTagsClick = (v: RouteItem, k: number) => {
+	state.tagsRefsIndex = k;
+	router.push(v);
+};
+// 处理 url,地址栏链接有参数时,tagsview 右键菜单刷新功能失效问题,感谢 @ZzZz-RIPPER、@dejavuuuuu
+// https://gitee.com/lyt-top/vue-next-admin/issues/I5K3YO
+// https://gitee.com/lyt-top/vue-next-admin/issues/I61VS9
+const transUrlParams = (v: RouteItem) => {
+	let params = v.query && Object.keys(v.query).length > 0 ? v.query : v.params;
+	if (!params) return '';
+	let path = '';
+	for (let [key, value] of Object.entries(params)) {
+		if (v.meta?.isDynamic) path += `/${value}`;
+		else path += `&${key}=${value}`;
+	}
+	// 判断是否是动态路由(xxx/:id/:name")isDynamic
+	if (v.meta?.isDynamic) {
+		/**
+		 *
+		 * isFnClick 用于判断是通过方法调用,还是直接右键菜单点击(此处只针对动态路由)
+		 * 原因:
+		 * 1、右键菜单点击时,路由的 path 还是原始定义的路由格式,如:/params/dynamic/details/:t/:id/:tagsViewName
+		 * 2、通过事件调用时,路由的 path 不是原始定义的路由格式,如:/params/dynamic/details/vue-next-admin/111/我是动态路由测试tagsViewName(非国际化)
+		 *
+		 * 所以右侧菜单点击时,需要处理路径拼接 v.path.split(':')[0],得到路径 + 参数的完整路径
+		 */
+		return v.isFnClick ? decodeURI(v.path) : `${v.path.split(':')[0]}${path.replace(/^\//, '')}`;
+	} else {
+		return `${v.path}${path.replace(/^&/, '?')}`;
+	}
+};
+// 处理 tagsView 高亮(多标签详情时使用,单标签详情未使用)
+const setTagsViewHighlight = (v: RouteToFrom) => {
+	let params = v.query && Object.keys(v.query).length > 0 ? v.query : v.params;
+	if (!params || Object.keys(params).length <= 0) return v.path;
+	let path = '';
+	for (let i in params) {
+		path += params[i];
+	}
+	// 判断是否是动态路由(xxx/:id/:name")
+	return `${v.meta?.isDynamic ? v.meta.isDynamicPath : v.path}-${path}`;
+};
+// 鼠标滚轮滚动
+const onHandleScroll = (e: WheelEventType) => {
+	scrollbarRef.value.$refs.wrapRef.scrollLeft += e.wheelDelta / 4;
+};
+// tagsView 横向滚动
+const tagsViewmoveToCurrentTag = () => {
+	nextTick(() => {
+		if (tagsRefs.value.length <= 0) return false;
+		// 当前 li 元素
+		let liDom = tagsRefs.value[state.tagsRefsIndex];
+		// 当前 li 元素下标
+		let liIndex = state.tagsRefsIndex;
+		// 当前 ul 下 li 元素总长度
+		let liLength = tagsRefs.value.length;
+		// 最前 li
+		let liFirst = tagsRefs.value[0];
+		// 最后 li
+		let liLast = tagsRefs.value[tagsRefs.value.length - 1];
+		// 当前滚动条的值
+		let scrollRefs = scrollbarRef.value.$refs.wrapRef;
+		// 当前滚动条滚动宽度
+		let scrollS = scrollRefs.scrollWidth;
+		// 当前滚动条偏移宽度
+		let offsetW = scrollRefs.offsetWidth;
+		// 当前滚动条偏移距离
+		let scrollL = scrollRefs.scrollLeft;
+		// 上一个 tags li dom
+		let liPrevTag = tagsRefs.value[state.tagsRefsIndex - 1];
+		// 下一个 tags li dom
+		let liNextTag = tagsRefs.value[state.tagsRefsIndex + 1];
+		// 上一个 tags li dom 的偏移距离
+		let beforePrevL = 0;
+		// 下一个 tags li dom 的偏移距离
+		let afterNextL = 0;
+		if (liDom === liFirst) {
+			// 头部
+			scrollRefs.scrollLeft = 0;
+		} else if (liDom === liLast) {
+			// 尾部
+			scrollRefs.scrollLeft = scrollS - offsetW;
+		} else {
+			// 非头/尾部
+			if (liIndex === 0) beforePrevL = liFirst.offsetLeft - 5;
+			else beforePrevL = liPrevTag?.offsetLeft - 5;
+			if (liIndex === liLength) afterNextL = liLast.offsetLeft + liLast.offsetWidth + 5;
+			else afterNextL = liNextTag.offsetLeft + liNextTag.offsetWidth + 5;
+			if (afterNextL > scrollL + offsetW) {
+				scrollRefs.scrollLeft = afterNextL - offsetW;
+			} else if (beforePrevL < scrollL) {
+				scrollRefs.scrollLeft = beforePrevL;
 			}
-			// 判断是否是动态路由(xxx/:id/:name")
-			return `${v.meta.isDynamic ? v.meta.isDynamicPath : v.path}-${path}`;
-		};
-		// 鼠标滚轮滚动
-		const onHandleScroll = (e: any) => {
-			scrollbarRef.value.$refs.wrap$.scrollLeft += e.wheelDelta / 4;
-		};
-		// tagsView 横向滚动
-		const tagsViewmoveToCurrentTag = () => {
-			nextTick(() => {
-				if (tagsRefs.value.length <= 0) return false;
-				// 当前 li 元素
-				let liDom = tagsRefs.value[state.tagsRefsIndex];
-				// 当前 li 元素下标
-				let liIndex = state.tagsRefsIndex;
-				// 当前 ul 下 li 元素总长度
-				let liLength = tagsRefs.value.length;
-				// 最前 li
-				let liFirst: any = tagsRefs.value[0];
-				// 最后 li
-				let liLast: any = tagsRefs.value[tagsRefs.value.length - 1];
-				// 当前滚动条的值
-				let scrollRefs = scrollbarRef.value.$refs.wrap$;
-				// 当前滚动条滚动宽度
-				let scrollS = scrollRefs.scrollWidth;
-				// 当前滚动条偏移宽度
-				let offsetW = scrollRefs.offsetWidth;
-				// 当前滚动条偏移距离
-				let scrollL = scrollRefs.scrollLeft;
-				// 上一个 tags li dom
-				let liPrevTag: any = tagsRefs.value[state.tagsRefsIndex - 1];
-				// 下一个 tags li dom
-				let liNextTag: any = tagsRefs.value[state.tagsRefsIndex + 1];
-				// 上一个 tags li dom 的偏移距离
-				let beforePrevL: any = '';
-				// 下一个 tags li dom 的偏移距离
-				let afterNextL: any = '';
-				if (liDom === liFirst) {
-					// 头部
-					scrollRefs.scrollLeft = 0;
-				} else if (liDom === liLast) {
-					// 尾部
-					scrollRefs.scrollLeft = scrollS - offsetW;
-				} else {
-					// 非头/尾部
-					if (liIndex === 0) beforePrevL = liFirst.offsetLeft - 5;
-					else beforePrevL = liPrevTag?.offsetLeft - 5;
-					if (liIndex === liLength) afterNextL = liLast.offsetLeft + liLast.offsetWidth + 5;
-					else afterNextL = liNextTag.offsetLeft + liNextTag.offsetWidth + 5;
-					if (afterNextL > scrollL + offsetW) {
-						scrollRefs.scrollLeft = afterNextL - offsetW;
-					} else if (beforePrevL < scrollL) {
-						scrollRefs.scrollLeft = beforePrevL;
-					}
-				}
-				// 更新滚动条,防止不出现
-				scrollbarRef.value.update();
-			});
-		};
-		// 获取 tagsView 的下标:用于处理 tagsView 点击时的横向滚动
-		const getTagsRefsIndex = (path: string | unknown) => {
-			nextTick(async () => {
-				// await 使用该写法,防止拿取不到 tagsViewList 列表数据不完整
-				let tagsViewList = await state.tagsViewList;
-				state.tagsRefsIndex = tagsViewList.findIndex((v: any) => {
-					if (getThemeConfig.value.isShareTagsView) {
-						return v.path === path;
-					} else {
-						return v.url === path;
-					}
+		}
+		// 更新滚动条,防止不出现
+		scrollbarRef.value.update();
+	});
+};
+// 获取 tagsView 的下标:用于处理 tagsView 点击时的横向滚动
+const getTagsRefsIndex = (path: string | unknown) => {
+	nextTick(async () => {
+		// await 使用该写法,防止拿取不到 tagsViewList 列表数据不完整
+		let tagsViewList = await state.tagsViewList;
+		state.tagsRefsIndex = tagsViewList.findIndex((v: RouteItem) => {
+			if (getThemeConfig.value.isShareTagsView) {
+				return v.path === path;
+			} else {
+				return v.url === path;
+			}
+		});
+		// 添加初始化横向滚动条移动到对应位置
+		tagsViewmoveToCurrentTag();
+	});
+};
+// 设置 tagsView 可以进行拖拽
+const initSortable = async () => {
+	const el = <HTMLElement>document.querySelector('.layout-navbars-tagsview-ul');
+	if (!el) return false;
+	state.sortable.el && state.sortable.destroy();
+	state.sortable = Sortable.create(el, {
+		animation: 300,
+		dataIdAttr: 'data-url',
+		disabled: getThemeConfig.value.isSortableTagsView ? false : true,
+		onEnd: () => {
+			const sortEndList: RouteItem[] = [];
+			state.sortable.toArray().map((val: string) => {
+				state.tagsViewList.map((v: RouteItem) => {
+					if (v.url === val) sortEndList.push({ ...v });
 				});
-				// 添加初始化横向滚动条移动到对应位置
-				tagsViewmoveToCurrentTag();
-			});
-		};
-		// 设置 tagsView 可以进行拖拽
-		const initSortable = async () => {
-			const el = <HTMLElement>document.querySelector('.layout-navbars-tagsview-ul');
-			if (!el) return false;
-			state.sortable.el && state.sortable.destroy();
-			state.sortable = Sortable.create(el, {
-				animation: 300,
-				dataIdAttr: 'data-url',
-				disabled: getThemeConfig.value.isSortableTagsView ? false : true,
-				onEnd: () => {
-					const sortEndList: any = [];
-					state.sortable.toArray().map((val: any) => {
-						state.tagsViewList.map((v: any) => {
-							if (v.url === val) sortEndList.push({ ...v });
-						});
-					});
-					addBrowserSetSession(sortEndList);
-				},
-			});
-		};
-		// 拖动问题,https://gitee.com/lyt-top/vue-next-admin/issues/I3ZRRI
-		const onSortableResize = async () => {
-			await initSortable();
-			if (other.isMobile()) state.sortable.el && state.sortable.destroy();
-		};
-		// 页面加载前
-		onBeforeMount(() => {
-			// 初始化,防止手机端直接访问时还可以拖拽
-			onSortableResize();
-			// 拖动问题,https://gitee.com/lyt-top/vue-next-admin/issues/I3ZRRI
-			window.addEventListener('resize', onSortableResize);
-			// 监听非本页面调用 0 刷新当前,1 关闭当前,2 关闭其它,3 关闭全部 4 当前页全屏
-			mittBus.on('onCurrentContextmenuClick', (data: CurrentContextmenu) => {
-				onCurrentContextmenuClick(data);
 			});
-			// 监听布局配置界面开启/关闭拖拽
-			mittBus.on('openOrCloseSortable', () => {
-				initSortable();
-			});
-			// 监听布局配置开启 TagsView 共用,为了演示还原默认值
-			mittBus.on('openShareTagsView', () => {
-				if (getThemeConfig.value.isShareTagsView) {
-					router.push('/home');
-					state.tagsViewList = [];
-					state.tagsViewRoutesList.map((v: any) => {
-						if (v.meta.isAffix && !v.meta.isHide) {
-							v.url = setTagsViewHighlight(v);
-							state.tagsViewList.push({ ...v });
-						}
-					});
+			addBrowserSetSession(sortEndList);
+		},
+	});
+};
+// 拖动问题,https://gitee.com/lyt-top/vue-next-admin/issues/I3ZRRI
+const onSortableResize = async () => {
+	await initSortable();
+	if (other.isMobile()) state.sortable.el && state.sortable.destroy();
+};
+// 页面加载前
+onBeforeMount(() => {
+	// 初始化,防止手机端直接访问时还可以拖拽
+	onSortableResize();
+	// 拖动问题,https://gitee.com/lyt-top/vue-next-admin/issues/I3ZRRI
+	window.addEventListener('resize', onSortableResize);
+	// 监听非本页面调用 0 刷新当前,1 关闭当前,2 关闭其它,3 关闭全部 4 当前页全屏
+	mittBus.on('onCurrentContextmenuClick', (data: RouteItem) => {
+		// 通过方法点击关闭 tagsView
+		data.isFnClick = true;
+		onCurrentContextmenuClick(data);
+	});
+	// 监听布局配置界面开启/关闭拖拽
+	mittBus.on('openOrCloseSortable', () => {
+		initSortable();
+	});
+	// 监听布局配置开启 TagsView 共用,为了演示还原默认值
+	mittBus.on('openShareTagsView', () => {
+		if (getThemeConfig.value.isShareTagsView) {
+			router.push('/home');
+			state.tagsViewList = [];
+			state.tagsViewRoutesList.map((v: RouteItem) => {
+				if (v.meta?.isAffix && !v.meta.isHide) {
+					v.url = setTagsViewHighlight(v);
+					state.tagsViewList.push({ ...v });
 				}
 			});
-		});
-		// 页面卸载时
-		onUnmounted(() => {
-			// 取消非本页面调用监听
-			mittBus.off('onCurrentContextmenuClick', () => {});
-			// 取消监听布局配置界面开启/关闭拖拽
-			mittBus.off('openOrCloseSortable', () => {});
-			// 取消监听布局配置开启 TagsView 共用
-			mittBus.off('openShareTagsView', () => {});
-			// 取消窗口 resize 监听
-			window.removeEventListener('resize', onSortableResize);
-		});
-		// 页面更新时
-		onBeforeUpdate(() => {
-			tagsRefs.value = [];
-		});
-		// 页面加载时
-		onMounted(() => {
-			// 初始化 pinia 中的 tagsViewRoutes 列表
-			getTagsViewRoutes();
-			initSortable();
-		});
-		// 路由更新时(组件内生命钩子)
-		onBeforeRouteUpdate(async (to) => {
-			state.routeActive = setTagsViewHighlight(to);
-			state.routePath = to.meta.isDynamic ? to.meta.isDynamicPath : to.path;
-			await addTagsView(to.path, to);
-			getTagsRefsIndex(getThemeConfig.value.isShareTagsView ? state.routePath : state.routeActive);
-		});
-		// 监听路由的变化,动态赋值给 tagsView
-		watch(
-			pinia.state,
-			(val) => {
-				if (val.tagsViewRoutes.tagsViewRoutes.length === state.tagsViewRoutesList.length) return false;
-				getTagsViewRoutes();
-			},
-			{
-				deep: true,
-			}
-		);
-		return {
-			isActive,
-			onContextmenu,
-			onMousedownMenu,
-			onTagsClick,
-			tagsRefs,
-			contextmenuRef,
-			scrollbarRef,
-			tagsUlRef,
-			onHandleScroll,
-			getThemeConfig,
-			setTagsStyle,
-			setTagsViewNameI18n,
-			refreshCurrentTagsView,
-			closeCurrentTagsView,
-			onCurrentContextmenuClick,
-			...toRefs(state),
-		};
-	},
+		}
+	});
 });
+// 页面卸载时
+onUnmounted(() => {
+	// 取消非本页面调用监听
+	mittBus.off('onCurrentContextmenuClick', () => {});
+	// 取消监听布局配置界面开启/关闭拖拽
+	mittBus.off('openOrCloseSortable', () => {});
+	// 取消监听布局配置开启 TagsView 共用
+	mittBus.off('openShareTagsView', () => {});
+	// 取消窗口 resize 监听
+	window.removeEventListener('resize', onSortableResize);
+});
+// 页面更新时
+onBeforeUpdate(() => {
+	tagsRefs.value = [];
+});
+// 页面加载时
+onMounted(() => {
+	// 初始化 pinia 中的 tagsViewRoutes 列表
+	getTagsViewRoutes();
+	initSortable();
+});
+// 路由更新时(组件内生命钩子)
+onBeforeRouteUpdate(async (to) => {
+	state.routeActive = setTagsViewHighlight(to);
+	state.routePath = to.meta.isDynamic ? to.meta.isDynamicPath : to.path;
+	await addTagsView(to.path, <RouteToFrom>to);
+	getTagsRefsIndex(getThemeConfig.value.isShareTagsView ? state.routePath : state.routeActive);
+});
+// 监听路由的变化,动态赋值给 tagsView
+watch(
+	pinia.state,
+	(val) => {
+		if (val.tagsViewRoutes.tagsViewRoutes.length === state.tagsViewRoutesList.length) return false;
+		getTagsViewRoutes();
+	},
+	{
+		deep: true,
+	}
+);
 </script>
 
 <style scoped lang="scss">

+ 99 - 110
Web/src/layout/navMenu/horizontal.vue

@@ -1,7 +1,7 @@
 <template>
 	<div class="el-menu-horizontal-warp">
 		<el-scrollbar @wheel.native.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef">
-			<el-menu router :default-active="defaultActive" :ellipsis="false" background-color="transparent" mode="horizontal">
+			<el-menu router :default-active="state.defaultActive" :ellipsis="false" background-color="transparent" mode="horizontal">
 				<template v-for="val in menuLists">
 					<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
 						<template #title>
@@ -30,122 +30,111 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, toRefs, reactive, computed, defineComponent, onMounted, nextTick, onBeforeMount, ref } from 'vue';
-import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
+<script setup lang="ts" name="navMenuHorizontal">
+import { defineAsyncComponent, reactive, computed, onMounted, nextTick, onBeforeMount, ref } from 'vue';
+import { useRoute, onBeforeRouteUpdate, RouteRecordRaw } from 'vue-router';
 import { storeToRefs } from 'pinia';
 import { useRoutesList } from '/@/stores/routesList';
 import { useThemeConfig } from '/@/stores/themeConfig';
-import { verifyUrl } from '/@/utils/toolsValidate';
+import other from '/@/utils/other';
 import mittBus from '/@/utils/mitt';
 
-export default defineComponent({
-	name: 'navMenuHorizontal',
-	components: {
-		SubItem: defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue')),
-	},
-	props: {
-		menuList: {
-			type: Array,
-			default: () => [],
-		},
+// 引入组件
+const SubItem = defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue'));
+
+// 定义父组件传过来的值
+const props = defineProps({
+	// 菜单列表
+	menuList: {
+		type: Array<RouteRecordRaw>,
+		default: () => [],
 	},
-	setup(props) {
-		const elMenuHorizontalScrollRef = ref();
-		const stores = useRoutesList();
-		const storesThemeConfig = useThemeConfig();
-		const { routesList } = storeToRefs(stores);
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		const route = useRoute();
-		const router = useRouter();
-		const state = reactive({
-			defaultActive: null,
-		});
-		// 获取父级菜单数据
-		const menuLists = computed(() => {
-			return <any>props.menuList;
-		});
-		// 设置横向滚动条可以鼠标滚轮滚动
-		const onElMenuHorizontalScroll = (e: any) => {
-			const eventDelta = e.wheelDelta || -e.deltaY * 40;
-			elMenuHorizontalScrollRef.value.$refs.wrap$.scrollLeft = elMenuHorizontalScrollRef.value.$refs.wrap$.scrollLeft + eventDelta / 4;
-		};
-		// 初始化数据,页面刷新时,滚动条滚动到对应位置
-		const initElMenuOffsetLeft = () => {
-			nextTick(() => {
-				let els: any = document.querySelector('.el-menu.el-menu--horizontal li.is-active');
-				if (!els) return false;
-				elMenuHorizontalScrollRef.value.$refs.wrap$.scrollLeft = els.offsetLeft;
-			});
-		};
-		// 路由过滤递归函数
-		const filterRoutesFun = (arr: Array<string>) => {
-			return arr
-				.filter((item: any) => !item.meta.isHide)
-				.map((item: any) => {
-					item = Object.assign({}, item);
-					if (item.children) item.children = filterRoutesFun(item.children);
-					return item;
-				});
-		};
-		// 传送当前子级数据到菜单中
-		const setSendClassicChildren = (path: string) => {
-			const currentPathSplit = path.split('/');
-			let currentData: any = {};
-			filterRoutesFun(routesList.value).map((v, k) => {
-				if (v.path === `/${currentPathSplit[1]}`) {
-					v['k'] = k;
-					currentData['item'] = [{ ...v }];
-					currentData['children'] = [{ ...v }];
-					if (v.children) currentData['children'] = v.children;
-				}
-			});
-			return currentData;
-		};
-		// 设置页面当前路由高亮
-		const setCurrentRouterHighlight = (currentRoute: any) => {
-			const { path, meta } = currentRoute;
-			if (themeConfig.value.layout === 'classic') {
-				(<any>state.defaultActive) = `/${path.split('/')[1]}`;
-			} else {
-				const pathSplit = meta.isDynamic ? meta.isDynamicPath.split('/') : path.split('/');
-				if (pathSplit.length >= 4 && meta.isHide) state.defaultActive = pathSplit.splice(0, 3).join('/');
-				else state.defaultActive = path;
-			}
-		};
-		// 打开外部链接
-		const onALinkClick = (val: any) => {
-			const { origin, pathname } = window.location;
-			router.push(val.path);
-			if (verifyUrl(val.meta.isLink)) window.open(val.meta.isLink);
-			else window.open(`${origin}${pathname}#${val.meta.isLink}`);
-		};
-		// 页面加载前
-		onBeforeMount(() => {
-			setCurrentRouterHighlight(route);
-		});
-		// 页面加载时
-		onMounted(() => {
-			initElMenuOffsetLeft();
-		});
-		// 路由更新时
-		onBeforeRouteUpdate((to) => {
-			// 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
-			setCurrentRouterHighlight(to);
-			// 修复经典布局开启切割菜单时,点击tagsView后左侧导航菜单数据不变的问题
-			let { layout, isClassicSplitMenu } = themeConfig.value;
-			if (layout === 'classic' && isClassicSplitMenu) {
-				mittBus.emit('setSendClassicChildren', setSendClassicChildren(to.path));
-			}
+});
+
+// 定义变量内容
+const elMenuHorizontalScrollRef = ref();
+const stores = useRoutesList();
+const storesThemeConfig = useThemeConfig();
+const { routesList } = storeToRefs(stores);
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const route = useRoute();
+const state = reactive({
+	defaultActive: '' as string | undefined,
+});
+
+// 获取父级菜单数据
+const menuLists = computed(() => {
+	return <RouteItems>props.menuList;
+});
+// 设置横向滚动条可以鼠标滚轮滚动
+const onElMenuHorizontalScroll = (e: WheelEventType) => {
+	const eventDelta = e.wheelDelta || -e.deltaY * 40;
+	elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft = elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft + eventDelta / 4;
+};
+// 初始化数据,页面刷新时,滚动条滚动到对应位置
+const initElMenuOffsetLeft = () => {
+	nextTick(() => {
+		let els = <HTMLElement>document.querySelector('.el-menu.el-menu--horizontal li.is-active');
+		if (!els) return false;
+		elMenuHorizontalScrollRef.value.$refs.wrapRef.scrollLeft = els.offsetLeft;
+	});
+};
+// 路由过滤递归函数
+const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
+	return arr
+		.filter((item: T) => !item.meta?.isHide)
+		.map((item: T) => {
+			item = Object.assign({}, item);
+			if (item.children) item.children = filterRoutesFun(item.children);
+			return item;
 		});
-		return {
-			elMenuHorizontalScrollRef,
-			menuLists,
-			onElMenuHorizontalScroll,
-			onALinkClick,
-			...toRefs(state),
-		};
-	},
+};
+// 传送当前子级数据到菜单中
+const setSendClassicChildren = (path: string) => {
+	const currentPathSplit = path.split('/');
+	let currentData: MittMenu = { children: [] };
+	filterRoutesFun(routesList.value).map((v, k) => {
+		if (v.path === `/${currentPathSplit[1]}`) {
+			v['k'] = k;
+			currentData['item'] = { ...v };
+			currentData['children'] = [{ ...v }];
+			if (v.children) currentData['children'] = v.children;
+		}
+	});
+	return currentData;
+};
+// 设置页面当前路由高亮
+const setCurrentRouterHighlight = (currentRoute: RouteToFrom) => {
+	const { path, meta } = currentRoute;
+	if (themeConfig.value.layout === 'classic') {
+		state.defaultActive = `/${path?.split('/')[1]}`;
+	} else {
+		const pathSplit = meta?.isDynamic ? meta.isDynamicPath!.split('/') : path!.split('/');
+		if (pathSplit.length >= 4 && meta?.isHide) state.defaultActive = pathSplit.splice(0, 3).join('/');
+		else state.defaultActive = path;
+	}
+};
+// 打开外部链接
+const onALinkClick = (val: RouteItem) => {
+	other.handleOpenLink(val);
+};
+// 页面加载前
+onBeforeMount(() => {
+	setCurrentRouterHighlight(route);
+});
+// 页面加载时
+onMounted(() => {
+	initElMenuOffsetLeft();
+});
+// 路由更新时
+onBeforeRouteUpdate((to) => {
+	// 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
+	setCurrentRouterHighlight(to);
+	// 修复经典布局开启切割菜单时,点击tagsView后左侧导航菜单数据不变的问题
+	let { layout, isClassicSplitMenu } = themeConfig.value;
+	if (layout === 'classic' && isClassicSplitMenu) {
+		mittBus.emit('setSendClassicChildren', setSendClassicChildren(to.path));
+	}
 });
 </script>
 

+ 19 - 29
Web/src/layout/navMenu/subItem.vue

@@ -24,36 +24,26 @@
 	</template>
 </template>
 
-<script lang="ts">
-import { computed, defineComponent } from 'vue';
-import { useRouter } from 'vue-router';
-import { verifyUrl } from '/@/utils/toolsValidate';
+<script setup lang="ts" name="navMenuSubItem">
+import { computed } from 'vue';
+import { RouteRecordRaw } from 'vue-router';
+import other from '/@/utils/other';
 
-export default defineComponent({
-	name: 'navMenuSubItem',
-	props: {
-		chil: {
-			type: Array,
-			default: () => [],
-		},
-	},
-	setup(props) {
-		const router = useRouter();
-		// 获取父级菜单数据
-		const chils = computed(() => {
-			return <any>props.chil;
-		});
-		// 打开外部链接
-		const onALinkClick = (val: any) => {
-			const { origin, pathname } = window.location;
-			router.push(val.path);
-			if (verifyUrl(val.meta.isLink)) window.open(val.meta.isLink);
-			else window.open(`${origin}${pathname}#${val.meta.isLink}`);
-		};
-		return {
-			chils,
-			onALinkClick,
-		};
+// 定义父组件传过来的值
+const props = defineProps({
+	// 菜单列表
+	chil: {
+		type: Array<RouteRecordRaw>,
+		default: () => [],
 	},
 });
+
+// 获取父级菜单数据
+const chils = computed(() => {
+	return <RouteItems>props.chil;
+});
+// 打开外部链接
+const onALinkClick = (val: RouteItem) => {
+	other.handleOpenLink(val);
+};
 </script>

+ 65 - 82
Web/src/layout/navMenu/vertical.vue

@@ -1,12 +1,5 @@
 <template>
-	<el-menu
-		router
-		:default-active="defaultActive"
-		background-color="transparent"
-		:collapse="isCollapse"
-		:unique-opened="getThemeConfig.isUniqueOpened"
-		:collapse-transition="false"
-	>
+	<el-menu router :default-active="state.defaultActive" background-color="transparent" :collapse="state.isCollapse" :unique-opened="getThemeConfig.isUniqueOpened" :collapse-transition="false">
 		<template v-for="val in menuLists">
 			<el-sub-menu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
 				<template #title>
@@ -30,83 +23,73 @@
 	</el-menu>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, toRefs, reactive, computed, defineComponent, onMounted, watch } from 'vue';
-import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
+<script setup lang="ts" name="navMenuVertical">
+import { defineAsyncComponent, reactive, computed, onMounted, watch } from 'vue';
+import { useRoute, onBeforeRouteUpdate, RouteRecordRaw } from 'vue-router';
 import { storeToRefs } from 'pinia';
 import { useThemeConfig } from '/@/stores/themeConfig';
-import { verifyUrl } from '/@/utils/toolsValidate';
+import other from '/@/utils/other';
 
-export default defineComponent({
-	name: 'navMenuVertical',
-	components: {
-		SubItem: defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue')),
-	},
-	props: {
-		menuList: {
-			type: Array,
-			default: () => [],
-		},
-	},
-	setup(props) {
-		const storesThemeConfig = useThemeConfig();
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		const route = useRoute();
-		const router = useRouter();
-		const state = reactive({
-			// 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
-			defaultActive: route.meta.isDynamic ? route.meta.isDynamicPath : route.path,
-			isCollapse: false,
-		});
-		// 获取父级菜单数据
-		const menuLists = computed(() => {
-			return <any>props.menuList;
-		});
-		// 获取布局配置信息
-		const getThemeConfig = computed(() => {
-			return themeConfig.value;
-		});
-		// 菜单高亮(详情时,父级高亮)
-		const setParentHighlight = (currentRoute: any) => {
-			const { path, meta } = currentRoute;
-			const pathSplit = meta.isDynamic ? meta.isDynamicPath.split('/') : path.split('/');
-			if (pathSplit.length >= 4 && meta.isHide) return pathSplit.splice(0, 3).join('/');
-			else return path;
-		};
-		// 打开外部链接
-		const onALinkClick = (val: any) => {
-			const { origin, pathname } = window.location;
-			router.push(val.path);
-			if (verifyUrl(val.meta.isLink)) window.open(val.meta.isLink);
-			else window.open(`${origin}${pathname}#${val.meta.isLink}`);
-		};
-		// 设置菜单的收起/展开
-		watch(
-			themeConfig.value,
-			() => {
-				document.body.clientWidth <= 1000 ? (state.isCollapse = false) : (state.isCollapse = themeConfig.value.isCollapse);
-			},
-			{
-				immediate: true,
-			}
-		);
-		// 页面加载时
-		onMounted(() => {
-			state.defaultActive = setParentHighlight(route);
-		});
-		// 路由更新时
-		onBeforeRouteUpdate((to) => {
-			// 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
-			state.defaultActive = setParentHighlight(to);
-			const clientWidth = document.body.clientWidth;
-			if (clientWidth < 1000) themeConfig.value.isCollapse = false;
-		});
-		return {
-			menuLists,
-			getThemeConfig,
-			onALinkClick,
-			...toRefs(state),
-		};
+// 引入组件
+const SubItem = defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue'));
+
+// 定义父组件传过来的值
+const props = defineProps({
+	// 菜单列表
+	menuList: {
+		type: Array<RouteRecordRaw>,
+		default: () => [],
 	},
 });
+
+// 定义变量内容
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const route = useRoute();
+const state = reactive({
+	// 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
+	defaultActive: route.meta.isDynamic ? route.meta.isDynamicPath : route.path,
+	isCollapse: false,
+});
+
+// 获取父级菜单数据
+const menuLists = computed(() => {
+	return <RouteItems>props.menuList;
+});
+// 获取布局配置信息
+const getThemeConfig = computed(() => {
+	return themeConfig.value;
+});
+// 菜单高亮(详情时,父级高亮)
+const setParentHighlight = (currentRoute: RouteToFrom) => {
+	const { path, meta } = currentRoute;
+	const pathSplit = meta?.isDynamic ? meta.isDynamicPath!.split('/') : path!.split('/');
+	if (pathSplit.length >= 4 && meta?.isHide) return pathSplit.splice(0, 3).join('/');
+	else return path;
+};
+// 打开外部链接
+const onALinkClick = (val: RouteItem) => {
+	other.handleOpenLink(val);
+};
+// 页面加载时
+onMounted(() => {
+	state.defaultActive = setParentHighlight(route);
+});
+// 路由更新时
+onBeforeRouteUpdate((to) => {
+	// 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
+	state.defaultActive = setParentHighlight(to);
+	const clientWidth = document.body.clientWidth;
+	if (clientWidth < 1000) themeConfig.value.isCollapse = false;
+});
+// 设置菜单的收起/展开
+watch(
+	themeConfig.value,
+	() => {
+		document.body.clientWidth <= 1000 ? (state.isCollapse = false) : (state.isCollapse = themeConfig.value.isCollapse);
+	},
+	{
+		immediate: true,
+	}
+);
 </script>

+ 72 - 75
Web/src/layout/routerView/iframes.vue

@@ -10,85 +10,82 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { defineComponent, computed, watch, ref, nextTick } from 'vue';
+<script setup lang="ts" name="layoutIframeView">
+import { computed, watch, ref, nextTick } from 'vue';
 import { useRoute } from 'vue-router';
 
-export default defineComponent({
-	name: 'layoutIframeView',
-	props: {
-		refreshKey: {
-			type: String,
-			default: () => '',
-		},
-		name: {
-			type: String,
-			default: () => 'slide-right',
-		},
-		list: {
-			type: Array,
-			default: () => [],
-		},
+// 定义父组件传过来的值
+const props = defineProps({
+	// 刷新 iframe
+	refreshKey: {
+		type: String,
+		default: () => '',
 	},
-	setup(props) {
-		const iframeRef = ref();
-		const route = useRoute();
-		// 处理 list 列表,当打开时,才进行加载
-		const setIframeList = computed(() => {
-			return (<any>props.list).filter((v: any) => v.meta.isIframeOpen);
+	// 过渡动画 name
+	name: {
+		type: String,
+		default: () => 'slide-right',
+	},
+	// iframe 列表
+	list: {
+		type: Array,
+		default: () => [],
+	},
+});
+
+// 定义变量内容
+const iframeRef = ref();
+const route = useRoute();
+
+// 处理 list 列表,当打开时,才进行加载
+const setIframeList = computed(() => {
+	return (<RouteItems>props.list).filter((v: RouteItem) => v.meta?.isIframeOpen);
+});
+// 获取 iframe 当前路由 path
+const getRoutePath = computed(() => {
+	return route.path;
+});
+// 关闭 iframe loading
+const closeIframeLoading = (val: string, item: RouteItem) => {
+	nextTick(() => {
+		if (!iframeRef.value) return false;
+		iframeRef.value.forEach((v: HTMLElement) => {
+			if (v.dataset.url === val) {
+				v.onload = () => {
+					if (item.meta?.isIframeOpen && item.meta.loading) item.meta.loading = false;
+				};
+			}
 		});
-		// 获取 iframe 当前路由 path
-		const getRoutePath = computed(() => {
-			return route.path;
+	});
+};
+// 监听路由变化,初始化 iframe 数据,防止多个 iframe 时,切换不生效
+watch(
+	() => route.fullPath,
+	(val) => {
+		const item: any = props.list.find((v: any) => v.path === val);
+		if (!item) return false;
+		if (!item.meta.isIframeOpen) item.meta.isIframeOpen = true;
+		closeIframeLoading(val, item);
+	},
+	{
+		immediate: true,
+	}
+);
+// 监听 iframe refreshKey 变化,用于 tagsview 右键菜单刷新
+watch(
+	() => props.refreshKey,
+	() => {
+		const item: any = props.list.find((v: any) => v.path === route.path);
+		if (!item) return false;
+		if (item.meta.isIframeOpen) item.meta.isIframeOpen = false;
+		setTimeout(() => {
+			item.meta.isIframeOpen = true;
+			item.meta.loading = true;
+			closeIframeLoading(route.fullPath, item);
 		});
-		// 关闭 iframe loading
-		const closeIframeLoading = (val: string, item: any) => {
-			nextTick(() => {
-				if (!iframeRef.value) return false;
-				iframeRef.value.forEach((v: any) => {
-					if (v.dataset.url === val) {
-						v.onload = () => {
-							if (item.meta.isIframeOpen && item.meta.loading) item.meta.loading = false;
-						};
-					}
-				});
-			});
-		};
-		// 监听路由变化,初始化 iframe 数据,防止多个 iframe 时,切换不生效
-		watch(
-			() => route.fullPath,
-			(val) => {
-				const item: any = props.list.find((v: any) => v.path === val);
-				if (!item) return false;
-				if (!item.meta.isIframeOpen) item.meta.isIframeOpen = true;
-				closeIframeLoading(val, item);
-			},
-			{
-				immediate: true,
-			}
-		);
-		// 监听 iframe refreshKey 变化,用于 tagsview 右键菜单刷新
-		watch(
-			() => props.refreshKey,
-			() => {
-				const item: any = props.list.find((v: any) => v.path === route.path);
-				if (!item) return false;
-				if (item.meta.isIframeOpen) item.meta.isIframeOpen = false;
-				setTimeout(() => {
-					item.meta.isIframeOpen = true;
-					item.meta.loading = true;
-					closeIframeLoading(route.fullPath, item);
-				});
-			},
-			{
-				deep: true,
-			}
-		);
-		return {
-			iframeRef,
-			setIframeList,
-			getRoutePath,
-		};
 	},
-});
+	{
+		deep: true,
+	}
+);
 </script>

+ 26 - 46
Web/src/layout/routerView/link.vue

@@ -3,7 +3,7 @@
 		<div class="layout-padding-auto layout-padding-view">
 			<div class="layout-link-warp">
 				<i class="layout-link-icon iconfont icon-xingqiu"></i>
-				<div class="layout-link-msg">页面 "{{ $t(currentRouteMeta.title) }}" 已在新窗口中打开</div>
+				<div class="layout-link-msg">页面 "{{ $t(state.title) }}" 已在新窗口中打开</div>
 				<el-button class="mt30" round size="default" @click="onGotoFullPage">
 					<i class="iconfont icon-lianjie"></i>
 					<span>立即前往体验</span>
@@ -13,55 +13,35 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { defineComponent, toRefs, reactive, watch } from 'vue';
-import { useRoute, RouteMeta } from 'vue-router';
+<script setup lang="ts" name="layoutLinkView">
+import { reactive, watch } from 'vue';
+import { useRoute } from 'vue-router';
 import { verifyUrl } from '/@/utils/toolsValidate';
 
-// 定义接口来定义对象的类型
-interface LinkViewState {
-	currentRouteMeta: {
-		isLink: string;
-		title: string;
-	};
-}
-interface LinkViewRouteMeta extends RouteMeta {
-	isLink: string;
-	title: string;
-}
+// 定义变量内容
+const route = useRoute();
+const state = reactive<LinkViewState>({
+	title: '',
+	isLink: '',
+});
 
-export default defineComponent({
-	name: 'layoutLinkView',
-	setup() {
-		const route = useRoute();
-		const state = reactive<LinkViewState>({
-			currentRouteMeta: {
-				isLink: '',
-				title: '',
-			},
-		});
-		// 立即前往
-		const onGotoFullPage = () => {
-			const { origin, pathname } = window.location;
-			if (verifyUrl(state.currentRouteMeta.isLink)) window.open(state.currentRouteMeta.isLink);
-			else window.open(`${origin}${pathname}#${state.currentRouteMeta.isLink}`);
-		};
-		// 监听路由的变化,设置内容
-		watch(
-			() => route.path,
-			() => {
-				state.currentRouteMeta = <LinkViewRouteMeta>route.meta;
-			},
-			{
-				immediate: true,
-			}
-		);
-		return {
-			onGotoFullPage,
-			...toRefs(state),
-		};
+// 立即前往
+const onGotoFullPage = () => {
+	const { origin, pathname } = window.location;
+	if (verifyUrl(<string>state.isLink)) window.open(state.isLink);
+	else window.open(`${origin}${pathname}#${state.isLink}`);
+};
+// 监听路由的变化,设置内容
+watch(
+	() => route.path,
+	() => {
+		state.title = <string>route.meta.title;
+		state.isLink = <string>route.meta.isLink;
 	},
-});
+	{
+		immediate: true,
+	}
+);
 </script>
 
 <style scoped lang="scss">

+ 79 - 97
Web/src/layout/routerView/parent.vue

@@ -3,18 +3,18 @@
 		<router-view v-slot="{ Component }">
 			<transition :name="setTransitionName" mode="out-in">
 				<keep-alive :include="getKeepAliveNames">
-					<component :is="Component" :key="refreshRouterViewKey" class="w100" v-show="!isIframePage" />
+					<component :is="Component" :key="state.refreshRouterViewKey" class="w100" v-show="!isIframePage" />
 				</keep-alive>
 			</transition>
 		</router-view>
 		<transition :name="setTransitionName" mode="out-in">
-			<Iframes class="w100" v-show="isIframePage" :refreshKey="iframeRefreshKey" :name="setTransitionName" :list="iframeList" />
+			<Iframes class="w100" v-show="isIframePage" :refreshKey="state.iframeRefreshKey" :name="setTransitionName" :list="state.iframeList" />
 		</transition>
 	</div>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, computed, defineComponent, toRefs, reactive, onBeforeMount, onUnmounted, nextTick, watch, onMounted } from 'vue';
+<script setup lang="ts" name="layoutParentView">
+import { defineAsyncComponent, computed, reactive, onBeforeMount, onUnmounted, nextTick, watch, onMounted } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { storeToRefs } from 'pinia';
 import { useKeepALiveNames } from '/@/stores/keepAliveNames';
@@ -22,101 +22,83 @@ import { useThemeConfig } from '/@/stores/themeConfig';
 import { Session } from '/@/utils/storage';
 import mittBus from '/@/utils/mitt';
 
-// 定义接口来定义对象的类型
-interface ParentViewState {
-	refreshRouterViewKey: string;
-	iframeRefreshKey: string;
-	keepAliveNameList: string[];
-	iframeList: string[];
-}
+// 引入组件
+const Iframes = defineAsyncComponent(() => import('/@/layout/routerView/iframes.vue'));
 
-export default defineComponent({
-	name: 'layoutParentView',
-	components: {
-		Iframes: defineAsyncComponent(() => import('/@/layout/routerView/iframes.vue')),
-	},
-	setup() {
-		const route = useRoute();
-		const router = useRouter();
-		const storesKeepAliveNames = useKeepALiveNames();
-		const storesThemeConfig = useThemeConfig();
-		const { keepAliveNames, cachedViews } = storeToRefs(storesKeepAliveNames);
-		const { themeConfig } = storeToRefs(storesThemeConfig);
-		const state = reactive<ParentViewState>({
-			refreshRouterViewKey: '', // 非 iframe tagsview 右键菜单刷新时
-			iframeRefreshKey: '', // iframe tagsview 右键菜单刷新时
-			keepAliveNameList: [],
-			iframeList: [],
-		});
-		// 设置主界面切换动画
-		const setTransitionName = computed(() => {
-			return themeConfig.value.animation;
-		});
-		// 获取组件缓存列表(name值)
-		const getKeepAliveNames = computed(() => {
-			return themeConfig.value.isTagsview ? cachedViews.value : state.keepAliveNameList;
-		});
-		// 设置 iframe 显示/隐藏
-		const isIframePage = computed(() => {
-			return route.meta.isIframe;
-		});
-		// 获取 iframe 组件列表(未进行渲染)
-		const getIframeListRoutes = async () => {
-			router.getRoutes().forEach((v: any) => {
-				if (v.meta.isIframe) {
-					v.meta.isIframeOpen = false;
-					v.meta.loading = true;
-					state.iframeList.push({ ...v });
-				}
-			});
-		};
-		// 页面加载前,处理缓存,页面刷新时路由缓存处理
-		onBeforeMount(() => {
+// 定义变量内容
+const route = useRoute();
+const router = useRouter();
+const storesKeepAliveNames = useKeepALiveNames();
+const storesThemeConfig = useThemeConfig();
+const { keepAliveNames, cachedViews } = storeToRefs(storesKeepAliveNames);
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const state = reactive<ParentViewState>({
+	refreshRouterViewKey: '', // 非 iframe tagsview 右键菜单刷新时
+	iframeRefreshKey: '', // iframe tagsview 右键菜单刷新时
+	keepAliveNameList: [],
+	iframeList: [],
+});
+
+// 设置主界面切换动画
+const setTransitionName = computed(() => {
+	return themeConfig.value.animation;
+});
+// 获取组件缓存列表(name值)
+const getKeepAliveNames = computed(() => {
+	return themeConfig.value.isTagsview ? cachedViews.value : state.keepAliveNameList;
+});
+// 设置 iframe 显示/隐藏
+const isIframePage = computed(() => {
+	return route.meta.isIframe;
+});
+// 获取 iframe 组件列表(未进行渲染)
+const getIframeListRoutes = async () => {
+	router.getRoutes().forEach((v) => {
+		if (v.meta.isIframe) {
+			v.meta.isIframeOpen = false;
+			v.meta.loading = true;
+			state.iframeList.push({ ...v });
+		}
+	});
+};
+// 页面加载前,处理缓存,页面刷新时路由缓存处理
+onBeforeMount(() => {
+	state.keepAliveNameList = keepAliveNames.value;
+	mittBus.on('onTagsViewRefreshRouterView', (fullPath: string) => {
+		state.keepAliveNameList = keepAliveNames.value.filter((name: string) => route.name !== name);
+		state.refreshRouterViewKey = '';
+		state.iframeRefreshKey = '';
+		nextTick(() => {
+			state.refreshRouterViewKey = fullPath;
+			state.iframeRefreshKey = fullPath;
 			state.keepAliveNameList = keepAliveNames.value;
-			mittBus.on('onTagsViewRefreshRouterView', (fullPath: string) => {
-				state.keepAliveNameList = keepAliveNames.value.filter((name: string) => route.name !== name);
-				state.refreshRouterViewKey = '';
-				state.iframeRefreshKey = '';
-				nextTick(() => {
-					state.refreshRouterViewKey = fullPath;
-					state.iframeRefreshKey = fullPath;
-					state.keepAliveNameList = keepAliveNames.value;
-				});
-			});
-		});
-		// 页面加载时
-		onMounted(() => {
-			getIframeListRoutes();
-			// https://gitee.com/lyt-top/vue-next-admin/issues/I58U75
-			// https://gitee.com/lyt-top/vue-next-admin/issues/I59RXK
-			nextTick(() => {
-				setTimeout(() => {
-					if (themeConfig.value.isCacheTagsView) cachedViews.value = Session.get('tagsViewList')?.map((item: any) => item.name);
-				}, 0);
-			});
-		});
-		// 页面卸载时
-		onUnmounted(() => {
-			mittBus.off('onTagsViewRefreshRouterView', () => {});
 		});
-		// 监听路由变化,防止 tagsView 多标签时,切换动画消失
-		// https://toscode.gitee.com/lyt-top/vue-next-admin/pulls/38/files
-		watch(
-			() => route.fullPath,
-			() => {
-				state.refreshRouterViewKey = decodeURI(route.fullPath);
-			},
-			{
-				immediate: true,
-			}
-		);
-		return {
-			route,
-			setTransitionName,
-			getKeepAliveNames,
-			isIframePage,
-			...toRefs(state),
-		};
-	},
+	});
+});
+// 页面加载时
+onMounted(() => {
+	getIframeListRoutes();
+	// https://gitee.com/lyt-top/vue-next-admin/issues/I58U75
+	// https://gitee.com/lyt-top/vue-next-admin/issues/I59RXK
+	nextTick(() => {
+		setTimeout(() => {
+			if (themeConfig.value.isCacheTagsView) cachedViews.value = Session.get('tagsViewList')?.map((item: RouteItem) => item.name);
+		}, 0);
+	});
 });
+// 页面卸载时
+onUnmounted(() => {
+	mittBus.off('onTagsViewRefreshRouterView', () => {});
+});
+// 监听路由变化,防止 tagsView 多标签时,切换动画消失
+// https://toscode.gitee.com/lyt-top/vue-next-admin/pulls/38/files
+watch(
+	() => route.fullPath,
+	() => {
+		state.refreshRouterViewKey = decodeURI(route.fullPath);
+	},
+	{
+		immediate: true,
+	}
+);
 </script>

+ 1 - 1
Web/src/main.ts

@@ -2,7 +2,7 @@ import { createApp } from 'vue';
 import pinia from '/@/stores/index';
 import App from './App.vue';
 import router from './router';
-import { directive } from '/@/utils/directive';
+import { directive } from '/@/directive/index';
 import { i18n } from '/@/i18n/index';
 import other from '/@/utils/other';
 

+ 6 - 7
Web/src/router/backEnd.ts

@@ -13,9 +13,6 @@ import { getAPI } from '/@/utils/axios-utils';
 import { SysMenuApi } from '/@/api-services/api';
 import { ElMessage } from 'element-plus';
 
-const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}');
-const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}');
-
 // 后端控制路由
 
 /**
@@ -23,6 +20,8 @@ const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}');
  * @method import.meta.glob
  * @link 参考:https://cn.vitejs.dev/guide/features.html#json
  */
+const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}');
+const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}');
 const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...layouModules }, { ...viewsModules });
 
 /**
@@ -31,7 +30,7 @@ const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...lay
  * @method useUserInfo().setUserInfos() 触发初始化用户信息 pinia
  * @method useRequestOldRoutes().setRequestOldRoutes() 存储接口原始路由(未处理component),根据需求选择使用
  * @method setAddRoute 添加动态路由
- * @method setFilterMenuAndCacheTagsViewRoutes 设置路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+ * @method setFilterMenuAndCacheTagsViewRoutes 设置路由到 pinia routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
  */
 export async function initBackEndControlRoutes() {
 	// 界面 loading 动画开始执行
@@ -49,12 +48,12 @@ export async function initBackEndControlRoutes() {
 	dynamicRoutes[0].children = await backEndComponent(res);
 	// 添加动态路由
 	await setAddRoute();
-	// 设置路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+	// 设置路由到 pinia routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
 	await setFilterMenuAndCacheTagsViewRoutes();
 }
 
 /**
- * 设置路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+ * 设置路由到 pinia routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
  * @description 用于左侧菜单、横向菜单的显示
  * @description 用于 tagsView、菜单搜索中:未过滤隐藏的(isHide)
  */
@@ -80,7 +79,7 @@ export function setCacheTagsViewRoutes() {
  */
 export function setFilterRouteEnd() {
 	let filterRouteEnd: any = formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes));
-    // notFoundAndNoPower 防止 404、401 不在 layout 布局中,不设置的话,404、401 界面将全屏显示
+	// notFoundAndNoPower 防止 404、401 不在 layout 布局中,不设置的话,404、401 界面将全屏显示
 	// 关联问题 No match found for location with path 'xxx'
 	filterRouteEnd[0].children = [...filterRouteEnd[0].children, ...notFoundAndNoPower];
 	return filterRouteEnd;

+ 4 - 4
Web/src/router/frontEnd.ts

@@ -16,7 +16,7 @@ import { NextLoading } from '/@/utils/loading';
  * @method  NextLoading 界面 loading 动画开始执行
  * @method useUserInfo(pinia).setUserInfos() 触发初始化用户信息 pinia
  * @method setAddRoute 添加动态路由
- * @method setFilterMenuAndCacheTagsViewRoutes 设置递归过滤有权限的路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+ * @method setFilterMenuAndCacheTagsViewRoutes 设置递归过滤有权限的路由到 pinia routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
  */
 export async function initFrontEndControlRoutes() {
 	// 界面 loading 动画开始执行
@@ -28,7 +28,7 @@ export async function initFrontEndControlRoutes() {
 	await useUserInfo(pinia).setUserInfos();
 	// 添加动态路由
 	await setAddRoute();
-	// 设置递归过滤有权限的路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+	// 设置递归过滤有权限的路由到 pinia routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
 	await setFilterMenuAndCacheTagsViewRoutes();
 }
 
@@ -64,7 +64,7 @@ export async function frontEndsResetRoute() {
  */
 export function setFilterRouteEnd() {
 	let filterRouteEnd: any = formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes));
-    // notFoundAndNoPower 防止 404、401 不在 layout 布局中,不设置的话,404、401 界面将全屏显示
+	// notFoundAndNoPower 防止 404、401 不在 layout 布局中,不设置的话,404、401 界面将全屏显示
 	// 关联问题 No match found for location with path 'xxx'
 	filterRouteEnd[0].children = [...setFilterRoute(filterRouteEnd[0].children), ...notFoundAndNoPower];
 	return filterRouteEnd;
@@ -108,7 +108,7 @@ export function setCacheTagsViewRoutes() {
 }
 
 /**
- * 设置递归过滤有权限的路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+ * 设置递归过滤有权限的路由到 pinia routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
  * @description 用于左侧菜单、横向菜单的显示
  * @description 用于 tagsView、菜单搜索中:未过滤隐藏的(isHide)
  */

+ 16 - 0
Web/src/router/route.ts

@@ -1,6 +1,8 @@
 import { RouteRecordRaw } from 'vue-router';
 
 /**
+ * 建议:路由 path 路径与文件夹名称相同,找文件可浏览器地址找,方便定位文件位置
+ *
  * 路由meta对象参数说明
  * meta: {
  *      title:          菜单栏及 tagsView 栏、菜单搜索名称(国际化)
@@ -14,6 +16,20 @@ import { RouteRecordRaw } from 'vue-router';
  * }
  */
 
+// 扩展 RouteMeta 接口
+declare module 'vue-router' {
+	interface RouteMeta {
+		title?: string;
+		isLink?: string;
+		isHide?: boolean;
+		isKeepAlive?: boolean;
+		isAffix?: boolean;
+		isIframe?: boolean;
+		roles?: string[];
+		icon?: string;
+	}
+}
+
 /**
  * 定义动态路由
  * 前端添加路由,请在顶级节点的 `children 数组` 里添加

+ 0 - 101
Web/src/stores/interface/index.ts

@@ -1,101 +0,0 @@
-/**
- * 定义接口来定义对象的类型
- * `stores` 全部类型定义在这里
- */
-
-import { ConstOutput } from '/@/api-services/models';
-
-// 用户信息
-export interface UserInfosState {
-	account: string;
-	realName: string;
-	avatar: string;
-	address: string;
-	signature: string;
-	orgId: string;
-	orgName: string;
-	posName: string;
-	roles: string[];
-	authBtnList: string[];
-	time: number;
-}
-export interface UserInfosStates {
-	userInfos: UserInfosState;
-	constList: Array<ConstOutput>;
-}
-
-// 路由缓存列表
-export interface KeepAliveNamesState {
-	keepAliveNames: string[];
-	cachedViews: string[];
-}
-
-// 后端返回原始路由(未处理时)
-export interface RequestOldRoutesState {
-	requestOldRoutes: string[];
-}
-
-// TagsView 路由列表
-export interface TagsViewRoutesState {
-	tagsViewRoutes: string[];
-	isTagsViewCurrenFull: Boolean;
-}
-
-// 路由列表
-export interface RoutesListState {
-	routesList: string[];
-	isColumnsMenuHover: Boolean;
-	isColumnsNavHover: Boolean;
-}
-
-// 布局配置
-export interface ThemeConfigState {
-	isDrawer: boolean;
-	primary: string;
-	topBar: string;
-	topBarColor: string;
-	isTopBarColorGradual: boolean;
-	menuBar: string;
-	menuBarColor: string;
-	isMenuBarColorGradual: boolean;
-	columnsMenuBar: string;
-	columnsMenuBarColor: string;
-	isColumnsMenuBarColorGradual: boolean;
-	isColumnsMenuHoverPreload: boolean;
-	isCollapse: boolean;
-	isUniqueOpened: boolean;
-	isFixedHeader: boolean;
-	isFixedHeaderChange: boolean;
-	isClassicSplitMenu: boolean;
-	isLockScreen: boolean;
-	lockScreenTime: number;
-	isShowLogo: boolean;
-	isShowLogoChange: boolean;
-	isBreadcrumb: boolean;
-	isTagsview: boolean;
-	isBreadcrumbIcon: boolean;
-	isTagsviewIcon: boolean;
-	isCacheTagsView: boolean;
-	isSortableTagsView: boolean;
-	isShareTagsView: boolean;
-	isFooter: boolean;
-	isGrayscale: boolean;
-	isInvert: boolean;
-	isIsDark: boolean;
-	isWartermark: boolean;
-	wartermarkText: string;
-	tagsStyle: string;
-	animation: string;
-	columnsAsideStyle: string;
-	columnsAsideLayout: string;
-	layout: string;
-	isRequestRoutes: boolean;
-	globalTitle: string;
-	globalViceTitle: string;
-	globalViceTitleMsg: string;
-	globalI18n: string;
-	globalComponentSize: string;
-}
-export interface ThemeConfigStates {
-	themeConfig: ThemeConfigState;
-}

+ 0 - 1
Web/src/stores/keepAliveNames.ts

@@ -1,5 +1,4 @@
 import { defineStore } from 'pinia';
-import { KeepAliveNamesState } from './interface';
 
 /**
  * 路由缓存列表

+ 0 - 1
Web/src/stores/requestOldRoutes.ts

@@ -1,5 +1,4 @@
 import { defineStore } from 'pinia';
-import { RequestOldRoutesState } from './interface';
 
 /**
  * 后端返回原始路由(未处理时)

+ 0 - 1
Web/src/stores/routesList.ts

@@ -1,5 +1,4 @@
 import { defineStore } from 'pinia';
-import { RoutesListState } from './interface';
 
 /**
  * 路由列表

+ 0 - 1
Web/src/stores/tagsViewRoutes.ts

@@ -1,5 +1,4 @@
 import { defineStore } from 'pinia';
-import { TagsViewRoutesState } from './interface';
 import { Session } from '/@/utils/storage';
 
 /**

+ 2 - 3
Web/src/stores/themeConfig.ts

@@ -1,5 +1,4 @@
 import { defineStore } from 'pinia';
-import { ThemeConfigStates, ThemeConfigState } from './interface';
 
 /**
  * 布局配置
@@ -10,7 +9,7 @@ import { ThemeConfigStates, ThemeConfigState } from './interface';
  * 2、或者点击布局配置最底部 `一键恢复默认` 按钮即可看到效果
  */
 export const useThemeConfig = defineStore('themeConfig', {
-	state: (): ThemeConfigStates => ({
+	state: (): ThemeConfigState => ({
 		themeConfig: {
 			// 是否开启布局配置抽屉
 			isDrawer: false,
@@ -144,7 +143,7 @@ export const useThemeConfig = defineStore('themeConfig', {
 	}),
 	actions: {
 		setThemeConfig(data: ThemeConfigState) {
-			this.themeConfig = data;
+			this.themeConfig = data.themeConfig;
 		},
 	},
 });

+ 3 - 5
Web/src/stores/userInfo.ts

@@ -1,19 +1,17 @@
 import { defineStore } from 'pinia';
-import { UserInfosState, UserInfosStates } from './interface';
 import { Session } from '/@/utils/storage';
 
 import { getAPI } from '/@/utils/axios-utils';
 import { SysAuthApi, SysConstApi } from '/@/api-services/api';
-import { ConstOutput } from '/@/api-services/models';
 
 /**
  * 用户信息
  * @methods setUserInfos 设置用户信息
  */
 export const useUserInfo = defineStore('userInfo', {
-	state: (): UserInfosStates => ({
-		userInfos: {} as UserInfosState,
-		constList: [] as Array<ConstOutput>,
+	state: (): UserInfosState => ({
+		userInfos: {} as any,
+		constList: [] as any,
 	}),
 	getters: {
 		// 获取系统常量列表

+ 7 - 0
Web/src/theme/dark.scss

@@ -58,6 +58,7 @@
 	--el-fill-color-light: var(--next-color-hover-rgba) !important;
 	--el-bg-color-overlay: var(--el-color-primary-light-9) !important;
 	--el-mask-color: rgb(42 42 42 / 80%);
+	--el-fill-color-lighter: var(--next-color-hover-rgba) !important;
 
 	// button
 	.el-button {
@@ -167,6 +168,12 @@
 	.el-pagination.is-background .el-pager li {
 		background-color: var(--next-bg-color);
 	}
+	/*深色模式时分页高亮问题*/
+	.el-pagination.is-background .btn-next.is-active,
+	.el-pagination.is-background .btn-prev.is-active,
+	.el-pagination.is-background .el-pager li.is-active {
+		color: var(--next-color-white) !important;
+	}
 
 	// radio
 	.el-radio-button:not(.is-active) .el-radio-button__inner {

+ 14 - 1
Web/src/theme/element.scss

@@ -3,7 +3,7 @@
 /* Button 按钮
 ------------------------------- */
 // 第三方字体图标大小
-.el-button i.el-icon,
+.el-button:not(.is-circle) i.el-icon,
 .el-button i.iconfont,
 .el-button i.fa,
 .el-button--default i.iconfont,
@@ -315,6 +315,19 @@
 		}
 	}
 }
+
+/* Pagination 分页
+------------------------------- */
+.el-pagination__editor {
+	margin-right: 8px;
+}
+/*深色模式时分页高亮问题*/
+.el-pagination.is-background .btn-next.is-active,
+.el-pagination.is-background .btn-prev.is-active,
+.el-pagination.is-background .el-pager li.is-active {
+	background-color: var(--el-color-primary) !important;
+	color: var(--el-color-white) !important;
+}
 // 分页组件靠右显示
 .el-pagination {
 	float: right;

+ 4 - 0
Web/src/theme/media/layout.scss

@@ -52,4 +52,8 @@
 			color: #666666;
 		}
 	}
+	// pagination 分页中的工具栏
+	.table-footer-tool {
+		display: none !important;
+	}
 }

+ 5 - 5
Web/src/theme/media/pagination.scss

@@ -7,9 +7,9 @@
 	.el-pagination__jump {
 		display: none !important;
 	}
-}
-
-// 默认居中对齐
-.el-pagination {
-	text-align: center !important;
+	// 默认居中对齐
+	.el-pagination,
+	.table-footer {
+		justify-content: center !important;
+	}
 }

+ 25 - 0
Web/src/theme/tableTool.scss

@@ -0,0 +1,25 @@
+.table-tool-popper {
+	padding: 0 !important;
+	.tool-box {
+		display: flex;
+		border-bottom: 1px solid var(--el-border-color-lighter);
+		box-sizing: border-box;
+		color: var(--el-text-color-primary);
+		height: 40px;
+		align-items: center;
+	}
+	.tool-item {
+		display: flex;
+		box-sizing: border-box;
+		color: var(--el-text-color-primary);
+		height: 35px;
+		align-items: center;
+		padding: var(--el-popover-padding);
+		&:hover {
+			background: var(--el-fill-color-lighter);
+		}
+		i {
+			opacity: 0.7;
+		}
+	}
+}

+ 13 - 0
Web/src/types/axios.d.ts

@@ -0,0 +1,13 @@
+/* eslint-disable */
+import * as axios from 'axios';
+
+// 扩展 axios 数据返回类型,可自行扩展
+declare module 'axios' {
+	export interface AxiosResponse<T = any> {
+		code: number;
+		data: T;
+		message: string;
+		type?: string;
+		[key: string]: T;
+	}
+}

+ 112 - 0
Web/src/types/global.d.ts

@@ -0,0 +1,112 @@
+// 申明外部 npm 插件模块
+declare module 'vue-grid-layout';
+declare module 'qrcodejs2-fixes';
+declare module 'splitpanes';
+declare module 'js-cookie';
+declare module '@wangeditor/editor-for-vue';
+declare module 'js-table2excel';
+declare module 'vform3-builds'
+declare module 'vue-signature-pad'
+
+// 声明一个模块,防止引入文件时报错
+declare module '*.json';
+declare module '*.png';
+declare module '*.jpg';
+declare module '*.scss';
+declare module '*.ts';
+declare module '*.js';
+
+// 声明文件,*.vue 后缀的文件交给 vue 模块来处理
+declare module '*.vue' {
+	import type { DefineComponent } from 'vue';
+	const component: DefineComponent<{}, {}, any>;
+	export default component;
+}
+
+// 声明文件,定义全局变量
+/* eslint-disable */
+declare interface Window {
+	nextLoading: boolean;
+}
+
+// 声明路由当前项类型
+declare type RouteItem<T = any> = {
+	path: string;
+	name?: string | symbol | undefined | null;
+	redirect?: string;
+	k?: T;
+	meta?: {
+		title?: string;
+		isLink?: string;
+		isHide?: boolean;
+		isKeepAlive?: boolean;
+		isAffix?: boolean;
+		isIframe?: boolean;
+		roles?: string[];
+		icon?: string;
+		isDynamic?: boolean;
+		isDynamicPath?: string;
+		isIframeOpen?: string;
+		loading?: boolean;
+	};
+	children: T[];
+	query?: { [key: string]: T };
+	params?: { [key: string]: T };
+	contextMenuClickId?: string | number;
+	commonUrl?: string;
+	isFnClick?: boolean;
+	url?: string;
+	transUrl?: string;
+	title?: string;
+	id?: string | number;
+};
+
+// 声明路由 to from
+declare interface RouteToFrom<T = any> extends RouteItem {
+	path?: string;
+	children?: T[];
+}
+
+// 声明路由当前项类型集合
+declare type RouteItems<T extends RouteItem = any> = T[];
+
+// 声明 ref
+declare type RefType<T = any> = T | null;
+
+// 声明 HTMLElement
+declare type HtmlType = HTMLElement | string | undefined | null;
+
+// 申明 children 可选
+declare type ChilType<T = any> = {
+	children?: T[];
+};
+
+// 申明 数组
+declare type EmptyArrayType<T = any> = T[];
+
+// 申明 对象
+declare type EmptyObjectType<T = any> = {
+	[key: string]: T;
+};
+
+// 申明 select option
+declare type SelectOptionType = {
+	value: string | number;
+	label: string | number;
+};
+
+// 鼠标滚轮滚动类型
+declare interface WheelEventType extends WheelEvent {
+	wheelDelta: number;
+}
+
+// table 数据格式公共类型
+declare interface TableType<T = any> {
+	total: number;
+	loading: boolean;
+	param: {
+		pageNum: number;
+		pageSize: number;
+		[key: string]: T;
+	};
+}

+ 59 - 0
Web/src/types/layout.d.ts

@@ -0,0 +1,59 @@
+// aside
+declare type AsideState = {
+	menuList: RouteRecordRaw[];
+	clientWidth: number;
+};
+
+// columnsAside
+declare type ColumnsAsideState<T = any> = {
+	columnsAsideList: T[];
+	liIndex: number;
+	liOldIndex: null | number;
+	liHoverIndex: null | number;
+	liOldPath: null | string;
+	difference: number;
+	routeSplit: string[];
+};
+
+// navBars breadcrumb
+declare type BreadcrumbState<T = any> = {
+	breadcrumbList: T[];
+	routeSplit: string[];
+	routeSplitFirst: string;
+	routeSplitIndex: number;
+};
+
+// navBars search
+declare type SearchState<T = any> = {
+	isShowSearch: boolean;
+	menuQuery: string;
+	tagsViewList: T[];
+};
+
+// navBars tagsView
+declare type TagsViewState<T = any> = {
+	routeActive: string | T;
+	routePath: string | unknown;
+	dropdown: {
+		x: string | number;
+		y: string | number;
+	};
+	sortable: T;
+	tagsRefsIndex: number;
+	tagsViewList: T[];
+	tagsViewRoutesList: T[];
+};
+
+// navBars parent
+declare type ParentViewState<T = any> = {
+	refreshRouterViewKey: string;
+	iframeRefreshKey: string;
+	keepAliveNameList: string[];
+	iframeList: T[];
+};
+
+// navBars link
+declare type LinkViewState = {
+	title: string;
+	isLink: string;
+};

+ 46 - 0
Web/src/types/mitt.d.ts

@@ -0,0 +1,46 @@
+/**
+ * mitt 事件类型定义
+ *
+ * @method openSetingsDrawer 打开布局设置弹窗
+ * @method restoreDefault 分栏布局,鼠标移入、移出数据显示
+ * @method setSendColumnsChildren 分栏布局,鼠标移入、移出菜单数据传入到 navMenu 下的菜单中
+ * @method setSendClassicChildren 经典布局,开启切割菜单时,菜单数据传入到 navMenu 下的菜单中
+ * @method getBreadcrumbIndexSetFilterRoutes 布局设置弹窗,开启切割菜单时,菜单数据传入到 navMenu 下的菜单中
+ * @method layoutMobileResize 浏览器窗口改变时,用于适配移动端界面显示
+ * @method openOrCloseSortable 布局设置弹窗,开启 TagsView 拖拽
+ * @method openShareTagsView 布局设置弹窗,开启 TagsView 共用
+ * @method onTagsViewRefreshRouterView tagsview 刷新界面
+ * @method onCurrentContextmenuClick tagsview 右键菜单每项点击时
+ */
+declare type MittType<T = any> = {
+	openSetingsDrawer?: string;
+	restoreDefault?: string;
+	setSendColumnsChildren: T;
+	setSendClassicChildren: T;
+	getBreadcrumbIndexSetFilterRoutes?: string;
+	layoutMobileResize: T;
+	openOrCloseSortable?: string;
+	openShareTagsView?: string;
+	onTagsViewRefreshRouterView?: T;
+	onCurrentContextmenuClick?: T;
+
+	// 自定义
+	submitRefresh?: string; // 通用编辑提交刷新
+	submitRefreshDictData?: string; // 字典编辑提交刷新
+	submitRefreshTable?: string; // 库表管理表刷新
+	submitRefreshColumn?: string; // 库表管理列刷新
+	addTableSubmitted?: string; // 库表管理增加表刷新
+	submitRefreshFk?: string; // 代码生成主键刷新
+};
+
+// mitt 参数类型定义
+declare type LayoutMobileResize = {
+	layout: string;
+	clientWidth: number;
+};
+
+// mitt 参数菜单类型
+declare type MittMenu = {
+	children: RouteRecordRaw[];
+	item?: RouteItem;
+};

+ 0 - 22
Web/src/types/mitt.ts

@@ -1,22 +0,0 @@
-// mitt 事件类型定义
-export type MittType = {
-	openSetingsDrawer?: string; // 打开布局设置弹窗
-	restoreDefault?: string; // 分栏布局,鼠标移入、移出数据显示
-	setSendColumnsChildren?: string; // 分栏布局,鼠标移入、移出菜单数据传入到 navMenu 下的菜单中
-	setSendClassicChildren?: string; // 经典布局,开启切割菜单时,菜单数据传入到 navMenu 下的菜单中
-	getBreadcrumbIndexSetFilterRoutes?: string; // 布局设置弹窗,开启切割菜单时,菜单数据传入到 navMenu 下的菜单中
-	layoutMobileResize?: object; // 浏览器窗口改变时,用于适配移动端界面显示
-	openOrCloseSortable?: string; // 布局设置弹窗,开启 TagsView 拖拽
-	openShareTagsView?: string; // 布局设置弹窗,开启 TagsView 共用
-	onTagsViewRefreshRouterView?: any; // tagsview 刷新界面
-	onCurrentContextmenuClick?: any; // tagsview 右键菜单每项点击时
-	setHoverPreload?: string; // 分栏菜单鼠标悬停预加载
-
-	// 自定义
-	submitRefresh?: string; // 通用编辑提交刷新
-	submitRefreshDictData?: string; // 字典编辑提交刷新
-	submitRefreshTable?: string; // 库表管理表刷新
-	submitRefreshColumn?: string; // 库表管理列刷新
-	addTableSubmitted?: string; // 库表管理增加表刷新
-	submitRefreshFk?: string; // 代码生成主键刷新
-};

+ 97 - 0
Web/src/types/pinia.d.ts

@@ -0,0 +1,97 @@
+/**
+ * pinia 类型定义
+ */
+
+// 用户信息
+declare interface UserInfosState<T = any> {
+	userInfos: {
+		account: string;
+		realName: string;
+		avatar: string;
+		address: string;
+		signature: string;
+		orgId: string;
+		orgName: string;
+		posName: string;
+		roles: string[];
+		authBtnList: string[];
+		time: number;
+		[key: string]: T;
+	};
+	constList: Array<ConstOutput>;
+}
+
+// 路由缓存列表
+declare interface KeepAliveNamesState {
+	keepAliveNames: string[];
+	cachedViews: string[];
+}
+
+// 后端返回原始路由(未处理时)
+declare interface RequestOldRoutesState {
+	requestOldRoutes: string[];
+}
+
+// TagsView 路由列表
+declare interface TagsViewRoutesState<T = any> {
+	tagsViewRoutes: T[];
+	isTagsViewCurrenFull: Boolean;
+}
+
+// 路由列表
+declare interface RoutesListState<T = any> {
+	routesList: T[];
+	isColumnsMenuHover: Boolean;
+	isColumnsNavHover: Boolean;
+}
+
+// 布局配置
+declare interface ThemeConfigState {
+	themeConfig: {
+		isDrawer: boolean;
+		primary: string;
+		topBar: string;
+		topBarColor: string;
+		isTopBarColorGradual: boolean;
+		menuBar: string;
+		menuBarColor: string;
+		isMenuBarColorGradual: boolean;
+		columnsMenuBar: string;
+		columnsMenuBarColor: string;
+		isColumnsMenuBarColorGradual: boolean;
+		isColumnsMenuHoverPreload: boolean;
+		isCollapse: boolean;
+		isUniqueOpened: boolean;
+		isFixedHeader: boolean;
+		isFixedHeaderChange: boolean;
+		isClassicSplitMenu: boolean;
+		isLockScreen: boolean;
+		lockScreenTime: number;
+		isShowLogo: boolean;
+		isShowLogoChange: boolean;
+		isBreadcrumb: boolean;
+		isTagsview: boolean;
+		isBreadcrumbIcon: boolean;
+		isTagsviewIcon: boolean;
+		isCacheTagsView: boolean;
+		isSortableTagsView: boolean;
+		isShareTagsView: boolean;
+		isFooter: boolean;
+		isGrayscale: boolean;
+		isInvert: boolean;
+		isIsDark: boolean;
+		isWartermark: boolean;
+		wartermarkText: string;
+		tagsStyle: string;
+		animation: string;
+		columnsAsideStyle: string;
+		columnsAsideLayout: string;
+		layout: string;
+		isRequestRoutes: boolean;
+		globalTitle: string;
+		globalViceTitle: string;
+		globalViceTitleMsg: string;
+		globalI18n: string;
+		globalComponentSize: string;
+	};
+}

+ 322 - 0
Web/src/types/views.d.ts

@@ -0,0 +1,322 @@
+/**
+ * views personal
+ */
+type NewInfo = {
+	title: string;
+	date: string;
+	link: string;
+};
+type Recommend = {
+	title: string;
+	msg: string;
+	icon: string;
+	bg: string;
+	iconColor: string;
+};
+declare type PersonalState = {
+	newsInfoList: NewInfo[];
+	recommendList: Recommend[];
+	personalForm: {
+		name: string;
+		email: string;
+		autograph: string;
+		occupation: string;
+		phone: string;
+		sex: string;
+	};
+};
+
+/**
+ * views visualizing
+ */
+declare type Demo2State<T = any> = {
+	time: {
+		txt: string;
+		fun: number;
+	};
+	dropdownList: T[];
+	dropdownActive: string;
+	skyList: T[];
+	dBtnList: T[];
+	chartData4Index: number;
+	dBtnActive: number;
+	earth3DBtnList: T[];
+	chartData4List: T[];
+	myCharts: T[];
+	the3DEarth: HTMLDivElement | null;
+};
+
+/**
+ * views params
+ */
+declare type ParamsState = {
+	value: string;
+	tagsViewName: string;
+	tagsViewNameIsI18n: boolean;
+};
+
+/**
+ * views system
+ */
+// role
+declare interface RowRoleType {
+	roleName: string;
+	roleSign: string;
+	describe: string;
+	sort: number;
+	status: boolean;
+	createTime: string;
+}
+
+interface SysRoleTableType extends TableType {
+	data: RowRoleType[];
+}
+
+declare interface SysRoleState {
+	tableData: SysRoleTableType;
+}
+
+declare type TreeType = {
+	id: number;
+	label: string;
+	children?: TreeType[];
+};
+
+// user
+declare type RowUserType<T = any> = {
+	userName: string;
+	userNickname: string;
+	roleSign: string;
+	department: string[];
+	phone: string;
+	email: string;
+	sex: string;
+	password: string;
+	overdueTime: T;
+	status: boolean;
+	describe: string;
+	createTime: T;
+};
+
+interface SysUserTableType extends TableType {
+	data: RowUserType[];
+}
+
+declare interface SysUserState {
+	tableData: SysUserTableType;
+}
+
+declare type DeptTreeType = {
+	deptName: string;
+	createTime: string;
+	status: boolean;
+	sort: number;
+	describe: string;
+	id: number | string;
+	children?: DeptTreeType[];
+};
+
+// dept
+declare interface RowDeptType extends DeptTreeType {
+	deptLevel: string[];
+	person: string;
+	phone: string;
+	email: string;
+}
+
+interface SysDeptTableType extends TableType {
+	data: DeptTreeType[];
+}
+
+declare interface SysDeptState {
+	tableData: SysDeptTableType;
+}
+
+// dic
+type ListType = {
+	id: number;
+	label: string;
+	value: string;
+};
+
+declare interface RowDicType {
+	dicName: string;
+	fieldName: string;
+	describe: string;
+	status: boolean;
+	createTime: string;
+	list: ListType[];
+}
+
+interface SysDicTableType extends TableType {
+	data: RowDicType[];
+}
+
+declare interface SysDicState {
+	tableData: SysDicTableType;
+}
+
+/**
+ * views pages
+ */
+//  filtering
+declare type FilteringChilType = {
+	id: number | string;
+	label: string;
+	active: boolean;
+};
+
+declare type FilterListType = {
+	img: string;
+	title: string;
+	evaluate: string;
+	collection: string;
+	price: string;
+	monSales: string;
+	id: number | string;
+	loading?: boolean;
+};
+
+declare type FilteringRowType = {
+	title: string;
+	isMore: boolean;
+	isShowMore: boolean;
+	id: number | string;
+	children: FilteringChilType[];
+};
+
+// tableRules
+declare type TableRulesHeaderType = {
+	prop: string;
+	width: string | number;
+	label: string;
+	isRequired?: boolean;
+	isTooltip?: boolean;
+	type: string;
+};
+
+declare type TableRulesState = {
+	tableData: {
+		data: EmptyObjectType[];
+		header: TableRulesHeaderType[];
+		option: SelectOptionType[];
+	};
+};
+
+declare type TableRulesOneProps = {
+	name: string;
+	email: string;
+	autograph: string;
+	occupation: string;
+};
+
+// tree
+declare type RowTreeType = {
+	id: number;
+	label: string;
+	label1: string;
+	label2: string;
+	isShow: boolean;
+	children?: RowTreeType[];
+};
+
+// workflow index
+declare type NodeListState = {
+	id: string | number;
+	nodeId: string | undefined;
+	class: HTMLElement | string;
+	left: number | string;
+	top: number | string;
+	icon: string;
+	name: string;
+};
+
+declare type LineListState = {
+	sourceId: string;
+	targetId: string;
+	label: string;
+};
+
+declare type XyState = {
+	x: string | number;
+	y: string | number;
+};
+
+declare type WorkflowState<T = any> = {
+	workflowRightRef: HTMLDivElement | null;
+	leftNavRefs: HTMLElement[];
+	leftNavList: T[];
+	dropdownNode: XyState;
+	dropdownLine: XyState;
+	isShow: boolean;
+	jsPlumb: T;
+	jsPlumbNodeIndex: null | number;
+	jsplumbDefaults: T;
+	jsplumbMakeSource: T;
+	jsplumbMakeTarget: T;
+	jsplumbConnect: T;
+	jsplumbData: {
+		nodeList: NodeListState[];
+		lineList: LineListState[];
+	};
+};
+
+// workflow drawer
+declare type WorkflowDrawerNodeState<T = any> = {
+	node: { [key: string]: T };
+	nodeRules: T;
+	form: T;
+	tabsActive: string;
+	loading: {
+		extend: boolean;
+	};
+};
+
+declare type WorkflowDrawerLabelType = {
+	type: string;
+	label: string;
+};
+
+declare type WorkflowDrawerState<T = any> = {
+	isOpen: boolean;
+	nodeData: {
+		type: string;
+	};
+	jsplumbConn: T;
+};
+
+/**
+ * views make
+ */
+// tableDemo
+declare type TableDemoPageType = {
+	pageNum: number;
+	pageSize: number;
+};
+
+declare type TableHeaderType = {
+	key: string;
+	width: string;
+	title: string;
+	type: string | number;
+	colWidth: string;
+	width?: string | number;
+	height?: string | number;
+	isCheck: boolean;
+};
+
+declare type TableDemoState = {
+	tableData: {
+		data: EmptyObjectType[];
+		header: TableHeaderType[];
+		param: EmptyObjectType;
+		config: {
+			total: number;
+			loading: boolean;
+			isBorder: boolean;
+			isSelection: boolean;
+			isSerialNo: boolean;
+			isOperate: boolean;
+		};
+	};
+};

+ 6 - 7
Web/src/utils/arrayOperation.ts

@@ -23,7 +23,7 @@ export function judementSameArr(newArr: unknown[] | string[], oldArr: string[]):
  * @param b 要比较的对象二
  * @returns 相同返回 true,反之则反
  */
-export function isObjectValueEqual(a: { [key: string]: any }, b: { [key: string]: any }) {
+export function isObjectValueEqual<T>(a: T, b: T): boolean {
 	if (!a || !b) return false;
 	let aProps = Object.getOwnPropertyNames(a);
 	let bProps = Object.getOwnPropertyNames(b);
@@ -48,19 +48,18 @@ export function isObjectValueEqual(a: { [key: string]: any }, b: { [key: string]
  * @param attr 需要去重的键值(数组对象)
  * @returns
  */
-export function removeDuplicate(arr: any, attr?: string) {
-	if (!arr && !arr.length) {
+export function removeDuplicate(arr: EmptyArrayType, attr?: string) {
+	if (!Object.keys(arr).length) {
 		return arr;
 	} else {
 		if (attr) {
-			const obj: any = {};
-			const newArr = arr.reduce((cur: any, item: any) => {
+			const obj: EmptyObjectType = {};
+			return arr.reduce((cur: EmptyArrayType[], item: EmptyArrayType) => {
 				obj[item[attr]] ? '' : (obj[item[attr]] = true && item[attr] && cur.push(item));
 				return cur;
 			}, []);
-			return newArr;
 		} else {
-			return Array.from(new Set([...arr]));
+			return [...new Set(arr)];
 		}
 	}
 }

+ 11 - 10
Web/src/utils/commonFunction.ts

@@ -7,22 +7,23 @@ import { useI18n } from 'vue-i18n';
 export default function () {
 	const { t } = useI18n();
 	const { toClipboard } = useClipboard();
-	//百分比格式化
-	const percentFormat = (row: any, column: number, cellValue: any) => {
+
+	// 百分比格式化
+	const percentFormat = (row: EmptyArrayType, column: number, cellValue: string) => {
 		return cellValue ? `${cellValue}%` : '-';
 	};
-	//列表日期时间格式化
-	const dateFormatYMD = (row: any, column: number, cellValue: any) => {
+	// 列表日期时间格式化
+	const dateFormatYMD = (row: EmptyArrayType, column: number, cellValue: string) => {
 		if (!cellValue) return '-';
 		return formatDate(new Date(cellValue), 'YYYY-mm-dd');
 	};
-	//列表日期时间格式化
-	const dateFormatYMDHMS = (row: any, column: number, cellValue: any) => {
+	// 列表日期时间格式化
+	const dateFormatYMDHMS = (row: EmptyArrayType, column: number, cellValue: string) => {
 		if (!cellValue) return '-';
 		return formatDate(new Date(cellValue), 'YYYY-mm-dd HH:MM:SS');
 	};
-	//列表日期时间格式化
-	const dateFormatHMS = (row: any, column: number, cellValue: any) => {
+	// 列表日期时间格式化
+	const dateFormatHMS = (row: EmptyArrayType, column: number, cellValue: string) => {
 		if (!cellValue) return '-';
 		let time = 0;
 		if (typeof row === 'number') time = row;
@@ -30,11 +31,11 @@ export default function () {
 		return formatDate(new Date(time * 1000), 'HH:MM:SS');
 	};
 	// 小数格式化
-	const scaleFormat = (value: any = 0, scale: number = 4) => {
+	const scaleFormat = (value: string = '0', scale: number = 4) => {
 		return Number.parseFloat(value).toFixed(scale);
 	};
 	// 小数格式化
-	const scale2Format = (value: any = 0) => {
+	const scale2Format = (value: string = '0') => {
 		return Number.parseFloat(value).toFixed(2);
 	};
 	// 点击复制文本

+ 0 - 1
Web/src/utils/mitt.ts

@@ -1,6 +1,5 @@
 // https://www.npmjs.com/package/mitt
 import mitt, { Emitter } from 'mitt';
-import { MittType } from '/@/types/mitt';
 
 // 类型
 const emitter: Emitter<MittType> = mitt<MittType>();

+ 31 - 13
Web/src/utils/other.ts

@@ -1,4 +1,4 @@
-import { nextTick } from 'vue';
+import { nextTick, defineAsyncComponent } from 'vue';
 import type { App } from 'vue';
 import * as svg from '@element-plus/icons-vue';
 import router from '/@/router/index';
@@ -7,7 +7,10 @@ import { storeToRefs } from 'pinia';
 import { useThemeConfig } from '/@/stores/themeConfig';
 import { i18n } from '/@/i18n/index';
 import { Local } from '/@/utils/storage';
-import SvgIcon from '/@/components/svgIcon/index.vue';
+import { verifyUrl } from '/@/utils/toolsValidate';
+
+// 引入组件
+const SvgIcon = defineAsyncComponent(() => import('/@/components/svgIcon/index.vue'));
 
 /**
  * 导出全局注册 element plus svg 图标
@@ -34,7 +37,7 @@ export function useTitle() {
 		let globalTitle: string = themeConfig.value.globalTitle;
 		const { path, meta } = router.currentRoute.value;
 		if (path === '/login') {
-			webTitle = <any>meta.title;
+			webTitle = <string>meta.title;
 		} else {
 			webTitle = setTagsViewNameI18n(router.currentRoute.value);
 		}
@@ -48,7 +51,7 @@ export function useTitle() {
  * @returns 返回当前 tagsViewName 名称
  */
 export function setTagsViewNameI18n(item: any) {
-	let tagsViewName: any = '';
+	let tagsViewName: string = '';
 	const { query, params, meta } = item;
 	if (query?.tagsViewName || params?.tagsViewName) {
 		if (/\/zh-cn|en|zh-tw\//.test(query?.tagsViewName) || /\/zh-cn|en|zh-tw\//.test(params?.tagsViewName)) {
@@ -61,7 +64,7 @@ export function setTagsViewNameI18n(item: any) {
 		}
 	} else {
 		// 非自定义 tagsView 名称
-		tagsViewName = i18n.global.t(<any>meta.title);
+		tagsViewName = i18n.global.t(meta.title);
 	}
 	return tagsViewName;
 }
@@ -72,7 +75,7 @@ export function setTagsViewNameI18n(item: any) {
  * @param arr 列表数据
  * @description data-xxx 属性用于存储页面或应用程序的私有自定义数据
  */
-export const lazyImg = (el: any, arr: any) => {
+export const lazyImg = (el: string, arr: EmptyArrayType) => {
 	const io = new IntersectionObserver((res) => {
 		res.forEach((v: any) => {
 			if (v.isIntersecting) {
@@ -105,8 +108,8 @@ export const globalComponentSize = (): string => {
  * @param obj 源对象
  * @returns 克隆后的对象
  */
-export function deepClone(obj: any) {
-	let newObj: any;
+export function deepClone(obj: EmptyObjectType) {
+	let newObj: EmptyObjectType;
 	try {
 		newObj = obj.push ? [] : {};
 	} catch (error) {
@@ -143,7 +146,7 @@ export function isMobile() {
  * @param list 数组对象
  * @returns 删除空值后的数组对象
  */
-export function handleEmpty(list: any) {
+export function handleEmpty(list: EmptyArrayType) {
 	const arr = [];
 	for (const i in list) {
 		const d = [];
@@ -158,6 +161,17 @@ export function handleEmpty(list: any) {
 	return arr;
 }
 
+/**
+ * 打开外部链接
+ * @param val 当前点击项菜单
+ */
+export function handleOpenLink(val: RouteItem) {
+	const { origin, pathname } = window.location;
+	router.push(val.path);
+	if (verifyUrl(<string>val.meta?.isLink)) window.open(val.meta?.isLink);
+	else window.open(`${origin}${pathname}#${val.meta?.isLink}`);
+}
+
 /**
  * 统一批量导出
  * @method elSvg 导出全局注册 element plus svg 图标
@@ -168,6 +182,7 @@ export function handleEmpty(list: any) {
  * @method deepClone 对象深克隆
  * @method isMobile 判断是否是移动端
  * @method handleEmpty 判断数组对象中所有属性是否为空,为空则删除当前行对象
+ * @method handleOpenLink 打开外部链接
  */
 const other = {
 	elSvg: (app: App) => {
@@ -176,24 +191,27 @@ const other = {
 	useTitle: () => {
 		useTitle();
 	},
-	setTagsViewNameI18n(route: any) {
+	setTagsViewNameI18n(route: RouteToFrom) {
 		return setTagsViewNameI18n(route);
 	},
-	lazyImg: (el: any, arr: any) => {
+	lazyImg: (el: string, arr: EmptyArrayType) => {
 		lazyImg(el, arr);
 	},
 	globalComponentSize: () => {
 		return globalComponentSize();
 	},
-	deepClone: (obj: any) => {
+	deepClone: (obj: EmptyObjectType) => {
 		return deepClone(obj);
 	},
 	isMobile: () => {
 		return isMobile();
 	},
-	handleEmpty: (list: any) => {
+	handleEmpty: (list: EmptyArrayType) => {
 		return handleEmpty(list);
 	},
+	handleOpenLink: (val: RouteItem) => {
+		handleOpenLink(val);
+	},
 };
 
 // 统一批量导出

+ 24 - 163
Web/src/utils/request.ts

@@ -1,74 +1,20 @@
-import axios, { AxiosInstance } from 'axios';
-import { ElMessage } from 'element-plus';
-import { Local, Session } from '/@/utils/storage';
+import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { Session } from '/@/utils/storage';
 
 // 配置新建一个 axios 实例
-export const service = axios.create({
-	baseURL: import.meta.env.VITE_API_URL as any,
+const service: AxiosInstance = axios.create({
+	baseURL: import.meta.env.VITE_API_URL,
 	timeout: 50000,
 	headers: { 'Content-Type': 'application/json' },
 });
 
-// token 键定义
-export const accessTokenKey = 'access-token';
-export const refreshAccessTokenKey = `x-${accessTokenKey}`;
-
-// 获取 token
-export const getToken = () => {
-	return Local.get(accessTokenKey);
-};
-
-// 清除 token
-export const clearAccessTokens = () => {
-	Local.remove(accessTokenKey);
-	Local.remove(refreshAccessTokenKey);
-
-	// 清除其他
-	Session.clear();
-
-	// 刷新浏览器
-	window.location.reload();
-};
-
-// axios 默认实例
-export const axiosInstance: AxiosInstance = axios;
-
 // 添加请求拦截器
 service.interceptors.request.use(
-	(config) => {
-		// // 在发送请求之前做些什么 token
-		// if (Session.get('token')) {
-		// 	(<any>config.headers).common['Authorization'] = `${Session.get('token')}`;
-		// }
-
-		// 获取本地的 token
-		const accessToken = Local.get(accessTokenKey);
-		if (accessToken) {
-			// 将 token 添加到请求报文头中
-			config.headers!['Authorization'] = `Bearer ${accessToken}`;
-
-			// 判断 accessToken 是否过期
-			const jwt: any = decryptJWT(accessToken);
-			const exp = getJWTDate(jwt.exp as number);
-
-			// token 已经过期
-			if (new Date() >= exp) {
-				// 获取刷新 token
-				const refreshAccessToken = Local.get(refreshAccessTokenKey);
-
-				// 携带刷新 token
-				if (refreshAccessToken) {
-					config.headers!['X-Authorization'] = `Bearer ${refreshAccessToken}`;
-				}
-			}
-			// debugger
-			// get请求映射params参数
-			if (config.method?.toLowerCase() === 'get' && config.data) {
-				let url = config.url + '?' + tansParams(config.data);
-				url = url.slice(0, -1);
-				config.data = {};
-				config.url = url;
-			}
+	(config: AxiosRequestConfig) => {
+		// 在发送请求之前做些什么 token
+		if (Session.get('token')) {
+			config.headers!['Authorization'] = `${Session.get('token')}`;
 		}
 		return config;
 	},
@@ -80,61 +26,24 @@ service.interceptors.request.use(
 
 // 添加响应拦截器
 service.interceptors.response.use(
-	(res) => {
-		// 获取状态码和返回数据
-		var status = res.status;
-		var serve = res.data;
-
-		// 处理 401
-		if (status === 401) {
-			clearAccessTokens();
-		}
-
-		// 处理未进行规范化处理的
-		if (status >= 400) {
-			throw new Error(res.statusText || 'Request Error.');
-		}
-
-		// 处理规范化结果错误
-		if (serve && serve.hasOwnProperty('errors') && serve.errors) {
-			throw new Error(JSON.stringify(serve.errors || 'Request Error.'));
-		}
-
-		// 读取响应报文头 token 信息
-		var accessToken = res.headers[accessTokenKey];
-		var refreshAccessToken = res.headers[refreshAccessTokenKey];
-
-		// 判断是否是无效 token
-		if (accessToken === 'invalid_token') {
-			clearAccessTokens();
-		}
-		// 判断是否存在刷新 token,如果存在则存储在本地
-		else if (refreshAccessToken && accessToken && accessToken !== 'invalid_token') {
-			Local.set(accessTokenKey, accessToken);
-			Local.set(refreshAccessTokenKey, refreshAccessToken);
-		}
-
-		// 响应拦截及自定义处理
-		if (serve.code === 401) {
-			clearAccessTokens();
-		} else if (serve.code === undefined) {
-			return Promise.resolve(res);
-		} else if (serve.code !== 200) {
-			var message = JSON.stringify(serve.message);
-			ElMessage.error(message);
-			throw new Error(message);
+	(response) => {
+		// 对响应数据做点什么
+		const res = response.data;
+		if (res.code && res.code !== 0) {
+			// `token` 过期或者账号已在别处登录
+			if (res.code === 401 || res.code === 4001) {
+				Session.clear(); // 清除浏览器全部临时缓存
+				window.location.href = '/'; // 去登录页
+				ElMessageBox.alert('你已被登出,请重新登录', '提示', {})
+					.then(() => {})
+					.catch(() => {});
+			}
+			return Promise.reject(service.interceptors.response);
+		} else {
+			return response.data;
 		}
-
-		return res;
 	},
 	(error) => {
-		// 处理响应错误
-		if (error.response) {
-			if (error.response.status === 401) {
-				clearAccessTokens();
-			}
-		}
-
 		// 对响应错误做点什么
 		if (error.message.indexOf('timeout') != -1) {
 			ElMessage.error('网络超时');
@@ -144,57 +53,9 @@ service.interceptors.response.use(
 			if (error.response.data) ElMessage.error(error.response.statusText);
 			else ElMessage.error('接口路径找不到');
 		}
-
 		return Promise.reject(error);
 	}
 );
 
-/**
- *  参数处理
- * @param {*} params  参数
- */
-export function tansParams(params: any) {
-	let result = '';
-	for (const propName of Object.keys(params)) {
-		const value = params[propName];
-		var part = encodeURIComponent(propName) + '=';
-		if (value !== null && value !== '' && typeof value !== 'undefined') {
-			if (typeof value === 'object') {
-				for (const key of Object.keys(value)) {
-					if (value[key] !== null && value[key] !== '' && typeof value[key] !== 'undefined') {
-						let params = propName + '[' + key + ']';
-						var subPart = encodeURIComponent(params) + '=';
-						result += subPart + encodeURIComponent(value[key]) + '&';
-					}
-				}
-			} else {
-				result += part + encodeURIComponent(value) + '&';
-			}
-		}
-	}
-	return result;
-}
-
-/**
- * 解密 JWT token 的信息
- * @param token jwt token 字符串
- * @returns <any>object
- */
-export function decryptJWT(token: string): any {
-	token = token.replace(/_/g, '/').replace(/-/g, '+');
-	var json = decodeURIComponent(escape(window.atob(token.split('.')[1])));
-	return JSON.parse(json);
-}
-
-/**
- * 将 JWT 时间戳转换成 Date
- * @description 主要针对 `exp`,`iat`,`nbf`
- * @param timestamp 时间戳
- * @returns Date 对象
- */
-export function getJWTDate(timestamp: number): Date {
-	return new Date(timestamp * 1000);
-}
-
 // 导出 axios 实例
 export default service;

+ 1 - 1
Web/src/utils/setIconfont.ts

@@ -1,6 +1,6 @@
 // 字体图标 url
 const cssCdnUrlList: Array<string> = [
-	'//at.alicdn.com/t/font_2298093_y6u00apwst.css',
+	'//at.alicdn.com/t/c/font_2298093_rnp72ifj3ba.css',
 	'//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css',
 ];
 // 第三方 js url

+ 2 - 2
Web/src/utils/storage.ts

@@ -14,7 +14,7 @@ export const Local = {
 	},
 	// 获取永久缓存
 	get(key: string) {
-		let json: any = window.localStorage.getItem(key);
+		let json = <string>window.localStorage.getItem(key);
 		return JSON.parse(json);
 	},
 	// 移除永久缓存
@@ -43,7 +43,7 @@ export const Session = {
 	// 获取临时缓存
 	get(key: string) {
 		if (key === 'token') return Cookies.get(key);
-		let json: any = window.sessionStorage.getItem(key);
+		let json = <string>window.sessionStorage.getItem(key);
 		return JSON.parse(json);
 	},
 	// 移除临时缓存

+ 58 - 54
Web/src/utils/theme.ts

@@ -1,59 +1,63 @@
 import { ElMessage } from 'element-plus';
 
 /**
- * hex颜色转rgb颜色
- * @param str 颜色值字符串
- * @returns 返回处理后的颜色值
+ * 颜色转换函数
+ * @method hexToRgb hex 颜色转 rgb 颜色
+ * @method rgbToHex rgb 颜色转 Hex 颜色
+ * @method getDarkColor 加深颜色值
+ * @method getLightColor 变浅颜色值
  */
-export function hexToRgb(str: any) {
-	let hexs: any = '';
-	let reg = /^\#?[0-9A-Fa-f]{6}$/;
-	if (!reg.test(str)) return ElMessage.warning('输入错误的hex');
-	str = str.replace('#', '');
-	hexs = str.match(/../g);
-	for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16);
-	return hexs;
-}
-
-/**
- * rgb颜色转Hex颜色
- * @param r 代表红色
- * @param g 代表绿色
- * @param b 代表蓝色
- * @returns 返回处理后的颜色值
- */
-export function rgbToHex(r: any, g: any, b: any) {
-	let reg = /^\d{1,3}$/;
-	if (!reg.test(r) || !reg.test(g) || !reg.test(b)) return ElMessage.warning('输入错误的rgb颜色值');
-	let hexs = [r.toString(16), g.toString(16), b.toString(16)];
-	for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}`;
-	return `#${hexs.join('')}`;
-}
-
-/**
- * 加深颜色值
- * @param color 颜色值字符串
- * @param level 加深的程度,限0-1之间
- * @returns 返回处理后的颜色值
- */
-export function getDarkColor(color: string, level: number) {
-	let reg = /^\#?[0-9A-Fa-f]{6}$/;
-	if (!reg.test(color)) return ElMessage.warning('输入错误的hex颜色值');
-	let rgb = hexToRgb(color);
-	for (let i = 0; i < 3; i++) rgb[i] = Math.floor(rgb[i] * (1 - level));
-	return rgbToHex(rgb[0], rgb[1], rgb[2]);
-}
-
-/**
- * 变浅颜色值
- * @param color 颜色值字符串
- * @param level 加深的程度,限0-1之间
- * @returns 返回处理后的颜色值
- */
-export function getLightColor(color: string, level: number) {
-	let reg = /^\#?[0-9A-Fa-f]{6}$/;
-	if (!reg.test(color)) return ElMessage.warning('输入错误的hex颜色值');
-	let rgb = hexToRgb(color);
-	for (let i = 0; i < 3; i++) rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i]);
-	return rgbToHex(rgb[0], rgb[1], rgb[2]);
+export function useChangeColor() {
+	// str 颜色值字符串
+	const hexToRgb = (str: string): any => {
+		let hexs: any = '';
+		let reg = /^\#?[0-9A-Fa-f]{6}$/;
+		if (!reg.test(str)) {
+			ElMessage.warning('输入错误的hex');
+			return '';
+		}
+		str = str.replace('#', '');
+		hexs = str.match(/../g);
+		for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16);
+		return hexs;
+	};
+	// r 代表红色 | g 代表绿色 | b 代表蓝色
+	const rgbToHex = (r: any, g: any, b: any): string => {
+		let reg = /^\d{1,3}$/;
+		if (!reg.test(r) || !reg.test(g) || !reg.test(b)) {
+			ElMessage.warning('输入错误的rgb颜色值');
+			return '';
+		}
+		let hexs = [r.toString(16), g.toString(16), b.toString(16)];
+		for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}`;
+		return `#${hexs.join('')}`;
+	};
+	// color 颜色值字符串 | level 变浅的程度,限0-1之间
+	const getDarkColor = (color: string, level: number): string => {
+		let reg = /^\#?[0-9A-Fa-f]{6}$/;
+		if (!reg.test(color)) {
+			ElMessage.warning('输入错误的hex颜色值');
+			return '';
+		}
+		let rgb = useChangeColor().hexToRgb(color);
+		for (let i = 0; i < 3; i++) rgb[i] = Math.floor(rgb[i] * (1 - level));
+		return useChangeColor().rgbToHex(rgb[0], rgb[1], rgb[2]);
+	};
+	// color 颜色值字符串 | level 加深的程度,限0-1之间
+	const getLightColor = (color: string, level: number): string => {
+		let reg = /^\#?[0-9A-Fa-f]{6}$/;
+		if (!reg.test(color)) {
+			ElMessage.warning('输入错误的hex颜色值');
+			return '';
+		}
+		let rgb = useChangeColor().hexToRgb(color);
+		for (let i = 0; i < 3; i++) rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i]);
+		return useChangeColor().rgbToHex(rgb[0], rgb[1], rgb[2]);
+	};
+	return {
+		hexToRgb,
+		rgbToHex,
+		getDarkColor,
+		getLightColor,
+	};
 }

+ 2 - 2
Web/src/utils/wartermark.ts

@@ -5,11 +5,11 @@ const setWatermark = (str: string) => {
 	const can = document.createElement('canvas');
 	can.width = 200;
 	can.height = 130;
-	const cans: any = can.getContext('2d');
+	const cans = <CanvasRenderingContext2D>can.getContext('2d');
 	cans.rotate((-20 * Math.PI) / 180);
 	cans.font = '12px Vedana';
 	cans.fillStyle = 'rgba(200, 200, 200, 0.30)';
-	cans.textBaseline = 'Middle';
+	cans.textBaseline = 'middle';
 	cans.fillText(str, can.width / 10, can.height / 2);
 	const div = document.createElement('div');
 	div.id = id;

+ 24 - 30
Web/src/views/chart/head.vue

@@ -2,7 +2,7 @@
 	<div class="big-data-up mb15">
 		<div class="up-left">
 			<i class="el-icon-time mr5"></i>
-			<span>{{ time.txt }}</span>
+			<span>{{ state.time.txt }}</span>
 		</div>
 		<div class="up-center">
 			<span>智慧农业系统平台</span>
@@ -10,39 +10,33 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { reactive, toRefs, onBeforeMount, onUnmounted, defineComponent } from 'vue';
+<script setup lang="ts" name="chartHead">
+import { reactive, onBeforeMount, onUnmounted } from 'vue';
 import { formatDate } from '/@/utils/formatTime';
 
-export default defineComponent({
-	name: 'chartHead',
-	setup() {
-		const state = reactive({
-			time: {
-				txt: '',
-				fun: 0,
-			},
-		});
-		// 初始化时间
-		const initTime = () => {
-			state.time.txt = formatDate(new Date(), 'YYYY-mm-dd HH:MM:SS WWW QQQQ');
-			state.time.fun = window.setInterval(() => {
-				state.time.txt = formatDate(new Date(), 'YYYY-mm-dd HH:MM:SS WWW QQQQ');
-			}, 1000);
-		};
-		// 页面加载前
-		onBeforeMount(() => {
-			initTime();
-		});
-		// 页面卸载时
-		onUnmounted(() => {
-			window.clearInterval(state.time.fun);
-		});
-		return {
-			...toRefs(state),
-		};
+// 定义变量内容
+const state = reactive({
+	time: {
+		txt: '',
+		fun: 0,
 	},
 });
+
+// 初始化时间
+const initTime = () => {
+	state.time.txt = formatDate(new Date(), 'YYYY-mm-dd HH:MM:SS WWW QQQQ');
+	state.time.fun = window.setInterval(() => {
+		state.time.txt = formatDate(new Date(), 'YYYY-mm-dd HH:MM:SS WWW QQQQ');
+	}, 1000);
+};
+// 页面加载前
+onBeforeMount(() => {
+	initTime();
+});
+// 页面卸载时
+onUnmounted(() => {
+	window.clearInterval(state.time.fun);
+});
 </script>
 
 <style scoped lang="scss">

+ 258 - 268
Web/src/views/chart/index.vue

@@ -26,7 +26,7 @@
 									</div>
 								</div>
 								<div class="sky-dd">
-									<div class="sky-dl" v-for="(v, k) in skyList" :key="k" :class="{ 'sky-dl-first': k === 1 }">
+									<div class="sky-dl" v-for="(v, k) in state.skyList" :key="k" :class="{ 'sky-dl-first': k === 1 }">
 										<div>{{ v.v1 }}</div>
 										<div v-if="v.type === 'title'">{{ v.v2 }}</div>
 										<div v-else>
@@ -68,7 +68,7 @@
 									</div>
 								</div>
 								<div class="d-btn">
-									<div class="d-btn-item" v-for="(v, k) in dBtnList" :key="k">
+									<div class="d-btn-item" v-for="(v, k) in state.dBtnList" :key="k">
 										<i class="d-btn-item-left el-icon-money"></i>
 										<div class="d-btn-item-center">
 											<div>{{ v.v2 }}|{{ v.v3 }}</div>
@@ -104,7 +104,7 @@
 							</div>
 							<div class="flex-content">
 								<div class="flex-content-left">
-									<div class="monitor-item" v-for="(v, k) in chartData4List" :key="k">
+									<div class="monitor-item" v-for="(v, k) in state.chartData4List" :key="k">
 										<div class="monitor-wave">
 											<div class="monitor-z-index">
 												<div class="monitor-item-label">{{ v.label }}</div>
@@ -201,285 +201,275 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { toRefs, reactive, onMounted, watch, nextTick, onActivated, defineComponent, ref } from 'vue';
-import ChartHead from '/@/views/chart/head.vue';
+<script setup lang="ts" name="chartIndex">
+import { defineAsyncComponent, reactive, onMounted, watch, nextTick, onActivated, ref } from 'vue';
 import * as echarts from 'echarts';
 import 'echarts-wordcloud';
 import { storeToRefs } from 'pinia';
 import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
 import { skyList, dBtnList, chartData4List } from '/@/views/chart/chart';
 
-export default defineComponent({
-	name: 'chartIndex',
-	components: { ChartHead },
-	setup() {
-		const chartsCenterOneRef = ref();
-		const chartsSevenDaysRef = ref();
-		const chartsWarningRef = ref();
-		const chartsMonitorRef = ref();
-		const chartsInvestmentRef = ref();
-		const storesTagsViewRoutes = useTagsViewRoutes();
-		const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
-		const state = reactive({
-			skyList,
-			dBtnList,
-			chartData4List,
-			myCharts: [],
-		});
-		// 初始化中间图表1
-		const initChartsCenterOne = () => {
-			const myChart = echarts.init(chartsCenterOneRef.value);
-			const option = {
-				grid: {
-					top: 15,
-					right: 15,
-					bottom: 20,
-					left: 30,
-				},
-				tooltip: {},
-				series: [
-					{
-						type: 'wordCloud',
-						sizeRange: [12, 40],
-						rotationRange: [0, 0],
-						rotationStep: 45,
-						gridSize: Math.random() * 20 + 5,
-						shape: 'circle',
-						width: '100%',
-						height: '100%',
-						textStyle: {
-							fontFamily: 'sans-serif',
-							fontWeight: 'bold',
-							color: function () {
-								return `rgb(${[Math.round(Math.random() * 160), Math.round(Math.random() * 160), Math.round(Math.random() * 160)].join(',')})`;
-							},
-						},
-						data: [
-							{ name: 'vue-next-admin', value: 520 },
-							{ name: 'lyt', value: 520 },
-							{ name: 'next-admin', value: 500 },
-							{ name: '更名', value: 420 },
-							{ name: '智慧农业', value: 520 },
-							{ name: '男神', value: 2.64 },
-							{ name: '好身材', value: 4.03 },
-							{ name: '校草', value: 24.95 },
-							{ name: '酷', value: 4.04 },
-							{ name: '时尚', value: 5.27 },
-							{ name: '阳光活力', value: 5.8 },
-							{ name: '初恋', value: 3.09 },
-							{ name: '英俊潇洒', value: 24.71 },
-							{ name: '霸气', value: 6.33 },
-							{ name: '腼腆', value: 2.55 },
-							{ name: '蠢萌', value: 3.88 },
-							{ name: '青春', value: 8.04 },
-							{ name: '网红', value: 5.87 },
-							{ name: '萌', value: 6.97 },
-							{ name: '认真', value: 2.53 },
-							{ name: '古典', value: 2.49 },
-							{ name: '温柔', value: 3.91 },
-							{ name: '有个性', value: 3.25 },
-							{ name: '可爱', value: 9.93 },
-							{ name: '幽默诙谐', value: 3.65 },
-						],
+// 引入组件
+const ChartHead = defineAsyncComponent(() => import('/@/views/chart/head.vue'));
+
+// 定义变量内容
+const chartsCenterOneRef = ref();
+const chartsSevenDaysRef = ref();
+const chartsWarningRef = ref();
+const chartsMonitorRef = ref();
+const chartsInvestmentRef = ref();
+const storesTagsViewRoutes = useTagsViewRoutes();
+const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
+const state = reactive({
+	skyList,
+	dBtnList,
+	chartData4List,
+	myCharts: [] as EmptyArrayType,
+});
+
+// 初始化中间图表1
+const initChartsCenterOne = () => {
+	const myChart = echarts.init(chartsCenterOneRef.value);
+	const option = {
+		grid: {
+			top: 15,
+			right: 15,
+			bottom: 20,
+			left: 30,
+		},
+		tooltip: {},
+		series: [
+			{
+				type: 'wordCloud',
+				sizeRange: [12, 40],
+				rotationRange: [0, 0],
+				rotationStep: 45,
+				gridSize: Math.random() * 20 + 5,
+				shape: 'circle',
+				width: '100%',
+				height: '100%',
+				textStyle: {
+					fontFamily: 'sans-serif',
+					fontWeight: 'bold',
+					color: function () {
+						return `rgb(${[Math.round(Math.random() * 160), Math.round(Math.random() * 160), Math.round(Math.random() * 160)].join(',')})`;
 					},
-				],
-			};
-			myChart.setOption(option);
-			(<any>state.myCharts).push(myChart);
-		};
-		// 初始化近7天产品追溯扫码统计
-		const initChartsSevenDays = () => {
-			const myChart = echarts.init(chartsSevenDaysRef.value);
-			const option = {
-				grid: {
-					top: 15,
-					right: 15,
-					bottom: 20,
-					left: 30,
-				},
-				tooltip: {
-					trigger: 'axis',
 				},
-				xAxis: {
-					type: 'category',
-					boundaryGap: false,
-					data: ['1天', '2天', '3天', '4天', '5天', '6天', '7天'],
-				},
-				yAxis: {
-					type: 'value',
-				},
-				series: [
-					{
-						name: '邮件营销',
-						type: 'line',
-						stack: '总量',
-						data: [12, 32, 11, 34, 90, 23, 21],
-					},
-					{
-						name: '联盟广告',
-						type: 'line',
-						stack: '总量',
-						data: [22, 82, 91, 24, 90, 30, 30],
-					},
-					{
-						name: '视频广告',
-						type: 'line',
-						stack: '总量',
-						data: [50, 32, 18, 14, 90, 30, 50],
-					},
+				data: [
+					{ name: 'vue-next-admin', value: 520 },
+					{ name: 'lyt', value: 520 },
+					{ name: 'next-admin', value: 500 },
+					{ name: '更名', value: 420 },
+					{ name: '智慧农业', value: 520 },
+					{ name: '男神', value: 2.64 },
+					{ name: '好身材', value: 4.03 },
+					{ name: '校草', value: 24.95 },
+					{ name: '酷', value: 4.04 },
+					{ name: '时尚', value: 5.27 },
+					{ name: '阳光活力', value: 5.8 },
+					{ name: '初恋', value: 3.09 },
+					{ name: '英俊潇洒', value: 24.71 },
+					{ name: '霸气', value: 6.33 },
+					{ name: '腼腆', value: 2.55 },
+					{ name: '蠢萌', value: 3.88 },
+					{ name: '青春', value: 8.04 },
+					{ name: '网红', value: 5.87 },
+					{ name: '萌', value: 6.97 },
+					{ name: '认真', value: 2.53 },
+					{ name: '古典', value: 2.49 },
+					{ name: '温柔', value: 3.91 },
+					{ name: '有个性', value: 3.25 },
+					{ name: '可爱', value: 9.93 },
+					{ name: '幽默诙谐', value: 3.65 },
 				],
-			};
-			myChart.setOption(option);
-			(<any>state.myCharts).push(myChart);
-		};
-		// 初始化近30天预警总数
-		const initChartsWarning = () => {
-			const myChart = echarts.init(chartsWarningRef.value);
-			const option = {
-				grid: {
-					top: 50,
-					right: 20,
-					bottom: 30,
-					left: 30,
-				},
-				tooltip: {
-					trigger: 'item',
+			},
+		],
+	};
+	myChart.setOption(option);
+	state.myCharts.push(myChart);
+};
+// 初始化近7天产品追溯扫码统计
+const initChartsSevenDays = () => {
+	const myChart = echarts.init(chartsSevenDaysRef.value);
+	const option = {
+		grid: {
+			top: 15,
+			right: 15,
+			bottom: 20,
+			left: 30,
+		},
+		tooltip: {
+			trigger: 'axis',
+		},
+		xAxis: {
+			type: 'category',
+			boundaryGap: false,
+			data: ['1天', '2天', '3天', '4天', '5天', '6天', '7天'],
+		},
+		yAxis: {
+			type: 'value',
+		},
+		series: [
+			{
+				name: '邮件营销',
+				type: 'line',
+				stack: '总量',
+				data: [12, 32, 11, 34, 90, 23, 21],
+			},
+			{
+				name: '联盟广告',
+				type: 'line',
+				stack: '总量',
+				data: [22, 82, 91, 24, 90, 30, 30],
+			},
+			{
+				name: '视频广告',
+				type: 'line',
+				stack: '总量',
+				data: [50, 32, 18, 14, 90, 30, 50],
+			},
+		],
+	};
+	myChart.setOption(option);
+	state.myCharts.push(myChart);
+};
+// 初始化近30天预警总数
+const initChartsWarning = () => {
+	const myChart = echarts.init(chartsWarningRef.value);
+	const option = {
+		grid: {
+			top: 50,
+			right: 20,
+			bottom: 30,
+			left: 30,
+		},
+		tooltip: {
+			trigger: 'item',
+		},
+		series: [
+			{
+				name: '面积模式',
+				type: 'pie',
+				radius: [20, 50],
+				center: ['50%', '50%'],
+				roseType: 'area',
+				itemStyle: {
+					borderRadius: 8,
 				},
-				series: [
-					{
-						name: '面积模式',
-						type: 'pie',
-						radius: [20, 50],
-						center: ['50%', '50%'],
-						roseType: 'area',
-						itemStyle: {
-							borderRadius: 8,
-						},
-						data: [
-							{ value: 40, name: '监测设备预警' },
-							{ value: 38, name: '天气预警' },
-							{ value: 32, name: '任务预警' },
-							{ value: 30, name: '病虫害预警' },
-						],
-					},
+				data: [
+					{ value: 40, name: '监测设备预警' },
+					{ value: 38, name: '天气预警' },
+					{ value: 32, name: '任务预警' },
+					{ value: 30, name: '病虫害预警' },
 				],
-			};
-			myChart.setOption(option);
-			(<any>state.myCharts).push(myChart);
-		};
-		// 初始化当前设备监测
-		const initChartsMonitor = () => {
-			const myChart = echarts.init(chartsMonitorRef.value);
-			const option = {
-				grid: {
-					top: 15,
-					right: 15,
-					bottom: 20,
-					left: 30,
-				},
-				tooltip: {
-					trigger: 'axis',
-				},
-				xAxis: {
-					type: 'category',
-					boundaryGap: false,
-					data: ['02:00', '04:00', '06:00', '08:00', '10:00', '12:00', '14:00'],
-				},
-				yAxis: {
-					type: 'value',
-				},
-				series: [
-					{
-						itemStyle: {
-							color: '#289df5',
-							borderColor: '#289df5',
-							areaStyle: {
-								type: 'default',
-								opacity: 0.1,
-							},
-						},
-						data: [20, 32, 31, 34, 12, 13, 20],
-						type: 'line',
-						areaStyle: {},
+			},
+		],
+	};
+	myChart.setOption(option);
+	state.myCharts.push(myChart);
+};
+// 初始化当前设备监测
+const initChartsMonitor = () => {
+	const myChart = echarts.init(chartsMonitorRef.value);
+	const option = {
+		grid: {
+			top: 15,
+			right: 15,
+			bottom: 20,
+			left: 30,
+		},
+		tooltip: {
+			trigger: 'axis',
+		},
+		xAxis: {
+			type: 'category',
+			boundaryGap: false,
+			data: ['02:00', '04:00', '06:00', '08:00', '10:00', '12:00', '14:00'],
+		},
+		yAxis: {
+			type: 'value',
+		},
+		series: [
+			{
+				itemStyle: {
+					color: '#289df5',
+					borderColor: '#289df5',
+					areaStyle: {
+						type: 'default',
+						opacity: 0.1,
 					},
-				],
-			};
-			myChart.setOption(option);
-			(<any>state.myCharts).push(myChart);
-		};
-		// 初始化近7天投入品记录
-		const initChartsInvestment = () => {
-			const myChart = echarts.init(chartsInvestmentRef.value);
-			const option = {
-				grid: {
-					top: 15,
-					right: 15,
-					bottom: 20,
-					left: 30,
 				},
-				tooltip: {
-					trigger: 'axis',
-				},
-				xAxis: {
-					type: 'category',
-					data: ['1天', '2天', '3天', '4天', '5天', '6天', '7天'],
-				},
-				yAxis: {
-					type: 'value',
-				},
-				series: [
-					{
-						data: [10, 20, 15, 80, 70, 11, 30],
-						type: 'bar',
-					},
-				],
-			};
-			myChart.setOption(option);
-			(<any>state.myCharts).push(myChart);
-		};
-		// 批量设置 echarts resize
-		const initEchartsResizeFun = () => {
-			nextTick(() => {
-				for (let i = 0; i < state.myCharts.length; i++) {
-					(<any>state.myCharts[i]).resize();
-				}
-			});
-		};
-		// 批量设置 echarts resize
-		const initEchartsResize = () => {
-			window.addEventListener('resize', initEchartsResizeFun);
-		};
-		// 页面加载时
-		onMounted(() => {
-			initChartsCenterOne();
-			initChartsSevenDays();
-			initChartsWarning();
-			initChartsMonitor();
-			initChartsInvestment();
-			initEchartsResize();
-		});
-		// 由于页面缓存原因,keep-alive
-		onActivated(() => {
-			initEchartsResizeFun();
-		});
-		// 监听 vuex 中的 tagsview 开启全屏变化,重新 resize 图表,防止不出现/大小不变等
-		watch(
-			() => isTagsViewCurrenFull.value,
-			() => {
-				initEchartsResizeFun();
-			}
-		);
-		return {
-			chartsCenterOneRef,
-			chartsSevenDaysRef,
-			chartsWarningRef,
-			chartsMonitorRef,
-			chartsInvestmentRef,
-			...toRefs(state),
-		};
-	},
+				data: [20, 32, 31, 34, 12, 13, 20],
+				type: 'line',
+				areaStyle: {},
+			},
+		],
+	};
+	myChart.setOption(option);
+	state.myCharts.push(myChart);
+};
+// 初始化近7天投入品记录
+const initChartsInvestment = () => {
+	const myChart = echarts.init(chartsInvestmentRef.value);
+	const option = {
+		grid: {
+			top: 15,
+			right: 15,
+			bottom: 20,
+			left: 30,
+		},
+		tooltip: {
+			trigger: 'axis',
+		},
+		xAxis: {
+			type: 'category',
+			data: ['1天', '2天', '3天', '4天', '5天', '6天', '7天'],
+		},
+		yAxis: {
+			type: 'value',
+		},
+		series: [
+			{
+				data: [10, 20, 15, 80, 70, 11, 30],
+				type: 'bar',
+			},
+		],
+	};
+	myChart.setOption(option);
+	state.myCharts.push(myChart);
+};
+// 批量设置 echarts resize
+const initEchartsResizeFun = () => {
+	nextTick(() => {
+		for (let i = 0; i < state.myCharts.length; i++) {
+			state.myCharts[i].resize();
+		}
+	});
+};
+// 批量设置 echarts resize
+const initEchartsResize = () => {
+	window.addEventListener('resize', initEchartsResizeFun);
+};
+// 页面加载时
+onMounted(() => {
+	initChartsCenterOne();
+	initChartsSevenDays();
+	initChartsWarning();
+	initChartsMonitor();
+	initChartsInvestment();
+	initEchartsResize();
+});
+// 由于页面缓存原因,keep-alive
+onActivated(() => {
+	initEchartsResizeFun();
 });
+// 监听 pinia 中的 tagsview 开启全屏变化,重新 resize 图表,防止不出现/大小不变等
+watch(
+	() => isTagsViewCurrenFull.value,
+	() => {
+		initEchartsResizeFun();
+	}
+);
 </script>
 
 <style scoped lang="scss">

+ 8 - 17
Web/src/views/error/401.vue

@@ -22,25 +22,16 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script setup lang="ts" name="noPower">
 import { Session } from '/@/utils/storage';
 
-export default defineComponent({
-	name: '401',
-	setup() {
-		const onSetAuth = () => {
-			// https://gitee.com/lyt-top/vue-next-admin/issues/I5C3JS
-			// 清除缓存/token等
-			Session.clear();
-			// 使用 reload 时,不需要调用 resetRoute() 重置路由
-			window.location.reload();
-		};
-		return {
-			onSetAuth,
-		};
-	},
-});
+const onSetAuth = () => {
+	// https://gitee.com/lyt-top/vue-next-admin/issues/I5C3JS
+	// 清除缓存/token等
+	Session.clear();
+	// 使用 reload 时,不需要调用 resetRoute() 重置路由
+	window.location.reload();
+};
 </script>
 
 <style scoped lang="scss">

+ 7 - 14
Web/src/views/error/404.vue

@@ -22,22 +22,15 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script setup lang="ts" name="notFound">
 import { useRouter } from 'vue-router';
 
-export default defineComponent({
-	name: '404',
-	setup() {
-		const router = useRouter();
-		const onGoHome = () => {
-			router.push('/');
-		};
-		return {
-			onGoHome,
-		};
-	},
-});
+// 定义变量内容
+const router = useRouter();
+
+const onGoHome = () => {
+	router.push('/');
+};
 </script>
 
 <style scoped lang="scss">

+ 10 - 20
Web/src/views/fun/clipboard/index.vue

@@ -7,34 +7,24 @@
 				:closable="false"
 				class="mb15"
 			></el-alert>
-			<el-input placeholder="请输入内容" v-model="copyVal">
+			<el-input placeholder="请输入内容" v-model="state.copyVal">
 				<template #append>
-					<el-button @click="copyText(copyVal)">复制链接</el-button>
+					<el-button @click="copyText(state.copyVal)">复制链接</el-button>
 				</template>
 			</el-input>
-			<el-input placeholder="先点击上方 `复制链接` 按钮,然后 `Ctrl + V` 进行粘贴! " v-model="shearVal" class="mt15"> </el-input>
+			<el-input placeholder="先点击上方 `复制链接` 按钮,然后 `Ctrl + V` 进行粘贴! " v-model="state.shearVal" class="mt15"> </el-input>
 		</el-card>
 	</div>
 </template>
 
-<script lang="ts">
-import { reactive, toRefs, onMounted, defineComponent } from 'vue';
+<script setup lang="ts" name="funClipboard">
+import { reactive } from 'vue';
 import commonFunction from '/@/utils/commonFunction';
 
-export default defineComponent({
-	name: 'funClipboard',
-	setup() {
-		const { copyText } = commonFunction();
-		const state = reactive({
-			copyVal: 'https://gitee.com/lyt-top/vue-next-admin',
-			shearVal: '',
-		});
-		// 页面加载时
-		onMounted(() => {});
-		return {
-			copyText,
-			...toRefs(state),
-		};
-	},
+// 定义变量内容
+const { copyText } = commonFunction();
+const state = reactive({
+	copyVal: 'https://gitee.com/lyt-top/vue-next-admin',
+	shearVal: '',
 });
 </script>

+ 61 - 68
Web/src/views/fun/countup/index.vue

@@ -8,7 +8,7 @@
 				class="mb15"
 			></el-alert>
 			<el-row :gutter="20">
-				<el-col :sm="6" class="mb15" v-for="(v, k) in topCardItemList" :key="k">
+				<el-col :sm="6" class="mb15" v-for="(v, k) in state.topCardItemList" :key="k">
 					<div class="countup-card-item countup-card-item-box" :style="{ background: `var(${v.color})` }">
 						<div class="countup-card-item-flex" ref="topCardItemRefs">
 							<div class="countup-card-item-title pb3">{{ v.title }}</div>
@@ -36,76 +36,69 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { reactive, toRefs, onMounted, nextTick, defineComponent } from 'vue';
+<script setup lang="ts" name="funCountup">
+import { reactive, onMounted, nextTick, ref } from 'vue';
 import { CountUp } from 'countup.js';
 
-export default defineComponent({
-	name: 'funCountup',
-	setup() {
-		const state = reactive({
-			topCardItemRefs: null as any,
-			topCardItemList: [
-				{
-					title: '今日访问人数',
-					titleNum: '123',
-					tip: '在场人数',
-					tipNum: '911',
-					color: '--el-color-primary',
-					iconColor: '#ffcb47',
-					icon: 'iconfont icon-jinridaiban',
-				},
-				{
-					title: '实验室总数',
-					titleNum: '123',
-					tip: '使用中',
-					tipNum: '611',
-					color: '--el-color-success',
-					iconColor: '#70cf41',
-					icon: 'iconfont icon-AIshiyanshi',
-				},
-				{
-					title: '申请人数(月)',
-					titleNum: '123',
-					tip: '通过人数',
-					tipNum: '911',
-					color: '--el-color-warning',
-					iconColor: '#dfae64',
-					icon: 'iconfont icon-shenqingkaiban',
-				},
-				{
-					title: '销售情况',
-					titleNum: '123',
-					tip: '销售数',
-					tipNum: '911',
-					color: '--el-color-danger',
-					iconColor: '#e56565',
-					icon: 'iconfont icon-ditu',
-				},
-			],
-		});
-		// 初始化数字滚动
-		const initNumCountUp = () => {
-			nextTick(() => {
-				state.topCardItemRefs.forEach((v: HTMLDivElement) => {
-					new CountUp(v.querySelector('.countup-card-item-title-num') as HTMLDivElement, Math.random() * 10000).start();
-					new CountUp(v.querySelector('.countup-card-item-tip-num') as HTMLDivElement, Math.random() * 1000).start();
-				});
-			});
-		};
-		// 重置/刷新数值
-		const refreshCurrent = () => {
-			initNumCountUp();
-		};
-		// 页面加载时
-		onMounted(() => {
-			initNumCountUp();
+// 定义变量内容
+const topCardItemRefs = ref<RefType[]>([]);
+const state = reactive({
+	topCardItemList: [
+		{
+			title: '今日访问人数',
+			titleNum: '123',
+			tip: '在场人数',
+			tipNum: '911',
+			color: '--el-color-primary',
+			iconColor: '#ffcb47',
+			icon: 'iconfont icon-jinridaiban',
+		},
+		{
+			title: '实验室总数',
+			titleNum: '123',
+			tip: '使用中',
+			tipNum: '611',
+			color: '--el-color-success',
+			iconColor: '#70cf41',
+			icon: 'iconfont icon-AIshiyanshi',
+		},
+		{
+			title: '申请人数(月)',
+			titleNum: '123',
+			tip: '通过人数',
+			tipNum: '911',
+			color: '--el-color-warning',
+			iconColor: '#dfae64',
+			icon: 'iconfont icon-shenqingkaiban',
+		},
+		{
+			title: '销售情况',
+			titleNum: '123',
+			tip: '销售数',
+			tipNum: '911',
+			color: '--el-color-danger',
+			iconColor: '#e56565',
+			icon: 'iconfont icon-ditu',
+		},
+	],
+});
+
+// 初始化数字滚动
+const initNumCountUp = () => {
+	nextTick(() => {
+		topCardItemRefs.value.forEach((v: HTMLDivElement) => {
+			new CountUp(v.querySelector('.countup-card-item-title-num') as HTMLDivElement, Math.random() * 10000).start();
+			new CountUp(v.querySelector('.countup-card-item-tip-num') as HTMLDivElement, Math.random() * 1000).start();
 		});
-		return {
-			refreshCurrent,
-			...toRefs(state),
-		};
-	},
+	});
+};
+// 重置/刷新数值
+const refreshCurrent = () => {
+	initNumCountUp();
+};
+// 页面加载时
+onMounted(() => {
+	initNumCountUp();
 });
 </script>
 

+ 15 - 23
Web/src/views/fun/cropper/index.vue

@@ -9,7 +9,7 @@
 			></el-alert>
 			<div class="cropper-img-warp">
 				<div class="mb15 mt15">
-					<img class="cropper-img" :src="cropperImg" />
+					<img class="cropper-img" :src="state.cropperImg" />
 				</div>
 				<el-button type="primary" size="default" @click="onCropperDialogOpen">
 					<el-icon>
@@ -23,30 +23,22 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, ref, toRefs, reactive, defineComponent } from 'vue';
+<script setup lang="ts" name="funCropper">
+import { defineAsyncComponent, ref, reactive } from 'vue';
 
-export default defineComponent({
-	name: 'funCropper',
-	components: {
-		CropperDialog: defineAsyncComponent(() => import('/@/components/cropper/index.vue')),
-	},
-	setup() {
-		const cropperDialogRef = ref();
-		const state = reactive({
-			cropperImg: 'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500',
-		});
-		// 打开裁剪弹窗
-		const onCropperDialogOpen = () => {
-			cropperDialogRef.value.openDialog(state.cropperImg);
-		};
-		return {
-			cropperDialogRef,
-			onCropperDialogOpen,
-			...toRefs(state),
-		};
-	},
+// 引入组件
+const CropperDialog = defineAsyncComponent(() => import('/@/components/cropper/index.vue'));
+
+// 定义变量内容
+const cropperDialogRef = ref();
+const state = reactive({
+	cropperImg: 'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500',
 });
+
+// 打开裁剪弹窗
+const onCropperDialogOpen = () => {
+	cropperDialogRef.value.openDialog(state.cropperImg);
+};
 </script>
 
 <style scoped lang="scss">

+ 100 - 106
Web/src/views/fun/echartsMap/index.vue

@@ -6,118 +6,112 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { toRefs, reactive, onMounted, defineComponent } from 'vue';
+<script setup lang="ts" name="funEchartsMap">
+import { reactive, onMounted } from 'vue';
 import * as echarts from 'echarts';
 import 'echarts/extension/bmap/bmap';
 import { echartsMapList, echartsMapData } from './mock';
 
-export default defineComponent({
-	name: 'funEchartsMap',
-	setup() {
-		const state: any = reactive({
-			echartsMap: null,
-			echartsMapList,
-			echartsMapData,
-		});
-		// echartsMap 将坐标信息和对应物理量的值合在一起
-		const convertData = (data: any) => {
-			let res = [];
-			for (let i = 0; i < data.length; i++) {
-				let geoCoord = state.echartsMapData[data[i].name];
-				if (geoCoord) {
-					res.push({
-						name: data[i].name,
-						value: geoCoord.concat(data[i].value),
-					});
-				}
-			}
-			return res;
-		};
-		// 初始化 echartsMap
-		const initEchartsMap = () => {
-			const myChart = echarts.init(<HTMLElement>state.echartsMap);
-			const option = {
-				tooltip: {
-					trigger: 'item',
+// 定义变量内容
+const state = reactive({
+	echartsMap: '' as unknown,
+	echartsMapList,
+	echartsMapData,
+});
+
+// echartsMap 将坐标信息和对应物理量的值合在一起
+const convertData = (data: EmptyObjectType[]) => {
+	let res = [];
+	for (let i = 0; i < data.length; i++) {
+		let geoCoord = state.echartsMapData[data[i].name];
+		if (geoCoord) {
+			res.push({
+				name: data[i].name,
+				value: geoCoord.concat(data[i].value),
+			});
+		}
+	}
+	return res;
+};
+// 初始化 echartsMap
+const initEchartsMap = () => {
+	const myChart = echarts.init(<HTMLElement>state.echartsMap);
+	const option = {
+		tooltip: {
+			trigger: 'item',
+		},
+		color: ['#9a60b4', '#ea7ccc'],
+		bmap: {
+			center: [104.114129, 37.550339],
+			zoom: 5,
+			roam: true,
+			mapStyle: {},
+		},
+		series: [
+			{
+				name: 'pm2.5',
+				type: 'scatter',
+				coordinateSystem: 'bmap',
+				data: convertData(state.echartsMapList),
+				symbolSize: function (val: any) {
+					return val[2] / 10;
 				},
-				color: ['#9a60b4', '#ea7ccc'],
-				bmap: {
-					center: [104.114129, 37.550339],
-					zoom: 5,
-					roam: true,
-					mapStyle: {},
+				encode: {
+					value: 2,
 				},
-				series: [
-					{
-						name: 'pm2.5',
-						type: 'scatter',
-						coordinateSystem: 'bmap',
-						data: convertData(state.echartsMapList),
-						symbolSize: function (val: any) {
-							return val[2] / 10;
-						},
-						encode: {
-							value: 2,
-						},
-						label: {
-							formatter: '{b}',
-							position: 'right',
-							show: false,
-						},
-						emphasis: {
-							label: {
-								show: true,
-							},
-						},
-					},
-					{
-						name: 'Top 5',
-						type: 'effectScatter',
-						coordinateSystem: 'bmap',
-						data: convertData(
-							state.echartsMapList
-								.sort(function (a: any, b: any) {
-									return b.value - a.value;
-								})
-								.slice(0, 6)
-						),
-						symbolSize: function (val: any) {
-							return val[2] / 10;
-						},
-						encode: {
-							value: 2,
-						},
-						showEffectOn: 'render',
-						rippleEffect: {
-							brushType: 'stroke',
-						},
-						hoverAnimation: true,
-						label: {
-							formatter: '{b}',
-							position: 'right',
-							show: true,
-						},
-						itemStyle: {
-							shadowBlur: 10,
-							shadowColor: '#333',
-						},
-						zlevel: 1,
+				label: {
+					formatter: '{b}',
+					position: 'right',
+					show: false,
+				},
+				emphasis: {
+					label: {
+						show: true,
 					},
-				],
-			};
-			myChart.setOption(option);
-			window.addEventListener('resize', () => {
-				myChart.resize();
-			});
-		};
-		// 页面加载时
-		onMounted(() => {
-			initEchartsMap();
-		});
-		return {
-			...toRefs(state),
-		};
-	},
+				},
+			},
+			{
+				name: 'Top 5',
+				type: 'effectScatter',
+				coordinateSystem: 'bmap',
+				data: convertData(
+					state.echartsMapList
+						.sort(function (a: any, b: any) {
+							return b.value - a.value;
+						})
+						.slice(0, 6)
+				),
+				symbolSize: function (val: any) {
+					return val[2] / 10;
+				},
+				encode: {
+					value: 2,
+				},
+				showEffectOn: 'render',
+				rippleEffect: {
+					brushType: 'stroke',
+				},
+				hoverAnimation: true,
+				label: {
+					formatter: '{b}',
+					position: 'right',
+					show: true,
+				},
+				itemStyle: {
+					shadowBlur: 10,
+					shadowColor: '#333',
+				},
+				zlevel: 1,
+			},
+		],
+	};
+	myChart.setOption(option);
+	window.addEventListener('resize', () => {
+		myChart.resize();
+	});
+};
+// 页面加载时
+onMounted(() => {
+	initEchartsMap();
 });
 </script>

+ 15 - 22
Web/src/views/fun/gridLayout/index.vue

@@ -8,7 +8,7 @@
 				class="mb15"
 			></el-alert>
 			<grid-layout
-				v-model:layout="layouts"
+				v-model:layout="state.layouts"
 				:col-num="12"
 				:row-height="30"
 				:is-draggable="true"
@@ -18,7 +18,7 @@
 				:margin="[10, 10]"
 				:use-css-transforms="true"
 			>
-				<grid-item v-for="item in layouts" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i" :key="item.i">
+				<grid-item v-for="item in state.layouts" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :i="item.i" :key="item.i">
 					<div class="w100 h100 flex">
 						<span class="flex-margin font14">{{ item.i }}</span>
 					</div>
@@ -28,27 +28,20 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { toRefs, reactive, defineComponent } from 'vue';
+<script setup lang="ts" name="funGridLayout">
+import { reactive } from 'vue';
 
-export default defineComponent({
-	name: 'FunGridLayout',
-	setup() {
-		const state = reactive({
-			layouts: [
-				{ x: 0, y: 0, w: 2, h: 2, i: '0' },
-				{ x: 2, y: 0, w: 2, h: 4, i: '1' },
-				{ x: 4, y: 0, w: 2, h: 5, i: '2' },
-				{ x: 6, y: 0, w: 2, h: 3, i: '3' },
-				{ x: 8, y: 0, w: 2, h: 3, i: '4' },
-				{ x: 10, y: 0, w: 2, h: 3, i: '5' },
-				{ x: 0, y: 5, w: 2, h: 5, i: '6' },
-			],
-		});
-		return {
-			...toRefs(state),
-		};
-	},
+// 定义变量内容
+const state = reactive({
+	layouts: [
+		{ x: 0, y: 0, w: 2, h: 2, i: '0' },
+		{ x: 2, y: 0, w: 2, h: 4, i: '1' },
+		{ x: 4, y: 0, w: 2, h: 5, i: '2' },
+		{ x: 6, y: 0, w: 2, h: 3, i: '3' },
+		{ x: 8, y: 0, w: 2, h: 3, i: '4' },
+		{ x: 10, y: 0, w: 2, h: 3, i: '5' },
+		{ x: 0, y: 5, w: 2, h: 5, i: '6' },
+	],
 });
 </script>
 

+ 15 - 24
Web/src/views/fun/printJs/index.vue

@@ -15,30 +15,21 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { reactive, toRefs, defineComponent } from 'vue';
+<script setup lang="ts" name="funPrintJs">
+import { ref } from 'vue';
 import printJs from 'print-js';
 
-export default defineComponent({
-	name: 'funPrintJs',
-	setup() {
-		const state = reactive({
-			printRef: null as null | HTMLDivElement,
-		});
-		// 打印点击
-		const onPrintJs = () => {
-			printJs({
-				printable: state.printRef,
-				type: 'html',
-				css: ['//at.alicdn.com/t/font_2298093_o73r8wjdhlg.css', '//unpkg.com/element-plus/dist/index.css'],
-				scanStyles: false,
-				style: `@media print{.mb15{margin-bottom:15px;}.el-button--small i.iconfont{font-size: 12px !important;margin-right: 5px;}}`,
-			});
-		};
-		return {
-			onPrintJs,
-			...toRefs(state),
-		};
-	},
-});
+// 定义变量内容
+const printRef = ref();
+
+// 打印点击
+const onPrintJs = () => {
+	printJs({
+		printable: printRef.value,
+		type: 'html',
+		css: ['//at.alicdn.com/t/c/font_2298093_rnp72ifj3ba.css', '//unpkg.com/element-plus/dist/index.css'],
+		scanStyles: false,
+		style: `@media print{.mb15{margin-bottom:15px;}.el-button--small i.iconfont{font-size: 12px !important;margin-right: 5px;}}`,
+	});
+};
 </script>

+ 23 - 34
Web/src/views/fun/qrcode/index.vue

@@ -22,42 +22,31 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { toRefs, reactive, onMounted, defineComponent, ref } from 'vue';
+<script setup lang="ts" name="funQrcode">
+import { onMounted, ref } from 'vue';
 import QRCode from 'qrcodejs2-fixes';
 
-export default defineComponent({
-	name: 'funQrcode',
-	setup() {
-		const qrcodeRef = ref();
-		const state = reactive({
-			qrcode: '',
-		});
-		// 初始化生成二维码
-		const initQrcode = () => {
-			new QRCode(qrcodeRef.value, {
-				text: `https://lyt-top.gitee.io/vue-next-admin-preview/#/login?t=${new Date().getTime()}`,
-				width: 125,
-				height: 125,
-				colorDark: '#000000',
-				colorLight: '#ffffff',
-			});
-		};
-		// 重新生成
-		const onInitQrcode = () => {
-			qrcodeRef.value.innerHTML = '';
-			initQrcode();
-		};
-		// 页面加载时
-		onMounted(() => {
-			initQrcode();
-		});
-		return {
-			qrcodeRef,
-			onInitQrcode,
-			...toRefs(state),
-		};
-	},
+// 定义变量内容
+const qrcodeRef = ref();
+
+// 初始化生成二维码
+const initQrcode = () => {
+	new QRCode(qrcodeRef.value, {
+		text: `https://lyt-top.gitee.io/vue-next-admin-preview/#/login?t=${new Date().getTime()}`,
+		width: 125,
+		height: 125,
+		colorDark: '#000000',
+		colorLight: '#ffffff',
+	});
+};
+// 重新生成
+const onInitQrcode = () => {
+	qrcodeRef.value.innerHTML = '';
+	initQrcode();
+};
+// 页面加载时
+onMounted(() => {
+	initQrcode();
 });
 </script>
 

+ 4 - 14
Web/src/views/fun/splitpanes/index.vue

@@ -21,23 +21,13 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { toRefs, reactive, defineComponent } from 'vue';
+<script setup lang="ts" name="funSplitpanes">
+import { ref } from 'vue';
 import { Splitpanes, Pane } from 'splitpanes';
 import 'splitpanes/dist/splitpanes.css';
 
-export default defineComponent({
-	name: 'funSplitpanes',
-	components: { Splitpanes, Pane },
-	setup() {
-		const state = reactive({
-			paneSize: 50,
-		});
-		return {
-			...toRefs(state),
-		};
-	},
-});
+// 定义变量内容
+const paneSize = ref(50);
 </script>
 
 <style scoped lang="scss">

+ 29 - 41
Web/src/views/fun/tagsView/index.vue

@@ -62,50 +62,38 @@
 	</div>
 </template>
 
-<script lang="ts">
-import { defineAsyncComponent, reactive, toRefs, defineComponent } from 'vue';
+<script setup lang="ts" name="funTagsView">
+import { defineAsyncComponent } from 'vue';
 import { useRoute } from 'vue-router';
 import mittBus from '/@/utils/mitt';
 
-export default defineComponent({
-	name: 'funTagsView',
-	components: {
-		NoticeBar: defineAsyncComponent(() => import('/@/components/noticeBar/index.vue')),
-	},
-	setup() {
-		const route = useRoute();
-		const state = reactive({});
-		// 0 刷新当前,1 关闭当前,2 关闭其它,3 关闭全部 4 当前页全屏
-		// 1、刷新当前 tagsView
-		const refreshCurrentTagsView = () => {
-			mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 0, ...route }));
-		};
-		// 2、关闭当前 tagsView
-		const closeCurrentTagsView = () => {
-			mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 1, ...route }));
-		};
-		// 3、关闭其它 tagsView
-		const closeOtherTagsView = () => {
-			mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 2, ...route }));
-		};
-		// 4、关闭全部 tagsView
-		const closeAllTagsView = () => {
-			mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 3, ...route }));
-		};
-		// 5、开启当前页面全屏
-		const openCurrenFullscreen = () => {
-			mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 4, ...route }));
-		};
-		return {
-			refreshCurrentTagsView,
-			closeCurrentTagsView,
-			closeOtherTagsView,
-			closeAllTagsView,
-			openCurrenFullscreen,
-			...toRefs(state),
-		};
-	},
-});
+// 引入组件
+const NoticeBar = defineAsyncComponent(() => import('/@/components/noticeBar/index.vue'));
+
+// 定义变量内容
+const route = useRoute();
+
+// 0 刷新当前,1 关闭当前,2 关闭其它,3 关闭全部 4 当前页全屏
+// 1、刷新当前 tagsView
+const refreshCurrentTagsView = () => {
+	mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 0, ...route }));
+};
+// 2、关闭当前 tagsView
+const closeCurrentTagsView = () => {
+	mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 1, ...route }));
+};
+// 3、关闭其它 tagsView
+const closeOtherTagsView = () => {
+	mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 2, ...route }));
+};
+// 4、关闭全部 tagsView
+const closeAllTagsView = () => {
+	mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 3, ...route }));
+};
+// 5、开启当前页面全屏
+const openCurrenFullscreen = () => {
+	mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 4, ...route }));
+};
 </script>
 
 <style scoped lang="scss">

Some files were not shown because too many files changed in this diff