Compare commits

...

60 Commits

Author SHA1 Message Date
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
xiaoxian521
248fbf26ed release: update 2.9.0 2022-02-05 14:51:00 +08:00
xiaoxian521
d943550e10 chore: 重构图标 2022-02-05 14:45:20 +08:00
xiaoxian521
3a7832b7fe chore: update 2022-01-24 15:15:46 +08:00
xiaoxian521
ad3f964c32 fix: vite@2.7.0-beta.8 build incompatible template 2022-01-21 18:26:52 +08:00
xiaoxian521
5ee4add259 release: update 2.8.5 2022-01-21 16:48:20 +08:00
xiaoxian521
433ea6fa44 chore: update typescript 2022-01-21 16:29:23 +08:00
xiaoxian521
11514da24b chore: add @vueuse/shared 2022-01-21 14:50:23 +08:00
xiaoxian521
45025f668a perf: router 2022-01-21 14:03:00 +08:00
xiaoxian521
64eef1fc66 refactor: use @iconify-icons/ep 2022-01-21 11:32:58 +08:00
xiaoxian521
d7ea59194f chore: update 2022-01-20 00:00:25 +08:00
xiaoxian521
cb71468277 fix: update vscode extensions 2022-01-19 21:39:53 +08:00
xiaoxian521
a06bdad2b8 fix: update lintstagedrc 2022-01-18 17:09:07 +08:00
xiaoxian521
32af28fb9f fix: vscode settings.json 2022-01-18 17:00:30 +08:00
xiaoxian521
b8ada350a9 fix: vscode code-snippets 2022-01-18 16:59:35 +08:00
xiaoxian521
c472772c54 fix: add vite-plugin-live-reload 2022-01-09 12:14:17 +08:00
xiaoxian521
6a8c654936 chore: update vite-plugin-remove-console 2022-01-07 17:52:35 +08:00
xiaoxian521
1f07597320 feat: 添加WindiCSS支持 2022-01-07 17:26:05 +08:00
xiaoxian521
2d7debadff feat: 添加线上环境删console插件vite-plugin-remove-console 2022-01-07 15:29:14 +08:00
xiaoxian521
0bdaf55ff1 perf: 同步完整版分支代码 2022-01-07 15:08:21 +08:00
158 changed files with 7527 additions and 4545 deletions

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

@@ -37,7 +37,7 @@ module.exports = {
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/prettier/@typescript-eslint"
"@vue/eslint-config-typescript"
],
parser: "vue-eslint-parser",
parserOptions: {
@@ -50,6 +50,10 @@ module.exports = {
}
},
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
"no-debugger": "off",
"@typescript-eslint/explicit-module-boundary-types": "off", // setup()
@@ -57,6 +61,18 @@ module.exports = {
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-function": "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": [
"error",
{

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ dist
dist-ssr
*.local
.eslintcache
.stylelintcache
yarn.lock
npm-debug.log*

View File

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

View File

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

View File

@@ -1,11 +1,15 @@
{
"recommendations": [
"vscode-icons-team.vscode-icons",
"davidanson.vscode-markdownlint",
"stylelint.vscode-stylelint",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"johnsoncodehk.volar",
"lokalise.i18n-ally",
"mikestead.dotenv",
"antfu.iconify"
"eamodio.gitlens",
"antfu.iconify",
"antfu.unocss"
]
}

29
.vscode/settings.json vendored
View File

@@ -1,24 +1,9 @@
{
/** 便
* ESLint
* Prettier - Code formatter
* stylelint
* vscode-icons
* i18n Ally
* Iconify IntelliSense
* TypeScript Vue Plugin (Volar)
* Vue Language Features (Volar)
*/
"terminal.integrated.rendererType": "dom",
"editor.formatOnType": true,
"editor.formatOnSave": true,
"javascript.updateImportsOnFileMove.enabled": "always",
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"editor.tabSize": 2,
"editor.formatOnPaste": true,
"files.autoSave": "afterDelay",
@@ -27,34 +12,26 @@
"editor.suggestSelection": "first",
"editor.acceptSuggestionOnCommitCharacter": false,
"css.lint.propertyIgnoredDueToDisplay": "ignore",
// Prevent inline styles from being automatically formatted to all lowercase
"editor.quickSuggestions": {
"other": true,
"comments": true,
"strings": true
},
// Automatically fix some syntax errors of ts
"tslint.autoFixOnSave": true,
"files.associations": {
// Specifies the location of snippets in the suggestion widget
"editor.snippetSuggestions": "top"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"cSpell.userWords": ["sourcemap", "vite"],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"volar.tsPlugin": true,
"typescript.tsdk": "node_modules/typescript/lib",
"i18n-ally.localesPaths": ["src/plugins/i18n"],
"i18n-ally.localesPaths": "locales",
"i18n-ally.keystyle": "nested",
"i18n-ally.sortKeys": true,
"i18n-ally.namespace": true,
"i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
"i18n-ally.enabledParsers": ["ts"],
"i18n-ally.enabledParsers": ["yaml", "js"],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.enabledFrameworks": ["vue", "react"]
"i18n-ally.enabledFrameworks": ["vue"]
}

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 啝裳
Copyright (c) 2022 啝裳
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -6,11 +6,16 @@
## introduce
The lite version is based on the shelf extracted from https://github.com/xiaoxian521/vue-pure-admin, which contains the main functions and is more suitable for actual project development
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 `3MB`
## Supporting Video
- [Click Watch Tutorial](https://www.bilibili.com/video/BV1534y1S7HV)
- [Click Watch UI Design](https://www.bilibili.com/video/BV17g411T7rq)
## Docs
<https://pure-admin-doc.vercel.app/>
- [Click Watch Docs](https://pure-admin-doc.vercel.app)
## Usage
@@ -32,6 +37,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/
## ⚠️ 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,16 +6,16 @@
## 介绍
精简版是基于 https://github.com/xiaoxian521/vue-pure-admin 提炼出的架子,包含主体功能,更适合实际项目开发
精简版是基于[vue-pure-admin](https://github.com/xiaoxian521/vue-pure-admin)提炼出的架子,包含主体功能,更适合实际项目开发,打包后的大小仅 `3MB`
## 配套视频
教程:<https://www.bilibili.com/video/BV1534y1S7HV/>
UI 设计:<https://www.bilibili.com/video/BV17g411T7rq/>
- [点我查看教程](https://www.bilibili.com/video/BV1534y1S7HV)
- [点我查看 UI 设计](https://www.bilibili.com/video/BV17g411T7rq)
## 配套文档
<https://pure-admin-doc.vercel.app/>
- [点我查看文档](https://pure-admin-doc.vercel.app)
## 维护者
@@ -23,15 +23,15 @@ UI 设计:<https://www.bilibili.com/video/BV17g411T7rq/>
## 捐赠
如果你觉得这个项目对有帮助,可以帮作者买一杯咖啡表示支持
如果你觉得这个项目对有帮助,可以帮作者买一杯果汁 🍹 表示支持
<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="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f0697596aec84661b724f6eebdf8db17~tplv-k3u1fbpfcp-watermark.awebp?" width="150px" height="225px" />
## 用法
@@ -47,8 +47,15 @@ pnpm add 包名
pnpm remove 包名
我认为你应该先 fork 项目去开发,以便我更新时可以同步拉取更新!!!
我认为你应该先 fork 项目去开发,以便我更新时可以同步拉取更新!!!
## ⚠️ 注意
精简版不接受任何 issues 和 pr如果有问题请到完整版 https://github.com/xiaoxian521/vue-pure-admin/issues/new/choose 去提,谢谢!!!
- 精简版不接受任何 issues 和 pr如果有问题请到完整版 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 +0,0 @@
const productPlugins = [];
process.env.NODE_ENV === "production" &&
productPlugins.push("transform-remove-console");
module.exports = {
plugins: [...productPlugins]
};

85
build/info.ts Normal file
View File

@@ -0,0 +1,85 @@
import { readdir, stat } from "fs";
import type { Plugin } from "vite";
import dayjs, { Dayjs } from "dayjs";
import { sum } from "lodash-unified";
import duration from "dayjs/plugin/duration";
import { green, blue, bold } from "picocolors";
dayjs.extend(duration);
const staticPath = "dist";
const fileListTotal: number[] = [];
const recursiveDirectory = (folder: string, callback: Function): void => {
readdir(folder, (err, files: string[]) => {
if (err) throw err;
let count = 0;
const checkEnd = () => {
++count == files.length && callback();
};
files.forEach((item: string) => {
stat(folder + "/" + item, async (err, stats) => {
if (err) throw err;
if (stats.isFile()) {
fileListTotal.push(stats.size);
checkEnd();
} else if (stats.isDirectory()) {
recursiveDirectory(`${staticPath}/${item}/`, checkEnd);
}
});
});
files.length === 0 && callback();
});
};
const formatBytes = (a: number, b?: number): string => {
if (0 == a) return "0 Bytes";
const c = 1024,
d = b || 2,
e = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
f = Math.floor(Math.log(a) / Math.log(c));
return parseFloat((a / Math.pow(c, f)).toFixed(d)) + " " + e[f];
};
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() {
if (config.command === "build") {
startTime = dayjs(new Date());
}
},
closeBundle() {
if (config.command === "build") {
console.log(
bold(
green(
`👏欢迎使用${blue(
"[vue-pure-admin]"
)}如果您感觉不错记得点击后面链接给个star哦💖 https://github.com/xiaoxian521/vue-pure-admin`
)
)
);
endTime = dayjs(new Date());
recursiveDirectory(staticPath, () => {
console.log(
bold(
green(
`恭喜打包完成🎉(总用时${dayjs
.duration(endTime.diff(startTime))
.format("mm分ss秒")},打包后的大小为${formatBytes(
sum(fileListTotal)
)}`
)
)
);
});
}
}
};
}

73
build/plugins.ts Normal file
View File

@@ -0,0 +1,73 @@
import { resolve } from "path";
import Unocss from "unocss/vite";
import vue from "@vitejs/plugin-vue";
import { viteBuildInfo } from "./info";
import svgLoader from "vite-svg-loader";
import legacy from "@vitejs/plugin-legacy";
import vueJsx from "@vitejs/plugin-vue-jsx";
import { viteMockServe } from "vite-plugin-mock";
import VueI18n from "@intlify/vite-plugin-vue-i18n";
// import ElementPlus from "unplugin-element-plus/vite";
import { visualizer } from "rollup-plugin-visualizer";
import removeConsole from "vite-plugin-remove-console";
import themePreprocessorPlugin from "@pureadmin/theme";
import { genScssMultipleScopeVars } from "/@/layout/theme";
export function getPluginsList(command, VITE_LEGACY) {
const prodMock = true;
const lifecycle = process.env.npm_lifecycle_event;
return [
vue(),
// https://github.com/intlify/bundle-tools/tree/main/packages/vite-plugin-vue-i18n
VueI18n({
runtimeOnly: true,
compositionOnly: true,
include: [resolve("locales/**")]
}),
// jsx、tsx语法支持
vueJsx(),
Unocss(),
// 线上环境删除console
removeConsole(),
viteBuildInfo(),
// 自定义主题
themePreprocessorPlugin({
scss: {
multipleScopeVars: genScssMultipleScopeVars(),
// 在生产模式是否抽取独立的主题css文件extract为true以下属性有效
extract: true,
// 会选取defaultScopeName对应的主题css文件在html添加link
themeLinkTagId: "head",
// "head"||"head-prepend" || "body" ||"body-prepend"
themeLinkTagInjectTo: "head",
// 是否对抽取的css文件内对应scopeName的权重类名移除
removeCssScopeName: false
}
}),
// svg组件化支持
svgLoader(),
// ElementPlus({}),
// mock支持
viteMockServe({
mockPath: "mock",
localEnabled: command === "serve",
prodEnabled: command !== "serve" && prodMock,
injectCode: `
import { setupProdMockServer } from './mockProdServer';
setupProdMockServer();
`,
logger: false
}),
// 是否为打包后的文件提供传统浏览器兼容性支持
VITE_LEGACY
? legacy({
targets: ["ie >= 11"],
additionalLegacyPolyfills: ["regenerator-runtime/runtime"]
})
: null,
// 打包分析
lifecycle === "report"
? visualizer({ open: true, brotliSize: true, filename: "report.html" })
: null
];
}

View File

@@ -3,7 +3,6 @@
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/iconfont.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>pure-admin-thin</title>
<script>
@@ -14,35 +13,87 @@
<body>
<div id="app">
<style>
p {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 26px;
html,
body,
#app {
width: 100%;
height: 100%;
display: flex;
position: relative;
justify-content: center;
align-items: center;
overflow: hidden;
}
p::after {
.loader,
.loader:before,
.loader:after {
border-radius: 50%;
width: 2.5em;
height: 2.5em;
-webkit-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;
}
.loader {
color: #406eeb;
font-size: 10px;
margin: 80px auto;
position: relative;
text-indent: -9999em;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
top: 0;
transform: translate(-50%, 0);
}
.loader:before,
.loader:after {
content: "";
position: absolute;
top: 0%;
bottom: 0;
animation: dot 1s infinite steps(1, start);
top: 0;
}
@keyframes dot {
33.33% {
content: ".";
}
66.67% {
content: "..";
}
.loader:before {
left: -3.5em;
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.loader:after {
left: 3.5em;
}
@-webkit-keyframes loadAnimation {
0%,
80%,
100% {
content: "...";
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
@keyframes loadAnimation {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
</style>
<p>Loading</p>
<div class="loader"></div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>

32
locales/en.yaml Normal file
View File

@@ -0,0 +1,32 @@
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
menus:
hshome: Home
hslogin: Login
hserror: Error Page
hsfourZeroFour: "404"
hsfourZeroOne: "403"
hsFive: "500"
permission: Permission Manage
permissionPage: Page Permission
permissionButton: Button Permission
status:
hsLoad: Loading...

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

@@ -0,0 +1,32 @@
buttons:
hsLoginOut: 退出系统
hsfullscreen: 全屏
hsexitfullscreen: 退出全屏
hsrefreshRoute: 刷新路由
hslogin: 登陆
hsadd: 新增
hsmark: 标记/取消
hssave: 保存
hssearch: 搜索
hsexpendAll: 全部展开
hscollapseAll: 全部折叠
hssystemSet: 打开项目配置
hsdelete: 删除
hsreload: 重新加载
hscloseCurrentTab: 关闭当前标签页
hscloseLeftTabs: 关闭左侧标签页
hscloseRightTabs: 关闭右侧标签页
hscloseOtherTabs: 关闭其他标签页
hscloseAllTabs: 关闭全部标签页
menus:
hshome: 首页
hslogin: 登陆
hserror: 错误页面
hsfourZeroFour: "404"
hsfourZeroOne: "403"
hsFive: "500"
permission: 权限管理
permissionPage: 页面权限
permissionButton: 按钮权限
status:
hsLoad: 加载中...

View File

@@ -3,23 +3,18 @@ import { MockMethod } from "vite-plugin-mock";
const permissionRouter = {
path: "/permission",
name: "permission",
redirect: "/permission/page/index",
meta: {
title: "menus.permission",
icon: "Lollipop",
i18n: true,
showLink: true,
rank: 3
icon: "lollipop",
rank: 7
},
children: [
{
path: "/permission/page/index",
name: "permissionPage",
meta: {
title: "menus.permissionPage",
i18n: true,
showLink: true
title: "menus.permissionPage"
}
},
{
@@ -27,8 +22,6 @@ const permissionRouter = {
name: "permissionButton",
meta: {
title: "menus.permissionButton",
i18n: true,
showLink: true,
authority: []
}
}

View File

@@ -1,15 +1,13 @@
{
"name": "pure-admin-thin",
"version": "2.8.0",
"version": "3.3.0",
"private": true,
"engines": {
"node": ">= 16",
"pnpm": ">= 6"
},
"scripts": {
"dev": "cross-env --max_old_space_size=4096 vite",
"serve": "pnpm dev",
"build": "rimraf dist && cross-env vite build",
"build:staging": "rimraf dist && cross-env vite build --mode staging",
"report": "rimraf dist && cross-env vite build",
"preview": "vite preview",
"preview:build": "pnpm build && vite preview",
"clean:cache": "rm -rf node_modules && rm -rf .eslintcache && pnpm install",
@@ -18,7 +16,7 @@
"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:pretty": "pretty-quick --staged",
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint && pnpm lint:pretty",
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
"prepare": "husky install",
"preinstall": "npx only-allow pnpm"
},
@@ -28,81 +26,91 @@
"not op_mini all"
],
"dependencies": {
"@ctrl/tinycolor": "^3.4.0",
"@element-plus/icons-vue": "^0.2.4",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.0-5",
"@vue/compiler-sfc": "^3.2.24",
"@vueuse/core": "^6.7.1",
"@vueuse/motion": "^2.0.0-beta.4",
"@ctrl/tinycolor": "^3.4.1",
"@pureadmin/components": "^1.0.6",
"@vueuse/core": "^8.4.2",
"@vueuse/motion": "^2.0.0-beta.12",
"@vueuse/shared": "^8.4.2",
"animate.css": "^4.1.1",
"axios": "^0.21.1",
"axios": "^0.27.2",
"css-color-function": "^1.3.3",
"dayjs": "^1.10.7",
"element-plus": "1.3.0-beta.1",
"dayjs": "^1.11.2",
"element-plus": "2.1.11",
"element-resize-detector": "^1.2.3",
"font-awesome": "^4.7.0",
"js-cookie": "^3.0.1",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lodash-unified": "^1.0.2",
"mitt": "^3.0.0",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"path": "^0.12.7",
"pinia": "^2.0.0-rc.14",
"pinia": "^2.0.14",
"qrcode": "^1.5.0",
"qs": "^6.10.2",
"remixicon": "^2.5.0",
"resize-observer-polyfill": "^1.5.1",
"responsive-storage": "^1.0.11",
"rgb-hex": "^4.0.0",
"vue": "^3.2.24",
"vue-i18n": "^9.2.0-beta.3",
"vue-router": "^4.0.12",
"vue-types": "^4.1.0"
"vue": "^3.2.33",
"vue-i18n": "^9.2.0-beta.35",
"vue-router": "^4.0.15",
"vue-types": "^4.1.1"
},
"devDependencies": {
"@commitlint/cli": "13.1.0",
"@commitlint/config-conventional": "13.1.0",
"@iconify-icons/ep": "^1.2.4",
"@iconify-icons/ri": "^1.2.1",
"@iconify/vue": "^3.2.0",
"@intlify/vite-plugin-vue-i18n": "^3.4.0",
"@pureadmin/theme": "^1.1.0",
"@types/element-resize-detector": "1.1.3",
"@types/js-cookie": "^3.0.1",
"@types/lodash": "^4.14.180",
"@types/lodash-es": "^4.17.6",
"@types/mockjs": "1.0.3",
"@types/node": "14.14.14",
"@types/nprogress": "0.2.0",
"@types/qrcode": "^1.4.2",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "4.31.0",
"@typescript-eslint/parser": "4.31.0",
"@vitejs/plugin-legacy": "^1.6.4",
"@vitejs/plugin-vue": "^1.10.2",
"@vitejs/plugin-vue-jsx": "^1.3.1",
"@vue/eslint-config-prettier": "6.0.0",
"@vue/eslint-config-typescript": "7.0.0",
"@zougt/vite-plugin-theme-preprocessor": "^1.4.0",
"autoprefixer": "10.2.4",
"babel-plugin-transform-remove-console": "6.9.4",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@typescript-eslint/parser": "^5.10.2",
"@vitejs/plugin-legacy": "^1.8.2",
"@vitejs/plugin-vue": "^2.3.2",
"@vitejs/plugin-vue-jsx": "^1.3.10",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^10.0.0",
"autoprefixer": "^10.4.5",
"cross-env": "7.0.3",
"eslint": "7.30.0",
"eslint-plugin-prettier": "3.4.0",
"eslint-plugin-vue": "7.17.0",
"eslint": "^8.8.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.4.1",
"husky": "7.0.2",
"lint-staged": "11.1.2",
"postcss": "8.2.6",
"picocolors": "^1.0.0",
"postcss": "^8.4.6",
"postcss-html": "^1.3.0",
"postcss-import": "14.0.0",
"prettier": "2.3.2",
"postcss-scss": "^4.0.3",
"prettier": "^2.5.1",
"pretty-quick": "3.1.1",
"rimraf": "3.0.2",
"sass": "^1.45.0",
"sass-loader": "^12.3.0",
"stylelint": "13.13.1",
"stylelint-config-prettier": "8.0.2",
"stylelint-config-standard": "22.0.0",
"stylelint-order": "4.1.0",
"typescript": "4.4.2",
"unplugin-element-plus": "^0.1.3",
"vite": "2.6.14",
"rollup": "^2.70.1",
"rollup-plugin-visualizer": "^5.6.0",
"sass": "^1.51.0",
"stylelint": "^14.3.0",
"stylelint-config-html": "^1.0.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended": "^6.0.0",
"stylelint-config-standard": "^24.0.0",
"stylelint-order": "^5.0.0",
"typescript": "^4.6.3",
"unocss": "^0.33.2",
"vite": "^2.9.8",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-style-import": "^1.2.1",
"vite-svg-loader": "^2.2.0",
"vue-eslint-parser": "7.10.0"
"vite-plugin-remove-console": "^0.0.7",
"vite-svg-loader": "^3.3.0",
"vue-eslint-parser": "^8.2.0"
},
"repository": "git@github.com:xiaoxian521/vue-pure-admin.git",
"author": "xiaoxian521",

4443
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
@font-face {
font-family: "iconfont"; /* project id 1098500 */
src: url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.eot");
src: url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.eot?#iefix")
format("embedded-opentype"),
url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.woff2") format("woff2"),
url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.woff") format("woff"),
url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.ttf") format("truetype"),
url("//at.alicdn.com/t/font_1098500_3d6un9zwltz.svg#iconfont") format("svg");
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -1,5 +1,5 @@
{
"Version": "2.8.0",
"Version": "3.3.0",
"Title": "PureAdmin",
"FixedHeader": true,
"HiddenSideBar": false,
@@ -15,13 +15,5 @@
"SidebarStatus": true,
"EpThemeColor": "#409EFF",
"ShowLogo": true,
"ShowModel": "smart",
"MapConfigure": {
"amapKey": "97b3248d1553172e81f168cf94ea667e",
"options": {
"resizeEnable": true,
"center": [113.6401, 34.72468],
"zoom": 12
}
}
"ShowModel": "smart"
}

View File

@@ -5,10 +5,11 @@
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { ElConfigProvider } from "element-plus";
import zhCn from "element-plus/lib/locale/lang/zh-cn";
import en from "element-plus/lib/locale/lang/en";
export default {
export default defineComponent({
name: "app",
components: {
[ElConfigProvider.name]: ElConfigProvider
@@ -18,5 +19,5 @@ export default {
return this.$storage.locale?.locale === "zh" ? zhCn : en;
}
}
};
});
</script>

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 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

@@ -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,98 +1,13 @@
import { App, defineComponent } from "vue";
import icon from "./src/Icon.vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { iconComponents } from "/@/plugins/element-plus";
import iconifyIconOffline from "./src/iconifyIconOffline";
import iconifyIconOnline from "./src/iconifyIconOnline";
import fontIcon from "./src/iconfont";
/**
* find icon component
* @param icon icon图标
* @returns component
*/
export function findIconReg(icon: string) {
// fontawesome4
const fa4Reg = /^fa-/;
// fontawesome5+
const fa5Reg = /^FA-/;
// iconfont
const iFReg = /^IF-/;
// remixicon
const riReg = /^RI-/;
// typeof icon === "function" 属于SVG
if (fa5Reg.test(icon)) {
const text = icon.split(fa5Reg)[1];
return findIcon(
text.slice(0, text.indexOf(" ") == -1 ? text.length : text.indexOf(" ")),
"FA",
text.slice(text.indexOf(" ") + 1, text.length)
);
} else if (fa4Reg.test(icon)) {
return findIcon(icon.split(fa4Reg)[1], "fa");
} else if (iFReg.test(icon)) {
return findIcon(icon.split(iFReg)[1], "IF");
} else if (typeof icon === "function") {
return findIcon(icon, "SVG");
} else if (riReg.test(icon)) {
return findIcon(icon.split(riReg)[1], "RI");
} else {
return findIcon(icon, "EL");
}
}
// 支持fontawesome、iconfont、remixicon、element-plus/icons、自定义svg
export function findIcon(icon: String, type = "EL", property?: string) {
if (type === "FA") {
return defineComponent({
name: "FaIcon",
setup() {
return { icon, property };
},
components: { FontAwesomeIcon },
template: `<font-awesome-icon :icon="icon" v-bind:[property]="true" />`
});
} else if (type === "fa") {
return defineComponent({
name: "faIcon",
data() {
return { icon: `fa ${icon}` };
},
template: `<i :class="icon" />`
});
} else if (type === "IF") {
return defineComponent({
name: "IfIcon",
data() {
return { icon: `iconfont ${icon}` };
},
template: `<i :class="icon" />`
});
} else if (type === "RI") {
return defineComponent({
name: "RIIcon",
data() {
return { icon: `ri-${icon}` };
},
template: `<i :class="icon" />`
});
} else if (type === "EL") {
const components = iconComponents.filter(
component => component.name === icon
);
if (components.length > 0) {
return components[0];
} else {
return null;
}
} else if (type === "SVG") {
return icon;
}
}
export const Icon = Object.assign(icon, {
install(app: App) {
app.component(icon.name, icon);
}
});
export const IconifyIconOffline = iconifyIconOffline;
export const IconifyIconOnline = iconifyIconOnline;
export const FontIcon = fontIcon;
export default {
Icon
IconifyIconOffline,
IconifyIconOnline,
FontIcon
};

View File

@@ -1,97 +0,0 @@
<script lang="ts">
export default {
name: "Icon"
};
</script>
<script setup lang="ts">
import { ref, computed } from "vue";
const props = defineProps({
content: {
type: String,
default: ""
},
size: {
type: Number,
default: 18
},
width: {
type: Number,
default: 20
},
height: {
type: Number,
default: 20
},
color: {
type: String,
default: ""
},
svg: {
type: Boolean,
default: false
}
});
const emit = defineEmits<{
(e: "click"): void;
}>();
let text = ref("");
let className = computed(() => {
if (props.content.indexOf("fa-") > -1) {
return props.content.indexOf("fa ") === 0
? props.content
: ["fa", props.content];
} else if (props.content.indexOf("el-icon-") > -1) {
return props.content;
} else if (props.content.indexOf("#") > -1) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
text.value = props.content;
return "iconfont";
} else {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
text.value = props.content;
return "";
}
});
let iconStyle = computed(() => {
return (
"font-size: " +
props.size +
"px; color: " +
props.color +
"; width: " +
props.width +
"px; height: " +
props.height +
"px; font-style: normal;"
);
});
const clickHandle = () => {
emit("click");
};
</script>
<template>
<i
v-if="!props.svg"
:class="className"
:style="iconStyle"
v-html="text"
@click="clickHandle"
></i>
<svg
class="icon-svg"
v-if="props.svg"
aria-hidden="true"
:style="iconStyle"
@click="clickHandle"
>
<use :xlink:href="`#${props.content}`" />
</svg>
</template>

View File

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

View File

@@ -0,0 +1,48 @@
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
export default defineComponent({
name: "fontIcon",
props: {
icon: {
type: String,
default: ""
}
},
render() {
const attrs = this.$attrs;
if (Object.keys(attrs).includes("uni") || attrs?.iconType === "uni") {
return h(
"i",
{
class: "iconfont",
...attrs
},
this.icon
);
} else if (
Object.keys(attrs).includes("svg") ||
attrs?.iconType === "svg"
) {
return h(
"svg",
{
class: "icon-svg",
"aria-hidden": true
},
{
default: () => [
h("use", {
"xlink:href": `#${this.icon}`
})
]
}
);
} else {
return h("i", {
class: `iconfont ${this.icon}`,
...attrs
});
}
}
});

View File

@@ -0,0 +1,67 @@
import { h, defineComponent } from "vue";
import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline";
// element-plus icon
import Check from "@iconify-icons/ep/check";
import HomeFilled from "@iconify-icons/ep/home-filled";
import Setting from "@iconify-icons/ep/setting";
import Lollipop from "@iconify-icons/ep/lollipop";
import RefreshRight from "@iconify-icons/ep/refresh-right";
import ArrowDown from "@iconify-icons/ep/arrow-down";
import CloseBold from "@iconify-icons/ep/close-bold";
import Bell from "@iconify-icons/ep/bell";
import Search from "@iconify-icons/ep/search";
addIcon("check", Check);
addIcon("home-filled", HomeFilled);
addIcon("setting", Setting);
addIcon("lollipop", Lollipop);
addIcon("refresh-right", RefreshRight);
addIcon("arrow-down", ArrowDown);
addIcon("close-bold", CloseBold);
addIcon("bell", Bell);
addIcon("search", Search);
// remixicon
import ArrowRightSLine from "@iconify-icons/ri/arrow-right-s-line";
import ArrowLeftSLine from "@iconify-icons/ri/arrow-left-s-line";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import InformationLine from "@iconify-icons/ri/information-line";
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
import Bookmark from "@iconify-icons/ri/bookmark-2-line";
import User from "@iconify-icons/ri/user-3-fill";
import Lock from "@iconify-icons/ri/lock-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", Bookmark);
addIcon("user", User);
addIcon("lock", Lock);
// Iconify Icon在Vue里离线使用用于内网环境https://docs.iconify.design/icon-components/vue/offline.html
export default defineComponent({
name: "IconifyIcon",
components: { IconifyIcon },
props: {
icon: {
type: String,
default: ""
}
},
render() {
const attrs = this.$attrs;
return h(
IconifyIcon,
{
icon: `${this.icon}`,
...attrs
},
{
default: () => []
}
);
}
});

View File

@@ -0,0 +1,27 @@
import { h, defineComponent } from "vue";
import { Icon as IconifyIcon } from "@iconify/vue";
// Iconify Icon在Vue里在线使用用于外网环境 https://docs.iconify.design/icon-components/vue/offline.html
export default defineComponent({
name: "IconifyIcon",
components: { IconifyIcon },
props: {
icon: {
type: String,
default: ""
}
},
render() {
const attrs = this.$attrs;
return h(
IconifyIcon,
{
icon: `${this.icon}`,
...attrs
},
{
default: () => []
}
);
}
});

View File

@@ -0,0 +1,18 @@
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;
onLoad?: Function;
// all icon
style?: object;
}

View File

@@ -0,0 +1,12 @@
import { App } from "vue";
import reImageVerify from "./src/index.vue";
export const ReImageVerify = Object.assign(reImageVerify, {
install(app: App) {
app.component(reImageVerify.name, reImageVerify);
}
});
export default {
ReImageVerify
};

View File

@@ -0,0 +1,85 @@
import { ref, onMounted } from "vue";
/**
* 绘制图形验证码
* @param width - 图形宽度
* @param height - 图形高度
*/
export const useImageVerify = (width = 120, height = 40) => {
const domRef = ref<HTMLCanvasElement>();
const imgCode = ref("");
function setImgCode(code: string) {
imgCode.value = code;
}
function getImgCode() {
if (!domRef.value) return;
imgCode.value = draw(domRef.value, width, height);
}
onMounted(() => {
getImgCode();
});
return {
domRef,
imgCode,
setImgCode,
getImgCode
};
};
function randomNum(min: number, max: number) {
const num = Math.floor(Math.random() * (max - min) + min);
return num;
}
function randomColor(min: number, max: number) {
const r = randomNum(min, max);
const g = randomNum(min, max);
const b = randomNum(min, max);
return `rgb(${r},${g},${b})`;
}
function draw(dom: HTMLCanvasElement, width: number, height: number) {
let imgCode = "";
const NUMBER_STRING = "0123456789";
const ctx = dom.getContext("2d");
if (!ctx) return imgCode;
ctx.fillStyle = randomColor(180, 230);
ctx.fillRect(0, 0, width, height);
for (let i = 0; i < 4; i += 1) {
const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)];
imgCode += text;
const fontSize = randomNum(18, 41);
const deg = randomNum(-30, 30);
ctx.font = `${fontSize}px Simhei`;
ctx.textBaseline = "top";
ctx.fillStyle = randomColor(80, 150);
ctx.save();
ctx.translate(30 * i + 15, 15);
ctx.rotate((deg * Math.PI) / 180);
ctx.fillText(text, -15 + 5, -15);
ctx.restore();
}
for (let i = 0; i < 5; i += 1) {
ctx.beginPath();
ctx.moveTo(randomNum(0, width), randomNum(0, height));
ctx.lineTo(randomNum(0, width), randomNum(0, height));
ctx.strokeStyle = randomColor(180, 230);
ctx.closePath();
ctx.stroke();
}
for (let i = 0; i < 41; i += 1) {
ctx.beginPath();
ctx.arc(randomNum(0, width), randomNum(0, height), 1, 0, 2 * Math.PI);
ctx.closePath();
ctx.fillStyle = randomColor(150, 200);
ctx.fill();
}
return imgCode;
}

View File

@@ -0,0 +1,48 @@
<script lang="ts">
export default {
name: "ReImageVerify"
};
</script>
<script setup lang="ts">
import { watch } from "vue";
import { useImageVerify } from "./hooks";
interface Props {
code?: string;
}
interface Emits {
(e: "update:code", code: string): void;
}
const props = withDefaults(defineProps<Props>(), {
code: ""
});
const emit = defineEmits<Emits>();
const { domRef, imgCode, setImgCode, getImgCode } = useImageVerify();
watch(
() => props.code,
newValue => {
setImgCode(newValue);
}
);
watch(imgCode, newValue => {
emit("update:code", newValue);
});
defineExpose({ getImgCode });
</script>
<template>
<canvas
ref="domRef"
width="120"
height="40"
class="cursor-pointer"
@click="getImgCode"
/>
</template>

View File

@@ -0,0 +1,10 @@
import { App } from "vue";
import reQrcode from "./src/index";
export const ReQrcode = Object.assign(reQrcode, {
install(app: App) {
app.component(reQrcode.name, reQrcode);
}
});
export default ReQrcode;

View File

@@ -0,0 +1,8 @@
.qrcode {
&--disabled {
background: rgba(255, 255, 255, 0.95);
& > div {
transform: translate(-50%, -50%);
}
}
}

View File

@@ -0,0 +1,262 @@
import {
ref,
unref,
watch,
nextTick,
computed,
PropType,
defineComponent
} from "vue";
import "./index.scss";
import { isString } from "/@/utils/is";
import { cloneDeep } from "lodash-unified";
import { propTypes } from "/@/utils/propTypes";
import { IconifyIconOffline } from "../../ReIcon";
import QRCode, { QRCodeRenderersOptions } from "qrcode";
interface QrcodeLogo {
src?: string;
logoSize?: number;
bgColor?: string;
borderSize?: number;
crossOrigin?: string;
borderRadius?: number;
logoRadius?: number;
}
const props = {
// img 或者 canvas,img不支持logo嵌套
tag: propTypes.string
.validate((v: string) => ["canvas", "img"].includes(v))
.def("canvas"),
// 二维码内容
text: {
type: [String, Array] as PropType<string | Recordable[]>,
default: null
},
// qrcode.js配置项
options: {
type: Object as PropType<QRCodeRenderersOptions>,
default: (): QRCodeRenderersOptions => ({})
},
// 宽度
width: propTypes.number.def(200),
// logo
logo: {
type: [String, Object] as PropType<Partial<QrcodeLogo> | string>,
default: (): QrcodeLogo | string => ""
},
// 是否过期
disabled: propTypes.bool.def(false),
// 过期提示内容
disabledText: propTypes.string.def("")
};
export default defineComponent({
name: "ReQrcode",
props,
emits: ["done", "click", "disabled-click"],
setup(props, { emit }) {
const { toCanvas, toDataURL } = QRCode;
const loading = ref(true);
const wrapRef = ref<Nullable<HTMLCanvasElement | HTMLImageElement>>(null);
const renderText = computed(() => String(props.text));
const wrapStyle = computed(() => {
return {
width: props.width + "px",
height: props.width + "px"
};
});
const initQrcode = async () => {
await nextTick();
const options = cloneDeep(props.options || {});
if (props.tag === "canvas") {
// 容错率,默认对内容少的二维码采用高容错率,内容多的二维码采用低容错率
options.errorCorrectionLevel =
options.errorCorrectionLevel ||
getErrorCorrectionLevel(unref(renderText));
const _width: number = await getOriginWidth(unref(renderText), options);
options.scale =
props.width === 0 ? undefined : (props.width / _width) * 4;
const canvasRef: HTMLCanvasElement = await toCanvas(
unref(wrapRef) as HTMLCanvasElement,
unref(renderText),
options
);
if (props.logo) {
const url = await createLogoCode(canvasRef);
emit("done", url);
loading.value = false;
} else {
emit("done", canvasRef.toDataURL());
loading.value = false;
}
} else {
const url = await toDataURL(renderText.value, {
errorCorrectionLevel: "H",
width: props.width,
...options
});
(unref(wrapRef) as HTMLImageElement).src = url;
emit("done", url);
loading.value = false;
}
};
watch(
() => renderText.value,
val => {
if (!val) return;
initQrcode();
},
{
deep: true,
immediate: true
}
);
const createLogoCode = (canvasRef: HTMLCanvasElement) => {
const canvasWidth = canvasRef.width;
const logoOptions: QrcodeLogo = Object.assign(
{
logoSize: 0.15,
bgColor: "#ffffff",
borderSize: 0.05,
crossOrigin: "anonymous",
borderRadius: 8,
logoRadius: 0
},
isString(props.logo) ? {} : props.logo
);
const {
logoSize = 0.15,
bgColor = "#ffffff",
borderSize = 0.05,
crossOrigin = "anonymous",
borderRadius = 8,
logoRadius = 0
} = logoOptions;
const logoSrc = isString(props.logo) ? props.logo : props.logo.src;
const logoWidth = canvasWidth * logoSize;
const logoXY = (canvasWidth * (1 - logoSize)) / 2;
const logoBgWidth = canvasWidth * (logoSize + borderSize);
const logoBgXY = (canvasWidth * (1 - logoSize - borderSize)) / 2;
const ctx = canvasRef.getContext("2d");
if (!ctx) return;
// logo 底色
canvasRoundRect(ctx)(
logoBgXY,
logoBgXY,
logoBgWidth,
logoBgWidth,
borderRadius
);
ctx.fillStyle = bgColor;
ctx.fill();
// logo
const image = new Image();
if (crossOrigin || logoRadius) {
image.setAttribute("crossOrigin", crossOrigin);
}
(image as any).src = logoSrc;
// 使用image绘制可以避免某些跨域情况
const drawLogoWithImage = (image: HTMLImageElement) => {
ctx.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
};
// 使用canvas绘制以获得更多的功能
const drawLogoWithCanvas = (image: HTMLImageElement) => {
const canvasImage = document.createElement("canvas");
canvasImage.width = logoXY + logoWidth;
canvasImage.height = logoXY + logoWidth;
const imageCanvas = canvasImage.getContext("2d");
if (!imageCanvas || !ctx) return;
imageCanvas.drawImage(image, logoXY, logoXY, logoWidth, logoWidth);
canvasRoundRect(ctx)(logoXY, logoXY, logoWidth, logoWidth, logoRadius);
if (!ctx) return;
const fillStyle = ctx.createPattern(canvasImage, "no-repeat");
if (fillStyle) {
ctx.fillStyle = fillStyle;
ctx.fill();
}
};
// 将 logo绘制到 canvas上
return new Promise((resolve: any) => {
image.onload = () => {
logoRadius ? drawLogoWithCanvas(image) : drawLogoWithImage(image);
resolve(canvasRef.toDataURL());
};
});
};
// 得到原QrCode的大小以便缩放得到正确的QrCode大小
const getOriginWidth = async (
content: string,
options: QRCodeRenderersOptions
) => {
const _canvas = document.createElement("canvas");
await toCanvas(_canvas, content, options);
return _canvas.width;
};
// 对于内容少的QrCode增大容错率
const getErrorCorrectionLevel = (content: string) => {
if (content.length > 36) {
return "M";
} else if (content.length > 16) {
return "Q";
} else {
return "H";
}
};
// 用于绘制圆角
const canvasRoundRect = (ctx: CanvasRenderingContext2D) => {
return (x: number, y: number, w: number, h: number, r: number) => {
const minSize = Math.min(w, h);
if (r > minSize / 2) {
r = minSize / 2;
}
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
return ctx;
};
};
const clickCode = () => {
emit("click");
};
const disabledClick = () => {
emit("disabled-click");
};
return () => (
<>
<div
v-loading={unref(loading)}
class="qrcode relative inline-block"
style={unref(wrapStyle)}
>
{props.tag === "canvas" ? (
<canvas ref={wrapRef} onClick={clickCode}></canvas>
) : (
<img ref={wrapRef} onClick={clickCode}></img>
)}
{props.disabled && (
<div
class="qrcode--disabled absolute top-0 left-0 flex w-full h-full items-center justify-center"
onClick={disabledClick}
>
<div class="absolute top-[50%] left-[50%] font-bold">
<IconifyIconOffline
class="cursor-pointer outline-none"
icon="refresh-right"
width="30"
color="var(--el-color-primary)"
/>
<div>{props.disabledText}</div>
</div>
</div>
)}
</div>
</>
);
}
});

View File

@@ -7,8 +7,7 @@ import {
defineComponent,
getCurrentInstance
} from "vue";
import { RouterView } from "vue-router";
import backTop from "/@/assets/svg/back_top.svg";
import backTop from "/@/assets/svg/back_top.svg?component";
import { usePermissionStoreHook } from "/@/store/modules/permission";
const props = defineProps({

View File

@@ -1,70 +1,45 @@
<script setup lang="ts">
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import { emitter } from "/@/utils/mitt";
import { useNav } from "../hooks/nav";
import { useRoute } from "vue-router";
import Search from "./search/index.vue";
import Notice from "./notice/index.vue";
import mixNav from "./sidebar/mixNav.vue";
import avatars from "/@/assets/avatars.jpg";
import { transformI18n } from "/@/plugins/i18n";
import Hamburger from "./sidebar/hamBurger.vue";
import { useRouter, useRoute } from "vue-router";
import { storageSession } from "/@/utils/storage";
import { watch, getCurrentInstance } from "vue";
import Breadcrumb from "./sidebar/breadCrumb.vue";
import { useAppStoreHook } from "/@/store/modules/app";
import { unref, watch, getCurrentInstance } from "vue";
import { deviceDetection } from "/@/utils/deviceDetection";
import screenfull from "../components/screenfull/index.vue";
import globalization from "/@/assets/svg/globalization.svg";
import globalization from "/@/assets/svg/globalization.svg?component";
const route = useRoute();
const { locale, t } = useI18n();
const instance =
getCurrentInstance().appContext.config.globalProperties.$storage;
const pureApp = useAppStoreHook();
const router = useRouter();
const route = useRoute();
let usename = storageSession.getItem("info")?.username;
const { locale } = useI18n();
const getDropdownItemStyle = computed(() => {
return t => {
return {
background: locale.value === t ? "#1b2a47" : "",
color: locale.value === t ? "#f4f4f5" : "#000"
};
};
});
const {
logout,
onPanel,
changeTitle,
toggleSideBar,
pureApp,
username,
avatarsStyle,
getDropdownItemStyle
} = useNav();
watch(
() => locale.value,
() => {
//@ts-ignore
document.title = transformI18n(
//@ts-ignore
unref(route.meta.title),
unref(route.meta.i18n)
); // 动态title
changeTitle(route.meta);
}
);
// 退出登录
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";
@@ -74,14 +49,19 @@ function translationEn() {
<template>
<div class="navbar">
<Hamburger
v-if="pureApp.layout !== 'mix'"
:is-active="pureApp.sidebar.opened"
class="hamburger-container"
@toggleClick="toggleSideBar"
/>
<Breadcrumb class="breadcrumb-container" />
<Breadcrumb v-if="pureApp.layout !== 'mix'" class="breadcrumb-container" />
<div class="vertical-header-right">
<mixNav v-if="pureApp.layout === 'mix'" />
<div v-if="pureApp.layout === 'vertical'" class="vertical-header-right">
<!-- 菜单搜索 -->
<Search />
<!-- 通知 -->
<Notice id="header-notice" />
<!-- 全屏 -->
@@ -92,44 +72,49 @@ function translationEn() {
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle('zh')"
:style="getDropdownItemStyle(locale, 'zh')"
@click="translationCh"
><el-icon class="check-zh" v-show="locale === 'zh'"
><check /></el-icon
>简体中文</el-dropdown-item
><IconifyIconOffline
class="check-zh"
v-show="locale === 'zh'"
icon="check"
/>简体中文</el-dropdown-item
>
<el-dropdown-item
:style="getDropdownItemStyle('en')"
:style="getDropdownItemStyle(locale, 'en')"
@click="translationEn"
><el-icon class="check-en" v-show="locale === 'en'"
><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>
</template>
</el-dropdown>
<!-- 退出登陆 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<img :src="avatars" />
<p>{{ usename }}</p>
<img v-if="avatars" :src="avatars" :style="avatarsStyle" />
<p v-if="username">{{ username }}</p>
</span>
<template #dropdown>
<el-dropdown-menu class="logout">
<el-dropdown-item @click="logout">
<i class="ri-logout-circle-r-line"></i
>{{ $t("buttons.hsLoginOut") }}</el-dropdown-item
<IconifyIconOffline
icon="logout-circle-r-line"
style="margin: 5px"
/>{{ t("buttons.hsLoginOut") }}</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-icon
<span
class="el-icon-setting"
:title="$t('buttons.hssystemSet')"
:title="t('buttons.hssystemSet')"
@click="onPanel"
>
<Setting />
</el-icon>
<IconifyIconOffline icon="setting" />
</span>
</div>
</div>
</template>
@@ -149,10 +134,6 @@ function translationEn() {
cursor: pointer;
transition: background 0.3s;
-webkit-tap-highlight-color: transparent;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
.vertical-header-right {
@@ -189,7 +170,6 @@ function translationEn() {
}
.el-dropdown-link {
width: 100px;
height: 48px;
padding: 10px;
display: flex;
@@ -233,14 +213,8 @@ function translationEn() {
}
.translation {
.el-dropdown-menu__item {
padding: 5px 40px !important;
}
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
::v-deep(.el-dropdown-menu__item) {
padding: 5px 40px;
}
.check-zh {
@@ -257,16 +231,10 @@ function translationEn() {
.logout {
max-width: 120px;
.el-dropdown-menu__item {
::v-deep(.el-dropdown-menu__item) {
min-width: 100%;
display: inline-flex;
flex-wrap: wrap;
}
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
}
}
</style>

View File

@@ -108,7 +108,7 @@ export const noticesData: TabItem[] = [
{
avatar: "",
title: "任务名称",
description: "任务需要在 2021-11-16 20:00 前启动",
description: "任务需要在 2022-11-16 20:00 前启动",
datetime: "",
extra: "未开始",
status: "info",
@@ -118,7 +118,7 @@ export const noticesData: TabItem[] = [
avatar: "",
title: "第三方紧急代码变更",
description:
"一拳提交于 2021-11-16需在 2021-11-18 前完成代码变更任务",
"一拳提交于 2022-11-16需在 2022-11-18 前完成代码变更任务",
datetime: "",
extra: "马上到期",
status: "danger",
@@ -127,7 +127,7 @@ export const noticesData: TabItem[] = [
{
avatar: "",
title: "信息安全考试",
description: "指派小仙于 2021-12-12 前完成更新并发布",
description: "指派小仙于 2022-12-12 前完成更新并发布",
datetime: "",
extra: "已耗时 8 天",
status: "warning",

View File

@@ -1,8 +1,11 @@
<script setup lang="ts">
import { ref } from "vue";
import NoticeList from "./noticeList.vue";
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 notices = ref(noticesData);
@@ -10,36 +13,51 @@ let noticesNum = ref(0);
notices.value.forEach(notice => {
noticesNum.value += notice.list.length;
});
function tabClick() {
// @ts-expect-error
dropdownDom.value.handleOpen();
}
</script>
<template>
<el-dropdown trigger="click" placement="bottom-end">
<el-dropdown ref="dropdownDom" trigger="click" placement="bottom-end">
<span class="dropdown-badge">
<el-badge :value="noticesNum" :max="99">
<el-icon class="header-notice-icon"><bell /></el-icon>
<span class="header-notice-icon">
<IconifyIconOffline icon="bell" />
</span>
</el-badge>
</span>
<template #dropdown>
<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">
<el-tab-pane
:label="`${item.name}(${item.list.length})`"
:name="item.name"
>
<TabPane :tab="`${item.name}(${item.list.length})`">
<el-scrollbar max-height="330px">
<div class="noticeList-container">
<NoticeList :list="item.list" />
</div>
</el-scrollbar>
</el-tab-pane>
</TabPane>
</template>
</el-tabs>
</Tabs>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<style>
.ant-tabs-dropdown {
z-index: 2900 !important;
}
</style>
<style lang="scss" scoped>
.dropdown-badge {
display: flex;
@@ -77,4 +95,8 @@ notices.value.forEach(notice => {
padding: 15px 24px 0 24px;
}
}
:deep(.ant-tabs-nav) {
margin-bottom: 0;
}
</style>

View File

@@ -10,8 +10,8 @@ const props = defineProps({
});
const titleRef = ref(null);
const descriptionRef = ref(null);
const titleTooltip = ref(false);
const descriptionRef = ref(null);
const descriptionTooltip = ref(false);
function hoverTitle() {
@@ -50,7 +50,7 @@ function hoverDescription(event, description) {
:size="30"
:src="props.noticeItem.avatar"
class="notice-container-avatar"
></el-avatar>
/>
<div class="notice-container-text">
<div class="notice-text-title">
<el-tooltip

View File

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

View File

@@ -23,10 +23,10 @@ emitter.on("openPanel", () => {
<div class="project-configuration">
<h3>项目配置</h3>
<el-icon title="关闭配置" class="el-icon-close" @click="show = !show">
<Close />
<IconifyIconOffline icon="close-bold" />
</el-icon>
</div>
<div style="border-bottom: 1px solid #dcdfe6"></div>
<div style="border-bottom: 1px solid #dcdfe6" />
<slot />
</div>
</div>
@@ -54,7 +54,7 @@ emitter.on("openPanel", () => {
.right-panel {
width: 100%;
max-width: 300px;
max-width: 315px;
height: 100vh;
position: fixed;
top: 0;
@@ -105,8 +105,8 @@ emitter.on("openPanel", () => {
.right-panel-items {
margin-top: 60px;
height: 100vh;
overflow: auto;
height: calc(100vh - 60px);
overflow-y: auto;
}
.project-configuration {
@@ -120,12 +120,12 @@ emitter.on("openPanel", () => {
margin-left: 10px;
i {
font-size: 20px;
font-size: 16px;
margin-right: 20px;
&:hover {
cursor: pointer;
color: red;
color: var(--el-color-primary);
}
}
}

View File

@@ -1,22 +1,19 @@
<script setup lang="ts">
import { useFullscreen } from "@vueuse/core";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { isFullscreen, toggle } = useFullscreen();
</script>
<template>
<div class="screen-full" @click="toggle">
<i
<FontIcon
:title="
isFullscreen
? $t('buttons.hsexitfullscreen')
: $t('buttons.hsfullscreen')
isFullscreen ? t('buttons.hsexitfullscreen') : t('buttons.hsfullscreen')
"
:class="
isFullscreen
? 'iconfont team-iconexit-fullscreen'
: 'iconfont team-iconfullscreen'
"
></i>
:icon="isFullscreen ? 'team-iconexit-fullscreen' : 'team-iconfullscreen'"
/>
</div>
</template>

View File

@@ -0,0 +1,42 @@
<template>
<div class="search-footer">
<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;
color: #333;
.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,165 @@
<script lang="ts" setup>
import { useRouter } from "vue-router";
import SearchResult from "./SearchResult.vue";
import SearchFooter from "./SearchFooter.vue";
import { deleteChildren } from "/@/utils/tree";
import { transformI18n } from "/@/plugins/i18n";
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 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(usePermissionStoreHook().menusTree);
});
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" 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,91 @@
<template>
<div class="result">
<template v-for="item in options" :key="item.path">
<div
class="result-item"
:style="{
background:
item?.path === active ? useEpThemeStoreHook().epThemeColor : '',
color: item.path === active ? '#fff' : ''
}"
@click="handleTo"
@mouseenter="handleMouse(item)"
>
<component :is="useRenderIcon(item.meta?.icon ?? 'bookmark')" />
<span class="result-item-title">{{ t(item.meta?.title) }}</span>
<enterOutlined />
</div>
</template>
</div>
</template>
<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 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>
<style lang="scss" scoped>
.result {
padding-bottom: 12px;
&-item {
display: flex;
align-items: center;
height: 56px;
margin-top: 8px;
padding: 14px;
border-radius: 4px;
background: #e5e7eb;
cursor: pointer;
&-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,30 @@
<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" @click="handleSearch">
<IconifyIconOffline icon="search" />
</div>
<SearchModal v-model:value="show" />
</template>
<style lang="scss" scoped>
.search-container {
display: flex;
align-items: center;
justify-content: center;
height: 48px;
width: 40px;
cursor: pointer;
&:hover {
background: #f6f6f6;
}
}
</style>

View File

@@ -9,16 +9,13 @@ import {
useCssModule,
getCurrentInstance
} from "vue";
import rgbHex from "rgb-hex";
import { find } from "lodash-es";
import { find } from "lodash-unified";
import { getConfig } from "/@/config";
import { useRouter } from "vue-router";
import panel from "../panel/index.vue";
import { emitter } from "/@/utils/mitt";
import { templateRef } from "@vueuse/core";
import dayIcon from "/@/assets/svg/day.svg";
import { debounce } from "/@/utils/debounce";
import darkIcon from "/@/assets/svg/dark.svg";
import { themeColorsType } from "../../types";
import { useAppStoreHook } from "/@/store/modules/app";
import { shadeBgColor } from "../../theme/element-plus";
@@ -26,7 +23,10 @@ import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import { storageLocal, storageSession } from "/@/utils/storage";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { createNewStyle, writeNewStyle } from "../../theme/element-plus";
import { toggleTheme } from "@zougt/vite-plugin-theme-preprocessor/dist/browser-utils";
import { toggleTheme } from "@pureadmin/theme/dist/browser-utils";
import dayIcon from "/@/assets/svg/day.svg?component";
import darkIcon from "/@/assets/svg/dark.svg?component";
const router = useRouter();
const { isSelect } = useCssModule();
@@ -39,27 +39,28 @@ const instanceConfig =
let themeColors = ref<Array<themeColorsType>>([
// 道奇蓝(默认)
{ rgb: "27, 42, 71", themeColor: "default" },
{ color: "#1b2a47", themeColor: "default" },
// 亮白色
{ rgb: "255, 255, 255", themeColor: "light" },
{ color: "#ffffff", themeColor: "light" },
// 猩红色
{ rgb: "245, 34, 45", themeColor: "dusk" },
{ color: "#f5222d", themeColor: "dusk" },
// 橙红色
{ rgb: "250, 84, 28", themeColor: "volcano" },
{ color: "#fa541c", themeColor: "volcano" },
// 金色
{ rgb: "250, 219, 20", themeColor: "yellow" },
{ color: "#fadb14", themeColor: "yellow" },
// 绿宝石
{ rgb: "19, 194, 194", themeColor: "mingQing" },
{ color: "#13c2c2", themeColor: "mingQing" },
// 酸橙绿
{ rgb: "82, 196, 26", themeColor: "auroraGreen" },
{ color: "#52c41a", themeColor: "auroraGreen" },
// 深粉色
{ rgb: "235, 47, 150", themeColor: "pink" },
{ color: "#eb2f96", themeColor: "pink" },
// 深紫罗兰色
{ rgb: "114, 46, 209", themeColor: "saucePurple" }
{ color: "#722ed1", themeColor: "saucePurple" }
]);
const verticalRef = templateRef<HTMLElement | null>("verticalRef", null);
const horizontalRef = templateRef<HTMLElement | null>("horizontalRef", null);
const mixRef = templateRef<HTMLElement | null>("mixRef", null);
let layoutTheme =
ref(storageLocal.getItem("responsive-layout")) ||
@@ -95,12 +96,12 @@ const settings = reactive({
});
const getThemeColorStyle = computed(() => {
return rgb => {
return { background: `rgb(${rgb})` };
return color => {
return { background: color };
};
});
function changeStorageConfigure(key, val) {
function storageConfigureChange<T>(key: string, val: T): void {
const storageConfigure = instance.configure;
storageConfigure[key] = val;
instance.configure = storageConfigure;
@@ -109,14 +110,14 @@ function changeStorageConfigure(key, val) {
function toggleClass(flag: boolean, clsName: string, target?: HTMLElement) {
const targetEl = target || document.body;
let { className } = targetEl;
className = className.replace(clsName, "");
className = className.replace(clsName, "").trim();
targetEl.className = flag ? `${className} ${clsName} ` : className;
}
// 灰色模式设置
const greyChange = (value): void => {
toggleClass(settings.greyVal, "html-grey", document.querySelector("html"));
changeStorageConfigure("grey", value);
storageConfigureChange("grey", value);
};
// 色弱模式设置
@@ -126,58 +127,54 @@ const weekChange = (value): void => {
"html-weakness",
document.querySelector("html")
);
changeStorageConfigure("weak", value);
storageConfigureChange("weak", value);
};
const tagsChange = () => {
let showVal = settings.tabsVal;
changeStorageConfigure("hideTabs", showVal);
storageConfigureChange("hideTabs", showVal);
emitter.emit("tagViewsChange", showVal);
};
const multiTagsCacheChange = () => {
let multiTagsCache = settings.multiTagsCache;
changeStorageConfigure("multiTagsCache", multiTagsCache);
storageConfigureChange("multiTagsCache", multiTagsCache);
useMultiTagsStoreHook().multiTagsCacheChange(multiTagsCache);
};
// 清空缓存并返回登录页
function onReset() {
toggleClass(getConfig().Grey, "html-grey", document.querySelector("html"));
toggleClass(
getConfig().Weak,
"html-weakness",
document.querySelector("html")
);
router.push("/login");
const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } = getConfig();
useAppStoreHook().setLayout(Layout);
useEpThemeStoreHook().setEpThemeColor(EpThemeColor);
useMultiTagsStoreHook().multiTagsCacheChange(MultiTagsCache);
toggleClass(Grey, "html-grey", document.querySelector("html"));
toggleClass(Weak, "html-weakness", document.querySelector("html"));
useMultiTagsStoreHook().handleTags("equal", [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "menus.hshome",
icon: "HomeFilled",
i18n: true,
showLink: true
icon: "home-filled"
}
}
]);
useMultiTagsStoreHook().multiTagsCacheChange(getConfig().MultiTagsCache);
useEpThemeStoreHook().setEpThemeColor(getConfig().EpThemeColor);
storageLocal.clear();
storageSession.clear();
router.push("/login");
}
function onChange(label) {
changeStorageConfigure("showModel", label);
storageConfigureChange("showModel", label);
emitter.emit("tagViewsShowModel", label);
}
// 侧边栏Logo
function logoChange() {
unref(logoVal)
? changeStorageConfigure("showLogo", true)
: changeStorageConfigure("showLogo", false);
? storageConfigureChange("showLogo", true)
: storageConfigureChange("showLogo", false);
emitter.emit("logoChange", unref(logoVal));
}
@@ -192,10 +189,17 @@ watch(instance, ({ layout }) => {
case "vertical":
toggleClass(true, isSelect, unref(verticalRef));
debounce(setFalse([horizontalRef]), 50);
debounce(setFalse([mixRef]), 50);
break;
case "horizontal":
toggleClass(true, isSelect, unref(horizontalRef));
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;
}
});
@@ -255,13 +259,13 @@ function setLayoutThemeColor(theme: string) {
setEpThemeColor(getConfig().EpThemeColor);
} else {
const colors = find(themeColors.value, { themeColor: theme });
const color = "#" + rgbHex(colors.rgb);
setEpThemeColor(color);
setEpThemeColor(colors.color);
}
}
// 设置ep主题色
const setEpThemeColor = (color: string) => {
// @ts-expect-error
writeNewStyle(createNewStyle(color));
useEpThemeStoreHook().setEpThemeColor(color);
body.style.setProperty("--el-color-primary-active", shadeBgColor(color));
@@ -294,7 +298,7 @@ nextTick(() => {
settings.weakVal &&
document.querySelector("html")?.setAttribute("class", "html-weakness");
settings.tabsVal && tagsChange();
// @ts-expect-error
writeNewStyle(createNewStyle(epThemeColor.value));
dataThemeChange();
});
@@ -310,30 +314,40 @@ nextTick(() => {
:active-icon="dayIcon"
:inactive-icon="darkIcon"
@change="dataThemeChange"
>
</el-switch>
/>
<el-divider>导航栏模式</el-divider>
<ul class="pure-theme">
<el-tooltip class="item" content="左侧菜单模式" placement="bottom">
<el-tooltip class="item" content="左侧模式" placement="bottom">
<li
:class="layoutTheme.layout === 'vertical' ? $style.isSelect : ''"
ref="verticalRef"
@click="setLayoutModel('vertical')"
>
<div></div>
<div></div>
<div />
<div />
</li>
</el-tooltip>
<el-tooltip class="item" content="顶部菜单模式" placement="bottom">
<el-tooltip class="item" content="顶部模式" placement="bottom">
<li
:class="layoutTheme.layout === 'horizontal' ? $style.isSelect : ''"
ref="horizontalRef"
@click="setLayoutModel('horizontal')"
>
<div></div>
<div></div>
<div />
<div />
</li>
</el-tooltip>
<el-tooltip class="item" content="混合模式" placement="bottom">
<li
:class="layoutTheme.layout === 'mix' ? $style.isSelect : ''"
ref="mixRef"
@click="setLayoutModel('mix')"
>
<div />
<div />
</li>
</el-tooltip>
</ul>
@@ -343,7 +357,7 @@ nextTick(() => {
<li
v-for="(item, index) in themeColors"
:key="index"
:style="getThemeColorStyle(item.rgb)"
:style="getThemeColorStyle(item.color)"
@click="setLayoutThemeColor(item.themeColor)"
>
<el-icon
@@ -351,7 +365,7 @@ nextTick(() => {
:size="17"
:color="getThemeColor(item.themeColor)"
>
<Check />
<IconifyIconOffline icon="check" />
</el-icon>
</li>
</ul>
@@ -367,8 +381,7 @@ nextTick(() => {
active-text=""
inactive-text=""
@change="greyChange"
>
</el-switch>
/>
</li>
<li v-show="!dataTheme">
<span>色弱模式</span>
@@ -379,8 +392,7 @@ nextTick(() => {
active-text=""
inactive-text=""
@change="weekChange"
>
</el-switch>
/>
</li>
<li>
<span>隐藏标签页</span>
@@ -391,8 +403,7 @@ nextTick(() => {
active-text=""
inactive-text=""
@change="tagsChange"
>
</el-switch>
/>
</li>
<li>
<span>侧边栏Logo</span>
@@ -405,8 +416,7 @@ nextTick(() => {
active-text=""
inactive-text=""
@change="logoChange"
>
</el-switch>
/>
</li>
<li>
<span>标签页持久化</span>
@@ -417,8 +427,7 @@ nextTick(() => {
active-text=""
inactive-text=""
@change="multiTagsCacheChange"
>
</el-switch>
/>
</li>
<li>
@@ -436,7 +445,12 @@ nextTick(() => {
style="width: 90%; margin: 24px 15px"
@click="onReset"
>
<i class="fa fa-sign-out"></i>
<IconifyIconOffline
icon="logout-circle-r-line"
width="15"
height="15"
style="margin-right: 4px"
/>
清空缓存并返回登录页</el-button
>
</panel>
@@ -444,7 +458,7 @@ nextTick(() => {
<style scoped module>
.isSelect {
border: 2px solid #0960bd;
border: 2px solid var(--el-color-primary);
}
</style>
@@ -476,15 +490,14 @@ nextTick(() => {
.pure-theme {
margin-top: 25px;
width: 100%;
height: 100px;
height: 50px;
display: flex;
flex-wrap: wrap;
justify-content: space-around;
li {
margin: 10px;
width: 36%;
height: 70px;
width: 18%;
height: 45px;
background: #f0f2f5;
position: relative;
overflow: hidden;
@@ -522,6 +535,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,6 +1,6 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { isEqual } from "lodash-es";
import { isEqual } from "lodash-unified";
import { transformI18n } from "/@/plugins/i18n";
import { getParentPaths, findRouteByPath } from "/@/router/utils";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
@@ -65,7 +65,7 @@ const getBreadcrumb = (): void => {
{
path: "/welcome",
parentPath: "/",
meta: { title: "menus.hshome", i18n: true }
meta: { title: "menus.hshome" }
} as unknown as RouteLocationMatched
].concat(matched);
}
@@ -104,10 +104,10 @@ const handleLink = (item: RouteLocationMatched): any => {
<span
v-if="item.redirect === 'noRedirect' || index == levelList.length - 1"
class="no-redirect"
>{{ transformI18n(item.meta.title, item.meta.i18n) }}</span
>{{ transformI18n(item.meta.title) }}</span
>
<a v-else @click.prevent="handleLink(item)">
{{ transformI18n(item.meta.title, item.meta.i18n) }}
{{ transformI18n(item.meta.title) }}
</a>
</el-breadcrumb-item>
</transition-group>

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import { ref } from "vue";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
export interface Props {
isActive: boolean;
}
@@ -7,6 +9,8 @@ const props = withDefaults(defineProps<Props>(), {
isActive: false
});
const fillColor = ref<string>("");
const emit = defineEmits<{
(e: "toggleClick"): void;
}>();
@@ -21,8 +25,11 @@ const toggleClick = () => {
: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"

View File

@@ -1,144 +1,85 @@
<script setup lang="ts">
import {
computed,
unref,
watch,
nextTick,
onMounted,
getCurrentInstance
} from "vue";
import { useI18n } from "vue-i18n";
import { emitter } from "/@/utils/mitt";
import { useNav } from "../../hooks/nav";
import Search from "../search/index.vue";
import Notice from "../notice/index.vue";
import { templateRef } from "@vueuse/core";
import SidebarItem from "./sidebarItem.vue";
import avatars from "/@/assets/avatars.jpg";
import { algorithm } from "/@/utils/algorithm";
import screenfull from "../screenfull/index.vue";
import { useRoute, useRouter } from "vue-router";
import { storageSession } from "/@/utils/storage";
import Icon from "/@/components/ReIcon/src/Icon.vue";
import { deviceDetection } from "/@/utils/deviceDetection";
import globalization from "/@/assets/svg/globalization.svg";
import { watch, nextTick, onMounted, getCurrentInstance } from "vue";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import globalization from "/@/assets/svg/globalization.svg?component";
const route = useRoute();
const { locale, t } = useI18n();
const routers = useRouter().options.routes;
const menuRef = templateRef<ElRef | null>("menu", null);
const instance =
getCurrentInstance().appContext.config.globalProperties.$storage;
const title =
getCurrentInstance().appContext.config.globalProperties.$config?.Title;
const menuRef = templateRef<ElRef | null>("menu", null);
const route = useRoute();
const router = useRouter();
const routers = useRouter().options.routes;
let usename = storageSession.getItem("info")?.username;
const { locale, t } = useI18n();
const {
logout,
backHome,
onPanel,
changeTitle,
handleResize,
menuSelect,
username,
avatarsStyle,
getDropdownItemStyle
} = useNav();
const getDropdownItemStyle = computed(() => {
return t => {
return {
background: locale.value === t ? "#1b2a47" : "",
color: locale.value === t ? "#f4f4f5" : "#000"
};
};
onMounted(() => {
nextTick(() => {
handleResize(menuRef.value);
});
});
watch(
() => locale.value,
() => {
//@ts-ignore
// 动态title
document.title = t(unref(route.meta.title));
changeTitle(route.meta);
}
);
// 退出登录
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;
watch(
() => route.path,
() => {
menuSelect(route.path, routers);
}
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(algorithm.increaseIndexes(routers));
};
function backHome() {
router.push("/welcome");
}
function handleResize() {
// @ts-ignore
menuRef.value.handleResize();
}
// 简体中文
function translationCh() {
instance.locale = { locale: "zh" };
locale.value = "zh";
handleResize();
handleResize(menuRef.value);
}
// English
function translationEn() {
instance.locale = { locale: "en" };
locale.value = "en";
handleResize();
handleResize(menuRef.value);
}
onMounted(() => {
nextTick(() => {
handleResize();
});
});
</script>
<template>
<div class="horizontal-header">
<div class="horizontal-header-left" @click="backHome">
<Icon svg :width="35" :height="35" content="team-iconlogo" />
<FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" />
<h4>{{ title }}</h4>
</div>
<el-menu
ref="menu"
:default-active="activeMenu"
unique-opened
router
class="horizontal-header-menu"
mode="horizontal"
@select="menuSelect"
:default-active="route.path"
router
@select="indexPath => menuSelect(indexPath, routers)"
>
<sidebar-item
v-for="route in usePermissionStoreHook().wholeMenus"
@@ -148,6 +89,8 @@ onMounted(() => {
/>
</el-menu>
<div class="horizontal-header-right">
<!-- 菜单搜索 -->
<Search />
<!-- 通知 -->
<Notice id="header-notice" />
<!-- 全屏 -->
@@ -158,17 +101,19 @@ onMounted(() => {
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle('zh')"
:style="getDropdownItemStyle(locale, 'zh')"
@click="translationCh"
><el-icon class="check-zh" v-show="locale === 'zh'"
><check /></el-icon
>简体中文</el-dropdown-item
>
<span class="check-zh" v-show="locale === 'zh'">
<IconifyIconOffline icon="check" /> </span
>简体中文
</el-dropdown-item>
<el-dropdown-item
:style="getDropdownItemStyle('en')"
:style="getDropdownItemStyle(locale, 'en')"
@click="translationEn"
><el-icon class="check-en" v-show="locale === 'en'"
><check /></el-icon
>
<span class="check-en" v-show="locale === 'en'">
<IconifyIconOffline icon="check" /> </span
>English</el-dropdown-item
>
</el-dropdown-menu>
@@ -177,39 +122,36 @@ onMounted(() => {
<!-- 退出登陆 -->
<el-dropdown trigger="click">
<span class="el-dropdown-link">
<img :src="avatars" />
<p>{{ usename }}</p>
<img v-if="avatars" :src="avatars" :style="avatarsStyle" />
<p v-if="username">{{ username }}</p>
</span>
<template #dropdown>
<el-dropdown-menu class="logout">
<el-dropdown-item @click="logout">
<i class="ri-logout-circle-r-line"></i
>{{ $t("buttons.hsLoginOut") }}</el-dropdown-item
<IconifyIconOffline
icon="logout-circle-r-line"
style="margin: 5px"
/>
{{ t("buttons.hsLoginOut") }}</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-icon
<span
class="el-icon-setting"
:title="$t('buttons.hssystemSet')"
:title="t('buttons.hssystemSet')"
@click="onPanel"
>
<Setting />
</el-icon>
<IconifyIconOffline icon="setting" />
</span>
</div>
</div>
</template>
<style lang="scss" scoped>
.translation {
.el-dropdown-menu__item {
padding: 5px 40px !important;
}
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
::v-deep(.el-dropdown-menu__item) {
padding: 5px 40px;
}
.check-zh {
@@ -226,16 +168,10 @@ onMounted(() => {
.logout {
max-width: 120px;
.el-dropdown-menu__item {
::v-deep(.el-dropdown-menu__item) {
min-width: 100%;
display: inline-flex;
flex-wrap: wrap;
}
.el-dropdown-menu__item:focus,
.el-dropdown-menu__item:not(.is-disabled):hover {
color: #606266;
background: #f0f0f0;
}
}
</style>

View File

@@ -1,6 +1,5 @@
<script setup lang="ts">
import { getCurrentInstance } from "vue";
import Icon from "/@/components/ReIcon/src/Icon.vue";
const props = defineProps({
collapse: Boolean
});
@@ -19,7 +18,7 @@ const title =
class="sidebar-logo-link"
to="/"
>
<Icon svg :width="35" :height="35" content="team-iconlogo" />
<FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" />
<span class="sidebar-title">{{ title }}</span>
</router-link>
<router-link
@@ -29,7 +28,7 @@ const title =
class="sidebar-logo-link"
to="/"
>
<Icon svg :width="35" :height="35" content="team-iconlogo" />
<FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" />
<span class="sidebar-title">{{ title }}</span>
</router-link>
</transition>

View File

@@ -0,0 +1,241 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import Search from "../search/index.vue";
import Notice from "../notice/index.vue";
import { useNav } from "../../hooks/nav";
import { templateRef } from "@vueuse/core";
import avatars from "/@/assets/avatars.jpg";
import { transformI18n } from "/@/plugins/i18n";
import screenfull from "../screenfull/index.vue";
import { useRoute, useRouter } from "vue-router";
import { deviceDetection } from "/@/utils/deviceDetection";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
import { getParentPaths, findRouteByPath } from "/@/router/utils";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import globalization from "/@/assets/svg/globalization.svg?component";
import { ref, watch, nextTick, onMounted, getCurrentInstance } from "vue";
const route = useRoute();
const { locale, t } = useI18n();
const routers = useRouter().options.routes;
const menuRef = templateRef<ElRef | null>("menu", null);
const instance =
getCurrentInstance().appContext.config.globalProperties.$storage;
const {
logout,
onPanel,
changeTitle,
toggleSideBar,
handleResize,
menuSelect,
resolvePath,
pureApp,
username,
avatarsStyle,
getDropdownItemStyle
} = useNav();
let defaultActive = ref(null);
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(() => {
handleResize(menuRef.value);
});
});
watch(
() => locale.value,
() => {
changeTitle(route.meta);
}
);
watch(
() => route.path,
() => {
getDefaultActive(route.path);
}
);
function translationCh() {
instance.locale = { locale: "zh" };
locale.value = "zh";
handleResize(menuRef.value);
}
function translationEn() {
instance.locale = { locale: "en" };
locale.value = "en";
handleResize(menuRef.value);
}
</script>
<template>
<div class="horizontal-header">
<div
:class="classes.container"
:title="pureApp.sidebar.opened ? '点击折叠' : '点击展开'"
@click="toggleSideBar"
>
<svg
:fill="useEpThemeStoreHook().fill"
:class="[
'hamburger',
pureApp.sidebar.opened ? 'is-active-hamburger' : ''
]"
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>
<el-menu
ref="menu"
class="horizontal-header-menu"
mode="horizontal"
:default-active="defaultActive"
router
@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-show="route.meta.icon" :class="['el-icon', route.meta.icon]">
<component :is="useRenderIcon(route.meta && route.meta.icon)" />
</div>
<span>{{ 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" />
<!-- 全屏 -->
<screenfull id="header-screenfull" v-show="!deviceDetection()" />
<!-- 国际化 -->
<el-dropdown id="header-translation" trigger="click">
<globalization />
<template #dropdown>
<el-dropdown-menu class="translation">
<el-dropdown-item
:style="getDropdownItemStyle(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')"
@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">
<img v-if="avatars" :src="avatars" :style="avatarsStyle" />
<p v-if="username">{{ 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="el-icon-setting"
:title="t('buttons.hssystemSet')"
@click="onPanel"
>
<IconifyIconOffline icon="setting" />
</span>
</div>
</div>
</template>
<style module="classes" scoped>
.container {
padding: 0 15px;
}
</style>
<style lang="scss" scoped>
.hamburger {
width: 20px;
height: 20px;
&:hover {
cursor: pointer;
}
}
.is-active-hamburger {
transform: rotate(180deg);
}
.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,22 +1,14 @@
<script setup lang="ts">
import {
ref,
PropType,
nextTick,
computed,
CSSProperties,
getCurrentInstance
} from "vue";
import path from "path";
import { useNav } from "../../hooks/nav";
import { childrenType } from "../../types";
import { transformI18n } from "/@/plugins/i18n";
import { findIconReg } from "/@/components/ReIcon";
import Icon from "/@/components/ReIcon/src/Icon.vue";
import { useAppStoreHook } from "/@/store/modules/app";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
import { ref, PropType, nextTick, computed, CSSProperties } from "vue";
const instance = getCurrentInstance().appContext.app.config.globalProperties;
const menuMode = instance.$storage.layout?.layout === "vertical";
const pureApp = useAppStoreHook();
const { pureApp } = useNav();
const menuMode = ["vertical", "mix"].includes(pureApp.layout);
const props = defineProps({
item: {
@@ -32,6 +24,19 @@ const props = defineProps({
}
});
const getExtraIconStyle = computed((): CSSProperties => {
if (useAppStoreHook().getSidebarStatus) {
return {
position: "absolute",
right: "10px"
};
} else {
return {
position: "static"
};
}
});
const getNoDropdownStyle = computed((): CSSProperties => {
return {
display: "flex",
@@ -93,7 +98,6 @@ function hoverMenu(key) {
: Object.assign(key, {
showTooltip: false
});
hoverMenuMap.set(key, true);
});
}
@@ -107,6 +111,10 @@ function hasOneShowingChild(
return true;
});
if (showingChildren[0]?.meta?.showParent) {
return false;
}
if (showingChildren.length === 1) {
return true;
}
@@ -120,8 +128,8 @@ function hasOneShowingChild(
function resolvePath(routePath) {
const httpReg = /^http(s?):\/\//;
if (httpReg.test(routePath)) {
return props.basePath + "/" + routePath;
if (httpReg.test(routePath) || httpReg.test(props.basePath)) {
return routePath || props.basePath;
} else {
return path.resolve(props.basePath, routePath);
}
@@ -140,20 +148,32 @@ function resolvePath(routePath) {
:class="{ 'submenu-title-noDropdown': !isNest }"
:style="getNoDropdownStyle"
>
<el-icon v-show="props.item.meta.icon">
<div class="sub-menu-icon" v-show="props.item.meta.icon">
<component
:is="
findIconReg(
useRenderIcon(
onlyOneChild.meta.icon ||
(props.item.meta && props.item.meta.icon)
)
"
></component>
</el-icon>
/>
</div>
<div
v-if="
!pureApp.sidebar.opened &&
pureApp.layout === 'mix' &&
props.item?.pathList?.length === 2
"
:style="getDivStyle"
>
<span :style="getMenuTextStyle">
{{ transformI18n(onlyOneChild.meta.title) }}
</span>
</div>
<template #title>
<div :style="getDivStyle">
<span v-if="!menuMode">{{
transformI18n(onlyOneChild.meta.title, onlyOneChild.meta.i18n)
transformI18n(onlyOneChild.meta.title)
}}</span>
<el-tooltip
v-else
@@ -162,24 +182,23 @@ function resolvePath(routePath) {
:disabled="!onlyOneChild.showTooltip"
>
<template #content>
{{
transformI18n(onlyOneChild.meta.title, onlyOneChild.meta.i18n)
}}
{{ transformI18n(onlyOneChild.meta.title) }}
</template>
<span
ref="menuTextRef"
:style="getMenuTextStyle"
@mouseover="hoverMenu(onlyOneChild)"
>
{{
transformI18n(onlyOneChild.meta.title, onlyOneChild.meta.i18n)
}}
{{ transformI18n(onlyOneChild.meta.title) }}
</span>
</el-tooltip>
<Icon
<FontIcon
v-if="onlyOneChild.meta.extraIcon"
width="30px"
height="30px"
:style="getExtraIconStyle"
:icon="onlyOneChild.meta.extraIcon.name"
:svg="onlyOneChild.meta.extraIcon.svg ? true : false"
:content="`${onlyOneChild.meta.extraIcon.name}`"
/>
</div>
</template>
@@ -193,14 +212,12 @@ function resolvePath(routePath) {
popper-append-to-body
>
<template #title>
<el-icon v-show="props.item.meta.icon" :class="props.item.meta.icon">
<div v-show="props.item.meta.icon" class="sub-menu-icon">
<component
:is="findIconReg(props.item.meta && props.item.meta.icon)"
></component>
</el-icon>
<span v-if="!menuMode">{{
transformI18n(props.item.meta.title, props.item.meta.i18n)
}}</span>
:is="useRenderIcon(props.item.meta && props.item.meta.icon)"
/>
</div>
<span v-if="!menuMode">{{ transformI18n(props.item.meta.title) }}</span>
<el-tooltip
v-else
placement="top"
@@ -208,7 +225,7 @@ function resolvePath(routePath) {
:disabled="!pureApp.sidebar.opened || !props.item.showTooltip"
>
<template #content>
{{ transformI18n(props.item.meta.title, props.item.meta.i18n) }}
{{ transformI18n(props.item.meta.title) }}
</template>
<div
ref="menuTextRef"
@@ -216,14 +233,17 @@ function resolvePath(routePath) {
@mouseover="hoverMenu(props.item)"
>
<span :style="getSpanStyle">
{{ transformI18n(props.item.meta.title, props.item.meta.i18n) }}
{{ transformI18n(props.item.meta.title) }}
</span>
</div>
</el-tooltip>
<Icon
<FontIcon
v-if="props.item.meta.extraIcon"
width="30px"
height="30px"
style="position: absolute; right: 10px"
:icon="props.item.meta.extraIcon.name"
:svg="props.item.meta.extraIcon.svg ? true : false"
:content="`${props.item.meta.extraIcon.name}`"
/>
</template>
<sidebar-item

View File

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

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 {
from {
transform: rotate(0deg);
@@ -80,7 +50,7 @@
.scroll-item {
border-radius: 3px 3px 0 0;
padding: 0 6px 0 6px;
padding: 0 6px;
box-shadow: 0 0 1px #888;
position: relative;
margin-right: 4px;
@@ -92,7 +62,7 @@
.el-icon-close {
font-size: 10px;
color: #1890ff;
color: var(--el-color-primary);
cursor: pointer;
position: absolute;
top: 50%;
@@ -123,7 +93,7 @@
a {
text-decoration: none;
color: #666;
padding: 0 4px 0 4px;
padding: 0 4px;
}
.scroll-container {
@@ -190,7 +160,8 @@
align-items: center;
&:hover {
background: #eee;
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
svg {
@@ -236,7 +207,7 @@
}
.scroll-item.is-active {
background-color: #eaf4fe;
background-color: var(--el-color-primary-light-9);
position: relative;
color: #fff;
@@ -249,17 +220,27 @@
}
a {
color: #1890ff;
color: var(--el-color-primary);
}
}
.ri-arrow-left-s-line {
.arrow-left,
.arrow-right {
width: 40px;
height: 38px;
line-height: 38px;
text-align: center;
font-size: 20px;
color: #00000073;
position: relative;
svg {
width: 20px;
height: 20px;
position: absolute;
left: 50%;
transform: translate(-50%, 50%);
}
}
.arrow-left {
box-shadow: 5px 0 5px -6px #ccc;
&:hover {
@@ -267,15 +248,9 @@
}
}
.ri-arrow-right-s-line {
width: 40px;
height: 38px;
line-height: 38px;
text-align: center;
font-size: 20px;
border-right: 1px solid #ccc;
color: #00000073;
.arrow-right {
box-shadow: -5px 0 5px -6px #ccc;
border-right: 1px solid #ccc;
&:hover {
cursor: e-resize;
@@ -284,10 +259,10 @@
/* 卡片模式下鼠标移入显示蓝色边框 */
.card-in {
color: #1890ff;
color: var(--el-color-primary);
a {
color: #1890ff;
color: var(--el-color-primary);
}
}
@@ -308,7 +283,7 @@
position: absolute;
left: 0;
bottom: 0;
background: #1890ff;
background: var(--el-color-primary);
}
/* 灵动模式下鼠标移入显示蓝色进度条 */
@@ -318,7 +293,7 @@
position: absolute;
left: 0;
bottom: 0;
background: #1890ff;
background: var(--el-color-primary);
animation: scheduleInWidth 400ms ease-in;
}
@@ -329,14 +304,11 @@
position: absolute;
left: 0;
bottom: 0;
background: #1890ff;
background: var(--el-color-primary);
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

@@ -3,6 +3,8 @@ import {
ref,
watch,
unref,
toRaw,
reactive,
nextTick,
computed,
ComputedRef,
@@ -11,28 +13,28 @@ import {
getCurrentInstance
} from "vue";
import close from "/@/assets/svg/close.svg";
import refresh from "/@/assets/svg/refresh.svg";
import closeAll from "/@/assets/svg/close_all.svg";
import closeLeft from "/@/assets/svg/close_left.svg";
import closeOther from "/@/assets/svg/close_other.svg";
import closeRight from "/@/assets/svg/close_right.svg";
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 { $t as t } from "/@/plugins/i18n";
import { useI18n } from "vue-i18n";
import { emitter } from "/@/utils/mitt";
import { isEqual, isEmpty } from "lodash-es";
import { transformI18n } from "/@/plugins/i18n";
import { storageLocal } from "/@/utils/storage";
import { useRoute, useRouter } from "vue-router";
import { isEqual, isEmpty } from "lodash-unified";
import { transformI18n, $t } from "/@/plugins/i18n";
import { RouteConfigs, tagsViewsType } from "../../types";
import { useSettingStoreHook } from "/@/store/modules/settings";
import { handleAliveRoute, delAliveRoutes } from "/@/router/utils";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import { toggleClass, removeClass, hasClass } from "/@/utils/operate";
import { templateRef, useResizeObserver, useDebounceFn } from "@vueuse/core";
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const translateX = ref<number>(0);
@@ -40,11 +42,11 @@ const activeIndex = ref<number>(-1);
let refreshButton = "refresh-button";
const instance = getCurrentInstance();
const pureSetting = useSettingStoreHook();
const showTags = ref(storageLocal.getItem("tagsVal") || false);
const tabDom = templateRef<HTMLElement | null>("tabDom", null);
const containerDom = templateRef<HTMLElement | null>("containerDom", null);
const scrollbarDom = templateRef<HTMLElement | null>("scrollbarDom", null);
const showTags =
ref(storageLocal.getItem("responsive-configure").hideTabs) ?? "false";
let multiTags: ComputedRef<Array<RouteConfigs>> = computed(() => {
return useMultiTagsStoreHook()?.multiTags;
});
@@ -106,7 +108,11 @@ const iconIsActive = computed(() => {
const dynamicTagView = () => {
const index = multiTags.value.findIndex(item => {
return item.path === route.path;
if (item?.query) {
return isEqual(route?.query, item?.query);
} else {
return item.path === route.path;
}
});
moveToView(index);
};
@@ -128,15 +134,15 @@ const moveToView = (index: number): void => {
if (!instance.refs["dynamic" + index]) {
return;
}
const tabItemEl = instance.refs["dynamic" + index];
const tabItemElOffsetLeft = (tabItemEl as HTMLElement).offsetLeft;
const tabItemOffsetWidth = (tabItemEl as HTMLElement).offsetWidth;
const tabItemEl = instance.refs["dynamic" + index][0];
const tabItemElOffsetLeft = (tabItemEl as HTMLElement)?.offsetLeft;
const tabItemOffsetWidth = (tabItemEl as HTMLElement)?.offsetWidth;
// 标签页导航栏可视长度(不包含溢出部分)
const scrollbarDomWidth = scrollbarDom.value
? scrollbarDom.value.offsetWidth
? scrollbarDom.value?.offsetWidth
: 0;
// 已有标签页总长度(包含溢出部分)
const tabDomWidth = tabDom.value ? tabDom.value.offsetWidth : 0;
const tabDomWidth = tabDom.value ? tabDom.value?.offsetWidth : 0;
if (tabDomWidth < scrollbarDomWidth || tabItemElOffsetLeft === 0) {
translateX.value = 0;
@@ -186,45 +192,45 @@ const handleScroll = (offset: number): void => {
}
};
const tagsViews = ref<Array<tagsViewsType>>([
const tagsViews = reactive<Array<tagsViewsType>>([
{
icon: refresh,
text: t("buttons.hsreload"),
text: $t("buttons.hsreload"),
divided: false,
disabled: false,
show: true
},
{
icon: close,
text: t("buttons.hscloseCurrentTab"),
text: $t("buttons.hscloseCurrentTab"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeLeft,
text: t("buttons.hscloseLeftTabs"),
text: $t("buttons.hscloseLeftTabs"),
divided: true,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeRight,
text: t("buttons.hscloseRightTabs"),
text: $t("buttons.hscloseRightTabs"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
},
{
icon: closeOther,
text: t("buttons.hscloseOtherTabs"),
text: $t("buttons.hscloseOtherTabs"),
divided: true,
disabled: multiTags.value.length > 2 ? false : true,
show: true
},
{
icon: closeAll,
text: t("buttons.hscloseAllTabs"),
text: $t("buttons.hscloseAllTabs"),
divided: false,
disabled: multiTags.value.length > 1 ? false : true,
show: true
@@ -313,9 +319,7 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
parentPath: "/",
meta: {
title: "menus.hshome",
i18n: true,
icon: "el-icon-s-home",
showLink: true
icon: "home-filled"
}
},
obj
@@ -424,6 +428,12 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
});
}
function handleCommand(command: object) {
// @ts-expect-error
const { key, item } = command;
onClickDrop(key, item);
}
// 触发右键中菜单的点击事件
function selectTag(key, item) {
onClickDrop(key, item, currentSelect.value);
@@ -435,13 +445,13 @@ function closeMenu() {
function showMenus(value: boolean) {
Array.of(1, 2, 3, 4, 5).forEach(v => {
tagsViews.value[v].show = value;
tagsViews[v].show = value;
});
}
function disabledMenus(value: boolean) {
Array.of(1, 2, 3, 4, 5).forEach(v => {
tagsViews.value[v].disabled = value;
tagsViews[v].disabled = value;
});
}
@@ -463,35 +473,34 @@ function showMenuModel(
showMenus(true);
if (refresh) {
tagsViews.value[0].show = true;
tagsViews[0].show = true;
}
/**
* currentIndex为1时左侧的菜单是首页则不显示关闭左侧标签页
* 如果currentIndex等于routeLength-1右侧没有菜单则不显示关闭右侧标签页
*/
if (currentIndex === 1 && routeLength !== 2) {
// 左侧的菜单是首页,右侧存在别的菜单
tagsViews.value[2].show = false;
tagsViews[2].show = false;
Array.of(1, 3, 4, 5).forEach(v => {
tagsViews.value[v].disabled = false;
tagsViews[v].disabled = false;
});
tagsViews.value[2].disabled = true;
tagsViews[2].disabled = true;
} else if (currentIndex === 1 && routeLength === 2) {
disabledMenus(false);
// 左侧的菜单是首页,右侧不存在别的菜单
Array.of(2, 3, 4).forEach(v => {
tagsViews.value[v].show = false;
tagsViews.value[v].disabled = true;
tagsViews[v].show = false;
tagsViews[v].disabled = true;
});
} else if (routeLength - 1 === currentIndex && currentIndex !== 0) {
// 当前路由是所有路由中的最后一个
tagsViews.value[3].show = false;
tagsViews[3].show = false;
Array.of(1, 2, 4, 5).forEach(v => {
tagsViews.value[v].disabled = false;
tagsViews[v].disabled = false;
});
tagsViews.value[3].disabled = true;
tagsViews[3].disabled = true;
} else if (currentIndex === 0 || currentPath === "/redirect/welcome") {
// 当前路由为首页
disabledMenus(true);
@@ -505,10 +514,10 @@ function openMenu(tag, e) {
if (tag.path === "/welcome") {
// 右键菜单为首页,只显示刷新
showMenus(false);
tagsViews.value[0].show = true;
tagsViews[0].show = true;
} else if (route.path !== tag.path) {
// 右键菜单不匹配当前路由,隐藏刷新
tagsViews.value[0].show = false;
tagsViews[0].show = false;
showMenuModel(tag.path, tag.query);
} else if (
// eslint-disable-next-line no-dupe-else-if
@@ -517,7 +526,7 @@ function openMenu(tag, e) {
) {
showMenus(true);
// 只有两个标签时不显示关闭其他标签页
tagsViews.value[4].show = false;
tagsViews[4].show = false;
} else if (route.path === tag.path) {
// 右键当前激活的菜单
showMenuModel(tag.path, tag.query, true);
@@ -552,30 +561,32 @@ function tagOnClick(item) {
}
// 鼠标移入
function onMouseenter(item, index) {
function onMouseenter(index) {
if (index) activeIndex.value = index;
if (unref(showModel) === "smart") {
if (hasClass(instance.refs["schedule" + index], "schedule-active")) return;
toggleClass(true, "schedule-in", instance.refs["schedule" + index]);
toggleClass(false, "schedule-out", instance.refs["schedule" + index]);
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], "card-active")) return;
toggleClass(true, "card-in", instance.refs["dynamic" + index]);
toggleClass(false, "card-out", instance.refs["dynamic" + index]);
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(item, index) {
function onMouseleave(index) {
activeIndex.value = -1;
if (unref(showModel) === "smart") {
if (hasClass(instance.refs["schedule" + index], "schedule-active")) return;
toggleClass(false, "schedule-in", instance.refs["schedule" + index]);
toggleClass(true, "schedule-out", instance.refs["schedule" + index]);
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], "card-active")) return;
toggleClass(false, "card-in", instance.refs["dynamic" + index]);
toggleClass(true, "card-out", instance.refs["dynamic" + index]);
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]);
}
}
@@ -629,7 +640,9 @@ const getContextMenuStyle = computed((): CSSProperties => {
<template>
<div ref="containerDom" class="tags-view" v-if="!showTags">
<i class="ri-arrow-left-s-line" @click="handleScroll(200)"></i>
<div class="arrow-left">
<IconifyIconOffline icon="arrow-left-s-line" @click="handleScroll(200)" />
</div>
<div ref="scrollbarDom" class="scroll-container">
<div class="tab" ref="tabDom" :style="getTabStyle">
<div
@@ -644,14 +657,14 @@ const getContextMenuStyle = computed((): CSSProperties => {
: ''
]"
@contextmenu.prevent="openMenu(item, $event)"
@mouseenter.prevent="onMouseenter(item, index)"
@mouseleave.prevent="onMouseleave(item, index)"
@mouseenter.prevent="onMouseenter(index)"
@mouseleave.prevent="onMouseleave(index)"
@click="tagOnClick(item)"
>
<router-link :to="item.path"
>{{ transformI18n(item.meta.title, item.meta.i18n) }}
>{{ transformI18n(item.meta.title) }}
</router-link>
<el-icon
<span
v-if="
iconIsActive(item, index) ||
(index === activeIndex && index !== 0)
@@ -659,17 +672,22 @@ const getContextMenuStyle = computed((): CSSProperties => {
class="el-icon-close"
@click.stop="deleteMenu(item)"
>
<CloseBold />
</el-icon>
<IconifyIconOffline icon="close-bold" />
</span>
<div
:ref="'schedule' + index"
v-if="showModel !== 'card'"
:class="[scheduleIsActive(item)]"
></div>
/>
</div>
</div>
</div>
<i class="ri-arrow-right-s-line" @click="handleScroll(-200)"></i>
<span class="arrow-right">
<IconifyIconOffline
icon="arrow-right-s-line"
@click="handleScroll(-200)"
/>
</span>
<!-- 右键菜单按钮 -->
<transition name="el-zoom-in-top">
<ul
@@ -684,8 +702,8 @@ const getContextMenuStyle = computed((): CSSProperties => {
style="display: flex; align-items: center"
>
<li v-if="item.show" @click="selectTag(key, item)">
<component :is="item.icon" :key="key" />
{{ $t(item.text) }}
<component :is="toRaw(item.icon)" :key="key" />
{{ t(item.text) }}
</li>
</div>
</ul>
@@ -693,37 +711,43 @@ const getContextMenuStyle = computed((): CSSProperties => {
<!-- 右侧功能按钮 -->
<ul class="right-button">
<li>
<el-icon
:title="$t('buttons.hsrefreshRoute')"
<span
:title="t('buttons.hsrefreshRoute')"
class="el-icon-refresh-right rotate"
@click="onFresh"
>
<RefreshRight />
</el-icon>
<IconifyIconOffline icon="refresh-right" />
</span>
</li>
<li>
<el-dropdown trigger="click" placement="bottom-end">
<el-icon>
<ArrowDown />
</el-icon>
<el-dropdown
trigger="click"
placement="bottom-end"
@command="handleCommand"
>
<IconifyIconOffline icon="arrow-down" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="(item, key) in tagsViews"
:key="key"
:command="{ key, item }"
:divided="item.divided"
:disabled="item.disabled"
@click="onClickDrop(key, item)"
>
<component :is="item.icon" :key="key" />
{{ $t(item.text) }}
<component
:is="toRaw(item.icon)"
:key="key"
style="margin-right: 6px"
/>
{{ t(item.text) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</li>
<li>
<slot></slot>
<slot />
</li>
</ul>
</div>

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

@@ -0,0 +1,70 @@
<template>
<div
class="frame"
v-loading="loading"
:element-loading-text="t('status.hsLoad')"
>
<iframe :src="frameSrc" class="frame-iframe" ref="frameRef" />
</div>
</template>
<script lang="ts" setup>
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { ref, unref, onMounted, nextTick } from "vue";
const { t } = useI18n();
const loading = ref(false);
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;
}
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(() => {
loading.value = true;
init();
});
</script>
<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>

126
src/layout/hooks/nav.ts Normal file
View File

@@ -0,0 +1,126 @@
import { computed } from "vue";
import { router } from "/@/router";
import { getConfig } from "/@/config";
import { emitter } from "/@/utils/mitt";
import { routeMetaType } from "../types";
import { remainingPaths } from "/@/router";
import { transformI18n } from "/@/plugins/i18n";
import { storageSession } from "/@/utils/storage";
import { useAppStoreHook } from "/@/store/modules/app";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
const errorInfo = "当前路由配置不正确,请检查配置";
export function useNav() {
const pureApp = useAppStoreHook();
// 用户名
const username: string = storageSession.getItem("info")?.username;
// 设置国际化选中后的样式
const getDropdownItemStyle = computed(() => {
return (locale, t) => {
return {
background: locale === t ? useEpThemeStoreHook().epThemeColor : "",
color: locale === t ? "#f4f4f5" : "#000"
};
};
});
const avatarsStyle = computed(() => {
return username ? { marginRight: "10px" } : "";
});
const isCollapse = computed(() => {
return !pureApp.getSidebarStatus;
});
// 动态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() {
storageSession.removeItem("info");
router.push("/login");
}
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 {
logout,
backHome,
onPanel,
changeTitle,
toggleSideBar,
menuSelect,
handleResize,
resolvePath,
isCollapse,
pureApp,
username,
avatarsStyle,
getDropdownItemStyle
};
}

View File

@@ -0,0 +1,26 @@
import { ref } from "vue";
export default 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

@@ -11,14 +11,15 @@ import { setType } from "./types";
import { useI18n } from "vue-i18n";
import { routerArrays } from "./types";
import { emitter } from "/@/utils/mitt";
import backTop from "/@/assets/svg/back_top.svg";
import { useAppStoreHook } from "/@/store/modules/app";
import fullScreen from "/@/assets/svg/full_screen.svg";
import exitScreen from "/@/assets/svg/exit_screen.svg";
import { deviceDetection } from "/@/utils/deviceDetection";
import { useMultiTagsStore } from "/@/store/modules/multiTags";
import { useSettingStoreHook } from "/@/store/modules/settings";
import backTop from "/@/assets/svg/back_top.svg?component";
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 tag from "./components/tag/index.vue";
import appMain from "./components/appMain.vue";
@@ -170,7 +171,8 @@ const layoutHeader = defineComponent({
},
{
default: () => [
!pureSetting.hiddenSideBar && layout.value.includes("vertical")
!pureSetting.hiddenSideBar &&
(layout.value.includes("vertical") || layout.value.includes("mix"))
? h(navbar)
: h("div"),
!pureSetting.hiddenSideBar && layout.value.includes("horizontal")
@@ -212,7 +214,10 @@ const layoutHeader = defineComponent({
@click="useAppStoreHook().toggleSideBar()"
/>
<Vertical
v-show="!pureSetting.hiddenSideBar && layout.includes('vertical')"
v-show="
!pureSetting.hiddenSideBar &&
(layout.includes('vertical') || layout.includes('mix'))
"
/>
<div
:class="[

View File

@@ -16,5 +16,5 @@ replace({
</script>
<template>
<div></div>
<div />
</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,8 +1,8 @@
/* 动态改变element-plus主题色 */
import rgbHex from "rgb-hex";
import color from "css-color-function";
import epCss from "./element.scss";
import { TinyColor } from "@ctrl/tinycolor";
import epCss from "element-plus/dist/index.css";
import { convert } from "css-color-function";
// 色值表
const formula = {
@@ -26,7 +26,9 @@ export const writeNewStyle = (newStyle: string): void => {
};
// 根据主题色,生成最新的样式表
export const createNewStyle = (primaryStyle: string): string => {
export const createNewStyle = (
primaryStyle: Record<string, any>
): Record<string, any> => {
// 根据主色生成色值表
const colors = createColors(primaryStyle);
// 在当前ep的默认样式表中标记需要替换的色值
@@ -41,19 +43,21 @@ export const createNewStyle = (primaryStyle: string): string => {
return cssText;
};
export const createColors = (primary: string) => {
export const createColors = (
primary: Record<string, any>
): Record<string, any> => {
if (!primary) return;
const colors = {
primary
};
Object.keys(formula).forEach(key => {
const value = formula[key].replace(/primary/, primary);
colors[key] = "#" + rgbHex(color.convert(value));
colors[key] = "#" + rgbHex(convert(value));
});
return colors;
};
const getStyleTemplate = (data: string): string => {
const getStyleTemplate = (data: Record<string, any>): Record<string, any> => {
const colorMap = {
"#3a8ee6": "shade-1",
"#409eff": "primary",

View File

@@ -0,0 +1,2 @@
/* 通过scss模块本地导入element-plus的全局样式文件解决vite2.7.13版本后使用 import epCss from "element-plus/dist/index.css",打包后加载不到样式的问题 */
@import "element-plus/dist/index.css";

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

@@ -0,0 +1,136 @@
const themeColors = {
default: {
color: "#409EFF",
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: {
color: "#409EFF",
subMenuActiveText: "#409eff",
menuBg: "#fff",
menuHover: "#e0ebf6",
subMenuBg: "#fff",
subMenuActiveBg: "#e0ebf6",
navTextColor: "#7a80b4",
menuText: "#7a80b4",
sidebarLogo: "#fff",
menuTitleHover: "#000",
menuActiveBefore: "#4091f7"
},
dusk: {
color: "#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: {
color: "#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: {
color: "#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: {
color: "#13c2c2",
subMenuActiveText: "#fff",
menuBg: "#032121",
menuHover: "#59bfc1",
subMenuBg: "#000",
subMenuActiveBg: "#59bfc1",
navTextColor: "#7a80b4",
menuText: "#7a80b4",
sidebarLogo: "#053434",
menuTitleHover: "#fff",
menuActiveBefore: "#59bfc1"
},
auroraGreen: {
color: "#52c41a",
subMenuActiveText: "#fff",
menuBg: "#0b1e15",
menuHover: "#60ac80",
subMenuBg: "#000",
subMenuActiveBg: "#60ac80",
navTextColor: "#7a80b4",
menuText: "#7a80b4",
sidebarLogo: "#112f21",
menuTitleHover: "#fff",
menuActiveBefore: "#60ac80"
},
pink: {
color: "#eb2f96",
subMenuActiveText: "#fff",
menuBg: "#28081a",
menuHover: "#d84493",
subMenuBg: "#000",
subMenuActiveBg: "#d84493",
navTextColor: "#7a80b4",
menuText: "#7a80b4",
sidebarLogo: "#3f0d29",
menuTitleHover: "#fff",
menuActiveBefore: "#d84493"
},
saucePurple: {
color: "#722ed1",
subMenuActiveText: "#fff",
menuBg: "#130824",
menuHover: "#693ac9",
subMenuBg: "#000",
subMenuActiveBg: "#693ac9",
navTextColor: "#7a80b4",
menuText: "#7a80b4",
sidebarLogo: "#1f0c38",
menuTitleHover: "#fff",
menuActiveBefore: "#693ac9"
}
};
type MultipleScopeVarsItem = {
scopeName: string;
path: string;
varsContent: string;
};
export function genScssMultipleScopeVars(): MultipleScopeVarsItem[] {
const result = [] as MultipleScopeVarsItem[];
Object.keys(themeColors).forEach(key => {
result.push({
scopeName: `layout-theme-${key}`,
varsContent: `$primary-color: ${themeColors[key].color} !default;$vxe-primary-color: $primary-color;$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;

View File

@@ -1,28 +1,29 @@
import { Component } from "vue";
export const routerArrays: Array<RouteConfigs> = [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "menus.hshome",
i18n: true,
icon: "HomeFilled",
showLink: true
icon: "home-filled"
}
}
];
export type routeMetaType = {
title?: string;
i18n?: boolean;
icon?: string;
showLink?: boolean;
savedPosition?: boolean;
authority?: Array<string>;
};
export type RouteConfigs = {
path?: string;
parentPath?: string;
query?: object;
meta?: {
title?: string;
i18n?: boolean;
icon?: string;
showLink?: boolean;
savedPosition?: boolean;
authority?: Array<string>;
};
meta?: routeMetaType;
children?: RouteConfigs[];
name?: string;
};
@@ -32,7 +33,7 @@ export type multiTagsType = {
};
export type tagsViewsType = {
icon: string;
icon: Component;
text: string;
divided: boolean;
disabled: boolean;
@@ -65,16 +66,19 @@ export type childrenType = {
icon?: string;
title?: string;
i18n?: boolean;
showParent?: boolean;
extraIcon?: {
svg?: boolean;
name?: string;
};
};
showTooltip?: boolean;
parentId?: number;
pathList?: number[];
};
export type themeColorsType = {
rgb: string;
color: string;
themeColor: string;
};

View File

@@ -1,17 +1,22 @@
import App from "./App.vue";
import router from "./router";
import { setupStore } from "/@/store";
import ElementPlus from "element-plus";
import { getServerConfig } from "./config";
import { createApp, Directive } from "vue";
import { usI18n } from "../src/plugins/i18n";
import { useI18n } from "../src/plugins/i18n";
import { MotionPlugin } from "@vueuse/motion";
import { useFontawesome } from "../src/plugins/fontawesome";
import { useElementPlus } from "../src/plugins/element-plus";
import { injectResponsiveStorage } from "/@/utils/storage/responsive";
import "uno.css";
import "animate.css";
// 引入重置样式
import "./style/reset.scss";
// 导入公共样式
import "./style/index.scss";
import "element-plus/dist/index.css";
import "@pureadmin/components/dist/index.css";
import "@pureadmin/components/dist/theme.css";
// 导入字体图标
import "./assets/iconfont/iconfont.js";
import "./assets/iconfont/iconfont.css";
@@ -24,15 +29,21 @@ Object.keys(directives).forEach(key => {
app.directive(key, (directives as { [key: string]: Directive })[key]);
});
// 全局注册`@iconify/vue`图标库
import {
IconifyIconOffline,
IconifyIconOnline,
FontIcon
} from "./components/ReIcon";
app.component("IconifyIconOffline", IconifyIconOffline);
app.component("IconifyIconOnline", IconifyIconOnline);
app.component("FontIcon", FontIcon);
getServerConfig(app).then(async config => {
app.use(router);
await router.isReady();
injectResponsiveStorage(app, config);
setupStore(app);
app
.use(router)
.use(MotionPlugin)
.use(useElementPlus)
.use(usI18n)
.use(useFontawesome);
await router.isReady();
app.use(MotionPlugin).use(useI18n).use(ElementPlus);
app.mount("#app");
});

View File

@@ -1,7 +1,11 @@
import { createProdMockServer } from "vite-plugin-mock/es/createProdMockServer";
import asyncRoutesMock from "../mock/asyncRoutes";
export const mockModules = [...asyncRoutesMock];
const modules = import.meta.globEager("../mock/*.ts");
const mockModules = [];
Object.keys(modules).forEach(key => {
mockModules.push(...modules[key].default);
});
export function setupProdMockServer() {
createProdMockServer(mockModules);

View File

@@ -1,8 +1,6 @@
import { App, Component } from "vue";
import {
ElTag,
ElAffix,
ElSkeleton,
ElBreadcrumb,
ElBreadcrumbItem,
ElScrollbar,
@@ -10,30 +8,21 @@ import {
ElButton,
ElCol,
ElRow,
ElSpace,
ElDivider,
ElCard,
ElDropdown,
ElDialog,
ElMenu,
ElMenuItem,
ElDropdownItem,
ElDropdownMenu,
ElIcon,
ElInput,
ElForm,
ElFormItem,
ElPopover,
ElPopper,
ElTooltip,
ElDrawer,
ElPagination,
ElAlert,
ElRadio,
ElRadioButton,
ElRadioGroup,
ElDescriptions,
ElDescriptionsItem,
ElBacktop,
ElSwitch,
ElBadge,
@@ -43,7 +32,8 @@ import {
ElEmpty,
ElCollapse,
ElCollapseItem,
ElTreeV2,
ElDialog,
ElCard,
// 指令
ElLoading,
ElInfiniteScroll
@@ -54,8 +44,6 @@ const plugins = [ElLoading, ElInfiniteScroll];
const components = [
ElTag,
ElAffix,
ElSkeleton,
ElBreadcrumb,
ElBreadcrumbItem,
ElScrollbar,
@@ -63,30 +51,21 @@ const components = [
ElButton,
ElCol,
ElRow,
ElSpace,
ElDivider,
ElCard,
ElDropdown,
ElDialog,
ElMenu,
ElMenuItem,
ElDropdownItem,
ElDropdownMenu,
ElIcon,
ElInput,
ElForm,
ElFormItem,
ElPopover,
ElPopper,
ElTooltip,
ElDrawer,
ElPagination,
ElAlert,
ElRadio,
ElRadioButton,
ElRadioGroup,
ElDescriptions,
ElDescriptionsItem,
ElBacktop,
ElSwitch,
ElBadge,
@@ -96,59 +75,8 @@ const components = [
ElEmpty,
ElCollapse,
ElCollapseItem,
ElTreeV2
];
// https://element-plus.org/zh-CN/component/icon.html
import {
Check,
Menu,
HomeFilled,
SetUp,
Edit,
Setting,
Lollipop,
Link,
Position,
Histogram,
RefreshRight,
ArrowDown,
Close,
CloseBold,
Bell,
Guide,
User,
Iphone,
Location,
Tickets,
OfficeBuilding,
Notebook
} from "@element-plus/icons-vue";
// Icon
export const iconComponents = [
Check,
Menu,
HomeFilled,
SetUp,
Edit,
Setting,
Lollipop,
Link,
Position,
Histogram,
RefreshRight,
ArrowDown,
Close,
CloseBold,
Bell,
Guide,
User,
Iphone,
Location,
Tickets,
OfficeBuilding,
Notebook
ElDialog,
ElCard
];
export function useElementPlus(app: App) {
@@ -160,8 +88,4 @@ export function useElementPlus(app: App) {
plugins.forEach(plugin => {
app.use(plugin);
});
// 注册图标
iconComponents.forEach((component: Component) => {
app.component(component.name, component);
});
}

View File

@@ -1,21 +0,0 @@
/** 兼容fontawesome4和5版本
* 4版本: www.fontawesome.com.cn/faicons/
* 5版本https://fontawesome.com/v5.15/icons?d=gallery&p=2&m=free
* https://github.com/FortAwesome/vue-fontawesome
*/
import { App } from "vue";
import "font-awesome/css/font-awesome.css";
import { library } from "@fortawesome/fontawesome-svg-core";
import {
faUserSecret,
faCoffee,
faSpinner
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
// github.com/Remix-Design/RemixIcon/blob/master/README_CN.md#%E5%AE%89%E8%A3%85%E5%BC%95%E5%85%A5
import "remixicon/fonts/remixicon.css";
export function useFontawesome(app: App) {
library.add(faUserSecret, faCoffee, faSpinner);
app.component("font-awesome-icon", FontAwesomeIcon);
}

72
src/plugins/i18n.ts Normal file
View File

@@ -0,0 +1,72 @@
// 多组件库的国际化和本地项目国际化兼容
import { App, WritableComputedRef } from "vue";
import { storageLocal } from "/@/utils/storage";
import { type I18n, createI18n } from "vue-i18n";
// element-plus国际化
import enLocale from "element-plus/lib/locale/lang/en";
import zhLocale from "element-plus/lib/locale/lang/zh-cn";
function siphonI18n(prefix = "zh-CN") {
return Object.fromEntries(
Object.entries(import.meta.globEager("../../locales/*.y(a)?ml")).map(
([key, value]) => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)[1];
return [matched, value.default];
}
)
)[prefix];
}
export const localesConfigs = {
zh: {
...siphonI18n("zh-CN"),
...zhLocale
},
en: {
...siphonI18n("en"),
...enLocale
}
};
/**
* 国际化转换工具函数自动读取根目录locales文件夹下文件进行国际化匹配
* @param message message
* @returns 转化后的message
*/
export function transformI18n(message: any = "") {
if (!message) {
return "";
}
// 处理存储动态路由的title,格式 {zh:"",en:""}
if (typeof message === "object") {
const locale: string | WritableComputedRef<string> | any =
i18n.global.locale;
return message[locale?.value];
}
const key = message.match(/(\S*)\./)?.[1];
if (key && Object.keys(siphonI18n("zh-CN")).includes(key)) {
return i18n.global.t.call(i18n.global.locale, message);
} else if (!key && Object.keys(siphonI18n("zh-CN")).includes(message)) {
// 兼容非嵌套形式的国际化写法
return i18n.global.t.call(i18n.global.locale, message);
} else {
return message;
}
}
// 此函数只是配合i18n Ally插件来进行国际化智能提示并无实际意义只对提示起作用如果不需要国际化可删除
export const $t = (key: string) => key;
export const i18n: I18n = createI18n({
legacy: false,
locale: storageLocal.getItem("responsive-locale")?.locale ?? "zh",
fallbackLocale: "en",
messages: localesConfigs
});
export function useI18n(app: App) {
app.use(i18n);
}

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