Browse Source

第一次提交

master
wserver/wangdisen 3 years ago
commit
dd58e0e3f4
  1. 8
      .env
  2. 5
      .env.development
  3. 10
      .env.production
  4. 18
      .eslintignore
  5. 63
      .eslintrc.js
  6. 23
      .gitignore
  7. 39
      .prettierrc.js
  8. 307
      CHANGELOG.md
  9. 21
      LICENSE
  10. 62
      README.md
  11. 32
      index.html
  12. 6896
      package-lock.json
  13. 80
      package.json
  14. 3
      plugins.d.ts
  15. BIN
      public/favicon.ico
  16. 13
      shim.d.ts
  17. 6
      source.d.ts
  18. 92
      src/App.vue
  19. 34
      src/api/login/index.ts
  20. 42
      src/api/system/config/index.ts
  21. 27
      src/api/system/dbInit/index.ts
  22. 36
      src/api/system/dept/index.ts
  23. 70
      src/api/system/dict/data.ts
  24. 43
      src/api/system/dict/type.ts
  25. 57
      src/api/system/menu/index.ts
  26. 28
      src/api/system/monitor/loginLog/index.ts
  27. 9
      src/api/system/monitor/server/index.ts
  28. 35
      src/api/system/post/index.ts
  29. 50
      src/api/system/role/index.ts
  30. 75
      src/api/system/user/index.ts
  31. BIN
      src/assets/401.png
  32. BIN
      src/assets/404.png
  33. BIN
      src/assets/bg.jpg
  34. 9
      src/assets/login-icon-two.svg
  35. 9
      src/assets/logo-mini.svg
  36. 27
      src/components/auth/auth.vue
  37. 28
      src/components/auth/authAll.vue
  38. 33
      src/components/auth/auths.vue
  39. 148
      src/components/cropper/index.vue
  40. 79
      src/components/editor/index.vue
  41. 242
      src/components/iconSelector/index.vue
  42. 194
      src/components/noticeBar/index.vue
  43. 101
      src/components/pagination/index.vue
  44. 42
      src/components/svgIcon/index.vue
  45. 58
      src/i18n/index.ts
  46. 181
      src/i18n/lang/en.ts
  47. 181
      src/i18n/lang/zh-cn.ts
  48. 181
      src/i18n/lang/zh-tw.ts
  49. 13
      src/i18n/pages/formI18n/en.ts
  50. 13
      src/i18n/pages/formI18n/zh-cn.ts
  51. 13
      src/i18n/pages/formI18n/zh-tw.ts
  52. 29
      src/i18n/pages/login/en.ts
  53. 28
      src/i18n/pages/login/zh-cn.ts
  54. 28
      src/i18n/pages/login/zh-tw.ts
  55. 157
      src/layout/component/aside.vue
  56. 297
      src/layout/component/columnsAside.vue
  57. 32
      src/layout/component/header.vue
  58. 84
      src/layout/component/main.vue
  59. 46
      src/layout/footer/index.vue
  60. 55
      src/layout/index.vue
  61. 370
      src/layout/lockScreen/index.vue
  62. 82
      src/layout/logo/index.vue
  63. 36
      src/layout/main/classic.vue
  64. 38
      src/layout/main/columns.vue
  65. 44
      src/layout/main/defaults.vue
  66. 16
      src/layout/main/transverse.vue
  67. 152
      src/layout/navBars/breadcrumb/breadcrumb.vue
  68. 63
      src/layout/navBars/breadcrumb/closeFull.vue
  69. 115
      src/layout/navBars/breadcrumb/index.vue
  70. 134
      src/layout/navBars/breadcrumb/search.vue
  71. 795
      src/layout/navBars/breadcrumb/setings.vue
  72. 302
      src/layout/navBars/breadcrumb/user.vue
  73. 114
      src/layout/navBars/breadcrumb/userNews.vue
  74. 37
      src/layout/navBars/index.vue
  75. 137
      src/layout/navBars/tagsView/contextmenu.vue
  76. 681
      src/layout/navBars/tagsView/tagsView.vue
  77. 151
      src/layout/navMenu/horizontal.vue
  78. 47
      src/layout/navMenu/subItem.vue
  79. 98
      src/layout/navMenu/vertical.vue
  80. 60
      src/layout/routerView/iframes.vue
  81. 59
      src/layout/routerView/link.vue
  82. 83
      src/layout/routerView/parent.vue
  83. 39
      src/main.ts
  84. 108
      src/router/backEnd.ts
  85. 25
      src/router/frontEnd.ts
  86. 258
      src/router/index.ts
  87. 1028
      src/router/route.ts
  88. 27
      src/store/index.ts
  89. 98
      src/store/interface/index.ts
  90. 23
      src/store/modules/keepAliveNames.ts
  91. 23
      src/store/modules/requestOldRoutes.ts
  92. 41
      src/store/modules/routesList.ts
  93. 34
      src/store/modules/tagsViewRoutes.ts
  94. 153
      src/store/modules/themeConfig.ts
  95. 47
      src/store/modules/userInfos.ts
  96. 283
      src/theme/app.scss
  97. 94
      src/theme/common/transition.scss
  98. 219
      src/theme/dark.scss
  99. 224
      src/theme/element.scss
  100. 70
      src/theme/iconSelector.scss
  101. Some files were not shown because too many files have changed in this diff Show More

8
.env

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

5
.env.development

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

10
.env.production

@ -0,0 +1,10 @@
# 线上环境
ENV = 'production'
# public path 配置线上环境路径(打包)、本地通过 http-server 访问时,请置空即可
VITE_PUBLIC_PATH = /sys/
# 线上环境接口地址
VITE_API_URL = '/'

18
.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
.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',
},
};

23
.gitignore vendored

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

39
.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',
};

307
CHANGELOG.md

@ -0,0 +1,307 @@
# <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.2
`2022.03.04`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 Alert 提示添加边框
- 🎯 优化 功能 / 数字滚动 演示界面
- 🐞 修复 全局主题按钮颜色 :active 问题
- 🐞 修复 Dropdown 下拉菜单样式问题
- 🐞 修复 SvgIcon 图标组件动态切换时报警告问题,[SvgIcon 改变 name 时可能导致图像不显示](https://gitee.com/lyt-top/vue-next-admin/issues/I4VGE0),感谢@axcc1234
## 2.0.1
`2022.02.25`
- 🌟 更新 依赖更新最新版本
- 🎯 优化 svgIcon 图标组件
- 🎯 优化 vite.config.ts 打包,感谢群友@YourObjec
- 🐞 修复 tagViews 开启图标不显示问题(风格 5),感谢群友@坏人
- 🐞 修复 [Element Plus 1.2.0-beta.6 以后的版本 el-table 在移动端无法左右滑动](https://gitee.com/lyt-top/vue-next-admin/issues/I4UPTP),感谢@YGDada
## 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
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.

62
README.md

@ -0,0 +1,62 @@
<div align="center">
<img src="https://yxh-1301841944.cos.ap-chongqing.myqcloud.com/gfast/2022-04-19/gfastlogo.png">
<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 的后台开源免费模板,希望减少工作量,帮助大家实现快速开发。
#### 🚧 安装 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/tiger1103/gfast-ui.git
# 进入项目
cd gfast-ui
# 安装依赖
cnpm install
# 运行项目
cnpm run dev
# 打包发布
cnpm run build
```
## ❤特别鸣谢
* 感谢[VUE-NEXT-ADMIN](https://github.com/lyt-Top/vue-next-admin)

32
index.html

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

6896
package-lock.json generated

File diff suppressed because it is too large Load Diff

80
package.json

@ -0,0 +1,80 @@
{
"name": "vue-next-admin",
"version": "2.0.2",
"description": "vue3 vite next admin template",
"author": "lyt_20201208",
"license": "MIT",
"scripts": {
"dev": "vite --force",
"build": "vite build",
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/"
},
"dependencies": {
"@element-plus/icons-vue": "^1.0.0",
"axios": "^0.26.0",
"countup.js": "^2.1.0",
"cropperjs": "^1.5.12",
"echarts": "^5.3.0",
"echarts-gl": "^2.0.9",
"echarts-wordcloud": "^2.0.0",
"element-plus": "^2.0.4",
"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.1.1",
"vue": "^3.2.31",
"vue-clipboard3": "^1.0.1",
"vue-grid-layout": "^3.0.0-beta1",
"vue-i18n": "^9.1.9",
"vue-router": "^4.0.13",
"vuex": "^4.0.2",
"wangeditor": "^4.7.12"
},
"devDependencies": {
"@types/node": "^17.0.21",
"@types/nprogress": "^0.2.0",
"@types/sortablejs": "^1.10.7",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"@vitejs/plugin-vue": "^2.2.4",
"@vue/compiler-sfc": "^3.2.31",
"dotenv": "^16.0.0",
"eslint": "8.22.0",
"eslint-plugin-vue": "^8.5.0",
"prettier": "^2.5.1",
"sass": "^1.49.9",
"sass-loader": "^12.6.0",
"typescript": "^4.6.2",
"vite": "^2.8.6",
"vue-eslint-parser": "^8.3.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
plugins.d.ts vendored

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

BIN
public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

13
shim.d.ts vendored

@ -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
source.d.ts vendored

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

92
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();
});
// i18nApp.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>

34
src/api/login/index.ts

@ -0,0 +1,34 @@
import request from '/@/utils/request';
/**
* api接口集合
* @method signIn
*/
export function login(params: object){
return request({
url: '/api/v1/system/login',
method: 'post',
data: params,
});
}
/**
*
*/
export function captcha(){
return request({
url:"/api/v1/pub/captcha/get",
method:"get"
})
}
/**
* 退
*/
export function signOut(params: object){
return request({
url: '/api/v1/user/signOut',
method: 'post',
data: params,
});
}

42
src/api/system/config/index.ts

@ -0,0 +1,42 @@
import request from '/@/utils/request';
export function getConfigList(query:Object) {
return request({
url: '/api/v1/system/config/list',
method: 'get',
params:query
})
}
export function getConfig(id:number) {
return request({
url: '/api/v1/system/config/get',
method: 'get',
params:{id}
})
}
export function addConfig(data:any) {
return request({
url: '/api/v1/system/config/add',
method: 'post',
data:data
})
}
export function editConfig(data:any) {
return request({
url: '/api/v1/system/config/edit',
method: 'put',
data:data
})
}
export function deleteConfig(ids:number[]) {
return request({
url: '/api/v1/system/config/delete',
method: 'delete',
data:{ids}
})
}

27
src/api/system/dbInit/index.ts

@ -0,0 +1,27 @@
import request from '/@/utils/request';
// 是否已初始化
export function isInit() {
return request({
url: '/api/v1/system/dbInit/isInit',
method: 'get'
})
}
// 获取环境信息
export function getEnvInfo() {
return request({
url: '/api/v1/system/dbInit/getEnvInfo',
method: 'get'
})
}
// 创建数据库
export function createDb (data:object) {
return request({
url: '/api/v1/system/dbInit/createDb',
method: 'post',
data: data
})
}

36
src/api/system/dept/index.ts

@ -0,0 +1,36 @@
import request from '/@/utils/request';
export function getDeptList(query?:Object) {
return request({
url: '/api/v1/system/dept/list',
method: 'get',
params:query
})
}
export function addDept(data:object) {
return request({
url: '/api/v1/system/dept/add',
method: 'post',
data:data
})
}
export function editDept(data:object) {
return request({
url: '/api/v1/system/dept/edit',
method: 'put',
data:data
})
}
export function deleteDept(id:number) {
return request({
url: '/api/v1/system/dept/delete',
method: 'delete',
data:{id}
})
}

70
src/api/system/dict/data.ts

@ -0,0 +1,70 @@
import request from '/@/utils/request';
import {ref ,toRefs,ToRefs} from 'vue'
// 根据字典类型查询字典数据信息
export function getDicts(dictType :string,defaultValue?:string):Promise<any> {
let dv = defaultValue??''
let params ={
dictType:dictType,
defaultValue:dv
}
return request({
url: '/api/v1/system/dict/data/getDictData',
method: 'get',
params:params
})
}
/**
*
*/
export function useDict(...args:string[]):ToRefs<any>{
const res:any = ref({});
args.forEach((d:string) => {
res.value[d] = [];
getDicts(d).then(resp => {
res.value[d] = resp.data.values.map((p:any) => ({ label: p.value, value: p.key, isDefault: p.isDefault }))
})
})
return toRefs(res.value);
}
export function getDataList(query:Object) {
return request({
url: '/api/v1/system/dict/data/list',
method: 'get',
params:query
})
}
export function getData(dictCode:number) {
return request({
url: '/api/v1/system/dict/data/get',
method: 'get',
params:{dictCode}
})
}
export function addData(data:any) {
return request({
url: '/api/v1/system/dict/data/add',
method: 'post',
data:data
})
}
export function editData(data:any) {
return request({
url: '/api/v1/system/dict/data/edit',
method: 'put',
data:data
})
}
export function deleteData(ids:number[]) {
return request({
url: '/api/v1/system/dict/data/delete',
method: 'delete',
data:{ids}
})
}

43
src/api/system/dict/type.ts

@ -0,0 +1,43 @@
import request from '/@/utils/request';
export function getTypeList(query:Object) {
return request({
url: '/api/v1/system/dict/type/list',
method: 'get',
params:query
})
}
export function getType(dictId:number) {
return request({
url: '/api/v1/system/dict/type/get',
method: 'get',
params:{dictId}
})
}
export function addType(data:any) {
return request({
url: '/api/v1/system/dict/type/add',
method: 'post',
data:data
})
}
export function editType(data:any) {
return request({
url: '/api/v1/system/dict/type/edit',
method: 'put',
data:data
})
}
export function deleteType(dictIds:number[]) {
return request({
url: '/api/v1/system/dict/type/delete',
method: 'delete',
data:{dictIds}
})
}

57
src/api/system/menu/index.ts

@ -0,0 +1,57 @@
import request from '/@/utils/request';
export function getMenuList(query:Object) {
return request({
url: '/api/v1/system/menu/list',
method: 'get',
params:query
})
}
export function getUserMenus() {
return request({
url: '/api/v1/system/user/getUserMenus',
method: 'get'
})
}
export function getMenuParams() {
return request({
url: '/api/v1/system/menu/getParams',
method: 'get'
})
}
export function addMenu(data:Object) {
return request({
url: '/api/v1/system/menu/add',
method: 'post',
data:data
})
}
export function getMenuInfo(id:number) {
return request({
url: '/api/v1/system/menu/get',
method: 'get',
params:{id}
})
}
export function updateMenu(data:Object) {
return request({
url: '/api/v1/system/menu/update',
method: 'put',
data:data
})
}
// 删除菜单
export function delMenu(menuId:number) {
return request({
url: '/api/v1/system/menu/delete',
method: 'delete',
data:{ids:[menuId]}
})
}

28
src/api/system/monitor/loginLog/index.ts

@ -0,0 +1,28 @@
import request from '/@/utils/request';
export function logList(query:object) {
return request({
url: '/api/v1/system/loginLog/list',
method: 'get',
params:query
})
}
export function deleteLog(ids:number[]) {
return request({
url: '/api/v1/system/loginLog/delete',
method: 'delete',
params:{ids}
})
}
export function clearLog() {
return request({
url: '/api/v1/system/loginLog/clear',
method: 'delete',
})
}

9
src/api/system/monitor/server/index.ts

@ -0,0 +1,9 @@
import request from '/@/utils/request';
export function getSysInfo() {
return request({
url: '/api/v1/system/monitor/server',
method: 'get'
})
}

35
src/api/system/post/index.ts

@ -0,0 +1,35 @@
import request from '/@/utils/request';
export function getPostList(query:Object) {
return request({
url: '/api/v1/system/post/list',
method: 'get',
params:query
})
}
export function addPost(data:object) {
return request({
url: '/api/v1/system/post/add',
method: 'post',
data:data
})
}
export function editPost(data:object) {
return request({
url: '/api/v1/system/post/edit',
method: 'put',
data:data
})
}
export function deletePost(ids:number[]) {
return request({
url: '/api/v1/system/post/delete',
method: 'delete',
data:{ids}
})
}

50
src/api/system/role/index.ts

@ -0,0 +1,50 @@
import request from '/@/utils/request';
export function getRoleList(query:Object) {
return request({
url: '/api/v1/system/role/list',
method: 'get',
params:query
})
}
export function getRoleParams() {
return request({
url: '/api/v1/system/role/getParams',
method: 'get'
})
}
export function addRole(data:object) {
return request({
url: '/api/v1/system/role/add',
method: 'post',
data:data
})
}
export function getRole(id:number) {
return request({
url: '/api/v1/system/role/get',
method: 'get',
params:{id}
})
}
export function editRole(data:object) {
return request({
url: '/api/v1/system/role/edit',
method: 'put',
data:data
})
}
export function deleteRole(id:number) {
return request({
url: '/api/v1/system/role/delete',
method: 'delete',
data:{ids:[id]}
})
}

75
src/api/system/user/index.ts

@ -0,0 +1,75 @@
import request from '/@/utils/request';
export function getUserList(query:Object) {
return request({
url: '/api/v1/system/user/list',
method: 'get',
params:query
})
}
export function getDeptTree() {
return request({
url: '/api/v1/system/dept/treeSelect',
method: 'get'
})
}
export function getParams() {
return request({
url: '/api/v1/system/user/params',
method: 'get'
})
}
export function getEditUser(id:number) {
return request({
url: '/api/v1/system/user/getEdit',
method: 'get',
params:{id}
})
}
export function addUser(data:object) {
return request({
url: '/api/v1/system/user/add',
method: 'post',
data:data
})
}
export function editUser(data:object) {
return request({
url: '/api/v1/system/user/edit',
method: 'put',
data:data
})
}
export function resetUserPwd(userId:number, password:string) {
return request({
url: '/api/v1/system/user/resetPwd',
method: 'put',
data:{userId,password}
})
}
export function changeUserStatus(userId:number, status:number) {
return request({
url: '/api/v1/system/user/setStatus',
method: 'put',
data:{userId,status}
})
}
export function deleteUser(ids:number[]) {
return request({
url: '/api/v1/system/user/delete',
method: 'delete',
data:{ids}
})
}

BIN
src/assets/401.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
src/assets/404.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
src/assets/bg.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

9
src/assets/login-icon-two.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

9
src/assets/logo-mini.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.0 KiB

27
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
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
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
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" size="default"> </el-button>
<el-button type="primary" @click="onSubmit" size="default"> </el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { reactive, toRefs, nextTick, defineComponent } from 'vue';
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
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
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
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>

101
src/components/pagination/index.vue

@ -0,0 +1,101 @@
<template>
<div :class="{'hidden':hidden}" class="pagination-container">
<el-pagination
:background="background"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:layout="layout"
:page-sizes="pageSizes"
:pager-count="pagerCount"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script lang="ts">
import { toRefs, defineComponent,computed } from 'vue';
const props = {
total: {
required: true,
type: Number
},
page: {
type: Number,
default: 1
},
limit: {
type: Number,
default: 20
},
pageSizes: {
type: Array,
default() {
return [10, 20, 30, 50]
}
},
// 5
pagerCount: {
type: Number,
default: document.body.clientWidth < 992 ? 5 : 7
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
background: {
type: Boolean,
default: true
},
hidden: {
type: Boolean,
default: false
}
};
export default defineComponent({
name: 'pagination',
props: props,
setup(props,{emit}){
const { page,limit,pageSizes } = toRefs(props);
const currentPage = computed({
get() {
return page.value;
},
set(val) {
emit('update:page', val)
}
});
const pageSize = computed({
get() {
return limit.value
},
set(val) {
emit('update:limit', val)
}
});
const handleSizeChange = (val:number) => {
emit('pagination', { page: currentPage.value, limit: val })
};
const handleCurrentChange=(val:number) => {
emit('pagination', { page: val, limit: pageSizes.value })
}
return {
currentPage,
pageSize,
handleSizeChange,
handleCurrentChange
}
}
});
</script>
<style scoped lang="scss">
.pagination-container {
background: #fff;
padding: 32px 16px;
}
.pagination-container.hidden {
display: none;
}
</style>

42
src/components/svgIcon/index.vue

@ -0,0 +1,42 @@
<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: any[] = ['https', 'http', '/src', '/assets', import.meta.env.VITE_PUBLIC_PATH];
const onLineStyle: string = `font-size: ${props.size}px;color: ${props.color}`;
const localsStyle: string = `width: ${props.size}px;height: ${props.size}px`;
const eleSetStyle = { class: 'el-icon', style: onLineStyle };
//
if (props.name?.startsWith('ele-')) return () => h('i', eleSetStyle, [props.name === 'ele-' ? '' : 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>

58
src/i18n/index.ts

@ -0,0 +1,58 @@
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 nextEn from '/@/i18n/lang/en';
import nextZhtw from '/@/i18n/lang/zh-tw';
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: {
...nextEn,
...pagesLoginEn,
...pagesFormI18nEn,
},
},
[zhtwLocale.name]: {
...zhtwLocale,
message: {
...nextZhtw,
...pagesLoginZhtw,
...pagesFormI18nZhtw,
},
},
};
// 导出语言国际化
export const i18n = createI18n({
legacy:false,
locale: store.state.themeConfig.themeConfig.globalI18n,
fallbackLocale: zhcnLocale.name,
globalInjection: true,
messages,
});

181
src/i18n/lang/en.ts

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

181
src/i18n/lang/zh-cn.ts

@ -0,0 +1,181 @@
// 定义内容
export default {
router: {
home: '首页',
system: '系统设置',
systemMenu: '菜单管理',
systemRole: '角色管理',
systemUser: '用户管理',
systemDept: '部门管理',
systemDic: '字典管理',
limits: '权限管理',
limitsFrontEnd: '前端控制',
limitsFrontEndPage: '页面权限',
limitsFrontEndBtn: '按钮权限',
limitsBackEnd: '后端控制',
limitsBackEndEndPage: '页面权限',
menu: '菜单嵌套',
menu1: '菜单1',
menu11: '菜单11',
menu12: '菜单12',
menu121: '菜单121',
menu122: '菜单122',
menu13: '菜单13',
menu2: '菜单2',
funIndex: '功能',
funTagsView: 'tagsView 操作',
funCountup: '数字滚动',
funWangEditor: 'Editor 编辑器',
funCropper: '图片裁剪',
funQrcode: '二维码生成',
funEchartsMap: '地理坐标/地图',
funPrintJs: '页面打印',
funClipboard: '复制剪切',
funGridLayout: '拖拽布局',
funSplitpanes: '窗格拆分器',
funDragVerify: '验证器',
pagesIndex: '页面',
pagesFiltering: '过滤筛选组件',
pagesFilteringDetails: '过滤筛选组件详情',
pagesFilteringDetails1: '过滤筛选组件详情111',
pagesIocnfont: 'ali 字体图标',
pagesElement: 'ele 字体图标',
pagesAwesome: 'awe 字体图标',
pagesFormAdapt: '表单自适应',
pagesTableRules: '表单表格验证',
pagesFormI18n: '表单国际化',
pagesFormRules: '多表单验证',
pagesDynamicForm: '动态复杂表单',
pagesWorkflow: '工作流',
pagesListAdapt: '列表自适应',
pagesWaterfall: '瀑布屏',
pagesSteps: '步骤条',
pagesPreview: '大图预览',
pagesWaves: '波浪效果',
pagesTree: '树形改表格',
pagesDrag: '拖动指令',
pagesLazyImg: '图片懒加载',
makeIndex: '组件封装',
makeSelector: '图标选择器',
makeNoticeBar: '滚动通知栏',
makeSvgDemo: 'svgIcon 演示',
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: '复制失败!',
},
};

181
src/i18n/lang/zh-tw.ts

@ -0,0 +1,181 @@
// 定义内容
export default {
router: {
home: '首頁',
system: '系統設置',
systemMenu: '選單管理',
systemRole: '角色管理',
systemUser: '用戶管理',
systemDept: '部門管理',
systemDic: '字典管理',
limits: '許可權管理',
limitsFrontEnd: '前端控制',
limitsFrontEndPage: '頁面許可權',
limitsFrontEndBtn: '按鈕許可權',
limitsBackEnd: '後端控制',
limitsBackEndEndPage: '頁面許可權',
menu: '選單嵌套',
menu1: '選單1',
menu11: '選單11',
menu12: '選單12',
menu121: '選單121',
menu122: '選單122',
menu13: '選單13',
menu2: '選單2',
funIndex: '功能',
funTagsView: 'tagsView 操作',
funCountup: '數位滾動',
funWangEditor: 'Editor 編輯器',
funCropper: '圖片裁剪',
funQrcode: '二維碼生成',
funEchartsMap: '地理座標/地圖',
funPrintJs: '頁面列印',
funClipboard: '複製剪切',
funGridLayout: '拖拽佈局',
funSplitpanes: '窗格折開器',
funDragVerify: '驗證器',
pagesIndex: '頁面',
pagesFiltering: '過濾篩選組件',
pagesFilteringDetails: '過濾篩選組件詳情',
pagesFilteringDetails1: '過濾篩選組件詳情111',
pagesIocnfont: 'ali 字體圖標',
pagesElement: 'ele 字體圖標',
pagesAwesome: 'awe 字體圖標',
pagesFormAdapt: '表單自我調整',
pagesTableRules: '表單表格驗證',
pagesFormI18n: '表單國際化',
pagesFormRules: '多表單驗證',
pagesDynamicForm: '動態複雜表單',
pagesWorkflow: '工作流',
pagesListAdapt: '清單自我調整',
pagesWaterfall: '瀑布屏',
pagesSteps: '步驟條',
pagesPreview: '大圖預覽',
pagesWaves: '波浪效果',
pagesTree: '樹形改表格',
pagesDrag: '拖動指令',
pagesLazyImg: '圖片懶加載',
makeIndex: '組件封裝',
makeSelector: '圖標選擇器',
makeNoticeBar: '滾動通知欄',
makeSvgDemo: 'svgIcon 演示',
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
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
src/i18n/pages/formI18n/zh-cn.ts

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

13
src/i18n/pages/formI18n/zh-tw.ts

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

29
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
src/i18n/pages/login/zh-cn.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: '欢迎回来!',
};

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

297
src/layout/component/columnsAside.vue

@ -0,0 +1,297 @@
<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="v.meta.title.indexOf('.')>0?$t(v.meta.title):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">
{{tMenuTitle(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">
{{tMenuTitle(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';
import {useI18n} from "vue-i18n";
//
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 { t } = useI18n();
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 tMenuTitle = (title:string):string=>{
let rTitle = title.indexOf('.')>0?t(title):title;
rTitle && rTitle.length >= 4
? rTitle.substring(0, store.state.themeConfig.themeConfig.columnsAsideLayout === 'columns-vertical' ? 4 : 3)
: rTitle
return rTitle
};
//
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,
tMenuTitle,
...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
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
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
src/layout/footer/index.vue

@ -0,0 +1,46 @@
<template>
<div class="layout-footer mt15" v-show="isDelayFooter">
<div class="layout-footer-warp">
<div>Copyright © 2021-2023 g-fast.cn All Rights Reserved.</div>
<div class="mt5">厦门二维塔科技有限公司版权所有</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>

55
src/layout/index.vue

@ -0,0 +1,55 @@
<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);
});
return {
getThemeConfig,
};
},
});
</script>

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

82
src/layout/logo/index.vue

@ -0,0 +1,82 @@
<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: 45px;
}
}
.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
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
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
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
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
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" />{{ v.meta.title.indexOf('.')>0?$t(v.meta.title):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" />{{ v.meta.title.indexOf('.')>0?$t(v.meta.title):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
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
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
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" />
{{ item.meta.title.indexOf('.')>0?$t(item.meta.title):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>

795
src/layout/navBars/breadcrumb/setings.vue

@ -0,0 +1,795 @@
<template>
<div class="layout-breadcrumb-seting">
<el-drawer
:title="$t('message.layout.configTitle')"
v-model="getThemeConfig.isDrawer"
direction="rtl"
destroy-on-close
size="260px"
@close="onDrawerClose"
>
<el-scrollbar class="layout-breadcrumb-seting-bar">
<!-- 全局主题 -->
<el-divider content-position="left">{{ $t('message.layout.oneTitle') }}</el-divider>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">primary</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="getThemeConfig.primary" size="default" @change="onColorPickerChange"> </el-color-picker>
</div>
</div>
<!--<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsDark') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="getThemeConfig.isIsDark" size="small" @change="onAddDarkChange"></el-switch>
</div>
</div>-->
<!-- 顶栏设置 -->
<el-divider content-position="left">{{ $t('message.layout.twoTopTitle') }}</el-divider>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoTopBar') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="getThemeConfig.topBar" size="default" @change="onBgColorPickerChange('topBar')"> </el-color-picker>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoTopBarColor') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="getThemeConfig.topBarColor" size="default" @change="onBgColorPickerChange('topBarColor')"> </el-color-picker>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt10">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoIsTopBarColorGradual') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="getThemeConfig.isTopBarColorGradual" size="small" @change="onTopBarGradualChange"></el-switch>
</div>
</div>
<!-- 菜单设置 -->
<el-divider content-position="left">{{ $t('message.layout.twoMenuTitle') }}</el-divider>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoMenuBar') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="getThemeConfig.menuBar" size="default" @change="onBgColorPickerChange('menuBar')"> </el-color-picker>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoMenuBarColor') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="getThemeConfig.menuBarColor" size="default" @change="onBgColorPickerChange('menuBarColor')"> </el-color-picker>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt14">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoIsMenuBarColorGradual') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="getThemeConfig.isMenuBarColorGradual" size="small" @change="onMenuBarGradualChange"></el-switch>
</div>
</div>
<!-- 分栏设置 -->
<el-divider content-position="left" :style="{ opacity: getThemeConfig.layout !== 'columns' ? 0.5 : 1 }">{{
$t('message.layout.twoColumnsTitle')
}}</el-divider>
<div class="layout-breadcrumb-seting-bar-flex" :style="{ opacity: getThemeConfig.layout !== 'columns' ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoColumnsMenuBar') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker
v-model="getThemeConfig.columnsMenuBar"
size="default"
@change="onBgColorPickerChange('columnsMenuBar')"
:disabled="getThemeConfig.layout !== 'columns'"
>
</el-color-picker>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex" :style="{ opacity: getThemeConfig.layout !== 'columns' ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoColumnsMenuBarColor') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker
v-model="getThemeConfig.columnsMenuBarColor"
size="default"
@change="onBgColorPickerChange('columnsMenuBarColor')"
:disabled="getThemeConfig.layout !== 'columns'"
>
</el-color-picker>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt14" :style="{ opacity: getThemeConfig.layout !== 'columns' ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.twoIsColumnsMenuBarColorGradual') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch
v-model="getThemeConfig.isColumnsMenuBarColorGradual"
size="small"
@change="onColumnsMenuBarGradualChange"
:disabled="getThemeConfig.layout !== 'columns'"
></el-switch>
</div>
</div>
<!-- 界面设置 -->
<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"
size="default"
style="width: 90px"
>
</el-input-number>
</div>
</div>
<!-- 界面显示 -->
<el-divider content-position="left">{{ $t('message.layout.fourTitle') }}</el-divider>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsShowLogo') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="getThemeConfig.isShowLogo" size="small" @change="onIsShowLogoChange"></el-switch>
</div>
</div>
<div
class="layout-breadcrumb-seting-bar-flex mt15"
:style="{ opacity: getThemeConfig.layout === 'classic' || getThemeConfig.layout === 'transverse' ? 0.5 : 1 }"
>
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsBreadcrumb') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch
v-model="getThemeConfig.isBreadcrumb"
:disabled="getThemeConfig.layout === 'classic' || getThemeConfig.layout === 'transverse'"
size="small"
@change="onIsBreadcrumbChange"
></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsBreadcrumbIcon') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="getThemeConfig.isBreadcrumbIcon" size="small" @change="setLocalThemeConfig"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsTagsview') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="getThemeConfig.isTagsview" size="small" @change="setLocalThemeConfig"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsTagsviewIcon') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="getThemeConfig.isTagsviewIcon" size="small" @change="setLocalThemeConfig"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsCacheTagsView') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="getThemeConfig.isCacheTagsView" size="small" @change="setLocalThemeConfig"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: 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" size="default" 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="请选择" size="default" style="width: 90px" @change="setLocalThemeConfig">
<el-option label="风格1" value="tags-style-one"></el-option>
<el-option label="风格4" value="tags-style-four"></el-option>
<el-option label="风格5" value="tags-style-five"></el-option>
</el-select>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fiveAnimation') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-select v-model="getThemeConfig.animation" placeholder="请选择" size="default" style="width: 90px" @change="setLocalThemeConfig">
<el-option label="slide-right" value="slide-right"></el-option>
<el-option label="slide-left" value="slide-left"></el-option>
<el-option label="opacitys" value="opacitys"></el-option>
</el-select>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: getThemeConfig.layout !== 'columns' ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fiveColumnsAsideStyle') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-select
v-model="getThemeConfig.columnsAsideStyle"
placeholder="请选择"
size="default"
style="width: 90px"
:disabled="getThemeConfig.layout !== 'columns' ? true : false"
@change="setLocalThemeConfig"
>
<el-option label="圆角" value="columns-round"></el-option>
<el-option label="卡片" value="columns-card"></el-option>
</el-select>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15 mb27" :style="{ opacity: getThemeConfig.layout !== 'columns' ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fiveColumnsAsideLayout') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-select
v-model="getThemeConfig.columnsAsideLayout"
placeholder="请选择"
size="default"
style="width: 90px"
:disabled="getThemeConfig.layout !== 'columns' ? true : false"
@change="setLocalThemeConfig"
>
<el-option label="水平" value="columns-horizontal"></el-option>
<el-option label="垂直" value="columns-vertical"></el-option>
</el-select>
</div>
</div>
<!-- 布局切换 -->
<el-divider content-position="left">{{ $t('message.layout.sixTitle') }}</el-divider>
<div class="layout-drawer-content-flex">
<!-- defaults 布局 -->
<div class="layout-drawer-content-item" @click="onSetLayout('defaults')">
<section class="el-container el-circular" :class="{ 'drawer-layout-active': getThemeConfig.layout === 'defaults' }">
<aside class="el-aside" style="width: 20px"></aside>
<section class="el-container is-vertical">
<header class="el-header" style="height: 10px"></header>
<main class="el-main"></main>
</section>
</section>
<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': getThemeConfig.layout === 'defaults' }">
<div class="layout-tips-box">
<p class="layout-tips-txt">{{ $t('message.layout.sixDefaults') }}</p>
</div>
</div>
</div>
<!-- classic 布局 -->
<div class="layout-drawer-content-item" @click="onSetLayout('classic')">
<section class="el-container is-vertical el-circular" :class="{ 'drawer-layout-active': getThemeConfig.layout === 'classic' }">
<header class="el-header" style="height: 10px"></header>
<section class="el-container">
<aside class="el-aside" style="width: 20px"></aside>
<section class="el-container is-vertical">
<main class="el-main"></main>
</section>
</section>
</section>
<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': getThemeConfig.layout === 'classic' }">
<div class="layout-tips-box">
<p class="layout-tips-txt">{{ $t('message.layout.sixClassic') }}</p>
</div>
</div>
</div>
<!-- transverse 布局 -->
<div class="layout-drawer-content-item" @click="onSetLayout('transverse')">
<section class="el-container is-vertical el-circular" :class="{ 'drawer-layout-active': getThemeConfig.layout === 'transverse' }">
<header class="el-header" style="height: 10px"></header>
<section class="el-container">
<section class="el-container is-vertical">
<main class="el-main"></main>
</section>
</section>
</section>
<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': getThemeConfig.layout === 'transverse' }">
<div class="layout-tips-box">
<p class="layout-tips-txt">{{ $t('message.layout.sixTransverse') }}</p>
</div>
</div>
</div>
<!-- columns 布局 -->
<div class="layout-drawer-content-item" @click="onSetLayout('columns')">
<section class="el-container el-circular" :class="{ 'drawer-layout-active': getThemeConfig.layout === 'columns' }">
<aside class="el-aside-dark" style="width: 10px"></aside>
<aside class="el-aside" style="width: 20px"></aside>
<section class="el-container is-vertical">
<header class="el-header" style="height: 10px"></header>
<main class="el-main"></main>
</section>
</section>
<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': getThemeConfig.layout === 'columns' }">
<div class="layout-tips-box">
<p class="layout-tips-txt">{{ $t('message.layout.sixColumns') }}</p>
</div>
</div>
</div>
</div>
<div class="copy-config">
<el-alert :title="$t('message.layout.tipText')" type="warning" :closable="false"> </el-alert>
<el-button size="default" class="copy-config-btn" type="primary" ref="copyConfigBtnRef" @click="onCopyConfigClick">
<el-icon class="mr5">
<ele-CopyDocument />
</el-icon>
{{ $t('message.layout.copyText') }}
</el-button>
<el-button size="default" class="copy-config-btn-reset" type="info" @click="onResetConfigClick">
<el-icon class="mr5">
<ele-RefreshRight />
</el-icon>
{{ $t('message.layout.resetText') }}
</el-button>
</div>
</el-scrollbar>
</el-drawer>
</div>
</template>
<script lang="ts">
import { nextTick, onUnmounted, onMounted, getCurrentInstance, defineComponent, computed, reactive, toRefs } from 'vue';
import { useStore } from '/@/store/index';
import { getLightColor, getDarkColor } 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-dark-2', `${getDarkColor(getThemeConfig.value.primary, 0.1)}`);
document.documentElement.style.setProperty('--el-color-primary', getThemeConfig.value.primary);
//
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(getThemeConfig.value.primary, i / 10)}`);
}
setDispatchThemeConfig();
};
// 2 /
const onBgColorPickerChange = (bg: string) => {
document.documentElement.style.setProperty(`--next-bg-${bg}`, (<any>getThemeConfig.value)[bg]);
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: any) => {
getThemeConfig.value.wartermarkText = verifyAndSpace(val);
if (getThemeConfig.value.wartermarkText === '') return false;
if (getThemeConfig.value.isWartermark) Watermark.set(getThemeConfig.value.wartermarkText);
setLocalThemeConfig();
};
// 5
const onSetLayout = (layout: string) => {
Local.set('oldLayout', layout);
if (getThemeConfig.value.layout === layout) return false;
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>

302
src/layout/navBars/breadcrumb/user.vue

@ -0,0 +1,302 @@
<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-item command="en" :disabled="disabledI18n === 'en'">English</el-dropdown-item>
<el-dropdown-item command="zh-tw" :disabled="disabledI18n === 'zh-tw'">繁體中文</el-dropdown-item>
</el-dropdown-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.avatar" 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 command="/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item>
<el-dropdown-item command="wareHouse">{{ $t('message.user.dropdown6') }}</el-dropdown-item>
<el-dropdown-item command="/personal">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
<el-dropdown-item command="/404">{{ $t('message.user.dropdown3') }}</el-dropdown-item>
<el-dropdown-item command="/401">{{ $t('message.user.dropdown4') }}</el-dropdown-item>
<el-dropdown-item divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<Search ref="searchRef" />
</div>
</template>
<script 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://github.com/tiger1103/gfast');
} 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[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
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: 'GFast基于全新Go Frame 2.0+Vue3+Element Plus开发的全栈前后端分离的管理系统,正式发布时间:2022年04月21日!',
time: '2022-04-21',
},
{
label: '关于学习交流的通知',
value: 'QQ群号码 865697297,欢迎小伙伴入群学习交流探讨!',
time: '2022-04-21',
},
],
});
//
const onAllReadClick = () => {
state.newsList = [];
};
//
const onGoToGiteeClick = () => {
window.open('https://github.com/tiger1103/gfast');
};
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
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
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
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" v-if="isActive(v)"></i>
<SvgIcon :name="v.meta.icon" v-if="!isActive(v) && getThemeConfig.isTagsviewIcon" class="pr5" />
<span>{{ v.meta.title.indexOf('.')>0?$t(v.meta.title):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 tagsViewisHide 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 tagsViewisAffix
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 tagsViewisAffix
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 tagsViewisAffix
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
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>{{ val.meta.title.indexOf('.')>0?$t(val.meta.title):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" />
{{ val.meta.title.indexOf('.')>0?$t(val.meta.title):val.meta.title }}
</template>
<template #title v-else>
<a :href="val.meta.isLink" target="_blank" rel="opener" class="w100">
<SvgIcon :name="val.meta.icon" />
{{ val.meta.title.indexOf('.')>0?$t(val.meta.title):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
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>{{ val.meta.title.indexOf('.')>0?$t(val.meta.title):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>{{ val.meta.title.indexOf('.')>0?$t(val.meta.title):val.meta.title }}</span>
</template>
<template v-else>
<a :href="val.meta.isLink" target="_blank" rel="opener" class="w100">
<SvgIcon :name="val.meta.icon" />
{{ val.meta.title.indexOf('.')>0?$t(val.meta.title):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
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>{{ val.meta.title.indexOf('.')>0?$t(val.meta.title):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>{{ val.meta.title.indexOf('.')>0?$t(val.meta.title):val.meta.title }}</span>
</template>
<template #title v-else>
<a :href="val.meta.isLink" target="_blank" rel="opener" class="w100">{{ val.meta.title.indexOf('.')>0?$t(val.meta.title):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
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
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">
{{ currentRouteMeta.title.indexOf('.')>0?$t(currentRouteMeta.title):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
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>

39
src/main.ts

@ -0,0 +1,39 @@
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 {getUpFileUrl, handleTree, selectDictLabel} from "/@/utils/gfast";
import {useDict} from "/@/api/system/dict/data";
// 分页组件
import pagination from '/@/components/pagination/index.vue'
const app = createApp(App);
directive(app);
other.elSvg(app);
app.component('pagination', pagination)
app.use(router)
.use(store, key)
.use(ElementPlus, { i18n: i18n.global.t })
.use(i18n)
.use(VueGridLayout)
.mount('#app');
// 全局挂载
app.config.globalProperties.getUpFileUrl=getUpFileUrl
app.config.globalProperties.handleTree=handleTree
app.config.globalProperties.useDict=useDict
app.config.globalProperties.selectDictLabel=selectDictLabel
app.config.globalProperties.mittBus = mitt();

108
src/router/backEnd.ts

@ -0,0 +1,108 @@
import { store } from '/@/store/index.ts';
import { Session } from '/@/utils/storage';
import { NextLoading } from '/@/utils/loading';
import { setAddRoute, setFilterMenuAndCacheTagsViewRoutes } from '/@/router/index';
import {demoRoutes, dynamicRoutes} from '/@/router/route';
import { getUserMenus } from '/@/api/system/menu';
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');
store.dispatch('userInfos/setPermissions');
let menuRoute = Session.get('userMenu')
let permissions = Session.get('permissions')
if (!menuRoute || !permissions) {
await getBackEndControlRoutes(); // 获取路由
menuRoute = Session.get('userMenu')
}
// 获取路由菜单数据
// 存储接口原始路由(未处理component),根据需求选择使用
store.dispatch('requestOldRoutes/setBackEndControlRoutes', JSON.parse(JSON.stringify(menuRoute)));
// 处理路由(component),替换 dynamicRoutes(/@/router/route)第一个顶级 children 的路由
dynamicRoutes[0].children?.push(...await backEndComponent(menuRoute),...demoRoutes) ;
// 添加动态路由
await setAddRoute();
// 设置递归过滤有权限的路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
setFilterMenuAndCacheTagsViewRoutes();
}
/**
*
* @description isRequestRoutes true
* @returns
*/
export async function getBackEndControlRoutes() {
return getUserMenus().then((res:any)=>{
Session.set('userMenu',res.data.menuList)
Session.set('permissions',res.data.permissions)
store.dispatch('userInfos/setPermissions',res.data.permissions)
})
}
/**
*
* @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;
}
}

25
src/router/frontEnd.ts

@ -0,0 +1,25 @@
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');
store.dispatch('userInfos/setPermissions');
// 添加动态路由
await setAddRoute();
// 设置递归过滤有权限的路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组
setFilterMenuAndCacheTagsViewRoutes();
}

258
src/router/index.ts

@ -0,0 +1,258 @@
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 = dynamicRoutes
// 添加到 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)) {
menu.push(item);
}
});
return menu;
}
/**
* vuex routesList
* @description
* @description tagsView(isHide)
*/
export function setFilterMenuAndCacheTagsViewRoutes() {
store.dispatch('routesList/setRoutesList', dynamicRoutes[0].children);
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 = [...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();
import {isInit} from "/@/api/system/dbInit"
// 路由加载前
router.beforeEach(async (to, from, next) => {
NProgress.configure({ showSpinner: false });
if (to.meta.title) NProgress.start();
// 系统初始化
if (to.path === '/dbInit') {
next();
NProgress.done();
}
if (Session.get("isInit") !== true) {
try{
const res:any = await isInit()
let {code, data} = res
if (code === 0 ) {
if (data === false) {
next('/dbInit');
NProgress.done();
return
} else {
Session.set("isInit", true)
}
}
}catch (e){
console.log(e)
}
}
// 正常流程
const token = Session.get('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();
});
// 导出路由
export default router;

1028
src/router/route.ts

File diff suppressed because it is too large Load Diff

27
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);
}

98
src/store/interface/index.ts

@ -0,0 +1,98 @@
// 接口类型声明
// 布局配置
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: {
id:number;
userName: string;
userNickname:string;
avatar: string;
roles: string[];
time: number;
};
permissions:string[]
}
// 后端返回原始路由(未处理时)
export interface RequestOldRoutesState {
requestOldRoutes: object[];
}
// 主接口(顶级类型声明)
export interface RootStateTypes {
themeConfig: ThemeConfigState;
routesList: RoutesListState;
keepAliveNames: KeepAliveNamesState;
tagsViewRoutes: TagsViewRoutesState;
userInfos: UserInfosState;
requestOldRoutes: RequestOldRoutesState;
}

23
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
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
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
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
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: '#354E67',
// 默认菜单导航字体颜色
menuBarColor: '#eaeaea',
// 是否开启菜单背景颜色渐变
isMenuBarColorGradual: false,
// 默认分栏菜单背景颜色
columnsMenuBar: '#545c64',
// 默认分栏菜单字体颜色
columnsMenuBarColor: '#e6e6e6',
// 是否开启分栏菜单背景颜色渐变
isColumnsMenuBarColorGradual: false,
/**
*
*/
// 是否开启菜单水平折叠效果
isCollapse: false,
// 是否开启菜单手风琴效果
isUniqueOpened: false,
// 是否开启固定 Header
isFixedHeader: false,
// 初始化变量,用于更新菜单 el-scrollbar 的高度,请勿删除
isFixedHeaderChange: false,
// 是否开启经典布局分割菜单(仅经典布局生效)
isClassicSplitMenu: false,
// 是否开启自动锁屏
isLockScreen: false,
// 开启自动锁屏倒计时(s/秒)
lockScreenTime: 30,
/**
*
*/
// 是否开启侧边栏 Logo
isShowLogo: true,
// 初始化变量,用于 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: true,
// 是否开启灰色模式
isGrayscale: false,
// 是否开启色弱模式
isInvert: false,
// 是否开启深色模式
isIsDark: false,
// 是否开启水印
isWartermark: false,
// 水印文案
wartermarkText: 'GFast-v3.0',
/**
*
*/
// 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: 'large',
},
},
mutations: {
// 设置布局配置
getThemeConfig(state: any, data: object) {
state.themeConfig = data;
},
},
actions: {
// 设置布局配置
setThemeConfig({ commit }, data: object) {
commit('getThemeConfig', data);
},
},
};
export default themeConfigModule;

47
src/store/modules/userInfos.ts

@ -0,0 +1,47 @@
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: {
id:0,
userName: '',
userNickname:'',
avatar: '',
roles: [],
time: 0
},
permissions:[],
},
mutations: {
// 设置用户信息
getUserInfos(state, data: any) {
state.userInfos = data;
},
// 设置按钮权限
getPermissions(state, data: any) {
state.permissions = data;
},
},
actions: {
// 设置用户信息
async setUserInfos({ commit }, data: UserInfosState) {
if (data) {
commit('getUserInfos', data);
} else {
if (Session.get('userInfo')) commit('getUserInfos', Session.get('userInfo'));
}
},
async setPermissions({ commit }, data: string[]) {
if (data) {
commit('getPermissions', data);
} else {
if (Session.get('permissions')) commit('getPermissions', Session.get('permissions'));
}
},
},
};
export default userInfosModule;

283
src/theme/app.scss

@ -0,0 +1,283 @@
/* 初始化样式
------------------------------- */
* {
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;
}
}
.link-type, .link-type:focus {
color: #337ab7;
cursor: pointer;
text-decoration: none;
}

94
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);
}
}
/* 404401 过渡动画
------------------------------- */
@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
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);
}
}

224
src/theme/element.scss

@ -0,0 +1,224 @@
@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;
}
/* Alert 警告
------------------------------- */
.el-alert {
border: 1px solid;
}
.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 {
list-style: none !important; /*修复 Dropdown 下拉菜单样式问题 2022.03.04*/
}
.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 {
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
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);
}
}
}
}
}
}

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

Loading…
Cancel
Save