Compare commits

...

35 Commits

Author SHA1 Message Date
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
100 changed files with 3887 additions and 3019 deletions

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()

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,16 @@
{
"recommendations": [
"johnsoncodehk.vscode-typescript-vue-plugin",
"voorjaar.windicss-intellisense",
"vscode-icons-team.vscode-icons",
"davidanson.vscode-markdownlint",
"stylelint.vscode-stylelint",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"johnsoncodehk.volar",
"lokalise.i18n-ally",
"mikestead.dotenv",
"eamodio.gitlens",
"antfu.iconify"
]
}

17
.vscode/settings.json vendored
View File

@@ -1,15 +1,4 @@
{
/** 便
* 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",
@@ -27,26 +16,20 @@
"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.keystyle": "nested",

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 `2MB`
## 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
@@ -35,3 +40,7 @@ bilibili: https://www.bilibili.com/video/BV1534y1S7HV/
## ⚠️ Note
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! ! !
## 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)提炼出的架子,包含主体功能,更适合实际项目开发,打包后的大小仅 `2MB`
## 配套视频
教程:<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)
## 维护者
@@ -27,11 +27,11 @@ UI 设计:<https://www.bilibili.com/video/BV17g411T7rq/>
<img src="http://yiming_chang.gitee.io/manages/pay.jpg" width="150px" height="150px" />
## 付费咨询、需求定制
## QQ 交流群
作者精力有限,需要提供技术服务的可扫下面的二维码加微信,添加请备注来意
群里严禁`黄``赌``毒``vpn`等违法行为!
<img src="http://yiming_chang.gitee.io/manages/wechat.jpg" width="150px" height="150px" />
<img src="http://yiming_chang.gitee.io/manages/qq.jpg" width="150px" height="225px" />
## 用法
@@ -52,3 +52,7 @@ pnpm remove 包名
## ⚠️ 注意
精简版不接受任何 issues 和 pr如果有问题请到完整版 https://github.com/xiaoxian521/vue-pure-admin/issues/new/choose 去提,谢谢!!!
## 许可证
原则上不收取任何费用及版权,可以放心使用,不过如需二次开源(比如用此平台二次开发并开源)请联系作者获取许可!

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

110
build/plugins.ts Normal file
View File

@@ -0,0 +1,110 @@
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 WindiCSS from "vite-plugin-windicss";
import { viteMockServe } from "vite-plugin-mock";
import liveReload from "vite-plugin-live-reload";
import ElementPlus from "unplugin-element-plus/vite";
import { visualizer } from "rollup-plugin-visualizer";
import removeConsole from "vite-plugin-remove-console";
import themePreprocessorPlugin from "@zougt/vite-plugin-theme-preprocessor";
export function getPluginsList(command, VITE_LEGACY) {
const prodMock = true;
const lifecycle = process.env.npm_lifecycle_event;
return [
vue(),
// jsx、tsx语法支持
vueJsx(),
WindiCSS(),
// 线上环境删除console
removeConsole(),
viteBuildInfo(),
// 修改layout文件夹下的文件时自动重载浏览器 解决 https://github.com/xiaoxian521/vue-pure-admin/issues/170
liveReload(["src/layout/**/*", "src/router/**/*"]),
// 自定义主题
themePreprocessorPlugin({
scss: {
multipleScopeVars: [
{
scopeName: "layout-theme-default",
path: "src/layout/theme/default-vars.scss"
},
{
scopeName: "layout-theme-light",
path: "src/layout/theme/light-vars.scss"
},
{
scopeName: "layout-theme-dusk",
path: "src/layout/theme/dusk-vars.scss"
},
{
scopeName: "layout-theme-volcano",
path: "src/layout/theme/volcano-vars.scss"
},
{
scopeName: "layout-theme-yellow",
path: "src/layout/theme/yellow-vars.scss"
},
{
scopeName: "layout-theme-mingQing",
path: "src/layout/theme/mingQing-vars.scss"
},
{
scopeName: "layout-theme-auroraGreen",
path: "src/layout/theme/auroraGreen-vars.scss"
},
{
scopeName: "layout-theme-pink",
path: "src/layout/theme/pink-vars.scss"
},
{
scopeName: "layout-theme-saucePurple",
path: "src/layout/theme/saucePurple-vars.scss"
}
],
// 默认取 multipleScopeVars[0].scopeName
defaultScopeName: "",
// 在生产模式是否抽取独立的主题css文件extract为true以下属性有效
extract: true,
// 独立主题css文件的输出路径默认取 viteConfig.build.assetsDir 相对于 (viteConfig.build.outDir)
outputDir: "",
// 会选取defaultScopeName对应的主题css文件在html添加link
themeLinkTagId: "head",
// "head"||"head-prepend" || "body" ||"body-prepend"
themeLinkTagInjectTo: "head",
// 是否对抽取的css文件内对应scopeName的权重类名移除
removeCssScopeName: false,
// 可以自定义css文件名称的函数
customThemeCssFileName: scopeName => scopeName
}
}),
// svg组件化支持
svgLoader(),
ElementPlus({}),
// mock支持
viteMockServe({
mockPath: "mock",
localEnabled: command === "serve",
prodEnabled: command !== "serve" && prodMock,
injectCode: `
import { setupProdMockServer } from './mockProdServer';
setupProdMockServer();
`,
logger: true
}),
// 是否为打包后的文件提供传统浏览器兼容性支持
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: ".";
.loader:before {
left: -3.5em;
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
66.67% {
content: "..";
.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>

View File

@@ -7,9 +7,8 @@ const permissionRouter = {
redirect: "/permission/page/index",
meta: {
title: "menus.permission",
icon: "Lollipop",
icon: "lollipop",
i18n: true,
showLink: true,
rank: 3
},
children: [
@@ -18,8 +17,7 @@ const permissionRouter = {
name: "permissionPage",
meta: {
title: "menus.permissionPage",
i18n: true,
showLink: true
i18n: true
}
},
{
@@ -28,7 +26,6 @@ const permissionRouter = {
meta: {
title: "menus.permissionButton",
i18n: true,
showLink: true,
authority: []
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "pure-admin-thin",
"version": "2.8.0",
"version": "3.1.0",
"private": true,
"engines": {
"node": ">= 16",
@@ -10,6 +10,7 @@
"dev": "cross-env --max_old_space_size=4096 vite",
"serve": "pnpm dev",
"build": "rimraf dist && cross-env vite build",
"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 +19,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"
},
@@ -29,80 +30,90 @@
],
"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",
"@vueuse/core": "^7.6.2",
"@vueuse/motion": "^2.0.0-beta.9",
"@vueuse/shared": "^7.6.2",
"animate.css": "^4.1.1",
"axios": "^0.21.1",
"axios": "^0.25.0",
"css-color-function": "^1.3.3",
"dayjs": "^1.10.7",
"element-plus": "1.3.0-beta.1",
"element-plus": "^2.0.3",
"element-resize-detector": "^1.2.3",
"font-awesome": "^4.7.0",
"js-cookie": "^3.0.1",
"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.11",
"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.31",
"vue-i18n": "^9.2.0-beta.30",
"vue-router": "^4.0.13",
"vue-types": "^4.1.1"
},
"devDependencies": {
"@commitlint/cli": "13.1.0",
"@commitlint/config-conventional": "13.1.0",
"@iconify-icons/ep": "^1.1.3",
"@iconify-icons/fa": "^1.1.1",
"@iconify-icons/fa-solid": "^1.1.2",
"@iconify-icons/ri": "^1.1.1",
"@iconify/vue": "^3.1.3",
"@types/element-resize-detector": "1.1.3",
"@types/js-cookie": "^3.0.1",
"@types/lodash-es": "^4.17.6",
"@types/mockjs": "1.0.3",
"@types/node": "14.14.14",
"@types/nprogress": "0.2.0",
"@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.7.1",
"@vitejs/plugin-vue": "^2.2.4",
"@vitejs/plugin-vue-jsx": "^1.3.8",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^10.0.0",
"@zougt/vite-plugin-theme-preprocessor": "^1.4.4",
"autoprefixer": "^10.4.2",
"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-plugin-visualizer": "^5.6.0",
"sass": "^1.49.0",
"sass-loader": "^12.4.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.5.5",
"unplugin-element-plus": "^0.2.0",
"vite": "^2.8.6",
"vite-plugin-live-reload": "^2.1.0",
"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.6",
"vite-plugin-style-import": "1.4.1",
"vite-plugin-windicss": "^1.8.2",
"vite-svg-loader": "2.2.0",
"vue-eslint-parser": "^8.2.0",
"windicss": "^3.5.1"
},
"repository": "git@github.com:xiaoxian521/vue-pure-admin.git",
"author": "xiaoxian521",

3439
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.1.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: 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

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,91 @@
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 Menu from "@iconify-icons/ep/menu";
import HomeFilled from "@iconify-icons/ep/home-filled";
import SetUp from "@iconify-icons/ep/set-up";
import Edit from "@iconify-icons/ep/edit";
import Setting from "@iconify-icons/ep/setting";
import Lollipop from "@iconify-icons/ep/lollipop";
import Link from "@iconify-icons/ep/link";
import Position from "@iconify-icons/ep/position";
import Histogram from "@iconify-icons/ep/histogram";
import RefreshRight from "@iconify-icons/ep/refresh-right";
import ArrowDown from "@iconify-icons/ep/arrow-down";
import Close from "@iconify-icons/ep/close";
import CloseBold from "@iconify-icons/ep/close-bold";
import Bell from "@iconify-icons/ep/bell";
import Guide from "@iconify-icons/ep/guide";
import User from "@iconify-icons/ep/user";
import Iphone from "@iconify-icons/ep/iphone";
import Location from "@iconify-icons/ep/location";
import Tickets from "@iconify-icons/ep/tickets";
import OfficeBuilding from "@iconify-icons/ep/office-building";
import Notebook from "@iconify-icons/ep/notebook";
addIcon("check", Check);
addIcon("menu", Menu);
addIcon("home-filled", HomeFilled);
addIcon("set-up", SetUp);
addIcon("edit", Edit);
addIcon("setting", Setting);
addIcon("lollipop", Lollipop);
addIcon("link", Link);
addIcon("position", Position);
addIcon("histogram", Histogram);
addIcon("refresh-right", RefreshRight);
addIcon("arrow-down", ArrowDown);
addIcon("close", Close);
addIcon("close-bold", CloseBold);
addIcon("bell", Bell);
addIcon("guide", Guide);
addIcon("user", User);
addIcon("iphone", Iphone);
addIcon("location", Location);
addIcon("tickets", Tickets);
addIcon("office-building", OfficeBuilding);
addIcon("notebook", Notebook);
// 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 nodeTree from "@iconify-icons/ri/node-tree";
addIcon("arrow-right-s-line", arrowRightSLine);
addIcon("arrow-left-s-line", arrowLeftSLine);
addIcon("logout-circle-r-line", logoutCircleRLine);
addIcon("node-tree", nodeTree);
// Font Awesome 4
import faUser from "@iconify-icons/fa/user";
import faLock from "@iconify-icons/fa/lock";
import faSignOut from "@iconify-icons/fa/sign-out";
addIcon("fa-user", faUser);
addIcon("fa-lock", faLock);
addIcon("fa-sign-out", faSignOut);
// 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,32 @@
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: ""
},
// default element plus icon
type: {
type: String,
default: "ep:"
}
},
render() {
const attrs = this.$attrs;
return h(
IconifyIcon,
{
icon: `${this.type}${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

@@ -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,43 @@
<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 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 } = 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,
usename,
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 +47,17 @@ 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">
<!-- 通知 -->
<Notice id="header-notice" />
<!-- 全屏 -->
@@ -92,17 +68,19 @@ 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
><IconifyIconOffline icon="check" /></el-icon
>English</el-dropdown-item
>
</el-dropdown-menu>
@@ -117,8 +95,10 @@ function translationEn() {
<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>
@@ -128,7 +108,7 @@ function translationEn() {
:title="$t('buttons.hssystemSet')"
@click="onPanel"
>
<Setting />
<IconifyIconOffline icon="setting" />
</el-icon>
</div>
</div>
@@ -149,10 +129,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 {
@@ -233,14 +209,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 +227,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

@@ -16,7 +16,9 @@ notices.value.forEach(notice => {
<el-dropdown trigger="click" placement="bottom-end">
<span class="dropdown-badge">
<el-badge :value="noticesNum" :max="99">
<el-icon class="header-notice-icon"><bell /></el-icon>
<el-icon class="header-notice-icon"
><IconifyIconOffline icon="bell"
/></el-icon>
</el-badge>
</span>
<template #dropdown>

View File

@@ -23,7 +23,7 @@ emitter.on("openPanel", () => {
<div class="project-configuration">
<h3>项目配置</h3>
<el-icon title="关闭配置" class="el-icon-close" @click="show = !show">
<Close />
<IconifyIconOffline icon="close" />
</el-icon>
</div>
<div style="border-bottom: 1px solid #dcdfe6"></div>
@@ -125,7 +125,7 @@ emitter.on("openPanel", () => {
&:hover {
cursor: pointer;
color: red;
color: var(--el-color-primary);
}
}
}

View File

@@ -5,18 +5,14 @@ const { isFullscreen, toggle } = useFullscreen();
<template>
<div class="screen-full" @click="toggle">
<i
<FontIcon
:title="
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

@@ -10,15 +10,13 @@ import {
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";
@@ -28,6 +26,9 @@ 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 dayIcon from "/@/assets/svg/day.svg?component";
import darkIcon from "/@/assets/svg/dark.svg?component";
const router = useRouter();
const { isSelect } = useCssModule();
const body = document.documentElement as HTMLElement;
@@ -60,6 +61,7 @@ let themeColors = ref<Array<themeColorsType>>([
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")) ||
@@ -100,7 +102,7 @@ const getThemeColorStyle = computed(() => {
};
});
function changeStorageConfigure(key, val) {
function storageConfigureChange<T>(key: string, val: T): void {
const storageConfigure = instance.configure;
storageConfigure[key] = val;
instance.configure = storageConfigure;
@@ -109,14 +111,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 +128,55 @@ 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",
i18n: true
}
}
]);
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 +191,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;
}
});
@@ -315,7 +321,7 @@ nextTick(() => {
<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"
@@ -326,7 +332,7 @@ nextTick(() => {
</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"
@@ -336,6 +342,17 @@ nextTick(() => {
<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>
<div></div>
</li>
</el-tooltip>
</ul>
<el-divider v-show="!dataTheme">主题色</el-divider>
@@ -351,7 +368,7 @@ nextTick(() => {
:size="17"
:color="getThemeColor(item.themeColor)"
>
<Check />
<IconifyIconOffline icon="check" />
</el-icon>
</li>
</ul>
@@ -436,7 +453,12 @@ nextTick(() => {
style="width: 90%; margin: 24px 15px"
@click="onReset"
>
<i class="fa fa-sign-out"></i>
<IconifyIconOffline
icon="fa-sign-out"
width="15"
height="15"
style="margin-right: 4px"
/>
清空缓存并返回登录页</el-button
>
</panel>
@@ -444,7 +466,7 @@ nextTick(() => {
<style scoped module>
.isSelect {
border: 2px solid #0960bd;
border: 2px solid var(--el-color-primary);
}
</style>
@@ -476,15 +498,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 +543,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";

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,87 @@
<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 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 } = 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,
usename,
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");
};
watch(
() => route.path,
() => {
menuSelect(route.path, routers);
}
);
function onPanel() {
emitter.emit("openPanel");
}
const activeMenu = computed((): string => {
const { meta, path } = route;
if (meta.activeMenu) {
// @ts-ignore
return meta.activeMenu;
}
return path;
});
const menuSelect = (indexPath: string): void => {
let parentPath = "";
let parentPathIndex = indexPath.lastIndexOf("/");
if (parentPathIndex > 0) {
parentPath = indexPath.slice(0, parentPathIndex);
}
// 找到当前路由的信息
function findCurrentRoute(routes) {
return routes.map(item => {
if (item.path === indexPath) {
// 切换左侧菜单 通知标签页
emitter.emit("changLayoutRoute", {
indexPath,
parentPath
});
} else {
if (item.children) findCurrentRoute(item.children);
}
});
}
findCurrentRoute(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"
></FontIcon>
<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"
@@ -158,17 +101,17 @@ 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
><IconifyIconOffline icon="check" /></el-icon
>简体中文</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
><IconifyIconOffline icon="check" /></el-icon
>English</el-dropdown-item
>
</el-dropdown-menu>
@@ -183,8 +126,11 @@ onMounted(() => {
<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>
@@ -194,7 +140,7 @@ onMounted(() => {
:title="$t('buttons.hssystemSet')"
@click="onPanel"
>
<Setting />
<IconifyIconOffline icon="setting" />
</el-icon>
</div>
</div>
@@ -202,14 +148,8 @@ onMounted(() => {
<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 +166,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,11 @@ 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"
></FontIcon>
<span class="sidebar-title">{{ title }}</span>
</router-link>
<router-link
@@ -29,7 +32,11 @@ 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"
></FontIcon>
<span class="sidebar-title">{{ title }}</span>
</router-link>
</transition>

View File

@@ -0,0 +1,239 @@
<script setup lang="ts">
import { useI18n } from "vue-i18n";
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 } = 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,
usename,
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>
<el-icon v-show="route.meta.icon" :class="route.meta.icon">
<component
:is="useRenderIcon(route.meta && route.meta.icon)"
></component>
</el-icon>
<span>{{ transformI18n(route.meta.title, route.meta.i18n) }}</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"
></FontIcon>
</template>
</el-menu-item>
</el-menu>
<div class="horizontal-header-right">
<!-- 通知 -->
<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"
><el-icon class="check-zh" v-show="locale === 'zh'"
><IconifyIconOffline icon="check" /></el-icon
>简体中文</el-dropdown-item
>
<el-dropdown-item
:style="getDropdownItemStyle(locale, 'en')"
@click="translationEn"
><el-icon class="check-en" v-show="locale === 'en'"
><IconifyIconOffline icon="check" /></el-icon
>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>
</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>
<el-icon
class="el-icon-setting"
:title="$t('buttons.hssystemSet')"
@click="onPanel"
>
<IconifyIconOffline icon="setting" />
</el-icon>
</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 { ref, PropType, nextTick, computed, CSSProperties } 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";
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);
}
@@ -143,13 +151,25 @@ function resolvePath(routePath) {
<el-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
v-if="
!pureApp.sidebar.opened &&
pureApp.layout === 'mix' &&
props.item?.pathList?.length === 2
"
:style="getDivStyle"
>
<span :style="getMenuTextStyle">
{{ transformI18n(onlyOneChild.meta.title, onlyOneChild.meta.i18n) }}
</span>
</div>
<template #title>
<div :style="getDivStyle">
<span v-if="!menuMode">{{
@@ -176,11 +196,14 @@ function resolvePath(routePath) {
}}
</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}`"
/>
></FontIcon>
</div>
</template>
</el-menu-item>
@@ -195,7 +218,7 @@ function resolvePath(routePath) {
<template #title>
<el-icon v-show="props.item.meta.icon" :class="props.item.meta.icon">
<component
:is="findIconReg(props.item.meta && props.item.meta.icon)"
:is="useRenderIcon(props.item.meta && props.item.meta.icon)"
></component>
</el-icon>
<span v-if="!menuMode">{{
@@ -220,11 +243,14 @@ function resolvePath(routePath) {
</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}`"
/>
></FontIcon>
</template>
<sidebar-item
v-for="child in props.item.children"

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);
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;
}
// 找到当前路由的信息
// 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));
};
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,7 @@ import {
ref,
watch,
unref,
reactive,
nextTick,
computed,
ComputedRef,
@@ -11,26 +12,25 @@ 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 { emitter } from "/@/utils/mitt";
import { isEqual, isEmpty } from "lodash-es";
import { $t as t } from "/@/plugins/i18n";
import { transformI18n } from "/@/plugins/i18n";
import { storageLocal } from "/@/utils/storage";
import { useRoute, useRouter } from "vue-router";
import { isEqual, isEmpty } from "lodash-unified";
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 route = useRoute();
@@ -40,11 +40,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 +106,11 @@ const iconIsActive = computed(() => {
const dynamicTagView = () => {
const index = multiTags.value.findIndex(item => {
if (item?.query) {
return isEqual(route?.query, item?.query);
} else {
return item.path === route.path;
}
});
moveToView(index);
};
@@ -128,15 +132,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,7 +190,7 @@ const handleScroll = (offset: number): void => {
}
};
const tagsViews = ref<Array<tagsViewsType>>([
const tagsViews = reactive<Array<tagsViewsType>>([
{
icon: refresh,
text: t("buttons.hsreload"),
@@ -314,8 +318,7 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
meta: {
title: "menus.hshome",
i18n: true,
icon: "el-icon-s-home",
showLink: true
icon: "home-filled"
}
},
obj
@@ -424,6 +427,11 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
});
}
function handleCommand(command: object) {
const { key, item } = command;
onClickDrop(key, item);
}
// 触发右键中菜单的点击事件
function selectTag(key, item) {
onClickDrop(key, item, currentSelect.value);
@@ -435,13 +443,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 +471,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 +512,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 +524,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 +559,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 +638,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,8 +655,8 @@ 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"
@@ -659,7 +670,7 @@ const getContextMenuStyle = computed((): CSSProperties => {
class="el-icon-close"
@click.stop="deleteMenu(item)"
>
<CloseBold />
<IconifyIconOffline icon="close-bold" />
</el-icon>
<div
:ref="'schedule' + index"
@@ -669,7 +680,12 @@ const getContextMenuStyle = computed((): CSSProperties => {
</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
@@ -698,24 +714,32 @@ const getContextMenuStyle = computed((): CSSProperties => {
class="el-icon-refresh-right rotate"
@click="onFresh"
>
<RefreshRight />
<IconifyIconOffline icon="refresh-right" />
</el-icon>
</li>
<li>
<el-dropdown trigger="click" placement="bottom-end">
<el-dropdown
trigger="click"
placement="bottom-end"
@command="handleCommand"
>
<el-icon>
<ArrowDown />
<IconifyIconOffline icon="arrow-down" />
</el-icon>
<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" />
<component
:is="item.icon"
:key="key"
style="margin-right: 6px"
/>
{{ $t(item.text) }}
</el-dropdown-item>
</el-dropdown-menu>

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

@@ -0,0 +1,63 @@
<template>
<div class="frame" v-loading="loading">
<iframe :src="frameSrc" class="frame-iframe" ref="frameRef"></iframe>
</div>
</template>
<script lang="ts" setup>
import { useRoute } from "vue-router";
import { ref, unref, onMounted, nextTick } from "vue";
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: 100vh;
z-index: 998;
.frame-iframe {
width: 100%;
height: 100%;
overflow: hidden;
border: 0;
box-sizing: border-box;
}
}
.main-content {
margin: 0 !important;
}
</style>

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

@@ -0,0 +1,117 @@
import { computed } from "vue";
import { router } from "/@/router";
import { emitter } from "/@/utils/mitt";
import { routeMetaType } from "../types";
import { transformI18n } from "/@/plugins/i18n";
import { storageSession } from "/@/utils/storage";
import { useAppStoreHook } from "/@/store/modules/app";
import { remainingPaths } from "/@/router/modules/index";
import { Title } from "../../../public/serverConfig.json";
import { useEpThemeStoreHook } from "/@/store/modules/epTheme";
export function useNav() {
const pureApp = useAppStoreHook();
// 用户名
const usename: string = storageSession.getItem("info")?.username;
// 设置国际化选中后的样式
const getDropdownItemStyle = computed(() => {
return (locale, t) => {
return {
background: locale === t ? useEpThemeStoreHook().epThemeColor : "",
color: locale === t ? "#f4f4f5" : "#000"
};
};
});
const isCollapse = computed(() => {
return !pureApp.getSidebarStatus;
});
// 动态title
function changeTitle(meta: routeMetaType) {
if (Title)
document.title = `${transformI18n(meta.title, meta.i18n)} | ${Title}`;
else document.title = transformI18n(meta.title, meta.i18n);
}
// 退出登录
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) {
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) {
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,
usename,
getDropdownItemStyle
};
}

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

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

@@ -19,7 +19,7 @@ $subMenuBg: #0f0303 !default;
/* 有无子集的激活菜单背景 */
$subMenuActiveBg: #4091f7 !default;
$navTextColor: #fff !default;
$menuText: rgba(254, 254, 254, 0.65) !default;
$menuText: rgb(254 254 254 / 65%) !default;
/* logo背景颜色 */
$sidebarLogo: #002140 !default;

View File

@@ -5,7 +5,7 @@ $menuHover: #e13c39;
$subMenuBg: #000;
$subMenuActiveBg: #e13c39;
$navTextColor: red;
$menuText: rgba(254, 254, 254, 0.651);
$menuText: rgb(254 254 254 / 65.1%);
$sidebarLogo: #42090c;
$menuTitleHover: #fff;
$menuActiveBefore: #e13c39;

View File

@@ -1,6 +1,6 @@
/* 动态改变element-plus主题色 */
import rgbHex from "rgb-hex";
import color from "css-color-function";
import { convert } from "css-color-function";
import { TinyColor } from "@ctrl/tinycolor";
import epCss from "element-plus/dist/index.css";
@@ -48,7 +48,7 @@ export const createColors = (primary: string) => {
};
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;
};

View File

@@ -5,7 +5,7 @@ $menuHover: #e85f33;
$subMenuBg: #0f0603;
$subMenuActiveBg: #e85f33;
$navTextColor: #fff;
$menuText: rgba(254, 254, 254, 0.65);
$menuText: rgb(254 254 254 / 65%);
$sidebarLogo: #441708;
$menuTitleHover: #fff;
$menuActiveBefore: #e85f33;

View File

@@ -5,7 +5,7 @@ $menuHover: #f6da4d;
$subMenuBg: #0f0603;
$subMenuActiveBg: #f6da4d;
$navTextColor: #fff;
$menuText: rgba(254, 254, 254, 0.65);
$menuText: rgb(254 254 254 / 65%);
$sidebarLogo: #443b05;
$menuTitleHover: #fff;
$menuActiveBefore: #f6da4d;

View File

@@ -1,3 +1,4 @@
import { Component } from "vue";
export const routerArrays: Array<RouteConfigs> = [
{
path: "/welcome",
@@ -5,17 +6,12 @@ export const routerArrays: Array<RouteConfigs> = [
meta: {
title: "menus.hshome",
i18n: true,
icon: "HomeFilled",
showLink: true
icon: "home-filled"
}
}
];
export type RouteConfigs = {
path?: string;
parentPath?: string;
query?: object;
meta?: {
export type routeMetaType = {
title?: string;
i18n?: boolean;
icon?: string;
@@ -23,6 +19,12 @@ export type RouteConfigs = {
savedPosition?: boolean;
authority?: Array<string>;
};
export type RouteConfigs = {
path?: string;
parentPath?: string;
query?: object;
meta?: routeMetaType;
children?: RouteConfigs[];
name?: string;
};
@@ -32,7 +34,7 @@ export type multiTagsType = {
};
export type tagsViewsType = {
icon: string;
icon: Component;
text: string;
divided: boolean;
disabled: boolean;
@@ -65,12 +67,15 @@ 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 = {

View File

@@ -5,11 +5,11 @@ import { getServerConfig } from "./config";
import { createApp, Directive } from "vue";
import { usI18n } 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 "animate.css";
import "virtual:windi.css";
// 导入公共样式
import "./style/index.scss";
// 导入字体图标
@@ -24,15 +24,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(useElementPlus).use(usI18n);
app.mount("#app");
});

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,6 @@ import {
ElEmpty,
ElCollapse,
ElCollapseItem,
ElTreeV2,
// 指令
ElLoading,
ElInfiniteScroll
@@ -54,8 +42,6 @@ const plugins = [ElLoading, ElInfiniteScroll];
const components = [
ElTag,
ElAffix,
ElSkeleton,
ElBreadcrumb,
ElBreadcrumbItem,
ElScrollbar,
@@ -63,30 +49,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,
@@ -95,60 +72,7 @@ const components = [
ElAvatar,
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
ElCollapseItem
];
export function useElementPlus(app: App) {
@@ -160,8 +84,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);
}

View File

@@ -3,23 +3,9 @@ export default {
hslogin: "Login",
hssysManagement: "System Manage",
hsBaseinfo: "Base Info",
hsDict: "Dict Manage",
hseditor: "Editor",
hserror: "Error Page",
hsfourZeroFour: "404",
hsfourZeroOne: "401",
hscomponents: "Components",
hsvideo: "Video Components",
hsmap: "Map Components",
hsdraggable: "Draggable Components",
hssplitPane: "Split Pane",
hsbutton: "Button Components",
hscropping: "Picture Cropping",
hscountTo: "Digital Animation",
hsselector: "Selector Components",
hsflowChart: "Flow Chart",
hsseamless: "Seamless Scroll",
hscontextmenu: "Context Menu",
hsmenus: "MultiLevel Menu",
hsmenu1: "Menu1",
"hsmenu1-1": "Menu1-1",
@@ -31,8 +17,5 @@ export default {
permission: "Permission Manage",
permissionPage: "Page Permission",
permissionButton: "Button Permission",
hstabs: "Tabs Operate",
hsMenuTree: "Menu Tree",
hsguide: "Guide",
externalLink: "External Link"
};

View File

@@ -1,6 +1,6 @@
// 多组件库的国际化和本地项目国际化兼容
import { App } from "vue";
import { set } from "lodash-es";
import { set } from "lodash-unified";
import { createI18n } from "vue-i18n";
import { localesConfigs } from "./config";
import { storageLocal } from "/@/utils/storage";
@@ -11,7 +11,10 @@ import { storageLocal } from "/@/utils/storage";
* @param isI18n 如果true,获取对应的消息,否则返回本身
* @returns message
*/
export function transformI18n(message: string | object = "", isI18n = false) {
export function transformI18n(
message: string | unknown | object = "",
isI18n: boolean | unknown = false
) {
if (!message) {
return "";
}

View File

@@ -3,23 +3,9 @@ export default {
hslogin: "登陆",
hssysManagement: "系统管理",
hsBaseinfo: "基础信息",
hsDict: "字典管理",
hseditor: "编辑器",
hserror: "错误页面",
hsfourZeroFour: "404",
hsfourZeroOne: "401",
hscomponents: "组件",
hsvideo: "视频组件",
hsmap: "地图组件",
hsdraggable: "拖拽组件",
hssplitPane: "切割面板",
hsbutton: "按钮组件",
hscropping: "图片裁剪",
hscountTo: "数字动画",
hsselector: "选择器组件",
hsflowChart: "流程图",
hsseamless: "无缝滚动",
hscontextmenu: "右键菜单",
hsmenus: "多级菜单",
hsmenu1: "菜单1",
"hsmenu1-1": "菜单1-1",
@@ -31,8 +17,5 @@ export default {
permission: "权限管理",
permissionPage: "页面权限",
permissionButton: "按钮权限",
hstabs: "标签页操作",
hsMenuTree: "菜单树结构",
hsguide: "引导页",
externalLink: "外链"
};

View File

@@ -1,11 +1,13 @@
import { isUrl } from "/@/utils/is";
import { toRouteType } from "./types";
import { openLink } from "/@/utils/link";
import NProgress from "/@/utils/progress";
import { constantRoutes } from "./modules";
import { split, findIndex } from "lodash-es";
import { findIndex } from "lodash-unified";
import { transformI18n } from "/@/plugins/i18n";
import remainingRouter from "./modules/remaining";
import { storageSession } from "/@/utils/storage";
import { Title } from "../../public/serverConfig.json";
import { useMultiTagsStoreHook } from "/@/store/modules/multiTags";
import { usePermissionStoreHook } from "/@/store/modules/permission";
import { Router, RouteMeta, createRouter, RouteRecordName } from "vue-router";
@@ -51,21 +53,22 @@ router.beforeEach((to: toRouteType, _from, next) => {
}
const name = storageSession.getItem("info");
NProgress.start();
const externalLink = to?.redirectedFrom?.fullPath;
const externalLink = isUrl(to?.name);
if (!externalLink)
to.matched.some(item => {
item.meta.title
? (document.title = transformI18n(
item.meta.title as string,
item.meta?.i18n as boolean
))
: "";
if (!item.meta.title) return "";
if (Title)
document.title = `${transformI18n(
item.meta.title,
item.meta?.i18n
)} | ${Title}`;
else document.title = transformI18n(item.meta.title, item.meta?.i18n);
});
if (name) {
if (_from?.name) {
// 如果路由包含http 则是超链接 反之是普通路由
if (externalLink && externalLink.includes("http")) {
openLink(`http${split(externalLink, "http")[1]}`);
// name为超链接
if (externalLink) {
openLink(to?.name);
NProgress.done();
} else {
next();

View File

@@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const errorRouter = {
path: "/error",
@@ -7,9 +7,8 @@ const errorRouter = {
component: Layout,
redirect: "/error/401",
meta: {
icon: "Position",
icon: "position",
title: $t("menus.hserror"),
showLink: true,
i18n: true,
rank: 7
},
@@ -20,8 +19,7 @@ const errorRouter = {
component: () => import("/@/views/error/401.vue"),
meta: {
title: $t("menus.hsfourZeroOne"),
i18n: true,
showLink: true
i18n: true
}
},
{
@@ -30,8 +28,7 @@ const errorRouter = {
component: () => import("/@/views/error/404.vue"),
meta: {
title: $t("menus.hsfourZeroFour"),
i18n: true,
showLink: true
i18n: true
}
}
]

View File

@@ -1,25 +1,22 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const externalLink = {
path: "/external",
name: "external",
path: "/externals",
component: Layout,
meta: {
icon: "Link",
icon: "link",
title: $t("menus.externalLink"),
showLink: true,
i18n: true,
rank: 190
},
children: [
{
path: "https://github.com/xiaoxian521/vue-pure-admin",
path: "/external",
name: "https://pure-admin-doc.vercel.app",
meta: {
title: $t("menus.externalLink"),
showLink: true,
i18n: true,
rank: 191
i18n: true
}
}
]

View File

@@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const homeRouter = {
path: "/",
@@ -7,9 +7,8 @@ const homeRouter = {
component: Layout,
redirect: "/welcome",
meta: {
icon: "HomeFilled",
icon: "home-filled",
title: $t("menus.hshome"),
showLink: true,
i18n: true,
rank: 0
},
@@ -20,8 +19,7 @@ const homeRouter = {
component: () => import("/@/views/welcome.vue"),
meta: {
title: $t("menus.hshome"),
i18n: true,
showLink: true
i18n: true
}
}
]

View File

@@ -10,16 +10,22 @@ import {
formatTwoStageRoutes,
formatFlatteningRoutes
} from "../utils";
import { buildHierarchyTree } from "/@/utils/tree";
// 原始静态路由(未做任何处理)
const routes = [homeRouter, errorRouter, externalLink];
// 导出处理后的静态路由(三级及以上的路由全部拍成二级)
export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes(
formatFlatteningRoutes(ascending(routes))
formatFlatteningRoutes(buildHierarchyTree(ascending(routes)))
);
// 用于渲染菜单,保持原始层级
export const constantMenus: Array<RouteComponent> = ascending(routes).concat(
...remainingRouter
);
// 不参与菜单的路由
export const remainingPaths = Object.keys(remainingRouter).map(v => {
return remainingRouter[v].path;
});

View File

@@ -1,5 +1,5 @@
import { $t } from "/@/plugins/i18n";
import Layout from "/@/layout/index.vue";
const Layout = () => import("/@/layout/index.vue");
const remainingRouter = [
{
@@ -18,7 +18,7 @@ const remainingRouter = [
name: "redirect",
component: Layout,
meta: {
icon: "HomeFilled",
icon: "home-filled",
title: $t("menus.hshome"),
i18n: true,
showLink: false,

View File

@@ -8,10 +8,12 @@ import {
} from "vue-router";
import { router } from "./index";
import { loadEnv } from "../../build";
import Layout from "/@/layout/index.vue";
import { useTimeoutFn } from "@vueuse/core";
import { RouteConfigs } from "/@/layout/types";
import { buildHierarchyTree } from "/@/utils/tree";
import { usePermissionStoreHook } from "/@/store/modules/permission";
const Layout = () => import("/@/layout/index.vue");
const IFrame = () => import("/@/layout/frameView.vue");
// https://cn.vitejs.dev/guide/features.html#glob-import
const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}");
@@ -19,37 +21,37 @@ const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}");
import { getAsyncRoutes } from "/@/api/routes";
// 按照路由中meta下的rank等级升序来排序路由
const ascending = (arr: any[]) => {
function ascending(arr: any[]) {
return arr.sort(
(a: { meta: { rank: number } }, b: { meta: { rank: number } }) => {
return a?.meta?.rank - b?.meta?.rank;
}
);
};
}
// 过滤meta中showLink为false的路由
const filterTree = (data: RouteComponent[]) => {
function filterTree(data: RouteComponent[]) {
const newTree = data.filter(
(v: { meta: { showLink: boolean } }) => v.meta.showLink
(v: { meta: { showLink: boolean } }) => v.meta?.showLink !== false
);
newTree.forEach(
(v: { children }) => v.children && (v.children = filterTree(v.children))
);
return newTree;
};
}
// 批量删除缓存路由(keepalive)
const delAliveRoutes = (delAliveRouteList: Array<RouteConfigs>) => {
function delAliveRoutes(delAliveRouteList: Array<RouteConfigs>) {
delAliveRouteList.forEach(route => {
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: route?.name
});
});
};
}
// 通过path获取父级路径
const getParentPaths = (path: string, routes: RouteRecordRaw[]) => {
function getParentPaths(path: string, routes: RouteRecordRaw[]) {
// 深度遍历查找
function dfs(routes: RouteRecordRaw[], path: string, parents: string[]) {
for (let i = 0; i < routes.length; i++) {
@@ -70,10 +72,10 @@ const getParentPaths = (path: string, routes: RouteRecordRaw[]) => {
}
return dfs(routes, path, []);
};
}
// 查找对应path的路由信息
const findRouteByPath = (path: string, routes: RouteRecordRaw[]) => {
function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
let res = routes.find((item: { path: string }) => item.path == path);
if (res) {
return res;
@@ -91,20 +93,20 @@ const findRouteByPath = (path: string, routes: RouteRecordRaw[]) => {
}
return null;
}
};
}
// 重置路由
const resetRouter = (): void => {
function resetRouter(): void {
router.getRoutes().forEach(route => {
const { name } = route;
if (name) {
router.hasRoute(name) && router.removeRoute(name);
}
});
};
}
// 初始化路由
const initRouter = (name: string) => {
function initRouter(name: string) {
return new Promise(resolve => {
getAsyncRoutes({ name }).then(({ info }) => {
if (info.length === 0) {
@@ -125,6 +127,10 @@ const initRouter = (name: string) => {
// 最终路由进行升序
ascending(router.options.routes[0].children);
if (!router.hasRoute(v?.name)) router.addRoute(v);
const flattenRouters = router
.getRoutes()
.find(n => n.path === "/");
router.addRoute(flattenRouters);
}
resolve(router);
}
@@ -137,24 +143,25 @@ const initRouter = (name: string) => {
});
});
});
};
}
/**
* 将多级嵌套路由处理成一维数组
* @param routesList 传入路由
* @returns 返回处理后的一维路由
*/
const formatFlatteningRoutes = (routesList: RouteRecordRaw[]) => {
if (routesList.length <= 0) return routesList;
for (let i = 0; i < routesList.length; i++) {
if (routesList[i].children) {
routesList = routesList
function formatFlatteningRoutes(routesList: RouteRecordRaw[]) {
if (routesList.length === 0) return routesList;
let hierarchyList = buildHierarchyTree(routesList);
for (let i = 0; i < hierarchyList.length; i++) {
if (hierarchyList[i].children) {
hierarchyList = hierarchyList
.slice(0, i + 1)
.concat(routesList[i].children, routesList.slice(i + 1));
.concat(hierarchyList[i].children, hierarchyList.slice(i + 1));
}
}
return routesList;
};
return hierarchyList;
}
/**
* 一维数组处理成多级嵌套数组三级及以上的路由全部拍成二级keep-alive 只支持到二级缓存)
@@ -162,8 +169,8 @@ const formatFlatteningRoutes = (routesList: RouteRecordRaw[]) => {
* @param routesList 处理后的一维路由菜单数组
* @returns 返回将一维数组重新处理成规定路由的格式
*/
const formatTwoStageRoutes = (routesList: RouteRecordRaw[]) => {
if (routesList.length <= 0) return routesList;
function formatTwoStageRoutes(routesList: RouteRecordRaw[]) {
if (routesList.length === 0) return routesList;
const newRoutesList: RouteRecordRaw[] = [];
routesList.forEach((v: RouteRecordRaw) => {
if (v.path === "/") {
@@ -180,10 +187,10 @@ const formatTwoStageRoutes = (routesList: RouteRecordRaw[]) => {
}
});
return newRoutesList;
};
}
// 处理缓存路由(添加、删除、刷新)
const handleAliveRoute = (matched: RouteRecordNormalized[], mode?: string) => {
function handleAliveRoute(matched: RouteRecordNormalized[], mode?: string) {
switch (mode) {
case "add":
matched.forEach(v => {
@@ -207,15 +214,17 @@ const handleAliveRoute = (matched: RouteRecordNormalized[], mode?: string) => {
});
}, 100);
}
};
}
// 过滤后端传来的动态路由 重新生成规范路由
const addAsyncRoutes = (arrRoutes: Array<RouteRecordRaw>) => {
function addAsyncRoutes(arrRoutes: Array<RouteRecordRaw>) {
if (!arrRoutes || !arrRoutes.length) return;
const modulesRoutesKeys = Object.keys(modulesRoutes);
arrRoutes.forEach((v: RouteRecordRaw) => {
if (v.redirect) {
v.component = Layout;
} else if (v.meta?.frameSrc) {
v.component = IFrame;
} else {
const index = modulesRoutesKeys.findIndex(ev => ev.includes(v.path));
v.component = modulesRoutes[modulesRoutesKeys[index]];
@@ -225,10 +234,10 @@ const addAsyncRoutes = (arrRoutes: Array<RouteRecordRaw>) => {
}
});
return arrRoutes;
};
}
// 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html
const getHistoryMode = (): RouterHistory => {
function getHistoryMode(): RouterHistory {
const routerHistory = loadEnv().VITE_ROUTER_HISTORY;
// len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1
const historyMode = routerHistory.split(",");
@@ -249,10 +258,10 @@ const getHistoryMode = (): RouterHistory => {
return createWebHistory(rightMode);
}
}
};
}
// 是否有权限
const hasPermissions = (value: Array<string>): boolean => {
function hasPermissions(value: Array<string>): boolean {
if (value && value instanceof Array && value.length > 0) {
const roles = usePermissionStoreHook().buttonAuth;
const permissionRoles = value;
@@ -268,7 +277,7 @@ const hasPermissions = (value: Array<string>): boolean => {
} else {
return false;
}
};
}
export {
ascending,

View File

@@ -8,16 +8,29 @@ export const useEpThemeStore = defineStore({
state: () => ({
epThemeColor:
storageLocal.getItem("responsive-layout")?.epThemeColor ??
getConfig().EpThemeColor
getConfig().EpThemeColor,
epTheme:
storageLocal.getItem("responsive-layout")?.theme ?? getConfig().Theme
}),
getters: {
getEpThemeColor() {
return this.epThemeColor;
},
// 用于mix导航模式下hamburger-svg的fill属性
fill() {
if (this.epTheme === "light") {
return "#409eff";
} else if (this.epTheme === "yellow") {
return "#d25f00";
} else {
return "#fff";
}
}
},
actions: {
setEpThemeColor(newColor) {
const layout = storageLocal.getItem("responsive-layout");
this.epTheme = layout?.theme;
this.epThemeColor = newColor;
layout.epThemeColor = newColor;
storageLocal.setItem("responsive-layout", layout);

View File

@@ -1,6 +1,7 @@
import { defineStore } from "pinia";
import { store } from "/@/store";
import { isEqual } from "lodash-es";
import { isUrl } from "/@/utils/is";
import { isEqual } from "lodash-unified";
import { storageLocal } from "/@/utils/storage";
import { multiType, positionType } from "./types";
@@ -16,9 +17,8 @@ export const useMultiTagsStore = defineStore({
parentPath: "/",
meta: {
title: "menus.hshome",
icon: "HomeFilled",
i18n: true,
showLink: true
icon: "home-filled",
i18n: true
}
}
],
@@ -55,6 +55,7 @@ export const useMultiTagsStore = defineStore({
case "push":
{
const tagVal = value as multiType;
if (isUrl(tagVal?.name)) return;
const tagPath = tagVal?.path;
// 判断tag是否已存在
const tagHasExits = this.multiTags.some(tag => {

View File

@@ -1,7 +1,7 @@
import { defineStore } from "pinia";
import { store } from "/@/store";
import { cacheType } from "./types";
import { cloneDeep } from "lodash-es";
import { cloneDeep } from "lodash-unified";
import { RouteConfigs } from "/@/layout/types";
import { constantMenus } from "/@/router/modules";
import { ascending, filterTree } from "/@/router/utils";

View File

@@ -59,9 +59,8 @@ export const useUserStore = defineStore({
parentPath: "/",
meta: {
title: "menus.hshome",
icon: "HomeFilled",
i18n: true,
showLink: true
icon: "home-filled",
i18n: true
}
}
]);

View File

@@ -18,7 +18,7 @@
}
.el-overlay {
background-color: rgba(0, 0, 0, 0.05) !important;
background-color: rgb(0 0 0 / 5%) !important;
}
.el-drawer {

View File

@@ -25,7 +25,7 @@
}
.el-dropdown-menu {
padding: 2px 0 2px 0 !important;
padding: 2px 0 !important;
}
.el-range-separator {
@@ -38,14 +38,16 @@
/* 动态改变cssvar 用于主题切换 https://github.com/element-plus/element-plus/issues/4856#issuecomment-1000174357 */
.el-button--primary {
--el-button-bg-color: var(--el-color-primary) !important;
--el-button-border-color: var(--el-color-primary) !important;
--el-button-hover-bg-color: var(--el-color-primary-light-2) !important;
--el-button-hover-border-color: var(--el-color-primary-light-2) !important;
--el-button-active-bg-color: var(--el-color-primary-active) !important;
--el-button-active-border-color: var(--el-color-primary-active) !important;
}
/* button--primary plain */
.el-button--primary.is-plain {
--el-button-active-bg-color: var(--el-color-primary) !important;
--el-button-active-border-color: var(--el-color-primary) !important;
}
/* nprogress适配ep的primary */
#nprogress {
& .bar {

View File

@@ -11,9 +11,9 @@ body {
padding: 0;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB,
Microsoft YaHei, Arial, sans-serif;
text-rendering: optimizelegibility;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
}
html {
@@ -22,6 +22,11 @@ html {
box-sizing: border-box;
}
#app {
width: 100%;
height: 100%;
}
label {
font-weight: 700;
}
@@ -74,19 +79,11 @@ ul {
/* 灰色模式 */
.html-grey {
filter: grayscale(100%);
-webkit-filter: grayscale(100%);
-moz-filter: grayscale(100%);
-ms-filter: grayscale(100%);
-o-filter: grayscale(100%);
}
/* 色弱模式 */
.html-weakness {
filter: invert(80%);
-webkit-filter: invert(80%);
-moz-filter: invert(80%);
-ms-filter: invert(80%);
-o-filter: invert(80%);
}
.pc-spacing {

View File

@@ -85,7 +85,7 @@
align-items: center;
}
.icon i {
.icon svg {
color: #d9d9d9;
transition: 0.5s;
}
@@ -107,7 +107,7 @@
padding: 0;
}
.input-group.focus .icon i {
.input-group.focus .icon svg {
color: #5392f0;
}

View File

@@ -23,16 +23,6 @@
margin-left: $sideBarWidth;
position: relative;
background: #f0f2f5;
@media screen and (min-width: 150px) and (max-width: 420px) {
.app-main .el-scrollbar__view:first-child {
overflow-y: hidden;
}
}
@media screen and (min-width: 420px) {
.app-main .el-scrollbar__view:first-child {
overflow: hidden;
}
}
}
.fixed-header {
@@ -306,6 +296,10 @@
height: 48px;
line-height: 48px;
background: $menuBg;
svg {
position: static !important;
}
}
.is-active > .el-sub-menu__title,
@@ -498,9 +492,7 @@
background-color: $menuActiveBefore;
content: "";
clear: both;
-webkit-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
-webkit-transform: translateY(0);
transform: translateY(0);
}
@@ -513,9 +505,7 @@
background-color: $menuActiveBefore;
content: "";
clear: both;
-webkit-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
-webkit-transform: translateY(0);
transform: translateY(0);
}
@@ -526,7 +516,6 @@
position: absolute;
height: 0;
width: 3px;
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
top: 50%;
}
@@ -600,16 +589,6 @@ body[layout="vertical"] {
}
}
.el-sub-menu {
overflow: hidden;
& > .el-sub-menu__title {
.el-sub-menu__icon-arrow {
display: none;
}
}
}
/* 菜单折叠 */
.el-menu--collapse {
.el-sub-menu {
@@ -649,3 +628,64 @@ body[layout="horizontal"] {
transition: none !important;
}
}
body[layout="mix"] {
$sideBarWidth: 210px;
@include merge-style($sideBarWidth);
.el-menu {
--el-menu-hover-bg-color: transparent !important;
}
.hideSidebar {
.fixed-header {
width: calc(100% - 54px);
transition: width 0.28s;
}
.sidebar-container {
width: 54px !important;
}
.main-container {
margin-left: 54px;
}
.submenu-title-noDropdown {
padding: 0 !important;
position: relative;
.el-tooltip {
padding: 0 !important;
}
}
/* 菜单折叠 */
.el-menu--collapse {
.el-sub-menu {
& > .el-sub-menu__title {
& > span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
/* 无子菜单 */
.el-menu-item [class^="el-icon"] {
right: 5px;
}
.el-sub-menu__title [class^="el-icon"] {
right: 2px;
}
.submenu-title-noDropdown {
background: transparent !important;
}
}
}
}

View File

@@ -1,21 +0,0 @@
interface ProxyAlgorithm {
increaseIndexes<T>(val: Array<T>): Array<T>;
}
class algorithmProxy implements ProxyAlgorithm {
constructor() {}
// 数组每一项添加索引字段
public increaseIndexes<T>(val: Array<T>): Array<T> {
return Object.keys(val)
.map(v => {
return {
...val[v],
key: v
};
})
.filter(v => v.meta && v.meta.showLink);
}
}
export const algorithm = new algorithmProxy();

View File

@@ -3,7 +3,7 @@ import {
resultType,
PureHttpError,
RequestMethods,
PureHttpResoponse,
PureHttpResponse,
PureHttpRequestConfig
} from "./types.d";
import qs from "qs";
@@ -91,7 +91,7 @@ class PureHttp {
private httpInterceptorsResponse(): void {
const instance = PureHttp.axiosInstance;
instance.interceptors.response.use(
(response: PureHttpResoponse) => {
(response: PureHttpResponse) => {
const $config = response.config;
// 关闭进度条动画
NProgress.done();
@@ -145,21 +145,21 @@ class PureHttp {
}
// 单独抽离的post工具函数
public post<T>(
public post<T, P>(
url: string,
params?: T,
config?: PureHttpRequestConfig
): Promise<T> {
return this.request<T>("post", url, params, config);
): Promise<P> {
return this.request<P>("post", url, params, config);
}
// 单独抽离的get工具函数
public get<T>(
public get<T, P>(
url: string,
params?: T,
config?: PureHttpRequestConfig
): Promise<T> {
return this.request<T>("get", url, params, config);
): Promise<P> {
return this.request<P>("get", url, params, config);
}
}

View File

@@ -18,13 +18,13 @@ export interface PureHttpError extends AxiosError {
isCancelRequest?: boolean;
}
export interface PureHttpResoponse extends AxiosResponse {
export interface PureHttpResponse extends AxiosResponse {
config: PureHttpRequestConfig;
}
export interface PureHttpRequestConfig extends AxiosRequestConfig {
beforeRequestCallback?: (request: PureHttpRequestConfig) => void;
beforeResponseCallback?: (response: PureHttpResoponse) => void;
beforeResponseCallback?: (response: PureHttpResponse) => void;
}
export default class PureHttp {
@@ -34,6 +34,14 @@ export default class PureHttp {
param?: AxiosRequestConfig,
axiosConfig?: PureHttpRequestConfig
): Promise<T>;
post<T>(url: string, params?: T, config?: PureHttpRequestConfig): Promise<T>;
get<T>(url: string, params?: T, config?: PureHttpRequestConfig): Promise<T>;
post<T, P>(
url: string,
params?: T,
config?: PureHttpRequestConfig
): Promise<P>;
get<T, P>(
url: string,
params?: T,
config?: PureHttpRequestConfig
): Promise<P>;
}

View File

@@ -94,8 +94,9 @@ export const isServer = typeof window === "undefined";
export const isClient = !isServer;
export function isUrl(path: string): boolean {
export function isUrl<T>(path: T): boolean {
const reg =
/(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
// @ts-expect-error
return reg.test(path);
}

View File

@@ -1,5 +1,6 @@
export const openLink = (link: string) => {
export const openLink = <T>(link: T): void => {
const $a: HTMLElement = document.createElement("a");
// @ts-expect-error
$a.setAttribute("href", link);
$a.setAttribute("target", "_blank");
$a.setAttribute("rel", "noreferrer noopener");

View File

@@ -1,3 +1,5 @@
import type { FunctionArgs } from "@vueuse/core";
export const hasClass = (ele: RefType<any>, cls: string): any => {
return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)"));
};
@@ -40,3 +42,16 @@ export const toggleClass = (
className = className.replace(clsName, "");
targetEl.className = flag ? `${className} ${clsName} ` : className;
};
export function useRafThrottle<T extends FunctionArgs>(fn: T): T {
let locked = false;
// @ts-ignore
return function (...args) {
if (locked) return;
locked = true;
window.requestAnimationFrame(() => {
fn.apply(this, args);
locked = false;
});
};
}

226
src/utils/print.ts Normal file
View File

@@ -0,0 +1,226 @@
interface PrintFunction {
extendOptions: Function;
getStyle: Function;
setDomHeight: Function;
toPrint: Function;
}
const Print = function (dom, options?: object): PrintFunction {
options = options || {};
// @ts-expect-error
if (!(this instanceof Print)) return new Print(dom, options);
this.conf = {
styleStr: "",
// Elements that need to dynamically get and set the height
setDomHeightArr: [],
// Echart dom List
echartDomArr: [],
// Callback before printing
printBeforeFn: null,
// Callback after printing
printDoneCallBack: null
};
for (const key in this.conf) {
// eslint-disable-next-line no-prototype-builtins
if (key && options.hasOwnProperty(key)) {
this.conf[key] = options[key];
}
}
if (typeof dom === "string") {
this.dom = document.querySelector(dom);
} else {
this.dom = this.isDOM(dom) ? dom : dom.$el;
}
if (this.conf.setDomHeightArr && this.conf.setDomHeightArr.length) {
this.setDomHeight(this.conf.setDomHeightArr);
}
this.init();
};
Print.prototype = {
/**
* init
*/
init: function (): void {
const content = this.getStyle() + this.getHtml();
this.writeIframe(content);
},
/**
* Configuration property extension
* @param {Object} obj
* @param {Object} obj2
*/
extendOptions: function <T>(obj, obj2: T): T {
for (const k in obj2) {
obj[k] = obj2[k];
}
return obj;
},
/**
Copy all styles of the original page
*/
getStyle: function (): string {
let str = "";
const styles: NodeListOf<Element> = document.querySelectorAll("style,link");
for (let i = 0; i < styles.length; i++) {
str += styles[i].outerHTML;
}
str += `<style>.no-print{display:none;}${this.conf.styleStr}</style>`;
return str;
},
// form assignment
getHtml: function (): Element {
const inputs = document.querySelectorAll("input");
const selects = document.querySelectorAll("select");
const textareas = document.querySelectorAll("textarea");
for (let k = 0; k < inputs.length; k++) {
if (inputs[k].type == "checkbox" || inputs[k].type == "radio") {
if (inputs[k].checked == true) {
inputs[k].setAttribute("checked", "checked");
} else {
inputs[k].removeAttribute("checked");
}
} else if (inputs[k].type == "text") {
inputs[k].setAttribute("value", inputs[k].value);
} else {
inputs[k].setAttribute("value", inputs[k].value);
}
}
for (let k2 = 0; k2 < textareas.length; k2++) {
if (textareas[k2].type == "textarea") {
textareas[k2].innerHTML = textareas[k2].value;
}
}
for (let k3 = 0; k3 < selects.length; k3++) {
if (selects[k3].type == "select-one") {
const child = selects[k3].children;
for (const i in child) {
if (child[i].tagName == "OPTION") {
// @ts-ignore
if (child[i].selected == true) {
child[i].setAttribute("selected", "selected");
} else {
child[i].removeAttribute("selected");
}
}
}
}
}
return this.dom.outerHTML;
},
/**
create iframe
*/
writeIframe: function (content) {
let w: Document | Window;
let doc: Document;
const iframe: HTMLIFrameElement = document.createElement("iframe");
const f: HTMLIFrameElement = document.body.appendChild(iframe);
iframe.id = "myIframe";
iframe.setAttribute(
"style",
"position:absolute;width:0;height:0;top:-10px;left:-10px;"
);
// eslint-disable-next-line prefer-const
w = f.contentWindow || f.contentDocument;
// eslint-disable-next-line prefer-const
doc = f.contentDocument || f.contentWindow.document;
doc.open();
doc.write(content);
doc.close();
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
iframe.onload = function (): void {
// Before popping, callback
if (_this.conf.printBeforeFn) {
_this.conf.printBeforeFn({ doc });
}
_this.drawEchartImg(doc).then(() => {
_this.toPrint(w);
setTimeout(function () {
document.body.removeChild(iframe);
// After popup, callback
if (_this.conf.printDoneCallBack) {
_this.conf.printDoneCallBack();
}
}, 100);
});
};
},
/**
* echarts printing
* @param {Object} doc iframe window
*/
drawEchartImg(doc): Promise<void> {
return new Promise<void>(resolve => {
if (this.conf.echartDomArr && this.conf.echartDomArr.length > 0) {
this.conf.echartDomArr.forEach(e => {
const dom = doc.querySelector("#" + e.$el.id);
const img = new Image();
const w = dom.offsetWidth + "px";
const H = dom.offsetHeight + "px";
img.style.width = w;
img.style.height = H;
img.src = e.imgSrc;
dom.innerHTML = "";
dom.appendChild(img);
});
}
resolve();
});
},
/**
Print
*/
toPrint: function (frameWindow): void {
try {
setTimeout(function () {
frameWindow.focus();
try {
if (!frameWindow.document.execCommand("print", false, null)) {
frameWindow.print();
}
} catch (e) {
frameWindow.print();
}
frameWindow.close();
}, 10);
} catch (err) {
console.error(err);
}
},
isDOM:
typeof HTMLElement === "object"
? function (obj) {
return obj instanceof HTMLElement;
}
: function (obj) {
return (
obj &&
typeof obj === "object" &&
obj.nodeType === 1 &&
typeof obj.nodeName === "string"
);
},
/**
* Set the height of the specified dom element by getting the existing height of the dom element and setting
* @param {Array} arr
*/
setDomHeight(arr) {
if (arr && arr.length) {
arr.forEach(name => {
const domArr = document.querySelectorAll(name);
domArr.forEach(dom => {
dom.style.height = dom.offsetHeight + "px";
});
});
}
}
};
export default Print;

View File

@@ -20,7 +20,7 @@ export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
theme: config.Theme ?? "default",
darkMode: config.DarkMode ?? false,
sidebarStatus: config.SidebarStatus ?? true,
epThemeColor: config.EpThemeColor ?? "409EFF"
epThemeColor: config.EpThemeColor ?? "#409EFF"
}
},
configure: {
@@ -47,8 +47,7 @@ export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
meta: {
title: "menus.hshome",
i18n: true,
icon: "HomeFilled",
showLink: true
icon: "home-filled"
}
}
]

67
src/utils/tree.ts Normal file
View File

@@ -0,0 +1,67 @@
/**
* 提取菜单树中的每一项uniqueId
* @param {Array} {menuTree 菜单树}
* @param {return}} expandedPaths 每一项uniqueId组成的数组
*/
const expandedPaths = [];
export function extractPathList(menuTree) {
if (!Array.isArray(menuTree)) {
console.warn("menuTree must be an array");
return;
}
if (!menuTree || menuTree.length === 0) return;
for (const node of menuTree) {
const hasChildren = node.children && node.children.length > 0;
if (hasChildren) {
extractPathList(node.children);
}
expandedPaths.push(node.uniqueId);
}
return expandedPaths;
}
/**
* 如果父级下children的length为1删除children并自动组建唯一uniqueId
* @param {Array} {menuTree 菜单树}
* @param {Array} {pathList 每一项的id组成的数组}
* @param {return}}
*/
export function deleteChildren(menuTree, pathList = []) {
if (!Array.isArray(menuTree)) {
console.warn("menuTree must be an array");
return;
}
if (!menuTree || menuTree.length === 0) return;
for (const [key, node] of menuTree.entries()) {
if (node.children && node.children.length === 1) delete node.children;
node.id = key;
node.parentId = pathList.length ? pathList[pathList.length - 1] : null;
node.pathList = [...pathList, node.id];
node.uniqueId =
node.pathList.length > 1 ? node.pathList.join("-") : node.pathList[0];
const hasChildren = node.children && node.children.length > 0;
if (hasChildren) {
deleteChildren(node.children, node.pathList);
}
}
return menuTree;
}
// 创建层级关系
export function buildHierarchyTree(menuTree, pathList = []) {
if (!Array.isArray(menuTree)) {
console.warn("menuTree must be an array");
return;
}
if (!menuTree || menuTree.length === 0) return;
for (const [key, node] of menuTree.entries()) {
node.id = key;
node.parentId = pathList.length ? pathList[pathList.length - 1] : null;
node.pathList = [...pathList, node.id];
const hasChildren = node.children && node.children.length > 0;
if (hasChildren) {
buildHierarchyTree(node.children, node.pathList);
}
}
return menuTree;
}

116
src/utils/watermark.ts Normal file
View File

@@ -0,0 +1,116 @@
import {
ref,
Ref,
unref,
shallowRef,
onBeforeUnmount,
getCurrentInstance
} from "vue";
import { isDef } from "/@/utils/is";
import { useRafThrottle } from "/@/utils/operate";
import { addResizeListener, removeResizeListener } from "/@/utils/resize";
const domSymbol = Symbol("watermark-dom");
type attr = {
font?: string;
fillStyle?: string;
};
export function useWatermark(
appendEl: Ref<HTMLElement | null> = ref(document.body) as Ref<HTMLElement>
) {
const func = useRafThrottle(function () {
const el = unref(appendEl);
if (!el) return;
const { clientHeight: height, clientWidth: width } = el;
updateWatermark({ height, width });
});
const id = domSymbol.toString();
const watermarkEl = shallowRef<HTMLElement>();
const clear = () => {
const domId = unref(watermarkEl);
watermarkEl.value = undefined;
const el = unref(appendEl);
if (!el) return;
domId && el.removeChild(domId);
removeResizeListener(el, func);
};
function createBase64(str: string, attr?: attr) {
const can = document.createElement("canvas");
const width = 300;
const height = 240;
Object.assign(can, { width, height });
const cans = can.getContext("2d");
if (cans) {
cans.rotate((-20 * Math.PI) / 120);
cans.font = attr?.font ?? "15px Reggae One";
cans.fillStyle = attr?.fillStyle ?? "rgba(0, 0, 0, 0.15)";
cans.textAlign = "left";
cans.textBaseline = "middle";
cans.fillText(str, width / 20, height);
}
return can.toDataURL("image/png");
}
function updateWatermark(
options: {
width?: number;
height?: number;
str?: string;
attr?: attr;
} = {}
) {
const el = unref(watermarkEl);
if (!el) return;
if (isDef(options.width)) {
el.style.width = `${options.width}px`;
}
if (isDef(options.height)) {
el.style.height = `${options.height}px`;
}
if (isDef(options.str)) {
el.style.background = `url(${createBase64(
options.str,
options.attr
)}) left top repeat`;
}
}
const createWatermark = (str: string, attr?: attr) => {
if (unref(watermarkEl)) {
updateWatermark({ str, attr });
return id;
}
const div = document.createElement("div");
watermarkEl.value = div;
div.id = id;
div.style.pointerEvents = "none";
div.style.top = "0px";
div.style.left = "0px";
div.style.position = "absolute";
div.style.zIndex = "100000";
const el = unref(appendEl);
if (!el) return id;
const { clientHeight: height, clientWidth: width } = el;
updateWatermark({ str, width, height, attr });
el.appendChild(div);
return id;
};
function setWatermark(str: string, attr?: attr) {
createWatermark(str, attr);
addResizeListener(document.documentElement, func);
const instance = getCurrentInstance();
if (instance) {
onBeforeUnmount(() => {
clear();
});
}
}
return { setWatermark, clear };
}

View File

@@ -1,43 +1,15 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { ref } from "vue";
import { useRouter } from "vue-router";
import { initRouter } from "/@/router/utils";
import { storageSession } from "/@/utils/storage";
import { addClass, removeClass } from "/@/utils/operate";
import bg from "/@/assets/login/bg.png";
import avatar from "/@/assets/login/avatar.svg";
import illustration0 from "/@/assets/login/illustration0.svg";
import illustration1 from "/@/assets/login/illustration1.svg";
import illustration2 from "/@/assets/login/illustration2.svg";
import illustration3 from "/@/assets/login/illustration3.svg";
import illustration4 from "/@/assets/login/illustration4.svg";
import illustration5 from "/@/assets/login/illustration5.svg";
import illustration6 from "/@/assets/login/illustration6.svg";
import avatar from "/@/assets/login/avatar.svg?component";
import illustration from "/@/assets/login/illustration.svg?component";
const router = useRouter();
// eslint-disable-next-line vue/return-in-computed-property
const currentWeek = computed(() => {
switch (String(new Date().getDay())) {
case "0":
return illustration0;
case "1":
return illustration1;
case "2":
return illustration2;
case "3":
return illustration3;
case "4":
return illustration4;
case "5":
return illustration5;
case "6":
return illustration6;
default:
return illustration4;
}
});
let user = ref("admin");
let pwd = ref("123456");
@@ -73,7 +45,7 @@ function onPwdBlur() {
<img :src="bg" class="wave" />
<div class="login-container">
<div class="img">
<component :is="currentWeek"></component>
<illustration />
</div>
<div class="login-box">
<div class="login-form">
@@ -110,7 +82,7 @@ function onPwdBlur() {
}"
>
<div class="icon">
<i class="fa fa-user"></i>
<IconifyIconOffline icon="fa-user" width="14" height="14" />
</div>
<div>
<h5>用户名</h5>
@@ -139,7 +111,7 @@ function onPwdBlur() {
}"
>
<div class="icon">
<i class="fa fa-lock"></i>
<IconifyIconOffline icon="fa-lock" width="14" height="14" />
</div>
<div>
<h5>密码</h5>

View File

@@ -7,6 +7,7 @@ export default {
<script setup lang="ts">
import { ref, unref } from "vue";
import { storageSession } from "/@/utils/storage";
import { useRenderIcon } from "/@/components/ReIcon/src/hooks";
let purview = ref<string>(storageSession.getItem("info").username);
@@ -36,6 +37,11 @@ function changRole() {
查看左侧菜单变化(系统管理)模拟后台根据不同角色返回对应路由
</p>
</h4>
<el-button type="primary" @click="changRole">切换角色</el-button>
<el-button
type="primary"
@click="changRole"
:icon="useRenderIcon('user', { color: '#fff' })"
>切换角色</el-button
>
</div>
</template>

View File

@@ -1,18 +1,20 @@
module.exports = {
root: true,
plugins: ["stylelint-order"],
customSyntax: "postcss-html",
extends: ["stylelint-config-standard", "stylelint-config-prettier"],
rules: {
"selector-class-pattern": null,
"selector-pseudo-class-no-unknown": [
true,
{
ignorePseudoClasses: ["deep"]
ignorePseudoClasses: ["global"]
}
],
"selector-pseudo-element-no-unknown": [
true,
{
ignorePseudoElements: ["v-deep", ":deep"]
ignorePseudoElements: ["v-deep"]
}
],
"at-rule-no-unknown": [
@@ -65,5 +67,26 @@ module.exports = {
{ severity: "warning" }
]
},
ignoreFiles: ["**/*.js", "**/*.jsx", "**/*.tsx", "**/*.ts", "**/*.json"]
ignoreFiles: ["**/*.js", "**/*.jsx", "**/*.tsx", "**/*.ts", "**/*.json"],
overrides: [
{
files: ["*.vue", "**/*.vue", "*.html", "**/*.html"],
extends: ["stylelint-config-recommended", "stylelint-config-html"],
rules: {
"keyframes-name-pattern": null,
"selector-pseudo-class-no-unknown": [
true,
{
ignorePseudoClasses: ["deep", "global"]
}
],
"selector-pseudo-element-no-unknown": [
true,
{
ignorePseudoElements: ["v-deep", "v-global", "v-slotted"]
}
]
}
}
]
};

10
types/global.d.ts vendored
View File

@@ -6,6 +6,15 @@ import type {
PropType as VuePropType
} from "vue";
// GlobalComponents for Volar
declare module "vue" {
export interface GlobalComponents {
IconifyIconOffline: typeof import("../src/components/ReIcon")["IconifyIconOffline"];
IconifyIconOnline: typeof import("../src/components/ReIcon")["IconifyIconOnline"];
FontIcon: typeof import("../src/components/ReIcon")["FontIcon"];
}
}
declare global {
const __APP_INFO__: {
pkg: {
@@ -23,7 +32,6 @@ declare global {
mozCancelAnimationFrame: (handle: number) => void;
oCancelAnimationFrame: (handle: number) => void;
msCancelAnimationFrame: (handle: number) => void;
webkitRequestAnimationFrame: (callback: FrameRequestCallback) => number;
mozRequestAnimationFrame: (callback: FrameRequestCallback) => number;
oRequestAnimationFrame: (callback: FrameRequestCallback) => number;

View File

@@ -1,13 +1,9 @@
import dayjs from "dayjs";
import { resolve } from "path";
import vue from "@vitejs/plugin-vue";
import svgLoader from "vite-svg-loader";
import legacy from "@vitejs/plugin-legacy";
import vueJsx from "@vitejs/plugin-vue-jsx";
import pkg from "./package.json";
import { warpperEnv, regExps } from "./build";
import { viteMockServe } from "vite-plugin-mock";
import ElementPlus from "unplugin-element-plus/vite";
import { getPluginsList } from "./build/plugins";
import { UserConfigExport, ConfigEnv, loadEnv } from "vite";
import themePreprocessorPlugin from "@zougt/vite-plugin-theme-preprocessor";
// 当前执行node命令时文件夹的地址工作目录
const root: string = process.cwd();
@@ -25,6 +21,12 @@ const alias: Record<string, string> = {
"vue-i18n": "vue-i18n/dist/vue-i18n.cjs.js"
};
const { dependencies, devDependencies, name, version } = pkg;
const __APP_INFO__ = {
pkg: { dependencies, devDependencies, name, version },
lastBuildTime: dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss")
};
export default ({ command, mode }: ConfigEnv): UserConfigExport => {
const {
VITE_PORT,
@@ -33,7 +35,6 @@ export default ({ command, mode }: ConfigEnv): UserConfigExport => {
VITE_PROXY_DOMAIN,
VITE_PROXY_DOMAIN_REAL
} = warpperEnv(loadEnv(mode, root));
const prodMock = true;
return {
base: VITE_PUBLIC_PATH,
root,
@@ -77,93 +78,16 @@ export default ({ command, mode }: ConfigEnv): UserConfigExport => {
}
: null
},
plugins: [
vue(),
// jsx、tsx语法支持
vueJsx(),
// 自定义主题
themePreprocessorPlugin({
scss: {
multipleScopeVars: [
{
scopeName: "layout-theme-default",
path: pathResolve("src/layout/theme/default-vars.scss")
},
{
scopeName: "layout-theme-light",
path: pathResolve("src/layout/theme/light-vars.scss")
},
{
scopeName: "layout-theme-dusk",
path: pathResolve("src/layout/theme/dusk-vars.scss")
},
{
scopeName: "layout-theme-volcano",
path: pathResolve("src/layout/theme/volcano-vars.scss")
},
{
scopeName: "layout-theme-yellow",
path: pathResolve("src/layout/theme/yellow-vars.scss")
},
{
scopeName: "layout-theme-mingQing",
path: pathResolve("src/layout/theme/mingQing-vars.scss")
},
{
scopeName: "layout-theme-auroraGreen",
path: pathResolve("src/layout/theme/auroraGreen-vars.scss")
},
{
scopeName: "layout-theme-pink",
path: pathResolve("src/layout/theme/pink-vars.scss")
},
{
scopeName: "layout-theme-saucePurple",
path: pathResolve("src/layout/theme/saucePurple-vars.scss")
}
],
// 默认取 multipleScopeVars[0].scopeName
defaultScopeName: "",
// 在生产模式是否抽取独立的主题css文件extract为true以下属性有效
extract: true,
// 独立主题css文件的输出路径默认取 viteConfig.build.assetsDir 相对于 (viteConfig.build.outDir)
outputDir: "",
// 会选取defaultScopeName对应的主题css文件在html添加link
themeLinkTagId: "head",
// "head"||"head-prepend" || "body" ||"body-prepend"
themeLinkTagInjectTo: "head",
// 是否对抽取的css文件内对应scopeName的权重类名移除
removeCssScopeName: false,
// 可以自定义css文件名称的函数
customThemeCssFileName: scopeName => scopeName
}
}),
// svg组件化支持
svgLoader(),
ElementPlus({}),
// mock支持
viteMockServe({
mockPath: "mock",
localEnabled: command === "serve",
prodEnabled: command !== "serve" && prodMock,
injectCode: `
import { setupProdMockServer } from './mockProdServer';
setupProdMockServer();
`,
logger: true
}),
// 是否为打包后的文件提供传统浏览器兼容性支持
VITE_LEGACY
? legacy({
targets: ["ie >= 11"],
additionalLegacyPolyfills: ["regenerator-runtime/runtime"]
})
: null
],
plugins: getPluginsList(command, VITE_LEGACY),
optimizeDeps: {
include: [
"element-plus/lib/locale/lang/zh-cn",
"element-plus/lib/locale/lang/en"
"pinia",
"vue-i18n",
"lodash-es",
"@vueuse/core",
"@iconify/vue",
"element-plus/lib/locale/lang/en",
"element-plus/lib/locale/lang/zh-cn"
],
exclude: ["@zougt/vite-plugin-theme-preprocessor/dist/browser-utils"]
},
@@ -174,7 +98,8 @@ export default ({ command, mode }: ConfigEnv): UserConfigExport => {
chunkSizeWarningLimit: 2000
},
define: {
__INTLIFY_PROD_DEVTOOLS__: false
__INTLIFY_PROD_DEVTOOLS__: false,
__APP_INFO__: JSON.stringify(__APP_INFO__)
}
};
};

50
windi.config.ts Normal file
View File

@@ -0,0 +1,50 @@
// https://cn.windicss.org/ 中文文档
import { defineConfig } from "windicss/helpers";
import colors from "windicss/colors";
import typography from "windicss/plugin/typography";
export default defineConfig({
darkMode: "class",
attributify: true,
plugins: [typography()],
theme: {
extend: {
zIndex: {
"-1": "-1"
},
screens: {
sm: "576px",
md: "768px",
lg: "992px",
xl: "1200px",
"2xl": "1600px"
},
typography: {
DEFAULT: {
css: {
maxWidth: "65ch",
color: "inherit",
a: {
color: "inherit",
opacity: 0.75,
fontWeight: "500",
textDecoration: "underline",
"&:hover": {
opacity: 1,
color: colors.teal[600]
}
},
b: { color: "inherit" },
strong: { color: "inherit" },
em: { color: "inherit" },
h1: { color: "inherit" },
h2: { color: "inherit" },
h3: { color: "inherit" },
h4: { color: "inherit" },
code: { color: "inherit" }
}
}
}
}
}
});