57 Commits

Author SHA1 Message Date
lyt
15f4144da5 'admin-23.04.12:发布v1.2.3版本,更新日志查看CHNAGELOG.md' 2023-04-12 13:33:36 +08:00
lyt
0ddb742253 'admin-23.03.37:图片显示问题,无bug' 2023-03-27 11:00:03 +08:00
lyt
87d1057174 'admin-23.03.07:优化分栏' 2023-03-07 02:50:04 +08:00
lyt
734208180f 'admin-23.02.23:发布v1.2.2版本,感谢赞助商.驰骋工作流引擎-表单引擎-低代码开发平台' 2023-02-23 01:24:57 +08:00
lyt
4589ecb6e8 'admin-22.12.12:发布v1.2.1版本,具体更新内容查看CHANGELOG.md' 2022-12-12 15:06:28 +08:00
lyt
1704a5d61c 'admin-22.12.06:发布v1.2.0版本,具体更新内容查看CHANGELOG.md' 2022-12-06 19:08:37 +08:00
lyt
5dec3d59c6 'admin-22.11.17:优化打包问题' 2022-11-17 09:55:50 +08:00
lyt
fbbc525398 'admin-22.06.27:修复模板编译可链式操作符报错' 2022-06-27 15:35:38 +08:00
lyt
4b263f3d79 'admin-22.06.12:更新v1.1.0版本' 2022-06-12 01:01:45 +08:00
lyt
81982f735f 'admin-21.12.22:优化更新请看CHANGELOG.md文件' 2021-12-22 21:11:29 +08:00
lyt
5bf34eb254 'admin-21.12.16:优化更新请看CHANGELOG.md文件' 2021-12-16 20:57:55 +08:00
lyt
d6e5455490 'admin-21.11.27:修复登录问题,感谢@jerrod,issues(#I4GIKU)' 2021-11-27 19:31:17 +08:00
lyt
c5107bd3af 'admin-21.11.08:更新优化,更新内容查看根目录CHANGELOG.md' 2021-11-08 00:46:20 +08:00
lyt
90f61ff7ca 'admin-21.10.30:更新最新依赖' 2021-10-30 20:23:08 +08:00
lyt
48d6157f45 'admin-21.09.10:更新最新依赖' 2021-09-10 23:42:46 +08:00
79e3b436a8 'admin-21.08.07:更新README.md' 2021-08-07 19:46:08 +08:00
856606c009 'admin-21.08.01:更新最新依赖' 2021-08-01 18:47:59 +08:00
4ebb31367f 'admin-21.07.03:右键菜单文字换行问题' 2021-07-03 17:35:39 +08:00
7551d8ae28 'admin-21.07.02:修复screenfull全屏时,按键盘esc键图标不改变问题,感谢群友@伯牙已遇钟子期' 2021-07-02 22:03:34 +08:00
lyt
e0786adc02 'admin-21.06.30:新增表单自适应演示界面、更新最新依赖' 2021-06-30 15:48:38 +08:00
lyt
cd43791fc3 'admin-21.06.30:新增表单自适应演示界面、更新最新依赖' 2021-06-30 15:37:33 +08:00
6708c42172 'admin-21.06.27:修复切换布局报错问题' 2021-06-27 17:53:42 +08:00
fda27398d8 'admin-21.06.19:修复诸多问题,具体查看CHANGELOG.md文件1.04更新日志' 2021-06-19 18:52:55 +08:00
e1c4d20756 'admin-21.06.19:修复诸多问题,具体查看CHANGELOG.md文件1.04更新日志' 2021-06-19 18:42:43 +08:00
c4f33fbc23 'admin-21.06.19:修复诸多问题,具体查看CHANGELOG.md文件1.04更新日志' 2021-06-19 18:27:45 +08:00
lyt
1574f1ace9 'admin-21.06.03:更正更新文案时间错误' 2021-06-03 11:49:01 +08:00
lyt
bed5e8d840 'admin-21.06.02:优化方法、更新最新依赖' 2021-06-02 17:32:55 +08:00
77e1fa9412 'admin-21.06.01:修复菜单搜索中文不可以搜索的问题,感谢群友@逍遥天意' 2021-06-01 21:08:41 +08:00
2bc69ff31f 'admin-21.05.31:修复分栏、经典布局路由设置meta.isHide为true时报错问题,感谢群友@29、@芭芭拉' 2021-05-31 22:14:51 +08:00
lyt
abef357ec0 'admin-21.05.31:修复分栏布局路由设置meta.isHide为true时报错问题,感谢群友@29' 2021-05-31 16:29:27 +08:00
lyt
cba977ad2b 'admin-21.05.28:修复布局配置设置初始值不生效问题' 2021-05-28 17:52:30 +08:00
lyt
0eaf78a849 'admin-21.05.28:布局配置新增其它设置功能' 2021-05-28 10:40:23 +08:00
103815bac1 'admin-21.05.27:新增utils下的storage.js读取浏览器缓存新写法,建议使用' 2021-05-27 21:18:47 +08:00
d95977ced4 'admin-21.05.27:新增utils下的storage.js读取浏览器缓存新写法,建议使用' 2021-05-27 21:17:46 +08:00
6386c2b952 'admin-21.05.27:新增utils下的storage.js读取浏览器缓存新写法,建议使用' 2021-05-27 21:15:26 +08:00
lyt
6bef276e48 'admin-21.05.26:更新最新依赖' 2021-05-26 11:02:08 +08:00
af6b082ebd 'admin-21.05.23:修复改变浏览器窗口时,部分布局配置失效问题' 2021-05-23 16:50:21 +08:00
lyt
8ec374e3b4 'admin-21.05.18:新增分栏布局分栏导航菜单可设置水平、垂直布局' 2021-05-18 16:09:07 +08:00
799f2b06be 'admin-21.05.16:修复全局大小件报错,优化iframe、更新最新依赖、规范工具类命名' 2021-05-16 18:37:47 +08:00
e1cb7d1a21 'admin-21.05.16:修复全局大小件报错,优化iframe、更新最新依赖、规范工具类命名' 2021-05-16 17:26:52 +08:00
lyt
c22fe7c3e5 'admin-21.05.13:修复全局改变组件大小默认值报错问题' 2021-05-13 16:49:28 +08:00
lyt
0378226aee 'admin-21.05.12:优化全局切换组件size不生效、更新最新依赖等' 2021-05-12 11:47:00 +08:00
lyt
3994b89d4a 'admin-21.05.07:更新最新依赖' 2021-05-07 15:51:49 +08:00
lyt
9dc3220fc7 'admin-21.04.29:优化自定义图标库' 2021-04-29 17:52:37 +08:00
lyt
3f7ef9f45a 'admin-2021.04.29:新增全局改变组件大小功能' 2021-04-29 10:36:05 +08:00
a2f5eb107a 'admin-21.04.28:修复themeConfig.js设置默认布局不生效的问题' 2021-04-28 20:37:09 +08:00
lyt
110482c3fb 'admin-21.04.27:更新最新依赖' 2021-04-27 16:50:21 +08:00
lyt
18ae7ff1f7 'admin-21.04.23:修复NProgress进度条不连贯问题' 2021-04-23 16:35:35 +08:00
lyt
a6ae98f284 'admin-21.04.22:部分电脑登录后首屏卡断问题,使用location.href替代router.push' 2021-04-22 17:22:24 +08:00
lyt
fcced5d3bb 'admin-21.04.22:优化加载进度条、新增在线签名页面等' 2021-04-22 17:21:30 +08:00
1c55dbcddb 'admin-21.04.21:修改异步引入组件,webpack报异常问题,感谢群友@joeb' 2021-04-21 21:26:55 +08:00
lyt
e248ed8c85 'admin-21.04.20:修改文案、优化进度条问题' 2021-04-20 11:56:35 +08:00
365a2eed96 'admin-21.04.19:新增tagsView非当前页、当前页操作示例' 2021-04-19 20:06:35 +08:00
779ea5cbc6 'admin-21.04.18:更新最新依赖、优化进度条问题' 2021-04-18 20:05:58 +08:00
lyt
ea6b527cfb 'admin-21.04.15:修改文案、链接等' 2021-04-15 15:36:53 +08:00
lyt
134c332f1f 'admin-21.04.15:修改文案、图片链接等' 2021-04-15 13:47:44 +08:00
lyt
1dbf22f584 'admin-21.04.15:基于vue2.x、elementui重构后的第一次提交' 2021-04-15 10:59:41 +08:00
177 changed files with 7757 additions and 17072 deletions

10
.env
View File

@ -1,11 +1,5 @@
# port 端口号
VITE_PORT = 8888
VUE_APP_PORT = 9999
# open 运行 npm run dev 时自动打开浏览器
VITE_OPEN = false
# 打包是否开启 cdn源文件 utils/build.ts可自行修改
VITE_OPEN_CDN = false
# public path 配置线上环境路径(打包)、本地通过 http-server 访问时,请置空即可
VITE_PUBLIC_PATH =
VUE_APP_OPEN = false

View File

@ -1,5 +1,2 @@
# 本地环境
ENV = development
# 本地环境接口地址
VITE_API_URL = http://localhost:8888/
# 开发环境
VUE_APP_BASE_API = http://localhost:9999/

View File

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

View File

@ -1,4 +1,3 @@
*.sh
node_modules
lib
@ -6,6 +5,7 @@ lib
*.scss
*.woff
*.ttf
*.json
.vscode
.idea
dist

View File

@ -1,76 +1,22 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
sourceType: 'module',
parser: '@babel/eslint-parser',
},
extends: ['plugin:vue/vue3-essential', 'plugin:vue/essential', 'eslint:recommended'],
plugins: ['vue', '@typescript-eslint'],
overrides: [
{
files: ['*.ts', '*.tsx', '*.vue'],
rules: {
'no-undef': 'off',
},
},
],
plugins: ['vue'],
extends: ['plugin:vue/essential', 'eslint:recommended'],
rules: {
// http://eslint.cn/docs/rules/
// https://eslint.vuejs.org/rules/
// https://typescript-eslint.io/rules/no-unused-vars/
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-redeclare': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/no-unused-vars': [2],
'vue/custom-event-name-casing': 'off',
'vue/attributes-order': 'off',
'vue/one-component-per-file': 'off',
'vue/html-closing-bracket-newline': 'off',
'vue/max-attributes-per-line': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off',
'vue/html-self-closing': 'off',
'vue/no-multiple-template-root': 'off',
'vue/require-default-prop': 'off',
'vue/no-v-model-argument': 'off',
'vue/no-arrow-functions-in-watch': 'off',
'vue/no-template-key': 'off',
'vue/no-v-html': 'off',
'vue/comment-directive': 'off',
'vue/no-parsing-error': 'off',
'vue/no-deprecated-v-on-native-modifier': 'off',
'vue/multi-word-component-names': 'off',
'no-useless-escape': 'off',
'no-sparse-arrays': 'off',
'no-prototype-builtins': 'off',
'no-constant-condition': 'off',
'no-use-before-define': 'off',
'no-restricted-globals': 'off',
'no-restricted-syntax': 'off',
'generator-star-spacing': 'off',
'no-unreachable': 'off',
'no-multiple-template-root': 'off',
'no-unused-vars': 'error',
'no-v-model-argument': 'off',
'no-case-declarations': 'off',
'no-dupe-args': 'error',
'no-empty': 'off',
'no-extra-semi': 'off',
'no-constant-condition': 'off',
'no-console': 'error',
'no-redeclare': 'off',
'vue/multi-word-component-names': 'off',
},
};

5
.gitignore vendored
View File

@ -1,8 +1,7 @@
.DS_Store
node_modules
.DS_Store
/dist
# local env files
.env.local
.env.*.local
@ -20,4 +19,4 @@ pnpm-debug.log*
*.ntvs*
*.njsproj
*.sln
*.sw?
*.sw?

View File

@ -1,94 +1,124 @@
# <a href="https://gitee.com/lyt-top/vue-next-admin" target="_blank">vue-next-admin-template不带国际化 更新日志</a>
# <a href="https://gitee.com/lyt-top/vue-next-admin/tree/vue-prev-admin" target="_blank">vue-prev-admin 更新日志</a>
🎉🎉🔥 `vue-next-admin-template` 基于 vue-next-admin-v2.4.33 版本) vue3.x 、Typescript、vite、Element plus 等适配手机、平板、pc 的后台开源免费模板库vue2.x 请切换 vue-prev-admin 分支)
🎉🎉🔥 `vue-prev-admin` 基于 vue2.x + webpack + element ui适配手机、平板、pc 的后台开源免费模板库vue3.x 请切换 master 分支)
## 2.4.33
## 1.2.3
`2023.04.12`
- 🎉 同步 master 分支 v2.4.33 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md)
- 🐞 修复 [#I6UW2I 关闭标签页后,分栏没有消失,需要手动点击首页才会消失](https://gitee.com/lyt-top/vue-next-admin/issues/I6UW2I),感谢[@小菜鸟儿](https://gitee.com/cainiaoer)
- 🎯 优化 `layout/navBars/breadcrumb` 文件夹名称改成 `layout/navBars/topBar` 更易理解(可全局替换),感谢群友@傲世盛唐
- 🎯 优化 `layout/navBars/topBar/user.vue` 组件,`UserNews` 点击消息图标触发范围,防止点击消息通知背景色时不触发 Popover 弹出框
## 2.4.32
## 1.2.2
`2023.03.26`
- 🎉 同步 master 分支 v2.4.32 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md)
## 2.4.31
`2023.03.10`
- 🎉 同步 master 分支 v2.4.31 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md)
## 2.4.3
`2023.02.22`
`2023.02.23`
🚩🚩🚩 感谢 [驰骋工作流引擎-表单引擎-低代码开发平台](http://www.ccflow.org/) 赞助商的赞助。驰骋公司为社会提供流程引擎+表单引擎+低代码开发平台一体的开源软件解决方案,欢迎广大开发者前去体验!
- 🎉 同步 master 分支 v2.4.3 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md)
- 🎉 新增 赞助商组件(`/src/layout/sponsors`[项目目录结构查看](https://lyt-top.gitee.io/vue-next-admin-doc-preview/config/)
- 🎯 优化 `/src/utils/storage``key` 编写成 `${__NEXT_NAME__}:${key}`,防止部署多套系统到同一域名不同目录时,变量共用的问题(`__NEXT_NAME__``package.json` 中的 `name`
## 2.4.21
## 1.2.1
`2022.12.12`
- 🎉 同步 master 分支 v2.4.21 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md)
- 🐞 优化 版本升级提示
- 🐞 优化 深色模式
## 2.4.2
## 1.2.0
`2022.12.10`
`2022.12.06`
- 🎉 同步 master 分支 v2.4.2 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md)
- 🎉 新增 版本升级提示
- 🎉 新增 主题色修改
- 🎉 新增 深色模式
- 🐞 优化 外链界面 `/src/layout/routerView/link.vue`
- 🐞 修复 `菜单水平折叠` 刷新界面还原默认值问题
- 💔 移除 `vue.config.js` 打包加时间戳方法,因为打包报错了
## 2.4.1
## 1.1.1
`2022.11.30`
`2022.11.17`
- 🎉 同步 master 分支 v2.4.1 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md)
- 🐞 优化 [vue2 版本打包出来配置路由懒加载无效。](https://gitee.com/lyt-top/vue-next-admin/issues/I5RFQT),感谢[@林建生](https://gitee.com/ljsshuai)
## 2.3.0
## 1.1.0
`2022.11.16`
`2022.06.12`
- 🎉 同步 master 分支 v2.3.0 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md)
- 🐞 优化 部分界面演示图片不出来问题
- 🐞 修复 [vue-prev-admin 全屏模式下,滚动条无法滚到底 #I4S79C](https://gitee.com/lyt-top/vue-next-admin/issues/I4S79C),感谢[@qfvh](https://gitee.com/qfvh)
## 2.2.0
## 1.0.9
`2022.07.11`
`2021.12.22`
- 🎉 同步 master 分支 v2.2.0 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md)
- 🎯 优化 部署后每次都要强制刷新清浏览器缓存问题
- 🎉 新增 工具类百分比验证演示
- 🐞 修复 tag-view 标签右键会超出浏览器
## 2.1.1
## 1.0.8
- 🎉 同步 master 分支 v2.1.1 版本内容,具体查看 [master CHANGELOG.md](https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md)
## 2.0.2
- 🎉 同步 master 分支 v2.0.2 版本内容,具体查看 master CHANGELOG.md
## 0.2.2
`2021.12.21`
- 🎉 同步 master 分支 v1.2.2 版本内容,具体查看 master CHANGELOG.md
## 0.2.1
`2021.12.12`
`2021.12.16`
- 🌟 更新 依赖更新最新版本
- 🐞 修复 浏览器标题问题
- 🐞 修复 element plus svg 图标引入
- 🐞 修复 默认显示英文问题,改成默认显示中文
- 🎯 优化 登录界面逻辑、权限管理逻辑
- 🎯 优化 同步 vue-next-admin-images 后端控制菜单模拟数据
- 🎯 优化 菜单格式(对象改数组)
- 🐞 修复 登录页手机端样式问题
## 0.2.0
## 1.0.7
`2021.12.04`
`2021.11.27`
- 🎉 同步 master 分支 v1.2.0 版本内容,具体查看 master CHANGELOG.md
- 🌟 更新 依赖更新最新版本
- 🐞 修复 登录问题 [#I4GIKU](https://gitee.com/lyt-top/vue-next-admin/issues/I4GIKU)
## 0.1.0
## 1.0.6
`2021.10.17`
`2021.11.08`
- 🎉 新增 vue-next-admin-template 基础版本(不带国际化),切换 `vue-next-admin-template` 分支
- 🌟 更新 依赖更新最新版本
- 🎯 优化 目录移动 `@/views/layout` 移动 `@/layout` (可全局替换)
- 🎯 优化 eslint 语法检测问题,`@babel/eslint-parser` 替换已废弃的 `babel-eslint`,出现报错,请尝试降级 eslint
- 🎯 优化 vuex 文件自动导入
## 1.0.5
`2021.06.30`
- 🌟 更新 依赖更新最新版本
- 🎉 新增 表单自适应演示界面
- 🎯 优化 去掉内嵌 iframe 内边距padding
## 1.0.4
`2021.06.19`
- 🌟 更新 依赖更新最新版本
- 🎉 新增 项目仓库地址
- 🎯 优化 移除 utils/storage.ts 下的旧写法(改动较大)
- 🐞 修复 鼠标移入顶部用户信息栏 开/关全屏 文字反向问题
## 1.0.3
`2021.06.02`
- 🌟 更新 依赖更新最新版本
- 💯 优化 动态加载后端返回路由路由(模拟数据)方法
## 1.0.2
`2021.06.01`
- 🐞 修复 菜单搜索中文不可以搜索的问题,感谢群友@逍遥天意
## 1.0.1
`2021.05.31`
- 🎉 新增 新增个人中心演示空白页
- 🎉 新增 更新日志文件 `CHANGELOG.md`,以后每次更新都会在这里显示对应内容
- 🌟 更新 依赖更新最新版本
- 🐞 修复 分栏布局路由设置 `meta.isHide``true` 时报错问题,感谢群友@29@芭芭拉

40
LICENSE
View File

@ -1,21 +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
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.

View File

@ -1,20 +1,17 @@
<div align="center">
<img src="https://i.hd-r.cn/6ce52e5724fae609444b5b48bdc4accb.png">
<img src="https://i.hd-r.cn/07fd893a28d4f62926fe6ad895af96a2.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 href="https://cn.vuejs.org/" target="_blank">
<img src="https://img.shields.io/badge/vue.js-vue2.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 href="https://element.eleme.cn/#/zh-CN/component/changelog" target="_blank">
<img src="https://img.shields.io/badge/element--ui-%3E1.0.0-blue" alt="element ui">
</a>
<a href="https://www.tslang.cn/" target="_blank">
<img src="https://img.shields.io/badge/typescript-%3E4.0.0-blue" alt="typescript">
<a href="https://v4.webpack.docschina.org/concepts/" target="_blank">
<img src="https://img.shields.io/badge/webpack-%3E1.0.0-success" alt="webpack">
</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 href="https://gitee.com/lyt-top/vue-next-admin/blob/vue-prev-admin/LICENSE" target="_blank">
<img src="https://img.shields.io/badge/license-MIT-yellow" alt="license">
</a>
</p>
<p>&nbsp;</p>
@ -26,9 +23,9 @@
<img src="./src/assets/ccflowRightNextAdmin.png" width="50%" height="70px">
</a>
#### 🌈 介绍 基础版 ts不带国际化
#### 🌈 介绍
基于 vue3.x + CompositionAPI setup 语法糖 + typescript + vite + element plus + vue-router-next + pinia 技术适配手机、平板、pc 的后台开源免费模板,希望减少工作量,帮助大家实现快速开发。
基于 vue2.x + webpack + element ui适配手机、平板、pc 的后台开源免费模板,希望减少工作量,帮助大家实现快速开发。
#### ⛱️ 线上预览
@ -46,19 +43,11 @@
- 复制代码(桌面 cmd 运行) `npm install -g cnpm --registry=https://registry.npm.taobao.org`
- 复制代码(桌面 cmd 运行) `npm install -g yarn`
#### 🏭 环境支持
| Edge | Firefox | Chrome | Safari |
| --------- | ------------ | ----------- | ----------- |
| Edge ≥ 88 | Firefox ≥ 78 | Chrome ≥ 87 | Safari ≥ 13 |
> 由于 Vue3 不再支持 IE11故而 ElementPlus 也不支持 IE11 及之前版本。
#### ⚡ 使用说明
建议使用 cnpm因为 yarn 有时会报错。<a href="http://nodejs.cn/" target="_blank">node 版本 > 14.18+/16+</a>
建议使用 cnpm因为 yarn 有时会报错。`npm install` 安装报错的话,请使用 `cnpm install`
> Vite 不再支持 Node 12 / 13 / 15因为上述版本已经进入了 EOL 阶段。现在你必须使用 Node 14.18+ / 16+ 版本
> 注意:`node` 需大于 `12.xxx` 小于等于 `v16.14.0`,否则安装依赖将报错
```bash
# 克隆项目
@ -68,7 +57,7 @@ git clone https://gitee.com/lyt-top/vue-next-admin.git
cd vue-next-admin
# 切换分支
git checkout vue-next-admin-template
git checkout vue-prev-admin
# 安装依赖
cnpm install
@ -103,7 +92,6 @@ cnpm run build
- <a target="_blank" href="https://gitee.com/zsvg/vboot-net">@zsvg vboot-net</a>
- <a target="_blank" href="https://gitee.com/zsvg/vboot-java">@zsvg vboot-java</a>
- <a target="_blank" href="https://gitee.com/wonderful-code/buildadmin">@青红造了个白 buildadmin</a>
- <a target="_blank" href="https://github.com/xiaodingding/iotfast">@Goodwell iotfast(一个开源的物联网平台)</a>
#### ❤️ 鸣谢列表
@ -115,22 +103,15 @@ cnpm run build
- <a href="https://github.com/vuejs/vuex" target="_blank">vuex</a>
- <a href="https://github.com/apache/echarts" target="_blank">echarts</a>
- <a href="https://github.com/axios/axios" target="_blank">axios</a>
- <a href="https://github.com/developit/mitt" target="_blank">mitt</a>
- <a href="https://github.com/zenorocha/clipboard.js" target="_blank">clipboard</a>
- <a href="https://github.com/inorganik/countUp.js" target="_blank">countUp</a>
- <a href="https://github.com/rstacruz/nprogress" target="_blank">nprogress</a>
- <a href="https://github.com/sindresorhus/screenfull.js" target="_blank">screenfull</a>
- <a href="https://github.com/SortableJS/Sortable" target="_blank">sortablejs</a>
- <a href="https://github.com/sass/sass" target="_blank">sass</a>
- <a href="https://github.com/microsoft/TypeScript" target="_blank">typescript</a>
- <a href="https://github.com/vitejs/vite" target="_blank">vite</a>
- <a href="https://pinia.vuejs.org/" target="_blank">pinia</a>
- <a href="https://github.com/js-cookie/js-cookie" target="_blank">js-cookie</a>
- <a href="https://github.com/mmf-fe/vite-plugin-cdn-import" target="_blank">vite-plugin-cdn-import</a>
- <a href="https://github.com/vbenjs/vite-plugin-compression" target="_blank">vite-plugin-compression</a>
- <a href="https://github.com/chenxch/vite-plugin-vue-setup-extend-plus" target="_blank">vite-plugin-vue-setup-extend-plus</a>
#### 💕 特别感谢
特别感谢老哥的建议、指导与帮忙,谢谢!
特别感谢群里老哥的建议、指导与帮忙,谢谢!
- <a href="https://gitee.com/click33/sa-plus" target="_blank">@省长</a>
- <a href="https://gitee.com/jskz/Jskz-SpringCloud" target="_blank">@唐参</a>

4
babel.config.js Normal file
View File

@ -0,0 +1,4 @@
module.exports = {
presets: ['@vue/cli-plugin-babel/preset'],
plugins: ['@babel/plugin-proposal-optional-chaining'],
};

11
jsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"allowJs": true,
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"/@/*": ["src/*"]
}
}
}

5494
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,49 +1,45 @@
{
"name": "vue-next-admin-template",
"version": "2.4.33",
"description": "vue3 vite next admin template",
"name": "vue-prev-admin",
"version": "1.2.3",
"private": true,
"description": "vue2 webpack 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/"
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"webpack": "webpack --version"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"axios": "^1.3.5",
"echarts": "^5.4.2",
"element-plus": "^2.3.3",
"js-cookie": "^3.0.1",
"mitt": "^3.0.0",
"nprogress": "^0.2.0",
"pinia": "^2.0.34",
"qrcodejs2-fixes": "^0.0.2",
"screenfull": "^6.0.2",
"sortablejs": "^1.15.0",
"vue": "^3.2.47",
"vue-clipboard3": "^2.0.0",
"vue-demi": "^0.13.11",
"qs": "^6.11.1",
"vue-router": "^4.1.6"
"axios": "0.24.0",
"clipboard": "2.0.8",
"countup.js": "2.0.8",
"echarts": "5.2.2",
"element-ui": "2.15.6",
"nprogress": "0.2.0",
"screenfull": "5.2.0",
"sign-canvas": "1.1.4",
"vue": "2.6.14",
"vue-i18n": "8.26.7",
"vue-particles": "1.0.9",
"vue-router": "3.5.3",
"vue-seamless-scroll": "1.1.23",
"vuex": "3.6.2"
},
"devDependencies": {
"@types/node": "^18.15.11",
"@types/nprogress": "^0.2.0",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
"@vitejs/plugin-vue": "^4.1.0",
"@vue/compiler-sfc": "^3.2.47",
"eslint": "^8.38.0",
"eslint-plugin-vue": "^9.10.0",
"prettier": "^2.8.7",
"sass": "^1.61.0",
"typescript": "^5.0.4",
"vite": "^4.2.1",
"vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-setup-extend-plus": "^0.1.0",
"vue-eslint-parser": "^9.1.1"
"@babel/eslint-parser": "7.16.5",
"@babel/plugin-proposal-optional-chaining": "7.17.12",
"@vue/cli-plugin-babel": "4.5.15",
"@vue/cli-plugin-eslint": "4.5.15",
"@vue/cli-plugin-router": "4.5.15",
"@vue/cli-plugin-vuex": "4.5.15",
"@vue/cli-service": "4.5.15",
"eslint": "8.4.1",
"eslint-plugin-vue": "8.2.0",
"sass": "1.45.0",
"sass-loader": "10.1.1",
"vue-template-compiler": "2.6.14"
},
"browserslist": [
"> 1%",
@ -54,14 +50,13 @@
"url": "https://gitee.com/lyt-top/vue-next-admin/issues"
},
"engines": {
"node": ">=16.0.0",
"npm": ">= 7.0.0"
"node": ">=12.0.0",
"npm": ">= 6.0.0"
},
"keywords": [
"vue",
"vue3",
"vuejs/vue-next",
"vuejs/vue-next-template",
"element-ui",
"element-plus",
"vue-next-admin",

290
public/admin.json Normal file
View File

@ -0,0 +1,290 @@
{
"code": 0,
"data": [
{
"path": "/home",
"name": "home",
"component": "home",
"meta": {
"title": "message.router.home",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": true,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-shouye"
}
},
{
"path": "/tools",
"name": "tools",
"component": "tools",
"meta": {
"title": "message.router.tools",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-gongju"
}
},
{
"path": "/menu",
"name": "menu",
"component": "layout/routerView/parent",
"redirect": "/menu/menu1",
"meta": {
"title": "message.router.menu",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-caidan"
},
"children": [
{
"path": "/menu/menu1",
"name": "menu1",
"component": "layout/routerView/parent",
"redirect": "/menu/menu1/menu11",
"meta": {
"title": "message.router.menu1",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-caidan"
},
"children": [
{
"path": "/menu/menu1/menu11",
"name": "menu11",
"component": "menu/menu1/menu11/index",
"meta": {
"title": "message.router.menu11",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-caidan"
}
},
{
"path": "/menu/menu1/menu12",
"name": "menu12",
"component": "layout/routerView/parent",
"redirect": "/menu/menu1/menu12/menu121",
"meta": {
"title": "message.router.menu12",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-caidan"
},
"children": [
{
"path": "/menu/menu1/menu12/menu121",
"name": "menu121",
"component": "menu/menu1/menu12/menu121/index",
"meta": {
"title": "message.router.menu121",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-caidan"
}
},
{
"path": "/menu/menu1/menu12/menu122",
"name": "menu122",
"component": "menu/menu1/menu12/menu122/index",
"meta": {
"title": "message.router.menu122",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-caidan"
}
}
]
},
{
"path": "/menu/menu1/menu13",
"name": "menu13",
"component": "menu/menu1/menu13/index",
"meta": {
"title": "message.router.menu13",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-caidan"
}
}
]
},
{
"path": "/menu/menu2",
"name": "menu2",
"component": "menu/menu2/index",
"meta": {
"title": "message.router.menu2",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-caidan"
}
}
]
},
{
"path": "/fun",
"name": "funIndex",
"component": "layout/routerView/parent",
"redirect": "/fun/tagsView",
"meta": {
"title": "message.router.funIndex",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-crew_feature"
},
"children": [
{
"path": "/fun/tagsView",
"name": "funTagsView",
"component": "fun/tagsView/index",
"meta": {
"title": "message.router.funTagsView",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "el-icon-thumb"
}
},
{
"path": "/fun/signCanvas",
"name": "funSignCanvas",
"component": "fun/signCanvas/index",
"meta": {
"title": "message.router.funSignCanvas",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "el-icon-edit"
}
}
]
},
{
"path": "/pages",
"name": "pagesIndex",
"component": "layout/routerView/parent",
"redirect": "/pages/formAdapt",
"meta": {
"title": "message.router.pagesIndex",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-fuzhiyemian"
},
"children": [
{
"path": "/pages/formAdapt",
"name": "pagesFormAdapt",
"component": "pages/formAdapt/index",
"meta": {
"title": "message.router.pagesFormAdapt",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-biaodan"
}
}
]
},
{
"path": "/personal",
"name": "personal",
"component": "personal/index",
"meta": {
"title": "message.router.personal",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-gerenzhongxin"
}
},
{
"path": "/link",
"name": "layoutLinkView",
"component": "layout/routerView/parent",
"meta": {
"title": "message.router.layoutLinkView",
"isLink": "https://element-plus.gitee.io/#/zh-CN/component/installation",
"isHide": false,
"isKeepAlive": false,
"isAffix": false,
"isIframe": false,
"roles": ["admin"],
"icon": "iconfont icon-caozuo-wailian"
}
},
{
"path": "/iframes",
"name": "layoutIfameView",
"component": "layout/routerView/parent",
"meta": {
"title": "message.router.layoutIfameView",
"isLink": "https://element-plus.gitee.io/zh-CN/#/zh-CN/component/installation",
"isHide": false,
"isKeepAlive": false,
"isAffix": true,
"isIframe": true,
"roles": ["admin"],
"icon": "iconfont icon-neiqianshujuchucun"
}
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="utf-8" />
<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
@ -12,20 +12,19 @@
name="description"
content="vue-next-admin基于 vue3 + CompositionAPI + typescript + vite + element plus适配手机、平板、pc 的后台开源免费管理系统模板vue-prev-admin基于 vue2 + element ui适配手机、平板、pc 的后台开源免费管理系统模板!"
/>
<link rel="icon" href="/favicon.ico" />
<title>vue-next-admin</title>
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title>vue-prev-admin</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript">
var _hmt = _hmt || [];
(function () {
(function() {
var hm = document.createElement('script');
hm.src = 'https://hm.baidu.com/hm.js?d9c8b87d10717013641458b300c552e4';
hm.src = 'https://hm.baidu.com/hm.js?9d1e524198ede8205ac7c938c243344c';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

227
public/test.json Normal file
View File

@ -0,0 +1,227 @@
{
"code": 0,
"data": [
{
"path": "/home",
"name": "home",
"component": "home",
"meta": {
"title": "message.router.home",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": true,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-shouye"
}
},
{
"path": "/tools",
"name": "tools",
"component": "tools",
"meta": {
"title": "message.router.tools",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin"],
"icon": "iconfont icon-gongju"
}
},
{
"path": "/menu",
"name": "menu",
"component": "layout/routerView/parent",
"redirect": "/menu/menu1",
"meta": {
"title": "message.router.menu",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin"],
"icon": "iconfont icon-caidan"
},
"children": [
{
"path": "/menu/menu1",
"name": "menu1",
"component": "layout/routerView/parent",
"redirect": "/menu/menu1/menu11",
"meta": {
"title": "message.router.menu1",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-caidan"
},
"children": [
{
"path": "/menu/menu1/menu11",
"name": "menu11",
"component": "menu/menu1/menu11/index",
"meta": {
"title": "message.router.menu11",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-caidan"
}
},
{
"path": "/menu/menu1/menu12",
"name": "menu12",
"component": "layout/routerView/parent",
"redirect": "/menu/menu1/menu12/menu121",
"meta": {
"title": "message.router.menu12",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-caidan"
},
"children": [
{
"path": "/menu/menu1/menu12/menu121",
"name": "menu121",
"component": "menu/menu1/menu12/menu121/index",
"meta": {
"title": "message.router.menu121",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-caidan"
}
},
{
"path": "/menu/menu1/menu12/menu122",
"name": "menu122",
"component": "menu/menu1/menu12/menu122/index",
"meta": {
"title": "message.router.menu122",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-caidan"
}
}
]
},
{
"path": "/menu/menu1/menu13",
"name": "menu13",
"component": "menu/menu1/menu13/index",
"meta": {
"title": "message.router.menu13",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-caidan"
}
}
]
},
{
"path": "/menu/menu2",
"name": "menu2",
"component": "menu/menu2/index",
"meta": {
"title": "message.router.menu2",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-caidan"
}
}
]
},
{
"path": "/fun",
"name": "funIndex",
"component": "layout/routerView/parent",
"redirect": "/fun/tagsView",
"meta": {
"title": "message.router.funIndex",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "iconfont icon-crew_feature"
},
"children": [
{
"path": "/fun/tagsView",
"name": "funTagsView",
"component": "fun/tagsView/index",
"meta": {
"title": "message.router.funTagsView",
"isLink": "",
"isHide": false,
"isKeepAlive": true,
"isAffix": false,
"isIframe": false,
"roles": ["admin", "common"],
"icon": "el-icon-thumb"
}
}
]
},
{
"path": "/link",
"name": "layoutLinkView",
"component": "layout/routerView/parent",
"meta": {
"title": "message.router.layoutLinkView",
"isLink": "https://element-plus.gitee.io/#/zh-CN/component/installation",
"isHide": false,
"isKeepAlive": false,
"isAffix": false,
"isIframe": false,
"roles": ["admin"],
"icon": "iconfont icon-caozuo-wailian"
}
},
{
"path": "/iframes",
"name": "layoutIfameView",
"component": "layout/routerView/parent",
"meta": {
"title": "message.router.layoutIfameView",
"isLink": "https://gitee.com/lyt-top/vue-next-admin",
"isHide": false,
"isKeepAlive": false,
"isAffix": true,
"isIframe": true,
"roles": ["admin"],
"icon": "iconfont icon-neiqianshujuchucun"
}
}
]
}

View File

@ -1,96 +1,78 @@
<template>
<el-config-provider :size="getGlobalComponentSize" :locale="zhCn">
<router-view v-show="setLockScreen" />
<LockScreen v-if="themeConfig.isLockScreen" />
<Setings ref="setingsRef" v-show="setLockScreen" />
<CloseFull v-if="!themeConfig.isLockScreen" />
<div id="app">
<router-view />
<Setings ref="setingsRef" />
<Upgrade v-if="getVersion" />
<Sponsors />
</el-config-provider>
</div>
</template>
<script setup lang="ts" name="app">
import { defineAsyncComponent, computed, ref, onBeforeMount, onMounted, onUnmounted, nextTick, watch } from 'vue';
import { useRoute } from 'vue-router';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import { useThemeConfig } from '/@/stores/themeConfig';
import other from '/@/utils/other';
import { Local, Session } from '/@/utils/storage';
import mittBus from '/@/utils/mitt';
import setIntroduction from '/@/utils/setIconfont';
// 引入组件
const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue'));
const Setings = defineAsyncComponent(() => import('/@/layout/navBars/topBar/setings.vue'));
const CloseFull = defineAsyncComponent(() => import('/@/layout/navBars/topBar/closeFull.vue'));
const Upgrade = defineAsyncComponent(() => import('/@/layout/upgrade/index.vue'));
const Sponsors = defineAsyncComponent(() => import('/@/layout/sponsors/index.vue'));
// 定义变量内容
const setingsRef = ref();
const route = useRoute();
const stores = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
// 设置锁屏时组件显示隐藏
const setLockScreen = computed(() => {
// 防止锁屏后,刷新出现不相关界面
// https://gitee.com/lyt-top/vue-next-admin/issues/I6AF8P
return themeConfig.value.isLockScreen ? themeConfig.value.lockScreenTime > 1 : themeConfig.value.lockScreenTime >= 0;
});
// 获取版本号
const getVersion = computed(() => {
let isVersion = false;
if (route.path !== '/login') {
// @ts-ignore
if ((Local.get('version') && Local.get('version') !== __NEXT_VERSION__) || !Local.get('version')) isVersion = true;
}
return isVersion;
});
// 获取全局组件大小
const getGlobalComponentSize = computed(() => {
return other.globalComponentSize();
});
// 设置初始化,防止刷新时恢复默认
onBeforeMount(() => {
// 设置批量第三方 icon 图标
setIntroduction.cssCdn();
// 设置批量第三方 js
setIntroduction.jsCdn();
});
// 页面加载时
onMounted(() => {
nextTick(() => {
// 监听布局配'置弹窗点击打开
mittBus.on('openSetingsDrawer', () => {
setingsRef.value.openDrawer();
});
// 获取缓存中的布局配置
if (Local.get('themeConfig')) {
storesThemeConfig.setThemeConfig({ themeConfig: Local.get('themeConfig') });
document.documentElement.style.cssText = Local.get('themeConfigStyle');
}
// 获取缓存中的全屏配置
if (Session.get('isTagsViewCurrenFull')) {
stores.setCurrenFullscreen(Session.get('isTagsViewCurrenFull'));
}
});
});
// 页面销毁时,关闭监听布局配置/i18n监听
onUnmounted(() => {
mittBus.off('openSetingsDrawer', () => {});
});
// 监听路由的变化,设置网站标题
watch(
() => route.path,
() => {
other.useTitle();
<script>
import config from '/package.json';
import setIntroduction from '@/utils/setIconfont.js';
import { Local } from '@/utils/storage.js';
import Setings from '@/layout/navBars/topBar/setings.vue';
import Upgrade from '@/layout/upgrade/index.vue';
import Sponsors from '@/layout/sponsors/index.vue';
export default {
name: 'App',
components: { Setings, Upgrade, Sponsors },
mounted() {
this.initSetIconfont();
this.openSetingsDrawer();
this.getLayoutThemeConfig();
},
{
deep: true,
}
);
computed: {
// 获取版本号
getVersion() {
let isVersion = false;
if (this.$route.path !== '/login') {
if ((Local.get('version') && Local.get('version') !== config.version) || !Local.get('version')) isVersion = true;
}
return isVersion;
},
},
methods: {
// 设置初始化,防止刷新时恢复默认
initSetIconfont() {
// 设置批量第三方 icon 图标
setIntroduction.cssCdn();
// 设置批量第三方 js
setIntroduction.jsCdn();
},
// 布局配置弹窗打开
openSetingsDrawer() {
this.bus.$on('openSetingsDrawer', () => {
this.$refs.setingsRef.openDrawer();
});
},
// 获取缓存中的布局配置
getLayoutThemeConfig() {
if (Local.get('themeConfigPrev')) {
this.$store.dispatch('themeConfig/setThemeConfig', Local.get('themeConfigPrev'));
document.documentElement.style.cssText = Local.get('themeConfigStyle');
} else {
Local.set('themeConfigPrev', this.$store.state.themeConfig.themeConfig);
}
},
},
watch: {
// 监听路由的变化
$route: {
handler(to) {
this.$nextTick(() => {
let webTitle = '';
let { globalTitle } = this.$store.state.themeConfig.themeConfig;
to.path === '/login' ? (webTitle = to.meta.title) : (webTitle = this.$t(to.meta.title));
document.title = `${webTitle} - ${globalTitle}` || globalTitle;
});
},
deep: true,
immediate: true,
},
},
destroyed() {
this.bus.$off('openSetingsDrawer');
},
};
</script>

View File

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

48
src/api/menu/index.js Normal file
View File

@ -0,0 +1,48 @@
import request from '@/utils/request';
/**
* webpack 的代理只是本地开发生效,打包后需要在部署环境 搭建 nginx 代理
* 所以:
* 开发环境,请求跨越的接口。为了配置跨越示例
* 线上环境,请求目录下的 `json` 数据
* 一般后端接口都会处理跨越问题,可根据具体情况进行修改
* json 格式地址https://gitee.com/lyt-top/vue-next-admin-images/tree/master/vue2
* 本地菜单地址public/xxx.json
*/
/**
* 后端控制菜单模拟json路径在 https://gitee.com/lyt-top/vue-next-admin-images/tree/master/menu
* 后端控制路由isRequestRoutes 为 true则开启后端控制路由
* @method getMenuAdmin 获取后端动态路由菜单(admin)
* @method getMenuTest 获取后端动态路由菜单(test)
*/
export function useMenuApi() {
return {
getMenuAdmin: (params) => {
// 本地数据,路径:`/public/xxx.json`
return request({
url: './admin.json',
method: 'get',
params,
});
// 模拟跨域
// return request({
// url: '/gitee/lyt-top/vue-next-admin-images/raw/master/vue2/admin.json',
// method: 'get',
// });
},
getMenuTest: (params) => {
// 本地数据,路径:`/public/xxx.json`
return request({
url: './test.json',
method: 'get',
params,
});
// 模拟跨域
// return request({
// url: '/gitee/lyt-top/vue-next-admin-images/raw/master/vue2/test.json',
// method: 'get',
// });
},
};
}

View File

@ -1,30 +0,0 @@
import request from '/@/utils/request';
/**
* 以下为模拟接口地址gitee 的不通,就换自己的真实接口地址
*
* (不建议写成 request.post(xxx),因为这样 post 时,无法 params 与 data 同时传参)
*
* 后端控制菜单模拟json路径在 https://gitee.com/lyt-top/vue-next-admin-images/tree/master/menu
* 后端控制路由isRequestRoutes 为 true则开启后端控制路由
* @method getAdminMenu 获取后端动态路由菜单(admin)
* @method getTestMenu 获取后端动态路由菜单(test)
*/
export function useMenuApi() {
return {
getAdminMenu: (params?: object) => {
return request({
url: '/gitee/lyt-top/vue-next-admin-images/raw/master/menu/adminMenu.json',
method: 'get',
params,
});
},
getTestMenu: (params?: object) => {
return request({
url: '/gitee/lyt-top/vue-next-admin-images/raw/master/menu/testMenu.json',
method: 'get',
params,
});
},
};
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 36 KiB

View File

@ -1,9 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 50" width="64" height="50">
<defs>
<image width="64" height="50" id="img1" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAyCAYAAADsg90UAAAAAXNSR0IB2cksfwAACI5JREFUeJy1WgtsW9UZdvNq0xU6WhZoCCVx7Gunbld1GSsMdYpGREhwr+2IMbQWqRoTk+iWPZi2wWhm1kwEmqV52feRJrbvvd6DiUnbQCAemzY2xh4tSB0wOqZIqAtLYzultIWEpt13btLq+sap/R8nRzpK4vh+5/++85///8851yEk1V+hX6B0tyE3OWwt0x789VR78AKlZ0KBz1oxBE3+BdUW9PNuQ7nJbk/BDQ/v4hi0x44z1R64jyrAVCjQ/fLNN8/ZoUWdgqGc57AFE6I86giH+QRw6XI9QE4RBz3iTAyVW3EyQdEDUmeIIhyeFNtMHO/o0B4e8qYASfVvLk2p4BKgdnioFCAvEQeddhnyFivOpOgvBaFXiAKcygQDrk927S/zjkae5xUA/X3YI/C5AFwHLrSPw+2+YYfCMugiCvDaCbGt0hc9uAXuP1OEAPACZQ+fAGguQ9nGgglx0OfsOAhq24lBcG/YYQa/kWLIz3XlCW4B6mKRCoCMEQdNu3VpnRVnMhBYA2L/LVCATCooXuNJSHXFzv58P+mMyyu5RQDAKNntdLnNjoN0+NMCM0D8901NDkGXuyBAseRZn3Ubagu3AFjTd1GXAdZdpCqurMgSIBS4BwRn8wgwje/d4pV6yz2adGQJyM/bo/ZyC+DSlWqApIiDHqtPyFdlCyBeD4Lv5xHgr5mAWO4ZjTRh9s8tlQDoryIbrOISoLYvvAIALxIHnIHn3GnFmfDfztLhH/IEv/vYd72xyKElJM/6Gdjj4fYCuNA3yYMa6ogdBwQfvIwAk+mguHZz3+MbMPsfLrEALD3v5RfAkH3UOID+Jqqwj2ULIDZeJvhF2He8I0NfX2ry8wI85VSy41LBzaUPsXT4L/Iy0OUbrTgpsXUlyI7lEOAsvGOboPRdKejK28shAPqpek1etxjHArxAiXCo/pAdB2QPLRQg8BLcv8ITi4h4bnaZBGD2+IsQQG0DCDUyv2LfjcHVQyB93iLAecz+bvY/T1x6YrnImwIkVakIAeSrAfIucdCz7qRcY8VJh8RrQDplWfvHxttaVvkGe6vg/h8tpwDoR926wpcOb9CkEhQ4Mbrqym4rTkrcydLhs5bU95Wwwwx+311m8qxPQ4CtvE6AdKjcLlCrQkNJXNXdnYUD0t+eF+AE1v76rV0/qvTEo/8kkkGJq/yFLIKhPsgtgEszl8E4cdAxPHdllgDBwGZW9k6Fguaa9A4PtMEwkrDe0chhCPAZ/E5dNn/kPiWqGe5mVSH1rJCdze2w4rDNDsi/gdn/FPtb0KTfUmeyQenvuE45WCLQd6unsc3fwKeAw0yHX+Zwu247DpbB3emQv3R7x7du8GgybdtrKGc39Ty23rQnqfKk5y9wCwD1XACZJg56xGUMZFVhx1taytjPBnWAfuihyT+3TEiAQwC1Mhzmqwrr9QhbBoeJg36A6Ou0Y/kGe2qQ+mhiGspHDcODTZfs0eRr8fkE0Z4xpyGt4RJgTnX1carq8Jw9dhzv6BB5k+VJyK9t6+y8dNK74bGHMSHK00Scc/CC7fwC6MotAjX6GuqTzsTBkosYW/f9YKUnIb1OFiAW/c4Ce5Lq18jLKKnu5xaApTWBHn0n8Nz6ixhw4zvJ5HXlvU2R3gWui02XizwhrEznbTW9vQ64nUEckBUut13EQOHzFFUA70jErBv+09xcYrXHqUXK8H9qIXXapSs1dm4FN1SF95DdzlD72LMNUt/15CMvXT65NfzIRtQOq5FCv7jAHkPppdqDZ3YvZFZgY8WEQN8dvvFxpB8hLvWT3T8uyWxckG9CEfX38fk0ahGgmboMMIk/2xg5UJKbYZ62sf9RbI7UPxGJzLgT0R2Y/RM0z1HO+QZ6Gl/3+dh2Wmdb6HQwUJc9IfI6fPc40Z5xCLeW2wvw8Pfoy0B5mfoMAuYLn+7oKE0FxZqp+YvWTHugw24PvvtLIjZLh83cAghzmxHqASb5qntz/4EvsfEy7UHroepvJoKtWdUcPPKrVGwsA6k2FuYTAPXAGoC8RfYCStflqRvv3VuB4Hc1SI9bBJjAZ1VWe+p1+TqBHpfG+M8Ksa0EgLacAnhiETNzIPjdbztLPIfPWrME0CSkQ+UfxDFmkQ5v5RPAYcYBckFTcDfUWc+hwZsw06tA+EiOi5RoDns66ctA/Qm3AE4tuhYgZ5Zl9uPR32165PtlINoMwjM5jtPfSvn9WTe/iEufw7PUW+U3a2NSKZ8CTU0s+DyzHAJgq9yuNDaywxNtkQuVWaTDBqs5SIdsQt4hjsXOCvmvzrDu6Fdnebo3Hj3u69xXngoFqkH09KL3ie2BB6y2XCsfZNt1apnOqsL7uY/KoB57k+SDJRUhIZlX2iD4UJ4b5ef+3dqabY9hlunUdPt0XXyIryqs1yKVAv3qbPFuKNNedaAR5FeD4NE8AqRTwZ1ZmxqnHv0EcI4Rxz3hNqSqxThevrEXqpJq31IJgOD3TN3+h0tYmmPpLt9bJex80W4SRBzmEF7kE8Bhbo5aONwutwCx6K4JUVyByu/JfOTn+6EFAiTNN1uoAgxyC1BvqFcAZLJo8pr8rlvuq8wExfpFUl+u/s7/dt6R9UKkS5ergHeaOP6Yc2SQLw7Mqa6SDznsHakvzLAyoeAPCyRvVoXpUGCb1ZZqRWFnhdQXPWcg3Jac5App7C2MImf/5Kb+nupJ0X8FSL1NEOACEyyHPQ9QbWA73CIEkBuEIqrCzUO9CsNB6esCqQ8pAqD/Oe33l9vs2SLQd6svuhJSWW6GeVptLFKGbMD9eltjZ2cjw5m4o60ChF4lCvBeJhjIuntAYF4N3KNEO9Lwgmo+F/D5mNt1cbr/2OeD916qx+HSB4gCsDdNdmXZM7dbjRJtYXeZd/EJ4DDX3Q4uARLSj604yO23UgVA2jRy2EO+OkNX/w+fZNm8pw5QbAAAAABJRU5ErkJggg=="/>
</defs>
<style>
tspan { white-space:pre }
</style>
<use id="Background" href="#img1" x="0" y="0" />
<path id="p " fill="#409eff" d="M32.37 1.82L32.37 1.82Q33.13 1.88 33.13 2.05L33.77 1.94Q44.03 1.94 49.24 11.55Q50.12 13.71 50.12 15.94Q50.12 20.75 45.61 24.03Q41.86 26.31 37.93 26.31L37.82 26.31Q34.24 26.31 30.02 25.14Q28.32 30.71 25.86 40.61L26.04 40.84Q25.39 43.36 24.46 48.4Q24.34 48.4 24.05 48.52Q19.13 46.23 19.13 42.84Q20.18 36.45 20.71 34.75Q20.71 31.82 16.31 25.67Q14.73 22.21 14.73 20.1L14.85 19.87Q14.85 19.75 14.73 19.75Q15.67 14.54 16.31 14.54Q17.07 11.96 22.64 6.63Q25.45 4.46 28.85 2.99Q30.67 2.52 32.37 1.82ZM35.77 4.69L35.77 4.69L35.77 4.81Q35.77 7.09 31.72 19.05Q31.72 19.4 30.67 22.56L30.55 23.21L30.55 23.62Q30.9 24.14 34.18 24.14Q38.23 24.14 42.09 21.57Q45.43 19.11 46.37 15.71L46.25 15.41Q46.66 14.54 46.66 14.01L46.66 12.31Q46.66 10.02 43.91 6.74Q40.8 4.69 36.53 4.69L35.77 4.69ZM18.48 16.88L18.48 16.88Q21.88 20.22 24.05 21.27Q24.81 19.52 26.04 15.18L27.45 10.79L28.73 7.27Q29.03 7.27 29.03 6.51L28.85 6.51Q22.64 8.97 18.77 16.12Q18.77 16.18 18.48 16.88ZM18.13 17.41L18.13 17.41Q17.6 19.05 17.31 21.57Q17.31 25.9 21.59 31L23.93 22.33Q23.93 22.04 21.06 20.1Q18.48 17.76 18.48 17.41L18.13 17.41Z" />
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,241 +0,0 @@
<template>
<div class="icon-selector w100 h100">
<el-input
v-model="state.fontIconSearch"
:placeholder="state.fontIconPlaceholder"
:clearable="clearable"
:disabled="disabled"
:size="size"
ref="inputWidthRef"
@clear="onClearFontIcon"
@focus="onIconFocus"
@blur="onIconBlur"
>
<template #prepend>
<SvgIcon
:name="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix"
class="font14"
v-if="state.fontIconPrefix === '' ? prepend?.indexOf('ele-') > -1 : state.fontIconPrefix?.indexOf('ele-') > -1"
/>
<i v-else :class="state.fontIconPrefix === '' ? prepend : state.fontIconPrefix" class="font14"></i>
</template>
</el-input>
<el-popover
placement="bottom"
:width="state.fontIconWidth"
transition="el-zoom-in-top"
popper-class="icon-selector-popper"
trigger="click"
:virtual-ref="inputWidthRef"
virtual-triggering
>
<template #default>
<div class="icon-selector-warp">
<div class="icon-selector-warp-title">{{ title }}</div>
<el-tabs v-model="state.fontIconTabActive" @tab-click="onIconClick">
<el-tab-pane lazy label="ali" name="ali">
<IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />
</el-tab-pane>
<el-tab-pane lazy label="ele" name="ele">
<IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />
</el-tab-pane>
<el-tab-pane lazy label="awe" name="awe">
<IconList :list="fontIconSheetsFilterList" :empty="emptyDescription" :prefix="state.fontIconPrefix" @get-icon="onColClick" />
</el-tab-pane>
</el-tabs>
</div>
</template>
</el-popover>
</div>
</template>
<script setup lang="ts" name="iconSelector">
import { defineAsyncComponent, ref, reactive, onMounted, nextTick, computed, watch } from 'vue';
import type { TabsPaneContext } from 'element-plus';
import initIconfont from '/@/utils/getStyleSheets';
import '/@/theme/iconSelector.scss';
// 定义父组件传过来的值
const props = defineProps({
// 输入框前置内容
prepend: {
type: String,
default: () => 'ele-Pointer',
},
// 输入框占位文本
placeholder: {
type: String,
default: () => '请输入内容搜索图标或者选择图标',
},
// 输入框占位文本
size: {
type: String,
default: () => 'default',
},
// 弹窗标题
title: {
type: String,
default: () => '请选择图标',
},
// 禁用
disabled: {
type: Boolean,
default: () => false,
},
// 是否可清空
clearable: {
type: Boolean,
default: () => true,
},
// 自定义空状态描述文字
emptyDescription: {
type: String,
default: () => '无相关图标',
},
// 双向绑定值,默认为 modelValue
// 参考https://v3.cn.vuejs.org/guide/migration/v-model.html#%E8%BF%81%E7%A7%BB%E7%AD%96%E7%95%A5
// 参考https://v3.cn.vuejs.org/guide/component-custom-events.html#%E5%A4%9A%E4%B8%AA-v-model-%E7%BB%91%E5%AE%9A
modelValue: String,
});
// 定义子组件向父组件传值/事件
const emit = defineEmits(['update:modelValue', 'get', 'clear']);
// 引入组件
const IconList = defineAsyncComponent(() => import('/@/components/iconSelector/list.vue'));
// 定义变量内容
const inputWidthRef = ref();
const state = reactive({
fontIconPrefix: '',
fontIconWidth: 0,
fontIconSearch: '',
fontIconPlaceholder: '',
fontIconTabActive: 'ali',
fontIconList: {
ali: [],
ele: [],
awe: [],
},
});
// 处理 input 获取焦点时modelValue 有值时,改变 input 的 placeholder 值
const onIconFocus = () => {
if (!props.modelValue) return false;
state.fontIconSearch = '';
state.fontIconPlaceholder = props.modelValue;
};
// 处理 input 失去焦点时,为空将清空 input 值,为点击选中图标时,将取原先值
const onIconBlur = () => {
const list = fontIconTabNameList();
setTimeout(() => {
const icon = list.filter((icon: string) => icon === state.fontIconSearch);
if (icon.length <= 0) state.fontIconSearch = '';
}, 300);
};
// 图标搜索及图标数据显示
const fontIconSheetsFilterList = computed(() => {
const list = fontIconTabNameList();
if (!state.fontIconSearch) return list;
let search = state.fontIconSearch.trim().toLowerCase();
return list.filter((item: string) => {
if (item.toLowerCase().indexOf(search) !== -1) return item;
});
});
// 根据 tab name 类型设置图标
const fontIconTabNameList = () => {
let iconList: any = [];
if (state.fontIconTabActive === 'ali') iconList = state.fontIconList.ali;
else if (state.fontIconTabActive === 'ele') iconList = state.fontIconList.ele;
else if (state.fontIconTabActive === 'awe') iconList = state.fontIconList.awe;
return iconList;
};
// 处理 icon 双向绑定数值回显
const initModeValueEcho = () => {
if (props.modelValue === '') return ((<string | undefined>state.fontIconPlaceholder) = props.placeholder);
(<string | undefined>state.fontIconPlaceholder) = props.modelValue;
(<string | undefined>state.fontIconPrefix) = props.modelValue;
};
// 处理 icon 类型用于回显时tab 高亮与初始化数据
const initFontIconName = () => {
let name = 'ali';
if (props.modelValue!.indexOf('iconfont') > -1) name = 'ali';
else if (props.modelValue!.indexOf('ele-') > -1) name = 'ele';
else if (props.modelValue!.indexOf('fa') > -1) name = 'awe';
// 初始化 tab 高亮回显
state.fontIconTabActive = name;
return name;
};
// 初始化数据
const initFontIconData = async (name: string) => {
if (name === 'ali') {
// 阿里字体图标使用 `iconfont xxx`
if (state.fontIconList.ali.length > 0) return;
await initIconfont.ali().then((res: any) => {
state.fontIconList.ali = res.map((i: string) => `iconfont ${i}`);
});
} else if (name === 'ele') {
// element plus 图标
if (state.fontIconList.ele.length > 0) return;
await initIconfont.ele().then((res: any) => {
state.fontIconList.ele = res;
});
} else if (name === 'awe') {
// fontawesome字体图标使用 `fa xxx`
if (state.fontIconList.awe.length > 0) return;
await initIconfont.awe().then((res: any) => {
state.fontIconList.awe = res.map((i: string) => `fa ${i}`);
});
}
// 初始化 input 的 placeholder
// 参考单项数据流https://cn.vuejs.org/v2/guide/components-props.html?#%E5%8D%95%E5%90%91%E6%95%B0%E6%8D%AE%E6%B5%81
state.fontIconPlaceholder = props.placeholder;
// 初始化双向绑定回显
initModeValueEcho();
};
// 图标点击切换
const onIconClick = (pane: TabsPaneContext) => {
initFontIconData(pane.paneName as string);
inputWidthRef.value.focus();
};
// 获取当前点击的 icon 图标
const onColClick = (v: string) => {
state.fontIconPlaceholder = v;
state.fontIconPrefix = v;
emit('get', state.fontIconPrefix);
emit('update:modelValue', state.fontIconPrefix);
inputWidthRef.value.focus();
};
// 清空当前点击的 icon 图标
const onClearFontIcon = () => {
state.fontIconPrefix = '';
emit('clear', state.fontIconPrefix);
emit('update:modelValue', state.fontIconPrefix);
};
// 获取 input 的宽度
const getInputWidth = () => {
nextTick(() => {
state.fontIconWidth = inputWidthRef.value.$el.offsetWidth;
});
};
// 监听页面宽度改变
const initResize = () => {
window.addEventListener('resize', () => {
getInputWidth();
});
};
// 页面加载时
onMounted(() => {
initFontIconData(initFontIconName());
initResize();
getInputWidth();
});
// 监听双向绑定 modelValue 的变化
watch(
() => props.modelValue,
() => {
initModeValueEcho();
initFontIconName();
}
);
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

60
src/i18n/index.js Normal file
View File

@ -0,0 +1,60 @@
import Vue from 'vue';
import VueI18n from 'vue-i18n';
import zhcnLocale from 'element-ui/lib/locale/lang/zh-CN';
import enLocale from 'element-ui/lib/locale/lang/en';
import zhtwLocale from 'element-ui/lib/locale/lang/zh-TW';
import store from '@/store/index.js';
import nextZhcn from '@/i18n/lang/zh-cn.js';
import nextEn from '@/i18n/lang/en.js';
import nextZhtw from '@/i18n/lang/zh-tw.js';
import pagesHomeZhcn from '@/i18n/pages/home/zh-cn.js';
import pagesHomeEn from '@/i18n/pages/home/en.js';
import pagesHomeZhtw from '@/i18n/pages/home/zh-tw.js';
import pagesLoginZhcn from '@/i18n/pages/login/zh-cn.js';
import pagesLoginEn from '@/i18n/pages/login/en.js';
import pagesLoginZhtw from '@/i18n/pages/login/zh-tw.js';
// 使用插件
Vue.use(VueI18n);
// 定义语言国际化内容
/**
* 说明:
* /src/i18n/lang 下的 js 为框架的国际化内容
* /src/i18n/pages 下的 js 为各界面的国际化内容
*/
const messages = {
'zh-cn': {
...zhcnLocale,
message: {
...nextZhcn,
...pagesHomeZhcn,
...pagesLoginZhcn,
},
},
en: {
...enLocale,
message: {
...nextEn,
...pagesHomeEn,
...pagesLoginEn,
},
},
'zh-tw': {
...zhtwLocale,
message: {
...nextZhtw,
...pagesHomeZhtw,
...pagesLoginZhtw,
},
},
};
// 导出语言国际化
export const i18n = new VueI18n({
locale: store.state.themeConfig.themeConfig.globalI18n,
fallbackLocale: 'zh-cn',
messages,
});

164
src/i18n/lang/en.js Normal file
View File

@ -0,0 +1,164 @@
// 定义内容
export default {
router: {
home: 'home',
system: 'system',
systemMenu: 'systemMenu',
systemUser: 'systemUser',
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',
funSignCanvas: 'Online signature',
funCountup: 'countup',
funEchartsTree: 'echartsTree',
funSelector: 'funSelector',
funWangEditor: 'wangEditor',
funCropper: 'cropper',
funMindMap: 'G6 MindMap',
funQrcode: 'qrcode',
funEchartsMap: 'EchartsMap',
funPrintJs: 'PrintJs',
funClipboard: 'Copy cut',
funScreenShort: 'screenCapture',
pagesIndex: 'pages',
pagesFiltering: 'Filtering',
pagesFilteringDetails: 'FilteringDetails',
pagesFilteringDetails1: 'FilteringDetails1',
pagesIocnfont: 'iconfont icon',
pagesElement: 'element icon',
pagesAwesome: 'awesome icon',
pagesCityLinkage: 'CityLinkage',
pagesFormAdapt: 'FormAdapt',
pagesListAdapt: 'ListAdapt',
pagesWaterfall: 'Waterfall',
pagesSteps: 'Steps',
chartIndex: 'chartIndex',
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',
dropdownDefault: 'default',
dropdownMedium: 'medium',
dropdownSmall: 'small',
dropdownMini: 'mini',
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',
},
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',
twoTitle: 'Menu / top bar',
twoTopBar: 'Top bar background',
twoMenuBar: 'Menu background',
twoColumnsMenuBar: 'Column menu background',
twoTopBarColor: 'Top bar default font color',
twoMenuBarColor: 'Menu default font color',
twoColumnsMenuBarColor: 'Default font color bar menu',
twoIsTopBarColorGradual: 'Top bar gradient',
twoIsMenuBarColorGradual: 'Menu gradient',
twoIsMenuBarColorHighlight: 'Menu font highlight',
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',
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.js` It has been modified in.',
copyText: 'replication configuration',
resetText: 'restore default',
copyTextSuccess: 'Copy succeeded!',
copyTextError: 'Copy failed!',
},
upgrade: {
title: 'New version',
msg: 'The new version is available, please update it now! Dont worry, the update is fast!',
desc: 'Prompt: Update will restore the default configuration',
btnOne: 'Cruel refusal',
btnTwo: 'Update now',
btnTwoLoading: 'Updating',
},
};

164
src/i18n/lang/zh-cn.js Normal file
View File

@ -0,0 +1,164 @@
// 定义内容
export default {
router: {
home: '首页',
system: '系统设置',
systemMenu: '菜单管理',
systemUser: '用户管理',
limits: '权限管理',
limitsFrontEnd: '前端控制',
limitsFrontEndPage: '页面权限',
limitsFrontEndBtn: '按钮权限',
limitsBackEnd: '后端控制',
limitsBackEndEndPage: '页面权限',
menu: '菜单嵌套',
menu1: '菜单1',
menu11: '菜单11',
menu12: '菜单12',
menu121: '菜单121',
menu122: '菜单122',
menu13: '菜单13',
menu2: '菜单2',
funIndex: '功能',
funTagsView: 'tagsView 操作',
funSignCanvas: '在线签名',
funCountup: 'countup 数字滚动',
funEchartsTree: 'echartsTree 树图',
funSelector: '图标选择器',
funWangEditor: 'wangEditor 编辑器',
funCropper: 'cropper 图片裁剪',
funMindMap: 'G6 思维导图',
funQrcode: 'qrcode 二维码生成',
funEchartsMap: '地理坐标/地图',
funPrintJs: '页面打印',
funClipboard: '复制剪切',
funScreenShort: 'web端自定义截屏',
pagesIndex: '页面',
pagesFiltering: '过滤筛选组件',
pagesFilteringDetails: '过滤筛选组件详情',
pagesFilteringDetails1: '过滤筛选组件详情111',
pagesIocnfont: 'iconfont 字体图标',
pagesElement: 'element 字体图标',
pagesAwesome: 'awesome 字体图标',
pagesCityLinkage: '城市多级联动',
pagesFormAdapt: '表单自适应',
pagesListAdapt: '列表自适应',
pagesWaterfall: '瀑布屏',
pagesSteps: '步骤条',
chartIndex: '大数据图表',
personal: '个人中心',
tools: '工具类集合',
layoutLinkView: '外链',
layoutIfameView: '内嵌 iframe',
},
staticRoutes: {
signIn: '登录',
notFound: '找不到此页面',
noPower: '没有权限',
},
user: {
title0: '组件大小',
title1: '语言切换',
title2: '菜单搜索',
title3: '布局配置',
title4: '消息',
title5: '开全屏',
title6: '关全屏',
dropdownDefault: '默认',
dropdownMedium: '中等',
dropdownSmall: '小型',
dropdownMini: '超小',
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: '当前页全屏',
},
notFound: {
foundTitle: '地址输入错误,请重新输入地址~',
foundMsg: '您可以先检查网址,然后重新输入或给我们反馈问题。',
foundBtn: '返回首页',
},
noAccess: {
accessTitle: '您未被授权,没有操作权限~',
accessMsg: '联系方式加QQ群探讨 665452019',
accessBtn: '重新授权',
},
layout: {
configTitle: '布局配置',
oneTitle: '全局主题',
twoTitle: '菜单 / 顶栏',
twoTopBar: '顶栏背景',
twoMenuBar: '菜单背景',
twoColumnsMenuBar: '分栏菜单背景',
twoTopBarColor: '顶栏默认字体颜色',
twoMenuBarColor: '菜单默认字体颜色',
twoColumnsMenuBarColor: '分栏菜单默认字体颜色',
twoIsTopBarColorGradual: '顶栏背景渐变',
twoIsMenuBarColorGradual: '菜单背景渐变',
twoIsMenuBarColorHighlight: '菜单字体背景高亮',
threeTitle: '界面设置',
threeIsCollapse: '菜单水平折叠',
threeIsUniqueOpened: '菜单手风琴',
threeIsFixedHeader: '固定 Header',
threeIsClassicSplitMenu: '经典布局分割菜单',
threeIsLockScreen: '开启锁屏',
threeLockScreenTime: '自动锁屏(s/秒)',
fourTitle: '界面显示',
fourIsShowLogo: '侧边栏 Logo',
fourIsBreadcrumb: '开启 Breadcrumb',
fourIsBreadcrumbIcon: '开启 Breadcrumb 图标',
fourIsTagsview: '开启 Tagsview',
fourIsTagsviewIcon: '开启 Tagsview 图标',
fourIsCacheTagsView: '开启 TagsView 缓存',
fourIsSortableTagsView: '开启 TagsView 拖拽',
fourIsFooter: '开启 Footer',
fourIsGrayscale: '灰色模式',
fourIsInvert: '色弱模式',
fourIsDark: '深色模式',
fourIsWartermark: '开启水印',
fourWartermarkText: '水印文案',
fiveTitle: '其它设置',
fiveTagsStyle: 'Tagsview 风格',
fiveAnimation: '主页面切换动画',
fiveColumnsAsideStyle: '分栏高亮风格',
fiveColumnsAsideLayout: '分栏布局风格',
sixTitle: '布局切换',
sixDefaults: '默认',
sixClassic: '经典',
sixTransverse: '横向',
sixColumns: '分栏',
tipText: '点击下方按钮,复制布局配置去 `src/store/modules/themeConfig.js` 中修改。',
copyText: '一键复制配置',
resetText: '一键恢复默认',
copyTextSuccess: '复制成功!',
copyTextError: '复制失败!',
},
upgrade: {
title: '新版本升级',
msg: '新版本来啦,马上更新尝鲜吧!不用担心,更新很快的哦!',
desc: '提示:更新会还原默认配置',
btnOne: '残忍拒绝',
btnTwo: '马上更新',
btnTwoLoading: '更新中',
},
};

164
src/i18n/lang/zh-tw.js Normal file
View File

@ -0,0 +1,164 @@
// 定义内容
export default {
router: {
home: '首頁',
system: '系統設置',
systemMenu: '選單管理',
systemUser: '用戶管理',
limits: '許可權管理',
limitsFrontEnd: '前端控制',
limitsFrontEndPage: '頁面許可權',
limitsFrontEndBtn: '按鈕許可權',
limitsBackEnd: '後端控制',
limitsBackEndEndPage: '頁面許可權',
menu: '選單嵌套',
menu1: '選單1',
menu11: '選單11',
menu12: '選單12',
menu121: '選單121',
menu122: '選單122',
menu13: '選單13',
menu2: '選單2',
funIndex: '功能',
funTagsView: 'tagsView 操作',
funSignCanvas: '線上簽名',
funCountup: 'countup 數位滾動',
funEchartsTree: 'echartsTree 樹圖',
funSelector: '圖標選擇器',
funWangEditor: 'wangEditor 編輯器',
funCropper: 'cropper 圖片裁剪',
funMindMap: 'G6 心智圖',
funQrcode: 'qrcode 二維碼生成',
funEchartsMap: '地理座標/地圖',
funPrintJs: '頁面列印',
funClipboard: '複製剪切',
funScreenShort: '自定義截圖',
pagesIndex: '頁面',
pagesFiltering: '過濾篩選組件',
pagesFilteringDetails: '過濾篩選組件詳情',
pagesFilteringDetails1: '過濾篩選組件詳情111',
pagesIocnfont: 'iconfont 字體圖標',
pagesElement: 'element 字體圖標',
pagesAwesome: 'awesome 字體圖標',
pagesCityLinkage: '都市多級聯動',
pagesFormAdapt: '表單自我調整',
pagesListAdapt: '清單自我調整',
pagesWaterfall: '瀑布屏',
pagesSteps: '步驟條',
chartIndex: '大資料圖表',
personal: '個人中心',
tools: '工具類集合',
layoutLinkView: '外鏈',
layoutIfameView: '内嵌 iframe',
},
staticRoutes: {
signIn: '登入',
notFound: '找不到此頁面',
noPower: '沒有許可權',
},
user: {
title0: '組件大小',
title1: '語言切換',
title2: '選單蒐索',
title3: '佈局配寘',
title4: '消息',
title5: '開全屏',
title6: '關全屏',
dropdownDefault: '默認',
dropdownMedium: '中等',
dropdownSmall: '小型',
dropdownMini: '超小',
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: '當前頁全屏',
},
notFound: {
foundTitle: '地址輸入錯誤,請重新輸入地址~',
foundMsg: '您可以先檢查網址,然後重新輸入或給我們迴響問題。',
foundBtn: '返回首頁',
},
noAccess: {
accessTitle: '您未被授權,沒有操作許可權~',
accessMsg: '聯繫方式加QQ群探討665452019',
accessBtn: '重新授權',
},
layout: {
configTitle: '佈局配寘',
oneTitle: '全域主題',
twoTitle: '選單 / 頂欄',
twoTopBar: '頂欄背景',
twoMenuBar: '選單背景',
twoColumnsMenuBar: '分欄選單背景',
twoTopBarColor: '頂欄默認字體顏色',
twoMenuBarColor: '選單默認字體顏色',
twoColumnsMenuBarColor: '分欄選單默認字體顏色',
twoIsTopBarColorGradual: '頂欄背景漸變',
twoIsMenuBarColorGradual: '選單背景漸變',
twoIsMenuBarColorHighlight: '選單字體背景高亮',
threeTitle: '介面設定',
threeIsCollapse: '選單水准折疊',
threeIsUniqueOpened: '選單手風琴',
threeIsFixedHeader: '固定 Header',
threeIsClassicSplitMenu: '經典佈局分割選單',
threeIsLockScreen: '開啟鎖屏',
threeLockScreenTime: '自動鎖屏(s/秒)',
fourTitle: '介面顯示',
fourIsShowLogo: '側邊欄 Logo',
fourIsBreadcrumb: '開啟 Breadcrumb',
fourIsBreadcrumbIcon: '開啟 Breadcrumb 圖標',
fourIsTagsview: '開啟 Tagsview',
fourIsTagsviewIcon: '開啟 Tagsview 圖標',
fourIsCacheTagsView: '開啟 TagsView 緩存',
fourIsSortableTagsView: '開啟 TagsView 拖拽',
fourIsFooter: '開啟 Footer',
fourIsGrayscale: '灰色模式',
fourIsInvert: '色弱模式',
fourIsDark: '深色模式',
fourIsWartermark: '開啟浮水印',
fourWartermarkText: '浮水印文案',
fiveTitle: '其它設定',
fiveTagsStyle: 'Tagsview 風格',
fiveAnimation: '主頁面切換動畫',
fiveColumnsAsideStyle: '分欄高亮風格',
fiveColumnsAsideLayout: '分欄佈局風格',
sixTitle: '佈局切換',
sixDefaults: '默認',
sixClassic: '經典',
sixTransverse: '橫向',
sixColumns: '分欄',
tipText: '點擊下方按鈕,複製佈局配寘去`src/store/modules/themeConfig.js`中修改。',
copyText: '一鍵複製配寘',
resetText: '一鍵恢復默認',
copyTextSuccess: '複製成功!',
copyTextError: '複製失敗!',
},
upgrade: {
title: '新版本陞級',
msg: '新版本來啦,馬上更新嘗鮮吧! 不用擔心,更新很快的哦!',
desc: '提示:更新會還原默認配寘',
btnOne: '殘忍拒絕',
btnTwo: '馬上更新',
btnTwoLoading: '更新中',
},
};

14
src/i18n/pages/home/en.js Normal file
View File

@ -0,0 +1,14 @@
// 定义内容
export default {
card: {
title1: 'My desk',
title2: 'Message notification',
title3: 'more',
title4: 'Marketing recommendation',
title5: 'more',
title6: 'Inventory operations',
title7: 'Performance',
title8: 'Out of stock monitoring',
title9: 'Performance overtime warning',
},
};

View File

@ -0,0 +1,14 @@
// 定义内容
export default {
card: {
title1: '我的工作台',
title2: '消息通知',
title3: '更多',
title4: '营销推荐',
title5: '更多',
title6: '库存作业',
title7: '履约情况',
title8: '缺货监控',
title9: '履约超时预警',
},
};

View File

@ -0,0 +1,14 @@
// 定义内容
export default {
card: {
title1: '我的工作臺',
title2: '消息通知',
title3: '更多',
title4: '行銷推薦',
title5: '更多',
title6: '庫存工作',
title7: '履約情况',
title8: '缺貨監控',
title9: '履約超時預警',
},
};

View File

@ -0,0 +1,18 @@
// 定义内容
export default {
login: {
placeholder1: 'The user name admin or not is test',
placeholder2: 'Password: 123456',
placeholder3: 'Please enter the verification code',
btnText: 'Sign in',
link: {
one1: 'Third party login',
one2: 'Links',
},
signInText: 'welcome back!',
copyright: {
one5: 'Copyright: Shenzhen XXX Software Technology Co., Ltd',
two6: 'Copyright: Shenzhen XXX software technology Guangdong ICP preparation no.05010000',
},
},
};

View File

@ -0,0 +1,18 @@
// 定义内容
export default {
login: {
placeholder1: '用户名 admin 或不输均为 test',
placeholder2: '密码123456',
placeholder3: '请输入验证码',
btnText: '登 录',
link: {
one1: '第三方登录',
one2: '友情链接',
},
signInText: '欢迎回来!',
copyright: {
one5: '版权所有深圳市xxx软件科技有限公司',
two6: 'Copyright: Shenzhen XXX Software Technology 粤ICP备05010000号',
},
},
};

View File

@ -0,0 +1,18 @@
// 定义内容
export default {
login: {
placeholder1: '用戶名admin或不輸均為test',
placeholder2: '密碼123456',
placeholder3: '請輸入驗證碼',
btnText: '登 录',
link: {
one1: '協力廠商登入',
one2: '友情連結',
},
signInText: '歡迎回來!',
copyright: {
one5: '版權所有深圳市xxx軟件科技有限公司',
two6: 'Copyright: Shenzhen XXX Software Technology 粵ICP備05010000號',
},
},
};

View File

@ -1,162 +1,106 @@
<template>
<div class="h100" v-show="!isTagsViewCurrenFull">
<el-aside class="layout-aside" :class="setCollapseStyle">
<el-aside class="layout-aside" :class="setCollapseWidth" v-if="clientWidth > 1000">
<Logo v-if="setShowLogo" />
<el-scrollbar class="flex-auto" ref="layoutAsideRef">
<Vertical :menuList="menuList" :class="setCollapseWidth" />
</el-scrollbar>
</el-aside>
<el-drawer :visible.sync="getThemeConfig.isCollapse" :with-header="false" direction="ltr" size="220px" v-else>
<el-aside class="layout-aside w100 h100">
<Logo v-if="setShowLogo" />
<el-scrollbar class="flex-auto" ref="layoutAsideScrollbarRef" @mouseenter="onAsideEnterLeave(true)" @mouseleave="onAsideEnterLeave(false)">
<Vertical :menuList="state.menuList" />
<el-scrollbar class="flex-auto" ref="layoutAsideRef">
<Vertical :menuList="menuList" />
</el-scrollbar>
</el-aside>
</div>
</el-drawer>
</template>
<script setup lang="ts" name="layoutAside">
import { defineAsyncComponent, reactive, computed, watch, onBeforeMount, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import mittBus from '/@/utils/mitt';
// 引入组件
const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue'));
const Vertical = defineAsyncComponent(() => import('/@/layout/navMenu/vertical.vue'));
// 定义变量内容
const layoutAsideScrollbarRef = ref();
const stores = useRoutesList();
const storesThemeConfig = useThemeConfig();
const storesTagsViewRoutes = useTagsViewRoutes();
const { routesList } = storeToRefs(stores);
const { themeConfig } = storeToRefs(storesThemeConfig);
const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
const state = reactive<AsideState>({
menuList: [],
clientWidth: 0,
});
// 设置菜单展开/收起时的宽度
const setCollapseStyle = computed(() => {
const { layout, isCollapse, menuBar } = themeConfig.value;
const asideBrTheme = ['#FFFFFF', '#FFF', '#fff', '#ffffff'];
const asideBrColor = asideBrTheme.includes(menuBar) ? 'layout-el-aside-br-color' : '';
// 判断是否是手机端
if (state.clientWidth <= 1000) {
if (isCollapse) {
document.body.setAttribute('class', 'el-popup-parent--hidden');
const asideEle = document.querySelector('.layout-container') as HTMLElement;
const modeDivs = document.createElement('div');
modeDivs.setAttribute('class', 'layout-aside-mobile-mode');
asideEle.appendChild(modeDivs);
modeDivs.addEventListener('click', closeLayoutAsideMobileMode);
return [asideBrColor, 'layout-aside-mobile', 'layout-aside-mobile-open'];
} else {
// 关闭弹窗
closeLayoutAsideMobileMode();
return [asideBrColor, 'layout-aside-mobile', 'layout-aside-mobile-close'];
}
} else {
if (layout === 'columns' || layout === 'classic') {
// 分栏布局、经典布局,菜单收起时宽度给 1px防止切换动画消失
if (isCollapse) return [asideBrColor, 'layout-aside-pc-1'];
else return [asideBrColor, 'layout-aside-pc-220'];
} else {
// 其它布局给 64px
if (isCollapse) return [asideBrColor, 'layout-aside-pc-64'];
else return [asideBrColor, 'layout-aside-pc-220'];
}
}
});
// 设置显示/隐藏 logo
const setShowLogo = computed(() => {
let { layout, isShowLogo } = themeConfig.value;
return (isShowLogo && layout === 'defaults') || (isShowLogo && layout === 'columns');
});
// 关闭移动端蒙版
const closeLayoutAsideMobileMode = () => {
const el = document.querySelector('.layout-aside-mobile-mode');
el?.setAttribute('style', 'animation: error-img-two 0.3s');
setTimeout(() => {
el?.parentNode?.removeChild(el);
}, 300);
const clientWidth = document.body.clientWidth;
if (clientWidth < 1000) themeConfig.value.isCollapse = false;
document.body.setAttribute('class', '');
};
// 设置/过滤路由(非静态路由/是否显示在菜单中)
const setFilterRoutes = () => {
if (themeConfig.value.layout === 'columns') return false;
state.menuList = filterRoutesFun(routesList.value);
};
// 路由过滤递归函数
const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
return arr
.filter((item: T) => !item.meta?.isHide)
.map((item: T) => {
item = Object.assign({}, item);
if (item.children) item.children = filterRoutesFun(item.children);
return item;
});
};
// 设置菜单导航是否固定(移动端)
const initMenuFixed = (clientWidth: number) => {
state.clientWidth = clientWidth;
};
// 鼠标移入、移出
const onAsideEnterLeave = (bool: Boolean) => {
const { layout, isColumnsMenuHoverPreload } = themeConfig.value;
if (layout !== 'columns') return false;
if (!bool) mittBus.emit('restoreDefault');
// 开启 `分栏菜单鼠标悬停预加载` 才设置,防止 columnsAside.vue 监听 pinia.state
if (isColumnsMenuHoverPreload) stores.setColumnsMenuHover(bool);
};
// 页面加载前
onBeforeMount(() => {
initMenuFixed(document.body.clientWidth);
setFilterRoutes();
// 此界面不需要取消监听(mittBus.off('setSendColumnsChildren))
// 因为切换布局时有的监听需要使用,取消了监听,某些操作将不生效
mittBus.on('setSendColumnsChildren', (res: MittMenu) => {
state.menuList = res.children;
});
// 开启经典布局分割菜单时,设置菜单数据
mittBus.on('setSendClassicChildren', (res: MittMenu) => {
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
// 经典布局分割菜单只要一项子级时,收起左侧导航菜单
res.children.length <= 1 ? (themeConfig.value.isCollapse = true) : (themeConfig.value.isCollapse = false);
state.menuList = [];
state.menuList = res.children;
}
});
// 开启经典布局分割菜单时,重新处理菜单数据
mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
setFilterRoutes();
});
// 监听窗口大小改变时(适配移动端)
mittBus.on('layoutMobileResize', (res: LayoutMobileResize) => {
initMenuFixed(res.clientWidth);
closeLayoutAsideMobileMode();
});
});
// 监听 pinia 值的变化,动态赋值给菜单中
watch(
() => [themeConfig.value.isShowLogoChange, themeConfig.value.isShowLogo, themeConfig.value.layout, themeConfig.value.isClassicSplitMenu],
([isShowLogoChange, isShowLogo, layout, isClassicSplitMenu]) => {
if (isShowLogoChange !== isShowLogo) {
if (layoutAsideScrollbarRef.value) layoutAsideScrollbarRef.value.update();
}
if (layout === 'classic' && isClassicSplitMenu) return false;
setFilterRoutes();
<script>
import Vertical from '@/layout/navMenu/vertical.vue';
import Logo from '@/layout/logo/index.vue';
export default {
name: 'layoutAside',
components: { Vertical, Logo },
data() {
return {
menuList: [],
clientWidth: '',
};
},
{
deep: true,
}
);
// 监听用户权限切换,用于演示 `权限管理 -> 前端控制 -> 页面权限` 权限切换不生效
watch(
() => routesList.value,
() => {
setFilterRoutes();
}
);
computed: {
// 设置左侧菜单的具体宽度
setCollapseWidth() {
let { layout, isCollapse } = this.$store.state.themeConfig.themeConfig;
let asideBrColor = '';
layout === 'classic' || layout === 'columns' ? (asideBrColor = 'layout-el-aside-br-color') : '';
if (layout === 'columns') {
// 分栏布局,菜单收起时宽度给 1px
if (isCollapse) {
return ['layout-aside-width1', asideBrColor];
} else {
return ['layout-aside-width-default', asideBrColor];
}
} else {
// 其它布局给 64px
if (isCollapse) {
return ['layout-aside-width64', asideBrColor];
} else {
return ['layout-aside-width-default', asideBrColor];
}
}
},
// 设置 logo 是否显示
setShowLogo() {
let { layout, isShowLogo } = this.$store.state.themeConfig.themeConfig;
return (isShowLogo && layout === 'defaults') || (isShowLogo && layout === 'columns');
},
// 获取布局配置信息
getThemeConfig() {
return this.$store.state.themeConfig.themeConfig;
},
},
created() {
this.initMenuFixed(document.body.clientWidth);
this.setFilterRoutes();
this.bus.$on('setSendColumnsChildren', (res) => {
this.menuList = res.children;
});
this.bus.$on('layoutMobileResize', (res) => {
this.initMenuFixed(res.clientWidth);
});
// 菜单滚动条监听
this.bus.$on('updateElScrollBar', () => {
setTimeout(() => {
this.$refs.layoutAsideRef.update();
}, 300);
});
},
methods: {
// 设置/过滤路由(非静态路由/是否显示在菜单中)
setFilterRoutes() {
if (this.$store.state.themeConfig.themeConfig.layout === 'columns') return false;
this.menuList = this.filterRoutesFun(this.$store.state.routesList.routesList);
},
// 设置/过滤路由 递归函数
filterRoutesFun(arr) {
return arr
.filter((item) => !item.meta.isHide)
.map((item) => {
item = Object.assign({}, item);
if (item.children) item.children = this.filterRoutesFun(item.children);
return item;
});
},
// 设置菜单导航是否固定(移动端)
initMenuFixed(clientWidth) {
this.clientWidth = clientWidth;
},
},
// 页面销毁时
destroyed() {
// 取消菜单滚动条监听
this.bus.$off('updateElScrollBar', () => {});
},
};
</script>

View File

@ -1,244 +1,179 @@
<template>
<div class="layout-columns-aside">
<el-scrollbar>
<ul @mouseleave="onColumnsAsideMenuMouseleave()">
<ul>
<li
v-for="(v, k) in state.columnsAsideList"
v-for="(v, k) in columnsAsideList"
:key="k"
@click="onColumnsAsideMenuClick(v)"
@mouseenter="onColumnsAsideMenuMouseenter(v, k)"
:ref="
(el) => {
if (el) columnsAsideOffsetTopRefs[k] = el;
}
"
:class="{ 'layout-columns-active': state.liIndex === k, 'layout-columns-hover': state.liHoverIndex === k }"
:title="v.meta.title"
ref="columnsAsideOffsetTopRefs"
:class="{ 'layout-columns-active': liIndex === k }"
:title="$t(v.meta.title)"
>
<div :class="themeConfig.columnsAsideLayout" v-if="!v.meta.isLink || (v.meta.isLink && v.meta.isIframe)">
<SvgIcon :name="v.meta.icon" />
<div class="columns-vertical-title font12">
<div :class="setColumnsAsidelayout" v-if="!v.meta.isLink || (v.meta.isLink && v.meta.isIframe)">
<i :class="v.meta.icon"></i>
<div class="font12">
{{
v.meta.title && v.meta.title.length >= 4
? v.meta.title.substr(0, themeConfig.columnsAsideLayout === 'columns-vertical' ? 4 : 3)
: v.meta.title
$t(v.meta.title) && $t(v.meta.title).length >= 4
? $t(v.meta.title).substr(0, setColumnsAsidelayout === 'columns-vertical' ? 4 : 3)
: $t(v.meta.title)
}}
</div>
</div>
<div :class="themeConfig.columnsAsideLayout" v-else>
<div :class="setColumnsAsidelayout" v-else>
<a :href="v.meta.isLink" target="_blank">
<SvgIcon :name="v.meta.icon" />
<div class="columns-vertical-title font12">
<i :class="v.meta.icon"></i>
<div class="font12">
{{
v.meta.title && v.meta.title.length >= 4
? v.meta.title.substr(0, themeConfig.columnsAsideLayout === 'columns-vertical' ? 4 : 3)
: v.meta.title
$t(v.meta.title) && $t(v.meta.title).length >= 4
? $t(v.meta.title).substr(0, setColumnsAsidelayout === 'columns-vertical' ? 4 : 3)
: $t(v.meta.title)
}}
</div>
</a>
</div>
</li>
<div ref="columnsAsideActiveRef" :class="themeConfig.columnsAsideStyle"></div>
<div ref="columnsAsideActiveRef" :class="setColumnsAsideStyle"></div>
</ul>
</el-scrollbar>
</div>
</template>
<script setup lang="ts" name="layoutColumnsAside">
import { reactive, ref, onMounted, nextTick, watch, onUnmounted } from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate, RouteRecordRaw } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig';
import mittBus from '/@/utils/mitt';
// 定义变量内容
const columnsAsideOffsetTopRefs = ref<RefType>([]);
const columnsAsideActiveRef = ref();
const stores = useRoutesList();
const storesThemeConfig = useThemeConfig();
const { routesList, isColumnsMenuHover, isColumnsNavHover } = storeToRefs(stores);
const { themeConfig } = storeToRefs(storesThemeConfig);
const route = useRoute();
const router = useRouter();
const state = reactive<ColumnsAsideState>({
columnsAsideList: [],
liIndex: 0,
liOldIndex: null,
liHoverIndex: null,
liOldPath: null,
difference: 0,
routeSplit: [],
});
// 设置菜单高亮位置移动
const setColumnsAsideMove = (k: number) => {
if (k === undefined) return false;
state.liIndex = k;
columnsAsideActiveRef.value.style.top = `${columnsAsideOffsetTopRefs.value[k].offsetTop + state.difference}px`;
};
// 菜单高亮点击事件
const onColumnsAsideMenuClick = (v: RouteItem) => {
let { path, redirect } = v;
if (redirect) {
onColumnsAsideDown(v.k);
if (route.path.startsWith(redirect)) mittBus.emit('setSendColumnsChildren', setSendChildren(redirect));
else router.push(redirect);
} else {
if (!v.children) {
router.push(path);
} else {
// 显示子级菜单
const resData: MittMenu = setSendChildren(path);
if (Object.keys(resData).length <= 0) return false;
onColumnsAsideDown(resData.item?.k);
mittBus.emit('setSendColumnsChildren', resData);
}
}
// 一个路由设置自动收起菜单
// https://gitee.com/lyt-top/vue-next-admin/issues/I6HW7H
if (!v.children) themeConfig.value.isCollapse = true;
else if (v.children.length > 1) themeConfig.value.isCollapse = false;
};
// 鼠标移入时,显示当前的子级菜单
const onColumnsAsideMenuMouseenter = (v: RouteRecordRaw, k: number) => {
if (!themeConfig.value.isColumnsMenuHoverPreload) return false;
let { path } = v;
state.liOldPath = path;
state.liOldIndex = k;
state.liHoverIndex = k;
mittBus.emit('setSendColumnsChildren', setSendChildren(path));
stores.setColumnsMenuHover(false);
stores.setColumnsNavHover(true);
};
// 鼠标移走时,显示原来的子级菜单
const onColumnsAsideMenuMouseleave = async () => {
if (!themeConfig.value.isColumnsMenuHoverPreload) return false;
await stores.setColumnsNavHover(false);
// 添加延时器,防止拿到的 store.state.routesList 值不是最新的
setTimeout(() => {
if (!isColumnsMenuHover && !isColumnsNavHover) mittBus.emit('restoreDefault');
}, 100);
};
// 设置高亮动态位置
const onColumnsAsideDown = (k: number) => {
nextTick(() => {
setColumnsAsideMove(k);
});
};
// 设置只有一个路由时设置自动收起菜单
// https://gitee.com/lyt-top/vue-next-admin/issues/I6UW2I
const setMenuAutoCollaps = (path: string) => {
const resData: MittMenu = setSendChildren(path);
// https://gitee.com/lyt-top/vue-next-admin/issues/I6HW7H
resData.children.length <= 1 ? (themeConfig.value.isCollapse = true) : (themeConfig.value.isCollapse = false);
return resData;
};
// 设置/过滤路由(非静态路由/是否显示在菜单中)
const setFilterRoutes = () => {
state.columnsAsideList = filterRoutesFun(routesList.value);
const resData: MittMenu = setMenuAutoCollaps(route.path);
onColumnsAsideDown(resData.item?.k);
// 延迟 500 毫秒更新,防止 aside.vue 组件 setSendColumnsChildren 还没有注册
setTimeout(() => {
mittBus.emit('setSendColumnsChildren', resData);
}, 500);
};
// 传送当前子级数据到菜单中
const setSendChildren = (path: string) => {
const currentPathSplit = path.split('/');
let currentData: MittMenu = { children: [] };
state.columnsAsideList.map((v: RouteItem, k: number) => {
if (v.path === `/${currentPathSplit[1]}`) {
v['k'] = k;
currentData['item'] = { ...v };
currentData['children'] = [{ ...v }];
if (v.children) currentData['children'] = v.children;
}
});
return currentData;
};
// 路由过滤递归函数
const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
return arr
.filter((item: T) => !item.meta?.isHide)
.map((item: T) => {
item = Object.assign({}, item);
if (item.children) item.children = filterRoutesFun(item.children);
return item;
});
};
// tagsView 点击时,根据路由查找下标 columnsAsideList实现左侧菜单高亮
const setColumnsMenuHighlight = (path: string) => {
state.routeSplit = path.split('/');
state.routeSplit.shift();
const routeFirst = `/${state.routeSplit[0]}`;
const currentSplitRoute = state.columnsAsideList.find((v: RouteItem) => v.path === routeFirst);
if (!currentSplitRoute) return false;
// 延迟拿值,防止取不到
setTimeout(() => {
onColumnsAsideDown(currentSplitRoute.k);
}, 0);
};
// 页面加载时
onMounted(() => {
setFilterRoutes();
// 销毁变量,防止鼠标再次移入时,保留了上次的记录
mittBus.on('restoreDefault', () => {
state.liOldIndex = null;
state.liOldPath = null;
});
});
// 页面卸载时
onUnmounted(() => {
mittBus.off('restoreDefault', () => {});
});
// 路由更新时
onBeforeRouteUpdate((to) => {
const resData = setMenuAutoCollaps(to.path);
setColumnsMenuHighlight(to.path);
mittBus.emit('setSendColumnsChildren', resData);
});
// 监听布局配置信息的变化,动态增加菜单高亮位置移动像素
watch(
[() => themeConfig.value.columnsAsideStyle, isColumnsMenuHover, isColumnsNavHover],
() => {
themeConfig.value.columnsAsideStyle === 'columnsRound' ? (state.difference = 3) : (state.difference = 0);
if (!isColumnsMenuHover.value && !isColumnsNavHover.value) {
state.liHoverIndex = null;
mittBus.emit('setSendColumnsChildren', setSendChildren(route.path));
} else {
state.liHoverIndex = state.liOldIndex;
if (!state.liOldPath) return false;
mittBus.emit('setSendColumnsChildren', setSendChildren(state.liOldPath));
}
<script>
export default {
name: 'layoutColumnsAside',
data() {
return {
columnsAsideList: [],
liIndex: 0,
difference: 0,
routeSplit: [],
};
},
{
deep: true,
}
);
computed: {
// 设置分栏高亮风格
setColumnsAsideStyle() {
return this.$store.state.themeConfig.themeConfig.columnsAsideStyle;
},
// 设置分栏布局风格
setColumnsAsidelayout() {
return this.$store.state.themeConfig.themeConfig.columnsAsideLayout;
},
},
mounted() {
this.setFilterRoutes();
},
methods: {
// 设置菜单高亮位置移动
setColumnsAsideMove(k) {
if (k === undefined) return false;
const els = this.$refs.columnsAsideOffsetTopRefs;
this.liIndex = k;
this.$refs.columnsAsideActiveRef.style.top = `${els[k].offsetTop + this.difference}px`;
},
// 菜单高亮点击事件
onColumnsAsideMenuClick(v) {
let { path, redirect } = v;
if (redirect) this.$router.push(redirect);
else this.$router.push(path);
// 一个路由设置自动收起菜单
// https://gitee.com/lyt-top/vue-next-admin/issues/I6HW7H
if (!v.children || v.children.length <= 1) this.$store.state.themeConfig.themeConfig.isCollapse = true;
else if (v.children.length > 1) this.$store.state.themeConfig.themeConfig.isCollapse = false;
},
// 设置高亮动态位置
onColumnsAsideDown(k) {
this.$nextTick(() => {
this.setColumnsAsideMove(k);
});
},
// 设置只有一个路由时设置自动收起菜单
// https://gitee.com/lyt-top/vue-next-admin/issues/I6UW2I
setMenuAutoCollaps(path) {
const resData = this.setSendChildren(path);
// https://gitee.com/lyt-top/vue-next-admin/issues/I6HW7H
resData.children.length <= 1
? (this.$store.state.themeConfig.themeConfig.isCollapse = true)
: (this.$store.state.themeConfig.themeConfig.isCollapse = false);
return resData;
},
// 设置/过滤路由(非静态路由/是否显示在菜单中)
setFilterRoutes() {
if (this.$store.state.routesList.routesList.length <= 0) return false;
this.columnsAsideList = this.filterRoutesFun(this.$store.state.routesList.routesList);
const resData = this.setMenuAutoCollaps(this.$route.path);
this.onColumnsAsideDown(resData.item[0].k);
this.bus.$emit('setSendColumnsChildren', resData);
},
// 传送当前子级数据到菜单中
setSendChildren(path) {
const currentPathSplit = path.split('/');
let currentData = {};
this.columnsAsideList.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;
},
// 路由过滤递归函数
filterRoutesFun(arr) {
return arr
.filter((item) => !item.meta.isHide)
.map((item) => {
item = Object.assign({}, item);
if (item.children) item.children = this.filterRoutesFun(item.children);
return item;
});
},
// tagsView 点击时,根据路由查找下标 columnsAsideList实现左侧菜单高亮
setColumnsMenuHighlight(path) {
this.routeSplit = path.split('/');
this.routeSplit.shift();
const routeFirst = `/${this.routeSplit[0]}`;
const currentSplitRoute = this.columnsAsideList.find((v) => v.path === routeFirst);
if (!currentSplitRoute) return false;
// 延迟拿值,防止取不到
setTimeout(() => {
this.onColumnsAsideDown(currentSplitRoute.k);
}, 0);
},
},
watch: {
// 监听 vuex 数据变化
'$store.state': {
handler(val) {
val.themeConfig.themeConfig.columnsAsideStyle === 'columnsRound' ? (this.difference = 3) : (this.difference = 0);
if (val.routesList.routesList.length === this.columnsAsideList.length) return false;
this.setFilterRoutes();
},
deep: true,
},
// 监听路由的变化
$route: {
handler(to) {
const resData = this.setMenuAutoCollaps(to.path);
this.setColumnsMenuHighlight(to.path);
this.bus.$emit('setSendColumnsChildren', resData);
},
deep: true,
},
},
};
</script>
<style scoped lang="scss">
.layout-columns-aside {
width: 70px;
height: 100%;
background: var(--next-bg-columnsMenuBar);
background: var(--prev-bg-columnsMenuBar);
ul {
position: relative;
.layout-columns-active,
.layout-columns-active a {
color: var(--next-bg-columnsMenuBarColor) !important;
transition: 0.3s ease-in-out;
}
.layout-columns-hover {
color: var(--el-color-primary);
a {
color: var(--el-color-primary);
}
}
li {
color: var(--next-bg-columnsMenuBarColor);
color: var(--prev-bg-columnsMenuBarColor);
width: 100%;
height: 50px;
text-align: center;
@ -246,9 +181,6 @@ watch(
cursor: pointer;
position: relative;
z-index: 1;
&:hover {
@extend .layout-columns-hover;
}
.columns-vertical {
margin: auto;
.columns-vertical-title {
@ -273,16 +205,21 @@ watch(
}
a {
text-decoration: none;
color: var(--next-bg-columnsMenuBarColor);
color: var(--prev-bg-columnsMenuBarColor);
}
}
.layout-columns-active,
.layout-columns-active a {
color: var(--prev-color-text-white);
transition: 0.3s ease-in-out;
}
.columns-round {
background: var(--el-color-primary);
color: var(--el-color-white);
background: var(--prev-color-primary);
color: var(--prev-color-text-white);
position: absolute;
left: 50%;
top: 2px;
height: 44px;
height: 50px;
width: 65px;
transform: translateX(-50%);
z-index: 0;

View File

@ -1,18 +1,24 @@
<template>
<el-header class="layout-header" v-show="!isTagsViewCurrenFull">
<el-header class="layout-header" :height="setHeaderHeight">
<NavBarsIndex />
</el-header>
</template>
<script setup lang="ts" name="layoutHeader">
import { defineAsyncComponent } from 'vue';
import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
// 引入组件
const NavBarsIndex = defineAsyncComponent(() => import('/@/layout/navBars/index.vue'));
// 定义变量内容
const storesTagsViewRoutes = useTagsViewRoutes();
const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
<script>
import NavBarsIndex from '@/layout/navBars/index.vue';
export default {
name: 'layoutHeader',
components: { NavBarsIndex },
data() {
return {};
},
computed: {
// 设置顶部 header 的具体高度
setHeaderHeight() {
let { isTagsview, layout } = this.$store.state.themeConfig.themeConfig;
if (isTagsview && layout !== 'classic') return '84px';
else return '50px';
},
},
};
</script>

View File

@ -1,65 +1,93 @@
<template>
<el-main class="layout-main" :style="isFixedHeader ? `height: calc(100% - ${setMainHeight})` : `minHeight: calc(100% - ${setMainHeight})`">
<el-main class="layout-main">
<el-scrollbar
ref="layoutMainScrollbarRef"
class="layout-main-scroll layout-backtop-header-fixed"
wrap-class="layout-main-scroll"
view-class="layout-main-scroll"
class="layout-scrollbar"
ref="layoutScrollbarRef"
v-show="!currentRouteMeta.isLink && !currentRouteMeta.isIframe"
:style="{ minHeight: `calc(100vh - ${headerHeight}` }"
>
<LayoutParentView />
<LayoutFooter v-if="isFooter" />
<Footers v-if="getThemeConfig.isFooter" />
</el-scrollbar>
<el-backtop :target="setBacktopClass" />
<Links
:style="{ height: `calc(100vh - ${headerHeight}` }"
:meta="currentRouteMeta"
v-if="currentRouteMeta.isLink && !currentRouteMeta.isIframe"
/>
<Iframes
:style="{ height: `calc(100vh - ${headerHeight}` }"
:meta="currentRouteMeta"
v-if="currentRouteMeta.isLink && currentRouteMeta.isIframe && isShowLink"
@getCurrentRouteMeta="onGetCurrentRouteMeta"
/>
</el-main>
</template>
<script setup lang="ts" name="layoutMain">
import { defineAsyncComponent, onMounted, computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
import { useThemeConfig } from '/@/stores/themeConfig';
import { NextLoading } from '/@/utils/loading';
// 引入组件
const LayoutParentView = defineAsyncComponent(() => import('/@/layout/routerView/parent.vue'));
const LayoutFooter = defineAsyncComponent(() => import('/@/layout/footer/index.vue'));
// 定义变量内容
const layoutMainScrollbarRef = ref();
const route = useRoute();
const storesTagsViewRoutes = useTagsViewRoutes();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { isTagsViewCurrenFull } = storeToRefs(storesTagsViewRoutes);
// 设置 footer 显示/隐藏
const isFooter = computed(() => {
return themeConfig.value.isFooter && !route.meta.isIframe;
});
// 设置 header 固定
const isFixedHeader = computed(() => {
return themeConfig.value.isFixedHeader;
});
// 设置 Backtop 回到顶部
const setBacktopClass = computed(() => {
if (themeConfig.value.isFixedHeader) return `.layout-backtop-header-fixed .el-scrollbar__wrap`;
else return `.layout-backtop .el-scrollbar__wrap`;
});
// 设置主内容区的高度
const setMainHeight = computed(() => {
if (isTagsViewCurrenFull.value) return '0px';
const { isTagsview, layout } = themeConfig.value;
if (isTagsview && layout !== 'classic') return '85px';
else return '51px';
});
// 页面加载前
onMounted(() => {
NextLoading.done(600);
});
// 暴露变量
defineExpose({
layoutMainScrollbarRef,
});
<script>
import LayoutParentView from '@/layout/routerView/parent.vue';
import Footers from '@/layout/footer/index.vue';
import Links from '@/layout/routerView/link.vue';
import Iframes from '@/layout/routerView/iframes.vue';
export default {
name: 'layoutMain',
components: { LayoutParentView, Footers, Links, Iframes },
data() {
return {
headerHeight: '',
currentRouteMeta: {},
isShowLink: false,
};
},
computed: {
// 获取布局配置信息
getThemeConfig() {
return this.$store.state.themeConfig.themeConfig;
},
},
mounted() {
this.initHeaderHeight();
this.initCurrentRouteMeta(this.$route.meta);
},
methods: {
// 初始化当前路由 meta 信息
initCurrentRouteMeta(meta) {
this.isShowLink = false;
this.currentRouteMeta = meta;
setTimeout(() => {
this.isShowLink = true;
}, 100);
},
// 设置 main 的高度
initHeaderHeight() {
let { isTagsview } = this.$store.state.themeConfig.themeConfig;
if (isTagsview) return (this.headerHeight = `84px`);
else return (this.headerHeight = `50px`);
},
// 子组件触发更新
onGetCurrentRouteMeta() {
this.initCurrentRouteMeta(this.$route.meta);
},
},
watch: {
// 监听 vuex 数据变化
'$store.state.themeConfig.themeConfig': {
handler(val) {
this.headerHeight = val.isTagsview ? '84px' : '50px';
if (val.isFixedHeaderChange !== val.isFixedHeader) {
if (!this.$refs.layoutScrollbarRef) return false;
this.$refs.layoutScrollbarRef.update();
}
},
deep: true,
},
// 监听路由的变化
$route: {
handler(to) {
this.initCurrentRouteMeta(to.meta);
this.$refs.layoutScrollbarRef.wrap.scrollTop = 0;
},
deep: true,
},
},
};
</script>

View File

@ -1,14 +1,19 @@
<template>
<div class="layout-footer pb15">
<div class="layout-footer mt15">
<div class="layout-footer-warp">
<div>vue-next-adminMade by lyt with </div>
<div class="mt5">深圳市 xxx 公司版权所有</div>
<div>vue-prev-adminMade by lyt with </div>
<div class="mt5">{{ $t('message.login.copyright.one5') }}</div>
</div>
</div>
</template>
<script setup lang="ts" name="layoutFooter">
// 此处需有内容(注释也得),否则缓存将失败
<script>
export default {
name: 'layoutFooter',
data() {
return {};
},
};
</script>
<style scoped lang="scss">
@ -17,9 +22,8 @@
display: flex;
&-warp {
margin: auto;
color: var(--el-text-color-secondary);
color: var(--prev-color-text-secondary);
text-align: center;
animation: error-num 0.3s ease;
}
}
</style>

View File

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

View File

@ -1,352 +0,0 @@
<template>
<div v-show="state.isShowLockScreen">
<div class="layout-lock-screen-mask"></div>
<div class="layout-lock-screen-img" :class="{ 'layout-lock-screen-filter': state.isShowLoockLogin }"></div>
<div class="layout-lock-screen">
<div
class="layout-lock-screen-date"
ref="layoutLockScreenDateRef"
@mousedown="onDownPc"
@mousemove="onMovePc"
@mouseup="onEnd"
@touchstart.stop="onDownApp"
@touchmove.stop="onMoveApp"
@touchend.stop="onEnd"
>
<div class="layout-lock-screen-date-box">
<div class="layout-lock-screen-date-box-time">
{{ state.time.hm }}<span class="layout-lock-screen-date-box-minutes">{{ state.time.s }}</span>
</div>
<div class="layout-lock-screen-date-box-info">{{ state.time.mdq }}</div>
</div>
<div class="layout-lock-screen-date-top">
<SvgIcon name="ele-Top" />
<div class="layout-lock-screen-date-top-text">上滑解锁</div>
</div>
</div>
<transition name="el-zoom-in-center">
<div v-show="state.isShowLoockLogin" class="layout-lock-screen-login">
<div class="layout-lock-screen-login-box">
<div class="layout-lock-screen-login-box-img">
<img src="https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500" />
</div>
<div class="layout-lock-screen-login-box-name">Administrator</div>
<div class="layout-lock-screen-login-box-value">
<el-input
placeholder="请输入密码"
ref="layoutLockScreenInputRef"
v-model="state.lockScreenPassword"
@keyup.enter.native.stop="onLockScreenSubmit()"
>
<template #append>
<el-button @click="onLockScreenSubmit">
<el-icon class="el-input__icon">
<ele-Right />
</el-icon>
</el-button>
</template>
</el-input>
</div>
</div>
<div class="layout-lock-screen-login-icon">
<SvgIcon name="ele-Microphone" :size="20" />
<SvgIcon name="ele-AlarmClock" :size="20" />
<SvgIcon name="ele-SwitchButton" :size="20" />
</div>
</div>
</transition>
</div>
</div>
</template>
<script setup lang="ts" name="layoutLockScreen">
import { nextTick, onMounted, reactive, ref, onUnmounted } from 'vue';
import { formatDate } from '/@/utils/formatTime';
import { Local } from '/@/utils/storage';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
// 定义变量内容
const layoutLockScreenDateRef = ref<HtmlType>();
const layoutLockScreenInputRef = ref();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const state = reactive({
transparency: 1,
downClientY: 0,
moveDifference: 0,
isShowLoockLogin: false,
isFlags: false,
querySelectorEl: '' as HtmlType,
time: {
hm: '',
s: '',
mdq: '',
},
setIntervalTime: 0,
isShowLockScreen: false,
isShowLockScreenIntervalTime: 0,
lockScreenPassword: '',
});
// 鼠标按下 pc
const onDownPc = (down: MouseEvent) => {
state.isFlags = true;
state.downClientY = down.clientY;
};
// 鼠标按下 app
const onDownApp = (down: TouchEvent) => {
state.isFlags = true;
state.downClientY = down.touches[0].clientY;
};
// 鼠标移动 pc
const onMovePc = (move: MouseEvent) => {
state.moveDifference = move.clientY - state.downClientY;
onMove();
};
// 鼠标移动 app
const onMoveApp = (move: TouchEvent) => {
state.moveDifference = move.touches[0].clientY - state.downClientY;
onMove();
};
// 鼠标移动事件
const onMove = () => {
if (state.isFlags) {
const el = <HTMLElement>state.querySelectorEl;
const opacitys = (state.transparency -= 1 / 200);
if (state.moveDifference >= 0) return false;
el.setAttribute('style', `top:${state.moveDifference}px;cursor:pointer;opacity:${opacitys};`);
if (state.moveDifference < -400) {
el.setAttribute('style', `top:${-el.clientHeight}px;cursor:pointer;transition:all 0.3s ease;`);
state.moveDifference = -el.clientHeight;
setTimeout(() => {
el && el.parentNode?.removeChild(el);
}, 300);
}
if (state.moveDifference === -el.clientHeight) {
state.isShowLoockLogin = true;
layoutLockScreenInputRef.value.focus();
}
}
};
// 鼠标松开
const onEnd = () => {
state.isFlags = false;
state.transparency = 1;
if (state.moveDifference >= -400) {
(<HTMLElement>state.querySelectorEl).setAttribute('style', `top:0px;opacity:1;transition:all 0.3s ease;`);
}
};
// 获取要拖拽的初始元素
const initGetElement = () => {
nextTick(() => {
state.querySelectorEl = layoutLockScreenDateRef.value;
});
};
// 时间初始化
const initTime = () => {
state.time.hm = formatDate(new Date(), 'HH:MM');
state.time.s = formatDate(new Date(), 'SS');
state.time.mdq = formatDate(new Date(), 'mm月dd日WWW');
};
// 时间初始化定时器
const initSetTime = () => {
initTime();
state.setIntervalTime = window.setInterval(() => {
initTime();
}, 1000);
};
// 锁屏时间定时器
const initLockScreen = () => {
if (themeConfig.value.isLockScreen) {
state.isShowLockScreenIntervalTime = window.setInterval(() => {
if (themeConfig.value.lockScreenTime <= 1) {
state.isShowLockScreen = true;
setLocalThemeConfig();
return false;
}
themeConfig.value.lockScreenTime--;
}, 1000);
} else {
clearInterval(state.isShowLockScreenIntervalTime);
}
};
// 存储布局配置
const setLocalThemeConfig = () => {
themeConfig.value.isDrawer = false;
Local.set('themeConfig', themeConfig.value);
};
// 密码输入点击事件
const onLockScreenSubmit = () => {
themeConfig.value.isLockScreen = false;
themeConfig.value.lockScreenTime = 30;
setLocalThemeConfig();
};
// 页面加载时
onMounted(() => {
initGetElement();
initSetTime();
initLockScreen();
});
// 页面卸载时
onUnmounted(() => {
window.clearInterval(state.setIntervalTime);
window.clearInterval(state.isShowLockScreenIntervalTime);
});
</script>
<style scoped lang="scss">
.layout-lock-screen-fixed {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.layout-lock-screen-filter {
filter: blur(1px);
}
.layout-lock-screen-mask {
background: var(--el-color-white);
@extend .layout-lock-screen-fixed;
z-index: 9999990;
}
.layout-lock-screen-img {
@extend .layout-lock-screen-fixed;
background-image: url('https://i.hd-r.cn/e4a19d84364f185266666765ac21a5db.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(--el-color-white);
}
&-info {
font-size: 40px;
color: var(--el-color-white);
}
&-minutes {
font-size: 16px;
}
}
&-top {
width: 40px;
height: 40px;
line-height: 40px;
border-radius: 100%;
border: 1px solid var(--el-border-color-light, #ebeef5);
background: rgba(255, 255, 255, 0.1);
color: var(--el-color-white);
opacity: 0.8;
position: absolute;
right: 30px;
bottom: 50px;
text-align: center;
overflow: hidden;
transition: all 0.3s ease;
i {
transition: all 0.3s ease;
}
&-text {
opacity: 0;
position: absolute;
top: 150%;
font-size: 12px;
color: var(--el-color-white);
left: 50%;
line-height: 1.2;
transform: translate(-50%, -50%);
transition: all 0.3s ease;
width: 35px;
}
&:hover {
border: 1px solid rgba(255, 255, 255, 0.5);
background: rgba(255, 255, 255, 0.2);
box-shadow: 0 0 12px 0 rgba(255, 255, 255, 0.5);
color: var(--el-color-white);
opacity: 1;
transition: all 0.3s ease;
i {
transform: translateY(-40px);
transition: all 0.3s ease;
}
.layout-lock-screen-date-top-text {
opacity: 1;
top: 50%;
transition: all 0.3s ease;
}
}
}
}
&-login {
position: relative;
z-index: 9999994;
width: 100%;
height: 100%;
left: 0;
top: 0;
display: flex;
flex-direction: column;
justify-content: center;
color: var(--el-color-white);
&-box {
text-align: center;
margin: auto;
&-img {
width: 180px;
height: 180px;
margin: auto;
img {
width: 100%;
height: 100%;
border-radius: 100%;
}
}
&-name {
font-size: 26px;
margin: 15px 0 30px;
}
}
&-icon {
position: absolute;
right: 30px;
bottom: 30px;
i {
font-size: 20px;
margin-left: 15px;
cursor: pointer;
opacity: 0.8;
&:hover {
opacity: 1;
}
}
}
}
}
:deep(.el-input-group__append) {
background: var(--el-color-white);
padding: 0px 15px;
}
:deep(.el-input__inner) {
border-right-color: var(--el-border-color-extra-light);
&:hover {
border-color: var(--el-border-color-extra-light);
}
}
</style>

View File

@ -1,32 +1,39 @@
<template>
<div class="layout-logo" v-if="setShowLogo" @click="onThemeConfigChange">
<img :src="logoMini" class="layout-logo-medium-img" />
<span>{{ themeConfig.globalTitle }}</span>
<img :src="logo" 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" />
<img :src="logo" class="layout-logo-size-img" />
</div>
</template>
<script setup lang="ts" name="layoutLogo">
import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import logoMini from '/@/assets/logo-mini.svg';
// 定义变量内容
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
// 设置 logo 的显示。classic 经典布局默认显示 logo
const setShowLogo = computed(() => {
let { isCollapse, layout } = themeConfig.value;
return !isCollapse || layout === 'classic' || document.body.clientWidth < 1000;
});
// logo 点击实现菜单展开/收起
const onThemeConfigChange = () => {
if (themeConfig.value.layout === 'transverse') return false;
themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
<script>
export default {
name: 'layoutLogo',
data() {
return {
logo: require('@/assets/logo-mini.svg'),
};
},
computed: {
// 获取布局配置信息
getThemeConfig() {
return this.$store.state.themeConfig.themeConfig;
},
// 设置 logo 是否显示
setShowLogo() {
let { isCollapse, layout } = this.$store.state.themeConfig.themeConfig;
return !isCollapse || layout === 'classic' || document.body.clientWidth < 1000;
},
},
methods: {
// logo 点击实现菜单展开/收起
onThemeConfigChange() {
if (this.$store.state.themeConfig.themeConfig.layout === 'transverse') return false;
this.$store.state.themeConfig.themeConfig.isCollapse = !this.$store.state.themeConfig.themeConfig.isCollapse;
},
},
};
</script>
@ -38,22 +45,20 @@ const onThemeConfigChange = () => {
align-items: center;
justify-content: center;
box-shadow: rgb(0 21 41 / 2%) 0px 1px 4px;
color: var(--el-color-primary);
color: var(--prev-color-primary);
font-size: 16px;
cursor: pointer;
animation: logoAnimation 0.3s ease-in-out;
span {
white-space: nowrap;
display: inline-block;
}
&:hover {
span {
color: var(--color-primary-light-2);
opacity: 0.9;
}
}
&-medium-img {
width: 20px;
margin-right: 5px;
position: relative;
top: 2px;
}
}
.layout-logo-size {
@ -61,15 +66,10 @@ const onThemeConfigChange = () => {
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;
}
animation: logoAnimation 0.3s ease-in-out;
}
}
</style>

View File

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

View File

@ -1,73 +1,33 @@
<template>
<el-container class="layout-container">
<ColumnsAside />
<el-container class="layout-columns-warp layout-container-view h100">
<LayoutAside />
<el-scrollbar ref="layoutScrollbarRef" class="layout-backtop">
<LayoutHeader />
<LayoutMain ref="layoutMainRef" />
</el-scrollbar>
</el-container>
<div class="layout-columns-warp">
<Asides />
<el-container class="flex-center layout-backtop">
<Headers v-if="isFixedHeader" />
<el-scrollbar>
<Headers v-if="!isFixedHeader" />
<Mains />
</el-scrollbar>
</el-container>
</div>
<el-backtop target=".layout-backtop .el-scrollbar__wrap"></el-backtop>
</el-container>
</template>
<script setup lang="ts" name="layoutColumns">
import { defineAsyncComponent, watch, onMounted, nextTick, ref } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
// 引入组件
const LayoutAside = defineAsyncComponent(() => import('/@/layout/component/aside.vue'));
const LayoutHeader = defineAsyncComponent(() => import('/@/layout/component/header.vue'));
const LayoutMain = defineAsyncComponent(() => import('/@/layout/component/main.vue'));
const ColumnsAside = defineAsyncComponent(() => import('/@/layout/component/columnsAside.vue'));
// 定义变量内容
const layoutScrollbarRef = ref<RefType>('');
const layoutMainRef = ref<InstanceType<typeof LayoutMain>>();
const route = useRoute();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
// 重置滚动条高度
const updateScrollbar = () => {
// 更新父级 scrollbar
layoutScrollbarRef.value.update();
// 更新子级 scrollbar
layoutMainRef.value && layoutMainRef.value!.layoutMainScrollbarRef.update();
};
// 重置滚动条高度,由于组件是异步引入的
const initScrollBarHeight = () => {
nextTick(() => {
setTimeout(() => {
updateScrollbar();
layoutScrollbarRef.value.wrapRef.scrollTop = 0;
layoutMainRef.value!.layoutMainScrollbarRef.wrapRef.scrollTop = 0;
}, 500);
});
};
// 页面加载时
onMounted(() => {
initScrollBarHeight();
});
// 监听路由的变化,切换界面时,滚动条置顶
watch(
() => route.path,
() => {
initScrollBarHeight();
}
);
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(
() => [themeConfig.value.isTagsview, themeConfig.value.isFixedHeader],
() => {
nextTick(() => {
updateScrollbar();
});
<script>
import Asides from '@/layout/component/aside.vue';
import Headers from '@/layout/component/header.vue';
import Mains from '@/layout/component/main.vue';
import ColumnsAside from '@/layout/component/columnsAside.vue';
export default {
name: 'layoutColumns',
components: { Asides, Headers, Mains, ColumnsAside },
computed: {
// 是否开启固定 header
isFixedHeader() {
return this.$store.state.themeConfig.themeConfig.isFixedHeader;
},
},
{
deep: true,
}
);
};
</script>

View File

@ -1,73 +1,41 @@
<template>
<el-container class="layout-container">
<LayoutAside />
<el-container class="layout-container-view h100">
<el-scrollbar ref="layoutScrollbarRef" class="layout-backtop">
<LayoutHeader />
<LayoutMain ref="layoutMainRef" />
<Asides />
<el-container class="flex-center layout-backtop">
<Headers v-if="isFixedHeader" />
<el-scrollbar ref="layoutDefaultsScrollbarRef">
<Headers v-if="!isFixedHeader" />
<Mains />
</el-scrollbar>
</el-container>
<el-backtop target=".layout-backtop .el-scrollbar__wrap"></el-backtop>
</el-container>
</template>
<script setup lang="ts" name="layoutDefaults">
import { defineAsyncComponent, watch, onMounted, nextTick, ref } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { NextLoading } from '/@/utils/loading';
// 引入组件
const LayoutAside = defineAsyncComponent(() => import('/@/layout/component/aside.vue'));
const LayoutHeader = defineAsyncComponent(() => import('/@/layout/component/header.vue'));
const LayoutMain = defineAsyncComponent(() => import('/@/layout/component/main.vue'));
// 定义变量内容
const layoutScrollbarRef = ref<RefType>('');
const layoutMainRef = ref<InstanceType<typeof LayoutMain>>();
const route = useRoute();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
// 重置滚动条高度
const updateScrollbar = () => {
// 更新父级 scrollbar
layoutScrollbarRef.value.update();
// 更新子级 scrollbar
layoutMainRef.value!.layoutMainScrollbarRef.update();
};
// 重置滚动条高度,由于组件是异步引入的
const initScrollBarHeight = () => {
nextTick(() => {
setTimeout(() => {
updateScrollbar();
layoutScrollbarRef.value.wrapRef.scrollTop = 0;
layoutMainRef.value!.layoutMainScrollbarRef.wrapRef.scrollTop = 0;
}, 500);
});
};
// 页面加载时
onMounted(() => {
initScrollBarHeight();
NextLoading.done(600);
});
// 监听路由的变化,切换界面时,滚动条置顶
watch(
() => route.path,
() => {
initScrollBarHeight();
}
);
// 监听 themeConfig 配置文件的变化,更新菜单 el-scrollbar 的高度
watch(
() => [themeConfig.value.isTagsview, themeConfig.value.isFixedHeader],
() => {
nextTick(() => {
updateScrollbar();
});
<script>
import Asides from '@/layout/component/aside.vue';
import Headers from '@/layout/component/header.vue';
import Mains from '@/layout/component/main.vue';
export default {
name: 'layoutDefaults',
components: { Asides, Headers, Mains },
data() {
return {};
},
{
deep: true,
}
);
computed: {
// 是否开启固定 header
isFixedHeader() {
return this.$store.state.themeConfig.themeConfig.isFixedHeader;
},
},
watch: {
// 监听路由的变化
$route: {
handler() {
this.$refs.layoutDefaultsScrollbarRef.wrap.scrollTop = 0;
},
deep: true,
},
},
};
</script>

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,18 @@
<template>
<div v-if="isShowBreadcrumb" class="layout-navbars-breadcrumb">
<SvgIcon
<div class="layout-navbars-breadcrumb" :style="{ display: isShowBreadcrumb }">
<i
class="layout-navbars-breadcrumb-icon"
:name="themeConfig.isCollapse ? 'ele-Expand' : 'ele-Fold'"
:size="16"
:class="getThemeConfig.isCollapse ? 'el-icon-s-unfold' : 'el-icon-s-fold'"
@click="onThemeConfigChange"
/>
></i>
<el-breadcrumb class="layout-navbars-breadcrumb-hide">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(v, k) in state.breadcrumbList" :key="!v.meta.tagsViewName ? v.meta.title : v.meta.tagsViewName">
<span v-if="k === state.breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />
<div v-if="!v.meta.tagsViewName">{{ v.meta.title }}</div>
<div v-else>{{ v.meta.tagsViewName }}</div>
<transition-group name="breadcrumb" mode="out-in">
<el-breadcrumb-item v-for="(v, k) in breadcrumbList" :key="v.path">
<span v-if="k === breadcrumbList.length - 1" class="layout-navbars-breadcrumb-span">
<i :class="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="getThemeConfig.isBreadcrumbIcon"></i>{{ $t(v.meta.title) }}
</span>
<a v-else @click.prevent="onBreadcrumbClick(v)">
<SvgIcon :name="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="themeConfig.isBreadcrumbIcon" />{{ v.meta.title }}
<i :class="v.meta.icon" class="layout-navbars-breadcrumb-iconfont" v-if="getThemeConfig.isBreadcrumbIcon"></i>{{ $t(v.meta.title) }}
</a>
</el-breadcrumb-item>
</transition-group>
@ -23,86 +20,94 @@
</div>
</template>
<script setup lang="ts" name="layoutBreadcrumb">
import { reactive, computed, onMounted } from 'vue';
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router';
import { Local } from '/@/utils/storage';
import other from '/@/utils/other';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { useRoutesList } from '/@/stores/routesList';
// 定义变量内容
const stores = useRoutesList();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { routesList } = storeToRefs(stores);
const route = useRoute();
const router = useRouter();
const state = reactive<BreadcrumbState>({
breadcrumbList: [],
routeSplit: [],
routeSplitFirst: '',
routeSplitIndex: 1,
});
// 动态设置经典、横向布局不显示
const isShowBreadcrumb = computed(() => {
initRouteSplit(route.path);
const { layout, isBreadcrumb } = themeConfig.value;
if (layout === 'classic' || layout === 'transverse') return false;
else return isBreadcrumb ? true : false;
});
// 面包屑点击时
const onBreadcrumbClick = (v: RouteItem) => {
const { redirect, path } = v;
if (redirect) router.push(redirect);
else router.push(path);
};
// 展开/收起左侧菜单点击
const onThemeConfigChange = () => {
themeConfig.value.isCollapse = !themeConfig.value.isCollapse;
setLocalThemeConfig();
};
// 存储布局配置
const setLocalThemeConfig = () => {
Local.remove('themeConfig');
Local.set('themeConfig', themeConfig.value);
};
// 处理面包屑数据
const getBreadcrumbList = (arr: RouteItems) => {
arr.forEach((item: RouteItem) => {
state.routeSplit.forEach((v: string, k: number, arrs: string[]) => {
if (state.routeSplitFirst === item.path) {
state.routeSplitFirst += `/${arrs[state.routeSplitIndex]}`;
state.breadcrumbList.push(item);
state.routeSplitIndex++;
if (item.children) getBreadcrumbList(item.children);
<script>
import { Local } from '@/utils/storage.js';
export default {
name: 'layoutBreadcrumb',
data() {
return {
breadcrumbList: [],
routeSplit: [],
routeSplitFirst: '',
routeSplitIndex: 1,
};
},
computed: {
// 获取布局配置信息
getThemeConfig() {
return this.$store.state.themeConfig.themeConfig;
},
// 动态设置经典、横向布局不显示
isShowBreadcrumb() {
const { layout, isBreadcrumb } = this.$store.state.themeConfig.themeConfig;
if (layout === 'classic' || layout === 'transverse') {
return 'none';
} else {
return isBreadcrumb ? '' : 'none';
}
});
});
},
},
mounted() {
this.initRouteSplit(this.$route.path);
},
methods: {
// breadcrumb 当前项点击时
onBreadcrumbClick(v) {
const { redirect, path } = v;
if (redirect) this.$router.push(redirect);
else this.$router.push(path);
},
// breadcrumb icon 点击菜单展开与收起
onThemeConfigChange() {
this.$store.state.themeConfig.themeConfig.isCollapse = !this.$store.state.themeConfig.themeConfig.isCollapse;
this.setLocalThemeConfig();
},
// 存储布局配置
setLocalThemeConfig() {
Local.remove('themeConfigPrev');
Local.set('themeConfigPrev', this.$store.state.themeConfig.themeConfig);
},
// 递归设置 breadcrumb
getBreadcrumbList(arr) {
arr.map((item) => {
this.routeSplit.map((v, k, arrs) => {
if (this.routeSplitFirst === item.path) {
this.routeSplitFirst += `/${arrs[this.routeSplitIndex]}`;
this.breadcrumbList.push(item);
this.routeSplitIndex++;
if (item.children) this.getBreadcrumbList(item.children);
}
});
});
},
// 当前路由分割处理
initRouteSplit(path) {
this.breadcrumbList = [
{
path: '/',
meta: {
title: this.$store.state.routesList.routesList[0].meta.title,
icon: this.$store.state.routesList.routesList[0].meta.icon,
},
},
];
this.routeSplit = path.split('/');
this.routeSplit.shift();
this.routeSplitFirst = `/${this.routeSplit[0]}`;
this.routeSplitIndex = 1;
this.getBreadcrumbList(this.$store.state.routesList.routesList);
},
},
// 监听路由的变化
watch: {
$route: {
handler(newVal) {
this.initRouteSplit(newVal.path);
},
deep: true,
},
},
};
// 当前路由字符串切割成数组,并删除第一项空内容
const initRouteSplit = (path: string) => {
if (!themeConfig.value.isBreadcrumb) return false;
state.breadcrumbList = [routesList.value[0]];
state.routeSplit = path.split('/');
state.routeSplit.shift();
state.routeSplitFirst = `/${state.routeSplit[0]}`;
state.routeSplitIndex = 1;
getBreadcrumbList(routesList.value);
if (route.name === 'home' || (route.name === 'notFound' && state.breadcrumbList.length > 1)) state.breadcrumbList.shift();
if (state.breadcrumbList.length > 0)
state.breadcrumbList[state.breadcrumbList.length - 1].meta.tagsViewName = other.setTagsViewNameI18n(<RouteToFrom>route);
};
// 页面加载时
onMounted(() => {
initRouteSplit(route.path);
});
// 路由更新时
onBeforeRouteUpdate((to) => {
initRouteSplit(to.path);
});
</script>
<style scoped lang="scss">
@ -111,36 +116,24 @@ onBeforeRouteUpdate((to) => {
height: inherit;
display: flex;
align-items: center;
padding-left: 15px;
.layout-navbars-breadcrumb-icon {
cursor: pointer;
font-size: 18px;
color: var(--next-bg-topBarColor);
height: 100%;
width: 40px;
margin-right: 15px;
color: var(--prev-bg-topBarColor);
opacity: 0.8;
&:hover {
opacity: 1;
}
}
.layout-navbars-breadcrumb-span {
display: flex;
opacity: 0.7;
color: var(--next-bg-topBarColor);
color: var(--prev-bg-topBarColor);
}
.layout-navbars-breadcrumb-iconfont {
font-size: 14px;
margin-right: 5px;
}
:deep(.el-breadcrumb__separator) {
opacity: 0.7;
color: var(--next-bg-topBarColor);
}
:deep(.el-breadcrumb__inner a, .el-breadcrumb__inner.is-link) {
font-weight: unset !important;
color: var(--next-bg-topBarColor);
&:hover {
color: var(--el-color-primary) !important;
}
}
}
</style>

View File

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

View File

@ -2,98 +2,66 @@
<div class="layout-navbars-breadcrumb-index">
<Logo v-if="setIsShowLogo" />
<Breadcrumb />
<Horizontal :menuList="state.menuList" v-if="isLayoutTransverse" />
<Horizontal :menuList="menuList" v-if="isLayoutTransverse" />
<User />
</div>
</template>
<script setup lang="ts" name="layoutBreadcrumbIndex">
import { defineAsyncComponent, computed, reactive, onMounted, onUnmounted } from 'vue';
import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig';
import mittBus from '/@/utils/mitt';
// 引入组件
const Breadcrumb = defineAsyncComponent(() => import('/@/layout/navBars/topBar/breadcrumb.vue'));
const User = defineAsyncComponent(() => import('/@/layout/navBars/topBar/user.vue'));
const Logo = defineAsyncComponent(() => import('/@/layout/logo/index.vue'));
const Horizontal = defineAsyncComponent(() => import('/@/layout/navMenu/horizontal.vue'));
// 定义变量内容
const stores = useRoutesList();
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { routesList } = storeToRefs(stores);
const route = useRoute();
const state = reactive({
menuList: [] as RouteItems,
});
// 设置 logo 显示/隐藏
const setIsShowLogo = computed(() => {
let { isShowLogo, layout } = themeConfig.value;
return (isShowLogo && layout === 'classic') || (isShowLogo && layout === 'transverse');
});
// 设置是否显示横向导航菜单
const isLayoutTransverse = computed(() => {
let { layout, isClassicSplitMenu } = themeConfig.value;
return layout === 'transverse' || (isClassicSplitMenu && layout === 'classic');
});
// 设置/过滤路由(非静态路由/是否显示在菜单中)
const setFilterRoutes = () => {
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
state.menuList = delClassicChildren(filterRoutesFun(routesList.value));
const resData = setSendClassicChildren(route.path);
mittBus.emit('setSendClassicChildren', resData);
} else {
state.menuList = filterRoutesFun(routesList.value);
}
<script>
import Breadcrumb from '@/layout/navBars/topBar/breadcrumb.vue';
import User from '@/layout/navBars/topBar/user.vue';
import Logo from '@/layout/logo/index.vue';
import Horizontal from '@/layout/navMenu/horizontal.vue';
export default {
name: 'layoutNavBars',
components: { Breadcrumb, User, Logo, Horizontal },
data() {
return {
menuList: [],
};
},
computed: {
// 设置 logo 是否显示
setIsShowLogo() {
let { isShowLogo, layout } = this.$store.state.themeConfig.themeConfig;
return (isShowLogo && layout === 'classic') || (isShowLogo && layout === 'transverse');
},
// 设置是否显示横向菜单
isLayoutTransverse() {
let { layout, isClassicSplitMenu } = this.$store.state.themeConfig.themeConfig;
return layout === 'transverse' || (isClassicSplitMenu && layout === 'classic');
},
},
mounted() {
this.setFilterRoutes();
},
methods: {
// 设置路由的过滤
setFilterRoutes() {
this.menuList = this.filterRoutesFun(this.$store.state.routesList.routesList);
},
// 设置路由的过滤递归函数
filterRoutesFun(arr) {
return arr
.filter((item) => !item.meta.isHide)
.map((item) => {
item = Object.assign({}, item);
if (item.children) item.children = this.filterRoutesFun(item.children);
return item;
});
},
},
watch: {
// 监听 vuex 数据变化
'$store.state': {
handler(val) {
if (val.routesList.routesList.length === this.menuList.length) return false;
this.setFilterRoutes();
},
deep: true,
},
},
};
// 设置了分割菜单时,删除底下 children
const delClassicChildren = <T extends ChilType>(arr: T[]): T[] => {
arr.map((v: T) => {
if (v.children) delete v.children;
});
return arr;
};
// 路由过滤递归函数
const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
return arr
.filter((item: T) => !item.meta?.isHide)
.map((item: T) => {
item = Object.assign({}, item);
if (item.children) item.children = filterRoutesFun(item.children);
return item;
});
};
// 传送当前子级数据到菜单中
const setSendClassicChildren = (path: string) => {
const currentPathSplit = path.split('/');
let currentData: MittMenu = { children: [] };
filterRoutesFun(routesList.value).map((v: RouteItem, k: number) => {
if (v.path === `/${currentPathSplit[1]}`) {
v['k'] = k;
currentData['item'] = { ...v };
currentData['children'] = [{ ...v }];
if (v.children) currentData['children'] = v.children;
}
});
return currentData;
};
// 页面加载时
onMounted(() => {
setFilterRoutes();
mittBus.on('getBreadcrumbIndexSetFilterRoutes', () => {
setFilterRoutes();
});
});
// 页面卸载时
onUnmounted(() => {
mittBus.off('getBreadcrumbIndexSetFilterRoutes', () => {});
});
</script>
<style scoped lang="scss">
@ -101,7 +69,9 @@ onUnmounted(() => {
height: 50px;
display: flex;
align-items: center;
background: var(--next-bg-topBar);
border-bottom: 1px solid var(--next-border-color-light);
padding-right: 15px;
overflow: hidden;
background: var(--prev-bg-topBar);
border-bottom: 1px solid var(--prev-border-color-lighter);
}
</style>

View File

@ -1,121 +1,98 @@
<template>
<div class="layout-search-dialog">
<el-dialog v-model="state.isShowSearch" destroy-on-close :show-close="false">
<template #footer>
<el-autocomplete
v-model="state.menuQuery"
:fetch-suggestions="menuSearch"
placeholder="菜单搜索:支持中文、路由路径"
ref="layoutMenuAutocompleteRef"
@select="onHandleSelect"
:fit-input-width="true"
>
<template #prefix>
<el-icon class="el-input__icon">
<ele-Search />
</el-icon>
</template>
<template #default="{ item }">
<div>
<SvgIcon :name="item.meta.icon" class="mr5" />
{{ item.meta.title }}
</div>
</template>
</el-autocomplete>
</template>
<el-dialog :visible.sync="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')"
prefix-icon="el-icon-search"
ref="layoutMenuAutocompleteRef"
@select="onHandleSelect"
@blur="onSearchBlur"
>
<template slot-scope="{ item }">
<div><i :class="item.meta.icon" class="mr10"></i>{{ $t(item.meta.title) }}</div>
</template>
</el-autocomplete>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="layoutBreadcrumbSearch">
import { reactive, ref, nextTick } from 'vue';
import { useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useTagsViewRoutes } from '/@/stores/tagsViewRoutes';
// 定义变量内容
const storesTagsViewRoutes = useTagsViewRoutes();
const { tagsViewRoutes } = storeToRefs(storesTagsViewRoutes);
const layoutMenuAutocompleteRef = ref();
const router = useRouter();
const state = reactive<SearchState>({
isShowSearch: false,
menuQuery: '',
tagsViewList: [],
});
// 搜索弹窗打开
const openSearch = () => {
state.menuQuery = '';
state.isShowSearch = true;
initTageView();
nextTick(() => {
setTimeout(() => {
layoutMenuAutocompleteRef.value.focus();
});
});
<script>
export default {
name: 'layoutBreadcrumbSearch',
data() {
return {
isShowSearch: false,
menuQuery: '',
tagsViewList: [],
};
},
methods: {
// 搜索弹窗打开
openSearch() {
this.menuQuery = '';
this.isShowSearch = true;
this.initTageView();
this.$nextTick(() => {
this.$refs.layoutMenuAutocompleteRef.focus();
});
},
// 搜索弹窗关闭
closeSearch() {
setTimeout(() => {
this.isShowSearch = false;
}, 150);
},
// 菜单搜索数据过滤
menuSearch(queryString, cb) {
let results = queryString ? this.tagsViewList.filter(this.createFilter(queryString)) : this.tagsViewList;
cb(results);
},
// 菜单搜索过滤
createFilter(queryString) {
return (restaurant) => {
return (
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
restaurant.meta.title.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
this.$t(restaurant.meta.title).toLowerCase().indexOf(queryString.toLowerCase()) > -1
);
};
},
// 初始化菜单数据
initTageView() {
if (this.tagsViewList.length > 0) return false;
this.$store.state.tagsViewRoutes.tagsViewRoutes.map((v) => {
if (!v.meta.isHide) this.tagsViewList.push({ ...v });
});
},
// 当前菜单选中时
onHandleSelect(item) {
let { path, redirect } = item;
if (item.meta.isLink && !item.meta.isIframe) window.open(item.meta.isLink);
else if (redirect) this.$router.push(redirect);
else this.$router.push(path);
this.closeSearch();
},
// input 失去焦点时
onSearchBlur() {
this.closeSearch();
},
},
};
// 搜索弹窗关闭
const closeSearch = () => {
state.isShowSearch = false;
};
// 菜单搜索数据过滤
const menuSearch = (queryString: string, cb: Function) => {
let results = queryString ? state.tagsViewList.filter(createFilter(queryString)) : state.tagsViewList;
cb(results);
};
// 菜单搜索过滤
const createFilter = (queryString: string) => {
return (restaurant: RouteItem) => {
return (
restaurant.path.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
restaurant.meta!.title!.toLowerCase().indexOf(queryString.toLowerCase()) > -1 ||
restaurant.meta!.title!.indexOf(queryString.toLowerCase()) > -1
);
};
};
// 初始化菜单数据
const initTageView = () => {
if (state.tagsViewList.length > 0) return false;
tagsViewRoutes.value.map((v: RouteItem) => {
if (!v.meta?.isHide) state.tagsViewList.push({ ...v });
});
};
// 当前菜单选中时
const onHandleSelect = (item: RouteItem) => {
let { path, redirect } = item;
if (item.meta?.isLink && !item.meta?.isIframe) window.open(item.meta?.isLink);
else if (redirect) router.push(redirect);
else router.push(path);
closeSearch();
};
// 暴露变量
defineExpose({
openSearch,
});
</script>
<style scoped lang="scss">
.layout-search-dialog {
position: relative;
:deep(.el-dialog) {
.el-dialog__header,
.el-dialog__body {
display: none;
}
.el-dialog__footer {
width: 100%;
position: absolute;
left: 50%;
transform: translateX(-50%);
top: -53vh;
}
::v-deep .el-dialog {
box-shadow: unset !important;
border-radius: 0 !important;
background: rgba(0, 0, 0, 0.5);
}
:deep(.el-autocomplete) {
::v-deep .el-autocomplete {
width: 560px;
position: absolute;
top: 150px;
top: 100px;
left: 50%;
transform: translateX(-50%);
}

View File

@ -1,331 +1,148 @@
<template>
<div class="layout-breadcrumb-seting">
<el-drawer title="布局配置" v-model="getThemeConfig.isDrawer" direction="rtl" destroy-on-close size="260px" @close="onDrawerClose">
<el-drawer
:title="$t('message.layout.configTitle')"
:visible.sync="getThemeConfig.isDrawer"
direction="rtl"
destroy-on-close
size="240px"
@close="onDrawerClose"
>
<el-scrollbar class="layout-breadcrumb-seting-bar">
<!-- 全局主题 -->
<el-divider content-position="left">全局主题</el-divider>
<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>
<el-color-picker v-model="getThemeConfig.primary" size="small" @change="onColorPickerChange"> </el-color-picker>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">深色模式</div>
<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">顶栏设置</el-divider>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">顶栏背景</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">顶栏默认字体颜色</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">顶栏背景渐变</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">菜单设置</el-divider>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">菜单背景</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">菜单默认字体颜色</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker v-model="getThemeConfig.menuBarColor" size="default" @change="onBgColorPickerChange('menuBarColor')"> </el-color-picker>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex">
<div class="layout-breadcrumb-seting-bar-flex-label">菜单高亮背景色</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-color-picker
v-model="getThemeConfig.menuBarActiveColor"
size="default"
show-alpha
@change="onBgColorPickerChange('menuBarActiveColor')"
/>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt14">
<div class="layout-breadcrumb-seting-bar-flex-label">菜单背景渐变</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 }">分栏设置</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">分栏菜单背景</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">分栏菜单默认字体颜色</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">分栏菜单背景渐变</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch
v-model="getThemeConfig.isColumnsMenuBarColorGradual"
size="small"
@change="onColumnsMenuBarGradualChange"
:disabled="getThemeConfig.layout !== 'columns'"
></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt14" :style="{ opacity: getThemeConfig.layout !== 'columns' ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex-label">分栏菜单鼠标悬停预加载</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch
v-model="getThemeConfig.isColumnsMenuHoverPreload"
size="small"
@change="onColumnsMenuHoverPreloadChange"
:disabled="getThemeConfig.layout !== 'columns'"
></el-switch>
<el-switch v-model="getThemeConfig.isIsDark" :width="35" @change="onAddDarkChange"></el-switch>
</div>
</div>
<!-- 界面设置 -->
<el-divider content-position="left">界面设置</el-divider>
<div class="layout-breadcrumb-seting-bar-flex" :style="{ opacity: getThemeConfig.layout === 'transverse' ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex-label">菜单水平折叠</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"
:disabled="getThemeConfig.layout === 'transverse'"
size="small"
@change="onThemeConfigChange"
></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: getThemeConfig.layout === 'transverse' ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex-label">菜单手风琴</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch
v-model="getThemeConfig.isUniqueOpened"
:disabled="getThemeConfig.layout === 'transverse'"
size="small"
@change="setLocalThemeConfig"
></el-switch>
<el-switch v-model="getThemeConfig.isCollapse" :width="35" @change="setLocalThemeConfig"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">固定 Header</div>
<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.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">经典布局分割菜单</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>
<el-switch v-model="getThemeConfig.isUniqueOpened" :width="35" @change="setLocalThemeConfig"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">开启锁屏</div>
<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.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">自动锁屏(s/)</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>
<el-switch v-model="getThemeConfig.isFixedHeader" :width="35" @change="setLocalThemeConfig"></el-switch>
</div>
</div>
<!-- 界面显示 -->
<el-divider content-position="left">界面显示</el-divider>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">侧边栏 Logo</div>
<el-divider content-position="left">{{ $t('message.layout.fourTitle') }}</el-divider>
<div class="layout-breadcrumb-seting-bar-flex">
<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>
<el-switch v-model="getThemeConfig.isShowLogo" :width="35" @change="setLocalThemeConfig"></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">开启 Breadcrumb</div>
<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"
:width="35"
@change="setLocalThemeConfig"
></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">开启 Breadcrumb 图标</div>
<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>
<el-switch v-model="getThemeConfig.isBreadcrumbIcon" :width="35" @change="setLocalThemeConfig"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">开启 Tagsview</div>
<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>
<el-switch v-model="getThemeConfig.isTagsview" :width="35" @change="setLocalThemeConfig"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">开启 Tagsview 图标</div>
<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>
<el-switch v-model="getThemeConfig.isTagsviewIcon" :width="35" @change="setLocalThemeConfig"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">开启 TagsView 缓存</div>
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('message.layout.fourIsCacheTagsView') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch v-model="getThemeConfig.isCacheTagsView" size="small" @change="setLocalThemeConfig"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15" :style="{ opacity: state.isMobile ? 0.5 : 1 }">
<div class="layout-breadcrumb-seting-bar-flex-label">开启 TagsView 拖拽</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-switch
v-model="getThemeConfig.isSortableTagsView"
:disabled="state.isMobile ? true : false"
size="small"
@change="onSortableTagsViewChange"
></el-switch>
<el-switch v-model="getThemeConfig.isCacheTagsView" :width="35" @change="setLocalThemeConfig"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">开启 TagsView 共用</div>
<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.isShareTagsView" size="small" @change="onShareTagsViewChange"></el-switch>
<el-switch v-model="getThemeConfig.isFooter" :width="35" @change="setLocalThemeConfig"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">开启 Footer</div>
<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.isFooter" size="small" @change="setLocalThemeConfig"></el-switch>
<el-switch v-model="getThemeConfig.isGrayscale" :width="35" @change="onAddFilterChange('grayscale')"></el-switch>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<div class="layout-breadcrumb-seting-bar-flex-label">灰色模式</div>
<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.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">色弱模式</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">开启水印</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">水印文案</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-input v-model="getThemeConfig.wartermarkText" size="default" style="width: 90px" @input="onWartermarkTextInput($event)"></el-input>
<el-switch v-model="getThemeConfig.isInvert" :width="35" @change="onAddFilterChange('invert')"></el-switch>
</div>
</div>
<!-- 其它设置 -->
<el-divider content-position="left">其它设置</el-divider>
<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">Tagsview 风格</div>
<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-select v-model="getThemeConfig.tagsStyle" placeholder="请选择" size="mini" 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">主页面切换动画</div>
<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-select v-model="getThemeConfig.animation" placeholder="请选择" size="mini" 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">分栏高亮风格</div>
<div class="layout-breadcrumb-seting-bar-flex mt15">
<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-select v-model="getThemeConfig.columnsAsideStyle" placeholder="请选择" size="mini" style="width: 90px" @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">分栏布局风格</div>
<div class="layout-breadcrumb-seting-bar-flex mt15 mb28">
<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-select v-model="getThemeConfig.columnsAsideLayout" placeholder="请选择" size="mini" style="width: 90px" @change="setLocalThemeConfig">
<el-option label="水平" value="columns-horizontal"></el-option>
<el-option label="垂直" value="columns-vertical"></el-option>
</el-select>
@ -333,7 +150,7 @@
</div>
<!-- 布局切换 -->
<el-divider content-position="left">布局切换</el-divider>
<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')">
@ -346,7 +163,7 @@
</section>
<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': getThemeConfig.layout === 'defaults' }">
<div class="layout-tips-box">
<p class="layout-tips-txt">默认</p>
<p class="layout-tips-txt">{{ $t('message.layout.sixDefaults') }}</p>
</div>
</div>
</div>
@ -363,7 +180,7 @@
</section>
<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': getThemeConfig.layout === 'classic' }">
<div class="layout-tips-box">
<p class="layout-tips-txt">经典</p>
<p class="layout-tips-txt">{{ $t('message.layout.sixClassic') }}</p>
</div>
</div>
</div>
@ -379,7 +196,7 @@
</section>
<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': getThemeConfig.layout === 'transverse' }">
<div class="layout-tips-box">
<p class="layout-tips-txt">横向</p>
<p class="layout-tips-txt">{{ $t('message.layout.sixTransverse') }}</p>
</div>
</div>
</div>
@ -395,24 +212,24 @@
</section>
<div class="layout-tips-warp" :class="{ 'layout-tips-warp-active': getThemeConfig.layout === 'columns' }">
<div class="layout-tips-box">
<p class="layout-tips-txt">分栏</p>
<p class="layout-tips-txt">{{ $t('message.layout.sixColumns') }}</p>
</div>
</div>
</div>
</div>
<div class="copy-config">
<el-alert title="点击下方按钮,复制布局配置去 `src/store/modules/themeConfig.ts` 中修改。" 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>
一键复制配置
<el-alert :title="$t('message.layout.tipText')" type="warning" :closable="false"> </el-alert>
<el-button
size="small"
class="copy-config-btn"
icon="el-icon-document-copy"
type="primary"
ref="copyConfigBtnRef"
@click="onCopyConfigClick"
>{{ $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>
一键恢复默认
<el-button size="small" class="copy-config-btn-reset" type="info" icon="el-icon-refresh-right" @click="onResetConfigClick">
{{ $t('message.layout.resetText') }}
</el-button>
</div>
</el-scrollbar>
@ -420,272 +237,180 @@
</div>
</template>
<script setup lang="ts" name="layoutBreadcrumbSeting">
import { nextTick, onUnmounted, onMounted, computed, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { useChangeColor } from '/@/utils/theme';
import { verifyAndSpace } from '/@/utils/toolsValidate';
import { Local } from '/@/utils/storage';
import Watermark from '/@/utils/watermark';
import commonFunction from '/@/utils/commonFunction';
import other from '/@/utils/other';
import mittBus from '/@/utils/mitt';
// 定义变量内容
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const { copyText } = commonFunction();
const { getLightColor, getDarkColor } = useChangeColor();
const state = reactive({
isMobile: false,
});
// 获取布局配置信息
const getThemeConfig = computed(() => {
return themeConfig.value;
});
// 1、全局主题
const onColorPickerChange = () => {
if (!getThemeConfig.value.primary) return ElMessage.warning('全局主题 primary 颜色值不能为空');
// 颜色加深
document.documentElement.style.setProperty('--el-color-primary-dark-2', `${getDarkColor(getThemeConfig.value.primary, 0.1)}`);
document.documentElement.style.setProperty('--el-color-primary', getThemeConfig.value.primary);
// 颜色变浅
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(getThemeConfig.value.primary, i / 10)}`);
}
setDispatchThemeConfig();
};
// 2、菜单 / 顶栏
const onBgColorPickerChange = (bg: string) => {
document.documentElement.style.setProperty(`--next-bg-${bg}`, themeConfig.value[bg]);
if (bg === 'menuBar') {
document.documentElement.style.setProperty(`--next-bg-menuBar-light-1`, getLightColor(getThemeConfig.value.menuBar, 0.05));
}
onTopBarGradualChange();
onMenuBarGradualChange();
onColumnsMenuBarGradualChange();
setDispatchThemeConfig();
};
// 2、菜单 / 顶栏 --> 顶栏背景渐变
const onTopBarGradualChange = () => {
setGraduaFun('.layout-navbars-breadcrumb-index', getThemeConfig.value.isTopBarColorGradual, getThemeConfig.value.topBar);
};
// 2、菜单 / 顶栏 --> 菜单背景渐变
const onMenuBarGradualChange = () => {
setGraduaFun('.layout-container .el-aside', getThemeConfig.value.isMenuBarColorGradual, getThemeConfig.value.menuBar);
};
// 2、菜单 / 顶栏 --> 分栏菜单背景渐变
const onColumnsMenuBarGradualChange = () => {
setGraduaFun('.layout-container .layout-columns-aside', getThemeConfig.value.isColumnsMenuBarColorGradual, getThemeConfig.value.columnsMenuBar);
};
// 2、菜单 / 顶栏 --> 背景渐变函数
const setGraduaFun = (el: string, bool: boolean, color: string) => {
nextTick(() => {
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 , ${color}, ${getLightColor(color, 0.5)})`);
else els.setAttribute('style', ``);
setLocalThemeConfig();
}, 300);
});
};
// 2、分栏设置 ->
const onColumnsMenuHoverPreloadChange = () => {
setLocalThemeConfig();
};
// 3、界面设置 --> 菜单水平折叠
const onThemeConfigChange = () => {
setDispatchThemeConfig();
};
// 3、界面设置 --> 固定 Header
const onIsFixedHeaderChange = () => {
getThemeConfig.value.isFixedHeaderChange = getThemeConfig.value.isFixedHeader ? false : true;
setLocalThemeConfig();
};
// 3、界面设置 --> 经典布局分割菜单
const onClassicSplitMenuChange = () => {
getThemeConfig.value.isBreadcrumb = false;
setLocalThemeConfig();
mittBus.emit('getBreadcrumbIndexSetFilterRoutes');
};
// 4、界面显示 --> 侧边栏 Logo
const onIsShowLogoChange = () => {
getThemeConfig.value.isShowLogoChange = getThemeConfig.value.isShowLogo ? false : true;
setLocalThemeConfig();
};
// 4、界面显示 --> 面包屑 Breadcrumb
const onIsBreadcrumbChange = () => {
if (getThemeConfig.value.layout === 'classic') {
getThemeConfig.value.isClassicSplitMenu = false;
}
setLocalThemeConfig();
};
// 4、界面显示 --> 开启 TagsView 拖拽
const onSortableTagsViewChange = () => {
mittBus.emit('openOrCloseSortable');
setLocalThemeConfig();
};
// 4、界面显示 --> 开启 TagsView 共用
const onShareTagsViewChange = () => {
mittBus.emit('openShareTagsView');
setLocalThemeConfig();
};
// 4、界面显示 --> 灰色模式/色弱模式
const onAddFilterChange = (attr: string) => {
if (attr === 'grayscale') {
if (getThemeConfig.value.isGrayscale) getThemeConfig.value.isInvert = false;
} else {
if (getThemeConfig.value.isInvert) getThemeConfig.value.isGrayscale = false;
}
const cssAttr =
attr === 'grayscale' ? `grayscale(${getThemeConfig.value.isGrayscale ? 1 : 0})` : `invert(${getThemeConfig.value.isInvert ? '80%' : '0%'})`;
const appEle = document.body;
appEle.setAttribute('style', `filter: ${cssAttr}`);
setLocalThemeConfig();
};
// 4、界面显示 --> 深色模式
const onAddDarkChange = () => {
const body = document.documentElement as HTMLElement;
if (getThemeConfig.value.isIsDark) body.setAttribute('data-theme', 'dark');
else body.setAttribute('data-theme', '');
};
// 4、界面显示 --> 开启水印
const onWartermarkChange = () => {
getThemeConfig.value.isWartermark ? Watermark.set(getThemeConfig.value.wartermarkText) : Watermark.del();
setLocalThemeConfig();
};
// 4、界面显示 --> 水印文案
const onWartermarkTextInput = (val: string) => {
getThemeConfig.value.wartermarkText = verifyAndSpace(val);
if (getThemeConfig.value.wartermarkText === '') return false;
if (getThemeConfig.value.isWartermark) Watermark.set(getThemeConfig.value.wartermarkText);
setLocalThemeConfig();
};
// 5、布局切换
const onSetLayout = (layout: string) => {
Local.set('oldLayout', layout);
if (getThemeConfig.value.layout === layout) return false;
if (layout === 'transverse') getThemeConfig.value.isCollapse = false;
getThemeConfig.value.layout = layout;
getThemeConfig.value.isDrawer = false;
initLayoutChangeFun();
};
// 设置布局切换函数
const initLayoutChangeFun = () => {
onBgColorPickerChange('menuBar');
onBgColorPickerChange('menuBarColor');
onBgColorPickerChange('menuBarActiveColor');
onBgColorPickerChange('topBar');
onBgColorPickerChange('topBarColor');
onBgColorPickerChange('columnsMenuBar');
onBgColorPickerChange('columnsMenuBarColor');
};
// 关闭弹窗时,初始化变量。变量用于处理 layoutScrollbarRef.value.update() 更新滚动条高度
const onDrawerClose = () => {
getThemeConfig.value.isFixedHeaderChange = false;
getThemeConfig.value.isShowLogoChange = false;
getThemeConfig.value.isDrawer = false;
setLocalThemeConfig();
};
// 布局配置弹窗打开
const openDrawer = () => {
getThemeConfig.value.isDrawer = true;
};
// 触发 store 布局配置更新
const setDispatchThemeConfig = () => {
setLocalThemeConfig();
setLocalThemeConfigStyle();
};
// 存储布局配置
const setLocalThemeConfig = () => {
Local.remove('themeConfig');
Local.set('themeConfig', getThemeConfig.value);
};
// 存储布局配置全局主题样式html根标签
const setLocalThemeConfigStyle = () => {
Local.set('themeConfigStyle', document.documentElement.style.cssText);
};
// 一键复制配置
const onCopyConfigClick = () => {
let copyThemeConfig = Local.get('themeConfig');
copyThemeConfig.isDrawer = false;
copyText(JSON.stringify(copyThemeConfig)).then(() => {
getThemeConfig.value.isDrawer = false;
});
};
// 一键恢复默认
const onResetConfigClick = () => {
Local.clear();
window.location.reload();
// @ts-ignore
Local.set('version', __NEXT_VERSION__);
};
// 初始化菜单样式等
const initSetStyle = () => {
// 2、菜单 / 顶栏 --> 顶栏背景渐变
onTopBarGradualChange();
// 2、菜单 / 顶栏 --> 菜单背景渐变
onMenuBarGradualChange();
// 2、菜单 / 顶栏 --> 分栏菜单背景渐变
onColumnsMenuBarGradualChange();
};
onMounted(() => {
nextTick(() => {
<script>
import ClipboardJS from 'clipboard';
import { Local } from '@/utils/storage.js';
import { useChangeColor } from '@/utils/theme.js';
import config from '/package.json';
export default {
name: 'layoutBreadcrumbSeting',
computed: {
// 获取布局配置信息
getThemeConfig() {
return this.$store.state.themeConfig.themeConfig;
},
},
created() {
// 判断当前布局是否不相同不相同则初始化当前布局的样式防止监听窗口大小改变时布局配置logo、菜单背景等部分布局失效问题
if (!Local.get('frequency')) initLayoutChangeFun();
if (!Local.get('frequency')) this.initSetLayoutChange();
Local.set('frequency', 1);
// 监听窗口大小改变,非默认布局,设置成默认布局(适配移动端)
mittBus.on('layoutMobileResize', (res: LayoutMobileResize) => {
getThemeConfig.value.layout = res.layout;
getThemeConfig.value.isDrawer = false;
initLayoutChangeFun();
state.isMobile = other.isMobile();
this.bus.$on('layoutMobileResize', (res) => {
if (this.$store.state.themeConfig.themeConfig.layout === res.layout) return false;
this.$store.state.themeConfig.themeConfig.layout = res.layout;
this.$store.state.themeConfig.themeConfig.isDrawer = false;
this.$store.state.themeConfig.themeConfig.isCollapse = false;
this.initSetLayoutChange();
});
setTimeout(() => {
// 默认样式
onColorPickerChange();
// 灰色模式
if (getThemeConfig.value.isGrayscale) onAddFilterChange('grayscale');
// 色弱模式
if (getThemeConfig.value.isInvert) onAddFilterChange('invert');
// 深色模式
if (getThemeConfig.value.isIsDark) onAddDarkChange();
// 开启水印
onWartermarkChange();
// 初始化菜单样式等
initSetStyle();
}, 100);
});
});
onUnmounted(() => {
mittBus.off('layoutMobileResize', () => {});
});
// 暴露变量
defineExpose({
openDrawer,
});
},
mounted() {
this.initLayoutConfig();
},
methods: {
// 全局主题
onColorPickerChange() {
if (!this.getThemeConfig.primary) return;
// 颜色加深
document.documentElement.style.setProperty('--prev-color-primary', this.getThemeConfig.primary);
// 颜色变浅
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(
`--prev-color-primary-light-${i}`,
`${useChangeColor().getLightColor(this.getThemeConfig.primary, i / 10)}`
);
}
this.setLocalThemeConfig();
},
// 深色模式
onAddDarkChange() {
const body = document.documentElement;
if (this.getThemeConfig.isIsDark) body.setAttribute('data-theme', 'dark');
else body.setAttribute('data-theme', '');
this.setLocalThemeConfig();
},
// 初始化:刷新页面时,设置了值,直接取缓存中的值进行初始化
initLayoutConfig() {
window.addEventListener('load', () => {
// 默认样式
this.onColorPickerChange();
// 灰色模式
if (this.$store.state.themeConfig.themeConfig.isGrayscale) this.onAddFilterChange('grayscale');
// 色弱模式
if (this.$store.state.themeConfig.themeConfig.isInvert) this.onAddFilterChange('invert');
// 深色模式
if (this.$store.state.themeConfig.themeConfig.isIsDark) this.onAddDarkChange();
// 语言国际化
if (Local.get('themeConfigPrev')) this.$i18n.locale = Local.get('themeConfigPrev').globalI18n;
});
},
// 存储布局配置
setLocalThemeConfig() {
Local.remove('themeConfigPrev');
Local.set('themeConfigPrev', this.$store.state.themeConfig.themeConfig);
this.setLocalThemeConfigStyle();
},
// 存储布局配置全局主题样式html根标签
setLocalThemeConfigStyle() {
Local.set('themeConfigStyle', document.documentElement.style.cssText);
},
// 布局配置弹窗打开
openDrawer() {
this.$store.state.themeConfig.themeConfig.isDrawer = true;
},
// 关闭弹窗时,初始化变量
onDrawerClose() {
this.$store.state.themeConfig.themeConfig.isDrawer = false;
this.setLocalThemeConfig();
},
// 灰色模式/色弱模式
onAddFilterChange(attr) {
if (attr === 'grayscale') {
if (this.$store.state.themeConfig.themeConfig.isGrayscale) this.$store.state.themeConfig.themeConfig.isInvert = false;
} else {
if (this.$store.state.themeConfig.themeConfig.isInvert) this.$store.state.themeConfig.themeConfig.isGrayscale = false;
}
const cssAttr =
attr === 'grayscale'
? `grayscale(${this.$store.state.themeConfig.themeConfig.isGrayscale ? 1 : 0})`
: `invert(${this.$store.state.themeConfig.themeConfig.isInvert ? '80%' : '0%'})`;
const appEle = document.body;
appEle.setAttribute('style', `filter: ${cssAttr};`);
this.setLocalThemeConfig();
},
// 布局切换
onSetLayout(layout) {
Local.set('oldLayout', layout);
if (this.$store.state.themeConfig.themeConfig.layout === layout) return false;
this.$store.state.themeConfig.themeConfig.layout = layout;
this.$store.state.themeConfig.themeConfig.isDrawer = false;
this.initSetLayoutChange();
},
// 设置布局切换,重置主题样式
initSetLayoutChange() {
if (this.$store.state.themeConfig.themeConfig.layout === 'classic') {
this.onBgColorPickerChange('menuBar', '#ffffff');
this.onBgColorPickerChange('menuBarColor', '#606266');
this.onBgColorPickerChange('topBar', '#ffffff');
this.onBgColorPickerChange('topBarColor', '#606266');
} else if (this.$store.state.themeConfig.themeConfig.layout === 'transverse') {
this.onBgColorPickerChange('menuBarColor', '#ffffff');
this.onBgColorPickerChange('topBar', '#545c64');
this.onBgColorPickerChange('topBarColor', '#ffffff');
} else if (this.$store.state.themeConfig.themeConfig.layout === 'columns') {
this.onBgColorPickerChange('menuBar', '#ffffff');
this.onBgColorPickerChange('menuBarColor', '#606266');
this.onBgColorPickerChange('topBar', '#ffffff');
this.onBgColorPickerChange('topBarColor', '#606266');
} else {
this.onBgColorPickerChange('menuBar', '#545c64');
this.onBgColorPickerChange('menuBarColor', '#eaeaea');
this.onBgColorPickerChange('topBar', '#ffffff');
this.onBgColorPickerChange('topBarColor', '#606266');
}
},
// 菜单 / 顶栏背景等
onBgColorPickerChange(bg, rgb) {
document.documentElement.style.setProperty(`--prev-bg-${bg}`, rgb);
this.setLocalThemeConfigStyle();
},
// 一键复制配置
onCopyConfigClick() {
this.$store.state.themeConfig.themeConfig.isDrawer = false;
let clipboardJS = new ClipboardJS('.copy-config-btn', {
text: () => JSON.stringify(this.$store.state.themeConfig.themeConfig),
});
clipboardJS.on('success', () => {
this.$message.success('配置复制成功');
this.isDrawer = false;
clipboardJS.destroy();
});
clipboardJS.on('error', () => {
this.$message.error('配置复制失败');
});
},
// 一键恢复默认
onResetConfigClick() {
Local.clear();
window.location.reload();
Local.set('version', config.version);
},
},
};
</script>
<style scoped lang="scss">
.layout-breadcrumb-seting-bar {
height: calc(100vh - 50px);
padding: 0 15px;
:deep(.el-scrollbar__view) {
::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);
color: var(--prev-color-text-primary);
}
}
.layout-drawer-content-flex {
@ -704,16 +429,16 @@ defineExpose({
.el-container {
height: 100%;
.el-aside-dark {
background-color: var(--next-color-seting-header);
background-color: var(--prev-color-seting-header);
}
.el-aside {
background-color: var(--next-color-seting-aside);
background-color: var(--prev-color-seting-aside);
}
.el-header {
background-color: var(--next-color-seting-header);
background-color: var(--prev-color-seting-header);
}
.el-main {
background-color: var(--next-color-seting-main);
background-color: var(--prev-color-seting-main);
}
}
.el-circular {
@ -724,7 +449,7 @@ defineExpose({
}
.drawer-layout-active {
border: 1px solid;
border-color: var(--el-color-primary);
border-color: var(--prev-color-primary);
}
.layout-tips-warp,
.layout-tips-warp-active {
@ -734,7 +459,7 @@ defineExpose({
top: 50%;
transform: translate(-50%, -50%);
border: 1px solid;
border-color: var(--el-color-primary-light-5);
border-color: var(--prev-color-primary-light-5);
border-radius: 100%;
padding: 4px;
.layout-tips-box {
@ -743,7 +468,7 @@ defineExpose({
height: 30px;
z-index: 9;
border: 1px solid;
border-color: var(--el-color-primary-light-5);
border-color: var(--prev-color-primary-light-5);
border-radius: 100%;
.layout-tips-txt {
transition: inherit;
@ -753,11 +478,11 @@ defineExpose({
line-height: 1;
letter-spacing: 2px;
white-space: nowrap;
color: var(--el-color-primary-light-5);
color: var(--prev-color-primary-light-5);
text-align: center;
transform: rotate(30deg);
left: -1px;
background-color: var(--next-color-seting-main);
background-color: var(--prev-color-seting-main);
width: 32px;
height: 17px;
line-height: 17px;
@ -766,13 +491,13 @@ defineExpose({
}
.layout-tips-warp-active {
border: 1px solid;
border-color: var(--el-color-primary);
border-color: var(--prev-color-primary);
.layout-tips-box {
border: 1px solid;
border-color: var(--el-color-primary);
border-color: var(--prev-color-primary);
.layout-tips-txt {
color: var(--el-color-primary) !important;
background-color: var(--next-color-seting-main) !important;
color: var(--prev-color-primary) !important;
background-color: var(--prev-color-seting-main) !important;
}
}
}
@ -780,18 +505,18 @@ defineExpose({
.el-circular {
transition: all 0.3s ease-in-out;
border: 1px solid;
border-color: var(--el-color-primary);
border-color: var(--prev-color-primary);
}
.layout-tips-warp {
transition: all 0.3s ease-in-out;
border-color: var(--el-color-primary);
border-color: var(--prev-color-primary);
.layout-tips-box {
transition: inherit;
border-color: var(--el-color-primary);
border-color: var(--prev-color-primary);
.layout-tips-txt {
transition: inherit;
color: var(--el-color-primary) !important;
background-color: var(--next-color-seting-main) !important;
color: var(--prev-color-primary) !important;
background-color: var(--prev-color-seting-main) !important;
}
}
}

View File

@ -1,193 +1,220 @@
<template>
<div class="layout-navbars-breadcrumb-user pr15" :style="{ flex: layoutUserFlexNum }">
<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="组件大小"></i>
<i class="iconfont icon-ziti" :title="$t('message.user.title0')"></i>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="large" :disabled="state.disabledSize === 'large'">大型</el-dropdown-item>
<el-dropdown-item command="default" :disabled="state.disabledSize === 'default'">默认</el-dropdown-item>
<el-dropdown-item command="small" :disabled="state.disabledSize === 'small'">小型</el-dropdown-item>
<el-dropdown-item command="" :disabled="disabledSize === ''">{{ $t('message.user.dropdownDefault') }}</el-dropdown-item>
<el-dropdown-item command="medium" :disabled="disabledSize === 'medium'">{{ $t('message.user.dropdownMedium') }}</el-dropdown-item>
<el-dropdown-item command="small" :disabled="disabledSize === 'small'">{{ $t('message.user.dropdownSmall') }}</el-dropdown-item>
<el-dropdown-item command="mini" :disabled="disabledSize === 'mini'">{{ $t('message.user.dropdownMini') }}</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>
<el-dropdown-menu slot="dropdown">
<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>
</el-dropdown>
<div class="layout-navbars-breadcrumb-user-icon" @click="onSearchClick">
<el-icon title="菜单搜索">
<ele-Search />
</el-icon>
<i class="el-icon-search" :title="$t('message.user.title2')"></i>
</div>
<div class="layout-navbars-breadcrumb-user-icon" @click="onLayoutSetingClick">
<i class="icon-skin iconfont" title="布局配置"></i>
<i class="icon-skin iconfont" :title="$t('message.user.title3')"></i>
</div>
<div class="layout-navbars-breadcrumb-user-icon" ref="userNewsBadgeRef" v-click-outside="onUserNewsClick">
<el-badge :is-dot="true">
<el-icon title="消息">
<ele-Bell />
</el-icon>
</el-badge>
<div class="layout-navbars-breadcrumb-user-icon" @click.stop="isShowUserNewsPopover = !isShowUserNewsPopover">
<el-popover placement="bottom" trigger="click" v-model="isShowUserNewsPopover" :width="300" popper-class="el-popover-pupop-user-news">
<el-badge :is-dot="true" slot="reference">
<i class="el-icon-bell" :title="$t('message.user.title4')"></i>
</el-badge>
<transition name="el-zoom-in-top">
<UserNews v-show="isShowUserNewsPopover" />
</transition>
</el-popover>
</div>
<el-popover
ref="userNewsRef"
:virtual-ref="userNewsBadgeRef"
placement="bottom"
trigger="click"
transition="el-zoom-in-top"
virtual-triggering
:width="300"
:persistent="false"
>
<UserNews />
</el-popover>
<div class="layout-navbars-breadcrumb-user-icon mr10" @click="onScreenfullClick">
<i
class="iconfont"
:title="state.isScreenfull ? '关全屏' : '开全屏'"
:class="!state.isScreenfull ? 'icon-fullscreen' : 'icon-tuichuquanping'"
: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">
<el-dropdown :show-timeout="70" :hide-timeout="50" @command="onDropdownCommand">
<span class="layout-navbars-breadcrumb-user-link">
<img :src="userInfos.photo" class="layout-navbars-breadcrumb-user-link-photo mr5" />
{{ userInfos.userName === '' ? 'common' : userInfos.userName }}
<el-icon class="el-icon--right">
<ele-ArrowDown />
</el-icon>
<img :src="getUserInfos.photo" class="layout-navbars-breadcrumb-user-link-photo mr5" />
{{ getUserInfos.userName === '' ? 'test' : getUserInfos.userName }}
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<template #dropdown>
<el-dropdown-menu
><el-dropdown-item command="/home">首页</el-dropdown-item>
<el-dropdown-item command="wareHouse">代码仓库</el-dropdown-item>
<el-dropdown-item command="/404">404</el-dropdown-item>
<el-dropdown-item command="/401">401</el-dropdown-item>
<el-dropdown-item divided command="logOut">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
<el-dropdown-menu slot="dropdown">
<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>
</el-dropdown>
<Search ref="searchRef" />
</div>
</template>
<script setup lang="ts" name="layoutBreadcrumbUser">
import { defineAsyncComponent, ref, unref, computed, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessageBox, ElMessage, ClickOutside as vClickOutside } from 'element-plus';
<script>
import screenfull from 'screenfull';
import { storeToRefs } from 'pinia';
import { useUserInfo } from '/@/stores/userInfo';
import { useThemeConfig } from '/@/stores/themeConfig';
import mittBus from '/@/utils/mitt';
import { Session, Local } from '/@/utils/storage';
// 引入组件
const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/topBar/userNews.vue'));
const Search = defineAsyncComponent(() => import('/@/layout/navBars/topBar/search.vue'));
// 定义变量内容
const userNewsRef = ref();
const userNewsBadgeRef = ref();
const router = useRouter();
const stores = useUserInfo();
const storesThemeConfig = useThemeConfig();
const { userInfos } = storeToRefs(stores);
const { themeConfig } = storeToRefs(storesThemeConfig);
const searchRef = ref();
const state = reactive({
isScreenfull: false,
disabledSize: 'large',
});
// 设置分割样式
const layoutUserFlexNum = computed(() => {
let num: string | number = '';
const { layout, isClassicSplitMenu } = themeConfig.value;
const layoutArr: string[] = ['defaults', 'columns'];
if (layoutArr.includes(layout) || (layout === 'classic' && !isClassicSplitMenu)) num = '1';
else num = '';
return num;
});
// 全屏点击时
const onScreenfullClick = () => {
if (!screenfull.isEnabled) {
ElMessage.warning('暂不不支持全屏');
return false;
}
screenfull.toggle();
screenfull.on('change', () => {
if (screenfull.isFullscreen) state.isScreenfull = true;
else state.isScreenfull = false;
});
import { Session, Local } from '@/utils/storage.js';
import UserNews from '@/layout/navBars/topBar/userNews.vue';
import Search from '@/layout/navBars/topBar/search.vue';
export default {
name: 'layoutBreadcrumbUser',
components: { UserNews, Search },
data() {
return {
isScreenfull: false,
isShowUserNewsPopover: false,
disabledI18n: 'zh-cn',
disabledSize: '',
};
},
computed: {
// 获取用户信息
getUserInfos() {
return this.$store.state.userInfos.userInfos;
},
// 设置弹性盒子布局 flex
layoutUserFlexNum() {
let { layout, isClassicSplitMenu } = this.$store.state.themeConfig.themeConfig;
let num = '';
if (layout === 'defaults' || (layout === 'classic' && !isClassicSplitMenu) || layout === 'columns') num = 1;
else num = null;
return num;
},
},
mounted() {
if (Local.get('themeConfigPrev')) {
this.initI18n();
this.initComponentSize();
}
},
methods: {
// 搜索点击
onSearchClick() {
this.$refs.searchRef.openSearch();
},
// 布局配置点击
onLayoutSetingClick() {
this.bus.$emit('openSetingsDrawer');
},
// 全屏点击
onScreenfullClick() {
if (!screenfull.isEnabled) {
this.$message.warning('暂不不支持全屏');
return false;
}
screenfull.toggle();
screenfull.on('change', () => {
if (screenfull.isFullscreen) this.isScreenfull = true;
else this.isScreenfull = false;
});
// 监听菜单 horizontal.vue 滚动条高度更新
this.bus.$emit('updateElScrollBar');
},
// 组件大小改变
onComponentSizeChange(size) {
Local.remove('themeConfigPrev');
this.$store.state.themeConfig.themeConfig.globalComponentSize = size;
Local.set('themeConfigPrev', this.$store.state.themeConfig.themeConfig);
this.$ELEMENT.size = size;
this.initComponentSize();
window.location.reload();
},
// 语言切换
onLanguageChange(lang) {
Local.remove('themeConfigPrev');
this.$store.state.themeConfig.themeConfig.globalI18n = lang;
Local.set('themeConfigPrev', this.$store.state.themeConfig.themeConfig);
this.$i18n.locale = lang;
this.initI18n();
},
// 初始化言语国际化
initI18n() {
switch (Local.get('themeConfigPrev').globalI18n) {
case 'zh-cn':
this.disabledI18n = 'zh-cn';
break;
case 'en':
this.disabledI18n = 'en';
break;
case 'zh-tw':
this.disabledI18n = 'zh-tw';
break;
}
},
// 初始化全局组件大小
initComponentSize() {
switch (Local.get('themeConfigPrev').globalComponentSize) {
case '':
this.disabledSize = '';
break;
case 'medium':
this.disabledSize = 'medium';
break;
case 'small':
this.disabledSize = 'small';
break;
case 'mini':
this.disabledSize = 'mini';
break;
}
},
// `dropdown 下拉菜单` 当前项点击
onDropdownCommand(path) {
if (path === 'logOut') {
setTimeout(() => {
this.$msgbox({
closeOnClickModal: false,
closeOnPressEscape: false,
title: this.$t('message.user.logOutTitle'),
message: this.$t('message.user.logOutMessage'),
showCancelButton: true,
confirmButtonText: this.$t('message.user.logOutConfirm'),
cancelButtonText: this.$t('message.user.logOutCancel'),
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
instance.confirmButtonText = this.$t('message.user.logOutExit');
setTimeout(() => {
done();
setTimeout(() => {
instance.confirmButtonLoading = false;
}, 300);
}, 700);
} else {
done();
}
},
})
.then(() => {
// 清除缓存/token等
Session.clear();
// 使用 reload 时,不需要调用 resetRoute() 重置路由
window.location.reload();
})
.catch(() => {});
}, 150);
} else if (path === 'wareHouse') {
window.open('https://gitee.com/lyt-top/vue-next-admin');
} else {
this.$router.push(path);
}
},
},
};
// 消息通知点击时
const onUserNewsClick = () => {
unref(userNewsRef).popperRef?.delayHide?.();
};
// 布局配置 icon 点击时
const onLayoutSetingClick = () => {
mittBus.emit('openSetingsDrawer');
};
// 下拉菜单点击时
const onHandleCommandClick = (path: string) => {
if (path === 'logOut') {
ElMessageBox({
closeOnClickModal: false,
closeOnPressEscape: false,
title: '提示',
message: '此操作将退出登录, 是否继续?',
showCancelButton: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
buttonSize: 'default',
beforeClose: (action, instance, done) => {
if (action === 'confirm') {
instance.confirmButtonLoading = true;
instance.confirmButtonText = '退出中';
setTimeout(() => {
done();
setTimeout(() => {
instance.confirmButtonLoading = false;
}, 300);
}, 700);
} else {
done();
}
},
})
.then(async () => {
// 清除缓存/token等
Session.clear();
// 使用 reload 时,不需要调用 resetRoute() 重置路由
window.location.reload();
})
.catch(() => {});
} else if (path === 'wareHouse') {
window.open('https://gitee.com/lyt-top/vue-next-admin');
} else {
router.push(path);
}
};
// 菜单搜索点击
const onSearchClick = () => {
searchRef.value.openSearch();
};
// 组件大小改变
const onComponentSizeChange = (size: string) => {
Local.remove('themeConfig');
themeConfig.value.globalComponentSize = size;
Local.set('themeConfig', themeConfig.value);
initI18nOrSize('globalComponentSize', 'disabledSize');
window.location.reload();
};
// 初始化组件大小/i18n
const initI18nOrSize = (value: string, attr: string) => {
(<any>state)[attr] = Local.get('themeConfig')[value];
};
// 页面加载时
onMounted(() => {
if (Local.get('themeConfig')) {
initI18nOrSize('globalComponentSize', 'disabledSize');
}
});
</script>
<style scoped lang="scss">
@ -209,29 +236,29 @@ onMounted(() => {
&-icon {
padding: 0 10px;
cursor: pointer;
color: var(--next-bg-topBarColor);
color: var(--prev-bg-topBarColor);
height: 50px;
line-height: 50px;
display: flex;
align-items: center;
&:hover {
background: var(--next-color-user-hover);
background: var(--prev-color-hover);
i {
display: inline-block;
animation: logoAnimation 0.3s ease-in-out;
}
}
}
:deep(.el-dropdown) {
color: var(--next-bg-topBarColor);
& ::v-deep .el-dropdown {
color: var(--prev-bg-topBarColor);
}
:deep(.el-badge) {
& ::v-deep .el-badge {
height: 40px;
line-height: 40px;
display: flex;
align-items: center;
}
:deep(.el-badge__content.is-fixed) {
& ::v-deep .el-badge__content.is-fixed {
top: 12px;
}
}

View File

@ -1,12 +1,12 @@
<template>
<div class="layout-navbars-breadcrumb-user-news">
<div class="head-box">
<div class="head-box-title">通知</div>
<div class="head-box-btn" v-if="state.newsList.length > 0" @click="onAllReadClick">全部已读</div>
<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="state.newsList.length > 0">
<div class="content-box-item" v-for="(v, k) in state.newsList" :key="k">
<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 }}
@ -14,38 +14,46 @@
<div class="content-box-time">{{ v.time }}</div>
</div>
</template>
<el-empty description="暂无通知" v-else></el-empty>
<div class="content-box-empty" v-else>
<div class="content-box-empty-margin">
<i class="el-icon-s-promotion"></i>
<div class="mt15">{{ $t('message.user.newDesc') }}</div>
</div>
</div>
</div>
<div class="foot-box" @click="onGoToGiteeClick" v-if="state.newsList.length > 0">前往通知中心</div>
<div class="foot-box" @click="onGoToGiteeClick" v-if="newsList.length > 0">{{ $t('message.user.newGo') }}</div>
</div>
</template>
<script setup lang="ts" name="layoutBreadcrumbUserNews">
import { reactive } from 'vue';
// 定义变量内容
const state = reactive({
newsList: [
{
label: '关于版本发布的通知',
value: 'vue-next-admin基于 vue3 + CompositionAPI + typescript + vite + element plus正式发布时间2021年02月28日!',
time: '2020-12-08',
<script>
export default {
name: 'layoutBreadcrumbUserNews',
data() {
return {
newsList: [
{
label: '关于版本发布的通知',
value: '基于 vue2.x + element ui正式发布时间2020年11月15日!',
time: '2020-11-15',
},
{
label: '关于学习交流的通知',
value: 'QQ群号码 665452019欢迎小伙伴入群学习交流探讨',
time: '2020-11-15',
},
],
};
},
methods: {
// 全部已读点击
onAllReadClick() {
this.newsList = [];
},
{
label: '关于学习交流的通知',
value: 'QQ群号码 665452019欢迎小伙伴入群学习交流探讨',
time: '2020-12-08',
// 前往通知中心点击
onGoToGiteeClick() {
window.open('https://gitee.com/lyt-top/vue-next-admin');
},
],
});
// 全部已读点击
const onAllReadClick = () => {
state.newsList = [];
};
// 前往通知中心点击
const onGoToGiteeClick = () => {
window.open('https://gitee.com/lyt-top/vue-next-admin');
},
};
</script>
@ -53,14 +61,14 @@ const onGoToGiteeClick = () => {
.layout-navbars-breadcrumb-user-news {
.head-box {
display: flex;
border-bottom: 1px solid var(--el-border-color-lighter);
border-bottom: 1px solid var(--prev-border-color-lighter);
box-sizing: border-box;
color: var(--el-text-color-primary);
color: var(--prev-color-text-primary);
justify-content: space-between;
height: 35px;
align-items: center;
.head-box-btn {
color: var(--el-color-primary);
color: var(--prev-color-primary);
font-size: 13px;
cursor: pointer;
opacity: 0.8;
@ -77,30 +85,41 @@ const onGoToGiteeClick = () => {
padding-bottom: 12px;
}
.content-box-msg {
color: var(--el-text-color-secondary);
color: var(--prev-color-text-secondary);
margin-top: 5px;
margin-bottom: 5px;
}
.content-box-time {
color: var(--el-text-color-secondary);
color: var(--prev-color-text-secondary);
}
}
.content-box-empty {
height: 260px;
display: flex;
.content-box-empty-margin {
margin: auto;
text-align: center;
i {
font-size: 60px;
}
}
}
}
.foot-box {
height: 35px;
color: var(--el-color-primary);
color: var(--prev-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);
border-top: 1px solid var(--prev-border-color-lighter);
&:hover {
opacity: 1;
}
}
:deep(.el-empty__description p) {
::v-deep(.el-empty__description p) {
font-size: 13px;
}
}

View File

@ -1,121 +1,117 @@
<template>
<div class="el-menu-horizontal-warp">
<el-menu router :default-active="state.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 }}</span>
<el-scrollbar @wheel.native.prevent="onElMenuHorizontalScroll" ref="elMenuHorizontalScrollRef">
<el-menu router :default-active="defaultActive" background-color="transparent" mode="horizontal" @select="onHorizontalSelect">
<template v-for="val in menuList">
<el-submenu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
<template slot="title">
<i :class="val.meta.icon ? val.meta.icon : ''"></i>
<span>{{ $t(val.meta.title) }}</span>
</template>
<SubItem :chil="val.children" />
</el-submenu>
<template v-else>
<el-menu-item :index="val.path" :key="val.path">
<template slot="title" v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
<i :class="val.meta.icon ? val.meta.icon : ''"></i>
{{ $t(val.meta.title) }}
</template>
<template slot="title" v-else>
<a :href="val.meta.isLink" target="_blank">
<i :class="val.meta.icon ? val.meta.icon : ''"></i>
{{ $t(val.meta.title) }}
</a>
</template>
</el-menu-item>
</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 }}
</template>
<template #title v-else>
<a class="w100" @click.prevent="onALinkClick(val)">
<SvgIcon :name="val.meta.icon" />
{{ val.meta.title }}
</a>
</template>
</el-menu-item>
</template>
</template>
</el-menu>
</el-menu>
</el-scrollbar>
</div>
</template>
<script setup lang="ts" name="navMenuHorizontal">
import { defineAsyncComponent, reactive, computed, onBeforeMount } from 'vue';
import { useRoute, onBeforeRouteUpdate, RouteRecordRaw } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useRoutesList } from '/@/stores/routesList';
import { useThemeConfig } from '/@/stores/themeConfig';
import other from '/@/utils/other';
import mittBus from '/@/utils/mitt';
// 引入组件
const SubItem = defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue'));
// 定义父组件传过来的值
const props = defineProps({
// 菜单列表
menuList: {
type: Array<RouteRecordRaw>,
default: () => [],
<script>
import SubItem from '@/layout/navMenu/subItem.vue';
export default {
name: 'navMenuHorizontal',
components: { SubItem },
props: {
menuList: {
type: Array,
default: () => [],
},
},
data() {
return {
defaultActive: null,
};
},
mounted() {
this.initElMenuOffsetLeft();
this.setCurrentRouterHighlight(this.$route.path);
},
methods: {
// 设置横向滚动条可以鼠标滚轮滚动
onElMenuHorizontalScroll(e) {
const eventDelta = e.wheelDelta || -e.deltaY * 40;
this.$refs.elMenuHorizontalScrollRef.$refs.wrap.scrollLeft = this.$refs.elMenuHorizontalScrollRef.$refs.wrap.scrollLeft + eventDelta / 4;
},
// 初始化数据,页面刷新时,滚动条滚动到对应位置
initElMenuOffsetLeft() {
this.$nextTick(() => {
let els = document.querySelector('.el-menu.el-menu--horizontal li.is-active');
if (!els) return false;
this.$refs.elMenuHorizontalScrollRef.$refs.wrap.scrollLeft = els.offsetLeft;
});
},
// 路由过滤递归函数
filterRoutesFun(arr) {
return arr
.filter((item) => !item.meta.isHide)
.map((item) => {
item = Object.assign({}, item);
if (item.children) item.children = this.filterRoutesFun(item.children);
return item;
});
},
// 传送当前子级数据到菜单中
setSendClassicChildren(path) {
const currentPathSplit = path.split('/');
let currentData = {};
this.filterRoutesFun(this.$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;
},
// 菜单激活回调
onHorizontalSelect(path) {
this.bus.$emit('setSendClassicChildren', this.setSendClassicChildren(path));
},
// 设置页面当前路由高亮
setCurrentRouterHighlight(path) {
const currentPathSplit = path.split('/');
if (this.$store.state.themeConfig.themeConfig.layout === 'classic') {
this.defaultActive = `/${currentPathSplit[1]}`;
} else {
this.defaultActive = path;
}
},
},
watch: {
// 监听路由的变化
$route: {
handler(to) {
this.setCurrentRouterHighlight(to.path);
},
deep: true,
},
},
});
// 定义变量内容
const stores = useRoutesList();
const storesThemeConfig = useThemeConfig();
const { routesList } = storeToRefs(stores);
const { themeConfig } = storeToRefs(storesThemeConfig);
const route = useRoute();
const state = reactive({
defaultActive: '' as string | undefined,
});
// 获取父级菜单数据
const menuLists = computed(() => {
return <RouteItems>props.menuList;
});
// 路由过滤递归函数
const filterRoutesFun = <T extends RouteItem>(arr: T[]): T[] => {
return arr
.filter((item: T) => !item.meta?.isHide)
.map((item: T) => {
item = Object.assign({}, item);
if (item.children) item.children = filterRoutesFun(item.children);
return item;
});
};
// 传送当前子级数据到菜单中
const setSendClassicChildren = (path: string) => {
const currentPathSplit = path.split('/');
let currentData: MittMenu = { children: [] };
filterRoutesFun(routesList.value).map((v, k) => {
if (v.path === `/${currentPathSplit[1]}`) {
v['k'] = k;
currentData['item'] = { ...v };
currentData['children'] = [{ ...v }];
if (v.children) currentData['children'] = v.children;
}
});
return currentData;
};
// 设置页面当前路由高亮
const setCurrentRouterHighlight = (currentRoute: RouteToFrom) => {
const { path, meta } = currentRoute;
if (themeConfig.value.layout === 'classic') {
state.defaultActive = `/${path?.split('/')[1]}`;
} else {
const pathSplit = meta?.isDynamic ? meta.isDynamicPath!.split('/') : path!.split('/');
if (pathSplit.length >= 4 && meta?.isHide) state.defaultActive = pathSplit.splice(0, 3).join('/');
else state.defaultActive = path;
}
};
// 打开外部链接
const onALinkClick = (val: RouteItem) => {
other.handleOpenLink(val);
};
// 页面加载前
onBeforeMount(() => {
setCurrentRouterHighlight(route);
});
// 路由更新时
onBeforeRouteUpdate((to) => {
// 修复https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
setCurrentRouterHighlight(to);
// 修复经典布局开启切割菜单时点击tagsView后左侧导航菜单数据不变的问题
let { layout, isClassicSplitMenu } = themeConfig.value;
if (layout === 'classic' && isClassicSplitMenu) {
mittBus.emit('setSendClassicChildren', setSendClassicChildren(to.path));
}
});
</script>
<style scoped lang="scss">
@ -123,10 +119,13 @@ onBeforeRouteUpdate((to) => {
flex: 1;
overflow: hidden;
margin-right: 30px;
:deep(.el-scrollbar__bar.is-vertical) {
::v-deep .el-scrollbar__bar.is-vertical {
display: none;
}
:deep(a) {
::v-deep .el-scrollbar__wrap {
overflow-y: hidden !important;
}
::v-deep a {
width: 100%;
}
.el-menu.el-menu--horizontal {

View File

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

View File

@ -1,28 +1,28 @@
<template>
<el-menu
router
:default-active="state.defaultActive"
background-color="transparent"
:collapse="state.isCollapse"
:default-active="defaultActive"
:collapse="setIsCollapse"
: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 }}</span>
<template v-for="val in menuList">
<el-submenu :index="val.path" v-if="val.children && val.children.length > 0" :key="val.path">
<template slot="title">
<i :class="val.meta.icon ? val.meta.icon : ''"></i>
<span>{{ $t(val.meta.title) }}</span>
</template>
<SubItem :chil="val.children" />
</el-sub-menu>
</el-submenu>
<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 }}</span>
<i :class="val.meta.icon ? val.meta.icon : ''"></i>
<template slot="title" v-if="!val.meta.isLink || (val.meta.isLink && val.meta.isIframe)">
<span>{{ $t(val.meta.title) }}</span>
</template>
<template #title v-else>
<a class="w100" @click.prevent="onALinkClick(val)">{{ val.meta.title }}</a>
<template slot="title" v-else>
<a :href="val.meta.isLink" target="_blank">{{ $t(val.meta.title) }}</a>
</template>
</el-menu-item>
</template>
@ -30,73 +30,44 @@
</el-menu>
</template>
<script setup lang="ts" name="navMenuVertical">
import { defineAsyncComponent, reactive, computed, onMounted, watch } from 'vue';
import { useRoute, onBeforeRouteUpdate, RouteRecordRaw } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import other from '/@/utils/other';
// 引入组件
const SubItem = defineAsyncComponent(() => import('/@/layout/navMenu/subItem.vue'));
// 定义父组件传过来的值
const props = defineProps({
// 菜单列表
menuList: {
type: Array<RouteRecordRaw>,
default: () => [],
<script>
import SubItem from '@/layout/navMenu/subItem.vue';
export default {
name: 'navMenuVertical',
components: { SubItem },
props: {
menuList: {
type: Array,
default() {
return [];
},
},
},
});
// 定义变量内容
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const route = useRoute();
const state = reactive({
// 修复https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
defaultActive: route.meta.isDynamic ? route.meta.isDynamicPath : route.path,
isCollapse: false,
});
// 获取父级菜单数据
const menuLists = computed(() => {
return <RouteItems>props.menuList;
});
// 获取布局配置信息
const getThemeConfig = computed(() => {
return themeConfig.value;
});
// 菜单高亮(详情时,父级高亮)
const setParentHighlight = (currentRoute: RouteToFrom) => {
const { path, meta } = currentRoute;
const pathSplit = meta?.isDynamic ? meta.isDynamicPath!.split('/') : path!.split('/');
if (pathSplit.length >= 4 && meta?.isHide) return pathSplit.splice(0, 3).join('/');
else return path;
};
// 打开外部链接
const onALinkClick = (val: RouteItem) => {
other.handleOpenLink(val);
};
// 页面加载时
onMounted(() => {
state.defaultActive = setParentHighlight(route);
});
// 路由更新时
onBeforeRouteUpdate((to) => {
// 修复https://gitee.com/lyt-top/vue-next-admin/issues/I3YX6G
state.defaultActive = setParentHighlight(to);
const clientWidth = document.body.clientWidth;
if (clientWidth < 1000) themeConfig.value.isCollapse = false;
});
// 设置菜单的收起/展开
watch(
() => themeConfig.value.isCollapse,
(isCollapse) => {
document.body.clientWidth <= 1000 ? (state.isCollapse = false) : (state.isCollapse = isCollapse);
data() {
return {
defaultActive: this.$route.path,
};
},
{
immediate: true,
}
);
computed: {
// 获取布局配置信息
getThemeConfig() {
return this.$store.state.themeConfig.themeConfig;
},
// 设置左侧菜单是否展开/收起
setIsCollapse() {
return document.body.clientWidth < 1000 ? false : this.$store.state.themeConfig.themeConfig.isCollapse;
},
},
watch: {
// 监听路由的变化
$route: {
handler(to) {
this.defaultActive = to.path;
const clientWidth = document.body.clientWidth;
if (clientWidth < 1000) this.$store.state.themeConfig.themeConfig.isCollapse = false;
},
deep: true,
},
},
};
</script>

View File

@ -1,101 +1,46 @@
<template>
<div class="layout-padding layout-padding-unset layout-iframe">
<div class="layout-padding-auto layout-padding-view">
<div class="w100" v-for="v in setIframeList" :key="v.path" v-loading="v.meta.loading" element-loading-background="white">
<transition-group :name="name">
<iframe
:src="v.meta.isLink"
:key="v.path"
frameborder="0"
height="100%"
width="100%"
style="position: absolute"
:data-url="v.path"
v-show="getRoutePath === v.path"
ref="iframeRef"
/>
</transition-group>
</div>
<div>
<div class="layout-view-bg-white flex h100" v-loading="iframeLoading">
<iframe :src="meta.isLink" frameborder="0" height="100%" width="100%" id="iframe"></iframe>
</div>
</div>
</template>
<script setup lang="ts" name="layoutIframeView">
import { computed, watch, ref, nextTick } from 'vue';
import { useRoute } from 'vue-router';
// 定义父组件传过来的值
const props = defineProps({
// 刷新 iframe
refreshKey: {
type: String,
default: () => '',
<script>
export default {
name: 'layoutIfameView',
props: {
meta: {
type: Object,
default: () => {},
},
},
// 过渡动画 name
name: {
type: String,
default: () => 'slide-right',
data() {
return {
iframeLoading: true,
};
},
// iframe 列表
list: {
type: Array,
default: () => [],
created() {
this.bus.$on('onTagsViewRefreshRouterView', (path) => {
if (this.$route.path !== path) return false;
this.$emit('getCurrentRouteMeta');
});
},
});
// 定义变量内容
const iframeRef = ref();
const route = useRoute();
// 处理 list 列表,当打开时,才进行加载
const setIframeList = computed(() => {
return (<RouteItems>props.list).filter((v: RouteItem) => v.meta?.isIframeOpen);
});
// 获取 iframe 当前路由 path
const getRoutePath = computed(() => {
return route.path;
});
// 关闭 iframe loading
const closeIframeLoading = (val: string, item: RouteItem) => {
nextTick(() => {
if (!iframeRef.value) return false;
iframeRef.value.forEach((v: HTMLElement) => {
if (v.dataset.url === val) {
v.onload = () => {
if (item.meta?.isIframeOpen && item.meta.loading) item.meta.loading = false;
mounted() {
this.initIframeLoad();
},
methods: {
// 初始化页面加载 loading
initIframeLoad() {
this.$nextTick(() => {
this.iframeLoading = true;
const iframe = document.getElementById('iframe');
if (!iframe) return false;
iframe.onload = () => {
this.iframeLoading = false;
};
}
});
});
});
},
},
};
// 监听路由变化,初始化 iframe 数据,防止多个 iframe 时,切换不生效
watch(
() => route.fullPath,
(val) => {
const item: any = props.list.find((v: any) => v.path === val);
if (!item) return false;
if (!item.meta.isIframeOpen) item.meta.isIframeOpen = true;
closeIframeLoading(val, item);
},
{
immediate: true,
}
);
// 监听 iframe refreshKey 变化,用于 tagsview 右键菜单刷新
watch(
() => props.refreshKey,
() => {
const item: any = props.list.find((v: any) => v.path === route.path);
if (!item) return false;
if (item.meta.isIframeOpen) item.meta.isIframeOpen = false;
setTimeout(() => {
item.meta.isIframeOpen = true;
item.meta.loading = true;
closeIframeLoading(route.fullPath, item);
});
},
{
deep: true,
}
);
</script>

View File

@ -1,10 +1,10 @@
<template>
<div class="layout-padding layout-link-container">
<div class="layout-padding-auto layout-padding-view">
<div class="layout-scrollbar layout-link-container">
<div class="layout-view-bg-white flex layout-view-link">
<div class="layout-link-warp">
<i class="layout-link-icon iconfont icon-xingqiu"></i>
<div class="layout-link-msg">页面 {{ state.title }} 已在新窗口中打开</div>
<el-button class="mt30" round size="default" @click="onGotoFullPage">
<div class="layout-link-msg">页面 "{{ $t(meta.title) }}" 已在新窗口中打开</div>
<el-button class="mt30" round size="small" @click="onGotoFullPage">
<i class="iconfont icon-lianjie"></i>
<span>立即前往体验</span>
</el-button>
@ -13,35 +13,25 @@
</div>
</template>
<script setup lang="ts" name="layoutLinkView">
import { reactive, watch } from 'vue';
import { useRoute } from 'vue-router';
import { verifyUrl } from '/@/utils/toolsValidate';
// 定义变量内容
const route = useRoute();
const state = reactive<LinkViewState>({
title: '',
isLink: '',
});
// 立即前往
const onGotoFullPage = () => {
const { origin, pathname } = window.location;
if (verifyUrl(<string>state.isLink)) window.open(state.isLink);
else window.open(`${origin}${pathname}#${state.isLink}`);
};
// 监听路由的变化,设置内容
watch(
() => route.path,
() => {
state.title = <string>route.meta.title;
state.isLink = <string>route.meta.isLink;
<script>
import { verifyUrl } from '@/utils/toolsValidate';
export default {
name: 'layoutLinkView',
props: {
meta: {
type: Object,
default: () => {},
},
},
{
immediate: true,
}
);
methods: {
// 立即前往
onGotoFullPage() {
const { origin, pathname } = window.location;
if (verifyUrl(this.meta.isLink)) window.open(this.meta.isLink);
else window.open(`${origin}${pathname}#${this.meta.isLink}`);
},
},
};
</script>
<style scoped lang="scss">
@ -55,7 +45,7 @@ watch(
i.layout-link-icon {
position: relative;
font-size: 100px;
color: var(--el-color-primary);
color: var(--prev-color-primary);
&::after {
content: '';
position: absolute;
@ -84,7 +74,7 @@ watch(
}
.layout-link-msg {
font-size: 12px;
color: var(--next-bg-topBarColor);
color: var(--prev-bg-topBarColor);
opacity: 0.7;
margin-top: 15px;
}

View File

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

View File

@ -1,62 +1,66 @@
<template>
<div class="sponsors-container" title="点击前往体验" v-show="state.sponsors.isShow" @click="onSponsorsClick">
<div class="sponsors-container" title="点击前往体验" v-show="sponsors.isShow" @click="onSponsorsClick">
<el-carousel height="240px" indicator-position="none" :arrow="setCarouselShow" @change="onCarouselChange">
<el-carousel-item v-for="(v, k) in state.sponsors.list" :key="k">
<el-carousel-item v-for="(v, k) in sponsors.list" :key="k">
<img :src="v.url" class="sponsors-img" />
<div class="sponsors-text" v-html="v.text"></div>
</el-carousel-item>
</el-carousel>
<div class="sponsors-close">
<SvgIcon name="ele-Close" :size="12" title="关闭赞助商" @click.stop="onCloseSponsors" />
<div class="sponsors-close" title="关闭赞助商" @click.stop="onCloseSponsors">
<i class="el-icon-close"></i>
</div>
</div>
</template>
<script setup lang="ts" name="layoutSponsors">
import { reactive, computed, onMounted } from 'vue';
import sponsorsOne from '/@/assets/ccflowRightNextAdmin.png';
// 定义变量内容
const state = reactive({
sponsors: {
list: [
{
url: sponsorsOne,
text: `驰骋BPM系统包含表单引擎+流程引擎+权限控制,方便集成,配置灵活,功能强大,适合中国国情的工作流引擎.演示:http://demo.ccflow.org。右上角点star方可加群: 1060674395`,
link: 'http://www.ccflow.org/',
<script>
import sponsorsOne from '@/assets/ccflowRightNextAdmin.png';
export default {
name: 'layoutSponsors',
data() {
return {
sponsors: {
list: [
{
url: sponsorsOne,
text: `驰骋BPM系统包含表单引擎+流程引擎+权限控制,方便集成,配置灵活,功能强大,适合中国国情的工作流引擎.演示:http://demo.ccflow.org。右上角点star方可加群: 1060674395`,
link: 'http://www.ccflow.org/',
},
],
isShow: false,
index: 0,
},
],
isShow: false,
index: 0,
};
},
computed: {
// 设置轮播图箭头显示
setCarouselShow() {
return this.sponsors.list.length <= 1 ? 'never' : 'hover';
},
},
methods: {
// 关闭赞助商
onCloseSponsors() {
this.sponsors.isShow = false;
},
// 轮播图改变时
onCarouselChange(e) {
this.sponsors.index = e;
},
// 当前项内容点击
onSponsorsClick() {
window.open(this.sponsors.list[this.sponsors.index].link);
},
// 延迟显示,防止影响其它界面加载
delayShow() {
setTimeout(() => {
this.sponsors.isShow = true;
}, 3000);
},
},
mounted() {
this.delayShow();
},
});
// 设置轮播图箭头显示
const setCarouselShow = computed(() => {
return state.sponsors.list.length <= 1 ? 'never' : 'hover';
});
// 关闭赞助商
const onCloseSponsors = () => {
state.sponsors.isShow = false;
};
// 轮播图改变时
const onCarouselChange = (e: number) => {
state.sponsors.index = e;
};
// 当前项内容点击
const onSponsorsClick = () => {
window.open(state.sponsors.list[state.sponsors.index].link);
};
// 延迟显示,防止影响其它界面加载
const delayShow = () => {
setTimeout(() => {
state.sponsors.isShow = true;
}, 3000);
};
// 页面加载时
onMounted(() => {
delayShow();
});
</script>
<style scoped lang="scss">
@ -66,8 +70,8 @@ onMounted(() => {
bottom: 15px;
z-index: 3;
width: 200px;
background-color: var(--next-bg-main-color);
box-shadow: var(--el-box-shadow-lighter);
background-color: var(--prev-bg-main-color);
box-shadow: 0px 0px 6px rgba(0, 0, 0, 0.12);
border-radius: 5px;
overflow: hidden;
cursor: pointer;
@ -77,8 +81,8 @@ onMounted(() => {
}
.sponsors-text {
padding: 10px;
color: var(--el-text-color-regular);
font-size: var(--el-dialog-content-font-size);
color: var(--prev-color-text-regular);
font-size: 14px;
}
.sponsors-close {
width: 60px;
@ -89,7 +93,8 @@ onMounted(() => {
position: absolute;
right: -35px;
bottom: -35px;
:deep(i) {
z-index: 5;
i {
position: absolute;
left: 9px;
top: 9px;
@ -98,8 +103,8 @@ onMounted(() => {
}
&:hover {
transition: all 0.3s ease;
:deep(i) {
color: var(--el-color-primary);
i {
color: var(--prev-color-primary);
transition: all 0.3s ease;
}
}

View File

@ -1,7 +1,7 @@
<template>
<div class="upgrade-dialog">
<el-dialog
v-model="state.isUpgrade"
:visible.sync="isUpgrade"
width="300px"
destroy-on-close
:show-close="false"
@ -10,80 +10,79 @@
>
<div class="upgrade-title">
<div class="upgrade-title-warp">
<span class="upgrade-title-warp-txt">新版本升级</span>
<span class="upgrade-title-warp-version">v{{ state.version }}</span>
<span class="upgrade-title-warp-txt">{{ $t('message.upgrade.title') }}</span>
<span class="upgrade-title-warp-version">v{{ version }}</span>
</div>
</div>
<div class="upgrade-content">
{{ getThemeConfig.globalTitle }} 新版本来啦马上更新尝鲜吧不用担心更新很快的哦
{{ getThemeConfig.globalTitle }} {{ $t('message.upgrade.msg') }}
<div class="mt5">
<el-link type="primary" class="font12" href="https://gitee.com/lyt-top/vue-next-admin/blob/master/CHANGELOG.md" target="_black">
<el-link type="primary" class="font12" href="https://gitee.com/lyt-top/vue-next-admin/blob/vue-prev-admin/CHANGELOG.md" target="_black">
CHANGELOG.md
</el-link>
</div>
<div class="upgrade-content-desc mt5">提示更新会还原默认配置</div>
<div class="upgrade-content-desc mt5">{{ $t('message.upgrade.desc') }}</div>
</div>
<div class="upgrade-btn">
<el-button round size="default" type="info" text @click="onCancel">残忍拒绝</el-button>
<el-button type="primary" round size="default" @click="onUpgrade" :loading="state.isLoading">{{ state.btnTxt }}</el-button>
<el-button round size="small" @click="onCancel">{{ $t('message.upgrade.btnOne') }}</el-button>
<el-button type="primary" round size="small" @click="onUpgrade" :loading="isLoading">{{ btnTxt }}</el-button>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="layoutUpgrade">
import { reactive, computed, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '/@/stores/themeConfig';
import { Local } from '/@/utils/storage';
// 定义变量内容
const storesThemeConfig = useThemeConfig();
const { themeConfig } = storeToRefs(storesThemeConfig);
const state = reactive({
isUpgrade: false,
// @ts-ignore
version: __NEXT_VERSION__,
isLoading: false,
btnTxt: '',
});
// 获取布局配置信息
const getThemeConfig = computed(() => {
return themeConfig.value;
});
// 残忍拒绝
const onCancel = () => {
state.isUpgrade = false;
<script>
import { Local } from '@/utils/storage';
import config from '/package.json';
export default {
data() {
return {
isUpgrade: false,
version: config.version,
isLoading: false,
btnTxt: '',
};
},
computed: {
// 获取布局配置信息
getThemeConfig() {
return this.$store.state.themeConfig.themeConfig;
},
},
methods: {
// 残忍拒绝
onCancel() {
this.isUpgrade = false;
},
// 马上更新
onUpgrade() {
this.isLoading = true;
this.btnTxt = this.$t('message.upgrade.btnTwoLoading');
setTimeout(() => {
Local.clear();
window.location.reload();
Local.set('version', this.version);
}, 2000);
},
// 延迟显示,防止刷新时界面显示太快
delayShow() {
setTimeout(() => {
this.isUpgrade = true;
}, 2000);
},
},
mounted() {
this.delayShow();
setTimeout(() => {
this.btnTxt = this.$t('message.upgrade.btnTwo');
}, 200);
},
};
// 马上更新
const onUpgrade = () => {
state.isLoading = true;
state.btnTxt = '更新中';
setTimeout(() => {
Local.clear();
window.location.reload();
Local.set('version', state.version);
}, 2000);
};
// 延迟显示,防止刷新时界面显示太快
const delayShow = () => {
setTimeout(() => {
state.isUpgrade = true;
}, 2000);
};
// 页面加载时
onMounted(() => {
delayShow();
setTimeout(() => {
state.btnTxt = '马上更新';
}, 200);
});
</script>
<style scoped lang="scss">
.upgrade-dialog {
:deep(.el-dialog) {
& ::v-deep .el-dialog {
.el-dialog__body {
padding: 0 !important;
}
@ -100,7 +99,7 @@ onMounted(() => {
&::after {
content: '';
position: absolute;
background-color: var(--el-color-primary-light-1);
background-color: var(--prev-color-primary-light-1);
width: 130%;
height: 130px;
border-bottom-left-radius: 100%;
@ -110,13 +109,13 @@ onMounted(() => {
z-index: 1;
position: relative;
.upgrade-title-warp-txt {
color: var(--next-color-white);
color: var(--prev-color-text-white);
font-size: 22px;
letter-spacing: 3px;
}
.upgrade-title-warp-version {
color: var(--next-color-white);
background-color: var(--el-color-primary-light-4);
background-color: var(--prev-color-primary-light-4);
color: var(--prev-color-text-white);
font-size: 12px;
position: absolute;
display: flex;
@ -130,13 +129,14 @@ onMounted(() => {
.upgrade-content {
padding: 20px;
line-height: 22px;
color: var(--prev-color-text-regular);
.upgrade-content-desc {
color: var(--el-color-info-light-5);
color: var(--prev-color-text-placeholder);
font-size: 12px;
}
}
.upgrade-btn {
border-top: 1px solid var(--el-border-color-lighter, #ebeef5);
border-top: 1px solid var(--prev-border-color-lighter);
display: flex;
justify-content: space-around;
padding: 15px 20px;

24
src/main.js Normal file
View File

@ -0,0 +1,24 @@
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import Particles from 'vue-particles';
import Element from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import '@/theme/index.scss';
import { i18n } from '@/i18n/index.js';
import { globalComponentSize } from '@/utils/componentSize.js';
Vue.use(Particles);
Vue.use(Element, { i18n: (key, value) => i18n.t(key, value), size: globalComponentSize });
Vue.config.productionTip = false;
Vue.prototype.bus = new Vue();
new Vue({
router,
store,
i18n,
render: (h) => h(App),
}).$mount('#app');

View File

@ -1,16 +0,0 @@
import { createApp } from 'vue';
import pinia from '/@/stores/index';
import App from '/@/App.vue';
import router from '/@/router';
import { directive } from '/@/directive/index';
import other from '/@/utils/other';
import ElementPlus from 'element-plus';
import '/@/theme/index.scss';
const app = createApp(App);
directive(app);
other.elSvg(app);
app.use(pinia).use(router).use(ElementPlus).mount('#app');

View File

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

View File

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

275
src/router/index.js Normal file
View File

@ -0,0 +1,275 @@
import Vue from 'vue';
import store from '../store';
import VueRouter from 'vue-router';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import { Session } from '@/utils/storage';
import { PrevLoading } from '@/utils/loading.js';
import { useMenuApi } from '@/api/menu';
const menuApi = useMenuApi();
// 解决 `element ui` 导航栏重复点菜单报错问题
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch((err) => err);
};
// 安装 VueRouter 插件
Vue.use(VueRouter);
// 定义动态路由
const dynamicRoutes = [
{
path: '/',
name: '/',
component: 'layout/index',
redirect: '/home',
meta: {
isKeepAlive: true,
},
children: [],
},
];
// 定义静态路由
const staticRoutes = [
{
path: '/login',
name: 'login',
component: () => import('@/views/login'),
meta: {
title: '登录',
},
},
{
path: '/404',
name: 'notFound',
component: () => import('@/views/error/404.vue'),
meta: {
title: 'message.staticRoutes.notFound',
},
},
{
path: '/401',
name: 'noPower',
component: () => import('@/views/error/401.vue'),
meta: {
title: 'message.staticRoutes.noPower',
},
},
];
// 加载静态路由
const createRouter = () =>
new VueRouter({
routes: staticRoutes,
});
// 创建路由
const router = createRouter();
// 加载 loading
PrevLoading.start();
// 多级嵌套数组处理成一维数组
export function formatFlatteningRoutes(arr) {
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;
}
// 处理 tagsViewList 数据,默认路由全部缓存
// isKeepAlive 处理 `name` 值,进行路由缓存
export function formatTwoStageRoutes(arr) {
if (arr.length <= 0) return false;
const newArr = [];
const cacheList = [];
arr.forEach((v) => {
newArr.push({ ...v });
cacheList.push(v.name);
store.dispatch('keepAliveNames/setCacheKeepAlive', cacheList);
});
return newArr;
}
// 判断路由 meta.roles 中是否包含当前登录用户权限字段
export function hasAuth(roles, route) {
if (route.meta && route.meta.roles) return roles.some((role) => route.meta.roles.includes(role));
else return true;
}
// 递归过滤有权限的路由
export function setFilterMenuFun(routes, role) {
const menu = [];
routes.forEach((route) => {
const item = { ...route };
if (hasAuth(role, item)) {
if (item.children) item.children = setFilterMenuFun(item.children, role);
menu.push(item);
}
});
return menu;
}
// 缓存多级嵌套数组处理后的一维数组(tagsView、菜单搜索中使用未过滤隐藏的(isHide))
export function setCacheTagsViewRoutes(arr) {
// 先处理有权限的路由,否则 tagsView、菜单搜索中无权限的路由也将显示
let rolesRoutes = setFilterMenuFun(arr, store.state.userInfos.userInfos.roles);
// 添加到 vuex setTagsViewRoutes 中
store.dispatch('tagsViewRoutes/setTagsViewRoutes', formatTwoStageRoutes(formatFlatteningRoutes(rolesRoutes)));
}
// 递归处理多余的 layout : <router-view>,让需要访问的组件保持在第一层 layout 层。
// 因为 `keep-alive` 只能缓存二级路由
// 默认初始化时就执行
export function keepAliveSplice(to) {
if (to.matched && to.matched.length > 2) {
to.matched.map((v, k) => {
if (v.components.default instanceof Function) {
v.components.default().then((components) => {
if (components.default.name === 'parent') {
to.matched.splice(k, 1);
router.push({ path: to.path, query: to.query });
keepAliveSplice(to);
}
});
} else {
if (v.components.default.name === 'parent') {
to.matched.splice(k, 1);
keepAliveSplice(to);
}
}
});
}
}
// 处理后端返回的 `component` 路径,拼装实现懒加载
export function loadView(path) {
/**
* 打包成一个 js、一个 css
*/
// if (path.indexOf('layout') > -1) return () => Promise.resolve(require(`@/${path}`));
// else return () => Promise.resolve(require(`@/views/${path}`));
/**
* 打包成多个 js、多个 css
*/
if (path.indexOf('layout') > -1) return () => import(`@/${path}`);
else return () => import(`@/views/${path}`);
}
// 递归处理每一项 `component` 中的路径
export function dynamicRouter(routes) {
return routes.map((view) => {
if (view.component) view.component = loadView(view.component);
if (view.children) dynamicRouter(view.children);
return view;
});
}
// 添加路由,模拟数据与方法,可自行进行修改 admin
// 添加动态路由,`{ path: '*', redirect: '/404' }` 防止页面刷新,静态路由丢失问题
// next({ ...to, replace: true }) 动态路由 addRoute 完毕后才放行,防止刷新时 NProgress 进度条加载2次
// 文档地址https://router.vuejs.org/zh/api/#router-addroutes
export function adminUser(router, to, next) {
resetRouter();
menuApi
.getMenuAdmin()
.then(async (res) => {
// 读取用户信息,获取对应权限进行判断
store.dispatch('userInfos/setUserInfos');
store.dispatch('routesList/setRoutesList', setFilterMenuFun(res.data, store.state.userInfos.userInfos.roles));
dynamicRoutes[0].children = res.data;
const awaitRoute = await dynamicRouter(dynamicRoutes);
[...awaitRoute, { path: '*', redirect: '/404' }].forEach((route) => {
router.addRoute({ ...route });
});
setCacheTagsViewRoutes(JSON.parse(JSON.stringify(res.data)));
next({ ...to, replace: true });
})
.catch(() => {});
}
// 添加路由,模拟数据与方法,可自行进行修改 test
// 添加动态路由,`{ path: '*', redirect: '/404' }` 防止页面刷新,静态路由丢失问题
export function testUser(router, to, next) {
resetRouter();
menuApi
.getMenuTest()
.then(async (res) => {
// 读取用户信息,获取对应权限进行判断
store.dispatch('userInfos/setUserInfos');
store.dispatch('routesList/setRoutesList', setFilterMenuFun(res.data, store.state.userInfos.userInfos.roles));
dynamicRoutes[0].children = res.data;
const awaitRoute = await dynamicRouter(dynamicRoutes);
[...awaitRoute, { path: '*', redirect: '/404' }].forEach((route) => {
router.addRoute({ ...route });
});
setCacheTagsViewRoutes(JSON.parse(JSON.stringify(res.data)));
next({ ...to, replace: true });
})
.catch(() => {});
}
// 重置路由
export function resetRouter() {
router.matcher = createRouter().matcher;
}
// 延迟关闭进度条
export function delayNProgressDone(time = 300) {
setTimeout(() => {
NProgress.done();
}, time);
}
// 动态加载后端返回路由路由(模拟数据)
export function getRouterList(router, to, next) {
if (!Session.get('userInfo')) return false;
if (Session.get('userInfo').userName === 'admin') adminUser(router, to, next);
else if (Session.get('userInfo').userName === 'test') testUser(router, to, next);
}
// 路由加载前
router.beforeEach((to, from, next) => {
keepAliveSplice(to);
NProgress.configure({ showSpinner: false });
if (to.meta.title && to.path !== '/login') NProgress.start();
let token = Session.get('token');
if (to.path === '/login' && !token) {
NProgress.start();
next();
delayNProgressDone();
} else {
if (!token) {
NProgress.start();
next('/login');
Session.clear();
delayNProgressDone();
} else if (token && to.path === '/login') {
next('/home');
delayNProgressDone();
} else {
if (Object.keys(store.state.routesList.routesList).length <= 0) {
getRouterList(router, to, next);
} else {
next();
delayNProgressDone(0);
}
}
}
});
// 路由加载后
router.afterEach(() => {
PrevLoading.done();
delayNProgressDone();
});
// 导出路由
export default router;

View File

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

View File

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

15
src/store/index.js Normal file
View File

@ -0,0 +1,15 @@
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
let moduleFn = require.context('./modules', false, /\.js$/);
let modules = moduleFn.keys().reduce((p, c) => {
let mod = moduleFn(c).default;
mod = { ...mod, namespaced: true };
let modName = c.match(/\.\/(\w+)\.js$/)[1];
p[modName] = mod;
return p;
}, {});
export default new Vuex.Store({ modules });

View File

@ -0,0 +1,20 @@
const keepAliveNamesModule = {
namespaced: true,
state: {
keepAliveNames: [],
},
mutations: {
// 设置路由缓存name字段
getCacheKeepAlive(state, data) {
state.keepAliveNames = data;
},
},
actions: {
// 设置路由缓存name字段
async setCacheKeepAlive({ commit }, data) {
commit('getCacheKeepAlive', data);
},
},
};
export default keepAliveNamesModule;

View File

@ -0,0 +1,20 @@
const routesListModule = {
namespaced: true,
state: {
routesList: [],
},
mutations: {
// 设置路由,菜单中使用到
getRoutesList(state, data) {
state.routesList = data;
},
},
actions: {
// 设置路由,菜单中使用到
async setRoutesList({ commit }, data) {
commit('getRoutesList', data);
},
},
};
export default routesListModule;

View File

@ -0,0 +1,20 @@
const tagsViewRoutesModule = {
namespaced: true,
state: {
tagsViewRoutes: [],
},
mutations: {
// 设置 TagsView 路由
getTagsViewRoutes(state, data) {
state.tagsViewRoutes = data;
},
},
actions: {
// 设置 TagsView 路由
async setTagsViewRoutes({ commit }, data) {
commit('getTagsViewRoutes', data);
},
},
};
export default tagsViewRoutesModule;

View File

@ -0,0 +1,122 @@
/**
* 2020.05.28 by lyt 优化
* 修改一下配置时,需要每次都清理 `window.localStorage` 浏览器永久缓存,配置才会生效
*/
const themeConfigModule = {
namespaced: true,
state: {
themeConfig: {
// 是否开启布局配置抽屉
isDrawer: false,
/**
* 全局主题
*/
// 默认 primary 主题颜色
primary: '#409eff',
// 是否开启深色模式
isIsDark: false,
/**
* 菜单 / 顶栏
* 请注意:
* 需要同时修改 `/@/theme/common/var.scss` 对应的值,
* 不提供像 vue-next-admin 一样的实现
*/
// 默认顶栏导航背景颜色
topBar: '#ffffff',
// 默认顶栏导航字体颜色
topBarColor: '#606266',
// 默认菜单导航背景颜色
menuBar: '#545c64',
// 默认菜单导航字体颜色
menuBarColor: '#eaeaea',
// 默认分栏菜单背景颜色
columnsMenuBar: '#545c64',
// 默认分栏菜单字体颜色
columnsMenuBarColor: '#e6e6e6',
/**
* 界面设置
*/
// 是否开启菜单水平折叠效果
isCollapse: false,
// 是否开启菜单手风琴效果
isUniqueOpened: false,
// 是否开启固定 Header
isFixedHeader: false,
/**
* 界面显示
*/
// 是否开启侧边栏 Logo
isShowLogo: false,
// 是否开启 Breadcrumb
isBreadcrumb: true,
// 是否开启 Breadcrumb 图标
isBreadcrumbIcon: false,
// 是否开启 Tagsview
isTagsview: true,
// 是否开启 Tagsview 图标
isTagsviewIcon: false,
// 是否开启 TagsView 缓存
isCacheTagsView: false,
// 是否开启 Footer 底部版权信息
isFooter: false,
// 是否开启灰色模式
isGrayscale: false,
// 是否开启色弱模式
isInvert: false,
/**
* 其它设置
*/
// 默认 Tagsview 风格,可选 1、 tags-style-one自行扩展
// 1、需修改 @/layout/navBars/topBar/setings.vue `getThemeConfig.tagsStyle` el-option
// 2、需修改 @/layout/navBars/tagsView/tagsView.vue 代码最底部注释部分 css 样式
tagsStyle: 'tags-style-one',
// 主页面切换动画:可选值"<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/topBar/setings.vue
* 中的 `initSetLayoutChange(设置布局切换,重置主题样式)` 方法
*/
// 布局切换:可选值"<defaults|classic|transverse|columns>",默认 defaults
layout: 'defaults',
/**
* 全局网站标题 / 副标题
*/
// 网站主标题(菜单导航、浏览器当前网页标题)
globalTitle: 'vue-prev-admin',
// 网站副标题(登录页顶部文字)
globalViceTitle: 'SMALL@小柒',
// 网站描述(登录页顶部文字)
globalViceDes: 'vue2.x后台管理系统免费开源模板',
// 默认初始语言,可选值"<zh-cn|en|zh-tw>",默认 zh-cn
globalI18n: 'zh-cn',
// 默认全局组件大小,可选值"<|medium|small|mini>",默认 ''
globalComponentSize: '',
},
},
mutations: {
// 设置布局配置
getThemeConfig(state, data) {
state.themeConfig = data;
},
},
actions: {
// 设置布局配置
setThemeConfig({ commit }, data) {
commit('getThemeConfig', data);
},
},
};
export default themeConfigModule;

View File

@ -0,0 +1,26 @@
import { Session } from '@/utils/storage.js';
const userInfosModule = {
namespaced: true,
state: {
userInfos: {},
},
mutations: {
// 设置用户信息
getUserInfos(state, data) {
state.userInfos = data;
},
},
actions: {
// 设置用户信息
async setUserInfos({ commit }, data) {
if (data) {
commit('getUserInfos', data);
} else {
if (Session.get('userInfo')) commit('getUserInfos', Session.get('userInfo'));
}
},
},
};
export default userInfosModule;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,41 +4,44 @@
margin: 0;
padding: 0;
box-sizing: border-box;
outline: none !important;
outline: none;
}
:root {
--next-color-white: #ffffff;
--next-bg-main-color: #f8f8f8;
--next-bg-color: #f5f5ff;
--next-border-color-light: #f1f2f3;
--next-color-primary-lighter: #ecf5ff;
--next-color-success-lighter: #f0f9eb;
--next-color-warning-lighter: #fdf6ec;
--next-color-danger-lighter: #fef0f0;
--next-color-dark-hover: #0000001a;
--next-color-menu-hover: rgba(0, 0, 0, 0.2);
--next-color-user-hover: rgba(0, 0, 0, 0.04);
--next-color-seting-main: #e9eef3;
--next-color-seting-aside: #d3dce6;
--next-color-seting-header: #b3c0d1;
--prev-bg-menuBar: #545c64;
--prev-bg-menuBarColor: #eaeaea;
--prev-bg-topBar: #ffffff;
--prev-bg-topBarColor: #606266;
--prev-bg-columnsMenuBar: #545c64;
--prev-bg-columnsMenuBarColor: #e6e6e6;
--prev-bg-main-color: #f8f8f8;
--prev-bg-color: #f5f7fa;
--prev-bg-white: #ffffff;
--prev-color-primary: #409eff;
--prev-color-text-white: #ffffff;
--prev-color-text-black: #000000;
--prev-color-text-primary: #303133;
--prev-color-text-regular: #606266;
--prev-color-text-secondary: #909399;
--prev-color-text-placeholder: #c0c4cc;
--prev-color-hover: rgba(0, 0, 0, 0.04);
--prev-color-seting-main: #e9eef3;
--prev-color-seting-aside: #d3dce6;
--prev-color-seting-header: #b3c0d1;
--prev-border-color-hover: #c0c4cc;
--prev-border-color-base: #dcdfe6;
--prev-border-color-light: #e4e7ed;
--prev-border-color-lighter: #ebeef5;
--prev-border-color-extra-light: #f2f6fc;
}
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);
background-color: var(--prev-bg-main-color);
font-size: 14px;
overflow: hidden;
position: relative;
}
/* 主布局样式
@ -46,16 +49,8 @@ body,
.layout-container {
width: 100%;
height: 100%;
.layout-pd {
padding: 15px !important;
}
.layout-flex {
display: flex;
flex-direction: column;
flex: 1;
}
.layout-aside {
background: var(--next-bg-menuBar);
background: var(--prev-bg-menuBar);
box-shadow: 2px 0 6px rgb(0 21 41 / 1%);
height: inherit;
position: relative;
@ -69,107 +64,41 @@ body,
}
.layout-header {
padding: 0 !important;
height: auto !important;
}
.layout-main {
padding: 0 !important;
overflow: hidden;
width: 100%;
background-color: var(--next-bg-main-color);
display: flex;
flex-direction: column;
// 内层 el-scrollbar样式用于界面高度自适应main.vue
.layout-main-scroll {
@extend .layout-flex;
.layout-parent {
@extend .layout-flex;
position: relative;
}
}
}
// 用于界面高度自适应
.layout-padding {
@extend .layout-pd;
position: absolute;
left: 0;
top: 0;
height: 100%;
overflow: hidden;
@extend .layout-flex;
&-auto {
height: inherit;
@extend .layout-flex;
}
&-view {
background: var(--el-color-white);
width: 100%;
height: 100%;
border-radius: 4px;
border: 1px solid var(--el-border-color-light, #ebeef5);
overflow: hidden;
}
}
// 用于界面高度自适应,主视图区 main 的内边距,用于 iframe
.layout-padding-unset {
padding: 0 !important;
&-view {
border-radius: 0 !important;
border: none !important;
}
}
// 用于设置 iframe loading 时的高度loading 垂直居中显示)
.layout-iframe {
.el-loading-parent--relative {
height: 100%;
}
background-color: var(--prev-bg-main-color);
}
.el-scrollbar {
width: 100%;
}
.layout-el-aside-br-color {
border-right: 1px solid var(--el-border-color-light, #ebeef5);
.layout-view-bg-white {
background: var(--prev-bg-white);
width: 100%;
height: 100%;
border-radius: 4px;
border: 1px solid var(--prev-border-color-lighter);
}
// pc端左侧导航样式
.layout-aside-pc-220 {
.layout-el-aside-br-color {
border-right: 1px solid rgb(238, 238, 238);
}
.layout-aside-width-default {
width: 220px !important;
transition: width 0.3s ease;
}
.layout-aside-pc-64 {
.layout-aside-width64 {
width: 64px !important;
transition: width 0.3s ease;
}
.layout-aside-pc-1 {
.layout-aside-width1 {
width: 1px !important;
transition: width 0.3s ease;
position: relative;
left: -1px;
}
// 手机端左侧导航样式
.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);
@ -184,21 +113,10 @@ body,
}
}
/* 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;
}
#nprogress .bar {
background: var(--prev-color-primary) !important;
}
/* flex 弹性布局
@ -208,7 +126,6 @@ body,
}
.flex-auto {
flex: 1;
overflow: hidden;
}
.flex-center {
@extend .flex;
@ -232,26 +149,6 @@ body,
}
}
}
/* cursor 鼠标形状
------------------------------- */
// 默认
.cursor-default {
cursor: default !important;
}
// 帮助
.cursor-help {
cursor: help !important;
}
// 手指
.cursor-pointer {
cursor: pointer !important;
}
// 移动
.cursor-move {
cursor: move !important;
}
/* 宽高 100%
------------------------------- */
.w100 {
@ -273,19 +170,36 @@ body,
/* 颜色值
------------------------------- */
.color-primary {
color: var(--el-color-primary);
color: var(--prev-color-primary);
}
.color-success {
color: var(--el-color-success);
color: var(--prev-color-success);
}
.color-warning {
color: var(--el-color-warning);
color: var(--prev-color-warning);
}
.color-danger {
color: var(--el-color-danger);
color: var(--prev-color-danger);
}
.color-info {
color: var(--el-color-info);
color: var(--prev-color-info);
}
/* 溢出省略号
------------------------------- */
.one-text-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.two-text-overflow {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.overflow {
overflow: hidden !important;
}
/* 字体大小全局样式
@ -298,7 +212,7 @@ body,
/* 外边距、内边距全局样式
------------------------------- */
@for $i from 1 through 35 {
@for $i from 5 through 35 {
.mt#{$i} {
margin-top: #{$i}px !important;
}

1
src/theme/base.scss Normal file
View File

@ -0,0 +1 @@
@import 'common/transition.scss';

View File

@ -8,20 +8,20 @@
transition: all 0.3s ease;
}
// slide-right
.slide-right-enter-from {
.slide-right-enter {
opacity: 0;
transform: translateX(-20px);
}
.slide-right-leave-to {
.slide-right-leave-active {
opacity: 0;
transform: translateX(20px);
}
// slide-left
.slide-left-enter-from {
@extend .slide-right-leave-to;
@extend .slide-right-leave-active;
}
.slide-left-leave-to {
@extend .slide-right-enter-from;
@extend .slide-right-enter;
}
// opacitys
.opacitys-enter-active,
@ -29,8 +29,8 @@
will-change: transform;
transition: all 0.3s ease;
}
.opacitys-enter-from,
.opacitys-leave-to {
.opacitys-enter,
.opacitys-leave-active {
opacity: 0;
}
@ -38,16 +38,18 @@
------------------------------- */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all 0.5s ease;
transition: all 0.3s;
}
.breadcrumb-enter-from,
.breadcrumb-enter,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-move {
transition: all 0.3s;
}
.breadcrumb-leave-active {
position: absolute;
z-index: -1;
}
/* logo 过渡动画
@ -84,53 +86,6 @@
opacity: 1;
}
}
@keyframes error-img-two {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
/* 登录页动画
------------------------------- */
@keyframes loginLeft {
0% {
left: -100%;
}
50%,
100% {
left: 100%;
}
}
@keyframes loginTop {
0% {
top: -100%;
}
50%,
100% {
top: 100%;
}
}
@keyframes loginRight {
0% {
right: -100%;
}
50%,
100% {
right: 100%;
}
}
@keyframes loginBottom {
0% {
bottom: -100%;
}
50%,
100% {
bottom: 100%;
}
}
/* 左右左 link.vue
------------------------------- */

View File

@ -1,255 +1,109 @@
/* 深色模式样式
------------------------------- */
[data-theme='dark'] {
// 变量(自定义时,只需修改这里的值)
--next-bg-main: #1f1f1f;
--next-color-white: #ffffff;
--next-color-disabled: #191919;
--next-color-bar: #dadada;
--next-color-primary: #303030;
--next-border-color: #424242;
--next-border-black: #333333;
--next-border-columns: #2a2a2a;
--next-color-seting: #505050;
--next-text-color-regular: #9b9da1;
--next-text-color-placeholder: #7a7a7a;
--next-color-hover: #3c3c3c;
--next-color-hover-rgba: rgba(0, 0, 0, 0.3);
// root
--next-bg-main-color: var(--next-bg-main) !important;
--next-bg-topBar: var(--next-color-disabled) !important;
--next-bg-topBarColor: var(--next-color-bar) !important;
--next-bg-menuBar: var(--next-color-disabled) !important;
--next-bg-menuBarColor: var(--next-color-bar) !important;
--next-bg-menuBarActiveColor: var(--next-color-hover-rgba) !important;
--next-bg-columnsMenuBar: var(--next-color-disabled) !important;
--next-bg-columnsMenuBarColor: var(--next-color-bar) !important;
--next-border-color-light: var(--next-border-black) !important;
--next-color-primary-lighter: var(--next-color-primary) !important;
--next-color-success-lighter: var(--next-color-primary) !important;
--next-color-warning-lighter: var(--next-color-primary) !important;
--next-color-danger-lighter: var(--next-color-primary) !important;
--next-bg-color: var(--next-color-primary) !important;
--next-color-dark-hover: var(--next-color-hover) !important;
--next-color-menu-hover: var(--next-color-hover-rgba) !important;
--next-color-user-hover: var(--next-color-hover-rgba) !important;
--next-color-seting-main: var(--next-color-seting) !important;
--next-color-seting-aside: var(--next-color-hover) !important;
--next-color-seting-header: var(--next-color-primary) !important;
// element plus
--el-color-white: var(--next-color-disabled) !important;
--el-text-color-primary: var(--next-color-bar) !important;
--el-border-color: var(--next-border-black) !important;
--el-border-color-light: var(--next-border-black) !important;
--el-border-color-lighter: var(--next-border-black) !important;
--el-border-color-extra-light: var(--el-color-primary-light-8) !important;
--el-text-color-regular: var(--next-text-color-regular) !important;
--el-bg-color: var(--next-color-disabled) !important;
--el-color-primary-light-9: var(--next-color-hover) !important;
--el-text-color-disabled: var(--next-text-color-placeholder) !important;
--el-text-color-disabled-base: var(--el-color-primary) !important;
--el-text-color-placeholder: var(--next-text-color-placeholder) !important;
--el-disabled-bg-color: var(--next-color-disabled) !important;
--el-fill-base: var(--next-color-white) !important;
--el-fill-colo: var(--next-color-hover-rgba) !important;
--el-fill-color: var(--next-color-hover-rgba) !important;
--el-fill-color-blank: var(--next-color-disabled) !important;
--el-fill-color-light: var(--next-color-hover-rgba) !important;
--el-bg-color-overlay: var(--el-color-primary-light-9) !important;
--el-mask-color: rgb(42 42 42 / 80%);
--el-fill-color-lighter: var(--next-color-hover-rgba) !important;
// button
.el-button {
&:hover {
border-color: var(--next-border-color) !important;
}
}
.el-button--primary,
.el-button--info,
.el-button--danger,
.el-button--success,
.el-button--warning {
--el-button-text-color: var(--next-color-white) !important;
--el-button-hover-text-color: var(--next-color-white) !important;
--el-button-disabled-text-color: var(--next-color-white) !important;
&:hover {
border-color: var(--el-button-hover-border-color, var(--el-button-hover-bg-color)) !important;
}
}
// drawer
.el-divider__text {
background-color: var(--el-color-white) !important;
}
.el-drawer {
border-left: 1px solid var(--next-border-color-light) !important;
}
// tabs
.el-tabs--border-card {
background-color: var(--el-color-white) !important;
}
.el-tabs--border-card > .el-tabs__header .el-tabs__item.is-active {
background: var(--next-color-primary-lighter);
}
// alert / notice-bar
.home-card-item {
border: 1px solid var(--next-border-color-light) !important;
}
.el-alert,
.notice-bar {
border: 1px solid var(--next-border-color) !important;
background-color: var(--next-color-disabled) !important;
}
// topBar
.layout-navbars-breadcrumb-index {
background: none !important;
}
--prev-bg-menuBar: #191919 !important;
--prev-bg-menuBarColor: #dadada !important;
--prev-bg-topBar: #191919 !important;
--prev-bg-topBarColor: #dadada !important;
--prev-bg-columnsMenuBar: #191919 !important;
--prev-bg-columnsMenuBarColor: #dadada !important;
--prev-bg-main-color: #1f1f1f !important;
--prev-bg-color: rgba(0, 0, 0, 0.3) !important;
--prev-bg-white: #191919 !important;
--prev-color-text-black: #ffffff !important;
--prev-color-text-primary: #dadada !important;
--prev-color-text-regular: #dadada !important;
--prev-color-text-secondary: #a3a3a3 !important;
--prev-color-hover: rgba(0, 0, 0, 0.3) !important;
--prev-color-seting-main: #505050 !important;
--prev-color-seting-aside: #3c3c3c !important;
--prev-color-seting-header: #303030 !important;
--prev-border-color-hover: #616161 !important;
--prev-border-color-base: #333333 !important;
--prev-border-color-light: #333333 !important;
--prev-border-color-lighter: #333333 !important;
--prev-border-color-extra-light: #333333 !important;
// menu
.layout-aside {
border-right: 1px solid var(--next-border-color-light) !important;
@extend .layout-navbars-breadcrumb-index;
border-right: 1px solid var(--prev-border-color-lighter) !important;
}
// colorPicker
.el-color-picker__mask {
background: unset !important;
}
.el-color-picker__trigger {
border: 1px solid var(--next-border-color-light) !important;
// drawer
.el-drawer {
border-left: 1px solid var(--prev-border-color-lighter) !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);
// button
.el-button--default {
background: var(--prev-bg-white);
color: var(--prev-color-text-primary);
border-color: var(--prev-border-color-lighter);
&:hover,
&:focus {
color: var(--prev-color-primary) !important;
background: var(--prev-color-primary-light-8) !important;
border-color: var(--prev-color-primary-light-6) !important;
}
a {
color: var(--el-text-color-primary) !important;
&:focus {
border-color: var(--prev-color-primary-light-1) !important;
}
&:active {
border-color: var(--prev-color-primary-light-6) !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;
.el-tag.el-tag--info {
background-color: var(--prev-bg-white) !important;
border-color: var(--prev-border-color-light) !important;
color: var(--prev-color-text-regular) !important;
}
// pagination
.el-pagination.is-background .el-pager li:not(.disabled).active {
color: var(--next-color-white) !important;
}
.el-pagination.is-background .btn-next,
.el-pagination.is-background .btn-prev,
.el-pagination.is-background .el-pager li {
background-color: var(--next-bg-color);
}
/*深色模式时分页高亮问题*/
.el-pagination.is-background .btn-next.is-active,
.el-pagination.is-background .btn-prev.is-active,
.el-pagination.is-background .el-pager li.is-active {
color: var(--next-color-white) !important;
}
// radio
.el-radio-button:not(.is-active) .el-radio-button__inner {
border: 1px solid var(--next-border-color-light) !important;
border-left: 0 !important;
}
.el-radio-button.is-active .el-radio-button__inner {
color: var(--next-color-white) !important;
}
// countup
.countup-card-item-flex {
color: var(--el-text-color-primary) !important;
}
// editor
.editor-container {
.w-e-toolbar {
background: var(--el-color-white) !important;
border: 1px solid var(--next-border-color-light) !important;
.w-e-menu:hover {
background: var(--next-color-user-hover) !important;
i {
color: var(--el-text-color-primary) !important;
}
}
}
.w-e-text-container {
border: 1px solid var(--next-border-color-light) !important;
border-top: none !important;
.w-e-text {
background: var(--el-color-white) !important;
}
// switch
.el-switch:not(.is-checked) {
.el-switch__core {
border-color: var(--prev-border-color-base) !important;
background-color: var(--prev-border-color-base) !important;
}
}
// date-picker
.el-picker-panel {
background: var(--el-color-white) !important;
// TimePicker
.el-time-spinner__item.active:not(.disabled) {
color: var(--prev-color-primary) !important;
}
// dialog
.el-dialog {
border: 1px solid var(--el-border-color-lighter);
.el-dialog__header {
color: var(--el-text-color-primary) !important;
}
// date
.el-date-table td.in-range div,
.el-date-table td.in-range div:hover,
.el-date-table.is-week-mode .el-date-table__row.current div,
.el-date-table.is-week-mode .el-date-table__row:hover div,
.el-date-table td.selected div,
.el-month-table td.in-range div,
.el-month-table td.in-range div:hover {
background-color: var(--prev-bg-color) !important;
}
// columns
.layout-columns-aside ul .layout-columns-active {
color: var(--next-color-white) !important;
}
.layout-columns-aside {
border-right: 1px solid var(--next-border-columns);
}
// tagsView
.tags-style-one {
.is-active {
color: var(--el-text-color-primary) !important;
}
.layout-navbars-tagsview-ul-li:hover {
border-color: var(--el-border-color-lighter) !important;
}
// transfer
.el-transfer-panel,
.el-transfer-panel .el-transfer-panel__header {
background-color: var(--prev-bg-color) !important;
}
// loading
.el-loading-mask {
background-color: var(--next-bg-main) !important;
background-color: var(--prev-bg-color) !important;
}
// dropdown
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
background-color: var(--prev-color-hover) !important;
}
// dialog
.el-dialog,
.el-calendar {
border: 1px solid var(--prev-border-color-lighter);
}
}

View File

@ -1,13 +1,35 @@
@import 'mixins/index.scss';
/* 防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)
------------------------------- */
.el-scrollbar {
overflow: hidden;
position: relative;
height: 100%;
}
.el-scrollbar__wrap {
overflow: auto !important;
overflow-x: hidden !important;
max-height: 100%; /*防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)*/
}
.el-select-dropdown .el-scrollbar__wrap {
overflow-x: scroll !important;
}
.el-select-dropdown__wrap {
max-height: 274px !important; /*修复Select 选择器高度问题*/
}
.el-autocomplete-suggestion__wrap {
max-height: 280px !important;
}
/* Button 按钮
------------------------------- */
// 第三方字体图标大小
.el-button:not(.is-circle) i.el-icon,
.el-button i.iconfont,
.el-button i.fa,
.el-button--default i.iconfont,
.el-button--default i.fa {
.el-button i.fa {
font-size: 14px !important;
margin-right: 5px;
}
.el-button--medium i.iconfont,
.el-button--medium i.fa {
font-size: 14px !important;
margin-right: 5px;
}
@ -16,237 +38,23 @@
font-size: 12px !important;
margin-right: 5px;
}
/* Input 输入框、InputNumber 计数器
------------------------------- */
// 菜单搜索
.el-autocomplete-suggestion__wrap {
max-height: 280px !important;
}
/* Form 表单
------------------------------- */
.el-form {
// 用于修改弹窗时表单内容间隔太大问题,如系统设置的新增菜单弹窗里的表单内容
.el-form-item:last-of-type {
margin-bottom: 0 !important;
}
// 修复行内表单最后一个 el-form-item 位置下移问题
&.el-form--inline {
.el-form-item--large.el-form-item:last-of-type {
margin-bottom: 22px !important;
}
.el-form-item--default.el-form-item:last-of-type,
.el-form-item--small.el-form-item:last-of-type {
margin-bottom: 18px !important;
}
}
// https://gitee.com/lyt-top/vue-next-admin/issues/I5K1PM
.el-form-item .el-form-item__label .el-icon {
margin-right: 0px;
}
}
/* 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-bg-menuBarActiveColor) !important;
}
// 默认样式修改
.el-menu {
border-right: none !important;
width: 220px;
}
.el-menu-item {
height: 56px !important;
line-height: 56px !important;
}
.el-menu-item,
.el-sub-menu__title {
color: var(--next-bg-menuBarColor);
}
// 修复点击左侧菜单折叠再展开时,宽度不跟随问题
.el-menu--collapse {
width: 64px !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;
}
// 水平菜单、横向菜单高亮 背景色,鼠标 hover 时,有子级菜单的背景色
.el-menu-item.is-active,
.el-sub-menu.is-active .el-sub-menu__title,
.el-sub-menu:not(.is-opened):hover .el-sub-menu__title {
@extend .el-menu-hover-bg-color;
}
.el-menu-item:hover {
@extend .el-menu-hover-bg-color;
}
.el-sub-menu.is-active.is-opened .el-sub-menu__title {
background-color: unset !important;
}
// 子级菜单背景颜色
// .el-menu--inline {
// background: var(--next-bg-menuBar-light-1);
// }
// 水平菜单、横向菜单折叠 a 标签
.el-popper.is-dark a {
color: var(--el-color-white) !important;
text-decoration: none;
}
// 水平菜单、横向菜单折叠背景色
.el-popper.is-pure.is-light {
// 水平菜单
.el-menu--vertical {
background: var(--next-bg-menuBar);
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
.el-popper.is-pure.is-light {
.el-menu--vertical {
.el-sub-menu .el-sub-menu__title {
background-color: unset !important;
color: var(--next-bg-menuBarColor);
}
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
}
// 横向菜单
.el-menu--horizontal {
background: var(--next-bg-topBar);
.el-menu-item,
.el-sub-menu {
height: 48px !important;
line-height: 48px !important;
color: var(--next-bg-topBarColor);
.el-sub-menu__title {
height: 48px !important;
line-height: 48px !important;
color: var(--next-bg-topBarColor);
}
.el-popper.is-pure.is-light {
.el-menu--horizontal {
.el-sub-menu .el-sub-menu__title {
background-color: unset !important;
color: var(--next-bg-topBarColor);
}
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
}
.el-menu-item.is-active,
.el-sub-menu.is-active .el-sub-menu__title {
color: var(--el-menu-active-color);
}
}
}
// 横向菜单(经典、横向)布局
.el-menu.el-menu--horizontal {
border-bottom: none !important;
width: 100% !important;
.el-menu-item,
.el-sub-menu__title {
height: 48px !important;
color: var(--next-bg-topBarColor);
}
.el-menu-item:not(.is-active):hover,
.el-sub-menu:not(.is-active):hover .el-sub-menu__title {
color: var(--next-bg-topBarColor);
}
}
// 菜单收起时,图标不居中问题
.el-menu--collapse {
.el-menu-item .iconfont,
.el-sub-menu .iconfont,
.el-menu-item .fa,
.el-sub-menu .fa {
margin-right: 0 !important;
}
.el-sub-menu__title {
padding-right: 0 !important;
}
}
/* 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;
&:not(.is-disabled):hover {
background-color: var(--el-dropdown-menuItem-hover-fill);
color: var(--el-dropdown-menuItem-hover-color);
}
}
/* Steps 步骤条
------------------------------- */
.el-step__icon-inner {
font-size: 30px !important;
font-weight: 400 !important;
}
.el-step__title {
font-size: 14px;
.el-button--mini i.iconfont,
.el-button--mini i.fa {
font-size: 12px !important;
margin-right: 5px;
}
/* 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-overlay,
.el-dialog__wrapper {
display: flex;
align-items: center;
justify-content: center;
.el-dialog {
margin: 0 auto !important;
.el-dialog__body {
padding: 20px !important;
}
}
}
@ -256,78 +64,156 @@
overflow-x: hidden;
}
/* Card 卡片
/* Alert 警告
------------------------------- */
.el-card__header {
padding: 15px 20px;
.el-alert--warning.is-light {
border: 1px solid rgba(230, 162, 60, 0.3) !important;
}
.el-alert--success.is-light {
border: 1px solid rgba(103, 194, 58, 0.3) !important;
}
.el-alert--info.is-light {
border: 1px solid rgba(144, 147, 153, 0.3) !important;
}
.el-alert--error.is-light {
border: 1px solid rgba(245, 108, 108, 0.3) !important;
}
/* Table 表格 element plus 2.2.0 版本
/* Table 表格
------------------------------- */
.el-table {
.el-button.is-text {
padding: 0;
.el-table-column--selection {
.el-checkbox {
margin-right: unset !important;
}
}
.el-table::before,
.el-table--group::after,
.el-table--border::after {
z-index: 99 !important;
}
/* scrollbar
/* 下拉选择器/时间选择器滚动条
------------------------------- */
.el-scrollbar__bar {
z-index: 4;
}
/*防止页面切换时,滚动条高度不变的问题(滚动条高度非滚动条滚动高度)*/
.el-scrollbar__wrap {
max-height: 100%;
}
.el-select-dropdown .el-scrollbar__wrap {
.el-select-dropdown .el-scrollbar__wrap,
.el-picker-panel .el-scrollbar__wrap {
overflow-x: scroll !important;
}
/*修复Select 选择器高度问题*/
.el-select-dropdown__wrap {
max-height: 274px !important;
}
/*修复Cascader 级联选择器高度问题*/
.el-cascader-menu__wrap.el-scrollbar__wrap {
height: 204px !important;
}
/*用于界面高度自适应main.vue区分 scrollbar__view防止其它使用 scrollbar 的地方出现滚动条消失*/
.layout-container-view .el-scrollbar__view {
height: 100%;
}
/*防止分栏布局二级菜单很多时,滚动条消失问题*/
.layout-columns-warp .layout-aside .el-scrollbar__view {
height: unset !important;
}
/* Pagination 分页
/* NavMenu 导航菜单
------------------------------- */
.el-pagination__editor {
margin-right: 8px;
// 默认样式修改
.el-menu {
border-right: none !important;
}
/*深色模式时分页高亮问题*/
.el-pagination.is-background .btn-next.is-active,
.el-pagination.is-background .btn-prev.is-active,
.el-pagination.is-background .el-pager li.is-active {
background-color: var(--el-color-primary) !important;
color: var(--el-color-white) !important;
.el-menu-item,
.el-submenu__title {
height: 50px !important;
line-height: 50px !important;
color: var(--prev-bg-menuBarColor) !important;
transition: none !important;
}
/* 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);
color: var(--el-text-color-primary);
}
.el-drawer__body {
width: 100%;
height: 100%;
overflow: auto;
// horizontal 水平方向时
.el-menu--horizontal > .el-menu-item.is-active,
.el-menu--horizontal > .el-submenu.is-active .el-submenu__title {
border-bottom: 3px solid !important;
border-bottom-color: var(--prev-color-primary) !important;
color: var(--prev-color-primary) !important;
}
.el-menu--horizontal .el-menu-item:not(.is-disabled):focus,
.el-menu--horizontal .el-menu-item:not(.is-disabled):hover,
.el-menu--horizontal > .el-submenu:focus .el-submenu__title,
.el-menu--horizontal > .el-submenu:hover .el-submenu__title,
.el-menu--horizontal .el-menu .el-menu-item.is-active,
.el-menu--horizontal .el-menu .el-submenu.is-active > .el-submenu__title {
color: var(--prev-color-primary) !important;
}
.el-menu.el-menu--horizontal {
border-bottom: none !important;
}
.el-menu--horizontal > .el-menu-item,
.el-menu--horizontal > .el-submenu .el-submenu__title {
color: var(--bg-topBarColor) !important;
}
// 外部链接时
.el-menu-item a,
.el-menu-item a:hover,
.el-menu-item i,
.el-submenu__title i {
color: inherit;
text-decoration: none;
}
.el-menu-item a {
width: 86%;
display: inline-block;
}
// 默认 hover 时
.el-menu-item:hover,
.el-submenu__title:hover {
color: var(--prev-color-primary) !important;
background-color: transparent !important;
i {
color: var(--prev-color-primary) !important;
}
}
// 高亮时
.el-menu-item.is-active {
color: var(--prev-color-primary) !important;
}
.el-active-extend {
color: #ffffff !important;
background-color: var(--prev-color-primary) !important;
i {
color: #ffffff !important;
}
}
#add-is-active {
@extend .el-active-extend;
&:hover {
@extend .el-active-extend;
}
}
// 菜单收起时且是a链接
.is-dark a {
color: #ffffff !important;
text-decoration: none;
}
// 菜单收起时鼠标经过背景颜色/字体颜色
.el-menu--vertical {
background: var(--prev-bg-menuBar) !important;
}
.el-menu--horizontal {
.el-menu {
background: var(--bg-topBar) !important;
}
.el-menu-item,
.el-submenu__title {
color: var(--bg-topBarColor);
}
}
// 第三方图标字体间距/大小设置
.el-menu-item .iconfont,
.el-submenu .iconfont,
.el-menu-item .fa,
.el-submenu__title .fa {
font-size: 14px !important;
display: inline-block;
vertical-align: middle;
margin-right: 5px;
width: 24px;
text-align: center;
}
// element plus 本身字体图标
.el-submenu [class^='el-icon-'],
.el-menu-item [class^='el-icon-'] {
font-size: 14px !important;
}
// 去掉离开浏览器时,菜单的默认高亮
.el-menu-item:focus {
background-color: transparent !important;
}
/* Alert 警告
------------------------------- */
.el-alert__title {
word-break: break-all;
}

View File

@ -1,31 +0,0 @@
/* Popover 弹出框(图标选择器)
------------------------------- */
.icon-selector-popper {
padding: 0 !important;
.icon-selector-warp {
height: 260px;
overflow: hidden;
position: relative;
.icon-selector-warp-title {
position: absolute;
height: 40px;
line-height: 40px;
left: 15px;
}
.el-tabs__header {
display: flex;
justify-content: flex-end;
padding: 0 15px;
border-bottom: 1px solid var(--el-border-color-light);
margin: 0 !important;
.el-tabs__nav-wrap {
&::after {
height: 0 !important;
}
.el-tabs__item {
padding: 0 5px !important;
}
}
}
}
}

View File

@ -1,8 +1,7 @@
@import 'element-plus/dist/index.css';
@import './base.scss';
@import './app.scss';
@import 'common/transition.scss';
@import './other.scss';
@import './element.scss';
@import './media/media.scss';
@import './waves.scss';
@import './variables.scss';
@import './dark.scss';

View File

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

View File

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

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