Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f27d6cd9e | ||
|
|
4b435d0e0f | ||
|
|
872e0bbd5b | ||
|
|
b6859d7920 | ||
|
|
6e02ae14a0 | ||
|
|
c28066fb1f | ||
|
|
d9ab1b1198 | ||
|
|
4e95672cb4 | ||
|
|
9c0872fa6a | ||
|
|
7678aa64e2 | ||
|
|
1e30b31be2 | ||
|
|
2c887ee1d9 | ||
|
|
635da1dc52 |
21
.dockerignore
Normal file
@@ -0,0 +1,21 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.eslintcache
|
||||
report.html
|
||||
|
||||
yarn.lock
|
||||
npm-debug.log*
|
||||
.pnpm-error.log*
|
||||
.pnpm-debug.log
|
||||
tests/**/coverage/
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
tsconfig.tsbuildinfo
|
||||
3
.env
@@ -1,2 +1,5 @@
|
||||
# 平台本地运行端口号
|
||||
VITE_PORT = 8848
|
||||
|
||||
# 是否隐藏首页 隐藏 true 不隐藏 false (勿删除,VITE_HIDE_HOME只需在.env文件配置)
|
||||
VITE_HIDE_HOME = false
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
VITE_PORT = 8848
|
||||
|
||||
# 开发环境读取配置文件路径
|
||||
VITE_PUBLIC_PATH = /
|
||||
VITE_PUBLIC_PATH = ./
|
||||
|
||||
# 开发环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 线上环境平台打包路径
|
||||
VITE_PUBLIC_PATH = /
|
||||
VITE_PUBLIC_PATH = ./
|
||||
|
||||
# 线上环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 预发布也需要生产环境的行为
|
||||
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
|
||||
NODE_ENV=production
|
||||
# NODE_ENV = development
|
||||
|
||||
VITE_PUBLIC_PATH = /
|
||||
VITE_PUBLIC_PATH = ./
|
||||
|
||||
# 预发布环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||
VITE_ROUTER_HISTORY = "hash"
|
||||
@@ -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"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/dist/*
|
||||
/public/*
|
||||
public/*
|
||||
public/*
|
||||
src/style/reset.scss
|
||||
1
.vscode/extensions.json
vendored
@@ -3,6 +3,7 @@
|
||||
"christian-kohler.path-intellisense",
|
||||
"vscode-icons-team.vscode-icons",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"stylelint.vscode-stylelint",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"dbaeumer.vscode-eslint",
|
||||
|
||||
12
.vscode/vue3.0.code-snippets
vendored
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"Vue3.0快速生成模板": {
|
||||
"scope": "vue",
|
||||
"prefix": "Vue3.0",
|
||||
"body": [
|
||||
"<template>",
|
||||
"\t<div>\n",
|
||||
"\t</div>",
|
||||
"\t<div>test</div>",
|
||||
"</template>\n",
|
||||
"<script lang='ts'>",
|
||||
"export default {",
|
||||
"\tsetup(){",
|
||||
"\t\treturn{\n\n\t\t}",
|
||||
"\t},",
|
||||
"\tsetup() {",
|
||||
"\t\treturn {}",
|
||||
"\t}",
|
||||
"}",
|
||||
"</script>\n",
|
||||
"<style scoped>\n",
|
||||
"<style lang='scss' scoped>\n",
|
||||
"</style>",
|
||||
"$2"
|
||||
],
|
||||
|
||||
6
.vscode/vue3.2.code-snippets
vendored
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"Vue3.2+快速生成模板": {
|
||||
"scope": "vue",
|
||||
"prefix": "Vue3.2+",
|
||||
"body": [
|
||||
"<script setup lang='ts'>",
|
||||
"</script>\n",
|
||||
"<template>",
|
||||
"\t<div>\n",
|
||||
"\t</div>",
|
||||
"\t<div>test</div>",
|
||||
"</template>\n",
|
||||
"<style scoped>\n",
|
||||
"<style lang='scss' scoped>\n",
|
||||
"</style>",
|
||||
"$2"
|
||||
],
|
||||
|
||||
20
.vscode/vue3.3.code-snippets
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"Vue3.3+defineOptions快速生成模板": {
|
||||
"scope": "vue",
|
||||
"prefix": "Vue3.3+",
|
||||
"body": [
|
||||
"<script setup lang='ts'>",
|
||||
"defineOptions({",
|
||||
"\tname: ''",
|
||||
"})",
|
||||
"</script>\n",
|
||||
"<template>",
|
||||
"\t<div>test</div>",
|
||||
"</template>\n",
|
||||
"<style lang='scss' scoped>\n",
|
||||
"</style>",
|
||||
"$2"
|
||||
],
|
||||
"description": "Vue3.3+defineOptions快速生成模板"
|
||||
}
|
||||
}
|
||||
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
||||
FROM node:16-alpine as build-stage
|
||||
|
||||
WORKDIR /app
|
||||
RUN corepack enable
|
||||
RUN corepack prepare pnpm@7.32.1 --activate
|
||||
|
||||
RUN npm config set registry https://registry.npmmirror.com
|
||||
|
||||
COPY .npmrc package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
FROM nginx:stable-alpine as production-stage
|
||||
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
2
LICENSE
@@ -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
|
||||
|
||||
@@ -1,47 +1,36 @@
|
||||
<h1>vue-pure-admin Lite Edition(no i18n version)</h1>
|
||||
|
||||
[](LICENSE)
|
||||
[](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)
|
||||
- [documentation site](https://yiming_chang.gitee.io/pure-admin-doc)
|
||||
|
||||
## Usage
|
||||
## Preview
|
||||
|
||||
### Installation dependencies
|
||||
- [Click me to view the preview station](https://pure-admin-thin.netlify.app/#/login)
|
||||
|
||||
pnpm install
|
||||
## Maintainer
|
||||
|
||||
### Install a package
|
||||
|
||||
pnpm add packageName
|
||||
|
||||
### Uninstall a package
|
||||
|
||||
pnpm remove packageName
|
||||
|
||||
I think you should fork the project first to develop, so that you can pull the update synchronously when I update! ! !
|
||||
|
||||
## Supporting video tutorial
|
||||
|
||||
bilibili: https://www.bilibili.com/video/BV1534y1S7HV/
|
||||
[xiaoxian521](https://github.com/xiaoxian521)
|
||||
|
||||
## ⚠️ 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 [issues](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!
|
||||
In principle, no fees and copyrights are charged, and it is commercially available, but if you need secondary open source (such as using this platform for secondary development and open source, the front-end code must be open source and free), please contact the author for permission! (Free, just take a record)
|
||||
|
||||
[MIT © 2020-present, pure-admin](./LICENSE)
|
||||
|
||||
49
README.md
@@ -1,63 +1,40 @@
|
||||
<h1>vue-pure-admin精简版(非国际化版本)</h1>
|
||||
|
||||
[](LICENSE)
|
||||
[](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://yiming_chang.gitee.io/pure-admin-doc)
|
||||
|
||||
## 预览
|
||||
|
||||
- [查看预览](https://pure-admin-thin.netlify.app/#/login)
|
||||
|
||||
## 维护者
|
||||
|
||||
[xiaoxian521](https://github.com/xiaoxian521)
|
||||
|
||||
## 支持
|
||||
|
||||
如果你觉得这个项目对您有帮助,可以帮作者买一杯果汁 🍹 表示支持
|
||||
|
||||
<img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f69bf13c5b854ed5b699807cafa0e3ce~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp?" width="150px" height="150px" />
|
||||
|
||||
## `QQ` 交流群
|
||||
|
||||
[点击去加入](https://yiming_chang.gitee.io/pure-admin-doc/pages/support/#qq-%E4%BA%A4%E6%B5%81%E7%BE%A4)
|
||||
|
||||
## 用法
|
||||
|
||||
### 安装依赖
|
||||
|
||||
pnpm install
|
||||
|
||||
### 安装一个包
|
||||
|
||||
pnpm add 包名
|
||||
|
||||
### 卸载一个包
|
||||
|
||||
pnpm remove 包名
|
||||
|
||||
我认为你应该先 `fork` 项目去开发,以便我更新时您可以同步拉取更新!!!
|
||||
|
||||
## ⚠️ 注意
|
||||
|
||||
- 精简版不接受任何 `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)
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -10,12 +10,12 @@ const include = [
|
||||
"dayjs",
|
||||
"axios",
|
||||
"pinia",
|
||||
"echarts",
|
||||
"js-cookie",
|
||||
"sortablejs",
|
||||
"pinyin-pro",
|
||||
"@vueuse/core",
|
||||
"@pureadmin/utils",
|
||||
"responsive-storage",
|
||||
"element-resize-detector"
|
||||
"responsive-storage"
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(),
|
||||
|
||||
38
index.html
@@ -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% {
|
||||
|
||||
153
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pure-admin-thin",
|
||||
"version": "3.9.7",
|
||||
"version": "4.5.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
|
||||
@@ -13,10 +13,10 @@
|
||||
"typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
|
||||
"svgo": "svgo -f src/assets/svg -o src/assets/svg",
|
||||
"cloc": "NODE_OPTIONS=--max-old-space-size=4096 cloc . --exclude-dir=node_modules --exclude-lang=YAML",
|
||||
"clean:cache": "rm -rf node_modules && rm -rf .eslintcache && pnpm install",
|
||||
"clean:cache": "rimraf node_modules && rimraf .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 \"**/*.{html,vue,css,scss}\" --fix --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",
|
||||
"@vueuse/motion": "2.0.0-beta.12",
|
||||
"@pureadmin/descriptions": "^1.1.1",
|
||||
"@pureadmin/table": "^2.3.2",
|
||||
"@pureadmin/utils": "^1.9.6",
|
||||
"@vueuse/core": "^10.2.0",
|
||||
"@vueuse/motion": "^2.0.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.2.0",
|
||||
"dayjs": "^1.11.6",
|
||||
"echarts": "^5.4.0",
|
||||
"element-plus": "^2.2.27",
|
||||
"element-resize-detector": "^1.2.4",
|
||||
"js-cookie": "^3.0.1",
|
||||
"axios": "^1.4.0",
|
||||
"dayjs": "^1.11.8",
|
||||
"echarts": "^5.4.2",
|
||||
"element-plus": "^2.3.7",
|
||||
"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",
|
||||
"vue-router": "^4.1.6",
|
||||
"vue-types": "^4.2.1"
|
||||
"pinia": "^2.1.4",
|
||||
"pinyin-pro": "^3.15.2",
|
||||
"qs": "^6.11.2",
|
||||
"responsive-storage": "^2.2.0",
|
||||
"sortablejs": "^1.15.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.2",
|
||||
"vue-types": "^5.1.0"
|
||||
},
|
||||
"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",
|
||||
"@pureadmin/theme": "^3.0.0",
|
||||
"@types/element-resize-detector": "1.1.3",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
"@commitlint/cli": "^17.6.6",
|
||||
"@commitlint/config-conventional": "^17.6.6",
|
||||
"@iconify-icons/ep": "^1.2.12",
|
||||
"@iconify-icons/ri": "^1.2.9",
|
||||
"@iconify/vue": "^4.1.1",
|
||||
"@pureadmin/theme": "^3.1.0",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/mockjs": "^1.0.7",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/node": "^20.3.1",
|
||||
"@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.60.0",
|
||||
"@typescript-eslint/parser": "^5.60.0",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@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.43.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-vue": "^9.15.1",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.2.2",
|
||||
"picocolors": "^1.0.0",
|
||||
"postcss": "^8.4.18",
|
||||
"postcss": "^8.4.24",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-import": "^15.0.0",
|
||||
"postcss-scss": "^4.0.5",
|
||||
"prettier": "^2.5.1",
|
||||
"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",
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-scss": "^4.0.6",
|
||||
"prettier": "^2.8.8",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"rimraf": "^5.0.1",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"sass": "^1.63.6",
|
||||
"sass-loader": "^13.3.2",
|
||||
"stylelint": "^15.9.0",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-recess-order": "^4.2.0",
|
||||
"stylelint-config-recommended": "^12.0.0",
|
||||
"stylelint-config-recommended-scss": "^12.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.1",
|
||||
"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.18.1",
|
||||
"typescript": "5.0.4",
|
||||
"vite": "^4.3.9",
|
||||
"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-mock": "2.9.6",
|
||||
"vite-plugin-remove-console": "^2.1.1",
|
||||
"vite-svg-loader": "^4.0.0",
|
||||
"vue-eslint-parser": "^9.3.1",
|
||||
"vue-tsc": "^1.8.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"ignoreMissing": [
|
||||
"rollup",
|
||||
"webpack"
|
||||
"webpack",
|
||||
"core-js"
|
||||
]
|
||||
},
|
||||
"allowedDeprecatedVersions": {
|
||||
"sourcemap-codec": "*",
|
||||
"w3c-hr-time": "*",
|
||||
"stable": "*"
|
||||
}
|
||||
},
|
||||
"repository": "git@github.com:xiaoxian521/pure-admin-thin.git",
|
||||
"repository": "git@github.com:pure-admin/pure-admin-thin.git",
|
||||
"author": "xiaoxian521",
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
5073
pnpm-lock.yaml
generated
@@ -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
@@ -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 |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"Version": "3.9.7",
|
||||
"Version": "4.5.0",
|
||||
"Title": "PureAdmin",
|
||||
"FixedHeader": true,
|
||||
"HiddenSideBar": false,
|
||||
@@ -17,5 +17,6 @@
|
||||
"ShowModel": "smart",
|
||||
"MenuArrowIconNoTransition": true,
|
||||
"CachingAsyncRoutes": false,
|
||||
"TooltipEffect": "light"
|
||||
"TooltipEffect": "light",
|
||||
"ResponsiveStorageNameSpace": "responsive-"
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
BIN
src/assets/user.jpg
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
29
src/components/ReCol/index.ts
Normal 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() }
|
||||
);
|
||||
}
|
||||
});
|
||||
64
src/components/ReDialog/index.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
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 });
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 更改弹框自身属性值
|
||||
* @param value 属性值
|
||||
* @param key 属性,默认`title`
|
||||
* @param index 弹框索引(默认`0`,代表只有一个弹框,对于嵌套弹框要改哪个弹框的属性值就把该弹框索引赋给`index`)
|
||||
*/
|
||||
const updateDialog = (value: any, key = "title", index = 0) => {
|
||||
dialogStore.value[index][key] = value;
|
||||
};
|
||||
|
||||
/** 关闭所有弹框 */
|
||||
const closeAllDialog = () => {
|
||||
dialogStore.value = [];
|
||||
};
|
||||
|
||||
/** 千万别忘了在下面这三处引入并注册下,放心注册,不使用`addDialog`调用就不会被挂载
|
||||
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L4
|
||||
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L13
|
||||
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L18
|
||||
*/
|
||||
const ReDialog = withInstall(reDialog);
|
||||
|
||||
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };
|
||||
export {
|
||||
ReDialog,
|
||||
dialogStore,
|
||||
addDialog,
|
||||
closeDialog,
|
||||
updateDialog,
|
||||
closeAllDialog
|
||||
};
|
||||
157
src/components/ReDialog/index.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
closeDialog,
|
||||
dialogStore,
|
||||
type EventType,
|
||||
type ButtonProps,
|
||||
type DialogOptions
|
||||
} from "./index";
|
||||
import { ref, computed } from "vue";
|
||||
import { isFunction } from "@pureadmin/utils";
|
||||
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
|
||||
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
|
||||
|
||||
const fullscreen = ref(false);
|
||||
|
||||
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>);
|
||||
};
|
||||
});
|
||||
|
||||
const fullscreenClass = computed(() => {
|
||||
return [
|
||||
"el-icon",
|
||||
"el-dialog__close",
|
||||
"-translate-x-2",
|
||||
"cursor-pointer",
|
||||
"hover:!text-[red]"
|
||||
];
|
||||
});
|
||||
|
||||
function eventsCallBack(
|
||||
event: EventType,
|
||||
options: DialogOptions,
|
||||
index: number
|
||||
) {
|
||||
fullscreen.value = options?.fullscreen ?? false;
|
||||
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
|
||||
class="pure-dialog"
|
||||
v-for="(options, index) in dialogStore"
|
||||
:key="index"
|
||||
v-bind="options"
|
||||
v-model="options.visible"
|
||||
:fullscreen="fullscreen ? true : options?.fullscreen ? true : false"
|
||||
@close="handleClose(options, index)"
|
||||
@opened="eventsCallBack('open', options, index)"
|
||||
@openAutoFocus="eventsCallBack('openAutoFocus', options, index)"
|
||||
@closeAutoFocus="eventsCallBack('closeAutoFocus', options, index)"
|
||||
>
|
||||
<!-- header -->
|
||||
<template
|
||||
v-if="options?.fullscreenIcon || options?.headerRenderer"
|
||||
#header="{ close, titleId, titleClass }"
|
||||
>
|
||||
<div
|
||||
v-if="options?.fullscreenIcon"
|
||||
class="flex items-center justify-between"
|
||||
>
|
||||
<span :id="titleId" :class="titleClass">{{ options?.title }}</span>
|
||||
<i
|
||||
v-if="!options?.fullscreen"
|
||||
:class="fullscreenClass"
|
||||
@click="fullscreen = !fullscreen"
|
||||
>
|
||||
<IconifyIconOffline
|
||||
class="pure-dialog-svg"
|
||||
:icon="
|
||||
options?.fullscreen
|
||||
? ExitFullscreen
|
||||
: fullscreen
|
||||
? ExitFullscreen
|
||||
: Fullscreen
|
||||
"
|
||||
/>
|
||||
</i>
|
||||
</div>
|
||||
<component
|
||||
v-else
|
||||
:is="options?.headerRenderer({ close, titleId, titleClass })"
|
||||
/>
|
||||
</template>
|
||||
<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>
|
||||
218
src/components/ReDialog/type.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
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` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
|
||||
fullscreen?: boolean;
|
||||
/** 是否显示全屏操作图标,默认 `false`,`fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
|
||||
fullscreenIcon?: 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 };
|
||||
5
src/components/RePureTableBar/index.ts
Normal 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);
|
||||
355
src/components/RePureTableBar/src/bar.tsx
Normal file
@@ -0,0 +1,355 @@
|
||||
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
|
||||
import { defineComponent, ref, computed, type PropType, nextTick } from "vue";
|
||||
import {
|
||||
delay,
|
||||
cloneDeep,
|
||||
isBoolean,
|
||||
isFunction,
|
||||
getKeyList
|
||||
} from "@pureadmin/utils";
|
||||
|
||||
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);
|
||||
const filterColumns = cloneDeep(props?.columns).filter(column =>
|
||||
isBoolean(column?.hide)
|
||||
? !column.hide
|
||||
: !(isFunction(column?.hide) && column?.hide())
|
||||
);
|
||||
let checkColumnList = getKeyList(cloneDeep(props?.columns), "label");
|
||||
const checkedColumns = ref(getKeyList(cloneDeep(filterColumns), "label"));
|
||||
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 = getKeyList(cloneDeep(filterColumns), "label");
|
||||
}
|
||||
|
||||
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-2 px-2 pb-2 bg-bg_color">
|
||||
<div class="flex justify-between w-full h-[60px] p-4">
|
||||
{slots?.title ? (
|
||||
slots.title()
|
||||
) : (
|
||||
<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}
|
||||
placement="bottom-start"
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
1
src/components/RePureTableBar/src/svg/collapse.svg
Normal 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 |
1
src/components/RePureTableBar/src/svg/drag.svg
Normal 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 |
1
src/components/RePureTableBar/src/svg/expand.svg
Normal 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 |
1
src/components/RePureTableBar/src/svg/refresh.svg
Normal 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 |
1
src/components/RePureTableBar/src/svg/settings.svg
Normal 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 |
@@ -49,4 +49,7 @@ export const getServerConfig = async (app: App): Promise<undefined> => {
|
||||
});
|
||||
};
|
||||
|
||||
export { getConfig, setConfig };
|
||||
/** 本地响应式存储的命名空间 */
|
||||
const responsiveStorageNameSpace = () => getConfig().ResponsiveStorageNameSpace;
|
||||
|
||||
export { getConfig, setConfig, responsiveStorageNameSpace };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { hasAuth } from "@/router/utils";
|
||||
import { Directive, type DirectiveBinding } from "vue";
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
export const auth: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
@@ -7,7 +7,9 @@ export const auth: Directive = {
|
||||
if (value) {
|
||||
!hasAuth(value) && el.parentNode?.removeChild(el);
|
||||
} else {
|
||||
throw new Error("need auths! Like v-auth=\"['btn.add','btn.edit']\"");
|
||||
throw new Error(
|
||||
"[Directive: auth]: need auths! Like v-auth=\"['btn.add','btn.edit']\""
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
33
src/directives/copy/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { message } from "@/utils/message";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import { copyTextToClipboard } from "@pureadmin/utils";
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
interface CopyEl extends HTMLElement {
|
||||
copyValue: string;
|
||||
}
|
||||
|
||||
/** 文本复制指令(默认双击复制) */
|
||||
export const copy: Directive = {
|
||||
mounted(el: CopyEl, binding: DirectiveBinding) {
|
||||
const { value } = binding;
|
||||
if (value) {
|
||||
el.copyValue = value;
|
||||
const arg = binding.arg ?? "dblclick";
|
||||
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
|
||||
useEventListener(el, arg, () => {
|
||||
const success = copyTextToClipboard(el.copyValue);
|
||||
success
|
||||
? message("复制成功", { type: "success" })
|
||||
: message("复制失败", { type: "error" });
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
'[Directive: copy]: need value! Like v-copy="modelValue"'
|
||||
);
|
||||
}
|
||||
},
|
||||
updated(el: CopyEl, binding: DirectiveBinding) {
|
||||
el.copyValue = binding.value;
|
||||
}
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Directive, type DirectiveBinding, type VNode } from "vue";
|
||||
import elementResizeDetectorMaker from "element-resize-detector";
|
||||
import type { Erd } from "element-resize-detector";
|
||||
import { emitter } from "@/utils/mitt";
|
||||
|
||||
const erd: Erd = elementResizeDetectorMaker({
|
||||
strategy: "scroll"
|
||||
});
|
||||
|
||||
export const resize: Directive = {
|
||||
mounted(el: HTMLElement, binding?: DirectiveBinding, vnode?: VNode) {
|
||||
erd.listenTo(el, elem => {
|
||||
const width = elem.offsetWidth;
|
||||
const height = elem.offsetHeight;
|
||||
if (binding?.instance) {
|
||||
emitter.emit("resize", { detail: { width, height } });
|
||||
} else {
|
||||
vnode.el.dispatchEvent(
|
||||
new CustomEvent("resize", { detail: { width, height } })
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
unmounted(el: HTMLElement) {
|
||||
erd.uninstall(el);
|
||||
}
|
||||
};
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from "./auth";
|
||||
export * from "./elResizeDetector";
|
||||
export * from "./copy";
|
||||
export * from "./longpress";
|
||||
export * from "./optimize";
|
||||
|
||||
63
src/directives/longpress/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
import { subBefore, subAfter, isFunction } from "@pureadmin/utils";
|
||||
|
||||
export const longpress: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
const cb = binding.value;
|
||||
if (cb && isFunction(cb)) {
|
||||
let timer = null;
|
||||
let interTimer = null;
|
||||
let num = 500;
|
||||
let interNum = null;
|
||||
const isInter = binding?.arg?.includes(":") ?? false;
|
||||
|
||||
if (isInter) {
|
||||
num = Number(subBefore(binding.arg, ":"));
|
||||
interNum = Number(subAfter(binding.arg, ":"));
|
||||
} else if (binding.arg) {
|
||||
num = Number(binding.arg);
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
if (interTimer) {
|
||||
clearInterval(interTimer);
|
||||
interTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const onDownInter = (ev: PointerEvent) => {
|
||||
ev.preventDefault();
|
||||
if (interTimer === null) {
|
||||
interTimer = setInterval(() => cb(), interNum);
|
||||
}
|
||||
};
|
||||
|
||||
const onDown = (ev: PointerEvent) => {
|
||||
clear();
|
||||
ev.preventDefault();
|
||||
if (timer === null) {
|
||||
timer = isInter
|
||||
? setTimeout(() => {
|
||||
cb();
|
||||
onDownInter(ev);
|
||||
}, num)
|
||||
: setTimeout(() => cb(), num);
|
||||
}
|
||||
};
|
||||
|
||||
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
|
||||
useEventListener(el, "pointerdown", onDown);
|
||||
useEventListener(el, "pointerup", clear);
|
||||
useEventListener(el, "pointerleave", clear);
|
||||
} else {
|
||||
throw new Error(
|
||||
'[Directive: longpress]: need callback and callback must be a function! Like v-longpress="callback"'
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
55
src/directives/optimize/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
isFunction,
|
||||
isObject,
|
||||
isArray,
|
||||
debounce,
|
||||
throttle
|
||||
} from "@pureadmin/utils";
|
||||
import { useEventListener } from "@vueuse/core";
|
||||
import type { Directive, DirectiveBinding } from "vue";
|
||||
|
||||
/** 防抖(v-optimize或v-optimize:debounce)、节流(v-optimize:throttle)指令 */
|
||||
export const optimize: Directive = {
|
||||
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||
const { value } = binding;
|
||||
const optimizeType = binding.arg ?? "debounce";
|
||||
const type = ["debounce", "throttle"].find(t => t === optimizeType);
|
||||
if (type) {
|
||||
if (value && value.event && isFunction(value.fn)) {
|
||||
let params = value?.params;
|
||||
if (params) {
|
||||
if (isArray(params) || isObject(params)) {
|
||||
params = isObject(params) ? Array.of(params) : params;
|
||||
} else {
|
||||
throw new Error(
|
||||
"[Directive: optimize]: `params` must be an array or object"
|
||||
);
|
||||
}
|
||||
}
|
||||
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
|
||||
useEventListener(
|
||||
el,
|
||||
value.event,
|
||||
type === "debounce"
|
||||
? debounce(
|
||||
params ? () => value.fn(...params) : value.fn,
|
||||
value?.timeout ?? 200,
|
||||
value?.immediate ?? false
|
||||
)
|
||||
: throttle(
|
||||
params ? () => value.fn(...params) : value.fn,
|
||||
value?.timeout ?? 1000
|
||||
)
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
"[Directive: optimize]: `event` and `fn` are required, and `fn` must be a function"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"[Directive: optimize]: only `debounce` and `throttle` are supported"
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -15,6 +15,7 @@ const {
|
||||
onPanel,
|
||||
pureApp,
|
||||
username,
|
||||
userAvatar,
|
||||
avatarsStyle,
|
||||
toggleSideBar
|
||||
} = useNav();
|
||||
@@ -46,10 +47,7 @@ const {
|
||||
<!-- 退出登录 -->
|
||||
<el-dropdown trigger="click">
|
||||
<span class="el-dropdown-link navbar-bg-hover select-none">
|
||||
<img
|
||||
src="https://avatars.githubusercontent.com/u/44761321?v=4"
|
||||
:style="avatarsStyle"
|
||||
/>
|
||||
<img :src="userAvatar" :style="avatarsStyle" />
|
||||
<p v-if="username" class="dark:text-white">{{ username }}</p>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
@@ -82,28 +80,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 +125,9 @@ const {
|
||||
max-width: 120px;
|
||||
|
||||
::v-deep(.el-dropdown-menu__item) {
|
||||
min-width: 100%;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,19 +22,31 @@ notices.value.map(v => (noticesNum.value += v.list.length));
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-tabs :stretch="true" v-model="activeKey" class="dropdown-tabs">
|
||||
<template v-for="item in notices" :key="item.key">
|
||||
<el-tab-pane
|
||||
:label="`${item.name}(${item.list.length})`"
|
||||
:name="`${item.key}`"
|
||||
>
|
||||
<el-scrollbar max-height="330px">
|
||||
<div class="noticeList-container">
|
||||
<NoticeList :list="item.list" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
<el-tabs
|
||||
:stretch="true"
|
||||
v-model="activeKey"
|
||||
class="dropdown-tabs"
|
||||
:style="{ width: notices.length === 0 ? '200px' : '330px' }"
|
||||
>
|
||||
<el-empty
|
||||
v-if="notices.length === 0"
|
||||
description="暂无消息"
|
||||
:image-size="60"
|
||||
/>
|
||||
<span v-else>
|
||||
<template v-for="item in notices" :key="item.key">
|
||||
<el-tab-pane
|
||||
:label="`${item.name}(${item.list.length})`"
|
||||
:name="`${item.key}`"
|
||||
>
|
||||
<el-scrollbar max-height="330px">
|
||||
<div class="noticeList-container">
|
||||
<NoticeList :list="item.list" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-tab-pane>
|
||||
</template>
|
||||
</span>
|
||||
</el-tabs>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -46,8 +58,9 @@ notices.value.map(v => (noticesNum.value += v.list.length));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 48px;
|
||||
width: 60px;
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
|
||||
.header-notice-icon {
|
||||
@@ -56,10 +69,8 @@ notices.value.map(v => (noticesNum.value += v.list.length));
|
||||
}
|
||||
|
||||
.dropdown-tabs {
|
||||
width: 330px;
|
||||
|
||||
.noticeList-container {
|
||||
padding: 15px 24px 0 24px;
|
||||
padding: 15px 24px 0;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__header) {
|
||||
@@ -71,7 +82,7 @@ notices.value.map(v => (noticesNum.value += v.list.length));
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-wrap) {
|
||||
padding: 0 36px 0 36px;
|
||||
padding: 0 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { emitter } from "@/utils/mitt";
|
||||
import { onClickOutside } from "@vueuse/core";
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||
import Close from "@iconify-icons/ep/close";
|
||||
|
||||
const target = ref(null);
|
||||
@@ -27,8 +27,15 @@ onClickOutside(target, (event: any) => {
|
||||
show.value = false;
|
||||
});
|
||||
|
||||
emitter.on("openPanel", () => {
|
||||
show.value = true;
|
||||
onMounted(() => {
|
||||
emitter.on("openPanel", () => {
|
||||
show.value = true;
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 解绑`openPanel`公共事件,防止多次触发
|
||||
emitter.off("openPanel");
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -60,9 +67,9 @@ emitter.on("openPanel", () => {
|
||||
|
||||
<style>
|
||||
.showright-panel {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: calc(100% - 15px);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -71,23 +78,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 +102,9 @@ emitter.on("openPanel", () => {
|
||||
|
||||
.right-panel-background {
|
||||
z-index: 20000;
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
@@ -106,20 +113,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 +135,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,3 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
|
||||
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
|
||||
import { useNav } from "@/layout/hooks/useNav";
|
||||
import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
|
||||
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
|
||||
|
||||
const props = withDefaults(defineProps<{ total: number }>(), {
|
||||
total: 0
|
||||
});
|
||||
|
||||
const { device } = useNav();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-footer text-[#333] dark:text-white">
|
||||
<span class="search-footer-item">
|
||||
@@ -13,16 +27,15 @@
|
||||
<mdiKeyboardEsc class="icon" />
|
||||
关闭
|
||||
</span>
|
||||
<p
|
||||
v-if="device !== 'mobile' && props.total > 0"
|
||||
class="search-footer-total"
|
||||
>
|
||||
共{{ props.total }}项
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
|
||||
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
|
||||
import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
|
||||
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-footer {
|
||||
display: flex;
|
||||
@@ -40,5 +53,10 @@ import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
|
||||
box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff,
|
||||
0 1px 2px 1px #1e235a66;
|
||||
}
|
||||
|
||||
.search-footer-total {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { match } from "pinyin-pro";
|
||||
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 { cloneDeep, isAllEmpty } from "@pureadmin/utils";
|
||||
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";
|
||||
import Search from "@iconify-icons/ri/search-line";
|
||||
|
||||
interface Props {
|
||||
/** 弹窗显隐 */
|
||||
@@ -25,6 +25,8 @@ const props = withDefaults(defineProps<Props>(), {});
|
||||
const router = useRouter();
|
||||
|
||||
const keyword = ref("");
|
||||
const scrollbarRef = ref();
|
||||
const resultRef = ref();
|
||||
const activePath = ref("");
|
||||
const inputRef = ref<HTMLInputElement | null>(null);
|
||||
const resultOptions = shallowRef([]);
|
||||
@@ -32,7 +34,7 @@ const handleSearch = useDebounceFn(search, 300);
|
||||
|
||||
/** 菜单树形结构 */
|
||||
const menusData = computed(() => {
|
||||
return deleteChildren(cloneDeep(usePermissionStoreHook().wholeMenus));
|
||||
return cloneDeep(usePermissionStoreHook().wholeMenus);
|
||||
});
|
||||
|
||||
const show = computed({
|
||||
@@ -44,14 +46,6 @@ const show = computed({
|
||||
}
|
||||
});
|
||||
|
||||
watch(show, async val => {
|
||||
if (val) {
|
||||
/** 自动聚焦 */
|
||||
await nextTick();
|
||||
inputRef.value?.focus();
|
||||
}
|
||||
});
|
||||
|
||||
/** 将菜单树形结构扁平化为一维数组,用于菜单查询 */
|
||||
function flatTree(arr) {
|
||||
const res = [];
|
||||
@@ -68,12 +62,18 @@ function flatTree(arr) {
|
||||
/** 查询 */
|
||||
function search() {
|
||||
const flatMenusData = flatTree(menusData.value);
|
||||
resultOptions.value = flatMenusData.filter(
|
||||
menu =>
|
||||
keyword.value &&
|
||||
menu.meta?.title
|
||||
.toLocaleLowerCase()
|
||||
.includes(keyword.value.toLocaleLowerCase().trim())
|
||||
resultOptions.value = flatMenusData.filter(menu =>
|
||||
keyword.value
|
||||
? menu.meta?.title
|
||||
.toLocaleLowerCase()
|
||||
.includes(keyword.value.toLocaleLowerCase().trim()) ||
|
||||
!isAllEmpty(
|
||||
match(
|
||||
menu.meta?.title.toLocaleLowerCase(),
|
||||
keyword.value.toLocaleLowerCase().trim()
|
||||
)
|
||||
)
|
||||
: false
|
||||
);
|
||||
if (resultOptions.value?.length > 0) {
|
||||
activePath.value = resultOptions.value[0].path;
|
||||
@@ -91,6 +91,11 @@ function handleClose() {
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function scrollTo(index) {
|
||||
const scrollTop = resultRef.value.handleScroll(index);
|
||||
scrollbarRef.value.setScrollTop(scrollTop);
|
||||
}
|
||||
|
||||
/** key up */
|
||||
function handleUp() {
|
||||
const { length } = resultOptions.value;
|
||||
@@ -100,8 +105,10 @@ function handleUp() {
|
||||
);
|
||||
if (index === 0) {
|
||||
activePath.value = resultOptions.value[length - 1].path;
|
||||
scrollTo(resultOptions.value.length - 1);
|
||||
} else {
|
||||
activePath.value = resultOptions.value[index - 1].path;
|
||||
scrollTo(index - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +124,7 @@ function handleDown() {
|
||||
} else {
|
||||
activePath.value = resultOptions.value[index + 1].path;
|
||||
}
|
||||
scrollTo(index + 1);
|
||||
}
|
||||
|
||||
/** key enter */
|
||||
@@ -135,39 +143,56 @@ onKeyStroke("ArrowDown", handleDown);
|
||||
<template>
|
||||
<el-dialog
|
||||
top="5vh"
|
||||
:width="device === 'mobile' ? '80vw' : '50vw'"
|
||||
class="pure-search-dialog"
|
||||
v-model="show"
|
||||
:show-close="false"
|
||||
:width="device === 'mobile' ? '80vw' : '40vw'"
|
||||
:before-close="handleClose"
|
||||
:style="{
|
||||
borderRadius: '6px'
|
||||
}"
|
||||
append-to-body
|
||||
@opened="inputRef.focus()"
|
||||
@closed="inputRef.blur()"
|
||||
>
|
||||
<el-input
|
||||
ref="inputRef"
|
||||
size="large"
|
||||
v-model="keyword"
|
||||
clearable
|
||||
placeholder="请输入关键词搜索"
|
||||
placeholder="搜索菜单"
|
||||
@input="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<span class="el-input__icon">
|
||||
<IconifyIconOffline :icon="Search" />
|
||||
</span>
|
||||
<IconifyIconOffline
|
||||
:icon="Search"
|
||||
class="text-primary w-[24px] h-[24px]"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="search-result-container">
|
||||
<el-empty v-if="resultOptions.length === 0" description="暂无搜索结果" />
|
||||
<SearchResult
|
||||
v-else
|
||||
v-model:value="activePath"
|
||||
:options="resultOptions"
|
||||
@click="handleEnter"
|
||||
/>
|
||||
<el-scrollbar ref="scrollbarRef" max-height="calc(90vh - 140px)">
|
||||
<el-empty
|
||||
v-if="resultOptions.length === 0"
|
||||
description="暂无搜索结果"
|
||||
/>
|
||||
<SearchResult
|
||||
v-else
|
||||
ref="resultRef"
|
||||
v-model:value="activePath"
|
||||
:options="resultOptions"
|
||||
@click="handleEnter"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<template #footer>
|
||||
<SearchFooter />
|
||||
<SearchFooter :total="resultOptions.length" />
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-result-container {
|
||||
margin-top: 20px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useResizeObserver } from "@vueuse/core";
|
||||
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
|
||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||
import { ref, computed, getCurrentInstance, onMounted } from "vue";
|
||||
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
|
||||
import Bookmark2Line from "@iconify-icons/ri/bookmark-2-line";
|
||||
|
||||
@@ -23,8 +24,11 @@ interface Emits {
|
||||
(e: "enter"): void;
|
||||
}
|
||||
|
||||
const resultRef = ref();
|
||||
const innerHeight = ref();
|
||||
const props = withDefaults(defineProps<Props>(), {});
|
||||
const emit = defineEmits<Emits>();
|
||||
const instance = getCurrentInstance()!;
|
||||
|
||||
const itemStyle = computed(() => {
|
||||
return item => {
|
||||
@@ -54,22 +58,46 @@ async function handleMouse(item) {
|
||||
function handleTo() {
|
||||
emit("enter");
|
||||
}
|
||||
|
||||
function resizeResult() {
|
||||
// el-scrollbar max-height="calc(90vh - 140px)"
|
||||
innerHeight.value = window.innerHeight - window.innerHeight / 10 - 140;
|
||||
}
|
||||
|
||||
useResizeObserver(resultRef, () => {
|
||||
resizeResult();
|
||||
});
|
||||
|
||||
function handleScroll(index: number) {
|
||||
const curInstance = instance?.proxy?.$refs[`resultItemRef${index}`];
|
||||
if (!curInstance) return 0;
|
||||
const curRef = curInstance[0] as ElRef;
|
||||
const scrollTop = curRef.offsetTop + 128; // 128 两个result-item(56px+56px=112px)高度加上下margin(8px+8px=16px)
|
||||
return scrollTop > innerHeight.value ? scrollTop - innerHeight.value : 0;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
resizeResult();
|
||||
});
|
||||
|
||||
defineExpose({ handleScroll });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="result">
|
||||
<template v-for="item in options" :key="item.path">
|
||||
<div
|
||||
class="result-item dark:bg-[#1d1d1d]"
|
||||
:style="itemStyle(item)"
|
||||
@click="handleTo"
|
||||
@mouseenter="handleMouse(item)"
|
||||
>
|
||||
<component :is="useRenderIcon(item.meta?.icon ?? Bookmark2Line)" />
|
||||
<span class="result-item-title">{{ item.meta?.title }}</span>
|
||||
<enterOutlined />
|
||||
</div>
|
||||
</template>
|
||||
<div ref="resultRef" class="result">
|
||||
<div
|
||||
v-for="(item, index) in options"
|
||||
:key="item.path"
|
||||
:ref="'resultItemRef' + index"
|
||||
class="result-item dark:bg-[#1d1d1d]"
|
||||
:style="itemStyle(item)"
|
||||
@click="handleTo"
|
||||
@mouseenter="handleMouse(item)"
|
||||
>
|
||||
<component :is="useRenderIcon(item.meta?.icon ?? Bookmark2Line)" />
|
||||
<span class="result-item-title">{{ item.meta?.title }}</span>
|
||||
<enterOutlined />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -81,11 +109,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 {
|
||||
|
||||
@@ -218,7 +218,6 @@ watch($storage, ({ layout }) => {
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
dataThemeChange();
|
||||
/* 初始化项目配置 */
|
||||
nextTick(() => {
|
||||
settings.greyVal &&
|
||||
@@ -418,35 +417,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 +458,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 +490,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 +504,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 +518,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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
20
src/layout/components/sidebar/extraIcon.vue
Normal 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>
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import Search from "../search/index.vue";
|
||||
import Notice from "../notice/index.vue";
|
||||
import { ref, watch, nextTick } from "vue";
|
||||
import SidebarItem from "./sidebarItem.vue";
|
||||
import { isAllEmpty } from "@pureadmin/utils";
|
||||
import { ref, nextTick, computed } from "vue";
|
||||
import { useNav } from "@/layout/hooks/useNav";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
|
||||
@@ -13,25 +14,21 @@ const menuRef = ref();
|
||||
const {
|
||||
route,
|
||||
title,
|
||||
routers,
|
||||
logout,
|
||||
backHome,
|
||||
backTopMenu,
|
||||
onPanel,
|
||||
menuSelect,
|
||||
username,
|
||||
userAvatar,
|
||||
avatarsStyle
|
||||
} = useNav();
|
||||
|
||||
const defaultActive = computed(() =>
|
||||
!isAllEmpty(route.meta?.activePath) ? route.meta.activePath : route.path
|
||||
);
|
||||
|
||||
nextTick(() => {
|
||||
menuRef.value?.handleResize();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
menuSelect(route.path, routers);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -39,17 +36,16 @@ 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
|
||||
ref="menuRef"
|
||||
mode="horizontal"
|
||||
class="horizontal-header-menu"
|
||||
:default-active="route.path"
|
||||
@select="indexPath => menuSelect(indexPath, routers)"
|
||||
:default-active="defaultActive"
|
||||
>
|
||||
<sidebar-item
|
||||
v-for="route in usePermissionStoreHook().wholeMenus"
|
||||
@@ -66,10 +62,7 @@ watch(
|
||||
<!-- 退出登录 -->
|
||||
<el-dropdown trigger="click">
|
||||
<span class="el-dropdown-link navbar-bg-hover">
|
||||
<img
|
||||
src="https://avatars.githubusercontent.com/u/44761321?v=4"
|
||||
:style="avatarsStyle"
|
||||
/>
|
||||
<img :src="userAvatar" :style="avatarsStyle" />
|
||||
<p v-if="username" class="dark:text-white">{{ username }}</p>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
@@ -104,9 +97,9 @@ watch(
|
||||
max-width: 120px;
|
||||
|
||||
::v-deep(.el-dropdown-menu__item) {
|
||||
min-width: 100%;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -24,7 +24,7 @@ const iconClass = computed(() => {
|
||||
"align-middle",
|
||||
"text-primary",
|
||||
"cursor-pointer",
|
||||
"duration-[360ms]",
|
||||
"duration-[100ms]",
|
||||
"hover:text-primary",
|
||||
"dark:hover:!text-white"
|
||||
];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { getTopMenu } from "@/router/utils";
|
||||
import { useNav } from "@/layout/hooks/useNav";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -16,9 +17,9 @@ const { title } = useNav();
|
||||
key="props.collapse"
|
||||
:title="title"
|
||||
class="sidebar-logo-link"
|
||||
to="/"
|
||||
:to="getTopMenu()?.path ?? '/'"
|
||||
>
|
||||
<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 +27,9 @@ const { title } = useNav();
|
||||
key="expand"
|
||||
:title="title"
|
||||
class="sidebar-logo-link"
|
||||
to="/"
|
||||
:to="getTopMenu()?.path ?? '/'"
|
||||
>
|
||||
<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 +41,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import extraIcon from "./extraIcon.vue";
|
||||
import Search from "../search/index.vue";
|
||||
import Notice from "../notice/index.vue";
|
||||
import { isAllEmpty } from "@pureadmin/utils";
|
||||
import { useNav } from "@/layout/hooks/useNav";
|
||||
import { ref, toRaw, watch, onMounted, nextTick } from "vue";
|
||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||
@@ -15,12 +17,12 @@ const defaultActive = ref(null);
|
||||
const {
|
||||
route,
|
||||
device,
|
||||
routers,
|
||||
logout,
|
||||
onPanel,
|
||||
menuSelect,
|
||||
resolvePath,
|
||||
username,
|
||||
userAvatar,
|
||||
getDivStyle,
|
||||
avatarsStyle
|
||||
} = useNav();
|
||||
|
||||
@@ -28,10 +30,9 @@ function getDefaultActive(routePath) {
|
||||
const wholeMenus = usePermissionStoreHook().wholeMenus;
|
||||
/** 当前路由的父级路径 */
|
||||
const parentRoutes = getParentPaths(routePath, wholeMenus)[0];
|
||||
defaultActive.value = findRouteByPath(
|
||||
parentRoutes,
|
||||
wholeMenus
|
||||
)?.children[0]?.path;
|
||||
defaultActive.value = !isAllEmpty(route.meta?.activePath)
|
||||
? route.meta.activePath
|
||||
: findRouteByPath(parentRoutes, wholeMenus)?.children[0]?.path;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -62,7 +63,6 @@ watch(
|
||||
mode="horizontal"
|
||||
class="horizontal-header-menu"
|
||||
:default-active="defaultActive"
|
||||
@select="indexPath => menuSelect(indexPath, routers)"
|
||||
>
|
||||
<el-menu-item
|
||||
v-for="route in usePermissionStoreHook().wholeMenus"
|
||||
@@ -78,15 +78,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>
|
||||
@@ -98,10 +95,7 @@ watch(
|
||||
<!-- 退出登录 -->
|
||||
<el-dropdown trigger="click">
|
||||
<span class="el-dropdown-link navbar-bg-hover select-none">
|
||||
<img
|
||||
src="https://avatars.githubusercontent.com/u/44761321?v=4"
|
||||
:style="avatarsStyle"
|
||||
/>
|
||||
<img :src="userAvatar" :style="avatarsStyle" />
|
||||
<p v-if="username" class="dark:text-white">{{ username }}</p>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
@@ -136,9 +130,9 @@ watch(
|
||||
max-width: 120px;
|
||||
|
||||
::v-deep(.el-dropdown-menu__item) {
|
||||
min-width: 100%;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -5,19 +5,20 @@ import { emitter } from "@/utils/mitt";
|
||||
import SidebarItem from "./sidebarItem.vue";
|
||||
import leftCollapse from "./leftCollapse.vue";
|
||||
import { useNav } from "@/layout/hooks/useNav";
|
||||
import { storageLocal } from "@pureadmin/utils";
|
||||
import { ref, computed, watch, onBeforeMount } from "vue";
|
||||
import { responsiveStorageNameSpace } from "@/config";
|
||||
import { storageLocal, isAllEmpty } from "@pureadmin/utils";
|
||||
import { findRouteByPath, getParentPaths } from "@/router/utils";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
|
||||
|
||||
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 } =
|
||||
useNav();
|
||||
const { device, pureApp, isCollapse, menuSelect, toggleSideBar } = useNav();
|
||||
|
||||
const subMenuData = ref([]);
|
||||
|
||||
@@ -27,7 +28,18 @@ const menuData = computed(() => {
|
||||
: usePermissionStoreHook().wholeMenus;
|
||||
});
|
||||
|
||||
function getSubMenuData(path: string) {
|
||||
const loading = computed(() =>
|
||||
pureApp.layout === "mix" ? false : menuData.value.length === 0 ? true : false
|
||||
);
|
||||
|
||||
const defaultActive = computed(() =>
|
||||
!isAllEmpty(route.meta?.activePath) ? route.meta.activePath : route.path
|
||||
);
|
||||
|
||||
function getSubMenuData() {
|
||||
let path = "";
|
||||
path = defaultActive.value;
|
||||
subMenuData.value = [];
|
||||
// path的上级路由组成的数组
|
||||
const parentPathArr = getParentPaths(
|
||||
path,
|
||||
@@ -42,26 +54,32 @@ function getSubMenuData(path: string) {
|
||||
subMenuData.value = parenetRoute?.children;
|
||||
}
|
||||
|
||||
getSubMenuData(route.path);
|
||||
watch(
|
||||
() => [route.path, usePermissionStoreHook().wholeMenus],
|
||||
() => {
|
||||
if (route.path.includes("/redirect")) return;
|
||||
getSubMenuData();
|
||||
menuSelect(route.path);
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
getSubMenuData();
|
||||
|
||||
onBeforeMount(() => {
|
||||
emitter.on("logoChange", key => {
|
||||
showLogo.value = key;
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [route.path, usePermissionStoreHook().wholeMenus],
|
||||
() => {
|
||||
getSubMenuData(route.path);
|
||||
menuSelect(route.path, routers);
|
||||
}
|
||||
);
|
||||
onBeforeUnmount(() => {
|
||||
// 解绑`logoChange`公共事件,防止多次触发
|
||||
emitter.off("logoChange");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-loading="menuData.length === 0"
|
||||
v-loading="loading"
|
||||
:class="['sidebar-container', showLogo ? 'has-logo' : '']"
|
||||
>
|
||||
<Logo v-if="showLogo" :collapse="isCollapse" />
|
||||
@@ -75,9 +93,8 @@ watch(
|
||||
mode="vertical"
|
||||
class="outer-most select-none"
|
||||
:collapse="isCollapse"
|
||||
:default-active="route.path"
|
||||
:default-active="defaultActive"
|
||||
:collapse-transition="false"
|
||||
@select="indexPath => menuSelect(indexPath, routers)"
|
||||
>
|
||||
<sidebar-item
|
||||
v-for="routes in menuData"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import { emitter } from "@/utils/mitt";
|
||||
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 { useResizeObserver, useFullscreen } from "@vueuse/core";
|
||||
import { isEqual, isAllEmpty, debounce } from "@pureadmin/utils";
|
||||
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
||||
import { useResizeObserver, useDebounceFn, useFullscreen } from "@vueuse/core";
|
||||
import { ref, watch, unref, toRaw, nextTick, onBeforeUnmount } from "vue";
|
||||
|
||||
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
|
||||
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
|
||||
@@ -48,22 +48,26 @@ 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 = () => {
|
||||
const dynamicTagView = async () => {
|
||||
await nextTick();
|
||||
const index = multiTags.value.findIndex(item => {
|
||||
if (item.query) {
|
||||
if (!isAllEmpty(route.query)) {
|
||||
return isEqual(route.query, item.query);
|
||||
} else if (item.params) {
|
||||
} else if (!isAllEmpty(route.params)) {
|
||||
return isEqual(route.params, item.params);
|
||||
} else {
|
||||
return item.path === route.path;
|
||||
return route.path === item.path;
|
||||
}
|
||||
});
|
||||
moveToView(index);
|
||||
};
|
||||
|
||||
const moveToView = (index: number): void => {
|
||||
const moveToView = async (index: number): Promise<void> => {
|
||||
await nextTick();
|
||||
const tabNavPadding = 10;
|
||||
if (!instance.refs["dynamic" + index]) return;
|
||||
const tabItemEl = instance.refs["dynamic" + index][0];
|
||||
@@ -73,8 +77,10 @@ const moveToView = (index: number): void => {
|
||||
const scrollbarDomWidth = scrollbarDom.value
|
||||
? scrollbarDom.value?.offsetWidth
|
||||
: 0;
|
||||
|
||||
// 已有标签页总长度(包含溢出部分)
|
||||
const tabDomWidth = tabDom.value ? tabDom.value?.offsetWidth : 0;
|
||||
|
||||
scrollbarDomWidth <= tabDomWidth
|
||||
? (isShowArrow.value = true)
|
||||
: (isShowArrow.value = false);
|
||||
@@ -126,31 +132,29 @@ const handleScroll = (offset: number): void => {
|
||||
}
|
||||
};
|
||||
|
||||
function dynamicRouteTag(value: string, parentPath: string): void {
|
||||
function dynamicRouteTag(value: string): void {
|
||||
const hasValue = multiTags.value.some(item => {
|
||||
return item.path === value;
|
||||
});
|
||||
|
||||
function concatPath(arr: object[], value: string, parentPath: string) {
|
||||
function concatPath(arr: object[], value: string) {
|
||||
if (!hasValue) {
|
||||
arr.forEach((arrItem: any) => {
|
||||
const pathConcat = parentPath + arrItem.path;
|
||||
if (arrItem.path === value || pathConcat === value) {
|
||||
if (arrItem.path === value || arrItem.path === value) {
|
||||
useMultiTagsStoreHook().handleTags("push", {
|
||||
path: value,
|
||||
parentPath: `/${parentPath.split("/")[1]}`,
|
||||
meta: arrItem.meta,
|
||||
name: arrItem.name
|
||||
});
|
||||
} else {
|
||||
if (arrItem.children && arrItem.children.length > 0) {
|
||||
concatPath(arrItem.children, value, parentPath);
|
||||
concatPath(arrItem.children, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
concatPath(router.options.routes as any, value, parentPath);
|
||||
concatPath(router.options.routes as any, value);
|
||||
}
|
||||
|
||||
/** 刷新路由 */
|
||||
@@ -158,13 +162,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 +188,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 +213,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 +223,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 +237,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 +284,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 +341,7 @@ function disabledMenus(value: boolean) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */
|
||||
/** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是顶级菜单,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */
|
||||
function showMenuModel(
|
||||
currentPath: string,
|
||||
query: object = {},
|
||||
@@ -360,11 +363,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 +375,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 +387,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 +397,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) {
|
||||
@@ -457,7 +460,17 @@ function tagOnClick(item) {
|
||||
// showMenuModel(item?.path, item?.query);
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
watch(route, () => {
|
||||
activeIndex.value = -1;
|
||||
dynamicTagView();
|
||||
});
|
||||
|
||||
watch(isFullscreen, () => {
|
||||
tagsViews[6].icon = Fullscreen;
|
||||
tagsViews[6].text = "全屏";
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (!instance) return;
|
||||
|
||||
// 根据当前路由初始化操作标签页的禁用状态
|
||||
@@ -475,27 +488,25 @@ onBeforeMount(() => {
|
||||
});
|
||||
|
||||
// 接收侧边栏切换传递过来的参数
|
||||
emitter.on("changLayoutRoute", ({ indexPath, parentPath }) => {
|
||||
dynamicRouteTag(indexPath, parentPath);
|
||||
emitter.on("changLayoutRoute", indexPath => {
|
||||
dynamicRouteTag(indexPath);
|
||||
setTimeout(() => {
|
||||
showMenuModel(indexPath);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
watch([route], () => {
|
||||
activeIndex.value = -1;
|
||||
dynamicTagView();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
useResizeObserver(
|
||||
scrollbarDom,
|
||||
useDebounceFn(() => {
|
||||
dynamicTagView();
|
||||
}, 200)
|
||||
debounce(() => dynamicTagView())
|
||||
);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 解绑`tagViewsChange`、`tagViewsShowModel`、`changLayoutRoute`公共事件,防止多次触发
|
||||
emitter.off("tagViewsChange");
|
||||
emitter.off("tagViewsShowModel");
|
||||
emitter.off("changLayoutRoute");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { computed } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { getConfig } from "@/config";
|
||||
import { emitter } from "@/utils/mitt";
|
||||
import { routeMetaType } from "../types";
|
||||
import userAvatar from "@/assets/user.jpg";
|
||||
import { getTopMenu } from "@/router/utils";
|
||||
import { useGlobal } from "@pureadmin/utils";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import { router, remainingPaths } from "@/router";
|
||||
import { computed, type CSSProperties } from "vue";
|
||||
import { useAppStoreHook } from "@/store/modules/app";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||
@@ -20,6 +22,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 +70,8 @@ export function useNav() {
|
||||
useUserStoreHook().logOut();
|
||||
}
|
||||
|
||||
function backHome() {
|
||||
router.push("/welcome");
|
||||
function backTopMenu() {
|
||||
router.push(getTopMenu()?.path);
|
||||
}
|
||||
|
||||
function onPanel() {
|
||||
@@ -85,38 +97,13 @@ export function useNav() {
|
||||
}
|
||||
}
|
||||
|
||||
function menuSelect(indexPath: string, routers): void {
|
||||
if (wholeMenus.value.length === 0) return;
|
||||
if (isRemaining(indexPath)) return;
|
||||
let parentPath = "";
|
||||
const parentPathIndex = indexPath.lastIndexOf("/");
|
||||
if (parentPathIndex > 0) {
|
||||
parentPath = indexPath.slice(0, parentPathIndex);
|
||||
}
|
||||
/** 找到当前路由的信息 */
|
||||
function findCurrentRoute(indexPath: string, routes) {
|
||||
if (!routes) return console.error(errorInfo);
|
||||
return routes.map(item => {
|
||||
if (item.path === indexPath) {
|
||||
if (item.redirect) {
|
||||
findCurrentRoute(item.redirect, item.children);
|
||||
} else {
|
||||
/** 切换左侧菜单 通知标签页 */
|
||||
emitter.emit("changLayoutRoute", {
|
||||
indexPath,
|
||||
parentPath
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (item.children) findCurrentRoute(indexPath, item.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
findCurrentRoute(indexPath, routers);
|
||||
function menuSelect(indexPath: string) {
|
||||
if (wholeMenus.value.length === 0 || isRemaining(indexPath)) return;
|
||||
emitter.emit("changLayoutRoute", indexPath);
|
||||
}
|
||||
|
||||
/** 判断路径是否参与菜单 */
|
||||
function isRemaining(path: string): boolean {
|
||||
function isRemaining(path: string) {
|
||||
return remainingPaths.includes(path);
|
||||
}
|
||||
|
||||
@@ -128,8 +115,9 @@ export function useNav() {
|
||||
logout,
|
||||
routers,
|
||||
$storage,
|
||||
backHome,
|
||||
backTopMenu,
|
||||
onPanel,
|
||||
getDivStyle,
|
||||
changeTitle,
|
||||
toggleSideBar,
|
||||
menuSelect,
|
||||
@@ -138,6 +126,7 @@ export function useNav() {
|
||||
isCollapse,
|
||||
pureApp,
|
||||
username,
|
||||
userAvatar,
|
||||
avatarsStyle,
|
||||
tooltipEffect
|
||||
};
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -3,12 +3,21 @@ import "animate.css";
|
||||
// 引入 src/components/ReIcon/src/offlineIcon.ts 文件中所有使用addIcon添加过的本地图标
|
||||
import "@/components/ReIcon/src/offlineIcon";
|
||||
import { setType } from "./types";
|
||||
import { emitter } from "@/utils/mitt";
|
||||
import { useLayout } from "./hooks/useLayout";
|
||||
import { useResizeObserver } from "@vueuse/core";
|
||||
import { useAppStoreHook } from "@/store/modules/app";
|
||||
import { useSettingStoreHook } from "@/store/modules/settings";
|
||||
import { deviceDetection, useDark, useGlobal } from "@pureadmin/utils";
|
||||
import { h, reactive, computed, onMounted, defineComponent } from "vue";
|
||||
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
|
||||
import {
|
||||
h,
|
||||
ref,
|
||||
reactive,
|
||||
computed,
|
||||
onMounted,
|
||||
onBeforeMount,
|
||||
defineComponent
|
||||
} from "vue";
|
||||
|
||||
import navbar from "./components/navbar.vue";
|
||||
import tag from "./components/tag/index.vue";
|
||||
@@ -18,6 +27,7 @@ import Vertical from "./components/sidebar/vertical.vue";
|
||||
import Horizontal from "./components/sidebar/horizontal.vue";
|
||||
import backTop from "@/assets/svg/back_top.svg?component";
|
||||
|
||||
const appWrapperRef = ref();
|
||||
const { isDark } = useDark();
|
||||
const { layout } = useLayout();
|
||||
const isMobile = deviceDetection();
|
||||
@@ -70,10 +80,10 @@ function toggle(device: string, bool: boolean) {
|
||||
// 判断是否可自动关闭菜单栏
|
||||
let isAutoCloseSidebar = true;
|
||||
|
||||
// 监听容器
|
||||
emitter.on("resize", ({ detail }) => {
|
||||
useResizeObserver(appWrapperRef, entries => {
|
||||
if (isMobile) return;
|
||||
const { width } = detail;
|
||||
const entry = entries[0];
|
||||
const { width } = entry.contentRect;
|
||||
width <= 760 ? setTheme("vertical") : setTheme(useAppStoreHook().layout);
|
||||
/** width app-wrapper类容器宽度
|
||||
* 0 < width <= 760 隐藏侧边栏
|
||||
@@ -88,11 +98,12 @@ emitter.on("resize", ({ detail }) => {
|
||||
toggle("desktop", false);
|
||||
isAutoCloseSidebar = false;
|
||||
}
|
||||
} else if (width > 990) {
|
||||
if (!set.sidebar.isClickCollapse) {
|
||||
toggle("desktop", true);
|
||||
isAutoCloseSidebar = true;
|
||||
}
|
||||
} else if (width > 990 && !set.sidebar.isClickCollapse) {
|
||||
toggle("desktop", true);
|
||||
isAutoCloseSidebar = true;
|
||||
} else {
|
||||
toggle("desktop", false);
|
||||
isAutoCloseSidebar = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -102,6 +113,10 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
useDataThemeChange().dataThemeChange();
|
||||
});
|
||||
|
||||
const layoutHeader = defineComponent({
|
||||
render() {
|
||||
return h(
|
||||
@@ -134,7 +149,7 @@ const layoutHeader = defineComponent({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['app-wrapper', set.classes]" v-resize>
|
||||
<div ref="appWrapperRef" :class="['app-wrapper', set.classes]">
|
||||
<div
|
||||
v-show="
|
||||
set.device === 'mobile' &&
|
||||
@@ -179,20 +194,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 +212,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 {
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
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",
|
||||
meta: {
|
||||
title: "首页",
|
||||
icon: "homeFilled"
|
||||
}
|
||||
}
|
||||
]
|
||||
: [];
|
||||
|
||||
export type routeMetaType = {
|
||||
title?: string;
|
||||
@@ -21,7 +24,6 @@ export type routeMetaType = {
|
||||
|
||||
export type RouteConfigs = {
|
||||
path?: string;
|
||||
parentPath?: string;
|
||||
query?: object;
|
||||
params?: object;
|
||||
meta?: routeMetaType;
|
||||
@@ -58,23 +60,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 = {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "vue-router";
|
||||
import {
|
||||
ascending,
|
||||
getTopMenu,
|
||||
initRouter,
|
||||
isOneOfArray,
|
||||
getHistoryMode,
|
||||
@@ -21,7 +22,7 @@ import {
|
||||
formatFlatteningRoutes
|
||||
} from "./utils";
|
||||
import { buildHierarchyTree } from "@/utils/tree";
|
||||
import { isUrl, openLink, storageSession } from "@pureadmin/utils";
|
||||
import { isUrl, openLink, storageSession, isAllEmpty } from "@pureadmin/utils";
|
||||
|
||||
import remainingRouter from "./modules/remaining";
|
||||
|
||||
@@ -45,13 +46,13 @@ Object.keys(modules).forEach(key => {
|
||||
|
||||
/** 导出处理后的静态路由(三级及以上的路由全部拍成二级) */
|
||||
export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes(
|
||||
formatFlatteningRoutes(buildHierarchyTree(ascending(routes)))
|
||||
formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity))))
|
||||
);
|
||||
|
||||
/** 用于渲染菜单,保持原始层级 */
|
||||
export const constantMenus: Array<RouteComponent> = ascending(routes).concat(
|
||||
...remainingRouter
|
||||
);
|
||||
export const constantMenus: Array<RouteComponent> = ascending(
|
||||
routes.flat(Infinity)
|
||||
).concat(...remainingRouter);
|
||||
|
||||
/** 不参与菜单的路由 */
|
||||
export const remainingPaths = Object.keys(remainingRouter).map(v => {
|
||||
@@ -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) {
|
||||
@@ -85,7 +86,9 @@ export function resetRouter() {
|
||||
if (name && router.hasRoute(name) && meta?.backstage) {
|
||||
router.removeRoute(name);
|
||||
router.options.routes = formatTwoStageRoutes(
|
||||
formatFlatteningRoutes(buildHierarchyTree(ascending(routes)))
|
||||
formatFlatteningRoutes(
|
||||
buildHierarchyTree(ascending(routes.flat(Infinity)))
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -95,13 +98,14 @@ export function resetRouter() {
|
||||
/** 路由白名单 */
|
||||
const whiteList = ["/login"];
|
||||
|
||||
router.beforeEach((to: toRouteType, _from, next) => {
|
||||
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 +128,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 +145,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,17 +153,31 @@ router.beforeEach((to: toRouteType, _from, next) => {
|
||||
path,
|
||||
router.options.routes[0].children
|
||||
);
|
||||
getTopMenu(true);
|
||||
// query、params模式路由传参数的标签页不在此处处理
|
||||
if (route && route.meta?.title) {
|
||||
useMultiTagsStoreHook().handleTags("push", {
|
||||
path: route.path,
|
||||
name: route.name,
|
||||
meta: route.meta
|
||||
});
|
||||
if (isAllEmpty(route.parentId) && route.meta?.backstage) {
|
||||
// 此处为动态顶级路由(目录)
|
||||
const { path, name, meta } = route.children[0];
|
||||
useMultiTagsStoreHook().handleTags("push", {
|
||||
path,
|
||||
name,
|
||||
meta
|
||||
});
|
||||
} else {
|
||||
const { path, name, meta } = route;
|
||||
useMultiTagsStoreHook().handleTags("push", {
|
||||
path,
|
||||
name,
|
||||
meta
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
router.push(to.fullPath);
|
||||
// 确保动态路由完全加入路由列表并且不影响静态路由(注意:动态路由刷新时router.beforeEach可能会触发两次,第一次触发动态路由还未完全添加,第二次动态路由才完全添加到路由列表,如果需要在router.beforeEach做一些判断可以在to.name存在的条件下去判断,这样就只会触发一次)
|
||||
if (isAllEmpty(to.name)) router.push(to.fullPath);
|
||||
});
|
||||
}
|
||||
toCorrectRoute();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -15,10 +15,9 @@ export default [
|
||||
path: "/redirect",
|
||||
component: Layout,
|
||||
meta: {
|
||||
icon: "homeFilled",
|
||||
title: "首页",
|
||||
title: "加载中...",
|
||||
showLink: false,
|
||||
rank: 104
|
||||
rank: 102
|
||||
},
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { defineStore } from "pinia";
|
||||
import { store } from "@/store";
|
||||
import { cacheType } from "./types";
|
||||
import { constantMenus } from "@/router";
|
||||
import { useMultiTagsStoreHook } from "./multiTags";
|
||||
import { debounce, getKeyList } from "@pureadmin/utils";
|
||||
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;
|
||||
}
|
||||
/** 监听缓存页面是否存在于标签页,不存在则删除 */
|
||||
debounce(() => {
|
||||
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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -23,7 +23,6 @@ export type appType = {
|
||||
|
||||
export type multiType = {
|
||||
path: string;
|
||||
parentPath: string;
|
||||
name: string;
|
||||
meta: any;
|
||||
query?: object;
|
||||
|
||||
@@ -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,22 @@ 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%);
|
||||
|
||||
.pure-dialog-svg {
|
||||
color: rgb(255 255 255 / 85%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 克隆并自定义 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,9 +102,40 @@ 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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义菜单搜索样式 */
|
||||
.pure-search-dialog {
|
||||
.el-dialog__footer {
|
||||
box-shadow: 0 -1px 0 0 #555a64, 0 -3px 6px 0 rgb(69 98 155 / 12%);
|
||||
}
|
||||
|
||||
.search-footer {
|
||||
.search-footer-item {
|
||||
color: rgb(235 235 235 / 60%);
|
||||
|
||||
.icon {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ReSegmented 组件 */
|
||||
.pure-segmented {
|
||||
color: rgb(255 255 255 / 65%);
|
||||
background-color: #000;
|
||||
|
||||
.pure-segmented-item-selected {
|
||||
background-color: #1f1f1f;
|
||||
}
|
||||
|
||||
.pure-segmented-item-disabled {
|
||||
color: rgb(255 255 255 / 25%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.pure-dialog {
|
||||
.pure-dialog-svg {
|
||||
color: var(--el-color-info);
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
top: 20px;
|
||||
right: 14px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 全局覆盖element-plus的el-dialog、el-drawer、el-message-box、el-notification组件右上角关闭图标的样式,表现更鲜明 */
|
||||
.el-dialog__headerbtn,
|
||||
.el-message-box__headerbtn {
|
||||
@@ -78,6 +91,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
&.el-dialog__close,
|
||||
&.el-drawer__close,
|
||||
@@ -85,22 +99,27 @@
|
||||
&.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%);
|
||||
|
||||
.pure-dialog-svg {
|
||||
color: rgb(0 0 0 / 88%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 克隆并自定义 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 +138,34 @@
|
||||
}
|
||||
|
||||
& .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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义菜单搜索样式 */
|
||||
.pure-search-dialog {
|
||||
.el-dialog__header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding-bottom: 10px;
|
||||
box-shadow: 0 -1px 0 0 #e0e3e8, 0 -3px 6px 0 rgb(69 98 155 / 12%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/* 灰色模式 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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: " ";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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的展开收起动画时长
|
||||
*/
|
||||
|
||||
@@ -60,10 +60,10 @@ class PureHttp {
|
||||
/** 请求拦截 */
|
||||
private httpInterceptorsRequest(): void {
|
||||
PureHttp.axiosInstance.interceptors.request.use(
|
||||
async (config: PureHttpRequestConfig) => {
|
||||
async (config: PureHttpRequestConfig): Promise<any> => {
|
||||
// 开启进度条动画
|
||||
NProgress.start();
|
||||
// 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
|
||||
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
|
||||
if (typeof config.beforeRequestCallback === "function") {
|
||||
config.beforeRequestCallback(config);
|
||||
return config;
|
||||
@@ -123,7 +123,7 @@ class PureHttp {
|
||||
const $config = response.config;
|
||||
// 关闭进度条动画
|
||||
NProgress.done();
|
||||
// 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
|
||||
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
|
||||
if (typeof $config.beforeResponseCallback === "function") {
|
||||
$config.beforeResponseCallback(response);
|
||||
return response.data;
|
||||
@@ -159,7 +159,7 @@ class PureHttp {
|
||||
...axiosConfig
|
||||
} as PureHttpRequestConfig;
|
||||
|
||||
// 单独处理自定义请求/响应回掉
|
||||
// 单独处理自定义请求/响应回调
|
||||
return new Promise((resolve, reject) => {
|
||||
PureHttp.axiosInstance
|
||||
.request(config)
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import type { Emitter } from "mitt";
|
||||
import mitt from "mitt";
|
||||
|
||||
/** 全局公共事件需要在此处添加类型 */
|
||||
type Events = {
|
||||
resize: {
|
||||
detail: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
};
|
||||
openPanel: string;
|
||||
tagViewsChange: string;
|
||||
tagViewsShowModel: string;
|
||||
logoChange: boolean;
|
||||
changLayoutRoute: {
|
||||
indexPath: string;
|
||||
parentPath: string;
|
||||
};
|
||||
changLayoutRoute: string;
|
||||
};
|
||||
|
||||
export const emitter: Emitter<Events> = mitt<Events>();
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||