Compare commits

..

6 Commits

Author SHA1 Message Date
xiaoxian521
4e95672cb4 release: update 4.1.0 2023-05-12 13:27:40 +08:00
xiaoxian521
9c0872fa6a perf: export addPathMatch utils 2023-03-07 21:02:36 +08:00
xiaoxian521
7678aa64e2 chore: update propTypes 2023-03-01 20:03:47 +08:00
xiaoxian521
1e30b31be2 chore: 同步完整版代码 2023-02-28 22:42:12 +08:00
xiaoxian521
2c887ee1d9 fix: 调整登录页 initRouter方法顺序,防止登录页热更新报错 2023-02-12 18:58:39 +08:00
xiaoxian521
635da1dc52 chore: update link 2023-02-08 20:10:08 +08:00
89 changed files with 4796 additions and 2738 deletions

3
.env
View File

@@ -1,2 +1,5 @@
# 平台本地运行端口号
VITE_PORT = 8848
# 是否隐藏首页 隐藏 true 不隐藏 false 勿删除VITE_HIDE_HOME只需在.env文件配置
VITE_HIDE_HOME = false

View File

@@ -13,4 +13,4 @@ VITE_CDN = true
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION = "both-clear"
VITE_COMPRESSION = "none"

View File

@@ -1,3 +1,4 @@
/dist/*
/public/*
public/*
public/*
src/style/reset.scss

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 啝裳
Copyright (c) 2020-present, pure-admin
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

@@ -1,22 +1,26 @@
<h1>vue-pure-admin Lite Editionno i18n version</h1>
[![license](https://img.shields.io/github/license/xiaoxian521/vue-pure-admin.svg)](LICENSE)
[![license](https://img.shields.io/github/license/pure-admin/vue-pure-admin.svg)](LICENSE)
**English** | [中文](./README.md)
## Introduce
The simplified version is based on the shelf extracted from [vue-pure-admin](https://github.com/xiaoxian521/vue-pure-admin), which contains main functions and is more suitable for actual project development. The packaged size is introduced globally [element-plus](https://element-plus.org) is still below `2.3MB`, and the full version of the code will be permanently synchronized. After enabling `brotli` compression and `cdn` to replace the local library mode, the package size is less than `350kb`
The simplified version is based on the shelf extracted from [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin), which contains main functions and is more suitable for actual project development. The packaged size is introduced globally [element-plus](https://element-plus.org) is still below `2.3MB`, and the full version of the code will be permanently synchronized. After enabling `brotli` compression and `cdn` to replace the local library mode, the package size is less than `350kb`
## Supporting Video
- [Click Watch Tutorial](https://www.bilibili.com/video/BV1534y1S7HV)
- [Click Watch Tutorial](https://www.bilibili.com/video/BV1kg411v7QT)
- [Click Watch UI Design](https://www.bilibili.com/video/BV17g411T7rq)
## Docs
- [Click me to view the domestic documentation site](https://yiming_chang.gitee.io/pure-admin-doc)
- [Click me to view foreign document site](https://xiaoxian521.github.io/pure-admin-doc)
- [Click me to view foreign document site](https://pure-admin.github.io/pure-admin-doc)
## Preview
- [Click me to view the preview station](https://pure-admin-thin.netlify.app/#/login)
## Usage
@@ -40,8 +44,10 @@ bilibili: https://www.bilibili.com/video/BV1534y1S7HV/
## ⚠️ Attention
- The Lite version does not accept any issues and prs. If you have any questions, please go to the full version https://github.com/xiaoxian521/vue-pure-admin/issues/new/choose to mention, thank you! ! !
- The Lite version does not accept any issues and prs. If you have any questions, please go to the full version https://github.com/pure-admin/vue-pure-admin/issues/new/choose to mention, 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!
[MIT © 2020-present, pure-admin](./LICENSE)

View File

@@ -1,26 +1,30 @@
<h1>vue-pure-admin精简版非国际化版本</h1>
[![license](https://img.shields.io/github/license/xiaoxian521/vue-pure-admin.svg)](LICENSE)
[![license](https://img.shields.io/github/license/pure-admin/vue-pure-admin.svg)](LICENSE)
**中文** | [English](./README.en-US.md)
## 介绍
精简版是基于 [vue-pure-admin](https://github.com/xiaoxian521/vue-pure-admin) 提炼出的架子,包含主体功能,更适合实际项目开发,打包后的大小在全局引入 [element-plus](https://element-plus.org) 的情况下仍然低于 `2.3MB`,并且会永久同步完整版的代码。开启 `brotli` 压缩和 `cdn` 替换本地库模式后,打包大小低于 `350kb`
精简版是基于 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin) 提炼出的架子,包含主体功能,更适合实际项目开发,打包后的大小在全局引入 [element-plus](https://element-plus.org) 的情况下仍然低于 `2.3MB`,并且会永久同步完整版的代码。开启 `brotli` 压缩和 `cdn` 替换本地库模式后,打包大小低于 `350kb`
## 版本选择
当前是非国际化版本哦,如果您需要国际化版本 [请点击](https://github.com/xiaoxian521/pure-admin-thin/tree/i18n)
当前是非国际化版本哦,如果您需要国际化版本 [请点击](https://github.com/pure-admin/pure-admin-thin/tree/i18n)
## 配套视频
- [点我查看教程](https://www.bilibili.com/video/BV1534y1S7HV)
- [点我查看教程](https://www.bilibili.com/video/BV1kg411v7QT)
- [点我查看 UI 设计](https://www.bilibili.com/video/BV17g411T7rq)
## 配套文档
- [点我查看国内文档站](https://yiming_chang.gitee.io/pure-admin-doc)
- [点我查看国外文档站](https://xiaoxian521.github.io/pure-admin-doc)
- [点我查看国外文档站](https://pure-admin.github.io/pure-admin-doc)
## 预览
- [点我查看预览站](https://pure-admin-thin.netlify.app/#/login)
## 维护者
@@ -54,10 +58,10 @@ pnpm remove 包名
## ⚠️ 注意
- 精简版不接受任何 `issues``pr`,如果有问题请到完整版 [issues](https://github.com/xiaoxian521/vue-pure-admin/issues/new/choose) 去提,谢谢!!!
- 精简版不接受任何 `issues``pr`,如果有问题请到完整版 [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) 去提,谢谢!!!
## 许可证
原则上不收取任何费用及版权,可以放心使用,不过如需二次开源(比如用此平台二次开发并开源)请联系作者获取许可!
[MIT © xiaoxian521-2020](./LICENSE)
[MIT © 2020-present, pure-admin](./LICENSE)

View File

@@ -6,6 +6,7 @@ const warpperEnv = (envConf: Recordable): ViteEnv => {
VITE_PUBLIC_PATH: "",
VITE_ROUTER_HISTORY: "",
VITE_CDN: false,
VITE_HIDE_HOME: "false",
VITE_COMPRESSION: "none"
};

View File

@@ -1,8 +1,8 @@
import type { Plugin } from "vite";
import dayjs, { Dayjs } from "dayjs";
import utils from "@pureadmin/utils";
import duration from "dayjs/plugin/duration";
import { green, blue, bold } from "picocolors";
import { getPackageSize } from "@pureadmin/utils";
dayjs.extend(duration);
export function viteBuildInfo(): Plugin {
@@ -22,7 +22,7 @@ export function viteBuildInfo(): Plugin {
green(
`👏欢迎使用${blue(
"[vue-pure-admin]"
)}如果您感觉不错记得点击后面链接给个star哦💖 https://github.com/xiaoxian521/vue-pure-admin`
)}如果您感觉不错记得点击后面链接给个star哦💖 https://github.com/pure-admin/vue-pure-admin`
)
)
);
@@ -33,7 +33,7 @@ export function viteBuildInfo(): Plugin {
closeBundle() {
if (config.command === "build") {
endTime = dayjs(new Date());
getPackageSize({
utils.getPackageSize({
folder: outDir,
callback: (size: string) => {
console.log(

View File

@@ -10,8 +10,8 @@ const include = [
"dayjs",
"axios",
"pinia",
"echarts",
"js-cookie",
"sortablejs",
"@vueuse/core",
"@pureadmin/utils",
"responsive-storage",

View File

@@ -9,7 +9,6 @@ import { configCompressPlugin } from "./compress";
import { visualizer } from "rollup-plugin-visualizer";
import removeConsole from "vite-plugin-remove-console";
import themePreprocessorPlugin from "@pureadmin/theme";
import DefineOptions from "unplugin-vue-define-options/vite";
import { genScssMultipleScopeVars } from "../src/layout/theme";
export function getPluginsList(
@@ -25,7 +24,6 @@ export function getPluginsList(
vueJsx(),
VITE_CDN ? cdn : null,
configCompressPlugin(VITE_COMPRESSION),
DefineOptions(),
// 线上环境删除console
removeConsole({ external: ["src/assets/iconfont/iconfont.js"] }),
viteBuildInfo(),

View File

@@ -21,54 +21,54 @@
html,
body,
#app {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
display: flex;
position: relative;
justify-content: center;
align-items: center;
overflow: hidden;
}
.loader,
.loader:before,
.loader:after {
border-radius: 50%;
.loader::before,
.loader::after {
width: 2.5em;
height: 2.5em;
border-radius: 50%;
animation: load-animation 1.8s infinite ease-in-out;
animation-fill-mode: both;
animation: loadAnimation 1.8s infinite ease-in-out;
}
.loader {
color: #406eeb;
font-size: 10px;
margin: 80px auto;
position: relative;
top: 0;
margin: 80px auto;
font-size: 10px;
color: #406eeb;
text-indent: -9999em;
transform: translateZ(0);
animation-delay: -0.16s;
top: 0;
transform: translate(-50%, 0);
animation-delay: -0.16s;
}
.loader:before,
.loader:after {
content: "";
.loader::before,
.loader::after {
position: absolute;
top: 0;
content: "";
}
.loader:before {
.loader::before {
left: -3.5em;
animation-delay: -0.32s;
}
.loader:after {
.loader::after {
left: 3.5em;
}
@keyframes loadAnimation {
@keyframes load-animation {
0%,
80%,
100% {

View File

@@ -1,6 +1,6 @@
{
"name": "pure-admin-thin",
"version": "3.9.7",
"version": "4.1.0",
"private": true,
"scripts": {
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
@@ -15,8 +15,8 @@
"cloc": "NODE_OPTIONS=--max-old-space-size=4096 cloc . --exclude-dir=node_modules --exclude-lang=YAML",
"clean:cache": "rm -rf node_modules && rm -rf .eslintcache && pnpm install",
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{vue,css,scss,postcss,less}\" --cache --cache-location node_modules/.cache/stylelint/",
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --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",
@@ -29,96 +29,107 @@
"not op_mini all"
],
"dependencies": {
"@pureadmin/descriptions": "^1.1.0",
"@pureadmin/table": "^1.9.0",
"@pureadmin/utils": "^1.8.5",
"@vueuse/core": "^9.6.0",
"@pureadmin/descriptions": "^1.1.1",
"@pureadmin/table": "^2.1.0",
"@pureadmin/utils": "^1.8.9",
"@vueuse/core": "^10.1.2",
"@vueuse/motion": "2.0.0-beta.12",
"animate.css": "^4.1.1",
"axios": "^1.2.0",
"dayjs": "^1.11.6",
"echarts": "^5.4.0",
"element-plus": "^2.2.27",
"axios": "^1.4.0",
"dayjs": "^1.11.7",
"echarts": "^5.4.2",
"element-plus": "^2.3.4",
"element-resize-detector": "^1.2.4",
"js-cookie": "^3.0.1",
"js-cookie": "^3.0.5",
"mitt": "^3.0.0",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"path": "^0.12.7",
"pinia": "^2.0.28",
"qs": "^6.11.0",
"responsive-storage": "^2.1.0",
"vue": "^3.2.45",
"pinia": "^2.0.36",
"qs": "^6.11.1",
"responsive-storage": "^2.2.0",
"sortablejs": "^1.15.0",
"vue": "^3.3.1",
"vue-router": "^4.1.6",
"vue-types": "^4.2.1"
"vue-types": "^5.0.2"
},
"devDependencies": {
"@commitlint/cli": "13.1.0",
"@commitlint/config-conventional": "13.1.0",
"@iconify-icons/ep": "^1.2.7",
"@iconify-icons/ri": "^1.2.3",
"@iconify/vue": "^4.0.0",
"@commitlint/cli": "^17.6.3",
"@commitlint/config-conventional": "^17.6.3",
"@iconify-icons/ep": "^1.2.11",
"@iconify-icons/ri": "^1.2.7",
"@iconify/vue": "^4.1.1",
"@pureadmin/theme": "^3.0.0",
"@types/element-resize-detector": "1.1.3",
"@types/js-cookie": "^3.0.1",
"@types/js-cookie": "^3.0.3",
"@types/mockjs": "^1.0.7",
"@types/node": "^18.11.9",
"@types/node": "^18.15.12",
"@types/nprogress": "0.2.0",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.42.1",
"@typescript-eslint/parser": "^5.42.1",
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.2",
"autoprefixer": "^10.4.13",
"cloc": "^2.10.0",
"cssnano": "^5.1.14",
"eslint": "^8.8.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^9.7.0",
"husky": "^7.0.4",
"lint-staged": "11.1.2",
"@types/sortablejs": "^1.15.1",
"@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/parser": "^5.59.5",
"@vitejs/plugin-vue": "^4.2.2",
"@vitejs/plugin-vue-jsx": "^3.0.1",
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.14",
"cloc": "^2.11.0",
"cssnano": "^6.0.1",
"eslint": "^8.40.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.12.0",
"husky": "^8.0.3",
"lint-staged": "^13.2.2",
"picocolors": "^1.0.0",
"postcss": "^8.4.18",
"postcss": "^8.4.23",
"postcss-html": "^1.5.0",
"postcss-import": "^15.0.0",
"postcss-scss": "^4.0.5",
"prettier": "^2.5.1",
"postcss-import": "^15.1.0",
"postcss-scss": "^4.0.6",
"prettier": "^2.8.7",
"pretty-quick": "3.1.1",
"rimraf": "3.0.2",
"rollup-plugin-visualizer": "^5.8.3",
"sass": "^1.56.1",
"sass-loader": "^13.2.0",
"stylelint": "^14.3.0",
"stylelint-config-html": "^1.0.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-recommended": "^9.0.0",
"stylelint-config-standard": "^29.0.0",
"stylelint-order": "^5.0.0",
"rimraf": "^5.0.0",
"rollup-plugin-visualizer": "^5.9.0",
"sass": "^1.62.1",
"sass-loader": "^13.2.2",
"stylelint": "^15.6.1",
"stylelint-config-html": "^1.1.0",
"stylelint-config-recess-order": "^4.0.0",
"stylelint-config-recommended": "^12.0.0",
"stylelint-config-recommended-scss": "^11.0.0",
"stylelint-config-recommended-vue": "^1.4.0",
"stylelint-config-standard": "^33.0.0",
"stylelint-config-standard-scss": "^9.0.0",
"stylelint-order": "^6.0.3",
"stylelint-prettier": "^3.0.0",
"stylelint-scss": "^5.0.0",
"svgo": "^3.0.2",
"tailwindcss": "^3.2.4",
"terser": "^5.16.1",
"typescript": "^4.9.3",
"unplugin-vue-define-options": "^1.1.1",
"vite": "^4.0.3",
"tailwindcss": "^3.3.2",
"terser": "^5.17.1",
"typescript": "^5.0.4",
"vite": "^4.3.5",
"vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-remove-console": "^1.3.0",
"vite-svg-loader": "^3.6.0",
"vue-eslint-parser": "^9.1.0",
"vue-tsc": "^1.0.9"
"vite-plugin-remove-console": "^2.1.1",
"vite-svg-loader": "^4.0.0",
"vue-eslint-parser": "^9.2.1",
"vue-tsc": "^1.6.4"
},
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"rollup",
"webpack"
"webpack",
"core-js"
]
},
"allowedDeprecatedVersions": {
"sourcemap-codec": "*",
"stable": "*"
}
},
"repository": "git@github.com:xiaoxian521/pure-admin-thin.git",
"repository": "git@github.com:pure-admin/pure-admin-thin.git",
"author": "xiaoxian521",
"license": "MIT"
}

4416
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
module.exports = {
plugins: {
"postcss-import": {},
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {})

1
public/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109z"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665z"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.114 323.114 0 0 1-107.769-242.852z"/></svg>

After

Width:  |  Height:  |  Size: 712 B

View File

@@ -1,5 +1,5 @@
{
"Version": "3.9.7",
"Version": "4.1.0",
"Title": "PureAdmin",
"FixedHeader": true,
"HiddenSideBar": false,
@@ -17,5 +17,6 @@
"ShowModel": "smart",
"MenuArrowIconNoTransition": true,
"CachingAsyncRoutes": false,
"TooltipEffect": "light"
"TooltipEffect": "light",
"ResponsiveStorageNameSpace": "responsive-"
}

View File

@@ -1,6 +1,7 @@
<template>
<el-config-provider :locale="currentLocale">
<router-view />
<ReDialog />
</el-config-provider>
</template>
@@ -8,10 +9,12 @@
import { defineComponent } from "vue";
import { ElConfigProvider } from "element-plus";
import zhCn from "element-plus/lib/locale/lang/zh-cn";
import { ReDialog } from "@/components/ReDialog";
export default defineComponent({
name: "app",
components: {
[ElConfigProvider.name]: ElConfigProvider
[ElConfigProvider.name]: ElConfigProvider,
ReDialog
},
computed: {
currentLocale() {

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 2208059 */
src: url("iconfont.woff2?t=1638023560828") format("woff2"),
url("iconfont.woff?t=1638023560828") format("woff"),
url("iconfont.ttf?t=1638023560828") format("truetype");
src: url("iconfont.woff2?t=1671895108120") format("woff2"),
url("iconfont.woff?t=1671895108120") format("woff"),
url("iconfont.ttf?t=1671895108120") format("truetype");
}
.iconfont {
@@ -13,26 +13,14 @@
-moz-osx-font-smoothing: grayscale;
}
.team-icontabs::before {
.pure-iconfont-tabs:before {
content: "\e63e";
}
.team-iconlogo::before {
.pure-iconfont-logo:before {
content: "\e620";
}
.team-iconxinpin::before {
content: "\e614";
}
.team-iconxinpinrenqiwang::before {
.pure-iconfont-new:before {
content: "\e615";
}
.team-iconexit-fullscreen::before {
content: "\e62a";
}
.team-iconfullscreen::before {
content: "\e62b";
}

File diff suppressed because one or more lines are too long

View File

@@ -2,50 +2,29 @@
"id": "2208059",
"name": "pure-admin",
"font_family": "iconfont",
"css_prefix_text": "team-icon",
"description": "pure-admin",
"css_prefix_text": "pure-iconfont-",
"description": "pure-admin-iconfont",
"glyphs": [
{
"icon_id": "20594647",
"name": "标签页",
"name": "Tabs",
"font_class": "tabs",
"unicode": "e63e",
"unicode_decimal": 58942
},
{
"icon_id": "22129506",
"name": "水能",
"name": "PureLogo",
"font_class": "logo",
"unicode": "e620",
"unicode_decimal": 58912
},
{
"icon_id": "7795613",
"name": "新品",
"font_class": "xinpin",
"unicode": "e614",
"unicode_decimal": 58900
},
{
"icon_id": "7795615",
"name": "新品人气王",
"font_class": "xinpinrenqiwang",
"name": "New",
"font_class": "new",
"unicode": "e615",
"unicode_decimal": 58901
},
{
"icon_id": "5698509",
"name": "全屏缩小",
"font_class": "exit-fullscreen",
"unicode": "e62a",
"unicode_decimal": 58922
},
{
"icon_id": "5698510",
"name": "全屏显示",
"font_class": "fullscreen",
"unicode": "e62b",
"unicode_decimal": 58923
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,29 @@
import { ElCol } from "element-plus";
import { h, defineComponent } from "vue";
// 封装element-plus的el-col组件
export default defineComponent({
name: "ReCol",
props: {
value: {
type: Number,
default: 24
}
},
render() {
const attrs = this.$attrs;
const val = this.value;
return h(
ElCol,
{
xs: val,
sm: val,
md: val,
lg: val,
xl: val,
...attrs
},
{ default: () => this.$slots.default() }
);
}
});

View File

@@ -0,0 +1,39 @@
import { ref } from "vue";
import reDialog from "./index.vue";
import { useTimeoutFn } from "@vueuse/core";
import { withInstall } from "@pureadmin/utils";
import type {
EventType,
ArgsType,
DialogProps,
ButtonProps,
DialogOptions
} from "./type";
const dialogStore = ref<Array<DialogOptions>>([]);
const addDialog = (options: DialogOptions) => {
const open = () =>
dialogStore.value.push(Object.assign(options, { visible: true }));
if (options?.openDelay) {
useTimeoutFn(() => {
open();
}, options.openDelay);
} else {
open();
}
};
const closeDialog = (options: DialogOptions, index: number, args?: any) => {
dialogStore.value.splice(index, 1);
options.closeCallBack && options.closeCallBack({ options, index, args });
};
const closeAllDialog = () => {
dialogStore.value = [];
};
const ReDialog = withInstall(reDialog);
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };
export { ReDialog, dialogStore, addDialog, closeDialog, closeAllDialog };

View File

@@ -0,0 +1,118 @@
<script setup lang="ts">
import { computed } from "vue";
import { isFunction } from "@pureadmin/utils";
import {
type DialogOptions,
type ButtonProps,
type EventType,
dialogStore,
closeDialog
} from "./index";
const footerButtons = computed(() => {
return (options: DialogOptions) => {
return options?.footerButtons?.length > 0
? options.footerButtons
: ([
{
label: "取消",
text: true,
bg: true,
btnClick: ({ dialog: { options, index } }) => {
const done = () =>
closeDialog(options, index, { command: "cancel" });
if (options?.beforeCancel && isFunction(options?.beforeCancel)) {
options.beforeCancel(done, { options, index });
} else {
done();
}
}
},
{
label: "确定",
type: "primary",
text: true,
bg: true,
btnClick: ({ dialog: { options, index } }) => {
const done = () =>
closeDialog(options, index, { command: "sure" });
if (options?.beforeSure && isFunction(options?.beforeSure)) {
options.beforeSure(done, { options, index });
} else {
done();
}
}
}
] as Array<ButtonProps>);
};
});
function eventsCallBack(
event: EventType,
options: DialogOptions,
index: number
) {
if (options?.[event] && isFunction(options?.[event])) {
return options?.[event]({ options, index });
}
}
function handleClose(
options: DialogOptions,
index: number,
args = { command: "close" }
) {
closeDialog(options, index, args);
eventsCallBack("close", options, index);
}
</script>
<template>
<el-dialog
v-for="(options, index) in dialogStore"
:key="index"
v-bind="options"
v-model="options.visible"
@opened="eventsCallBack('open', options, index)"
@close="handleClose(options, index)"
@openAutoFocus="eventsCallBack('openAutoFocus', options, index)"
@closeAutoFocus="eventsCallBack('closeAutoFocus', options, index)"
>
<!-- header -->
<template
v-if="options?.headerRenderer"
#header="{ close, titleId, titleClass }"
>
<component
:is="options?.headerRenderer({ close, titleId, titleClass })"
/>
</template>
<!-- default -->
<component
v-bind="options?.props"
:is="options.contentRenderer({ options, index })"
@close="args => handleClose(options, index, args)"
/>
<!-- footer -->
<template v-if="!options?.hideFooter" #footer>
<template v-if="options?.footerRenderer">
<component :is="options?.footerRenderer({ options, index })" />
</template>
<span v-else>
<el-button
v-for="(btn, key) in footerButtons(options)"
:key="key"
v-bind="btn"
@click="
btn.btnClick({
dialog: { options, index },
button: { btn, index: key }
})
"
>
{{ btn?.label }}
</el-button>
</span>
</template>
</el-dialog>
</template>

View File

@@ -0,0 +1,216 @@
import type { CSSProperties, VNode, Component } from "vue";
type DoneFn = (cancel?: boolean) => void;
type EventType = "open" | "close" | "openAutoFocus" | "closeAutoFocus";
type ArgsType = {
/** `cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或者空白页 */
command: "cancel" | "sure" | "close";
};
/** https://element-plus.org/zh-CN/component/dialog.html#attributes */
type DialogProps = {
/** `Dialog` 的显示与隐藏 */
visible?: boolean;
/** `Dialog` 的标题 */
title?: string;
/** `Dialog` 的宽度,默认 `50%` */
width?: string | number;
/** 是否为全屏 `Dialog`,默认 `false` */
fullscreen?: boolean;
/** `Dialog CSS` 中的 `margin-top` 值,默认 `15vh` */
top?: string;
/** 是否需要遮罩层,默认 `true` */
modal?: boolean;
/** `Dialog` 自身是否插入至 `body` 元素上。嵌套的 `Dialog` 必须指定该属性并赋值为 `true`,默认 `false` */
appendToBody?: boolean;
/** 是否在 `Dialog` 出现时将 `body` 滚动锁定,默认 `true` */
lockScroll?: boolean;
/** `Dialog` 的自定义类名 */
class?: string;
/** `Dialog` 的自定义样式 */
style?: CSSProperties;
/** `Dialog` 打开的延时时间,单位毫秒,默认 `0` */
openDelay?: number;
/** `Dialog` 关闭的延时时间,单位毫秒,默认 `0` */
closeDelay?: number;
/** 是否可以通过点击 `modal` 关闭 `Dialog`,默认 `true` */
closeOnClickModal?: boolean;
/** 是否可以通过按下 `ESC` 关闭 `Dialog`,默认 `true` */
closeOnPressEscape?: boolean;
/** 是否显示关闭按钮,默认 `true` */
showClose?: boolean;
/** 关闭前的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
beforeClose?: (done: DoneFn) => void;
/** 为 `Dialog` 启用可拖拽功能,默认 `false` */
draggable?: boolean;
/** 是否让 `Dialog` 的 `header` 和 `footer` 部分居中排列,默认 `false` */
center?: boolean;
/** 是否水平垂直对齐对话框,默认 `false` */
alignCenter?: boolean;
/** 当关闭 `Dialog` 时,销毁其中的元素,默认 `false` */
destroyOnClose?: boolean;
};
type BtnClickDialog = {
options?: DialogOptions;
index?: number;
};
type BtnClickButton = {
btn?: ButtonProps;
index?: number;
};
/** https://element-plus.org/zh-CN/component/button.html#button-attributes */
type ButtonProps = {
/** 按钮文字 */
label: string;
/** 按钮尺寸 */
size?: "large" | "default" | "small";
/** 按钮类型 */
type?: "primary" | "success" | "warning" | "danger" | "info";
/** 是否为朴素按钮,默认 `false` */
plain?: boolean;
/** 是否为文字按钮,默认 `false` */
text?: boolean;
/** 是否显示文字按钮背景颜色,默认 `false` */
bg?: boolean;
/** 是否为链接按钮,默认 `false` */
link?: boolean;
/** 是否为圆角按钮,默认 `false` */
round?: boolean;
/** 是否为圆形按钮,默认 `false` */
circle?: boolean;
/** 是否为加载中状态,默认 `false` */
loading?: boolean;
/** 自定义加载中状态图标组件 */
loadingIcon?: string | Component;
/** 按钮是否为禁用状态,默认 `false` */
disabled?: boolean;
/** 图标组件 */
icon?: string | Component;
/** 是否开启原生 `autofocus` 属性,默认 `false` */
autofocus?: boolean;
/** 原生 `type` 属性,默认 `button` */
nativeType?: "button" | "submit" | "reset";
/** 自动在两个中文字符之间插入空格 */
autoInsertSpace?: boolean;
/** 自定义按钮颜色, 并自动计算 `hover` 和 `active` 触发后的颜色 */
color?: string;
/** `dark` 模式, 意味着自动设置 `color` 为 `dark` 模式的颜色,默认 `false` */
dark?: boolean;
/** 自定义元素标签 */
tag?: string | Component;
/** 点击按钮后触发的回调 */
btnClick?: ({
dialog,
button
}: {
/** 当前 `Dialog` 信息 */
dialog: BtnClickDialog;
/** 当前 `button` 信息 */
button: BtnClickButton;
}) => void;
};
interface DialogOptions extends DialogProps {
/** 内容区组件的 `props`,可通过 `defineProps` 接收 */
props?: any;
/** 是否隐藏 `Dialog` 按钮操作区的内容 */
hideFooter?: boolean;
/**
* @description 自定义对话框标题的内容渲染器
* @see {@link https://element-plus.org/zh-CN/component/dialog.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%A4%B4%E9%83%A8}
*/
headerRenderer?: ({
close,
titleId,
titleClass
}: {
close: Function;
titleId: string;
titleClass: string;
}) => VNode | Component;
/** 自定义内容渲染器 */
contentRenderer?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => VNode | Component;
/** 自定义按钮操作区的内容渲染器,会覆盖`footerButtons`以及默认的 `取消` 和 `确定` 按钮 */
footerRenderer?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => VNode | Component;
/** 自定义底部按钮操作 */
footerButtons?: Array<ButtonProps>;
/** `Dialog` 打开后的回调 */
open?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => void;
/** `Dialog` 关闭后的回调(只有点击右上角关闭按钮或者空白页关闭页面时才会触发) */
close?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => void;
/** `Dialog` 关闭后的回调。 `args` 返回的 `command` 值解析:`cancel` 点击取消按钮、`sure` 点击确定按钮、`close` 点击右上角关闭按钮或者空白页 */
closeCallBack?: ({
options,
index,
args
}: {
options: DialogOptions;
index: number;
args: any;
}) => void;
/** 输入焦点聚焦在 `Dialog` 内容时的回调 */
openAutoFocus?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => void;
/** 输入焦点从 `Dialog` 内容失焦时的回调 */
closeAutoFocus?: ({
options,
index
}: {
options: DialogOptions;
index: number;
}) => void;
/** 点击底部取消按钮的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
beforeCancel?: (
done: Function,
{
options,
index
}: {
options: DialogOptions;
index: number;
}
) => void;
/** 点击底部确定按钮的回调,会暂停 `Dialog` 的关闭. 回调函数内执行 `done` 参数方法的时候才是真正关闭对话框的时候 */
beforeSure?: (
done: Function,
{
options,
index
}: {
options: DialogOptions;
index: number;
}
) => void;
}
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };

View File

@@ -0,0 +1,5 @@
import pureTableBar from "./src/bar";
import { withInstall } from "@pureadmin/utils";
/** 配合 `@pureadmin/table` 实现快速便捷的表格操作 https://github.com/pure-admin/pure-admin-table */
export const PureTableBar = withInstall(pureTableBar);

View File

@@ -0,0 +1,339 @@
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
import { delay, getKeyList, cloneDeep } from "@pureadmin/utils";
import { defineComponent, ref, computed, type PropType, nextTick } from "vue";
import Sortable from "sortablejs";
import DragIcon from "./svg/drag.svg?component";
import ExpandIcon from "./svg/expand.svg?component";
import RefreshIcon from "./svg/refresh.svg?component";
import SettingIcon from "./svg/settings.svg?component";
import CollapseIcon from "./svg/collapse.svg?component";
const props = {
/** 头部最左边的标题 */
title: {
type: String,
default: "列表"
},
/** 对于树形表格如果想启用展开和折叠功能传入当前表格的ref即可 */
tableRef: {
type: Object as PropType<any>
},
/** 需要展示的列 */
columns: {
type: Array as PropType<TableColumnList>,
default: () => []
}
};
export default defineComponent({
name: "PureTableBar",
props,
emits: ["refresh"],
setup(props, { emit, slots, attrs }) {
const buttonRef = ref();
const size = ref("default");
const isExpandAll = ref(true);
const loading = ref(false);
const checkAll = ref(true);
const isIndeterminate = ref(false);
let checkColumnList = getKeyList(cloneDeep(props?.columns), "label");
const checkedColumns = ref(checkColumnList);
const dynamicColumns = ref(cloneDeep(props?.columns));
const getDropdownItemStyle = computed(() => {
return s => {
return {
background:
s === size.value ? useEpThemeStoreHook().epThemeColor : "",
color: s === size.value ? "#fff" : "var(--el-text-color-primary)"
};
};
});
const iconClass = computed(() => {
return [
"text-black",
"dark:text-white",
"duration-100",
"hover:!text-primary",
"cursor-pointer",
"outline-none"
];
});
const topClass = computed(() => {
return [
"flex",
"justify-between",
"pt-[3px]",
"px-[11px]",
"border-b-[1px]",
"border-solid",
"border-[#dcdfe6]",
"dark:border-[#303030]"
];
});
function onReFresh() {
loading.value = true;
emit("refresh");
delay(500).then(() => (loading.value = false));
}
function onExpand() {
isExpandAll.value = !isExpandAll.value;
toggleRowExpansionAll(props.tableRef.data, isExpandAll.value);
}
function toggleRowExpansionAll(data, isExpansion) {
data.forEach(item => {
props.tableRef.toggleRowExpansion(item, isExpansion);
if (item.children !== undefined && item.children !== null) {
toggleRowExpansionAll(item.children, isExpansion);
}
});
}
function handleCheckAllChange(val: boolean) {
checkedColumns.value = val ? checkColumnList : [];
isIndeterminate.value = false;
dynamicColumns.value.map(column =>
val ? (column.hide = false) : (column.hide = true)
);
}
function handleCheckedColumnsChange(value: string[]) {
const checkedCount = value.length;
checkAll.value = checkedCount === checkColumnList.length;
isIndeterminate.value =
checkedCount > 0 && checkedCount < checkColumnList.length;
}
function handleCheckColumnListChange(val: boolean, label: string) {
dynamicColumns.value.filter(item => item.label === label)[0].hide = !val;
}
async function onReset() {
checkAll.value = true;
isIndeterminate.value = false;
dynamicColumns.value = cloneDeep(props?.columns);
checkColumnList = [];
checkColumnList = await getKeyList(cloneDeep(props?.columns), "label");
checkedColumns.value = checkColumnList;
}
const dropdown = {
dropdown: () => (
<el-dropdown-menu class="translation">
<el-dropdown-item
style={getDropdownItemStyle.value("large")}
onClick={() => (size.value = "large")}
>
</el-dropdown-item>
<el-dropdown-item
style={getDropdownItemStyle.value("default")}
onClick={() => (size.value = "default")}
>
</el-dropdown-item>
<el-dropdown-item
style={getDropdownItemStyle.value("small")}
onClick={() => (size.value = "small")}
>
</el-dropdown-item>
</el-dropdown-menu>
)
};
/** 列展示拖拽排序 */
const rowDrop = (event: { preventDefault: () => void }) => {
event.preventDefault();
nextTick(() => {
const wrapper: HTMLElement = document.querySelector(
".el-checkbox-group>div"
);
Sortable.create(wrapper, {
animation: 300,
handle: ".drag-btn",
onEnd: ({ newIndex, oldIndex, item }) => {
const targetThElem = item;
const wrapperElem = targetThElem.parentNode as HTMLElement;
const oldColumn = dynamicColumns.value[oldIndex];
const newColumn = dynamicColumns.value[newIndex];
if (oldColumn?.fixed || newColumn?.fixed) {
// 当前列存在fixed属性 则不可拖拽
const oldThElem = wrapperElem.children[oldIndex] as HTMLElement;
if (newIndex > oldIndex) {
wrapperElem.insertBefore(targetThElem, oldThElem);
} else {
wrapperElem.insertBefore(
targetThElem,
oldThElem ? oldThElem.nextElementSibling : oldThElem
);
}
return;
}
const currentRow = dynamicColumns.value.splice(oldIndex, 1)[0];
dynamicColumns.value.splice(newIndex, 0, currentRow);
}
});
});
};
const isFixedColumn = (label: string) => {
return dynamicColumns.value.filter(item => item.label === label)[0].fixed
? true
: false;
};
const reference = {
reference: () => (
<SettingIcon
class={["w-[16px]", iconClass.value]}
onMouseover={e => (buttonRef.value = e.currentTarget)}
/>
)
};
return () => (
<>
<div {...attrs} class="w-[99/100] mt-6 p-2 bg-bg_color">
<div class="flex justify-between w-full h-[60px] p-4">
<p class="font-bold truncate">{props.title}</p>
<div class="flex items-center justify-around">
{slots?.buttons ? (
<div class="flex mr-4">{slots.buttons()}</div>
) : null}
{props.tableRef?.size ? (
<>
<el-tooltip
effect="dark"
content={isExpandAll.value ? "折叠" : "展开"}
placement="top"
>
<ExpandIcon
class={["w-[16px]", iconClass.value]}
style={{
transform: isExpandAll.value ? "none" : "rotate(-90deg)"
}}
onClick={() => onExpand()}
/>
</el-tooltip>
<el-divider direction="vertical" />
</>
) : null}
<el-tooltip effect="dark" content="刷新" placement="top">
<RefreshIcon
class={[
"w-[16px]",
iconClass.value,
loading.value ? "animate-spin" : ""
]}
onClick={() => onReFresh()}
/>
</el-tooltip>
<el-divider direction="vertical" />
<el-tooltip effect="dark" content="密度" placement="top">
<el-dropdown v-slots={dropdown} trigger="click">
<CollapseIcon class={["w-[16px]", iconClass.value]} />
</el-dropdown>
</el-tooltip>
<el-divider direction="vertical" />
<el-popover
v-slots={reference}
popper-style={{ padding: 0 }}
width="160"
trigger="click"
>
<div class={[topClass.value]}>
<el-checkbox
class="!-mr-1"
label="列展示"
v-model={checkAll.value}
indeterminate={isIndeterminate.value}
onChange={value => handleCheckAllChange(value)}
/>
<el-button type="primary" link onClick={() => onReset()}>
</el-button>
</div>
<div class="pt-[6px] pl-[11px]">
<el-checkbox-group
v-model={checkedColumns.value}
onChange={value => handleCheckedColumnsChange(value)}
>
<el-space
direction="vertical"
alignment="flex-start"
size={0}
>
{checkColumnList.map(item => {
return (
<div class="flex items-center">
<DragIcon
class={[
"drag-btn w-[16px] mr-2",
isFixedColumn(item)
? "!cursor-no-drop"
: "!cursor-grab"
]}
onMouseenter={(event: {
preventDefault: () => void;
}) => rowDrop(event)}
/>
<el-checkbox
key={item}
label={item}
onChange={value =>
handleCheckColumnListChange(value, item)
}
>
<span
title={item}
class="inline-block w-[120px] truncate hover:text-text_color_primary"
>
{item}
</span>
</el-checkbox>
</div>
);
})}
</el-space>
</el-checkbox-group>
</div>
</el-popover>
</div>
<el-tooltip
popper-options={{
modifiers: [
{
name: "computeStyles",
options: {
adaptive: false,
enabled: false
}
}
]
}}
placement="top"
virtual-ref={buttonRef.value}
virtual-triggering
trigger="hover"
content="列设置"
/>
</div>
{slots.default({
size: size.value,
dynamicColumns: dynamicColumns.value
})}
</div>
</>
);
}
});

View File

@@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M13.79 10.21a1 1 0 0 0 1.42 0 1 1 0 0 0 0-1.42l-2.5-2.5a1 1 0 0 0-.33-.21 1 1 0 0 0-.76 0 1 1 0 0 0-.33.21l-2.5 2.5a1 1 0 0 0 1.42 1.42l.79-.8v5.18l-.79-.8a1 1 0 0 0-1.42 1.42l2.5 2.5a1 1 0 0 0 .33.21.94.94 0 0 0 .76 0 1 1 0 0 0 .33-.21l2.5-2.5a1 1 0 0 0-1.42-1.42l-.79.8V9.41ZM7 4h10a1 1 0 0 0 0-2H7a1 1 0 0 0 0 2Zm10 16H7a1 1 0 0 0 0 2h10a1 1 0 0 0 0-2Z"/></svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -0,0 +1 @@
<svg width="32" height="32" fill="currentColor" aria-hidden="true" data-icon="holder" viewBox="64 64 896 896"><path d="M300 276.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97zm0 284a56 56 0 1 0 56-97 56 56 0 0 0-56 97zM640 228a56 56 0 1 0 112 0 56 56 0 0 0-112 0zm0 284a56 56 0 1 0 112 0 56 56 0 0 0-112 0zM300 844.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97zM640 796a56 56 0 1 0 112 0 56 56 0 0 0-112 0z"/></svg>

After

Width:  |  Height:  |  Size: 398 B

View File

@@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M22 4V2H2v2h9v14.17l-5.5-5.5-1.42 1.41L12 22l7.92-7.92-1.42-1.41-5.5 5.5V4h9Z"/></svg>

After

Width:  |  Height:  |  Size: 163 B

View File

@@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"/></svg>

After

Width:  |  Height:  |  Size: 235 B

View File

@@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M3.34 17a10.018 10.018 0 0 1-.978-2.326 3 3 0 0 0 .002-5.347A9.99 9.99 0 0 1 4.865 4.99a3 3 0 0 0 4.631-2.674 9.99 9.99 0 0 1 5.007.002 3 3 0 0 0 4.632 2.672A9.99 9.99 0 0 1 20.66 7c.433.749.757 1.53.978 2.326a3 3 0 0 0-.002 5.347 9.99 9.99 0 0 1-2.501 4.337 3 3 0 0 0-4.631 2.674 9.99 9.99 0 0 1-5.007-.002 3 3 0 0 0-4.632-2.672A10.018 10.018 0 0 1 3.34 17zm5.66.196a4.993 4.993 0 0 1 2.25 2.77c.499.047 1 .048 1.499.001A4.993 4.993 0 0 1 15 17.197a4.993 4.993 0 0 1 3.525-.565c.29-.408.54-.843.748-1.298A4.993 4.993 0 0 1 18 12c0-1.26.47-2.437 1.273-3.334a8.126 8.126 0 0 0-.75-1.298A4.993 4.993 0 0 1 15 6.804a4.993 4.993 0 0 1-2.25-2.77c-.499-.047-1-.048-1.499-.001A4.993 4.993 0 0 1 9 6.803a4.993 4.993 0 0 1-3.525.565 7.99 7.99 0 0 0-.748 1.298A4.993 4.993 0 0 1 6 12a4.99 4.99 0 0 1-1.273 3.334 8.126 8.126 0 0 0 .75 1.298A4.993 4.993 0 0 1 9 17.196zM12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></svg>

After

Width:  |  Height:  |  Size: 1011 B

View File

@@ -49,4 +49,7 @@ export const getServerConfig = async (app: App): Promise<undefined> => {
});
};
export { getConfig, setConfig };
/** 本地响应式存储的命名空间 */
const responsiveStorageNameSpace = () => getConfig().ResponsiveStorageNameSpace;
export { getConfig, setConfig, responsiveStorageNameSpace };

View File

@@ -130,16 +130,16 @@ const transitionMain = defineComponent({
<style scoped>
.app-main {
position: relative;
width: 100%;
height: 100vh;
position: relative;
overflow-x: hidden;
}
.app-main-nofixed-header {
position: relative;
width: 100%;
min-height: 100vh;
position: relative;
}
.main-content {

View File

@@ -82,28 +82,28 @@ const {
overflow: hidden;
.hamburger-container {
line-height: 48px;
height: 100%;
float: left;
height: 100%;
line-height: 48px;
cursor: pointer;
}
.vertical-header-right {
display: flex;
align-items: center;
justify-content: flex-end;
min-width: 280px;
height: 48px;
align-items: center;
color: #000000d9;
justify-content: flex-end;
.el-dropdown-link {
height: 48px;
padding: 10px;
display: flex;
align-items: center;
justify-content: space-around;
cursor: pointer;
height: 48px;
padding: 10px;
color: #000000d9;
cursor: pointer;
p {
font-size: 14px;
@@ -127,9 +127,9 @@ const {
max-width: 120px;
::v-deep(.el-dropdown-menu__item) {
min-width: 100%;
display: inline-flex;
flex-wrap: wrap;
min-width: 100%;
}
}
</style>

View File

@@ -46,8 +46,8 @@ notices.value.map(v => (noticesNum.value += v.list.length));
display: flex;
align-items: center;
justify-content: center;
height: 48px;
width: 60px;
height: 48px;
cursor: pointer;
.header-notice-icon {
@@ -59,7 +59,7 @@ notices.value.map(v => (noticesNum.value += v.list.length));
width: 330px;
.noticeList-container {
padding: 15px 24px 0 24px;
padding: 15px 24px 0;
}
:deep(.el-tabs__header) {
@@ -71,7 +71,7 @@ notices.value.map(v => (noticesNum.value += v.list.length));
}
:deep(.el-tabs__nav-wrap) {
padding: 0 36px 0 36px;
padding: 0 36px;
}
}
</style>

View File

@@ -2,6 +2,7 @@
import { ListItem } from "./data";
import { ref, PropType, nextTick } from "vue";
import { useNav } from "@/layout/hooks/useNav";
import { deviceDetection } from "@pureadmin/utils";
const props = defineProps({
noticeItem: {
@@ -15,6 +16,7 @@ const titleTooltip = ref(false);
const descriptionRef = ref(null);
const descriptionTooltip = ref(false);
const { tooltipEffect } = useNav();
const isMobile = deviceDetection();
function hoverTitle() {
nextTick(() => {
@@ -63,6 +65,7 @@ function hoverDescription(event, description) {
:disabled="!titleTooltip"
:content="props.noticeItem.title"
placement="top-start"
:enterable="!isMobile"
>
<div
ref="titleRef"
@@ -115,6 +118,7 @@ function hoverDescription(event, description) {
align-items: flex-start;
justify-content: space-between;
padding: 12px 0;
// border-bottom: 1px solid #f0f0f0;
.notice-container-avatar {
@@ -124,15 +128,15 @@ function hoverDescription(event, description) {
.notice-container-text {
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
flex: 1;
.notice-text-title {
display: flex;
margin-bottom: 8px;
font-weight: 400;
font-size: 14px;
font-weight: 400;
line-height: 1.5715;
cursor: pointer;
@@ -140,8 +144,8 @@ function hoverDescription(event, description) {
flex: 1;
width: 200px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
white-space: nowrap;
}
.notice-title-extra {
@@ -159,8 +163,8 @@ function hoverDescription(event, description) {
.notice-text-description {
display: -webkit-box;
text-overflow: ellipsis;
overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}

View File

@@ -60,9 +60,9 @@ emitter.on("openPanel", () => {
<style>
.showright-panel {
overflow: hidden;
position: relative;
width: calc(100% - 15px);
overflow: hidden;
}
</style>
@@ -71,23 +71,23 @@ emitter.on("openPanel", () => {
position: fixed;
top: 0;
left: 0;
z-index: -1;
background: rgb(0 0 0 / 20%);
opacity: 0;
transition: opacity 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
background: rgba(0, 0, 0, 0.2);
z-index: -1;
}
.right-panel {
width: 100%;
max-width: 315px;
height: 100vh;
position: fixed;
top: 0;
right: 0;
box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.05);
z-index: 40000;
width: 100%;
max-width: 315px;
height: 100vh;
box-shadow: 0 0 15px 0 rgb(0 0 0 / 5%);
transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
transform: translate(100%);
z-index: 40000;
}
.show {
@@ -95,9 +95,9 @@ emitter.on("openPanel", () => {
.right-panel-background {
z-index: 20000;
opacity: 1;
width: 100%;
height: 100%;
opacity: 1;
}
.right-panel {
@@ -106,20 +106,20 @@ emitter.on("openPanel", () => {
}
.handle-button {
position: absolute;
top: 45%;
left: -48px;
z-index: 0;
width: 48px;
height: 48px;
position: absolute;
left: -48px;
text-align: center;
font-size: 24px;
border-radius: 6px 0 0 6px !important;
z-index: 0;
line-height: 48px;
color: #fff;
text-align: center;
pointer-events: auto;
cursor: pointer;
color: #fff;
line-height: 48px;
top: 45%;
background: rgb(24, 144, 255);
background: rgb(24 144 255);
border-radius: 6px 0 0 6px !important;
i {
font-size: 24px;
@@ -128,24 +128,24 @@ emitter.on("openPanel", () => {
}
.right-panel-items {
margin-top: 60px;
height: calc(100vh - 60px);
margin-top: 60px;
overflow-y: auto;
}
.project-configuration {
position: fixed;
top: 15px;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
height: 30px;
position: fixed;
justify-content: space-between;
align-items: center;
top: 15px;
margin-left: 10px;
}
:deep(.el-divider--horizontal) {
width: 90%;
margin: 20px auto 0 auto;
margin: 20px auto 0;
}
</style>

View File

@@ -1,17 +0,0 @@
<script setup lang="ts">
import { useFullscreen } from "@vueuse/core";
const { isFullscreen, toggle } = useFullscreen();
</script>
<template>
<div
class="screen-full w-[36px] h-[48px] flex-ac cursor-pointer navbar-bg-hover"
@click="toggle"
>
<FontIcon
:title="isFullscreen ? '退出全屏' : '全屏'"
:icon="isFullscreen ? 'team-iconexit-fullscreen' : 'team-iconfullscreen'"
/>
</div>
</template>

View File

@@ -3,10 +3,9 @@ import { useRouter } from "vue-router";
import { cloneDeep } from "@pureadmin/utils";
import SearchResult from "./SearchResult.vue";
import SearchFooter from "./SearchFooter.vue";
import { deleteChildren } from "@/utils/tree";
import { useNav } from "@/layout/hooks/useNav";
import { ref, computed, shallowRef } from "vue";
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
import { ref, watch, computed, nextTick, shallowRef } from "vue";
import { usePermissionStoreHook } from "@/store/modules/permission";
import Search from "@iconify-icons/ep/search";
@@ -32,7 +31,7 @@ const handleSearch = useDebounceFn(search, 300);
/** 菜单树形结构 */
const menusData = computed(() => {
return deleteChildren(cloneDeep(usePermissionStoreHook().wholeMenus));
return cloneDeep(usePermissionStoreHook().wholeMenus);
});
const show = computed({
@@ -44,14 +43,6 @@ const show = computed({
}
});
watch(show, async val => {
if (val) {
/** 自动聚焦 */
await nextTick();
inputRef.value?.focus();
}
});
/** 将菜单树形结构扁平化为一维数组,用于菜单查询 */
function flatTree(arr) {
const res = [];
@@ -135,9 +126,11 @@ onKeyStroke("ArrowDown", handleDown);
<template>
<el-dialog
top="5vh"
:width="device === 'mobile' ? '80vw' : '50vw'"
v-model="show"
:width="device === 'mobile' ? '80vw' : '50vw'"
:before-close="handleClose"
@opened="inputRef.focus()"
@closed="inputRef.blur()"
>
<el-input
ref="inputRef"

View File

@@ -81,11 +81,11 @@ function handleTo() {
display: flex;
align-items: center;
height: 56px;
margin-top: 8px;
padding: 14px;
border-radius: 4px;
margin-top: 8px;
cursor: pointer;
border: 0.1px solid #ccc;
border-radius: 4px;
transition: all 0.3s;
&-title {

View File

@@ -418,35 +418,35 @@ onBeforeMount(() => {
li {
display: flex;
justify-content: space-between;
align-items: center;
justify-content: space-between;
margin: 25px;
}
}
.pure-datatheme {
display: block;
width: 100%;
height: 50px;
text-align: center;
display: block;
padding-top: 25px;
text-align: center;
}
.pure-theme {
margin-top: 25px;
width: 100%;
height: 50px;
display: flex;
flex-wrap: wrap;
justify-content: space-around;
width: 100%;
height: 50px;
margin-top: 25px;
li {
position: relative;
width: 18%;
height: 45px;
background: #f0f2f5;
position: relative;
overflow: hidden;
cursor: pointer;
background: #f0f2f5;
border-radius: 4px;
box-shadow: 0 1px 2.5px 0 rgb(0 0 0 / 18%);
@@ -459,13 +459,13 @@ onBeforeMount(() => {
}
&:nth-child(2) {
width: 70%;
height: 30%;
position: absolute;
top: 0;
right: 0;
width: 70%;
height: 30%;
background: #fff;
box-shadow: 0 0 1px #888;
position: absolute;
}
}
}
@@ -491,13 +491,13 @@ onBeforeMount(() => {
}
&:nth-child(2) {
width: 30%;
height: 70%;
position: absolute;
bottom: 0;
left: 0;
width: 30%;
height: 70%;
background: #fff;
box-shadow: 0 0 1px #888;
position: absolute;
}
}
}
@@ -505,11 +505,11 @@ onBeforeMount(() => {
}
.theme-color {
display: flex;
justify-content: center;
width: 100%;
height: 40px;
margin-top: 20px;
display: flex;
justify-content: center;
li {
float: left;
@@ -519,8 +519,8 @@ onBeforeMount(() => {
margin-right: 8px;
font-weight: 700;
text-align: center;
border-radius: 2px;
cursor: pointer;
border-radius: 2px;
&:nth-child(2) {
border: 1px solid #ddd;

View File

@@ -11,12 +11,6 @@ const router = useRouter();
const routes: any = router.options.routes;
const multiTags: any = useMultiTagsStoreHook().multiTags;
const isDashboard = (route: RouteLocationMatched): boolean | string => {
const name = route && (route.name as string);
if (!name) return false;
return name.trim().toLocaleLowerCase() === "Welcome".toLocaleLowerCase();
};
const getBreadcrumb = (): void => {
// 当前路由信息
let currentRoute;
@@ -34,28 +28,24 @@ const getBreadcrumb = (): void => {
}
});
} else {
currentRoute = findRouteByPath(router.currentRoute.value.path, multiTags);
currentRoute = findRouteByPath(router.currentRoute.value.path, routes);
}
// 当前路由的父级路径组成的数组
const parentRoutes = getParentPaths(router.currentRoute.value.path, routes);
const parentRoutes = getParentPaths(
router.currentRoute.value.name as string,
routes,
"name"
);
// 存放组成面包屑的数组
let matched = [];
const matched = [];
// 获取每个父级路径对应的路由信息
parentRoutes.forEach(path => {
if (path !== "/") matched.push(findRouteByPath(path, routes));
});
if (currentRoute?.path !== "/welcome") matched.push(currentRoute);
if (!isDashboard(matched[0])) {
matched = [
{
path: "/welcome",
parentPath: "/",
meta: { title: "首页" }
} as unknown as RouteLocationMatched
].concat(matched);
}
matched.push(currentRoute);
matched.forEach((item, index) => {
if (currentRoute?.query || currentRoute?.params) return;
@@ -90,6 +80,9 @@ watch(
() => route.path,
() => {
getBreadcrumb();
},
{
deep: true
}
);
</script>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
import { toRaw } from "vue";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
const props = defineProps({
extraIcon: {
type: String,
default: ""
}
});
</script>
<template>
<div v-if="props.extraIcon" class="flex justify-center items-center">
<component
:is="useRenderIcon(toRaw(props.extraIcon))"
class="w-[30px] h-[30px]"
/>
</div>
</template>

View File

@@ -15,7 +15,7 @@ const {
title,
routers,
logout,
backHome,
backTopMenu,
onPanel,
menuSelect,
username,
@@ -39,9 +39,9 @@ watch(
v-loading="usePermissionStoreHook().wholeMenus.length === 0"
class="horizontal-header"
>
<div class="horizontal-header-left" @click="backHome">
<FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" />
<h4>{{ title }}</h4>
<div class="horizontal-header-left" @click="backTopMenu">
<img src="/logo.svg" alt="logo" />
<span>{{ title }}</span>
</div>
<el-menu
router
@@ -104,9 +104,9 @@ watch(
max-width: 120px;
::v-deep(.el-dropdown-menu__item) {
min-width: 100%;
display: inline-flex;
flex-wrap: wrap;
min-width: 100%;
}
}
</style>

View File

@@ -24,7 +24,7 @@ const iconClass = computed(() => {
"align-middle",
"text-primary",
"cursor-pointer",
"duration-[360ms]",
"duration-[100ms]",
"hover:text-primary",
"dark:hover:!text-white"
];

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { getTopMenu } from "@/router/utils";
import { useNav } from "@/layout/hooks/useNav";
const props = defineProps({
@@ -6,6 +7,7 @@ const props = defineProps({
});
const { title } = useNav();
const topPath = getTopMenu().path;
</script>
<template>
@@ -16,9 +18,9 @@ const { title } = useNav();
key="props.collapse"
:title="title"
class="sidebar-logo-link"
to="/"
:to="topPath"
>
<FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" />
<img src="/logo.svg" alt="logo" />
<span class="sidebar-title">{{ title }}</span>
</router-link>
<router-link
@@ -26,9 +28,9 @@ const { title } = useNav();
key="expand"
:title="title"
class="sidebar-logo-link"
to="/"
:to="topPath"
>
<FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" />
<img src="/logo.svg" alt="logo" />
<span class="sidebar-title">{{ title }}</span>
</router-link>
</transition>
@@ -40,34 +42,30 @@ const { title } = useNav();
position: relative;
width: 100%;
height: 48px;
text-align: center;
overflow: hidden;
.sidebar-logo-link {
display: flex;
flex-wrap: nowrap;
align-items: center;
height: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-top: 5px;
img {
display: inline-block;
height: 32px;
}
.sidebar-title {
display: block;
width: 160px;
display: inline-block;
height: 32px;
margin: 2px 0 0 12px;
overflow: hidden;
font-size: 18px;
font-weight: 600;
line-height: 32px;
color: $subMenuActiveText;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
color: #1890ff;
font-weight: 600;
font-size: 20px;
margin-top: 10px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
}
}
.collapses {
.sidebar-logo {
margin-right: 0;
}
}
}

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import extraIcon from "./extraIcon.vue";
import Search from "../search/index.vue";
import Notice from "../notice/index.vue";
import { useNav } from "@/layout/hooks/useNav";
@@ -21,6 +22,7 @@ const {
menuSelect,
resolvePath,
username,
getDivStyle,
avatarsStyle
} = useNav();
@@ -78,15 +80,12 @@ watch(
:is="useRenderIcon(route.meta && toRaw(route.meta.icon))"
/>
</div>
<span class="select-none">{{ route.meta.title }}</span>
<FontIcon
v-if="route.meta.extraIcon"
width="30px"
height="30px"
style="position: absolute; right: 10px"
:icon="route.meta.extraIcon.name"
:svg="route.meta.extraIcon.svg ? true : false"
/>
<div :style="getDivStyle">
<span class="select-none">
{{ route.meta.title }}
</span>
<extraIcon :extraIcon="route.meta.extraIcon" />
</div>
</template>
</el-menu-item>
</el-menu>
@@ -136,9 +135,9 @@ watch(
max-width: 120px;
::v-deep(.el-dropdown-menu__item) {
min-width: 100%;
display: inline-flex;
flex-wrap: wrap;
min-width: 100%;
}
}
</style>

View File

@@ -1,21 +1,22 @@
<script setup lang="ts">
import path from "path";
import { getConfig } from "@/config";
import { childrenType } from "../../types";
import { menuType } from "../../types";
import extraIcon from "./extraIcon.vue";
import { useNav } from "@/layout/hooks/useNav";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { ref, toRaw, PropType, nextTick, computed, CSSProperties } from "vue";
import ArrowUp from "@iconify-icons/ep/arrow-up";
import EpArrowDown from "@iconify-icons/ep/arrow-down";
import ArrowLeft from "@iconify-icons/ep/arrow-left";
import ArrowRight from "@iconify-icons/ep/arrow-right";
import ArrowUp from "@iconify-icons/ep/arrow-up-bold";
import EpArrowDown from "@iconify-icons/ep/arrow-down-bold";
import ArrowLeft from "@iconify-icons/ep/arrow-left-bold";
import ArrowRight from "@iconify-icons/ep/arrow-right-bold";
const { layout, isCollapse, tooltipEffect } = useNav();
const { layout, isCollapse, tooltipEffect, getDivStyle } = useNav();
const props = defineProps({
item: {
type: Object as PropType<childrenType>
type: Object as PropType<menuType>
},
isNest: {
type: Boolean,
@@ -27,17 +28,11 @@ const props = defineProps({
}
});
const getExtraIconStyle = computed((): CSSProperties => {
if (!isCollapse.value) {
return {
position: "absolute",
right: "10px"
};
} else {
return {
position: "static"
};
}
const getSpanStyle = computed((): CSSProperties => {
return {
width: "100%",
textAlign: "center"
};
});
const getNoDropdownStyle = computed((): CSSProperties => {
@@ -47,16 +42,6 @@ const getNoDropdownStyle = computed((): CSSProperties => {
};
});
const getDivStyle = computed((): CSSProperties => {
return {
width: !isCollapse.value ? "" : "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
overflow: "hidden"
};
});
const getMenuTextStyle = computed(() => {
return {
overflow: "hidden",
@@ -65,19 +50,54 @@ const getMenuTextStyle = computed(() => {
};
});
const getSubTextStyle = computed((): CSSProperties => {
const getsubMenuIconStyle = computed((): CSSProperties => {
return {
width: !isCollapse.value ? "210px" : "",
display: "inline-block",
overflow: "hidden",
textOverflow: "ellipsis"
display: "flex",
justifyContent: "center",
alignItems: "center",
margin:
layout.value === "horizontal"
? "0 5px 0 0"
: isCollapse.value
? "0 auto"
: "0 5px 0 0"
};
});
const getSpanStyle = computed(() => {
return {
overflow: "hidden",
textOverflow: "ellipsis"
const getSubTextStyle = computed((): CSSProperties => {
if (!isCollapse.value) {
return {
width: "210px",
display: "inline-block",
overflow: "hidden",
textOverflow: "ellipsis"
};
} else {
return {
width: ""
};
}
});
const getSubMenuDivStyle = computed((): any => {
return item => {
return !isCollapse.value
? {
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
overflow: "hidden"
}
: {
width: "100%",
textAlign:
item?.parentId === null
? "center"
: layout.value === "mix" && item?.pathList?.length === 2
? "center"
: ""
};
};
});
@@ -91,7 +111,7 @@ const expandCloseIcon = computed(() => {
};
});
const onlyOneChild: childrenType = ref(null);
const onlyOneChild: menuType = ref(null);
// 存放菜单是否存在showTooltip属性标识
const hoverMenuMap = new WeakMap();
// 存储菜单文本dom元素
@@ -114,10 +134,21 @@ function hoverMenu(key) {
});
}
function hasOneShowingChild(
children: childrenType[] = [],
parent: childrenType
) {
// 左侧菜单折叠后,当菜单没有图标时只显示第一个文字并加上省略号
function overflowSlice(text, item?: any) {
const newText =
(text?.length > 1 ? text.toString().slice(0, 1) : text) + "...";
if (item && !(isCollapse.value && item?.parentId === null)) {
return layout.value === "mix" &&
item?.pathList?.length === 2 &&
isCollapse.value
? newText
: text;
}
return newText;
}
function hasOneShowingChild(children: menuType[] = [], parent: menuType) {
const showingChildren = children.filter((item: any) => {
onlyOneChild.value = item;
return true;
@@ -150,84 +181,78 @@ function resolvePath(routePath) {
</script>
<template>
<template
<el-menu-item
v-if="
hasOneShowingChild(props.item.children, props.item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren)
"
:index="resolvePath(onlyOneChild.path)"
:class="{ 'submenu-title-noDropdown': !isNest }"
:style="getNoDropdownStyle"
>
<el-menu-item
:index="resolvePath(onlyOneChild.path)"
:class="{ 'submenu-title-noDropdown': !isNest }"
:style="getNoDropdownStyle"
<div
v-if="toRaw(props.item.meta.icon)"
class="sub-menu-icon"
:style="getsubMenuIconStyle"
>
<div class="sub-menu-icon" v-if="toRaw(props.item.meta.icon)">
<component
:is="
useRenderIcon(
toRaw(onlyOneChild.meta.icon) ||
(props.item.meta && toRaw(props.item.meta.icon))
)
"
/>
</div>
<div
v-if="
isCollapse &&
layout === 'vertical' &&
props.item?.pathList?.length === 1
<component
:is="
useRenderIcon(
toRaw(onlyOneChild.meta.icon) ||
(props.item.meta && toRaw(props.item.meta.icon))
)
"
:style="getDivStyle"
>
<span :style="getMenuTextStyle">
/>
</div>
<span
v-if="
!props.item?.meta.icon &&
isCollapse &&
layout === 'vertical' &&
props.item?.pathList?.length === 1
"
:style="getSpanStyle"
>
{{ overflowSlice(onlyOneChild.meta.title) }}
</span>
<span
v-if="
!onlyOneChild.meta.icon &&
isCollapse &&
layout === 'mix' &&
props.item?.pathList?.length === 2
"
:style="getSpanStyle"
>
{{ overflowSlice(onlyOneChild.meta.title) }}
</span>
<template #title>
<div :style="getDivStyle">
<span v-if="layout === 'horizontal'">
{{ onlyOneChild.meta.title }}
</span>
</div>
<div
v-if="
isCollapse && layout === 'mix' && props.item?.pathList?.length === 2
"
:style="getDivStyle"
>
<span :style="getMenuTextStyle">
{{ onlyOneChild.meta.title }}
</span>
</div>
<template #title>
<div :style="getDivStyle">
<span v-if="layout === 'horizontal'">
<el-tooltip
v-else
placement="top"
:effect="tooltipEffect"
:offset="-10"
:disabled="!onlyOneChild.showTooltip"
>
<template #content>
{{ onlyOneChild.meta.title }}
</template>
<span
ref="menuTextRef"
:style="getMenuTextStyle"
@mouseover="hoverMenu(onlyOneChild)"
>
{{ onlyOneChild.meta.title }}
</span>
<el-tooltip
v-else
placement="top"
:effect="tooltipEffect"
:offset="-10"
:disabled="!onlyOneChild.showTooltip"
>
<template #content>
{{ onlyOneChild.meta.title }}
</template>
<span
ref="menuTextRef"
:style="getMenuTextStyle"
@mouseover="hoverMenu(onlyOneChild)"
>
{{ onlyOneChild.meta.title }}
</span>
</el-tooltip>
<FontIcon
v-if="onlyOneChild.meta.extraIcon"
width="30px"
height="30px"
:style="getExtraIconStyle"
:icon="onlyOneChild.meta.extraIcon.name"
:svg="onlyOneChild.meta.extraIcon.svg ? true : false"
/>
</div>
</template>
</el-menu-item>
</template>
</el-tooltip>
<extraIcon :extraIcon="onlyOneChild.meta.extraIcon" />
</div>
</template>
</el-menu-item>
<el-sub-menu
v-else
@@ -236,7 +261,11 @@ function resolvePath(routePath) {
:index="resolvePath(props.item.path)"
>
<template #title>
<div v-if="toRaw(props.item.meta.icon)" class="sub-menu-icon">
<div
v-if="toRaw(props.item.meta.icon)"
:style="getsubMenuIconStyle"
class="sub-menu-icon"
>
<component
:is="useRenderIcon(props.item.meta && toRaw(props.item.meta.icon))"
/>
@@ -244,34 +273,36 @@ function resolvePath(routePath) {
<span v-if="layout === 'horizontal'">
{{ props.item.meta.title }}
</span>
<el-tooltip
v-else
placement="top"
:effect="tooltipEffect"
:offset="-10"
:disabled="!props.item.showTooltip"
<div
:style="getSubMenuDivStyle(props.item)"
v-if="
!(
isCollapse &&
toRaw(props.item.meta.icon) &&
props.item.parentId === null
)
"
>
<template #content>
{{ props.item.meta.title }}
</template>
<div
ref="menuTextRef"
:style="getSubTextStyle"
@mouseover="hoverMenu(props.item)"
<el-tooltip
v-if="layout !== 'horizontal'"
placement="top"
:effect="tooltipEffect"
:offset="-10"
:disabled="!props.item.showTooltip"
>
<span :style="getSpanStyle">
<template #content>
{{ props.item.meta.title }}
</template>
<span
ref="menuTextRef"
:style="getSubTextStyle"
@mouseover="hoverMenu(props.item)"
>
{{ overflowSlice(props.item.meta.title, props.item) }}
</span>
</div>
</el-tooltip>
<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"
/>
</el-tooltip>
<extraIcon v-if="!isCollapse" :extraIcon="props.item.meta.extraIcon" />
</div>
</template>
<sidebar-item
v-for="child in props.item.children"

View File

@@ -6,14 +6,16 @@ import SidebarItem from "./sidebarItem.vue";
import leftCollapse from "./leftCollapse.vue";
import { useNav } from "@/layout/hooks/useNav";
import { storageLocal } from "@pureadmin/utils";
import { responsiveStorageNameSpace } from "@/config";
import { ref, computed, watch, onBeforeMount } from "vue";
import { findRouteByPath, getParentPaths } from "@/router/utils";
import { usePermissionStoreHook } from "@/store/modules/permission";
const route = useRoute();
const showLogo = ref(
storageLocal().getItem<StorageConfigs>("responsive-configure")?.showLogo ??
true
storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}configure`
)?.showLogo ?? true
);
const { routers, device, pureApp, isCollapse, menuSelect, toggleSideBar } =
@@ -27,7 +29,12 @@ const menuData = computed(() => {
: usePermissionStoreHook().wholeMenus;
});
const loading = computed(() =>
pureApp.layout === "mix" ? false : menuData.value.length === 0 ? true : false
);
function getSubMenuData(path: string) {
subMenuData.value = [];
// path的上级路由组成的数组
const parentPathArr = getParentPaths(
path,
@@ -53,6 +60,7 @@ onBeforeMount(() => {
watch(
() => [route.path, usePermissionStoreHook().wholeMenus],
() => {
if (route.path.includes("/redirect")) return;
getSubMenuData(route.path);
menuSelect(route.path, routers);
}
@@ -61,7 +69,7 @@ watch(
<template>
<div
v-loading="menuData.length === 0"
v-loading="loading"
:class="['sidebar-container', showLogo ? 'has-logo' : '']"
>
<Logo v-if="showLogo" :collapse="isCollapse" />

View File

@@ -1,4 +1,4 @@
@keyframes scheduleInWidth {
@keyframes schedule-in-width {
from {
width: 0;
}
@@ -8,7 +8,7 @@
}
}
@keyframes scheduleOutWidth {
@keyframes schedule-out-width {
from {
width: 100%;
}
@@ -39,41 +39,41 @@
}
.tags-view {
width: 100%;
font-size: 14px;
position: relative;
display: flex;
align-items: center;
width: 100%;
font-size: 14px;
color: var(--el-text-color-primary);
background: #fff;
position: relative;
box-shadow: 0 0 1px #888;
.scroll-item {
border-radius: 3px 3px 0 0;
padding: 0 6px;
box-shadow: 0 0 1px #888;
position: relative;
margin-right: 4px;
height: 28px;
display: inline-block;
height: 28px;
padding: 0 6px;
margin-right: 4px;
line-height: 28px;
transition: all 0.4s;
cursor: pointer;
border-radius: 3px 3px 0 0;
box-shadow: 0 0 1px #888;
transition: all 0.4s;
.el-icon-close {
position: absolute;
top: 50%;
font-size: 10px;
color: var(--el-color-primary);
cursor: pointer;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
transition: font-size 0.2s;
transform: translate(-50%, -50%);
&:hover {
border-radius: 50%;
font-size: 13px;
color: #fff;
background: #b4bccc;
font-size: 13px;
border-radius: 50%;
}
}
@@ -91,24 +91,24 @@
}
a {
text-decoration: none;
color: var(--el-text-color-primary);
padding: 0 4px;
color: var(--el-text-color-primary);
text-decoration: none;
}
.scroll-container {
flex: 1;
overflow: hidden;
padding: 5px 0;
white-space: nowrap;
position: relative;
flex: 1;
padding: 5px 0;
overflow: hidden;
white-space: nowrap;
.tab {
position: relative;
float: left;
list-style: none;
overflow: visible;
white-space: nowrap;
list-style: none;
transition: transform 0.5s ease-in-out;
.scroll-item {
@@ -123,29 +123,28 @@
/* 右键菜单 */
.contextmenu {
margin: 0;
background: #fff;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
color: var(--el-text-color-primary);
font-weight: normal;
margin: 0;
font-size: 13px;
font-weight: normal;
color: var(--el-text-color-primary);
white-space: nowrap;
list-style-type: none;
background: #fff;
border-radius: 4px;
outline: 0;
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
li {
width: 100%;
margin: 0;
padding: 7px 12px;
cursor: pointer;
display: flex;
align-items: center;
width: 100%;
padding: 7px 12px;
margin: 0;
cursor: pointer;
&:hover {
// background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
}
@@ -159,11 +158,11 @@
.el-dropdown-menu {
li {
display: flex;
align-items: center;
width: 100%;
margin: 0;
cursor: pointer;
display: flex;
align-items: center;
svg {
display: block;
@@ -184,6 +183,7 @@
:deep(.el-dropdown-menu__item--divided) {
margin: 1px 0;
}
.el-dropdown-menu__item--divided::before {
margin: 0;
}
@@ -193,7 +193,6 @@
}
.scroll-item.is-active {
// background-color: var(--el-color-primary-light-9);
position: relative;
color: #fff;
@@ -213,16 +212,16 @@
.arrow-left,
.arrow-right,
.arrow-down {
position: relative;
width: 40px;
height: 38px;
color: var(--el-text-color-primary);
position: relative;
svg {
width: 20px;
height: 20px;
position: absolute;
left: 50%;
width: 20px;
height: 20px;
transform: translate(-50%, 50%);
}
}
@@ -236,8 +235,8 @@
}
.arrow-right {
box-shadow: -5px 0 5px -6px #ccc;
border-right: 0.5px solid #ccc;
box-shadow: -5px 0 5px -6px #ccc;
&:hover {
cursor: e-resize;
@@ -255,8 +254,8 @@
/* 卡片模式下鼠标移出隐藏蓝色边框 */
.card-out {
border: none;
color: #666;
border: none;
a {
color: #666;
@@ -265,32 +264,32 @@
/* 灵动模式 */
.schedule-active {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
position: absolute;
left: 0;
bottom: 0;
background: var(--el-color-primary);
}
/* 灵动模式下鼠标移入显示蓝色进度条 */
.schedule-in {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 2px;
position: absolute;
left: 0;
bottom: 0;
background: var(--el-color-primary);
animation: scheduleInWidth 400ms ease-in;
animation: schedule-in-width 200ms ease-in;
}
/* 灵动模式下鼠标移出隐藏蓝色进度条 */
.schedule-out {
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
position: absolute;
left: 0;
bottom: 0;
background: var(--el-color-primary);
animation: scheduleOutWidth 400ms ease-in;
animation: schedule-out-width 200ms ease-in;
}

View File

@@ -4,10 +4,10 @@ import { RouteConfigs } from "../../types";
import { useTags } from "../../hooks/useTag";
import { routerArrays } from "@/layout/types";
import { isEqual, isAllEmpty } from "@pureadmin/utils";
import { handleAliveRoute, getTopMenu } from "@/router/utils";
import { useSettingStoreHook } from "@/store/modules/settings";
import { ref, watch, unref, nextTick, onBeforeMount } from "vue";
import { handleAliveRoute, delAliveRoutes } from "@/router/utils";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { ref, watch, unref, toRaw, nextTick, onBeforeMount } from "vue";
import { useResizeObserver, useDebounceFn, useFullscreen } from "@vueuse/core";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
@@ -48,6 +48,8 @@ const tabDom = ref();
const containerDom = ref();
const scrollbarDom = ref();
const isShowArrow = ref(false);
const topPath = getTopMenu().path;
const { VITE_HIDE_HOME } = import.meta.env;
const { isFullscreen, toggle } = useFullscreen();
const dynamicTagView = () => {
@@ -63,7 +65,7 @@ const dynamicTagView = () => {
moveToView(index);
};
const moveToView = (index: number): void => {
const moveToView = async (index: number): Promise<void> => {
const tabNavPadding = 10;
if (!instance.refs["dynamic" + index]) return;
const tabItemEl = instance.refs["dynamic" + index][0];
@@ -73,8 +75,13 @@ const moveToView = (index: number): void => {
const scrollbarDomWidth = scrollbarDom.value
? scrollbarDom.value?.offsetWidth
: 0;
// 获取视图更新后dom
await nextTick();
// 已有标签页总长度(包含溢出部分)
const tabDomWidth = tabDom.value ? tabDom.value?.offsetWidth : 0;
scrollbarDomWidth <= tabDomWidth
? (isShowArrow.value = true)
: (isShowArrow.value = false);
@@ -158,13 +165,12 @@ function onFresh() {
const { fullPath, query } = unref(route);
router.replace({
path: "/redirect" + fullPath,
query: query
query
});
handleAliveRoute(route as toRouteType, "refresh");
}
function deleteDynamicTag(obj: any, current: any, tag?: string) {
// 存放被删除的缓存路由
let delAliveRouteList = [];
const valueIndex: number = multiTags.value.findIndex((item: any) => {
if (item.query) {
if (item.path === obj.path) {
@@ -185,13 +191,17 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
other?: boolean
): void => {
if (other) {
useMultiTagsStoreHook().handleTags("equal", [routerArrays[0], obj]);
useMultiTagsStoreHook().handleTags("equal", [
VITE_HIDE_HOME === "false" ? routerArrays[0] : toRaw(getTopMenu()),
obj
]);
} else {
delAliveRouteList = useMultiTagsStoreHook().handleTags("splice", "", {
useMultiTagsStoreHook().handleTags("splice", "", {
startIndex,
length
}) as any;
}
dynamicTagView();
};
if (tag === "other") {
@@ -206,10 +216,6 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
}
const newRoute = useMultiTagsStoreHook().handleTags("slice");
if (current === route.path) {
// 删除缓存路由
tag
? delAliveRoutes(delAliveRouteList)
: handleAliveRoute(route.matched, "delete");
// 如果删除当前激活tag就自动切换到最后一个tag
if (tag === "left") return;
if (newRoute[0]?.query) {
@@ -220,8 +226,6 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
router.push({ path: newRoute[0].path });
}
} else {
// 删除缓存路由
tag ? delAliveRoutes(delAliveRouteList) : delAliveRoutes([obj]);
if (!multiTags.value.length) return;
if (multiTags.value.some(item => item.path === route.path)) return;
if (newRoute[0]?.query) {
@@ -236,6 +240,7 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
function deleteMenu(item, tag?: string) {
deleteDynamicTag(item, item.path, tag);
handleAliveRoute(route as toRouteType);
}
function onClickDrop(key, item, selectRoute?: RouteConfigs) {
@@ -282,7 +287,8 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
startIndex: 1,
length: multiTags.value.length
});
router.push("/welcome");
router.push(topPath);
handleAliveRoute(route as toRouteType);
break;
case 6:
// 整体页面全屏
@@ -338,7 +344,7 @@ function disabledMenus(value: boolean) {
});
}
/** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */
/** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是顶级菜单,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */
function showMenuModel(
currentPath: string,
query: object = {},
@@ -360,11 +366,11 @@ function showMenuModel(
}
/**
* currentIndex为1时左侧的菜单是首页,则不显示关闭左侧标签页
* currentIndex为1时左侧的菜单顶级菜单,则不显示关闭左侧标签页
* 如果currentIndex等于routeLength-1右侧没有菜单则不显示关闭右侧标签页
*/
if (currentIndex === 1 && routeLength !== 2) {
// 左侧的菜单是首页,右侧存在别的菜单
// 左侧的菜单是顶级菜单,右侧存在别的菜单
tagsViews[2].show = false;
Array.of(1, 3, 4, 5).forEach(v => {
tagsViews[v].disabled = false;
@@ -372,7 +378,7 @@ function showMenuModel(
tagsViews[2].disabled = true;
} else if (currentIndex === 1 && routeLength === 2) {
disabledMenus(false);
// 左侧的菜单是首页,右侧不存在别的菜单
// 左侧的菜单是顶级菜单,右侧不存在别的菜单
Array.of(2, 3, 4).forEach(v => {
tagsViews[v].show = false;
tagsViews[v].disabled = true;
@@ -384,8 +390,8 @@ function showMenuModel(
tagsViews[v].disabled = false;
});
tagsViews[3].disabled = true;
} else if (currentIndex === 0 || currentPath === "/redirect/welcome") {
// 当前路由为首页
} else if (currentIndex === 0 || currentPath === `/redirect${topPath}`) {
// 当前路由为顶级菜单
disabledMenus(true);
} else {
disabledMenus(false);
@@ -394,8 +400,8 @@ function showMenuModel(
function openMenu(tag, e) {
closeMenu();
if (tag.path === "/welcome") {
// 右键菜单为首页,只显示刷新
if (tag.path === topPath) {
// 右键菜单为顶级菜单,只显示刷新
showMenus(false);
tagsViews[0].show = true;
} else if (route.path !== tag.path && route.name !== tag.name) {
@@ -488,6 +494,11 @@ watch([route], () => {
dynamicTagView();
});
watch(isFullscreen, () => {
tagsViews[6].icon = Fullscreen;
tagsViews[6].text = "全屏";
});
onMounted(() => {
useResizeObserver(
scrollbarDom,
@@ -512,7 +523,7 @@ onMounted(() => {
:class="[
'scroll-item is-closable',
linkIsActive(item),
$route.path === item.path && showModel === 'card'
route.path === item.path && showModel === 'card'
? 'card-active'
: ''
]"
@@ -596,5 +607,5 @@ onMounted(() => {
</template>
<style lang="scss" scoped>
@import "./index.scss";
@import url("./index.scss");
</style>

View File

@@ -50,15 +50,15 @@ onMounted(() => {
<style lang="scss" scoped>
.frame {
height: calc(100vh - 88px);
z-index: 998;
height: calc(100vh - 88px);
.frame-iframe {
box-sizing: border-box;
width: 100%;
height: 100%;
overflow: hidden;
border: 0;
box-sizing: border-box;
}
}

View File

@@ -38,7 +38,7 @@ export function useDataThemeChange() {
const body = document.documentElement as HTMLElement;
/** 设置导航主题色 */
function setLayoutThemeColor(theme = "default") {
function setLayoutThemeColor(theme = getConfig().Theme ?? "default") {
layoutTheme.value.theme = theme;
toggleTheme({
scopeName: `layout-theme-${theme}`

View File

@@ -1,9 +1,10 @@
import { computed } from "vue";
import { storeToRefs } from "pinia";
import { getConfig } from "@/config";
import { emitter } from "@/utils/mitt";
import { routeMetaType } from "../types";
import { getTopMenu } from "@/router/utils";
import { useGlobal } from "@pureadmin/utils";
import { computed, CSSProperties } from "vue";
import { useRouter, useRoute } from "vue-router";
import { router, remainingPaths } from "@/router";
import { useAppStoreHook } from "@/store/modules/app";
@@ -20,6 +21,16 @@ export function useNav() {
/** 平台`layout`中所有`el-tooltip`的`effect`配置,默认`light` */
const tooltipEffect = getConfig()?.TooltipEffect ?? "light";
const getDivStyle = computed((): CSSProperties => {
return {
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
overflow: "hidden"
};
});
/** 用户名 */
const username = computed(() => {
return useUserStoreHook()?.username;
@@ -58,8 +69,8 @@ export function useNav() {
useUserStoreHook().logOut();
}
function backHome() {
router.push("/welcome");
function backTopMenu() {
router.push(getTopMenu().path);
}
function onPanel() {
@@ -128,8 +139,9 @@ export function useNav() {
logout,
routers,
$storage,
backHome,
backTopMenu,
onPanel,
getDivStyle,
changeTitle,
toggleSideBar,
menuSelect,

View File

@@ -11,10 +11,16 @@ import {
import { tagsViewsType } from "../types";
import { useEventListener } from "@vueuse/core";
import { useRoute, useRouter } from "vue-router";
import { isEqual, isBoolean } from "@pureadmin/utils";
import { responsiveStorageNameSpace } from "@/config";
import { useSettingStoreHook } from "@/store/modules/settings";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { storageLocal, toggleClass, hasClass } from "@pureadmin/utils";
import {
isEqual,
isBoolean,
storageLocal,
toggleClass,
hasClass
} from "@pureadmin/utils";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import CloseAllTags from "@iconify-icons/ri/subtract-line";
@@ -40,13 +46,16 @@ export function useTags() {
/** 显示模式,默认灵动模式 */
const showModel = ref(
storageLocal().getItem<StorageConfigs>("responsive-configure")?.showModel ||
"smart"
storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}configure`
)?.showModel || "smart"
);
/** 是否隐藏标签页,默认显示 */
const showTags =
ref(
storageLocal().getItem<StorageConfigs>("responsive-configure").hideTabs
storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}configure`
).hideTabs
) ?? ref("false");
const multiTags: any = computed(() => {
return useMultiTagsStoreHook().multiTags;
@@ -195,10 +204,13 @@ export function useTags() {
onMounted(() => {
if (!showModel.value) {
const configure = storageLocal().getItem<StorageConfigs>(
"responsive-configure"
`${responsiveStorageNameSpace()}configure`
);
configure.showModel = "card";
storageLocal().setItem("responsive-configure", configure);
storageLocal().setItem(
`${responsiveStorageNameSpace()}configure`,
configure
);
}
});

View File

@@ -179,20 +179,16 @@ const layoutHeader = defineComponent({
</template>
<style lang="scss" scoped>
@mixin clearfix {
.app-wrapper {
position: relative;
width: 100%;
height: 100%;
&::after {
content: "";
display: table;
clear: both;
content: "";
}
}
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar {
position: fixed;
@@ -201,13 +197,13 @@ const layoutHeader = defineComponent({
}
.app-mask {
position: absolute;
top: 0;
z-index: 999;
width: 100%;
height: 100%;
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.re-screen {

View File

@@ -1,15 +1,19 @@
import type { IconifyIcon } from "@iconify/vue";
const { VITE_HIDE_HOME } = import.meta.env;
export const routerArrays: Array<RouteConfigs> = [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "首页",
icon: "homeFilled"
}
}
];
export const routerArrays: Array<RouteConfigs> =
VITE_HIDE_HOME === "false"
? [
{
path: "/welcome",
parentPath: "/",
meta: {
title: "首页",
icon: "homeFilled"
}
}
]
: [];
export type routeMetaType = {
title?: string;
@@ -58,23 +62,23 @@ export interface setType {
hideTabs: boolean;
}
export type childrenType = {
export type menuType = {
id?: number;
path?: string;
noShowingChildren?: boolean;
children?: childrenType[];
children?: menuType[];
value: unknown;
meta?: {
icon?: string;
title?: string;
rank?: number;
showParent?: boolean;
extraIcon?: {
svg?: boolean;
name?: string;
};
extraIcon?: string;
};
showTooltip?: boolean;
parentId?: number;
pathList?: number[];
redirect?: string;
};
export type themeColorsType = {

View File

@@ -12,6 +12,7 @@ import {
} from "vue-router";
import {
ascending,
getTopMenu,
initRouter,
isOneOfArray,
getHistoryMode,
@@ -60,7 +61,7 @@ export const remainingPaths = Object.keys(remainingRouter).map(v => {
/** 创建路由实例 */
export const router: Router = createRouter({
history: getHistoryMode(),
history: getHistoryMode(import.meta.env.VITE_ROUTER_HISTORY),
routes: constantRoutes.concat(...(remainingRouter as any)),
strict: true,
scrollBehavior(to, from, savedPosition) {
@@ -95,13 +96,14 @@ export function resetRouter() {
/** 路由白名单 */
const whiteList = ["/login"];
const { VITE_HIDE_HOME } = import.meta.env;
router.beforeEach((to: toRouteType, _from, next) => {
if (to.meta?.keepAlive) {
const newMatched = to.matched;
handleAliveRoute(newMatched, "add");
handleAliveRoute(to, "add");
// 页面整体刷新和点击标签页刷新
if (_from.name === undefined || _from.name === "Redirect") {
handleAliveRoute(newMatched);
handleAliveRoute(to);
}
}
const userInfo = storageSession().getItem<DataInfo<number>>(sessionKey);
@@ -124,6 +126,10 @@ router.beforeEach((to: toRouteType, _from, next) => {
if (to.meta?.roles && !isOneOfArray(to.meta?.roles, userInfo?.roles)) {
next({ path: "/error/403" });
}
// 开启隐藏首页后在浏览器地址栏手动输入首页welcome路由则跳转到404页面
if (VITE_HIDE_HOME === "true" && to.fullPath === "/welcome") {
next({ path: "/error/404" });
}
if (_from?.name) {
// name为超链接
if (externalLink) {
@@ -137,7 +143,7 @@ router.beforeEach((to: toRouteType, _from, next) => {
if (
usePermissionStoreHook().wholeMenus.length === 0 &&
to.path !== "/login"
)
) {
initRouter().then((router: Router) => {
if (!useMultiTagsStoreHook().getMultiTagsCache) {
const { path } = to;
@@ -145,6 +151,7 @@ router.beforeEach((to: toRouteType, _from, next) => {
path,
router.options.routes[0].children
);
getTopMenu(true);
// query、params模式路由传参数的标签页不在此处处理
if (route && route.meta?.title) {
useMultiTagsStoreHook().handleTags("push", {
@@ -156,6 +163,7 @@ router.beforeEach((to: toRouteType, _from, next) => {
}
router.push(to.fullPath);
});
}
toCorrectRoute();
}
} else {

View File

@@ -1,3 +1,4 @@
const { VITE_HIDE_HOME } = import.meta.env;
const Layout = () => import("@/layout/index.vue");
export default {
@@ -16,7 +17,8 @@ export default {
name: "Welcome",
component: () => import("@/views/welcome/index.vue"),
meta: {
title: "首页"
title: "首页",
showLink: VITE_HIDE_HOME === "true" ? false : true
}
}
]

View File

@@ -15,10 +15,9 @@ export default [
path: "/redirect",
component: Layout,
meta: {
icon: "homeFilled",
title: "首页",
title: "加载中...",
showLink: false,
rank: 104
rank: 102
},
children: [
{

View File

@@ -3,13 +3,11 @@ import {
RouteRecordRaw,
RouteComponent,
createWebHistory,
createWebHashHistory,
RouteRecordNormalized
createWebHashHistory
} from "vue-router";
import { router } from "./index";
import { isProxy, toRaw } from "vue";
import { useTimeoutFn } from "@vueuse/core";
import { RouteConfigs } from "@/layout/types";
import {
isString,
cloneDeep,
@@ -19,8 +17,10 @@ import {
isIncludeAllChildren
} from "@pureadmin/utils";
import { getConfig } from "@/config";
import { menuType } from "@/layout/types";
import { buildHierarchyTree } from "@/utils/tree";
import { sessionKey, type DataInfo } from "@/utils/auth";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { usePermissionStoreHook } from "@/store/modules/permission";
const IFrame = () => import("@/layout/frameView.vue");
// https://cn.vitejs.dev/guide/features.html#glob-import
@@ -94,30 +94,20 @@ function filterNoPermissionTree(data: RouteComponent[]) {
return filterChildrenTree(newTree);
}
/** 批量删除缓存路由(keepalive) */
function delAliveRoutes(delAliveRouteList: Array<RouteConfigs>) {
delAliveRouteList.forEach(route => {
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: route?.name
});
});
}
/** 通过path获取父级路径 */
function getParentPaths(path: string, routes: RouteRecordRaw[]) {
/** 通过指定 `key` 获取父级路径集合,默认 `key` 为 `path` */
function getParentPaths(value: string, routes: RouteRecordRaw[], key = "path") {
// 深度遍历查找
function dfs(routes: RouteRecordRaw[], path: string, parents: string[]) {
function dfs(routes: RouteRecordRaw[], value: string, parents: string[]) {
for (let i = 0; i < routes.length; i++) {
const item = routes[i];
// 找到path则返回父级path
if (item.path === path) return parents;
// 返回父级path
if (item[key] === value) return parents;
// children不存在或为空则不递归
if (!item.children || !item.children.length) continue;
// 往下查找时将当前path入栈
parents.push(item.path);
if (dfs(item.children, path, parents).length) return parents;
if (dfs(item.children, value, parents).length) return parents;
// 深度遍历查找未找到时当前path 出栈
parents.pop();
}
@@ -125,10 +115,10 @@ function getParentPaths(path: string, routes: RouteRecordRaw[]) {
return [];
}
return dfs(routes, path, []);
return dfs(routes, value, []);
}
/** 查找对应path的路由信息 */
/** 查找对应 `path` 的路由信息 */
function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
let res = routes.find((item: { path: string }) => item.path == path);
if (res) {
@@ -241,7 +231,7 @@ function formatFlatteningRoutes(routesList: RouteRecordRaw[]) {
/**
* 一维数组处理成多级嵌套数组三级及以上的路由全部拍成二级keep-alive 只支持到二级缓存)
* https://github.com/xiaoxian521/vue-pure-admin/issues/67
* https://github.com/pure-admin/vue-pure-admin/issues/67
* @param routesList 处理后的一维路由菜单数组
* @returns 返回将一维数组重新处理成规定路由的格式
*/
@@ -266,27 +256,35 @@ function formatTwoStageRoutes(routesList: RouteRecordRaw[]) {
}
/** 处理缓存路由(添加、删除、刷新) */
function handleAliveRoute(matched: RouteRecordNormalized[], mode?: string) {
function handleAliveRoute({ name }: toRouteType, mode?: string) {
switch (mode) {
case "add":
matched.forEach(v => {
usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name });
usePermissionStoreHook().cacheOperate({
mode: "add",
name
});
break;
case "delete":
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: matched[matched.length - 1].name
name
});
break;
case "refresh":
usePermissionStoreHook().cacheOperate({
mode: "refresh",
name
});
break;
default:
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: matched[matched.length - 1].name
name
});
useTimeoutFn(() => {
matched.forEach(v => {
usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name });
usePermissionStoreHook().cacheOperate({
mode: "add",
name
});
}, 100);
}
@@ -322,8 +320,7 @@ function addAsyncRoutes(arrRoutes: Array<RouteRecordRaw>) {
}
/** 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html */
function getHistoryMode(): RouterHistory {
const routerHistory = import.meta.env.VITE_ROUTER_HISTORY;
function getHistoryMode(routerHistory): RouterHistory {
// len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1
const historyMode = routerHistory.split(",");
const leftMode = historyMode[0];
@@ -362,16 +359,24 @@ function hasAuth(value: string | Array<string>): boolean {
return isAuths ? true : false;
}
/** 获取所有菜单中的第一个菜单(顶级菜单)*/
function getTopMenu(tag = false): menuType {
const topMenu = usePermissionStoreHook().wholeMenus[0]?.children[0];
tag && useMultiTagsStoreHook().handleTags("push", topMenu);
return topMenu;
}
export {
hasAuth,
getAuths,
ascending,
filterTree,
initRouter,
getTopMenu,
addPathMatch,
isOneOfArray,
getHistoryMode,
addAsyncRoutes,
delAliveRoutes,
getParentPaths,
findRouteByPath,
handleAliveRoute,

View File

@@ -1,7 +1,7 @@
import { store } from "@/store";
import { appType } from "./types";
import { defineStore } from "pinia";
import { getConfig } from "@/config";
import { getConfig, responsiveStorageNameSpace } from "@/config";
import { deviceDetection, storageLocal } from "@pureadmin/utils";
export const useAppStore = defineStore({
@@ -9,29 +9,32 @@ export const useAppStore = defineStore({
state: (): appType => ({
sidebar: {
opened:
storageLocal().getItem<StorageConfigs>("responsive-layout")
?.sidebarStatus ?? getConfig().SidebarStatus,
storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}layout`
)?.sidebarStatus ?? getConfig().SidebarStatus,
withoutAnimation: false,
isClickCollapse: false
},
// 这里的layout用于监听容器拖拉后恢复对应的导航模式
layout:
storageLocal().getItem<StorageConfigs>("responsive-layout")?.layout ??
getConfig().Layout,
storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}layout`
)?.layout ?? getConfig().Layout,
device: deviceDetection() ? "mobile" : "desktop"
}),
getters: {
getSidebarStatus() {
return this.sidebar.opened;
getSidebarStatus(state) {
return state.sidebar.opened;
},
getDevice() {
return this.device;
getDevice(state) {
return state.device;
}
},
actions: {
TOGGLE_SIDEBAR(opened?: boolean, resize?: string) {
const layout =
storageLocal().getItem<StorageConfigs>("responsive-layout");
const layout = storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}layout`
);
if (opened && resize) {
this.sidebar.withoutAnimation = true;
this.sidebar.opened = true;
@@ -46,7 +49,7 @@ export const useAppStore = defineStore({
this.sidebar.isClickCollapse = !this.sidebar.opened;
layout.sidebarStatus = this.sidebar.opened;
}
storageLocal().setItem("responsive-layout", layout);
storageLocal().setItem(`${responsiveStorageNameSpace()}layout`, layout);
},
async toggleSideBar(opened?: boolean, resize?: string) {
await this.TOGGLE_SIDEBAR(opened, resize);

View File

@@ -1,27 +1,29 @@
import { store } from "@/store";
import { defineStore } from "pinia";
import { getConfig } from "@/config";
import { storageLocal } from "@pureadmin/utils";
import { getConfig, responsiveStorageNameSpace } from "@/config";
export const useEpThemeStore = defineStore({
id: "pure-epTheme",
state: () => ({
epThemeColor:
storageLocal().getItem<StorageConfigs>("responsive-layout")
?.epThemeColor ?? getConfig().EpThemeColor,
storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}layout`
)?.epThemeColor ?? getConfig().EpThemeColor,
epTheme:
storageLocal().getItem<StorageConfigs>("responsive-layout")?.theme ??
getConfig().Theme
storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}layout`
)?.theme ?? getConfig().Theme
}),
getters: {
getEpThemeColor() {
return this.epThemeColor;
getEpThemeColor(state) {
return state.epThemeColor;
},
/** 用于mix导航模式下hamburger-svg的fill属性 */
fill() {
if (this.epTheme === "light") {
fill(state) {
if (state.epTheme === "light") {
return "#409eff";
} else if (this.epTheme === "yellow") {
} else if (state.epTheme === "yellow") {
return "#d25f00";
} else {
return "#fff";
@@ -30,13 +32,14 @@ export const useEpThemeStore = defineStore({
},
actions: {
setEpThemeColor(newColor: string): void {
const layout =
storageLocal().getItem<StorageConfigs>("responsive-layout");
const layout = storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}layout`
);
this.epTheme = layout?.theme;
this.epThemeColor = newColor;
if (!layout) return;
layout.epThemeColor = newColor;
storageLocal().setItem("responsive-layout", layout);
storageLocal().setItem(`${responsiveStorageNameSpace()}layout`, layout);
}
}
});

View File

@@ -1,39 +1,48 @@
import { defineStore } from "pinia";
import { store } from "@/store";
import { isEqual } from "@pureadmin/utils";
import { routerArrays } from "@/layout/types";
import { multiType, positionType } from "./types";
import { isUrl, storageLocal } from "@pureadmin/utils";
import { responsiveStorageNameSpace } from "@/config";
import { isEqual, isBoolean, isUrl, storageLocal } from "@pureadmin/utils";
export const useMultiTagsStore = defineStore({
id: "pure-multiTags",
state: () => ({
// 存储标签页信息(路由信息)
multiTags: storageLocal().getItem<StorageConfigs>("responsive-configure")
?.multiTagsCache
? storageLocal().getItem<StorageConfigs>("responsive-tags")
multiTags: storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}configure`
)?.multiTagsCache
? storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}tags`
)
: [...routerArrays],
multiTagsCache: storageLocal().getItem<StorageConfigs>(
"responsive-configure"
`${responsiveStorageNameSpace()}configure`
)?.multiTagsCache
}),
getters: {
getMultiTagsCache() {
return this.multiTagsCache;
getMultiTagsCache(state) {
return state.multiTagsCache;
}
},
actions: {
multiTagsCacheChange(multiTagsCache: boolean) {
this.multiTagsCache = multiTagsCache;
if (multiTagsCache) {
storageLocal().setItem("responsive-tags", this.multiTags);
storageLocal().setItem(
`${responsiveStorageNameSpace()}tags`,
this.multiTags
);
} else {
storageLocal().removeItem("responsive-tags");
storageLocal().removeItem(`${responsiveStorageNameSpace()}tags`);
}
},
tagsCache(multiTags) {
this.getMultiTagsCache &&
storageLocal().setItem("responsive-tags", multiTags);
storageLocal().setItem(
`${responsiveStorageNameSpace()}tags`,
multiTags
);
},
handleTags<T>(
mode: string,
@@ -54,6 +63,9 @@ export const useMultiTagsStore = defineStore({
if (isUrl(tagVal?.name)) return;
// 如果title为空拒绝添加空信息到标签页
if (tagVal?.meta?.title.length === 0) return;
// showLink:false 不添加到标签页
if (isBoolean(tagVal?.meta?.showLink) && !tagVal?.meta?.showLink)
return;
const tagPath = tagVal.path;
// 判断tag是否已存在
const tagHasExits = this.multiTags.some(tag => {

View File

@@ -2,6 +2,8 @@ import { defineStore } from "pinia";
import { store } from "@/store";
import { cacheType } from "./types";
import { constantMenus } from "@/router";
import { getKeyList } from "@pureadmin/utils";
import { useMultiTagsStoreHook } from "./multiTags";
import { ascending, filterTree, filterNoPermissionTree } from "@/router/utils";
export const usePermissionStore = defineStore({
@@ -22,17 +24,32 @@ export const usePermissionStore = defineStore({
);
},
cacheOperate({ mode, name }: cacheType) {
const delIndex = this.cachePageList.findIndex(v => v === name);
switch (mode) {
case "refresh":
this.cachePageList = this.cachePageList.filter(v => v !== name);
break;
case "add":
this.cachePageList.push(name);
this.cachePageList = [...new Set(this.cachePageList)];
break;
case "delete":
// eslint-disable-next-line no-case-declarations
const delIndex = this.cachePageList.findIndex(v => v === name);
delIndex !== -1 && this.cachePageList.splice(delIndex, 1);
break;
}
/** 监听缓存页面是否存在于标签页,不存在则删除 */
(() => {
let cacheLength = this.cachePageList.length;
const nameList = getKeyList(useMultiTagsStoreHook().multiTags, "name");
while (cacheLength > 0) {
nameList.findIndex(v => v === this.cachePageList[cacheLength - 1]) ===
-1 &&
this.cachePageList.splice(
this.cachePageList.indexOf(this.cachePageList[cacheLength - 1]),
1
);
cacheLength--;
}
})();
},
/** 清空缓存页面 */
clearAllCachePage() {

View File

@@ -11,20 +11,19 @@ export const useSettingStore = defineStore({
hiddenSideBar: getConfig().HiddenSideBar
}),
getters: {
getTitle() {
return this.title;
getTitle(state) {
return state.title;
},
getFixedHeader() {
return this.fixedHeader;
getFixedHeader(state) {
return state.fixedHeader;
},
getHiddenSideBar() {
return this.HiddenSideBar;
getHiddenSideBar(state) {
return state.hiddenSideBar;
}
},
actions: {
CHANGE_SETTING({ key, value }) {
// eslint-disable-next-line no-prototype-builtins
if (this.hasOwnProperty(key)) {
if (Reflect.has(this, key)) {
this[key] = value;
}
},

View File

@@ -30,8 +30,8 @@ html.dark {
.tags-view {
.arrow-left,
.arrow-right {
box-shadow: none;
border-right: 1px solid $border-style;
box-shadow: none;
}
.arrow-right {
@@ -44,6 +44,7 @@ html.dark {
.el-divider__text {
--el-bg-color: var(--el-bg-color);
}
.el-divider--horizontal {
border-top: none;
}
@@ -53,14 +54,18 @@ html.dark {
.el-table__cell {
background: var(--el-bg-color);
}
.el-card {
--el-card-bg-color: var(--el-bg-color);
// border: none !important;
}
.el-backtop {
--el-backtop-bg-color: var(--el-color-primary-light-9);
--el-backtop-hover-bg-color: var(--el-color-primary);
}
.el-dropdown-menu__item:not(.is-disabled):hover {
background: transparent;
}
@@ -72,18 +77,18 @@ html.dark {
&.el-message-box__close,
&.el-notification__closeBtn {
&:hover {
color: rgba(255, 255, 255, 0.85) !important;
background-color: rgba(255, 255, 255, 0.12);
color: rgb(255 255 255 / 85%) !important;
background-color: rgb(255 255 255 / 12%);
}
}
}
/* 克隆并自定义 ElMessage 样式,不会影响 ElMessage 原本样式,在 src/utils/message.ts 中调用自定义样式 ElMessage 方法即可,非暗黑模式在 src/style/element-plus.scss 文件进行了适配 */
.pure-message {
background-color: rgb(36 37 37) !important;
background-image: initial !important;
background-color: rgb(36, 37, 37) !important;
box-shadow: rgb(13 13 13 / 12%) 0px 3px 6px -4px,
rgb(13 13 13 / 8%) 0px 6px 16px 0px, rgb(13 13 13 / 5%) 0px 9px 28px 8px !important;
box-shadow: rgb(13 13 13 / 12%) 0 3px 6px -4px,
rgb(13 13 13 / 8%) 0 6px 16px 0, rgb(13 13 13 / 5%) 0 9px 28px 8px !important;
& .el-message__content {
color: $color-white !important;
@@ -93,8 +98,8 @@ html.dark {
& .el-message__closeBtn {
&:hover {
color: rgba(255, 255, 255, 0.85);
background-color: rgba(255, 255, 255, 0.12);
color: rgb(255 255 255 / 85%);
background-color: rgb(255 255 255 / 12%);
}
}
}

View File

@@ -78,6 +78,7 @@
}
}
}
.el-icon {
&.el-dialog__close,
&.el-drawer__close,
@@ -85,22 +86,23 @@
&.el-notification__closeBtn {
width: 24px;
height: 24px;
outline: none;
border-radius: 4px;
outline: none;
transition: background-color 0.2s, color 0.2s;
&:hover {
color: rgba(0, 0, 0, 0.88) !important;
background-color: rgba(0, 0, 0, 0.06);
color: rgb(0 0 0 / 88%) !important;
text-decoration: none;
background-color: rgb(0 0 0 / 6%);
}
}
}
/* 克隆并自定义 ElMessage 样式,不会影响 ElMessage 原本样式,在 src/utils/message.ts 中调用自定义样式 ElMessage 方法即可,暗黑模式在 src/style/dark.scss 文件进行了适配 */
.pure-message {
border-width: 0 !important;
background: #fff !important;
padding: 10px 13px !important;
background: #fff !important;
border-width: 0 !important;
box-shadow: 0 3px 6px -4px #0000001f, 0 6px 16px #00000014,
0 9px 28px 8px #0000000d !important;
@@ -119,13 +121,13 @@
}
& .el-message__closeBtn {
outline: none;
border-radius: 4px;
right: 9px !important;
border-radius: 4px;
outline: none;
transition: background-color 0.2s, color 0.2s;
&:hover {
background-color: rgba(0, 0, 0, 0.06);
background-color: rgb(0 0 0 / 6%);
}
}
}

View File

@@ -1,12 +1,12 @@
@import "./mixin.scss";
@import "./transition.scss";
@import "./element-plus.scss";
@import "./sidebar.scss";
@import "./dark.scss";
@import "./transition";
@import "./element-plus";
@import "./sidebar";
@import "./dark";
/* 自定义全局 CssVar */
:root {
--pure-transition-duration: 0.016s;
/* 左侧菜单展开、收起动画时长 */
--pure-transition-duration: 0.3s;
}
/* 灰色模式 */

View File

@@ -1,28 +0,0 @@
@mixin clearfix {
&::after {
content: "";
display: table;
clear: both;
}
}
@mixin relative {
position: relative;
width: 100%;
height: 100%;
}
@mixin scrollBar {
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}

View File

@@ -2,9 +2,9 @@
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: currentColor;
border-style: solid;
border-width: 0;
}
#app {
@@ -13,25 +13,24 @@
}
html {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
-moz-tab-size: 4;
tab-size: 4;
box-sizing: border-box;
width: 100%;
height: 100%;
box-sizing: border-box;
line-height: 1.5;
tab-size: 4;
text-size-adjust: 100%;
}
body {
margin: 0;
line-height: inherit;
width: 100%;
height: 100%;
margin: 0;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
line-height: inherit;
-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;
}
hr {
@@ -69,9 +68,9 @@ small {
sub,
sup {
position: relative;
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
@@ -85,8 +84,8 @@ sup {
table {
text-indent: 0;
border-color: inherit;
border-collapse: collapse;
border-color: inherit;
}
button,
@@ -94,12 +93,12 @@ input,
optgroup,
select,
textarea {
padding: 0;
margin: 0;
font-family: inherit;
font-size: 100%;
line-height: inherit;
color: inherit;
margin: 0;
padding: 0;
}
button,
@@ -160,8 +159,8 @@ pre {
}
fieldset {
margin: 0;
padding: 0;
margin: 0;
}
legend {
@@ -171,9 +170,9 @@ legend {
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
margin: 0;
list-style: none;
}
textarea {
@@ -182,8 +181,8 @@ textarea {
input::placeholder,
textarea::placeholder {
opacity: 1;
color: #9ca3af;
opacity: 1;
}
button,
@@ -238,9 +237,9 @@ a:active {
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
cursor: pointer;
}
div:focus {
@@ -249,11 +248,11 @@ div:focus {
.clearfix {
&::after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
clear: both;
font-size: 0;
visibility: hidden;
content: " ";
}
}

View File

@@ -2,47 +2,50 @@
@mixin merge-style($sideBarWidth) {
$menuActiveText: #7a80b4;
@media screen and (min-width: 150px) and (max-width: 420px) {
@media screen and (width >= 150px) and (width <= 420px) {
.app-main-nofixed-header {
overflow-y: hidden;
}
}
@media screen and (min-width: 420px) {
@media screen and (width >= 420px) {
.app-main-nofixed-header {
overflow: hidden;
}
}
.sub-menu-icon {
vertical-align: middle;
font-size: 18px;
display: inline-flex;
justify-content: center;
align-items: center;
margin-right: 5px;
font-size: 18px;
svg {
width: 18px;
height: 18px;
}
}
.set-icon {
height: 48px;
width: 40px;
display: flex;
cursor: pointer;
align-items: center;
justify-content: center;
width: 40px;
height: 48px;
cursor: pointer;
}
.main-container {
position: relative;
height: 100vh;
min-height: 100%;
/* main-content 属性动画 */
transition: margin-left var(--pure-transition-duration);
margin-left: $sideBarWidth;
position: relative;
background: #f0f2f5;
/* main-content 属性动画 */
transition: margin-left var(--pure-transition-duration);
.el-scrollbar__wrap {
overflow: auto;
height: 100%;
overflow: auto;
}
}
@@ -52,6 +55,7 @@
right: 0;
z-index: 998;
width: calc(100% - 210px);
/* fixed-header 属性左上角动画 */
transition: width var(--pure-transition-duration);
}
@@ -69,20 +73,21 @@
}
.sidebar-container {
/* 展开动画 */
transition: width var(--pure-transition-duration);
width: $sideBarWidth !important;
background: $menuBg;
height: 100%;
position: fixed;
font-size: 0;
top: 0;
bottom: 0;
left: 0;
z-index: 1001;
width: $sideBarWidth !important;
height: 100%;
overflow: hidden;
font-size: 0;
background: $menuBg;
box-shadow: 0 0 1px #888;
/* 展开动画 */
transition: width var(--pure-transition-duration);
.scrollbar-wrapper {
overflow-x: hidden !important;
}
@@ -100,6 +105,7 @@
/* logo: 48px、leftCollapse: 40px、leftCollapse-shadow: 4px */
height: calc(100% - 92px);
}
.el-scrollbar.mobile {
height: 100%;
}
@@ -112,15 +118,15 @@
a {
display: inline-block;
display: flex;
padding-left: 10px;
flex-wrap: wrap;
width: 100%;
padding-left: 10px;
}
.el-menu {
border: none;
height: 100%;
background-color: transparent !important;
border: none;
}
.el-menu-item,
@@ -157,152 +163,72 @@
}
.is-active {
transition: color 0.3s;
color: $subMenuActiveText !important;
transition: color 0.3s;
}
.el-menu-item.is-active.nest-menu > * {
z-index: 1;
color: #fff;
}
.el-menu-item.is-active.nest-menu::before {
position: absolute;
inset: 0 8px;
margin: 4px 0;
clear: both;
content: "";
background: var(--el-color-primary) !important;
border-radius: 3px;
}
.el-menu .el-menu--inline .el-sub-menu__title,
& .el-sub-menu .el-menu-item {
font-size: 12px;
min-width: $sideBarWidth !important;
font-size: 12px;
background-color: $subMenuBg !important;
}
/* 有子集的激活菜单左侧小竖条 */
.el-menu--collapse
.is-active.outer-most.el-sub-menu
> .el-sub-menu__title::before {
position: absolute;
top: 0;
left: 0;
width: 2px;
height: 100%;
clear: both;
content: "";
background-color: $menuActiveBefore;
transition: all var(--pure-transition-duration) ease-in-out;
transform: translateY(0);
}
.el-menu--collapse .outer-most.el-sub-menu > .el-sub-menu__title::before {
position: absolute;
top: 50%;
display: block;
width: 3px;
height: 0;
content: "";
transform: translateY(-50%);
}
/* 无子集的激活菜单背景 */
.is-active.submenu-title-noDropdown.outer-most {
background: $subMenuActiveBg !important;
.is-active.submenu-title-noDropdown.outer-most > * {
z-index: 1;
color: #fff;
}
/* 有子集的激活菜单背景 */
.is-active.nest-menu {
background: $subMenuActiveBg !important;
}
}
.horizontal-header {
display: flex;
justify-content: space-around;
background: $menuBg;
width: 100%;
height: 48px;
align-items: center;
.horizontal-header-left {
display: flex;
height: 100%;
width: auto;
min-width: 200px;
align-items: center;
padding-left: 10px;
cursor: pointer;
transition: all 0.125s ease;
i {
font-size: 30px;
color: #1890ff;
margin-right: 4px;
}
h4 {
font-size: 16px;
font-weight: 700;
color: $subMenuActiveText;
transition: all 0.5s;
}
}
.horizontal-header-menu {
height: 100%;
min-width: 0;
flex: 1;
align-items: center;
}
.horizontal-header-right {
display: flex;
min-width: 340px;
align-items: center;
color: $subMenuActiveText;
justify-content: flex-end;
/* 搜索 */
.search-container,
/* 告警 */
.dropdown-badge,
/* 用户名 */
.el-dropdown-link,
/* 设置 */
.set-icon {
&:hover {
background: $menuHover;
}
}
.dropdown-badge {
height: 48px;
color: $subMenuActiveText;
}
.el-dropdown-link {
height: 48px;
padding: 10px;
display: flex;
cursor: pointer;
align-items: center;
justify-content: space-around;
color: $subMenuActiveText;
p {
font-size: 14px;
}
img {
width: 22px;
height: 22px;
border-radius: 50%;
}
}
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
background-color: transparent;
}
.el-menu-item,
.el-sub-menu__title {
color: $menuText;
&:hover {
color: $menuTitleHover !important;
}
}
.submenu-title-noDropdown,
.el-sub-menu__title {
height: 48px;
line-height: 48px;
background: $menuBg;
svg {
position: static !important;
}
}
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: $subMenuActiveText !important;
i {
color: $subMenuActiveText !important;
}
}
.is-active {
transition: color 0.3s;
color: $subMenuActiveText !important;
.is-active.submenu-title-noDropdown.outer-most::before {
position: absolute;
inset: 0 8px;
margin: 4px 0;
clear: both;
content: "";
background: var(--el-color-primary) !important;
border-radius: 3px;
}
}
@@ -335,8 +261,8 @@
/* 子菜单中还有子菜单 */
.el-menu .el-sub-menu__title {
font-size: 12px;
min-width: $sideBarWidth !important;
font-size: 12px;
background-color: $subMenuBg !important;
}
@@ -353,29 +279,33 @@
}
.is-active {
transition: color 0.3s;
color: $subMenuActiveText !important;
transition: color 0.3s;
}
.el-menu-item.is-active.nest-menu {
background: $subMenuActiveBg !important;
.el-menu-item.is-active.nest-menu > * {
z-index: 1;
color: #fff;
}
.el-menu-item.is-active.nest-menu::before {
position: absolute;
inset: 0 8px;
clear: both;
content: "";
background: var(--el-color-primary) !important;
border-radius: 3px;
}
.el-menu-item,
.el-sub-menu {
// i {
// width: 20px;
// text-align: center;
// font-size: 16px;
// }
.iconfont {
font-size: 18px;
}
// i.fa {
// margin-right: 5px;
// font-size: 16px;
// }
.el-menu-tooltip__trigger {
width: 54px;
padding: 18px !important;
padding: 0;
}
}
}
@@ -412,8 +342,8 @@
/* 子菜单中还有子菜单 */
.el-menu .el-sub-menu__title {
font-size: 12px;
min-width: $sideBarWidth !important;
font-size: 12px;
background-color: $subMenuBg !important;
&:hover {
@@ -437,14 +367,156 @@
}
}
/* 有子集的激活菜单背景 */
.is-active.nest-menu {
background: $subMenuActiveBg !important;
.el-menu-item.is-active {
color: $subMenuActiveText !important;
transition: color 0.3s;
}
.el-menu-item.is-active {
transition: color 0.3s;
.el-menu-item.is-active.nest-menu > * {
z-index: 1;
color: #fff;
}
.el-menu-item.is-active.nest-menu::before {
position: absolute;
inset: 0 5px;
clear: both;
content: "";
background: var(--el-color-primary) !important;
border-radius: 3px;
}
}
.horizontal-header {
display: flex;
align-items: center;
justify-content: space-around;
width: 100%;
height: 48px;
background: $menuBg;
.horizontal-header-left {
display: flex;
align-items: center;
width: auto;
min-width: 200px;
height: 100%;
padding-left: 10px;
cursor: pointer;
transition: all var(--pure-transition-duration) ease;
img {
display: inline-block;
height: 32px;
}
span {
display: inline-block;
height: 32px;
margin: 2px 0 0 12px;
overflow: hidden;
font-size: 18px;
font-weight: 600;
line-height: 32px;
color: $subMenuActiveText;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.horizontal-header-menu {
flex: 1;
align-items: center;
min-width: 0;
height: 100%;
}
.horizontal-header-right {
display: flex;
align-items: center;
justify-content: flex-end;
min-width: 340px;
color: $subMenuActiveText;
/* 搜索 */
.search-container,
/* 告警 */
.dropdown-badge,
/* 用户名 */
.el-dropdown-link,
/* 设置 */
.set-icon {
&:hover {
background: $menuHover;
}
}
.dropdown-badge {
height: 48px;
color: $subMenuActiveText;
}
.el-dropdown-link {
display: flex;
align-items: center;
justify-content: space-around;
height: 48px;
padding: 10px;
color: $subMenuActiveText;
cursor: pointer;
p {
font-size: 14px;
}
img {
width: 22px;
height: 22px;
border-radius: 50%;
}
}
}
.el-menu {
width: 100% !important;
height: 100%;
background-color: transparent;
border: none;
}
.el-menu-item,
.el-sub-menu__title {
padding-right: var(--el-menu-base-level-padding);
color: $menuText;
&:hover {
color: $menuTitleHover !important;
}
}
.submenu-title-noDropdown,
.el-sub-menu__title {
height: 48px;
line-height: 48px;
background: $menuBg;
svg {
position: static !important;
}
}
.is-active > .el-sub-menu__title,
.is-active.submenu-title-noDropdown {
color: $subMenuActiveText !important;
i {
color: $subMenuActiveText !important;
}
}
.is-active {
color: $subMenuActiveText !important;
transition: color 0.3s;
}
}
@@ -452,35 +524,6 @@
min-width: $sideBarWidth !important;
}
/* 有子菜单 */
.el-menu--collapse
.is-active.outer-most.el-sub-menu
> .el-sub-menu__title::before,
/* 无子菜单 */
.el-menu--collapse .is-active.submenu-title-noDropdown.outer-most::before {
position: absolute;
top: 0;
left: 2px;
width: 2px;
height: 100%;
background-color: $menuActiveBefore;
content: "";
clear: both;
transition: all 0.125s ease-in-out;
transform: translateY(0);
}
.el-menu--collapse .outer-most.el-sub-menu > .el-sub-menu__title::before,
.el-menu--collapse .submenu-title-noDropdown.outer-most::before {
content: "";
display: block;
position: absolute;
height: 0;
width: 3px;
transform: translateY(-50%);
top: 50%;
}
/* 手机端 */
.mobile {
.fixed-header {
@@ -493,8 +536,8 @@
}
.sidebar-container {
transition: transform var(--pure-transition-duration);
width: $sideBarWidth;
transition: transform var(--pure-transition-duration);
}
&.hideSidebar {
@@ -509,6 +552,7 @@
body[layout="vertical"] {
$sideBarWidth: 210px;
@include merge-style($sideBarWidth);
.el-menu--collapse {
@@ -526,8 +570,8 @@ body[layout="vertical"] {
}
.sidebar-container {
transition: width 0.125s;
width: 54px !important;
transition: width var(--pure-transition-duration);
.is-active.submenu-title-noDropdown.outer-most {
background: transparent !important;
@@ -543,11 +587,10 @@ body[layout="vertical"] {
.el-sub-menu {
& > .el-sub-menu__title {
& > span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
width: 100%;
height: 100%;
text-align: center;
visibility: visible;
}
}
}
@@ -557,7 +600,7 @@ body[layout="vertical"] {
}
.el-sub-menu__title {
padding: 0 18px !important;
padding: 0;
}
}
@@ -582,16 +625,22 @@ body[layout="vertical"] {
body[layout="horizontal"] {
$sideBarWidth: 0;
@include merge-style($sideBarWidth);
.fixed-header,
.main-container {
transition: none !important;
}
.fixed-header {
width: 100%;
transition: none !important;
}
}
body[layout="mix"] {
$sideBarWidth: 210px;
@include merge-style($sideBarWidth);
.el-menu--collapse {
@@ -609,8 +658,8 @@ body[layout="mix"] {
}
.sidebar-container {
transition: width 0.125s;
width: 54px !important;
transition: width var(--pure-transition-duration);
.is-active.submenu-title-noDropdown.outer-most {
background: transparent !important;
@@ -625,12 +674,13 @@ body[layout="mix"] {
.el-menu--collapse {
.el-sub-menu {
& > .el-sub-menu__title {
padding: 0;
& > span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
width: 100%;
height: 100%;
text-align: center;
visibility: visible;
}
}
}

View File

@@ -31,6 +31,7 @@
}
.breadcrumb-leave-active {
position: absolute;
transition: all 0.3s;
}
@@ -40,10 +41,6 @@
transform: translateX(20px);
}
.breadcrumb-leave-active {
position: absolute;
}
/**
* @description 重置el-menu的展开收起动画时长
*/

View File

@@ -60,7 +60,7 @@ class PureHttp {
/** 请求拦截 */
private httpInterceptorsRequest(): void {
PureHttp.axiosInstance.interceptors.request.use(
async (config: PureHttpRequestConfig) => {
async (config: PureHttpRequestConfig): Promise<any> => {
// 开启进度条动画
NProgress.start();
// 优先判断post/get等方法是否传入回掉否则执行初始化设置等回掉

View File

@@ -13,8 +13,6 @@ const Print = function (dom, options?: object): PrintFunction {
styleStr: "",
// Elements that need to dynamically get and set the height
setDomHeightArr: [],
// Echart dom List
echartDomArr: [],
// Callback before printing
printBeforeFn: null,
// Callback after printing
@@ -73,6 +71,8 @@ Print.prototype = {
const inputs = document.querySelectorAll("input");
const selects = document.querySelectorAll("select");
const textareas = document.querySelectorAll("textarea");
const canvass = document.querySelectorAll("canvas");
for (let k = 0; k < inputs.length; k++) {
if (inputs[k].type == "checkbox" || inputs[k].type == "radio") {
if (inputs[k].checked == true) {
@@ -108,6 +108,15 @@ Print.prototype = {
}
}
for (let k4 = 0; k4 < canvass.length; k4++) {
const imageURL = canvass[k4].toDataURL("image/png");
const img = document.createElement("img");
img.src = imageURL;
img.setAttribute("style", "max-width: 100%;");
img.className = "isNeedRemove";
canvass[k4].parentNode.insertBefore(img, canvass[k4].nextElementSibling);
}
return this.dom.outerHTML;
},
/**
@@ -130,6 +139,12 @@ Print.prototype = {
doc.open();
doc.write(content);
doc.close();
const removes = document.querySelectorAll(".isNeedRemove");
for (let k = 0; k < removes.length; k++) {
removes[k].parentNode.removeChild(removes[k]);
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
iframe.onload = function (): void {
@@ -137,42 +152,16 @@ Print.prototype = {
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);
});
_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
*/

View File

@@ -1,5 +1,10 @@
import { CSSProperties, VNodeChild } from "vue";
import { createTypes, VueTypeValidableDef, VueTypesInterface } from "vue-types";
import type { CSSProperties, VNodeChild } from "vue";
import {
createTypes,
toValidableType,
VueTypesInterface,
VueTypeValidableDef
} from "vue-types";
export type VueNode = VNodeChild | JSX.Element;
@@ -8,7 +13,7 @@ type PropTypes = VueTypesInterface & {
readonly VNodeChild: VueTypeValidableDef<VueNode>;
};
const propTypes = createTypes({
const newPropTypes = createTypes({
func: undefined,
bool: undefined,
string: undefined,
@@ -17,18 +22,18 @@ const propTypes = createTypes({
integer: undefined
}) as PropTypes;
propTypes.extend([
{
name: "style",
getter: true,
type: [String, Object],
default: undefined
},
{
name: "VNodeChild",
getter: true,
type: undefined
// 从 vue-types v5.0 开始extend()方法已经废弃当前已改为官方推荐的ES6+方法 https://dwightjack.github.io/vue-types/advanced/extending-vue-types.html#the-extend-method
export default class propTypes extends newPropTypes {
// a native-like validator that supports the `.validable` method
static get style() {
return toValidableType("style", {
type: [String, Object]
});
}
]);
export { propTypes };
static get VNodeChild() {
return toValidableType("VNodeChild", {
type: undefined
});
}
}

View File

@@ -2,10 +2,10 @@
import { App } from "vue";
import Storage from "responsive-storage";
import { routerArrays } from "@/layout/types";
const nameSpace = "responsive-";
import { responsiveStorageNameSpace } from "@/config";
export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
const nameSpace = responsiveStorageNameSpace();
const configObj = Object.assign(
{
// layout模式以及主题
@@ -27,7 +27,7 @@ export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
},
config.MultiTagsCache
? {
// 默认显示首页tag
// 默认显示顶级菜单tag
tags: Storage.getData("tags", nameSpace) ?? routerArrays
}
: {}

View File

@@ -1,9 +1,12 @@
<script setup lang="ts">
import { useRouter } from "vue-router";
import noAccess from "@/assets/status/403.svg?component";
defineOptions({
name: "403"
});
const router = useRouter();
</script>
<template>
@@ -46,7 +49,7 @@ defineOptions({
</p>
<el-button
type="primary"
@click="$router.push('/')"
@click="router.push('/')"
v-motion
:initial="{
opacity: 0,

View File

@@ -1,9 +1,12 @@
<script setup lang="ts">
import { useRouter } from "vue-router";
import noExist from "@/assets/status/404.svg?component";
defineOptions({
name: "404"
});
const router = useRouter();
</script>
<template>
@@ -46,7 +49,7 @@ defineOptions({
</p>
<el-button
type="primary"
@click="$router.push('/')"
@click="router.push('/')"
v-motion
:initial="{
opacity: 0,

View File

@@ -1,9 +1,12 @@
<script setup lang="ts">
import { useRouter } from "vue-router";
import noServer from "@/assets/status/500.svg?component";
defineOptions({
name: "500"
});
const router = useRouter();
</script>
<template>
@@ -46,7 +49,7 @@ defineOptions({
</p>
<el-button
type="primary"
@click="$router.push('/')"
@click="router.push('/')"
v-motion
:initial="{
opacity: 0,

View File

@@ -3,11 +3,11 @@ import Motion from "./utils/motion";
import { useRouter } from "vue-router";
import { message } from "@/utils/message";
import { loginRules } from "./utils/rule";
import { initRouter } from "@/router/utils";
import { useNav } from "@/layout/hooks/useNav";
import type { FormInstance } from "element-plus";
import { useLayout } from "@/layout/hooks/useLayout";
import { useUserStoreHook } from "@/store/modules/user";
import { initRouter, getTopMenu } from "@/router/utils";
import { bg, avatar, illustration } from "./utils/static";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { ref, reactive, toRaw, onMounted, onBeforeUnmount } from "vue";
@@ -48,7 +48,7 @@ const onLogin = async (formEl: FormInstance | undefined) => {
if (res.success) {
// 获取后端路由
initRouter().then(() => {
router.push("/");
router.push(getTopMenu(true).path);
message("登录成功", { type: "success" });
});
}

View File

@@ -1,20 +1,39 @@
module.exports = {
root: true,
plugins: ["stylelint-order"],
customSyntax: "postcss-html",
extends: ["stylelint-config-standard", "stylelint-config-prettier"],
extends: [
"stylelint-config-standard",
"stylelint-config-html/vue",
"stylelint-config-recess-order"
],
plugins: ["stylelint-order", "stylelint-prettier", "stylelint-scss"],
overrides: [
{
files: ["**/*.(css|html|vue)"],
customSyntax: "postcss-html"
},
{
files: ["*.scss", "**/*.scss"],
customSyntax: "postcss-scss",
extends: [
"stylelint-config-standard-scss",
"stylelint-config-recommended-vue/scss"
]
}
],
rules: {
"selector-class-pattern": null,
"no-descending-specificity": null,
"scss/dollar-variable-pattern": null,
"selector-pseudo-class-no-unknown": [
true,
{
ignorePseudoClasses: ["global"]
ignorePseudoClasses: ["deep", "global"]
}
],
"selector-pseudo-element-no-unknown": [
true,
{
ignorePseudoElements: ["v-deep"]
ignorePseudoElements: ["v-deep", "v-global", "v-slotted"]
}
],
"at-rule-no-unknown": [
@@ -30,17 +49,11 @@ module.exports = {
"if",
"each",
"include",
"mixin"
"mixin",
"use"
]
}
],
"no-empty-source": null,
"named-grid-areas-no-invalid": null,
"unicode-bom": "never",
"no-descending-specificity": null,
"font-family-no-missing-generic-family-keyword": null,
"declaration-colon-space-after": "always-single-line",
"declaration-colon-space-before": "never",
"rule-empty-line-before": [
"always",
{
@@ -67,26 +80,5 @@ module.exports = {
{ severity: "warning" }
]
},
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"]
}
]
}
}
]
ignoreFiles: ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx"]
};

View File

@@ -10,10 +10,8 @@ module.exports = {
colors: {
bg_color: "var(--el-bg-color)",
primary: "var(--el-color-primary)",
primary_light_9: "var(--el-color-primary-light-9)",
text_color_primary: "var(--el-text-color-primary)",
text_color_regular: "var(--el-text-color-regular)",
text_color_disabled: "var(--el-text-color-disabled)"
text_color_regular: "var(--el-text-color-regular)"
}
}
}

View File

@@ -18,7 +18,6 @@
"allowJs": false,
"resolveJsonModule": true,
"lib": ["dom", "esnext"],
"incremental": true,
"paths": {
"@/*": ["src/*"],
"@build/*": ["build/*"]
@@ -28,10 +27,9 @@
"vite/client",
"element-plus/global",
"@pureadmin/table/volar",
"@pureadmin/descriptions/volar",
"unplugin-vue-define-options/macros-global"
"@pureadmin/descriptions/volar"
],
"typeRoots": ["./node_modules/@types/", "./types"]
"typeRoots": ["./types", "./node_modules/@types/"]
},
"include": [
"mock/*.ts",
@@ -41,5 +39,5 @@
"types/*.d.ts",
"vite.config.ts"
],
"exclude": ["node_modules", "dist", "**/*.js"]
"exclude": ["dist", "**/*.js", "node_modules"]
}

View File

@@ -11,7 +11,7 @@ declare module "vue" {
}
/**
* todohttps://github.com/element-plus/element-plus/blob/dev/global.d.ts#L2
* TODO https://github.com/element-plus/element-plus/blob/dev/global.d.ts#L2
* No need to install @vue/runtime-core
*/
declare module "vue" {

9
types/global.d.ts vendored
View File

@@ -63,6 +63,7 @@ declare global {
VITE_PUBLIC_PATH: string;
VITE_ROUTER_HISTORY: string;
VITE_CDN: boolean;
VITE_HIDE_HOME: string;
VITE_COMPRESSION: ViteCompression;
}
@@ -96,6 +97,7 @@ declare global {
MenuArrowIconNoTransition?: boolean;
CachingAsyncRoutes?: boolean;
TooltipEffect?: Effect;
ResponsiveStorageNameSpace?: string;
}
/**
@@ -176,11 +178,8 @@ declare global {
title: string;
/** 菜单图标 `可选` */
icon?: string | FunctionalComponent | IconifyIcon;
/** 菜单名称右侧的额外图标,支持`fontawesome`、`iconfont`、`element-plus-icon` `可选` */
extraIcon?: {
svg?: boolean;
name?: string;
};
/** 菜单名称右侧的额外图标 */
extraIcon?: string | FunctionalComponent | IconifyIcon;
/** 是否在菜单中显示(默认`true``可选` */
showLink?: boolean;
/** 是否显示父级菜单 `可选` */