文毅 1 jaar geleden
bovenliggende
commit
76cc7bf1b0
100 gewijzigde bestanden met toevoegingen van 17391 en 0 verwijderingen
  1. 12 0
      h5/.env
  2. 12 0
      h5/.env.custom
  3. 5 0
      h5/.env.development
  4. 5 0
      h5/.env.production
  5. 12 0
      h5/.env.test
  6. 18 0
      h5/.eslintignore
  7. 76 0
      h5/.eslintrc.js
  8. 23 0
      h5/.gitignore
  9. 39 0
      h5/.prettierrc.js
  10. 435 0
      h5/CHANGELOG.md
  11. 21 0
      h5/LICENSE
  12. 1 0
      h5/Z_START.bat
  13. 32 0
      h5/index.html
  14. 6843 0
      h5/package-lock.json
  15. 89 0
      h5/package.json
  16. BIN
      h5/public/favicon.ico
  17. 93 0
      h5/src/App.vue
  18. 4 0
      h5/src/Update.md
  19. 10 0
      h5/src/api/Menu.ts
  20. 258 0
      h5/src/api/Urls.ts
  21. 116 0
      h5/src/api/department/department.ts
  22. 21 0
      h5/src/api/index.ts
  23. 27 0
      h5/src/api/login/index.ts
  24. 10 0
      h5/src/api/login/login.ts
  25. 67 0
      h5/src/api/menu/index.ts
  26. 38 0
      h5/src/api/role/role.ts
  27. 19 0
      h5/src/assets/login-bg.svg
  28. 1 0
      h5/src/assets/login-icon-two.svg
  29. 1 0
      h5/src/assets/login-main.svg
  30. 421 0
      h5/src/assets/login_main.svg
  31. 9 0
      h5/src/assets/logo-mini.svg
  32. 26 0
      h5/src/components/auth/auth.vue
  33. 27 0
      h5/src/components/auth/authAll.vue
  34. 32 0
      h5/src/components/auth/auths.vue
  35. 143 0
      h5/src/components/cropper/index.vue
  36. 101 0
      h5/src/components/editor/index.vue
  37. 241 0
      h5/src/components/iconSelector/index.vue
  38. 84 0
      h5/src/components/iconSelector/list.vue
  39. 191 0
      h5/src/components/noticeBar/index.vue
  40. 63 0
      h5/src/components/svgIcon/index.vue
  41. 256 0
      h5/src/components/table/index.vue
  42. 62 0
      h5/src/config.ts
  43. 40 0
      h5/src/directive/authDirective.ts
  44. 178 0
      h5/src/directive/customDirective.ts
  45. 18 0
      h5/src/directive/index.ts
  46. 68 0
      h5/src/i18n/index.ts
  47. 192 0
      h5/src/i18n/lang/en.ts
  48. 194 0
      h5/src/i18n/lang/zh-cn.ts
  49. 192 0
      h5/src/i18n/lang/zh-tw.ts
  50. 13 0
      h5/src/i18n/pages/formI18n/en.ts
  51. 13 0
      h5/src/i18n/pages/formI18n/zh-cn.ts
  52. 13 0
      h5/src/i18n/pages/formI18n/zh-tw.ts
  53. 29 0
      h5/src/i18n/pages/login/en.ts
  54. 28 0
      h5/src/i18n/pages/login/zh-cn.ts
  55. 28 0
      h5/src/i18n/pages/login/zh-tw.ts
  56. 154 0
      h5/src/layout/component/aside.vue
  57. 272 0
      h5/src/layout/component/columnsAside.vue
  58. 18 0
      h5/src/layout/component/header.vue
  59. 65 0
      h5/src/layout/component/main.vue
  60. 25 0
      h5/src/layout/footer/index.vue
  61. 50 0
      h5/src/layout/index.vue
  62. 352 0
      h5/src/layout/lockScreen/index.vue
  63. 75 0
      h5/src/layout/logo/index.vue
  64. 71 0
      h5/src/layout/main/classic.vue
  65. 71 0
      h5/src/layout/main/columns.vue
  66. 71 0
      h5/src/layout/main/defaults.vue
  67. 58 0
      h5/src/layout/main/transverse.vue
  68. 146 0
      h5/src/layout/navBars/breadcrumb/breadcrumb.vue
  69. 53 0
      h5/src/layout/navBars/breadcrumb/closeFull.vue
  70. 143 0
      h5/src/layout/navBars/breadcrumb/index.vue
  71. 124 0
      h5/src/layout/navBars/breadcrumb/search.vue
  72. 824 0
      h5/src/layout/navBars/breadcrumb/setings.vue
  73. 272 0
      h5/src/layout/navBars/breadcrumb/user.vue
  74. 156 0
      h5/src/layout/navBars/breadcrumb/userNews.vue
  75. 35 0
      h5/src/layout/navBars/index.vue
  76. 138 0
      h5/src/layout/navBars/tagsView/contextmenu.vue
  77. 726 0
      h5/src/layout/navBars/tagsView/tagsView.vue
  78. 159 0
      h5/src/layout/navMenu/horizontal.vue
  79. 49 0
      h5/src/layout/navMenu/subItem.vue
  80. 102 0
      h5/src/layout/navMenu/vertical.vue
  81. 101 0
      h5/src/layout/routerView/iframes.vue
  82. 93 0
      h5/src/layout/routerView/link.vue
  83. 108 0
      h5/src/layout/routerView/parent.vue
  84. 151 0
      h5/src/layout/upgrade/index.vue
  85. 19 0
      h5/src/main.ts
  86. 175 0
      h5/src/router/backEnd.ts
  87. 153 0
      h5/src/router/frontEnd.ts
  88. 143 0
      h5/src/router/index.ts
  89. 225 0
      h5/src/router/route.ts
  90. 8 0
      h5/src/stores/index.ts
  91. 35 0
      h5/src/stores/keepAliveNames.ts
  92. 16 0
      h5/src/stores/requestOldRoutes.ts
  93. 26 0
      h5/src/stores/routesList.ts
  94. 23 0
      h5/src/stores/tagsViewRoutes.ts
  95. 156 0
      h5/src/stores/themeConfig.ts
  96. 80 0
      h5/src/stores/userInfo.ts
  97. 324 0
      h5/src/theme/app.scss
  98. 147 0
      h5/src/theme/common/transition.scss
  99. 249 0
      h5/src/theme/dark.scss
  100. 0 0
      h5/src/theme/element.scss

+ 12 - 0
h5/.env

@@ -0,0 +1,12 @@
+# port 端口号
+VITE_PORT = 8888
+
+# open 运行 npm run dev 时自动打开浏览器
+VITE_OPEN = false
+
+# public path 配置线上环境路径(打包)、本地通过 http-server 访问时,请置空即可
+VITE_PUBLIC_PATH = "/admin/"
+VITE_OUTPUT_DIR= "../api/public/admin"
+VITE_APP_API_SIGN_SECRET = "api_sign_secret"
+# VITE_PUBLIC_PATH = /vue-next-admin-preview/
+# VITE_APP_API_SIGN_SECRET = YCKJ2015

+ 12 - 0
h5/.env.custom

@@ -0,0 +1,12 @@
+# port 端口号
+VITE_PORT = 8888
+
+# open 运行 npm run dev 时自动打开浏览器
+VITE_OPEN = false
+
+# public path 配置线上环境路径(打包)、本地通过 http-server 访问时,请置空即可
+VITE_PUBLIC_PATH = "/custom/"
+VITE_OUTPUT_DIR= "../api/public/custom"
+VITE_APP_API_SIGN_SECRET = "api_sign_secret"
+# VITE_PUBLIC_PATH = /vue-next-admin-preview/
+# VITE_APP_API_SIGN_SECRET = YCKJ2015

+ 5 - 0
h5/.env.development

@@ -0,0 +1,5 @@
+# 本地环境
+ENV = 'development'
+
+# 本地环境接口地址
+VITE_API_URL = 'http://localhost:8888/'

+ 5 - 0
h5/.env.production

@@ -0,0 +1,5 @@
+# 线上环境
+ENV = 'production'
+
+# 线上环境接口地址
+VITE_API_URL = 'https://lyt-top.gitee.io/vue-next-admin-preview/'

+ 12 - 0
h5/.env.test

@@ -0,0 +1,12 @@
+# port 端口号
+VITE_PORT = 8888
+
+# open 运行 npm run dev 时自动打开浏览器
+VITE_OPEN = false
+
+# public path 配置线上环境路径(打包)、本地通过 http-server 访问时,请置空即可
+VITE_PUBLIC_PATH = "/test/"
+VITE_OUTPUT_DIR= "../api/public/test"
+VITE_APP_API_SIGN_SECRET = "api_sign_secret"
+# VITE_PUBLIC_PATH = /vue-next-admin-preview/
+# VITE_APP_API_SIGN_SECRET = YCKJ2015

+ 18 - 0
h5/.eslintignore

@@ -0,0 +1,18 @@
+
+*.sh
+node_modules
+lib
+*.md
+*.scss
+*.woff
+*.ttf
+.vscode
+.idea
+dist
+mock
+public
+bin
+build
+config
+index.html
+src/assets

+ 76 - 0
h5/.eslintrc.js

@@ -0,0 +1,76 @@
+module.exports = {
+	root: true,
+	env: {
+		browser: true,
+		es2021: true,
+		node: true,
+	},
+	parser: 'vue-eslint-parser',
+	parserOptions: {
+		ecmaVersion: 12,
+		parser: '@typescript-eslint/parser',
+		sourceType: 'module',
+	},
+	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',
+		'@typescript-eslint/no-var-requires': 'off',
+		'@typescript-eslint/no-empty-function': 'off',
+		'@typescript-eslint/no-use-before-define': 'off',
+		'@typescript-eslint/ban-ts-comment': 'off',
+		'@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',
+		'vue/html-closing-bracket-newline': 'off',
+		'vue/max-attributes-per-line': 'off',
+		'vue/multiline-html-element-content-newline': 'off',
+		'vue/singleline-html-element-content-newline': 'off',
+		'vue/attribute-hyphenation': 'off',
+		'vue/html-self-closing': 'off',
+		'vue/no-multiple-template-root': 'off',
+		'vue/require-default-prop': 'off',
+		'vue/no-v-model-argument': 'off',
+		'vue/no-arrow-functions-in-watch': 'off',
+		'vue/no-template-key': 'off',
+		'vue/no-v-html': 'off',
+		'vue/comment-directive': 'off',
+		'vue/no-parsing-error': 'off',
+		'vue/no-deprecated-v-on-native-modifier': 'off',
+		'vue/multi-word-component-names': 'off',
+		'no-useless-escape': 'off',
+		'no-sparse-arrays': 'off',
+		'no-prototype-builtins': 'off',
+		'no-constant-condition': 'off',
+		'no-use-before-define': 'off',
+		'no-restricted-globals': 'off',
+		'no-restricted-syntax': 'off',
+		'generator-star-spacing': 'off',
+		'no-unreachable': 'off',
+		'no-multiple-template-root': 'off',
+		'no-unused-vars': 'error',
+		'no-v-model-argument': 'off',
+		'no-case-declarations': 'off',
+		'no-console': 'error',
+		'no-redeclare': 'off',
+	},
+};

+ 23 - 0
h5/.gitignore

@@ -0,0 +1,23 @@
+.DS_Store
+node_modules
+/dist
+
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 39 - 0
h5/.prettierrc.js

@@ -0,0 +1,39 @@
+module.exports = {
+	// 一行最多多少个字符
+	printWidth: 150,
+	// 指定每个缩进级别的空格数
+	tabWidth: 2,
+	// 使用制表符而不是空格缩进行
+	useTabs: true,
+	// 在语句末尾打印分号
+	semi: true,
+	// 使用单引号而不是双引号
+	singleQuote: true,
+	// 更改引用对象属性的时间 可选值"<as-needed|consistent|preserve>"
+	quoteProps: 'as-needed',
+	// 在JSX中使用单引号而不是双引号
+	jsxSingleQuote: false,
+	// 多行时尽可能打印尾随逗号。(例如,单行数组永远不会出现逗号结尾。) 可选值"<none|es5|all>",默认none
+	trailingComma: 'es5',
+	// 在对象文字中的括号之间打印空格
+	bracketSpacing: true,
+	// jsx 标签的反尖括号需要换行
+	jsxBracketSameLine: false,
+	// 在单独的箭头函数参数周围包括括号 always:(x) => x \ avoid:x => x
+	arrowParens: 'always',
+	// 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码
+	rangeStart: 0,
+	rangeEnd: Infinity,
+	// 指定要使用的解析器,不需要写文件开头的 @prettier
+	requirePragma: false,
+	// 不需要自动在文件开头插入 @prettier
+	insertPragma: false,
+	// 使用默认的折行标准 always\never\preserve
+	proseWrap: 'preserve',
+	// 指定HTML文件的全局空格敏感度 css\strict\ignore
+	htmlWhitespaceSensitivity: 'css',
+	// Vue文件脚本和样式标签缩进
+	vueIndentScriptAndStyle: false,
+	// 换行符使用 lf 结尾是 可选值"<auto|lf|crlf|cr>"
+	endOfLine: 'lf',
+};

File diff suppressed because it is too large
+ 435 - 0
h5/CHANGELOG.md


+ 21 - 0
h5/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 lyt-Top
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 1 - 0
h5/Z_START.bat

@@ -0,0 +1 @@
+cmd /k npm run dev

+ 32 - 0
h5/index.html

@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+	<head>
+		<meta charset="utf-8" />
+		<meta http-equiv="X-UA-Compatible" content="IE=edge" />
+		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+		<meta
+			name="keywords"
+			content="vue-next-admin,vue-prev-admin,vue-admin-wonderful,后台管理系统一站式平台模板,希望可以帮你完成快速开发。vue2.x,vue2.0,vue2,vue3,vue3.x,vue3.0,CompositionAPI,typescript,element plus,element,plus,admin,wonderful,wonderful-next,vue-next-admin,vite,vite-admin,快速,高效,后台模板,后台系统,管理系统"
+		/>
+		<meta
+			name="description"
+			content="vue-next-admin,基于 vue3 + CompositionAPI + typescript + vite + element plus,适配手机、平板、pc 的后台开源免费管理系统模板!vue-prev-admin,基于 vue2 +  element ui,适配手机、平板、pc 的后台开源免费管理系统模板!"
+		/>
+		<link rel="icon" href="/favicon.ico" />
+		<title>vue-next-admin</title>
+	</head>
+	<body>
+		<div id="app"></div>
+		<script type="text/javascript">
+			var _hmt = _hmt || [];
+			(function () {
+				var hm = document.createElement('script');
+				hm.src = 'https://hm.baidu.com/hm.js?d9c8b87d10717013641458b300c552e4';
+				var s = document.getElementsByTagName('script')[0];
+				s.parentNode.insertBefore(hm, s);
+			})();
+		</script>
+		<script type="module" src="/src/main.ts"></script>
+		<script type="text/javascript" src="https://api.map.baidu.com/api?v=3.0&ak=wsijQt8sLXrCW71YesmispvYHitfG9gv&s=1"></script>
+	</body>
+</html>

File diff suppressed because it is too large
+ 6843 - 0
h5/package-lock.json


+ 89 - 0
h5/package.json

@@ -0,0 +1,89 @@
+{
+	"name": "vue-next-admin",
+	"version": "2.4.21",
+	"description": "vue3 vite next admin template",
+	"author": "lyt_20201208",
+	"license": "MIT",
+	"scripts": {
+		"dev": "vite --force --mode development",
+		"build": "vite build",
+		"build:test": "set NODE_ENV=test && vite build --mode test ",
+		"build:company": "set NODE_ENV=company && vite build --mode company ",
+		"build:customer": "set NODE_ENV=customer && vite build --mode customer ",
+		"build:release": "set NODE_ENV=release && vite build --mode release ",
+		"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
+	},
+	"dependencies": {
+		"@element-plus/icons-vue": "^2.0.10",
+		"@wangeditor/editor": "^5.1.23",
+		"@wangeditor/editor-for-vue": "^5.1.12",
+		"await-to-js": "^3.0.0",
+		"axios": "^1.2.1",
+		"countup.js": "^2.3.2",
+		"cropperjs": "^1.5.13",
+		"echarts": "^5.4.1",
+		"echarts-gl": "^2.0.9",
+		"echarts-wordcloud": "^2.1.0",
+		"element-plus": "^2.2.26",
+		"js-cookie": "^3.0.1",
+		"js-md5": "^0.7.3",
+		"js-table2excel": "^1.0.3",
+		"jsplumb": "^2.15.6",
+		"mitt": "^3.0.0",
+		"nprogress": "^0.2.0",
+		"pinia": "^2.0.28",
+		"print-js": "^1.6.0",
+		"qrcodejs2-fixes": "^0.0.2",
+		"qs": "^6.11.0",
+		"screenfull": "^6.0.2",
+		"sortablejs": "^1.15.0",
+		"splitpanes": "^3.1.5",
+		"vue": "^3.2.45",
+		"vue-clipboard3": "^2.0.0",
+		"vue-grid-layout": "^3.0.0-beta1",
+		"vue-i18n": "^9.2.2",
+		"vue-router": "^4.1.6"
+	},
+	"devDependencies": {
+		"@types/node": "^18.11.13",
+		"@types/nprogress": "^0.2.0",
+		"@types/sortablejs": "^1.15.0",
+		"@typescript-eslint/eslint-plugin": "^5.46.0",
+		"@typescript-eslint/parser": "^5.46.0",
+		"@vitejs/plugin-vue": "^4.0.0",
+		"@vue/compiler-sfc": "^3.2.45",
+		"eslint": "^8.29.0",
+		"eslint-plugin-vue": "^9.8.0",
+		"prettier": "^2.8.1",
+		"sass": "^1.56.2",
+		"typescript": "^4.9.4",
+		"vite": "^4.0.0",
+		"vite-plugin-vue-setup-extend": "^0.4.0",
+		"vue-eslint-parser": "^9.1.0"
+	},
+	"browserslist": [
+		"> 1%",
+		"last 2 versions",
+		"not dead"
+	],
+	"bugs": {
+		"url": "https://gitee.com/lyt-top/vue-next-admin/issues"
+	},
+	"engines": {
+		"node": ">=16.0.0",
+		"npm": ">= 7.0.0"
+	},
+	"keywords": [
+		"vue",
+		"vue3",
+		"vuejs/vue-next",
+		"element-ui",
+		"element-plus",
+		"vue-next-admin",
+		"next-admin"
+	],
+	"repository": {
+		"type": "git",
+		"url": "https://gitee.com/lyt-top/vue-next-admin.git"
+	}
+}

BIN
h5/public/favicon.ico


+ 93 - 0
h5/src/App.vue

@@ -0,0 +1,93 @@
+<template>
+	<el-config-provider :size="getGlobalComponentSize" :locale="getGlobalI18n">
+		<router-view v-show="themeConfig.lockScreenTime > 1" />
+		<LockScreen v-if="themeConfig.isLockScreen" />
+		<Setings ref="setingsRef" v-show="themeConfig.lockScreenTime > 1" />
+		<CloseFull v-if="!themeConfig.isLockScreen" />
+		<!-- <Upgrade v-if="getVersion" /> -->
+	</el-config-provider>
+</template>
+
+<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';
+import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
+import { useThemeConfig } from '/@/stores/themeConfig';
+import other from '/@/utils/other';
+import { Local, Session } from '/@/utils/storage';
+import mittBus from '/@/utils/mitt';
+import setIntroduction from '/@/utils/setIconfont';
+
+// 引入组件
+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 Upgrade = defineAsyncComponent(() => import('/@/layout/upgrade/index.vue'));
+
+// 定义变量内容
+const { messages, locale } = useI18n();
+const setingsRef = ref();
+const route = useRoute();
+const stores = useTagsViewRoutes();
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+
+// 获取版本号
+const getVersion = computed(() => {
+	let isVersion = false;
+	if (route.path !== '/login') {
+		// @ts-ignore
+		if ((Local.get('version') && Local.get('version') !== __VERSION__) || !Local.get('version')) isVersion = true;
+	}
+	return isVersion;
+});
+// 获取全局组件大小
+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({ 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>

+ 4 - 0
h5/src/Update.md

@@ -0,0 +1,4 @@
+## 2023年3月15日
+### 更新 utils工具类
+### 新增 apiConfig页面样板
+### 更新 config配置文件自动读取接口地址的参考

+ 10 - 0
h5/src/api/Menu.ts

@@ -0,0 +1,10 @@
+import Http from "../utils/net/Http";
+import Urls from "./Urls";
+const Menu = {
+    async getList() {
+        let url = Urls.common.menuList;
+        let res = await Http.get(url);
+        return res;
+    }
+};
+export default Menu;

+ 258 - 0
h5/src/api/Urls.ts

@@ -0,0 +1,258 @@
+const Urls = {
+    // 登录
+    index: {
+        login: "/admin/index/login",
+        balanceUpload: "/balance/index/upload",
+        message: "/admin/message/list",
+        messageEdit: "/admin/message/edit",
+    },
+    // 菜单列表
+    common: {
+        menuList: "/admin/menu/list"
+    },
+    // 上传类
+    upload: {
+        file: "/admin/upload/file"
+    },
+
+    /* 基础数据 */
+    // 公司信息
+    company: {
+         detail: "/admin/company/detail", //企业详情
+         edit: "/admin/company/edit", //更新企业信息
+    },
+    // 角色权限
+    role: {
+        init: "admin/role/init", //初始化
+        list: "admin/role/list", //列表
+        detail: "admin/role/detail", //详情
+        edit: "admin/role/edit", //更新
+        add: "admin/role/add", //添加
+        del: "admin/role/del", //删除/批量删除
+    },
+    // 部门人员
+    department: {
+        init: "/admin/department/init", //部门初始化
+        list: "/admin/department/list", //部门列表
+        detail: "/admin/department/detail", //部门详情
+        add: "/admin/department/add", //部门添加
+        edit: "/admin/department/edit", //部门更新
+        del: "/admin/department/del", //部门删除/批量删除
+        initAdmin: "admin/admin/init", //人员初始化
+        listAdmin: "/admin/admin/list", //人员列表
+        addAdmin: "/admin/admin/add", //人员添加
+        detailAdmin: "/admin/admin/detail", //人员详情
+        editAdmin: "/admin/admin/edit", //人员更新
+        delAdmin: "/admin/admin/del", //人员删除/批量删除
+        resetPassword: "/admin/admin/resetPassword", //重置密码
+        passAdmin: "/admin/admin/pass", //审核
+        rePassAdmin: "/admin/admin/rePass", //反审核
+        import: "admin/admin/import", //人员导入
+        export: "admin/admin/export", //人员导出
+    },
+    // 机台管理
+    machine: {
+        init: "/admin/machine/init", //初始化
+        list: "/admin/machine/list", //列表
+        add: "/admin/machine/add", //添加
+        edit: "/admin/machine/edit", //更新
+        del: "/admin/machine/del", //删除/批量删除
+        pass: "/admin/machine/pass", //审核
+        rePass: "/admin/machine/rePass", //反审核
+        import: "admin/machine/import", //机台导入
+        export: "admin/machine/export", //机台导出
+    },
+    // 计量单位
+    unitMeasurement: {
+        init: "/admin/unitMeasurement/init", //初始化
+        list: "/admin/unitMeasurement/list", //列表
+        add: "/admin/unitMeasurement/add", //添加
+        edit: "/admin/unitMeasurement/edit", //更新
+        del: "/admin/unitMeasurement/del", //删除/批量删除
+        pass: "/admin/unitMeasurement/pass", //审核
+        rePass: "/admin/unitMeasurement/rePass", //反审核
+        import: "admin/unitMeasurement/import", //导入
+        export: "admin/unitMeasurement/export", //导出
+    },
+    // 产品类别
+    productType: {
+        init: "admin/productType/init", //初始化
+        list: "admin/productType/list", //列表
+        add: "admin/productType/add", //添加
+        edit: "admin/productType/edit", //更新
+        del: "admin/productType/del", //删除/批量删除
+        pass: "/admin/productType/pass", //审核
+        rePass: "/admin/productType/rePass", //反审核
+        import: "admin/productType/import", //导入
+        export: "admin/productType/export", //导出
+    },
+    // 产品管理
+    product: {
+        init: "admin/product/init", //初始化
+        list: "admin/product/list", //列表
+        add: "admin/product/add", //添加
+        edit: "admin/product/edit", //更新
+        del: "admin/product/del", //删除/批量删除
+        pass: "/admin/product/pass", //审核
+        rePass: "/admin/product/rePass", //反审核
+        detail: "admin/product/detail", //详情
+        import: "admin/product/import", //导入
+        export: "admin/product/export", //导出
+        productProcedureDetail: "/admin/productProcedure/detail", //工序设置详情
+        productProcedureEdit: "/admin/productProcedure/edit", //工序设置更新
+    },
+    // 工序管理
+    procedure: {
+        init: "admin/procedure/init", //初始化
+        list: "admin/procedure/list", //列表
+        add: "admin/procedure/add", //添加
+        edit: "admin/procedure/edit", //更新
+        del: "admin/procedure/del", //删除/批量删除
+        detail: "admin/procedure/detail", //详情
+        import: "admin/procedure/import", //导入
+        export: "admin/procedure/export", //导出
+    },
+    // 工资项目
+    salaryItem: {
+        init: "admin/salaryItem/init", //初始化
+        list: "admin/salaryItem/list", //列表
+        detail: "admin/salaryItem/detail", //详情
+        add: "admin/salaryItem/add", //添加
+        edit: "admin/salaryItem/edit", //更新
+        del: "admin/salaryItem/del", //删除/批量删除
+        pass: "/admin/salaryItem/pass", //审核
+        rePass: "/admin/salaryItem/rePass", //反审核
+    },
+    // 车间管理
+    workershop: {
+        list: "admin/workshop/list", //列表
+        detail: "admin/workshop/detail", //详情
+        add: "admin/workshop/add", //添加
+        edit: "admin/workshop/edit", //更新
+        del: "admin/workshop/del", //删除/批量删除
+        pass: "admin/workshop/pass", //审核
+        rePass: "admin/workshop/rePass", //反审核
+    },
+    // 包装工序(收缩编码)
+    procedurePackage: {
+        init: "admin/procedurePackage/init", //初始化
+        list: "admin/procedurePackage/list", //列表
+        detail: "admin/procedurePackage/detail", //详情
+        add: "admin/procedurePackage/add", //添加
+        edit: "admin/procedurePackage/edit", //更新
+        del: "admin/procedurePackage/del", //删除/批量删除
+        pass: "admin/procedurePackage/pass", //审核
+        rePass: "admin/procedurePackage/rePass", //反审核
+        import: "admin/procedurePackage/import", //导入
+        export: "admin/procedurePackage/export", //导出
+    },
+
+    /* 工资模块 */
+    // 班次管理
+    classes: {
+        init: "admin/classes/init", //初始化
+        list: "admin/classes/list", //列表
+        detail: "admin/classes/detail", //详情
+        add: "admin/classes/add", //添加
+        edit: "admin/classes/edit", //更新
+        del: "admin/classes/del", //删除/批量删除
+        pass: "/admin/classes/pass", //审核
+        rePass: "/admin/classes/rePass", //反审核
+    },
+    // 排班管理(考勤管理)
+    rostering: {
+        init: "/admin/rostering/init", //初始化
+        list: "admin/rostering/list", //列表
+        edit: "admin/rostering/edit", //更新
+        detail: "admin/rostering/detail", //详情
+        import: "admin/rostering/import", //排班导入
+        export: "admin/rostering/export", //排班导出
+        index: "api/rostering/index", //一键排班
+    },
+    // 考勤管理(考勤报表)
+    rosteringRecord: {
+        init: "/admin/rosteringRecord/init", //初始化
+        list: "admin/rosteringRecord/list", //列表
+        edit: "admin/rosteringRecord/edit", //更新
+        detail: "admin/rosteringRecord/detail", //详情
+        index: "api/rosteringRecord/index", //考勤统计
+    },
+    // 包装记录
+    packing: {
+        initDetail: "/admin/packing/initDetail", //初始化
+        init: "/admin/packing/init", //初始化
+        list: "admin/packing/list", //列表
+        edit: "admin/packing/edit", //更新
+        del: "admin/packing/del", //删除/批量删除
+        add: "admin/packing/add", //添加
+        detail: "admin/packing/detail", //详情
+    },
+    // 非包装记录
+    noPacking: {
+        init: "/admin/noPacking/init", //初始化
+        list: "admin/noPacking/list", //列表
+        edit: "admin/noPacking/edit", //更新
+        del: "admin/noPacking/del", //删除/批量删除
+        add: "admin/noPacking/add", //添加
+        detail: "admin/noPacking/detail", //详情
+    },
+    // 拆包管理
+    unPacking: {
+        init: "/admin/unPacking/init", //初始化
+        list: "/admin/unPacking/list", //列表
+        detail: "/admin/unPacking/detail", //详情
+        open: "/admin/unPacking/open", //拆包
+    },
+
+    /* 报表管理*/
+    // 计时工资总表
+    salary_hourly: {
+        init: "admin/salary_hourly/init", //初始化
+        list: "admin/salary_hourly/list", //列表
+        detail: "admin/salary_hourly/detail", //详情
+        add: "admin/salary_hourly/add", //添加
+        edit: "admin/salary_hourly/edit", //更新
+        del: "admin/salary_hourly/del", //删除/批量删除
+    },
+    // 计件工资总表
+    salary_rate: {
+        init: "admin/salary_rate/init", //初始化
+        list: "admin/salary_rate/list", //列表
+        detail: "admin/salary_rate/detail", //详情
+        add: "admin/salary_rate/add", //添加
+        edit: "admin/salary_rate/edit", //更新
+        del: "admin/salary_rate/del", //删除/批量删除
+    },
+    // 机台产能报表
+    machineCapacity: {
+        init: "admin/machineCapacity/init", //初始化
+        list: "admin/machineCapacity/list", //列表
+        detail: "admin/machineCapacity/detail", //详情
+        add: "admin/machineCapacity/add", //添加
+        edit: "admin/machineCapacity/edit", //更新
+        del: "admin/machineCapacity/del", //删除/批量删除
+    },
+    // 员工工资表
+    salary: {
+        init: "admin/salary/init", //初始化
+        list: "admin/salary/list", //列表
+        detail: "admin/salary/detail", //详情
+        add: "admin/salary/add", //添加
+        edit: "admin/salary/edit", //批量更新
+        del: "admin/salary/del", //删除/批量删除
+        submit: "admin/salary/submit", //提交更新
+        index: "api/salary/index", //工资计算
+        editSingle: "/admin/salary/editSingle", //更新
+    },
+    // 库存查询
+    salaryRateStock: {
+        init: "/admin/salaryRateStock/init", //初始化
+        list: "/admin/salaryRateStock/list", //列表
+    },
+    // 数据看板
+    statistics: {
+        init: "admin/statistics/init", //初始化
+        detail: "admin/statistics/detail", //详情
+    },
+};
+export default Urls;

+ 116 - 0
h5/src/api/department/department.ts

@@ -0,0 +1,116 @@
+import Http from "../../utils/net/Http";
+import Urls from "../Urls";
+const Department = {
+    // 部门初始化
+    async init() {
+        let url = Urls.department.init;
+        let param: object = {};
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 部门列表
+    async list(param: object) {
+        let url = Urls.department.list;
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 部门详情
+    async detail(id: number) {
+        let url = Urls.department.detail;
+        let param: object = {id};
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 部门添加
+    async add(param: object) {
+        let url = Urls.department.add;
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 部门更新
+    async edit(param: object) {
+        let url = Urls.department.edit;
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 部门删除/批量删除
+    async del(ids: string | number | object) {
+        let url = Urls.department.del;
+        let param: object = {ids};
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 人员初始化
+    async initAdmin() {
+        let url = Urls.department.initAdmin;
+        let param: object = {};
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 人员列表
+    async listAdmin(param: object) {
+        let url = Urls.department.listAdmin;
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 人员添加
+    async addAdmin(param: object) {
+        let url = Urls.department.addAdmin;
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 人员详情
+    async detailAdmin(id: number) {
+        let url = Urls.department.detailAdmin;
+        let param: object = {id};
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 人员更新
+    async editAdmin(param: object) {
+        let url = Urls.department.editAdmin;
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 人员删除/批量删除
+    async delAdmin(ids: string | number | object) {
+        let url = Urls.department.delAdmin;
+        let param: object = {ids};
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 重置密码
+    async resetPassword(param: object) {
+        let url = Urls.department.resetPassword;
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 部门人员审核
+    async passAdmin(ids: string | number | object) {
+        let url = Urls.department.passAdmin;
+        let param: object = {ids};
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 部门人员反审核
+    async rePassAdmin(ids: string | number | object) {
+        let url = Urls.department.rePassAdmin;
+        let param: object = {ids};
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 人员导入
+    async import(param: object) {
+        let url = Urls.department.import;
+        let res = await Http.post(url, param);
+        return res;
+    },
+    // 人员导出
+    async export(path: string | number | object) {
+        let url = Urls.department.export;
+        let param: object = { path };
+        let res = await Http.post(url, param);
+        return res;
+    },
+};
+export default Department;

+ 21 - 0
h5/src/api/index.ts

@@ -0,0 +1,21 @@
+import Http from "../utils/net/Http";
+import Urls from "./Urls";
+const Index = {
+    async balanceUpload(param:object) {
+        let url = Urls.index.balanceUpload;
+        let res = await Http.post(url,param);
+        return res;
+    },
+    async message(param:object) {
+        let url = Urls.index.message;
+        let res = await Http.post(url,param);
+        return res;
+    },
+    async messageEdit(ids: string | number | object) {
+        let url = Urls.index.messageEdit;
+        let param: object = {ids};
+        let res = await Http.post(url, param);
+        return res;
+    },
+};
+export default Index;

+ 27 - 0
h5/src/api/login/index.ts

@@ -0,0 +1,27 @@
+import request from '/@/utils/request';
+
+/**
+ * (不建议写成 request.post(xxx),因为这样 post 时,无法 params 与 data 同时传参)
+ *
+ * 登录api接口集合
+ * @method signIn 用户登录
+ * @method signOut 用户退出登录
+ */
+export function useLoginApi() {
+	return {
+		signIn: (data: object) => {
+			return request({
+				url: '/user/signIn',
+				method: 'post',
+				data,
+			});
+		},
+		signOut: (data: object) => {
+			return request({
+				url: '/user/signOut',
+				method: 'post',
+				data,
+			});
+		},
+	};
+}

+ 10 - 0
h5/src/api/login/login.ts

@@ -0,0 +1,10 @@
+import Http from "../../utils/net/Http";
+import Urls from "../Urls";
+const Login = {
+    async login(param: object) {
+        let url = Urls.index.login;
+        let res = await Http.get(url, param);
+        return res;
+    }
+};
+export default Login;

+ 67 - 0
h5/src/api/menu/index.ts

@@ -0,0 +1,67 @@
+import request from '/@/utils/request';
+import Http from '/@/utils/net/Http';
+import Menu from '../Menu';
+import { Session } from '/@/utils/storage';
+
+let menus:any = []; 
+/**
+ * 以下为模拟接口地址,gitee 的不通,就换自己的真实接口地址
+ *
+ * (不建议写成 request.post(xxx),因为这样 post 时,无法 params 与 data 同时传参)
+ *
+ * 后端控制菜单模拟json,路径在 https://gitee.com/lyt-top/vue-next-admin-images/tree/master/menu
+ * 后端控制路由,isRequestRoutes 为 true,则开启后端控制路由
+ * @method getAdminMenu 获取后端动态路由菜单(admin)
+ * @method getTestMenu 获取后端动态路由菜单(test)
+ */
+export function useMenuApi() {
+	return {
+		getMenuAdmin: async (params?: object) => {
+			//TODO: 要改成远程获取
+			let res: any = await Menu.getList();
+			menus = res.data.menu;
+			// Session.set('menus',menus);
+			// backEndTest(menus,res.data.role);
+			console.log('menus',menus);
+			let menu = {
+				code: 0,
+				type: "adminMenu",
+				data: menus,
+				role: res.data.role
+			}
+			console.log("menu result", menu);
+			return menu;
+		},
+		getAdminMenu: (params?: object) => {
+			return request({
+				url: '/gitee/lyt-top/vue-next-admin-images/raw/master/menu/adminMenu.json',
+				method: 'get',
+				params,
+			});
+		},
+		getTestMenu: (params?: object) => {
+			return request({
+				url: '/gitee/lyt-top/vue-next-admin-images/raw/master/menu/testMenu.json',
+				method: 'get',
+				params,
+			});
+		},
+	};
+}
+
+// export function backEndTest(routers: any,roles: any) {
+// 	routers.forEach((item:any,index:number)=>{
+// 		item.isHide = true;
+// 		item.meta.roles[0] = 'common';
+// 		roles.forEach((item2:any)=>{
+// 			if(item2==item.name){
+// 				item.isHide = false;
+// 				item.meta.roles[0] = 'admin';
+// 			}
+// 		})
+// 		if(item.children){
+// 			backEndTest(item.children,roles);
+// 		}
+// 		return item;
+// 	})
+// }

+ 38 - 0
h5/src/api/role/role.ts

@@ -0,0 +1,38 @@
+import Http from "../../utils/net/Http";
+import Urls from "../Urls";
+const Role = {
+    async init() {
+        let url = Urls.role.init;
+        let param: object = {};
+        let res = await Http.post(url, param);
+        return res;
+    },
+    async list(param: object) {
+        let url = Urls.role.list;
+        let res = await Http.post(url, param);
+        return res;
+    },
+    async add(param: object) {
+        let url = Urls.role.add;
+        let res = await Http.post(url, param);
+        return res;
+    },
+    async edit(param: object) {
+        let url = Urls.role.edit;
+        let res = await Http.post(url, param);
+        return res;
+    },
+    async del(ids: number | string | object) {
+        let url = Urls.role.del;
+        let param: object = { ids };
+        let res = await Http.post(url, param);
+        return res;
+    },
+    async detail(id: number | string | object) {
+        let url = Urls.role.detail;
+        let param: object = { id };
+        let res = await Http.post(url, param);
+        return res;
+    },
+};
+export default Role;

File diff suppressed because it is too large
+ 19 - 0
h5/src/assets/login-bg.svg


File diff suppressed because it is too large
+ 1 - 0
h5/src/assets/login-icon-two.svg


File diff suppressed because it is too large
+ 1 - 0
h5/src/assets/login-main.svg


+ 421 - 0
h5/src/assets/login_main.svg

@@ -0,0 +1,421 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="1030px" height="730px" viewBox="0 0 1030 730" enable-background="new 0 0 1030 730" xml:space="preserve">  <image id="image0" width="1030" height="730" x="0" y="0"
+    href="data:image/png;base64,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" />
+</svg>

File diff suppressed because it is too large
+ 9 - 0
h5/src/assets/logo-mini.svg


+ 26 - 0
h5/src/components/auth/auth.vue

@@ -0,0 +1,26 @@
+<template>
+	<slot v-if="getUserAuthBtnList" />
+</template>
+
+<script setup lang="ts" name="auth">
+import { computed } from 'vue';
+import { storeToRefs } from 'pinia';
+import { useUserInfo } from '/@/stores/userInfo';
+
+// 定义父组件传过来的值
+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>

+ 27 - 0
h5/src/components/auth/authAll.vue

@@ -0,0 +1,27 @@
+<template>
+	<slot v-if="getUserAuthBtnList" />
+</template>
+
+<script setup lang="ts" name="authAll">
+import { computed } from 'vue';
+import { storeToRefs } from 'pinia';
+import { useUserInfo } from '/@/stores/userInfo';
+import { judementSameArr } from '/@/utils/arrayOperation';
+
+// 定义父组件传过来的值
+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>

+ 32 - 0
h5/src/components/auth/auths.vue

@@ -0,0 +1,32 @@
+<template>
+	<slot v-if="getUserAuthBtnList" />
+</template>
+
+<script setup lang="ts" name="auths">
+import { computed } from 'vue';
+import { storeToRefs } from 'pinia';
+import { useUserInfo } from '/@/stores/userInfo';
+
+// 定义父组件传过来的值
+const props = defineProps({
+	value: {
+		type: Array,
+		default: () => [],
+	},
+});
+
+// 定义变量内容
+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 flag;
+});
+</script>

+ 143 - 0
h5/src/components/cropper/index.vue

@@ -0,0 +1,143 @@
+<template>
+	<div>
+		<el-dialog title="更换头像" v-model="state.isShowDialog" width="769px">
+			<div class="cropper-warp">
+				<div class="cropper-warp-left">
+					<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="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="state.cropperImgBase64" class="cropper-warp-right-value-img cropper-size" />
+						</div>
+						<div class="cropper-warp-right-label">50 x 50</div>
+					</div>
+				</div>
+			</div>
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button @click="onCancel" size="default">取 消</el-button>
+					<el-button type="primary" @click="onSubmit" size="default">更 换</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script setup lang="ts" name="cropper">
+import { reactive, nextTick } from 'vue';
+import Cropper from 'cropperjs';
+import 'cropperjs/dist/cropper.css';
+
+// 定义变量内容
+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: false,
+		autoCropArea: 0.6,
+		zoomOnWheel: false,
+		crop: () => {
+			state.cropperImgBase64 = state.cropper.getCroppedCanvas().toDataURL('image/jpeg');
+		},
+	});
+};
+
+// 暴露变量
+defineExpose({
+	openDialog,
+});
+</script>
+
+<style scoped lang="scss">
+.cropper-warp {
+	display: flex;
+	.cropper-warp-left {
+		position: relative;
+		display: inline-block;
+		height: 350px;
+		flex: 1;
+		border: 1px solid var(--el-border-color);
+		background: var(--el-color-white);
+		overflow: hidden;
+		background-repeat: no-repeat;
+		cursor: move;
+		border-radius: var(--el-border-radius-base);
+		.cropper-warp-left-img {
+			width: 100%;
+			height: 100%;
+		}
+	}
+	.cropper-warp-right {
+		width: 150px;
+		height: 350px;
+		.cropper-warp-right-title {
+			text-align: center;
+			height: 20px;
+			line-height: 20px;
+		}
+		.cropper-warp-right-item {
+			margin: 15px 0;
+			.cropper-warp-right-value {
+				display: flex;
+				.cropper-warp-right-value-img {
+					width: 100px;
+					height: 100px;
+					border-radius: var(--el-border-radius-circle);
+					margin: auto;
+				}
+				.cropper-size {
+					width: 50px;
+					height: 50px;
+				}
+			}
+			.cropper-warp-right-label {
+				text-align: center;
+				font-size: 12px;
+				color: var(--el-text-color-primary);
+				height: 30px;
+				line-height: 30px;
+			}
+		}
+	}
+}
+</style>

+ 101 - 0
h5/src/components/editor/index.vue

@@ -0,0 +1,101 @@
+<template>
+	<div class="editor-container">
+		<Toolbar :editor="editorRef" :mode="mode" />
+		<Editor
+			:mode="mode"
+			:defaultConfig="state.editorConfig"
+			:style="{ height }"
+			v-model="state.editorVal"
+			@onCreated="handleCreated"
+			@onChange="handleChange"
+		/>
+	</div>
+</template>
+
+<script setup lang="ts" name="wngEditor">
+// https://www.wangeditor.com/v5/for-frame.html#vue3
+import '@wangeditor/editor/dist/css/style.css';
+import { reactive, shallowRef, watch, onBeforeUnmount } from 'vue';
+import { IDomEditor } from '@wangeditor/editor';
+import { Toolbar, Editor } from '@wangeditor/editor-for-vue';
+
+// 定义父组件传过来的值
+const props = defineProps({
+	// 是否禁用
+	disable: {
+		type: Boolean,
+		default: () => false,
+	},
+	// 内容框默认 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,
+	}
+);
+// 监听双向绑定值改变,用于回显
+watch(
+	() => props.getHtml,
+	(val) => {
+		state.editorVal = val;
+	},
+	{
+		deep: true,
+	}
+);
+</script>

+ 241 - 0
h5/src/components/iconSelector/index.vue

@@ -0,0 +1,241 @@
+<template>
+	<div class="icon-selector w100 h100">
+		<el-input
+			v-model="state.fontIconSearch"
+			:placeholder="state.fontIconPlaceholder"
+			:clearable="clearable"
+			:disabled="disabled"
+			:size="size"
+			ref="inputWidthRef"
+			@clear="onClearFontIcon"
+			@focus="onIconFocus"
+			@blur="onIconBlur"
+		>
+			<template #prepend>
+				<SvgIcon
+					:name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix"
+					class="font14"
+					v-if="state.fontIconPrefix === '' ? prepend?.indexOf('ele-') > -1 : state.fontIconPrefix?.indexOf('ele-') > -1"
+				/>
+				<i v-else :class="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix" class="font14"></i>
+			</template>
+		</el-input>
+		<el-popover
+			placement="bottom"
+			:width="state.fontIconWidth"
+			transition="el-zoom-in-top"
+			popper-class="icon-selector-popper"
+			trigger="click"
+			:virtual-ref="inputWidthRef"
+			virtual-triggering
+		>
+			<template #default>
+				<div class="icon-selector-warp">
+					<div class="icon-selector-warp-title">{{ title }}</div>
+					<el-tabs v-model="state.fontIconTabActive" @tab-click="onIconClick">
+						<el-tab-pane lazy label="ali" name="ali">
+							<IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />
+						</el-tab-pane>
+						<el-tab-pane lazy label="ele" name="ele">
+							<IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />
+						</el-tab-pane>
+						<el-tab-pane lazy label="awe" name="awe">
+							<IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />
+						</el-tab-pane>
+					</el-tabs>
+				</div>
+			</template>
+		</el-popover>
+	</div>
+</template>
+
+<script setup lang="ts" name="iconSelector">
+import { defineAsyncComponent, ref, reactive, onMounted, nextTick, computed, watch } from 'vue';
+import type { TabsPaneContext } from 'element-plus';
+import initIconfont from '/@/utils/getStyleSheets';
+import '/@/theme/iconSelector.scss';
+
+// 定义父组件传过来的值
+const props = defineProps({
+	// 输入框前置内容
+	prepend: {
+		type: String,
+		default: () => 'ele-Pointer',
+	},
+	// 输入框占位文本
+	placeholder: {
+		type: String,
+		default: () => '请输入内容搜索图标或者选择图标',
+	},
+	// 输入框占位文本
+	size: {
+		type: String,
+		default: () => 'default',
+	},
+	// 弹窗标题
+	title: {
+		type: String,
+		default: () => '请选择图标',
+	},
+	// 禁用
+	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 IconList = defineAsyncComponent(() => import('/@/components/iconSelector/list.vue'));
+
+// 定义变量内容
+const inputWidthRef = ref();
+const state = reactive({
+	fontIconPrefix: '',
+	fontIconWidth: 0,
+	fontIconSearch: '',
+	fontIconPlaceholder: '',
+	fontIconTabActive: 'ali',
+	fontIconList: {
+		ali: [],
+		ele: [],
+		awe: [],
+	},
+});
+
+// 处理 input 获取焦点时,modelValue 有值时,改变 input 的 placeholder 值
+const onIconFocus = () => {
+	if (!props.modelValue) return false;
+	state.fontIconSearch = '';
+	state.fontIconPlaceholder = props.modelValue;
+};
+// 处理 input 失去焦点时,为空将清空 input 值,为点击选中图标时,将取原先值
+const onIconBlur = () => {
+	const list = fontIconTabNameList();
+	setTimeout(() => {
+		const icon = list.filter((icon: string) => icon === state.fontIconSearch);
+		if (icon.length <= 0) state.fontIconSearch = '';
+	}, 300);
+};
+// 图标搜索及图标数据显示
+const fontIconSheetsFilterList = computed(() => {
+	const list = fontIconTabNameList();
+	if (!state.fontIconSearch) return list;
+	let search = state.fontIconSearch.trim().toLowerCase();
+	return list.filter((item: string) => {
+		if (item.toLowerCase().indexOf(search) !== -1) return item;
+	});
+});
+// 根据 tab name 类型设置图标
+const fontIconTabNameList = () => {
+	let iconList: any = [];
+	if (state.fontIconTabActive === 'ali') iconList = state.fontIconList.ali;
+	else if (state.fontIconTabActive === 'ele') iconList = state.fontIconList.ele;
+	else if (state.fontIconTabActive === 'awe') iconList = state.fontIconList.awe;
+	return iconList;
+};
+// 处理 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 类型,用于回显时,tab 高亮与初始化数据
+const initFontIconName = () => {
+	let name = 'ali';
+	if (props.modelValue!.indexOf('iconfont') > -1) name = 'ali';
+	else if (props.modelValue!.indexOf('ele-') > -1) name = 'ele';
+	else if (props.modelValue!.indexOf('fa') > -1) name = 'awe';
+	// 初始化 tab 高亮回显
+	state.fontIconTabActive = name;
+	return name;
+};
+// 初始化数据
+const initFontIconData = async (name: string) => {
+	if (name === 'ali') {
+		// 阿里字体图标使用 `iconfont xxx`
+		if (state.fontIconList.ali.length > 0) return;
+		await initIconfont.ali().then((res: any) => {
+			state.fontIconList.ali = res.map((i: string) => `iconfont ${i}`);
+		});
+	} else if (name === 'ele') {
+		// element plus 图标
+		if (state.fontIconList.ele.length > 0) return;
+		await initIconfont.ele().then((res: any) => {
+			state.fontIconList.ele = res;
+		});
+	} else if (name === 'awe') {
+		// fontawesome字体图标使用 `fa xxx`
+		if (state.fontIconList.awe.length > 0) return;
+		await initIconfont.awe().then((res: any) => {
+			state.fontIconList.awe = 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 onIconClick = (pane: TabsPaneContext) => {
+	initFontIconData(pane.paneName as string);
+	inputWidthRef.value.focus();
+};
+// 获取当前点击的 icon 图标
+const onColClick = (v: string) => {
+	state.fontIconPlaceholder = v;
+	state.fontIconPrefix = v;
+	emit('get', state.fontIconPrefix);
+	emit('update:modelValue', state.fontIconPrefix);
+	inputWidthRef.value.focus();
+};
+// 清空当前点击的 icon 图标
+const onClearFontIcon = () => {
+	state.fontIconPrefix = '';
+	emit('clear', state.fontIconPrefix);
+	emit('update:modelValue', state.fontIconPrefix);
+};
+// 获取 input 的宽度
+const getInputWidth = () => {
+	nextTick(() => {
+		state.fontIconWidth = inputWidthRef.value.$el.offsetWidth;
+	});
+};
+// 监听页面宽度改变
+const initResize = () => {
+	window.addEventListener('resize', () => {
+		getInputWidth();
+	});
+};
+// 页面加载时
+onMounted(() => {
+	initFontIconData(initFontIconName());
+	initResize();
+	getInputWidth();
+});
+// 监听双向绑定 modelValue 的变化
+watch(
+	() => props.modelValue,
+	() => {
+		initModeValueEcho();
+		initFontIconName();
+	}
+);
+</script>

+ 84 - 0
h5/src/components/iconSelector/list.vue

@@ -0,0 +1,84 @@
+<template>
+	<div class="icon-selector-warp-row">
+		<el-scrollbar ref="selectorScrollbarRef">
+			<el-row :gutter="10" v-if="props.list.length > 0">
+				<el-col :xs="6" :sm="4" :md="4" :lg="4" :xl="4" v-for="(v, k) in list" :key="k" @click="onColClick(v)">
+					<div class="icon-selector-warp-item" :class="{ 'icon-selector-active': prefix === v }">
+						<SvgIcon :name="v" />
+					</div>
+				</el-col>
+			</el-row>
+			<el-empty :image-size="100" v-if="list.length <= 0" :description="empty"></el-empty>
+		</el-scrollbar>
+	</div>
+</template>
+
+<script setup lang="ts" name="iconSelectorList">
+// 定义父组件传过来的值
+const props = defineProps({
+	// 图标列表数据
+	list: {
+		type: Array,
+		default: () => [],
+	},
+	// 自定义空状态描述文字
+	empty: {
+		type: String,
+		default: () => '无相关图标',
+	},
+	// 高亮当前选中图标
+	prefix: {
+		type: String,
+		default: () => '',
+	},
+});
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['get-icon']);
+
+// 当前 icon 图标点击时
+const onColClick = (v: unknown | string) => {
+	emit('get-icon', v);
+};
+</script>
+
+<style scoped lang="scss">
+.icon-selector-warp-row {
+	height: 230px;
+	overflow: hidden;
+	.el-row {
+		padding: 15px;
+	}
+	.el-scrollbar__bar.is-horizontal {
+		display: none;
+	}
+	.icon-selector-warp-item {
+		display: flex;
+		justify-content: center;
+		align-items: center;
+		border: 1px solid var(--el-border-color);
+		border-radius: 5px;
+		margin-bottom: 10px;
+		height: 30px;
+		i {
+			font-size: 20px;
+			color: var(--el-text-color-regular);
+		}
+		&:hover {
+			cursor: pointer;
+			background-color: var(--el-color-primary-light-9);
+			border: 1px solid var(--el-color-primary-light-5);
+			i {
+				color: var(--el-color-primary);
+			}
+		}
+	}
+	.icon-selector-active {
+		background-color: var(--el-color-primary-light-9);
+		border: 1px solid var(--el-color-primary-light-5);
+		i {
+			color: var(--el-color-primary);
+		}
+	}
+}
+</style>

+ 191 - 0
h5/src/components/noticeBar/index.vue

@@ -0,0 +1,191 @@
+<template>
+	<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">
+				<div class="notice-bar-warp-text" ref="noticeBarTextRef" v-if="!scrollable">{{ text }}</div>
+				<div class="notice-bar-warp-slot" v-else><slot /></div>
+			</div>
+			<SvgIcon :name="rightIcon" v-if="rightIcon" class="notice-bar-warp-right-icon" @click="onRightIconClick" />
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts" name="noticeBar">
+import { reactive, ref, onMounted, nextTick } from 'vue';
+
+// 定义父组件传过来的值
+const props = defineProps({
+	// 通知栏模式,可选值为 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 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>
+
+<style scoped lang="scss">
+.notice-bar {
+	padding: 0 15px;
+	width: 100%;
+	border-radius: 4px;
+	.notice-bar-warp {
+		display: flex;
+		align-items: center;
+		width: 100%;
+		height: inherit;
+		.notice-bar-warp-text-box {
+			flex: 1;
+			height: inherit;
+			display: flex;
+			align-items: center;
+			overflow: hidden;
+			position: relative;
+			.notice-bar-warp-text {
+				white-space: nowrap;
+				position: absolute;
+				left: 0;
+			}
+			.notice-bar-warp-slot {
+				width: 100%;
+				white-space: nowrap;
+				:deep(.el-carousel__item) {
+					display: flex;
+					align-items: center;
+				}
+			}
+		}
+		.notice-bar-warp-left-icon {
+			width: 24px;
+			font-size: inherit !important;
+		}
+		.notice-bar-warp-right-icon {
+			width: 24px;
+			text-align: right;
+			font-size: inherit !important;
+			&:hover {
+				cursor: pointer;
+			}
+		}
+	}
+}
+</style>

+ 63 - 0
h5/src/components/svgIcon/index.vue

@@ -0,0 +1,63 @@
+<template>
+	<i v-if="isShowIconSvg" class="el-icon" :style="setIconSvgStyle">
+		<component :is="getIconName" />
+	</i>
+	<div v-else-if="isShowIconImg" :style="setIconImgOutStyle">
+		<img :src="getIconName" :style="setIconSvgInsStyle" />
+	</div>
+	<i v-else :class="getIconName" :style="setIconSvgStyle" />
+</template>
+
+<script setup lang="ts" name="svgIcon">
+import { computed } from 'vue';
+
+// 定义父组件传过来的值
+const props = defineProps({
+	// svg 图标组件名字
+	name: {
+		type: String,
+	},
+	// 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>

+ 256 - 0
h5/src/components/table/index.vue

@@ -0,0 +1,256 @@
+<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.type === '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 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>
+						<el-scrollbar>
+							<div ref="toolSetRef" class="tool-sortable">
+								<div class="tool-sortable-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" size="default" class="ml12 mr8" :label="v.title" @change="onCheckChange" />
+								</div>
+							</div>
+						</el-scrollbar>
+					</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: () => [],
+	},
+	// 配置项
+	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 = <T>(val: T) => {
+	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>

+ 62 - 0
h5/src/config.ts

@@ -0,0 +1,62 @@
+import { Local, Session } from '/@/utils/storage';
+/**
+ * 系统设置
+ */
+const config = {
+    /**
+     * 版本号
+     */
+    version: "V23.02.09.1.Test",
+    /**
+     * 请求接口域名
+     */
+    file: "http://ssl.ycxxkj.com:38071",
+    host: "http://ssl.ycxxkj.com:38071/index.php",
+    /**
+     * 接口签名密钥
+     */
+    apiSignKey: "32a1ff74699ff2d6ce4c497cb94cb5c8",
+}
+let env = import.meta.env;
+console.log("==env.Mode:==", env.MODE);
+if (env.MODE == "development") {
+    // config.file = "http://192.168.0.83:6969";
+    // config.host = "http://192.168.0.83:6969/index.php";
+    // config.file = "http://ssl.ycxxkj.com:38071";
+	// config.host = "http://ssl.ycxxkj.com:38071/index.php";
+    config.file = "http://192.168.0.126:6969";
+	config.host = "http://192.168.0.126:6969/index.php";
+    // config.file = "http://192.168.0.173:6969";
+	// config.host = "http://192.168.0.173:6969/index.php";
+	// config.domain = "http://192.168.0.173:6969";
+}
+if (env.MODE == "release") {
+    //本地
+    config.file = "http://192.168.0.83:5252";
+    config.host = "http://192.168.0.83:5252/index.php";
+    //公网
+    // config.file = "http://nxlz.ycxxkj.com:38071";
+    // config.host = "http://nxlz.ycxxkj.com:38071/index.php";
+}
+if (env.MODE == "test") {
+    //本地
+    // config.file = "http://192.168.0.83:6969";
+    // config.host = "http://192.168.0.83:6969/index.php";
+    //公网
+    config.file = "http://ssl.ycxxkj.com:38071";
+    config.host = "http://ssl.ycxxkj.com:38071/index.php";
+}
+if (env.MODE == "customer") {
+    //公网
+    config.file = "http://ssl.ycxxkj.com:38071";
+    config.host = "http://ssl.ycxxkj.com:38071/index.php";
+}
+
+// 如果需要自动获取接口地址
+// console.log('href',window.location.href);
+// console.log('host',window.location.host);
+// config.file = Local.get('file') ? Local.get('file') : 'http://'+window.location.host;
+// config.host = Local.get('host') ? Local.get('host') : config.file+'/index.php';
+
+console.log("==config:==", config);
+export default config;

+ 40 - 0
h5/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
h5/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
h5/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);
+}

+ 68 - 0
h5/src/i18n/index.ts

@@ -0,0 +1,68 @@
+import { createI18n } from 'vue-i18n';
+import pinia from '/@/stores/index';
+import { storeToRefs } from 'pinia';
+import { useThemeConfig } from '/@/stores/themeConfig';
+
+// 定义语言国际化内容
+
+/**
+ * 说明:
+ * 须在 pages 下新建文件夹(建议 `要国际化界面目录` 与 `i18n 目录` 相同,方便查找),
+ * 注意国际化定义的字段,不要与原有的定义字段相同。
+ * 1、/src/i18n/lang 下的 ts 为框架的国际化内容
+ * 2、/src/i18n/pages 下的 ts 为各界面的国际化内容
+ */
+
+// element plus 自带国际化
+import enLocale from 'element-plus/lib/locale/lang/en';
+import zhcnLocale from 'element-plus/lib/locale/lang/zh-cn';
+import zhtwLocale from 'element-plus/lib/locale/lang/zh-tw';
+
+// 定义变量内容
+const messages = {};
+const element = { en: enLocale, 'zh-cn': zhcnLocale, 'zh-tw': zhtwLocale };
+const itemize = { en: [], 'zh-cn': [], 'zh-tw': [] };
+const modules: Record<string, any> = import.meta.glob('./**/*.ts', { eager: true });
+
+// 对自动引入的 modules 进行分类 en、zh-cn、zh-tw
+// https://vitejs.cn/vite3-cn/guide/features.html#glob-import
+for (const path in modules) {
+	const key = path.match(/(\S+)\/(\S+).ts/);
+	if (itemize[key![2]]) itemize[key![2]].push(modules[path].default);
+	else itemize[key![2]] = modules[path];
+}
+
+// 合并数组对象(非标准数组对象,数组中对象的每项 key、value 都不同)
+function mergeArrObj<T>(list: T, key: string) {
+	let obj = {};
+	list[key].forEach((i: EmptyObjectType) => {
+		obj = Object.assign({}, obj, i);
+	});
+	return obj;
+}
+
+// 处理最终格式
+for (const key in itemize) {
+	messages[key] = {
+		name: key,
+		el: element[key].el,
+		message: mergeArrObj(itemize, key),
+	};
+}
+
+// 读取 pinia 默认语言
+const stores = useThemeConfig(pinia);
+const { themeConfig } = storeToRefs(stores);
+
+// 导出语言国际化
+// https://vue-i18n.intlify.dev/guide/essentials/fallback.html#explicit-fallback-with-one-locale
+export const i18n = createI18n({
+	legacy: false,
+	silentTranslationWarn: true,
+	missingWarn: false,
+	silentFallbackWarn: true,
+	fallbackWarn: false,
+	locale: themeConfig.value.globalI18n,
+	fallbackLocale: zhcnLocale.name,
+	messages,
+});

+ 192 - 0
h5/src/i18n/lang/en.ts

@@ -0,0 +1,192 @@
+// 定义内容
+export default {
+	router: {
+		home: 'home',
+		system: 'system',
+		systemMenu: 'systemMenu',
+		systemRole: 'systemRole',
+		systemUser: 'systemUser',
+		systemDept: 'systemDept',
+		systemDic: 'systemDic',
+		limits: 'limits',
+		limitsFrontEnd: 'FrontEnd',
+		limitsFrontEndPage: 'FrontEndPage',
+		limitsFrontEndBtn: 'FrontEndBtn',
+		limitsBackEnd: 'BackEnd',
+		limitsBackEndEndPage: 'BackEndEndPage',
+		menu: 'menu',
+		menu1: 'menu1',
+		menu11: 'menu11',
+		menu12: 'menu12',
+		menu121: 'menu121',
+		menu122: 'menu122',
+		menu13: 'menu13',
+		menu2: 'menu2',
+		funIndex: 'function',
+		funTagsView: 'funTagsView',
+		funCountup: 'countup',
+		funWangEditor: 'wangEditor',
+		funCropper: 'cropper',
+		funQrcode: 'qrcode',
+		funEchartsMap: 'EchartsMap',
+		funPrintJs: 'PrintJs',
+		funClipboard: 'Copy cut',
+		funGridLayout: 'Drag layout',
+		funSplitpanes: 'Pane splitter',
+		funDragVerify: 'Validator',
+		pagesIndex: 'pages',
+		pagesFiltering: 'Filtering',
+		pagesFilteringDetails: 'FilteringDetails',
+		pagesFilteringDetails1: 'FilteringDetails1',
+		pagesIocnfont: 'iconfont icon',
+		pagesElement: 'element icon',
+		pagesAwesome: 'awesome icon',
+		pagesFormAdapt: 'FormAdapt',
+		pagesTableRules: 'pagesTableRules',
+		pagesFormI18n: 'FormI18n',
+		pagesFormRules: 'Multi form validation',
+		pagesDynamicForm: 'Dynamic complex form',
+		pagesWorkflow: 'Workflow',
+		pagesListAdapt: 'ListAdapt',
+		pagesWaterfall: 'Waterfall',
+		pagesSteps: 'Steps',
+		pagesPreview: 'Large preview',
+		pagesWaves: 'Wave effect',
+		pagesTree: 'tree alter table',
+		pagesDrag: 'Drag command',
+		pagesLazyImg: 'Image lazy loading',
+		makeIndex: 'makeIndex',
+		makeSelector: 'Icon selector',
+		makeNoticeBar: 'notification bar',
+		makeSvgDemo: 'Svgicon demo',
+		makeTableDemo: 'table demo',
+		paramsIndex: 'Routing parameters',
+		paramsCommon: 'General routing',
+		paramsDynamic: 'Dynamic routing',
+		paramsCommonDetails: 'General routing details',
+		paramsDynamicDetails: 'Dynamic routing details',
+		chartIndex: 'chartIndex',
+		visualizingIndex: 'visualizingIndex',
+		visualizingLinkDemo1: 'visualizingLinkDemo1',
+		visualizingLinkDemo2: 'visualizingLinkDemo2',
+		personal: 'personal',
+		tools: 'tools',
+		layoutLinkView: 'LinkView',
+		layoutIframeViewOne: 'IframeViewOne',
+		layoutIframeViewTwo: 'IframeViewTwo',
+	},
+	staticRoutes: {
+		signIn: 'signIn',
+		notFound: 'notFound',
+		noPower: 'noPower',
+	},
+	user: {
+		title0: 'Component size',
+		title1: 'Language switching',
+		title2: 'Menu search',
+		title3: 'Layout configuration',
+		title4: 'news',
+		title5: 'Full screen on',
+		title6: 'Full screen off',
+		dropdownLarge: 'large',
+		dropdownDefault: 'default',
+		dropdownSmall: 'small',
+		dropdown1: 'home page',
+		dropdown2: 'Personal Center',
+		dropdown3: '404',
+		dropdown4: '401',
+		dropdown5: 'Log out',
+		dropdown6: 'Code warehouse',
+		searchPlaceholder: 'Menu search: support Chinese, routing path',
+		newTitle: 'notice',
+		newBtn: 'All read',
+		newGo: 'Go to the notification center',
+		newDesc: 'No notice',
+		logOutTitle: 'Tips',
+		logOutMessage: 'This operation will log out. Do you want to continue?',
+		logOutConfirm: 'determine',
+		logOutCancel: 'cancel',
+		logOutExit: 'Exiting',
+	},
+	tagsView: {
+		refresh: 'refresh',
+		close: 'close',
+		closeOther: 'closeOther',
+		closeAll: 'closeAll',
+		fullscreen: 'fullscreen',
+		closeFullscreen: 'closeFullscreen',
+	},
+	notFound: {
+		foundTitle: 'Wrong address input, please re-enter the address~',
+		foundMsg: 'You can check the web address first, and then re-enter or give us feedback.',
+		foundBtn: 'Back to home page',
+	},
+	noAccess: {
+		accessTitle: 'You are not authorized to operate~',
+		accessMsg: 'Contact information: add QQ group discussion 665452019',
+		accessBtn: 'Reauthorization',
+	},
+	layout: {
+		configTitle: 'Layout configuration',
+		oneTitle: 'Global Themes',
+		twoTopTitle: 'top bar set up',
+		twoMenuTitle: 'Menu set up',
+		twoColumnsTitle: 'Columns set up',
+		twoTopBar: 'Top bar background',
+		twoTopBarColor: 'Top bar default font color',
+		twoIsTopBarColorGradual: 'Top bar gradient',
+		twoMenuBar: 'Menu background',
+		twoMenuBarColor: 'Menu default font color',
+		twoMenuBarActiveColor: 'Menu Highlight Color',
+		twoIsMenuBarColorGradual: 'Menu gradient',
+		twoColumnsMenuBar: 'Column menu background',
+		twoColumnsMenuBarColor: 'Default font color bar menu',
+		twoIsColumnsMenuBarColorGradual: 'Column gradient',
+		twoIsColumnsMenuHoverPreload: 'Column Menu Hover Preload',
+		threeTitle: 'Interface settings',
+		threeIsCollapse: 'Menu horizontal collapse',
+		threeIsUniqueOpened: 'Menu accordion',
+		threeIsFixedHeader: 'Fixed header',
+		threeIsClassicSplitMenu: 'Classic layout split menu',
+		threeIsLockScreen: 'Open the lock screen',
+		threeLockScreenTime: 'screen locking(s/s)',
+		fourTitle: 'Interface display',
+		fourIsShowLogo: 'Sidebar logo',
+		fourIsBreadcrumb: 'Open breadcrumb',
+		fourIsBreadcrumbIcon: 'Open breadcrumb icon',
+		fourIsTagsview: 'Open tagsview',
+		fourIsTagsviewIcon: 'Open tagsview Icon',
+		fourIsCacheTagsView: 'Enable tagsview cache',
+		fourIsSortableTagsView: 'Enable tagsview drag',
+		fourIsShareTagsView: 'Enable tagsview sharing',
+		fourIsFooter: 'Open footer',
+		fourIsGrayscale: 'Grey model',
+		fourIsInvert: 'Color weak mode',
+		fourIsDark: 'Dark Mode',
+		fourIsWartermark: 'Turn on watermark',
+		fourWartermarkText: 'Watermark copy',
+		fiveTitle: 'Other settings',
+		fiveTagsStyle: 'Tagsview style',
+		fiveAnimation: 'page animation',
+		fiveColumnsAsideStyle: 'Column style',
+		fiveColumnsAsideLayout: 'Column layout',
+		sixTitle: 'Layout switch',
+		sixDefaults: 'One',
+		sixClassic: 'Two',
+		sixTransverse: 'Three',
+		sixColumns: 'Four',
+		tipText: 'Click the button below to copy the layout configuration to `/src/stores/themeConfig.ts` It has been modified in.',
+		copyText: 'replication configuration',
+		resetText: 'restore default',
+		copyTextSuccess: 'Copy succeeded!',
+		copyTextError: 'Copy failed!',
+	},
+	upgrade: {
+		title: 'New version',
+		msg: 'The new version is available, please update it now! Dont worry, the update is fast!',
+		desc: 'Prompt: Update will restore the default configuration',
+		btnOne: 'Cruel refusal',
+		btnTwo: 'Update now',
+		btnTwoLoading: 'Updating',
+	},
+};

+ 194 - 0
h5/src/i18n/lang/zh-cn.ts

@@ -0,0 +1,194 @@
+// 定义内容
+export default {
+	router: {
+		home: '首页',
+		underlying: '基础数据',
+			underlyingRoleManage: '账号权限02',
+			underlyingDepartment: '部门人员03',
+		system: '系统设置',
+			systemBaseSettings: '基础设置25',
+			systemMessage: '通知中心26',
+		limits: '权限管理',
+		limitsFrontEnd: '前端控制',
+		limitsFrontEndPage: '页面权限',
+		limitsFrontEndBtn: '按钮权限',
+		limitsBackEnd: '后端控制',
+		limitsBackEndEndPage: '页面权限',
+		menu: '菜单嵌套',
+		menu1: '菜单1',
+		menu11: '菜单11',
+		menu12: '菜单12',
+		menu121: '菜单121',
+		menu122: '菜单122',
+		menu13: '菜单13',
+		menu2: '菜单2',
+		funIndex: '功能',
+		funTagsView: 'tagsView 操作',
+		funCountup: '数字滚动',
+		funWangEditor: 'Editor 编辑器',
+		funCropper: '图片裁剪',
+		funQrcode: '二维码生成',
+		funEchartsMap: '地理坐标/地图',
+		funPrintJs: '页面打印',
+		funClipboard: '复制剪切',
+		funGridLayout: '拖拽布局',
+		funSplitpanes: '窗格拆分器',
+		funDragVerify: '验证器',
+		pagesIndex: '页面',
+		pagesFiltering: '过滤筛选组件',
+		pagesFilteringDetails: '过滤筛选组件详情',
+		pagesFilteringDetails1: '过滤筛选组件详情111',
+		pagesIocnfont: 'ali 字体图标',
+		pagesElement: 'ele 字体图标',
+		pagesAwesome: 'awe 字体图标',
+		pagesFormAdapt: '表单自适应',
+		pagesTableRules: '表单表格验证',
+		pagesFormI18n: '表单国际化',
+		pagesFormRules: '多表单验证',
+		pagesDynamicForm: '动态复杂表单',
+		pagesWorkflow: '工作流',
+		pagesListAdapt: '列表自适应',
+		pagesWaterfall: '瀑布屏',
+		pagesSteps: '步骤条',
+		pagesPreview: '大图预览',
+		pagesWaves: '波浪效果',
+		pagesTree: '树形改表格',
+		pagesDrag: '拖动指令',
+		pagesLazyImg: '图片懒加载',
+		makeIndex: '组件封装',
+		makeSelector: '图标选择器',
+		makeNoticeBar: '滚动通知栏',
+		makeSvgDemo: 'svgIcon 演示',
+		makeTableDemo: '表格封装演示',
+		paramsIndex: '路由参数',
+		paramsCommon: '普通路由',
+		paramsDynamic: '动态路由',
+		paramsCommonDetails: '普通路由详情',
+		paramsDynamicDetails: '动态路由详情',
+		chartIndex: '大数据图表',
+		visualizingIndex: '数据可视化',
+		visualizingLinkDemo1: '数据可视化演示1',
+		visualizingLinkDemo2: '数据可视化演示2',
+		personal: '个人中心',
+		tools: '工具类集合',
+		layoutLinkView: '外链',
+		layoutIframeViewOne: '内嵌 iframe1',
+		layoutIframeViewTwo: '内嵌 iframe2',
+	},
+	staticRoutes: {
+		signIn: '登录',
+		notFound: '找不到此页面',
+		noPower: '没有权限',
+	},
+	user: {
+		title0: '组件大小',
+		title1: '语言切换',
+		title2: '菜单搜索',
+		title3: '布局配置',
+		title4: '消息',
+		title5: '开全屏',
+		title6: '关全屏',
+		dropdownLarge: '大型',
+		dropdownDefault: '默认',
+		dropdownSmall: '小型',
+		dropdown1: '首页',
+		dropdown2: '个人中心',
+		dropdown3: '404',
+		dropdown4: '401',
+		dropdown5: '退出登录',
+		dropdown6: '代码仓库',
+		searchPlaceholder: '菜单搜索:支持中文、路由路径',
+		newTitle: '通知',
+		newBtn: '全部已读',
+		newGo: '前往通知中心',
+		newDesc: '暂无通知',
+		logOutTitle: '提示',
+		logOutMessage: '此操作将退出登录, 是否继续?',
+		logOutConfirm: '确定',
+		logOutCancel: '取消',
+		logOutExit: '退出中',
+	},
+	tagsView: {
+		refresh: '刷新',
+		close: '关闭',
+		closeOther: '关闭其它',
+		closeAll: '全部关闭',
+		fullscreen: '当前页全屏',
+		closeFullscreen: '关闭全屏',
+	},
+	notFound: {
+		foundTitle: '您未被授权,没有查阅权限~',
+		foundMsg: '请联系管理员获取授权。',
+		// foundTitle: '地址输入错误,请重新输入地址~',
+		// foundMsg: '您可以先检查网址,然后重新输入或给我们反馈问题。',
+		foundBtn: '返回首页',
+	},
+	noAccess: {
+		accessTitle: '您未被授权,没有操作权限~',
+		accessMsg: '联系方式:加QQ群探讨 665452019',
+		accessBtn: '重新授权',
+	},
+	layout: {
+		configTitle: '布局配置',
+		oneTitle: '全局主题',
+		twoTopTitle: '顶栏设置',
+		twoMenuTitle: '菜单设置',
+		twoColumnsTitle: '分栏设置',
+		twoTopBar: '顶栏背景',
+		twoTopBarColor: '顶栏默认字体颜色',
+		twoIsTopBarColorGradual: '顶栏背景渐变',
+		twoMenuBar: '菜单背景',
+		twoMenuBarColor: '菜单默认字体颜色',
+		twoMenuBarActiveColor: '菜单高亮背景色',
+		twoIsMenuBarColorGradual: '菜单背景渐变',
+		twoColumnsMenuBar: '分栏菜单背景',
+		twoColumnsMenuBarColor: '分栏菜单默认字体颜色',
+		twoIsColumnsMenuBarColorGradual: '分栏菜单背景渐变',
+		twoIsColumnsMenuHoverPreload: '分栏菜单鼠标悬停预加载',
+		threeTitle: '界面设置',
+		threeIsCollapse: '菜单水平折叠',
+		threeIsUniqueOpened: '菜单手风琴',
+		threeIsFixedHeader: '固定 Header',
+		threeIsClassicSplitMenu: '经典布局分割菜单',
+		threeIsLockScreen: '开启锁屏',
+		threeLockScreenTime: '自动锁屏(s/秒)',
+		fourTitle: '界面显示',
+		fourIsShowLogo: '侧边栏 Logo',
+		fourIsBreadcrumb: '开启 Breadcrumb',
+		fourIsBreadcrumbIcon: '开启 Breadcrumb 图标',
+		fourIsTagsview: '开启 Tagsview',
+		fourIsTagsviewIcon: '开启 Tagsview 图标',
+		fourIsCacheTagsView: '开启 TagsView 缓存',
+		fourIsSortableTagsView: '开启 TagsView 拖拽',
+		fourIsShareTagsView: '开启 TagsView 共用',
+		fourIsFooter: '开启 Footer',
+		fourIsGrayscale: '灰色模式',
+		fourIsInvert: '色弱模式',
+		fourIsDark: '深色模式',
+		fourIsWartermark: '开启水印',
+		fourWartermarkText: '水印文案',
+		fiveTitle: '其它设置',
+		fiveTagsStyle: 'Tagsview 风格',
+		fiveAnimation: '主页面切换动画',
+		fiveColumnsAsideStyle: '分栏高亮风格',
+		fiveColumnsAsideLayout: '分栏布局风格',
+		sixTitle: '布局切换',
+		sixDefaults: '默认',
+		sixClassic: '经典',
+		sixTransverse: '横向',
+		sixColumns: '分栏',
+		tipText: '点击下方按钮,复制布局配置去 `src/stores/themeConfig.ts` 中修改。',
+		copyText: '一键复制配置',
+		resetText: '一键恢复默认',
+		copyTextSuccess: '复制成功!',
+		copyTextError: '复制失败!',
+	},
+	upgrade: {
+		title: '新版本升级',
+		msg: '新版本来啦,马上更新尝鲜吧!不用担心,更新很快的哦!',
+		desc: '提示:更新会还原默认配置',
+		btnOne: '残忍拒绝',
+		btnTwo: '马上更新',
+		btnTwoLoading: '更新中',
+	},
+};

+ 192 - 0
h5/src/i18n/lang/zh-tw.ts

@@ -0,0 +1,192 @@
+// 定义内容
+export default {
+	router: {
+		home: '首頁',
+		system: '系統設置',
+		systemMenu: '選單管理',
+		systemRole: '角色管理',
+		systemUser: '用戶管理',
+		systemDept: '部門管理',
+		systemDic: '字典管理',
+		limits: '許可權管理',
+		limitsFrontEnd: '前端控制',
+		limitsFrontEndPage: '頁面許可權',
+		limitsFrontEndBtn: '按鈕許可權',
+		limitsBackEnd: '後端控制',
+		limitsBackEndEndPage: '頁面許可權',
+		menu: '選單嵌套',
+		menu1: '選單1',
+		menu11: '選單11',
+		menu12: '選單12',
+		menu121: '選單121',
+		menu122: '選單122',
+		menu13: '選單13',
+		menu2: '選單2',
+		funIndex: '功能',
+		funTagsView: 'tagsView 操作',
+		funCountup: '數位滾動',
+		funWangEditor: 'Editor 編輯器',
+		funCropper: '圖片裁剪',
+		funQrcode: '二維碼生成',
+		funEchartsMap: '地理座標/地圖',
+		funPrintJs: '頁面列印',
+		funClipboard: '複製剪切',
+		funGridLayout: '拖拽佈局',
+		funSplitpanes: '窗格折開器',
+		funDragVerify: '驗證器',
+		pagesIndex: '頁面',
+		pagesFiltering: '過濾篩選組件',
+		pagesFilteringDetails: '過濾篩選組件詳情',
+		pagesFilteringDetails1: '過濾篩選組件詳情111',
+		pagesIocnfont: 'ali 字體圖標',
+		pagesElement: 'ele 字體圖標',
+		pagesAwesome: 'awe 字體圖標',
+		pagesFormAdapt: '表單自我調整',
+		pagesTableRules: '表單表格驗證',
+		pagesFormI18n: '表單國際化',
+		pagesFormRules: '多表單驗證',
+		pagesDynamicForm: '動態複雜表單',
+		pagesWorkflow: '工作流',
+		pagesListAdapt: '清單自我調整',
+		pagesWaterfall: '瀑布屏',
+		pagesSteps: '步驟條',
+		pagesPreview: '大圖預覽',
+		pagesWaves: '波浪效果',
+		pagesTree: '樹形改表格',
+		pagesDrag: '拖動指令',
+		pagesLazyImg: '圖片懶加載',
+		makeIndex: '組件封裝',
+		makeSelector: '圖標選擇器',
+		makeNoticeBar: '滾動通知欄',
+		makeSvgDemo: 'svgIcon 演示',
+		makeTableDemo: '表格封裝演示',
+		paramsIndex: '路由參數',
+		paramsCommon: '普通路由',
+		paramsDynamic: '動態路由',
+		paramsCommonDetails: '普通路由詳情',
+		paramsDynamicDetails: '動態路由詳情',
+		chartIndex: '大資料圖表',
+		visualizingIndex: '數據視覺化',
+		visualizingLinkDemo1: '數據視覺化演示1',
+		visualizingLinkDemo2: '數據視覺化演示2',
+		personal: '個人中心',
+		tools: '工具類集合',
+		layoutLinkView: '外鏈',
+		layoutIframeViewOne: '内嵌 iframe1',
+		layoutIframeViewTwo: '内嵌 iframe2',
+	},
+	staticRoutes: {
+		signIn: '登入',
+		notFound: '找不到此頁面',
+		noPower: '沒有許可權',
+	},
+	user: {
+		title0: '組件大小',
+		title1: '語言切換',
+		title2: '選單蒐索',
+		title3: '佈局配寘',
+		title4: '消息',
+		title5: '開全屏',
+		title6: '關全屏',
+		dropdownLarge: '大型',
+		dropdownDefault: '默認',
+		dropdownSmall: '小型',
+		dropdown1: '首頁',
+		dropdown2: '個人中心',
+		dropdown3: '404',
+		dropdown4: '401',
+		dropdown5: '登出',
+		dropdown6: '程式碼倉庫',
+		searchPlaceholder: '選單蒐索:支援中文、路由路徑',
+		newTitle: '通知',
+		newBtn: '全部已讀',
+		newGo: '前往通知中心',
+		newDesc: '暫無通知',
+		logOutTitle: '提示',
+		logOutMessage: '此操作將登出,是否繼續?',
+		logOutConfirm: '確定',
+		logOutCancel: '取消',
+		logOutExit: '退出中',
+	},
+	tagsView: {
+		refresh: '重繪',
+		close: '關閉',
+		closeOther: '關閉其它',
+		closeAll: '全部關閉',
+		fullscreen: '當前頁全屏',
+		closeFullscreen: '關閉全屏',
+	},
+	notFound: {
+		foundTitle: '地址輸入錯誤,請重新輸入地址~',
+		foundMsg: '您可以先檢查網址,然後重新輸入或給我們迴響問題。',
+		foundBtn: '返回首頁',
+	},
+	noAccess: {
+		accessTitle: '您未被授權,沒有操作許可權~',
+		accessMsg: '聯繫方式:加QQ群探討665452019',
+		accessBtn: '重新授權',
+	},
+	layout: {
+		configTitle: '佈局配寘',
+		oneTitle: '全域主題',
+		twoTopTitle: '頂欄設定',
+		twoMenuTitle: '選單設定',
+		twoColumnsTitle: '分欄設定',
+		twoTopBar: '頂欄背景',
+		twoTopBarColor: '頂欄默認字體顏色',
+		twoIsTopBarColorGradual: '頂欄背景漸變',
+		twoMenuBar: '選單背景',
+		twoMenuBarColor: '選單默認字體顏色',
+		twoMenuBarActiveColor: '選單高亮背景色',
+		twoIsMenuBarColorGradual: '選單背景漸變',
+		twoColumnsMenuBar: '分欄選單背景',
+		twoColumnsMenuBarColor: '分欄選單默認字體顏色',
+		twoIsColumnsMenuBarColorGradual: '分欄選單背景漸變',
+		twoIsColumnsMenuHoverPreload: '分欄選單滑鼠懸停預加載',
+		threeTitle: '介面設定',
+		threeIsCollapse: '選單水准折疊',
+		threeIsUniqueOpened: '選單手風琴',
+		threeIsFixedHeader: '固定 Header',
+		threeIsClassicSplitMenu: '經典佈局分割選單',
+		threeIsLockScreen: '開啟鎖屏',
+		threeLockScreenTime: '自動鎖屏(s/秒)',
+		fourTitle: '介面顯示',
+		fourIsShowLogo: '側邊欄 Logo',
+		fourIsBreadcrumb: '開啟 Breadcrumb',
+		fourIsBreadcrumbIcon: '開啟 Breadcrumb 圖標',
+		fourIsTagsview: '開啟 Tagsview',
+		fourIsTagsviewIcon: '開啟 Tagsview 圖標',
+		fourIsCacheTagsView: '開啟 TagsView 緩存',
+		fourIsSortableTagsView: '開啟 TagsView 拖拽',
+		fourIsShareTagsView: '開啟 TagsView 共用',
+		fourIsFooter: '開啟 Footer',
+		fourIsGrayscale: '灰色模式',
+		fourIsInvert: '色弱模式',
+		fourIsDark: '深色模式',
+		fourIsWartermark: '開啟浮水印',
+		fourWartermarkText: '浮水印文案',
+		fiveTitle: '其它設定',
+		fiveTagsStyle: 'Tagsview 風格',
+		fiveAnimation: '主頁面切換動畫',
+		fiveColumnsAsideStyle: '分欄高亮風格',
+		fiveColumnsAsideLayout: '分欄佈局風格',
+		sixTitle: '佈局切換',
+		sixDefaults: '默認',
+		sixClassic: '經典',
+		sixTransverse: '橫向',
+		sixColumns: '分欄',
+		tipText: '點擊下方按鈕,複製佈局配寘去`src/stores/themeConfig.ts`中修改。',
+		copyText: '一鍵複製配寘',
+		resetText: '一鍵恢復默認',
+		copyTextSuccess: '複製成功!',
+		copyTextError: '複製失敗!',
+	},
+	upgrade: {
+		title: '新版本陞級',
+		msg: '新版本來啦,馬上更新嘗鮮吧! 不用擔心,更新很快的哦!',
+		desc: '提示:更新會還原默認配寘',
+		btnOne: '殘忍拒絕',
+		btnTwo: '馬上更新',
+		btnTwoLoading: '更新中',
+	},
+};

+ 13 - 0
h5/src/i18n/pages/formI18n/en.ts

@@ -0,0 +1,13 @@
+// 定义内容
+export default {
+	formI18nLabel: {
+		name: 'name',
+		email: 'email',
+		autograph: 'autograph',
+	},
+	formI18nPlaceholder: {
+		name: 'Please enter your name',
+		email: 'Please enter the users Department',
+		autograph: 'Please enter the login account name',
+	},
+};

+ 13 - 0
h5/src/i18n/pages/formI18n/zh-cn.ts

@@ -0,0 +1,13 @@
+// 定义内容
+export default {
+	formI18nLabel: {
+		name: '姓名',
+		email: '用户归属部门',
+		autograph: '登陆账户名',
+	},
+	formI18nPlaceholder: {
+		name: '请输入姓名',
+		email: '请输入用户归属部门',
+		autograph: '请输入登陆账户名',
+	},
+};

+ 13 - 0
h5/src/i18n/pages/formI18n/zh-tw.ts

@@ -0,0 +1,13 @@
+// 定义内容
+export default {
+	formI18nLabel: {
+		name: '姓名',
+		email: '用戶歸屬部門',
+		autograph: '登入帳戶名',
+	},
+	formI18nPlaceholder: {
+		name: '請輸入姓名',
+		email: '請輸入用戶歸屬部門',
+		autograph: '請輸入登入帳戶名',
+	},
+};

+ 29 - 0
h5/src/i18n/pages/login/en.ts

@@ -0,0 +1,29 @@
+// 定义内容
+export default {
+	label: {
+		one1: 'User name login',
+		two2: 'Mobile number',
+	},
+	link: {
+		one3: 'Third party login',
+		two4: 'Links',
+	},
+	account: {
+		accountPlaceholder1: 'The user name admin or not is common',
+		accountPlaceholder2: 'Password: 123456',
+		accountPlaceholder3: 'Please enter the verification code',
+		accountBtnText: 'Sign in',
+	},
+	mobile: {
+		placeholder1: 'Please input mobile phone number',
+		placeholder2: 'Please enter the verification code',
+		codeText: 'Get code',
+		btnText: 'Sign in',
+		msgText:
+			'Warm tip: it is recommended to use Google, Microsoft edge, version 79.0.1072.62 and above browsers, and 360 browser, please use speed mode',
+	},
+	scan: {
+		text: 'Open the mobile phone to scan and quickly log in / register',
+	},
+	signInText: 'welcome back!',
+};

+ 28 - 0
h5/src/i18n/pages/login/zh-cn.ts

@@ -0,0 +1,28 @@
+// 定义内容
+export default {
+	label: {
+		one1: '用户名登录',
+		two2: '手机号登录',
+	},
+	link: {
+		one3: '第三方登录',
+		two4: '友情链接',
+	},
+	account: {
+		accountPlaceholder1: '请输入用户名',
+		accountPlaceholder2: '请输入密码',
+		accountPlaceholder3: '请输入验证码',
+		accountBtnText: '登 录',
+	},
+	mobile: {
+		placeholder1: '请输入手机号',
+		placeholder2: '请输入验证码',
+		codeText: '获取验证码',
+		btnText: '登 录',
+		msgText: '* 温馨提示:建议使用谷歌、Microsoft Edge,版本 79.0.1072.62 及以上浏览器,360浏览器请使用极速模式',
+	},
+	scan: {
+		text: '打开手机扫一扫,快速登录/注册',
+	},
+	signInText: '欢迎回来!',
+};

+ 28 - 0
h5/src/i18n/pages/login/zh-tw.ts

@@ -0,0 +1,28 @@
+// 定义内容
+export default {
+	label: {
+		one1: '用戶名登入',
+		two2: '手機號登入',
+	},
+	link: {
+		one3: '協力廠商登入',
+		two4: '友情連結',
+	},
+	account: {
+		accountPlaceholder1: '用戶名admin或不輸均為common',
+		accountPlaceholder2: '密碼:123456',
+		accountPlaceholder3: '請輸入驗證碼',
+		accountBtnText: '登入',
+	},
+	mobile: {
+		placeholder1: '請輸入手機號',
+		placeholder2: '請輸入驗證碼',
+		codeText: '獲取驗證碼',
+		btnText: '登入',
+		msgText: '* 溫馨提示:建議使用穀歌、Microsoft Edge,版本79.0.1072.62及以上瀏覽器,360瀏覽器請使用極速模式',
+	},
+	scan: {
+		text: '打開手機掃一掃,快速登錄/注册',
+	},
+	signInText: '歡迎回來!',
+};

+ 154 - 0
h5/src/layout/component/aside.vue

@@ -0,0 +1,154 @@
+<template>
+	<div class="h100" v-show="!isTagsViewCurrenFull">
+		<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="state.menuList" />
+			</el-scrollbar>
+		</el-aside>
+	</div>
+</template>
+
+<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';
+import { useThemeConfig } from '/@/stores/themeConfig';
+import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
+import mittBus from '/@/utils/mitt';
+
+// 引入组件
+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;
+		});
+};
+// 设置菜单导航是否固定(移动端)
+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>

+ 272 - 0
h5/src/layout/component/columnsAside.vue

@@ -0,0 +1,272 @@
+<template>
+	<div class="layout-columns-aside">
+		<el-scrollbar>
+			<ul @mouseleave="onColumnsAsideMenuMouseleave()">
+				<li
+					v-for="(v, k) in state.columnsAsideList"
+					:key="k"
+					@click="onColumnsAsideMenuClick(v, k)"
+					@mouseenter="onColumnsAsideMenuMouseenter(v, k)"
+					:ref="
+						(el) => {
+							if (el) columnsAsideOffsetTopRefs[k] = el;
+						}
+					"
+					: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)">
+						<SvgIcon :name="v.meta.icon" />
+						<div class="columns-vertical-title font12">
+							{{
+								$t(v.meta.title) && $t(v.meta.title).length >= 4
+									? $t(v.meta.title).substr(0, themeConfig.columnsAsideLayout === 'columns-vertical' ? 4 : 3)
+									: $t(v.meta.title)
+							}}
+						</div>
+					</div>
+					<div :class="themeConfig.columnsAsideLayout" v-else>
+						<a :href="v.meta.isLink" target="_blank">
+							<SvgIcon :name="v.meta.icon" />
+							<div class="columns-vertical-title font12">
+								{{
+									$t(v.meta.title) && $t(v.meta.title).length >= 4
+										? $t(v.meta.title).substr(0, themeConfig.columnsAsideLayout === 'columns-vertical' ? 4 : 3)
+										: $t(v.meta.title)
+								}}
+							</div>
+						</a>
+					</div>
+				</li>
+				<div ref="columnsAsideActiveRef" :class="themeConfig.columnsAsideStyle"></div>
+			</ul>
+		</el-scrollbar>
+	</div>
+</template>
+
+<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';
+
+// 定义变量内容
+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: [],
+});
+
+// 设置菜单高亮位置移动
+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;
+		});
+};
+// 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">
+.layout-columns-aside {
+	width: 70px;
+	height: 100%;
+	background: var(--next-bg-columnsMenuBar);
+	ul {
+		position: relative;
+		.layout-columns-active {
+			color: var(--next-bg-columnsMenuBarColor) !important;
+			transition: 0.3s ease-in-out;
+		}
+		.layout-columns-hover {
+			color: var(--el-color-primary);
+			a {
+				color: var(--el-color-primary);
+			}
+		}
+		li {
+			color: var(--next-bg-columnsMenuBarColor);
+			width: 100%;
+			height: 50px;
+			text-align: center;
+			display: flex;
+			cursor: pointer;
+			position: relative;
+			z-index: 1;
+			&:hover {
+				@extend .layout-columns-hover;
+			}
+			.columns-vertical {
+				margin: auto;
+				.columns-vertical-title {
+					padding-top: 1px;
+				}
+			}
+			.columns-horizontal {
+				display: flex;
+				height: 50px;
+				width: 100%;
+				align-items: center;
+				padding: 0 5px;
+				i {
+					margin-right: 3px;
+				}
+				a {
+					display: flex;
+					.columns-horizontal-title {
+						padding-top: 1px;
+					}
+				}
+			}
+			a {
+				text-decoration: none;
+				color: var(--next-bg-columnsMenuBarColor);
+			}
+		}
+		.columns-round {
+			background: var(--el-color-primary);
+			color: var(--el-color-white);
+			position: absolute;
+			left: 50%;
+			top: 2px;
+			height: 44px;
+			width: 65px;
+			transform: translateX(-50%);
+			z-index: 0;
+			transition: 0.3s ease-in-out;
+			border-radius: 5px;
+		}
+		.columns-card {
+			@extend .columns-round;
+			top: 0;
+			height: 50px;
+			width: 100%;
+			border-radius: 0;
+		}
+	}
+}
+</style>

+ 18 - 0
h5/src/layout/component/header.vue

@@ -0,0 +1,18 @@
+<template>
+	<el-header class="layout-header" v-show="!isTagsViewCurrenFull">
+		<NavBarsIndex />
+	</el-header>
+</template>
+
+<script setup lang="ts" name="layoutHeader">
+import { defineAsyncComponent } from 'vue';
+import { storeToRefs } from 'pinia';
+import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
+
+// 引入组件
+const NavBarsIndex = defineAsyncComponent(() => import('/@/layout/navBars/index.vue'));
+
+// 定义变量内容
+const storesTagsViewRoutes = useTagsViewRoutes();
+const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
+</script>

+ 65 - 0
h5/src/layout/component/main.vue

@@ -0,0 +1,65 @@
+<template>
+	<el-main class="layout-main" :style="isFixedHeader ? `height: calc(100% - ${setMainHeight})` : `minHeight: calc(100% - ${setMainHeight})`">
+		<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="setBacktopClass" />
+	</el-main>
+</template>
+
+<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';
+
+// 引入组件
+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>

+ 25 - 0
h5/src/layout/footer/index.vue

@@ -0,0 +1,25 @@
+<template>
+	<div class="layout-footer pb15">
+		<div class="layout-footer-warp">
+			<div>vue-next-admin,Made by lyt with ❤️</div>
+			<div class="mt5">深圳市 xxx 公司版权所有</div>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts" name="layoutFooter">
+// 此处需有内容(注释也得),否则缓存将失败
+</script>
+
+<style scoped lang="scss">
+.layout-footer {
+	width: 100%;
+	display: flex;
+	&-warp {
+		margin: auto;
+		color: var(--el-text-color-secondary);
+		text-align: center;
+		animation: error-num 0.3s ease;
+	}
+}
+</style>

+ 50 - 0
h5/src/layout/index.vue

@@ -0,0 +1,50 @@
+<template>
+	<component :is="layouts[themeConfig.layout]" />
+</template>
+
+<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';
+
+// 引入组件
+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,
+		});
+	} else {
+		mittBus.emit('layoutMobileResize', {
+			layout: Local.get('oldLayout') ? Local.get('oldLayout') : themeConfig.value.layout,
+			clientWidth,
+		});
+	}
+};
+// 页面加载前
+onBeforeMount(() => {
+	onLayoutResize();
+	window.addEventListener('resize', onLayoutResize);
+});
+// 页面卸载时
+onUnmounted(() => {
+	window.removeEventListener('resize', onLayoutResize);
+});
+</script>

+ 352 - 0
h5/src/layout/lockScreen/index.vue

@@ -0,0 +1,352 @@
+<template>
+	<div v-show="state.isShowLockScreen">
+		<div class="layout-lock-screen-mask"></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="onDownPc"
+				@mousemove="onMovePc"
+				@mouseup="onEnd"
+				@touchstart.stop="onDownApp"
+				@touchmove.stop="onMoveApp"
+				@touchend.stop="onEnd"
+			>
+				<div class="layout-lock-screen-date-box">
+					<div class="layout-lock-screen-date-box-time">
+						{{ state.time.hm }}<span class="layout-lock-screen-date-box-minutes">{{ state.time.s }}</span>
+					</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" />
+					<div class="layout-lock-screen-date-top-text">上滑解锁</div>
+				</div>
+			</div>
+			<transition name="el-zoom-in-center">
+				<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="state.lockScreenPassword"
+								@keyup.enter.native.stop="onLockScreenSubmit()"
+							>
+								<template #append>
+									<el-button @click="onLockScreenSubmit">
+										<el-icon class="el-input__icon">
+											<ele-Right />
+										</el-icon>
+									</el-button>
+								</template>
+							</el-input>
+						</div>
+					</div>
+					<div class="layout-lock-screen-login-icon">
+						<SvgIcon name="ele-Microphone" :size="20" />
+						<SvgIcon name="ele-AlarmClock" :size="20" />
+						<SvgIcon name="ele-SwitchButton" :size="20" />
+					</div>
+				</div>
+			</transition>
+		</div>
+	</div>
+</template>
+
+<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';
+
+// 定义变量内容
+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: '',
+		s: '',
+		mdq: '',
+	},
+	setIntervalTime: 0,
+	isShowLockScreen: false,
+	isShowLockScreenIntervalTime: 0,
+	lockScreenPassword: '',
+});
+
+// 鼠标按下 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;
+			}
+			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>
+
+<style scoped lang="scss">
+.layout-lock-screen-fixed {
+	position: fixed;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+}
+.layout-lock-screen-filter {
+	filter: blur(1px);
+}
+.layout-lock-screen-mask {
+	background: var(--el-color-white);
+	@extend .layout-lock-screen-fixed;
+	z-index: 9999990;
+}
+.layout-lock-screen-img {
+	@extend .layout-lock-screen-fixed;
+	background-image: url('https://img-blog.csdnimg.cn/afa9c317667f47d5bea34b85af45979e.png#pic_center');
+	background-size: 100% 100%;
+	z-index: 9999991;
+}
+.layout-lock-screen {
+	@extend .layout-lock-screen-fixed;
+	z-index: 9999992;
+	&-date {
+		position: absolute;
+		left: 0;
+		top: 0;
+		width: 100%;
+		height: 100%;
+		color: var(--el-color-white);
+		z-index: 9999993;
+		user-select: none;
+		&-box {
+			position: absolute;
+			left: 30px;
+			bottom: 50px;
+			&-time {
+				font-size: 100px;
+				color: var(--el-color-white);
+			}
+			&-info {
+				font-size: 40px;
+				color: var(--el-color-white);
+			}
+			&-minutes {
+				font-size: 16px;
+			}
+		}
+		&-top {
+			width: 40px;
+			height: 40px;
+			line-height: 40px;
+			border-radius: 100%;
+			border: 1px solid var(--el-border-color-light, #ebeef5);
+			background: rgba(255, 255, 255, 0.1);
+			color: var(--el-color-white);
+			opacity: 0.8;
+			position: absolute;
+			right: 30px;
+			bottom: 50px;
+			text-align: center;
+			overflow: hidden;
+			transition: all 0.3s ease;
+			i {
+				transition: all 0.3s ease;
+			}
+			&-text {
+				opacity: 0;
+				position: absolute;
+				top: 150%;
+				font-size: 12px;
+				color: var(--el-color-white);
+				left: 50%;
+				line-height: 1.2;
+				transform: translate(-50%, -50%);
+				transition: all 0.3s ease;
+				width: 35px;
+			}
+			&:hover {
+				border: 1px solid rgba(255, 255, 255, 0.5);
+				background: rgba(255, 255, 255, 0.2);
+				box-shadow: 0 0 12px 0 rgba(255, 255, 255, 0.5);
+				color: var(--el-color-white);
+				opacity: 1;
+				transition: all 0.3s ease;
+				i {
+					transform: translateY(-40px);
+					transition: all 0.3s ease;
+				}
+				.layout-lock-screen-date-top-text {
+					opacity: 1;
+					top: 50%;
+					transition: all 0.3s ease;
+				}
+			}
+		}
+	}
+	&-login {
+		position: relative;
+		z-index: 9999994;
+		width: 100%;
+		height: 100%;
+		left: 0;
+		top: 0;
+		display: flex;
+		flex-direction: column;
+		justify-content: center;
+		color: var(--el-color-white);
+		&-box {
+			text-align: center;
+			margin: auto;
+			&-img {
+				width: 180px;
+				height: 180px;
+				margin: auto;
+				img {
+					width: 100%;
+					height: 100%;
+					border-radius: 100%;
+				}
+			}
+			&-name {
+				font-size: 26px;
+				margin: 15px 0 30px;
+			}
+		}
+		&-icon {
+			position: absolute;
+			right: 30px;
+			bottom: 30px;
+			i {
+				font-size: 20px;
+				margin-left: 15px;
+				cursor: pointer;
+				opacity: 0.8;
+				&:hover {
+					opacity: 1;
+				}
+			}
+		}
+	}
+}
+:deep(.el-input-group__append) {
+	background: var(--el-color-white);
+	padding: 0px 15px;
+}
+:deep(.el-input__inner) {
+	border-right-color: var(--el-border-color-extra-light);
+	&:hover {
+		border-color: var(--el-border-color-extra-light);
+	}
+}
+</style>

+ 75 - 0
h5/src/layout/logo/index.vue

@@ -0,0 +1,75 @@
+<template>
+	<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
+		<img :src="logoMini" class="layout-logo-medium-img" />
+		<span>{{ themeConfig.globalTitle }}</span>
+	</div>
+	<div class="layout-logo-size" v-else @click="onThemeConfigChange">
+		<img :src="logoMini" class="layout-logo-size-img" />
+	</div>
+</template>
+
+<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';
+
+// 定义变量内容
+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;
+});
+// logo 点击实现菜单展开/收起
+const onThemeConfigChange = () => {
+	if (themeConfig.value.layout === 'transverse') return false;
+	themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
+};
+</script>
+
+<style scoped lang="scss">
+.layout-logo {
+	width: 220px;
+	height: 50px;
+	display: flex;
+	align-items: center;
+	justify-content: center;
+	box-shadow: rgb(0 21 41 / 2%) 0px 1px 4px;
+	color: var(--el-color-primary);
+	font-size: 16px;
+	cursor: pointer;
+	animation: logoAnimation 0.3s ease-in-out;
+	span {
+		white-space: nowrap;
+		display: inline-block;
+	}
+	&:hover {
+		span {
+			color: var(--color-primary-light-2);
+		}
+	}
+	&-medium-img {
+		width: 20px;
+		margin-right: 5px;
+	}
+}
+.layout-logo-size {
+	width: 100%;
+	height: 50px;
+	display: flex;
+	cursor: pointer;
+	animation: logoAnimation 0.3s ease-in-out;
+	&-img {
+		width: 20px;
+		margin: auto;
+	}
+	&:hover {
+		img {
+			animation: logoAnimation 0.3s ease-in-out;
+		}
+	}
+}
+</style>

+ 71 - 0
h5/src/layout/main/classic.vue

@@ -0,0 +1,71 @@
+<template>
+	<el-container class="layout-container flex-center">
+		<LayoutHeader />
+		<el-container class="layout-mian-height-50">
+			<LayoutAside />
+			<div class="flex-center layout-backtop">
+				<LayoutTagsView v-if="isTagsview" />
+				<LayoutMain ref="layoutMainRef" />
+			</div>
+		</el-container>
+	</el-container>
+</template>
+
+<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';
+
+// 引入组件
+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>

+ 71 - 0
h5/src/layout/main/columns.vue

@@ -0,0 +1,71 @@
+<template>
+	<el-container class="layout-container">
+		<ColumnsAside />
+		<el-container class="layout-columns-warp layout-container-view h100">
+			<LayoutAside />
+			<el-scrollbar ref="layoutScrollbarRef" class="layout-backtop">
+				<LayoutHeader />
+				<LayoutMain ref="layoutMainRef" />
+			</el-scrollbar>
+		</el-container>
+	</el-container>
+</template>
+
+<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';
+
+// 引入组件
+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>

+ 71 - 0
h5/src/layout/main/defaults.vue

@@ -0,0 +1,71 @@
+<template>
+	<el-container class="layout-container">
+		<LayoutAside />
+		<el-container class="layout-container-view h100">
+			<el-scrollbar ref="layoutScrollbarRef" class="layout-backtop">
+				<LayoutHeader />
+				<LayoutMain ref="layoutMainRef" />
+			</el-scrollbar>
+		</el-container>
+	</el-container>
+</template>
+
+<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';
+
+// 引入组件
+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>

+ 58 - 0
h5/src/layout/main/transverse.vue

@@ -0,0 +1,58 @@
+<template>
+	<el-container class="layout-container flex-center layout-backtop">
+		<LayoutHeader />
+		<LayoutMain ref="layoutMainRef" />
+	</el-container>
+</template>
+
+<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';
+
+// 引入组件
+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>

+ 146 - 0
h5/src/layout/navBars/breadcrumb/breadcrumb.vue

@@ -0,0 +1,146 @@
+<template>
+	<div v-if="isShowBreadcrumb" class="layout-navbars-breadcrumb">
+		<SvgIcon
+			class="layout-navbars-breadcrumb-icon"
+			:name="themeConfig.isCollapse ? 'ele-Expand' : 'ele-Fold'"
+			:size="16"
+			@click="onThemeConfigChange"
+		/>
+		<el-breadcrumb class="layout-navbars-breadcrumb-hide">
+			<transition-group name="breadcrumb">
+				<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>
+					</span>
+					<a v-else @click.prevent="onBreadcrumbClick(v)">
+						<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />{{ $t(v.meta.title) }}
+					</a>
+				</el-breadcrumb-item>
+			</transition-group>
+		</el-breadcrumb>
+	</div>
+</template>
+
+<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';
+import { storeToRefs } from 'pinia';
+import { useThemeConfig } from '/@/stores/themeConfig';
+import { useRoutesList } from '/@/stores/routesList';
+
+// 定义变量内容
+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: 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);
+			}
+		});
+	});
+};
+// 当前路由字符串切割成数组,并删除第一项空内容
+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>
+
+<style scoped lang="scss">
+.layout-navbars-breadcrumb {
+	flex: 1;
+	height: inherit;
+	display: flex;
+	align-items: center;
+	.layout-navbars-breadcrumb-icon {
+		cursor: pointer;
+		font-size: 18px;
+		color: var(--next-bg-topBarColor);
+		height: 100%;
+		width: 40px;
+		opacity: 0.8;
+		&:hover {
+			opacity: 1;
+		}
+	}
+	.layout-navbars-breadcrumb-span {
+		display: flex;
+		opacity: 0.7;
+		color: var(--next-bg-topBarColor);
+	}
+	.layout-navbars-breadcrumb-iconfont {
+		font-size: 14px;
+		margin-right: 5px;
+	}
+	:deep(.el-breadcrumb__separator) {
+		opacity: 0.7;
+		color: var(--next-bg-topBarColor);
+	}
+	:deep(.el-breadcrumb__inner a, .el-breadcrumb__inner.is-link) {
+		font-weight: unset !important;
+		color: var(--next-bg-topBarColor);
+		&:hover {
+			color: var(--el-color-primary) !important;
+		}
+	}
+}
+</style>

+ 53 - 0
h5/src/layout/navBars/breadcrumb/closeFull.vue

@@ -0,0 +1,53 @@
+<template>
+	<div class="layout-navbars-close-full" v-if="isTagsViewCurrenFull">
+		<div class="layout-navbars-close-full-icon">
+			<SvgIcon name="ele-Close" :title="$t('message.tagsView.closeFullscreen')" @click="onCloseFullscreen" />
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts" name="layoutCloseFull">
+import { storeToRefs } from 'pinia';
+import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
+
+// 定义变量内容
+const stores = useTagsViewRoutes();
+const { isTagsViewCurrenFull } = storeToRefs(stores);
+
+// 关闭当前全屏
+const onCloseFullscreen = () => {
+	stores.setCurrenFullscreen(false);
+};
+</script>
+
+<style scoped lang="scss">
+.layout-navbars-close-full {
+	position: fixed;
+	z-index: 9999999999;
+	right: -30px;
+	top: -30px;
+	.layout-navbars-close-full-icon {
+		width: 60px;
+		height: 60px;
+		border-radius: 100%;
+		cursor: pointer;
+		background: rgba(0, 0, 0, 0.1);
+		transition: all 0.3s ease;
+		position: relative;
+		:deep(i) {
+			position: absolute;
+			left: 10px;
+			top: 35px;
+			color: #333333;
+			transition: all 0.3s ease;
+		}
+	}
+	&:hover {
+		transition: all 0.3s ease;
+		:deep(i) {
+			color: var(--el-color-primary);
+			transition: all 0.3s ease;
+		}
+	}
+}
+</style>

+ 143 - 0
h5/src/layout/navBars/breadcrumb/index.vue

@@ -0,0 +1,143 @@
+<template>
+	<div class="layout-navbars-breadcrumb-index">
+		<Logo v-if="setIsShowLogo" />
+		<Breadcrumb />
+		<Horizontal :menuList="state.menuList" v-if="isLayoutTransverse" />
+		<User ref="userFun" @refresh="getList()" />
+	</div>
+</template>
+
+<script setup lang="ts" name="layoutBreadcrumbIndex">
+import { defineAsyncComponent, computed, reactive, onMounted, onUnmounted, ref } from 'vue';
+import { ElMessageBox, ElMessage } from 'element-plus';
+import { useRoute } from 'vue-router';
+import { storeToRefs } from 'pinia';
+import { useRoutesList } from '/@/stores/routesList';
+import { useThemeConfig } from '/@/stores/themeConfig';
+import mittBus from '/@/utils/mitt';
+import Index from '/@/api/index.ts';
+
+const userFun = ref();
+const getList = async() => {
+	let newsList = [];
+	let res = await Index.message(state.param);
+	if(res.code != 0){
+		return console.log(res.msg);
+	}
+	res.data.data.forEach((item:any) => {
+		if(item.is_read==0){
+			newsList.push({
+				label: item.title,
+				value: item.content,
+				time: item.update_time,
+				is_read: item.is_read,
+			});
+		}
+	});
+	if(newsList.length<=0){
+		console.log('newsList.length',newsList.length);
+		userFun.value.isDot(false);
+	}else{
+		console.log('newsList.length',newsList.length);
+		userFun.value.isDot(true);
+	}
+}
+
+// 引入组件
+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'));
+
+// 定义变量内容
+const stores = useRoutesList();
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const { routesList } = storeToRefs(stores);
+const route = useRoute();
+const state = reactive({
+	menuList: [] as RouteItems,
+	param: {
+		keyword: '',
+		page: 1,
+		list_rows: 10,
+	},
+});
+
+// 设置 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;
+		});
+};
+// 传送当前子级数据到菜单中
+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();
+	});
+	setTimeout(()=>{
+		getList();
+	},500)
+});
+// 页面卸载时
+onUnmounted(() => {
+	mittBus.off('getBreadcrumbIndexSetFilterRoutes', () => {});
+});
+</script>
+
+<style scoped lang="scss">
+.layout-navbars-breadcrumb-index {
+	height: 50px;
+	display: flex;
+	align-items: center;
+	background: var(--next-bg-topBar);
+	border-bottom: 1px solid var(--next-border-color-light);
+}
+</style>

+ 124 - 0
h5/src/layout/navBars/breadcrumb/search.vue

@@ -0,0 +1,124 @@
+<template>
+	<div class="layout-search-dialog">
+		<el-dialog v-model="state.isShowSearch" destroy-on-close :show-close="false">
+			<template #footer>
+				<el-autocomplete
+					v-model="state.menuQuery"
+					:fetch-suggestions="menuSearch"
+					:placeholder="$t('message.user.searchPlaceholder')"
+					ref="layoutMenuAutocompleteRef"
+					@select="onHandleSelect"
+					:fit-input-width="true"
+				>
+					<template #prefix>
+						<el-icon class="el-input__icon">
+							<ele-Search />
+						</el-icon>
+					</template>
+					<template #default="{ item }">
+						<div>
+							<SvgIcon :name="item.meta.icon" class="mr5" />
+							{{ $t(item.meta.title) }}
+						</div>
+					</template>
+				</el-autocomplete>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<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';
+
+// 定义变量内容
+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 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>
+
+<style scoped lang="scss">
+.layout-search-dialog {
+	position: relative;
+	:deep(.el-dialog) {
+		.el-dialog__header,
+		.el-dialog__body {
+			display: none;
+		}
+		.el-dialog__footer {
+			position: absolute;
+			left: 50%;
+			transform: translateX(-50%);
+			top: -53vh;
+		}
+	}
+	:deep(.el-autocomplete) {
+		width: 560px;
+		position: absolute;
+		top: 150px;
+		left: 50%;
+		transform: translateX(-50%);
+	}
+}
+</style>

+ 824 - 0
h5/src/layout/navBars/breadcrumb/setings.vue

@@ -0,0 +1,824 @@
+<template>
+	<div class="layout-breadcrumb-seting">
+		<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>
+				<div class="layout-breadcrumb-seting-bar-flex">
+					<div class="layout-breadcrumb-seting-bar-flex-label">primary</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-color-picker v-model="getThemeConfig.primary" size="default" @change="onColorPickerChange"> </el-color-picker>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsDark') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch v-model="getThemeConfig.isIsDark" size="small" @change="onAddDarkChange"></el-switch>
+					</div>
+				</div>
+
+				<!-- 顶栏设置 -->
+				<el-divider content-position="left">{{ $t('message.layout.twoTopTitle') }}</el-divider>
+				<div class="layout-breadcrumb-seting-bar-flex">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoTopBar') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-color-picker v-model="getThemeConfig.topBar" size="default" @change="onBgColorPickerChange('topBar')"> </el-color-picker>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoTopBarColor') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-color-picker v-model="getThemeConfig.topBarColor" size="default" @change="onBgColorPickerChange('topBarColor')"> </el-color-picker>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt10">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoIsTopBarColorGradual') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch v-model="getThemeConfig.isTopBarColorGradual" size="small" @change="onTopBarGradualChange"></el-switch>
+					</div>
+				</div>
+
+				<!-- 菜单设置 -->
+				<el-divider content-position="left">{{ $t('message.layout.twoMenuTitle') }}</el-divider>
+				<div class="layout-breadcrumb-seting-bar-flex">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoMenuBar') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-color-picker v-model="getThemeConfig.menuBar" size="default" @change="onBgColorPickerChange('menuBar')"> </el-color-picker>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoMenuBarColor') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-color-picker v-model="getThemeConfig.menuBarColor" size="default" @change="onBgColorPickerChange('menuBarColor')"> </el-color-picker>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoMenuBarActiveColor') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-color-picker
+							v-model="getThemeConfig.menuBarActiveColor"
+							size="default"
+							show-alpha
+							@change="onBgColorPickerChange('menuBarActiveColor')"
+						/>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt14">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoIsMenuBarColorGradual') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch v-model="getThemeConfig.isMenuBarColorGradual" size="small" @change="onMenuBarGradualChange"></el-switch>
+					</div>
+				</div>
+
+				<!-- 分栏设置 -->
+				<el-divider content-position="left" :style="{ opacity: getThemeConfig.layout !== 'columns' ? 0.5 : 1 }">{{
+					$t('message.layout.twoColumnsTitle')
+				}}</el-divider>
+				<div class="layout-breadcrumb-seting-bar-flex" :style="{ opacity: getThemeConfig.layout !== 'columns' ? 0.5 : 1 }">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoColumnsMenuBar') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-color-picker
+							v-model="getThemeConfig.columnsMenuBar"
+							size="default"
+							@change="onBgColorPickerChange('columnsMenuBar')"
+							:disabled="getThemeConfig.layout !== 'columns'"
+						>
+						</el-color-picker>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex" :style="{ opacity: getThemeConfig.layout !== 'columns' ? 0.5 : 1 }">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoColumnsMenuBarColor') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-color-picker
+							v-model="getThemeConfig.columnsMenuBarColor"
+							size="default"
+							@change="onBgColorPickerChange('columnsMenuBarColor')"
+							:disabled="getThemeConfig.layout !== 'columns'"
+						>
+						</el-color-picker>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt14" :style="{ opacity: getThemeConfig.layout !== 'columns' ? 0.5 : 1 }">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoIsColumnsMenuBarColorGradual') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch
+							v-model="getThemeConfig.isColumnsMenuBarColorGradual"
+							size="small"
+							@change="onColumnsMenuBarGradualChange"
+							:disabled="getThemeConfig.layout !== 'columns'"
+						></el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt14" :style="{ opacity: getThemeConfig.layout !== 'columns' ? 0.5 : 1 }">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoIsColumnsMenuHoverPreload') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch
+							v-model="getThemeConfig.isColumnsMenuHoverPreload"
+							size="small"
+							@change="onColumnsMenuHoverPreloadChange"
+							:disabled="getThemeConfig.layout !== 'columns'"
+						></el-switch>
+					</div>
+				</div>
+
+				<!-- 界面设置 -->
+				<el-divider content-position="left">{{ $t('message.layout.threeTitle') }}</el-divider>
+				<div class="layout-breadcrumb-seting-bar-flex" :style="{ opacity: getThemeConfig.layout === 'transverse' ? 0.5 : 1 }">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.threeIsCollapse') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch
+							v-model="getThemeConfig.isCollapse"
+							:disabled="getThemeConfig.layout === 'transverse'"
+							size="small"
+							@change="onThemeConfigChange"
+						></el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: getThemeConfig.layout === 'transverse' ? 0.5 : 1 }">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.threeIsUniqueOpened') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch
+							v-model="getThemeConfig.isUniqueOpened"
+							:disabled="getThemeConfig.layout === 'transverse'"
+							size="small"
+							@change="setLocalThemeConfig"
+						></el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.threeIsFixedHeader') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch v-model="getThemeConfig.isFixedHeader" size="small" @change="onIsFixedHeaderChange"></el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: getThemeConfig.layout !== 'classic' ? 0.5 : 1 }">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.threeIsClassicSplitMenu') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch
+							v-model="getThemeConfig.isClassicSplitMenu"
+							:disabled="getThemeConfig.layout !== 'classic'"
+							size="small"
+							@change="onClassicSplitMenuChange"
+						>
+						</el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.threeIsLockScreen') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch v-model="getThemeConfig.isLockScreen" size="small" @change="setLocalThemeConfig"></el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt11">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.threeLockScreenTime') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-input-number
+							v-model="getThemeConfig.lockScreenTime"
+							controls-position="right"
+							:min="1"
+							:max="9999"
+							@change="setLocalThemeConfig"
+							size="default"
+							style="width: 90px"
+						>
+						</el-input-number>
+					</div>
+				</div>
+
+				<!-- 界面显示 -->
+				<el-divider content-position="left">{{ $t('message.layout.fourTitle') }}</el-divider>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsShowLogo') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch v-model="getThemeConfig.isShowLogo" size="small" @change="onIsShowLogoChange"></el-switch>
+					</div>
+				</div>
+				<div
+					class="layout-breadcrumb-seting-bar-flex mt15"
+					:style="{ opacity: getThemeConfig.layout === 'classic' || getThemeConfig.layout === 'transverse' ? 0.5 : 1 }"
+				>
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsBreadcrumb') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch
+							v-model="getThemeConfig.isBreadcrumb"
+							:disabled="getThemeConfig.layout === 'classic' || getThemeConfig.layout === 'transverse'"
+							size="small"
+							@change="onIsBreadcrumbChange"
+						></el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsBreadcrumbIcon') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch v-model="getThemeConfig.isBreadcrumbIcon" size="small" @change="setLocalThemeConfig"></el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsTagsview') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch v-model="getThemeConfig.isTagsview" size="small" @change="setLocalThemeConfig"></el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsTagsviewIcon') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch v-model="getThemeConfig.isTagsviewIcon" size="small" @change="setLocalThemeConfig"></el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsCacheTagsView') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch v-model="getThemeConfig.isCacheTagsView" size="small" @change="setLocalThemeConfig"></el-switch>
+					</div>
+				</div>
+				<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="state.isMobile ? true : false"
+							size="small"
+							@change="onSortableTagsViewChange"
+						></el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsShareTagsView') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch v-model="getThemeConfig.isShareTagsView" size="small" @change="onShareTagsViewChange"></el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsFooter') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch v-model="getThemeConfig.isFooter" size="small" @change="setLocalThemeConfig"></el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsGrayscale') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch v-model="getThemeConfig.isGrayscale" size="small" @change="onAddFilterChange('grayscale')"></el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsInvert') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch v-model="getThemeConfig.isInvert" size="small" @change="onAddFilterChange('invert')"></el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsWartermark') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch v-model="getThemeConfig.isWartermark" size="small" @change="onWartermarkChange"></el-switch>
+					</div>
+				</div>
+				<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"></el-input>
+					</div>
+				</div>
+
+				<!-- 其它设置 -->
+				<el-divider content-position="left">{{ $t('message.layout.fiveTitle') }}</el-divider>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fiveTagsStyle') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-select v-model="getThemeConfig.tagsStyle" placeholder="请选择" size="default" style="width: 90px" @change="setLocalThemeConfig">
+							<el-option label="风格1" value="tags-style-one"></el-option>
+							<el-option label="风格4" value="tags-style-four"></el-option>
+							<el-option label="风格5" value="tags-style-five"></el-option>
+						</el-select>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fiveAnimation') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-select v-model="getThemeConfig.animation" placeholder="请选择" size="default" style="width: 90px" @change="setLocalThemeConfig">
+							<el-option label="slide-right" value="slide-right"></el-option>
+							<el-option label="slide-left" value="slide-left"></el-option>
+							<el-option label="opacitys" value="opacitys"></el-option>
+						</el-select>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: getThemeConfig.layout !== 'columns' ? 0.5 : 1 }">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fiveColumnsAsideStyle') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-select
+							v-model="getThemeConfig.columnsAsideStyle"
+							placeholder="请选择"
+							size="default"
+							style="width: 90px"
+							:disabled="getThemeConfig.layout !== 'columns' ? true : false"
+							@change="setLocalThemeConfig"
+						>
+							<el-option label="圆角" value="columns-round"></el-option>
+							<el-option label="卡片" value="columns-card"></el-option>
+						</el-select>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15 mb27" :style="{ opacity: getThemeConfig.layout !== 'columns' ? 0.5 : 1 }">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fiveColumnsAsideLayout') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-select
+							v-model="getThemeConfig.columnsAsideLayout"
+							placeholder="请选择"
+							size="default"
+							style="width: 90px"
+							:disabled="getThemeConfig.layout !== 'columns' ? true : false"
+							@change="setLocalThemeConfig"
+						>
+							<el-option label="水平" value="columns-horizontal"></el-option>
+							<el-option label="垂直" value="columns-vertical"></el-option>
+						</el-select>
+					</div>
+				</div>
+
+				<!-- 布局切换 -->
+				<el-divider content-position="left">{{ $t('message.layout.sixTitle') }}</el-divider>
+				<div class="layout-drawer-content-flex">
+					<!-- defaults 布局 -->
+					<div class="layout-drawer-content-item" @click="onSetLayout('defaults')">
+						<section class="el-container el-circular" :class="{ 'drawer-layout-active': getThemeConfig.layout === 'defaults' }">
+							<aside class="el-aside" style="width: 20px"></aside>
+							<section class="el-container is-vertical">
+								<header class="el-header" style="height: 10px"></header>
+								<main class="el-main"></main>
+							</section>
+						</section>
+						<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': getThemeConfig.layout === 'defaults' }">
+							<div class="layout-tips-box">
+								<p class="layout-tips-txt">{{ $t('message.layout.sixDefaults') }}</p>
+							</div>
+						</div>
+					</div>
+					<!-- classic 布局 -->
+					<div class="layout-drawer-content-item" @click="onSetLayout('classic')">
+						<section class="el-container is-vertical el-circular" :class="{ 'drawer-layout-active': getThemeConfig.layout === 'classic' }">
+							<header class="el-header" style="height: 10px"></header>
+							<section class="el-container">
+								<aside class="el-aside" style="width: 20px"></aside>
+								<section class="el-container is-vertical">
+									<main class="el-main"></main>
+								</section>
+							</section>
+						</section>
+						<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': getThemeConfig.layout === 'classic' }">
+							<div class="layout-tips-box">
+								<p class="layout-tips-txt">{{ $t('message.layout.sixClassic') }}</p>
+							</div>
+						</div>
+					</div>
+					<!-- transverse 布局 -->
+					<div class="layout-drawer-content-item" @click="onSetLayout('transverse')">
+						<section class="el-container is-vertical el-circular" :class="{ 'drawer-layout-active': getThemeConfig.layout === 'transverse' }">
+							<header class="el-header" style="height: 10px"></header>
+							<section class="el-container">
+								<section class="el-container is-vertical">
+									<main class="el-main"></main>
+								</section>
+							</section>
+						</section>
+						<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': getThemeConfig.layout === 'transverse' }">
+							<div class="layout-tips-box">
+								<p class="layout-tips-txt">{{ $t('message.layout.sixTransverse') }}</p>
+							</div>
+						</div>
+					</div>
+					<!-- columns 布局 -->
+					<div class="layout-drawer-content-item" @click="onSetLayout('columns')">
+						<section class="el-container el-circular" :class="{ 'drawer-layout-active': getThemeConfig.layout === 'columns' }">
+							<aside class="el-aside-dark" style="width: 10px"></aside>
+							<aside class="el-aside" style="width: 20px"></aside>
+							<section class="el-container is-vertical">
+								<header class="el-header" style="height: 10px"></header>
+								<main class="el-main"></main>
+							</section>
+						</section>
+						<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': getThemeConfig.layout === 'columns' }">
+							<div class="layout-tips-box">
+								<p class="layout-tips-txt">{{ $t('message.layout.sixColumns') }}</p>
+							</div>
+						</div>
+					</div>
+				</div>
+				<div class="copy-config">
+					<el-alert :title="$t('message.layout.tipText')" type="warning" :closable="false"> </el-alert>
+					<el-button size="default" class="copy-config-btn" type="primary" ref="copyConfigBtnRef" @click="onCopyConfigClick">
+						<el-icon class="mr5">
+							<ele-CopyDocument />
+						</el-icon>
+						{{ $t('message.layout.copyText') }}
+					</el-button>
+					<el-button size="default" class="copy-config-btn-reset" type="info" @click="onResetConfigClick">
+						<el-icon class="mr5">
+							<ele-RefreshRight />
+						</el-icon>
+						{{ $t('message.layout.resetText') }}
+					</el-button>
+				</div>
+			</el-scrollbar>
+		</el-drawer>
+	</div>
+</template>
+
+<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 { useChangeColor } from '/@/utils/theme';
+import { verifyAndSpace } from '/@/utils/toolsValidate';
+import { Local } from '/@/utils/storage';
+import Watermark from '/@/utils/wartermark';
+import commonFunction from '/@/utils/commonFunction';
+import other from '/@/utils/other';
+import mittBus from '/@/utils/mitt';
+
+// 定义变量内容
+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('menuBarActiveColor');
+	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();
+	// @ts-ignore
+	Local.set('version', __VERSION__);
+};
+// 初始化菜单样式等
+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();
+			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);
+	});
+});
+onUnmounted(() => {
+	mittBus.off('layoutMobileResize', () => {});
+});
+
+// 暴露变量
+defineExpose({
+	openDrawer,
+});
+</script>
+
+<style scoped lang="scss">
+.layout-breadcrumb-seting-bar {
+	height: calc(100vh - 50px);
+	padding: 0 15px;
+	:deep(.el-scrollbar__view) {
+		overflow-x: hidden !important;
+	}
+	.layout-breadcrumb-seting-bar-flex {
+		display: flex;
+		align-items: center;
+		margin-bottom: 5px;
+		&-label {
+			flex: 1;
+			color: var(--el-text-color-primary);
+		}
+	}
+	.layout-drawer-content-flex {
+		overflow: hidden;
+		display: flex;
+		flex-wrap: wrap;
+		align-content: flex-start;
+		margin: 0 -5px;
+		.layout-drawer-content-item {
+			width: 50%;
+			height: 70px;
+			cursor: pointer;
+			border: 1px solid transparent;
+			position: relative;
+			padding: 5px;
+			.el-container {
+				height: 100%;
+				.el-aside-dark {
+					background-color: var(--next-color-seting-header);
+				}
+				.el-aside {
+					background-color: var(--next-color-seting-aside);
+				}
+				.el-header {
+					background-color: var(--next-color-seting-header);
+				}
+				.el-main {
+					background-color: var(--next-color-seting-main);
+				}
+			}
+			.el-circular {
+				border-radius: 2px;
+				overflow: hidden;
+				border: 1px solid transparent;
+				transition: all 0.3s ease-in-out;
+			}
+			.drawer-layout-active {
+				border: 1px solid;
+				border-color: var(--el-color-primary);
+			}
+			.layout-tips-warp,
+			.layout-tips-warp-active {
+				transition: all 0.3s ease-in-out;
+				position: absolute;
+				left: 50%;
+				top: 50%;
+				transform: translate(-50%, -50%);
+				border: 1px solid;
+				border-color: var(--el-color-primary-light-5);
+				border-radius: 100%;
+				padding: 4px;
+				.layout-tips-box {
+					transition: inherit;
+					width: 30px;
+					height: 30px;
+					z-index: 9;
+					border: 1px solid;
+					border-color: var(--el-color-primary-light-5);
+					border-radius: 100%;
+					.layout-tips-txt {
+						transition: inherit;
+						position: relative;
+						top: 5px;
+						font-size: 12px;
+						line-height: 1;
+						letter-spacing: 2px;
+						white-space: nowrap;
+						color: var(--el-color-primary-light-5);
+						text-align: center;
+						transform: rotate(30deg);
+						left: -1px;
+						background-color: var(--next-color-seting-main);
+						width: 32px;
+						height: 17px;
+						line-height: 17px;
+					}
+				}
+			}
+			.layout-tips-warp-active {
+				border: 1px solid;
+				border-color: var(--el-color-primary);
+				.layout-tips-box {
+					border: 1px solid;
+					border-color: var(--el-color-primary);
+					.layout-tips-txt {
+						color: var(--el-color-primary) !important;
+						background-color: var(--next-color-seting-main) !important;
+					}
+				}
+			}
+			&:hover {
+				.el-circular {
+					transition: all 0.3s ease-in-out;
+					border: 1px solid;
+					border-color: var(--el-color-primary);
+				}
+				.layout-tips-warp {
+					transition: all 0.3s ease-in-out;
+					border-color: var(--el-color-primary);
+					.layout-tips-box {
+						transition: inherit;
+						border-color: var(--el-color-primary);
+						.layout-tips-txt {
+							transition: inherit;
+							color: var(--el-color-primary) !important;
+							background-color: var(--next-color-seting-main) !important;
+						}
+					}
+				}
+			}
+		}
+	}
+	.copy-config {
+		margin: 10px 0;
+		.copy-config-btn {
+			width: 100%;
+			margin-top: 15px;
+		}
+		.copy-config-btn-reset {
+			width: 100%;
+			margin: 10px 0 0;
+		}
+	}
+}
+</style>

+ 272 - 0
h5/src/layout/navBars/breadcrumb/user.vue

@@ -0,0 +1,272 @@
+<template>
+	<div class="layout-navbars-breadcrumb-user pr15" :style="{ flex: layoutUserFlexNum }">
+		<el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onComponentSizeChange">
+			<div class="layout-navbars-breadcrumb-user-icon">
+				<i class="iconfont icon-ziti" :title="$t('message.user.title0')"></i>
+			</div>
+			<template #dropdown>
+				<el-dropdown-menu>
+					<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="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="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>
+		<div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
+			<el-icon :title="$t('message.user.title2')">
+				<ele-Search />
+			</el-icon>
+		</div>
+		<div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick">
+			<i class="icon-skin iconfont" :title="$t('message.user.title3')"></i>
+		</div>
+		<!-- 通知中心 -->
+		<div class="layout-navbars-breadcrumb-user-icon">
+			<el-popover placement="bottom" trigger="click" transition="el-zoom-in-top" :width="300" :persistent="false">
+				<template #reference>
+					<el-badge :is-dot="state.isDot">
+						<el-icon :title="$t('message.user.title4')">
+							<ele-Bell />
+						</el-icon>
+					</el-badge>
+				</template>
+				<template #default>
+					<!--  ref="isDotRef" @refresh="getList()" -->
+					<UserNews />
+				</template>
+			</el-popover>
+		</div>
+		<div class="layout-navbars-breadcrumb-user-icon mr10" @click="onScreenfullClick">
+			<i
+				class="iconfont"
+				:title="state.isScreenfull ? $t('message.user.title6') : $t('message.user.title5')"
+				:class="!state.isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'"
+			></i>
+		</div>
+		<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onHandleCommandClick">
+			<span class="layout-navbars-breadcrumb-user-link">
+				<img :src="userInfos.photo" class="layout-navbars-breadcrumb-user-link-photo mr5" />
+				{{ userInfos.userName === '' ? 'common' : userInfos.userName }}
+				<el-icon class="el-icon--right">
+					<ele-ArrowDown />
+				</el-icon>
+			</span>
+			<template #dropdown>
+				<el-dropdown-menu>
+					<el-dropdown-item command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item>
+					<!-- <el-dropdown-item command="wareHouse">{{ $t('message.user.dropdown6') }}</el-dropdown-item> -->
+					<!-- <el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item> -->
+					<!-- <el-dropdown-item command="/404">{{ $t('message.user.dropdown3') }}</el-dropdown-item> -->
+					<!-- <el-dropdown-item command="/401">{{ $t('message.user.dropdown4') }}</el-dropdown-item> -->
+					<el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
+				</el-dropdown-menu>
+			</template>
+		</el-dropdown>
+		<Search ref="searchRef" />
+	</div>
+</template>
+
+<script setup lang="ts" name="layoutBreadcrumbUser">
+import { defineAsyncComponent, ref, computed, reactive, onMounted, inject } from 'vue';
+import { useRouter } from 'vue-router';
+import { ElMessageBox, ElMessage } from 'element-plus';
+import screenfull from 'screenfull';
+import { useI18n } from 'vue-i18n';
+import { storeToRefs } from 'pinia';
+import { useUserInfo } from '/@/stores/userInfo';
+import { useThemeConfig } from '/@/stores/themeConfig';
+import other from '/@/utils/other';
+import mittBus from '/@/utils/mitt';
+import { Session, Local } from '/@/utils/storage';
+
+const isDot = (e:any) => {
+	console.log('isDot=>',e);
+	state.isDot = e;
+}
+
+// 暴露变量
+defineExpose({
+	isDot,
+});
+
+// 引入组件
+// const isDotRef = ref();
+const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/userNews.vue'));
+const Search = defineAsyncComponent(() => import('/@/layout/navBars/breadcrumb/search.vue'));
+
+// 定义变量内容
+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 state = reactive({
+	isScreenfull: false,
+	disabledI18n: 'zh-cn',
+	disabledSize: 'large',
+	isDot: false,
+});
+
+// 设置分割样式
+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'),
+			showCancelButton: true,
+			confirmButtonText: t('message.user.logOutConfirm'),
+			cancelButtonText: t('message.user.logOutCancel'),
+			buttonSize: 'default',
+			beforeClose: (action, instance, done) => {
+				if (action === 'confirm') {
+					instance.confirmButtonLoading = true;
+					instance.confirmButtonText = t('message.user.logOutExit');
+					setTimeout(() => {
+						done();
+						setTimeout(() => {
+							instance.confirmButtonLoading = false;
+						}, 300);
+					}, 700);
+				} else {
+					done();
+				}
+			},
+		})
+			.then(async () => {
+				// 清除缓存/token等
+				Session.clear();
+				// 使用 reload 时,不需要调用 resetRoute() 重置路由
+				window.location.reload();
+			})
+			.catch(() => {});
+	} else if (path === 'wareHouse') {
+		window.open('https://gitee.com/lyt-top/vue-next-admin');
+	} else {
+		router.push(path);
+	}
+};
+// 菜单搜索点击
+const onSearchClick = () => {
+	searchRef.value.openSearch();
+};
+// 组件大小改变
+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) => {
+	state[attr] = Local.get('themeConfig')[value];
+};
+// 页面加载时
+onMounted(() => {
+	if (Local.get('themeConfig')) {
+		initI18nOrSize('globalComponentSize', 'disabledSize');
+		initI18nOrSize('globalI18n', 'disabledI18n');
+	}
+});
+</script>
+
+<style scoped lang="scss">
+.layout-navbars-breadcrumb-user {
+	display: flex;
+	align-items: center;
+	justify-content: flex-end;
+	&-link {
+		height: 100%;
+		display: flex;
+		align-items: center;
+		white-space: nowrap;
+		&-photo {
+			width: 25px;
+			height: 25px;
+			border-radius: 100%;
+		}
+	}
+	&-icon {
+		padding: 0 10px;
+		cursor: pointer;
+		color: var(--next-bg-topBarColor);
+		height: 50px;
+		line-height: 50px;
+		display: flex;
+		align-items: center;
+		&:hover {
+			background: var(--next-color-user-hover);
+			i {
+				display: inline-block;
+				animation: logoAnimation 0.3s ease-in-out;
+			}
+		}
+	}
+	:deep(.el-dropdown) {
+		color: var(--next-bg-topBarColor);
+	}
+	:deep(.el-badge) {
+		height: 40px;
+		line-height: 40px;
+		display: flex;
+		align-items: center;
+	}
+	:deep(.el-badge__content.is-fixed) {
+		top: 12px;
+	}
+}
+</style>

+ 156 - 0
h5/src/layout/navBars/breadcrumb/userNews.vue

@@ -0,0 +1,156 @@
+<template>
+	<div class="layout-navbars-breadcrumb-user-news">
+		<div class="head-box">
+			<div class="head-box-title">{{ $t('message.user.newTitle') }}</div>
+			<!-- <div class="head-box-btn" v-if="state.newsList.length > 0" @click="onAllReadClick">{{ $t('message.user.newBtn') }}</div> -->
+		</div>
+		<div class="content-box">
+			<template v-if="state.newsList.length > 0">
+				<div class="content-box-item" v-for="(v, k) in state.newsList" :key="k">
+					<div>{{ v.label }}</div>
+					<div class="content-box-msg">
+						{{ v.value }}
+					</div>
+					<div class="content-box-time">{{ v.time }}</div>
+				</div>
+			</template>
+			<el-empty :description="$t('message.user.newDesc')" v-else></el-empty>
+		</div>
+		<div class="foot-box" @click="onGoToGiteeClick" v-if="state.newsList.length > 0">{{ $t('message.user.newGo') }}</div>
+	</div>
+</template>
+
+<script setup lang="ts" name="layoutBreadcrumbUserNews">
+import { onMounted, provide, reactive } from 'vue';
+import { ElTable, ElMessage, TableColumnCtx, ElMessageBox } from 'element-plus';
+import router from '/@/router';
+import Index from '/@/api/index.ts';
+
+// 定义变量内容
+const state = reactive({
+	newsList: [
+		// {
+		// 	label: '关于版本发布的通知',
+		// 	value: 'vue-next-admin,基于 vue3 + CompositionAPI + typescript + vite + element plus,正式发布时间:2021年02月28日!',
+		// 	time: '2020-12-08',
+		// },
+		// {
+		// 	label: '关于学习交流的通知',
+		// 	value: 'QQ群号码 665452019,欢迎小伙伴入群学习交流探讨!',
+		// 	time: '2020-12-08',
+		// },
+	] as any,
+	param: {
+		keyword: '',
+		page: 1,
+		list_rows: 10,
+	},
+});
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['refresh']);
+
+const getList = async() => {
+	let res = await Index.message(state.param);
+	if(res.code != 0){
+		return ElMessage.error(res.msg);
+	}
+	res.data.data.forEach((item:any) => {
+		if(item.is_read==0){
+			state.newsList.push({
+				label: item.title,
+				value: item.content,
+				time: item.update_time,
+				is_read: item.is_read,
+			});
+		}
+	});
+	// if(state.newsList.length<=0){
+	// 	console.log('state.newsList.length',state.newsList.length);
+	// 	emit('refresh');
+	// }else{
+	// 	console.log('state.newsList.length',state.newsList.length);
+	// 	emit('refresh');
+	// }
+}
+
+onMounted(()=>{
+	getList();
+})
+
+// 全部已读点击
+const onAllReadClick = () => {
+	state.newsList = [];
+};
+// 前往通知中心点击
+const onGoToGiteeClick = () => {
+	let r = router.currentRoute;
+	if(r.value.name=='systemMessage'){
+		return ElMessage.warning('您已经在通知中心了');
+	}
+	router.replace({name: 'systemMessage'});
+	// window.open('https://gitee.com/lyt-top/vue-next-admin');
+};
+
+// 暴露变量
+defineExpose({
+	getList,
+});
+</script>
+
+<style scoped lang="scss">
+.layout-navbars-breadcrumb-user-news {
+	.head-box {
+		display: flex;
+		border-bottom: 1px solid var(--el-border-color-lighter);
+		box-sizing: border-box;
+		color: var(--el-text-color-primary);
+		justify-content: space-between;
+		height: 35px;
+		align-items: center;
+		.head-box-btn {
+			color: var(--el-color-primary);
+			font-size: 13px;
+			cursor: pointer;
+			opacity: 0.8;
+			&:hover {
+				opacity: 1;
+			}
+		}
+	}
+	.content-box {
+		font-size: 13px;
+		.content-box-item {
+			padding-top: 12px;
+			&:last-of-type {
+				padding-bottom: 12px;
+			}
+			.content-box-msg {
+				color: var(--el-text-color-secondary);
+				margin-top: 5px;
+				margin-bottom: 5px;
+			}
+			.content-box-time {
+				color: var(--el-text-color-secondary);
+			}
+		}
+	}
+	.foot-box {
+		height: 35px;
+		color: var(--el-color-primary);
+		font-size: 13px;
+		cursor: pointer;
+		opacity: 0.8;
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		border-top: 1px solid var(--el-border-color-lighter);
+		&:hover {
+			opacity: 1;
+		}
+	}
+	:deep(.el-empty__description p) {
+		font-size: 13px;
+	}
+}
+</style>

+ 35 - 0
h5/src/layout/navBars/index.vue

@@ -0,0 +1,35 @@
+<template>
+	<div class="layout-navbars-container">
+		<BreadcrumbIndex />
+		<TagsView v-if="setShowTagsView" />
+	</div>
+</template>
+
+<script setup lang="ts" name="layoutNavBars">
+import { defineAsyncComponent, computed } from 'vue';
+import { storeToRefs } from 'pinia';
+import { useThemeConfig } from '/@/stores/themeConfig';
+
+// 引入组件
+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>
+
+<style scoped lang="scss">
+.layout-navbars-container {
+	display: flex;
+	flex-direction: column;
+	width: 100%;
+	height: 100%;
+}
+</style>

+ 138 - 0
h5/src/layout/navBars/tagsView/contextmenu.vue

@@ -0,0 +1,138 @@
+<template>
+	<transition name="el-zoom-in-center">
+		<div
+			aria-hidden="true"
+			class="el-dropdown__popper el-popper is-light is-pure custom-contextmenu"
+			role="tooltip"
+			data-popper-placement="bottom"
+			:style="`top: ${dropdowns.y + 5}px;left: ${dropdowns.x}px;`"
+			:key="Math.random()"
+			v-show="state.isShow"
+		>
+			<ul class="el-dropdown-menu">
+				<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: `${state.arrowLeft}px` }"></div>
+		</div>
+	</transition>
+</template>
+
+<script setup lang="ts" name="layoutTagsViewContextmenu">
+import { computed, reactive, onMounted, onUnmounted, watch } from 'vue';
+
+// 定义父组件传过来的值
+const props = defineProps({
+	dropdown: {
+		type: Object,
+		default: () => {
+			return {
+				x: 0,
+				y: 0,
+			};
+		},
+	},
+});
+
+// 定义子组件向父组件传值/事件
+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 {
+			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>
+
+<style scoped lang="scss">
+.custom-contextmenu {
+	transform-origin: center top;
+	z-index: 2190;
+	position: fixed;
+	.el-dropdown-menu__item {
+		font-size: 12px !important;
+		white-space: nowrap;
+		i {
+			font-size: 12px !important;
+		}
+	}
+}
+</style>

File diff suppressed because it is too large
+ 726 - 0
h5/src/layout/navBars/tagsView/tagsView.vue


+ 159 - 0
h5/src/layout/navMenu/horizontal.vue

@@ -0,0 +1,159 @@
+<template>
+	<div class="el-menu-horizontal-warp">
+		<el-scrollbar @wheel.native.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef">
+			<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>
+							<SvgIcon :name="val.meta.icon" />
+							<span>{{ $t(val.meta.title) }}</span>
+						</template>
+						<SubItem :chil="val.children" />
+					</el-sub-menu>
+					<template v-else>
+						<el-menu-item :index="val.path" :key="val.path">
+							<template #title v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
+								<SvgIcon :name="val.meta.icon" />
+								{{ $t(val.meta.title) }}
+							</template>
+							<template #title v-else>
+								<a class="w100" @click.prevent="onALinkClick(val)">
+									<SvgIcon :name="val.meta.icon" />
+									{{ $t(val.meta.title) }}
+								</a>
+							</template>
+						</el-menu-item>
+					</template>
+				</template>
+			</el-menu>
+		</el-scrollbar>
+	</div>
+</template>
+
+<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 other from '/@/utils/other';
+import mittBus from '/@/utils/mitt';
+
+// 引入组件
+const SubItem = defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue'));
+
+// 定义父组件传过来的值
+const props = defineProps({
+	// 菜单列表
+	menuList: {
+		type: Array<RouteRecordRaw>,
+		default: () => [],
+	},
+});
+
+// 定义变量内容
+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;
+		});
+};
+// 传送当前子级数据到菜单中
+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>
+
+<style scoped lang="scss">
+.el-menu-horizontal-warp {
+	flex: 1;
+	overflow: hidden;
+	margin-right: 30px;
+	:deep(.el-scrollbar__bar.is-vertical) {
+		display: none;
+	}
+	:deep(a) {
+		width: 100%;
+	}
+	.el-menu.el-menu--horizontal {
+		display: flex;
+		height: 100%;
+		width: 100%;
+		box-sizing: border-box;
+	}
+}
+</style>

+ 49 - 0
h5/src/layout/navMenu/subItem.vue

@@ -0,0 +1,49 @@
+<template>
+	<template v-for="val in chils">
+		<el-sub-menu :index="val.path" :key="val.path" v-if="val.children && val.children.length > 0">
+			<template #title>
+				<SvgIcon :name="val.meta.icon" />
+				<span>{{ $t(val.meta.title) }}</span>
+			</template>
+			<sub-item :chil="val.children" />
+		</el-sub-menu>
+		<template v-else>
+			<el-menu-item :index="val.path" :key="val.path">
+				<template v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
+					<SvgIcon :name="val.meta.icon" />
+					<span>{{ $t(val.meta.title) }}</span>
+				</template>
+				<template v-else>
+					<a class="w100" @click.prevent="onALinkClick(val)">
+						<SvgIcon :name="val.meta.icon" />
+						{{ $t(val.meta.title) }}
+					</a>
+				</template>
+			</el-menu-item>
+		</template>
+	</template>
+</template>
+
+<script setup lang="ts" name="navMenuSubItem">
+import { computed } from 'vue';
+import { RouteRecordRaw } from 'vue-router';
+import other from '/@/utils/other';
+
+// 定义父组件传过来的值
+const props = defineProps({
+	// 菜单列表
+	chil: {
+		type: Array<RouteRecordRaw>,
+		default: () => [],
+	},
+});
+
+// 获取父级菜单数据
+const chils = computed(() => {
+	return <RouteItems>props.chil;
+});
+// 打开外部链接
+const onALinkClick = (val: RouteItem) => {
+	other.handleOpenLink(val);
+};
+</script>

+ 102 - 0
h5/src/layout/navMenu/vertical.vue

@@ -0,0 +1,102 @@
+<template>
+	<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>
+					<SvgIcon :name="val.meta.icon" />
+					<span>{{ $t(val.meta.title) }}</span>
+				</template>
+				<SubItem :chil="val.children" />
+			</el-sub-menu>
+			<template v-else>
+				<el-menu-item :index="val.path" :key="val.path">
+					<SvgIcon :name="val.meta.icon" />
+					<template #title v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
+						<span>{{ $t(val.meta.title) }}</span>
+					</template>
+					<template #title v-else>
+						<a class="w100" @click.prevent="onALinkClick(val)">{{ $t(val.meta.title) }}</a>
+					</template>
+				</el-menu-item>
+			</template>
+		</template>
+	</el-menu>
+</template>
+
+<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 other from '/@/utils/other';
+
+// 引入组件
+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>

+ 101 - 0
h5/src/layout/routerView/iframes.vue

@@ -0,0 +1,101 @@
+<template>
+	<div class="layout-padding layout-padding-unset layout-iframe">
+		<div class="layout-padding-auto layout-padding-view">
+			<div class="w100" v-for="v in setIframeList" :key="v.path" v-loading="v.meta.loading" element-loading-background="white">
+				<transition-group :name="name" mode="out-in">
+					<iframe
+						:src="v.meta.isLink"
+						:key="v.path"
+						frameborder="0"
+						height="100%"
+						width="100%"
+						style="position: absolute"
+						:data-url="v.path"
+						v-show="getRoutePath === v.path"
+						ref="iframeRef"
+					/>
+				</transition-group>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts" name="layoutIframeView">
+import { computed, watch, ref, nextTick } from 'vue';
+import { useRoute } from 'vue-router';
+
+// 定义父组件传过来的值
+const props = defineProps({
+	// 刷新 iframe
+	refreshKey: {
+		type: String,
+		default: () => '',
+	},
+	// 过渡动画 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 数据,防止多个 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,
+	}
+);
+</script>

+ 93 - 0
h5/src/layout/routerView/link.vue

@@ -0,0 +1,93 @@
+<template>
+	<div class="layout-padding layout-link-container">
+		<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(state.title) }}" 已在新窗口中打开</div>
+				<el-button class="mt30" round size="default" @click="onGotoFullPage">
+					<i class="iconfont icon-lianjie"></i>
+					<span>立即前往体验</span>
+				</el-button>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script setup lang="ts" name="layoutLinkView">
+import { reactive, watch } from 'vue';
+import { useRoute } from 'vue-router';
+import { verifyUrl } from '/@/utils/toolsValidate';
+
+// 定义变量内容
+const route = useRoute();
+const state = reactive<LinkViewState>({
+	title: '',
+	isLink: '',
+});
+
+// 立即前往
+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">
+.layout-link-container {
+	.layout-link-warp {
+		margin: auto;
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+		i.layout-link-icon {
+			position: relative;
+			font-size: 100px;
+			color: var(--el-color-primary);
+			&::after {
+				content: '';
+				position: absolute;
+				left: 50px;
+				top: 0;
+				width: 15px;
+				height: 100px;
+				background: linear-gradient(
+					rgba(255, 255, 255, 0.01),
+					rgba(255, 255, 255, 0.01),
+					rgba(255, 255, 255, 0.01),
+					rgba(255, 255, 255, 0.05),
+					rgba(255, 255, 255, 0.05),
+					rgba(255, 255, 255, 0.05),
+					rgba(235, 255, 255, 0.5),
+					rgba(255, 255, 255, 0.05),
+					rgba(255, 255, 255, 0.05),
+					rgba(255, 255, 255, 0.05),
+					rgba(255, 255, 255, 0.01),
+					rgba(255, 255, 255, 0.01),
+					rgba(255, 255, 255, 0.01)
+				);
+				transform: rotate(-15deg);
+				animation: toRight 5s linear infinite;
+			}
+		}
+		.layout-link-msg {
+			font-size: 12px;
+			color: var(--next-bg-topBarColor);
+			opacity: 0.7;
+			margin-top: 15px;
+		}
+	}
+}
+</style>

+ 108 - 0
h5/src/layout/routerView/parent.vue

@@ -0,0 +1,108 @@
+<template>
+	<div class="layout-parent">
+		<router-view v-slot="{ Component }">
+			<transition :name="setTransitionName" mode="out-in">
+				<keep-alive :include="getKeepAliveNames">
+					<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="state.iframeRefreshKey" :name="setTransitionName" :list="state.iframeList" />
+		</transition>
+	</div>
+</template>
+
+<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';
+import { useThemeConfig } from '/@/stores/themeConfig';
+import { Session } from '/@/utils/storage';
+import mittBus from '/@/utils/mitt';
+
+// 引入组件
+const Iframes = defineAsyncComponent(() => import('/@/layout/routerView/iframes.vue'));
+
+// 定义变量内容
+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;
+		});
+	});
+});
+// 页面加载时
+onMounted(() => {
+	getIframeListRoutes();
+	// https://gitee.com/lyt-top/vue-next-admin/issues/I58U75
+	// https://gitee.com/lyt-top/vue-next-admin/issues/I59RXK
+	// https://gitee.com/lyt-top/vue-next-admin/pulls/40
+	nextTick(() => {
+		setTimeout(() => {
+			if (themeConfig.value.isCacheTagsView) {
+				let tagsViewArr: RouteItem[] = Session.get('tagsViewList') || [];
+				cachedViews.value = tagsViewArr.filter((item) => item.meta?.isKeepAlive).map((item) => item.name as string);
+			}
+		}, 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>

+ 151 - 0
h5/src/layout/upgrade/index.vue

@@ -0,0 +1,151 @@
+<template>
+	<div class="upgrade-dialog">
+		<el-dialog
+			v-model="state.isUpgrade"
+			width="300px"
+			destroy-on-close
+			:show-close="false"
+			:close-on-click-modal="false"
+			:close-on-press-escape="false"
+		>
+			<div class="upgrade-title">
+				<div class="upgrade-title-warp">
+					<span class="upgrade-title-warp-txt">{{ $t('message.upgrade.title') }}</span>
+					<span class="upgrade-title-warp-version">v{{ state.version }}</span>
+				</div>
+			</div>
+			<div class="upgrade-content">
+				{{ getThemeConfig.globalTitle }} {{ $t('message.upgrade.msg') }}
+				<div class="mt5">
+					<el-link type="primary" class="font12" href="https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md" target="_black">
+						CHANGELOG.md
+					</el-link>
+				</div>
+				<div class="upgrade-content-desc mt5">{{ $t('message.upgrade.desc') }}</div>
+			</div>
+			<div class="upgrade-btn">
+				<el-button round size="default" type="info" text @click="onCancel">{{ $t('message.upgrade.btnOne') }}</el-button>
+				<el-button type="primary" round size="default" @click="onUpgrade" :loading="state.isLoading">{{ state.btnTxt }}</el-button>
+			</div>
+		</el-dialog>
+	</div>
+</template>
+
+<script setup lang="ts" name="layoutUpgrade">
+import { reactive, computed, onMounted } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { storeToRefs } from 'pinia';
+import { useThemeConfig } from '/@/stores/themeConfig';
+import { Local } from '/@/utils/storage';
+
+// 定义变量内容
+const { t } = useI18n();
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+const state = reactive({
+	isUpgrade: false,
+	// @ts-ignore
+	version: __VERSION__,
+	isLoading: false,
+	btnTxt: '',
+});
+
+// 获取布局配置信息
+const getThemeConfig = computed(() => {
+	return themeConfig.value;
+});
+// 残忍拒绝
+const onCancel = () => {
+	state.isUpgrade = false;
+};
+// 马上更新
+const onUpgrade = () => {
+	state.isLoading = true;
+	state.btnTxt = t('message.upgrade.btnTwoLoading');
+	setTimeout(() => {
+		Local.clear();
+		window.location.reload();
+		Local.set('version', state.version);
+	}, 2000);
+};
+// 延迟显示,防止刷新时界面显示太快
+const delayShow = () => {
+	setTimeout(() => {
+		state.isUpgrade = true;
+	}, 2000);
+};
+// 页面加载时
+onMounted(() => {
+	delayShow();
+	setTimeout(() => {
+		state.btnTxt = t('message.upgrade.btnTwo');
+	}, 200);
+});
+</script>
+
+<style scoped lang="scss">
+.upgrade-dialog {
+	:deep(.el-dialog) {
+		.el-dialog__body {
+			padding: 0 !important;
+		}
+		.el-dialog__header {
+			display: none !important;
+		}
+		.upgrade-title {
+			text-align: center;
+			height: 130px;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			position: relative;
+			&::after {
+				content: '';
+				position: absolute;
+				background-color: var(--el-color-primary-light-1);
+				width: 130%;
+				height: 130px;
+				border-bottom-left-radius: 100%;
+				border-bottom-right-radius: 100%;
+			}
+			.upgrade-title-warp {
+				z-index: 1;
+				position: relative;
+				.upgrade-title-warp-txt {
+					color: var(--next-color-white);
+					font-size: 22px;
+					letter-spacing: 3px;
+				}
+				.upgrade-title-warp-version {
+					color: var(--next-color-white);
+					background-color: var(--el-color-primary-light-4);
+					font-size: 12px;
+					position: absolute;
+					display: flex;
+					top: -2px;
+					right: -50px;
+					padding: 2px 4px;
+					border-radius: 2px;
+				}
+			}
+		}
+		.upgrade-content {
+			padding: 20px;
+			line-height: 22px;
+			.upgrade-content-desc {
+				color: var(--el-color-info-light-5);
+				font-size: 12px;
+			}
+		}
+		.upgrade-btn {
+			border-top: 1px solid var(--el-border-color-lighter, #ebeef5);
+			display: flex;
+			justify-content: space-around;
+			padding: 15px 20px;
+			.el-button {
+				width: 100%;
+			}
+		}
+	}
+}
+</style>

+ 19 - 0
h5/src/main.ts

@@ -0,0 +1,19 @@
+import { createApp } from 'vue';
+import pinia from '/@/stores/index';
+import App from './App.vue';
+import router from './router';
+import { directive } from '/@/directive/index';
+import { i18n } from '/@/i18n/index';
+import other from '/@/utils/other';
+
+import ElementPlus from 'element-plus';
+import 'element-plus/dist/index.css';
+import '/@/theme/index.scss';
+import VueGridLayout from 'vue-grid-layout';
+
+const app = createApp(App);
+
+directive(app);
+other.elSvg(app);
+
+app.use(pinia).use(router).use(ElementPlus, { i18n: i18n.global.t }).use(i18n).use(VueGridLayout).mount('#app');

+ 175 - 0
h5/src/router/backEnd.ts

@@ -0,0 +1,175 @@
+import { RouteRecordRaw } from 'vue-router';
+import { storeToRefs } from 'pinia';
+import pinia from '/@/stores/index';
+import { useUserInfo } from '/@/stores/userInfo';
+import { useRequestOldRoutes } from '/@/stores/requestOldRoutes';
+import { Session } from '/@/utils/storage';
+import { NextLoading } from '/@/utils/loading';
+import { dynamicRoutes, notFoundAndNoPower } from '/@/router/route';
+import { formatTwoStageRoutes, formatFlatteningRoutes, router } from '/@/router/index';
+import { useRoutesList } from '/@/stores/routesList';
+import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
+import { useMenuApi } from '/@/api/menu/index';
+
+// 后端控制路由
+
+// 引入 api 请求接口
+const menuApi = useMenuApi();
+
+/**
+ * 获取目录下的 .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 });
+
+/**
+ * 后端控制路由:初始化方法,防止刷新时路由丢失
+ * @method NextLoading 界面 loading 动画开始执行
+ * @method useUserInfo().setUserInfos() 触发初始化用户信息 pinia
+ * @method useRequestOldRoutes().setRequestOldRoutes() 存储接口原始路由(未处理component),根据需求选择使用
+ * @method setAddRoute 添加动态路由
+ * @method setFilterMenuAndCacheTagsViewRoutes 设置路由到 pinia routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+ */
+export async function initBackEndControlRoutes() {
+	// 界面 loading 动画开始执行
+	if (window.nextLoading === undefined) NextLoading.start();
+	// 无 token 停止执行下一步
+	if (!Session.get('token')) return false;
+	// 触发初始化用户信息 pinia
+	// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
+	await useUserInfo().setUserInfos();
+	// 获取路由菜单数据
+	const res = await getBackEndControlRoutes();
+	// 无登录权限时,添加判断
+	// https://gitee.com/lyt-top/vue-next-admin/issues/I64HVO
+	if (res.data.length <= 0) return Promise.resolve(true);
+	// 存储接口原始路由(未处理component),根据需求选择使用
+	useRequestOldRoutes().setRequestOldRoutes(JSON.parse(JSON.stringify(res.data)));
+	// 处理路由(component),替换 dynamicRoutes(/@/router/route)第一个顶级 children 的路由
+	dynamicRoutes[0].children = await backEndComponent(res.data);
+	// 添加动态路由
+	await setAddRoute();
+	// 设置路由到 pinia routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+	await setFilterMenuAndCacheTagsViewRoutes();
+}
+
+/**
+ * 设置路由到 pinia routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+ * @description 用于左侧菜单、横向菜单的显示
+ * @description 用于 tagsView、菜单搜索中:未过滤隐藏的(isHide)
+ */
+export async function setFilterMenuAndCacheTagsViewRoutes() {
+	const storesRoutesList = useRoutesList(pinia);
+	storesRoutesList.setRoutesList(dynamicRoutes[0].children as any);
+	setCacheTagsViewRoutes();
+}
+
+/**
+ * 缓存多级嵌套数组处理后的一维数组
+ * @description 用于 tagsView、菜单搜索中:未过滤隐藏的(isHide)
+ */
+export function setCacheTagsViewRoutes() {
+	const storesTagsView = useTagsViewRoutes(pinia);
+	/*
+		是一个用于设置标签视图路由的函数,它的参数是一个动态路由的格式化后的二级路由的第一个子路由。
+		具体来说,该函数的参数是:
+			首先,将动态路由格式化为扁平化路由:formatFlatteningRoutes(dynamicRoutes);
+			然后,将扁平化路由格式化为二级路由:formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes));
+			最后,取出二级路由的第一个子路由:formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes))[0].children。
+			因此,storesTagsView.setTagsViewRoutes(formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes))[0].children) 的作用是将动态路由格式化为二级路由,
+			并将其第一个子路由设置为标签视图路由。
+	*/
+	storesTagsView.setTagsViewRoutes(formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes))[0].children);
+}
+
+/**
+ * 处理路由格式及添加捕获所有路由或 404 Not found 路由
+ * @description 替换 dynamicRoutes(/@/router/route)第一个顶级 children 的路由
+ * @returns 返回替换后的路由数组
+ */
+export function setFilterRouteEnd() {
+	let filterRouteEnd: any = formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes));
+	// notFoundAndNoPower 防止 404、401 不在 layout 布局中,不设置的话,404、401 界面将全屏显示
+	// 关联问题 No match found for location with path 'xxx'
+	filterRouteEnd[0].children = [...filterRouteEnd[0].children, ...notFoundAndNoPower];
+	return filterRouteEnd;
+}
+
+/**
+ * 添加动态路由
+ * @method router.addRoute
+ * @description 此处循环为 dynamicRoutes(/@/router/route)第一个顶级 children 的路由一维数组,非多级嵌套
+ * @link 参考:https://next.router.vuejs.org/zh/api/#addroute
+ */
+export async function setAddRoute() {
+	await setFilterRouteEnd().forEach((route: RouteRecordRaw) => {
+		router.addRoute(route);
+	});
+}
+
+/**
+ * 请求后端路由菜单接口
+ * @description isRequestRoutes 为 true,则开启后端控制路由
+ * @returns 返回后端路由菜单数据
+ */
+export function getBackEndControlRoutes() {
+	// 模拟 admin 与 test
+	const stores = useUserInfo(pinia);
+	const { userInfos } = storeToRefs(stores);
+	const auth = userInfos.value.roles[0];
+	// 管理员 admin (默认是getAdminMenu)
+	return menuApi.getMenuAdmin();
+	// if (auth === 'admin') return menuApi.getAdminMenu();
+	// if (auth === 'admin') return menuApi.getMenuAdmin();
+	// 其它用户 test
+	// else return menuApi.getTestMenu();
+}
+
+/**
+ * 重新请求后端路由菜单接口
+ * @description 用于菜单管理界面刷新菜单(未进行测试)
+ * @description 路径:/src/views/system/menu/component/addMenu.vue
+ */
+export async function setBackEndControlRefreshRoutes() {
+	await getBackEndControlRoutes();
+}
+
+/**
+ * 后端路由 component 转换
+ * @param routes 后端返回的路由表数组
+ * @returns 返回处理成函数后的 component
+ */
+export function backEndComponent(routes: any) {
+	if (!routes) return;
+	return routes.map((item: any) => {
+		if (item.component) item.component = dynamicImport(dynamicViewsModules, item.component as string);
+		item.children && backEndComponent(item.children);
+		return item;
+	});
+}
+
+/**
+ * 后端路由 component 转换函数
+ * @param dynamicViewsModules 获取目录下的 .vue、.tsx 全部文件
+ * @param component 当前要处理项 component
+ * @returns 返回处理成函数后的 component
+ */
+export function dynamicImport(dynamicViewsModules: Record<string, Function>, component: string) {
+	const keys = Object.keys(dynamicViewsModules);
+	const matchKeys = keys.filter((key) => {
+		//这是一个 JavaScript 语句,用于替换字符串 key 中的 ../views 或 ../ 为空字符串,并将结果赋值给变量 k。
+		const k = key.replace(/..\/views|../, '');
+		//这是一个 JavaScript 语句,它的作用是检查 k 是否以 component 或 /component 开头,如果是,则返回 true,否则返回 false。
+		return k.startsWith(`${component}`) || k.startsWith(`/${component}`);
+	});
+	if (matchKeys?.length === 1) {
+		const matchKey = matchKeys[0];
+		return dynamicViewsModules[matchKey];
+	}
+	if (matchKeys?.length > 1) {
+		return false;
+	}
+}

+ 153 - 0
h5/src/router/frontEnd.ts

@@ -0,0 +1,153 @@
+import { RouteRecordRaw } from 'vue-router';
+import { storeToRefs } from 'pinia';
+import { formatTwoStageRoutes, formatFlatteningRoutes, router } from '/@/router/index';
+import { dynamicRoutes, notFoundAndNoPower } from '/@/router/route';
+import pinia from '/@/stores/index';
+import { Session } from '/@/utils/storage';
+import { useUserInfo } from '/@/stores/userInfo';
+import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
+import { useRoutesList } from '/@/stores/routesList';
+import { NextLoading } from '/@/utils/loading';
+
+// 前端控制路由
+
+/**
+ * 前端控制路由:初始化方法,防止刷新时路由丢失
+ * @method  NextLoading 界面 loading 动画开始执行
+ * @method useUserInfo(pinia).setUserInfos() 触发初始化用户信息 pinia
+ * @method setAddRoute 添加动态路由
+ * @method setFilterMenuAndCacheTagsViewRoutes 设置递归过滤有权限的路由到 pinia routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+ */
+export async function initFrontEndControlRoutes() {
+	// 界面 loading 动画开始执行
+	if (window.nextLoading === undefined) NextLoading.start();
+	// 无 token 停止执行下一步
+	if (!Session.get('token')) return false;
+	// 触发初始化用户信息 pinia
+	// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
+	await useUserInfo(pinia).setUserInfos();
+	// 无登录权限时,添加判断
+	// https://gitee.com/lyt-top/vue-next-admin/issues/I64HVO
+	if (useUserInfo().userInfos.roles.length <= 0) return Promise.resolve(true);
+	// 添加动态路由
+	await setAddRoute();
+	// 设置递归过滤有权限的路由到 pinia routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+	await setFilterMenuAndCacheTagsViewRoutes();
+}
+
+/**
+ * 添加动态路由
+ * @method router.addRoute
+ * @description 此处循环为 dynamicRoutes(/@/router/route)第一个顶级 children 的路由一维数组,非多级嵌套
+ * @link 参考:https://next.router.vuejs.org/zh/api/#addroute
+ */
+export async function setAddRoute() {
+	await setFilterRouteEnd().forEach((route: RouteRecordRaw) => {
+		router.addRoute(route);
+	});
+}
+
+/**
+ * 删除/重置路由
+ * @method router.removeRoute
+ * @description 此处循环为 dynamicRoutes(/@/router/route)第一个顶级 children 的路由一维数组,非多级嵌套
+ * @link 参考:https://next.router.vuejs.org/zh/api/#push
+ */
+export async function frontEndsResetRoute() {
+	await setFilterRouteEnd().forEach((route: RouteRecordRaw) => {
+		const routeName: any = route.name;
+		router.hasRoute(routeName) && router.removeRoute(routeName);
+	});
+}
+
+/**
+ * 获取有当前用户权限标识的路由数组,进行对原路由的替换
+ * @description 替换 dynamicRoutes(/@/router/route)第一个顶级 children 的路由
+ * @returns 返回替换后的路由数组
+ */
+export function setFilterRouteEnd() {
+	let filterRouteEnd: any = formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes));
+	// notFoundAndNoPower 防止 404、401 不在 layout 布局中,不设置的话,404、401 界面将全屏显示
+	// 关联问题 No match found for location with path 'xxx'
+	filterRouteEnd[0].children = [...setFilterRoute(filterRouteEnd[0].children), ...notFoundAndNoPower];
+	return filterRouteEnd;
+}
+
+/**
+ * 获取当前用户权限标识去比对路由表(未处理成多级嵌套路由)
+ * @description 这里主要用于动态路由的添加,router.addRoute
+ * @link 参考:https://next.router.vuejs.org/zh/api/#addroute
+ * @param chil dynamicRoutes(/@/router/route)第一个顶级 children 的下路由集合
+ * @returns 返回有当前用户权限标识的路由数组
+ */
+export function setFilterRoute(chil: any) {
+	const stores = useUserInfo(pinia);
+	const { userInfos } = storeToRefs(stores);
+	let filterRoute: any = [];
+	chil.forEach((route: any) => {
+		if (route.meta.roles) {
+			route.meta.roles.forEach((metaRoles: any) => {
+				userInfos.value.roles.forEach((roles: any) => {
+					if (metaRoles === roles) filterRoute.push({ ...route });
+				});
+			});
+		}
+	});
+	return filterRoute;
+}
+
+/**
+ * 缓存多级嵌套数组处理后的一维数组
+ * @description 用于 tagsView、菜单搜索中:未过滤隐藏的(isHide)
+ */
+export function setCacheTagsViewRoutes() {
+	// 获取有权限的路由,否则 tagsView、菜单搜索中无权限的路由也将显示
+	const stores = useUserInfo(pinia);
+	const storesTagsView = useTagsViewRoutes(pinia);
+	const { userInfos } = storeToRefs(stores);
+	let rolesRoutes = setFilterHasRolesMenu(dynamicRoutes, userInfos.value.roles);
+	// 添加到 pinia setTagsViewRoutes 中
+	storesTagsView.setTagsViewRoutes(formatTwoStageRoutes(formatFlatteningRoutes(rolesRoutes))[0].children);
+}
+
+/**
+ * 设置递归过滤有权限的路由到 pinia routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+ * @description 用于左侧菜单、横向菜单的显示
+ * @description 用于 tagsView、菜单搜索中:未过滤隐藏的(isHide)
+ */
+export function setFilterMenuAndCacheTagsViewRoutes() {
+	const stores = useUserInfo(pinia);
+	const storesRoutesList = useRoutesList(pinia);
+	const { userInfos } = storeToRefs(stores);
+	storesRoutesList.setRoutesList(setFilterHasRolesMenu(dynamicRoutes[0].children, userInfos.value.roles));
+	setCacheTagsViewRoutes();
+}
+
+/**
+ * 判断路由 `meta.roles` 中是否包含当前登录用户权限字段
+ * @param roles 用户权限标识,在 userInfos(用户信息)的 roles(登录页登录时缓存到浏览器)数组
+ * @param route 当前循环时的路由项
+ * @returns 返回对比后有权限的路由项
+ */
+export function hasRoles(roles: any, route: any) {
+	if (route.meta && route.meta.roles) return roles.some((role: any) => route.meta.roles.includes(role));
+	else return true;
+}
+
+/**
+ * 获取当前用户权限标识去比对路由表,设置递归过滤有权限的路由
+ * @param routes 当前路由 children
+ * @param roles 用户权限标识,在 userInfos(用户信息)的 roles(登录页登录时缓存到浏览器)数组
+ * @returns 返回有权限的路由数组 `meta.roles` 中控制
+ */
+export function setFilterHasRolesMenu(routes: any, roles: any) {
+	const menu: any = [];
+	routes.forEach((route: any) => {
+		const item = { ...route };
+		if (hasRoles(roles, item)) {
+			if (item.children) item.children = setFilterHasRolesMenu(item.children, roles);
+			menu.push(item);
+		}
+	});
+	return menu;
+}

+ 143 - 0
h5/src/router/index.ts

@@ -0,0 +1,143 @@
+import { createRouter, createWebHashHistory } from 'vue-router';
+import NProgress from 'nprogress';
+import 'nprogress/nprogress.css';
+import pinia from '/@/stores/index';
+import { storeToRefs } from 'pinia';
+import { useKeepALiveNames } from '/@/stores/keepAliveNames';
+import { useRoutesList } from '/@/stores/routesList';
+import { useThemeConfig } from '/@/stores/themeConfig';
+import { Session } from '/@/utils/storage';
+import { staticRoutes, notFoundAndNoPower } from '/@/router/route';
+import { initFrontEndControlRoutes } from '/@/router/frontEnd';
+import { initBackEndControlRoutes } from '/@/router/backEnd';
+
+/**
+ * 1、前端控制路由时:isRequestRoutes 为 false,需要写 roles,需要走 setFilterRoute 方法。
+ * 2、后端控制路由时:isRequestRoutes 为 true,不需要写 roles,不需要走 setFilterRoute 方法),
+ * 相关方法已拆解到对应的 `backEnd.ts` 与 `frontEnd.ts`(他们互不影响,不需要同时改 2 个文件)。
+ * 特别说明:
+ * 1、前端控制:路由菜单由前端去写(无菜单管理界面,有角色管理界面),角色管理中有 roles 属性,需返回到 userInfo 中。
+ * 2、后端控制:路由菜单由后端返回(有菜单管理界面、有角色管理界面)
+ */
+
+// 读取 `/src/stores/themeConfig.ts` 是否开启后端控制路由配置
+const storesThemeConfig = useThemeConfig(pinia);
+const { themeConfig } = storeToRefs(storesThemeConfig);
+let { isRequestRoutes } = themeConfig.value;
+isRequestRoutes = false; //是否开启后端控制路由
+// console.log('isRequestRoutes',isRequestRoutes);
+
+/**
+ * 创建一个可以被 Vue 应用程序使用的路由实例
+ * @method createRouter(options: RouterOptions): Router
+ * @link 参考:https://next.router.vuejs.org/zh/api/#createrouter
+ */
+export const router = createRouter({
+	history: createWebHashHistory(),
+	/**
+	 * 说明:
+	 * 1、notFoundAndNoPower 默认添加 404、401 界面,防止一直提示 No match found for location with path 'xxx'
+	 * 2、backEnd.ts(后端控制路由)、frontEnd.ts(前端控制路由) 中也需要加 notFoundAndNoPower 404、401 界面。
+	 *    防止 404、401 不在 layout 布局中,不设置的话,404、401 界面将全屏显示
+	 */
+	routes: [...notFoundAndNoPower, ...staticRoutes],
+});
+
+/**
+ * 路由多级嵌套数组处理成一维数组
+ * @param arr 传入路由菜单数据数组
+ * @returns 返回处理后的一维路由菜单数组
+ */
+export function formatFlatteningRoutes(arr: any) {
+	if (arr.length <= 0) return false;
+	for (let i = 0; i < arr.length; i++) {
+		if (arr[i].children) {
+			arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1));
+		}
+	}
+	return arr;
+}
+
+/**
+ * 一维数组处理成多级嵌套数组(只保留二级:也就是二级以上全部处理成只有二级,keep-alive 支持二级缓存)
+ * @description isKeepAlive 处理 `name` 值,进行缓存。顶级关闭,全部不缓存
+ * @link 参考:https://v3.cn.vuejs.org/api/built-in-components.html#keep-alive
+ * @param arr 处理后的一维路由菜单数组
+ * @returns 返回将一维数组重新处理成 `定义动态路由(dynamicRoutes)` 的格式
+ */
+export function formatTwoStageRoutes(arr: any) {
+	if (arr.length <= 0) return false;
+	const newArr: any = [];
+	const cacheList: Array<string> = [];
+	arr.forEach((v: any) => {
+		if (v.path === '/') {
+			newArr.push({ component: v.component, name: v.name, path: v.path, redirect: v.redirect, meta: v.meta, children: [] });
+		} else {
+			// 判断是否是动态路由(xx/:id/:name),用于 tagsView 等中使用
+			// 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
+			if (v.path.indexOf('/:') > -1) {
+				v.meta['isDynamic'] = true;
+				v.meta['isDynamicPath'] = v.path;
+			}
+			newArr[0].children.push({ ...v });
+			// 存 name 值,keep-alive 中 include 使用,实现路由的缓存
+			// 路径:/@/layout/routerView/parent.vue
+			if (newArr[0].meta.isKeepAlive && v.meta.isKeepAlive) {
+				cacheList.push(v.name);
+				const stores = useKeepALiveNames(pinia);
+				stores.setCacheKeepAlive(cacheList);
+			}
+		}
+	});
+	return newArr;
+}
+
+// 路由加载前
+router.beforeEach(async (to, from, next) => {
+	NProgress.configure({ showSpinner: false });
+	if (to.meta.title) NProgress.start();
+	const token = Session.get('token');
+	if (to.path === '/login' && !token) {
+		next();
+		NProgress.done();
+	} else {
+		if (!token) {
+			/*
+				这句话的意思是,使用 next() 函数跳转到 /login 页面,并将 to.path 和 to.query 或 to.params 作为参数传递给 /login 页面。
+				to.path 是当前页面的路径,to.query 是当前页面的查询参数,to.params 是当前页面的路由参数。
+			*/
+			next(`/login?redirect=${to.path}&params=${JSON.stringify(to.query ? to.query : to.params)}`);
+			Session.clear();
+			NProgress.done();
+		} else if (token && to.path === '/login') {
+			next('/home');
+			NProgress.done();
+		} else {
+			const storesRoutesList = useRoutesList(pinia);
+			const { routesList } = storeToRefs(storesRoutesList);
+			if (routesList.value.length === 0) {
+				if (isRequestRoutes) {
+					// 后端控制路由:路由数据初始化,防止刷新时丢失
+					await initBackEndControlRoutes();
+					// 解决刷新时,一直跳 404 页面问题,关联问题 No match found for location with path 'xxx'
+					// to.query 防止页面刷新时,普通路由带参数时,参数丢失。动态路由(xxx/:id/:name")isDynamic 无需处理
+					next({ path: to.path, query: to.query });
+				} else {
+					// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
+					await initFrontEndControlRoutes();
+					next({ path: to.path, query: to.query });
+				}
+			} else {
+				next();
+			}
+		}
+	}
+});
+
+// 路由加载后
+router.afterEach(() => {
+	NProgress.done();
+});
+
+// 导出路由
+export default router;

+ 225 - 0
h5/src/router/route.ts

@@ -0,0 +1,225 @@
+import { RouteRecordRaw } from 'vue-router';
+
+/**
+ * 建议:路由 path 路径与文件夹名称相同,找文件可浏览器地址找,方便定位文件位置
+ *
+ * 路由meta对象参数说明
+ * meta: {
+ *      title:          菜单栏及 tagsView 栏、菜单搜索名称(国际化)
+ *      isLink:        是否超链接菜单,开启外链条件,`1、isLink: 链接地址不为空 2、isIframe:false`
+ *      isHide:        是否隐藏此路由
+ *      isKeepAlive:   是否缓存组件状态
+ *      isAffix:       是否固定在 tagsView 栏上
+ *      isIframe:      是否内嵌窗口,开启条件,`1、isIframe:true 2、isLink:链接地址不为空`
+ *      roles:         当前路由权限标识,取角色管理。控制路由显示、隐藏。超级管理员:admin 普通角色:common
+ *      icon:          菜单、tagsView 图标,阿里:加 `iconfont xxx`,fontawesome:加 `fa xxx`
+ * }
+ */
+
+// 扩展 RouteMeta 接口
+declare module 'vue-router' {
+	interface RouteMeta {
+		title?: string;
+		isLink?: string;
+		isHide?: boolean;
+		isKeepAlive?: boolean;
+		isAffix?: boolean;
+		isIframe?: boolean;
+		roles?: string[];
+		icon?: string;
+	}
+}
+
+/**
+ * 定义动态路由
+ * 前端添加路由,请在顶级节点的 `children 数组` 里添加
+ * @description 未开启 isRequestRoutes 为 true 时使用(前端控制路由),开启时第一个顶级 children 的路由将被替换成接口请求回来的路由数据
+ * @description 各字段请查看 `/@/views/system/menu/component/addMenu.vue 下的 ruleForm`
+ * @returns 返回路由菜单数据
+ */
+export const dynamicRoutes: Array<RouteRecordRaw> = [
+	{
+		path: '/',
+		name: '/',
+		component: () => import('/@/layout/index.vue'),
+		redirect: '/home',
+		meta: {
+			isKeepAlive: true,
+		},
+		children: [
+			{
+				path: '/home',
+				name: 'home',
+				component: () => import('/@/views/home/index.vue'),
+				meta: {
+					title: 'message.router.home',
+					isLink: '',
+					isHide: false,
+					isKeepAlive: true,
+					isAffix: true,
+					isIframe: false,
+					roles: ['admin', 'common'],
+					icon: 'iconfont icon-shouye',
+				},
+			},
+			{
+				path: '/underlying',
+				name: 'underlying',
+				component: () => import('/@/layout/routerView/parent.vue'),
+				redirect: '/underlying/company',
+				meta: {
+					title: 'message.router.underlying',
+					isLink: '',
+					isHide: false,
+					isKeepAlive: true,
+					isAffix: false,
+					isIframe: false,
+					roles: ['admin'],
+					icon: 'iconfont icon-xitongshezhi',
+				},
+				children: [
+					{
+						path: '/underlying/roleManage',
+						name: 'underlyingRoleManage',
+						component: () => import('/@/views/underlying/roleManage/index.vue'),
+						meta: {
+							title: 'message.router.underlyingRoleManage', //角色权限
+							isLink: '',
+							isHide: false,
+							isKeepAlive: true,
+							isAffix: false,
+							isIframe: false,
+							roles: ['admin'],
+							icon: 'iconfont icon-caidan',
+						},
+					},
+					{
+						path: '/underlying/department',
+						name: 'underlyingDepartment',
+						component: () => import('/@/views/underlying/department/index.vue'),
+						meta: {
+							title: 'message.router.underlyingDepartment', //部门人员
+							isLink: '',
+							isHide: false,
+							isKeepAlive: true,
+							isAffix: false,
+							isIframe: false,
+							roles: ['admin'],
+							icon: 'iconfont icon-caidan',
+						},
+					},
+				],
+			},
+			{
+				path: '/system',
+				name: 'system',
+				component: () => import('/@/layout/routerView/parent.vue'),
+				redirect: '/system/baseSettings',
+				meta: {
+					title: 'message.router.system',
+					isLink: '',
+					isHide: false,
+					isKeepAlive: true,
+					isAffix: false,
+					isIframe: false,
+					roles: ['admin'],
+					icon: 'iconfont icon-xitongshezhi',
+				},
+				children: [
+					{
+						path: '/system/baseSettings',
+						name: 'systemBaseSettings',
+						component: () => import('/@/views/system/baseSettings/index.vue'),
+						meta: {
+							title: 'message.router.systemBaseSettings',
+							isLink: '',
+							isHide: false,
+							isKeepAlive: true,
+							isAffix: false,
+							isIframe: false,
+							roles: ['admin'],
+							icon: 'iconfont icon-caidan',
+						},
+					},
+					{
+						path: '/system/message',
+						name: 'systemMessage',
+						component: () => import('/@/views/system/message/index.vue'),
+						meta: {
+							title: 'message.router.systemMessage',
+							isLink: '',
+							isHide: true,
+							isKeepAlive: true,
+							isAffix: false,
+							isIframe: false,
+							roles: ['admin'],
+							icon: 'iconfont icon-caidan',
+						},
+					},
+				],
+			},
+		],
+	},
+];
+
+/**
+ * 定义404、401界面
+ * @link 参考:https://next.router.vuejs.org/zh/guide/essentials/history-mode.html#netlify
+ */
+export const notFoundAndNoPower = [
+	{
+		path: '/:path(.*)*',
+		name: 'notFound',
+		component: () => import('/@/views/error/404.vue'),
+		meta: {
+			title: 'message.staticRoutes.notFound',
+			isHide: true,
+		},
+	},
+	{
+		path: '/401',
+		name: 'noPower',
+		component: () => import('/@/views/error/401.vue'),
+		meta: {
+			title: 'message.staticRoutes.noPower',
+			isHide: true,
+		},
+	},
+];
+
+/**
+ * 定义静态路由(默认路由)
+ * 此路由不要动,前端添加路由的话,请在 `dynamicRoutes 数组` 中添加
+ * @description 前端控制直接改 dynamicRoutes 中的路由,后端控制不需要修改,请求接口路由数据时,会覆盖 dynamicRoutes 第一个顶级 children 的内容(全屏,不包含 layout 中的路由出口)
+ * @returns 返回路由菜单数据
+ */
+export const staticRoutes: Array<RouteRecordRaw> = [
+	{
+		path: '/login',
+		name: 'login',
+		component: () => import('/@/views/login/index.vue'),
+		meta: {
+			title: '登录',
+		},
+	},
+	/**
+	 * 提示:写在这里的为全屏界面,不建议写在这里
+	 * 请写在 `dynamicRoutes` 路由数组中
+	 */
+	{
+		path: '/visualizingDemo1',
+		name: 'visualizingDemo1',
+		component: () => import('/@/views/visualizing/demo1.vue'),
+		meta: {
+			title: 'message.router.visualizingLinkDemo1',
+		},
+	},
+	{
+		path: '/visualizingDemo2',
+		name: 'visualizingDemo2',
+		component: () => import('/@/views/visualizing/demo2.vue'),
+		meta: {
+			title: 'message.router.visualizingLinkDemo2',
+		},
+	},
+];

+ 8 - 0
h5/src/stores/index.ts

@@ -0,0 +1,8 @@
+// https://pinia.vuejs.org/
+import { createPinia } from 'pinia';
+
+// 创建
+const pinia = createPinia();
+
+// 导出
+export default pinia;

+ 35 - 0
h5/src/stores/keepAliveNames.ts

@@ -0,0 +1,35 @@
+import { defineStore } from 'pinia';
+
+/**
+ * 路由缓存列表
+ * @methods setCacheKeepAlive 设置要缓存的路由 names(开启 Tagsview)
+ * @methods addCachedView 添加要缓存的路由 names(关闭 Tagsview)
+ * @methods delCachedView 删除要缓存的路由 names(关闭 Tagsview)
+ * @methods delOthersCachedViews 右键菜单`关闭其它`,删除要缓存的路由 names(关闭 Tagsview)
+ * @methods delAllCachedViews 右键菜单`全部关闭`,删除要缓存的路由 names(关闭 Tagsview)
+ */
+export const useKeepALiveNames = defineStore('keepALiveNames', {
+	state: (): KeepAliveNamesState => ({
+		keepAliveNames: [],
+		cachedViews: [],
+	}),
+	actions: {
+		async setCacheKeepAlive(data: Array<string>) {
+			this.keepAliveNames = data;
+		},
+		async addCachedView(view: any) {
+			if (view.meta.isKeepAlive) this.cachedViews?.push(view.name);
+		},
+		async delCachedView(view: any) {
+			const index = this.cachedViews.indexOf(view.name);
+			index > -1 && this.cachedViews.splice(index, 1);
+		},
+		async delOthersCachedViews(view: any) {
+			if (view.meta.isKeepAlive) this.cachedViews = [view.name];
+			else this.cachedViews = [];
+		},
+		async delAllCachedViews() {
+			this.cachedViews = [];
+		},
+	},
+});

+ 16 - 0
h5/src/stores/requestOldRoutes.ts

@@ -0,0 +1,16 @@
+import { defineStore } from 'pinia';
+
+/**
+ * 后端返回原始路由(未处理时)
+ * @methods setCacheKeepAlive 设置接口原始路由数据
+ */
+export const useRequestOldRoutes = defineStore('requestOldRoutes', {
+	state: (): RequestOldRoutesState => ({
+		requestOldRoutes: [],
+	}),
+	actions: {
+		async setRequestOldRoutes(routes: Array<string>) {
+			this.requestOldRoutes = routes;
+		},
+	},
+});

+ 26 - 0
h5/src/stores/routesList.ts

@@ -0,0 +1,26 @@
+import { defineStore } from 'pinia';
+
+/**
+ * 路由列表
+ * @methods setRoutesList 设置路由数据
+ * @methods setColumnsMenuHover 设置分栏布局菜单鼠标移入 boolean
+ * @methods setColumnsNavHover 设置分栏布局最左侧导航鼠标移入 boolean
+ */
+export const useRoutesList = defineStore('routesList', {
+	state: (): RoutesListState => ({
+		routesList: [],
+		isColumnsMenuHover: false,
+		isColumnsNavHover: false,
+	}),
+	actions: {
+		async setRoutesList(data: Array<string>) {
+			this.routesList = data;
+		},
+		async setColumnsMenuHover(bool: Boolean) {
+			this.isColumnsMenuHover = bool;
+		},
+		async setColumnsNavHover(bool: Boolean) {
+			this.isColumnsNavHover = bool;
+		},
+	},
+});

+ 23 - 0
h5/src/stores/tagsViewRoutes.ts

@@ -0,0 +1,23 @@
+import { defineStore } from 'pinia';
+import { Session } from '/@/utils/storage';
+
+/**
+ * TagsView 路由列表
+ * @methods setTagsViewRoutes 设置 TagsView 路由列表
+ * @methods setCurrenFullscreen 设置开启/关闭全屏时的 boolean 状态
+ */
+export const useTagsViewRoutes = defineStore('tagsViewRoutes', {
+	state: (): TagsViewRoutesState => ({
+		tagsViewRoutes: [],
+		isTagsViewCurrenFull: false,
+	}),
+	actions: {
+		async setTagsViewRoutes(data: Array<string>) {
+			this.tagsViewRoutes = data;
+		},
+		setCurrenFullscreen(bool: Boolean) {
+			Session.set('isTagsViewCurrenFull', bool);
+			this.isTagsViewCurrenFull = bool;
+		},
+	},
+});

+ 156 - 0
h5/src/stores/themeConfig.ts

@@ -0,0 +1,156 @@
+import { defineStore } from 'pinia';
+
+/**
+ * 布局配置
+ * 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I567R1,感谢@lanbao123
+ * 2020.05.28 by lyt 优化。开发时配置不生效问题
+ * 修改配置时:
+ * 1、需要每次都清理 `window.localStorage` 浏览器永久缓存
+ * 2、或者点击布局配置最底部 `一键恢复默认` 按钮即可看到效果
+ */
+export const useThemeConfig = defineStore('themeConfig', {
+	state: (): ThemeConfigState => ({
+		themeConfig: {
+			// 是否开启布局配置抽屉
+			isDrawer: false,
+
+			/**
+			 * 全局主题
+			 */
+			// 默认 primary 主题颜色
+			primary: '#409eff',
+			// 是否开启深色模式
+			isIsDark: false,
+
+			/**
+			 * 顶栏设置
+			 */
+			// 默认顶栏导航背景颜色
+			topBar: '#ffffff',
+			// 默认顶栏导航字体颜色
+			topBarColor: '#606266',
+			// 是否开启顶栏背景颜色渐变
+			isTopBarColorGradual: false,
+
+			/**
+			 * 菜单设置
+			 */
+			// 默认菜单导航背景颜色
+			menuBar: '#545c64',
+			// 默认菜单导航字体颜色
+			menuBarColor: '#eaeaea',
+			// 默认菜单高亮背景色
+			menuBarActiveColor: 'rgba(0, 0, 0, 0.2)',
+			// 是否开启菜单背景颜色渐变
+			isMenuBarColorGradual: false,
+
+			/**
+			 * 分栏设置
+			 */
+			// 默认分栏菜单背景颜色
+			columnsMenuBar: '#545c64',
+			// 默认分栏菜单字体颜色
+			columnsMenuBarColor: '#e6e6e6',
+			// 是否开启分栏菜单背景颜色渐变
+			isColumnsMenuBarColorGradual: false,
+			// 是否开启分栏菜单鼠标悬停预加载(预览菜单)
+			isColumnsMenuHoverPreload: false,
+
+			/**
+			 * 界面设置
+			 */
+			// 是否开启菜单水平折叠效果
+			isCollapse: false,
+			// 是否开启菜单手风琴效果
+			isUniqueOpened: true,
+			// 是否开启固定 Header
+			isFixedHeader: true,
+			// 初始化变量,用于更新菜单 el-scrollbar 的高度,请勿删除
+			isFixedHeaderChange: false,
+			// 是否开启经典布局分割菜单(仅经典布局生效)
+			isClassicSplitMenu: false,
+			// 是否开启自动锁屏
+			isLockScreen: false,
+			// 开启自动锁屏倒计时(s/秒)
+			lockScreenTime: 30,
+
+			/**
+			 * 界面显示
+			 */
+			// 是否开启侧边栏 Logo
+			isShowLogo: false,
+			// 初始化变量,用于 el-scrollbar 的高度更新,请勿删除
+			isShowLogoChange: false,
+			// 是否开启 Breadcrumb,强制经典、横向布局不显示
+			isBreadcrumb: true,
+			// 是否开启 Tagsview
+			isTagsview: true,
+			// 是否开启 Breadcrumb 图标
+			isBreadcrumbIcon: false,
+			// 是否开启 Tagsview 图标
+			isTagsviewIcon: false,
+			// 是否开启 TagsView 缓存
+			isCacheTagsView: false,
+			// 是否开启 TagsView 拖拽
+			isSortableTagsView: true,
+			// 是否开启 TagsView 共用
+			isShareTagsView: false,
+			// 是否开启 Footer 底部版权信息
+			isFooter: false,
+			// 是否开启灰色模式
+			isGrayscale: false,
+			// 是否开启色弱模式
+			isInvert: false,
+			// 是否开启水印
+			isWartermark: false,
+			// 水印文案
+			wartermarkText: 'vue-next-admin',
+
+			/**
+			 * 其它设置
+			 */
+			// Tagsview 风格:可选值"<tags-style-one|tags-style-four|tags-style-five>",默认 tags-style-five
+			// 定义的值与 `/src/layout/navBars/tagsView/tagsView.vue` 中的 class 同名
+			tagsStyle: 'tags-style-five',
+			// 主页面切换动画:可选值"<slide-right|slide-left|opacitys>",默认 slide-right
+			animation: 'slide-right',
+			// 分栏高亮风格:可选值"<columns-round|columns-card>",默认 columns-round
+			columnsAsideStyle: 'columns-round',
+			// 分栏布局风格:可选值"<columns-horizontal|columns-vertical>",默认 columns-horizontal
+			columnsAsideLayout: 'columns-vertical',
+
+			/**
+			 * 布局切换
+			 * 注意:为了演示,切换布局时,颜色会被还原成默认,代码位置:/@/layout/navBars/breadcrumb/setings.vue
+			 * 中的 `initSetLayoutChange(设置布局切换,重置主题样式)` 方法
+			 */
+			// 布局切换:可选值"<defaults|classic|transverse|columns>",默认 defaults
+			layout: 'defaults',
+
+			/**
+			 * 后端控制路由
+			 */
+			// 是否开启后端控制路由
+			isRequestRoutes: true,
+
+			/**
+			 * 全局网站标题 / 副标题
+			 */
+			// 网站主标题(菜单导航、浏览器当前网页标题)
+			globalTitle: '计件工资管理系统',
+			// 网站副标题(登录页顶部文字)
+			globalViceTitle: '韶能集团广东绿洲生态科技有限公司',
+			// 网站副标题(登录页顶部文字)(专注、免费、开源、维护、解疑)
+			globalViceTitleMsg: '计件工资管理系统',
+			// 默认初始语言,可选值"<zh-cn|en|zh-tw>",默认 zh-cn
+			globalI18n: 'zh-cn',
+			// 默认全局组件大小,可选值"<large|'default'|small>",默认 'large'
+			globalComponentSize: 'large',
+		},
+	}),
+	actions: {
+		setThemeConfig(data: ThemeConfigState) {
+			this.themeConfig = data.themeConfig;
+		},
+	},
+});

+ 80 - 0
h5/src/stores/userInfo.ts

@@ -0,0 +1,80 @@
+import { defineStore } from 'pinia';
+import Cookies from 'js-cookie';
+import { Session } from '/@/utils/storage';
+
+/**
+ * 用户信息
+ * @methods setUserInfos 设置用户信息
+ */
+export const useUserInfo = defineStore('userInfo', {
+	state: (): UserInfosState => ({
+		userInfos: {
+			userName: '',
+			photo: '',
+			time: 0,
+			roles: [],
+			authBtnList: [],
+		},
+	}),
+	actions: {
+		async setUserInfos() {
+			// 存储用户信息到浏览器缓存
+			if (Session.get('userInfo')) {
+				// this.userInfos = Session.get('userInfo');
+				let use = Session.get('userInfo');
+				// console.log('use',use);
+				this.userInfos = {
+					userName: use.name,
+					photo: 'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500',
+					time: new Date().getTime(),
+					roles: ['admin'],
+					authBtnList: ['btn.add', 'btn.del', 'btn.edit', 'btn.link'],
+				}
+			} else {
+				const userInfos: any = await this.getApiUserInfo();
+				this.userInfos = userInfos;
+			}
+		},
+		// 模拟接口数据
+		// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
+		async getApiUserInfo() {
+			return new Promise((resolve) => {
+				setTimeout(() => {
+					// 模拟数据,请求接口时,记得删除多余代码及对应依赖的引入
+					const userName = Cookies.get('userName');
+					// 模拟数据
+					let defaultRoles: Array<string> = [];
+					let defaultAuthBtnList: Array<string> = [];
+					// admin 页面权限标识,对应路由 meta.roles,用于控制路由的显示/隐藏
+					let adminRoles: Array<string> = ['admin'];
+					// admin 按钮权限标识
+					let adminAuthBtnList: Array<string> = ['btn.add', 'btn.del', 'btn.edit', 'btn.link'];
+					// test 页面权限标识,对应路由 meta.roles,用于控制路由的显示/隐藏
+					let testRoles: Array<string> = ['common'];
+					// test 按钮权限标识
+					let testAuthBtnList: Array<string> = ['btn.add', 'btn.link'];
+					// 不同用户模拟不同的用户权限
+					if (userName === 'admin') {
+						defaultRoles = adminRoles;
+						defaultAuthBtnList = adminAuthBtnList;
+					} else {
+						defaultRoles = testRoles;
+						defaultAuthBtnList = testAuthBtnList;
+					}
+					// 用户信息模拟数据
+					const userInfos = {
+						userName: userName,
+						photo:
+							userName === 'admin'
+								? 'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500'
+								: 'https://img2.baidu.com/it/u=2370931438,70387529&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500',
+						time: new Date().getTime(),
+						roles: defaultRoles,
+						authBtnList: defaultAuthBtnList,
+					};
+					resolve(userInfos);
+				}, 0);
+			});
+		},
+	},
+});

+ 324 - 0
h5/src/theme/app.scss

@@ -0,0 +1,324 @@
+/* 初始化样式
+------------------------------- */
+* {
+	margin: 0;
+	padding: 0;
+	box-sizing: border-box;
+	outline: none !important;
+}
+
+:root {
+	--next-color-white: #ffffff;
+	--next-bg-main-color: #f8f8f8;
+	--next-bg-color: #f5f5ff;
+	--next-border-color-light: #f1f2f3;
+	--next-color-primary-lighter: #ecf5ff;
+	--next-color-success-lighter: #f0f9eb;
+	--next-color-warning-lighter: #fdf6ec;
+	--next-color-danger-lighter: #fef0f0;
+	--next-color-dark-hover: #0000001a;
+	--next-color-menu-hover: rgba(0, 0, 0, 0.2);
+	--next-color-user-hover: rgba(0, 0, 0, 0.04);
+	--next-color-seting-main: #e9eef3;
+	--next-color-seting-aside: #d3dce6;
+	--next-color-seting-header: #b3c0d1;
+}
+
+html,
+body,
+#app {
+	margin: 0;
+	padding: 0;
+	width: 100%;
+	height: 100%;
+	font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, SimSun, sans-serif;
+	font-weight: 400;
+	-webkit-font-smoothing: antialiased;
+	-webkit-tap-highlight-color: transparent;
+	background-color: var(--next-bg-main-color);
+	font-size: 14px;
+	overflow: hidden;
+	position: relative;
+}
+
+/* 主布局样式
+------------------------------- */
+.layout-container {
+	width: 100%;
+	height: 100%;
+	.layout-pd {
+		padding: 15px !important;
+	}
+	.layout-flex {
+		display: flex;
+		flex-direction: column;
+		flex: 1;
+	}
+	.layout-aside {
+		background: var(--next-bg-menuBar);
+		box-shadow: 2px 0 6px rgb(0 21 41 / 1%);
+		height: inherit;
+		position: relative;
+		z-index: 1;
+		display: flex;
+		flex-direction: column;
+		overflow-x: hidden !important;
+		.el-scrollbar__view {
+			overflow: hidden;
+		}
+	}
+	.layout-header {
+		padding: 0 !important;
+		height: auto !important;
+	}
+	.layout-main {
+		padding: 0 !important;
+		overflow: hidden;
+		width: 100%;
+		background-color: var(--next-bg-main-color);
+		display: flex;
+		flex-direction: column;
+		// 内层 el-scrollbar样式,用于界面高度自适应(main.vue)
+		.layout-main-scroll {
+			@extend .layout-flex;
+			.layout-parent {
+				@extend .layout-flex;
+				position: relative;
+			}
+		}
+	}
+	// 用于界面高度自适应
+	.layout-padding {
+		@extend .layout-pd;
+		position: absolute;
+		left: 0;
+		top: 0;
+		height: 100%;
+		overflow: hidden;
+		@extend .layout-flex;
+		&-auto {
+			height: inherit;
+			@extend .layout-flex;
+		}
+		&-view {
+			background: var(--el-color-white);
+			width: 100%;
+			height: 100%;
+			border-radius: 4px;
+			border: 1px solid var(--el-border-color-light, #ebeef5);
+			overflow: hidden;
+		}
+	}
+	// 用于界面高度自适应,主视图区 main 的内边距,用于 iframe
+	.layout-padding-unset {
+		padding: 0 !important;
+		&-view {
+			border-radius: 0 !important;
+			border: none !important;
+		}
+	}
+	// 用于设置 iframe loading 时的高度(loading 垂直居中显示)
+	.layout-iframe {
+		.el-loading-parent--relative {
+			height: 100%;
+		}
+	}
+	.el-scrollbar {
+		width: 100%;
+	}
+	.layout-el-aside-br-color {
+		border-right: 1px solid var(--el-border-color-light, #ebeef5);
+	}
+	// pc端左侧导航样式
+	.layout-aside-pc-220 {
+		width: 220px !important;
+		transition: width 0.3s ease;
+	}
+	.layout-aside-pc-64 {
+		width: 64px !important;
+		transition: width 0.3s ease;
+	}
+	.layout-aside-pc-1 {
+		width: 1px !important;
+		transition: width 0.3s ease;
+	}
+	// 手机端左侧导航样式
+	.layout-aside-mobile {
+		position: fixed;
+		top: 0;
+		left: -220px;
+		width: 220px;
+		z-index: 9999999;
+	}
+	.layout-aside-mobile-close {
+		left: -220px;
+		transition: all 0.3s cubic-bezier(0.39, 0.58, 0.57, 1);
+	}
+	.layout-aside-mobile-open {
+		left: 0;
+		transition: all 0.3s cubic-bezier(0.22, 0.61, 0.36, 1);
+	}
+	.layout-aside-mobile-mode {
+		position: fixed;
+		top: 0;
+		right: 0;
+		bottom: 0;
+		left: 0;
+		height: 100%;
+		background-color: rgba(0, 0, 0, 0.5);
+		z-index: 9999998;
+		animation: error-img 0.3s;
+	}
+	.layout-mian-height-50 {
+		height: calc(100vh - 50px);
+	}
+	.layout-columns-warp {
+		flex: 1;
+		display: flex;
+		overflow: hidden;
+	}
+	.layout-hide {
+		display: none;
+	}
+}
+
+/* element plus 全局样式
+------------------------------- */
+.layout-breadcrumb-seting {
+	.el-divider {
+		background-color: rgb(230, 230, 230);
+	}
+}
+
+/* nprogress 进度条跟随主题颜色
+------------------------------- */
+#nprogress {
+	.bar {
+		background: var(--el-color-primary) !important;
+		z-index: 9999999 !important;
+	}
+}
+
+/* flex 弹性布局
+------------------------------- */
+.flex {
+	display: flex;
+}
+.flex-auto {
+	flex: 1;
+	overflow: hidden;
+}
+.flex-center {
+	@extend .flex;
+	flex-direction: column;
+	width: 100%;
+	overflow: hidden;
+}
+.flex-margin {
+	margin: auto;
+}
+.flex-warp {
+	display: flex;
+	flex-wrap: wrap;
+	align-content: flex-start;
+	margin: 0 -5px;
+	.flex-warp-item {
+		padding: 5px;
+		.flex-warp-item-box {
+			width: 100%;
+			height: 100%;
+		}
+	}
+}
+
+/* cursor 鼠标形状
+------------------------------- */
+// 默认
+.cursor-default {
+	cursor: default !important;
+}
+// 帮助
+.cursor-help {
+	cursor: help !important;
+}
+// 手指
+.cursor-pointer {
+	cursor: pointer !important;
+}
+// 移动
+.cursor-move {
+	cursor: move !important;
+}
+
+/* 宽高 100%
+------------------------------- */
+.w100 {
+	width: 100% !important;
+}
+.h100 {
+	height: 100% !important;
+}
+.vh100 {
+	height: 100vh !important;
+}
+.max100vh {
+	max-height: 100vh !important;
+}
+.min100vh {
+	min-height: 100vh !important;
+}
+
+/* 颜色值
+------------------------------- */
+.color-primary {
+	color: var(--el-color-primary);
+}
+.color-success {
+	color: var(--el-color-success);
+}
+.color-warning {
+	color: var(--el-color-warning);
+}
+.color-danger {
+	color: var(--el-color-danger);
+}
+.color-info {
+	color: var(--el-color-info);
+}
+
+/* 字体大小全局样式
+------------------------------- */
+@for $i from 10 through 32 {
+	.font#{$i} {
+		font-size: #{$i}px !important;
+	}
+}
+
+/* 外边距、内边距全局样式
+------------------------------- */
+@for $i from 1 through 35 {
+	.mt#{$i} {
+		margin-top: #{$i}px !important;
+	}
+	.mr#{$i} {
+		margin-right: #{$i}px !important;
+	}
+	.mb#{$i} {
+		margin-bottom: #{$i}px !important;
+	}
+	.ml#{$i} {
+		margin-left: #{$i}px !important;
+	}
+	.pt#{$i} {
+		padding-top: #{$i}px !important;
+	}
+	.pr#{$i} {
+		padding-right: #{$i}px !important;
+	}
+	.pb#{$i} {
+		padding-bottom: #{$i}px !important;
+	}
+	.pl#{$i} {
+		padding-left: #{$i}px !important;
+	}
+}

+ 147 - 0
h5/src/theme/common/transition.scss

@@ -0,0 +1,147 @@
+/* 页面切换动画
+------------------------------- */
+.slide-right-enter-active,
+.slide-right-leave-active,
+.slide-left-enter-active,
+.slide-left-leave-active {
+	will-change: transform;
+	transition: all 0.3s ease;
+}
+// slide-right
+.slide-right-enter-from {
+	opacity: 0;
+	transform: translateX(-20px);
+}
+.slide-right-leave-to {
+	opacity: 0;
+	transform: translateX(20px);
+}
+// slide-left
+.slide-left-enter-from {
+	@extend .slide-right-leave-to;
+}
+.slide-left-leave-to {
+	@extend .slide-right-enter-from;
+}
+// opacitys
+.opacitys-enter-active,
+.opacitys-leave-active {
+	will-change: transform;
+	transition: all 0.3s ease;
+}
+.opacitys-enter-from,
+.opacitys-leave-to {
+	opacity: 0;
+}
+
+/* Breadcrumb 面包屑过渡动画
+------------------------------- */
+.breadcrumb-enter-active,
+.breadcrumb-leave-active {
+	transition: all 0.5s ease;
+}
+.breadcrumb-enter-from,
+.breadcrumb-leave-active {
+	opacity: 0;
+	transform: translateX(20px);
+}
+.breadcrumb-leave-active {
+	position: absolute;
+	z-index: -1;
+}
+
+/* logo 过渡动画
+------------------------------- */
+@keyframes logoAnimation {
+	0% {
+		transform: scale(0);
+	}
+	80% {
+		transform: scale(1.2);
+	}
+	100% {
+		transform: scale(1);
+	}
+}
+
+/* 404、401 过渡动画
+------------------------------- */
+@keyframes error-num {
+	0% {
+		transform: translateY(60px);
+		opacity: 0;
+	}
+	100% {
+		transform: translateY(0);
+		opacity: 1;
+	}
+}
+@keyframes error-img {
+	0% {
+		opacity: 0;
+	}
+	100% {
+		opacity: 1;
+	}
+}
+@keyframes error-img-two {
+	0% {
+		opacity: 1;
+	}
+	100% {
+		opacity: 0;
+	}
+}
+
+/* 登录页动画
+------------------------------- */
+@keyframes loginLeft {
+	0% {
+		left: -100%;
+	}
+	50%,
+	100% {
+		left: 100%;
+	}
+}
+@keyframes loginTop {
+	0% {
+		top: -100%;
+	}
+	50%,
+	100% {
+		top: 100%;
+	}
+}
+@keyframes loginRight {
+	0% {
+		right: -100%;
+	}
+	50%,
+	100% {
+		right: 100%;
+	}
+}
+@keyframes loginBottom {
+	0% {
+		bottom: -100%;
+	}
+	50%,
+	100% {
+		bottom: 100%;
+	}
+}
+
+/* 左右左 link.vue
+------------------------------- */
+@keyframes toRight {
+	0% {
+		left: -5px;
+	}
+	50% {
+		left: 100%;
+	}
+	100% {
+		left: -5px;
+	}
+}

+ 249 - 0
h5/src/theme/dark.scss

@@ -0,0 +1,249 @@
+/* 深色模式样式
+------------------------------- */
+[data-theme='dark'] {
+	// 变量(自定义时,只需修改这里的值)
+	--next-bg-main: #1f1f1f;
+	--next-color-white: #ffffff;
+	--next-color-disabled: #191919;
+	--next-color-bar: #dadada;
+	--next-color-primary: #303030;
+	--next-border-color: #424242;
+	--next-border-black: #333333;
+	--next-border-columns: #2a2a2a;
+	--next-color-seting: #505050;
+	--next-text-color-regular: #9b9da1;
+	--next-text-color-placeholder: #7a7a7a;
+	--next-color-hover: #3c3c3c;
+	--next-color-hover-rgba: rgba(0, 0, 0, 0.3);
+
+	// root
+	--next-bg-main-color: var(--next-bg-main) !important;
+	--next-bg-topBar: var(--next-color-disabled) !important;
+	--next-bg-topBarColor: var(--next-color-bar) !important;
+	--next-bg-menuBar: var(--next-color-disabled) !important;
+	--next-bg-menuBarColor: var(--next-color-bar) !important;
+	--next-bg-menuBarActiveColor: var(--next-color-hover-rgba) !important;
+	--next-bg-columnsMenuBar: var(--next-color-disabled) !important;
+	--next-bg-columnsMenuBarColor: var(--next-color-bar) !important;
+	--next-border-color-light: var(--next-border-black) !important;
+	--next-color-primary-lighter: var(--next-color-primary) !important;
+	--next-color-success-lighter: var(--next-color-primary) !important;
+	--next-color-warning-lighter: var(--next-color-primary) !important;
+	--next-color-danger-lighter: var(--next-color-primary) !important;
+	--next-bg-color: var(--next-color-primary) !important;
+	--next-color-dark-hover: var(--next-color-hover) !important;
+	--next-color-menu-hover: var(--next-color-hover-rgba) !important;
+	--next-color-user-hover: var(--next-color-hover-rgba) !important;
+	--next-color-seting-main: var(--next-color-seting) !important;
+	--next-color-seting-aside: var(--next-color-hover) !important;
+	--next-color-seting-header: var(--next-color-primary) !important;
+
+	// element plus
+	--el-color-white: var(--next-color-disabled) !important;
+	--el-text-color-primary: var(--next-color-bar) !important;
+	--el-border-color: var(--next-border-black) !important;
+	--el-border-color-light: var(--next-border-black) !important;
+	--el-border-color-lighter: var(--next-border-black) !important;
+	--el-border-color-extra-light: var(--el-color-primary-light-8) !important;
+	--el-text-color-regular: var(--next-text-color-regular) !important;
+	--el-bg-color: var(--next-color-disabled) !important;
+	--el-color-primary-light-9: var(--next-color-hover) !important;
+	--el-text-color-disabled: var(--next-text-color-placeholder) !important;
+	--el-text-color-disabled-base: var(--el-color-primary) !important;
+	--el-text-color-placeholder: var(--next-text-color-placeholder) !important;
+	--el-disabled-bg-color: var(--next-color-disabled) !important;
+	--el-fill-base: var(--next-color-white) !important;
+	--el-fill-colo: var(--next-color-hover-rgba) !important;
+	--el-fill-color: var(--next-color-hover-rgba) !important;
+	--el-fill-color-blank: var(--next-color-disabled) !important;
+	--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 {
+		&:hover {
+			border-color: var(--next-border-color) !important;
+		}
+	}
+	.el-button--primary,
+	.el-button--info,
+	.el-button--danger,
+	.el-button--success,
+	.el-button--warning {
+		--el-button-text-color: var(--next-color-white) !important;
+		--el-button-hover-text-color: var(--next-color-white) !important;
+		--el-button-disabled-text-color: var(--next-color-white) !important;
+		&:hover {
+			border-color: var(--el-button-hover-border-color, var(--el-button-hover-bg-color)) !important;
+		}
+	}
+
+	// drawer
+	.el-divider__text {
+		background-color: var(--el-color-white) !important;
+	}
+	.el-drawer {
+		border-left: 1px solid var(--next-border-color-light) !important;
+	}
+
+	// tabs
+	.el-tabs--border-card {
+		background-color: var(--el-color-white) !important;
+	}
+	.el-tabs--border-card > .el-tabs__header .el-tabs__item.is-active {
+		background: var(--next-color-primary-lighter);
+	}
+
+	// alert / notice-bar
+	.home-card-item {
+		border: 1px solid var(--next-border-color-light) !important;
+	}
+	.el-alert,
+	.notice-bar {
+		border: 1px solid var(--next-border-color) !important;
+		background-color: var(--next-color-disabled) !important;
+	}
+
+	// menu
+	.layout-aside {
+		border-right: 1px solid var(--next-border-color-light) !important;
+	}
+
+	// colorPicker
+	.el-color-picker__mask {
+		background: unset !important;
+	}
+	.el-color-picker__trigger {
+		border: 1px solid var(--next-border-color-light) !important;
+	}
+
+	// popper / dropdown
+	.el-popper {
+		border: 1px solid var(--next-border-color) !important;
+		color: var(--el-text-color-primary) !important;
+		.el-popper__arrow:before {
+			background: var(--el-color-white) !important;
+			border: 1px solid var(--next-border-color);
+		}
+		a {
+			color: var(--el-text-color-primary) !important;
+		}
+	}
+	.el-popper,
+	.el-dropdown-menu {
+		background: var(--el-color-white) !important;
+	}
+	.el-dropdown-menu__item:hover:not(.is-disabled) {
+		background: var(--el-bg-color) !important;
+	}
+	.el-dropdown-menu__item.is-disabled {
+		font-weight: 700 !important;
+	}
+
+	// input
+	.el-input-group__append,
+	.el-input-group__prepend {
+		border: var(--el-input-border) !important;
+		border-right: none !important;
+		background: var(--next-color-disabled) !important;
+		border-left: 0 !important;
+	}
+	.el-input-number__decrease,
+	.el-input-number__increase {
+		background: var(--next-color-disabled) !important;
+	}
+
+	// tag
+	.el-select .el-select__tags .el-tag {
+		background-color: var(--next-bg-color) !important;
+	}
+
+	// pagination
+	.el-pagination.is-background .el-pager li:not(.disabled).active {
+		color: var(--next-color-white) !important;
+	}
+	.el-pagination.is-background .btn-next,
+	.el-pagination.is-background .btn-prev,
+	.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 {
+		border: 1px solid var(--next-border-color-light) !important;
+		border-left: 0 !important;
+	}
+	.el-radio-button.is-active .el-radio-button__inner {
+		color: var(--next-color-white) !important;
+	}
+
+	// countup
+	.countup-card-item-flex {
+		color: var(--el-text-color-primary) !important;
+	}
+
+	// editor
+	.editor-container {
+		.w-e-toolbar {
+			background: var(--el-color-white) !important;
+			border: 1px solid var(--next-border-color-light) !important;
+			.w-e-menu:hover {
+				background: var(--next-color-user-hover) !important;
+				i {
+					color: var(--el-text-color-primary) !important;
+				}
+			}
+		}
+		.w-e-text-container {
+			border: 1px solid var(--next-border-color-light) !important;
+			border-top: none !important;
+			.w-e-text {
+				background: var(--el-color-white) !important;
+			}
+		}
+	}
+
+	// date-picker
+	.el-picker-panel {
+		background: var(--el-color-white) !important;
+	}
+
+	// dialog
+	.el-dialog {
+		border: 1px solid var(--el-border-color-lighter);
+		.el-dialog__header {
+			color: var(--el-text-color-primary) !important;
+		}
+	}
+
+	// columns
+	.layout-columns-aside ul .layout-columns-active {
+		color: var(--next-color-white) !important;
+	}
+	.layout-columns-aside {
+		border-right: 1px solid var(--next-border-columns);
+	}
+
+	// tagsView
+	.tags-style-one {
+		.is-active {
+			color: var(--el-text-color-primary) !important;
+		}
+		.layout-navbars-tagsview-ul-li:hover {
+			border-color: var(--el-border-color-lighter) !important;
+		}
+	}
+
+	// loading
+	.el-loading-mask {
+		background-color: var(--next-bg-main) !important;
+	}
+}

+ 0 - 0
h5/src/theme/element.scss


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