Compare commits

...

67 Commits

Author SHA1 Message Date
xiaoxian521
f14077bc6f release: update 3.6.0 2022-10-25 17:51:21 +08:00
xiaoxian521
1003a31b4d chore: update 2022-10-22 11:55:15 +08:00
xiaoxian521
cc8a07d25b chore: update @pureadmin/utils latest 2022-10-19 17:08:05 +08:00
xiaoxian521
24db509381 chore: 同步完整版 2022-10-19 14:09:42 +08:00
xiaoxian521
43069ac127 chore: update extensions.json 2022-09-13 17:56:41 +08:00
xiaoxian521
f7ba97c73e chore: 优化样式 2022-09-11 19:54:00 +08:00
xiaoxian521
ee65ec9f72 perf: 使用/** */替换//注释,对编辑器的智能提示更友好 2022-09-11 16:46:21 +08:00
xiaoxian521
230af57c3e perf: 从tailwind.css中移除不常用的@apply 2022-09-11 11:35:13 +08:00
xiaoxian521
cdaa4cdba8 release: update 3.5.0 2022-09-10 21:53:46 +08:00
xiaoxian521
496947b524 chore: update 2022-09-09 00:04:29 +08:00
xiaoxian521
d48058c28f feat: 添加cssnano,打包时压缩css体积 2022-09-08 17:15:22 +08:00
xiaoxian521
820f724d5b chore: use pnpm shell-emulator instead of cross-env 2022-09-07 21:53:26 +08:00
xiaoxian521
63e2ddc171 chore: 简化登录页 2022-09-07 16:47:57 +08:00
xiaoxian521
60cdaf4697 refactor: use tailwindcss replace unocss 2022-09-07 16:04:26 +08:00
xiaoxian521
d3cabb0f33 fix: token过期,刷新死循环 2022-09-07 15:23:06 +08:00
xiaoxian521
97193a71a6 chore: update 2022-09-05 22:22:36 +08:00
xiaoxian521
f8694e953c chore: update 2022-09-03 14:06:56 +08:00
xiaoxian521
55f97fce96 chore: update 2022-08-31 11:33:44 +08:00
xiaoxian521
29144aba44 perf: 重置路由时,清空缓存页面 2022-08-24 16:11:41 +08:00
xiaoxian521
9a448143ae chore: window.process定义在index.html中,防止低版本浏览器报错 2022-08-24 09:48:16 +08:00
xiaoxian521
a85a2f9022 chore: 开启vscode括号对指南 2022-08-23 20:49:25 +08:00
xiaoxian521
20fee1418c style: update 2022-08-23 18:02:00 +08:00
xiaoxian521
2888f8c4f1 release: update 3.4.6 2022-08-23 10:43:33 +08:00
xiaoxian521
c07e60e114 release: update 3.4.5 2022-08-22 21:34:55 +08:00
xiaoxian521
13427998f3 chore: lock responsive-storage version 1.0.11 2022-08-09 13:19:55 +08:00
xiaoxian521
46355bf5bb chore: update @pureadmin/theme 2022-07-04 13:26:07 +08:00
xiaoxian521
d9b62a9e62 perf: 同步完整版代码 2022-05-11 16:43:56 +08:00
xiaoxian521
6911688ba6 docs: update 2022-05-11 16:18:53 +08:00
xiaoxian521
45c08d779d refactor: use unocss replace windicss 2022-05-07 11:50:06 +08:00
xiaoxian521
41b35588c5 perf: 同步完整版代码 2022-05-01 08:34:33 +08:00
xiaoxian521
0a9eb30549 perf: update 2022-04-25 22:42:55 +08:00
xiaoxian521
a7119c1cbe perf: icon 2022-04-25 19:37:23 +08:00
xiaoxian521
5a463ccfe7 docs: update 2022-04-25 18:54:38 +08:00
xiaoxian521
73e98814e2 perf: 同步完整版代码 2022-04-25 18:46:25 +08:00
xiaoxian521
77049fdbd7 perf: 同步完整版代码 2022-04-18 11:15:46 +08:00
xiaoxian521
736f1c27cd perf: 同步完整版代码 2022-04-10 21:22:05 +08:00
xiaoxian521
2bac78478c perf: 同步完整版代码 2022-04-08 12:05:46 +08:00
xiaoxian521
bc8a0f3b35 perf: 同步完整版代码 2022-04-06 13:42:49 +08:00
xiaoxian521
53e19f7971 chore: update 2022-03-29 09:56:49 +08:00
xiaoxian521
abc43dad6e chore: update 2022-03-26 17:36:05 +08:00
xiaoxian521
f80fbbed20 release: update 3.2.0 2022-03-22 00:37:24 +08:00
xiaoxian521
12c2365a26 perf: route rank is null 2022-03-17 19:53:19 +08:00
xiaoxian521
8a926c509f chore: update pnpm-lock 2022-03-17 19:07:07 +08:00
xiaoxian521
e87c38a9d2 perf: router rank 2022-03-17 19:00:01 +08:00
xiaoxian521
340a79d286 fix: router 2022-03-17 18:12:44 +08:00
xiaoxian521
45743a7c74 feat: use @pureadmin/theme 2022-03-17 15:45:11 +08:00
xiaoxian521
6887d4b1b8 chore: update 2022-03-17 12:14:22 +08:00
xiaoxian521
b850783ca7 chore: update dependencies 2022-03-17 11:28:54 +08:00
xiaoxian521
05e55ae9a1 perf: 同步完整版代码 2022-03-14 19:46:29 +08:00
xiaoxian521
f5b387231a perf: 同步完整版代码 2022-03-14 14:49:02 +08:00
xiaoxian521
79ebfb9284 perf: 同步完整版代码 2022-03-04 11:17:08 +08:00
xiaoxian521
51fd06c6a1 perf: 同步精简版代码 2022-03-03 23:30:08 +08:00
xiaoxian521
4bb8647990 chore: update vite latest 2022-03-01 11:28:35 +08:00
xiaoxian521
a43d5ce865 perf: 同步精简版代码 2022-03-01 10:58:55 +08:00
xiaoxian521
aea8605a60 perf: 同步完整版代码 2022-02-28 22:33:56 +08:00
xiaoxian521
eb9d1e8238 perf: 同步完整版 2022-02-27 13:31:19 +08:00
xiaoxian521
138e0fd2e4 fix: delete routerView 2022-02-23 15:21:26 +08:00
xiaoxian521
526023e0b0 fix: axios type 2022-02-18 14:55:04 +08:00
xiaoxian521
885cbf2d9f perf: 同步完整版分支代码 2022-02-18 11:52:12 +08:00
xiaoxian521
e161102495 perf: 同步完整版分支代码 2022-02-15 23:16:15 +08:00
xiaoxian521
5300781d05 docs: update 2022-02-10 13:15:53 +08:00
xiaoxian521
2e7e2ee3ce fix: epThemeColor error 2022-02-10 13:00:48 +08:00
xiaoxian521
aa165ff70b docs: update 2022-02-10 12:31:04 +08:00
xiaoxian521
b77ba43572 chore: update eslint@8.8.0 2022-02-07 16:20:22 +08:00
xiaoxian521
7cc69ac52d chore: update element-plus@2.0.0 2022-02-07 13:53:59 +08:00
xiaoxian521
1b93ce989c perf: 同步完整版分支代码 2022-02-05 17:42:39 +08:00
xiaoxian521
9e5fe5edbc feat: add build report 2022-02-05 15:32:38 +08:00
177 changed files with 8177 additions and 6270 deletions

View File

@@ -8,4 +8,4 @@ VITE_ROUTER_HISTORY = "hash"
VITE_PROXY_DOMAIN_REAL = "" VITE_PROXY_DOMAIN_REAL = ""
# 是否为打包后的文件提供传统浏览器兼容性支持 支持 true 不支持 false # 是否为打包后的文件提供传统浏览器兼容性支持 支持 true 不支持 false
VITE_LEGACY = false VITE_LEGACY = false

14
.env.staging Normal file
View File

@@ -0,0 +1,14 @@
# 预发布也需要生产环境的行为
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
NODE_ENV=production
VITE_PUBLIC_PATH = /
# 线上环境路由历史模式
VITE_ROUTER_HISTORY = "hash"
# 线上环境后端地址
VITE_PROXY_DOMAIN_REAL = ""
# 是否为打包后的文件提供传统浏览器兼容性支持 支持 true 不支持 false
VITE_LEGACY = false

View File

@@ -1,4 +1,10 @@
public public
dist dist
*.d.ts *.d.ts
package.json package.json
.eslintrc.js
.prettierrc.js
commitlint.config.js
postcss.config.js
tailwind.config.js
stylelint.config.js

View File

@@ -37,7 +37,7 @@ module.exports = {
"eslint:recommended", "eslint:recommended",
"@vue/typescript/recommended", "@vue/typescript/recommended",
"@vue/prettier", "@vue/prettier",
"@vue/prettier/@typescript-eslint" "@vue/eslint-config-typescript"
], ],
parser: "vue-eslint-parser", parser: "vue-eslint-parser",
parserOptions: { parserOptions: {
@@ -49,7 +49,34 @@ module.exports = {
jsx: true jsx: true
} }
}, },
overrides: [
{
files: ["*.ts", "*.vue"],
rules: {
"no-undef": "off"
}
},
{
files: ["*.vue"],
parser: "vue-eslint-parser",
parserOptions: {
parser: "@typescript-eslint/parser",
extraFileExtensions: [".vue"],
ecmaVersion: "latest",
ecmaFeatures: {
jsx: true
}
},
rules: {
"no-undef": "off"
}
}
],
rules: { rules: {
"vue/no-v-html": "off",
"vue/require-default-prop": "off",
"vue/require-explicit-emits": "off",
"vue/multi-word-component-names": "off",
"@typescript-eslint/no-explicit-any": "off", // any "@typescript-eslint/no-explicit-any": "off", // any
"no-debugger": "off", "no-debugger": "off",
"@typescript-eslint/explicit-module-boundary-types": "off", // setup() "@typescript-eslint/explicit-module-boundary-types": "off", // setup()
@@ -57,6 +84,18 @@ module.exports = {
"@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off",
"vue/html-self-closing": [
"error",
{
html: {
void: "always",
normal: "always",
component: "always"
},
svg: "always",
math: "always"
}
],
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": [
"error", "error",
{ {

2
.gitignore vendored
View File

@@ -4,6 +4,7 @@ dist
dist-ssr dist-ssr
*.local *.local
.eslintcache .eslintcache
report.html
yarn.lock yarn.lock
npm-debug.log* npm-debug.log*
@@ -17,3 +18,4 @@ tests/**/coverage/
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
tsconfig.tsbuildinfo

View File

@@ -1,6 +1,6 @@
module.exports = { module.exports = {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"{!(package)*.json,.!(browserslist)*rc}": ["prettier --write--parser json"], "{!(package)*.json}": ["prettier --write--parser json"],
"package.json": ["prettier --write"], "package.json": ["prettier --write"],
"*.vue": ["eslint --fix", "prettier --write", "stylelint --fix"], "*.vue": ["eslint --fix", "prettier --write", "stylelint --fix"],
"*.{vue,css,scss,postcss,less}": ["stylelint --fix", "prettier --write"], "*.{vue,css,scss,postcss,less}": ["stylelint --fix", "prettier --write"],

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
shamefully-hoist=true
strict-peer-dependencies=false
shell-emulator=true

View File

@@ -1,6 +1,5 @@
module.exports = { module.exports = {
bracketSpacing: true, bracketSpacing: true,
jsxBracketSameLine: true,
singleQuote: false, singleQuote: false,
arrowParens: "avoid", arrowParens: "avoid",
trailingComma: "none" trailingComma: "none"

View File

@@ -1,16 +1,18 @@
{ {
"recommendations": [ "recommendations": [
"johnsoncodehk.vscode-typescript-vue-plugin", "christian-kohler.path-intellisense",
"voorjaar.windicss-intellisense",
"vscode-icons-team.vscode-icons", "vscode-icons-team.vscode-icons",
"davidanson.vscode-markdownlint", "davidanson.vscode-markdownlint",
"stylelint.vscode-stylelint", "stylelint.vscode-stylelint",
"bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"johnsoncodehk.volar",
"lokalise.i18n-ally", "lokalise.i18n-ally",
"redhat.vscode-yaml",
"csstools.postcss",
"mikestead.dotenv", "mikestead.dotenv",
"eamodio.gitlens", "eamodio.gitlens",
"antfu.iconify" "antfu.iconify",
"Vue.volar"
] ]
} }

14
.vscode/settings.json vendored
View File

@@ -1,15 +1,12 @@
{ {
"editor.formatOnType": true, "editor.formatOnType": true,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"javascript.updateImportsOnFileMove.enabled": "always",
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[javascript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"editor.tabSize": 2, "editor.tabSize": 2,
"editor.formatOnPaste": true, "editor.formatOnPaste": true,
"editor.guides.bracketPairs": "active",
"files.autoSave": "afterDelay", "files.autoSave": "afterDelay",
"git.confirmSync": false, "git.confirmSync": false,
"workbench.startupEditor": "newUntitledFile", "workbench.startupEditor": "newUntitledFile",
@@ -30,14 +27,13 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "source.fixAll.eslint": true
}, },
"typescript.tsdk": "node_modules/typescript/lib", "i18n-ally.localesPaths": "locales",
"i18n-ally.localesPaths": ["src/plugins/i18n"],
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true, "i18n-ally.sortKeys": true,
"i18n-ally.namespace": true, "i18n-ally.namespace": true,
"i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}", "i18n-ally.enabledParsers": ["yaml", "js"],
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.sourceLanguage": "en", "i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN", "i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue", "react"] "i18n-ally.enabledFrameworks": ["vue"],
"iconify.excludes": ["el"]
} }

View File

@@ -6,7 +6,7 @@
## introduce ## introduce
The lite version is based on the shelf extracted from [vue-pure-admin](https://github.com/xiaoxian521/vue-pure-admin), which contains the main functions and is more suitable for actual project development, the packaged size is only more than `2MB` The lite version is based on the shelf extracted from [vue-pure-admin](https://github.com/xiaoxian521/vue-pure-admin), which contains the main functions and is more suitable for actual project development, the packaged size is only `3MB`, and will permanently sync the full version of the code
## Supporting Video ## Supporting Video
@@ -15,7 +15,9 @@ The lite version is based on the shelf extracted from [vue-pure-admin](https://g
## Docs ## Docs
- [Click Watch Docs](https://pure-admin-doc.vercel.app) - [Click me to view the domestic documentation site](http://yiming_chang.gitee.io/pure-admin-doc)
- [Click me to view foreign document site 1](https://xiaoxian521.github.io/pure-admin-doc)
- [Click me to view foreign document site 2](https://pure-admin-doc.vercel.app)
## Usage ## Usage
@@ -37,6 +39,11 @@ I think you should fork the project first to develop, so that you can pull the u
bilibili: https://www.bilibili.com/video/BV1534y1S7HV/ bilibili: https://www.bilibili.com/video/BV1534y1S7HV/
## ⚠️ Note ## ⚠️ Attention
The lite version does not accept any issues and prs. If you have any questions, please go to the full version https://github.com/xiaoxian521/vue-pure-admin/issues/new/choose to mention it, thank you! ! ! - The Lite version does not accept any issues and prs. If you have any questions, please go to the full version https://github.com/xiaoxian521/vue-pure-admin/issues/new/choose to mention, thank you! ! !
- Don't use the `delete-i18n` branch code, this branch is just for you to completely delete the internationalized references, it won't sync the code! ! ! [Completely remove the internationalization tutorial](https://www.bilibili.com/video/BV1Ru411B7k3/), please be sure to use the code from the `main` branch! ! !
## License
In principle, no fees and copyrights are charged, and you can use it with confidence, but if you need secondary open source, please contact the author for permission!

View File

@@ -6,7 +6,7 @@
## 介绍 ## 介绍
精简版是基于[vue-pure-admin](https://github.com/xiaoxian521/vue-pure-admin)提炼出的架子,包含主体功能,更适合实际项目开发,打包后的大小仅 `2MB` 精简版是基于 [vue-pure-admin](https://github.com/xiaoxian521/vue-pure-admin) 提炼出的架子,包含主体功能,更适合实际项目开发,打包后的大小仅 `3MB`,并且会永久同步完整版的代码
## 配套视频 ## 配套视频
@@ -15,7 +15,9 @@
## 配套文档 ## 配套文档
- [点我查看文档](https://pure-admin-doc.vercel.app) - [点我查看国内文档](http://yiming_chang.gitee.io/pure-admin-doc)
- [点我查看国外文档站 1](https://xiaoxian521.github.io/pure-admin-doc)
- [点我查看国外文档站 2](https://pure-admin-doc.vercel.app)
## 维护者 ## 维护者
@@ -23,15 +25,15 @@
## 捐赠 ## 捐赠
如果你觉得这个项目对有帮助,可以帮作者买一杯咖啡表示支持 如果你觉得这个项目对有帮助,可以帮作者买一杯果汁 🍹 表示支持
<img src="http://yiming_chang.gitee.io/manages/pay.jpg" width="150px" height="150px" /> <img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f69bf13c5b854ed5b699807cafa0e3ce~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp?" width="150px" height="150px" />
## 付费咨询、需求定制 ## QQ 交流群
作者精力有限,需要提供技术服务的可扫下面的二维码加微信,添加请备注来意 一群已满,下面是二群,群里严禁 `黄``赌``毒``vpn` 等违法行为!
<img src="http://yiming_chang.gitee.io/manages/wechat.jpg" width="150px" height="150px" /> <img src="http://yiming_chang.gitee.io/pure-admin-doc/img/support/qq.png" width="150px" height="225px" />
## 用法 ## 用法
@@ -47,8 +49,15 @@ pnpm add 包名
pnpm remove 包名 pnpm remove 包名
我认为你应该先 fork 项目去开发,以便我更新时可以同步拉取更新!!! 我认为你应该先 `fork` 项目去开发,以便我更新时可以同步拉取更新!!!
## ⚠️ 注意 ## ⚠️ 注意
精简版不接受任何 issues 和 pr,如果有问题请到完整版 https://github.com/xiaoxian521/vue-pure-admin/issues/new/choose 去提,谢谢!!! - 精简版不接受任何 `issues``pr`,如果有问题请到完整版 [issues](https://github.com/xiaoxian521/vue-pure-admin/issues/new/choose) 去提,谢谢!!!
- 不要使用 `delete-i18n` 分支代码,这个分支只是给你们完全删除国际化的参考,不会同步代码的!!! [完全删除国际化教程](https://www.bilibili.com/video/BV1Ru411B7k3/) ,请务必使用 `main` 分支的代码!!!
## 许可证
原则上不收取任何费用及版权,可以放心使用,不过如需二次开源(比如用此平台二次开发并开源)请联系作者获取许可!
[MIT © xiaoxian521-2020](./LICENSE)

View File

@@ -1,6 +1,6 @@
// 处理环境变量 /** 处理环境变量 */
const warpperEnv = (envConf: Recordable): ViteEnv => { const warpperEnv = (envConf: Recordable): ViteEnv => {
// 此处为默认值,无需修改 /** 此处为默认值,无需修改 */
const ret: ViteEnv = { const ret: ViteEnv = {
VITE_PORT: 8848, VITE_PORT: 8848,
VITE_PUBLIC_PATH: "", VITE_PUBLIC_PATH: "",
@@ -28,12 +28,12 @@ const warpperEnv = (envConf: Recordable): ViteEnv => {
return ret; return ret;
}; };
// 跨域代理重写 /** 跨域代理重写 */
const regExps = (value: string, reg: string): string => { const regExps = (value: string, reg: string): string => {
return value.replace(new RegExp(reg, "g"), ""); return value.replace(new RegExp(`^${reg}`, "g"), "");
}; };
// 环境变量 /** 环境变量 */
const loadEnv = (): ViteEnv => { const loadEnv = (): ViteEnv => {
return import.meta.env; return import.meta.env;
}; };

50
build/info.ts Normal file
View File

@@ -0,0 +1,50 @@
import type { Plugin } from "vite";
import dayjs, { Dayjs } from "dayjs";
import duration from "dayjs/plugin/duration";
import { green, blue, bold } from "picocolors";
import { getPackageSize } from "@pureadmin/utils";
dayjs.extend(duration);
export function viteBuildInfo(): Plugin {
let config: { command: string };
let startTime: Dayjs;
let endTime: Dayjs;
return {
name: "vite:buildInfo",
configResolved(resolvedConfig: { command: string }) {
config = resolvedConfig;
},
buildStart() {
console.log(
bold(
green(
`👏欢迎使用${blue(
"[vue-pure-admin]"
)}如果您感觉不错记得点击后面链接给个star哦💖 https://github.com/xiaoxian521/vue-pure-admin`
)
)
);
if (config.command === "build") {
startTime = dayjs(new Date());
}
},
closeBundle() {
if (config.command === "build") {
endTime = dayjs(new Date());
getPackageSize({
callback: (size: string) => {
console.log(
bold(
green(
`🎉恭喜打包完成(总用时${dayjs
.duration(endTime.diff(startTime))
.format("mm分ss秒")},打包后的大小为${size}`
)
)
);
}
});
}
}
};
}

View File

@@ -1,85 +1,52 @@
import { resolve } from "path";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import { viteBuildInfo } from "./info";
import svgLoader from "vite-svg-loader"; import svgLoader from "vite-svg-loader";
import legacy from "@vitejs/plugin-legacy"; import legacy from "@vitejs/plugin-legacy";
import vueJsx from "@vitejs/plugin-vue-jsx"; import vueJsx from "@vitejs/plugin-vue-jsx";
import WindiCSS from "vite-plugin-windicss";
import { viteMockServe } from "vite-plugin-mock"; import { viteMockServe } from "vite-plugin-mock";
import liveReload from "vite-plugin-live-reload"; import VueI18n from "@intlify/vite-plugin-vue-i18n";
import ElementPlus from "unplugin-element-plus/vite"; // import ElementPlus from "unplugin-element-plus/vite";
import { visualizer } from "rollup-plugin-visualizer";
import removeConsole from "vite-plugin-remove-console"; import removeConsole from "vite-plugin-remove-console";
import themePreprocessorPlugin from "@zougt/vite-plugin-theme-preprocessor"; import themePreprocessorPlugin from "@pureadmin/theme";
import { genScssMultipleScopeVars } from "/@/layout/theme";
import DefineOptions from "unplugin-vue-define-options/vite";
export function getPluginsList(command, VITE_LEGACY) { export function getPluginsList(command, VITE_LEGACY) {
const prodMock = true; const prodMock = true;
const lifecycle = process.env.npm_lifecycle_event;
return [ return [
vue(), vue(),
// https://github.com/intlify/bundle-tools/tree/main/packages/vite-plugin-vue-i18n
VueI18n({
runtimeOnly: true,
compositionOnly: true,
include: [resolve("locales/**")]
}),
// jsx、tsx语法支持 // jsx、tsx语法支持
vueJsx(), vueJsx(),
WindiCSS(), DefineOptions(),
// 线上环境删除console // 线上环境删除console
removeConsole(), removeConsole({ external: ["src/assets/iconfont/iconfont.js"] }),
// 修改layout文件夹下的文件时自动重载浏览器 解决 https://github.com/xiaoxian521/vue-pure-admin/issues/170 viteBuildInfo(),
liveReload(["src/layout/**/*", "src/router/**/*"]),
// 自定义主题 // 自定义主题
themePreprocessorPlugin({ themePreprocessorPlugin({
scss: { scss: {
multipleScopeVars: [ multipleScopeVars: genScssMultipleScopeVars(),
{
scopeName: "layout-theme-default",
path: "src/layout/theme/default-vars.scss"
},
{
scopeName: "layout-theme-light",
path: "src/layout/theme/light-vars.scss"
},
{
scopeName: "layout-theme-dusk",
path: "src/layout/theme/dusk-vars.scss"
},
{
scopeName: "layout-theme-volcano",
path: "src/layout/theme/volcano-vars.scss"
},
{
scopeName: "layout-theme-yellow",
path: "src/layout/theme/yellow-vars.scss"
},
{
scopeName: "layout-theme-mingQing",
path: "src/layout/theme/mingQing-vars.scss"
},
{
scopeName: "layout-theme-auroraGreen",
path: "src/layout/theme/auroraGreen-vars.scss"
},
{
scopeName: "layout-theme-pink",
path: "src/layout/theme/pink-vars.scss"
},
{
scopeName: "layout-theme-saucePurple",
path: "src/layout/theme/saucePurple-vars.scss"
}
],
// 默认取 multipleScopeVars[0].scopeName
defaultScopeName: "",
// 在生产模式是否抽取独立的主题css文件extract为true以下属性有效 // 在生产模式是否抽取独立的主题css文件extract为true以下属性有效
extract: true, extract: true,
// 独立主题css文件的输出路径默认取 viteConfig.build.assetsDir 相对于 (viteConfig.build.outDir)
outputDir: "",
// 会选取defaultScopeName对应的主题css文件在html添加link // 会选取defaultScopeName对应的主题css文件在html添加link
themeLinkTagId: "head", themeLinkTagId: "head",
// "head"||"head-prepend" || "body" ||"body-prepend" // "head"||"head-prepend" || "body" ||"body-prepend"
themeLinkTagInjectTo: "head", themeLinkTagInjectTo: "head",
// 是否对抽取的css文件内对应scopeName的权重类名移除 // 是否对抽取的css文件内对应scopeName的权重类名移除
removeCssScopeName: false, removeCssScopeName: false
// 可以自定义css文件名称的函数
customThemeCssFileName: scopeName => scopeName
} }
}), }),
// svg组件化支持 // svg组件化支持
svgLoader(), svgLoader(),
ElementPlus({}), // ElementPlus({}),
// mock支持 // mock支持
viteMockServe({ viteMockServe({
mockPath: "mock", mockPath: "mock",
@@ -89,7 +56,7 @@ export function getPluginsList(command, VITE_LEGACY) {
import { setupProdMockServer } from './mockProdServer'; import { setupProdMockServer } from './mockProdServer';
setupProdMockServer(); setupProdMockServer();
`, `,
logger: true logger: false
}), }),
// 是否为打包后的文件提供传统浏览器兼容性支持 // 是否为打包后的文件提供传统浏览器兼容性支持
VITE_LEGACY VITE_LEGACY
@@ -97,6 +64,10 @@ export function getPluginsList(command, VITE_LEGACY) {
targets: ["ie >= 11"], targets: ["ie >= 11"],
additionalLegacyPolyfills: ["regenerator-runtime/runtime"] additionalLegacyPolyfills: ["regenerator-runtime/runtime"]
}) })
: null,
// 打包分析
lifecycle === "report"
? visualizer({ open: true, brotliSize: true, filename: "report.html" })
: null : null
]; ];
} }

View File

@@ -2,9 +2,14 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="renderer" content="webkit" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<title>pure-admin-thin</title> <title>pure-admin-thin</title>
<link rel="icon" href="/favicon.ico" />
<script> <script>
window.process = {}; window.process = {};
</script> </script>
@@ -13,20 +18,16 @@
<body> <body>
<div id="app"> <div id="app">
<style> <style>
* {
margin: 0;
padding: 0;
}
html, html,
body { body,
#app {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
position: relative;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;
font-family: "Reggae One", cursive;
} }
.loader, .loader,
@@ -35,9 +36,7 @@
border-radius: 50%; border-radius: 50%;
width: 2.5em; width: 2.5em;
height: 2.5em; height: 2.5em;
-webkit-animation-fill-mode: both;
animation-fill-mode: both; animation-fill-mode: both;
-webkit-animation: loadAnimation 1.8s infinite ease-in-out;
animation: loadAnimation 1.8s infinite ease-in-out; animation: loadAnimation 1.8s infinite ease-in-out;
} }
@@ -47,11 +46,10 @@
margin: 80px auto; margin: 80px auto;
position: relative; position: relative;
text-indent: -9999em; text-indent: -9999em;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0); transform: translateZ(0);
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s; animation-delay: -0.16s;
top: 0;
transform: translate(-50%, 0);
} }
.loader:before, .loader:before,
@@ -63,7 +61,6 @@
.loader:before { .loader:before {
left: -3.5em; left: -3.5em;
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s; animation-delay: -0.32s;
} }
@@ -71,18 +68,6 @@
left: 3.5em; left: 3.5em;
} }
@-webkit-keyframes loadAnimation {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
@keyframes loadAnimation { @keyframes loadAnimation {
0%, 0%,
80%, 80%,
@@ -95,7 +80,7 @@
} }
} }
</style> </style>
<div class="loader">Loading...</div> <div class="loader"></div>
</div> </div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>

43
locales/en.yaml Normal file
View File

@@ -0,0 +1,43 @@
buttons:
hsLoginOut: LoginOut
hsfullscreen: FullScreen
hsexitfullscreen: ExitFullscreen
hsrefreshRoute: RefreshRoute
hslogin: Login
hsadd: Add
hsmark: Mark/Cancel
hssave: Save
hssearch: Search
hsexpendAll: Expand All
hscollapseAll: Collapse All
hssystemSet: Open ProjectConfig
hsdelete: Delete
hsreload: Reload
hscloseCurrentTab: Close CurrentTab
hscloseLeftTabs: Close LeftTabs
hscloseRightTabs: Close RightTabs
hscloseOtherTabs: Close OtherTabs
hscloseAllTabs: Close AllTabs
hswholeFullScreen: Whole FullScreen
hswholeExitFullScreen: Whole ExitFullScreen
hscontentFullScreen: Content FullScreen
hscontentExitFullScreen: Content ExitFullScreen
menus:
hshome: Home
hslogin: Login
hsabnormal: Abnormal Page
hsfourZeroFour: "404"
hsfourZeroOne: "403"
hsFive: "500"
permission: Permission Manage
permissionPage: Page Permission
permissionButton: Button Permission
status:
hsLoad: Loading...
login:
username: Username
password: Password
login: Login
usernameReg: Please enter username
passwordReg: Please enter password
passwordRuleReg: The password format should be any combination of 8-18 digits

43
locales/zh-CN.yaml Normal file
View File

@@ -0,0 +1,43 @@
buttons:
hsLoginOut: 退出系统
hsfullscreen: 全屏
hsexitfullscreen: 退出全屏
hsrefreshRoute: 刷新路由
hslogin: 登录
hsadd: 新增
hsmark: 标记/取消
hssave: 保存
hssearch: 搜索
hsexpendAll: 全部展开
hscollapseAll: 全部折叠
hssystemSet: 打开项目配置
hsdelete: 删除
hsreload: 重新加载
hscloseCurrentTab: 关闭当前标签页
hscloseLeftTabs: 关闭左侧标签页
hscloseRightTabs: 关闭右侧标签页
hscloseOtherTabs: 关闭其他标签页
hscloseAllTabs: 关闭全部标签页
hswholeFullScreen: 整体页面全屏
hswholeExitFullScreen: 整体页面退出全屏
hscontentFullScreen: 内容区全屏
hscontentExitFullScreen: 内容区退出全屏
menus:
hshome: 首页
hslogin: 登录
hsabnormal: 异常页面
hsfourZeroFour: "404"
hsfourZeroOne: "403"
hsFive: "500"
permission: 权限管理
permissionPage: 页面权限
permissionButton: 按钮权限
status:
hsLoad: 加载中...
login:
username: 账号
password: 密码
login: 登录
usernameReg: 请输入账号
passwordReg: 请输入密码
passwordRuleReg: 密码格式应为8-18位数字、字母、符号的任意两种组合

View File

@@ -1,59 +1,49 @@
// 根据角色动态生成路由 // 模拟后端动态生成路由
import { MockMethod } from "vite-plugin-mock"; import { MockMethod } from "vite-plugin-mock";
/**
* roles页面级别权限这里模拟二种 "admin"、"common"
* admin管理员角色
* common普通角色
*/
const permissionRouter = { const permissionRouter = {
path: "/permission", path: "/permission",
name: "permission",
redirect: "/permission/page/index",
meta: { meta: {
title: "menus.permission", title: "menus.permission",
icon: "lollipop", icon: "lollipop",
i18n: true, rank: 10
rank: 3
}, },
children: [ children: [
{ {
path: "/permission/page/index", path: "/permission/page/index",
name: "permissionPage", name: "PermissionPage",
meta: { meta: {
title: "menus.permissionPage", roles: ["admin", "common"],
i18n: true title: "menus.permissionPage"
} }
}, },
{ {
path: "/permission/button/index", path: "/permission/button/index",
name: "permissionButton", name: "PermissionButton",
meta: { meta: {
title: "menus.permissionButton", title: "menus.permissionButton",
i18n: true, roles: ["admin", "common"],
authority: [] auths: ["btn_add", "btn_edit", "btn_delete"]
} }
} }
] ]
}; };
// 添加不同按钮权限到/permission/button页面中
function setDifAuthority(authority, routes) {
routes.children[1].meta.authority = [authority];
return routes;
}
export default [ export default [
{ {
url: "/getAsyncRoutes", url: "/getAsyncRoutes",
method: "get", method: "get",
response: ({ query }) => { response: () => {
if (query.name === "admin") { return {
return { success: true,
code: 0, data: [permissionRouter]
info: [setDifAuthority("v-admin", permissionRouter)] };
};
} else {
return {
code: 0,
info: [setDifAuthority("v-test", permissionRouter)]
};
}
} }
} }
] as MockMethod[]; ] as MockMethod[];

36
mock/login.ts Normal file
View File

@@ -0,0 +1,36 @@
// 根据角色动态生成路由
import { MockMethod } from "vite-plugin-mock";
export default [
{
url: "/login",
method: "post",
response: ({ body }) => {
if (body.username === "admin") {
return {
success: true,
data: {
username: "admin",
// 一个用户可能有多个角色
roles: ["admin"],
accessToken: "eyJhbGciOiJIUzUxMiJ9.admin",
refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
expires: "2023/10/30 00:00:00"
}
};
} else {
return {
success: true,
data: {
username: "common",
// 一个用户可能有多个角色
roles: ["common"],
accessToken: "eyJhbGciOiJIUzUxMiJ9.common",
refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh",
expires: "2023/10/30 00:00:00"
}
};
}
}
}
] as MockMethod[];

27
mock/refreshToken.ts Normal file
View File

@@ -0,0 +1,27 @@
import { MockMethod } from "vite-plugin-mock";
// 模拟刷新token接口
export default [
{
url: "/refreshToken",
method: "post",
response: ({ body }) => {
if (body.refreshToken) {
return {
success: true,
data: {
accessToken: "eyJhbGciOiJIUzUxMiJ9.admin",
refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
// `expires`选择这种日期格式是为了方便调试,后端直接设置时间戳或许更方便(每次都应该递增)。如果后端返回的是时间戳格式,前端开发请来到这个目录`src/utils/auth.ts`,把第`38`行的代码换成expires = data.expires即可。
expires: "2023/10/30 23:59:59"
}
};
} else {
return {
success: false,
data: {}
};
}
}
}
] as MockMethod[];

View File

@@ -1,20 +1,20 @@
{ {
"name": "pure-admin-thin", "name": "pure-admin-thin",
"version": "2.9.0", "version": "3.6.0",
"private": true, "private": true,
"engines": {
"node": ">= 16",
"pnpm": ">= 6"
},
"scripts": { "scripts": {
"dev": "cross-env --max_old_space_size=4096 vite", "dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
"serve": "pnpm dev", "serve": "pnpm dev",
"build": "rimraf dist && cross-env vite build", "build": "rimraf dist && NODE_OPTIONS=--max-old-space-size=8192 vite build",
"build:staging": "rimraf dist && vite build --mode staging",
"report": "rimraf dist && vite build",
"preview": "vite preview", "preview": "vite preview",
"preview:build": "pnpm build && vite preview", "preview:build": "pnpm build && vite preview",
"typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
"cloc": "NODE_OPTIONS=--max-old-space-size=4096 cloc . --exclude-dir=node_modules --exclude-lang=YAML",
"clean:cache": "rm -rf node_modules && rm -rf .eslintcache && pnpm install", "clean:cache": "rm -rf node_modules && rm -rf .eslintcache && pnpm install",
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock}/**/*.{vue,ts,tsx}\" --fix", "lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
"lint:prettier": "prettier --write \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"", "lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{vue,css,scss,postcss,less}\" --cache --cache-location node_modules/.cache/stylelint/", "lint:stylelint": "stylelint --cache --fix \"**/*.{vue,css,scss,postcss,less}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js", "lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
"lint:pretty": "pretty-quick --staged", "lint:pretty": "pretty-quick --staged",
@@ -28,84 +28,109 @@
"not op_mini all" "not op_mini all"
], ],
"dependencies": { "dependencies": {
"@ctrl/tinycolor": "^3.4.0", "@ctrl/tinycolor": "^3.4.1",
"@vueuse/core": "^7.5.5", "@pureadmin/components": "^1.1.0",
"@vueuse/motion": "^2.0.0-beta.9", "@pureadmin/descriptions": "^1.1.0",
"@vueuse/shared": "^7.5.5", "@pureadmin/table": "^1.2.0",
"@pureadmin/utils": "^1.1.5",
"@vueuse/core": "^9.3.0",
"@vueuse/motion": "^2.0.0-beta.12",
"@vueuse/shared": "^9.1.1",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"axios": "^0.25.0", "axios": "^0.27.2",
"css-color-function": "^1.3.3", "dayjs": "^1.11.4",
"element-plus": "1.3.0-beta.1", "echarts": "^5.3.3",
"element-plus": "^2.2.16",
"element-resize-detector": "^1.2.3", "element-resize-detector": "^1.2.3",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lodash-unified": "^1.0.2",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"path": "^0.12.7", "path": "^0.12.7",
"pinia": "^2.0.11", "pinia": "^2.0.21",
"qs": "^6.10.2", "qs": "^6.11.0",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"responsive-storage": "^1.0.11", "responsive-storage": "^2.1.0",
"rgb-hex": "^4.0.0", "vue": "^3.2.40",
"vue": "^3.2.29", "vue-i18n": "^9.2.2",
"vue-i18n": "^9.2.0-beta.30", "vue-router": "^4.1.5",
"vue-router": "^4.0.12", "vue-types": "^4.2.1",
"vue-types": "^4.1.1" "vxe-table": "^4.3.2",
"xe-utils": "^3.5.6"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "13.1.0", "@commitlint/cli": "13.1.0",
"@commitlint/config-conventional": "13.1.0", "@commitlint/config-conventional": "13.1.0",
"@iconify-icons/ep": "^1.1.3", "@iconify-icons/ep": "^1.2.7",
"@iconify-icons/fa": "^1.1.1", "@iconify-icons/ri": "^1.2.3",
"@iconify-icons/fa-solid": "^1.1.2", "@iconify/vue": "^3.2.1",
"@iconify-icons/ri": "^1.1.1", "@intlify/vite-plugin-vue-i18n": "^6.0.3",
"@iconify/vue": "^3.1.3", "@pureadmin/theme": "^2.4.0",
"@types/element-resize-detector": "1.1.3", "@types/element-resize-detector": "1.1.3",
"@types/js-cookie": "^3.0.1", "@types/js-cookie": "^3.0.1",
"@types/lodash": "^4.14.180",
"@types/lodash-es": "^4.17.6",
"@types/mockjs": "1.0.3", "@types/mockjs": "1.0.3",
"@types/node": "14.14.14", "@types/node": "14.14.14",
"@types/nprogress": "0.2.0", "@types/nprogress": "0.2.0",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "4.31.0", "@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "4.31.0", "@typescript-eslint/parser": "^5.10.2",
"@vitejs/plugin-legacy": "^1.6.4", "@vitejs/plugin-legacy": "^2.2.0",
"@vitejs/plugin-vue": "^2.1.0", "@vitejs/plugin-vue": "^3.1.2",
"@vitejs/plugin-vue-jsx": "^1.3.3", "@vitejs/plugin-vue-jsx": "^2.0.1",
"@vue/eslint-config-prettier": "6.0.0", "@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "7.0.0", "@vue/eslint-config-typescript": "^10.0.0",
"@zougt/vite-plugin-theme-preprocessor": "^1.4.4", "@vue/runtime-core": "^3.2.40",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.12",
"cross-env": "7.0.3", "cloc": "^2.10.0",
"eslint": "7.30.0", "cssnano": "^5.1.13",
"eslint-plugin-prettier": "3.4.0", "eslint": "^8.8.0",
"eslint-plugin-vue": "7.17.0", "eslint-plugin-prettier": "^4.0.0",
"husky": "7.0.2", "eslint-plugin-vue": "^8.4.1",
"font-awesome": "^4.7.0",
"husky": "^7.0.4",
"lint-staged": "11.1.2", "lint-staged": "11.1.2",
"postcss": "8.2.6", "picocolors": "^1.0.0",
"postcss-import": "14.0.0", "postcss": "^8.4.17",
"prettier": "2.3.2", "postcss-html": "^1.5.0",
"postcss-import": "^15.0.0",
"postcss-scss": "^4.0.5",
"prettier": "^2.5.1",
"pretty-quick": "3.1.1", "pretty-quick": "3.1.1",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"sass": "^1.49.0", "rollup-plugin-visualizer": "^5.8.2",
"sass-loader": "^12.4.0", "sass": "^1.53.0",
"stylelint": "13.13.1", "sass-loader": "^13.0.2",
"stylelint-config-prettier": "8.0.2", "stylelint": "^14.3.0",
"stylelint-config-standard": "22.0.0", "stylelint-config-html": "^1.0.0",
"stylelint-order": "4.1.0", "stylelint-config-prettier": "^9.0.3",
"typescript": "^4.5.5", "stylelint-config-recommended": "^6.0.0",
"unplugin-element-plus": "^0.2.0", "stylelint-config-standard": "^24.0.0",
"vite": "^2.7.13", "stylelint-order": "^5.0.0",
"vite-plugin-live-reload": "^2.1.0", "tailwindcss": "^3.1.8",
"terser": "^5.15.0",
"typescript": "^4.7.4",
"unplugin-vue-define-options": "0.7.3",
"vite": "^3.1.8",
"vite-plugin-mock": "^2.9.6", "vite-plugin-mock": "^2.9.6",
"vite-plugin-remove-console": "^0.0.6", "vite-plugin-remove-console": "^1.1.0",
"vite-plugin-style-import": "^1.4.1", "vite-svg-loader": "^3.6.0",
"vite-plugin-windicss": "^1.6.3", "vue-eslint-parser": "^8.2.0",
"vite-svg-loader": "2.2.0", "vue-tsc": "^0.40.13"
"vue-eslint-parser": "7.10.0",
"windicss": "^3.4.3"
}, },
"repository": "git@github.com:xiaoxian521/vue-pure-admin.git", "pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"rollup",
"webpack"
]
}
},
"repository": "git@github.com:xiaoxian521/pure-admin-thin.git",
"author": "xiaoxian521", "author": "xiaoxian521",
"license": "MIT" "license": "MIT"
} }

4903
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,8 @@
module.exports = { module.exports = {
plugins: [require("autoprefixer"), require("postcss-import")] plugins: {
"postcss-import": {},
tailwindcss: {},
autoprefixer: {},
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {})
}
}; };

View File

@@ -1,5 +1,5 @@
{ {
"Version": "2.9.0", "Version": "3.6.0",
"Title": "PureAdmin", "Title": "PureAdmin",
"FixedHeader": true, "FixedHeader": true,
"HiddenSideBar": false, "HiddenSideBar": false,

View File

@@ -1,5 +1,10 @@
import { http } from "../utils/http"; import { http } from "../utils/http";
export const getAsyncRoutes = (params?: object) => { type Result = {
return http.request("get", "/getAsyncRoutes", { params }); success: boolean;
data: Array<any>;
};
export const getAsyncRoutes = () => {
return http.request<Result>("get", "/getAsyncRoutes");
}; };

View File

@@ -1,26 +1,39 @@
import { http } from "../utils/http"; import { http } from "../utils/http";
interface userType extends Promise<any> { export type UserResult = {
svg?: string; success: boolean;
code?: number; data: {
info?: object; /** 用户名 */
} username: string;
/** 当前登陆用户的角色 */
// 获取验证码 roles: Array<string>;
export const getVerify = (): userType => { /** `token` */
return http.request("get", "/captcha"); accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
};
}; };
// 登录 export type RefreshTokenResult = {
export const getLogin = (data: object) => { success: boolean;
return http.request("post", "/login", { data }); data: {
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
};
}; };
// 刷新token /** 登录 */
export const refreshToken = (data: object) => { export const getLogin = (data?: object) => {
return http.request("post", "/refreshToken", { data }); return http.request<UserResult>("post", "/login", { data });
}; };
// export const searchVague = (data: object) => { /** 刷新token */
// return http.request("post", "/searchVague", { data }); export const refreshTokenApi = (data?: object) => {
// }; return http.request<RefreshTokenResult>("post", "/refreshToken", { data });
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 36 36"><path d="M19.41 18l8.29-8.29a1 1 0 0 0-1.41-1.41L18 16.59l-8.29-8.3a1 1 0 0 0-1.42 1.42l8.3 8.29l-8.3 8.29A1 1 0 1 0 9.7 27.7l8.3-8.29l8.29 8.29a1 1 0 0 0 1.41-1.41z" fill="currentColor"></path></svg>

Before

Width:  |  Height:  |  Size: 395 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 36 36"><path d="M26 17H10a1 1 0 0 0 0 2h16a1 1 0 0 0 0-2z" fill="currentColor"></path></svg>

Before

Width:  |  Height:  |  Size: 279 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M7 12l7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 12l7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M21 12H7.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" ></path><path d="M3 3v18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></svg>

Before

Width:  |  Height:  |  Size: 647 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 20 20"><path d="M3 5h14V3H3v2zm12 8V7H5v6h10zM3 17h14v-2H3v2z" fill="currentColor"></path></svg>

Before

Width:  |  Height:  |  Size: 284 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g transform="translate(24 0) scale(-1 1)"><g fill="none"><path d="M7 12l7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M7 12l7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M21 12H7.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path><path d="M3 3v18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></g></g></svg>

Before

Width:  |  Height:  |  Size: 693 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--ant-design" width="20" height="20" preserveAspectRatio="xMidYMid meet" viewBox="0 0 1024 1024"><path fill="currentColor" d="M864 170h-60c-4.4 0-8 3.6-8 8v518H310v-73c0-6.7-7.8-10.5-13-6.3l-141.9 112a8 8 0 0 0 0 12.6l141.9 112c5.3 4.2 13 .4 13-6.3v-75h498c35.3 0 64-28.7 64-64V178c0-4.4-3.6-8-8-8z"></path></svg>

After

Width:  |  Height:  |  Size: 448 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--mdi" width="20" height="20" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1V7m10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2Z"></path></svg>

After

Width:  |  Height:  |  Size: 477 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 512 512"><path d="M400 148l-21.12-24.57A191.43 191.43 0 0 0 240 64C134 64 48 150 48 256s86 192 192 192a192.09 192.09 0 0 0 181.07-128" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="32"></path><path d="M464 68.45V220a4 4 0 0 1-4 4H308.45a4 4 0 0 1-2.83-6.83L457.17 65.62a4 4 0 0 1 6.83 2.83z" fill="currentColor"></path></svg>

Before

Width:  |  Height:  |  Size: 561 B

View File

@@ -0,0 +1,5 @@
import auth from "./src/auth";
const Auth = auth;
export { Auth };

View File

@@ -0,0 +1,20 @@
import { defineComponent, Fragment } from "vue";
import { hasAuth } from "/@/router/utils";
export default defineComponent({
name: "Auth",
props: {
value: {
type: undefined,
default: []
}
},
setup(props, { slots }) {
return () => {
if (!slots) return null;
return hasAuth(props.value) ? (
<Fragment>{slots.default?.()}</Fragment>
) : null;
};
}
});

View File

@@ -2,12 +2,11 @@ import iconifyIconOffline from "./src/iconifyIconOffline";
import iconifyIconOnline from "./src/iconifyIconOnline"; import iconifyIconOnline from "./src/iconifyIconOnline";
import fontIcon from "./src/iconfont"; import fontIcon from "./src/iconfont";
export const IconifyIconOffline = iconifyIconOffline; /** 离线图标组件 */
export const IconifyIconOnline = iconifyIconOnline; const IconifyIconOffline = iconifyIconOffline;
export const FontIcon = fontIcon; /** 在线图标组件 */
const IconifyIconOnline = iconifyIconOnline;
/** iconfont组件 */
const FontIcon = fontIcon;
export default { export { IconifyIconOffline, IconifyIconOnline, FontIcon };
IconifyIconOffline,
IconifyIconOnline,
FontIcon
};

View File

@@ -1,8 +1,14 @@
import { iconType } from "./types";
import { h, defineComponent, Component } from "vue"; import { h, defineComponent, Component } from "vue";
import { IconifyIconOffline, FontIcon } from "../index"; import { IconifyIconOnline, IconifyIconOffline, FontIcon } from "../index";
// 支持fontawesome4、5+、iconfont、remixicon、element-plus的icons、自定义svg /**
export function useRenderIcon(icon: string): Component { * 支持fontawesome4、5+、iconfont、remixicon、element-plus的icons、自定义svg
* @param icon 必传 图标
* @param attrs 可选 iconType 属性
* @returns Component
*/
export function useRenderIcon(icon: any, attrs?: iconType): Component {
// iconfont // iconfont
const ifReg = /^IF-/; const ifReg = /^IF-/;
// typeof icon === "function" 属于SVG // typeof icon === "function" 属于SVG
@@ -19,19 +25,23 @@ export function useRenderIcon(icon: string): Component {
render() { render() {
return h(FontIcon, { return h(FontIcon, {
icon: iconName, icon: iconName,
iconType iconType,
...attrs
}); });
} }
}); });
} else if (typeof icon === "function") { } else if (typeof icon === "function" || typeof icon?.render === "function") {
// svg // svg
return icon; return icon;
} else { } else {
return defineComponent({ return defineComponent({
name: "Icon", name: "Icon",
render() { render() {
return h(IconifyIconOffline, { const IconifyIcon =
icon: icon attrs && attrs["online"] ? IconifyIconOnline : IconifyIconOffline;
return h(IconifyIcon, {
icon: icon,
...attrs
}); });
} }
}); });

View File

@@ -2,7 +2,7 @@ import { h, defineComponent } from "vue";
// 封装iconfont组件默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code // 封装iconfont组件默认`font-class`引用模式,支持`unicode`引用、`font-class`引用、`symbol`引用 https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.20&helptype=code
export default defineComponent({ export default defineComponent({
name: "fontIcon", name: "FontIcon",
props: { props: {
icon: { icon: {
type: String, type: String,

View File

@@ -3,69 +3,65 @@ import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline";
// element-plus icon // element-plus icon
import Check from "@iconify-icons/ep/check"; import Check from "@iconify-icons/ep/check";
import Menu from "@iconify-icons/ep/menu";
import HomeFilled from "@iconify-icons/ep/home-filled"; import HomeFilled from "@iconify-icons/ep/home-filled";
import SetUp from "@iconify-icons/ep/set-up";
import Edit from "@iconify-icons/ep/edit";
import Setting from "@iconify-icons/ep/setting";
import Lollipop from "@iconify-icons/ep/lollipop"; import Lollipop from "@iconify-icons/ep/lollipop";
import Link from "@iconify-icons/ep/link";
import Position from "@iconify-icons/ep/position";
import Histogram from "@iconify-icons/ep/histogram";
import RefreshRight from "@iconify-icons/ep/refresh-right"; import RefreshRight from "@iconify-icons/ep/refresh-right";
import ArrowDown from "@iconify-icons/ep/arrow-down";
import Close from "@iconify-icons/ep/close"; import Close from "@iconify-icons/ep/close";
import CloseBold from "@iconify-icons/ep/close-bold"; import CloseBold from "@iconify-icons/ep/close-bold";
import Bell from "@iconify-icons/ep/bell"; import Bell from "@iconify-icons/ep/bell";
import Guide from "@iconify-icons/ep/guide"; import Search from "@iconify-icons/ep/search";
import User from "@iconify-icons/ep/user";
import Iphone from "@iconify-icons/ep/iphone";
import Location from "@iconify-icons/ep/location";
import Tickets from "@iconify-icons/ep/tickets";
import OfficeBuilding from "@iconify-icons/ep/office-building";
import Notebook from "@iconify-icons/ep/notebook";
addIcon("check", Check); addIcon("check", Check);
addIcon("menu", Menu);
addIcon("home-filled", HomeFilled); addIcon("home-filled", HomeFilled);
addIcon("set-up", SetUp);
addIcon("edit", Edit);
addIcon("setting", Setting);
addIcon("lollipop", Lollipop); addIcon("lollipop", Lollipop);
addIcon("link", Link);
addIcon("position", Position);
addIcon("histogram", Histogram);
addIcon("refresh-right", RefreshRight); addIcon("refresh-right", RefreshRight);
addIcon("arrow-down", ArrowDown);
addIcon("close", Close); addIcon("close", Close);
addIcon("close-bold", CloseBold); addIcon("close-bold", CloseBold);
addIcon("bell", Bell); addIcon("bell", Bell);
addIcon("guide", Guide); addIcon("search", Search);
addIcon("user", User);
addIcon("iphone", Iphone);
addIcon("location", Location);
addIcon("tickets", Tickets);
addIcon("office-building", OfficeBuilding);
addIcon("notebook", Notebook);
// remixicon // remixicon
import arrowRightSLine from "@iconify-icons/ri/arrow-right-s-line"; import ArrowRightSLine from "@iconify-icons/ri/arrow-right-s-line";
import arrowLeftSLine from "@iconify-icons/ri/arrow-left-s-line"; import ArrowLeftSLine from "@iconify-icons/ri/arrow-left-s-line";
import logoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line"; import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
addIcon("arrow-right-s-line", arrowRightSLine); import InformationLine from "@iconify-icons/ri/information-line";
addIcon("arrow-left-s-line", arrowLeftSLine); import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
addIcon("logout-circle-r-line", logoutCircleRLine); import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
import Bookmark2Line from "@iconify-icons/ri/bookmark-2-line";
// Font Awesome 4 import User from "@iconify-icons/ri/user-3-fill";
import faUser from "@iconify-icons/fa/user"; import Lock from "@iconify-icons/ri/lock-fill";
import faLock from "@iconify-icons/fa/lock"; import MenuUnfold from "@iconify-icons/ri/menu-unfold-fill";
import faSignOut from "@iconify-icons/fa/sign-out"; import MenuFold from "@iconify-icons/ri/menu-fold-fill";
addIcon("fa-user", faUser); import Setting from "@iconify-icons/ri/settings-3-line";
addIcon("fa-lock", faLock); import ArrowDown from "@iconify-icons/ri/arrow-down-s-line";
addIcon("fa-sign-out", faSignOut); import CloseLeftTags from "@iconify-icons/ri/text-direction-r";
import CloseRightTags from "@iconify-icons/ri/text-direction-l";
import CloseOtherTags from "@iconify-icons/ri/text-spacing";
import CloseAllTags from "@iconify-icons/ri/subtract-line";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
addIcon("arrow-right-s-line", ArrowRightSLine);
addIcon("arrow-left-s-line", ArrowLeftSLine);
addIcon("logout-circle-r-line", LogoutCircleRLine);
addIcon("information-line", InformationLine);
addIcon("arrow-up-line", ArrowUpLine);
addIcon("arrow-down-line", ArrowDownLine);
addIcon("bookmark-2-line", Bookmark2Line);
addIcon("user", User);
addIcon("lock", Lock);
addIcon("menu-unfold", MenuUnfold);
addIcon("menu-fold", MenuFold);
addIcon("setting", Setting);
addIcon("arrow-down", ArrowDown);
addIcon("close-left-tags", CloseLeftTags);
addIcon("close-right-tags", CloseRightTags);
addIcon("close-other-tags", CloseOtherTags);
addIcon("close-all-tags", CloseAllTags);
addIcon("fullscreen", Fullscreen);
addIcon("exit-fullscreen", ExitFullscreen);
// Iconify Icon在Vue里离线使用用于内网环境https://docs.iconify.design/icon-components/vue/offline.html // Iconify Icon在Vue里离线使用用于内网环境https://docs.iconify.design/icon-components/vue/offline.html
export default defineComponent({ export default defineComponent({
name: "IconifyIcon", name: "IconifyIconOffline",
components: { IconifyIcon }, components: { IconifyIcon },
props: { props: {
icon: { icon: {
@@ -79,6 +75,9 @@ export default defineComponent({
IconifyIcon, IconifyIcon,
{ {
icon: `${this.icon}`, icon: `${this.icon}`,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs ...attrs
}, },
{ {

View File

@@ -1,19 +1,14 @@
import { h, defineComponent } from "vue"; import { h, defineComponent } from "vue";
import { Icon as IconifyIcon } from "@iconify/vue"; import { Icon as IconifyIcon } from "@iconify/vue";
// Iconify Icon在Vue里在线使用用于外网环境 https://docs.iconify.design/icon-components/vue/offline.html // Iconify Icon在Vue里在线使用用于外网环境
export default defineComponent({ export default defineComponent({
name: "IconifyIcon", name: "IconifyIconOnline",
components: { IconifyIcon }, components: { IconifyIcon },
props: { props: {
icon: { icon: {
type: String, type: String,
default: "" default: ""
},
// default element plus icon
type: {
type: String,
default: "ep:"
} }
}, },
render() { render() {
@@ -21,7 +16,10 @@ export default defineComponent({
return h( return h(
IconifyIcon, IconifyIcon,
{ {
icon: `${this.type}${this.icon}`, icon: `${this.icon}`,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs ...attrs
}, },
{ {

View File

@@ -0,0 +1,20 @@
export interface iconType {
// iconify (https://docs.iconify.design/icon-components/vue/#properties)
inline?: boolean;
width?: string | number;
height?: string | number;
horizontalFlip?: boolean;
verticalFlip?: boolean;
flip?: string;
rotate?: number | string;
color?: string;
horizontalAlign?: boolean;
verticalAlign?: boolean;
align?: string;
online?: boolean;
onLoad?: Function;
includes?: Function;
// all icon
style?: object;
}

View File

@@ -27,7 +27,7 @@ const getConfig = (key?: string): ServerConfigs => {
return config; return config;
}; };
// 获取项目动态全局配置 /** 获取项目动态全局配置 */
export const getServerConfig = async (app: App): Promise<undefined> => { export const getServerConfig = async (app: App): Promise<undefined> => {
app.config.globalProperties.$config = getConfig(); app.config.globalProperties.$config = getConfig();
return axios({ return axios({

View File

@@ -0,0 +1,13 @@
import { hasAuth } from "/@/router/utils";
import { Directive, type DirectiveBinding } from "vue";
export const auth: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding;
if (value) {
!hasAuth(value) && el.parentNode.removeChild(el);
} else {
throw new Error("need auths! Like v-auth=\"['btn.add','btn.edit']\"");
}
}
};

View File

@@ -1,5 +1,4 @@
import { Directive } from "vue"; import { Directive, type DirectiveBinding, type VNode } from "vue";
import type { DirectiveBinding, VNode } from "vue";
import elementResizeDetectorMaker from "element-resize-detector"; import elementResizeDetectorMaker from "element-resize-detector";
import type { Erd } from "element-resize-detector"; import type { Erd } from "element-resize-detector";
import { emitter } from "/@/utils/mitt"; import { emitter } from "/@/utils/mitt";

View File

@@ -1,2 +1,2 @@
export * from "./permission"; export * from "./auth";
export * from "./elResizeDetector"; export * from "./elResizeDetector";

View File

@@ -1,18 +0,0 @@
import { usePermissionStoreHook } from "/@/store/modules/permission";
import { Directive } from "vue";
import type { DirectiveBinding } from "vue";
export const auth: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding;
if (value) {
const authRoles = value;
const hasAuth = usePermissionStoreHook().buttonAuth.includes(authRoles);
if (!hasAuth) {
el.parentNode.removeChild(el);
}
} else {
throw new Error("need roles! Like v-auth=\"['admin','test']\"");
}
}
};

View File

@@ -1,24 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { import { useGlobal } from "@pureadmin/utils";
h,
ref,
computed,
Transition,
defineComponent,
getCurrentInstance
} from "vue";
import { RouterView } from "vue-router";
import backTop from "/@/assets/svg/back_top.svg?component"; import backTop from "/@/assets/svg/back_top.svg?component";
import { h, computed, Transition, defineComponent } from "vue";
import { usePermissionStoreHook } from "/@/store/modules/permission"; import { usePermissionStoreHook } from "/@/store/modules/permission";
const props = defineProps({ const props = defineProps({
fixedHeader: Boolean fixedHeader: Boolean
}); });
const keepAlive: Boolean = ref(
getCurrentInstance().appContext.config.globalProperties.$config?.KeepAlive const { $storage, $config } = useGlobal<GlobalPropertiesApi>();
);
const instance = const keepAlive = computed(() => {
getCurrentInstance().appContext.app.config.globalProperties.$storage; return $config?.KeepAlive;
});
const transitions = computed(() => { const transitions = computed(() => {
return route => { return route => {
@@ -27,11 +21,11 @@ const transitions = computed(() => {
}); });
const hideTabs = computed(() => { const hideTabs = computed(() => {
return instance?.configure.hideTabs; return $storage?.configure.hideTabs;
}); });
const layout = computed(() => { const layout = computed(() => {
return instance?.layout.layout === "vertical"; return $storage?.layout.layout === "vertical";
}); });
const getSectionStyle = computed(() => { const getSectionStyle = computed(() => {

View File

@@ -1,121 +1,90 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import Search from "./search/index.vue";
import { useI18n } from "vue-i18n";
import { emitter } from "/@/utils/mitt";
import Notice from "./notice/index.vue"; import Notice from "./notice/index.vue";
import mixNav from "./sidebar/mixNav.vue";
import avatars from "/@/assets/avatars.jpg"; import avatars from "/@/assets/avatars.jpg";
import { transformI18n } from "/@/plugins/i18n"; import { useNav } from "/@/layout/hooks/useNav";
import Hamburger from "./sidebar/hamBurger.vue";
import { useRouter, useRoute } from "vue-router";
import { storageSession } from "/@/utils/storage";
import Breadcrumb from "./sidebar/breadCrumb.vue"; import Breadcrumb from "./sidebar/breadCrumb.vue";
import { useAppStoreHook } from "/@/store/modules/app"; import topCollapse from "./sidebar/topCollapse.vue";
import { unref, watch, getCurrentInstance } from "vue"; import { useTranslationLang } from "../hooks/useTranslationLang";
import { deviceDetection } from "/@/utils/deviceDetection";
import screenfull from "../components/screenfull/index.vue";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import globalization from "/@/assets/svg/globalization.svg?component"; import globalization from "/@/assets/svg/globalization.svg?component";
const instance = const {
getCurrentInstance().appContext.config.globalProperties.$storage; layout,
const pureApp = useAppStoreHook(); device,
const router = useRouter(); logout,
const route = useRoute(); onPanel,
let usename = storageSession.getItem("info")?.username; pureApp,
const { locale } = useI18n(); username,
avatarsStyle,
toggleSideBar,
getDropdownItemStyle,
getDropdownItemClass
} = useNav();
const getDropdownItemStyle = computed(() => { const { t, locale, translationCh, translationEn } = useTranslationLang();
return t => {
return {
background: locale.value === t ? useEpThemeStoreHook().epThemeColor : "",
color: locale.value === t ? "#f4f4f5" : "#000"
};
};
});
watch(
() => locale.value,
() => {
//@ts-ignore
document.title = transformI18n(
//@ts-ignore
unref(route.meta.title),
unref(route.meta.i18n)
); // 动态title
}
);
// 退出登录
const logout = (): void => {
storageSession.removeItem("info");
router.push("/login");
};
function onPanel() {
emitter.emit("openPanel");
}
function toggleSideBar() {
pureApp.toggleSideBar();
}
// 简体中文
function translationCh() {
instance.locale = { locale: "zh" };
locale.value = "zh";
}
// English
function translationEn() {
instance.locale = { locale: "en" };
locale.value = "en";
}
</script> </script>
<template> <template>
<div class="navbar"> <div
<Hamburger class="navbar bg-[#fff] shadow-sm shadow-[rgba(0, 21, 41, 0.08)] dark:shadow-[#0d0d0d]"
:is-active="pureApp.sidebar.opened" >
<topCollapse
v-if="device === 'mobile'"
class="hamburger-container" class="hamburger-container"
:is-active="pureApp.sidebar.opened"
@toggleClick="toggleSideBar" @toggleClick="toggleSideBar"
/> />
<Breadcrumb class="breadcrumb-container" /> <Breadcrumb
v-if="layout !== 'mix' && device !== 'mobile'"
class="breadcrumb-container"
/>
<div class="vertical-header-right"> <mixNav v-if="layout === 'mix'" />
<div v-if="layout === 'vertical'" class="vertical-header-right">
<!-- 菜单搜索 -->
<Search />
<!-- 通知 --> <!-- 通知 -->
<Notice id="header-notice" /> <Notice id="header-notice" />
<!-- 全屏 -->
<screenfull id="header-screenfull" v-show="!deviceDetection()" />
<!-- 国际化 --> <!-- 国际化 -->
<el-dropdown id="header-translation" trigger="click"> <el-dropdown id="header-translation" trigger="click">
<globalization /> <globalization
class="navbar-bg-hover w-[40px] h-[48px] p-[11px] cursor-pointer outline-none"
/>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="translation"> <el-dropdown-menu class="translation">
<el-dropdown-item <el-dropdown-item
:style="getDropdownItemStyle('zh')" :style="getDropdownItemStyle(locale, 'zh')"
:class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]"
@click="translationCh" @click="translationCh"
><IconifyIconOffline >
<IconifyIconOffline
class="check-zh" class="check-zh"
v-show="locale === 'zh'" v-show="locale === 'zh'"
icon="check" icon="check"
/>简体中文</el-dropdown-item />
> 简体中文
</el-dropdown-item>
<el-dropdown-item <el-dropdown-item
:style="getDropdownItemStyle('en')" :style="getDropdownItemStyle(locale, 'en')"
:class="['dark:!text-white', getDropdownItemClass(locale, 'en')]"
@click="translationEn" @click="translationEn"
><el-icon class="check-en" v-show="locale === 'en'"
><IconifyIconOffline icon="check" /></el-icon
>English</el-dropdown-item
> >
<span class="check-en" v-show="locale === 'en'">
<IconifyIconOffline icon="check" />
</span>
English
</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<!-- 退出登 --> <!-- 退出登 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<span class="el-dropdown-link"> <span class="el-dropdown-link navbar-bg-hover">
<img :src="avatars" /> <img v-if="avatars" :src="avatars" :style="avatarsStyle" />
<p>{{ usename }}</p> <p v-if="username" class="dark:text-white">{{ username }}</p>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="logout"> <el-dropdown-menu class="logout">
@@ -123,18 +92,19 @@ function translationEn() {
<IconifyIconOffline <IconifyIconOffline
icon="logout-circle-r-line" icon="logout-circle-r-line"
style="margin: 5px" style="margin: 5px"
/>{{ $t("buttons.hsLoginOut") }}</el-dropdown-item />
> {{ t("buttons.hsLoginOut") }}
</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<el-icon <span
class="el-icon-setting" class="set-icon navbar-bg-hover"
:title="$t('buttons.hssystemSet')" :title="t('buttons.hssystemSet')"
@click="onPanel" @click="onPanel"
> >
<IconifyIconOffline icon="setting" /> <IconifyIconOffline icon="setting" />
</el-icon> </span>
</div> </div>
</div> </div>
</template> </template>
@@ -144,16 +114,12 @@ function translationEn() {
width: 100%; width: 100%;
height: 48px; height: 48px;
overflow: hidden; overflow: hidden;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
.hamburger-container { .hamburger-container {
line-height: 48px; line-height: 48px;
height: 100%; height: 100%;
float: left; float: left;
cursor: pointer; cursor: pointer;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
} }
.vertical-header-right { .vertical-header-right {
@@ -164,33 +130,7 @@ function translationEn() {
color: #000000d9; color: #000000d9;
justify-content: flex-end; justify-content: flex-end;
:deep(.dropdown-badge) {
&:hover {
background: #f6f6f6;
}
}
.screen-full {
cursor: pointer;
&:hover {
background: #f6f6f6;
}
}
.globalization {
height: 48px;
width: 40px;
padding: 11px;
cursor: pointer;
&:hover {
background: #f6f6f6;
}
}
.el-dropdown-link { .el-dropdown-link {
width: 100px;
height: 48px; height: 48px;
padding: 10px; padding: 10px;
display: flex; display: flex;
@@ -199,10 +139,6 @@ function translationEn() {
cursor: pointer; cursor: pointer;
color: #000000d9; color: #000000d9;
&:hover {
background: #f6f6f6;
}
p { p {
font-size: 14px; font-size: 14px;
} }
@@ -213,35 +149,17 @@ function translationEn() {
border-radius: 50%; border-radius: 50%;
} }
} }
.el-icon-setting {
height: 48px;
width: 38px;
padding: 12px;
display: flex;
cursor: pointer;
align-items: center;
&:hover {
background: #f6f6f6;
}
}
} }
.breadcrumb-container { .breadcrumb-container {
float: left; float: left;
margin-left: 16px;
} }
} }
.translation { .translation {
.el-dropdown-menu__item { ::v-deep(.el-dropdown-menu__item) {
padding: 5px 40px !important; padding: 5px 40px;
}
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
} }
.check-zh { .check-zh {
@@ -258,16 +176,10 @@ function translationEn() {
.logout { .logout {
max-width: 120px; max-width: 120px;
.el-dropdown-menu__item { ::v-deep(.el-dropdown-menu__item) {
min-width: 100%; min-width: 100%;
display: inline-flex; display: inline-flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
}
} }
</style> </style>

View File

@@ -1,8 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import NoticeList from "./noticeList.vue";
import { noticesData } from "./data"; import { noticesData } from "./data";
import NoticeList from "./noticeList.vue";
import { templateRef } from "@vueuse/core";
import { Tabs, TabPane } from "@pureadmin/components";
const dropdownDom = templateRef<ElRef | null>("dropdownDom", null);
const activeName = ref(noticesData[0].name); const activeName = ref(noticesData[0].name);
const notices = ref(noticesData); const notices = ref(noticesData);
@@ -10,38 +13,50 @@ let noticesNum = ref(0);
notices.value.forEach(notice => { notices.value.forEach(notice => {
noticesNum.value += notice.list.length; noticesNum.value += notice.list.length;
}); });
function tabClick() {
(dropdownDom as any).value.handleOpen();
}
</script> </script>
<template> <template>
<el-dropdown trigger="click" placement="bottom-end"> <el-dropdown ref="dropdownDom" trigger="click" placement="bottom-end">
<span class="dropdown-badge"> <span class="dropdown-badge navbar-bg-hover select-none">
<el-badge :value="noticesNum" :max="99"> <el-badge :value="noticesNum" :max="99">
<el-icon class="header-notice-icon" <span class="header-notice-icon">
><IconifyIconOffline icon="bell" <IconifyIconOffline icon="bell" />
/></el-icon> </span>
</el-badge> </el-badge>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-tabs v-model="activeName" class="dropdown-tabs"> <Tabs
centered
class="dropdown-tabs"
v-model:activeName="activeName"
@tabClick="tabClick"
>
<template v-for="item in notices" :key="item.key"> <template v-for="item in notices" :key="item.key">
<el-tab-pane <TabPane :tab="`${item.name}(${item.list.length})`">
:label="`${item.name}(${item.list.length})`"
:name="item.name"
>
<el-scrollbar max-height="330px"> <el-scrollbar max-height="330px">
<div class="noticeList-container"> <div class="noticeList-container">
<NoticeList :list="item.list" /> <NoticeList :list="item.list" />
</div> </div>
</el-scrollbar> </el-scrollbar>
</el-tab-pane> </TabPane>
</template> </template>
</el-tabs> </Tabs>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
</template> </template>
<style>
.ant-tabs-dropdown {
z-index: 2900 !important;
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
.dropdown-badge { .dropdown-badge {
display: flex; display: flex;
@@ -79,4 +94,8 @@ notices.value.forEach(notice => {
padding: 15px 24px 0 24px; padding: 15px 24px 0 24px;
} }
} }
:deep(.ant-tabs-nav) {
margin-bottom: 0;
}
</style> </style>

View File

@@ -10,8 +10,8 @@ const props = defineProps({
}); });
const titleRef = ref(null); const titleRef = ref(null);
const descriptionRef = ref(null);
const titleTooltip = ref(false); const titleTooltip = ref(false);
const descriptionRef = ref(null);
const descriptionTooltip = ref(false); const descriptionTooltip = ref(false);
function hoverTitle() { function hoverTitle() {
@@ -44,15 +44,17 @@ function hoverDescription(event, description) {
</script> </script>
<template> <template>
<div class="notice-container"> <div
class="notice-container border-b-[1px] border-solid border-[#f0f0f0] dark:border-[#303030]"
>
<el-avatar <el-avatar
v-if="props.noticeItem.avatar" v-if="props.noticeItem.avatar"
:size="30" :size="30"
:src="props.noticeItem.avatar" :src="props.noticeItem.avatar"
class="notice-container-avatar" class="notice-container-avatar"
></el-avatar> />
<div class="notice-container-text"> <div class="notice-container-text">
<div class="notice-text-title"> <div class="notice-text-title text-[#000000d9] dark:text-white">
<el-tooltip <el-tooltip
popper-class="notice-title-popper" popper-class="notice-title-popper"
:disabled="!titleTooltip" :disabled="!titleTooltip"
@@ -72,7 +74,8 @@ function hoverDescription(event, description) {
:type="props.noticeItem?.status" :type="props.noticeItem?.status"
size="small" size="small"
class="notice-title-extra" class="notice-title-extra"
>{{ props.noticeItem?.extra }} >
{{ props.noticeItem?.extra }}
</el-tag> </el-tag>
</div> </div>
@@ -90,7 +93,7 @@ function hoverDescription(event, description) {
{{ props.noticeItem.description }} {{ props.noticeItem.description }}
</div> </div>
</el-tooltip> </el-tooltip>
<div class="notice-text-datetime"> <div class="notice-text-datetime text-[#00000073] dark:text-white">
{{ props.noticeItem.datetime }} {{ props.noticeItem.datetime }}
</div> </div>
</div> </div>
@@ -108,7 +111,7 @@ function hoverDescription(event, description) {
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
padding: 12px 0; padding: 12px 0;
border-bottom: 1px solid #f0f0f0; // border-bottom: 1px solid #f0f0f0;
.notice-container-avatar { .notice-container-avatar {
margin-right: 16px; margin-right: 16px;
@@ -127,7 +130,6 @@ function hoverDescription(event, description) {
font-weight: 400; font-weight: 400;
font-size: 14px; font-size: 14px;
line-height: 1.5715; line-height: 1.5715;
color: rgba(0, 0, 0, 0.85);
cursor: pointer; cursor: pointer;
.notice-title-content { .notice-title-content {
@@ -149,7 +151,6 @@ function hoverDescription(event, description) {
.notice-text-datetime { .notice-text-datetime {
font-size: 12px; font-size: 12px;
line-height: 1.5715; line-height: 1.5715;
color: rgba(0, 0, 0, 0.45);
} }
.notice-text-description { .notice-text-description {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { PropType } from "vue"; import { PropType } from "vue";
import NoticeItem from "./noticeItem.vue";
import { ListItem } from "./data"; import { ListItem } from "./data";
import NoticeItem from "./noticeItem.vue";
const props = defineProps({ const props = defineProps({
list: { list: {
@@ -17,7 +17,7 @@ const props = defineProps({
v-for="(item, index) in props.list" v-for="(item, index) in props.list"
:noticeItem="item" :noticeItem="item"
:key="index" :key="index"
></NoticeItem> />
</div> </div>
<el-empty v-else description="暂无数据"></el-empty> <el-empty v-else description="暂无数据" />
</template> </template>

View File

@@ -5,7 +5,7 @@ import { emitter } from "/@/utils/mitt";
let show = ref<Boolean>(false); let show = ref<Boolean>(false);
const target = ref(null); const target = ref(null);
onClickOutside(target, event => { onClickOutside(target, (event: any) => {
if (event.clientX > target.value.offsetLeft) return; if (event.clientX > target.value.offsetLeft) return;
show.value = false; show.value = false;
}); });
@@ -18,15 +18,21 @@ emitter.on("openPanel", () => {
<template> <template>
<div :class="{ show: show }" class="right-panel-container"> <div :class="{ show: show }" class="right-panel-container">
<div class="right-panel-background" /> <div class="right-panel-background" />
<div ref="target" class="right-panel"> <div ref="target" class="right-panel bg-bg_color">
<div class="right-panel-items"> <div class="right-panel-items">
<div class="project-configuration"> <div class="project-configuration">
<h3>项目配置</h3> <h3 class="dark:text-white">项目配置</h3>
<el-icon title="关闭配置" class="el-icon-close" @click="show = !show"> <span title="关闭配置">
<IconifyIconOffline icon="close" /> <IconifyIconOffline
</el-icon> class="dark:text-white"
icon="close"
@click="show = !show"
/>
</span>
</div> </div>
<div style="border-bottom: 1px solid #dcdfe6"></div> <div
class="border-b-[1px] border-solid border-[#dcdfe6] dark:border-[#303030]"
/>
<slot /> <slot />
</div> </div>
</div> </div>
@@ -54,7 +60,7 @@ emitter.on("openPanel", () => {
.right-panel { .right-panel {
width: 100%; width: 100%;
max-width: 300px; max-width: 315px;
height: 100vh; height: 100vh;
position: fixed; position: fixed;
top: 0; top: 0;
@@ -62,7 +68,7 @@ emitter.on("openPanel", () => {
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.05);
transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1); transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
transform: translate(100%); transform: translate(100%);
background: #fff; // background: #fff;
z-index: 40000; z-index: 40000;
} }
@@ -105,8 +111,8 @@ emitter.on("openPanel", () => {
.right-panel-items { .right-panel-items {
margin-top: 60px; margin-top: 60px;
height: 100vh; height: calc(100vh - 60px);
overflow: auto; overflow-y: auto;
} }
.project-configuration { .project-configuration {
@@ -119,13 +125,13 @@ emitter.on("openPanel", () => {
top: 15px; top: 15px;
margin-left: 10px; margin-left: 10px;
i { svg {
font-size: 20px; font-size: 20px;
margin-right: 20px; margin-right: 20px;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
color: red; color: var(--el-color-primary);
} }
} }
} }

View File

@@ -1,27 +1,21 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useFullscreen } from "@vueuse/core"; import { useFullscreen } from "@vueuse/core";
const { t } = useI18n();
const { isFullscreen, toggle } = useFullscreen(); const { isFullscreen, toggle } = useFullscreen();
</script> </script>
<template> <template>
<div class="screen-full" @click="toggle"> <div
class="screen-full w-[36px] h-[48px] flex-ac cursor-pointer navbar-bg-hover"
@click="toggle"
>
<FontIcon <FontIcon
:title=" :title="
isFullscreen isFullscreen ? t('buttons.hsexitfullscreen') : t('buttons.hsfullscreen')
? $t('buttons.hsexitfullscreen')
: $t('buttons.hsfullscreen')
" "
:icon="isFullscreen ? 'team-iconexit-fullscreen' : 'team-iconfullscreen'" :icon="isFullscreen ? 'team-iconexit-fullscreen' : 'team-iconfullscreen'"
/> />
</div> </div>
</template> </template>
<style lang="scss" scoped>
.screen-full {
width: 36px;
height: 48px;
display: flex;
align-items: center;
justify-content: space-around;
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<div class="search-footer text-[#333] dark:text-white">
<span class="search-footer-item">
<enterOutlined class="icon" />
确认
</span>
<span class="search-footer-item">
<IconifyIconOffline icon="arrow-up-line" class="icon" />
<IconifyIconOffline icon="arrow-down-line" class="icon" />
切换
</span>
<span class="search-footer-item">
<mdiKeyboardEsc class="icon" />
关闭
</span>
</div>
</template>
<script lang="ts" setup>
import enterOutlined from "/@/assets/svg/enter_outlined.svg?component";
import mdiKeyboardEsc from "/@/assets/svg/mdi_keyboard_esc.svg?component";
</script>
<style lang="scss" scoped>
.search-footer {
display: flex;
.search-footer-item {
display: flex;
align-items: center;
margin-right: 14px;
}
.icon {
padding: 2px;
margin-right: 3px;
font-size: 20px;
box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff,
0 1px 2px 1px #1e235a66;
}
}
</style>

View File

@@ -0,0 +1,173 @@
<script lang="ts" setup>
import { useRouter } from "vue-router";
import { cloneDeep } from "lodash-unified";
import SearchResult from "./SearchResult.vue";
import SearchFooter from "./SearchFooter.vue";
import { useNav } from "/@/layout/hooks/useNav";
import { transformI18n } from "/@/plugins/i18n";
import { deleteChildren } from "@pureadmin/utils";
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
import { ref, watch, computed, nextTick, shallowRef } from "vue";
import { usePermissionStoreHook } from "/@/store/modules/permission";
interface Props {
/** 弹窗显隐 */
value: boolean;
}
interface Emits {
(e: "update:value", val: boolean): void;
}
const { device } = useNav();
const emit = defineEmits<Emits>();
const props = withDefaults(defineProps<Props>(), {});
const router = useRouter();
const keyword = ref("");
const activePath = ref("");
const inputRef = ref<HTMLInputElement | null>(null);
const resultOptions = shallowRef([]);
const handleSearch = useDebounceFn(search, 300);
/** 菜单树形结构 */
const menusData = computed(() => {
return deleteChildren(cloneDeep(usePermissionStoreHook().wholeMenus));
});
const show = computed({
get() {
return props.value;
},
set(val: boolean) {
emit("update:value", val);
}
});
watch(show, async val => {
if (val) {
/** 自动聚焦 */
await nextTick();
inputRef.value?.focus();
}
});
/** 将菜单树形结构扁平化为一维数组,用于菜单查询 */
function flatTree(arr) {
const res = [];
function deep(arr) {
arr.forEach(item => {
res.push(item);
item.children && deep(item.children);
});
}
deep(arr);
return res;
}
/** 查询 */
function search() {
const flatMenusData = flatTree(menusData.value);
resultOptions.value = flatMenusData.filter(
menu =>
keyword.value &&
transformI18n(menu.meta?.title)
.toLocaleLowerCase()
.includes(keyword.value.toLocaleLowerCase().trim())
);
if (resultOptions.value?.length > 0) {
activePath.value = resultOptions.value[0].path;
} else {
activePath.value = "";
}
}
function handleClose() {
show.value = false;
/** 延时处理防止用户看到某些操作 */
setTimeout(() => {
resultOptions.value = [];
keyword.value = "";
}, 200);
}
/** key up */
function handleUp() {
const { length } = resultOptions.value;
if (length === 0) return;
const index = resultOptions.value.findIndex(
item => item.path === activePath.value
);
if (index === 0) {
activePath.value = resultOptions.value[length - 1].path;
} else {
activePath.value = resultOptions.value[index - 1].path;
}
}
/** key down */
function handleDown() {
const { length } = resultOptions.value;
if (length === 0) return;
const index = resultOptions.value.findIndex(
item => item.path === activePath.value
);
if (index + 1 === length) {
activePath.value = resultOptions.value[0].path;
} else {
activePath.value = resultOptions.value[index + 1].path;
}
}
/** key enter */
function handleEnter() {
const { length } = resultOptions.value;
if (length === 0 || activePath.value === "") return;
router.push(activePath.value);
handleClose();
}
onKeyStroke("Enter", handleEnter);
onKeyStroke("ArrowUp", handleUp);
onKeyStroke("ArrowDown", handleDown);
</script>
<template>
<el-dialog
top="5vh"
:width="device === 'mobile' ? '80vw' : '50vw'"
v-model="show"
:before-close="handleClose"
>
<el-input
ref="inputRef"
v-model="keyword"
clearable
placeholder="请输入关键词搜索"
@input="handleSearch"
>
<template #prefix>
<span class="el-input__icon">
<IconifyIconOffline icon="search" />
</span>
</template>
</el-input>
<div class="search-result-container">
<el-empty v-if="resultOptions.length === 0" description="暂无搜索结果" />
<SearchResult
v-else
v-model:value="activePath"
:options="resultOptions"
@click="handleEnter"
/>
</div>
<template #footer>
<SearchFooter />
</template>
</el-dialog>
</template>
<style lang="scss" scoped>
.search-result-container {
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,100 @@
<script lang="ts" setup>
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
import enterOutlined from "/@/assets/svg/enter_outlined.svg?component";
const { t } = useI18n();
interface optionsItem {
path: string;
meta?: {
icon?: string;
title?: string;
};
}
interface Props {
value: string;
options: Array<optionsItem>;
}
interface Emits {
(e: "update:value", val: string): void;
(e: "enter"): void;
}
const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<Emits>();
const itemStyle = computed(() => {
return item => {
return {
background:
item?.path === active.value ? useEpThemeStoreHook().epThemeColor : "",
color: item.path === active.value ? "#fff" : "",
fontSize: item.path === active.value ? "16px" : "14px"
};
};
});
const active = computed({
get() {
return props.value;
},
set(val: string) {
emit("update:value", val);
}
});
/** 鼠标移入 */
async function handleMouse(item) {
active.value = item.path;
}
function handleTo() {
emit("enter");
}
</script>
<template>
<div class="result">
<template v-for="item in options" :key="item.path">
<div
class="result-item dark:bg-[#1d1d1d]"
:style="itemStyle(item)"
@click="handleTo"
@mouseenter="handleMouse(item)"
>
<component :is="useRenderIcon(item.meta?.icon ?? 'bookmark-2-line')" />
<span class="result-item-title">{{ t(item.meta?.title) }}</span>
<enterOutlined />
</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.result {
padding-bottom: 12px;
&-item {
display: flex;
align-items: center;
height: 56px;
margin-top: 8px;
padding: 14px;
border-radius: 4px;
cursor: pointer;
border: 0.1px solid #ccc;
transition: all 0.3s;
&-title {
display: flex;
flex: 1;
margin-left: 5px;
}
}
}
</style>

View File

@@ -0,0 +1,3 @@
import SearchModal from "./SearchModal.vue";
export { SearchModal };

View File

@@ -0,0 +1,18 @@
<script lang="ts" setup>
import { SearchModal } from "./components";
import { useBoolean } from "../../hooks/useBoolean";
const { bool: show, toggle } = useBoolean();
function handleSearch() {
toggle();
}
</script>
<template>
<div
class="search-container w-[40px] h-[48px] flex-c cursor-pointer navbar-bg-hover"
@click="handleSearch"
>
<IconifyIconOffline icon="search" />
</div>
<SearchModal v-model:value="show" />
</template>

View File

@@ -1,75 +1,58 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
reactive,
ref, ref,
unref, unref,
watch, watch,
reactive,
computed, computed,
nextTick, nextTick,
useCssModule, useCssModule
getCurrentInstance
} from "vue"; } from "vue";
import rgbHex from "rgb-hex";
import { find } from "lodash-es";
import { getConfig } from "/@/config"; import { getConfig } from "/@/config";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import panel from "../panel/index.vue"; import panel from "../panel/index.vue";
import { emitter } from "/@/utils/mitt"; import { emitter } from "/@/utils/mitt";
import { resetRouter } from "/@/router";
import { templateRef } from "@vueuse/core"; import { templateRef } from "@vueuse/core";
import { debounce } from "/@/utils/debounce"; import { removeToken } from "/@/utils/auth";
import { themeColorsType } from "../../types"; import { routerArrays } from "/@/layout/types";
import { useNav } from "/@/layout/hooks/useNav";
import { useAppStoreHook } from "/@/store/modules/app"; import { useAppStoreHook } from "/@/store/modules/app";
import { shadeBgColor } from "../../theme/element-plus";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import { storageLocal, storageSession } from "/@/utils/storage";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { createNewStyle, writeNewStyle } from "../../theme/element-plus"; import { useDataThemeChange } from "/@/layout/hooks/useDataThemeChange";
import { toggleTheme } from "@zougt/vite-plugin-theme-preprocessor/dist/browser-utils"; import {
useDark,
debounce,
useGlobal,
storageLocal,
storageSession
} from "@pureadmin/utils";
import { toggleTheme } from "@pureadmin/theme/dist/browser-utils";
import dayIcon from "/@/assets/svg/day.svg?component"; import dayIcon from "/@/assets/svg/day.svg?component";
import darkIcon from "/@/assets/svg/dark.svg?component"; import darkIcon from "/@/assets/svg/dark.svg?component";
const router = useRouter(); const router = useRouter();
const { device } = useNav();
const { isDark } = useDark();
const { isSelect } = useCssModule(); const { isSelect } = useCssModule();
const body = document.documentElement as HTMLElement; const { $storage } = useGlobal<GlobalPropertiesApi>();
const instance =
getCurrentInstance().appContext.app.config.globalProperties.$storage;
const instanceConfig =
getCurrentInstance().appContext.app.config.globalProperties.$config;
let themeColors = ref<Array<themeColorsType>>([
// 道奇蓝(默认)
{ rgb: "27, 42, 71", themeColor: "default" },
// 亮白色
{ rgb: "255, 255, 255", themeColor: "light" },
// 猩红色
{ rgb: "245, 34, 45", themeColor: "dusk" },
// 橙红色
{ rgb: "250, 84, 28", themeColor: "volcano" },
// 金色
{ rgb: "250, 219, 20", themeColor: "yellow" },
// 绿宝石
{ rgb: "19, 194, 194", themeColor: "mingQing" },
// 酸橙绿
{ rgb: "82, 196, 26", themeColor: "auroraGreen" },
// 深粉色
{ rgb: "235, 47, 150", themeColor: "pink" },
// 深紫罗兰色
{ rgb: "114, 46, 209", themeColor: "saucePurple" }
]);
const mixRef = templateRef<HTMLElement | null>("mixRef", null);
const verticalRef = templateRef<HTMLElement | null>("verticalRef", null); const verticalRef = templateRef<HTMLElement | null>("verticalRef", null);
const horizontalRef = templateRef<HTMLElement | null>("horizontalRef", null); const horizontalRef = templateRef<HTMLElement | null>("horizontalRef", null);
let layoutTheme = const {
ref(storageLocal.getItem("responsive-layout")) || body,
ref({ dataTheme,
layout: instanceConfig?.Layout ?? "vertical", layoutTheme,
theme: instanceConfig?.Theme ?? "default" themeColors,
}); dataThemeChange,
setEpThemeColor,
setLayoutThemeColor
} = useDataThemeChange();
// body添加layout属性作用于src/style/sidebar.scss /* body添加layout属性作用于src/style/sidebar.scss */
if (unref(layoutTheme)) { if (unref(layoutTheme)) {
let layout = unref(layoutTheme).layout; let layout = unref(layoutTheme).layout;
let theme = unref(layoutTheme).theme; let theme = unref(layoutTheme).theme;
@@ -79,105 +62,100 @@ if (unref(layoutTheme)) {
setLayoutModel(layout); setLayoutModel(layout);
} }
// 默认灵动模式 /** 默认灵动模式 */
const markValue = ref(instance.configure?.showModel ?? "smart"); const markValue = ref($storage.configure?.showModel ?? "smart");
const logoVal = ref(instance.configure?.showLogo ?? true); const logoVal = ref($storage.configure?.showLogo ?? true);
const epThemeColor = ref(useEpThemeStoreHook().getEpThemeColor);
const settings = reactive({ const settings = reactive({
greyVal: instance.configure.grey, greyVal: $storage.configure.grey,
weakVal: instance.configure.weak, weakVal: $storage.configure.weak,
tabsVal: instance.configure.hideTabs, tabsVal: $storage.configure.hideTabs,
showLogo: instance.configure.showLogo, showLogo: $storage.configure.showLogo,
showModel: instance.configure.showModel, showModel: $storage.configure.showModel,
multiTagsCache: instance.configure.multiTagsCache multiTagsCache: $storage.configure.multiTagsCache
}); });
const getThemeColorStyle = computed(() => { const getThemeColorStyle = computed(() => {
return rgb => { return color => {
return { background: `rgb(${rgb})` }; return { background: color };
}; };
}); });
function changeStorageConfigure(key, val) { /** 当网页为暗黑模式时不显示亮白色切换选项 */
const storageConfigure = instance.configure; const showThemeColors = computed(() => {
return themeColor => {
return themeColor === "light" && isDark.value ? false : true;
};
});
function storageConfigureChange<T>(key: string, val: T): void {
const storageConfigure = $storage.configure;
storageConfigure[key] = val; storageConfigure[key] = val;
instance.configure = storageConfigure; $storage.configure = storageConfigure;
} }
function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) { function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
const targetEl = target || document.body; const targetEl = target || document.body;
let { className } = targetEl; let { className } = targetEl;
className = className.replace(clsName, ""); className = className.replace(clsName, "").trim();
targetEl.className = flag ? `${className} ${clsName} ` : className; targetEl.className = flag ? `${className} ${clsName} ` : className;
} }
// 灰色模式设置 /** 灰色模式设置 */
const greyChange = (value): void => { const greyChange = (value): void => {
toggleClass(settings.greyVal, "html-grey", document.querySelector("html")); toggleClass(settings.greyVal, "html-grey", document.querySelector("html"));
changeStorageConfigure("grey", value); storageConfigureChange("grey", value);
}; };
// 色弱模式设置 /** 色弱模式设置 */
const weekChange = (value): void => { const weekChange = (value): void => {
toggleClass( toggleClass(
settings.weakVal, settings.weakVal,
"html-weakness", "html-weakness",
document.querySelector("html") document.querySelector("html")
); );
changeStorageConfigure("weak", value); storageConfigureChange("weak", value);
}; };
const tagsChange = () => { const tagsChange = () => {
let showVal = settings.tabsVal; let showVal = settings.tabsVal;
changeStorageConfigure("hideTabs", showVal); storageConfigureChange("hideTabs", showVal);
emitter.emit("tagViewsChange", showVal); emitter.emit("tagViewsChange", showVal as unknown as string);
}; };
const multiTagsCacheChange = () => { const multiTagsCacheChange = () => {
let multiTagsCache = settings.multiTagsCache; let multiTagsCache = settings.multiTagsCache;
changeStorageConfigure("multiTagsCache", multiTagsCache); storageConfigureChange("multiTagsCache", multiTagsCache);
useMultiTagsStoreHook().multiTagsCacheChange(multiTagsCache); useMultiTagsStoreHook().multiTagsCacheChange(multiTagsCache);
}; };
// 清空缓存并返回登录页 /** 清空缓存并返回登录页 */
function onReset() { function onReset() {
toggleClass(getConfig().Grey, "html-grey", document.querySelector("html")); removeToken();
toggleClass(
getConfig().Weak,
"html-weakness",
document.querySelector("html")
);
useMultiTagsStoreHook().handleTags("equal", [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "menus.hshome",
icon: "home-filled",
i18n: true
}
}
]);
useMultiTagsStoreHook().multiTagsCacheChange(getConfig().MultiTagsCache);
useEpThemeStoreHook().setEpThemeColor(getConfig().EpThemeColor);
storageLocal.clear(); storageLocal.clear();
storageSession.clear(); storageSession.clear();
const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } = getConfig();
useAppStoreHook().setLayout(Layout);
setEpThemeColor(EpThemeColor);
useMultiTagsStoreHook().multiTagsCacheChange(MultiTagsCache);
toggleClass(Grey, "html-grey", document.querySelector("html"));
toggleClass(Weak, "html-weakness", document.querySelector("html"));
router.push("/login"); router.push("/login");
useMultiTagsStoreHook().handleTags("equal", [...routerArrays]);
resetRouter();
} }
function onChange(label) { function onChange(label) {
changeStorageConfigure("showModel", label); storageConfigureChange("showModel", label);
emitter.emit("tagViewsShowModel", label); emitter.emit("tagViewsShowModel", label);
} }
// 侧边栏Logo /** 侧边栏Logo */
function logoChange() { function logoChange() {
unref(logoVal) unref(logoVal)
? changeStorageConfigure("showLogo", true) ? storageConfigureChange("showLogo", true)
: changeStorageConfigure("showLogo", false); : storageConfigureChange("showLogo", false);
emitter.emit("logoChange", unref(logoVal)); emitter.emit("logoChange", unref(logoVal));
} }
@@ -187,20 +165,29 @@ function setFalse(Doms): any {
}); });
} }
watch(instance, ({ layout }) => { watch($storage, ({ layout }) => {
/* 设置wangeditorV5主题色 */
body.style.setProperty("--w-e-toolbar-active-color", layout["epThemeColor"]);
switch (layout["layout"]) { switch (layout["layout"]) {
case "vertical": case "vertical":
toggleClass(true, isSelect, unref(verticalRef)); toggleClass(true, isSelect, unref(verticalRef));
debounce(setFalse([horizontalRef]), 50); debounce(setFalse([horizontalRef]), 50);
debounce(setFalse([mixRef]), 50);
break; break;
case "horizontal": case "horizontal":
toggleClass(true, isSelect, unref(horizontalRef)); toggleClass(true, isSelect, unref(horizontalRef));
debounce(setFalse([verticalRef]), 50); debounce(setFalse([verticalRef]), 50);
debounce(setFalse([mixRef]), 50);
break;
case "mix":
toggleClass(true, isSelect, unref(mixRef));
debounce(setFalse([verticalRef]), 50);
debounce(setFalse([horizontalRef]), 50);
break; break;
} }
}); });
// 主题色 激活选择项 /** 主题色 激活选择项 */
const getThemeColor = computed(() => { const getThemeColor = computed(() => {
return current => { return current => {
if ( if (
@@ -219,83 +206,27 @@ const getThemeColor = computed(() => {
}; };
}); });
// 设置导航模式 /** 设置导航模式 */
function setLayoutModel(layout: string) { function setLayoutModel(layout: string) {
layoutTheme.value.layout = layout; layoutTheme.value.layout = layout;
window.document.body.setAttribute("layout", layout); window.document.body.setAttribute("layout", layout);
instance.layout = { $storage.layout = {
layout, layout,
theme: layoutTheme.value.theme, theme: layoutTheme.value.theme,
darkMode: instance.layout.darkMode, darkMode: $storage.layout?.darkMode,
sidebarStatus: instance.layout.sidebarStatus, sidebarStatus: $storage.layout?.sidebarStatus,
epThemeColor: instance.layout.epThemeColor epThemeColor: $storage.layout?.epThemeColor
}; };
useAppStoreHook().setLayout(layout); useAppStoreHook().setLayout(layout);
} }
// 存放夜间主题切换前的导航主题色 /* 初始化项目配置 */
let tempLayoutThemeColor;
// 设置导航主题色
function setLayoutThemeColor(theme: string) {
tempLayoutThemeColor = instance.layout.theme;
layoutTheme.value.theme = theme;
toggleTheme({
scopeName: `layout-theme-${theme}`
});
instance.layout = {
layout: useAppStoreHook().layout,
theme,
darkMode: dataTheme.value,
sidebarStatus: instance.layout.sidebarStatus,
epThemeColor: instance.layout.epThemeColor
};
if (theme === "default" || theme === "light") {
setEpThemeColor(getConfig().EpThemeColor);
} else {
const colors = find(themeColors.value, { themeColor: theme });
const color = "#" + rgbHex(colors.rgb);
setEpThemeColor(color);
}
}
// 设置ep主题色
const setEpThemeColor = (color: string) => {
writeNewStyle(createNewStyle(color));
useEpThemeStoreHook().setEpThemeColor(color);
body.style.setProperty("--el-color-primary-active", shadeBgColor(color));
};
let dataTheme = ref<boolean>(instance.layout.darkMode);
// 日间、夜间主题切换
function dataThemeChange() {
if (dataTheme.value) {
body.setAttribute("data-theme", "dark");
setLayoutThemeColor("light");
} else {
body.setAttribute("data-theme", "");
tempLayoutThemeColor && setLayoutThemeColor(tempLayoutThemeColor);
instance.layout = {
layout: useAppStoreHook().layout,
theme: instance.layout.theme,
darkMode: dataTheme.value,
sidebarStatus: instance.layout.sidebarStatus,
epThemeColor: instance.layout.epThemeColor
};
}
}
//初始化项目配置
nextTick(() => { nextTick(() => {
settings.greyVal && settings.greyVal &&
document.querySelector("html")?.setAttribute("class", "html-grey"); document.querySelector("html")?.setAttribute("class", "html-grey");
settings.weakVal && settings.weakVal &&
document.querySelector("html")?.setAttribute("class", "html-weakness"); document.querySelector("html")?.setAttribute("class", "html-weakness");
settings.tabsVal && tagsChange(); settings.tabsVal && tagsChange();
writeNewStyle(createNewStyle(epThemeColor.value));
dataThemeChange(); dataThemeChange();
}); });
</script> </script>
@@ -310,40 +241,61 @@ nextTick(() => {
:active-icon="dayIcon" :active-icon="dayIcon"
:inactive-icon="darkIcon" :inactive-icon="darkIcon"
@change="dataThemeChange" @change="dataThemeChange"
> />
</el-switch>
<el-divider>导航栏模式</el-divider> <el-divider>导航栏模式</el-divider>
<ul class="pure-theme"> <ul class="pure-theme">
<el-tooltip class="item" content="左侧菜单模式" placement="bottom"> <el-tooltip class="item" content="左侧模式" placement="bottom">
<li <li
:class="layoutTheme.layout === 'vertical' ? $style.isSelect : ''" :class="layoutTheme.layout === 'vertical' ? $style.isSelect : ''"
ref="verticalRef" ref="verticalRef"
@click="setLayoutModel('vertical')" @click="setLayoutModel('vertical')"
> >
<div></div> <div />
<div></div> <div />
</li> </li>
</el-tooltip> </el-tooltip>
<el-tooltip class="item" content="顶部菜单模式" placement="bottom"> <el-tooltip
v-if="device !== 'mobile'"
class="item"
content="顶部模式"
placement="bottom"
>
<li <li
:class="layoutTheme.layout === 'horizontal' ? $style.isSelect : ''" :class="layoutTheme.layout === 'horizontal' ? $style.isSelect : ''"
ref="horizontalRef" ref="horizontalRef"
@click="setLayoutModel('horizontal')" @click="setLayoutModel('horizontal')"
> >
<div></div> <div />
<div></div> <div />
</li>
</el-tooltip>
<el-tooltip
v-if="device !== 'mobile'"
class="item"
content="混合模式"
placement="bottom"
>
<li
:class="layoutTheme.layout === 'mix' ? $style.isSelect : ''"
ref="mixRef"
@click="setLayoutModel('mix')"
>
<div />
<div />
</li> </li>
</el-tooltip> </el-tooltip>
</ul> </ul>
<el-divider v-show="!dataTheme">主题色</el-divider> <el-divider>主题色</el-divider>
<ul class="theme-color" v-show="!dataTheme"> <ul class="theme-color">
<li <li
v-for="(item, index) in themeColors" v-for="(item, index) in themeColors"
:key="index" :key="index"
:style="getThemeColorStyle(item.rgb)" v-show="showThemeColors(item.themeColor)"
:style="getThemeColorStyle(item.color)"
@click="setLayoutThemeColor(item.themeColor)" @click="setLayoutThemeColor(item.themeColor)"
> >
<el-icon <el-icon
@@ -358,8 +310,8 @@ nextTick(() => {
<el-divider>界面显示</el-divider> <el-divider>界面显示</el-divider>
<ul class="setting"> <ul class="setting">
<li v-show="!dataTheme"> <li>
<span>灰色模式</span> <span class="dark:text-white">灰色模式</span>
<el-switch <el-switch
v-model="settings.greyVal" v-model="settings.greyVal"
inline-prompt inline-prompt
@@ -367,11 +319,10 @@ nextTick(() => {
active-text="" active-text=""
inactive-text="" inactive-text=""
@change="greyChange" @change="greyChange"
> />
</el-switch>
</li> </li>
<li v-show="!dataTheme"> <li>
<span>色弱模式</span> <span class="dark:text-white">色弱模式</span>
<el-switch <el-switch
v-model="settings.weakVal" v-model="settings.weakVal"
inline-prompt inline-prompt
@@ -379,11 +330,10 @@ nextTick(() => {
active-text="" active-text=""
inactive-text="" inactive-text=""
@change="weekChange" @change="weekChange"
> />
</el-switch>
</li> </li>
<li> <li>
<span>隐藏标签页</span> <span class="dark:text-white">隐藏标签页</span>
<el-switch <el-switch
v-model="settings.tabsVal" v-model="settings.tabsVal"
inline-prompt inline-prompt
@@ -391,11 +341,10 @@ nextTick(() => {
active-text="" active-text=""
inactive-text="" inactive-text=""
@change="tagsChange" @change="tagsChange"
> />
</el-switch>
</li> </li>
<li> <li>
<span>侧边栏Logo</span> <span class="dark:text-white">侧边栏Logo</span>
<el-switch <el-switch
v-model="logoVal" v-model="logoVal"
inline-prompt inline-prompt
@@ -405,11 +354,10 @@ nextTick(() => {
active-text="" active-text=""
inactive-text="" inactive-text=""
@change="logoChange" @change="logoChange"
> />
</el-switch>
</li> </li>
<li> <li>
<span>标签页持久化</span> <span class="dark:text-white">标签页持久化</span>
<el-switch <el-switch
v-model="settings.multiTagsCache" v-model="settings.multiTagsCache"
inline-prompt inline-prompt
@@ -417,12 +365,11 @@ nextTick(() => {
active-text="" active-text=""
inactive-text="" inactive-text=""
@change="multiTagsCacheChange" @change="multiTagsCacheChange"
> />
</el-switch>
</li> </li>
<li> <li>
<span>标签风格</span> <span class="dark:text-white">标签风格</span>
<el-radio-group v-model="markValue" size="small" @change="onChange"> <el-radio-group v-model="markValue" size="small" @change="onChange">
<el-radio label="card">卡片</el-radio> <el-radio label="card">卡片</el-radio>
<el-radio label="smart">灵动</el-radio> <el-radio label="smart">灵动</el-radio>
@@ -442,8 +389,8 @@ nextTick(() => {
height="15" height="15"
style="margin-right: 4px" style="margin-right: 4px"
/> />
清空缓存并返回登录页</el-button 清空缓存并返回登录页
> </el-button>
</panel> </panel>
</template> </template>
@@ -481,15 +428,14 @@ nextTick(() => {
.pure-theme { .pure-theme {
margin-top: 25px; margin-top: 25px;
width: 100%; width: 100%;
height: 100px; height: 50px;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-around; justify-content: space-around;
li { li {
margin: 10px; width: 18%;
width: 36%; height: 45px;
height: 70px;
background: #f0f2f5; background: #f0f2f5;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@@ -527,6 +473,27 @@ nextTick(() => {
} }
} }
} }
&:nth-child(3) {
div {
&:nth-child(1) {
width: 100%;
height: 30%;
background: #1b2a47;
box-shadow: 0 0 1px #888;
}
&:nth-child(2) {
width: 30%;
height: 70%;
bottom: 0;
left: 0;
background: #fff;
box-shadow: 0 0 1px #888;
position: absolute;
}
}
}
} }
} }

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from "vue"; import { isEqual } from "lodash-unified";
import { isEqual } from "lodash-es";
import { transformI18n } from "/@/plugins/i18n"; import { transformI18n } from "/@/plugins/i18n";
import { ref, watch, onMounted, toRaw } from "vue";
import { getParentPaths, findRouteByPath } from "/@/router/utils"; import { getParentPaths, findRouteByPath } from "/@/router/utils";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { useRoute, useRouter, RouteLocationMatched } from "vue-router"; import { useRoute, useRouter, RouteLocationMatched } from "vue-router";
@@ -9,24 +9,29 @@ import { useRoute, useRouter, RouteLocationMatched } from "vue-router";
const route = useRoute(); const route = useRoute();
const levelList = ref([]); const levelList = ref([]);
const router = useRouter(); const router = useRouter();
const routes = router.options.routes; const routes: any = router.options.routes;
const multiTags = useMultiTagsStoreHook().multiTags; const multiTags: any = useMultiTagsStoreHook().multiTags;
const isDashboard = (route: RouteLocationMatched): boolean | string => { const isDashboard = (route: RouteLocationMatched): boolean | string => {
const name = route && (route.name as string); const name = route && (route.name as string);
if (!name) { if (!name) return false;
return false; return name.trim().toLocaleLowerCase() === "Welcome".toLocaleLowerCase();
}
return name.trim().toLocaleLowerCase() === "welcome".toLocaleLowerCase();
}; };
const getBreadcrumb = (): void => { const getBreadcrumb = (): void => {
// 当前路由信息 // 当前路由信息
let currentRoute; let currentRoute;
if (Object.keys(route.query).length > 0) { if (Object.keys(route.query).length > 0) {
multiTags.forEach(item => { multiTags.forEach(item => {
if (isEqual(route.query, item?.query)) { if (isEqual(route.query, item?.query)) {
currentRoute = item; currentRoute = toRaw(item);
}
});
} else if (Object.keys(route.params).length > 0) {
multiTags.forEach(item => {
if (isEqual(route.params, item?.params)) {
currentRoute = toRaw(item);
} }
}); });
} else { } else {
@@ -38,91 +43,66 @@ const getBreadcrumb = (): void => {
let matched = []; let matched = [];
// 获取每个父级路径对应的路由信息 // 获取每个父级路径对应的路由信息
parentRoutes.forEach(path => { parentRoutes.forEach(path => {
if (path !== "/") { if (path !== "/") matched.push(findRouteByPath(path, routes));
matched.push(findRouteByPath(path, routes));
}
}); });
if (router.currentRoute.value.meta?.refreshRedirect) {
matched.unshift(
findRouteByPath(
router.currentRoute.value.meta.refreshRedirect as string,
routes
)
);
} else {
// 过滤与子级相同标题的父级路由
matched = matched.filter(item => {
return !item.redirect || (item.redirect && item.children.length !== 1);
});
}
if (currentRoute?.path !== "/welcome") {
matched.push(currentRoute);
}
const first = matched[0]; if (currentRoute?.path !== "/welcome") matched.push(currentRoute);
if (!isDashboard(first)) {
if (!isDashboard(matched[0])) {
matched = [ matched = [
{ {
path: "/welcome", path: "/welcome",
parentPath: "/", parentPath: "/",
meta: { title: "menus.hshome", i18n: true } meta: { title: "menus.hshome" }
} as unknown as RouteLocationMatched } as unknown as RouteLocationMatched
].concat(matched); ].concat(matched);
} }
matched.forEach((item, index) => {
if (currentRoute?.query || currentRoute?.params) return;
if (item?.children) {
item.children.forEach(v => {
if (v?.meta?.title === item?.meta?.title) {
matched.splice(index, 1);
}
});
}
});
levelList.value = matched.filter( levelList.value = matched.filter(
item => item?.meta && item?.meta.title !== false item => item?.meta && item?.meta.title !== false
); );
}; };
getBreadcrumb(); const handleLink = (item: RouteLocationMatched): void => {
const { redirect, path } = item;
if (redirect) {
router.push(redirect as any);
} else {
router.push(path);
}
};
onMounted(() => {
getBreadcrumb();
});
watch( watch(
() => route.path, () => route.path,
() => getBreadcrumb() () => {
); getBreadcrumb();
watch(
() => route.query,
() => getBreadcrumb()
);
const handleLink = (item: RouteLocationMatched): any => {
const { redirect, path } = item;
if (redirect) {
router.push(redirect.toString());
return;
} }
router.push(path); );
};
</script> </script>
<template> <template>
<el-breadcrumb class="app-breadcrumb" separator="/"> <el-breadcrumb class="!leading-[50px] select-none" separator="/">
<transition-group appear name="breadcrumb"> <transition-group appear name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path"> <el-breadcrumb-item v-for="item in levelList" :key="item.path">
<span <a @click.prevent="handleLink(item)">
v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" {{ transformI18n(item.meta.title) }}
class="no-redirect"
>{{ transformI18n(item.meta.title, item.meta.i18n) }}</span
>
<a v-else @click.prevent="handleLink(item)">
{{ transformI18n(item.meta.title, item.meta.i18n) }}
</a> </a>
</el-breadcrumb-item> </el-breadcrumb-item>
</transition-group> </transition-group>
</el-breadcrumb> </el-breadcrumb>
</template> </template>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>

View File

@@ -1,63 +0,0 @@
<script setup lang="ts">
import { ref } from "vue";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
export interface Props {
isActive: boolean;
}
const props = withDefaults(defineProps<Props>(), {
isActive: false
});
const fillColor = ref<string>("");
const emit = defineEmits<{
(e: "toggleClick"): void;
}>();
const toggleClick = () => {
emit("toggleClick");
};
</script>
<template>
<div
:class="classes.container"
:title="props.isActive ? '点击折叠' : '点击展开'"
@click="toggleClick"
@mouseenter="fillColor = useEpThemeStoreHook().epThemeColor"
@mouseleave="fillColor = ''"
>
<svg
:fill="fillColor"
:class="['hamburger', props.isActive ? 'is-active' : '']"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path
d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"
/>
</svg>
</div>
</template>
<style module="classes" scoped>
.container {
padding: 0 15px;
}
</style>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.is-active {
transform: rotate(180deg);
}
</style>

View File

@@ -1,146 +1,56 @@
<script setup lang="ts"> <script setup lang="ts">
import { import Search from "../search/index.vue";
computed,
unref,
watch,
nextTick,
onMounted,
getCurrentInstance
} from "vue";
import { useI18n } from "vue-i18n";
import { emitter } from "/@/utils/mitt";
import Notice from "../notice/index.vue"; import Notice from "../notice/index.vue";
import { templateRef } from "@vueuse/core"; import { ref, watch, nextTick } from "vue";
import SidebarItem from "./sidebarItem.vue"; import SidebarItem from "./sidebarItem.vue";
import avatars from "/@/assets/avatars.jpg"; import avatars from "/@/assets/avatars.jpg";
import screenfull from "../screenfull/index.vue"; import { useNav } from "/@/layout/hooks/useNav";
import { useRoute, useRouter } from "vue-router"; import { useTranslationLang } from "../../hooks/useTranslationLang";
import { storageSession } from "/@/utils/storage";
import { deviceDetection } from "/@/utils/deviceDetection";
import { usePermissionStoreHook } from "/@/store/modules/permission"; import { usePermissionStoreHook } from "/@/store/modules/permission";
import globalization from "/@/assets/svg/globalization.svg?component"; import globalization from "/@/assets/svg/globalization.svg?component";
const instance = const menuRef = ref();
getCurrentInstance().appContext.config.globalProperties.$storage;
const title = const { t, route, locale, translationCh, translationEn } =
getCurrentInstance().appContext.config.globalProperties.$config?.Title; useTranslationLang(menuRef);
const {
title,
routers,
logout,
backHome,
onPanel,
menuSelect,
username,
avatarsStyle,
getDropdownItemStyle,
getDropdownItemClass
} = useNav();
const menuRef = templateRef<ElRef | null>("menu", null); nextTick(() => {
const route = useRoute(); menuRef.value?.handleResize();
const router = useRouter();
const routers = useRouter().options.routes;
let usename = storageSession.getItem("info")?.username;
const { locale, t } = useI18n();
const getDropdownItemStyle = computed(() => {
return t => {
return {
background: locale.value === t ? "#1b2a47" : "",
color: locale.value === t ? "#f4f4f5" : "#000"
};
};
}); });
watch( watch(
() => locale.value, () => route.path,
() => { () => {
//@ts-ignore menuSelect(route.path, routers);
// 动态title
document.title = t(unref(route.meta.title));
} }
); );
// 退出登录
const logout = (): void => {
storageSession.removeItem("info");
router.push("/login");
};
function onPanel() {
emitter.emit("openPanel");
}
const activeMenu = computed((): string => {
const { meta, path } = route;
if (meta.activeMenu) {
// @ts-ignore
return meta.activeMenu;
}
return path;
});
const menuSelect = (indexPath: string): void => {
let parentPath = "";
let parentPathIndex = indexPath.lastIndexOf("/");
if (parentPathIndex > 0) {
parentPath = indexPath.slice(0, parentPathIndex);
}
// 找到当前路由的信息
function findCurrentRoute(routes) {
return routes.map(item => {
if (item.path === indexPath) {
// 切换左侧菜单 通知标签页
emitter.emit("changLayoutRoute", {
indexPath,
parentPath
});
} else {
if (item.children) findCurrentRoute(item.children);
}
});
}
findCurrentRoute(routers);
};
function backHome() {
router.push("/welcome");
}
function handleResize() {
// @ts-ignore
menuRef.value.handleResize();
}
// 简体中文
function translationCh() {
instance.locale = { locale: "zh" };
locale.value = "zh";
handleResize();
}
// English
function translationEn() {
instance.locale = { locale: "en" };
locale.value = "en";
handleResize();
}
onMounted(() => {
nextTick(() => {
handleResize();
});
});
</script> </script>
<template> <template>
<div class="horizontal-header"> <div class="horizontal-header">
<div class="horizontal-header-left" @click="backHome"> <div class="horizontal-header-left" @click="backHome">
<FontIcon <FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" />
icon="team-iconlogo"
svg
style="width: 35px; height: 35px"
></FontIcon>
<h4>{{ title }}</h4> <h4>{{ title }}</h4>
</div> </div>
<el-menu <el-menu
ref="menu"
:default-active="activeMenu"
unique-opened
router router
class="horizontal-header-menu" ref="menuRef"
mode="horizontal" mode="horizontal"
@select="menuSelect" class="horizontal-header-menu"
:default-active="route.path"
@select="indexPath => menuSelect(indexPath, routers)"
> >
<sidebar-item <sidebar-item
v-for="route in usePermissionStoreHook().wholeMenus" v-for="route in usePermissionStoreHook().wholeMenus"
@@ -150,37 +60,45 @@ onMounted(() => {
/> />
</el-menu> </el-menu>
<div class="horizontal-header-right"> <div class="horizontal-header-right">
<!-- 菜单搜索 -->
<Search />
<!-- 通知 --> <!-- 通知 -->
<Notice id="header-notice" /> <Notice id="header-notice" />
<!-- 全屏 -->
<screenfull id="header-screenfull" v-show="!deviceDetection()" />
<!-- 国际化 --> <!-- 国际化 -->
<el-dropdown id="header-translation" trigger="click"> <el-dropdown id="header-translation" trigger="click">
<globalization /> <globalization
class="navbar-bg-hover w-[40px] h-[48px] p-[11px] cursor-pointer outline-none"
/>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="translation"> <el-dropdown-menu class="translation">
<el-dropdown-item <el-dropdown-item
:style="getDropdownItemStyle('zh')" :style="getDropdownItemStyle(locale, 'zh')"
:class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]"
@click="translationCh" @click="translationCh"
><el-icon class="check-zh" v-show="locale === 'zh'"
><IconifyIconOffline icon="check" /></el-icon
>简体中文</el-dropdown-item
> >
<span class="check-zh" v-show="locale === 'zh'">
<IconifyIconOffline icon="check" />
</span>
简体中文
</el-dropdown-item>
<el-dropdown-item <el-dropdown-item
:style="getDropdownItemStyle('en')" :style="getDropdownItemStyle(locale, 'en')"
:class="['dark:!text-white', getDropdownItemClass(locale, 'en')]"
@click="translationEn" @click="translationEn"
><el-icon class="check-en" v-show="locale === 'en'"
><IconifyIconOffline icon="check" /></el-icon
>English</el-dropdown-item
> >
<span class="check-en" v-show="locale === 'en'">
<IconifyIconOffline icon="check" />
</span>
English
</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<!-- 退出登 --> <!-- 退出登 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<span class="el-dropdown-link"> <span class="el-dropdown-link navbar-bg-hover">
<img :src="avatars" /> <img v-if="avatars" :src="avatars" :style="avatarsStyle" />
<p>{{ usename }}</p> <p v-if="username" class="dark:text-white">{{ username }}</p>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="logout"> <el-dropdown-menu class="logout">
@@ -189,32 +107,26 @@ onMounted(() => {
icon="logout-circle-r-line" icon="logout-circle-r-line"
style="margin: 5px" style="margin: 5px"
/> />
{{ $t("buttons.hsLoginOut") }}</el-dropdown-item {{ t("buttons.hsLoginOut") }}
> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<el-icon <span
class="el-icon-setting" class="set-icon navbar-bg-hover"
:title="$t('buttons.hssystemSet')" :title="t('buttons.hssystemSet')"
@click="onPanel" @click="onPanel"
> >
<IconifyIconOffline icon="setting" /> <IconifyIconOffline icon="setting" />
</el-icon> </span>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.translation { .translation {
.el-dropdown-menu__item { ::v-deep(.el-dropdown-menu__item) {
padding: 5px 40px !important; padding: 5px 40px;
}
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
} }
.check-zh { .check-zh {
@@ -231,16 +143,10 @@ onMounted(() => {
.logout { .logout {
max-width: 120px; max-width: 120px;
.el-dropdown-menu__item { ::v-deep(.el-dropdown-menu__item) {
min-width: 100%; min-width: 100%;
display: inline-flex; display: inline-flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
}
} }
</style> </style>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import { useDark } from "@pureadmin/utils";
interface Props {
isActive: boolean;
}
const props = withDefaults(defineProps<Props>(), {
isActive: false
});
const { isDark } = useDark();
const emit = defineEmits<{
(e: "toggleClick"): void;
}>();
const toggleClick = () => {
emit("toggleClick");
};
</script>
<template>
<div class="container">
<el-tooltip
placement="right"
:effect="isDark ? 'dark' : 'light'"
:content="props.isActive ? '点击折叠' : '点击展开'"
>
<IconifyIconOffline
:icon="props.isActive ? 'menu-fold' : 'menu-unfold'"
class="cursor-pointer inline-block align-middle text-primary hover:text-primary dark:hover:!text-white w-[16px] h-[16px] ml-4 mb-1"
@click="toggleClick"
/>
</el-tooltip>
</div>
</template>
<style lang="scss" scoped>
.container {
position: absolute;
bottom: 0;
width: 100%;
height: 40px;
line-height: 40px;
box-shadow: 0 0 6px -2px var(--el-color-primary);
}
</style>

View File

@@ -1,11 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { getCurrentInstance } from "vue"; import { useNav } from "/@/layout/hooks/useNav";
const props = defineProps({ const props = defineProps({
collapse: Boolean collapse: Boolean
}); });
const title = const { title } = useNav();
getCurrentInstance().appContext.config.globalProperties.$config?.Title;
</script> </script>
<template> <template>
@@ -18,11 +18,7 @@ const title =
class="sidebar-logo-link" class="sidebar-logo-link"
to="/" to="/"
> >
<FontIcon <FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" />
icon="team-iconlogo"
svg
style="width: 35px; height: 35px"
></FontIcon>
<span class="sidebar-title">{{ title }}</span> <span class="sidebar-title">{{ title }}</span>
</router-link> </router-link>
<router-link <router-link
@@ -32,11 +28,7 @@ const title =
class="sidebar-logo-link" class="sidebar-logo-link"
to="/" to="/"
> >
<FontIcon <FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" />
icon="team-iconlogo"
svg
style="width: 35px; height: 35px"
></FontIcon>
<span class="sidebar-title">{{ title }}</span> <span class="sidebar-title">{{ title }}</span>
</router-link> </router-link>
</transition> </transition>
@@ -59,6 +51,12 @@ const title =
margin-top: 5px; margin-top: 5px;
.sidebar-title { .sidebar-title {
display: block;
width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
color: #1890ff; color: #1890ff;
font-weight: 600; font-weight: 600;
font-size: 20px; font-size: 20px;

View File

@@ -0,0 +1,184 @@
<script setup lang="ts">
import Search from "../search/index.vue";
import Notice from "../notice/index.vue";
import avatars from "/@/assets/avatars.jpg";
import { useNav } from "/@/layout/hooks/useNav";
import { transformI18n } from "/@/plugins/i18n";
import { ref, toRaw, watch, onMounted, nextTick } from "vue";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
import { getParentPaths, findRouteByPath } from "/@/router/utils";
import { useTranslationLang } from "../../hooks/useTranslationLang";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import globalization from "/@/assets/svg/globalization.svg?component";
const menuRef = ref();
let defaultActive = ref(null);
const { t, route, locale, translationCh, translationEn } =
useTranslationLang(menuRef);
const {
device,
routers,
logout,
onPanel,
menuSelect,
resolvePath,
username,
avatarsStyle,
getDropdownItemStyle,
getDropdownItemClass
} = useNav();
function getDefaultActive(routePath) {
const wholeMenus = usePermissionStoreHook().wholeMenus;
/** 当前路由的父级路径 */
const parentRoutes = getParentPaths(routePath, wholeMenus)[0];
defaultActive.value = findRouteByPath(
parentRoutes,
wholeMenus
)?.children[0]?.path;
}
onMounted(() => {
getDefaultActive(route.path);
});
nextTick(() => {
menuRef.value?.handleResize();
});
watch(
() => [route.path, usePermissionStoreHook().wholeMenus],
() => {
getDefaultActive(route.path);
}
);
</script>
<template>
<div v-if="device !== 'mobile'" class="horizontal-header">
<el-menu
router
ref="menuRef"
mode="horizontal"
class="horizontal-header-menu"
:default-active="defaultActive"
@select="indexPath => menuSelect(indexPath, routers)"
>
<el-menu-item
v-for="route in usePermissionStoreHook().wholeMenus"
:key="route.path"
:index="resolvePath(route) || route.redirect"
>
<template #title>
<div
v-if="toRaw(route.meta.icon)"
:class="['sub-menu-icon', route.meta.icon]"
>
<component
:is="useRenderIcon(route.meta && toRaw(route.meta.icon))"
/>
</div>
<span class="select-none">{{ transformI18n(route.meta.title) }}</span>
<FontIcon
v-if="route.meta.extraIcon"
width="30px"
height="30px"
style="position: absolute; right: 10px"
:icon="route.meta.extraIcon.name"
:svg="route.meta.extraIcon.svg ? true : false"
/>
</template>
</el-menu-item>
</el-menu>
<div class="horizontal-header-right">
<!-- 菜单搜索 -->
<Search />
<!-- 通知 -->
<Notice id="header-notice" />
<!-- 国际化 -->
<el-dropdown id="header-translation" trigger="click">
<globalization
class="navbar-bg-hover w-[40px] h-[48px] p-[11px] cursor-pointer outline-none"
/>
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'zh')"
:class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]"
@click="translationCh"
>
<span class="check-zh" v-show="locale === 'zh'">
<IconifyIconOffline icon="check" />
</span>
简体中文
</el-dropdown-item>
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')"
:class="['dark:!text-white', getDropdownItemClass(locale, 'en')]"
@click="translationEn"
>
<span class="check-en" v-show="locale === 'en'">
<IconifyIconOffline icon="check" />
</span>
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<!-- 退出登录 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover">
<img v-if="avatars" :src="avatars" :style="avatarsStyle" />
<p v-if="username" class="dark:text-white">{{ username }}</p>
</span>
<template #dropdown>
<el-dropdown-menu class="logout">
<el-dropdown-item @click="logout">
<IconifyIconOffline
icon="logout-circle-r-line"
style="margin: 5px"
/>
{{ t("buttons.hsLoginOut") }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span
class="set-icon navbar-bg-hover"
:title="t('buttons.hssystemSet')"
@click="onPanel"
>
<IconifyIconOffline icon="setting" />
</span>
</div>
</div>
</template>
<style lang="scss" scoped>
.translation {
::v-deep(.el-dropdown-menu__item) {
padding: 5px 40px;
}
.check-zh {
position: absolute;
left: 20px;
}
.check-en {
position: absolute;
left: 20px;
}
}
.logout {
max-width: 120px;
::v-deep(.el-dropdown-menu__item) {
min-width: 100%;
display: inline-flex;
flex-wrap: wrap;
}
}
</style>

View File

@@ -1,21 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import {
ref,
PropType,
nextTick,
computed,
CSSProperties,
getCurrentInstance
} from "vue";
import path from "path"; import path from "path";
import { childrenType } from "../../types"; import { childrenType } from "../../types";
import { useNav } from "/@/layout/hooks/useNav";
import { transformI18n } from "/@/plugins/i18n"; import { transformI18n } from "/@/plugins/i18n";
import { useAppStoreHook } from "/@/store/modules/app";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks"; import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
import { ref, toRaw, PropType, nextTick, computed, CSSProperties } from "vue";
const instance = getCurrentInstance().appContext.app.config.globalProperties; const { layout, isCollapse } = useNav();
const menuMode = instance.$storage.layout?.layout === "vertical";
const pureApp = useAppStoreHook();
const props = defineProps({ const props = defineProps({
item: { item: {
@@ -31,6 +22,19 @@ const props = defineProps({
} }
}); });
const getExtraIconStyle = computed((): CSSProperties => {
if (!isCollapse.value) {
return {
position: "absolute",
right: "10px"
};
} else {
return {
position: "static"
};
}
});
const getNoDropdownStyle = computed((): CSSProperties => { const getNoDropdownStyle = computed((): CSSProperties => {
return { return {
display: "flex", display: "flex",
@@ -40,7 +44,7 @@ const getNoDropdownStyle = computed((): CSSProperties => {
const getDivStyle = computed((): CSSProperties => { const getDivStyle = computed((): CSSProperties => {
return { return {
width: pureApp.sidebar.opened ? "" : "100%", width: !isCollapse.value ? "" : "100%",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "space-between", justifyContent: "space-between",
@@ -48,9 +52,8 @@ const getDivStyle = computed((): CSSProperties => {
}; };
}); });
const getMenuTextStyle = computed((): CSSProperties => { const getMenuTextStyle = computed(() => {
return { return {
width: pureApp.sidebar.opened ? "125px" : "",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
outline: "none" outline: "none"
@@ -59,14 +62,14 @@ const getMenuTextStyle = computed((): CSSProperties => {
const getSubTextStyle = computed((): CSSProperties => { const getSubTextStyle = computed((): CSSProperties => {
return { return {
width: pureApp.sidebar.opened ? "125px" : "", width: !isCollapse.value ? "210px" : "",
display: "inline-block", display: "inline-block",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis" textOverflow: "ellipsis"
}; };
}); });
const getSpanStyle = computed((): CSSProperties => { const getSpanStyle = computed(() => {
return { return {
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis" textOverflow: "ellipsis"
@@ -92,7 +95,6 @@ function hoverMenu(key) {
: Object.assign(key, { : Object.assign(key, {
showTooltip: false showTooltip: false
}); });
hoverMenuMap.set(key, true); hoverMenuMap.set(key, true);
}); });
} }
@@ -106,6 +108,10 @@ function hasOneShowingChild(
return true; return true;
}); });
if (showingChildren[0]?.meta?.showParent) {
return false;
}
if (showingChildren.length === 1) { if (showingChildren.length === 1) {
return true; return true;
} }
@@ -119,8 +125,8 @@ function hasOneShowingChild(
function resolvePath(routePath) { function resolvePath(routePath) {
const httpReg = /^http(s?):\/\//; const httpReg = /^http(s?):\/\//;
if (httpReg.test(routePath)) { if (httpReg.test(routePath) || httpReg.test(props.basePath)) {
return props.basePath + "/" + routePath; return routePath || props.basePath;
} else { } else {
return path.resolve(props.basePath, routePath); return path.resolve(props.basePath, routePath);
} }
@@ -139,21 +145,43 @@ function resolvePath(routePath) {
:class="{ 'submenu-title-noDropdown': !isNest }" :class="{ 'submenu-title-noDropdown': !isNest }"
:style="getNoDropdownStyle" :style="getNoDropdownStyle"
> >
<el-icon v-show="props.item.meta.icon"> <div class="sub-menu-icon" v-if="toRaw(props.item.meta.icon)">
<component <component
:is=" :is="
useRenderIcon( useRenderIcon(
onlyOneChild.meta.icon || toRaw(onlyOneChild.meta.icon) ||
(props.item.meta && props.item.meta.icon) (props.item.meta && toRaw(props.item.meta.icon))
) )
" "
></component> />
</el-icon> </div>
<div
v-if="
isCollapse &&
layout === 'vertical' &&
props.item?.pathList?.length === 1
"
:style="getDivStyle"
>
<span :style="getMenuTextStyle">
{{ transformI18n(onlyOneChild.meta.title) }}
</span>
</div>
<div
v-if="
isCollapse && layout === 'mix' && props.item?.pathList?.length === 2
"
:style="getDivStyle"
>
<span :style="getMenuTextStyle">
{{ transformI18n(onlyOneChild.meta.title) }}
</span>
</div>
<template #title> <template #title>
<div :style="getDivStyle"> <div :style="getDivStyle">
<span v-if="!menuMode">{{ <span v-if="layout === 'horizontal'">
transformI18n(onlyOneChild.meta.title, onlyOneChild.meta.i18n) {{ transformI18n(onlyOneChild.meta.title) }}
}}</span> </span>
<el-tooltip <el-tooltip
v-else v-else
placement="top" placement="top"
@@ -161,53 +189,47 @@ function resolvePath(routePath) {
:disabled="!onlyOneChild.showTooltip" :disabled="!onlyOneChild.showTooltip"
> >
<template #content> <template #content>
{{ {{ transformI18n(onlyOneChild.meta.title) }}
transformI18n(onlyOneChild.meta.title, onlyOneChild.meta.i18n)
}}
</template> </template>
<span <span
ref="menuTextRef" ref="menuTextRef"
:style="getMenuTextStyle" :style="getMenuTextStyle"
@mouseover="hoverMenu(onlyOneChild)" @mouseover="hoverMenu(onlyOneChild)"
> >
{{ {{ transformI18n(onlyOneChild.meta.title) }}
transformI18n(onlyOneChild.meta.title, onlyOneChild.meta.i18n)
}}
</span> </span>
</el-tooltip> </el-tooltip>
<FontIcon <FontIcon
v-if="onlyOneChild.meta.extraIcon" v-if="onlyOneChild.meta.extraIcon"
width="30px"
height="30px"
:style="getExtraIconStyle"
:icon="onlyOneChild.meta.extraIcon.name" :icon="onlyOneChild.meta.extraIcon.name"
:svg="onlyOneChild.meta.extraIcon.svg ? true : false" :svg="onlyOneChild.meta.extraIcon.svg ? true : false"
></FontIcon> />
</div> </div>
</template> </template>
</el-menu-item> </el-menu-item>
</template> </template>
<el-sub-menu <el-sub-menu v-else ref="subMenu" :index="resolvePath(props.item.path)">
v-else
ref="subMenu"
:index="resolvePath(props.item.path)"
popper-append-to-body
>
<template #title> <template #title>
<el-icon v-show="props.item.meta.icon" :class="props.item.meta.icon"> <div v-if="toRaw(props.item.meta.icon)" class="sub-menu-icon">
<component <component
:is="useRenderIcon(props.item.meta && props.item.meta.icon)" :is="useRenderIcon(props.item.meta && toRaw(props.item.meta.icon))"
></component> />
</el-icon> </div>
<span v-if="!menuMode">{{ <span v-if="layout === 'horizontal'">
transformI18n(props.item.meta.title, props.item.meta.i18n) {{ transformI18n(props.item.meta.title) }}
}}</span> </span>
<el-tooltip <el-tooltip
v-else v-else
placement="top" placement="top"
:offset="-10" :offset="-10"
:disabled="!pureApp.sidebar.opened || !props.item.showTooltip" :disabled="!isCollapse || !props.item.showTooltip"
> >
<template #content> <template #content>
{{ transformI18n(props.item.meta.title, props.item.meta.i18n) }} {{ transformI18n(props.item.meta.title) }}
</template> </template>
<div <div
ref="menuTextRef" ref="menuTextRef"
@@ -215,15 +237,18 @@ function resolvePath(routePath) {
@mouseover="hoverMenu(props.item)" @mouseover="hoverMenu(props.item)"
> >
<span :style="getSpanStyle"> <span :style="getSpanStyle">
{{ transformI18n(props.item.meta.title, props.item.meta.i18n) }} {{ transformI18n(props.item.meta.title) }}
</span> </span>
</div> </div>
</el-tooltip> </el-tooltip>
<FontIcon <FontIcon
v-if="props.item.meta.extraIcon" v-if="props.item.meta.extraIcon"
width="30px"
height="30px"
style="position: absolute; right: 10px"
:icon="props.item.meta.extraIcon.name" :icon="props.item.meta.extraIcon.name"
:svg="props.item.meta.extraIcon.svg ? true : false" :svg="props.item.meta.extraIcon.svg ? true : false"
></FontIcon> />
</template> </template>
<sidebar-item <sidebar-item
v-for="child in props.item.children" v-for="child in props.item.children"

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
interface Props {
isActive: boolean;
}
const props = withDefaults(defineProps<Props>(), {
isActive: false
});
const emit = defineEmits<{
(e: "toggleClick"): void;
}>();
const toggleClick = () => {
emit("toggleClick");
};
</script>
<template>
<div
class="px-3 mr-1 navbar-bg-hover"
:title="props.isActive ? '点击折叠' : '点击展开'"
@click="toggleClick"
>
<IconifyIconOffline
:icon="props.isActive ? 'menu-fold' : 'menu-unfold'"
class="inline-block align-middle hover:text-primary dark:hover:!text-white"
/>
</div>
</template>

View File

@@ -1,84 +1,94 @@
<script setup lang="ts"> <script setup lang="ts">
import Logo from "./logo.vue"; import Logo from "./logo.vue";
import { useRoute } from "vue-router";
import { emitter } from "/@/utils/mitt"; import { emitter } from "/@/utils/mitt";
import SidebarItem from "./sidebarItem.vue"; import SidebarItem from "./sidebarItem.vue";
import { storageLocal } from "/@/utils/storage"; import leftCollapse from "./leftCollapse.vue";
import { useRoute, useRouter } from "vue-router"; import type { StorageConfigs } from "/#/index";
import { computed, ref, onBeforeMount } from "vue"; import { useNav } from "/@/layout/hooks/useNav";
import { useAppStoreHook } from "/@/store/modules/app"; import { storageLocal } from "@pureadmin/utils";
import { ref, computed, watch, onBeforeMount } from "vue";
import { findRouteByPath, getParentPaths } from "/@/router/utils";
import { usePermissionStoreHook } from "/@/store/modules/permission"; import { usePermissionStoreHook } from "/@/store/modules/permission";
const route = useRoute(); const route = useRoute();
const pureApp = useAppStoreHook();
const router = useRouter().options.routes;
const showLogo = ref( const showLogo = ref(
storageLocal.getItem("responsive-configure")?.showLogo ?? true storageLocal.getItem<StorageConfigs>("responsive-configure")?.showLogo ?? true
); );
const isCollapse = computed(() => {
return !pureApp.getSidebarStatus; const { routers, device, pureApp, isCollapse, menuSelect, toggleSideBar } =
}); useNav();
const activeMenu = computed((): string => {
const { meta, path } = route; let subMenuData = ref([]);
if (meta.activeMenu) {
// @ts-ignore const menuData = computed(() => {
return meta.activeMenu; return pureApp.layout === "mix" && device.value !== "mobile"
} ? subMenuData.value
return path; : usePermissionStoreHook().wholeMenus;
}); });
const menuSelect = (indexPath: string): void => { function getSubMenuData(path: string) {
let parentPath = ""; // path的上级路由组成的数组
let parentPathIndex = indexPath.lastIndexOf("/"); const parentPathArr = getParentPaths(
if (parentPathIndex > 0) { path,
parentPath = indexPath.slice(0, parentPathIndex); usePermissionStoreHook().wholeMenus
} );
// 找到当前路由的信息 // 当前路由的父级路由信息
// eslint-disable-next-line no-inner-declarations const parenetRoute = findRouteByPath(
function findCurrentRoute(routes) { parentPathArr[0] || path,
return routes.map(item => { usePermissionStoreHook().wholeMenus
if (item.path === indexPath) { );
// 切换左侧菜单 通知标签页 if (!parenetRoute?.children) return;
emitter.emit("changLayoutRoute", { subMenuData.value = parenetRoute?.children;
indexPath, }
parentPath
}); getSubMenuData(route.path);
} else {
if (item.children) findCurrentRoute(item.children);
}
});
}
findCurrentRoute(router);
};
onBeforeMount(() => { onBeforeMount(() => {
emitter.on("logoChange", key => { emitter.on("logoChange", key => {
showLogo.value = key; showLogo.value = key;
}); });
}); });
watch(
() => [route.path, usePermissionStoreHook().wholeMenus],
() => {
getSubMenuData(route.path);
menuSelect(route.path, routers);
}
);
</script> </script>
<template> <template>
<div :class="['sidebar-container', showLogo ? 'has-logo' : '']"> <div :class="['sidebar-container', showLogo ? 'has-logo' : '']">
<Logo v-if="showLogo" :collapse="isCollapse" /> <Logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper"> <el-scrollbar
wrap-class="scrollbar-wrapper"
:class="[device === 'mobile' ? 'mobile' : 'pc']"
>
<el-menu <el-menu
:default-active="activeMenu"
:collapse="isCollapse"
unique-opened
router router
:collapse-transition="false" unique-opened
mode="vertical" mode="vertical"
class="outer-most" class="outer-most select-none"
@select="menuSelect" :collapse="isCollapse"
:default-active="route.path"
:collapse-transition="false"
@select="indexPath => menuSelect(indexPath, routers)"
> >
<sidebar-item <sidebar-item
v-for="route in usePermissionStoreHook().wholeMenus" v-for="routes in menuData"
:key="route.path" :key="routes.path"
:item="route" :item="routes"
class="outer-most" :base-path="routes.path"
:base-path="route.path" class="outer-most select-none"
/> />
</el-menu> </el-menu>
</el-scrollbar> </el-scrollbar>
<leftCollapse
v-if="device !== 'mobile'"
:is-active="pureApp.sidebar.opened"
@toggleClick="toggleSideBar"
/>
</div> </div>
</template> </template>

View File

@@ -18,36 +18,6 @@
} }
} }
@-webkit-keyframes rotate {
from {
-webkit-transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
}
}
@-moz-keyframes rotate {
from {
-moz-transform: rotate(0deg);
}
to {
-moz-transform: rotate(360deg);
}
}
@-o-keyframes rotate {
from {
-o-transform: rotate(0deg);
}
to {
-o-transform: rotate(360deg);
}
}
@keyframes rotate { @keyframes rotate {
from { from {
transform: rotate(0deg); transform: rotate(0deg);
@@ -73,14 +43,14 @@
font-size: 14px; font-size: 14px;
display: flex; display: flex;
align-items: center; align-items: center;
color: var(--el-text-color-regular); color: var(--el-text-color-primary);
background: #fff; background: #fff;
position: relative; position: relative;
box-shadow: 0 0 1px #888; box-shadow: 0 0 1px #888;
.scroll-item { .scroll-item {
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
padding: 0 6px 0 6px; padding: 0 6px;
box-shadow: 0 0 1px #888; box-shadow: 0 0 1px #888;
position: relative; position: relative;
margin-right: 4px; margin-right: 4px;
@@ -92,7 +62,7 @@
.el-icon-close { .el-icon-close {
font-size: 10px; font-size: 10px;
color: #1890ff; color: var(--el-color-primary);
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
top: 50%; top: 50%;
@@ -122,8 +92,8 @@
a { a {
text-decoration: none; text-decoration: none;
color: #666; color: var(--el-text-color-primary);
padding: 0 4px 0 4px; padding: 0 4px;
} }
.scroll-container { .scroll-container {
@@ -151,21 +121,6 @@
} }
} }
.right-button {
display: flex;
font-size: 16px;
li {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 38px;
border-right: 1px solid #ccc;
cursor: pointer;
}
}
/* 右键菜单 */ /* 右键菜单 */
.contextmenu { .contextmenu {
margin: 0; margin: 0;
@@ -174,7 +129,7 @@
list-style-type: none; list-style-type: none;
padding: 5px 0; padding: 5px 0;
border-radius: 4px; border-radius: 4px;
color: #000000d9; color: var(--el-text-color-primary);
font-weight: normal; font-weight: normal;
font-size: 13px; font-size: 13px;
white-space: nowrap; white-space: nowrap;
@@ -190,7 +145,8 @@
align-items: center; align-items: center;
&:hover { &:hover {
background: #eee; // background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
} }
svg { svg {
@@ -202,8 +158,6 @@
} }
.el-dropdown-menu { .el-dropdown-menu {
padding: 0;
li { li {
width: 100%; width: 100%;
margin: 0; margin: 0;
@@ -227,6 +181,9 @@
margin-right: 10px; margin-right: 10px;
} }
:deep(.el-dropdown-menu__item--divided) {
margin: 1px 0;
}
.el-dropdown-menu__item--divided::before { .el-dropdown-menu__item--divided::before {
margin: 0; margin: 0;
} }
@@ -236,7 +193,7 @@
} }
.scroll-item.is-active { .scroll-item.is-active {
background-color: #eaf4fe; // background-color: var(--el-color-primary-light-9);
position: relative; position: relative;
color: #fff; color: #fff;
@@ -249,15 +206,16 @@
} }
a { a {
color: #1890ff; color: var(--el-color-primary) !important;
} }
} }
.arrow-left, .arrow-left,
.arrow-right { .arrow-right,
.arrow-down {
width: 40px; width: 40px;
height: 38px; height: 38px;
color: #00000073; color: var(--el-text-color-primary);
position: relative; position: relative;
svg { svg {
@@ -279,7 +237,7 @@
.arrow-right { .arrow-right {
box-shadow: -5px 0 5px -6px #ccc; box-shadow: -5px 0 5px -6px #ccc;
border-right: 1px solid #ccc; border-right: 0.5px solid #ccc;
&:hover { &:hover {
cursor: e-resize; cursor: e-resize;
@@ -288,10 +246,10 @@
/* 卡片模式下鼠标移入显示蓝色边框 */ /* 卡片模式下鼠标移入显示蓝色边框 */
.card-in { .card-in {
color: #1890ff; color: var(--el-color-primary);
a { a {
color: #1890ff; color: var(--el-color-primary);
} }
} }
@@ -312,7 +270,7 @@
position: absolute; position: absolute;
left: 0; left: 0;
bottom: 0; bottom: 0;
background: #1890ff; background: var(--el-color-primary);
} }
/* 灵动模式下鼠标移入显示蓝色进度条 */ /* 灵动模式下鼠标移入显示蓝色进度条 */
@@ -322,7 +280,7 @@
position: absolute; position: absolute;
left: 0; left: 0;
bottom: 0; bottom: 0;
background: #1890ff; background: var(--el-color-primary);
animation: scheduleInWidth 400ms ease-in; animation: scheduleInWidth 400ms ease-in;
} }
@@ -333,14 +291,6 @@
position: absolute; position: absolute;
left: 0; left: 0;
bottom: 0; bottom: 0;
background: #1890ff; background: var(--el-color-primary);
animation: scheduleOutWidth 400ms ease-in; animation: scheduleOutWidth 400ms ease-in;
} }
/* 刷新按钮动画效果 */
.refresh-button {
-webkit-animation: rotate 600ms linear infinite;
-moz-animation: rotate 600ms linear infinite;
-o-animation: rotate 600ms linear infinite;
animation: rotate 600ms linear infinite;
}

View File

@@ -1,133 +1,66 @@
<script setup lang="ts"> <script setup lang="ts">
import { import { $t } from "/@/plugins/i18n";
ref,
watch,
unref,
reactive,
nextTick,
computed,
ComputedRef,
CSSProperties,
onBeforeMount,
getCurrentInstance
} from "vue";
import close from "/@/assets/svg/close.svg?component";
import refresh from "/@/assets/svg/refresh.svg?component";
import closeAll from "/@/assets/svg/close_all.svg?component";
import closeLeft from "/@/assets/svg/close_left.svg?component";
import closeOther from "/@/assets/svg/close_other.svg?component";
import closeRight from "/@/assets/svg/close_right.svg?component";
import { emitter } from "/@/utils/mitt"; import { emitter } from "/@/utils/mitt";
import { $t as t } from "/@/plugins/i18n"; import { RouteConfigs } from "../../types";
import { isEqual, isEmpty } from "lodash-es"; import { useTags } from "../../hooks/useTag";
import { transformI18n } from "/@/plugins/i18n"; import { routerArrays } from "/@/layout/types";
import { storageLocal } from "/@/utils/storage"; import { isEqual, isEmpty } from "lodash-unified";
import { useRoute, useRouter } from "vue-router";
import { RouteConfigs, tagsViewsType } from "../../types";
import { useSettingStoreHook } from "/@/store/modules/settings"; import { useSettingStoreHook } from "/@/store/modules/settings";
import { ref, watch, unref, nextTick, onBeforeMount } from "vue";
import { handleAliveRoute, delAliveRoutes } from "/@/router/utils"; import { handleAliveRoute, delAliveRoutes } from "/@/router/utils";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { usePermissionStoreHook } from "/@/store/modules/permission"; import { useResizeObserver, useDebounceFn, useFullscreen } from "@vueuse/core";
import { toggleClass, removeClass, hasClass } from "/@/utils/operate";
import { templateRef, useResizeObserver, useDebounceFn } from "@vueuse/core";
const route = useRoute(); const {
const router = useRouter(); route,
const translateX = ref<number>(0); router,
const activeIndex = ref<number>(-1); visible,
let refreshButton = "refresh-button"; showTags,
const instance = getCurrentInstance(); instance,
const pureSetting = useSettingStoreHook(); multiTags,
const tabDom = templateRef<HTMLElement | null>("tabDom", null); tagsViews,
const containerDom = templateRef<HTMLElement | null>("containerDom", null); buttonTop,
const scrollbarDom = templateRef<HTMLElement | null>("scrollbarDom", null); buttonLeft,
const showTags = showModel,
ref(storageLocal.getItem("responsive-configure").hideTabs) ?? "false"; translateX,
let multiTags: ComputedRef<Array<RouteConfigs>> = computed(() => { pureSetting,
return useMultiTagsStoreHook()?.multiTags; activeIndex,
}); getTabStyle,
iconIsActive,
linkIsActive,
currentSelect,
scheduleIsActive,
getContextMenuStyle,
closeMenu,
onMounted,
onMouseenter,
onMouseleave,
transformI18n,
onContentFullScreen
} = useTags();
const linkIsActive = computed(() => { const tabDom = ref();
return item => { const containerDom = ref();
if (Object.keys(route.query).length === 0) { const scrollbarDom = ref();
if (route.path === item.path) { let isShowArrow = ref(false);
return "is-active"; const { isFullscreen, toggle } = useFullscreen();
} else {
return "";
}
} else {
if (isEqual(route?.query, item?.query)) {
return "is-active";
} else {
return "";
}
}
};
});
const scheduleIsActive = computed(() => {
return item => {
if (Object.keys(route.query).length === 0) {
if (route.path === item.path) {
return "schedule-active";
} else {
return "";
}
} else {
if (isEqual(route?.query, item?.query)) {
return "schedule-active";
} else {
return "";
}
}
};
});
const iconIsActive = computed(() => {
return (item, index) => {
if (index === 0) return;
if (Object.keys(route.query).length === 0) {
if (route.path === item.path) {
return true;
} else {
return false;
}
} else {
if (isEqual(route?.query, item?.query)) {
return true;
} else {
return false;
}
}
};
});
const dynamicTagView = () => { const dynamicTagView = () => {
const index = multiTags.value.findIndex(item => { const index = multiTags.value.findIndex(item => {
return item.path === route.path; if (item.query) {
return isEqual(route.query, item.query);
} else if (item.params) {
return isEqual(route.params, item.params);
} else {
return item.path === route.path;
}
}); });
moveToView(index); moveToView(index);
}; };
watch([route], () => {
activeIndex.value = -1;
dynamicTagView();
});
useResizeObserver(
scrollbarDom,
useDebounceFn(() => {
dynamicTagView();
}, 200)
);
const tabNavPadding = 10;
const moveToView = (index: number): void => { const moveToView = (index: number): void => {
if (!instance.refs["dynamic" + index]) { const tabNavPadding = 10;
return; if (!instance.refs["dynamic" + index]) return;
}
const tabItemEl = instance.refs["dynamic" + index][0]; const tabItemEl = instance.refs["dynamic" + index][0];
const tabItemElOffsetLeft = (tabItemEl as HTMLElement)?.offsetLeft; const tabItemElOffsetLeft = (tabItemEl as HTMLElement)?.offsetLeft;
const tabItemOffsetWidth = (tabItemEl as HTMLElement)?.offsetWidth; const tabItemOffsetWidth = (tabItemEl as HTMLElement)?.offsetWidth;
@@ -137,7 +70,9 @@ const moveToView = (index: number): void => {
: 0; : 0;
// 已有标签页总长度(包含溢出部分) // 已有标签页总长度(包含溢出部分)
const tabDomWidth = tabDom.value ? tabDom.value?.offsetWidth : 0; const tabDomWidth = tabDom.value ? tabDom.value?.offsetWidth : 0;
scrollbarDomWidth <= tabDomWidth
? (isShowArrow.value = true)
: (isShowArrow.value = false);
if (tabDomWidth < scrollbarDomWidth || tabItemElOffsetLeft === 0) { if (tabDomWidth < scrollbarDomWidth || tabItemElOffsetLeft === 0) {
translateX.value = 0; translateX.value = 0;
} else if (tabItemElOffsetLeft < -translateX.value) { } else if (tabItemElOffsetLeft < -translateX.value) {
@@ -186,68 +121,6 @@ const handleScroll = (offset: number): void => {
} }
}; };
const tagsViews = reactive<Array<tagsViewsType>>([
{
icon: refresh,
text: t("buttons.hsreload"),
divided: false,
disabled: false,
show: true
},
{
icon: close,
text: t("buttons.hscloseCurrentTab"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeLeft,
text: t("buttons.hscloseLeftTabs"),
divided: true,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeRight,
text: t("buttons.hscloseRightTabs"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeOther,
text: t("buttons.hscloseOtherTabs"),
divided: true,
disabled: multiTags.value.length > 2 ? false : true,
show: true
},
{
icon: closeAll,
text: t("buttons.hscloseAllTabs"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
}
]);
// 显示模式,默认灵动模式显示
const showModel = ref(
storageLocal.getItem("responsive-configure")?.showModel || "smart"
);
if (!showModel.value) {
const configure = storageLocal.getItem("responsive-configure");
configure.showModel = "card";
storageLocal.setItem("responsive-configure", configure);
}
let visible = ref(false);
let buttonLeft = ref(0);
let buttonTop = ref(0);
// 当前右键选中的路由信息
let currentSelect = ref({});
function dynamicRouteTag(value: string, parentPath: string): void { function dynamicRouteTag(value: string, parentPath: string): void {
const hasValue = multiTags.value.some(item => { const hasValue = multiTags.value.some(item => {
return item.path === value; return item.path === value;
@@ -272,20 +145,16 @@ function dynamicRouteTag(value: string, parentPath: string): void {
}); });
} }
} }
concatPath(router.options.routes, value, parentPath); concatPath(router.options.routes as any, value, parentPath);
} }
// 重新加载 /** 刷新路由 */
function onFresh() { function onFresh() {
toggleClass(true, refreshButton, document.querySelector(".rotate"));
const { fullPath, query } = unref(route); const { fullPath, query } = unref(route);
router.replace({ router.replace({
path: "/redirect" + fullPath, path: "/redirect" + fullPath,
query: query query: query
}); });
setTimeout(() => {
removeClass(document.querySelector(".rotate"), refreshButton);
}, 600);
} }
function deleteDynamicTag(obj: any, current: any, tag?: string) { function deleteDynamicTag(obj: any, current: any, tag?: string) {
@@ -296,6 +165,10 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
if (item.path === obj.path) { if (item.path === obj.path) {
return item.query === obj.query; return item.query === obj.query;
} }
} else if (item.params) {
if (item.path === obj.path) {
return item.params === obj.params;
}
} else { } else {
return item.path === obj.path; return item.path === obj.path;
} }
@@ -307,24 +180,12 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
other?: boolean other?: boolean
): void => { ): void => {
if (other) { if (other) {
useMultiTagsStoreHook().handleTags("equal", [ useMultiTagsStoreHook().handleTags("equal", [routerArrays[0], obj]);
{
path: "/welcome",
parentPath: "/",
meta: {
title: "menus.hshome",
i18n: true,
icon: "home-filled"
}
},
obj
]);
} else { } else {
// @ts-ignore
delAliveRouteList = useMultiTagsStoreHook().handleTags("splice", "", { delAliveRouteList = useMultiTagsStoreHook().handleTags("splice", "", {
startIndex, startIndex,
length length
}); }) as any;
} }
}; };
@@ -346,24 +207,25 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
: handleAliveRoute(route.matched, "delete"); : handleAliveRoute(route.matched, "delete");
// 如果删除当前激活tag就自动切换到最后一个tag // 如果删除当前激活tag就自动切换到最后一个tag
if (tag === "left") return; if (tag === "left") return;
nextTick(() => { if (newRoute[0]?.query) {
router.push({ router.push({ name: newRoute[0].name, query: newRoute[0].query });
path: newRoute[0].path, } else if (newRoute[0]?.params) {
query: newRoute[0].query router.push({ name: newRoute[0].name, params: newRoute[0].params });
}); } else {
}); router.push({ path: newRoute[0].path });
}
} else { } else {
// 删除缓存路由 // 删除缓存路由
tag ? delAliveRoutes(delAliveRouteList) : delAliveRoutes([obj]); tag ? delAliveRoutes(delAliveRouteList) : delAliveRoutes([obj]);
if (!multiTags.value.length) return; if (!multiTags.value.length) return;
let isHasActiveTag = multiTags.value.some(item => { if (multiTags.value.some(item => item.path === route.path)) return;
return item.path === route.path; if (newRoute[0]?.query) {
}); router.push({ name: newRoute[0].name, query: newRoute[0].query });
!isHasActiveTag && } else if (newRoute[0]?.params) {
router.push({ router.push({ name: newRoute[0].name, params: newRoute[0].params });
path: newRoute[0].path, } else {
query: newRoute[0].query router.push({ path: newRoute[0].path });
}); }
} }
} }
@@ -380,7 +242,8 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
path: selectRoute.path, path: selectRoute.path,
meta: selectRoute.meta, meta: selectRoute.meta,
name: selectRoute.name, name: selectRoute.name,
query: selectRoute.query query: selectRoute?.query,
params: selectRoute?.params
}; };
} else { } else {
selectTagRoute = { path: route.path, meta: route.meta }; selectTagRoute = { path: route.path, meta: route.meta };
@@ -389,7 +252,7 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
// 当前路由信息 // 当前路由信息
switch (key) { switch (key) {
case 0: case 0:
// 重新加载 // 刷新路由
onFresh(); onFresh();
break; break;
case 1: case 1:
@@ -414,22 +277,48 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
startIndex: 1, startIndex: 1,
length: multiTags.value.length length: multiTags.value.length
}); });
usePermissionStoreHook().clearAllCachePage();
router.push("/welcome"); router.push("/welcome");
break; break;
case 6:
// 整体页面全屏
toggle();
setTimeout(() => {
if (isFullscreen.value) {
tagsViews[6].icon = "exit-fullscreen";
tagsViews[6].text = $t("buttons.hswholeExitFullScreen");
} else {
tagsViews[6].icon = "fullscreen";
tagsViews[6].text = $t("buttons.hswholeFullScreen");
}
}, 100);
break;
case 7:
// 内容区全屏
onContentFullScreen();
setTimeout(() => {
if (pureSetting.hiddenSideBar) {
tagsViews[7].icon = "exit-fullscreen";
tagsViews[7].text = $t("buttons.hscontentExitFullScreen");
} else {
tagsViews[7].icon = "fullscreen";
tagsViews[7].text = $t("buttons.hscontentFullScreen");
}
}, 100);
break;
} }
setTimeout(() => { setTimeout(() => {
showMenuModel(route.fullPath, route.query); showMenuModel(route.fullPath, route.query);
}); });
} }
// 触发右键中菜单的点击事件 function handleCommand(command: any) {
function selectTag(key, item) { const { key, item } = command;
onClickDrop(key, item, currentSelect.value); onClickDrop(key, item);
} }
function closeMenu() { /** 触发右键中菜单的点击事件 */
visible.value = false; function selectTag(key, item) {
onClickDrop(key, item, currentSelect.value);
} }
function showMenus(value: boolean) { function showMenus(value: boolean) {
@@ -444,7 +333,7 @@ function disabledMenus(value: boolean) {
}); });
} }
// 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 /** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */
function showMenuModel( function showMenuModel(
currentPath: string, currentPath: string,
query: object = {}, query: object = {},
@@ -504,7 +393,7 @@ function openMenu(tag, e) {
// 右键菜单为首页,只显示刷新 // 右键菜单为首页,只显示刷新
showMenus(false); showMenus(false);
tagsViews[0].show = true; tagsViews[0].show = true;
} else if (route.path !== tag.path) { } else if (route.path !== tag.path && route.name !== tag.name) {
// 右键菜单不匹配当前路由,隐藏刷新 // 右键菜单不匹配当前路由,隐藏刷新
tagsViews[0].show = false; tagsViews[0].show = false;
showMenuModel(tag.path, tag.query); showMenuModel(tag.path, tag.query);
@@ -532,63 +421,36 @@ function openMenu(tag, e) {
} else { } else {
buttonLeft.value = left; buttonLeft.value = left;
} }
pureSetting.hiddenSideBar useSettingStoreHook().hiddenSideBar
? (buttonTop.value = e.clientY) ? (buttonTop.value = e.clientY)
: (buttonTop.value = e.clientY - 40); : (buttonTop.value = e.clientY - 40);
setTimeout(() => { nextTick(() => {
visible.value = true; visible.value = true;
}, 10);
}
// 触发tags标签切换
function tagOnClick(item) {
router.push({
path: item?.path,
query: item?.query
}); });
showMenuModel(item?.path, item?.query);
} }
// 鼠标移入 /** 触发tags标签切换 */
function onMouseenter(index) { function tagOnClick(item) {
if (index) activeIndex.value = index; const { name, path } = item;
if (unref(showModel) === "smart") { if (name) {
if (hasClass(instance.refs["schedule" + index][0], "schedule-active")) if (item.query) {
return; router.push({
toggleClass(true, "schedule-in", instance.refs["schedule" + index][0]); name,
toggleClass(false, "schedule-out", instance.refs["schedule" + index][0]); query: item.query
} else { });
if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return; } else if (item.params) {
toggleClass(true, "card-in", instance.refs["dynamic" + index][0]); router.push({
toggleClass(false, "card-out", instance.refs["dynamic" + index][0]); name,
} params: item.params
} });
// 鼠标移出
function onMouseleave(index) {
activeIndex.value = -1;
if (unref(showModel) === "smart") {
if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
return;
toggleClass(false, "schedule-in", instance.refs["schedule" + index][0]);
toggleClass(true, "schedule-out", instance.refs["schedule" + index][0]);
} else {
if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
toggleClass(false, "card-in", instance.refs["dynamic" + index][0]);
toggleClass(true, "card-out", instance.refs["dynamic" + index][0]);
}
}
watch(
() => visible.value,
val => {
if (val) {
document.body.addEventListener("click", closeMenu);
} else { } else {
document.body.removeEventListener("click", closeMenu); router.push({ name });
} }
} else {
router.push({ path });
} }
); // showMenuModel(item?.path, item?.query);
}
onBeforeMount(() => { onBeforeMount(() => {
if (!instance) return; if (!instance) return;
@@ -597,9 +459,9 @@ onBeforeMount(() => {
showMenuModel(route.fullPath); showMenuModel(route.fullPath);
// 触发隐藏标签页 // 触发隐藏标签页
emitter.on("tagViewsChange", key => { emitter.on("tagViewsChange", (key: any) => {
if (unref(showTags) === key) return; if (unref(showTags as any) === key) return;
showTags.value = key; (showTags as any).value = key;
}); });
// 改变标签风格 // 改变标签风格
@@ -616,22 +478,26 @@ onBeforeMount(() => {
}); });
}); });
const getTabStyle = computed((): CSSProperties => { watch([route], () => {
return { activeIndex.value = -1;
transform: `translateX(${translateX.value}px)` dynamicTagView();
};
}); });
const getContextMenuStyle = computed((): CSSProperties => { onMounted(() => {
return { left: buttonLeft.value + "px", top: buttonTop.value + "px" }; useResizeObserver(
scrollbarDom,
useDebounceFn(() => {
dynamicTagView();
}, 200)
);
}); });
</script> </script>
<template> <template>
<div ref="containerDom" class="tags-view" v-if="!showTags"> <div ref="containerDom" class="tags-view" v-if="!showTags">
<div class="arrow-left"> <span v-show="isShowArrow" class="arrow-left">
<IconifyIconOffline icon="arrow-left-s-line" @click="handleScroll(200)" /> <IconifyIconOffline icon="arrow-left-s-line" @click="handleScroll(200)" />
</div> </span>
<div ref="scrollbarDom" class="scroll-container"> <div ref="scrollbarDom" class="scroll-container">
<div class="tab" ref="tabDom" :style="getTabStyle"> <div class="tab" ref="tabDom" :style="getTabStyle">
<div <div
@@ -650,10 +516,13 @@ const getContextMenuStyle = computed((): CSSProperties => {
@mouseleave.prevent="onMouseleave(index)" @mouseleave.prevent="onMouseleave(index)"
@click="tagOnClick(item)" @click="tagOnClick(item)"
> >
<router-link :to="item.path" <router-link
>{{ transformI18n(item.meta.title, item.meta.i18n) }} :to="item.path"
class="dark:!text-text_color_primary dark:hover:!text-primary"
>
{{ transformI18n(item.meta.title) }}
</router-link> </router-link>
<el-icon <span
v-if=" v-if="
iconIsActive(item, index) || iconIsActive(item, index) ||
(index === activeIndex && index !== 0) (index === activeIndex && index !== 0)
@@ -662,16 +531,16 @@ const getContextMenuStyle = computed((): CSSProperties => {
@click.stop="deleteMenu(item)" @click.stop="deleteMenu(item)"
> >
<IconifyIconOffline icon="close-bold" /> <IconifyIconOffline icon="close-bold" />
</el-icon> </span>
<div <div
:ref="'schedule' + index" :ref="'schedule' + index"
v-if="showModel !== 'card'" v-if="showModel !== 'card'"
:class="[scheduleIsActive(item)]" :class="[scheduleIsActive(item)]"
></div> />
</div> </div>
</div> </div>
</div> </div>
<span class="arrow-right"> <span v-show="isShowArrow" class="arrow-right">
<IconifyIconOffline <IconifyIconOffline
icon="arrow-right-s-line" icon="arrow-right-s-line"
@click="handleScroll(-200)" @click="handleScroll(-200)"
@@ -686,53 +555,41 @@ const getContextMenuStyle = computed((): CSSProperties => {
class="contextmenu" class="contextmenu"
> >
<div <div
v-for="(item, key) in tagsViews" v-for="(item, key) in tagsViews.slice(0, 6)"
:key="key" :key="key"
style="display: flex; align-items: center" style="display: flex; align-items: center"
> >
<li v-if="item.show" @click="selectTag(key, item)"> <li v-if="item.show" @click="selectTag(key, item)">
<component :is="item.icon" :key="key" /> <IconifyIconOffline :icon="item.icon" />
{{ $t(item.text) }} {{ transformI18n(item.text) }}
</li> </li>
</div> </div>
</ul> </ul>
</transition> </transition>
<!-- 右侧功能按钮 --> <!-- 右侧功能按钮 -->
<ul class="right-button"> <el-dropdown
<li> trigger="click"
<el-icon placement="bottom-end"
:title="$t('buttons.hsrefreshRoute')" @command="handleCommand"
class="el-icon-refresh-right rotate" >
@click="onFresh" <span class="arrow-down">
> <IconifyIconOffline icon="arrow-down" class="dark:text-white" />
<IconifyIconOffline icon="refresh-right" /> </span>
</el-icon> <template #dropdown>
</li> <el-dropdown-menu>
<li> <el-dropdown-item
<el-dropdown trigger="click" placement="bottom-end"> v-for="(item, key) in tagsViews"
<el-icon> :key="key"
<IconifyIconOffline icon="arrow-down" /> :command="{ key, item }"
</el-icon> :divided="item.divided"
<template #dropdown> :disabled="item.disabled"
<el-dropdown-menu> >
<el-dropdown-item <IconifyIconOffline :icon="item.icon" />
v-for="(item, key) in tagsViews" {{ transformI18n(item.text) }}
:key="key" </el-dropdown-item>
:divided="item.divided" </el-dropdown-menu>
:disabled="item.disabled" </template>
@click="onClickDrop(key, item)" </el-dropdown>
>
<component :is="item.icon" :key="key" />
{{ $t(item.text) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</li>
<li>
<slot></slot>
</li>
</ul>
</div> </div>
</template> </template>

74
src/layout/frameView.vue Normal file
View File

@@ -0,0 +1,74 @@
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { ref, unref, onMounted, nextTick } from "vue";
defineOptions({
name: "FrameView"
});
const { t } = useI18n();
const loading = ref(true);
const currentRoute = useRoute();
const frameSrc = ref<string>("");
const frameRef = ref<HTMLElement | null>(null);
if (unref(currentRoute.meta)?.frameSrc) {
frameSrc.value = unref(currentRoute.meta)?.frameSrc as string;
}
unref(currentRoute.meta)?.frameLoading === false && hideLoading();
function hideLoading() {
loading.value = false;
}
function init() {
nextTick(() => {
const iframe = unref(frameRef);
if (!iframe) return;
const _frame = iframe as any;
if (_frame.attachEvent) {
_frame.attachEvent("onload", () => {
hideLoading();
});
} else {
iframe.onload = () => {
hideLoading();
};
}
});
}
onMounted(() => {
init();
});
</script>
<template>
<div
class="frame"
v-loading="loading"
:element-loading-text="t('status.hsLoad')"
>
<iframe :src="frameSrc" class="frame-iframe" ref="frameRef" />
</div>
</template>
<style lang="scss" scoped>
.frame {
height: calc(100vh - 88px);
z-index: 998;
.frame-iframe {
width: 100%;
height: 100%;
overflow: hidden;
border: 0;
box-sizing: border-box;
}
}
.main-content {
margin: 2px 0 0 !important;
}
</style>

View File

@@ -0,0 +1,26 @@
import { ref } from "vue";
export function useBoolean(initValue = false) {
const bool = ref(initValue);
function setBool(value: boolean) {
bool.value = value;
}
function setTrue() {
setBool(true);
}
function setFalse() {
setBool(false);
}
function toggle() {
setBool(!bool.value);
}
return {
bool,
setBool,
setTrue,
setFalse,
toggle
};
}

View File

@@ -0,0 +1,116 @@
import { ref } from "vue";
import { getConfig } from "/@/config";
import { find } from "lodash-unified";
import { useLayout } from "./useLayout";
import { themeColorsType } from "../types";
import { TinyColor } from "@ctrl/tinycolor";
import { useGlobal } from "@pureadmin/utils";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import {
darken,
lighten,
toggleTheme
} from "@pureadmin/theme/dist/browser-utils";
export function useDataThemeChange() {
const { layoutTheme, layout } = useLayout();
const themeColors = ref<Array<themeColorsType>>([
/* 道奇蓝(默认) */
{ color: "#1b2a47", themeColor: "default" },
/* 亮白色 */
{ color: "#ffffff", themeColor: "light" },
/* 猩红色 */
{ color: "#f5222d", themeColor: "dusk" },
/* 橙红色 */
{ color: "#fa541c", themeColor: "volcano" },
/* 金色 */
{ color: "#fadb14", themeColor: "yellow" },
/* 绿宝石 */
{ color: "#13c2c2", themeColor: "mingQing" },
/* 酸橙绿 */
{ color: "#52c41a", themeColor: "auroraGreen" },
/* 深粉色 */
{ color: "#eb2f96", themeColor: "pink" },
/* 深紫罗兰色 */
{ color: "#722ed1", themeColor: "saucePurple" }
]);
const { $storage } = useGlobal<GlobalPropertiesApi>();
const dataTheme = ref<boolean>($storage?.layout?.darkMode);
const body = document.documentElement as HTMLElement;
/** 设置导航主题色 */
function setLayoutThemeColor(theme = "default") {
layoutTheme.value.theme = theme;
toggleTheme({
scopeName: `layout-theme-${theme}`
});
$storage.layout = {
layout: layout.value,
theme,
darkMode: dataTheme.value,
sidebarStatus: $storage.layout?.sidebarStatus,
epThemeColor: $storage.layout?.epThemeColor
};
if (theme === "default" || theme === "light") {
setEpThemeColor(getConfig().EpThemeColor);
} else {
const colors = find(themeColors.value, { themeColor: theme });
setEpThemeColor(colors.color);
}
}
/**
* @description 自动计算hover和active颜色
* @see {@link https://element-plus.org/zh-CN/component/button.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E9%A2%9C%E8%89%B2}
*/
const shadeBgColor = (color: string): string => {
return new TinyColor(color).shade(10).toString();
};
/** 设置ep主题色 */
const setEpThemeColor = (color: string) => {
useEpThemeStoreHook().setEpThemeColor(color);
body.style.setProperty("--el-color-primary-active", shadeBgColor(color));
document.documentElement.style.setProperty("--el-color-primary", color);
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(
`--el-color-primary-light-${i}`,
lighten(color, i / 10)
);
}
for (let i = 1; i <= 2; i++) {
document.documentElement.style.setProperty(
`--el-color-primary-dark-${i}`,
darken(color, i / 10)
);
}
};
/** 日间、夜间主题切换 */
function dataThemeChange() {
/* 如果当前是light夜间主题默认切换到default主题 */
if (useEpThemeStoreHook().epTheme === "light" && dataTheme.value) {
setLayoutThemeColor("default");
} else {
setLayoutThemeColor(useEpThemeStoreHook().epTheme);
}
if (dataTheme.value) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
return {
body,
dataTheme,
layoutTheme,
themeColors,
dataThemeChange,
setEpThemeColor,
setLayoutThemeColor
};
}

View File

@@ -0,0 +1,60 @@
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { routerArrays } from "../types";
import { useGlobal } from "@pureadmin/utils";
import { useMultiTagsStore } from "/@/store/modules/multiTags";
export function useLayout() {
const { $storage, $config } = useGlobal<GlobalPropertiesApi>();
const initStorage = () => {
/** 路由 */
if (
useMultiTagsStore().multiTagsCache &&
(!$storage.tags || $storage.tags.length === 0)
) {
$storage.tags = routerArrays;
}
/** 国际化 */
if (!$storage.locale) {
$storage.locale = { locale: $config?.Locale ?? "zh" };
useI18n().locale.value = $config?.Locale ?? "zh";
}
/** 导航 */
if (!$storage.layout) {
$storage.layout = {
layout: $config?.Layout ?? "vertical",
theme: $config?.Theme ?? "default",
darkMode: $config?.DarkMode ?? false,
sidebarStatus: $config?.SidebarStatus ?? true,
epThemeColor: $config?.EpThemeColor ?? "#409EFF"
};
}
/** 灰色模式、色弱模式、隐藏标签页 */
if (!$storage.configure) {
$storage.configure = {
grey: $config?.Grey ?? false,
weak: $config?.Weak ?? false,
hideTabs: $config?.HideTabs ?? false,
showLogo: $config?.ShowLogo ?? true,
showModel: $config?.ShowModel ?? "smart",
multiTagsCache: $config?.MultiTagsCache ?? false
};
}
};
/** 清空缓存后从serverConfig.json读取默认配置并赋值到storage中 */
const layout = computed(() => {
return $storage?.layout.layout;
});
const layoutTheme = computed(() => {
return $storage.layout;
});
return {
layout,
layoutTheme,
initStorage
};
}

155
src/layout/hooks/useNav.ts Normal file
View File

@@ -0,0 +1,155 @@
import { computed } from "vue";
import { getConfig } from "/@/config";
import { useRouter } from "vue-router";
import { emitter } from "/@/utils/mitt";
import { routeMetaType } from "../types";
import { useGlobal } from "@pureadmin/utils";
import { transformI18n } from "/@/plugins/i18n";
import { router, remainingPaths } from "/@/router";
import { useAppStoreHook } from "/@/store/modules/app";
import { useUserStoreHook } from "/@/store/modules/user";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
const errorInfo = "当前路由配置不正确,请检查配置";
export function useNav() {
const pureApp = useAppStoreHook();
const routers = useRouter().options.routes;
/** 用户名 */
const username = computed(() => {
return useUserStoreHook()?.username;
});
/** 设置国际化选中后的样式 */
const getDropdownItemStyle = computed(() => {
return (locale, t) => {
return {
background: locale === t ? useEpThemeStoreHook().epThemeColor : "",
color: locale === t ? "#f4f4f5" : "#000"
};
};
});
const getDropdownItemClass = computed(() => {
return (locale, t) => {
return locale === t ? "" : "dark:hover:!text-primary";
};
});
const avatarsStyle = computed(() => {
return username.value ? { marginRight: "10px" } : "";
});
const isCollapse = computed(() => {
return !pureApp.getSidebarStatus;
});
const device = computed(() => {
return pureApp.getDevice;
});
const { $storage, $config } = useGlobal<GlobalPropertiesApi>();
const layout = computed(() => {
return $storage?.layout?.layout;
});
const title = computed(() => {
return $config.Title;
});
/** 动态title */
function changeTitle(meta: routeMetaType) {
const Title = getConfig().Title;
if (Title) document.title = `${transformI18n(meta.title)} | ${Title}`;
else document.title = transformI18n(meta.title);
}
/** 退出登录 */
function logout() {
useUserStoreHook().logOut();
}
function backHome() {
router.push("/welcome");
}
function onPanel() {
emitter.emit("openPanel");
}
function toggleSideBar() {
pureApp.toggleSideBar();
}
function handleResize(menuRef) {
menuRef?.handleResize();
}
function resolvePath(route) {
if (!route.children) return console.error(errorInfo);
const httpReg = /^http(s?):\/\//;
const routeChildPath = route.children[0]?.path;
if (httpReg.test(routeChildPath)) {
return route.path + "/" + routeChildPath;
} else {
return routeChildPath;
}
}
function menuSelect(indexPath: string, routers): void {
if (isRemaining(indexPath)) return;
let parentPath = "";
const parentPathIndex = indexPath.lastIndexOf("/");
if (parentPathIndex > 0) {
parentPath = indexPath.slice(0, parentPathIndex);
}
/** 找到当前路由的信息 */
function findCurrentRoute(indexPath: string, routes) {
if (!routes) return console.error(errorInfo);
return routes.map(item => {
if (item.path === indexPath) {
if (item.redirect) {
findCurrentRoute(item.redirect, item.children);
} else {
/** 切换左侧菜单 通知标签页 */
emitter.emit("changLayoutRoute", {
indexPath,
parentPath
});
}
} else {
if (item.children) findCurrentRoute(indexPath, item.children);
}
});
}
findCurrentRoute(indexPath, routers);
}
/** 判断路径是否参与菜单 */
function isRemaining(path: string): boolean {
return remainingPaths.includes(path);
}
return {
title,
device,
layout,
logout,
routers,
$storage,
backHome,
onPanel,
changeTitle,
toggleSideBar,
menuSelect,
handleResize,
resolvePath,
isCollapse,
pureApp,
username,
avatarsStyle,
getDropdownItemStyle,
getDropdownItemClass
};
}

235
src/layout/hooks/useTag.ts Normal file
View File

@@ -0,0 +1,235 @@
import {
ref,
unref,
watch,
computed,
reactive,
onMounted,
CSSProperties,
getCurrentInstance
} from "vue";
import { tagsViewsType } from "../types";
import { isEqual } from "lodash-unified";
import type { StorageConfigs } from "/#/index";
import { useEventListener } from "@vueuse/core";
import { useRoute, useRouter } from "vue-router";
import { transformI18n, $t } from "/@/plugins/i18n";
import { useSettingStoreHook } from "/@/store/modules/settings";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { storageLocal, toggleClass, hasClass } from "@pureadmin/utils";
export function useTags() {
const route = useRoute();
const router = useRouter();
const instance = getCurrentInstance();
const pureSetting = useSettingStoreHook();
const buttonTop = ref(0);
const buttonLeft = ref(0);
const translateX = ref(0);
const visible = ref(false);
const activeIndex = ref(-1);
// 当前右键选中的路由信息
const currentSelect = ref({});
/** 显示模式,默认灵动模式 */
const showModel = ref(
storageLocal.getItem<StorageConfigs>("responsive-configure")?.showModel ||
"smart"
);
/** 是否隐藏标签页,默认显示 */
const showTags =
ref(
storageLocal.getItem<StorageConfigs>("responsive-configure").hideTabs
) ?? ref("false");
const multiTags: any = computed(() => {
return useMultiTagsStoreHook().multiTags;
});
const tagsViews = reactive<Array<tagsViewsType>>([
{
icon: "refresh-right",
text: $t("buttons.hsreload"),
divided: false,
disabled: false,
show: true
},
{
icon: "close",
text: $t("buttons.hscloseCurrentTab"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: "close-left-tags",
text: $t("buttons.hscloseLeftTabs"),
divided: true,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: "close-right-tags",
text: $t("buttons.hscloseRightTabs"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: "close-other-tags",
text: $t("buttons.hscloseOtherTabs"),
divided: true,
disabled: multiTags.value.length > 2 ? false : true,
show: true
},
{
icon: "close-all-tags",
text: $t("buttons.hscloseAllTabs"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: "fullscreen",
text: $t("buttons.hswholeFullScreen"),
divided: true,
disabled: false,
show: true
},
{
icon: "fullscreen",
text: $t("buttons.hscontentFullScreen"),
divided: false,
disabled: false,
show: true
}
]);
function conditionHandle(item, previous, next) {
if (
Object.keys(route.query).length === 0 &&
Object.keys(route.params).length === 0
) {
return route.path === item.path ? previous : next;
} else if (Object.keys(route.query).length > 0) {
return isEqual(route.query, item.query) ? previous : next;
} else {
return isEqual(route.params, item.params) ? previous : next;
}
}
const iconIsActive = computed(() => {
return (item, index) => {
if (index === 0) return;
return conditionHandle(item, true, false);
};
});
const linkIsActive = computed(() => {
return item => {
return conditionHandle(item, "is-active", "");
};
});
const scheduleIsActive = computed(() => {
return item => {
return conditionHandle(item, "schedule-active", "");
};
});
const getTabStyle = computed((): CSSProperties => {
return {
transform: `translateX(${translateX.value}px)`
};
});
const getContextMenuStyle = computed((): CSSProperties => {
return { left: buttonLeft.value + "px", top: buttonTop.value + "px" };
});
const closeMenu = () => {
visible.value = false;
};
/** 鼠标移入添加激活样式 */
function onMouseenter(index) {
if (index) activeIndex.value = index;
if (unref(showModel) === "smart") {
if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
return;
toggleClass(true, "schedule-in", instance.refs["schedule" + index][0]);
toggleClass(false, "schedule-out", instance.refs["schedule" + index][0]);
} else {
if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
toggleClass(true, "card-in", instance.refs["dynamic" + index][0]);
toggleClass(false, "card-out", instance.refs["dynamic" + index][0]);
}
}
/** 鼠标移出恢复默认样式 */
function onMouseleave(index) {
activeIndex.value = -1;
if (unref(showModel) === "smart") {
if (hasClass(instance.refs["schedule" + index][0], "schedule-active"))
return;
toggleClass(false, "schedule-in", instance.refs["schedule" + index][0]);
toggleClass(true, "schedule-out", instance.refs["schedule" + index][0]);
} else {
if (hasClass(instance.refs["dynamic" + index][0], "card-active")) return;
toggleClass(false, "card-in", instance.refs["dynamic" + index][0]);
toggleClass(true, "card-out", instance.refs["dynamic" + index][0]);
}
}
function onContentFullScreen() {
pureSetting.hiddenSideBar
? pureSetting.changeSetting({ key: "hiddenSideBar", value: false })
: pureSetting.changeSetting({ key: "hiddenSideBar", value: true });
}
onMounted(() => {
if (!showModel.value) {
const configure = storageLocal.getItem<StorageConfigs>(
"responsive-configure"
);
configure.showModel = "card";
storageLocal.setItem("responsive-configure", configure);
}
});
watch(
() => visible.value,
() => {
useEventListener(document, "click", closeMenu);
}
);
return {
route,
router,
visible,
showTags,
instance,
multiTags,
showModel,
tagsViews,
buttonTop,
buttonLeft,
translateX,
pureSetting,
activeIndex,
getTabStyle,
iconIsActive,
linkIsActive,
currentSelect,
scheduleIsActive,
getContextMenuStyle,
$t,
closeMenu,
onMounted,
onMouseenter,
onMouseleave,
transformI18n,
onContentFullScreen
};
}

View File

@@ -0,0 +1,37 @@
import { useNav } from "./useNav";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { watch, type Ref } from "vue";
export function useTranslationLang(ref?: Ref) {
const { $storage, changeTitle, handleResize } = useNav();
const { locale, t } = useI18n();
const route = useRoute();
function translationCh() {
$storage.locale = { locale: "zh" };
locale.value = "zh";
ref && handleResize(ref.value);
}
function translationEn() {
$storage.locale = { locale: "en" };
locale.value = "en";
ref && handleResize(ref.value);
}
watch(
() => locale.value,
() => {
changeTitle(route.meta);
}
);
return {
t,
route,
locale,
translationCh,
translationEn
};
}

View File

@@ -1,24 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import {
h,
reactive,
computed,
onMounted,
defineComponent,
getCurrentInstance
} from "vue";
import { setType } from "./types"; import { setType } from "./types";
import { useI18n } from "vue-i18n";
import { routerArrays } from "./types";
import { emitter } from "/@/utils/mitt"; import { emitter } from "/@/utils/mitt";
import { useLayout } from "./hooks/useLayout";
import { useAppStoreHook } from "/@/store/modules/app"; import { useAppStoreHook } from "/@/store/modules/app";
import { deviceDetection } from "/@/utils/deviceDetection";
import { useMultiTagsStore } from "/@/store/modules/multiTags";
import { useSettingStoreHook } from "/@/store/modules/settings"; import { useSettingStoreHook } from "/@/store/modules/settings";
import { deviceDetection, useDark, useGlobal } from "@pureadmin/utils";
import backTop from "/@/assets/svg/back_top.svg?component"; import { h, reactive, computed, onMounted, defineComponent } from "vue";
import fullScreen from "/@/assets/svg/full_screen.svg?component";
import exitScreen from "/@/assets/svg/exit_screen.svg?component";
import navbar from "./components/navbar.vue"; import navbar from "./components/navbar.vue";
import tag from "./components/tag/index.vue"; import tag from "./components/tag/index.vue";
@@ -26,52 +13,13 @@ import appMain from "./components/appMain.vue";
import setting from "./components/setting/index.vue"; import setting from "./components/setting/index.vue";
import Vertical from "./components/sidebar/vertical.vue"; import Vertical from "./components/sidebar/vertical.vue";
import Horizontal from "./components/sidebar/horizontal.vue"; import Horizontal from "./components/sidebar/horizontal.vue";
import backTop from "/@/assets/svg/back_top.svg?component";
const { isDark } = useDark();
const { layout } = useLayout();
const isMobile = deviceDetection(); const isMobile = deviceDetection();
const pureSetting = useSettingStoreHook(); const pureSetting = useSettingStoreHook();
const instance = getCurrentInstance().appContext.app.config.globalProperties; const { $storage } = useGlobal<GlobalPropertiesApi>();
// 清空缓存后从serverConfig.json读取默认配置并赋值到storage中
const layout = computed(() => {
// 路由
if (
useMultiTagsStore().multiTagsCache &&
(!instance.$storage.tags || instance.$storage.tags.length === 0)
) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
instance.$storage.tags = routerArrays;
}
// 国际化
if (!instance.$storage.locale) {
// eslint-disable-next-line
instance.$storage.locale = { locale: instance.$config?.Locale ?? "zh" };
useI18n().locale.value = instance.$config?.Locale ?? "zh";
}
// 导航
if (!instance.$storage.layout) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
instance.$storage.layout = {
layout: instance.$config?.Layout ?? "vertical",
theme: instance.$config?.Theme ?? "default",
darkMode: instance.$config?.DarkMode ?? false,
sidebarStatus: instance.$config?.SidebarStatus ?? true,
epThemeColor: instance.$config?.EpThemeColor ?? "#409EFF"
};
}
// 灰色模式、色弱模式、隐藏标签页
if (!instance.$storage.configure) {
// eslint-disable-next-line
instance.$storage.configure = {
grey: instance.$config?.Grey ?? false,
weak: instance.$config?.Weak ?? false,
hideTabs: instance.$config?.HideTabs ?? false,
showLogo: instance.$config?.ShowLogo ?? true,
showModel: instance.$config?.ShowModel ?? "smart",
multiTagsCache: instance.$config?.MultiTagsCache ?? false
};
}
return instance.$storage?.layout.layout;
});
const set: setType = reactive({ const set: setType = reactive({
sidebar: computed(() => { sidebar: computed(() => {
@@ -96,18 +44,18 @@ const set: setType = reactive({
}), }),
hideTabs: computed(() => { hideTabs: computed(() => {
return instance.$storage?.configure.hideTabs; return $storage?.configure.hideTabs;
}) })
}); });
function setTheme(layoutModel: string) { function setTheme(layoutModel: string) {
window.document.body.setAttribute("layout", layoutModel); window.document.body.setAttribute("layout", layoutModel);
instance.$storage.layout = { $storage.layout = {
layout: `${layoutModel}`, layout: `${layoutModel}`,
theme: instance.$storage.layout?.theme, theme: $storage.layout?.theme,
darkMode: instance.$storage.layout?.darkMode, darkMode: $storage.layout?.darkMode,
sidebarStatus: instance.$storage.layout?.sidebarStatus, sidebarStatus: $storage.layout?.sidebarStatus,
epThemeColor: instance.$storage.layout?.epThemeColor epThemeColor: $storage.layout?.epThemeColor
}; };
} }
@@ -123,7 +71,7 @@ let isAutoCloseSidebar = true;
emitter.on("resize", ({ detail }) => { emitter.on("resize", ({ detail }) => {
if (isMobile) return; if (isMobile) return;
let { width } = detail; let { width } = detail;
width <= 670 ? setTheme("vertical") : setTheme(useAppStoreHook().layout); width <= 760 ? setTheme("vertical") : setTheme(useAppStoreHook().layout);
/** width app-wrapper类容器宽度 /** width app-wrapper类容器宽度
* 0 < width <= 760 隐藏侧边栏 * 0 < width <= 760 隐藏侧边栏
* 760 < width <= 990 折叠侧边栏 * 760 < width <= 990 折叠侧边栏
@@ -138,7 +86,7 @@ emitter.on("resize", ({ detail }) => {
isAutoCloseSidebar = false; isAutoCloseSidebar = false;
} }
} else if (width > 990) { } else if (width > 990) {
if (!set.sidebar.isClickHamburger) { if (!set.sidebar.isClickCollapse) {
toggle("desktop", true); toggle("desktop", true);
isAutoCloseSidebar = true; isAutoCloseSidebar = true;
} }
@@ -151,12 +99,6 @@ onMounted(() => {
} }
}); });
function onFullScreen() {
pureSetting.hiddenSideBar
? pureSetting.changeSetting({ key: "hiddenSideBar", value: false })
: pureSetting.changeSetting({ key: "hiddenSideBar", value: true });
}
const layoutHeader = defineComponent({ const layoutHeader = defineComponent({
render() { render() {
return h( return h(
@@ -165,35 +107,22 @@ const layoutHeader = defineComponent({
class: { "fixed-header": set.fixedHeader }, class: { "fixed-header": set.fixedHeader },
style: [ style: [
set.hideTabs && layout.value.includes("horizontal") set.hideTabs && layout.value.includes("horizontal")
? "box-shadow: 0 1px 4px rgb(0 21 41 / 8%);" ? isDark.value
? "box-shadow: 0 1px 4px #0d0d0d"
: "box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08)"
: "" : ""
] ]
}, },
{ {
default: () => [ default: () => [
!pureSetting.hiddenSideBar && layout.value.includes("vertical") !pureSetting.hiddenSideBar &&
(layout.value.includes("vertical") || layout.value.includes("mix"))
? h(navbar) ? h(navbar)
: h("div"), : null,
!pureSetting.hiddenSideBar && layout.value.includes("horizontal") !pureSetting.hiddenSideBar && layout.value.includes("horizontal")
? h(Horizontal) ? h(Horizontal)
: h("div"), : null,
h( h(tag)
tag,
{},
{
default: () => [
h(
"span",
{ onClick: onFullScreen },
{
default: () => [
!pureSetting.hiddenSideBar ? h(fullScreen) : h(exitScreen)
]
}
)
]
}
)
] ]
} }
); );
@@ -213,7 +142,10 @@ const layoutHeader = defineComponent({
@click="useAppStoreHook().toggleSideBar()" @click="useAppStoreHook().toggleSideBar()"
/> />
<Vertical <Vertical
v-show="!pureSetting.hiddenSideBar && layout.includes('vertical')" v-show="
!pureSetting.hiddenSideBar &&
(layout.includes('vertical') || layout.includes('mix'))
"
/> />
<div <div
:class="[ :class="[
@@ -230,7 +162,8 @@ const layoutHeader = defineComponent({
<el-backtop <el-backtop
title="回到顶部" title="回到顶部"
target=".main-container .el-scrollbar__wrap" target=".main-container .el-scrollbar__wrap"
><backTop /> >
<backTop />
</el-backtop> </el-backtop>
<layout-header /> <layout-header />
<!-- 主体内容 --> <!-- 主体内容 -->
@@ -264,10 +197,6 @@ const layoutHeader = defineComponent({
} }
} }
.main-hidden {
margin-left: 0 !important;
}
.app-mask { .app-mask {
background: #000; background: #000;
opacity: 0.3; opacity: 0.3;

View File

@@ -2,6 +2,10 @@
import { unref } from "vue"; import { unref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
defineOptions({
name: "Redirect"
});
const { currentRoute, replace } = useRouter(); const { currentRoute, replace } = useRouter();
const { params, query } = unref(currentRoute); const { params, query } = unref(currentRoute);
@@ -16,5 +20,5 @@ replace({
</script> </script>
<template> <template>
<div></div> <div />
</template> </template>

View File

@@ -1,15 +0,0 @@
<template>
<router-view>
<template #default="{ Component, route }">
<transition appear name="fade-transform" mode="out-in">
<component :is="Component" :key="route.fullPath" />
</transition>
</template>
</router-view>
</template>
<script lang="ts">
export default {
name: "layoutParentView"
};
</script>

View File

@@ -1,11 +0,0 @@
/* 酸橙绿 */
$subMenuActiveText: #fff;
$menuBg: #0b1e15;
$menuHover: #60ac80;
$subMenuBg: #000;
$subMenuActiveBg: #60ac80;
$navTextColor: #7a80b4;
$menuText: #7a80b4;
$sidebarLogo: #112f21;
$menuTitleHover: #fff;
$menuActiveBefore: #60ac80;

View File

@@ -1,29 +0,0 @@
/**
* 道奇蓝(默认)
* 此scss变量文件作为multipleScopeVars去编译时会自动移除!default以达到变量提升
* 同时此scss变量文件作为默认主题变量文件被其他.scss通过 @import 时,必需 !default
*/
/* 菜单选中后字体样式 */
$subMenuActiveText: #fff !default;
/* 菜单背景 */
$menuBg: #001529 !default;
/* 鼠标覆盖到菜单时的背景 */
$menuHover: #4091f7 !default;
/* 子菜单背景 */
$subMenuBg: #0f0303 !default;
/* 有无子集的激活菜单背景 */
$subMenuActiveBg: #4091f7 !default;
$navTextColor: #fff !default;
$menuText: rgba(254, 254, 254, 0.65) !default;
/* logo背景颜色 */
$sidebarLogo: #002140 !default;
/* 鼠标覆盖到菜单时的字体颜色 */
$menuTitleHover: #fff !default;
$menuActiveBefore: #4091f7 !default;

View File

@@ -1,11 +0,0 @@
/* 猩红色 */
$subMenuActiveText: #fff;
$menuBg: #2a0608;
$menuHover: #e13c39;
$subMenuBg: #000;
$subMenuActiveBg: #e13c39;
$navTextColor: red;
$menuText: rgba(254, 254, 254, 0.651);
$sidebarLogo: #42090c;
$menuTitleHover: #fff;
$menuActiveBefore: #e13c39;

View File

@@ -1,80 +0,0 @@
/* 动态改变element-plus主题色 */
import rgbHex from "rgb-hex";
import { convert } from "css-color-function";
import { TinyColor } from "@ctrl/tinycolor";
import epCss from "element-plus/dist/index.css";
// 色值表
const formula = {
"shade-1": "color(primary shade(10%))",
"light-1": "color(primary tint(10%))",
"light-2": "color(primary tint(20%))",
"light-3": "color(primary tint(30%))",
"light-4": "color(primary tint(40%))",
"light-5": "color(primary tint(50%))",
"light-6": "color(primary tint(60%))",
"light-7": "color(primary tint(70%))",
"light-8": "color(primary tint(80%))",
"light-9": "color(primary tint(90%))"
};
// 把生成的样式表写入到style中
export const writeNewStyle = (newStyle: string): void => {
const style = window.document.createElement("style");
style.innerText = newStyle;
window.document.head.appendChild(style);
};
// 根据主题色,生成最新的样式表
export const createNewStyle = (primaryStyle: string): string => {
// 根据主色生成色值表
const colors = createColors(primaryStyle);
// 在当前ep的默认样式表中标记需要替换的色值
let cssText = getStyleTemplate(epCss);
// 遍历生成的色值表,在 默认样式表 进行全局替换
Object.keys(colors).forEach(key => {
cssText = cssText.replace(
new RegExp("(:|\\s+)" + key, "g"),
"$1" + colors[key]
);
});
return cssText;
};
export const createColors = (primary: string) => {
if (!primary) return;
const colors = {
primary
};
Object.keys(formula).forEach(key => {
const value = formula[key].replace(/primary/, primary);
colors[key] = "#" + rgbHex(convert(value));
});
return colors;
};
const getStyleTemplate = (data: string): string => {
const colorMap = {
"#3a8ee6": "shade-1",
"#409eff": "primary",
"#53a8ff": "light-1",
"#66b1ff": "light-2",
"#79bbff": "light-3",
"#8cc5ff": "light-4",
"#a0cfff": "light-5",
"#b3d8ff": "light-6",
"#c6e2ff": "light-7",
"#d9ecff": "light-8",
"#ecf5ff": "light-9"
};
Object.keys(colorMap).forEach(key => {
const value = colorMap[key];
data = data.replace(new RegExp(key, "ig"), value);
});
return data;
};
// 自动计算hover和active颜色 https://element-plus.gitee.io/zh-CN/component/button.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E9%A2%9C%E8%89%B2-%E6%B5%8B%E8%AF%95%E7%89%88
export const shadeBgColor = (color: string): string => {
return new TinyColor(color).shade(10).toString();
};

159
src/layout/theme/index.ts Normal file
View File

@@ -0,0 +1,159 @@
/**
* @description ⚠️:此文件仅供主题插件使用,请不要在此文件中导出别的工具函数(仅在页面加载前运行)
*/
import { EpThemeColor } from "../../../public/serverConfig.json";
type MultipleScopeVarsItem = {
scopeName: string;
varsContent: string;
};
/** 将vxe默认主题色和ep默认主题色保持一致 */
const vxeColor = EpThemeColor;
/** 预设主题色 */
const themeColors = {
default: {
vxeColor,
subMenuActiveText: "#fff",
menuBg: "#001529",
menuHover: "#4091f7",
subMenuBg: "#0f0303",
subMenuActiveBg: "#4091f7",
navTextColor: "#fff",
menuText: "rgb(254 254 254 / 65%)",
sidebarLogo: "#002140",
menuTitleHover: "#fff",
menuActiveBefore: "#4091f7"
},
light: {
vxeColor,
subMenuActiveText: "#409eff",
menuBg: "#fff",
menuHover: "#e0ebf6",
subMenuBg: "#fff",
subMenuActiveBg: "#e0ebf6",
navTextColor: "#7a80b4",
menuText: "#7a80b4",
sidebarLogo: "#fff",
menuTitleHover: "#000",
menuActiveBefore: "#4091f7"
},
dusk: {
vxeColor: "#f5222d",
subMenuActiveText: "#fff",
menuBg: "#2a0608",
menuHover: "#e13c39",
subMenuBg: "#000",
subMenuActiveBg: "#e13c39",
navTextColor: "#red",
menuText: "rgb(254 254 254 / 65.1%)",
sidebarLogo: "#42090c",
menuTitleHover: "#fff",
menuActiveBefore: "#e13c39"
},
volcano: {
vxeColor: "#fa541c",
subMenuActiveText: "#fff",
menuBg: "#2b0e05",
menuHover: "#e85f33",
subMenuBg: "#0f0603",
subMenuActiveBg: "#e85f33",
navTextColor: "#fff",
menuText: "rgb(254 254 254 / 65%)",
sidebarLogo: "#441708",
menuTitleHover: "#fff",
menuActiveBefore: "#e85f33"
},
yellow: {
vxeColor: "#fadb14",
subMenuActiveText: "#d25f00",
menuBg: "#2b2503",
menuHover: "#f6da4d",
subMenuBg: "#0f0603",
subMenuActiveBg: "#f6da4d",
navTextColor: "#fff",
menuText: "rgb(254 254 254 / 65%)",
sidebarLogo: "#443b05",
menuTitleHover: "#fff",
menuActiveBefore: "#f6da4d"
},
mingQing: {
vxeColor: "#13c2c2",
subMenuActiveText: "#fff",
menuBg: "#032121",
menuHover: "#59bfc1",
subMenuBg: "#000",
subMenuActiveBg: "#59bfc1",
navTextColor: "#7a80b4",
menuText: "#7a80b4",
sidebarLogo: "#053434",
menuTitleHover: "#fff",
menuActiveBefore: "#59bfc1"
},
auroraGreen: {
vxeColor: "#52c41a",
subMenuActiveText: "#fff",
menuBg: "#0b1e15",
menuHover: "#60ac80",
subMenuBg: "#000",
subMenuActiveBg: "#60ac80",
navTextColor: "#7a80b4",
menuText: "#7a80b4",
sidebarLogo: "#112f21",
menuTitleHover: "#fff",
menuActiveBefore: "#60ac80"
},
pink: {
vxeColor: "#eb2f96",
subMenuActiveText: "#fff",
menuBg: "#28081a",
menuHover: "#d84493",
subMenuBg: "#000",
subMenuActiveBg: "#d84493",
navTextColor: "#7a80b4",
menuText: "#7a80b4",
sidebarLogo: "#3f0d29",
menuTitleHover: "#fff",
menuActiveBefore: "#d84493"
},
saucePurple: {
vxeColor: "#722ed1",
subMenuActiveText: "#fff",
menuBg: "#130824",
menuHover: "#693ac9",
subMenuBg: "#000",
subMenuActiveBg: "#693ac9",
navTextColor: "#7a80b4",
menuText: "#7a80b4",
sidebarLogo: "#1f0c38",
menuTitleHover: "#fff",
menuActiveBefore: "#693ac9"
}
};
/**
* @description 将预设主题色处理成主题插件所需格式
*/
export const genScssMultipleScopeVars = (): MultipleScopeVarsItem[] => {
const result = [] as MultipleScopeVarsItem[];
Object.keys(themeColors).forEach(key => {
result.push({
scopeName: `layout-theme-${key}`,
varsContent: `
$vxe-primary-color: ${themeColors[key].vxeColor} !default;
$subMenuActiveText: ${themeColors[key].subMenuActiveText} !default;
$menuBg: ${themeColors[key].menuBg} !default;
$menuHover: ${themeColors[key].menuHover} !default;
$subMenuBg: ${themeColors[key].subMenuBg} !default;
$subMenuActiveBg: ${themeColors[key].subMenuActiveBg} !default;
$navTextColor: ${themeColors[key].navTextColor} !default;
$menuText: ${themeColors[key].menuText} !default;
$sidebarLogo: ${themeColors[key].sidebarLogo} !default;
$menuTitleHover: ${themeColors[key].menuTitleHover} !default;
$menuActiveBefore: ${themeColors[key].menuActiveBefore} !default;
`
} as MultipleScopeVarsItem);
});
return result;
};

View File

@@ -1,11 +0,0 @@
/* 亮白色 */
$subMenuActiveText: #409eff;
$menuBg: #fff;
$menuHover: #e0ebf6;
$subMenuBg: #fff;
$subMenuActiveBg: #e0ebf6;
$navTextColor: #7a80b4;
$menuText: #7a80b4;
$sidebarLogo: #fff;
$menuTitleHover: #000;
$menuActiveBefore: #4091f7;

View File

@@ -1,11 +0,0 @@
/* 绿宝石 */
$subMenuActiveText: #fff;
$menuBg: #032121;
$menuHover: #59bfc1;
$subMenuBg: #000;
$subMenuActiveBg: #59bfc1;
$navTextColor: #7a80b4;
$menuText: #7a80b4;
$sidebarLogo: #053434;
$menuTitleHover: #fff;
$menuActiveBefore: #59bfc1;

View File

@@ -1,11 +0,0 @@
/* 深粉色 */
$subMenuActiveText: #fff;
$menuBg: #28081a;
$menuHover: #d84493;
$subMenuBg: #000;
$subMenuActiveBg: #d84493;
$navTextColor: #7a80b4;
$menuText: #7a80b4;
$sidebarLogo: #3f0d29;
$menuTitleHover: #fff;
$menuActiveBefore: #d84493;

View File

@@ -1,11 +0,0 @@
/* 深紫罗兰色 */
$subMenuActiveText: #fff;
$menuBg: #130824;
$menuHover: #693ac9;
$subMenuBg: #000;
$subMenuActiveBg: #693ac9;
$navTextColor: #7a80b4;
$menuText: #7a80b4;
$sidebarLogo: #1f0c38;
$menuTitleHover: #fff;
$menuActiveBefore: #693ac9;

View File

@@ -1,11 +0,0 @@
/* 橙红色 */
$subMenuActiveText: #fff;
$menuBg: #2b0e05;
$menuHover: #e85f33;
$subMenuBg: #0f0603;
$subMenuActiveBg: #e85f33;
$navTextColor: #fff;
$menuText: rgba(254, 254, 254, 0.65);
$sidebarLogo: #441708;
$menuTitleHover: #fff;
$menuActiveBefore: #e85f33;

View File

@@ -1,11 +0,0 @@
/* 金色 */
$subMenuActiveText: #d25f00;
$menuBg: #2b2503;
$menuHover: #f6da4d;
$subMenuBg: #0f0603;
$subMenuActiveBg: #f6da4d;
$navTextColor: #fff;
$menuText: rgba(254, 254, 254, 0.65);
$sidebarLogo: #443b05;
$menuTitleHover: #fff;
$menuActiveBefore: #f6da4d;

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