刘忠健 3 年之前
父節點
當前提交
08f0572e78
共有 100 個文件被更改,包括 9814 次插入0 次删除
  1. 11 0
      nextadmin_vue3/.env
  2. 6 0
      nextadmin_vue3/.env.development
  3. 5 0
      nextadmin_vue3/.env.production
  4. 6 0
      nextadmin_vue3/.env.test
  5. 18 0
      nextadmin_vue3/.eslintignore
  6. 63 0
      nextadmin_vue3/.eslintrc.js
  7. 24 0
      nextadmin_vue3/.gitignore
  8. 39 0
      nextadmin_vue3/.prettierrc.js
  9. 286 0
      nextadmin_vue3/CHANGELOG.md
  10. 21 0
      nextadmin_vue3/LICENSE
  11. 135 0
      nextadmin_vue3/README.md
  12. 1 0
      nextadmin_vue3/Z_START.bat
  13. 31 0
      nextadmin_vue3/index.html
  14. 84 0
      nextadmin_vue3/package.json
  15. 3 0
      nextadmin_vue3/plugins.d.ts
  16. 二進制
      nextadmin_vue3/public/favicon.ico
  17. 13 0
      nextadmin_vue3/shim.d.ts
  18. 6 0
      nextadmin_vue3/source.d.ts
  19. 92 0
      nextadmin_vue3/src/App.vue
  20. 80 0
      nextadmin_vue3/src/api/Company.ts
  21. 10 0
      nextadmin_vue3/src/api/Menu.ts
  22. 36 0
      nextadmin_vue3/src/api/Protocol.ts
  23. 59 0
      nextadmin_vue3/src/api/Urls.ts
  24. 63 0
      nextadmin_vue3/src/api/User.ts
  25. 25 0
      nextadmin_vue3/src/api/login/index.ts
  26. 31 0
      nextadmin_vue3/src/api/menu/index.ts
  27. 1 0
      nextadmin_vue3/src/assets/login-icon-two.svg
  28. 9 0
      nextadmin_vue3/src/assets/logo-mini.svg
  29. 27 0
      nextadmin_vue3/src/components/auth/auth.vue
  30. 28 0
      nextadmin_vue3/src/components/auth/authAll.vue
  31. 33 0
      nextadmin_vue3/src/components/auth/auths.vue
  32. 148 0
      nextadmin_vue3/src/components/cropper/index.vue
  33. 79 0
      nextadmin_vue3/src/components/editor/index.vue
  34. 242 0
      nextadmin_vue3/src/components/iconSelector/index.vue
  35. 194 0
      nextadmin_vue3/src/components/noticeBar/index.vue
  36. 41 0
      nextadmin_vue3/src/components/svgIcon/index.vue
  37. 27 0
      nextadmin_vue3/src/config.ts
  38. 55 0
      nextadmin_vue3/src/i18n/index.ts
  39. 184 0
      nextadmin_vue3/src/i18n/lang/zh-cn.ts
  40. 13 0
      nextadmin_vue3/src/i18n/pages/formI18n/en.ts
  41. 13 0
      nextadmin_vue3/src/i18n/pages/formI18n/zh-cn.ts
  42. 13 0
      nextadmin_vue3/src/i18n/pages/formI18n/zh-tw.ts
  43. 29 0
      nextadmin_vue3/src/i18n/pages/login/en.ts
  44. 28 0
      nextadmin_vue3/src/i18n/pages/login/zh-cn.ts
  45. 28 0
      nextadmin_vue3/src/i18n/pages/login/zh-tw.ts
  46. 157 0
      nextadmin_vue3/src/layout/component/aside.vue
  47. 294 0
      nextadmin_vue3/src/layout/component/columnsAside.vue
  48. 32 0
      nextadmin_vue3/src/layout/component/header.vue
  49. 84 0
      nextadmin_vue3/src/layout/component/main.vue
  50. 46 0
      nextadmin_vue3/src/layout/footer/index.vue
  51. 56 0
      nextadmin_vue3/src/layout/index.vue
  52. 370 0
      nextadmin_vue3/src/layout/lockScreen/index.vue
  53. 83 0
      nextadmin_vue3/src/layout/logo/index.vue
  54. 36 0
      nextadmin_vue3/src/layout/main/classic.vue
  55. 38 0
      nextadmin_vue3/src/layout/main/columns.vue
  56. 44 0
      nextadmin_vue3/src/layout/main/defaults.vue
  57. 16 0
      nextadmin_vue3/src/layout/main/transverse.vue
  58. 152 0
      nextadmin_vue3/src/layout/navBars/breadcrumb/breadcrumb.vue
  59. 63 0
      nextadmin_vue3/src/layout/navBars/breadcrumb/closeFull.vue
  60. 115 0
      nextadmin_vue3/src/layout/navBars/breadcrumb/index.vue
  61. 134 0
      nextadmin_vue3/src/layout/navBars/breadcrumb/search.vue
  62. 792 0
      nextadmin_vue3/src/layout/navBars/breadcrumb/setings.vue
  63. 295 0
      nextadmin_vue3/src/layout/navBars/breadcrumb/user.vue
  64. 114 0
      nextadmin_vue3/src/layout/navBars/breadcrumb/userNews.vue
  65. 37 0
      nextadmin_vue3/src/layout/navBars/index.vue
  66. 137 0
      nextadmin_vue3/src/layout/navBars/tagsView/contextmenu.vue
  67. 681 0
      nextadmin_vue3/src/layout/navBars/tagsView/tagsView.vue
  68. 151 0
      nextadmin_vue3/src/layout/navMenu/horizontal.vue
  69. 47 0
      nextadmin_vue3/src/layout/navMenu/subItem.vue
  70. 98 0
      nextadmin_vue3/src/layout/navMenu/vertical.vue
  71. 60 0
      nextadmin_vue3/src/layout/routerView/iframes.vue
  72. 59 0
      nextadmin_vue3/src/layout/routerView/link.vue
  73. 83 0
      nextadmin_vue3/src/layout/routerView/parent.vue
  74. 22 0
      nextadmin_vue3/src/main.ts
  75. 102 0
      nextadmin_vue3/src/router/backEnd.ts
  76. 24 0
      nextadmin_vue3/src/router/frontEnd.ts
  77. 232 0
      nextadmin_vue3/src/router/index.ts
  78. 1180 0
      nextadmin_vue3/src/router/route.bak.ts
  79. 88 0
      nextadmin_vue3/src/router/route.ts
  80. 27 0
      nextadmin_vue3/src/store/index.ts
  81. 94 0
      nextadmin_vue3/src/store/interface/index.ts
  82. 23 0
      nextadmin_vue3/src/store/modules/keepAliveNames.ts
  83. 23 0
      nextadmin_vue3/src/store/modules/requestOldRoutes.ts
  84. 41 0
      nextadmin_vue3/src/store/modules/routesList.ts
  85. 34 0
      nextadmin_vue3/src/store/modules/tagsViewRoutes.ts
  86. 153 0
      nextadmin_vue3/src/store/modules/themeConfig.ts
  87. 34 0
      nextadmin_vue3/src/store/modules/userInfos.ts
  88. 277 0
      nextadmin_vue3/src/theme/app.scss
  89. 94 0
      nextadmin_vue3/src/theme/common/transition.scss
  90. 219 0
      nextadmin_vue3/src/theme/dark.scss
  91. 227 0
      nextadmin_vue3/src/theme/element.scss
  92. 70 0
      nextadmin_vue3/src/theme/iconSelector.scss
  93. 9 0
      nextadmin_vue3/src/theme/index.scss
  94. 51 0
      nextadmin_vue3/src/theme/loading.scss
  95. 94 0
      nextadmin_vue3/src/theme/media/chart.scss
  96. 10 0
      nextadmin_vue3/src/theme/media/cityLinkage.scss
  97. 25 0
      nextadmin_vue3/src/theme/media/date.scss
  98. 12 0
      nextadmin_vue3/src/theme/media/dialog.scss
  99. 35 0
      nextadmin_vue3/src/theme/media/error.scss
  100. 0 0
      nextadmin_vue3/src/theme/media/form.scss

+ 11 - 0
nextadmin_vue3/.env

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

+ 6 - 0
nextadmin_vue3/.env.development

@@ -0,0 +1,6 @@
+# 本地环境
+ENV = 'development'
+
+# 本地环境接口地址
+VITE_API_URL = http://local.lzj/office_kpi/api/public/index.php
+VITE_PUBLIC_PATH = /dev/

+ 5 - 0
nextadmin_vue3/.env.production

@@ -0,0 +1,5 @@
+# 线上环境
+ENV = 'production'
+
+# 线上环境接口地址
+VITE_API_URL = http://local.lzj/office_kpi/api/public/index.php

+ 6 - 0
nextadmin_vue3/.env.test

@@ -0,0 +1,6 @@
+# 测试环境
+ENV = 'production'
+
+# 线上环境接口地址
+VITE_OUTPUT_DIR= ../api/public/platform_test
+VITE_PUBLIC_PATH = "/office_kpi/api/public/platform_test/"

+ 18 - 0
nextadmin_vue3/.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

+ 63 - 0
nextadmin_vue3/.eslintrc.js

@@ -0,0 +1,63 @@
+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'],
+	rules: {
+		// http://eslint.cn/docs/rules/
+		// https://eslint.vuejs.org/rules/
+		'@type-eslint/ban-ts-ignore': 'off',
+		'@type-eslint/explicit-function-return-type': 'off',
+		'@type-eslint/no-explicit-any': 'off',
+		'@type-eslint/no-var-requires': 'off',
+		'@type-eslint/no-empty-function': 'off',
+		'@type-eslint/no-use-before-define': 'off',
+		'@type-eslint/ban-ts-comment': 'off',
+		'@type-eslint/ban-types': 'off',
+		'@type-eslint/no-non-null-assertion': 'off',
+		'@type-eslint/explicit-module-boundary-types': 'off',
+		'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',
+	},
+};

+ 24 - 0
nextadmin_vue3/.gitignore

@@ -0,0 +1,24 @@
+.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?
+package-lock.json

+ 39 - 0
nextadmin_vue3/.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',
+};

+ 286 - 0
nextadmin_vue3/CHANGELOG.md

@@ -0,0 +1,286 @@
+# <a href="https://gitee.com/lyt-top/vue-next-admin" target="_blank">vue-next-admin 更新日志</a>
+
+🎉🎉🔥 `vue-next-admin` 基于 vue3.x 、Typescript、vite、Element plus 等,适配手机、平板、pc 的后台开源免费模板库(vue2.x 请切换 vue-prev-admin 分支)
+
+## 2.0.0
+
+`2022.02.21`
+
+⚡⚡⚡ 此版本为破环性更新,优化内容如下:(谨慎更新!谨慎更新!!谨慎更新!!!)。演示界面建议直接覆盖文件。如需使用之前版本,请前往[gitee 发行版](https://gitee.com/lyt-top/vue-next-admin/releases) 进行对应版本下载。基础版会基于 `master` 分支进行修改
+
+- 🌟 更新 依赖更新最新版本
+- 🌟 更新 登录页、首页
+- 💔 移除 vue-web-screen-shot
+- 💔 移除 城市多级联动,完整 json 数据请去 [vue-next-admin-images/menu](https://gitee.com/lyt-top/vue-next-admin-images/tree/master/menu) 仓库查看
+- 💔 移除 功能/echartsTree 树图
+- 💔 移除 其它设置/Tagsview 风格 2、Tagsview 风格 3
+- 💔 移除 功能/验证器
+- 🚧 调整 src/api 编写方式
+- 🚧 调整 自定义封装公用组件演示,更好的维护
+- 🎉 新增 Volar 支持,vs code 配置参考 [Vue Language Features (Volar)](https://lyt-top.gitee.io/vue-next-admin-doc-preview/home/vscode/)
+- 🎉 新增 `SvgIcon` 支持本地 svg 图标使用
+- 🎉 新增 表单表格验证演示
+- 🎯 优化 全局主题(移除 success、info、warning、danger)
+- 🎯 优化 工作流(开源)
+- 🎯 优化 element plus svg 图标,`elementXXX` 改成 `ele-XXX`
+- 🌈 重构 深色模式
+- 🌹 合并 [处理 parent 的 h100 由于外层有 min-height 导致失效的问题](https://gitee.com/lyt-top/vue-next-admin/pulls/20),感谢@MaxNull、@21030442-mao
+- 🐞 修复 element plus 升级 `^1.3.0-beta.5` 后 组件 size 大小问题(大改:涉及布局、演示界面)
+- 🐞 修复 vs code 使用 Vue Language Features (Volar) 插件 代码报红问题(可以把公用的 ts 类型定义封装起来公用)
+
+## 1.2.2
+
+`2021.12.21`
+
+- 🌟 更新 依赖更新最新版本
+- 🎯 优化 iframes 滚动条问题
+- 🎯 优化 部署后每次都要强制刷新清浏览器缓存问题
+- 🎉 新增 工具类百分比验证演示
+- 🐞 修复 [tag-view 标签右键会超出浏览器 #I4KN78](https://gitee.com/lyt-top/vue-next-admin/issues/I4KN78)
+
+## 1.2.1
+
+`2021.12.12`
+
+- 🌟 更新 依赖更新最新版本
+- 🎯 优化 cropper 裁剪时卡顿问题 [#I4M2VQ](https://gitee.com/lyt-top/vue-next-admin/issues/I4M2VQ)
+- 🎯 优化 Wangeditor 富文本编辑器的问题 [#I4LPC1](https://gitee.com/lyt-top/vue-next-admin/issues/I4LPC1)、[#I4LM7I](https://gitee.com/lyt-top/vue-next-admin/issues/I4LM7I)
+- 🐞 修复 浏览器标题问题
+- 🐞 修复 element plus svg 图标引入
+- 🐞 修复 工作流不可以拖线连接问题
+
+## 1.2.0
+
+`2021.11.28`
+
+- 🌟 更新 依赖更新最新版本
+- 🎯 优化 深色模式
+- 🎯 优化 `/@/utils` 文件夹,合并删除单一内容
+- 🎯 优化 系统设置:菜单管理(新增、修改)、角色管理(新增菜单权限)、用户管理、部门管理、字典管理
+- 🎯 优化 登录界面逻辑、权限管理逻辑
+- 🎯 优化 同步 [vue-next-admin-images](https://gitee.com/lyt-top/vue-next-admin-images/tree/master/menu) 后端控制菜单模拟数据
+- 🎉 新增 适配 Font Icon 向 SVG Icon 迁移(改动大,"element-plus": "^1.2.0-beta.4" 谨慎更新)
+- 🐞 修复 热更新问题,感谢@甜蜜蜜
+- 🐞 修复 页面/element 字体图标演示
+- 🐞 修复 功能/图标选择器演示,新增高级功能 [issues #I4GJXQ](https://gitee.com/lyt-top/vue-next-admin/issues/I4GJXQ)
+
+## 1.1.2
+
+`2021.10.17`
+
+- 🌟 更新 依赖更新最新版本
+- 🐞 修复 开启全屏时,刷新界面被还原成未全屏的状态
+- 🎯 优化 tagsView 右键菜单关闭逻辑
+- 🎯 优化 wangeditor 富文本编辑器(增加双向绑定)
+- 🎉 新增 工作流(暂不开源)
+- 🎉 新增 基础版 ts(不带国际化),切换 `vue-next-admin-template` 分支
+
+## 1.1.1
+
+`2021.09.25`
+
+- 🌟 更新 依赖更新最新版本(`"element-plus": "^1.1.0-beta.13"` 版本运行错误,`^1.1.0-beta.16`修复横向菜单卡死问题)
+- 🐞 修复 Dialog 弹窗位置错误、Drawer 抽屉内边距、el-menu 菜单收起时背景色问题
+- 🎯 优化 锁屏界面自动锁屏(s/秒)必须设置至少 1 秒
+- 🎉 新增 分栏布局,鼠标移入当前项时,显示当前项菜单内容
+- 🎉 新增 工作流(未完成)
+
+## 1.1.0
+
+`2021.09.10`
+
+- 🌟 更新 依赖更新最新版本
+- 🎯 优化 小屏模式下登录页二维码遮挡标题问题
+- 🎉 新增 图片验证器
+- 🎉 新增 动态复杂表单
+- 🎉 新增 工作流(未完成)
+- 🎉 新增 深色主题(伪深色,样式变动大,谨慎更新)
+
+## 1.0.18
+
+`2021.08.29`
+
+- 🌟 更新 依赖更新最新版本
+- 🎯 优化 权限组件去掉顶级 div(`/src/components/auth`)
+- 🎉 新增 布局配置添加恢复默认按钮
+- 🐞 修复 升级 <a href="https://element-plus.gitee.io/#/zh-CN/component/changelog" target="_blank">element plus 1.1.0-beta.7</a>后项目无法启动、el-menu 菜单
+- 🐞 修复 表格固定列时的层级、设置了相对定位时,遮挡左侧导航菜单问题
+
+## 1.0.17
+
+`2021.08.22`
+
+- 🌟 更新 依赖更新最新版本
+- 🎯 优化 去除设置布局切换,重置主题样式(initSetLayoutChange),切换布局需手动设置样式,设置的样式自动同步各布局
+- 🎯 优化 Dropdown 下拉菜单用户账号靠边时换行问题
+- 🎯 优化 左侧导航菜单,共用菜单树,防止 `布局配置` 设置 `菜单 / 顶栏` 时,样式丢失等问题
+- 🐞 修复 固定 header 后没有回到顶部的 bug,拉取项目后运行不起来的 bug。<a href="https://gitee.com/lyt-top/vue-next-admin/pulls/14" target="_blank">!14</a>,感谢<a href="https://gitee.com/wjs0509" target="_blank">@wjs0509</a>
+- 🐞 修复 tagView 右键全屏后,浏览器窗口大小发生任何变化都会导致左边菜单显示出来,并且可点击打开对应页面。<a href="https://gitee.com/lyt-top/vue-next-admin/issues/I46E6T" target="_blank">I46E6T</a>
+- 🐞 修复 默认设置 `菜单 / 顶栏` 样式不生效问题(/@/src/store/modules/themeConfig.ts)
+
+## 1.0.16
+
+`2021.08.14`
+
+- 🌟 更新 依赖更新最新版本
+- 🎯 优化 菜单高亮(详情且详情设置了 meta.isHide 时,顶级菜单高亮),感谢群友@YourObject
+- 🎯 优化 详情路径写法:如父级(/pages/filtering),那么详情为(/pages/filtering/details?id=1)。这样写可实现(详情时,父级菜单高亮),否则写成(/pages/filteringDetails?id=1)顶级菜单将不会高亮。可参考:`页面/过滤筛选组件`,点击当前图片进行测试
+- 🎯 优化 tagsView 右键菜单全屏时,打开的界面高度问题
+- 🎯 优化 图表批量 resize 问题
+- 🐞 修复 菜单收起时(设置全局主题:primary 且有二级菜单时),文字高亮颜色不对
+- 🐞 修复 国际化 <a href="https://gitee.com/lyt-top/vue-next-admin/issues/I43NPE" target="_blank">#I43NPE</a>。可参考:`页面/过滤筛选组件`,点击顶部语言切换,进行底部分页国际化查看
+
+## 1.0.15
+
+`2021.08.06`
+
+- 🌟 更新 依赖更新最新版本
+- 🎯 优化 tagsView 右键菜单点击时的字段名(id 已修改成 contextMenuClickId)与路由中返回的 id 名冲突问题,感谢群友@伯牙已遇钟子期
+- 🎉 新增 多个 form 表单验证界面演示
+
+## 1.0.14
+
+`2021.07.29`
+
+- 🌟 更新 依赖更新最新版本(vue、vuex、vue-router),出现问题,请手动降级。版本查看:<a href="https://www.npmjs.com/" target="_blank">vnpm</a>
+- 🎯 优化 数据可视化图表演示加载卡顿问题、优化有图表的演示界面
+- 🎯 优化 路由参数演示界面
+- 🎯 优化 tagsView 操作演示界面,由于存在相同路由多标签,必须要传全部参数值(query 或者 params)
+- 🎉 新增 开启 TagsView 共用,开启时:(多个路由菜单共用一个详情组件(参数为后点击的覆盖前面点击的),tagsView 中只会出现一个(不支持同时出现多个 tagsView 标签))。关闭时:(多个路由菜单共用一个详情组件,参数不同,会同时出现多个 tagsView 标签)
+- 🐞 修复 tagsView 共用(单标签)时,右键菜单功能点击,参数不对的问题(第 2n+个参数未覆盖第一个参数值)
+- 🐞 修复 多 tagsView 标签(参数不同)、单个 tagsView 标签公用(参数不同)所带来的刷新功能、横向自动滚动等问题
+- 🐞 修复 处理全屏若干问题,<a href="https://gitee.com/lyt-top/vue-next-admin/pulls/12" target="_blank">pr!12</a>,感谢群友@另一个前端
+
+## 1.0.13
+
+`2021.07.25`
+
+- 🌟 更新 依赖更新最新版本
+- 🎉 新增 数据可视化演示界面(/visualizingDemo1、/visualizingDemo2)
+- 🎉 新增 登录页扫码登录
+
+## 1.0.12
+
+`2021.07.16`
+
+- 🌟 更新 依赖更新最新版本
+- 🎉 新增 数据可视化演示空界面(待完善)
+- 🎯 优化 tagsView 动态路由(xxx/:id/:name)时的右键菜单刷新、关闭其它时参数丢失问题(2021.07.15 优化)
+- 🐞 修复 路由带参数时,复制路径到登录页,跳转后参数消失的问题
+- 🐞 修复 设置多个外链,点击后,页面内容停留在上一个内容(内容未改变)、国际化处理、打开新窗口 sessionStorage 共享等
+
+## 1.0.11
+
+`2021.07.14`
+
+- 🌟 更新 依赖更新最新版本
+- 🎉 新增 路由参数、图片懒加载界面演示
+- ⚠️ 警告 Form 表单 `binding value must be a string or number`,解决:加上 `label-position="top"` 不报警告(等待官方修复)
+- 🎯 优化 锁屏界面动画效果、首页图表显示
+- 🎯 优化 tagsView 右键菜单 `关闭` 功能逻辑
+- 🐞 修复 开启 TagsView 拖拽报错及小于 `1000px` 时自动设置禁止拖拽(<a href="https://gitee.com/lyt-top/vue-next-admin/issues/I3ZRRI" target="_blank">#I3ZRRI</a>)
+- 🐞 修复 `iframe 内嵌、外链` 高度问题,使用 computed 进行计算
+- 🐞 修复 默认布局开启 `侧边栏 Logo` 与关闭 `菜单水平折叠`,切换到横向布局时,菜单看不见的问题
+- 🐞 修复 切换不同布局时,再去开启 `经典布局分割菜单` 功能不生效问题
+- 🐞 修复 浏览器窗口标题中/英文切换不实时生效的问题
+- 🐞 修复 切换布局时,某些功能不可以使用。部分界面不需要取消事件监听(proxy.mittBus.off('xxx'))
+- 🐞 修复 动态路由带参数,router-link 跳转问题(<a href="hhttps://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G" target="_blank">#I3YX6G</a>)
+- 🐞 修复 横向菜单有二级菜单时,点击子级菜单不高亮问题
+- 🐞 修复 功能 tagsView 操作演示不生效
+
+## 1.0.10
+
+`2021.07.07`
+
+- 🌟 更新 依赖更新最新版本(字体图标无问题)
+- 🎯 优化 内嵌 iframe、外链,解决 tagsView 刷新问题
+
+## 1.0.9
+
+`2021.07.02`
+
+- 🌟 更新 依赖更新最新版本
+- 🎯 优化 图标选择器设置宽度、v-model 等问题
+- 🎯 优化 滚动通知栏在手机上的体验
+- 🎯 优化 系统管理/新增菜单(编辑菜单),使用 `图标选择器` 进行模拟
+- 🎯 优化 字体图标(自动载入) 逻辑
+- 🐞 修复 screenfull 全屏时,按键盘 esc 键图标不改变问题,感谢群友@伯牙已遇钟子期
+
+## 1.0.8
+
+`2021.06.29`
+
+- 🌟 更新 依赖更新最新版本
+- 🎉 新增 表单中英文切换演示
+- 🎯 优化 登录页查看密码 icon 图标
+- 🎯 优化 图标选择器
+- 🎯 优化 拖动指令
+- 🐞 修复 form 表单在页面小于 576px 时的排版问题
+
+## 1.0.7
+
+`2021.06.24`
+
+- 🌟 更新 依赖更新最新版本
+- 🎉 新增 拖动指令及其演示界面
+- 🎯 优化 锁屏界面,解锁提示
+- 🎯 优化 登录页在手机上显示的效果
+
+## 1.0.6
+
+`2021.06.23`
+
+- 🎯 优化 去掉内嵌 iframe 内边距(padding)
+- 🎯 优化 城市多级联动组件
+- 🎯 优化 Tree 树形控件改成表格组件
+- 🐞 修复 Cascader 级联选择器高度问题
+
+## 1.0.5
+
+`2021.06.22`
+
+- 🌟 更新 vite 降级为@vite2.3.7,降级方法 `cnpm install vite@2.3.7`,防止 element plus 字体图标消失
+- 🐞 修复 开启后端控制路由(isRequestRoutes = true)时,内嵌 iframe、外链不可使用的问题
+
+## 1.0.4
+
+`2021.06.19`
+
+- 🌟 更新 依赖更新最新版本("vite": "^2.3.7")热更新无问题
+- 🎉 新增 深克隆工具,方便开发,感谢<a href="https://gitee.com/kangert" target="_blank">@kangert</a>(<a href="https://gitee.com/lyt-top/vue-next-admin/pulls/6" target="_blank">#6</a>)
+- 🎯 优化 vuex 模块自动导入。感谢<a href="https://gitee.com/kangert" target="_blank">@kangert</a>(<a href="https://gitee.com/lyt-top/vue-next-admin/pulls/4" target="_blank">#4</a>),感谢群友@web 小学生-第五君
+- 🎯 优化 类型定义提高编码体验,修复不能将类型“string | undefined”分配给类型“string”的问题。感谢<a href="https://gitee.com/kangert" target="_blank">@kangert</a>(<a href="https://gitee.com/lyt-top/vue-next-admin/pulls/5" target="_blank">#5</a>)
+- 🎯 优化 `layout` 文件夹移动到与 `views` 文件夹同级(改动较大,`/@/views/layout` 变成 `/@/layout`)
+- 🎯 优化 页面有 `console.log` 时 `eslint` 不生效问题
+- 🎯 优化 页面、ts 中 `any` 类型问题(改动较大)
+- 🎯 优化 登录页在手机上显示的效果
+- 🎯 优化 多行注释信息,鼠标放到方法名即可查看,更加直观的知道方法参数等。引入方法时需去掉以 `.ts` 结尾的后缀(改动较大)
+- 🎯 优化 移除 `utils/storage.ts` 下的旧写法(改动较大)
+- 🎯 优化 拆分 `router` 下内容,路由、前端、后端控制分开写,方便理解
+- 🐞 修复 鼠标移入顶部用户信息栏 `开/关全屏` 文字反向问题
+- 🐞 修复 热更新时,NextLoading(界面 loading) 不消失问题 `window.nextLoading === undefined`
+- 🐞 修复 vuex 中不可以使用 `/@/api/xxx` 下的接口调用问题
+
+## 1.0.3
+
+`2021.06.02`
+
+- ❄️ 删除 G6 思维导图界面
+- 🌟 更新 手动更新 vue、vue-router、vuex 到最近最多人使用的版本,出现不可预测的问题请降低版本。版本查看:<a href="https://www.npmjs.com/package/vue" target="_blank">vue 版本查看</a>
+- 🐞 修复 开启后端控制路由 `isRequestRoutes` 在非首页刷新页面后,回到首页的问题,感谢群友@伯牙已遇钟子期
+
+## 1.0.2
+
+`2021.06.01`
+
+- 🌟 更新 依赖更新最新版本
+- 🐞 修复 菜单搜索中文不可以搜索的问题,感谢群友@逍遥天意
+
+## 1.0.1
+
+`2021.05.31`
+
+- 🎉 新增 更新日志文件 `CHANGELOG.md`,以后每次更新都会在这里显示对应内容
+- 🌟 更新 依赖更新最新版本
+- 🐞 修复 分栏、经典布局路由设置 `meta.isHide` 为 `true` 时报错问题,感谢群友@29、@芭芭拉
+- 🐞 修复 经典布局点击 `tagsView` 左侧菜单数据不变问题

+ 21 - 0
nextadmin_vue3/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.

+ 135 - 0
nextadmin_vue3/README.md

@@ -0,0 +1,135 @@
+<div align="center">
+	<img src="https://gitee.com/lyt-top/vue-next-admin-images/raw/master/logo/logo-text.svg">
+	<p align="center">
+	    <a href="https://v3.vuejs.org/" target="_blank">
+	        <img src="https://img.shields.io/badge/vue.js-vue3.x-green" alt="vue">
+	    </a>
+	    <a href="https://element-plus.gitee.io/#/zh-CN/component/changelog" target="_blank">
+	        <img src="https://img.shields.io/badge/element--plus-%3E1.0.0-blue" alt="element plus">
+	    </a>
+		<a href="https://www.tslang.cn/" target="_blank">
+	        <img src="https://img.shields.io/badge/typescript-%3E4.0.0-blue" alt="typescript">
+	    </a>
+		<a href="https://vitejs.dev/" target="_blank">
+		    <img src="https://img.shields.io/badge/vite-%3E2.0.0-yellow" alt="vite">
+		</a>
+		<a href="https://gitee.com/lyt-top/vue-next-admin/blob/master/LICENSE" target="_blank">
+		    <img src="https://img.shields.io/badge/license-MIT-success" alt="license">
+		</a>
+	</p>
+	<p>&nbsp;</p>
+</div>
+
+#### 🌈 介绍
+
+基于 vue3.x + CompositionAPI + typescript + vite + element plus + vue-router-next + next.vuex,适配手机、平板、pc 的后台开源免费模板,希望减少工作量,帮助大家实现快速开发。
+
+#### ⛱️ 线上预览
+
+- vue3.x 版本预览(vue-next-admin)<a href="https://lyt-top.gitee.io/vue-next-admin-preview/#/login" target="_blank">https://lyt-top.gitee.io/vue-next-admin-preview/#/login</a>
+- vue2.x 版本预览(vue-prev-admin)<a href="https://lyt-top.gitee.io/vue-prev-admin-preview/#/login" target="_blank">https://lyt-top.gitee.io/vue-prev-admin-preview/#/login</a>
+
+#### 💒 代码仓库
+
+- vue3.x 版本 <a href="https://gitee.com/lyt-top/vue-next-admin" target="_blank">https://gitee.com/lyt-top/vue-next-admin</a>
+- vue2.x 版本 <a href="https://gitee.com/lyt-top/vue-next-admin/tree/vue-prev-admin" target="_blank">https://gitee.com/lyt-top/vue-next-admin/tree/vue-prev-admin</a>
+
+#### 🚧 安装 cnpm、yarn
+
+- 复制代码(桌面 cmd 运行) `npm install -g cnpm --registry=https://registry.npm.taobao.org`
+- 复制代码(桌面 cmd 运行) `npm install -g yarn`
+
+#### 🏭 环境支持
+
+| Edge                                                                     | last 2 versions                                                                   | last 2 versions                                                                | last 2 versions                                                                |
+| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
+| ![Edge](https://cdn.jsdelivr.net/npm/@browser-logos/edge/edge_32x32.png) | ![Firefox](https://cdn.jsdelivr.net/npm/@browser-logos/firefox/firefox_32x32.png) | ![Chrome](https://cdn.jsdelivr.net/npm/@browser-logos/chrome/chrome_32x32.png) | ![Safari](https://cdn.jsdelivr.net/npm/@browser-logos/safari/safari_32x32.png) |
+
+> 由于 Vue3 不再支持 IE11,故而 ElementPlus 也不支持 IE11 及之前版本。
+
+#### ⚡ 使用说明
+
+建议使用 cnpm,因为 yarn 有时会报错。<a href="http://nodejs.cn/" target="_blank">node 版本 > 12xx.xx.x</a>
+
+```bash
+# 克隆项目
+git clone https://gitee.com/lyt-top/vue-next-admin.git
+
+# 进入项目
+cd vue-next-admin
+
+# 安装依赖
+cnpm install
+
+# 运行项目
+cnpm run dev
+
+# 打包发布
+cnpm run build
+```
+
+#### 💯 学习交流加 QQ 群
+
+- 若加群了没同意(一般秒过),那就是群满了(500 人群),请换一个群试试,3 群未满
+- 查看开发文档:<a href="https://lyt-top.gitee.io/vue-next-admin-doc-preview" target="_blank">vue-next-admin-doc</a>
+- 群号码:
+  1 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=RdUY97Vx0T0vZ_1OOu-X1yFNkWgDwbjC&jump_from=webapi">665452019</a>
+  2 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=zVfy3gNy7pNWVK3kMduDzwU369PZg2fw&jump_from=webapi">766356862</a>
+  3 群:<a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=02EWb5P2JkP-8iwzaDadgFdxA0HSHPpn&jump_from=webapi">795345435</a>
+
+  <a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=RdUY97Vx0T0vZ_1OOu-X1yFNkWgDwbjC&jump_from=webapi">
+  	<img src="https://gitee.com/lyt-top/vue-next-admin-images/raw/master/user/qq1.png" width="220" height="220" alt="vue-next-admin 讨论群1" title="vue-next-admin 讨论群1"/>
+  </a>
+  <a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=zVfy3gNy7pNWVK3kMduDzwU369PZg2fw&jump_from=webapi">
+  	<img src="https://gitee.com/lyt-top/vue-next-admin-images/raw/master/user/qq2.png" width="220" height="220" alt="vue-next-admin 讨论群2" title="vue-next-admin 讨论群2"/>
+  </a>
+  <a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=02EWb5P2JkP-8iwzaDadgFdxA0HSHPpn&jump_from=webapi">
+  	<img src="https://gitee.com/lyt-top/vue-next-admin-images/raw/master/user/qq3.png" width="220" height="220" alt="vue-next-admin 讨论群3" title="vue-next-admin 讨论群3"/>
+  </a>
+
+#### 💒 集成后端
+
+- <a target="_blank" href="https://github.com/PandaGoAdmin/PandaX">@熊猫 PandaGoAdmin</a>
+- <a target="_blank" href="https://www.gnet.top/public">@甜蜜蜜 GoPro 平台</a>
+
+#### ❤️ 鸣谢列表
+
+- <a href="https://github.com/vuejs/vue" target="_blank">vue</a>
+- <a href="https://github.com/vuejs/vue-next" target="_blank">vue-next</a>
+- <a href="https://github.com/ElemeFE/element" target="_blank">element-ui</a>
+- <a href="https://github.com/element-plus/element-plus" target="_blank">element-plus</a>
+- <a href="https://github.com/vuejs/vue-router-next" target="_blank">vue-router-next</a>
+- <a href="https://github.com/vuejs/vuex" target="_blank">vuex</a>
+- <a href="https://github.com/apache/echarts" target="_blank">echarts</a>
+- <a href="https://github.com/axios/axios" target="_blank">axios</a>
+- <a href="https://github.com/zenorocha/clipboard.js" target="_blank">clipboard</a>
+- <a href="https://github.com/inorganik/countUp.js" target="_blank">countUp</a>
+- <a href="https://github.com/developit/mitt" target="_blank">mitt</a>
+- <a href="https://github.com/rstacruz/nprogress" target="_blank">nprogress</a>
+- <a href="https://github.com/sindresorhus/screenfull.js" target="_blank">screenfull</a>
+- <a href="https://github.com/SortableJS/Sortable" target="_blank">sortablejs</a>
+- <a href="https://github.com/sass/sass" target="_blank">sass</a>
+- <a href="https://github.com/microsoft/TypeScript" target="_blank">typescript</a>
+- <a href="https://github.com/vitejs/vite" target="_blank">vite</a>
+- <a href="https://github.com/wangeditor-team/wangEditor" target="_blank">wangeditor</a>
+- <a href="https://github.com/fengyuanchen/cropperjs" target="_blank">cropperjs</a>
+- <a href="https://github.com/davidshimjs/qrcodejs" target="_blank">qrcodejs</a>
+- <a href="https://github.com/crabbly/Print.js" target="_blank">print-js</a>
+- <a href="https://github.com/likaia/screen-shot" target="_blank">vue-web-screen-shot</a>
+- <a href="https://github.com/jbaysolutions/vue-grid-layout" target="_blank">vue-grid-layout</a>
+- <a href="https://github.com/antoniandre/splitpanes" target="_blank">splitpanes</a>
+- <a href="https://github.com/yimijianfang/vue-drag-verify" target="_blank">vue-drag-verify</a>
+- <a href="https://github.com/jsplumb/jsplumb" target="_blank">jsplumb</a>
+
+#### 💕 特别感谢
+
+特别感谢老哥们的建议、指导与帮忙。谢谢!
+
+- <a href="https://gitee.com/click33/sa-plus" target="_blank">@省长</a>
+- <a href="https://gitee.com/jskz/Jskz-SpringCloud" target="_blank">@唐参
+- <a href="https://gitee.com/chuange" target="_blank">@川歌</a>
+
+#### 💌 支持作者
+
+如果觉得框架不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/lyt-Top/vue-next-admin">Github</a> 或者
+<a target="_blank" href="https://gitee.com/lyt-top/vue-next-admin">Gitee</a> 帮我点个 ⭐ Star,这将是对我极大的鼓励与支持。

+ 1 - 0
nextadmin_vue3/Z_START.bat

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

+ 31 - 0
nextadmin_vue3/index.html

@@ -0,0 +1,31 @@
+<!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>
+
+</body>
+
+</html>

+ 84 - 0
nextadmin_vue3/package.json

@@ -0,0 +1,84 @@
+{
+	"name": "office_kpi_platform",
+	"version": "2.0.0",
+	"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 ",
+		"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
+	},
+	"dependencies": {
+		"@element-plus/icons-vue": "^0.2.7",
+		"await-to-js": "^3.0.0",
+		"axios": "^0.26.0",
+		"countup.js": "^2.0.8",
+		"cropperjs": "^1.5.12",
+		"echarts": "^5.3.0",
+		"echarts-gl": "^2.0.8",
+		"echarts-wordcloud": "^2.0.0",
+		"element-plus": "^2.0.2",
+		"js-md5": "^0.7.3",
+		"jsplumb": "^2.15.6",
+		"mitt": "^3.0.0",
+		"nprogress": "^0.2.0",
+		"print-js": "^1.6.0",
+		"qrcodejs2-fixes": "^0.0.2",
+		"screenfull": "^6.0.1",
+		"sortablejs": "^1.14.0",
+		"splitpanes": "^3.0.6",
+		"vue": "^3.2.31",
+		"vue-clipboard3": "^1.0.1",
+		"vue-grid-layout": "^3.0.0-beta1",
+		"vue-i18n": "^9.1.9",
+		"vue-layer": "^1.2.5",
+		"vue-router": "^4.0.12",
+		"vuex": "^4.0.2",
+		"wangeditor": "^4.7.11"
+	},
+	"devDependencies": {
+		"@types/node": "^17.0.19",
+		"@types/nprogress": "^0.2.0",
+		"@types/sortablejs": "^1.10.7",
+		"@typescript-eslint/eslint-plugin": "^5.12.0",
+		"@typescript-eslint/parser": "^5.12.0",
+		"@vitejs/plugin-vue": "^2.2.2",
+		"@vue/compiler-sfc": "^3.2.31",
+		"dotenv": "^16.0.0",
+		"eslint": "^8.9.0",
+		"eslint-plugin-vue": "^8.4.1",
+		"prettier": "^2.5.1",
+		"sass": "^1.49.8",
+		"sass-loader": "^12.6.0",
+		"typescript": "^4.5.5",
+		"vite": "^2.8.4",
+		"vue-eslint-parser": "^8.2.0"
+	},
+	"browserslist": [
+		"> 1%",
+		"last 2 versions",
+		"not dead"
+	],
+	"bugs": {
+		"url": "https://gitee.com/lyt-top/vue-next-admin/issues"
+	},
+	"engines": {
+		"node": ">=12.0.0",
+		"npm": ">= 6.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"
+	}
+}

+ 3 - 0
nextadmin_vue3/plugins.d.ts

@@ -0,0 +1,3 @@
+declare module 'vue-grid-layout';
+declare module 'qrcodejs2-fixes';
+declare module 'splitpanes';

二進制
nextadmin_vue3/public/favicon.ico


+ 13 - 0
nextadmin_vue3/shim.d.ts

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

+ 6 - 0
nextadmin_vue3/source.d.ts

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

+ 92 - 0
nextadmin_vue3/src/App.vue

@@ -0,0 +1,92 @@
+<template>
+	<el-config-provider :size="getGlobalComponentSize" :locale="i18nLocale">
+		<router-view v-show="getThemeConfig.lockScreenTime !== 0" />
+		<LockScreen v-if="getThemeConfig.isLockScreen" />
+		<Setings ref="setingsRef" v-show="getThemeConfig.lockScreenTime !== 0" />
+		<CloseFull />
+	</el-config-provider>
+</template>
+
+<script lang="ts">
+import { computed, ref, getCurrentInstance, onBeforeMount, onMounted, onUnmounted, nextTick, defineComponent, watch, reactive, toRefs } from 'vue';
+import { useRoute } from 'vue-router';
+import { useStore } from '/@/store/index';
+import other from '/@/utils/other';
+import { Local, Session } from '/@/utils/storage';
+import setIntroduction from '/@/utils/setIconfont';
+import LockScreen from '/@/layout/lockScreen/index.vue';
+import Setings from '/@/layout/navBars/breadcrumb/setings.vue';
+import CloseFull from '/@/layout/navBars/breadcrumb/closeFull.vue';
+export default defineComponent({
+	name: 'app',
+	components: { LockScreen, Setings, CloseFull },
+	setup() {
+		const { proxy } = <any>getCurrentInstance();
+		const setingsRef = ref();
+		const route = useRoute();
+		const store = useStore();
+		const state = reactive({
+			i18nLocale: null,
+		});
+		// 获取布局配置信息
+		const getThemeConfig = computed(() => {
+			return store.state.themeConfig.themeConfig;
+		});
+		// 获取全局组件大小
+		const getGlobalComponentSize = computed(() => {
+			return other.globalComponentSize;
+		});
+		// 布局配置弹窗打开
+		const openSetingsDrawer = () => {
+			setingsRef.value.openDrawer();
+		};
+		// 设置初始化,防止刷新时恢复默认
+		onBeforeMount(() => {
+			// 设置批量第三方 icon 图标
+			setIntroduction.cssCdn();
+			// 设置批量第三方 js
+			setIntroduction.jsCdn();
+		});
+		// 页面加载时
+		onMounted(() => {
+			nextTick(() => {
+				// 监听布局配置弹窗点击打开
+				proxy.mittBus.on('openSetingsDrawer', () => {
+					openSetingsDrawer();
+				});
+				// 设置 i18n,App.vue 中的 el-config-provider
+				proxy.mittBus.on('getI18nConfig', (locale: string) => {
+					(state.i18nLocale as string | null) = locale;
+				});
+				// 获取缓存中的布局配置
+				if (Local.get('themeConfig')) {
+					store.dispatch('themeConfig/setThemeConfig', Local.get('themeConfig'));
+					document.documentElement.style.cssText = Local.get('themeConfigStyle');
+				}
+				// 获取缓存中的全屏配置
+				if (Session.get('isTagsViewCurrenFull')) {
+					store.dispatch('tagsViewRoutes/setCurrenFullscreen', Session.get('isTagsViewCurrenFull'));
+				}
+			});
+		});
+		// 页面销毁时,关闭监听布局配置/i18n监听
+		onUnmounted(() => {
+			proxy.mittBus.off('openSetingsDrawer', () => {});
+			proxy.mittBus.off('getI18nConfig', () => {});
+		});
+		// 监听路由的变化,设置网站标题
+		watch(
+			() => route.path,
+			() => {
+				other.useTitle();
+			}
+		);
+		return {
+			setingsRef,
+			getThemeConfig,
+			getGlobalComponentSize,
+			...toRefs(state),
+		};
+	},
+});
+</script>

+ 80 - 0
nextadmin_vue3/src/api/Company.ts

@@ -0,0 +1,80 @@
+import Http from "../utils/net/Http";
+import Urls from "./Urls";
+const tagInfo = "[/src/api/Company.ts]:";
+/**
+ * 企业模型
+ */
+let Company = {
+	/**
+	 * 添加企业
+	 * @returns
+	 */
+	async add(param: object = {}): Promise<any> {
+		let url = Urls.company.addEnterPrise;
+		let res = await Http.post(url, param);
+		return res;
+	},
+	async edit(param: object = {}): Promise<any> {
+		let url = Urls.company.editEnterPrise;
+		let res = await Http.post(url, param);
+		return res;
+	},
+	/**
+	 * 获取企业列表
+	 * @returns
+	 */
+	async getEnterpriseList(keyword: string = "", page: number = 1, pageSize: number = 10): Promise<any> {
+		let url = Urls.company.getEnterprise;
+		let param = {
+			keyword,
+			page,
+			pageSize
+		};
+		let res = await Http.get(url, param);
+		return res;
+	},
+	/**
+	 * 获取企业详情
+	 * @param id 企业id
+	 * @returns
+	 */
+	async getEnterpriseDetail(id: number | string): Promise<any> {
+		let url = Urls.company.getEnterpriseDetail;
+		let param = {
+			enterprise_id: id
+		};
+		let res = await Http.get(url, param);
+		if (res.code != 0) {
+			return res;
+		}
+		let photoList: Array<any> = [];
+		if (res.data.enterprise.photo) {
+			//如果有相册
+			let photoListOrg = res.data.enterprise.photo.split(",");
+			//console.log(tagInfo, "photoListOrg", photoListOrg);
+			photoListOrg.forEach((item: string) => {
+				let obj = {
+					url: item
+				};
+				photoList.push(obj);
+			});
+		}
+		res.data.enterprise.photo_list = photoList;
+		//console.log(tagInfo, "getEnterpriseDetail res.data.enterprise.photo_list", res.data.enterprise, res.data.enterprise.photo_list);
+		return res;
+	},
+	/**
+	 * 删除企业
+	 * @param ids  	删除的企业id集合以逗号隔开
+	 * @returns
+	 */
+	async delEnterPrise(ids: Array<number | string>): Promise<any> {
+		let url = Urls.company.delEnterPrise;
+		let param = {
+			ids
+		};
+		let res = await Http.get(url, param);
+		return res;
+	}
+};
+export default Company;

+ 10 - 0
nextadmin_vue3/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;

+ 36 - 0
nextadmin_vue3/src/api/Protocol.ts

@@ -0,0 +1,36 @@
+import Http from "../utils/net/Http";
+import Urls from "./Urls";
+const tagInfo = "[/src/api/Protocol.ts]:";
+
+let Protocol = {
+	async list(keyword:string = '',page: number = 1, pageSize: number = 10){
+		let url = Urls.protocol.list;
+		let param = {
+			keyword,
+			page,
+			pageSize
+		};
+		let res = await Http.get(url, param);
+		return res;
+	},
+	async detail(id: number){
+		let url = Urls.protocol.detail;
+		let param = {
+			id
+		};
+		let res = await Http.get(url, param);
+		return res;
+	},
+	async add(param: object = {}): Promise<any> {
+		let url = Urls.protocol.add;
+		let res = await Http.post(url, param);
+		return res;
+	},
+	async edit(param: object = {}): Promise<any> {
+		let url = Urls.protocol.edit;
+		let res = await Http.post(url, param);
+		return res;
+	}
+	
+};
+export default Protocol;

+ 59 - 0
nextadmin_vue3/src/api/Urls.ts

@@ -0,0 +1,59 @@
+const Urls = {
+    // 列表
+    index: {
+        login: "/api/index/login"
+    },
+    common: {
+        menuList: "/api/menu/getListPlatform"
+    },
+    /**
+     * 上传类
+     */
+    upload: {
+        file: "/api/upload/file"
+    },
+    /**
+     * 企业模块
+     */
+    company: {
+        /**
+         * 添加企业
+         */
+        addEnterPrise: "/api/enterprise/addEnterPrise",
+        /**
+         * 获取企业列表
+         */
+        getEnterprise: "/api/index/getEnterprise",
+        /**
+         * 企业详情
+         */
+        getEnterpriseDetail: "/api/Enterprise/getEnterpriseDetail",
+        /**
+         * 编辑企业
+         */
+        editEnterPrise: "/api/enterprise/editEnterPrise",
+        /**
+         * 删除企业
+         */
+        delEnterPrise: "/api/enterprise/delEnterPrise"
+    },
+    user: {
+        list: "/api/user/getUserList",//客户,员工,管理员列表
+        add: "/api/user/addUser", //添加用户
+        detail: "/api/user/getUserDetail",//客户,员工,管理员详情
+        edit: "/api/user/editUser",//编辑员工,客户
+        staffList: "/api/staff/getList",
+
+    },
+	
+	/**
+	 * 企业模块
+	 */
+	protocol: {
+	    list: "/api/protocol/getList",
+	    add: "/api/protocol/addProtocol",
+	    edit: "/api/protocol/editProtocol",
+		detail: "/api/protocol/getDetail",
+	},
+};
+export default Urls;

+ 63 - 0
nextadmin_vue3/src/api/User.ts

@@ -0,0 +1,63 @@
+import Http from "../utils/net/Http";
+import Urls from "./Urls";
+const User = {
+    /**
+     * 用户登录
+     * @param phone 
+     * @param password 
+     * @returns 
+     */
+    async login(phone: string, password: string): Promise<any> {
+        let url = Urls.index.login;
+        let res = await Http.get(url, { phone, password });
+        return res;
+    },
+    /**
+     * 添加用户
+     * @params params 
+     * @returns 
+     */
+    async add(params: any): Promise<any> {
+        let url = Urls.user.add;
+        let res = await Http.post(url, params);
+        return res;
+    },
+    /**
+     * 用户详情
+     */
+    async detail(id: number): Promise<any> {
+        let url = Urls.user.detail;
+        let params = {
+            id
+        };
+        let res = await Http.get(url, params);
+        return res;
+    },
+    /**
+     * 用户编辑
+     * @param params 
+     * @returns 
+     */
+    async edit(params: any): Promise<any> {
+        let url = Urls.user.edit;
+        let res = await Http.post(url, params);
+        return res;
+    },
+    /**
+     * 用户列表
+     * @param params 
+     * @returns 
+     */
+    async list(params: any): Promise<any> {
+        let url = Urls.user.list;
+        let res = await Http.get(url, params);
+        return res;
+    },
+    async staffList(params: any): Promise<any> {
+        let url = Urls.user.staffList;
+        let res = await Http.get(url, params);
+        return res;
+    }
+
+};
+export default User;

+ 25 - 0
nextadmin_vue3/src/api/login/index.ts

@@ -0,0 +1,25 @@
+import request from '/@/utils/request';
+
+/**
+ * 登录api接口集合
+ * @method signIn 用户登录
+ * @method signOut 用户退出登录
+ */
+export function useLoginApi() {
+	return {
+		signIn: (params: object) => {
+			return request({
+				url: '/user/signIn',
+				method: 'post',
+				data: params,
+			});
+		},
+		signOut: (params: object) => {
+			return request({
+				url: '/user/signOut',
+				method: 'post',
+				data: params,
+			});
+		},
+	};
+}

+ 31 - 0
nextadmin_vue3/src/api/menu/index.ts

@@ -0,0 +1,31 @@
+import request from '/@/utils/request';
+import Http from '/@/utils/net/Http';
+import Menu from '../Menu';
+/**
+ * 后端控制菜单模拟json,路径在 https://gitee.com/lyt-top/vue-next-admin-images/tree/master/menu
+ * 后端控制路由,isRequestRoutes 为 true,则开启后端控制路由
+ * @method getMenuAdmin 获取后端动态路由菜单(admin)
+ * @method getMenuTest 获取后端动态路由菜单(test)
+ */
+export function useMenuApi() {
+	return {
+		getMenuAdmin: async (params?: object) => {
+			//TODO: 要改成远程获取		
+			let res: any = await Menu.getList();
+			let menu = {
+				code: 0,
+				type: "adminMenu",
+				data: res.data.menu
+			}
+			console.log("menu result", menu);
+			return menu;
+		},
+		getMenuTest: (params?: object) => {
+			return request({
+				url: '/gitee/lyt-top/vue-next-admin-images/raw/master/menu/testMenu.json',
+				method: 'get',
+				params,
+			});
+		},
+	};
+}

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


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


+ 27 - 0
nextadmin_vue3/src/components/auth/auth.vue

@@ -0,0 +1,27 @@
+<template>
+	<slot v-if="getUserAuthBtnList" />
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import { useStore } from '/@/store/index';
+export default defineComponent({
+	name: 'auth',
+	props: {
+		value: {
+			type: String,
+			default: () => '',
+		},
+	},
+	setup(props) {
+		const store = useStore();
+		// 获取 vuex 中的用户权限
+		const getUserAuthBtnList = computed(() => {
+			return store.state.userInfos.userInfos.authBtnList.some((v: string) => v === props.value);
+		});
+		return {
+			getUserAuthBtnList,
+		};
+	},
+});
+</script>

+ 28 - 0
nextadmin_vue3/src/components/auth/authAll.vue

@@ -0,0 +1,28 @@
+<template>
+	<slot v-if="getUserAuthBtnList" />
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import { useStore } from '/@/store/index';
+import { judementSameArr } from '/@/utils/arrayOperation';
+export default defineComponent({
+	name: 'authAll',
+	props: {
+		value: {
+			type: Array,
+			default: () => [],
+		},
+	},
+	setup(props) {
+		const store = useStore();
+		// 获取 vuex 中的用户权限
+		const getUserAuthBtnList = computed(() => {
+			return judementSameArr(props.value, store.state.userInfos.userInfos.authBtnList);
+		});
+		return {
+			getUserAuthBtnList,
+		};
+	},
+});
+</script>

+ 33 - 0
nextadmin_vue3/src/components/auth/auths.vue

@@ -0,0 +1,33 @@
+<template>
+	<slot v-if="getUserAuthBtnList" />
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import { useStore } from '/@/store/index';
+export default defineComponent({
+	name: 'auths',
+	props: {
+		value: {
+			type: Array,
+			default: () => [],
+		},
+	},
+	setup(props) {
+		const store = useStore();
+		// 获取 vuex 中的用户权限
+		const getUserAuthBtnList = computed(() => {
+			let flag = false;
+			store.state.userInfos.userInfos.authBtnList.map((val: string) => {
+				props.value.map((v) => {
+					if (val === v) flag = true;
+				});
+			});
+			return flag;
+		});
+		return {
+			getUserAuthBtnList,
+		};
+	},
+});
+</script>

+ 148 - 0
nextadmin_vue3/src/components/cropper/index.vue

@@ -0,0 +1,148 @@
+<template>
+	<div>
+		<el-dialog title="更换头像" v-model="isShowDialog" width="769px">
+			<div class="cropper-warp">
+				<div class="cropper-warp-left">
+					<img :src="cropperImg" class="cropper-warp-left-img" />
+				</div>
+				<div class="cropper-warp-right">
+					<div class="cropper-warp-right-title">预览</div>
+					<div class="cropper-warp-right-item">
+						<div class="cropper-warp-right-value">
+							<img :src="cropperImgBase64" class="cropper-warp-right-value-img" />
+						</div>
+						<div class="cropper-warp-right-label">100 x 100</div>
+					</div>
+					<div class="cropper-warp-right-item">
+						<div class="cropper-warp-right-value">
+							<img :src="cropperImgBase64" class="cropper-warp-right-value-img cropper-size" />
+						</div>
+						<div class="cropper-warp-right-label">50 x 50</div>
+					</div>
+				</div>
+			</div>
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button @click="onCancel"  >取 消</el-button>
+					<el-button type="primary" @click="onSubmit"  >更 换</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script lang="ts">
+import { reactive, toRefs, nextTick, defineComponent } from 'vue';
+import Cropper from 'cropperjs';
+import 'cropperjs/dist/cropper.css';
+export default defineComponent({
+	name: 'cropperIndex',
+	setup() {
+		const state = reactive({
+			isShowDialog: false,
+			cropperImg: '',
+			cropperImgBase64: '',
+			cropper: null,
+		});
+		// 打开弹窗
+		const openDialog = (imgs: any) => {
+			state.cropperImg = imgs;
+			state.isShowDialog = true;
+			nextTick(() => {
+				initCropper();
+			});
+		};
+		// 关闭弹窗
+		const closeDialog = () => {
+			state.isShowDialog = false;
+		};
+		// 取消
+		const onCancel = () => {
+			closeDialog();
+		};
+		// 更换
+		const onSubmit = () => {
+			// state.cropperImgBase64 = state.cropper.getCroppedCanvas().toDataURL('image/jpeg');
+		};
+		// 初始化cropperjs图片裁剪
+		const initCropper = () => {
+			const letImg: any = document.querySelector('.cropper-warp-left-img');
+			(<any>state.cropper) = new Cropper(letImg, {
+				viewMode: 1,
+				dragMode: 'none',
+				initialAspectRatio: 1,
+				aspectRatio: 1,
+				preview: '.before',
+				background: false,
+				autoCropArea: 0.6,
+				zoomOnWheel: false,
+				crop: () => {
+					state.cropperImgBase64 = (<any>state.cropper).getCroppedCanvas().toDataURL('image/jpeg');
+				},
+			});
+		};
+		return {
+			openDialog,
+			closeDialog,
+			onCancel,
+			onSubmit,
+			initCropper,
+			...toRefs(state),
+		};
+	},
+});
+</script>
+
+<style scoped lang="scss">
+.cropper-warp {
+	display: flex;
+	.cropper-warp-left {
+		position: relative;
+		display: inline-block;
+		height: 350px;
+		flex: 1;
+		border: var(--el-border-base);
+		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>

+ 79 - 0
nextadmin_vue3/src/components/editor/index.vue

@@ -0,0 +1,79 @@
+<template>
+	<div class="editor-container">
+		<div :id="id"></div>
+	</div>
+</template>
+
+<script lang="ts">
+import { toRefs, reactive, onMounted, watch, defineComponent } from 'vue';
+import wangeditor from 'wangeditor';
+
+// 定义接口来定义对象的类型
+interface WangeditorState {
+	editor: any;
+}
+
+export default defineComponent({
+	name: 'wngEditor',
+	props: {
+		// 节点 id
+		id: {
+			type: String,
+			default: () => 'wangeditor',
+		},
+		// 是否禁用
+		isDisable: {
+			type: Boolean,
+			default: () => false,
+		},
+		// 内容框默认 placeholder
+		placeholder: {
+			type: String,
+			default: () => '请输入内容',
+		},
+		// 双向绑定
+		// 双向绑定值,字段名为固定,改了之后将不生效
+		// 参考:https://v3.cn.vuejs.org/guide/migration/v-model.html#%E8%BF%81%E7%A7%BB%E7%AD%96%E7%95%A5
+		modelValue: String,
+	},
+	setup(props, { emit }) {
+		const state = reactive<WangeditorState>({
+			editor: null,
+		});
+		// 初始化富文本
+		// https://doc.wangeditor.com/
+		const initWangeditor = () => {
+			state.editor = new wangeditor(`#${props.id}`);
+			state.editor.config.zIndex = 1;
+			state.editor.config.placeholder = props.placeholder;
+			state.editor.config.uploadImgShowBase64 = true;
+			state.editor.config.showLinkImg = false;
+			onWangeditorChange();
+			state.editor.create();
+			state.editor.txt.html(props.modelValue);
+			props.isDisable ? state.editor.disable() : state.editor.enable();
+		};
+		// 内容改变时
+		const onWangeditorChange = () => {
+			state.editor.config.onchange = (html: string) => {
+				emit('update:modelValue', html);
+			};
+		};
+		// 页面加载时
+		onMounted(() => {
+			initWangeditor();
+		});
+		// 监听双向绑定值的改变
+		// https://gitee.com/lyt-top/vue-next-admin/issues/I4LM7I
+		watch(
+			() => props.modelValue,
+			(value) => {
+				state.editor.txt.html(value);
+			}
+		);
+		return {
+			...toRefs(state),
+		};
+	},
+});
+</script>

+ 242 - 0
nextadmin_vue3/src/components/iconSelector/index.vue

@@ -0,0 +1,242 @@
+<template>
+	<div class="icon-selector">
+		<el-popover placement="bottom" :width="fontIconWidth" v-model:visible="fontIconVisible" popper-class="icon-selector-popper">
+			<template #reference>
+				<el-input
+					v-model="fontIconSearch"
+					:placeholder="fontIconPlaceholder"
+					:clearable="clearable"
+					:disabled="disabled"
+					:size="size"
+					ref="inputWidthRef"
+					@clear="onClearFontIcon"
+					@focus="onIconFocus"
+					@blur="onIconBlur"
+				>
+					<template #prepend>
+						<SvgIcon
+							:name="fontIconPrefix === '' ? prepend : fontIconPrefix"
+							class="font14"
+							v-if="fontIconPrefix === '' ? prepend?.indexOf('ele-') > -1 : fontIconPrefix?.indexOf('ele-') > -1"
+						/>
+						<i v-else :class="fontIconPrefix === '' ? prepend : fontIconPrefix" class="font14"></i>
+					</template>
+				</el-input>
+			</template>
+			<transition name="el-zoom-in-top">
+				<div class="icon-selector-warp" v-show="fontIconVisible">
+					<div class="icon-selector-warp-title flex">
+						<div class="flex-auto">{{ title }}</div>
+						<div class="icon-selector-warp-title-tab" v-if="type === 'all'">
+							<span :class="{ 'span-active': fontIconType === 'ali' }" @click="onIconChange('ali')" class="ml10" title="iconfont 图标">ali</span>
+							<span :class="{ 'span-active': fontIconType === 'ele' }" @click="onIconChange('ele')" class="ml10" title="elementPlus 图标">ele</span>
+							<span :class="{ 'span-active': fontIconType === 'awe' }" @click="onIconChange('awe')" class="ml10" title="fontawesome 图标">awe</span>
+						</div>
+					</div>
+					<div class="icon-selector-warp-row">
+						<el-scrollbar ref="selectorScrollbarRef">
+							<el-row :gutter="10" v-if="fontIconSheetsFilterList.length > 0">
+								<el-col :xs="6" :sm="4" :md="4" :lg="4" :xl="4" @click="onColClick(v)" v-for="(v, k) in fontIconSheetsFilterList" :key="k">
+									<div class="icon-selector-warp-item" :class="{ 'icon-selector-active': fontIconPrefix === v }">
+										<div class="flex-margin">
+											<div class="icon-selector-warp-item-value">
+												<SvgIcon :name="v" />
+											</div>
+										</div>
+									</div>
+								</el-col>
+							</el-row>
+							<el-empty :image-size="100" v-if="fontIconSheetsFilterList.length <= 0" :description="emptyDescription"></el-empty>
+						</el-scrollbar>
+					</div>
+				</div>
+			</transition>
+		</el-popover>
+	</div>
+</template>
+
+<script lang="ts">
+import { ref, toRefs, reactive, onMounted, nextTick, computed, watch, defineComponent } from 'vue';
+import initIconfont from '/@/utils/getStyleSheets';
+export default defineComponent({
+	name: 'iconSelector',
+	emits: ['update:modelValue', 'get', 'clear'],
+	props: {
+		// 输入框前置内容
+		prepend: {
+			type: String,
+			default: () => 'ele-Pointer',
+		},
+		// 输入框占位文本
+		placeholder: {
+			type: String,
+			default: () => '请输入内容搜索图标或者选择图标',
+		},
+		// 输入框占位文本
+		size: {
+			type: String,
+			default: () => 'default',
+		},
+		// 弹窗标题
+		title: {
+			type: String,
+			default: () => '请选择图标',
+		},
+		// icon 图标类型
+		type: {
+			type: String,
+			default: () => 'ele',
+		},
+		// 禁用
+		disabled: {
+			type: Boolean,
+			default: () => false,
+		},
+		// 是否可清空
+		clearable: {
+			type: Boolean,
+			default: () => true,
+		},
+		// 自定义空状态描述文字
+		emptyDescription: {
+			type: String,
+			default: () => '无相关图标',
+		},
+		// 双向绑定值,字段名为固定,改了之后将不生效
+		// 参考:https://v3.cn.vuejs.org/guide/migration/v-model.html#%E8%BF%81%E7%A7%BB%E7%AD%96%E7%95%A5
+		modelValue: String,
+	},
+	setup(props, { emit }) {
+		const inputWidthRef = ref();
+		const selectorScrollbarRef = ref();
+		const state = reactive({
+			fontIconPrefix: '',
+			fontIconVisible: false,
+			fontIconWidth: 0,
+			fontIconSearch: '',
+			fontIconTabsIndex: 0,
+			fontIconSheetsList: [],
+			fontIconPlaceholder: '',
+			fontIconType: 'ali',
+			fontIconShow: true,
+		});
+		// 处理 input 获取焦点时,modelValue 有值时,改变 input 的 placeholder 值
+		const onIconFocus = () => {
+			state.fontIconVisible = true;
+			if (!props.modelValue) return false;
+			state.fontIconSearch = '';
+			state.fontIconPlaceholder = props.modelValue;
+		};
+		// 处理 input 失去焦点时,为空将清空 input 值,为点击选中图标时,将取原先值
+		const onIconBlur = () => {
+			setTimeout(() => {
+				const icon = state.fontIconSheetsList.filter((icon: string) => icon === state.fontIconSearch);
+				if (icon.length <= 0) state.fontIconSearch = '';
+			}, 300);
+		};
+		// 处理 icon 双向绑定数值回显
+		const initModeValueEcho = () => {
+			if (props.modelValue === '') return false;
+			(<string | undefined>state.fontIconPlaceholder) = props.modelValue;
+			(<string | undefined>state.fontIconPrefix) = props.modelValue;
+		};
+		// 图标搜索及图标数据显示
+		const fontIconSheetsFilterList = computed(() => {
+			if (!state.fontIconSearch) return state.fontIconSheetsList;
+			let search = state.fontIconSearch.trim().toLowerCase();
+			return state.fontIconSheetsList.filter((item: any) => {
+				if (item.toLowerCase().indexOf(search) !== -1) return item;
+			});
+		});
+		// 获取 input 的宽度
+		const getInputWidth = () => {
+			nextTick(() => {
+				state.fontIconWidth = inputWidthRef.value.$el.offsetWidth;
+			});
+		};
+		// 监听页面宽度改变
+		const initResize = () => {
+			window.addEventListener('resize', () => {
+				getInputWidth();
+			});
+		};
+		// 初始化数据
+		const initFontIconData = async (type: string) => {
+			state.fontIconSheetsList = [];
+			if (type === 'ali') {
+				await initIconfont.ali().then((res: any) => {
+					// 阿里字体图标使用 `iconfont xxx`
+					state.fontIconSheetsList = res.map((i: string) => `iconfont ${i}`);
+				});
+			} else if (type === 'ele') {
+				await initIconfont.ele().then((res: any) => {
+					state.fontIconSheetsList = res;
+				});
+			} else if (type === 'awe') {
+				await initIconfont.awe().then((res: any) => {
+					// fontawesome字体图标使用 `fa xxx`
+					state.fontIconSheetsList = res.map((i: string) => `fa ${i}`);
+				});
+			}
+			// 初始化 input 的 placeholder
+			// 参考(单项数据流):https://cn.vuejs.org/v2/guide/components-props.html?#%E5%8D%95%E5%90%91%E6%95%B0%E6%8D%AE%E6%B5%81
+			state.fontIconPlaceholder = props.placeholder;
+			// 初始化双向绑定回显
+			initModeValueEcho();
+			// 切换时,滚动条置顶。感兴趣可以使用 keep-alive <component :is="xxx"/> 进行缓存
+			selectorScrollbarRef.value.wrap$.scrollTop = 0;
+		};
+		// 图标点击切换
+		const onIconChange = (type: string) => {
+			state.fontIconType = type;
+			initFontIconData(type);
+		};
+		// 获取当前点击的 icon 图标
+		const onColClick = (v: any) => {
+			state.fontIconPlaceholder = v;
+			state.fontIconVisible = false;
+			state.fontIconPrefix = v;
+			emit('get', state.fontIconPrefix);
+			emit('update:modelValue', state.fontIconPrefix);
+		};
+		// 清空当前点击的 icon 图标
+		const onClearFontIcon = () => {
+			state.fontIconPrefix = '';
+			emit('clear', state.fontIconPrefix);
+			emit('update:modelValue', state.fontIconPrefix);
+		};
+		// 页面加载时
+		onMounted(() => {
+			// 判断默认进来是什么类型图标,进行 tab 回显
+			if (props.type === 'all') {
+				if ((<any>props.modelValue)?.indexOf('iconfont') > -1) onIconChange('ali');
+				else if ((<any>props.modelValue)?.indexOf('ele-') > -1) onIconChange('ele');
+				else if ((<any>props.modelValue)?.indexOf('fa') > -1) onIconChange('awe');
+				else onIconChange('ali');
+			} else {
+				onIconChange(props.type);
+			}
+			initResize();
+			getInputWidth();
+		});
+		// 监听双向绑定 modelValue 的变化
+		watch(
+			() => props.modelValue,
+			() => {
+				initModeValueEcho();
+			}
+		);
+		return {
+			inputWidthRef,
+			selectorScrollbarRef,
+			fontIconSheetsFilterList,
+			onColClick,
+			onIconChange,
+			onClearFontIcon,
+			onIconFocus,
+			onIconBlur,
+			...toRefs(state),
+		};
+	},
+});
+</script>

+ 194 - 0
nextadmin_vue3/src/components/noticeBar/index.vue

@@ -0,0 +1,194 @@
+<template>
+	<div class="notice-bar" :style="{ background, height: `${height}px` }" v-show="!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 lang="ts">
+import { toRefs, reactive, defineComponent, ref, onMounted, nextTick } from 'vue';
+export default defineComponent({
+	name: 'noticeBar',
+	props: {
+		// 通知栏模式,可选值为 closeable link
+		mode: {
+			type: String,
+			default: () => '',
+		},
+		// 通知文本内容
+		text: {
+			type: String,
+			default: () => '',
+		},
+		// 通知文本颜色
+		color: {
+			type: String,
+			default: () => 'var(--color-warning)',
+		},
+		// 通知背景色
+		background: {
+			type: String,
+			default: () => 'var(--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: () => '',
+		},
+	},
+	setup(props, { emit }) {
+		const noticeBarWarpRef = ref();
+		const noticeBarTextRef = ref();
+		const state = reactive({
+			order: 1,
+			oneTime: 0,
+			twoTime: 0,
+			warpOWidth: 0,
+			textOWidth: 0,
+			isMode: false,
+		});
+		// 初始化 animation 各项参数
+		const initAnimation = () => {
+			nextTick(() => {
+				state.warpOWidth = noticeBarWarpRef.value.offsetWidth;
+				state.textOWidth = noticeBarTextRef.value.offsetWidth;
+				document.styleSheets[0].insertRule(`@keyframes oneAnimation {0% {left: 0px;} 100% {left: -${state.textOWidth}px;}}`);
+				document.styleSheets[0].insertRule(`@keyframes twoAnimation {0% {left: ${state.warpOWidth}px;} 100% {left: -${state.textOWidth}px;}}`);
+				computeAnimationTime();
+				setTimeout(() => {
+					changeAnimation();
+				}, props.delay * 1000);
+			});
+		};
+		// 计算 animation 滚动时长
+		const computeAnimationTime = () => {
+			state.oneTime = state.textOWidth / props.speed;
+			state.twoTime = (state.textOWidth + state.warpOWidth) / props.speed;
+		};
+		// 改变 animation 动画调用
+		const changeAnimation = () => {
+			if (state.order === 1) {
+				noticeBarTextRef.value.style.cssText = `animation: oneAnimation ${state.oneTime}s linear; opactity: 1;}`;
+				state.order = 2;
+			} else {
+				noticeBarTextRef.value.style.cssText = `animation: twoAnimation ${state.twoTime}s linear infinite; opacity: 1;`;
+			}
+		};
+		// 监听 animation 动画的结束
+		const listenerAnimationend = () => {
+			noticeBarTextRef.value.addEventListener(
+				'animationend',
+				() => {
+					changeAnimation();
+				},
+				false
+			);
+		};
+		// 右侧 icon 图标点击
+		const onRightIconClick = () => {
+			if (!props.mode) return false;
+			if (props.mode === 'closeable') {
+				state.isMode = true;
+				emit('close');
+			} else if (props.mode === 'link') {
+				emit('link');
+			}
+		};
+		// 页面加载时
+		onMounted(() => {
+			if (props.scrollable) return false;
+			initAnimation();
+			listenerAnimationend();
+		});
+		return {
+			noticeBarWarpRef,
+			noticeBarTextRef,
+			onRightIconClick,
+			...toRefs(state),
+		};
+	},
+});
+</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;
+				::v-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>

+ 41 - 0
nextadmin_vue3/src/components/svgIcon/index.vue

@@ -0,0 +1,41 @@
+<script lang="ts">
+// 渲染函数:https://v3.cn.vuejs.org/guide/render-function.html
+import { h, resolveComponent } from 'vue';
+
+// 定义接口来定义对象的类型
+interface SvgIconProps {
+	name: string;
+	size: number;
+	color: string;
+}
+
+export default {
+	name: 'svgIcon',
+	props: {
+		// svg 图标组件名字
+		name: {
+			type: String,
+		},
+		// svg 大小
+		size: {
+			type: Number,
+			default: () => 14,
+		},
+		// svg 颜色
+		color: {
+			type: String,
+		},
+	},
+	setup(props: SvgIconProps) {
+		// 定义变量
+		const linesString: string[] = ['https', 'http', '/src', '/assets'];
+		const onLineStyle: string = `font-size: ${props.size}px;color: ${props.color}`;
+		const localsStyle: string = `width: ${props.size}px;height: ${props.size}px`;
+
+		// 逻辑判断
+		if (props.name?.startsWith('ele-')) return () => h('i', { class: 'el-icon', style: onLineStyle }, [h(resolveComponent(props.name))]);
+		else if (linesString.find((str) => props.name?.startsWith(str))) return () => h('img', { src: props.name, style: localsStyle });
+		else return () => h('i', { class: props.name, style: onLineStyle });
+	},
+};
+</script>

+ 27 - 0
nextadmin_vue3/src/config.ts

@@ -0,0 +1,27 @@
+/**
+ * 系统设置
+ */
+const config = {
+    /**
+     * 请求接口域名
+     */
+    host: "http://ssl.ycxxkj.com/office_kpi/api/public/index.php",
+    /**
+     * 接口签名密钥
+     */
+    apiSignKey: "32a1ff74699ff2d6ce4c497cb94cb5c8",
+}
+let env = import.meta.env;
+console.log("==env.Mode:==", env.MODE);
+if (env.MODE == "development") {
+    // config.host = " http://local.lzj/office_kpi/api/public/index.php";
+	// config.host = " http://192.168.0.200:3838/index.php";
+	config.host = "http://192.168.0.200:3838/index.php";
+	config.domain = "http://192.168.0.200:3838";
+}
+if (env.MODE == "test") {
+    // config.host = " http://local.lzj/office_kpi/api/public/index.php";
+
+}
+console.log("==config:==", config);
+export default config;

+ 55 - 0
nextadmin_vue3/src/i18n/index.ts

@@ -0,0 +1,55 @@
+import { createI18n } from 'vue-i18n';
+import zhcnLocale from 'element-plus/lib/locale/lang/zh-cn';
+import enLocale from 'element-plus/lib/locale/lang/en';
+import zhtwLocale from 'element-plus/lib/locale/lang/zh-tw';
+import { store } from '/@/store/index';
+
+import nextZhcn from '/@/i18n/lang/zh-cn';
+
+
+import pagesLoginZhcn from '/@/i18n/pages/login/zh-cn';
+import pagesLoginEn from '/@/i18n/pages/login/en';
+import pagesLoginZhtw from '/@/i18n/pages/login/zh-tw';
+import pagesFormI18nZhcn from '/@/i18n/pages/formI18n/zh-cn';
+import pagesFormI18nEn from '/@/i18n/pages/formI18n/en';
+import pagesFormI18nZhtw from '/@/i18n/pages/formI18n/zh-tw';
+
+// 定义语言国际化内容
+/**
+ * 说明:
+ * /src/i18n/lang 下的 ts 为框架的国际化内容
+ * /src/i18n/pages 下的 ts 为各界面的国际化内容
+ */
+const messages = {
+	[zhcnLocale.name]: {
+		...zhcnLocale,
+		message: {
+			...nextZhcn,
+			...pagesLoginZhcn,
+			...pagesFormI18nZhcn,
+		},
+	},
+	[enLocale.name]: {
+		...enLocale,
+		message: {
+
+			...pagesLoginEn,
+			...pagesFormI18nEn,
+		},
+	},
+	[zhtwLocale.name]: {
+		...zhtwLocale,
+		message: {
+
+			...pagesLoginZhtw,
+			...pagesFormI18nZhtw,
+		},
+	},
+};
+
+// 导出语言国际化
+export const i18n = createI18n({
+	locale: store.state.themeConfig.themeConfig.globalI18n,
+	fallbackLocale: zhcnLocale.name,
+	messages,
+});

+ 184 - 0
nextadmin_vue3/src/i18n/lang/zh-cn.ts

@@ -0,0 +1,184 @@
+// 定义内容
+export default {
+	router: {
+		home: '首页',
+		company: '企业管理',
+		protocol: '协议管理',
+		//以下的准备弃用
+		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 演示',
+		paramsIndex: '路由参数',
+		paramsCommon: '普通路由',
+		paramsDynamic: '动态路由',
+		paramsCommonDetails: '普通路由详情',
+		paramsDynamicDetails: '动态路由详情',
+		chartIndex: '大数据图表',
+		visualizingIndex: '数据可视化',
+		visualizingLinkDemo1: '数据可视化演示1',
+		visualizingLinkDemo2: '数据可视化演示2',
+		personal: '个人中心',
+		tools: '工具类集合',
+		layoutLinkView: '外链',
+		layoutIfameView: '内嵌 iframe',
+	},
+	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: '退出中',
+		logOutSuccess: '安全退出成功!',
+	},
+	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: '菜单默认字体颜色',
+		twoIsMenuBarColorGradual: '菜单背景渐变',
+		twoColumnsMenuBar: '分栏菜单背景',
+		twoColumnsMenuBarColor: '分栏菜单默认字体颜色',
+		twoIsColumnsMenuBarColorGradual: '分栏菜单背景渐变',
+		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/store/modules/themeConfig.ts` 中修改。',
+		copyText: '一键复制配置',
+		resetText: '一键恢复默认',
+		copyTextSuccess: '复制成功!',
+		copyTextError: '复制失败!',
+	},
+};

+ 13 - 0
nextadmin_vue3/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
nextadmin_vue3/src/i18n/pages/formI18n/zh-cn.ts

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

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

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

+ 29 - 0
nextadmin_vue3/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
nextadmin_vue3/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
nextadmin_vue3/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: '歡迎回來!',
+};

+ 157 - 0
nextadmin_vue3/src/layout/component/aside.vue

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

+ 294 - 0
nextadmin_vue3/src/layout/component/columnsAside.vue

@@ -0,0 +1,294 @@
+<template>
+	<div class="layout-columns-aside">
+		<el-scrollbar>
+			<ul @mouseleave="onColumnsAsideMenuMouseleave()">
+				<li
+					v-for="(v, k) in columnsAsideList"
+					:key="k"
+					@click="onColumnsAsideMenuClick(v, k)"
+					@mouseenter="onColumnsAsideMenuMouseenter(v, k)"
+					:ref="
+						(el) => {
+							if (el) columnsAsideOffsetTopRefs[k] = el;
+						}
+					"
+					:class="{ 'layout-columns-active': liIndex === k, 'layout-columns-hover': liHoverIndex === k }"
+					:title="$t(v.meta.title)"
+				>
+					<div :class="setColumnsAsidelayout" 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, setColumnsAsidelayout === 'columns-vertical' ? 4 : 3)
+									: $t(v.meta.title)
+							}}
+						</div>
+					</div>
+					<div :class="setColumnsAsidelayout" 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, setColumnsAsidelayout === 'columns-vertical' ? 4 : 3)
+										: $t(v.meta.title)
+								}}
+							</div>
+						</a>
+					</div>
+				</li>
+				<div ref="columnsAsideActiveRef" :class="setColumnsAsideStyle"></div>
+			</ul>
+		</el-scrollbar>
+	</div>
+</template>
+
+<script lang="ts">
+import { reactive, toRefs, ref, computed, onMounted, nextTick, getCurrentInstance, watch, onUnmounted, defineComponent } from 'vue';
+import { useRoute, useRouter, onBeforeRouteUpdate, RouteRecordRaw } from 'vue-router';
+import { useStore } from '/@/store/index';
+
+// 定义接口来定义对象的类型
+interface ColumnsAsideState {
+	columnsAsideList: any[];
+	liIndex: number;
+	liOldIndex: null | number;
+	liHoverIndex: null | number;
+	liOldPath: null | string;
+	difference: number;
+	routeSplit: string[];
+	isNavHover: boolean;
+}
+
+export default defineComponent({
+	name: 'layoutColumnsAside',
+	setup() {
+		const columnsAsideOffsetTopRefs: any = ref([]);
+		const columnsAsideActiveRef = ref();
+		const { proxy } = <any>getCurrentInstance();
+		const store = useStore();
+		const route = useRoute();
+		const router = useRouter();
+		const state = reactive<ColumnsAsideState>({
+			columnsAsideList: [],
+			liIndex: 0,
+			liOldIndex: null,
+			liHoverIndex: null,
+			liOldPath: null,
+			difference: 0,
+			routeSplit: [],
+			isNavHover: false,
+		});
+		// 设置分栏高亮风格
+		const setColumnsAsideStyle = computed(() => {
+			return store.state.themeConfig.themeConfig.columnsAsideStyle;
+		});
+		// 设置分栏布局风格
+		const setColumnsAsidelayout = computed(() => {
+			return store.state.themeConfig.themeConfig.columnsAsideLayout;
+		});
+		// 设置菜单高亮位置移动
+		const setColumnsAsideMove = (k: number) => {
+			state.liIndex = k;
+			columnsAsideActiveRef.value.style.top = `${columnsAsideOffsetTopRefs.value[k].offsetTop + state.difference}px`;
+		};
+		// 菜单高亮点击事件
+		const onColumnsAsideMenuClick = (v: Object, k: number) => {
+			setColumnsAsideMove(k);
+			let { path, redirect } = v as any;
+			if (redirect) router.push(redirect);
+			else router.push(path);
+		};
+		// 鼠标移入时,显示当前的子级菜单
+		const onColumnsAsideMenuMouseenter = (v: RouteRecordRaw, k: number) => {
+			let { path } = v;
+			state.liOldPath = path;
+			state.liOldIndex = k;
+			state.liHoverIndex = k;
+			proxy.mittBus.emit('setSendColumnsChildren', setSendChildren(path));
+			store.dispatch('routesList/setColumnsMenuHover', false);
+			store.dispatch('routesList/setColumnsNavHover', true);
+			state.isNavHover = true;
+		};
+		// 鼠标移走时,显示原来的子级菜单
+		const onColumnsAsideMenuMouseleave = async () => {
+			await store.dispatch('routesList/setColumnsNavHover', false);
+			// 添加延时器,防止拿到的 store.state.routesList 值不是最新的
+			setTimeout(() => {
+				const { isColumnsMenuHover, isColumnsNavHover } = store.state.routesList;
+				if (!isColumnsMenuHover && !isColumnsNavHover) proxy.mittBus.emit('restoreDefault');
+			}, 100);
+			// state.isNavHover = false;
+		};
+		// 设置高亮动态位置
+		const onColumnsAsideDown = (k: number) => {
+			nextTick(() => {
+				setColumnsAsideMove(k);
+			});
+		};
+		// 设置/过滤路由(非静态路由/是否显示在菜单中)
+		const setFilterRoutes = () => {
+			state.columnsAsideList = filterRoutesFun(store.state.routesList.routesList);
+			const resData: any = setSendChildren(route.path);
+			if (Object.keys(resData).length <= 0) return false;
+			onColumnsAsideDown(resData.item[0].k);
+			proxy.mittBus.emit('setSendColumnsChildren', resData);
+		};
+		// 传送当前子级数据到菜单中
+		const setSendChildren = (path: string) => {
+			const currentPathSplit = path.split('/');
+			let currentData: any = {};
+			state.columnsAsideList.map((v: any, k: number) => {
+				if (v.path === `/${currentPathSplit[1]}`) {
+					v['k'] = k;
+					currentData['item'] = [{ ...v }];
+					currentData['children'] = [{ ...v }];
+					if (v.children) currentData['children'] = v.children;
+				}
+			});
+			return currentData;
+		};
+		// 路由过滤递归函数
+		const filterRoutesFun = (arr: Array<object>) => {
+			return arr
+				.filter((item: any) => !item.meta.isHide)
+				.map((item: any) => {
+					item = Object.assign({}, item);
+					if (item.children) item.children = filterRoutesFun(item.children);
+					return item;
+				});
+		};
+		// tagsView 点击时,根据路由查找下标 columnsAsideList,实现左侧菜单高亮
+		const setColumnsMenuHighlight = (path: string) => {
+			state.routeSplit = path.split('/');
+			state.routeSplit.shift();
+			const routeFirst = `/${state.routeSplit[0]}`;
+			const currentSplitRoute = state.columnsAsideList.find((v: any) => v.path === routeFirst);
+			if (!currentSplitRoute) return false;
+			// 延迟拿值,防止取不到
+			setTimeout(() => {
+				onColumnsAsideDown((<any>currentSplitRoute).k);
+			}, 0);
+		};
+		// 监听布局配置信息的变化,动态增加菜单高亮位置移动像素
+		watch(store.state, (val) => {
+			val.themeConfig.themeConfig.columnsAsideStyle === 'columnsRound' ? (state.difference = 3) : (state.difference = 0);
+			if (!val.routesList.isColumnsMenuHover && !val.routesList.isColumnsNavHover) {
+				state.liHoverIndex = null;
+				proxy.mittBus.emit('setSendColumnsChildren', setSendChildren(route.path));
+			} else {
+				state.liHoverIndex = state.liOldIndex;
+				if (!state.liOldPath) return false;
+				proxy.mittBus.emit('setSendColumnsChildren', setSendChildren(state.liOldPath));
+			}
+		});
+		// 页面加载时
+		onMounted(() => {
+			setFilterRoutes();
+			// 销毁变量,防止鼠标再次移入时,保留了上次的记录
+			proxy.mittBus.on('restoreDefault', () => {
+				state.liOldIndex = null;
+				state.liOldPath = null;
+			});
+		});
+		// 页面卸载时
+		onUnmounted(() => {
+			proxy.mittBus.off('restoreDefault', () => {});
+		});
+		// 路由更新时
+		onBeforeRouteUpdate((to) => {
+			setColumnsMenuHighlight(to.path);
+			proxy.mittBus.emit('setSendColumnsChildren', setSendChildren(to.path));
+		});
+		return {
+			columnsAsideOffsetTopRefs,
+			columnsAsideActiveRef,
+			onColumnsAsideDown,
+			setColumnsAsideStyle,
+			setColumnsAsidelayout,
+			onColumnsAsideMenuClick,
+			onColumnsAsideMenuMouseenter,
+			onColumnsAsideMenuMouseleave,
+			...toRefs(state),
+		};
+	},
+});
+</script>
+
+<style scoped lang="scss">
+.layout-columns-aside {
+	width: 70px;
+	height: 100%;
+	background: var(--next-bg-columnsMenuBar);
+	ul {
+		position: relative;
+		li {
+			color: var(--next-bg-columnsMenuBarColor);
+			width: 100%;
+			height: 50px;
+			text-align: center;
+			display: flex;
+			cursor: pointer;
+			position: relative;
+			z-index: 1;
+			.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);
+			}
+		}
+		.layout-columns-active {
+			color: var(--el-color-white);
+			transition: 0.3s ease-in-out;
+		}
+		.layout-columns-hover {
+			color: var(--el-color-primary);
+			a {
+				color: var(--el-color-primary);
+			}
+		}
+		.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>

+ 32 - 0
nextadmin_vue3/src/layout/component/header.vue

@@ -0,0 +1,32 @@
+<template>
+	<el-header class="layout-header" :height="setHeaderHeight" v-show="!isTagsViewCurrenFull">
+		<NavBarsIndex />
+	</el-header>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import { useStore } from '/@/store/index';
+import NavBarsIndex from '/@/layout/navBars/index.vue';
+export default defineComponent({
+	name: 'layoutHeader',
+	components: { NavBarsIndex },
+	setup() {
+		const store = useStore();
+		// 设置 header 的高度
+		const setHeaderHeight = computed(() => {
+			let { isTagsview, layout } = store.state.themeConfig.themeConfig;
+			if (isTagsview && layout !== 'classic') return '84px';
+			else return '50px';
+		});
+		// 获取卡片全屏信息
+		const isTagsViewCurrenFull = computed(() => {
+			return store.state.tagsViewRoutes.isTagsViewCurrenFull;
+		});
+		return {
+			setHeaderHeight,
+			isTagsViewCurrenFull,
+		};
+	},
+});
+</script>

+ 84 - 0
nextadmin_vue3/src/layout/component/main.vue

@@ -0,0 +1,84 @@
+<template>
+	<el-main class="layout-main">
+		<el-scrollbar
+			class="layout-scrollbar"
+			ref="layoutScrollbarRef"
+			:style="{ padding: currentRouteMeta.isLink && currentRouteMeta.isIframe ? 0 : '', transition: 'padding 0.3s ease-in-out' }"
+		>
+			<LayoutParentView :style="{ minHeight: `calc(100vh - ${headerHeight})` }" />
+			<Footer v-if="getThemeConfig.isFooter" />
+		</el-scrollbar>
+	</el-main>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, toRefs, reactive, getCurrentInstance, watch, onMounted } from 'vue';
+import { useStore } from '/@/store/index';
+import { useRoute } from 'vue-router';
+import LayoutParentView from '/@/layout/routerView/parent.vue';
+import Footer from '/@/layout/footer/index.vue';
+
+// 定义接口来定义对象的类型
+interface MainState {
+	headerHeight: string | number;
+	currentRouteMeta: any;
+}
+
+export default defineComponent({
+	name: 'layoutMain',
+	components: { LayoutParentView, Footer },
+	setup() {
+		const { proxy } = <any>getCurrentInstance();
+		const route = useRoute();
+		const store = useStore();
+		const state = reactive<MainState>({
+			headerHeight: '',
+			currentRouteMeta: {},
+		});
+		// 获取布局配置信息
+		const getThemeConfig = computed(() => {
+			return store.state.themeConfig.themeConfig;
+		});
+		// 设置 main 的高度
+		const initHeaderHeight = () => {
+			const bool = state.currentRouteMeta.isLink && state.currentRouteMeta.isIframe;
+			let { isTagsview } = store.state.themeConfig.themeConfig;
+			if (isTagsview) return (state.headerHeight = bool ? `85px` : `114px`);
+			else return (state.headerHeight = `51px`);
+		};
+		// 初始化获取当前路由 meta,用于设置 iframes padding
+		const initGetMeta = () => {
+			state.currentRouteMeta = route.meta;
+		};
+		// 页面加载前
+		onMounted(async () => {
+			await initGetMeta();
+			initHeaderHeight();
+		});
+		// 监听路由变化
+		watch(
+			() => route.path,
+			() => {
+				state.currentRouteMeta = route.meta;
+				const bool = state.currentRouteMeta.isLink && state.currentRouteMeta.isIframe;
+				state.headerHeight = bool ? `85px` : `114px`;
+				proxy.$refs.layoutScrollbarRef.update();
+			}
+		);
+		// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
+		watch(store.state.themeConfig.themeConfig, (val) => {
+			state.currentRouteMeta = route.meta;
+			const bool = state.currentRouteMeta.isLink && state.currentRouteMeta.isIframe;
+			state.headerHeight = val.isTagsview ? (bool ? `85px` : `114px`) : '51px';
+			if (val.isFixedHeaderChange !== val.isFixedHeader) {
+				if (!proxy.$refs.layoutScrollbarRef) return false;
+				proxy.$refs.layoutScrollbarRef.update();
+			}
+		});
+		return {
+			getThemeConfig,
+			...toRefs(state),
+		};
+	},
+});
+</script>

+ 46 - 0
nextadmin_vue3/src/layout/footer/index.vue

@@ -0,0 +1,46 @@
+<template>
+	<div class="layout-footer mt15" v-show="isDelayFooter">
+		<div class="layout-footer-warp">
+			<div>vue-next-admin,Made by lyt with ❤️</div>
+			<div class="mt5">深圳市 xxx 公司版权所有</div>
+		</div>
+	</div>
+</template>
+
+<script lang="ts">
+import { toRefs, reactive, defineComponent } from 'vue';
+import { onBeforeRouteUpdate } from 'vue-router';
+export default defineComponent({
+	name: 'layoutFooter',
+	setup() {
+		const state = reactive({
+			isDelayFooter: true,
+		});
+		// 路由改变时,等主界面动画加载完毕再显示 footer
+		onBeforeRouteUpdate(() => {
+			setTimeout(() => {
+				state.isDelayFooter = false;
+				setTimeout(() => {
+					state.isDelayFooter = true;
+				}, 800);
+			}, 0);
+		});
+		return {
+			...toRefs(state),
+		};
+	},
+});
+</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 1s ease-in-out;
+	}
+}
+</style>

+ 56 - 0
nextadmin_vue3/src/layout/index.vue

@@ -0,0 +1,56 @@
+<template>
+	<component :is="getThemeConfig.layout" />
+</template>
+
+<script lang="ts">
+import { computed, onBeforeMount, onUnmounted, getCurrentInstance, defineComponent, defineAsyncComponent } from 'vue';
+import { useStore } from '/@/store/index';
+import { Local } from '/@/utils/storage';
+export default defineComponent({
+	name: 'layout',
+	components: {
+		defaults: defineAsyncComponent(() => import('/@/layout/main/defaults.vue')),
+		classic: defineAsyncComponent(() => import('/@/layout/main/classic.vue')),
+		transverse: defineAsyncComponent(() => import('/@/layout/main/transverse.vue')),
+		columns: defineAsyncComponent(() => import('/@/layout/main/columns.vue')),
+	},
+	setup() {
+		const { proxy } = <any>getCurrentInstance();
+		const store = useStore();
+		// 获取布局配置信息
+		const getThemeConfig = computed(() => {
+			return store.state.themeConfig.themeConfig;
+		});
+		// 窗口大小改变时(适配移动端)
+		const onLayoutResize = () => {
+			if (!Local.get('oldLayout')) Local.set('oldLayout', getThemeConfig.value.layout);
+			const clientWidth = document.body.clientWidth;
+			if (clientWidth < 1000) {
+				getThemeConfig.value.isCollapse = false;
+				proxy.mittBus.emit('layoutMobileResize', {
+					layout: 'defaults',
+					clientWidth,
+				});
+			} else {
+				proxy.mittBus.emit('layoutMobileResize', {
+					layout: Local.get('oldLayout') ? Local.get('oldLayout') : getThemeConfig.value.layout,
+					clientWidth,
+				});
+			}
+		};
+		// 页面加载前
+		onBeforeMount(() => {
+			onLayoutResize();
+			window.addEventListener('resize', onLayoutResize);
+		});
+		// 页面卸载时
+		onUnmounted(() => {
+			window.removeEventListener('resize', onLayoutResize);
+		});
+		console.log('getThemeConfig', getThemeConfig);
+		return {
+			getThemeConfig,
+		};
+	},
+});
+</script>

+ 370 - 0
nextadmin_vue3/src/layout/lockScreen/index.vue

@@ -0,0 +1,370 @@
+<template>
+	<div v-show="isShowLockScreen">
+		<div class="layout-lock-screen-mask"></div>
+		<div class="layout-lock-screen-img" :class="{ 'layout-lock-screen-filter': isShowLoockLogin }"></div>
+		<div class="layout-lock-screen">
+			<div
+				class="layout-lock-screen-date"
+				ref="layoutLockScreenDateRef"
+				@mousedown="onDown"
+				@mousemove="onMove"
+				@mouseup="onEnd"
+				@touchstart.stop="onDown"
+				@touchmove.stop="onMove"
+				@touchend.stop="onEnd"
+			>
+				<div class="layout-lock-screen-date-box">
+					<div class="layout-lock-screen-date-box-time">
+						{{ time.hm }}<span class="layout-lock-screen-date-box-minutes">{{ time.s }}</span>
+					</div>
+					<div class="layout-lock-screen-date-box-info">{{ 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="isShowLoockLogin" class="layout-lock-screen-login">
+					<div class="layout-lock-screen-login-box">
+						<div class="layout-lock-screen-login-box-img">
+							<img src="https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=1813762643,1914315241&fm=26&gp=0.jpg" />
+						</div>
+						<div class="layout-lock-screen-login-box-name">Administrator</div>
+						<div class="layout-lock-screen-login-box-value">
+							<el-input
+								placeholder="请输入密码"
+								ref="layoutLockScreenInputRef"
+								v-model="lockScreenPassword"
+								@keyup.enter.native.stop="onLockScreenSubmit()"
+							>
+								<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" />
+						<SvgIcon name="ele-AlarmClock" />
+						<SvgIcon name="ele-SwitchButton" />
+					</div>
+				</div>
+			</transition>
+		</div>
+	</div>
+</template>
+
+<script lang="ts">
+import { nextTick, onMounted, reactive, toRefs, ref, onUnmounted, getCurrentInstance, defineComponent } from 'vue';
+import { useStore } from '/@/store/index';
+import { formatDate } from '/@/utils/formatTime';
+import { Local } from '/@/utils/storage';
+
+// 定义接口来定义对象的类型
+interface LockScreenState {
+	transparency: number;
+	downClientY: number;
+	moveDifference: number;
+	isShowLoockLogin: boolean;
+	isFlags: boolean;
+	querySelectorEl: HTMLElement | string;
+	time: {
+		hm: string;
+		s: string;
+		mdq: string;
+	};
+	setIntervalTime: number;
+	isShowLockScreen: boolean;
+	isShowLockScreenIntervalTime: number;
+	lockScreenPassword: string;
+}
+
+export default defineComponent({
+	name: 'layoutLockScreen',
+	setup() {
+		const { proxy } = <any>getCurrentInstance();
+		const layoutLockScreenInputRef = ref();
+		const store = useStore();
+		const state = reactive<LockScreenState>({
+			transparency: 1,
+			downClientY: 0,
+			moveDifference: 0,
+			isShowLoockLogin: false,
+			isFlags: false,
+			querySelectorEl: '',
+			time: {
+				hm: '',
+				s: '',
+				mdq: '',
+			},
+			setIntervalTime: 0,
+			isShowLockScreen: false,
+			isShowLockScreenIntervalTime: 0,
+			lockScreenPassword: '',
+		});
+		// 鼠标按下
+		const onDown = (down: any) => {
+			state.isFlags = true;
+			state.downClientY = down.touches ? down.touches[0].clientY : down.clientY;
+		};
+		// 鼠标移动
+		const onMove = (move: any) => {
+			if (state.isFlags) {
+				const el = <HTMLElement>state.querySelectorEl;
+				const opacitys = (state.transparency -= 1 / 200);
+				if (move.touches) {
+					state.moveDifference = move.touches[0].clientY - state.downClientY;
+				} else {
+					state.moveDifference = move.clientY - state.downClientY;
+				}
+				if (state.moveDifference >= 0) return false;
+				el.setAttribute('style', `top:${state.moveDifference}px;cursor:pointer;opacity:${opacitys};`);
+				if (state.moveDifference < -400) {
+					el.setAttribute('style', `top:${-el.clientHeight}px;cursor:pointer;transition:all 0.3s ease;`);
+					state.moveDifference = -el.clientHeight;
+					setTimeout(() => {
+						el && el.parentNode?.removeChild(el);
+					}, 300);
+				}
+				if (state.moveDifference === -el.clientHeight) {
+					state.isShowLoockLogin = true;
+					layoutLockScreenInputRef.value.focus();
+				}
+			}
+		};
+		// 鼠标松开
+		const onEnd = () => {
+			state.isFlags = false;
+			state.transparency = 1;
+			if (state.moveDifference >= -400) {
+				(<HTMLElement>state.querySelectorEl).setAttribute('style', `top:0px;opacity:1;transition:all 0.3s ease;`);
+			}
+		};
+		// 获取要拖拽的初始元素
+		const initGetElement = () => {
+			nextTick(() => {
+				state.querySelectorEl = proxy.$refs.layoutLockScreenDateRef;
+			});
+		};
+		// 时间初始化
+		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 (store.state.themeConfig.themeConfig.isLockScreen) {
+				state.isShowLockScreenIntervalTime = window.setInterval(() => {
+					if (store.state.themeConfig.themeConfig.lockScreenTime <= 1) {
+						state.isShowLockScreen = true;
+						setLocalThemeConfig();
+						return false;
+					}
+					store.state.themeConfig.themeConfig.lockScreenTime--;
+				}, 1000);
+			} else {
+				clearInterval(state.isShowLockScreenIntervalTime);
+			}
+		};
+		// 存储布局配置
+		const setLocalThemeConfig = () => {
+			store.state.themeConfig.themeConfig.isDrawer = false;
+			Local.set('themeConfig', store.state.themeConfig.themeConfig);
+		};
+		// 密码输入点击事件
+		const onLockScreenSubmit = () => {
+			store.state.themeConfig.themeConfig.isLockScreen = false;
+			store.state.themeConfig.themeConfig.lockScreenTime = 30;
+			setLocalThemeConfig();
+		};
+		// 页面加载时
+		onMounted(() => {
+			initGetElement();
+			initSetTime();
+			initLockScreen();
+		});
+		// 页面卸载时
+		onUnmounted(() => {
+			window.clearInterval(state.setIntervalTime);
+			window.clearInterval(state.isShowLockScreenIntervalTime);
+		});
+		return {
+			layoutLockScreenInputRef,
+			onDown,
+			onMove,
+			onEnd,
+			onLockScreenSubmit,
+			...toRefs(state),
+		};
+	},
+});
+</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://gitee.com/lyt-top/vue-next-admin-images/raw/master/images/03.jpg');
+	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(--color-whites);
+			}
+			&-info {
+				font-size: 40px;
+				color: var(--color-whites);
+			}
+			&-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(--color-whites);
+			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(--color-whites);
+				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(--color-whites);
+				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(--color-whites);
+		&-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;
+				}
+			}
+		}
+	}
+}
+::v-deep(.el-input-group__append) {
+	background: var(--el-color-white);
+	padding: 0px 15px;
+}
+::v-deep(.el-input__inner) {
+	border-right-color: var(--el-border-color-extra-light);
+	&:hover {
+		border-color: var(--el-border-color-extra-light);
+	}
+}
+</style>

+ 83 - 0
nextadmin_vue3/src/layout/logo/index.vue

@@ -0,0 +1,83 @@
+<template>
+	<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
+		<img :src="logoMini" class="layout-logo-medium-img" />
+		<span>{{ getThemeConfig.globalTitle }}</span>
+	</div>
+	<div class="layout-logo-size" v-else @click="onThemeConfigChange">
+		<img :src="logoMini" class="layout-logo-size-img" />
+	</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import { useStore } from '/@/store/index';
+
+import logoMini from '/@/assets/logo-mini.svg';
+
+export default defineComponent({
+	name: 'layoutLogo',
+	setup() {
+		const store = useStore();
+		// 获取布局配置信息
+		const getThemeConfig = computed(() => {
+			return store.state.themeConfig.themeConfig;
+		});
+		// 设置 logo 的显示。classic 经典布局默认显示 logo
+		const setShowLogo = computed(() => {
+			let { isCollapse, layout } = store.state.themeConfig.themeConfig;
+			return !isCollapse || layout === 'classic' || document.body.clientWidth < 1000;
+		});
+		// logo 点击实现菜单展开/收起
+		const onThemeConfigChange = () => {
+			if (store.state.themeConfig.themeConfig.layout === 'transverse') return false;
+			store.state.themeConfig.themeConfig.isCollapse = !store.state.themeConfig.themeConfig.isCollapse;
+		};
+		return {
+			logoMini,
+			setShowLogo,
+			getThemeConfig,
+			onThemeConfigChange,
+		};
+	},
+});
+</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;
+	&: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>

+ 36 - 0
nextadmin_vue3/src/layout/main/classic.vue

@@ -0,0 +1,36 @@
+<template>
+	<el-container class="layout-container flex-center">
+		<Header />
+		<el-container class="layout-mian-height-50">
+			<Aside />
+			<div class="flex-center layout-backtop">
+				<TagsView v-if="getThemeConfig.isTagsview" />
+				<Main />
+			</div>
+		</el-container>
+		<el-backtop target=".layout-backtop .el-main .el-scrollbar__wrap"></el-backtop>
+	</el-container>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import { useStore } from '/@/store/index';
+import Aside from '/@/layout/component/aside.vue';
+import Header from '/@/layout/component/header.vue';
+import Main from '/@/layout/component/main.vue';
+import TagsView from '/@/layout/navBars/tagsView/tagsView.vue';
+export default defineComponent({
+	name: 'layoutClassic',
+	components: { Aside, Header, Main, TagsView },
+	setup() {
+		const store = useStore();
+		// 获取布局配置信息
+		const getThemeConfig = computed(() => {
+			return store.state.themeConfig.themeConfig;
+		});
+		return {
+			getThemeConfig,
+		};
+	},
+});
+</script>

+ 38 - 0
nextadmin_vue3/src/layout/main/columns.vue

@@ -0,0 +1,38 @@
+<template>
+	<el-container class="layout-container">
+		<ColumnsAside />
+		<div class="layout-columns-warp">
+			<Aside />
+			<el-container class="flex-center layout-backtop" :class="{ 'layout-backtop': !isFixedHeader }">
+				<Header v-if="isFixedHeader" />
+				<el-scrollbar :class="{ 'layout-backtop': isFixedHeader }">
+					<Header v-if="!isFixedHeader" />
+					<Main />
+				</el-scrollbar>
+			</el-container>
+		</div>
+		<el-backtop target=".layout-backtop .el-scrollbar__wrap"></el-backtop>
+	</el-container>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import { useStore } from '/@/store/index';
+import Aside from '/@/layout/component/aside.vue';
+import Header from '/@/layout/component/header.vue';
+import Main from '/@/layout/component/main.vue';
+import ColumnsAside from '/@/layout/component/columnsAside.vue';
+export default defineComponent({
+	name: 'layoutColumns',
+	components: { Aside, Header, Main, ColumnsAside },
+	setup() {
+		const store = useStore();
+		const isFixedHeader = computed(() => {
+			return store.state.themeConfig.themeConfig.isFixedHeader;
+		});
+		return {
+			isFixedHeader,
+		};
+	},
+});
+</script>

+ 44 - 0
nextadmin_vue3/src/layout/main/defaults.vue

@@ -0,0 +1,44 @@
+<template>
+	<el-container class="layout-container">
+		<Aside />
+		<el-container class="flex-center" :class="{ 'layout-backtop': !isFixedHeader }">
+			<Header v-if="isFixedHeader" />
+			<el-scrollbar ref="layoutDefaultsScrollbarRef" :class="{ 'layout-backtop': isFixedHeader }">
+				<Header v-if="!isFixedHeader" />
+				<Main />
+			</el-scrollbar>
+		</el-container>
+		<el-backtop target=".layout-backtop .el-scrollbar__wrap"></el-backtop>
+	</el-container>
+</template>
+
+<script lang="ts">
+import { computed, getCurrentInstance, watch, defineComponent } from 'vue';
+import { useRoute } from 'vue-router';
+import { useStore } from '/@/store/index';
+import Aside from '/@/layout/component/aside.vue';
+import Header from '/@/layout/component/header.vue';
+import Main from '/@/layout/component/main.vue';
+export default defineComponent({
+	name: 'layoutDefaults',
+	components: { Aside, Header, Main },
+	setup() {
+		const { proxy } = getCurrentInstance() as any;
+		const store = useStore();
+		const route = useRoute();
+		const isFixedHeader = computed(() => {
+			return store.state.themeConfig.themeConfig.isFixedHeader;
+		});
+		// 监听路由的变化
+		watch(
+			() => route.path,
+			() => {
+				proxy.$refs.layoutDefaultsScrollbarRef.wrap$.scrollTop = 0;
+			}
+		);
+		return {
+			isFixedHeader,
+		};
+	},
+});
+</script>

+ 16 - 0
nextadmin_vue3/src/layout/main/transverse.vue

@@ -0,0 +1,16 @@
+<template>
+	<el-container class="layout-container flex-center layout-backtop">
+		<Header />
+		<Main />
+		<el-backtop target=".layout-backtop .el-main .el-scrollbar__wrap"></el-backtop>
+	</el-container>
+</template>
+
+<script lang="ts">
+import Header from '/@/layout/component/header.vue';
+import Main from '/@/layout/component/main.vue';
+export default {
+	name: 'layoutTransverse',
+	components: { Header, Main },
+};
+</script>

+ 152 - 0
nextadmin_vue3/src/layout/navBars/breadcrumb/breadcrumb.vue

@@ -0,0 +1,152 @@
+<template>
+	<div class="layout-navbars-breadcrumb" :style="{ display: isShowBreadcrumb }">
+		<SvgIcon
+			class="layout-navbars-breadcrumb-icon"
+			:name="getThemeConfig.isCollapse ? 'ele-Expand' : 'ele-Fold'"
+			:size="16"
+			@click="onThemeConfigChange"
+		/>
+		<el-breadcrumb class="layout-navbars-breadcrumb-hide">
+			<transition-group name="breadcrumb" mode="out-in">
+				<el-breadcrumb-item v-for="(v, k) in breadcrumbList" :key="v.meta.title">
+					<span v-if="k === breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
+						<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="getThemeConfig.isBreadcrumbIcon" />{{ $t(v.meta.title) }}
+					</span>
+					<a v-else @click.prevent="onBreadcrumbClick(v)">
+						<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="getThemeConfig.isBreadcrumbIcon" />{{ $t(v.meta.title) }}
+					</a>
+				</el-breadcrumb-item>
+			</transition-group>
+		</el-breadcrumb>
+	</div>
+</template>
+
+<script lang="ts">
+import { toRefs, reactive, computed, onMounted, defineComponent } from 'vue';
+import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
+import { useStore } from '/@/store/index';
+import { Local } from '/@/utils/storage';
+
+// 定义接口来定义对象的类型
+interface BreadcrumbState {
+	breadcrumbList: Array<any>;
+	routeSplit: Array<string>;
+	routeSplitFirst: string;
+	routeSplitIndex: number;
+}
+
+export default defineComponent({
+	name: 'layoutBreadcrumb',
+	setup() {
+		const store = useStore();
+		const route = useRoute();
+		const router = useRouter();
+		const state = reactive<BreadcrumbState>({
+			breadcrumbList: [],
+			routeSplit: [],
+			routeSplitFirst: '',
+			routeSplitIndex: 1,
+		});
+		// 获取布局配置信息
+		const getThemeConfig = computed(() => {
+			return store.state.themeConfig.themeConfig;
+		});
+		// 动态设置经典、横向布局不显示
+		const isShowBreadcrumb = computed(() => {
+			initRouteSplit(route.path);
+			const { layout, isBreadcrumb } = store.state.themeConfig.themeConfig;
+			if (layout === 'classic' || layout === 'transverse') return 'none';
+			else return isBreadcrumb ? '' : 'none';
+		});
+		// 面包屑点击时
+		const onBreadcrumbClick = (v: any) => {
+			const { redirect, path } = v;
+			if (redirect) router.push(redirect);
+			else router.push(path);
+		};
+		// 展开/收起左侧菜单点击
+		const onThemeConfigChange = () => {
+			store.state.themeConfig.themeConfig.isCollapse = !store.state.themeConfig.themeConfig.isCollapse;
+			setLocalThemeConfig();
+		};
+		// 存储布局配置
+		const setLocalThemeConfig = () => {
+			Local.remove('themeConfig');
+			Local.set('themeConfig', getThemeConfig.value);
+		};
+		// 处理面包屑数据
+		const getBreadcrumbList = (arr: Array<object>) => {
+			arr.map((item: any) => {
+				state.routeSplit.map((v: any, k: number, arrs: any) => {
+					if (state.routeSplitFirst === item.path) {
+						state.routeSplitFirst += `/${arrs[state.routeSplitIndex]}`;
+						state.breadcrumbList.push(item);
+						state.routeSplitIndex++;
+						if (item.children) getBreadcrumbList(item.children);
+					}
+				});
+			});
+		};
+		// 当前路由字符串切割成数组,并删除第一项空内容
+		const initRouteSplit = (path: string) => {
+			if (!store.state.themeConfig.themeConfig.isBreadcrumb) return false;
+			state.breadcrumbList = [store.state.routesList.routesList[0]];
+			state.routeSplit = path.split('/');
+			state.routeSplit.shift();
+			state.routeSplitFirst = `/${state.routeSplit[0]}`;
+			state.routeSplitIndex = 1;
+			getBreadcrumbList(store.state.routesList.routesList);
+		};
+		// 页面加载时
+		onMounted(() => {
+			initRouteSplit(route.path);
+		});
+		// 路由更新时
+		onBeforeRouteUpdate((to) => {
+			initRouteSplit(to.path);
+		});
+		return {
+			onThemeConfigChange,
+			isShowBreadcrumb,
+			getThemeConfig,
+			onBreadcrumbClick,
+			...toRefs(state),
+		};
+	},
+});
+</script>
+
+<style scoped lang="scss">
+.layout-navbars-breadcrumb {
+	flex: 1;
+	height: inherit;
+	display: flex;
+	align-items: center;
+	padding-left: 15px;
+	.layout-navbars-breadcrumb-icon {
+		cursor: pointer;
+		font-size: 18px;
+		margin-right: 15px;
+		color: var(--next-bg-topBarColor);
+	}
+	.layout-navbars-breadcrumb-span {
+		opacity: 0.7;
+		color: var(--next-bg-topBarColor);
+	}
+	.layout-navbars-breadcrumb-iconfont {
+		font-size: 14px;
+		margin-right: 5px;
+	}
+	::v-deep(.el-breadcrumb__separator) {
+		opacity: 0.7;
+		color: var(--next-bg-topBarColor);
+	}
+	::v-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>

+ 63 - 0
nextadmin_vue3/src/layout/navBars/breadcrumb/closeFull.vue

@@ -0,0 +1,63 @@
+<template>
+	<div class="layout-navbars-close-full" v-if="isTagsViewCurrenFull">
+		<div class="layout-navbars-close-full-box" :title="$t('message.tagsView.closeFullscreen')" @click="onCloseFullscreen">
+			<SvgIcon name="ele-Close" />
+		</div>
+	</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import { useStore } from '/@/store/index';
+export default defineComponent({
+	name: 'layoutCloseFull',
+	setup() {
+		const store = useStore();
+		// 获取卡片全屏信息
+		const isTagsViewCurrenFull = computed(() => {
+			return store.state.tagsViewRoutes.isTagsViewCurrenFull;
+		});
+		// 关闭当前全屏
+		const onCloseFullscreen = () => {
+			store.dispatch('tagsViewRoutes/setCurrenFullscreen', false);
+		};
+		return {
+			isTagsViewCurrenFull,
+			onCloseFullscreen,
+		};
+	},
+});
+</script>
+
+<style scoped lang="scss">
+.layout-navbars-close-full {
+	position: fixed;
+	z-index: 9999999999;
+	right: -30px;
+	top: -30px;
+	.layout-navbars-close-full-box {
+		width: 60px;
+		height: 60px;
+		border-radius: 100%;
+		position: relative;
+		cursor: pointer;
+		background: rgba(0, 0, 0, 0.1);
+		transition: all 0.3s ease;
+		i {
+			position: absolute;
+			left: 11px;
+			top: 35px;
+			color: #333333;
+			transition: all 0.3s ease;
+		}
+		&:hover {
+			background: rgba(0, 0, 0, 0.2);
+			transition: all 0.3s ease;
+			i {
+				color: var(--el-color-primary);
+				transition: all 0.3s ease;
+			}
+		}
+	}
+}
+</style>

+ 115 - 0
nextadmin_vue3/src/layout/navBars/breadcrumb/index.vue

@@ -0,0 +1,115 @@
+<template>
+	<div class="layout-navbars-breadcrumb-index">
+		<Logo v-if="setIsShowLogo" />
+		<Breadcrumb />
+		<Horizontal :menuList="menuList" v-if="isLayoutTransverse" />
+		<User />
+	</div>
+</template>
+
+<script lang="ts">
+import { computed, reactive, toRefs, onMounted, onUnmounted, getCurrentInstance, defineComponent } from 'vue';
+import { useRoute } from 'vue-router';
+import { useStore } from '/@/store/index';
+import Breadcrumb from '/@/layout/navBars/breadcrumb/breadcrumb.vue';
+import User from '/@/layout/navBars/breadcrumb/user.vue';
+import Logo from '/@/layout/logo/index.vue';
+import Horizontal from '/@/layout/navMenu/horizontal.vue';
+
+// 定义接口来定义对象的类型
+interface IndexState {
+	menuList: object[];
+}
+
+export default defineComponent({
+	name: 'layoutBreadcrumbIndex',
+	components: { Breadcrumb, User, Logo, Horizontal },
+	setup() {
+		const { proxy } = <any>getCurrentInstance();
+		const store = useStore();
+		const route = useRoute();
+		const state = reactive<IndexState>({
+			menuList: [],
+		});
+		// 设置 logo 显示/隐藏
+		const setIsShowLogo = computed(() => {
+			let { isShowLogo, layout } = store.state.themeConfig.themeConfig;
+			return (isShowLogo && layout === 'classic') || (isShowLogo && layout === 'transverse');
+		});
+		// 设置是否显示横向导航菜单
+		const isLayoutTransverse = computed(() => {
+			let { layout, isClassicSplitMenu } = store.state.themeConfig.themeConfig;
+			return layout === 'transverse' || (isClassicSplitMenu && layout === 'classic');
+		});
+		// 设置/过滤路由(非静态路由/是否显示在菜单中)
+		const setFilterRoutes = () => {
+			let { layout, isClassicSplitMenu } = store.state.themeConfig.themeConfig;
+			if (layout === 'classic' && isClassicSplitMenu) {
+				state.menuList = delClassicChildren(filterRoutesFun(store.state.routesList.routesList));
+				const resData = setSendClassicChildren(route.path);
+				proxy.mittBus.emit('setSendClassicChildren', resData);
+			} else {
+				state.menuList = filterRoutesFun(store.state.routesList.routesList);
+			}
+		};
+		// 设置了分割菜单时,删除底下 children
+		const delClassicChildren = (arr: Array<object>) => {
+			arr.map((v: any) => {
+				if (v.children) delete v.children;
+			});
+			return arr;
+		};
+		// 路由过滤递归函数
+		const filterRoutesFun = (arr: Array<object>) => {
+			return arr
+				.filter((item: any) => !item.meta.isHide)
+				.map((item: any) => {
+					item = Object.assign({}, item);
+					if (item.children) item.children = filterRoutesFun(item.children);
+					return item;
+				});
+		};
+		// 传送当前子级数据到菜单中
+		const setSendClassicChildren = (path: string) => {
+			const currentPathSplit = path.split('/');
+			let currentData: any = {};
+			filterRoutesFun(store.state.routesList.routesList).map((v, k) => {
+				if (v.path === `/${currentPathSplit[1]}`) {
+					v['k'] = k;
+					currentData['item'] = [{ ...v }];
+					currentData['children'] = [{ ...v }];
+					if (v.children) currentData['children'] = v.children;
+				}
+			});
+			return currentData;
+		};
+		// 页面加载时
+		onMounted(() => {
+			setFilterRoutes();
+			proxy.mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
+				setFilterRoutes();
+			});
+		});
+		// 页面卸载时
+		onUnmounted(() => {
+			proxy.mittBus.off('getBreadcrumbIndexSetFilterRoutes');
+		});
+		return {
+			setIsShowLogo,
+			isLayoutTransverse,
+			...toRefs(state),
+		};
+	},
+});
+</script>
+
+<style scoped lang="scss">
+.layout-navbars-breadcrumb-index {
+	height: 50px;
+	display: flex;
+	align-items: center;
+	padding-right: 15px;
+	background: var(--next-bg-topBar);
+	border-bottom: 1px solid var(--next-border-color-light);
+}
+</style>

+ 134 - 0
nextadmin_vue3/src/layout/navBars/breadcrumb/search.vue

@@ -0,0 +1,134 @@
+<template>
+	<div class="layout-search-dialog">
+		<el-dialog v-model="isShowSearch" width="300px" destroy-on-close :modal="false" fullscreen :show-close="false">
+			<el-autocomplete
+				v-model="menuQuery"
+				:fetch-suggestions="menuSearch"
+				:placeholder="$t('message.user.searchPlaceholder')"
+				ref="layoutMenuAutocompleteRef"
+				@select="onHandleSelect"
+				@blur="onSearchBlur"
+			>
+				<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>
+		</el-dialog>
+	</div>
+</template>
+
+<script lang="ts">
+import { reactive, toRefs, defineComponent, ref, nextTick } from 'vue';
+import { useRouter } from 'vue-router';
+import { useI18n } from 'vue-i18n';
+import { useStore } from '/@/store/index';
+
+// 定义接口来定义对象的类型
+interface SearchState {
+	isShowSearch: boolean;
+	menuQuery: string;
+	tagsViewList: object[];
+}
+interface Restaurant {
+	path: string;
+	meta: {
+		title: string;
+	};
+}
+
+export default defineComponent({
+	name: 'layoutBreadcrumbSearch',
+	setup() {
+		const layoutMenuAutocompleteRef = ref();
+		const { t } = useI18n();
+		const store = useStore();
+		const router = useRouter();
+		const state = reactive<SearchState>({
+			isShowSearch: false,
+			menuQuery: '',
+			tagsViewList: [],
+		});
+		// 搜索弹窗打开
+		const openSearch = () => {
+			state.menuQuery = '';
+			state.isShowSearch = true;
+			initTageView();
+			nextTick(() => {
+				layoutMenuAutocompleteRef.value.focus();
+			});
+		};
+		// 搜索弹窗关闭
+		const closeSearch = () => {
+			state.isShowSearch = false;
+		};
+		// 菜单搜索数据过滤
+		const menuSearch = (queryString: string, cb: Function) => {
+			let results = queryString ? state.tagsViewList.filter(createFilter(queryString)) : state.tagsViewList;
+			cb(results);
+		};
+		// 菜单搜索过滤
+		const createFilter: any = (queryString: string) => {
+			return (restaurant: Restaurant) => {
+				return (
+					restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
+					restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
+					t(restaurant.meta.title).indexOf(queryString.toLowerCase()) > -1
+				);
+			};
+		};
+		// 初始化菜单数据
+		const initTageView = () => {
+			if (state.tagsViewList.length > 0) return false;
+			store.state.tagsViewRoutes.tagsViewRoutes.map((v: any) => {
+				if (!v.meta.isHide) state.tagsViewList.push({ ...v });
+			});
+		};
+		// 当前菜单选中时
+		const onHandleSelect = (item: any) => {
+			let { path, redirect } = item;
+			if (item.meta.isLink && !item.meta.isIframe) window.open(item.meta.isLink);
+			else if (redirect) router.push(redirect);
+			else router.push(path);
+			closeSearch();
+		};
+		// input 失去焦点时
+		const onSearchBlur = () => {
+			closeSearch();
+		};
+		return {
+			layoutMenuAutocompleteRef,
+			openSearch,
+			closeSearch,
+			menuSearch,
+			onHandleSelect,
+			onSearchBlur,
+			...toRefs(state),
+		};
+	},
+});
+</script>
+
+<style scoped lang="scss">
+.layout-search-dialog {
+	::v-deep(.el-dialog) {
+		box-shadow: unset !important;
+		border-radius: 0 !important;
+		background: rgba(0, 0, 0, 0.5);
+	}
+	::v-deep(.el-autocomplete) {
+		width: 560px;
+		position: absolute;
+		top: 100px;
+		left: 50%;
+		transform: translateX(-50%);
+	}
+}
+</style>

+ 792 - 0
nextadmin_vue3/src/layout/navBars/breadcrumb/setings.vue

@@ -0,0 +1,792 @@
+<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"   @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"   @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"   @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"   @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"   @change="onBgColorPickerChange('menuBarColor')"> </el-color-picker>
+					</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"
+							 
+							@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"
+							 
+							@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>
+
+				<!-- 界面设置 -->
+				<el-divider content-position="left">{{ $t('message.layout.threeTitle') }}</el-divider>
+				<div class="layout-breadcrumb-seting-bar-flex">
+					<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" size="small" @change="onThemeConfigChange"></el-switch>
+					</div>
+				</div>
+				<div class="layout-breadcrumb-seting-bar-flex mt15">
+					<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" 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"
+							 
+							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: isMobile ? 0.5 : 1 }">
+					<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsSortableTagsView') }}</div>
+					<div class="layout-breadcrumb-seting-bar-flex-value">
+						<el-switch
+							v-model="getThemeConfig.isSortableTagsView"
+							:disabled="isMobile ? true : false"
+							size="small"
+							@change="onSortableTagsViewChange"
+						></el-switch>
+					</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"   style="width: 90px" @input="onWartermarkTextInput($event)"></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="请选择"   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="请选择"   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="请选择"
+							 
+							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="请选择"
+							 
+							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   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   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 lang="ts">
+import { nextTick, onUnmounted, onMounted, getCurrentInstance, defineComponent, computed, reactive, toRefs } from 'vue';
+import { useStore } from '/@/store/index';
+import { getLightColor } 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';
+export default defineComponent({
+	name: 'layoutBreadcrumbSeting',
+	setup() {
+		const { proxy } = <any>getCurrentInstance();
+		const store = useStore();
+		const { copyText } = commonFunction();
+		const state = reactive({
+			isMobile: false,
+		});
+		// 获取布局配置信息
+		const getThemeConfig = computed(() => {
+			return store.state.themeConfig.themeConfig;
+		});
+		// 1、全局主题
+		const onColorPickerChange = () => {
+			document.documentElement.style.setProperty('--el-color-primary', getThemeConfig.value.primary);
+			for (let i = 1; i <= 9; i++) {
+				document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(getThemeConfig.value.primary, i / 10)}`);
+			}
+			setDispatchThemeConfig();
+		};
+		// 2、菜单 / 顶栏
+		const onBgColorPickerChange = (bg: string) => {
+			document.documentElement.style.setProperty(`--next-bg-${bg}`, (<any>getThemeConfig.value)[bg]);
+			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);
+		};
+		// 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();
+			proxy.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 = () => {
+			proxy.mittBus.emit('openOrCloseSortable');
+			setLocalThemeConfig();
+		};
+		// 4、界面显示 --> 开启 TagsView 共用
+		const onShareTagsViewChange = () => {
+			proxy.mittBus.emit('openShareTagsView');
+			setLocalThemeConfig();
+		};
+		// 4、界面显示 --> 灰色模式/色弱模式
+		const onAddFilterChange = (attr: string) => {
+			if (attr === 'grayscale') {
+				if (getThemeConfig.value.isGrayscale) getThemeConfig.value.isInvert = false;
+			} else {
+				if (getThemeConfig.value.isInvert) getThemeConfig.value.isGrayscale = false;
+			}
+			const cssAttr =
+				attr === 'grayscale' ? `grayscale(${getThemeConfig.value.isGrayscale ? 1 : 0})` : `invert(${getThemeConfig.value.isInvert ? '80%' : '0%'})`;
+			const appEle: any = document.body;
+			appEle.setAttribute('style', `filter: ${cssAttr}`);
+			setLocalThemeConfig();
+		};
+		// 4、界面显示 --> 深色模式
+		const onAddDarkChange = () => {
+			const body = document.documentElement as HTMLElement;
+			if (getThemeConfig.value.isIsDark) body.setAttribute('data-theme', 'dark');
+			else body.setAttribute('data-theme', '');
+		};
+		// 4、界面显示 --> 开启水印
+		const onWartermarkChange = () => {
+			getThemeConfig.value.isWartermark ? Watermark.set(getThemeConfig.value.wartermarkText) : Watermark.del();
+			setLocalThemeConfig();
+		};
+		// 4、界面显示 --> 水印文案
+		const onWartermarkTextInput = (val: 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;
+			getThemeConfig.value.layout = layout;
+			getThemeConfig.value.isDrawer = false;
+			initLayoutChangeFun();
+		};
+		// 设置布局切换函数
+		const initLayoutChangeFun = () => {
+			onBgColorPickerChange('menuBar');
+			onBgColorPickerChange('menuBarColor');
+			onBgColorPickerChange('topBar');
+			onBgColorPickerChange('topBarColor');
+			onBgColorPickerChange('columnsMenuBar');
+			onBgColorPickerChange('columnsMenuBarColor');
+		};
+		// 关闭弹窗时,初始化变量。变量用于处理 proxy.$refs.layoutScrollbarRef.update()
+		const onDrawerClose = () => {
+			getThemeConfig.value.isFixedHeaderChange = false;
+			getThemeConfig.value.isShowLogoChange = false;
+			getThemeConfig.value.isDrawer = false;
+			setLocalThemeConfig();
+		};
+		// 布局配置弹窗打开
+		const openDrawer = () => {
+			getThemeConfig.value.isDrawer = true;
+		};
+		// 触发 store 布局配置更新
+		const setDispatchThemeConfig = () => {
+			setLocalThemeConfig();
+			setLocalThemeConfigStyle();
+		};
+		// 存储布局配置
+		const setLocalThemeConfig = () => {
+			Local.remove('themeConfig');
+			Local.set('themeConfig', getThemeConfig.value);
+		};
+		// 存储布局配置全局主题样式(html根标签)
+		const setLocalThemeConfigStyle = () => {
+			Local.set('themeConfigStyle', document.documentElement.style.cssText);
+		};
+		// 一键复制配置
+		const onCopyConfigClick = () => {
+			let copyThemeConfig = Local.get('themeConfig');
+			copyThemeConfig.isDrawer = false;
+			copyText(JSON.stringify(copyThemeConfig)).then(() => {
+				getThemeConfig.value.isDrawer = false;
+			});
+		};
+		// 一键恢复默认
+		const onResetConfigClick = () => {
+			Local.clear();
+			window.location.reload();
+		};
+		// 初始化菜单样式等
+		const initSetStyle = () => {
+			// 2、菜单 / 顶栏 --> 顶栏背景渐变
+			onTopBarGradualChange();
+			// 2、菜单 / 顶栏 --> 菜单背景渐变
+			onMenuBarGradualChange();
+			// 2、菜单 / 顶栏 --> 分栏菜单背景渐变
+			onColumnsMenuBarGradualChange();
+		};
+		onMounted(() => {
+			nextTick(() => {
+				// 判断当前布局是否不相同,不相同则初始化当前布局的样式,防止监听窗口大小改变时,布局配置logo、菜单背景等部分布局失效问题
+				if (!Local.get('frequency')) initLayoutChangeFun();
+				Local.set('frequency', 1);
+				// 监听窗口大小改变,非默认布局,设置成默认布局(适配移动端)
+				proxy.mittBus.on('layoutMobileResize', (res: any) => {
+					getThemeConfig.value.layout = res.layout;
+					getThemeConfig.value.isDrawer = false;
+					initLayoutChangeFun();
+					state.isMobile = other.isMobile();
+				});
+				setTimeout(() => {
+					// 灰色模式
+					if (getThemeConfig.value.isGrayscale) onAddFilterChange('grayscale');
+					// 色弱模式
+					if (getThemeConfig.value.isInvert) onAddFilterChange('invert');
+					// 深色模式
+					if (getThemeConfig.value.isIsDark) onAddDarkChange();
+					// 开启水印
+					onWartermarkChange();
+					// 语言国际化
+					if (Local.get('themeConfig')) proxy.$i18n.locale = Local.get('themeConfig').globalI18n;
+					// 初始化菜单样式等
+					initSetStyle();
+				}, 100);
+			});
+		});
+		onUnmounted(() => {
+			proxy.mittBus.off('layoutMobileResize');
+		});
+		return {
+			openDrawer,
+			onColorPickerChange,
+			onBgColorPickerChange,
+			onTopBarGradualChange,
+			onMenuBarGradualChange,
+			onColumnsMenuBarGradualChange,
+			onThemeConfigChange,
+			onIsFixedHeaderChange,
+			onIsShowLogoChange,
+			getThemeConfig,
+			onDrawerClose,
+			onAddFilterChange,
+			onAddDarkChange,
+			onWartermarkChange,
+			onWartermarkTextInput,
+			onSetLayout,
+			setLocalThemeConfig,
+			onClassicSplitMenuChange,
+			onIsBreadcrumbChange,
+			onSortableTagsViewChange,
+			onShareTagsViewChange,
+			onCopyConfigClick,
+			onResetConfigClick,
+			...toRefs(state),
+		};
+	},
+});
+</script>
+
+<style scoped lang="scss">
+.layout-breadcrumb-seting-bar {
+	height: calc(100vh - 50px);
+	padding: 0 15px;
+	::v-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-4);
+				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-4);
+					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-4);
+						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>

+ 295 - 0
nextadmin_vue3/src/layout/navBars/breadcrumb/user.vue

@@ -0,0 +1,295 @@
+<template>
+	<div class="layout-navbars-breadcrumb-user" :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="disabledSize === 'large'">{{ $t('message.user.dropdownLarge') }}</el-dropdown-item>
+					<el-dropdown-item command="default" :disabled="disabledSize === 'default'">{{ $t('message.user.dropdownDefault') }}</el-dropdown-item>
+					<el-dropdown-item command="small" :disabled="disabledSize === 'small'">{{ $t('message.user.dropdownSmall') }}</el-dropdown-item>
+				</el-dropdown-menu>
+			</template>
+		</el-dropdown>
+		<el-dropdown :show-timeout="70" :hide-timeout="50" trigger="click" @command="onLanguageChange">
+			<div class="layout-navbars-breadcrumb-user-icon">
+				<i class="iconfont" :class="disabledI18n === 'en' ? 'icon-fuhao-yingwen' : 'icon-fuhao-zhongwen'" :title="$t('message.user.title1')"></i>
+			</div>
+			<template #dropdown>
+				<el-dropdown-menu>
+					<el-dropdown-item command="zh-cn" :disabled="disabledI18n === 'zh-cn'">简体中文</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" :width="300">
+				<template #reference>
+					<el-badge :is-dot="true">
+						<el-icon :title="$t('message.user.title4')">
+							<ele-Bell />
+						</el-icon>
+					</el-badge>
+				</template>
+				<UserNews />
+			</el-popover>
+		</div>
+		<div class="layout-navbars-breadcrumb-user-icon mr10" @click="onScreenfullClick">
+			<i
+				class="iconfont"
+				:title="isScreenfull ? $t('message.user.title6') : $t('message.user.title5')"
+				:class="!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="getUserInfos.photo" class="layout-navbars-breadcrumb-user-link-photo mr5" />
+				{{ getUserInfos.userName === '' ? 'common' : getUserInfos.userName }}
+				<el-icon class="el-icon--right">
+					<ele-ArrowDown />
+				</el-icon>
+			</span>
+			<template #dropdown>
+				<el-dropdown-menu>
+					<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 lang="ts">
+import { ref, getCurrentInstance, computed, reactive, toRefs, onMounted, defineComponent } from 'vue';
+import { useRouter } from 'vue-router';
+import { ElMessageBox, ElMessage } from 'element-plus';
+import screenfull from 'screenfull';
+import { useI18n } from 'vue-i18n';
+import { resetRoute } from '/@/router/index';
+import { useStore } from '/@/store/index';
+import other from '/@/utils/other';
+import { Session, Local } from '/@/utils/storage';
+import UserNews from '/@/layout/navBars/breadcrumb/userNews.vue';
+import Search from '/@/layout/navBars/breadcrumb/search.vue';
+export default defineComponent({
+	name: 'layoutBreadcrumbUser',
+	components: { UserNews, Search },
+	setup() {
+		const { t } = useI18n();
+		const { proxy } = <any>getCurrentInstance();
+		const router = useRouter();
+		const store = useStore();
+		const searchRef = ref();
+		const state = reactive({
+			isScreenfull: false,
+			disabledI18n: 'zh-cn',
+			disabledSize: 'large',
+		});
+		// 获取用户信息 vuex
+		const getUserInfos = computed(() => {
+			return <any>store.state.userInfos.userInfos;
+		});
+		// 获取布局配置信息
+		const getThemeConfig = computed(() => {
+			return store.state.themeConfig.themeConfig;
+		});
+		// 设置分割样式
+		const layoutUserFlexNum = computed(() => {
+			let num: string | number = '';
+			const { layout, isClassicSplitMenu } = getThemeConfig.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 = () => {
+			proxy.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 () => {
+						Session.clear(); // 清除缓存/token等
+						await resetRoute(); // 删除/重置路由
+						ElMessage.success(t('message.user.logOutSuccess'));
+						setTimeout(() => {
+							window.location.href = ''; // 去登录页
+						}, 500);
+					})
+					.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');
+			getThemeConfig.value.globalComponentSize = size;
+			Local.set('themeConfig', getThemeConfig.value);
+			initComponentSize();
+			window.location.reload();
+		};
+		// 语言切换
+		const onLanguageChange = (lang: string) => {
+			Local.remove('themeConfig');
+			getThemeConfig.value.globalI18n = lang;
+			Local.set('themeConfig', getThemeConfig.value);
+			proxy.$i18n.locale = lang;
+			initI18n();
+			other.useTitle();
+		};
+		// 设置 element plus 组件的国际化
+		const setI18nConfig = (locale: string) => {
+			proxy.mittBus.emit('getI18nConfig', proxy.$i18n.messages[locale]);
+		};
+		// 初始化言语国际化
+		const initI18n = () => {
+			switch (Local.get('themeConfig').globalI18n) {
+				case 'zh-cn':
+					state.disabledI18n = 'zh-cn';
+					setI18nConfig('zh-cn');
+					break;
+				case 'en':
+					state.disabledI18n = 'en';
+					setI18nConfig('en');
+					break;
+				case 'zh-tw':
+					state.disabledI18n = 'zh-tw';
+					setI18nConfig('zh-tw');
+					break;
+			}
+		};
+		// 初始化全局组件大小
+		const initComponentSize = () => {
+			switch (Local.get('themeConfig').globalComponentSize) {
+				case 'large':
+					state.disabledSize = 'large';
+					break;
+				case 'default':
+					state.disabledSize = 'default';
+					break;
+				case 'small':
+					state.disabledSize = 'small';
+					break;
+			}
+		};
+		// 页面加载时
+		onMounted(() => {
+			if (Local.get('themeConfig')) {
+				initI18n();
+				initComponentSize();
+			}
+		});
+		return {
+			getUserInfos,
+			onLayoutSetingClick,
+			onHandleCommandClick,
+			onScreenfullClick,
+			onSearchClick,
+			onComponentSizeChange,
+			onLanguageChange,
+			searchRef,
+			layoutUserFlexNum,
+			...toRefs(state),
+		};
+	},
+});
+</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;
+			}
+		}
+	}
+	::v-deep(.el-dropdown) {
+		color: var(--next-bg-topBarColor);
+	}
+	::v-deep(.el-badge) {
+		height: 40px;
+		line-height: 40px;
+		display: flex;
+		align-items: center;
+	}
+	::v-deep(.el-badge__content.is-fixed) {
+		top: 12px;
+	}
+}
+</style>

+ 114 - 0
nextadmin_vue3/src/layout/navBars/breadcrumb/userNews.vue

@@ -0,0 +1,114 @@
+<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="newsList.length > 0" @click="onAllReadClick">{{ $t('message.user.newBtn') }}</div>
+		</div>
+		<div class="content-box">
+			<template v-if="newsList.length > 0">
+				<div class="content-box-item" v-for="(v, k) in 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="newsList.length > 0">{{ $t('message.user.newGo') }}</div>
+	</div>
+</template>
+
+<script lang="ts">
+import { reactive, toRefs, defineComponent } from 'vue';
+export default defineComponent({
+	name: 'layoutBreadcrumbUserNews',
+	setup() {
+		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',
+				},
+			],
+		});
+		// 全部已读点击
+		const onAllReadClick = () => {
+			state.newsList = [];
+		};
+		// 前往通知中心点击
+		const onGoToGiteeClick = () => {
+			window.open('https://gitee.com/lyt-top/vue-next-admin');
+		};
+		return {
+			onAllReadClick,
+			onGoToGiteeClick,
+			...toRefs(state),
+		};
+	},
+});
+</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;
+		}
+	}
+	::v-deep(.el-empty__description p) {
+		font-size: 13px;
+	}
+}
+</style>

+ 37 - 0
nextadmin_vue3/src/layout/navBars/index.vue

@@ -0,0 +1,37 @@
+<template>
+	<div class="layout-navbars-container">
+		<BreadcrumbIndex />
+		<TagsView v-if="setShowTagsView" />
+	</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import { useStore } from '/@/store/index';
+import BreadcrumbIndex from '/@/layout/navBars/breadcrumb/index.vue';
+import TagsView from '/@/layout/navBars/tagsView/tagsView.vue';
+export default defineComponent({
+	name: 'layoutNavBars',
+	components: { BreadcrumbIndex, TagsView },
+	setup() {
+		const store = useStore();
+		// 是否显示 tagsView
+		const setShowTagsView = computed(() => {
+			let { layout, isTagsview } = store.state.themeConfig.themeConfig;
+			return layout !== 'classic' && isTagsview;
+		});
+		return {
+			setShowTagsView,
+		};
+	},
+});
+</script>
+
+<style scoped lang="scss">
+.layout-navbars-container {
+	display: flex;
+	flex-direction: column;
+	width: 100%;
+	height: 100%;
+}
+</style>

+ 137 - 0
nextadmin_vue3/src/layout/navBars/tagsView/contextmenu.vue

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

+ 681 - 0
nextadmin_vue3/src/layout/navBars/tagsView/tagsView.vue

@@ -0,0 +1,681 @@
+<template>
+	<div class="layout-navbars-tagsview" :class="{ 'layout-navbars-tagsview-shadow': getThemeConfig.layout === 'classic' }">
+		<el-scrollbar ref="scrollbarRef" @wheel.native.prevent="onHandleScroll">
+			<ul class="layout-navbars-tagsview-ul" :class="setTagsStyle" ref="tagsUlRef">
+				<li
+					v-for="(v, k) in tagsViewList"
+					:key="k"
+					class="layout-navbars-tagsview-ul-li"
+					:data-url="v.url"
+					:class="{ 'is-active': isActive(v) }"
+					@contextmenu.prevent="onContextmenu(v, $event)"
+					@click="onTagsClick(v, k)"
+					:ref="
+						(el) => {
+							if (el) tagsRefs[k] = el;
+						}
+					"
+				>
+					<i class="iconfont icon-webicon318 layout-navbars-tagsview-ul-li-iconfont font14" v-if="isActive(v)"></i>
+					<SvgIcon :name="v.meta.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="!isActive(v) && getThemeConfig.isTagsviewIcon" />
+					<span>{{ $t(v.meta.title) }}</span>
+					<template v-if="isActive(v)">
+						<SvgIcon
+							name="ele-RefreshRight"
+							class="ml5 layout-navbars-tagsview-ul-li-refresh"
+							@click.stop="refreshCurrentTagsView($route.fullPath)"
+						/>
+						<SvgIcon
+							name="ele-Close"
+							class="layout-navbars-tagsview-ul-li-icon layout-icon-active"
+							v-if="!v.meta.isAffix"
+							@click.stop="closeCurrentTagsView(getThemeConfig.isShareTagsView ? v.path : v.url)"
+						/>
+					</template>
+					<SvgIcon
+						name="ele-Close"
+						class="layout-navbars-tagsview-ul-li-icon layout-icon-three"
+						v-if="!v.meta.isAffix"
+						@click.stop="closeCurrentTagsView(getThemeConfig.isShareTagsView ? v.path : v.url)"
+					/>
+				</li>
+			</ul>
+		</el-scrollbar>
+		<Contextmenu :dropdown="dropdown" ref="contextmenuRef" @currentContextmenuClick="onCurrentContextmenuClick" />
+	</div>
+</template>
+
+<script lang="ts">
+import {
+	toRefs,
+	reactive,
+	onMounted,
+	computed,
+	ref,
+	nextTick,
+	onBeforeUpdate,
+	onBeforeMount,
+	onUnmounted,
+	getCurrentInstance,
+	watch,
+	defineComponent,
+} from 'vue';
+import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
+import Sortable from 'sortablejs';
+import { ElMessage } from 'element-plus';
+import { useStore } from '/@/store/index';
+import { Session } from '/@/utils/storage';
+import { isObjectValueEqual } from '/@/utils/arrayOperation';
+import other from '/@/utils/other';
+import Contextmenu from '/@/layout/navBars/tagsView/contextmenu.vue';
+
+// 定义接口来定义对象的类型
+interface TagsViewState {
+	routeActive: string;
+	routePath: string | unknown;
+	dropdown: {
+		x: string | number;
+		y: string | number;
+	};
+	tagsRefsIndex: number;
+	tagsViewList: any[];
+	sortable: any;
+	tagsViewRoutesList: any[];
+}
+interface RouteParams {
+	path: string;
+	url: string;
+}
+interface CurrentContextmenu {
+	meta: {
+		isDynamic: boolean;
+	};
+	params: any;
+	query: any;
+	path: string;
+	contextMenuClickId: string | number;
+}
+
+export default defineComponent({
+	name: 'layoutTagsView',
+	components: { Contextmenu },
+	setup() {
+		const { proxy } = <any>getCurrentInstance();
+		const tagsRefs = ref<any[]>([]);
+		const scrollbarRef = ref();
+		const contextmenuRef = ref();
+		const tagsUlRef = ref();
+		const store = useStore();
+		const route = useRoute();
+		const router = useRouter();
+		const state = reactive<TagsViewState>({
+			routeActive: '',
+			routePath: route.path,
+			dropdown: { x: '', y: '' },
+			tagsRefsIndex: 0,
+			tagsViewList: [],
+			sortable: '',
+			tagsViewRoutesList: [],
+		});
+		// 动态设置 tagsView 风格样式
+		const setTagsStyle = computed(() => {
+			return store.state.themeConfig.themeConfig.tagsStyle;
+		});
+		// 获取布局配置信息
+		const getThemeConfig = computed(() => {
+			return store.state.themeConfig.themeConfig;
+		});
+		// 设置 tagsView 高亮
+		const isActive = (v: RouteParams) => {
+			if (getThemeConfig.value.isShareTagsView) {
+				return v.path === state.routePath;
+			} else {
+				return v.url ? v.url === state.routeActive : v.path === state.routeActive;
+			}
+		};
+		// 存储 tagsViewList 到浏览器临时缓存中,页面刷新时,保留记录
+		const addBrowserSetSession = (tagsViewList: Array<object>) => {
+			Session.set('tagsViewList', tagsViewList);
+		};
+		// 获取 vuex 中的 tagsViewRoutes 列表
+		const getTagsViewRoutes = async () => {
+			state.routeActive = await setTagsViewHighlight(route);
+			state.routePath = (await route.meta.isDynamic) ? route.meta.isDynamicPath : route.path;
+			state.tagsViewList = [];
+			state.tagsViewRoutesList = store.state.tagsViewRoutes.tagsViewRoutes;
+			initTagsView();
+		};
+		// vuex 中获取路由信息:如果是设置了固定的(isAffix),进行初始化显示
+		const initTagsView = async () => {
+			if (Session.get('tagsViewList') && getThemeConfig.value.isCacheTagsView) {
+				state.tagsViewList = await Session.get('tagsViewList');
+			} else {
+				await state.tagsViewRoutesList.map((v: any) => {
+					if (v.meta.isAffix && !v.meta.isHide) {
+						v.url = setTagsViewHighlight(v);
+						state.tagsViewList.push({ ...v });
+					}
+				});
+				await addTagsView(route.path, route);
+			}
+			// 初始化当前元素(li)的下标
+			getTagsRefsIndex(getThemeConfig.value.isShareTagsView ? state.routePath : state.routeActive);
+		};
+		// 处理可开启多标签详情,单标签详情(动态路由(xxx/:id/:name"),普通路由处理)
+		const solveAddTagsView = async (path: string, to?: any) => {
+			let isDynamicPath = to.meta.isDynamic ? to.meta.isDynamicPath : path;
+			let current = state.tagsViewList.filter(
+				(v: any) =>
+					v.path === isDynamicPath &&
+					isObjectValueEqual(
+						to.meta.isDynamic ? (v.params ? v.params : null) : v.query ? v.query : null,
+						to.meta.isDynamic ? (to?.params ? to?.params : null) : to?.query ? to?.query : null
+					)
+			);
+			if (current.length <= 0) {
+				// 防止:Avoid app logic that relies on enumerating keys on a component instance. The keys will be empty in production mode to avoid performance overhead.
+				let findItem = state.tagsViewRoutesList.find((v: any) => v.path === isDynamicPath);
+				if (findItem.meta.isAffix) return false;
+				if (findItem.meta.isLink && !findItem.meta.isIframe) return false;
+				to.meta.isDynamic ? (findItem.params = to.params) : (findItem.query = to.query);
+				findItem.url = setTagsViewHighlight(findItem);
+				state.tagsViewList.push({ ...findItem });
+				addBrowserSetSession(state.tagsViewList);
+			}
+		};
+		// 处理单标签时,第二次的值未覆盖第一次的 tagsViewList 值(Session Storage)
+		const singleAddTagsView = (path: string, to?: any) => {
+			let isDynamicPath = to.meta.isDynamic ? to.meta.isDynamicPath : path;
+			state.tagsViewList.forEach((v) => {
+				if (
+					v.path === isDynamicPath &&
+					!isObjectValueEqual(
+						to.meta.isDynamic ? (v.params ? v.params : null) : v.query ? v.query : null,
+						to.meta.isDynamic ? (to?.params ? to?.params : null) : to?.query ? to?.query : null
+					)
+				) {
+					to.meta.isDynamic ? (v.params = to.params) : (v.query = to.query);
+					v.url = setTagsViewHighlight(v);
+					addBrowserSetSession(state.tagsViewList);
+				}
+			});
+		};
+		// 1、添加 tagsView:未设置隐藏(isHide)也添加到在 tagsView 中(可开启多标签详情,单标签详情)
+		const addTagsView = (path: string, to?: any) => {
+			// 防止拿取不到路由信息
+			nextTick(async () => {
+				// 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
+				let item: any = '';
+				if (to && to.meta.isDynamic) {
+					// 动态路由(xxx/:id/:name"):参数不同,开启多个 tagsview
+					if (!getThemeConfig.value.isShareTagsView) await solveAddTagsView(path, to);
+					else await singleAddTagsView(path, to);
+					if (state.tagsViewList.some((v: any) => v.path === to.meta.isDynamicPath)) return false;
+					item = state.tagsViewRoutesList.find((v: any) => v.path === to.meta.isDynamicPath);
+				} else {
+					// 普通路由:参数不同,开启多个 tagsview
+					if (!getThemeConfig.value.isShareTagsView) await solveAddTagsView(path, to);
+					else await singleAddTagsView(path, to);
+					if (state.tagsViewList.some((v: any) => v.path === path)) return false;
+					item = state.tagsViewRoutesList.find((v: any) => v.path === path);
+				}
+				if (item.meta.isLink && !item.meta.isIframe) return false;
+				if (to && to.meta.isDynamic) item.params = to?.params ? to?.params : route.params;
+				else item.query = to?.query ? to?.query : route.query;
+				item.url = setTagsViewHighlight(item);
+				await state.tagsViewList.push({ ...item });
+				await addBrowserSetSession(state.tagsViewList);
+			});
+		};
+		// 2、刷新当前 tagsView:
+		const refreshCurrentTagsView = (fullPath: string) => {
+			proxy.mittBus.emit('onTagsViewRefreshRouterView', fullPath);
+		};
+		// 3、关闭当前 tagsView:如果是设置了固定的(isAffix),不可以关闭
+		const closeCurrentTagsView = (path: string) => {
+			state.tagsViewList.map((v: any, k: number, arr: any) => {
+				if (!v.meta.isAffix) {
+					if (getThemeConfig.value.isShareTagsView ? v.path === path : v.url === path) {
+						state.tagsViewList.splice(k, 1);
+						setTimeout(() => {
+							if (state.tagsViewList.length === k && getThemeConfig.value.isShareTagsView ? state.routePath === path : state.routeActive === path) {
+								// 最后一个且高亮时
+								if (arr[arr.length - 1].meta.isDynamic) {
+									// 动态路由(xxx/:id/:name")
+									if (k !== arr.length) router.push({ name: arr[k].name, params: arr[k].params });
+									else router.push({ name: arr[arr.length - 1].name, params: arr[arr.length - 1].params });
+								} else {
+									// 普通路由
+									if (k !== arr.length) router.push({ path: arr[k].path, query: arr[k].query });
+									else router.push({ path: arr[arr.length - 1].path, query: arr[arr.length - 1].query });
+								}
+							} else {
+								// 非最后一个且高亮时,跳转到下一个
+								if (state.tagsViewList.length !== k && getThemeConfig.value.isShareTagsView ? state.routePath === path : state.routeActive === path) {
+									if (arr[k].meta.isDynamic) {
+										// 动态路由(xxx/:id/:name")
+										router.push({ name: arr[k].name, params: arr[k].params });
+									} else {
+										// 普通路由
+										router.push({ path: arr[k].path, query: arr[k].query });
+									}
+								}
+							}
+						}, 0);
+					}
+				}
+			});
+			addBrowserSetSession(state.tagsViewList);
+		};
+		// 4、关闭其它 tagsView:如果是设置了固定的(isAffix),不进行关闭
+		const closeOtherTagsView = (path: string) => {
+			state.tagsViewList = [];
+			state.tagsViewRoutesList.map((v: any) => {
+				if (v.meta.isAffix && !v.meta.isHide) state.tagsViewList.push({ ...v });
+			});
+			addTagsView(path, route);
+		};
+		// 5、关闭全部 tagsView:如果是设置了固定的(isAffix),不进行关闭
+		const closeAllTagsView = () => {
+			state.tagsViewList = [];
+			state.tagsViewRoutesList.map((v: any) => {
+				if (v.meta.isAffix && !v.meta.isHide) {
+					state.tagsViewList.push({ ...v });
+					router.push({ path: state.tagsViewList[state.tagsViewList.length - 1].path });
+				}
+			});
+			addBrowserSetSession(state.tagsViewList);
+		};
+		// 6、开启当前页面全屏
+		const openCurrenFullscreen = async (path: string) => {
+			const item = state.tagsViewList.find((v: any) => (getThemeConfig.value.isShareTagsView ? v.path === path : v.url === path));
+			if (item.meta.isDynamic) await router.push({ name: item.name, params: item.params });
+			else await router.push({ name: item.name, query: item.query });
+			store.dispatch('tagsViewRoutes/setCurrenFullscreen', true);
+		};
+		// 当前项右键菜单点击,拿当前点击的路由路径对比 浏览器缓存中的 tagsView 路由数组,取当前点击项的详细路由信息
+		// 防止 tagsView 非当前页演示时,操作异常
+		const getCurrentRouteItem = (path: string, cParams: any) => {
+			const itemRoute = Session.get('tagsViewList') ? Session.get('tagsViewList') : state.tagsViewList;
+			return itemRoute.find((v: any) => {
+				if (
+					v.path === path &&
+					isObjectValueEqual(
+						v.meta.isDynamic ? (v.params ? v.params : null) : v.query ? v.query : null,
+						cParams && Object.keys(cParams ? cParams : {}).length > 0 ? cParams : null
+					)
+				) {
+					return v;
+				} else if (v.path === path && Object.keys(cParams ? cParams : {}).length <= 0) {
+					return v;
+				}
+			});
+		};
+		// 当前项右键菜单点击
+		const onCurrentContextmenuClick = async (item: CurrentContextmenu) => {
+			const cParams = item.meta.isDynamic ? item.params : item.query;
+			if (!getCurrentRouteItem(item.path, cParams)) return ElMessage({ type: 'warning', message: '请正确输入路径及完整参数(query、params)' });
+			const { path, name, params, query, meta, url } = getCurrentRouteItem(item.path, cParams);
+			switch (item.contextMenuClickId) {
+				case 0:
+					// 刷新当前
+					if (meta.isDynamic) await router.push({ name, params });
+					else await router.push({ path, query });
+					refreshCurrentTagsView(route.fullPath);
+					break;
+				case 1:
+					// 关闭当前
+					closeCurrentTagsView(getThemeConfig.value.isShareTagsView ? path : url);
+					break;
+				case 2:
+					// 关闭其它
+					if (meta.isDynamic) await router.push({ name, params });
+					else await router.push({ path, query });
+					closeOtherTagsView(path);
+					break;
+				case 3:
+					// 关闭全部
+					closeAllTagsView();
+					break;
+				case 4:
+					// 开启当前页面全屏
+					openCurrenFullscreen(getThemeConfig.value.isShareTagsView ? path : url);
+					break;
+			}
+		};
+		// 右键点击时:传 x,y 坐标值到子组件中(props)
+		const onContextmenu = (v: any, e: any) => {
+			const { clientX, clientY } = e;
+			state.dropdown.x = clientX;
+			state.dropdown.y = clientY;
+			contextmenuRef.value.openContextmenu(v);
+		};
+		// 当前的 tagsView 项点击时
+		const onTagsClick = (v: any, k: number) => {
+			state.tagsRefsIndex = k;
+			router.push(v);
+		};
+		// 处理 tagsView 高亮(多标签详情时使用,单标签详情未使用)
+		const setTagsViewHighlight = (v: any) => {
+			let params = v.query && Object.keys(v.query).length > 0 ? v.query : v.params;
+			if (!params || Object.keys(params).length <= 0) return v.path;
+			let path = '';
+			for (let i in params) {
+				path += params[i];
+			}
+			// 判断是否是动态路由(xxx/:id/:name")
+			return `${v.meta.isDynamic ? v.meta.isDynamicPath : v.path}-${path}`;
+		};
+		// 更新滚动条显示
+		const updateScrollbar = () => {
+			proxy.$refs.scrollbarRef.update();
+		};
+		// 鼠标滚轮滚动
+		const onHandleScroll = (e: any) => {
+			proxy.$refs.scrollbarRef.$refs.wrap$.scrollLeft += e.wheelDelta / 4;
+		};
+		// tagsView 横向滚动
+		const tagsViewmoveToCurrentTag = () => {
+			nextTick(() => {
+				if (tagsRefs.value.length <= 0) return false;
+				// 当前 li 元素
+				let liDom = tagsRefs.value[state.tagsRefsIndex];
+				// 当前 li 元素下标
+				let liIndex = state.tagsRefsIndex;
+				// 当前 ul 下 li 元素总长度
+				let liLength = tagsRefs.value.length;
+				// 最前 li
+				let liFirst: any = tagsRefs.value[0];
+				// 最后 li
+				let liLast: any = tagsRefs.value[tagsRefs.value.length - 1];
+				// 当前滚动条的值
+				let scrollRefs = proxy.$refs.scrollbarRef.$refs.wrap$;
+				// 当前滚动条滚动宽度
+				let scrollS = scrollRefs.scrollWidth;
+				// 当前滚动条偏移宽度
+				let offsetW = scrollRefs.offsetWidth;
+				// 当前滚动条偏移距离
+				let scrollL = scrollRefs.scrollLeft;
+				// 上一个 tags li dom
+				let liPrevTag: any = tagsRefs.value[state.tagsRefsIndex - 1];
+				// 下一个 tags li dom
+				let liNextTag: any = tagsRefs.value[state.tagsRefsIndex + 1];
+				// 上一个 tags li dom 的偏移距离
+				let beforePrevL: any = '';
+				// 下一个 tags li dom 的偏移距离
+				let afterNextL: any = '';
+				if (liDom === liFirst) {
+					// 头部
+					scrollRefs.scrollLeft = 0;
+				} else if (liDom === liLast) {
+					// 尾部
+					scrollRefs.scrollLeft = scrollS - offsetW;
+				} else {
+					// 非头/尾部
+					if (liIndex === 0) beforePrevL = liFirst.offsetLeft - 5;
+					else beforePrevL = liPrevTag?.offsetLeft - 5;
+					if (liIndex === liLength) afterNextL = liLast.offsetLeft + liLast.offsetWidth + 5;
+					else afterNextL = liNextTag.offsetLeft + liNextTag.offsetWidth + 5;
+					if (afterNextL > scrollL + offsetW) {
+						scrollRefs.scrollLeft = afterNextL - offsetW;
+					} else if (beforePrevL < scrollL) {
+						scrollRefs.scrollLeft = beforePrevL;
+					}
+				}
+				// 更新滚动条,防止不出现
+				updateScrollbar();
+			});
+		};
+		// 获取 tagsView 的下标:用于处理 tagsView 点击时的横向滚动
+		const getTagsRefsIndex = (path: string | unknown) => {
+			nextTick(async () => {
+				// await 使用该写法,防止拿取不到 tagsViewList 列表数据不完整
+				let tagsViewList = await state.tagsViewList;
+				state.tagsRefsIndex = tagsViewList.findIndex((v: any) => {
+					if (getThemeConfig.value.isShareTagsView) {
+						return v.path === path;
+					} else {
+						return v.url === path;
+					}
+				});
+				// 添加初始化横向滚动条移动到对应位置
+				tagsViewmoveToCurrentTag();
+			});
+		};
+		// 设置 tagsView 可以进行拖拽
+		const initSortable = async () => {
+			const el = <HTMLElement>document.querySelector('.layout-navbars-tagsview-ul');
+			if (!el) return false;
+			state.sortable.el && state.sortable.destroy();
+			state.sortable = Sortable.create(el, {
+				animation: 300,
+				dataIdAttr: 'data-url',
+				disabled: getThemeConfig.value.isSortableTagsView ? false : true,
+				onEnd: () => {
+					const sortEndList: any = [];
+					state.sortable.toArray().map((val: any) => {
+						state.tagsViewList.map((v: any) => {
+							if (v.url === val) sortEndList.push({ ...v });
+						});
+					});
+					addBrowserSetSession(sortEndList);
+				},
+			});
+		};
+		// 拖动问题,https://gitee.com/lyt-top/vue-next-admin/issues/I3ZRRI
+		const onSortableResize = async () => {
+			await initSortable();
+			if (other.isMobile()) state.sortable.el && state.sortable.destroy();
+		};
+		// 页面加载前
+		onBeforeMount(() => {
+			// 初始化,防止手机端直接访问时还可以拖拽
+			onSortableResize();
+			// 拖动问题,https://gitee.com/lyt-top/vue-next-admin/issues/I3ZRRI
+			window.addEventListener('resize', onSortableResize);
+			// 监听非本页面调用 0 刷新当前,1 关闭当前,2 关闭其它,3 关闭全部 4 当前页全屏
+			proxy.mittBus.on('onCurrentContextmenuClick', (data: CurrentContextmenu) => {
+				onCurrentContextmenuClick(data);
+			});
+			// 监听布局配置界面开启/关闭拖拽
+			proxy.mittBus.on('openOrCloseSortable', () => {
+				initSortable();
+			});
+			// 监听布局配置开启 TagsView 共用,为了演示还原默认值
+			proxy.mittBus.on('openShareTagsView', () => {
+				if (getThemeConfig.value.isShareTagsView) {
+					router.push('/home');
+					state.tagsViewList = [];
+					state.tagsViewRoutesList.map((v: any) => {
+						if (v.meta.isAffix && !v.meta.isHide) {
+							v.url = setTagsViewHighlight(v);
+							state.tagsViewList.push({ ...v });
+						}
+					});
+				}
+			});
+		});
+		// 页面卸载时
+		onUnmounted(() => {
+			// 取消非本页面调用监听
+			proxy.mittBus.off('onCurrentContextmenuClick');
+			// 取消监听布局配置界面开启/关闭拖拽
+			proxy.mittBus.off('openOrCloseSortable');
+			// 取消监听布局配置开启 TagsView 共用
+			proxy.mittBus.off('openShareTagsView');
+			// 取消窗口 resize 监听
+			window.removeEventListener('resize', onSortableResize);
+		});
+		// 页面更新时
+		onBeforeUpdate(() => {
+			tagsRefs.value = [];
+		});
+		// 页面加载时
+		onMounted(() => {
+			// 初始化 vuex 中的 tagsViewRoutes 列表
+			getTagsViewRoutes();
+			initSortable();
+		});
+		// 路由更新时
+		onBeforeRouteUpdate(async (to) => {
+			state.routeActive = setTagsViewHighlight(to);
+			state.routePath = to.meta.isDynamic ? to.meta.isDynamicPath : to.path;
+			await addTagsView(to.path, to);
+			getTagsRefsIndex(getThemeConfig.value.isShareTagsView ? state.routePath : state.routeActive);
+		});
+		// 监听路由的变化,动态赋值给 tagsView
+		watch(store.state, (val) => {
+			if (val.tagsViewRoutes.tagsViewRoutes.length === state.tagsViewRoutesList.length) return false;
+			getTagsViewRoutes();
+		});
+		return {
+			isActive,
+			onContextmenu,
+			onTagsClick,
+			tagsRefs,
+			contextmenuRef,
+			scrollbarRef,
+			tagsUlRef,
+			onHandleScroll,
+			getThemeConfig,
+			setTagsStyle,
+			refreshCurrentTagsView,
+			closeCurrentTagsView,
+			onCurrentContextmenuClick,
+			...toRefs(state),
+		};
+	},
+});
+</script>
+
+<style scoped lang="scss">
+.layout-navbars-tagsview {
+	background-color: var(--el-color-white);
+	border-bottom: 1px solid var(--next-border-color-light);
+	position: relative;
+	z-index: 4;
+	::v-deep(.el-scrollbar__wrap) {
+		overflow-x: auto !important;
+	}
+	&-ul {
+		list-style: none;
+		margin: 0;
+		padding: 0;
+		height: 34px;
+		display: flex;
+		align-items: center;
+		color: var(--el-text-color-regular);
+		font-size: 12px;
+		white-space: nowrap;
+		padding: 0 15px;
+		&-li {
+			height: 26px;
+			line-height: 26px;
+			display: flex;
+			align-items: center;
+			border: 1px solid #e6e6e6;
+			padding: 0 15px;
+			margin-right: 5px;
+			border-radius: 2px;
+			position: relative;
+			z-index: 0;
+			cursor: pointer;
+			justify-content: space-between;
+			&:hover {
+				background-color: var(--el-color-primary-light-9);
+				color: var(--el-color-primary);
+				border-color: var(--el-color-primary-light-6);
+			}
+			&-iconfont {
+				position: relative;
+				left: -5px;
+				font-size: 12px;
+			}
+			&-icon {
+				border-radius: 100%;
+				position: relative;
+				height: 14px;
+				width: 14px;
+				text-align: center;
+				line-height: 14px;
+				right: -5px;
+				&:hover {
+					color: var(--el-color-white);
+					background-color: var(--el-color-primary-light-3);
+				}
+			}
+			.layout-icon-active {
+				display: block;
+			}
+			.layout-icon-three {
+				display: none;
+			}
+		}
+		.is-active {
+			color: var(--el-color-white);
+			background: var(--el-color-primary);
+			border-color: var(--el-color-primary);
+			transition: border-color 3s ease;
+		}
+	}
+	// 风格4
+	.tags-style-four {
+		.layout-navbars-tagsview-ul-li {
+			margin-right: 0 !important;
+			border: none !important;
+			position: relative;
+			border-radius: 3px !important;
+			.layout-icon-active {
+				display: none;
+			}
+			.layout-icon-three {
+				display: block;
+			}
+			&:hover {
+				background: none !important;
+			}
+		}
+		.is-active {
+			background: none !important;
+			color: var(--el-color-primary) !important;
+		}
+	}
+	// 风格5
+	.tags-style-five {
+		align-items: flex-end;
+		.tags-style-five-svg {
+			-webkit-mask-box-image: url("data:image/svg+xml,%3Csvg width='68' height='34' viewBox='0 0 68 34' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m27,0c-7.99582,0 -11.95105,0.00205 -12,12l0,6c0,8.284 -0.48549,16.49691 -8.76949,16.49691l54.37857,-0.11145c-8.284,0 -8.60908,-8.10146 -8.60908,-16.38546l0,-6c0.11145,-12.08445 -4.38441,-12 -12,-12l-13,0z' fill='%23409eff'/%3E%3C/svg%3E")
+				12 27 15;
+		}
+		.layout-navbars-tagsview-ul-li {
+			padding: 0 5px;
+			border-width: 15px 27px 15px;
+			border-style: solid;
+			border-color: transparent;
+			margin: 0 -15px;
+			.layout-icon-active,
+			.layout-navbars-tagsview-ul-li-iconfont,
+			.layout-navbars-tagsview-ul-li-refresh {
+				display: none;
+			}
+			.layout-icon-three {
+				display: block;
+			}
+			&:hover {
+				@extend .tags-style-five-svg;
+				background: var(--el-color-primary-light-9);
+				color: unset;
+			}
+		}
+		.is-active {
+			@extend .tags-style-five-svg;
+			background: var(--el-color-primary-light-9) !important;
+			color: var(--el-color-primary) !important;
+			z-index: 1;
+		}
+	}
+}
+.layout-navbars-tagsview-shadow {
+	box-shadow: rgb(0 21 41 / 4%) 0px 1px 4px;
+}
+</style>

+ 151 - 0
nextadmin_vue3/src/layout/navMenu/horizontal.vue

@@ -0,0 +1,151 @@
+<template>
+	<div class="el-menu-horizontal-warp">
+		<el-scrollbar @wheel.native.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef">
+			<el-menu router :default-active="defaultActive" 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 :href="val.meta.isLink" target="_blank" rel="opener" class="w100">
+									<SvgIcon :name="val.meta.icon" />
+									{{ $t(val.meta.title) }}
+								</a>
+							</template>
+						</el-menu-item>
+					</template>
+				</template>
+			</el-menu>
+		</el-scrollbar>
+	</div>
+</template>
+
+<script lang="ts">
+import { toRefs, reactive, computed, defineComponent, getCurrentInstance, onMounted, nextTick, onBeforeMount } from 'vue';
+import { useRoute, onBeforeRouteUpdate } from 'vue-router';
+import { useStore } from '/@/store/index';
+import SubItem from '/@/layout/navMenu/subItem.vue';
+export default defineComponent({
+	name: 'navMenuHorizontal',
+	components: { SubItem },
+	props: {
+		menuList: {
+			type: Array,
+			default: () => [],
+		},
+	},
+	setup(props) {
+		const { proxy } = <any>getCurrentInstance();
+		const route = useRoute();
+		const store = useStore();
+		const state = reactive({
+			defaultActive: null,
+		});
+		// 获取父级菜单数据
+		const menuLists = computed(() => {
+			return <any>props.menuList;
+		});
+		// 设置横向滚动条可以鼠标滚轮滚动
+		const onElMenuHorizontalScroll = (e: any) => {
+			const eventDelta = e.wheelDelta || -e.deltaY * 40;
+			proxy.$refs.elMenuHorizontalScrollRef.$refs.wrap$.scrollLeft = proxy.$refs.elMenuHorizontalScrollRef.$refs.wrap$.scrollLeft + eventDelta / 4;
+		};
+		// 初始化数据,页面刷新时,滚动条滚动到对应位置
+		const initElMenuOffsetLeft = () => {
+			nextTick(() => {
+				let els: any = document.querySelector('.el-menu.el-menu--horizontal li.is-active');
+				if (!els) return false;
+				proxy.$refs.elMenuHorizontalScrollRef.$refs.wrap$.scrollLeft = els.offsetLeft;
+			});
+		};
+		// 路由过滤递归函数
+		const filterRoutesFun = (arr: Array<object>) => {
+			return arr
+				.filter((item: any) => !item.meta.isHide)
+				.map((item: any) => {
+					item = Object.assign({}, item);
+					if (item.children) item.children = filterRoutesFun(item.children);
+					return item;
+				});
+		};
+		// 传送当前子级数据到菜单中
+		const setSendClassicChildren = (path: string) => {
+			const currentPathSplit = path.split('/');
+			let currentData: any = {};
+			filterRoutesFun(store.state.routesList.routesList).map((v, k) => {
+				if (v.path === `/${currentPathSplit[1]}`) {
+					v['k'] = k;
+					currentData['item'] = [{ ...v }];
+					currentData['children'] = [{ ...v }];
+					if (v.children) currentData['children'] = v.children;
+				}
+			});
+			return currentData;
+		};
+		// 设置页面当前路由高亮
+		const setCurrentRouterHighlight = (currentRoute: any) => {
+			const { path, meta } = currentRoute;
+			if (store.state.themeConfig.themeConfig.layout === 'classic') {
+				(<any>state.defaultActive) = `/${path.split('/')[1]}`;
+			} else {
+				const pathSplit = meta.isDynamic ? meta.isDynamicPath.split('/') : path.split('/');
+				if (pathSplit.length >= 4 && meta.isHide) state.defaultActive = pathSplit.splice(0, 3).join('/');
+				else state.defaultActive = path;
+			}
+		};
+		// 页面加载前
+		onBeforeMount(() => {
+			setCurrentRouterHighlight(route);
+		});
+		// 页面加载时
+		onMounted(() => {
+			initElMenuOffsetLeft();
+		});
+		// 路由更新时
+		onBeforeRouteUpdate((to) => {
+			// 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
+			setCurrentRouterHighlight(to);
+			// 修复经典布局开启切割菜单时,点击tagsView后左侧导航菜单数据不变的问题
+			let { layout, isClassicSplitMenu } = store.state.themeConfig.themeConfig;
+			if (layout === 'classic' && isClassicSplitMenu) {
+				proxy.mittBus.emit('setSendClassicChildren', setSendClassicChildren(to.path));
+			}
+		});
+		return {
+			menuLists,
+			onElMenuHorizontalScroll,
+			...toRefs(state),
+		};
+	},
+});
+</script>
+
+<style scoped lang="scss">
+.el-menu-horizontal-warp {
+	flex: 1;
+	overflow: hidden;
+	margin-right: 30px;
+	::v-deep(.el-scrollbar__bar.is-vertical) {
+		display: none;
+	}
+	::v-deep(a) {
+		width: 100%;
+	}
+	.el-menu.el-menu--horizontal {
+		display: flex;
+		height: 100%;
+		width: 100%;
+		box-sizing: border-box;
+	}
+}
+</style>

+ 47 - 0
nextadmin_vue3/src/layout/navMenu/subItem.vue

@@ -0,0 +1,47 @@
+<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 :href="val.meta.isLink" target="_blank" rel="opener" class="w100">
+						<SvgIcon :name="val.meta.icon" />
+						{{ $t(val.meta.title) }}
+					</a>
+				</template>
+			</el-menu-item>
+		</template>
+	</template>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+export default defineComponent({
+	name: 'navMenuSubItem',
+	props: {
+		chil: {
+			type: Array,
+			default: () => [],
+		},
+	},
+	setup(props) {
+		// 获取父级菜单数据
+		const chils = computed(() => {
+			return <any>props.chil;
+		});
+		return {
+			chils,
+		};
+	},
+});
+</script>

+ 98 - 0
nextadmin_vue3/src/layout/navMenu/vertical.vue

@@ -0,0 +1,98 @@
+<template>
+	<el-menu
+		router
+		:default-active="defaultActive"
+		background-color="transparent"
+		:collapse="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 :href="val.meta.isLink" target="_blank" rel="opener" class="w100">{{ $t(val.meta.title) }}</a>
+					</template>
+				</el-menu-item>
+			</template>
+		</template>
+	</el-menu>
+</template>
+
+<script lang="ts">
+import { toRefs, reactive, computed, defineComponent, onMounted, watch } from 'vue';
+import { useRoute, onBeforeRouteUpdate } from 'vue-router';
+import { useStore } from '/@/store/index';
+import SubItem from '/@/layout/navMenu/subItem.vue';
+export default defineComponent({
+	name: 'navMenuVertical',
+	components: { SubItem },
+	props: {
+		menuList: {
+			type: Array,
+			default: () => [],
+		},
+	},
+	setup(props) {
+		const store = useStore();
+		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 <any>props.menuList;
+		});
+		// 获取布局配置信息
+		const getThemeConfig = computed(() => {
+			return store.state.themeConfig.themeConfig;
+		});
+		// 菜单高亮(详情时,父级高亮)
+		const setParentHighlight = (currentRoute: any) => {
+			const { path, meta } = currentRoute;
+			const pathSplit = meta.isDynamic ? meta.isDynamicPath.split('/') : path.split('/');
+			if (pathSplit.length >= 4 && meta.isHide) return pathSplit.splice(0, 3).join('/');
+			else return path;
+		};
+		// 设置菜单的收起/展开
+		watch(
+			store.state.themeConfig.themeConfig,
+			() => {
+				document.body.clientWidth <= 1000 ? (state.isCollapse = false) : (state.isCollapse = getThemeConfig.value.isCollapse);
+			},
+			{
+				immediate: true,
+			}
+		);
+		// 页面加载时
+		onMounted(() => {
+			state.defaultActive = setParentHighlight(route);
+		});
+		// 路由更新时
+		onBeforeRouteUpdate((to) => {
+			// 修复:https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
+			state.defaultActive = setParentHighlight(to);
+			const clientWidth = document.body.clientWidth;
+			if (clientWidth < 1000) getThemeConfig.value.isCollapse = false;
+		});
+		return {
+			menuLists,
+			getThemeConfig,
+			...toRefs(state),
+		};
+	},
+});
+</script>

+ 60 - 0
nextadmin_vue3/src/layout/routerView/iframes.vue

@@ -0,0 +1,60 @@
+<template>
+	<div class="layout-view-bg-white flex mt1" :style="{ height: `calc(100vh - ${setIframeHeight}`, border: 'none' }" v-loading="iframeLoading">
+		<iframe :src="iframeUrl" frameborder="0" height="100%" width="100%" id="iframe" v-show="!iframeLoading"></iframe>
+	</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive, toRefs, onMounted, nextTick, watch, computed } from 'vue';
+import { useRoute } from 'vue-router';
+import { useStore } from '/@/store/index';
+export default defineComponent({
+	name: 'layoutIfameView',
+	setup() {
+		const route = useRoute();
+		const store = useStore();
+		const state = reactive({
+			iframeLoading: true,
+			iframeUrl: '',
+		});
+		// 初始化页面加载 loading
+		const initIframeLoad = () => {
+			state.iframeUrl = <any>route.meta.isLink;
+			nextTick(() => {
+				state.iframeLoading = true;
+				const iframe = document.getElementById('iframe');
+				if (!iframe) return false;
+				iframe.onload = () => {
+					state.iframeLoading = false;
+				};
+			});
+		};
+		// 设置 iframe 的高度
+		const setIframeHeight = computed(() => {
+			let { isTagsview } = store.state.themeConfig.themeConfig;
+			let { isTagsViewCurrenFull } = store.state.tagsViewRoutes;
+			if (isTagsViewCurrenFull) {
+				return `1px`;
+			} else {
+				if (isTagsview) return `85px`;
+				else return `51px`;
+			}
+		});
+		// 页面加载时
+		onMounted(() => {
+			initIframeLoad();
+		});
+		// 监听路由变化,多个 iframe 时使用
+		watch(
+			() => route.path,
+			() => {
+				initIframeLoad();
+			}
+		);
+		return {
+			setIframeHeight,
+			...toRefs(state),
+		};
+	},
+});
+</script>

+ 59 - 0
nextadmin_vue3/src/layout/routerView/link.vue

@@ -0,0 +1,59 @@
+<template>
+	<div class="layout-view-bg-white flex layout-view-link" :style="{ height: `calc(100vh - ${setLinkHeight}` }">
+		<a :href="currentRouteMeta.isLink" target="_blank" rel="opener" class="flex-margin">
+			{{ $t(currentRouteMeta.title) }}:{{ currentRouteMeta.isLink }}
+		</a>
+	</div>
+</template>
+
+<script lang="ts">
+import { defineComponent, toRefs, reactive, computed, watch } from 'vue';
+import { useRoute, RouteMeta } from 'vue-router';
+import { useStore } from '/@/store/index';
+
+// 定义接口来定义对象的类型
+interface LinkViewState {
+	currentRouteMeta: {
+		isLink: string;
+		title: string;
+	};
+}
+interface LinkViewRouteMeta extends RouteMeta {
+	isLink: string;
+	title: string;
+}
+
+export default defineComponent({
+	name: 'layoutLinkView',
+	setup() {
+		const route = useRoute();
+		const store = useStore();
+		const state = reactive<LinkViewState>({
+			currentRouteMeta: {
+				isLink: '',
+				title: '',
+			},
+		});
+		// 设置 link 的高度
+		const setLinkHeight = computed(() => {
+			let { isTagsview } = store.state.themeConfig.themeConfig;
+			if (isTagsview) return `114px`;
+			else return `80px`;
+		});
+		// 监听路由的变化,设置内容
+		watch(
+			() => route.path,
+			() => {
+				state.currentRouteMeta = <LinkViewRouteMeta>route.meta;
+			},
+			{
+				immediate: true,
+			}
+		);
+		return {
+			setLinkHeight,
+			...toRefs(state),
+		};
+	},
+});
+</script>

+ 83 - 0
nextadmin_vue3/src/layout/routerView/parent.vue

@@ -0,0 +1,83 @@
+<template>
+	<div class="h100">
+		<router-view v-slot="{ Component }">
+			<transition :name="setTransitionName" mode="out-in">
+				<keep-alive :include="keepAliveNameList">
+					<component :is="Component" :key="refreshRouterViewKey" class="w100" :style="{ minHeight }" />
+				</keep-alive>
+			</transition>
+		</router-view>
+	</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, toRefs, reactive, getCurrentInstance, onBeforeMount, onUnmounted, nextTick, watch } from 'vue';
+import { useRoute } from 'vue-router';
+import { useStore } from '/@/store/index';
+
+// 定义接口来定义对象的类型
+interface ParentViewState {
+	refreshRouterViewKey: null | string;
+	keepAliveNameList: string[];
+}
+
+export default defineComponent({
+	name: 'layoutParentView',
+	props: {
+		minHeight: {
+			type: String,
+			default: '',
+		},
+	},
+	setup() {
+		const { proxy } = <any>getCurrentInstance();
+		const route = useRoute();
+		const store = useStore();
+		const state = reactive<ParentViewState>({
+			refreshRouterViewKey: null,
+			keepAliveNameList: [],
+		});
+		// 设置主界面切换动画
+		const setTransitionName = computed(() => {
+			return store.state.themeConfig.themeConfig.animation;
+		});
+		// 获取布局配置信息
+		const getThemeConfig = computed(() => {
+			return store.state.themeConfig.themeConfig;
+		});
+		// 获取组件缓存列表(name值)
+		const getKeepAliveNames = computed(() => {
+			return store.state.keepAliveNames.keepAliveNames;
+		});
+		// 页面加载前,处理缓存,页面刷新时路由缓存处理
+		onBeforeMount(() => {
+			state.keepAliveNameList = getKeepAliveNames.value;
+			proxy.mittBus.on('onTagsViewRefreshRouterView', (fullPath: string) => {
+				state.keepAliveNameList = getKeepAliveNames.value.filter((name: string) => route.name !== name);
+				state.refreshRouterViewKey = null;
+				nextTick(() => {
+					state.refreshRouterViewKey = fullPath;
+					state.keepAliveNameList = getKeepAliveNames.value;
+				});
+			});
+		});
+		// 页面卸载时
+		onUnmounted(() => {
+			proxy.mittBus.off('onTagsViewRefreshRouterView');
+		});
+		// 监听路由变化,防止 tagsView 多标签时,切换动画消失
+		watch(
+			() => route.fullPath,
+			() => {
+				state.refreshRouterViewKey = route.fullPath;
+			}
+		);
+		return {
+			getThemeConfig,
+			getKeepAliveNames,
+			setTransitionName,
+			...toRefs(state),
+		};
+	},
+});
+</script>

+ 22 - 0
nextadmin_vue3/src/main.ts

@@ -0,0 +1,22 @@
+import { createApp } from 'vue';
+import App from './App.vue';
+import router from './router';
+import { store, key } from './store';
+import { directive } from '/@/utils/directive';
+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 mitt from 'mitt';
+import VueGridLayout from 'vue-grid-layout';
+import 'vue-layer/lib/vue-layer.css';
+const app = createApp(App);
+
+directive(app);
+other.elSvg(app);
+
+app.use(router).use(store, key).use(ElementPlus, { i18n: i18n.global.t }).use(i18n).use(VueGridLayout).mount('#app');
+
+app.config.globalProperties.mittBus = mitt();

+ 102 - 0
nextadmin_vue3/src/router/backEnd.ts

@@ -0,0 +1,102 @@
+import { store } from '/@/store/index.ts';
+import { Session } from '/@/utils/storage';
+import { NextLoading } from '/@/utils/loading';
+import { setAddRoute, setFilterMenuAndCacheTagsViewRoutes } from '/@/router/index';
+import { dynamicRoutes } from '/@/router/route';
+import { useMenuApi } from '/@/api/menu/index';
+
+const menuApi = useMenuApi();
+
+const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}');
+const viewsModules: any = import.meta.glob('../views/**/*.{vue,tsx}');
+/**
+ * 获取目录下的 .vue、.tsx 全部文件
+ * @method import.meta.glob
+ * @link 参考:https://cn.vitejs.dev/guide/features.html#json
+ */
+const dynamicViewsModules: Record<string, Function> = Object.assign({}, { ...layouModules }, { ...viewsModules });
+
+/**
+ * 后端控制路由:初始化方法,防止刷新时路由丢失
+ * @method  NextLoading 界面 loading 动画开始执行
+ * @method store.dispatch('userInfos/setUserInfos') 触发初始化用户信息
+ * @method store.dispatch('requestOldRoutes/setBackEndControlRoutes') 存储接口原始路由(未处理component),根据需求选择使用
+ * @method setAddRoute 添加动态路由
+ * @method setFilterMenuAndCacheTagsViewRoutes 设置递归过滤有权限的路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+ */
+export async function initBackEndControlRoutes() {
+	// 界面 loading 动画开始执行
+	if (window.nextLoading === undefined) NextLoading.start();
+	// 无 token 停止执行下一步
+	if (!Session.get('token')) return false;
+	// 触发初始化用户信息
+	store.dispatch('userInfos/setUserInfos');
+	// 获取路由菜单数据	
+	const res = await getBackEndControlRoutes();
+	// 存储接口原始路由(未处理component),根据需求选择使用
+	store.dispatch('requestOldRoutes/setBackEndControlRoutes', JSON.parse(JSON.stringify(res.data)));
+	// 处理路由(component),替换 dynamicRoutes(/@/router/route)第一个顶级 children 的路由
+	dynamicRoutes[0].children = await backEndComponent(res.data);
+	// 添加动态路由
+	await setAddRoute();
+	// 设置递归过滤有权限的路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+	setFilterMenuAndCacheTagsViewRoutes();
+}
+
+/**
+ * 请求后端路由菜单接口
+ * @description isRequestRoutes 为 true,则开启后端控制路由
+ * @returns 返回后端路由菜单数据
+ */
+export function getBackEndControlRoutes() {
+	// 模拟 admin 与 test
+	const auth = store.state.userInfos.userInfos.roles[0];
+	// 管理员 admin
+	if (auth === 'admin') return menuApi.getMenuAdmin();
+	// 其它用户 test
+	else return menuApi.getMenuTest();
+}
+
+/**
+ * 重新请求后端路由菜单接口
+ * @description 用于菜单管理界面刷新菜单(未进行测试)
+ * @description 路径:/src/views/system/menu/component/addMenu.vue
+ */
+export function setBackEndControlRefreshRoutes() {
+	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) => {
+		const k = key.replace(/..\/views|../, '');
+		return k.startsWith(`${component}`) || k.startsWith(`/${component}`);
+	});
+	if (matchKeys?.length === 1) {
+		const matchKey = matchKeys[0];
+		return dynamicViewsModules[matchKey];
+	}
+	if (matchKeys?.length > 1) {
+		return false;
+	}
+}

+ 24 - 0
nextadmin_vue3/src/router/frontEnd.ts

@@ -0,0 +1,24 @@
+import { store } from '/@/store/index';
+import { Session } from '/@/utils/storage';
+import { NextLoading } from '/@/utils/loading';
+import { setAddRoute, setFilterMenuAndCacheTagsViewRoutes } from '/@/router/index';
+
+/**
+ * 前端控制路由:初始化方法,防止刷新时路由丢失
+ * @method  NextLoading 界面 loading 动画开始执行
+ * @method store.dispatch('userInfos/setUserInfos') 触发初始化用户信息
+ * @method setAddRoute 添加动态路由
+ * @method setFilterMenuAndCacheTagsViewRoutes 设置递归过滤有权限的路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+ */
+export async function initFrontEndControlRoutes() {
+	// 界面 loading 动画开始执行
+	if (window.nextLoading === undefined) NextLoading.start();
+	// 无 token 停止执行下一步
+	if (!Session.get('token')) return false;
+	// 触发初始化用户信息
+	store.dispatch('userInfos/setUserInfos');
+	// 添加动态路由
+	await setAddRoute();
+	// 设置递归过滤有权限的路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+	setFilterMenuAndCacheTagsViewRoutes();
+}

+ 232 - 0
nextadmin_vue3/src/router/index.ts

@@ -0,0 +1,232 @@
+import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
+import NProgress from 'nprogress';
+import 'nprogress/nprogress.css';
+import { store } from '/@/store/index.ts';
+import { Session } from '/@/utils/storage';
+import { NextLoading } from '/@/utils/loading';
+import { staticRoutes, dynamicRoutes } from '/@/router/route';
+import { initFrontEndControlRoutes } from '/@/router/frontEnd';
+import { initBackEndControlRoutes } from '/@/router/backEnd';
+
+/**
+ * 创建一个可以被 Vue 应用程序使用的路由实例
+ * @method createRouter(options: RouterOptions): Router
+ * @link 参考:https://next.router.vuejs.org/zh/api/#createrouter
+ */
+const router = createRouter({
+	history: createWebHashHistory(),
+	routes: staticRoutes,
+});
+
+/**
+ * 定义404界面
+ * @link 参考:https://next.router.vuejs.org/zh/guide/essentials/history-mode.html#netlify
+ */
+const pathMatch = {
+	path: '/:path(.*)*',
+	redirect: '/404',
+};
+
+/**
+ * 路由多级嵌套数组处理成一维数组
+ * @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);
+				store.dispatch('keepAliveNames/setCacheKeepAlive', cacheList);
+			}
+		}
+	});
+	return newArr;
+}
+
+/**
+ * 缓存多级嵌套数组处理后的一维数组
+ * @description 用于 tagsView、菜单搜索中:未过滤隐藏的(isHide)
+ */
+export function setCacheTagsViewRoutes() {
+	// 获取有权限的路由,否则 tagsView、菜单搜索中无权限的路由也将显示
+	let rolesRoutes = setFilterHasRolesMenu(dynamicRoutes, store.state.userInfos.userInfos.roles);
+	// 添加到 vuex setTagsViewRoutes 中
+	store.dispatch('tagsViewRoutes/setTagsViewRoutes', formatTwoStageRoutes(formatFlatteningRoutes(rolesRoutes))[0].children);
+}
+
+/**
+ * 判断路由 `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;
+}
+
+/**
+ * 设置递归过滤有权限的路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
+ * @description 用于左侧菜单、横向菜单的显示
+ * @description 用于 tagsView、菜单搜索中:未过滤隐藏的(isHide)
+ */
+export function setFilterMenuAndCacheTagsViewRoutes() {
+	store.dispatch('routesList/setRoutesList', setFilterHasRolesMenu(dynamicRoutes[0].children, store.state.userInfos.userInfos.roles));
+	setCacheTagsViewRoutes();
+}
+
+/**
+ * 获取当前用户权限标识去比对路由表(未处理成多级嵌套路由)
+ * @description 这里主要用于动态路由的添加,router.addRoute
+ * @link 参考:https://next.router.vuejs.org/zh/api/#addroute
+ * @param chil dynamicRoutes(/@/router/route)第一个顶级 children 的下路由集合
+ * @returns 返回有当前用户权限标识的路由数组
+ */
+export function setFilterRoute(chil: any) {
+	let filterRoute: any = [];
+	chil.forEach((route: any) => {
+		if (route.meta.roles) {
+			route.meta.roles.forEach((metaRoles: any) => {
+				store.state.userInfos.userInfos.roles.forEach((roles: any) => {
+					if (metaRoles === roles) filterRoute.push({ ...route });
+				});
+			});
+		}
+	});
+	return filterRoute;
+}
+
+/**
+ * 获取有当前用户权限标识的路由数组,进行对原路由的替换
+ * @description 替换 dynamicRoutes(/@/router/route)第一个顶级 children 的路由
+ * @returns 返回替换后的路由数组
+ */
+export function setFilterRouteEnd() {
+	let filterRouteEnd: any = formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes));
+	filterRouteEnd[0].children = [...setFilterRoute(filterRouteEnd[0].children), { ...pathMatch }];
+	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) => {
+		const routeName: any = route.name;
+		if (!router.hasRoute(routeName)) router.addRoute(route);
+	});
+}
+
+/**
+ * 删除/重置路由
+ * @method router.removeRoute
+ * @description 此处循环为 dynamicRoutes(/@/router/route)第一个顶级 children 的路由一维数组,非多级嵌套
+ * @link 参考:https://next.router.vuejs.org/zh/api/#push
+ */
+export async function resetRoute() {
+	await setFilterRouteEnd().forEach((route: RouteRecordRaw) => {
+		const routeName: any = route.name;
+		router.hasRoute(routeName) && router.removeRoute(routeName);
+	});
+}
+
+// isRequestRoutes 为 true,则开启后端控制路由,路径:`/src/store/modules/themeConfig.ts`
+const { isRequestRoutes } = store.state.themeConfig.themeConfig;
+
+// 前端控制路由:初始化方法,防止刷新时路由丢失
+if (!isRequestRoutes) initFrontEndControlRoutes();
+
+// 路由加载前
+router.beforeEach(async (to, from, next) => {
+	NProgress.configure({ showSpinner: false });
+	if (to.meta.title) NProgress.start();
+	const token = Session.get('token');//获取token,判断有没有登录 
+	if (to.path === '/login' && !token) {
+		next();
+		NProgress.done();
+	} else {
+		if (!token) {
+			next(`/login?redirect=${to.path}&params=${JSON.stringify(to.query ? to.query : to.params)}`);
+			Session.clear();
+			resetRoute();
+			NProgress.done();
+		} else if (token && to.path === '/login') {
+			next('/home');
+			NProgress.done();
+		} else {
+			if (store.state.routesList.routesList.length === 0) {
+				if (isRequestRoutes) {
+					// 后端控制路由:路由数据初始化,防止刷新时丢失
+					await initBackEndControlRoutes();
+					// 动态添加路由:防止非首页刷新时跳转回首页的问题
+					// 确保 addRoute() 时动态添加的路由已经被完全加载上去
+					next({ ...to, replace: true });
+				}
+			} else {
+				next();
+			}
+		}
+	}
+});
+
+// 路由加载后
+router.afterEach(() => {
+	NProgress.done();
+	NextLoading.done();//移除loding
+});
+
+// 导出路由
+export default router;

File diff suppressed because it is too large
+ 1180 - 0
nextadmin_vue3/src/router/route.bak.ts


+ 88 - 0
nextadmin_vue3/src/router/route.ts

@@ -0,0 +1,88 @@
+import { RouteRecordRaw } from 'vue-router';
+
+/**
+ * 路由meta对象参数说明
+ * meta: {
+ *      title:          菜单栏及 tagsView 栏、菜单搜索名称(国际化)
+ *      isLink:        是否超链接菜单,开启外链条件,`1、isLink:true 2、链接地址不为空`
+ *      isHide:        是否隐藏此路由
+ *      isKeepAlive:   是否缓存组件状态
+ *      isAffix:       是否固定在 tagsView 栏上
+ *      isIframe:      是否内嵌窗口,,开启条件,`1、isIframe:true 2、链接地址不为空`
+ *      roles:         当前路由权限标识,取角色管理。控制路由显示、隐藏。超级管理员:admin 普通角色:common
+ *      icon:          菜单、tagsView 图标,阿里:加 `iconfont xxx`,fontawesome:加 `fa xxx`
+ * }
+ */
+
+/**
+ * 定义动态路由
+ * @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: '/system_menu',
+				name: 'systemMenu',
+				component: () => import('/@/views/system/menu/index.vue'),
+				meta: {
+					title: 'message.router.systemMenu',
+					isLink: '',
+					isHide: false,
+					isKeepAlive: true,
+					isAffix: false,
+					isIframe: false,
+					roles: ['admin'],
+					icon: 'iconfont icon-caidan',
+				},
+			},
+		]
+	},
+
+];
+
+/**
+ * 定义静态路由
+ * @description 前端控制直接改 dynamicRoutes 中的路由,后端控制不需要修改,请求接口路由数据时,会覆盖 dynamicRoutes 第一个顶级 children 的内容(全屏,不包含 layout 中的路由出口)
+ * @returns 返回路由菜单数据
+ */
+export const staticRoutes: Array<RouteRecordRaw> = [
+	{
+		path: '/login',
+		name: 'login',
+		component: () => import('/@/views/login/index.vue'),
+		meta: {
+			title: '登录',
+		},
+	},
+	{
+		path: '/404',
+		name: 'notFound',
+		component: () => import('/@/views/error/404.vue'),
+		meta: {
+			title: 'message.staticRoutes.notFound',
+		},
+	},
+	{
+		path: '/401',
+		name: 'noPower',
+		component: () => import('/@/views/error/401.vue'),
+		meta: {
+			title: 'message.staticRoutes.noPower',
+		},
+	},
+	/**
+	 * 提示:写在这里的为全屏界面,不建议写在这里
+	 * 请写在 `dynamicRoutes` 路由数组中
+	 */
+
+];

+ 27 - 0
nextadmin_vue3/src/store/index.ts

@@ -0,0 +1,27 @@
+import { InjectionKey } from 'vue';
+import { createStore, useStore as baseUseStore, Store } from 'vuex';
+import { RootStateTypes } from '/@/store/interface/index';
+
+// Vite supports importing multiple modules from the file system using the special import.meta.glob function
+// see https://cn.vitejs.dev/guide/features.html#glob-import
+const modulesFiles = import.meta.globEager('./modules/*.ts');
+const pathList: string[] = [];
+
+for (const path in modulesFiles) {
+	pathList.push(path);
+}
+
+const modules = pathList.reduce((modules: { [x: string]: any }, modulePath: string) => {
+	const moduleName = modulePath.replace(/^\.\/modules\/(.*)\.\w+$/, '$1');
+	const value = modulesFiles[modulePath];
+	modules[moduleName] = value.default;
+	return modules;
+}, {});
+
+export const key: InjectionKey<Store<RootStateTypes>> = Symbol();
+
+export const store = createStore<RootStateTypes>({ modules });
+
+export function useStore() {
+	return baseUseStore(key);
+}

+ 94 - 0
nextadmin_vue3/src/store/interface/index.ts

@@ -0,0 +1,94 @@
+// 接口类型声明
+
+// 布局配置
+export interface ThemeConfigState {
+	themeConfig: {
+		isDrawer: boolean;
+		primary: string;
+		topBar: string;
+		topBarColor: string;
+		isTopBarColorGradual: boolean;
+		menuBar: string;
+		menuBarColor: string;
+		isMenuBarColorGradual: boolean;
+		columnsMenuBar: string;
+		columnsMenuBarColor: string;
+		isColumnsMenuBarColorGradual: boolean;
+		isCollapse: boolean;
+		isUniqueOpened: boolean;
+		isFixedHeader: boolean;
+		isFixedHeaderChange: boolean;
+		isClassicSplitMenu: boolean;
+		isLockScreen: boolean;
+		lockScreenTime: number;
+		isShowLogo: boolean;
+		isShowLogoChange: boolean;
+		isBreadcrumb: boolean;
+		isTagsview: boolean;
+		isBreadcrumbIcon: boolean;
+		isTagsviewIcon: boolean;
+		isCacheTagsView: boolean;
+		isSortableTagsView: boolean;
+		isShareTagsView: boolean;
+		isFooter: boolean;
+		isGrayscale: boolean;
+		isInvert: boolean;
+		isIsDark: boolean;
+		isWartermark: boolean;
+		wartermarkText: string;
+		tagsStyle: string;
+		animation: string;
+		columnsAsideStyle: string;
+		columnsAsideLayout: string;
+		layout: string;
+		isRequestRoutes: boolean;
+		globalTitle: string;
+		globalViceTitle: string;
+		globalI18n: string;
+		globalComponentSize: string;
+	};
+}
+
+// 路由列表
+export interface RoutesListState {
+	routesList: object[];
+	isColumnsMenuHover: Boolean;
+	isColumnsNavHover: Boolean;
+}
+
+// 路由缓存列表
+export interface KeepAliveNamesState {
+	keepAliveNames: string[];
+}
+
+// TagsView 路由列表
+export interface TagsViewRoutesState {
+	tagsViewRoutes: object[];
+	isTagsViewCurrenFull: Boolean;
+}
+
+// 用户信息
+export interface UserInfosState {
+	userInfos: {
+		authBtnList: string[];
+		photo: string;
+		roles: string[];
+		time: number;
+		userName: string;
+	};
+}
+
+// 后端返回原始路由(未处理时)
+export interface RequestOldRoutesState {
+	requestOldRoutes: object[];
+}
+
+// 主接口(顶级类型声明)
+export interface RootStateTypes {
+	themeConfig: ThemeConfigState;
+	routesList: RoutesListState;
+	keepAliveNames: KeepAliveNamesState;
+	tagsViewRoutes: TagsViewRoutesState;
+	userInfos: UserInfosState;
+	requestOldRoutes: RequestOldRoutesState;
+}

+ 23 - 0
nextadmin_vue3/src/store/modules/keepAliveNames.ts

@@ -0,0 +1,23 @@
+import { Module } from 'vuex';
+import { KeepAliveNamesState, RootStateTypes } from '/@/store/interface/index';
+
+const keepAliveNamesModule: Module<KeepAliveNamesState, RootStateTypes> = {
+	namespaced: true,
+	state: {
+		keepAliveNames: [],
+	},
+	mutations: {
+		// 设置路由缓存(name字段)
+		getCacheKeepAlive(state: any, data: Array<string>) {
+			state.keepAliveNames = data;
+		},
+	},
+	actions: {
+		// 设置路由缓存(name字段)
+		async setCacheKeepAlive({ commit }, data: Array<string>) {
+			commit('getCacheKeepAlive', data);
+		},
+	},
+};
+
+export default keepAliveNamesModule;

+ 23 - 0
nextadmin_vue3/src/store/modules/requestOldRoutes.ts

@@ -0,0 +1,23 @@
+import { Module } from 'vuex';
+import { RequestOldRoutesState, RootStateTypes } from '/@/store/interface/index';
+
+const requestOldRoutesModule: Module<RequestOldRoutesState, RootStateTypes> = {
+	namespaced: true,
+	state: {
+		requestOldRoutes: [],
+	},
+	mutations: {
+		// 后端控制路由
+		getBackEndControlRoutes(state: any, data: object) {
+			state.requestOldRoutes = data;
+		},
+	},
+	actions: {
+		// 后端控制路由
+		setBackEndControlRoutes({ commit }, routes: Array<string>) {
+			commit('getBackEndControlRoutes', routes);
+		},
+	},
+};
+
+export default requestOldRoutesModule;

+ 41 - 0
nextadmin_vue3/src/store/modules/routesList.ts

@@ -0,0 +1,41 @@
+import { Module } from 'vuex';
+import { RoutesListState, RootStateTypes } from '/@/store/interface/index';
+
+const routesListModule: Module<RoutesListState, RootStateTypes> = {
+	namespaced: true,
+	state: {
+		routesList: [],
+		isColumnsMenuHover: false,
+		isColumnsNavHover: false,
+	},
+	mutations: {
+		// 设置路由,菜单中使用到
+		getRoutesList(state: any, data: Array<object>) {
+			state.routesList = data;
+		},
+		// 设置分栏布局,鼠标是否移入移出(菜单)
+		getColumnsMenuHover(state: any, bool: Boolean) {
+			state.isColumnsMenuHover = bool;
+		},
+		// 设置分栏布局,鼠标是否移入移出(导航)
+		getColumnsNavHover(state: any, bool: Boolean) {
+			state.isColumnsNavHover = bool;
+		},
+	},
+	actions: {
+		// 设置路由,菜单中使用到
+		async setRoutesList({ commit }, data: any) {
+			commit('getRoutesList', data);
+		},
+		// 设置分栏布局,鼠标是否移入移出(菜单)
+		async setColumnsMenuHover({ commit }, bool: Boolean) {
+			commit('getColumnsMenuHover', bool);
+		},
+		// 设置分栏布局,鼠标是否移入移出(菜单)
+		async setColumnsNavHover({ commit }, bool: Boolean) {
+			commit('getColumnsNavHover', bool);
+		},
+	},
+};
+
+export default routesListModule;

+ 34 - 0
nextadmin_vue3/src/store/modules/tagsViewRoutes.ts

@@ -0,0 +1,34 @@
+import { Module } from 'vuex';
+import { TagsViewRoutesState, RootStateTypes } from '/@/store/interface/index';
+import { Session } from '/@/utils/storage';
+
+const tagsViewRoutesModule: Module<TagsViewRoutesState, RootStateTypes> = {
+	namespaced: true,
+	state: {
+		tagsViewRoutes: [],
+		isTagsViewCurrenFull: false,
+	},
+	mutations: {
+		// 设置 TagsView 路由
+		getTagsViewRoutes(state: any, data: Array<string>) {
+			state.tagsViewRoutes = data;
+		},
+		// 设置卡片全屏
+		getCurrenFullscreen(state: any, bool: boolean) {
+			Session.set('isTagsViewCurrenFull', bool);
+			state.isTagsViewCurrenFull = bool;
+		},
+	},
+	actions: {
+		// 设置 TagsView 路由
+		async setTagsViewRoutes({ commit }, data: Array<string>) {
+			commit('getTagsViewRoutes', data);
+		},
+		// 设置卡片全屏
+		setCurrenFullscreen({ commit }, bool: Boolean) {
+			commit('getCurrenFullscreen', bool);
+		},
+	},
+};
+
+export default tagsViewRoutesModule;

+ 153 - 0
nextadmin_vue3/src/store/modules/themeConfig.ts

@@ -0,0 +1,153 @@
+import { Module } from 'vuex';
+import { ThemeConfigState, RootStateTypes } from '/@/store/interface/index';
+
+/**
+ * 2020.05.28 by lyt 优化
+ * 修改一下配置时,需要每次都清理 `window.localStorage` 浏览器永久缓存,配置才会生效
+ * 哪个大佬有解决办法,欢迎pr,感谢💕!
+ */
+const themeConfigModule: Module<ThemeConfigState, RootStateTypes> = {
+	namespaced: true,
+	state: {
+		themeConfig: {
+			// 是否开启布局配置抽屉
+			isDrawer: false,
+
+			/**
+			 * 全局主题
+			 */
+			// 默认 primary 主题颜色
+			primary: '#409eff',
+
+			/**
+			 * 菜单 / 顶栏
+			 * 注意:v1.0.17 版本去除设置布局切换,重置主题样式(initSetLayoutChange),
+			 * 切换布局需手动设置样式,设置的样式自动同步各布局,
+			 * 代码位置:/@/layout/navBars/breadcrumb/setings.vue
+			 */
+			// 默认顶栏导航背景颜色
+			topBar: '#ffffff',
+			// 默认顶栏导航字体颜色
+			topBarColor: '#606266',
+			// 是否开启顶栏背景颜色渐变
+			isTopBarColorGradual: false,
+			// 默认菜单导航背景颜色
+			menuBar: '#545c64',
+			// 默认菜单导航字体颜色
+			menuBarColor: '#eaeaea',
+			// 是否开启菜单背景颜色渐变
+			isMenuBarColorGradual: false,
+			// 默认分栏菜单背景颜色
+			columnsMenuBar: '#545c64',
+			// 默认分栏菜单字体颜色
+			columnsMenuBarColor: '#e6e6e6',
+			// 是否开启分栏菜单背景颜色渐变
+			isColumnsMenuBarColorGradual: false,
+
+			/**
+			 * 界面设置
+			 */
+			// 是否开启菜单水平折叠效果
+			isCollapse: false,
+			// 是否开启菜单手风琴效果
+			isUniqueOpened: false,
+			// 是否开启固定 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,
+			// 是否开启深色模式
+			isIsDark: false,
+			// 是否开启水印
+			isWartermark: false,
+			// 水印文案
+			wartermarkText: 'yckj',
+
+			/**
+			 * 其它设置
+			 */
+			// 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: '绩效管理平台端',
+			// 默认初始语言,可选值"<zh-cn|en|zh-tw>",默认 zh-cn
+			globalI18n: 'zh-cn',
+			// 默认全局组件大小,可选值"<large|'default'|small>",默认 'large'
+			globalComponentSize: 'default',
+		},
+	},
+	mutations: {
+		// 设置布局配置
+		getThemeConfig(state: any, data: object) {
+			state.themeConfig = data;
+		},
+	},
+	actions: {
+		// 设置布局配置
+		setThemeConfig({ commit }, data: object) {
+			commit('getThemeConfig', data);
+		},
+	},
+};
+
+export default themeConfigModule;

+ 34 - 0
nextadmin_vue3/src/store/modules/userInfos.ts

@@ -0,0 +1,34 @@
+import { Module } from 'vuex';
+import { Session } from '/@/utils/storage';
+import { UserInfosState, RootStateTypes } from '/@/store/interface/index';
+
+const userInfosModule: Module<UserInfosState, RootStateTypes> = {
+	namespaced: true,
+	state: {
+		userInfos: {
+			authBtnList: [],
+			photo: '',
+			roles: [],
+			time: 0,
+			userName: '',
+		},
+	},
+	mutations: {
+		// 设置用户信息
+		getUserInfos(state, data: any) {
+			state.userInfos = data;
+		},
+	},
+	actions: {
+		// 设置用户信息
+		async setUserInfos({ commit }, data: UserInfosState) {
+			if (data) {
+				commit('getUserInfos', data);
+			} else {
+				if (Session.get('userInfo')) commit('getUserInfos', Session.get('userInfo'));
+			}
+		},
+	},
+};
+
+export default userInfosModule;

+ 277 - 0
nextadmin_vue3/src/theme/app.scss

@@ -0,0 +1,277 @@
+/* 初始化样式
+------------------------------- */
+* {
+	margin: 0;
+	padding: 0;
+	box-sizing: border-box;
+	outline: none !important;
+}
+
+:root {
+	--next-bg-main-color: #f8f8f8;
+	--next-bg-color: #f5f5ff;
+	--next-border-color-light: #f1f2f3;
+	--next-color-primary-lighter: #ecf5ff;
+	--next-color-dark-hover: #0000001a;
+	--next-color-menu-hover: rgba(0, 0, 0, 0.1);
+	--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-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;
+	}
+	.layout-main {
+		padding: 0 !important;
+		overflow: hidden;
+		width: 100%;
+		background-color: var(--next-bg-main-color);
+	}
+	.el-scrollbar {
+		width: 100%;
+	}
+	// 此字段多次用到,建议不删除,如需修改,请重写覆盖样式
+	.layout-view-bg-white {
+		background: var(--el-color-white);
+		width: 100%;
+		height: 100%;
+		border-radius: 4px;
+		border: 1px solid var(--el-border-color-light, #ebeef5);
+	}
+	.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-scrollbar {
+		@extend .el-scrollbar;
+		padding: 15px;
+	}
+	.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;
+	}
+}

+ 94 - 0
nextadmin_vue3/src/theme/common/transition.scss

@@ -0,0 +1,94 @@
+/* 页面切换动画
+------------------------------- */
+.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;
+	}
+}

+ 219 - 0
nextadmin_vue3/src/theme/dark.scss

@@ -0,0 +1,219 @@
+/* 深色模式样式
+------------------------------- */
+[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-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-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-base: var(--next-border-black) !important;
+	--el-border-color-light: var(--next-border-black) !important;
+	--el-text-color-regular: var(--next-text-color-regular) !important;
+	--el-bg-color: var(--next-color-hover-rgba) !important;
+	--el-color-success-lighter: var(--next-color-primary) !important;
+	--el-color-warning-lighter: var(--next-color-primary) !important;
+	--el-color-danger-lighter: var(--next-color-primary) !important;
+	--el-color-primary-lighter: var(--next-color-primary) !important;
+	--el-color-primary-light-9: var(--next-color-hover) !important;
+	--el-text-color-disabled-base: var(--el-color-primary) !important;
+	--el-border-color-lighter: var(--next-border-black) !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;
+
+	// 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);
+	}
+
+	// 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);
+	}
+}

+ 227 - 0
nextadmin_vue3/src/theme/element.scss

@@ -0,0 +1,227 @@
+@import 'mixins/index.scss';
+
+/* Button 按钮
+------------------------------- */
+// 第三方字体图标大小
+.el-button i.el-icon,
+.el-button i.iconfont,
+.el-button i.fa,
+.el-button--default i.iconfont,
+.el-button--default i.fa {
+	font-size: 14px !important;
+	margin-right: 5px;
+}
+.el-button--small i.iconfont,
+.el-button--small i.fa {
+	font-size: 12px !important;
+	margin-right: 5px;
+}
+
+/* Input 输入框、InputNumber 计数器
+------------------------------- */
+// 菜单搜索
+.el-autocomplete-suggestion__wrap {
+	max-height: 280px !important;
+}
+
+/* Form 表单
+------------------------------- */
+.el-form {
+	.el-form-item:last-of-type {
+		margin-bottom: 0 !important;
+	}
+}
+
+/* Alert 警告
+------------------------------- */
+.el-alert__title {
+	word-break: break-all;
+}
+
+/* Message 消息提示
+------------------------------- */
+.el-message {
+	min-width: unset !important;
+	padding: 15px !important;
+	box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.02);
+}
+
+/* NavMenu 导航菜单
+------------------------------- */
+// 鼠标 hover 时颜色
+.el-menu-hover-bg-color {
+	background-color: var(--next-color-menu-hover) !important;
+}
+// 默认样式修改
+.el-menu {
+	border-right: none !important;
+	width: 220px;
+}
+// 修复点击左侧菜单折叠再展开时,宽度不跟随问题
+.el-menu--collapse {
+	width: 64px !important;
+}
+.el-menu-item,
+.el-sub-menu__title {
+	color: var(--next-bg-menuBarColor);
+}
+.el-menu.el-menu--horizontal {
+	border-bottom: none !important;
+}
+.el-menu-item {
+	height: 56px !important;
+	line-height: 56px !important;
+}
+// 外部链接时
+.el-menu-item a,
+.el-menu-item a:hover,
+.el-menu-item i,
+.el-sub-menu__title i {
+	color: inherit;
+	text-decoration: none;
+}
+// 第三方图标字体间距/大小设置
+.el-menu-item .iconfont,
+.el-sub-menu .iconfont,
+.el-menu-item .fa,
+.el-sub-menu .fa {
+	@include generalIcon;
+}
+// 高亮时
+.el-menu-item.is-active,
+.el-sub-menu.is-active .el-sub-menu__title {
+	@extend .el-menu-hover-bg-color;
+	i,
+	span {
+		color: var(--el-menu-active-color) !important;
+	}
+}
+.el-sub-menu.is-active.is-opened {
+	.el-sub-menu__title {
+		background-color: unset !important;
+	}
+	i,
+	span {
+		color: unset !important;
+	}
+}
+// 鼠标 hover 时
+.el-menu-item:hover,
+.el-sub-menu__title:hover {
+	@extend .el-menu-hover-bg-color;
+}
+// 菜单收起时且时 a 链接
+.el-popper.is-dark a {
+	color: var(--el-color-white) !important;
+	text-decoration: none;
+}
+// 菜单收起时鼠标经过背景颜色/字体颜色
+.el-popper.is-light {
+	.el-menu--vertical {
+		.el-menu {
+			background: var(--next-bg-menuBar);
+		}
+	}
+	.el-menu--horizontal {
+		background: var(--next-bg-topBar);
+		.el-menu,
+		.el-menu-item,
+		.el-sub-menu__title {
+			color: var(--next-bg-topBarColor);
+			background: var(--next-bg-topBar);
+		}
+	}
+}
+
+/* Tabs 标签页
+------------------------------- */
+.el-tabs__nav-wrap::after {
+	height: 1px !important;
+}
+
+/* Dropdown 下拉菜单
+------------------------------- */
+.el-dropdown-menu .el-dropdown-menu__item {
+	white-space: nowrap;
+}
+
+/* Steps 步骤条
+------------------------------- */
+.el-step__icon-inner {
+	font-size: 30px !important;
+	font-weight: 400 !important;
+}
+.el-step__title {
+	font-size: 14px;
+}
+
+/* Dialog 对话框
+------------------------------- */
+.el-overlay {
+	overflow: hidden;
+	.el-overlay-dialog {
+		display: flex;
+		align-items: center;
+		justify-content: center;
+		position: unset !important;
+		width: 100%;
+		height: 100%;
+		.el-dialog {
+			margin: 0 auto !important;
+			position: absolute;
+			.el-dialog__body {
+				padding: 20px !important;
+			}
+		}
+	}
+}
+.el-dialog__body {
+	max-height: calc(90vh - 111px) !important;
+	overflow-y: auto;
+	overflow-x: hidden;
+}
+
+/* Card 卡片
+------------------------------- */
+.el-card__header {
+	padding: 15px 20px;
+}
+
+/* scrollbar
+------------------------------- */
+.el-scrollbar__bar {
+	z-index: 4;
+}
+.el-scrollbar__wrap {
+	overflow-x: hidden !important;
+	max-height: 100%; /*防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)*/
+}
+.el-select-dropdown .el-scrollbar__wrap {
+	overflow-x: scroll !important;
+}
+.el-select-dropdown__wrap {
+	max-height: 274px !important; /*修复Select 选择器高度问题*/
+}
+.el-cascader-menu__wrap.el-scrollbar__wrap {
+	height: 204px !important; /*修复Cascader 级联选择器高度问题*/
+}
+
+/* Drawer 抽屉
+------------------------------- */
+.el-drawer {
+	--el-drawer-padding-primary: unset !important;
+	.el-drawer__header {
+		padding: 0 15px !important;
+		height: 50px;
+		display: flex;
+		align-items: center;
+		margin-bottom: 0 !important;
+		border-bottom: 1px solid var(--el-border-color-base);
+		color: var(--el-text-color-primary);
+	}
+	.el-drawer__body {
+		width: 100%;
+		height: 100%;
+		overflow: auto;
+	}
+}

+ 70 - 0
nextadmin_vue3/src/theme/iconSelector.scss

@@ -0,0 +1,70 @@
+/* Popover 弹出框(图标选择器)
+------------------------------- */
+.icon-selector-popper {
+	padding: 0 !important;
+	.icon-selector-warp {
+		height: 260px;
+		overflow: hidden;
+		.icon-selector-warp-title {
+			height: 40px;
+			line-height: 40px;
+			padding: 0 15px;
+			.icon-selector-warp-title-tab {
+				span {
+					cursor: pointer;
+					&:hover {
+						color: var(--el-color-primary);
+						text-decoration: underline;
+					}
+				}
+				.span-active {
+					color: var(--el-color-primary);
+					text-decoration: underline;
+				}
+			}
+		}
+		.icon-selector-warp-row {
+			height: 230px;
+			overflow: hidden;
+			border-top: var(--el-border-base);
+			.el-row {
+				padding: 15px;
+			}
+			.el-scrollbar__bar.is-horizontal {
+				display: none;
+			}
+			.icon-selector-warp-item {
+				display: flex;
+				border: var(--el-border-base);
+				padding: 5px;
+				border-radius: 5px;
+				margin-bottom: 10px;
+				.icon-selector-warp-item-value {
+					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-6);
+					.icon-selector-warp-item-value {
+						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-6);
+				.icon-selector-warp-item-value {
+					i {
+						color: var(--el-color-primary);
+					}
+				}
+			}
+		}
+	}
+}

+ 9 - 0
nextadmin_vue3/src/theme/index.scss

@@ -0,0 +1,9 @@
+@import './app.scss';
+@import 'common/transition.scss';
+@import './other.scss';
+@import './element.scss';
+@import './iconSelector.scss';
+@import './media/media.scss';
+@import './waves.scss';
+@import './dark.scss';
+@import './yckj-common.scss';

+ 51 - 0
nextadmin_vue3/src/theme/loading.scss

@@ -0,0 +1,51 @@
+.loading-next {
+	width: 100%;
+	height: 100%;
+}
+.loading-next .loading-next-box {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	transform: translate(-50%, -50%);
+}
+.loading-next .loading-next-box-warp {
+	width: 80px;
+	height: 80px;
+}
+.loading-next .loading-next-box-warp .loading-next-box-item {
+	width: 33.333333%;
+	height: 33.333333%;
+	background: var(--el-color-primary);
+	float: left;
+	animation: loading-next-animation 1.2s infinite ease;
+	border-radius: 1px;
+}
+.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(7) {
+	animation-delay: 0s;
+}
+.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(4),
+.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(8) {
+	animation-delay: 0.1s;
+}
+.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(1),
+.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(5),
+.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(9) {
+	animation-delay: 0.2s;
+}
+.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(2),
+.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(6) {
+	animation-delay: 0.3s;
+}
+.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(3) {
+	animation-delay: 0.4s;
+}
+@keyframes loading-next-animation {
+	0%,
+	70%,
+	100% {
+		transform: scale3D(1, 1, 1);
+	}
+	35% {
+		transform: scale3D(0, 0, 1);
+	}
+}

+ 94 - 0
nextadmin_vue3/src/theme/media/chart.scss

@@ -0,0 +1,94 @@
+@import './index.scss';
+
+/* 页面宽度小于768px
+------------------------------- */
+@media screen and (max-width: $sm) {
+	.big-data-down-left {
+		width: 100% !important;
+		flex-direction: unset !important;
+		flex-wrap: wrap;
+		.flex-warp-item {
+			min-height: 196.24px;
+			padding: 0 7.5px 15px 15px !important;
+			.flex-warp-item-box {
+				border: none !important;
+				border-bottom: 1px solid #ebeef5 !important;
+			}
+		}
+	}
+	.big-data-down-center {
+		width: 100% !important;
+		.big-data-down-center-one,
+		.big-data-down-center-two {
+			min-height: 196.24px;
+			padding-left: 15px !important;
+			.big-data-down-center-one-content {
+				border: none !important;
+				border-bottom: 1px solid #ebeef5 !important;
+			}
+			.flex-warp-item-box {
+				@extend .big-data-down-center-one-content;
+			}
+		}
+	}
+	.big-data-down-right {
+		.flex-warp-item {
+			.flex-warp-item-box {
+				border: none !important;
+				border-bottom: 1px solid #ebeef5 !important;
+			}
+			&:nth-of-type(2) {
+				padding-left: 15px !important;
+			}
+			&:last-of-type {
+				.flex-warp-item-box {
+					border: none !important;
+				}
+			}
+		}
+	}
+}
+
+/* 页面宽度大于768px小于1200px
+------------------------------- */
+@media screen and (min-width: $sm) and (max-width: $lg) {
+	.chart-warp-bottom {
+		.big-data-down-left {
+			width: 50% !important;
+		}
+		.big-data-down-center {
+			width: 50% !important;
+		}
+		.big-data-down-right {
+			.flex-warp-item {
+				width: 50% !important;
+				&:nth-of-type(2) {
+					padding-left: 7.5px !important;
+				}
+			}
+		}
+	}
+}
+
+/* 页面宽度小于1200px
+------------------------------- */
+@media screen and (max-width: $lg) {
+	.chart-warp-top {
+		.up-left {
+			display: none;
+		}
+	}
+	.chart-warp-bottom {
+		overflow-y: auto !important;
+		flex-wrap: wrap;
+		.big-data-down-right {
+			width: 100% !important;
+			flex-direction: unset !important;
+			flex-wrap: wrap;
+			.flex-warp-item {
+				min-height: 196.24px;
+				padding: 0 7.5px 15px 15px !important;
+			}
+		}
+	}
+}

+ 10 - 0
nextadmin_vue3/src/theme/media/cityLinkage.scss

@@ -0,0 +1,10 @@
+@import './index.scss';
+
+/* 页面宽度小于576px
+------------------------------- */
+@media screen and (max-width: $xs) {
+	.el-cascader__dropdown.el-popper {
+		overflow: auto;
+		max-width: 100%;
+	}
+}

+ 25 - 0
nextadmin_vue3/src/theme/media/date.scss

@@ -0,0 +1,25 @@
+@import './index.scss';
+
+/* 页面宽度小于768px
+------------------------------- */
+@media screen and (max-width: $sm) {
+	// 时间选择器适配
+	.el-date-range-picker {
+		width: 100vw;
+		.el-picker-panel__body {
+			min-width: 100%;
+			.el-date-range-picker__content {
+				.el-date-range-picker__header div {
+					margin-left: 22px;
+					margin-right: 0px;
+				}
+				& + .el-date-range-picker__content {
+					.el-date-range-picker__header div {
+						margin-left: 0px;
+						margin-right: 22px;
+					}
+				}
+			}
+		}
+	}
+}

+ 12 - 0
nextadmin_vue3/src/theme/media/dialog.scss

@@ -0,0 +1,12 @@
+@import './index.scss';
+
+/* 页面宽度小于800px
+------------------------------- */
+@media screen and (max-width: 800px) {
+	.el-dialog {
+		width: 90% !important;
+	}
+	.el-dialog.is-fullscreen {
+		width: 100% !important;
+	}
+}

+ 35 - 0
nextadmin_vue3/src/theme/media/error.scss

@@ -0,0 +1,35 @@
+@import './index.scss';
+
+/* 页面宽度小于768px
+------------------------------- */
+@media screen and (max-width: $sm) {
+	.error {
+		.error-flex {
+			flex-direction: column-reverse !important;
+			height: auto !important;
+			width: 100% !important;
+		}
+		.right,
+		.left {
+			flex: unset !important;
+			display: flex !important;
+		}
+		.left-item {
+			margin: auto !important;
+		}
+		.right img {
+			max-width: 450px !important;
+			@extend .left-item;
+		}
+	}
+}
+
+/* 页面宽度大于768px小于992px
+------------------------------- */
+@media screen and (min-width: $sm) and (max-width: $md) {
+	.error {
+		.error-flex {
+			padding-left: 30px !important;
+		}
+	}
+}

+ 0 - 0
nextadmin_vue3/src/theme/media/form.scss


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