Compare commits

..

31 Commits

Author SHA1 Message Date
xiaoxian521
1f27d6cd9e release: update 4.5.0 2023-06-26 12:32:11 +08:00
xiaoxian521
4b435d0e0f chore: 同步完整版代码 2023-06-19 12:04:37 +08:00
xiaoxian521
872e0bbd5b chore: 同步完整版代码 2023-06-15 18:38:39 +08:00
xiaoxian521
b6859d7920 release: update 4.4.0 2023-06-14 16:29:45 +08:00
xiaoxian521
6e02ae14a0 chore: 同步完整版代码 2023-06-07 11:40:26 +08:00
xiaoxian521
c28066fb1f feat: 添加vscode-docker插件 2023-06-05 23:02:54 +08:00
xiaoxian521
d9ab1b1198 release: update 4.3.0 2023-06-05 15:44:49 +08:00
xiaoxian521
4e95672cb4 release: update 4.1.0 2023-05-12 13:27:40 +08:00
xiaoxian521
9c0872fa6a perf: export addPathMatch utils 2023-03-07 21:02:36 +08:00
xiaoxian521
7678aa64e2 chore: update propTypes 2023-03-01 20:03:47 +08:00
xiaoxian521
1e30b31be2 chore: 同步完整版代码 2023-02-28 22:42:12 +08:00
xiaoxian521
2c887ee1d9 fix: 调整登录页 initRouter方法顺序,防止登录页热更新报错 2023-02-12 18:58:39 +08:00
xiaoxian521
635da1dc52 chore: update link 2023-02-08 20:10:08 +08:00
xiaoxian521
6f65e67872 release: update 3.9.7 2022-12-26 14:51:38 +08:00
xiaoxian521
c79862a6c8 chore: update 2022-12-23 18:41:27 +08:00
xiaoxian521
799643a283 docs: update README.md 2022-12-21 11:53:54 +08:00
xiaoxian521
707200c71d release: update 3.9.6 2022-12-19 13:45:28 +08:00
xiaoxian521
bc548d500c chore: update @pureadmin/theme latest 2022-12-15 12:41:02 +08:00
xiaoxian521
4d57c9e3d7 release: update 3.9.5 2022-12-13 14:37:56 +08:00
xiaoxian521
1bcf391513 fix: 修复暗黑主题样式问题 2022-12-09 20:35:50 +08:00
xiaoxian521
30af2b78fe chore: update 2022-12-09 12:45:47 +08:00
xiaoxian521
0b1bd19179 fix: 修复动态路由 rank 问题 2022-12-07 17:22:07 +08:00
xiaoxian521
4ccf200059 perf: 优化路由 rank,当rank 不存在时,根据顺序自动创建,首页路由永远在第一位 2022-12-06 21:18:12 +08:00
xiaoxian521
27056e7560 release: update 3.9.4 2022-12-05 14:28:46 +08:00
xiaoxian521
132fbbade3 release: update 3.9.3 2022-12-04 18:00:16 +08:00
xiaoxian521
c6e25d6933 release: update 3.9.2 2022-12-03 15:20:30 +08:00
xiaoxian521
cabf1f85ef release: update 3.9.1 2022-12-02 20:20:50 +08:00
xiaoxian521
cb3d7cd552 perf: 优化 initRouter ,兼容 sso 场景 2022-12-01 16:05:06 +08:00
xiaoxian521
80453ec4b1 feat: 添加 CachingAsyncRoutes 是否开启动态路由缓存本地,默认 true 2022-12-01 11:42:10 +08:00
xiaoxian521
8ca8bbcee0 chore: update 2022-11-30 18:20:56 +08:00
xiaoxian521
2b67efe771 release: update 3.9.0 2022-11-30 16:33:46 +08:00
129 changed files with 6815 additions and 4154 deletions

21
.dockerignore Normal file
View 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
View File

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

View File

@@ -2,7 +2,7 @@
VITE_PORT = 8848 VITE_PORT = 8848
# 开发环境读取配置文件路径 # 开发环境读取配置文件路径
VITE_PUBLIC_PATH = / VITE_PUBLIC_PATH = ./
# 开发环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数" # 开发环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash" VITE_ROUTER_HISTORY = "hash"

View File

@@ -1,5 +1,5 @@
# 线上环境平台打包路径 # 线上环境平台打包路径
VITE_PUBLIC_PATH = / VITE_PUBLIC_PATH = ./
# 线上环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数" # 线上环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash" VITE_ROUTER_HISTORY = "hash"

View File

@@ -1,8 +1,8 @@
# 预发布也需要生产环境的行为 # 预发布也需要生产环境的行为
# https://cn.vitejs.dev/guide/env-and-mode.html#modes # 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参数" # 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash" VITE_ROUTER_HISTORY = "hash"
@@ -13,4 +13,4 @@ VITE_CDN = true
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件 # 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认 # 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认 # 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION = "both-clear" VITE_COMPRESSION = "none"

View File

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

View File

@@ -3,6 +3,7 @@
"christian-kohler.path-intellisense", "christian-kohler.path-intellisense",
"vscode-icons-team.vscode-icons", "vscode-icons-team.vscode-icons",
"davidanson.vscode-markdownlint", "davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker",
"stylelint.vscode-stylelint", "stylelint.vscode-stylelint",
"bradlc.vscode-tailwindcss", "bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",

View File

@@ -1,19 +1,19 @@
{ {
"Vue3.0快速生成模板": { "Vue3.0快速生成模板": {
"scope": "vue",
"prefix": "Vue3.0", "prefix": "Vue3.0",
"body": [ "body": [
"<template>", "<template>",
"\t<div>\n", "\t<div>test</div>",
"\t</div>",
"</template>\n", "</template>\n",
"<script lang='ts'>", "<script lang='ts'>",
"export default {", "export default {",
"\tsetup(){", "\tsetup() {",
"\t\treturn{\n\n\t\t}", "\t\treturn {}",
"\t},", "\t}",
"}", "}",
"</script>\n", "</script>\n",
"<style scoped>\n", "<style lang='scss' scoped>\n",
"</style>", "</style>",
"$2" "$2"
], ],

View File

@@ -1,14 +1,14 @@
{ {
"Vue3.2+快速生成模板": { "Vue3.2+快速生成模板": {
"scope": "vue",
"prefix": "Vue3.2+", "prefix": "Vue3.2+",
"body": [ "body": [
"<script setup lang='ts'>", "<script setup lang='ts'>",
"</script>\n", "</script>\n",
"<template>", "<template>",
"\t<div>\n", "\t<div>test</div>",
"\t</div>",
"</template>\n", "</template>\n",
"<style scoped>\n", "<style lang='scss' scoped>\n",
"</style>", "</style>",
"$2" "$2"
], ],

20
.vscode/vue3.3.code-snippets vendored Normal file
View 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
View 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;"]

View File

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

View File

@@ -1,47 +1,36 @@
<h1>vue-pure-admin Lite Editionno i18n version</h1> <h1>vue-pure-admin Lite Editionno i18n version</h1>
[![license](https://img.shields.io/github/license/xiaoxian521/vue-pure-admin.svg)](LICENSE) [![license](https://img.shields.io/github/license/pure-admin/vue-pure-admin.svg)](LICENSE)
**English** | [中文](./README.md) **English** | [中文](./README.md)
## Introduce ## 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 ## 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) - [Click Watch UI Design](https://www.bilibili.com/video/BV17g411T7rq)
## Docs ## Docs
- [Click me to view the domestic documentation site](https://yiming_chang.gitee.io/pure-admin-doc) - [documentation site](https://yiming_chang.gitee.io/pure-admin-doc)
- [Click me to view foreign document site](https://xiaoxian521.github.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 [xiaoxian521](https://github.com/xiaoxian521)
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/
## ⚠️ Attention ## ⚠️ 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 ## 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)

View File

@@ -1,65 +1,40 @@
<h1>vue-pure-admin精简版非国际化版本</h1> <h1>vue-pure-admin精简版非国际化版本</h1>
[![license](https://img.shields.io/github/license/xiaoxian521/vue-pure-admin.svg)](LICENSE) [![license](https://img.shields.io/github/license/pure-admin/vue-pure-admin.svg)](LICENSE)
**中文** | [English](./README.en-US.md) **中文** | [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) - [点我查看 UI 设计](https://www.bilibili.com/video/BV17g411T7rq)
## 配套文档 ## 配套保姆级文档
- [点我查看国内文档](https://yiming_chang.gitee.io/pure-admin-doc) - [查看文档](https://yiming_chang.gitee.io/pure-admin-doc)
- [点我查看国外文档站](https://xiaoxian521.github.io/pure-admin-doc)
## 预览
- [查看预览](https://pure-admin-thin.netlify.app/#/login)
## 维护者 ## 维护者
[xiaoxian521](https://github.com/xiaoxian521) [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 交流群
一群已满,下面是二群,群里严禁 `黄``赌``毒``vpn` 等违法行为!
<img src="http://yiming_chang.gitee.io/pure-admin-doc/img/support/qq.png" width="150px" height="225px" />
## 用法
### 安装依赖
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)

View File

@@ -7,7 +7,7 @@ import { Plugin as importToCDN } from "vite-plugin-cdn-import";
* 注意上面提到的仅限外网使用也不是完全肯定的如果你们公司内网部署的有相关js、css文件也可以将下面配置对应改一下整一套内网版cdn * 注意上面提到的仅限外网使用也不是完全肯定的如果你们公司内网部署的有相关js、css文件也可以将下面配置对应改一下整一套内网版cdn
*/ */
export const cdn = importToCDN({ export const cdn = importToCDN({
//prodUrl解释 name: 对应下面modules的nameversion: 自动读取本地package.json中dependencies依赖中对应包的版本号path: 对应下面modules的path //prodUrl解释 name: 对应下面modules的nameversion: 自动读取本地package.json中dependencies依赖中对应包的版本号path: 对应下面modules的path当然也可写完整路径会替换prodUrl
prodUrl: "https://cdn.bootcdn.net/ajax/libs/{name}/{version}/{path}", prodUrl: "https://cdn.bootcdn.net/ajax/libs/{name}/{version}/{path}",
modules: [ modules: [
{ {
@@ -51,12 +51,6 @@ export const cdn = importToCDN({
name: "echarts", name: "echarts",
var: "echarts", var: "echarts",
path: "echarts.min.js" path: "echarts.min.js"
},
{
name: "lodash",
var: "lodash",
// 可写`完整路径`,会替换`prodUrl`
path: "https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js"
} }
] ]
}); });

View File

@@ -6,6 +6,7 @@ const warpperEnv = (envConf: Recordable): ViteEnv => {
VITE_PUBLIC_PATH: "", VITE_PUBLIC_PATH: "",
VITE_ROUTER_HISTORY: "", VITE_ROUTER_HISTORY: "",
VITE_CDN: false, VITE_CDN: false,
VITE_HIDE_HOME: "false",
VITE_COMPRESSION: "none" VITE_COMPRESSION: "none"
}; };
@@ -27,9 +28,4 @@ const warpperEnv = (envConf: Recordable): ViteEnv => {
return ret; return ret;
}; };
/** 获取环境变量 */ export { warpperEnv };
const loadEnv = (): ViteEnv => {
return import.meta.env;
};
export { warpperEnv, loadEnv };

View File

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

View File

@@ -4,22 +4,28 @@
* 尤其当您禁用浏览器缓存时(这种情况只应该发生在调试阶段)必须将对应模块加入到 include里否则会遇到开发环境切换页面卡顿的问题vite 会认为它是一个新的依赖包会重新加载并强制刷新页面),因为它既无法使用浏览器缓存,又没有在本地 node_modules/.vite 里缓存 * 尤其当您禁用浏览器缓存时(这种情况只应该发生在调试阶段)必须将对应模块加入到 include里否则会遇到开发环境切换页面卡顿的问题vite 会认为它是一个新的依赖包会重新加载并强制刷新页面),因为它既无法使用浏览器缓存,又没有在本地 node_modules/.vite 里缓存
* 温馨提示:如果您使用的第三方库是全局引入,也就是引入到 src/main.ts 文件里,就不需要再添加到 include 里了,因为 vite 会自动将它们缓存到 node_modules/.vite * 温馨提示:如果您使用的第三方库是全局引入,也就是引入到 src/main.ts 文件里,就不需要再添加到 include 里了,因为 vite 会自动将它们缓存到 node_modules/.vite
*/ */
export const include = [ const include = [
"qs", "qs",
"mitt", "mitt",
"dayjs", "dayjs",
"axios", "axios",
"pinia", "pinia",
"lodash",
"echarts",
"xe-utils",
"vxe-table",
"js-cookie", "js-cookie",
"lodash-es", "sortablejs",
"pinyin-pro",
"@vueuse/core", "@vueuse/core",
"lodash-unified",
"@ctrl/tinycolor",
"@pureadmin/utils", "@pureadmin/utils",
"responsive-storage", "responsive-storage"
"element-resize-detector"
]; ];
/**
* 在预构建中强制排除的依赖项
* 温馨提示:所有以 `@iconify-icons/` 开头引入的的本地图标模块,都应该加入到下面的 `exclude` 里,因为平台推荐的使用方式是哪里需要哪里引入而且都是单个的引入,不需要预构建,直接让浏览器加载就好
*/
const exclude = [
"@iconify-icons/ep",
"@iconify-icons/ri",
"@pureadmin/theme/dist/browser-utils"
];
export { include, exclude };

View File

@@ -9,7 +9,6 @@ import { configCompressPlugin } from "./compress";
import { visualizer } from "rollup-plugin-visualizer"; import { visualizer } from "rollup-plugin-visualizer";
import removeConsole from "vite-plugin-remove-console"; import removeConsole from "vite-plugin-remove-console";
import themePreprocessorPlugin from "@pureadmin/theme"; import themePreprocessorPlugin from "@pureadmin/theme";
import DefineOptions from "unplugin-vue-define-options/vite";
import { genScssMultipleScopeVars } from "../src/layout/theme"; import { genScssMultipleScopeVars } from "../src/layout/theme";
export function getPluginsList( export function getPluginsList(
@@ -25,7 +24,6 @@ export function getPluginsList(
vueJsx(), vueJsx(),
VITE_CDN ? cdn : null, VITE_CDN ? cdn : null,
configCompressPlugin(VITE_COMPRESSION), configCompressPlugin(VITE_COMPRESSION),
DefineOptions(),
// 线上环境删除console // 线上环境删除console
removeConsole({ external: ["src/assets/iconfont/iconfont.js"] }), removeConsole({ external: ["src/assets/iconfont/iconfont.js"] }),
viteBuildInfo(), viteBuildInfo(),
@@ -33,14 +31,7 @@ export function getPluginsList(
themePreprocessorPlugin({ themePreprocessorPlugin({
scss: { scss: {
multipleScopeVars: genScssMultipleScopeVars(), multipleScopeVars: genScssMultipleScopeVars(),
// 在生产模式是否抽取独立的主题css文件extract为true以下属性有效 extract: true
extract: true,
// 会选取defaultScopeName对应的主题css文件在html添加link
themeLinkTagId: "head",
// "head"||"head-prepend" || "body" ||"body-prepend"
themeLinkTagInjectTo: "head",
// 是否对抽取的css文件内对应scopeName的权重类名移除
removeCssScopeName: false
} }
}), }),
// svg组件化支持 // svg组件化支持

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "pure-admin-thin", "name": "pure-admin-thin",
"version": "3.8.7", "version": "4.5.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite", "dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
@@ -13,10 +13,10 @@
"typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck", "typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
"svgo": "svgo -f src/assets/svg -o src/assets/svg", "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", "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: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:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{vue,css,scss,postcss,less}\" --cache --cache-location node_modules/.cache/stylelint/", "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:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
"lint:pretty": "pretty-quick --staged", "lint:pretty": "pretty-quick --staged",
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint", "lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
@@ -29,106 +29,107 @@
"not op_mini all" "not op_mini all"
], ],
"dependencies": { "dependencies": {
"@ctrl/tinycolor": "^3.4.1", "@pureadmin/descriptions": "^1.1.1",
"@pureadmin/descriptions": "^1.1.0", "@pureadmin/table": "^2.3.2",
"@pureadmin/table": "^1.8.0", "@pureadmin/utils": "^1.9.6",
"@pureadmin/utils": "^1.6.7", "@vueuse/core": "^10.2.0",
"@vueuse/core": "^9.6.0", "@vueuse/motion": "^2.0.0",
"@vueuse/motion": "2.0.0-beta.12",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"axios": "^1.2.0", "axios": "^1.4.0",
"dayjs": "^1.11.6", "dayjs": "^1.11.8",
"echarts": "^5.4.0", "echarts": "^5.4.2",
"element-plus": "^2.2.25", "element-plus": "^2.3.7",
"element-resize-detector": "^1.2.4", "js-cookie": "^3.0.5",
"js-cookie": "^3.0.1",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lodash-unified": "^1.0.2",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"path": "^0.12.7", "path": "^0.12.7",
"pinia": "^2.0.26", "pinia": "^2.1.4",
"qs": "^6.11.0", "pinyin-pro": "^3.15.2",
"responsive-storage": "^2.1.0", "qs": "^6.11.2",
"vue": "^3.2.45", "responsive-storage": "^2.2.0",
"vue-router": "^4.1.6", "sortablejs": "^1.15.0",
"vue-types": "^4.2.1", "vue": "^3.3.4",
"vxe-table": "^4.3.6", "vue-router": "^4.2.2",
"xe-utils": "^3.5.7" "vue-types": "^5.1.0"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "13.1.0", "@commitlint/cli": "^17.6.6",
"@commitlint/config-conventional": "13.1.0", "@commitlint/config-conventional": "^17.6.6",
"@iconify-icons/ep": "^1.2.7", "@iconify-icons/ep": "^1.2.12",
"@iconify-icons/ri": "^1.2.3", "@iconify-icons/ri": "^1.2.9",
"@iconify/vue": "^4.0.0", "@iconify/vue": "^4.1.1",
"@pureadmin/theme": "^2.4.0", "@pureadmin/theme": "^3.1.0",
"@types/element-resize-detector": "1.1.3", "@types/js-cookie": "^3.0.3",
"@types/js-cookie": "^3.0.1",
"@types/lodash": "^4.14.180",
"@types/lodash-es": "^4.17.6",
"@types/mockjs": "^1.0.7", "@types/mockjs": "^1.0.7",
"@types/node": "^18.11.9", "@types/node": "^20.3.1",
"@types/nprogress": "0.2.0", "@types/nprogress": "0.2.0",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.42.1", "@types/sortablejs": "^1.15.1",
"@typescript-eslint/parser": "^5.42.1", "@typescript-eslint/eslint-plugin": "^5.60.0",
"@vitejs/plugin-legacy": "^2.3.1", "@typescript-eslint/parser": "^5.60.0",
"@vitejs/plugin-vue": "^3.2.0", "@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^2.1.1", "@vitejs/plugin-vue-jsx": "^3.0.1",
"@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^11.0.2", "@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.14",
"cloc": "^2.10.0", "cloc": "^2.11.0",
"cssnano": "^5.1.14", "cssnano": "^6.0.1",
"eslint": "^8.8.0", "eslint": "^8.43.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.7.0", "eslint-plugin-vue": "^9.15.1",
"font-awesome": "^4.7.0", "husky": "^8.0.3",
"husky": "^7.0.4", "lint-staged": "^13.2.2",
"lint-staged": "11.1.2",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"postcss": "^8.4.18", "postcss": "^8.4.24",
"postcss-html": "^1.5.0", "postcss-html": "^1.5.0",
"postcss-import": "^15.0.0", "postcss-import": "^15.1.0",
"postcss-scss": "^4.0.5", "postcss-scss": "^4.0.6",
"prettier": "^2.5.1", "prettier": "^2.8.8",
"pretty-quick": "3.1.1", "pretty-quick": "^3.1.3",
"rimraf": "3.0.2", "rimraf": "^5.0.1",
"rollup-plugin-visualizer": "^5.8.3", "rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.56.1", "sass": "^1.63.6",
"sass-loader": "^13.2.0", "sass-loader": "^13.3.2",
"stylelint": "^14.3.0", "stylelint": "^15.9.0",
"stylelint-config-html": "^1.0.0", "stylelint-config-html": "^1.1.0",
"stylelint-config-prettier": "^9.0.3", "stylelint-config-recess-order": "^4.2.0",
"stylelint-config-recommended": "^9.0.0", "stylelint-config-recommended": "^12.0.0",
"stylelint-config-standard": "^29.0.0", "stylelint-config-recommended-scss": "^12.0.0",
"stylelint-order": "^5.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", "svgo": "^3.0.2",
"tailwindcss": "^3.2.4", "tailwindcss": "^3.3.2",
"terser": "^5.15.1", "terser": "^5.18.1",
"typescript": "^4.9.3", "typescript": "5.0.4",
"unplugin-vue-define-options": "^1.0.0", "vite": "^4.3.9",
"vite": "3.1.8",
"vite-plugin-cdn-import": "^0.3.5", "vite-plugin-cdn-import": "^0.3.5",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-mock": "^2.9.6", "vite-plugin-mock": "2.9.6",
"vite-plugin-remove-console": "^1.3.0", "vite-plugin-remove-console": "^2.1.1",
"vite-svg-loader": "^3.6.0", "vite-svg-loader": "^4.0.0",
"vue-eslint-parser": "^9.1.0", "vue-eslint-parser": "^9.3.1",
"vue-tsc": "^1.0.9" "vue-tsc": "^1.8.1"
}, },
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {
"ignoreMissing": [ "ignoreMissing": [
"rollup", "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", "author": "xiaoxian521",
"license": "MIT" "license": "MIT"
} }

5599
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

1
public/logo.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 712 B

View File

@@ -1,5 +1,5 @@
{ {
"Version": "3.8.7", "Version": "4.5.0",
"Title": "PureAdmin", "Title": "PureAdmin",
"FixedHeader": true, "FixedHeader": true,
"HiddenSideBar": false, "HiddenSideBar": false,
@@ -15,5 +15,8 @@
"EpThemeColor": "#409EFF", "EpThemeColor": "#409EFF",
"ShowLogo": true, "ShowLogo": true,
"ShowModel": "smart", "ShowModel": "smart",
"MenuArrowIconNoTransition": true "MenuArrowIconNoTransition": true,
"CachingAsyncRoutes": false,
"TooltipEffect": "light",
"ResponsiveStorageNameSpace": "responsive-"
} }

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src/assets/user.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

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

View File

@@ -0,0 +1,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
};

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

View 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 };

View File

@@ -34,6 +34,16 @@ export function useRenderIcon(icon: any, attrs?: iconType): Component {
} else if (typeof icon === "function" || typeof icon?.render === "function") { } else if (typeof icon === "function" || typeof icon?.render === "function") {
// svg // svg
return icon; return icon;
} else if (typeof icon === "object") {
return defineComponent({
name: "OfflineIcon",
render() {
return h(IconifyIconOffline, {
icon: icon,
...attrs
});
}
});
} else { } else {
// 通过是否存在 : 符号来判断是在线还是本地图标,存在即是在线图标,反之 // 通过是否存在 : 符号来判断是在线还是本地图标,存在即是在线图标,反之
return defineComponent({ return defineComponent({

View File

@@ -1,88 +1,22 @@
import { h, defineComponent } from "vue"; import { h, defineComponent } from "vue";
import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline"; import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline";
// element-plus icon
import Check from "@iconify-icons/ep/check";
import HomeFilled from "@iconify-icons/ep/home-filled";
import Lollipop from "@iconify-icons/ep/lollipop";
import RefreshRight from "@iconify-icons/ep/refresh-right";
import Close from "@iconify-icons/ep/close";
import CloseBold from "@iconify-icons/ep/close-bold";
import Bell from "@iconify-icons/ep/bell";
import Search from "@iconify-icons/ep/search";
import EpArrowDown from "@iconify-icons/ep/arrow-down";
import ArrowUp from "@iconify-icons/ep/arrow-up";
import ArrowRight from "@iconify-icons/ep/arrow-right";
import ArrowLeft from "@iconify-icons/ep/arrow-left";
addIcon("check", Check);
addIcon("home-filled", HomeFilled);
addIcon("lollipop", Lollipop);
addIcon("refresh-right", RefreshRight);
addIcon("close", Close);
addIcon("close-bold", CloseBold);
addIcon("bell", Bell);
addIcon("search", Search);
addIcon("ep-arrow-down", EpArrowDown);
addIcon("ep-arrow-up", ArrowUp);
addIcon("ep-arrow-right", ArrowRight);
addIcon("ep-arrow-left", ArrowLeft);
// remixicon
import ArrowRightSLine from "@iconify-icons/ri/arrow-right-s-line";
import ArrowLeftSLine from "@iconify-icons/ri/arrow-left-s-line";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import InformationLine from "@iconify-icons/ri/information-line";
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
import Bookmark2Line from "@iconify-icons/ri/bookmark-2-line";
import User from "@iconify-icons/ri/user-3-fill";
import Lock from "@iconify-icons/ri/lock-fill";
import MenuUnfold from "@iconify-icons/ri/menu-unfold-fill";
import MenuFold from "@iconify-icons/ri/menu-fold-fill";
import Setting from "@iconify-icons/ri/settings-3-line";
import ArrowDown from "@iconify-icons/ri/arrow-down-s-line";
import CloseLeftTags from "@iconify-icons/ri/text-direction-r";
import CloseRightTags from "@iconify-icons/ri/text-direction-l";
import CloseOtherTags from "@iconify-icons/ri/text-spacing";
import CloseAllTags from "@iconify-icons/ri/subtract-line";
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
addIcon("arrow-right-s-line", ArrowRightSLine);
addIcon("arrow-left-s-line", ArrowLeftSLine);
addIcon("logout-circle-r-line", LogoutCircleRLine);
addIcon("information-line", InformationLine);
addIcon("arrow-up-line", ArrowUpLine);
addIcon("arrow-down-line", ArrowDownLine);
addIcon("bookmark-2-line", Bookmark2Line);
addIcon("user", User);
addIcon("lock", Lock);
addIcon("menu-unfold", MenuUnfold);
addIcon("menu-fold", MenuFold);
addIcon("setting", Setting);
addIcon("arrow-down", ArrowDown);
addIcon("close-left-tags", CloseLeftTags);
addIcon("close-right-tags", CloseRightTags);
addIcon("close-other-tags", CloseOtherTags);
addIcon("close-all-tags", CloseAllTags);
addIcon("fullscreen", Fullscreen);
addIcon("exit-fullscreen", ExitFullscreen);
// Iconify Icon在Vue里本地使用用于内网环境https://docs.iconify.design/icon-components/vue/offline.html // Iconify Icon在Vue里本地使用用于内网环境https://docs.iconify.design/icon-components/vue/offline.html
export default defineComponent({ export default defineComponent({
name: "IconifyIconOffline", name: "IconifyIconOffline",
components: { IconifyIcon }, components: { IconifyIcon },
props: { props: {
icon: { icon: {
type: String, default: null
default: ""
} }
}, },
render() { render() {
if (typeof this.icon === "object") addIcon(this.icon, this.icon);
const attrs = this.$attrs; const attrs = this.$attrs;
return h( return h(
IconifyIcon, IconifyIcon,
{ {
icon: `${this.icon}`, icon: this.icon,
style: attrs?.style style: attrs?.style
? Object.assign(attrs.style, { outline: "none" }) ? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" }, : { outline: "none" },

View File

@@ -0,0 +1,14 @@
import { addIcon } from "@iconify/vue/dist/offline";
/**
* 这里存放本地图标,在 src/layout/index.vue 文件中加载,避免在首启动加载
*/
// 本地菜单图标后端在路由的icon中返回对应的图标字符串并且前端在此处使用addIcon添加即可渲染菜单图标
import HomeFilled from "@iconify-icons/ep/home-filled";
import InformationLine from "@iconify-icons/ri/information-line";
import Lollipop from "@iconify-icons/ep/lollipop";
addIcon("homeFilled", HomeFilled);
addIcon("informationLine", InformationLine);
addIcon("lollipop", Lollipop);

View File

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

View File

@@ -0,0 +1,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>
</>
);
}
});

View File

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

After

Width:  |  Height:  |  Size: 441 B

View File

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

After

Width:  |  Height:  |  Size: 398 B

View File

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

After

Width:  |  Height:  |  Size: 163 B

View File

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

After

Width:  |  Height:  |  Size: 235 B

View File

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

After

Width:  |  Height:  |  Size: 1011 B

View File

@@ -1,9 +1,8 @@
import { App } from "vue"; import { App } from "vue";
import axios from "axios"; import axios from "axios";
import { loadEnv } from "@build/index";
let config: object = {}; let config: object = {};
const { VITE_PUBLIC_PATH } = loadEnv(); const { VITE_PUBLIC_PATH } = import.meta.env;
const setConfig = (cfg?: unknown) => { const setConfig = (cfg?: unknown) => {
config = Object.assign(config, cfg); config = Object.assign(config, cfg);
@@ -50,4 +49,7 @@ export const getServerConfig = async (app: App): Promise<undefined> => {
}); });
}; };
export { getConfig, setConfig }; /** 本地响应式存储的命名空间 */
const responsiveStorageNameSpace = () => getConfig().ResponsiveStorageNameSpace;
export { getConfig, setConfig, responsiveStorageNameSpace };

View File

@@ -1,13 +1,15 @@
import { hasAuth } from "@/router/utils"; import { hasAuth } from "@/router/utils";
import { Directive, type DirectiveBinding } from "vue"; import type { Directive, DirectiveBinding } from "vue";
export const auth: Directive = { export const auth: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) { mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding; const { value } = binding;
if (value) { if (value) {
!hasAuth(value) && el.parentNode.removeChild(el); !hasAuth(value) && el.parentNode?.removeChild(el);
} else { } 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']\""
);
} }
} }
}; };

View 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;
}
};

View File

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

View File

@@ -1,2 +1,4 @@
export * from "./auth"; export * from "./auth";
export * from "./elResizeDetector"; export * from "./copy";
export * from "./longpress";
export * from "./optimize";

View 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"'
);
}
}
};

View 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"
);
}
}
};

View File

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

View File

@@ -5,6 +5,8 @@ import mixNav from "./sidebar/mixNav.vue";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import Breadcrumb from "./sidebar/breadCrumb.vue"; import Breadcrumb from "./sidebar/breadCrumb.vue";
import topCollapse from "./sidebar/topCollapse.vue"; import topCollapse from "./sidebar/topCollapse.vue";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import Setting from "@iconify-icons/ri/settings-3-line";
const { const {
layout, layout,
@@ -13,6 +15,7 @@ const {
onPanel, onPanel,
pureApp, pureApp,
username, username,
userAvatar,
avatarsStyle, avatarsStyle,
toggleSideBar toggleSideBar
} = useNav(); } = useNav();
@@ -44,17 +47,14 @@ const {
<!-- 退出登录 --> <!-- 退出登录 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none"> <span class="el-dropdown-link navbar-bg-hover select-none">
<img <img :src="userAvatar" :style="avatarsStyle" />
src="https://avatars.githubusercontent.com/u/44761321?v=4"
:style="avatarsStyle"
/>
<p v-if="username" class="dark:text-white">{{ username }}</p> <p v-if="username" class="dark:text-white">{{ username }}</p>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="logout"> <el-dropdown-menu class="logout">
<el-dropdown-item @click="logout"> <el-dropdown-item @click="logout">
<IconifyIconOffline <IconifyIconOffline
icon="logout-circle-r-line" :icon="LogoutCircleRLine"
style="margin: 5px" style="margin: 5px"
/> />
退出系统 退出系统
@@ -67,7 +67,7 @@ const {
title="打开项目配置" title="打开项目配置"
@click="onPanel" @click="onPanel"
> >
<IconifyIconOffline icon="setting" /> <IconifyIconOffline :icon="Setting" />
</span> </span>
</div> </div>
</div> </div>
@@ -80,28 +80,28 @@ const {
overflow: hidden; overflow: hidden;
.hamburger-container { .hamburger-container {
line-height: 48px;
height: 100%;
float: left; float: left;
height: 100%;
line-height: 48px;
cursor: pointer; cursor: pointer;
} }
.vertical-header-right { .vertical-header-right {
display: flex; display: flex;
align-items: center;
justify-content: flex-end;
min-width: 280px; min-width: 280px;
height: 48px; height: 48px;
align-items: center;
color: #000000d9; color: #000000d9;
justify-content: flex-end;
.el-dropdown-link { .el-dropdown-link {
height: 48px;
padding: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-around; justify-content: space-around;
cursor: pointer; height: 48px;
padding: 10px;
color: #000000d9; color: #000000d9;
cursor: pointer;
p { p {
font-size: 14px; font-size: 14px;
@@ -125,9 +125,9 @@ const {
max-width: 120px; max-width: 120px;
::v-deep(.el-dropdown-menu__item) { ::v-deep(.el-dropdown-menu__item) {
min-width: 100%;
display: inline-flex; display: inline-flex;
flex-wrap: wrap; flex-wrap: wrap;
min-width: 100%;
} }
} }
</style> </style>

View File

@@ -2,6 +2,7 @@
import { ref } from "vue"; import { ref } from "vue";
import { noticesData } from "./data"; import { noticesData } from "./data";
import NoticeList from "./noticeList.vue"; import NoticeList from "./noticeList.vue";
import Bell from "@iconify-icons/ep/bell";
const noticesNum = ref(0); const noticesNum = ref(0);
const notices = ref(noticesData); const notices = ref(noticesData);
@@ -15,25 +16,37 @@ notices.value.map(v => (noticesNum.value += v.list.length));
<span class="dropdown-badge navbar-bg-hover select-none"> <span class="dropdown-badge navbar-bg-hover select-none">
<el-badge :value="noticesNum" :max="99"> <el-badge :value="noticesNum" :max="99">
<span class="header-notice-icon"> <span class="header-notice-icon">
<IconifyIconOffline icon="bell" /> <IconifyIconOffline :icon="Bell" />
</span> </span>
</el-badge> </el-badge>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-tabs :stretch="true" v-model="activeKey" class="dropdown-tabs"> <el-tabs
<template v-for="item in notices" :key="item.key"> :stretch="true"
<el-tab-pane v-model="activeKey"
:label="`${item.name}(${item.list.length})`" class="dropdown-tabs"
:name="`${item.key}`" :style="{ width: notices.length === 0 ? '200px' : '330px' }"
> >
<el-scrollbar max-height="330px"> <el-empty
<div class="noticeList-container"> v-if="notices.length === 0"
<NoticeList :list="item.list" /> description="暂无消息"
</div> :image-size="60"
</el-scrollbar> />
</el-tab-pane> <span v-else>
</template> <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-tabs>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
@@ -45,8 +58,9 @@ notices.value.map(v => (noticesNum.value += v.list.length));
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 40px;
height: 48px; height: 48px;
width: 60px; margin-right: 10px;
cursor: pointer; cursor: pointer;
.header-notice-icon { .header-notice-icon {
@@ -55,10 +69,8 @@ notices.value.map(v => (noticesNum.value += v.list.length));
} }
.dropdown-tabs { .dropdown-tabs {
width: 330px;
.noticeList-container { .noticeList-container {
padding: 15px 24px 0 24px; padding: 15px 24px 0;
} }
:deep(.el-tabs__header) { :deep(.el-tabs__header) {
@@ -69,14 +81,8 @@ notices.value.map(v => (noticesNum.value += v.list.length));
height: 1px; height: 1px;
} }
// 如果上面的 notices 长度大于 3 请注释掉下面代码
:deep(.el-tabs__nav-wrap) { :deep(.el-tabs__nav-wrap) {
padding: 0 36px 0 36px; padding: 0 36px;
}
// 如果上面的 notices 长度大于 3 请注释掉下面代码
:deep(.el-tabs__active-bar) {
margin: 0 36px 0 36px;
} }
} }
</style> </style>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ListItem } from "./data"; import { ListItem } from "./data";
import { ref, PropType, nextTick } from "vue"; import { ref, PropType, nextTick } from "vue";
import { useNav } from "@/layout/hooks/useNav";
import { deviceDetection } from "@pureadmin/utils";
const props = defineProps({ const props = defineProps({
noticeItem: { noticeItem: {
@@ -13,6 +15,8 @@ const titleRef = ref(null);
const titleTooltip = ref(false); const titleTooltip = ref(false);
const descriptionRef = ref(null); const descriptionRef = ref(null);
const descriptionTooltip = ref(false); const descriptionTooltip = ref(false);
const { tooltipEffect } = useNav();
const isMobile = deviceDetection();
function hoverTitle() { function hoverTitle() {
nextTick(() => { nextTick(() => {
@@ -57,9 +61,11 @@ function hoverDescription(event, description) {
<div class="notice-text-title text-[#000000d9] dark:text-white"> <div class="notice-text-title text-[#000000d9] dark:text-white">
<el-tooltip <el-tooltip
popper-class="notice-title-popper" popper-class="notice-title-popper"
:effect="tooltipEffect"
:disabled="!titleTooltip" :disabled="!titleTooltip"
:content="props.noticeItem.title" :content="props.noticeItem.title"
placement="top-start" placement="top-start"
:enterable="!isMobile"
> >
<div <div
ref="titleRef" ref="titleRef"
@@ -81,6 +87,7 @@ function hoverDescription(event, description) {
<el-tooltip <el-tooltip
popper-class="notice-title-popper" popper-class="notice-title-popper"
:effect="tooltipEffect"
:disabled="!descriptionTooltip" :disabled="!descriptionTooltip"
:content="props.noticeItem.description" :content="props.noticeItem.description"
placement="top-start" placement="top-start"
@@ -111,6 +118,7 @@ function hoverDescription(event, description) {
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
padding: 12px 0; padding: 12px 0;
// border-bottom: 1px solid #f0f0f0; // border-bottom: 1px solid #f0f0f0;
.notice-container-avatar { .notice-container-avatar {
@@ -120,15 +128,15 @@ function hoverDescription(event, description) {
.notice-container-text { .notice-container-text {
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
flex: 1;
.notice-text-title { .notice-text-title {
display: flex; display: flex;
margin-bottom: 8px; margin-bottom: 8px;
font-weight: 400;
font-size: 14px; font-size: 14px;
font-weight: 400;
line-height: 1.5715; line-height: 1.5715;
cursor: pointer; cursor: pointer;
@@ -136,8 +144,8 @@ function hoverDescription(event, description) {
flex: 1; flex: 1;
width: 200px; width: 200px;
overflow: hidden; overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
} }
.notice-title-extra { .notice-title-extra {
@@ -155,8 +163,8 @@ function hoverDescription(event, description) {
.notice-text-description { .notice-text-description {
display: -webkit-box; display: -webkit-box;
text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }

View File

@@ -1,17 +1,41 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue";
import { onClickOutside } from "@vueuse/core";
import { emitter } from "@/utils/mitt"; 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 show = ref<Boolean>(false);
const target = ref(null); const target = ref(null);
const show = ref<Boolean>(false);
const iconClass = computed(() => {
return [
"mr-[20px]",
"outline-none",
"width-[20px]",
"height-[20px]",
"rounded-[4px]",
"cursor-pointer",
"transition-colors",
"hover:bg-[#0000000f]",
"dark:hover:bg-[#ffffff1f]",
"dark:hover:text-[#ffffffd9]"
];
});
onClickOutside(target, (event: any) => { onClickOutside(target, (event: any) => {
if (event.clientX > target.value.offsetLeft) return; if (event.clientX > target.value.offsetLeft) return;
show.value = false; show.value = false;
}); });
emitter.on("openPanel", () => { onMounted(() => {
show.value = true; emitter.on("openPanel", () => {
show.value = true;
});
});
onBeforeUnmount(() => {
// 解绑`openPanel`公共事件,防止多次触发
emitter.off("openPanel");
}); });
</script> </script>
@@ -22,10 +46,12 @@ emitter.on("openPanel", () => {
<div class="right-panel-items"> <div class="right-panel-items">
<div class="project-configuration"> <div class="project-configuration">
<h4 class="dark:text-white">项目配置</h4> <h4 class="dark:text-white">项目配置</h4>
<span title="关闭配置"> <span title="关闭配置" :class="iconClass">
<IconifyIconOffline <IconifyIconOffline
class="dark:text-white" class="dark:text-white"
icon="close" width="20px"
height="20px"
:icon="Close"
@click="show = !show" @click="show = !show"
/> />
</span> </span>
@@ -41,9 +67,9 @@ emitter.on("openPanel", () => {
<style> <style>
.showright-panel { .showright-panel {
overflow: hidden;
position: relative; position: relative;
width: calc(100% - 15px); width: calc(100% - 15px);
overflow: hidden;
} }
</style> </style>
@@ -52,24 +78,23 @@ emitter.on("openPanel", () => {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
z-index: -1;
background: rgb(0 0 0 / 20%);
opacity: 0; opacity: 0;
transition: opacity 0.3s cubic-bezier(0.7, 0.3, 0.1, 1); 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 { .right-panel {
width: 100%;
max-width: 315px;
height: 100vh;
position: fixed; position: fixed;
top: 0; top: 0;
right: 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); transition: all 0.25s cubic-bezier(0.7, 0.3, 0.1, 1);
transform: translate(100%); transform: translate(100%);
// background: #fff;
z-index: 40000;
} }
.show { .show {
@@ -77,9 +102,9 @@ emitter.on("openPanel", () => {
.right-panel-background { .right-panel-background {
z-index: 20000; z-index: 20000;
opacity: 1;
width: 100%; width: 100%;
height: 100%; height: 100%;
opacity: 1;
} }
.right-panel { .right-panel {
@@ -88,20 +113,20 @@ emitter.on("openPanel", () => {
} }
.handle-button { .handle-button {
position: absolute;
top: 45%;
left: -48px;
z-index: 0;
width: 48px; width: 48px;
height: 48px; height: 48px;
position: absolute;
left: -48px;
text-align: center;
font-size: 24px; font-size: 24px;
border-radius: 6px 0 0 6px !important; line-height: 48px;
z-index: 0; color: #fff;
text-align: center;
pointer-events: auto; pointer-events: auto;
cursor: pointer; cursor: pointer;
color: #fff; background: rgb(24 144 255);
line-height: 48px; border-radius: 6px 0 0 6px !important;
top: 45%;
background: rgb(24, 144, 255);
i { i {
font-size: 24px; font-size: 24px;
@@ -110,34 +135,24 @@ emitter.on("openPanel", () => {
} }
.right-panel-items { .right-panel-items {
margin-top: 60px;
height: calc(100vh - 60px); height: calc(100vh - 60px);
margin-top: 60px;
overflow-y: auto; overflow-y: auto;
} }
.project-configuration { .project-configuration {
position: fixed;
top: 15px;
display: flex; display: flex;
align-items: center;
justify-content: space-between;
width: 100%; width: 100%;
height: 30px; height: 30px;
position: fixed;
justify-content: space-between;
align-items: center;
top: 15px;
margin-left: 10px; margin-left: 10px;
svg {
font-size: 20px;
margin-right: 20px;
&:hover {
cursor: pointer;
color: var(--el-color-primary);
}
}
} }
:deep(.el-divider--horizontal) { :deep(.el-divider--horizontal) {
width: 90%; width: 90%;
margin: 20px auto 0 auto; margin: 20px auto 0;
} }
</style> </style>

View File

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

View File

@@ -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> <template>
<div class="search-footer text-[#333] dark:text-white"> <div class="search-footer text-[#333] dark:text-white">
<span class="search-footer-item"> <span class="search-footer-item">
@@ -5,22 +19,23 @@
确认 确认
</span> </span>
<span class="search-footer-item"> <span class="search-footer-item">
<IconifyIconOffline icon="arrow-up-line" class="icon" /> <IconifyIconOffline :icon="ArrowUpLine" class="icon" />
<IconifyIconOffline icon="arrow-down-line" class="icon" /> <IconifyIconOffline :icon="ArrowDownLine" class="icon" />
切换 切换
</span> </span>
<span class="search-footer-item"> <span class="search-footer-item">
<mdiKeyboardEsc class="icon" /> <mdiKeyboardEsc class="icon" />
关闭 关闭
</span> </span>
<p
v-if="device !== 'mobile' && props.total > 0"
class="search-footer-total"
>
{{ props.total }}
</p>
</div> </div>
</template> </template>
<script setup lang="ts">
import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
.search-footer { .search-footer {
display: flex; display: flex;
@@ -38,5 +53,10 @@ import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff, box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff,
0 1px 2px 1px #1e235a66; 0 1px 2px 1px #1e235a66;
} }
.search-footer-total {
position: absolute;
right: 20px;
}
} }
</style> </style>

View File

@@ -1,13 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { match } from "pinyin-pro";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { cloneDeep } from "lodash-unified";
import SearchResult from "./SearchResult.vue"; import SearchResult from "./SearchResult.vue";
import SearchFooter from "./SearchFooter.vue"; import SearchFooter from "./SearchFooter.vue";
import { deleteChildren } from "@/utils/tree";
import { useNav } from "@/layout/hooks/useNav"; 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 { useDebounceFn, onKeyStroke } from "@vueuse/core";
import { ref, watch, computed, nextTick, shallowRef } from "vue";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
import Search from "@iconify-icons/ri/search-line";
interface Props { interface Props {
/** 弹窗显隐 */ /** 弹窗显隐 */
@@ -24,6 +25,8 @@ const props = withDefaults(defineProps<Props>(), {});
const router = useRouter(); const router = useRouter();
const keyword = ref(""); const keyword = ref("");
const scrollbarRef = ref();
const resultRef = ref();
const activePath = ref(""); const activePath = ref("");
const inputRef = ref<HTMLInputElement | null>(null); const inputRef = ref<HTMLInputElement | null>(null);
const resultOptions = shallowRef([]); const resultOptions = shallowRef([]);
@@ -31,7 +34,7 @@ const handleSearch = useDebounceFn(search, 300);
/** 菜单树形结构 */ /** 菜单树形结构 */
const menusData = computed(() => { const menusData = computed(() => {
return deleteChildren(cloneDeep(usePermissionStoreHook().wholeMenus)); return cloneDeep(usePermissionStoreHook().wholeMenus);
}); });
const show = computed({ const show = computed({
@@ -43,14 +46,6 @@ const show = computed({
} }
}); });
watch(show, async val => {
if (val) {
/** 自动聚焦 */
await nextTick();
inputRef.value?.focus();
}
});
/** 将菜单树形结构扁平化为一维数组,用于菜单查询 */ /** 将菜单树形结构扁平化为一维数组,用于菜单查询 */
function flatTree(arr) { function flatTree(arr) {
const res = []; const res = [];
@@ -67,12 +62,18 @@ function flatTree(arr) {
/** 查询 */ /** 查询 */
function search() { function search() {
const flatMenusData = flatTree(menusData.value); const flatMenusData = flatTree(menusData.value);
resultOptions.value = flatMenusData.filter( resultOptions.value = flatMenusData.filter(menu =>
menu => keyword.value
keyword.value && ? menu.meta?.title
menu.meta?.title .toLocaleLowerCase()
.toLocaleLowerCase() .includes(keyword.value.toLocaleLowerCase().trim()) ||
.includes(keyword.value.toLocaleLowerCase().trim()) !isAllEmpty(
match(
menu.meta?.title.toLocaleLowerCase(),
keyword.value.toLocaleLowerCase().trim()
)
)
: false
); );
if (resultOptions.value?.length > 0) { if (resultOptions.value?.length > 0) {
activePath.value = resultOptions.value[0].path; activePath.value = resultOptions.value[0].path;
@@ -90,6 +91,11 @@ function handleClose() {
}, 200); }, 200);
} }
function scrollTo(index) {
const scrollTop = resultRef.value.handleScroll(index);
scrollbarRef.value.setScrollTop(scrollTop);
}
/** key up */ /** key up */
function handleUp() { function handleUp() {
const { length } = resultOptions.value; const { length } = resultOptions.value;
@@ -99,8 +105,10 @@ function handleUp() {
); );
if (index === 0) { if (index === 0) {
activePath.value = resultOptions.value[length - 1].path; activePath.value = resultOptions.value[length - 1].path;
scrollTo(resultOptions.value.length - 1);
} else { } else {
activePath.value = resultOptions.value[index - 1].path; activePath.value = resultOptions.value[index - 1].path;
scrollTo(index - 1);
} }
} }
@@ -116,6 +124,7 @@ function handleDown() {
} else { } else {
activePath.value = resultOptions.value[index + 1].path; activePath.value = resultOptions.value[index + 1].path;
} }
scrollTo(index + 1);
} }
/** key enter */ /** key enter */
@@ -134,39 +143,56 @@ onKeyStroke("ArrowDown", handleDown);
<template> <template>
<el-dialog <el-dialog
top="5vh" top="5vh"
:width="device === 'mobile' ? '80vw' : '50vw'" class="pure-search-dialog"
v-model="show" v-model="show"
:show-close="false"
:width="device === 'mobile' ? '80vw' : '40vw'"
:before-close="handleClose" :before-close="handleClose"
:style="{
borderRadius: '6px'
}"
append-to-body
@opened="inputRef.focus()"
@closed="inputRef.blur()"
> >
<el-input <el-input
ref="inputRef" ref="inputRef"
size="large"
v-model="keyword" v-model="keyword"
clearable clearable
placeholder="请输入关键词搜索" placeholder="搜索菜单"
@input="handleSearch" @input="handleSearch"
> >
<template #prefix> <template #prefix>
<span class="el-input__icon"> <IconifyIconOffline
<IconifyIconOffline icon="search" /> :icon="Search"
</span> class="text-primary w-[24px] h-[24px]"
/>
</template> </template>
</el-input> </el-input>
<div class="search-result-container"> <div class="search-result-container">
<el-empty v-if="resultOptions.length === 0" description="暂无搜索结果" /> <el-scrollbar ref="scrollbarRef" max-height="calc(90vh - 140px)">
<SearchResult <el-empty
v-else v-if="resultOptions.length === 0"
v-model:value="activePath" description="暂无搜索结果"
:options="resultOptions" />
@click="handleEnter" <SearchResult
/> v-else
ref="resultRef"
v-model:value="activePath"
:options="resultOptions"
@click="handleEnter"
/>
</el-scrollbar>
</div> </div>
<template #footer> <template #footer>
<SearchFooter /> <SearchFooter :total="resultOptions.length" />
</template> </template>
</el-dialog> </el-dialog>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.search-result-container { .search-result-container {
margin-top: 20px; margin-top: 12px;
} }
</style> </style>

View File

@@ -1,8 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { useResizeObserver } from "@vueuse/core";
import { useEpThemeStoreHook } from "@/store/modules/epTheme"; import { useEpThemeStoreHook } from "@/store/modules/epTheme";
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { ref, computed, getCurrentInstance, onMounted } from "vue";
import enterOutlined from "@/assets/svg/enter_outlined.svg?component"; import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
import Bookmark2Line from "@iconify-icons/ri/bookmark-2-line";
interface optionsItem { interface optionsItem {
path: string; path: string;
@@ -22,8 +24,11 @@ interface Emits {
(e: "enter"): void; (e: "enter"): void;
} }
const resultRef = ref();
const innerHeight = ref();
const props = withDefaults(defineProps<Props>(), {}); const props = withDefaults(defineProps<Props>(), {});
const emit = defineEmits<Emits>(); const emit = defineEmits<Emits>();
const instance = getCurrentInstance()!;
const itemStyle = computed(() => { const itemStyle = computed(() => {
return item => { return item => {
@@ -53,22 +58,46 @@ async function handleMouse(item) {
function handleTo() { function handleTo() {
emit("enter"); 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-item56px+56px=112px高度加上下margin8px+8px=16px
return scrollTop > innerHeight.value ? scrollTop - innerHeight.value : 0;
}
onMounted(() => {
resizeResult();
});
defineExpose({ handleScroll });
</script> </script>
<template> <template>
<div class="result"> <div ref="resultRef" class="result">
<template v-for="item in options" :key="item.path"> <div
<div v-for="(item, index) in options"
class="result-item dark:bg-[#1d1d1d]" :key="item.path"
:style="itemStyle(item)" :ref="'resultItemRef' + index"
@click="handleTo" class="result-item dark:bg-[#1d1d1d]"
@mouseenter="handleMouse(item)" :style="itemStyle(item)"
> @click="handleTo"
<component :is="useRenderIcon(item.meta?.icon ?? 'bookmark-2-line')" /> @mouseenter="handleMouse(item)"
<span class="result-item-title">{{ item.meta?.title }}</span> >
<enterOutlined /> <component :is="useRenderIcon(item.meta?.icon ?? Bookmark2Line)" />
</div> <span class="result-item-title">{{ item.meta?.title }}</span>
</template> <enterOutlined />
</div>
</div> </div>
</template> </template>
@@ -80,11 +109,11 @@ function handleTo() {
display: flex; display: flex;
align-items: center; align-items: center;
height: 56px; height: 56px;
margin-top: 8px;
padding: 14px; padding: 14px;
border-radius: 4px; margin-top: 8px;
cursor: pointer; cursor: pointer;
border: 0.1px solid #ccc; border: 0.1px solid #ccc;
border-radius: 4px;
transition: all 0.3s; transition: all 0.3s;
&-title { &-title {

View File

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

View File

@@ -6,8 +6,15 @@ import {
reactive, reactive,
computed, computed,
nextTick, nextTick,
useCssModule onBeforeMount
} from "vue"; } from "vue";
import {
useDark,
debounce,
useGlobal,
storageLocal,
storageSession
} from "@pureadmin/utils";
import { getConfig } from "@/config"; import { getConfig } from "@/config";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import panel from "../panel/index.vue"; import panel from "../panel/index.vue";
@@ -17,24 +24,18 @@ import { removeToken } from "@/utils/auth";
import { routerArrays } from "@/layout/types"; import { routerArrays } from "@/layout/types";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import { useAppStoreHook } from "@/store/modules/app"; import { useAppStoreHook } from "@/store/modules/app";
import { toggleTheme } from "@pureadmin/theme/dist/browser-utils";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange"; import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import {
useDark,
debounce,
useGlobal,
storageLocal,
storageSession
} from "@pureadmin/utils";
import { toggleTheme } from "@pureadmin/theme/dist/browser-utils";
import dayIcon from "@/assets/svg/day.svg?component"; import dayIcon from "@/assets/svg/day.svg?component";
import darkIcon from "@/assets/svg/dark.svg?component"; import darkIcon from "@/assets/svg/dark.svg?component";
import Check from "@iconify-icons/ep/check";
import Logout from "@iconify-icons/ri/logout-circle-r-line";
const router = useRouter(); const router = useRouter();
const { device } = useNav();
const { isDark } = useDark(); const { isDark } = useDark();
const { isSelect } = useCssModule(); const { device, tooltipEffect } = useNav();
const { $storage } = useGlobal<GlobalPropertiesApi>(); const { $storage } = useGlobal<GlobalPropertiesApi>();
const mixRef = ref(); const mixRef = ref();
@@ -42,7 +43,6 @@ const verticalRef = ref();
const horizontalRef = ref(); const horizontalRef = ref();
const { const {
body,
dataTheme, dataTheme,
layoutTheme, layoutTheme,
themeColors, themeColors,
@@ -132,8 +132,8 @@ const multiTagsCacheChange = () => {
/** 清空缓存并返回登录页 */ /** 清空缓存并返回登录页 */
function onReset() { function onReset() {
removeToken(); removeToken();
storageLocal.clear(); storageLocal().clear();
storageSession.clear(); storageSession().clear();
const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } = getConfig(); const { Grey, Weak, MultiTagsCache, EpThemeColor, Layout } = getConfig();
useAppStoreHook().setLayout(Layout); useAppStoreHook().setLayout(Layout);
setEpThemeColor(EpThemeColor); setEpThemeColor(EpThemeColor);
@@ -160,32 +160,10 @@ function logoChange() {
function setFalse(Doms): any { function setFalse(Doms): any {
Doms.forEach(v => { Doms.forEach(v => {
toggleClass(false, isSelect, unref(v)); toggleClass(false, "is-select", unref(v));
}); });
} }
watch($storage, ({ layout }) => {
/* 设置wangeditorV5主题色 */
body.style.setProperty("--w-e-toolbar-active-color", layout["epThemeColor"]);
switch (layout["layout"]) {
case "vertical":
toggleClass(true, isSelect, unref(verticalRef));
debounce(setFalse([horizontalRef]), 50);
debounce(setFalse([mixRef]), 50);
break;
case "horizontal":
toggleClass(true, isSelect, unref(horizontalRef));
debounce(setFalse([verticalRef]), 50);
debounce(setFalse([mixRef]), 50);
break;
case "mix":
toggleClass(true, isSelect, unref(mixRef));
debounce(setFalse([verticalRef]), 50);
debounce(setFalse([horizontalRef]), 50);
break;
}
});
/** 主题色 激活选择项 */ /** 主题色 激活选择项 */
const getThemeColor = computed(() => { const getThemeColor = computed(() => {
return current => { return current => {
@@ -219,14 +197,35 @@ function setLayoutModel(layout: string) {
useAppStoreHook().setLayout(layout); useAppStoreHook().setLayout(layout);
} }
/* 初始化项目配置 */ watch($storage, ({ layout }) => {
nextTick(() => { switch (layout["layout"]) {
settings.greyVal && case "vertical":
document.querySelector("html")?.setAttribute("class", "html-grey"); toggleClass(true, "is-select", unref(verticalRef));
settings.weakVal && debounce(setFalse([horizontalRef]), 50);
document.querySelector("html")?.setAttribute("class", "html-weakness"); debounce(setFalse([mixRef]), 50);
settings.tabsVal && tagsChange(); break;
dataThemeChange(); case "horizontal":
toggleClass(true, "is-select", unref(horizontalRef));
debounce(setFalse([verticalRef]), 50);
debounce(setFalse([mixRef]), 50);
break;
case "mix":
toggleClass(true, "is-select", unref(mixRef));
debounce(setFalse([verticalRef]), 50);
debounce(setFalse([horizontalRef]), 50);
break;
}
});
onBeforeMount(() => {
/* 初始化项目配置 */
nextTick(() => {
settings.greyVal &&
document.querySelector("html")?.setAttribute("class", "html-grey");
settings.weakVal &&
document.querySelector("html")?.setAttribute("class", "html-weakness");
settings.tabsVal && tagsChange();
});
}); });
</script> </script>
@@ -244,9 +243,15 @@ nextTick(() => {
<el-divider>导航栏模式</el-divider> <el-divider>导航栏模式</el-divider>
<ul class="pure-theme"> <ul class="pure-theme">
<el-tooltip class="item" content="左侧模式" placement="bottom"> <el-tooltip
:effect="tooltipEffect"
class="item"
content="左侧模式"
placement="bottom"
popper-class="pure-tooltip"
>
<li <li
:class="layoutTheme.layout === 'vertical' ? $style.isSelect : ''" :class="layoutTheme.layout === 'vertical' ? 'is-select' : ''"
ref="verticalRef" ref="verticalRef"
@click="setLayoutModel('vertical')" @click="setLayoutModel('vertical')"
> >
@@ -257,12 +262,14 @@ nextTick(() => {
<el-tooltip <el-tooltip
v-if="device !== 'mobile'" v-if="device !== 'mobile'"
:effect="tooltipEffect"
class="item" class="item"
content="顶部模式" content="顶部模式"
placement="bottom" placement="bottom"
popper-class="pure-tooltip"
> >
<li <li
:class="layoutTheme.layout === 'horizontal' ? $style.isSelect : ''" :class="layoutTheme.layout === 'horizontal' ? 'is-select' : ''"
ref="horizontalRef" ref="horizontalRef"
@click="setLayoutModel('horizontal')" @click="setLayoutModel('horizontal')"
> >
@@ -273,12 +280,14 @@ nextTick(() => {
<el-tooltip <el-tooltip
v-if="device !== 'mobile'" v-if="device !== 'mobile'"
:effect="tooltipEffect"
class="item" class="item"
content="混合模式" content="混合模式"
placement="bottom" placement="bottom"
popper-class="pure-tooltip"
> >
<li <li
:class="layoutTheme.layout === 'mix' ? $style.isSelect : ''" :class="layoutTheme.layout === 'mix' ? 'is-select' : ''"
ref="mixRef" ref="mixRef"
@click="setLayoutModel('mix')" @click="setLayoutModel('mix')"
> >
@@ -302,7 +311,7 @@ nextTick(() => {
:size="17" :size="17"
:color="getThemeColor(item.themeColor)" :color="getThemeColor(item.themeColor)"
> >
<IconifyIconOffline icon="check" /> <IconifyIconOffline :icon="Check" />
</el-icon> </el-icon>
</li> </li>
</ul> </ul>
@@ -383,7 +392,7 @@ nextTick(() => {
@click="onReset" @click="onReset"
> >
<IconifyIconOffline <IconifyIconOffline
icon="fa-sign-out" :icon="Logout"
width="15" width="15"
height="15" height="15"
style="margin-right: 4px" style="margin-right: 4px"
@@ -393,52 +402,50 @@ nextTick(() => {
</panel> </panel>
</template> </template>
<style scoped module>
.isSelect {
border: 2px solid var(--el-color-primary);
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
.setting {
width: 100%;
li {
display: flex;
justify-content: space-between;
align-items: center;
margin: 25px;
}
}
:deep(.el-divider__text) { :deep(.el-divider__text) {
font-size: 16px; font-size: 16px;
font-weight: 700; font-weight: 700;
} }
.is-select {
border: 2px solid var(--el-color-primary);
}
.setting {
width: 100%;
li {
display: flex;
align-items: center;
justify-content: space-between;
margin: 25px;
}
}
.pure-datatheme { .pure-datatheme {
display: block;
width: 100%; width: 100%;
height: 50px; height: 50px;
text-align: center;
display: block;
padding-top: 25px; padding-top: 25px;
text-align: center;
} }
.pure-theme { .pure-theme {
margin-top: 25px;
width: 100%;
height: 50px;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-around; justify-content: space-around;
width: 100%;
height: 50px;
margin-top: 25px;
li { li {
position: relative;
width: 18%; width: 18%;
height: 45px; height: 45px;
background: #f0f2f5;
position: relative;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
background: #f0f2f5;
border-radius: 4px; border-radius: 4px;
box-shadow: 0 1px 2.5px 0 rgb(0 0 0 / 18%); box-shadow: 0 1px 2.5px 0 rgb(0 0 0 / 18%);
@@ -451,13 +458,13 @@ nextTick(() => {
} }
&:nth-child(2) { &:nth-child(2) {
width: 70%; position: absolute;
height: 30%;
top: 0; top: 0;
right: 0; right: 0;
width: 70%;
height: 30%;
background: #fff; background: #fff;
box-shadow: 0 0 1px #888; box-shadow: 0 0 1px #888;
position: absolute;
} }
} }
} }
@@ -483,13 +490,13 @@ nextTick(() => {
} }
&:nth-child(2) { &:nth-child(2) {
width: 30%; position: absolute;
height: 70%;
bottom: 0; bottom: 0;
left: 0; left: 0;
width: 30%;
height: 70%;
background: #fff; background: #fff;
box-shadow: 0 0 1px #888; box-shadow: 0 0 1px #888;
position: absolute;
} }
} }
} }
@@ -497,11 +504,11 @@ nextTick(() => {
} }
.theme-color { .theme-color {
display: flex;
justify-content: center;
width: 100%; width: 100%;
height: 40px; height: 40px;
margin-top: 20px; margin-top: 20px;
display: flex;
justify-content: center;
li { li {
float: left; float: left;
@@ -511,8 +518,8 @@ nextTick(() => {
margin-right: 8px; margin-right: 8px;
font-weight: 700; font-weight: 700;
text-align: center; text-align: center;
border-radius: 2px;
cursor: pointer; cursor: pointer;
border-radius: 2px;
&:nth-child(2) { &:nth-child(2) {
border: 1px solid #ddd; border: 1px solid #ddd;

View File

@@ -11,12 +11,6 @@ const router = useRouter();
const routes: any = router.options.routes; const routes: any = router.options.routes;
const multiTags: any = useMultiTagsStoreHook().multiTags; 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 => { const getBreadcrumb = (): void => {
// 当前路由信息 // 当前路由信息
let currentRoute; let currentRoute;
@@ -34,28 +28,24 @@ const getBreadcrumb = (): void => {
} }
}); });
} else { } 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 => { parentRoutes.forEach(path => {
if (path !== "/") matched.push(findRouteByPath(path, routes)); if (path !== "/") matched.push(findRouteByPath(path, routes));
}); });
if (currentRoute?.path !== "/welcome") matched.push(currentRoute); matched.push(currentRoute);
if (!isDashboard(matched[0])) {
matched = [
{
path: "/welcome",
parentPath: "/",
meta: { title: "首页" }
} as unknown as RouteLocationMatched
].concat(matched);
}
matched.forEach((item, index) => { matched.forEach((item, index) => {
if (currentRoute?.query || currentRoute?.params) return; if (currentRoute?.query || currentRoute?.params) return;
@@ -90,14 +80,21 @@ watch(
() => route.path, () => route.path,
() => { () => {
getBreadcrumb(); getBreadcrumb();
},
{
deep: true
} }
); );
</script> </script>
<template> <template>
<el-breadcrumb class="!leading-[50px] select-none" separator="/"> <el-breadcrumb class="!leading-[50px] select-none" separator="/">
<transition-group appear name="breadcrumb"> <transition-group name="breadcrumb">
<el-breadcrumb-item v-for="item in levelList" :key="item.path"> <el-breadcrumb-item
class="!inline !items-stretch"
v-for="item in levelList"
:key="item.path"
>
<a @click.prevent="handleLink(item)"> <a @click.prevent="handleLink(item)">
{{ item.meta.title }} {{ item.meta.title }}
</a> </a>

View File

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

View File

@@ -1,50 +1,51 @@
<script setup lang="ts"> <script setup lang="ts">
import Search from "../search/index.vue"; import Search from "../search/index.vue";
import Notice from "../notice/index.vue"; import Notice from "../notice/index.vue";
import { ref, watch, nextTick } from "vue";
import SidebarItem from "./sidebarItem.vue"; import SidebarItem from "./sidebarItem.vue";
import { isAllEmpty } from "@pureadmin/utils";
import { ref, nextTick, computed } from "vue";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import Setting from "@iconify-icons/ri/settings-3-line";
const menuRef = ref(); const menuRef = ref();
const { const {
route, route,
title, title,
routers,
logout, logout,
backHome, backTopMenu,
onPanel, onPanel,
menuSelect,
username, username,
userAvatar,
avatarsStyle avatarsStyle
} = useNav(); } = useNav();
const defaultActive = computed(() =>
!isAllEmpty(route.meta?.activePath) ? route.meta.activePath : route.path
);
nextTick(() => { nextTick(() => {
menuRef.value?.handleResize(); menuRef.value?.handleResize();
}); });
watch(
() => route.path,
() => {
menuSelect(route.path, routers);
}
);
</script> </script>
<template> <template>
<div class="horizontal-header"> <div
<div class="horizontal-header-left" @click="backHome"> v-loading="usePermissionStoreHook().wholeMenus.length === 0"
<FontIcon icon="team-iconlogo" svg style="width: 35px; height: 35px" /> class="horizontal-header"
<h4>{{ title }}</h4> >
<div class="horizontal-header-left" @click="backTopMenu">
<img src="/logo.svg" alt="logo" />
<span>{{ title }}</span>
</div> </div>
<el-menu <el-menu
router router
ref="menuRef" ref="menuRef"
mode="horizontal" mode="horizontal"
class="horizontal-header-menu" class="horizontal-header-menu"
:default-active="route.path" :default-active="defaultActive"
@select="indexPath => menuSelect(indexPath, routers)"
> >
<sidebar-item <sidebar-item
v-for="route in usePermissionStoreHook().wholeMenus" v-for="route in usePermissionStoreHook().wholeMenus"
@@ -61,17 +62,14 @@ watch(
<!-- 退出登录 --> <!-- 退出登录 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover"> <span class="el-dropdown-link navbar-bg-hover">
<img <img :src="userAvatar" :style="avatarsStyle" />
src="https://avatars.githubusercontent.com/u/44761321?v=4"
:style="avatarsStyle"
/>
<p v-if="username" class="dark:text-white">{{ username }}</p> <p v-if="username" class="dark:text-white">{{ username }}</p>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="logout"> <el-dropdown-menu class="logout">
<el-dropdown-item @click="logout"> <el-dropdown-item @click="logout">
<IconifyIconOffline <IconifyIconOffline
icon="logout-circle-r-line" :icon="LogoutCircleRLine"
style="margin: 5px" style="margin: 5px"
/> />
退出系统 退出系统
@@ -84,20 +82,24 @@ watch(
title="打开项目配置" title="打开项目配置"
@click="onPanel" @click="onPanel"
> >
<IconifyIconOffline icon="setting" /> <IconifyIconOffline :icon="Setting" />
</span> </span>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.el-loading-mask) {
opacity: 0.45;
}
.logout { .logout {
max-width: 120px; max-width: 120px;
::v-deep(.el-dropdown-menu__item) { ::v-deep(.el-dropdown-menu__item) {
min-width: 100%;
display: inline-flex; display: inline-flex;
flex-wrap: wrap; flex-wrap: wrap;
min-width: 100%;
} }
} }
</style> </style>

View File

@@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDark } from "@pureadmin/utils"; import { ref, computed } from "vue";
import { useNav } from "@/layout/hooks/useNav";
import MenuFold from "@iconify-icons/ri/menu-fold-fill";
interface Props { interface Props {
isActive: boolean; isActive: boolean;
@@ -8,7 +10,25 @@ interface Props {
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
isActive: false isActive: false
}); });
const { isDark } = useDark();
const visible = ref(false);
const { tooltipEffect } = useNav();
const iconClass = computed(() => {
return [
"ml-4",
"mb-1",
"w-[16px]",
"h-[16px]",
"inline-block",
"align-middle",
"text-primary",
"cursor-pointer",
"duration-[100ms]",
"hover:text-primary",
"dark:hover:!text-white"
];
});
const emit = defineEmits<{ const emit = defineEmits<{
(e: "toggleClick"): void; (e: "toggleClick"): void;
@@ -23,13 +43,17 @@ const toggleClick = () => {
<div class="container"> <div class="container">
<el-tooltip <el-tooltip
placement="right" placement="right"
:effect="isDark ? 'dark' : 'light'" :visible="visible"
:effect="tooltipEffect"
:content="props.isActive ? '点击折叠' : '点击展开'" :content="props.isActive ? '点击折叠' : '点击展开'"
> >
<IconifyIconOffline <IconifyIconOffline
:icon="props.isActive ? 'menu-fold' : 'menu-unfold'" :icon="MenuFold"
class="cursor-pointer inline-block align-middle text-primary hover:text-primary dark:hover:!text-white w-[16px] h-[16px] ml-4 mb-1" :class="iconClass"
:style="{ transform: props.isActive ? 'none' : 'rotateY(180deg)' }"
@click="toggleClick" @click="toggleClick"
@mouseenter="visible = true"
@mouseleave="visible = false"
/> />
</el-tooltip> </el-tooltip>
</div> </div>

View File

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

View File

@@ -1,11 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import extraIcon from "./extraIcon.vue";
import Search from "../search/index.vue"; import Search from "../search/index.vue";
import Notice from "../notice/index.vue"; import Notice from "../notice/index.vue";
import { isAllEmpty } from "@pureadmin/utils";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import { ref, toRaw, watch, onMounted, nextTick } from "vue"; import { ref, toRaw, watch, onMounted, nextTick } from "vue";
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { getParentPaths, findRouteByPath } from "@/router/utils"; import { getParentPaths, findRouteByPath } from "@/router/utils";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
import LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
import Setting from "@iconify-icons/ri/settings-3-line";
const menuRef = ref(); const menuRef = ref();
const defaultActive = ref(null); const defaultActive = ref(null);
@@ -13,12 +17,12 @@ const defaultActive = ref(null);
const { const {
route, route,
device, device,
routers,
logout, logout,
onPanel, onPanel,
menuSelect,
resolvePath, resolvePath,
username, username,
userAvatar,
getDivStyle,
avatarsStyle avatarsStyle
} = useNav(); } = useNav();
@@ -26,10 +30,9 @@ function getDefaultActive(routePath) {
const wholeMenus = usePermissionStoreHook().wholeMenus; const wholeMenus = usePermissionStoreHook().wholeMenus;
/** 当前路由的父级路径 */ /** 当前路由的父级路径 */
const parentRoutes = getParentPaths(routePath, wholeMenus)[0]; const parentRoutes = getParentPaths(routePath, wholeMenus)[0];
defaultActive.value = findRouteByPath( defaultActive.value = !isAllEmpty(route.meta?.activePath)
parentRoutes, ? route.meta.activePath
wholeMenus : findRouteByPath(parentRoutes, wholeMenus)?.children[0]?.path;
)?.children[0]?.path;
} }
onMounted(() => { onMounted(() => {
@@ -49,14 +52,17 @@ watch(
</script> </script>
<template> <template>
<div v-if="device !== 'mobile'" class="horizontal-header"> <div
v-if="device !== 'mobile'"
class="horizontal-header"
v-loading="usePermissionStoreHook().wholeMenus.length === 0"
>
<el-menu <el-menu
router router
ref="menuRef" ref="menuRef"
mode="horizontal" mode="horizontal"
class="horizontal-header-menu" class="horizontal-header-menu"
:default-active="defaultActive" :default-active="defaultActive"
@select="indexPath => menuSelect(indexPath, routers)"
> >
<el-menu-item <el-menu-item
v-for="route in usePermissionStoreHook().wholeMenus" v-for="route in usePermissionStoreHook().wholeMenus"
@@ -72,15 +78,12 @@ watch(
:is="useRenderIcon(route.meta && toRaw(route.meta.icon))" :is="useRenderIcon(route.meta && toRaw(route.meta.icon))"
/> />
</div> </div>
<span class="select-none">{{ route.meta.title }}</span> <div :style="getDivStyle">
<FontIcon <span class="select-none">
v-if="route.meta.extraIcon" {{ route.meta.title }}
width="30px" </span>
height="30px" <extraIcon :extraIcon="route.meta.extraIcon" />
style="position: absolute; right: 10px" </div>
:icon="route.meta.extraIcon.name"
:svg="route.meta.extraIcon.svg ? true : false"
/>
</template> </template>
</el-menu-item> </el-menu-item>
</el-menu> </el-menu>
@@ -92,17 +95,14 @@ watch(
<!-- 退出登录 --> <!-- 退出登录 -->
<el-dropdown trigger="click"> <el-dropdown trigger="click">
<span class="el-dropdown-link navbar-bg-hover select-none"> <span class="el-dropdown-link navbar-bg-hover select-none">
<img <img :src="userAvatar" :style="avatarsStyle" />
src="https://avatars.githubusercontent.com/u/44761321?v=4"
:style="avatarsStyle"
/>
<p v-if="username" class="dark:text-white">{{ username }}</p> <p v-if="username" class="dark:text-white">{{ username }}</p>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="logout"> <el-dropdown-menu class="logout">
<el-dropdown-item @click="logout"> <el-dropdown-item @click="logout">
<IconifyIconOffline <IconifyIconOffline
icon="logout-circle-r-line" :icon="LogoutCircleRLine"
style="margin: 5px" style="margin: 5px"
/> />
退出系统 退出系统
@@ -115,20 +115,24 @@ watch(
title="打开项目配置" title="打开项目配置"
@click="onPanel" @click="onPanel"
> >
<IconifyIconOffline icon="setting" /> <IconifyIconOffline :icon="Setting" />
</span> </span>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.el-loading-mask) {
opacity: 0.45;
}
.logout { .logout {
max-width: 120px; max-width: 120px;
::v-deep(.el-dropdown-menu__item) { ::v-deep(.el-dropdown-menu__item) {
min-width: 100%;
display: inline-flex; display: inline-flex;
flex-wrap: wrap; flex-wrap: wrap;
min-width: 100%;
} }
} }
</style> </style>

View File

@@ -1,16 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import path from "path"; import path from "path";
import { getConfig } from "@/config"; import { getConfig } from "@/config";
import { childrenType } from "../../types"; import { menuType } from "../../types";
import extraIcon from "./extraIcon.vue";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { ref, toRaw, PropType, nextTick, computed, CSSProperties } from "vue"; import { ref, toRaw, PropType, nextTick, computed, CSSProperties } from "vue";
const { layout, isCollapse } = useNav(); 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, getDivStyle } = useNav();
const props = defineProps({ const props = defineProps({
item: { item: {
type: Object as PropType<childrenType> type: Object as PropType<menuType>
}, },
isNest: { isNest: {
type: Boolean, type: Boolean,
@@ -22,17 +28,11 @@ const props = defineProps({
} }
}); });
const getExtraIconStyle = computed((): CSSProperties => { const getSpanStyle = computed((): CSSProperties => {
if (!isCollapse.value) { return {
return { width: "100%",
position: "absolute", textAlign: "center"
right: "10px" };
};
} else {
return {
position: "static"
};
}
}); });
const getNoDropdownStyle = computed((): CSSProperties => { const getNoDropdownStyle = computed((): CSSProperties => {
@@ -42,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(() => { const getMenuTextStyle = computed(() => {
return { return {
overflow: "hidden", overflow: "hidden",
@@ -60,27 +50,68 @@ const getMenuTextStyle = computed(() => {
}; };
}); });
const getSubTextStyle = computed((): CSSProperties => { const getsubMenuIconStyle = computed((): CSSProperties => {
return { return {
width: !isCollapse.value ? "210px" : "", display: "flex",
display: "inline-block", justifyContent: "center",
overflow: "hidden", alignItems: "center",
textOverflow: "ellipsis" margin:
layout.value === "horizontal"
? "0 5px 0 0"
: isCollapse.value
? "0 auto"
: "0 5px 0 0"
}; };
}); });
const getSpanStyle = computed(() => { const getSubTextStyle = computed((): CSSProperties => {
return { if (!isCollapse.value) {
overflow: "hidden", return {
textOverflow: "ellipsis" 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"
: ""
};
}; };
}); });
const expandCloseIcon = computed(() => { const expandCloseIcon = computed(() => {
return getConfig()?.MenuArrowIconNoTransition ? "expand-close-icon" : ""; if (!getConfig()?.MenuArrowIconNoTransition) return "";
return {
"expand-close-icon": useRenderIcon(EpArrowDown),
"expand-open-icon": useRenderIcon(ArrowUp),
"collapse-close-icon": useRenderIcon(ArrowRight),
"collapse-open-icon": useRenderIcon(ArrowLeft)
};
}); });
const onlyOneChild: childrenType = ref(null); const onlyOneChild: menuType = ref(null);
// 存放菜单是否存在showTooltip属性标识 // 存放菜单是否存在showTooltip属性标识
const hoverMenuMap = new WeakMap(); const hoverMenuMap = new WeakMap();
// 存储菜单文本dom元素 // 存储菜单文本dom元素
@@ -103,10 +134,21 @@ function hoverMenu(key) {
}); });
} }
function hasOneShowingChild( // 左侧菜单折叠后,当菜单没有图标时只显示第一个文字并加上省略号
children: childrenType[] = [], function overflowSlice(text, item?: any) {
parent: childrenType 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) => { const showingChildren = children.filter((item: any) => {
onlyOneChild.value = item; onlyOneChild.value = item;
return true; return true;
@@ -132,101 +174,98 @@ function resolvePath(routePath) {
if (httpReg.test(routePath) || httpReg.test(props.basePath)) { if (httpReg.test(routePath) || httpReg.test(props.basePath)) {
return routePath || props.basePath; return routePath || props.basePath;
} else { } else {
return path.resolve(props.basePath, routePath); // 使用path.posix.resolve替代path.resolve 避免windows环境下使用electron出现盘符问题
return path.posix.resolve(props.basePath, routePath);
} }
} }
</script> </script>
<template> <template>
<template <el-menu-item
v-if=" v-if="
hasOneShowingChild(props.item.children, props.item) && hasOneShowingChild(props.item.children, props.item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren) (!onlyOneChild.children || onlyOneChild.noShowingChildren)
" "
:index="resolvePath(onlyOneChild.path)"
:class="{ 'submenu-title-noDropdown': !isNest }"
:style="getNoDropdownStyle"
> >
<el-menu-item <div
:index="resolvePath(onlyOneChild.path)" v-if="toRaw(props.item.meta.icon)"
:class="{ 'submenu-title-noDropdown': !isNest }" class="sub-menu-icon"
:style="getNoDropdownStyle" :style="getsubMenuIconStyle"
> >
<div class="sub-menu-icon" v-if="toRaw(props.item.meta.icon)"> <component
<component :is="
:is=" useRenderIcon(
useRenderIcon( toRaw(onlyOneChild.meta.icon) ||
toRaw(onlyOneChild.meta.icon) || (props.item.meta && toRaw(props.item.meta.icon))
(props.item.meta && toRaw(props.item.meta.icon)) )
)
"
/>
</div>
<div
v-if="
isCollapse &&
layout === 'vertical' &&
props.item?.pathList?.length === 1
" "
:style="getDivStyle" />
> </div>
<span :style="getMenuTextStyle"> <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 }} {{ onlyOneChild.meta.title }}
</span> </span>
</div> <el-tooltip
<div v-else
v-if=" placement="top"
isCollapse && layout === 'mix' && props.item?.pathList?.length === 2 :effect="tooltipEffect"
" :offset="-10"
:style="getDivStyle" :disabled="!onlyOneChild.showTooltip"
> >
<span :style="getMenuTextStyle"> <template #content>
{{ onlyOneChild.meta.title }} {{ onlyOneChild.meta.title }}
</span> </template>
</div> <span
<template #title> ref="menuTextRef"
<div :style="getDivStyle"> :style="getMenuTextStyle"
<span v-if="layout === 'horizontal'"> @mouseover="hoverMenu(onlyOneChild)"
>
{{ onlyOneChild.meta.title }} {{ onlyOneChild.meta.title }}
</span> </span>
<el-tooltip </el-tooltip>
v-else <extraIcon :extraIcon="onlyOneChild.meta.extraIcon" />
placement="top" </div>
:offset="-10" </template>
:disabled="!onlyOneChild.showTooltip" </el-menu-item>
>
<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-sub-menu <el-sub-menu
v-else v-else
ref="subMenu" ref="subMenu"
v-bind="expandCloseIcon"
:index="resolvePath(props.item.path)" :index="resolvePath(props.item.path)"
v-bind:[expandCloseIcon]="useRenderIcon('ep-arrow-down')"
:expand-open-icon="useRenderIcon('ep-arrow-up')"
:collapse-close-icon="useRenderIcon('ep-arrow-right')"
:collapse-open-icon="useRenderIcon('ep-arrow-left')"
> >
<template #title> <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 <component
:is="useRenderIcon(props.item.meta && toRaw(props.item.meta.icon))" :is="useRenderIcon(props.item.meta && toRaw(props.item.meta.icon))"
/> />
@@ -234,33 +273,36 @@ function resolvePath(routePath) {
<span v-if="layout === 'horizontal'"> <span v-if="layout === 'horizontal'">
{{ props.item.meta.title }} {{ props.item.meta.title }}
</span> </span>
<el-tooltip <div
v-else :style="getSubMenuDivStyle(props.item)"
placement="top" v-if="
:offset="-10" !(
:disabled="!isCollapse || !props.item.showTooltip" isCollapse &&
toRaw(props.item.meta.icon) &&
props.item.parentId === null
)
"
> >
<template #content> <el-tooltip
{{ props.item.meta.title }} v-if="layout !== 'horizontal'"
</template> placement="top"
<div :effect="tooltipEffect"
ref="menuTextRef" :offset="-10"
:style="getSubTextStyle" :disabled="!props.item.showTooltip"
@mouseover="hoverMenu(props.item)"
> >
<span :style="getSpanStyle"> <template #content>
{{ props.item.meta.title }} {{ props.item.meta.title }}
</template>
<span
ref="menuTextRef"
:style="getSubTextStyle"
@mouseover="hoverMenu(props.item)"
>
{{ overflowSlice(props.item.meta.title, props.item) }}
</span> </span>
</div> </el-tooltip>
</el-tooltip> <extraIcon v-if="!isCollapse" :extraIcon="props.item.meta.extraIcon" />
<FontIcon </div>
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"
/>
</template> </template>
<sidebar-item <sidebar-item
v-for="child in props.item.children" v-for="child in props.item.children"

View File

@@ -1,4 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import MenuFold from "@iconify-icons/ri/menu-fold-fill";
import MenuUnfold from "@iconify-icons/ri/menu-unfold-fill";
interface Props { interface Props {
isActive: boolean; isActive: boolean;
} }
@@ -23,7 +26,7 @@ const toggleClick = () => {
@click="toggleClick" @click="toggleClick"
> >
<IconifyIconOffline <IconifyIconOffline
:icon="props.isActive ? 'menu-fold' : 'menu-unfold'" :icon="props.isActive ? MenuFold : MenuUnfold"
class="inline-block align-middle hover:text-primary dark:hover:!text-white" class="inline-block align-middle hover:text-primary dark:hover:!text-white"
/> />
</div> </div>

View File

@@ -5,18 +5,20 @@ import { emitter } from "@/utils/mitt";
import SidebarItem from "./sidebarItem.vue"; import SidebarItem from "./sidebarItem.vue";
import leftCollapse from "./leftCollapse.vue"; import leftCollapse from "./leftCollapse.vue";
import { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import { storageLocal } from "@pureadmin/utils"; import { responsiveStorageNameSpace } from "@/config";
import { ref, computed, watch, onBeforeMount } from "vue"; import { storageLocal, isAllEmpty } from "@pureadmin/utils";
import { findRouteByPath, getParentPaths } from "@/router/utils"; import { findRouteByPath, getParentPaths } from "@/router/utils";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
import { ref, computed, watch, onMounted, onBeforeUnmount } from "vue";
const route = useRoute(); const route = useRoute();
const showLogo = ref( const showLogo = ref(
storageLocal.getItem<StorageConfigs>("responsive-configure")?.showLogo ?? true storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}configure`
)?.showLogo ?? true
); );
const { routers, device, pureApp, isCollapse, menuSelect, toggleSideBar } = const { device, pureApp, isCollapse, menuSelect, toggleSideBar } = useNav();
useNav();
const subMenuData = ref([]); const subMenuData = ref([]);
@@ -26,7 +28,18 @@ const menuData = computed(() => {
: usePermissionStoreHook().wholeMenus; : 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的上级路由组成的数组 // path的上级路由组成的数组
const parentPathArr = getParentPaths( const parentPathArr = getParentPaths(
path, path,
@@ -41,25 +54,34 @@ function getSubMenuData(path: string) {
subMenuData.value = parenetRoute?.children; 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 => { emitter.on("logoChange", key => {
showLogo.value = key; showLogo.value = key;
}); });
}); });
watch( onBeforeUnmount(() => {
() => [route.path, usePermissionStoreHook().wholeMenus], // 解绑`logoChange`公共事件,防止多次触发
() => { emitter.off("logoChange");
getSubMenuData(route.path); });
menuSelect(route.path, routers);
}
);
</script> </script>
<template> <template>
<div :class="['sidebar-container', showLogo ? 'has-logo' : '']"> <div
v-loading="loading"
:class="['sidebar-container', showLogo ? 'has-logo' : '']"
>
<Logo v-if="showLogo" :collapse="isCollapse" /> <Logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar <el-scrollbar
wrap-class="scrollbar-wrapper" wrap-class="scrollbar-wrapper"
@@ -71,9 +93,8 @@ watch(
mode="vertical" mode="vertical"
class="outer-most select-none" class="outer-most select-none"
:collapse="isCollapse" :collapse="isCollapse"
:default-active="route.path" :default-active="defaultActive"
:collapse-transition="false" :collapse-transition="false"
@select="indexPath => menuSelect(indexPath, routers)"
> >
<sidebar-item <sidebar-item
v-for="routes in menuData" v-for="routes in menuData"
@@ -91,3 +112,9 @@ watch(
/> />
</div> </div>
</template> </template>
<style scoped>
:deep(.el-loading-mask) {
opacity: 0.45;
}
</style>

View File

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

View File

@@ -3,12 +3,19 @@ import { emitter } from "@/utils/mitt";
import { RouteConfigs } from "../../types"; import { RouteConfigs } from "../../types";
import { useTags } from "../../hooks/useTag"; import { useTags } from "../../hooks/useTag";
import { routerArrays } from "@/layout/types"; import { routerArrays } from "@/layout/types";
import { isEqual, isEmpty } from "lodash-unified"; import { handleAliveRoute, getTopMenu } from "@/router/utils";
import { useSettingStoreHook } from "@/store/modules/settings"; import { useSettingStoreHook } from "@/store/modules/settings";
import { ref, watch, unref, nextTick, onBeforeMount } from "vue"; import { useResizeObserver, useFullscreen } from "@vueuse/core";
import { handleAliveRoute, delAliveRoutes } from "@/router/utils"; import { isEqual, isAllEmpty, debounce } from "@pureadmin/utils";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; 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";
import ArrowDown from "@iconify-icons/ri/arrow-down-s-line";
import ArrowRightSLine from "@iconify-icons/ri/arrow-right-s-line";
import ArrowLeftSLine from "@iconify-icons/ri/arrow-left-s-line";
import CloseBold from "@iconify-icons/ep/close-bold";
const { const {
route, route,
@@ -41,22 +48,26 @@ const tabDom = ref();
const containerDom = ref(); const containerDom = ref();
const scrollbarDom = ref(); const scrollbarDom = ref();
const isShowArrow = ref(false); const isShowArrow = ref(false);
const topPath = getTopMenu()?.path;
const { VITE_HIDE_HOME } = import.meta.env;
const { isFullscreen, toggle } = useFullscreen(); const { isFullscreen, toggle } = useFullscreen();
const dynamicTagView = () => { const dynamicTagView = async () => {
await nextTick();
const index = multiTags.value.findIndex(item => { const index = multiTags.value.findIndex(item => {
if (item.query) { if (!isAllEmpty(route.query)) {
return isEqual(route.query, item.query); return isEqual(route.query, item.query);
} else if (item.params) { } else if (!isAllEmpty(route.params)) {
return isEqual(route.params, item.params); return isEqual(route.params, item.params);
} else { } else {
return item.path === route.path; return route.path === item.path;
} }
}); });
moveToView(index); moveToView(index);
}; };
const moveToView = (index: number): void => { const moveToView = async (index: number): Promise<void> => {
await nextTick();
const tabNavPadding = 10; const tabNavPadding = 10;
if (!instance.refs["dynamic" + index]) return; if (!instance.refs["dynamic" + index]) return;
const tabItemEl = instance.refs["dynamic" + index][0]; const tabItemEl = instance.refs["dynamic" + index][0];
@@ -66,8 +77,10 @@ const moveToView = (index: number): void => {
const scrollbarDomWidth = scrollbarDom.value const scrollbarDomWidth = scrollbarDom.value
? scrollbarDom.value?.offsetWidth ? scrollbarDom.value?.offsetWidth
: 0; : 0;
// 已有标签页总长度(包含溢出部分) // 已有标签页总长度(包含溢出部分)
const tabDomWidth = tabDom.value ? tabDom.value?.offsetWidth : 0; const tabDomWidth = tabDom.value ? tabDom.value?.offsetWidth : 0;
scrollbarDomWidth <= tabDomWidth scrollbarDomWidth <= tabDomWidth
? (isShowArrow.value = true) ? (isShowArrow.value = true)
: (isShowArrow.value = false); : (isShowArrow.value = false);
@@ -119,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 => { const hasValue = multiTags.value.some(item => {
return item.path === value; return item.path === value;
}); });
function concatPath(arr: object[], value: string, parentPath: string) { function concatPath(arr: object[], value: string) {
if (!hasValue) { if (!hasValue) {
arr.forEach((arrItem: any) => { arr.forEach((arrItem: any) => {
const pathConcat = parentPath + arrItem.path; if (arrItem.path === value || arrItem.path === value) {
if (arrItem.path === value || pathConcat === value) {
useMultiTagsStoreHook().handleTags("push", { useMultiTagsStoreHook().handleTags("push", {
path: value, path: value,
parentPath: `/${parentPath.split("/")[1]}`,
meta: arrItem.meta, meta: arrItem.meta,
name: arrItem.name name: arrItem.name
}); });
} else { } else {
if (arrItem.children && arrItem.children.length > 0) { 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);
} }
/** 刷新路由 */ /** 刷新路由 */
@@ -151,13 +162,12 @@ function onFresh() {
const { fullPath, query } = unref(route); const { fullPath, query } = unref(route);
router.replace({ router.replace({
path: "/redirect" + fullPath, path: "/redirect" + fullPath,
query: query query
}); });
handleAliveRoute(route as ToRouteType, "refresh");
} }
function deleteDynamicTag(obj: any, current: any, tag?: string) { function deleteDynamicTag(obj: any, current: any, tag?: string) {
// 存放被删除的缓存路由
let delAliveRouteList = [];
const valueIndex: number = multiTags.value.findIndex((item: any) => { const valueIndex: number = multiTags.value.findIndex((item: any) => {
if (item.query) { if (item.query) {
if (item.path === obj.path) { if (item.path === obj.path) {
@@ -178,13 +188,17 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
other?: boolean other?: boolean
): void => { ): void => {
if (other) { if (other) {
useMultiTagsStoreHook().handleTags("equal", [routerArrays[0], obj]); useMultiTagsStoreHook().handleTags("equal", [
VITE_HIDE_HOME === "false" ? routerArrays[0] : toRaw(getTopMenu()),
obj
]);
} else { } else {
delAliveRouteList = useMultiTagsStoreHook().handleTags("splice", "", { useMultiTagsStoreHook().handleTags("splice", "", {
startIndex, startIndex,
length length
}) as any; }) as any;
} }
dynamicTagView();
}; };
if (tag === "other") { if (tag === "other") {
@@ -199,10 +213,6 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
} }
const newRoute = useMultiTagsStoreHook().handleTags("slice"); const newRoute = useMultiTagsStoreHook().handleTags("slice");
if (current === route.path) { if (current === route.path) {
// 删除缓存路由
tag
? delAliveRoutes(delAliveRouteList)
: handleAliveRoute(route.matched, "delete");
// 如果删除当前激活tag就自动切换到最后一个tag // 如果删除当前激活tag就自动切换到最后一个tag
if (tag === "left") return; if (tag === "left") return;
if (newRoute[0]?.query) { if (newRoute[0]?.query) {
@@ -213,8 +223,6 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
router.push({ path: newRoute[0].path }); router.push({ path: newRoute[0].path });
} }
} else { } else {
// 删除缓存路由
tag ? delAliveRoutes(delAliveRouteList) : delAliveRoutes([obj]);
if (!multiTags.value.length) return; if (!multiTags.value.length) return;
if (multiTags.value.some(item => item.path === route.path)) return; if (multiTags.value.some(item => item.path === route.path)) return;
if (newRoute[0]?.query) { if (newRoute[0]?.query) {
@@ -229,6 +237,7 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
function deleteMenu(item, tag?: string) { function deleteMenu(item, tag?: string) {
deleteDynamicTag(item, item.path, tag); deleteDynamicTag(item, item.path, tag);
handleAliveRoute(route as ToRouteType);
} }
function onClickDrop(key, item, selectRoute?: RouteConfigs) { function onClickDrop(key, item, selectRoute?: RouteConfigs) {
@@ -275,18 +284,19 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
startIndex: 1, startIndex: 1,
length: multiTags.value.length length: multiTags.value.length
}); });
router.push("/welcome"); router.push(topPath);
handleAliveRoute(route as ToRouteType);
break; break;
case 6: case 6:
// 整体页面全屏 // 整体页面全屏
toggle(); toggle();
setTimeout(() => { setTimeout(() => {
if (isFullscreen.value) { if (isFullscreen.value) {
tagsViews[6].icon = "exit-fullscreen"; tagsViews[6].icon = ExitFullscreen;
tagsViews[6].text = "整体页面退出全屏"; tagsViews[6].text = "退出全屏";
} else { } else {
tagsViews[6].icon = "fullscreen"; tagsViews[6].icon = Fullscreen;
tagsViews[6].text = "整体页面全屏"; tagsViews[6].text = "全屏";
} }
}, 100); }, 100);
break; break;
@@ -295,10 +305,10 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
onContentFullScreen(); onContentFullScreen();
setTimeout(() => { setTimeout(() => {
if (pureSetting.hiddenSideBar) { if (pureSetting.hiddenSideBar) {
tagsViews[7].icon = "exit-fullscreen"; tagsViews[7].icon = ExitFullscreen;
tagsViews[7].text = "内容区退出全屏"; tagsViews[7].text = "内容区退出全屏";
} else { } else {
tagsViews[7].icon = "fullscreen"; tagsViews[7].icon = Fullscreen;
tagsViews[7].text = "内容区全屏"; tagsViews[7].text = "内容区全屏";
} }
}, 100); }, 100);
@@ -331,7 +341,7 @@ function disabledMenus(value: boolean) {
}); });
} }
/** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */ /** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是顶级菜单,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */
function showMenuModel( function showMenuModel(
currentPath: string, currentPath: string,
query: object = {}, query: object = {},
@@ -340,7 +350,7 @@ function showMenuModel(
const allRoute = multiTags.value; const allRoute = multiTags.value;
const routeLength = multiTags.value.length; const routeLength = multiTags.value.length;
let currentIndex = -1; let currentIndex = -1;
if (isEmpty(query)) { if (isAllEmpty(query)) {
currentIndex = allRoute.findIndex(v => v.path === currentPath); currentIndex = allRoute.findIndex(v => v.path === currentPath);
} else { } else {
currentIndex = allRoute.findIndex(v => isEqual(v.query, query)); currentIndex = allRoute.findIndex(v => isEqual(v.query, query));
@@ -353,11 +363,11 @@ function showMenuModel(
} }
/** /**
* currentIndex为1时左侧的菜单是首页,则不显示关闭左侧标签页 * currentIndex为1时左侧的菜单顶级菜单,则不显示关闭左侧标签页
* 如果currentIndex等于routeLength-1右侧没有菜单则不显示关闭右侧标签页 * 如果currentIndex等于routeLength-1右侧没有菜单则不显示关闭右侧标签页
*/ */
if (currentIndex === 1 && routeLength !== 2) { if (currentIndex === 1 && routeLength !== 2) {
// 左侧的菜单是首页,右侧存在别的菜单 // 左侧的菜单是顶级菜单,右侧存在别的菜单
tagsViews[2].show = false; tagsViews[2].show = false;
Array.of(1, 3, 4, 5).forEach(v => { Array.of(1, 3, 4, 5).forEach(v => {
tagsViews[v].disabled = false; tagsViews[v].disabled = false;
@@ -365,7 +375,7 @@ function showMenuModel(
tagsViews[2].disabled = true; tagsViews[2].disabled = true;
} else if (currentIndex === 1 && routeLength === 2) { } else if (currentIndex === 1 && routeLength === 2) {
disabledMenus(false); disabledMenus(false);
// 左侧的菜单是首页,右侧不存在别的菜单 // 左侧的菜单是顶级菜单,右侧不存在别的菜单
Array.of(2, 3, 4).forEach(v => { Array.of(2, 3, 4).forEach(v => {
tagsViews[v].show = false; tagsViews[v].show = false;
tagsViews[v].disabled = true; tagsViews[v].disabled = true;
@@ -377,8 +387,8 @@ function showMenuModel(
tagsViews[v].disabled = false; tagsViews[v].disabled = false;
}); });
tagsViews[3].disabled = true; tagsViews[3].disabled = true;
} else if (currentIndex === 0 || currentPath === "/redirect/welcome") { } else if (currentIndex === 0 || currentPath === `/redirect${topPath}`) {
// 当前路由为首页 // 当前路由为顶级菜单
disabledMenus(true); disabledMenus(true);
} else { } else {
disabledMenus(false); disabledMenus(false);
@@ -387,8 +397,8 @@ function showMenuModel(
function openMenu(tag, e) { function openMenu(tag, e) {
closeMenu(); closeMenu();
if (tag.path === "/welcome") { if (tag.path === topPath) {
// 右键菜单为首页,只显示刷新 // 右键菜单为顶级菜单,只显示刷新
showMenus(false); showMenus(false);
tagsViews[0].show = true; tagsViews[0].show = true;
} else if (route.path !== tag.path && route.name !== tag.name) { } else if (route.path !== tag.path && route.name !== tag.name) {
@@ -450,7 +460,17 @@ function tagOnClick(item) {
// showMenuModel(item?.path, item?.query); // 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; if (!instance) return;
// 根据当前路由初始化操作标签页的禁用状态 // 根据当前路由初始化操作标签页的禁用状态
@@ -468,33 +488,31 @@ onBeforeMount(() => {
}); });
// 接收侧边栏切换传递过来的参数 // 接收侧边栏切换传递过来的参数
emitter.on("changLayoutRoute", ({ indexPath, parentPath }) => { emitter.on("changLayoutRoute", indexPath => {
dynamicRouteTag(indexPath, parentPath); dynamicRouteTag(indexPath);
setTimeout(() => { setTimeout(() => {
showMenuModel(indexPath); showMenuModel(indexPath);
}); });
}); });
});
watch([route], () => {
activeIndex.value = -1;
dynamicTagView();
});
onMounted(() => {
useResizeObserver( useResizeObserver(
scrollbarDom, scrollbarDom,
useDebounceFn(() => { debounce(() => dynamicTagView())
dynamicTagView();
}, 200)
); );
}); });
onBeforeUnmount(() => {
// 解绑`tagViewsChange`、`tagViewsShowModel`、`changLayoutRoute`公共事件,防止多次触发
emitter.off("tagViewsChange");
emitter.off("tagViewsShowModel");
emitter.off("changLayoutRoute");
});
</script> </script>
<template> <template>
<div ref="containerDom" class="tags-view" v-if="!showTags"> <div ref="containerDom" class="tags-view" v-if="!showTags">
<span v-show="isShowArrow" class="arrow-left"> <span v-show="isShowArrow" class="arrow-left">
<IconifyIconOffline icon="arrow-left-s-line" @click="handleScroll(200)" /> <IconifyIconOffline :icon="ArrowLeftSLine" @click="handleScroll(200)" />
</span> </span>
<div ref="scrollbarDom" class="scroll-container"> <div ref="scrollbarDom" class="scroll-container">
<div class="tab select-none" ref="tabDom" :style="getTabStyle"> <div class="tab select-none" ref="tabDom" :style="getTabStyle">
@@ -505,7 +523,7 @@ onMounted(() => {
:class="[ :class="[
'scroll-item is-closable', 'scroll-item is-closable',
linkIsActive(item), linkIsActive(item),
$route.path === item.path && showModel === 'card' route.path === item.path && showModel === 'card'
? 'card-active' ? 'card-active'
: '' : ''
]" ]"
@@ -528,7 +546,7 @@ onMounted(() => {
class="el-icon-close" class="el-icon-close"
@click.stop="deleteMenu(item)" @click.stop="deleteMenu(item)"
> >
<IconifyIconOffline icon="close-bold" /> <IconifyIconOffline :icon="CloseBold" />
</span> </span>
<div <div
:ref="'schedule' + index" :ref="'schedule' + index"
@@ -539,10 +557,7 @@ onMounted(() => {
</div> </div>
</div> </div>
<span v-show="isShowArrow" class="arrow-right"> <span v-show="isShowArrow" class="arrow-right">
<IconifyIconOffline <IconifyIconOffline :icon="ArrowRightSLine" @click="handleScroll(-200)" />
icon="arrow-right-s-line"
@click="handleScroll(-200)"
/>
</span> </span>
<!-- 右键菜单按钮 --> <!-- 右键菜单按钮 -->
<transition name="el-zoom-in-top"> <transition name="el-zoom-in-top">
@@ -571,7 +586,7 @@ onMounted(() => {
@command="handleCommand" @command="handleCommand"
> >
<span class="arrow-down"> <span class="arrow-down">
<IconifyIconOffline icon="arrow-down" class="dark:text-white" /> <IconifyIconOffline :icon="ArrowDown" class="dark:text-white" />
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
@@ -592,5 +607,5 @@ onMounted(() => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./index.scss"; @import url("./index.scss");
</style> </style>

View File

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

View File

@@ -1,9 +1,7 @@
import { ref } from "vue"; import { ref } from "vue";
import { getConfig } from "@/config"; import { getConfig } from "@/config";
import { find } from "lodash-unified";
import { useLayout } from "./useLayout"; import { useLayout } from "./useLayout";
import { themeColorsType } from "../types"; import { themeColorsType } from "../types";
import { TinyColor } from "@ctrl/tinycolor";
import { useGlobal } from "@pureadmin/utils"; import { useGlobal } from "@pureadmin/utils";
import { useEpThemeStoreHook } from "@/store/modules/epTheme"; import { useEpThemeStoreHook } from "@/store/modules/epTheme";
import { import {
@@ -40,7 +38,7 @@ export function useDataThemeChange() {
const body = document.documentElement as HTMLElement; const body = document.documentElement as HTMLElement;
/** 设置导航主题色 */ /** 设置导航主题色 */
function setLayoutThemeColor(theme = "default") { function setLayoutThemeColor(theme = getConfig().Theme ?? "default") {
layoutTheme.value.theme = theme; layoutTheme.value.theme = theme;
toggleTheme({ toggleTheme({
scopeName: `layout-theme-${theme}` scopeName: `layout-theme-${theme}`
@@ -56,35 +54,27 @@ export function useDataThemeChange() {
if (theme === "default" || theme === "light") { if (theme === "default" || theme === "light") {
setEpThemeColor(getConfig().EpThemeColor); setEpThemeColor(getConfig().EpThemeColor);
} else { } else {
const colors = find(themeColors.value, { themeColor: theme }); const colors = themeColors.value.find(v => v.themeColor === theme);
setEpThemeColor(colors.color); setEpThemeColor(colors.color);
} }
} }
/** function setPropertyPrimary(mode: string, i: number, color: string) {
* @description 自动计算hover和active颜色 document.documentElement.style.setProperty(
* @see {@link https://element-plus.org/zh-CN/component/button.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E9%A2%9C%E8%89%B2} `--el-color-primary-${mode}-${i}`,
*/ dataTheme.value ? darken(color, i / 10) : lighten(color, i / 10)
const shadeBgColor = (color: string): string => { );
return new TinyColor(color).shade(10).toString(); }
};
/** 设置 `element-plus` 主题色 */ /** 设置 `element-plus` 主题色 */
const setEpThemeColor = (color: string) => { const setEpThemeColor = (color: string) => {
useEpThemeStoreHook().setEpThemeColor(color); useEpThemeStoreHook().setEpThemeColor(color);
body.style.setProperty("--el-color-primary-active", shadeBgColor(color));
document.documentElement.style.setProperty("--el-color-primary", color); document.documentElement.style.setProperty("--el-color-primary", color);
for (let i = 1; i <= 9; i++) {
document.documentElement.style.setProperty(
`--el-color-primary-light-${i}`,
lighten(color, i / 10)
);
}
for (let i = 1; i <= 2; i++) { for (let i = 1; i <= 2; i++) {
document.documentElement.style.setProperty( setPropertyPrimary("dark", i, color);
`--el-color-primary-dark-${i}`, }
darken(color, i / 10) for (let i = 1; i <= 9; i++) {
); setPropertyPrimary("light", i, color);
} }
}; };

View File

@@ -1,11 +1,13 @@
import { computed } from "vue";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { getConfig } from "@/config"; import { getConfig } from "@/config";
import { emitter } from "@/utils/mitt"; import { emitter } from "@/utils/mitt";
import { routeMetaType } from "../types"; import { routeMetaType } from "../types";
import userAvatar from "@/assets/user.jpg";
import { getTopMenu } from "@/router/utils";
import { useGlobal } from "@pureadmin/utils"; import { useGlobal } from "@pureadmin/utils";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import { router, remainingPaths } from "@/router"; import { router, remainingPaths } from "@/router";
import { computed, type CSSProperties } from "vue";
import { useAppStoreHook } from "@/store/modules/app"; import { useAppStoreHook } from "@/store/modules/app";
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStoreHook } from "@/store/modules/user";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
@@ -17,6 +19,18 @@ export function useNav() {
const pureApp = useAppStoreHook(); const pureApp = useAppStoreHook();
const routers = useRouter().options.routes; const routers = useRouter().options.routes;
const { wholeMenus } = storeToRefs(usePermissionStoreHook()); const { wholeMenus } = storeToRefs(usePermissionStoreHook());
/** 平台`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(() => { const username = computed(() => {
@@ -56,8 +70,8 @@ export function useNav() {
useUserStoreHook().logOut(); useUserStoreHook().logOut();
} }
function backHome() { function backTopMenu() {
router.push("/welcome"); router.push(getTopMenu()?.path);
} }
function onPanel() { function onPanel() {
@@ -83,38 +97,13 @@ export function useNav() {
} }
} }
function menuSelect(indexPath: string, routers): void { function menuSelect(indexPath: string) {
if (wholeMenus.value.length === 0) return; if (wholeMenus.value.length === 0 || isRemaining(indexPath)) return;
if (isRemaining(indexPath)) return; emitter.emit("changLayoutRoute", indexPath);
let parentPath = "";
const parentPathIndex = indexPath.lastIndexOf("/");
if (parentPathIndex > 0) {
parentPath = indexPath.slice(0, parentPathIndex);
}
/** 找到当前路由的信息 */
function findCurrentRoute(indexPath: string, routes) {
if (!routes) return console.error(errorInfo);
return routes.map(item => {
if (item.path === indexPath) {
if (item.redirect) {
findCurrentRoute(item.redirect, item.children);
} else {
/** 切换左侧菜单 通知标签页 */
emitter.emit("changLayoutRoute", {
indexPath,
parentPath
});
}
} else {
if (item.children) findCurrentRoute(indexPath, item.children);
}
});
}
findCurrentRoute(indexPath, routers);
} }
/** 判断路径是否参与菜单 */ /** 判断路径是否参与菜单 */
function isRemaining(path: string): boolean { function isRemaining(path: string) {
return remainingPaths.includes(path); return remainingPaths.includes(path);
} }
@@ -126,8 +115,9 @@ export function useNav() {
logout, logout,
routers, routers,
$storage, $storage,
backHome, backTopMenu,
onPanel, onPanel,
getDivStyle,
changeTitle, changeTitle,
toggleSideBar, toggleSideBar,
menuSelect, menuSelect,
@@ -136,6 +126,8 @@ export function useNav() {
isCollapse, isCollapse,
pureApp, pureApp,
username, username,
avatarsStyle userAvatar,
avatarsStyle,
tooltipEffect
}; };
} }

View File

@@ -11,10 +11,24 @@ import {
import { tagsViewsType } from "../types"; import { tagsViewsType } from "../types";
import { useEventListener } from "@vueuse/core"; import { useEventListener } from "@vueuse/core";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { isEqual, isBoolean } from "@pureadmin/utils"; import { responsiveStorageNameSpace } from "@/config";
import { useSettingStoreHook } from "@/store/modules/settings"; import { useSettingStoreHook } from "@/store/modules/settings";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; 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";
import CloseOtherTags from "@iconify-icons/ri/text-spacing";
import CloseRightTags from "@iconify-icons/ri/text-direction-l";
import CloseLeftTags from "@iconify-icons/ri/text-direction-r";
import RefreshRight from "@iconify-icons/ep/refresh-right";
import Close from "@iconify-icons/ep/close";
export function useTags() { export function useTags() {
const route = useRoute(); const route = useRoute();
@@ -32,13 +46,16 @@ export function useTags() {
/** 显示模式,默认灵动模式 */ /** 显示模式,默认灵动模式 */
const showModel = ref( const showModel = ref(
storageLocal.getItem<StorageConfigs>("responsive-configure")?.showModel || storageLocal().getItem<StorageConfigs>(
"smart" `${responsiveStorageNameSpace()}configure`
)?.showModel || "smart"
); );
/** 是否隐藏标签页,默认显示 */ /** 是否隐藏标签页,默认显示 */
const showTags = const showTags =
ref( ref(
storageLocal.getItem<StorageConfigs>("responsive-configure").hideTabs storageLocal().getItem<StorageConfigs>(
`${responsiveStorageNameSpace()}configure`
).hideTabs
) ?? ref("false"); ) ?? ref("false");
const multiTags: any = computed(() => { const multiTags: any = computed(() => {
return useMultiTagsStoreHook().multiTags; return useMultiTagsStoreHook().multiTags;
@@ -46,56 +63,56 @@ export function useTags() {
const tagsViews = reactive<Array<tagsViewsType>>([ const tagsViews = reactive<Array<tagsViewsType>>([
{ {
icon: "refresh-right", icon: RefreshRight,
text: "重新加载", text: "重新加载",
divided: false, divided: false,
disabled: false, disabled: false,
show: true show: true
}, },
{ {
icon: "close", icon: Close,
text: "关闭当前标签页", text: "关闭当前标签页",
divided: false, divided: false,
disabled: multiTags.value.length > 1 ? false : true, disabled: multiTags.value.length > 1 ? false : true,
show: true show: true
}, },
{ {
icon: "close-left-tags", icon: CloseLeftTags,
text: "关闭左侧标签页", text: "关闭左侧标签页",
divided: true, divided: true,
disabled: multiTags.value.length > 1 ? false : true, disabled: multiTags.value.length > 1 ? false : true,
show: true show: true
}, },
{ {
icon: "close-right-tags", icon: CloseRightTags,
text: "关闭右侧标签页", text: "关闭右侧标签页",
divided: false, divided: false,
disabled: multiTags.value.length > 1 ? false : true, disabled: multiTags.value.length > 1 ? false : true,
show: true show: true
}, },
{ {
icon: "close-other-tags", icon: CloseOtherTags,
text: "关闭其他标签页", text: "关闭其他标签页",
divided: true, divided: true,
disabled: multiTags.value.length > 2 ? false : true, disabled: multiTags.value.length > 2 ? false : true,
show: true show: true
}, },
{ {
icon: "close-all-tags", icon: CloseAllTags,
text: "关闭全部标签页", text: "关闭全部标签页",
divided: false, divided: false,
disabled: multiTags.value.length > 1 ? false : true, disabled: multiTags.value.length > 1 ? false : true,
show: true show: true
}, },
{ {
icon: "fullscreen", icon: Fullscreen,
text: "整体页面全屏", text: "整体页面全屏",
divided: true, divided: true,
disabled: false, disabled: false,
show: true show: true
}, },
{ {
icon: "fullscreen", icon: Fullscreen,
text: "内容区全屏", text: "内容区全屏",
divided: false, divided: false,
disabled: false, disabled: false,
@@ -186,11 +203,14 @@ export function useTags() {
onMounted(() => { onMounted(() => {
if (!showModel.value) { if (!showModel.value) {
const configure = storageLocal.getItem<StorageConfigs>( const configure = storageLocal().getItem<StorageConfigs>(
"responsive-configure" `${responsiveStorageNameSpace()}configure`
); );
configure.showModel = "card"; configure.showModel = "card";
storageLocal.setItem("responsive-configure", configure); storageLocal().setItem(
`${responsiveStorageNameSpace()}configure`,
configure
);
} }
}); });

View File

@@ -1,11 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import "animate.css";
// 引入 src/components/ReIcon/src/offlineIcon.ts 文件中所有使用addIcon添加过的本地图标
import "@/components/ReIcon/src/offlineIcon";
import { setType } from "./types"; import { setType } from "./types";
import { emitter } from "@/utils/mitt";
import { useLayout } from "./hooks/useLayout"; import { useLayout } from "./hooks/useLayout";
import { useResizeObserver } from "@vueuse/core";
import { useAppStoreHook } from "@/store/modules/app"; import { useAppStoreHook } from "@/store/modules/app";
import { useSettingStoreHook } from "@/store/modules/settings"; import { useSettingStoreHook } from "@/store/modules/settings";
import { deviceDetection, useDark, useGlobal } from "@pureadmin/utils"; 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 navbar from "./components/navbar.vue";
import tag from "./components/tag/index.vue"; import tag from "./components/tag/index.vue";
@@ -15,6 +27,7 @@ import Vertical from "./components/sidebar/vertical.vue";
import Horizontal from "./components/sidebar/horizontal.vue"; import Horizontal from "./components/sidebar/horizontal.vue";
import backTop from "@/assets/svg/back_top.svg?component"; import backTop from "@/assets/svg/back_top.svg?component";
const appWrapperRef = ref();
const { isDark } = useDark(); const { isDark } = useDark();
const { layout } = useLayout(); const { layout } = useLayout();
const isMobile = deviceDetection(); const isMobile = deviceDetection();
@@ -67,10 +80,10 @@ function toggle(device: string, bool: boolean) {
// 判断是否可自动关闭菜单栏 // 判断是否可自动关闭菜单栏
let isAutoCloseSidebar = true; let isAutoCloseSidebar = true;
// 监听容器 useResizeObserver(appWrapperRef, entries => {
emitter.on("resize", ({ detail }) => {
if (isMobile) return; if (isMobile) return;
const { width } = detail; const entry = entries[0];
const { width } = entry.contentRect;
width <= 760 ? setTheme("vertical") : setTheme(useAppStoreHook().layout); width <= 760 ? setTheme("vertical") : setTheme(useAppStoreHook().layout);
/** width app-wrapper类容器宽度 /** width app-wrapper类容器宽度
* 0 < width <= 760 隐藏侧边栏 * 0 < width <= 760 隐藏侧边栏
@@ -85,11 +98,12 @@ emitter.on("resize", ({ detail }) => {
toggle("desktop", false); toggle("desktop", false);
isAutoCloseSidebar = false; isAutoCloseSidebar = false;
} }
} else if (width > 990) { } else if (width > 990 && !set.sidebar.isClickCollapse) {
if (!set.sidebar.isClickCollapse) { toggle("desktop", true);
toggle("desktop", true); isAutoCloseSidebar = true;
isAutoCloseSidebar = true; } else {
} toggle("desktop", false);
isAutoCloseSidebar = false;
} }
}); });
@@ -99,6 +113,10 @@ onMounted(() => {
} }
}); });
onBeforeMount(() => {
useDataThemeChange().dataThemeChange();
});
const layoutHeader = defineComponent({ const layoutHeader = defineComponent({
render() { render() {
return h( return h(
@@ -131,7 +149,7 @@ const layoutHeader = defineComponent({
</script> </script>
<template> <template>
<div :class="['app-wrapper', set.classes]" v-resize> <div ref="appWrapperRef" :class="['app-wrapper', set.classes]">
<div <div
v-show=" v-show="
set.device === 'mobile' && set.device === 'mobile' &&
@@ -176,20 +194,16 @@ const layoutHeader = defineComponent({
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@mixin clearfix { .app-wrapper {
position: relative;
width: 100%;
height: 100%;
&::after { &::after {
content: "";
display: table; display: table;
clear: both; clear: both;
content: "";
} }
}
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar { &.mobile.openSidebar {
position: fixed; position: fixed;
@@ -198,13 +212,13 @@ const layoutHeader = defineComponent({
} }
.app-mask { .app-mask {
position: absolute;
top: 0;
z-index: 999;
width: 100%;
height: 100%;
background: #000; background: #000;
opacity: 0.3; opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
} }
.re-screen { .re-screen {

View File

@@ -2,19 +2,11 @@
* @description ⚠️:此文件仅供主题插件使用,请不要在此文件中导出别的工具函数(仅在页面加载前运行) * @description ⚠️:此文件仅供主题插件使用,请不要在此文件中导出别的工具函数(仅在页面加载前运行)
*/ */
import { EpThemeColor } from "../../../public/serverConfig.json"; import { type multipleScopeVarsOptions } from "@pureadmin/theme";
type MultipleScopeVarsItem = {
scopeName: string;
varsContent: string;
};
/** 将vxe默认主题色和ep默认主题色保持一致 */
const vxeColor = EpThemeColor;
/** 预设主题色 */ /** 预设主题色 */
const themeColors = { const themeColors = {
default: { default: {
vxeColor,
subMenuActiveText: "#fff", subMenuActiveText: "#fff",
menuBg: "#001529", menuBg: "#001529",
menuHover: "#4091f7", menuHover: "#4091f7",
@@ -26,7 +18,6 @@ const themeColors = {
menuActiveBefore: "#4091f7" menuActiveBefore: "#4091f7"
}, },
light: { light: {
vxeColor,
subMenuActiveText: "#409eff", subMenuActiveText: "#409eff",
menuBg: "#fff", menuBg: "#fff",
menuHover: "#e0ebf6", menuHover: "#e0ebf6",
@@ -38,7 +29,6 @@ const themeColors = {
menuActiveBefore: "#4091f7" menuActiveBefore: "#4091f7"
}, },
dusk: { dusk: {
vxeColor: "#f5222d",
subMenuActiveText: "#fff", subMenuActiveText: "#fff",
menuBg: "#2a0608", menuBg: "#2a0608",
menuHover: "#e13c39", menuHover: "#e13c39",
@@ -50,7 +40,6 @@ const themeColors = {
menuActiveBefore: "#e13c39" menuActiveBefore: "#e13c39"
}, },
volcano: { volcano: {
vxeColor: "#fa541c",
subMenuActiveText: "#fff", subMenuActiveText: "#fff",
menuBg: "#2b0e05", menuBg: "#2b0e05",
menuHover: "#e85f33", menuHover: "#e85f33",
@@ -62,7 +51,6 @@ const themeColors = {
menuActiveBefore: "#e85f33" menuActiveBefore: "#e85f33"
}, },
yellow: { yellow: {
vxeColor: "#fadb14",
subMenuActiveText: "#d25f00", subMenuActiveText: "#d25f00",
menuBg: "#2b2503", menuBg: "#2b2503",
menuHover: "#f6da4d", menuHover: "#f6da4d",
@@ -74,7 +62,6 @@ const themeColors = {
menuActiveBefore: "#f6da4d" menuActiveBefore: "#f6da4d"
}, },
mingQing: { mingQing: {
vxeColor: "#13c2c2",
subMenuActiveText: "#fff", subMenuActiveText: "#fff",
menuBg: "#032121", menuBg: "#032121",
menuHover: "#59bfc1", menuHover: "#59bfc1",
@@ -86,7 +73,6 @@ const themeColors = {
menuActiveBefore: "#59bfc1" menuActiveBefore: "#59bfc1"
}, },
auroraGreen: { auroraGreen: {
vxeColor: "#52c41a",
subMenuActiveText: "#fff", subMenuActiveText: "#fff",
menuBg: "#0b1e15", menuBg: "#0b1e15",
menuHover: "#60ac80", menuHover: "#60ac80",
@@ -98,7 +84,6 @@ const themeColors = {
menuActiveBefore: "#60ac80" menuActiveBefore: "#60ac80"
}, },
pink: { pink: {
vxeColor: "#eb2f96",
subMenuActiveText: "#fff", subMenuActiveText: "#fff",
menuBg: "#28081a", menuBg: "#28081a",
menuHover: "#d84493", menuHover: "#d84493",
@@ -110,7 +95,6 @@ const themeColors = {
menuActiveBefore: "#d84493" menuActiveBefore: "#d84493"
}, },
saucePurple: { saucePurple: {
vxeColor: "#722ed1",
subMenuActiveText: "#fff", subMenuActiveText: "#fff",
menuBg: "#130824", menuBg: "#130824",
menuHover: "#693ac9", menuHover: "#693ac9",
@@ -126,13 +110,12 @@ const themeColors = {
/** /**
* @description 将预设主题色处理成主题插件所需格式 * @description 将预设主题色处理成主题插件所需格式
*/ */
export const genScssMultipleScopeVars = (): MultipleScopeVarsItem[] => { export const genScssMultipleScopeVars = (): multipleScopeVarsOptions[] => {
const result = [] as MultipleScopeVarsItem[]; const result = [] as multipleScopeVarsOptions[];
Object.keys(themeColors).forEach(key => { Object.keys(themeColors).forEach(key => {
result.push({ result.push({
scopeName: `layout-theme-${key}`, scopeName: `layout-theme-${key}`,
varsContent: ` varsContent: `
$vxe-primary-color: ${themeColors[key].vxeColor} !default;
$subMenuActiveText: ${themeColors[key].subMenuActiveText} !default; $subMenuActiveText: ${themeColors[key].subMenuActiveText} !default;
$menuBg: ${themeColors[key].menuBg} !default; $menuBg: ${themeColors[key].menuBg} !default;
$menuHover: ${themeColors[key].menuHover} !default; $menuHover: ${themeColors[key].menuHover} !default;
@@ -143,7 +126,7 @@ export const genScssMultipleScopeVars = (): MultipleScopeVarsItem[] => {
$menuTitleHover: ${themeColors[key].menuTitleHover} !default; $menuTitleHover: ${themeColors[key].menuTitleHover} !default;
$menuActiveBefore: ${themeColors[key].menuActiveBefore} !default; $menuActiveBefore: ${themeColors[key].menuActiveBefore} !default;
` `
} as MultipleScopeVarsItem); } as multipleScopeVarsOptions);
}); });
return result; return result;
}; };

View File

@@ -1,17 +1,22 @@
export const routerArrays: Array<RouteConfigs> = [ import type { IconifyIcon } from "@iconify/vue";
{ const { VITE_HIDE_HOME } = import.meta.env;
path: "/welcome",
parentPath: "/", export const routerArrays: Array<RouteConfigs> =
meta: { VITE_HIDE_HOME === "false"
title: "首页", ? [
icon: "home-filled" {
} path: "/welcome",
} meta: {
]; title: "首页",
icon: "homeFilled"
}
}
]
: [];
export type routeMetaType = { export type routeMetaType = {
title?: string; title?: string;
icon?: string; icon?: string | IconifyIcon;
showLink?: boolean; showLink?: boolean;
savedPosition?: boolean; savedPosition?: boolean;
auths?: Array<string>; auths?: Array<string>;
@@ -19,7 +24,6 @@ export type routeMetaType = {
export type RouteConfigs = { export type RouteConfigs = {
path?: string; path?: string;
parentPath?: string;
query?: object; query?: object;
params?: object; params?: object;
meta?: routeMetaType; meta?: routeMetaType;
@@ -32,7 +36,7 @@ export type multiTagsType = {
}; };
export type tagsViewsType = { export type tagsViewsType = {
icon: string; icon: string | IconifyIcon;
text: string; text: string;
divided: boolean; divided: boolean;
disabled: boolean; disabled: boolean;
@@ -56,23 +60,23 @@ export interface setType {
hideTabs: boolean; hideTabs: boolean;
} }
export type childrenType = { export type menuType = {
id?: number;
path?: string; path?: string;
noShowingChildren?: boolean; noShowingChildren?: boolean;
children?: childrenType[]; children?: menuType[];
value: unknown; value: unknown;
meta?: { meta?: {
icon?: string; icon?: string;
title?: string; title?: string;
rank?: number;
showParent?: boolean; showParent?: boolean;
extraIcon?: { extraIcon?: string;
svg?: boolean;
name?: string;
};
}; };
showTooltip?: boolean; showTooltip?: boolean;
parentId?: number; parentId?: number;
pathList?: number[]; pathList?: number[];
redirect?: string;
}; };
export type themeColorsType = { export type themeColorsType = {

View File

@@ -6,19 +6,18 @@ import { getServerConfig } from "./config";
import { createApp, Directive } from "vue"; import { createApp, Directive } from "vue";
import { MotionPlugin } from "@vueuse/motion"; import { MotionPlugin } from "@vueuse/motion";
// import { useEcharts } from "@/plugins/echarts"; // import { useEcharts } from "@/plugins/echarts";
// import { useTable } from "@/plugins/vxe-table";
import { injectResponsiveStorage } from "@/utils/responsive"; import { injectResponsiveStorage } from "@/utils/responsive";
// import Table from "@pureadmin/table"; // import Table from "@pureadmin/table";
// import PureDescriptions from "@pureadmin/descriptions"; // import PureDescriptions from "@pureadmin/descriptions";
import "animate.css";
// 引入重置样式 // 引入重置样式
import "./style/reset.scss"; import "./style/reset.scss";
// 导入公共样式 // 导入公共样式
import "./style/index.scss"; import "./style/index.scss";
// 一定要在main.ts中导入tailwind.css防止vite每次hmr都会请求src/style/index.scss整体css文件导致热更新慢的问题
import "./style/tailwind.css";
import "element-plus/dist/index.css"; import "element-plus/dist/index.css";
// 导入字体图标 // 导入字体图标
import "./assets/iconfont/iconfont.js"; import "./assets/iconfont/iconfont.js";
import "./assets/iconfont/iconfont.css"; import "./assets/iconfont/iconfont.css";
@@ -54,6 +53,5 @@ getServerConfig(app).then(async config => {
// .use(useEcharts); // .use(useEcharts);
// .use(Table); // .use(Table);
// .use(PureDescriptions); // .use(PureDescriptions);
// .use(useTable);
app.mount("#app"); app.mount("#app");
}); });

View File

@@ -1,6 +1,6 @@
import type { App } from "vue"; import type { App } from "vue";
import * as echarts from "echarts/core"; import * as echarts from "echarts/core";
import { SVGRenderer } from "echarts/renderers"; import { CanvasRenderer } from "echarts/renderers";
import { PieChart, BarChart, LineChart } from "echarts/charts"; import { PieChart, BarChart, LineChart } from "echarts/charts";
import { import {
GridComponent, GridComponent,
@@ -19,7 +19,7 @@ use([
PieChart, PieChart,
BarChart, BarChart,
LineChart, LineChart,
SVGRenderer, CanvasRenderer,
GridComponent, GridComponent,
TitleComponent, TitleComponent,
LegendComponent, LegendComponent,
@@ -33,6 +33,7 @@ use([
/** /**
* @description 按需引入echarts * @description 按需引入echarts
* @see {@link https://echarts.apache.org/handbook/zh/basics/import#%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5-echarts-%E5%9B%BE%E8%A1%A8%E5%92%8C%E7%BB%84%E4%BB%B6} * @see {@link https://echarts.apache.org/handbook/zh/basics/import#%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5-echarts-%E5%9B%BE%E8%A1%A8%E5%92%8C%E7%BB%84%E4%BB%B6}
* @see 温馨提示:必须将 `$echarts` 添加到全局 `globalProperties` ,为了配合 https://pure-admin-utils.netlify.app/hooks/useEcharts/useEcharts.html 使用
*/ */
export function useEcharts(app: App) { export function useEcharts(app: App) {
app.config.globalProperties.$echarts = echarts; app.config.globalProperties.$echarts = echarts;

View File

@@ -1,6 +0,0 @@
@import "vxe-table/styles/variable.scss";
@import "vxe-table/styles/modules.scss";
i {
border-color: initial;
}

View File

@@ -1,98 +0,0 @@
import "xe-utils";
import "./index.scss";
import { App } from "vue";
import "font-awesome/css/font-awesome.min.css";
import {
// 核心
VXETable,
// 表格功能
Icon,
Filter,
Edit,
Menu,
Export,
Keyboard,
Validator,
// 可选组件
Column,
Colgroup,
Grid,
Tooltip,
Toolbar,
Pager,
Form,
FormItem,
FormGather,
Checkbox,
CheckboxGroup,
Radio,
RadioGroup,
RadioButton,
Switch,
Input,
Select,
Optgroup,
Option,
Textarea,
Button,
Modal,
List,
Pulldown,
// 表格
Table
} from "vxe-table";
// 全局默认参数
VXETable.setup({
size: "medium",
version: 0,
zIndex: 1002,
table: {
// 自动监听父元素的变化去重新计算表格
autoResize: true,
// 鼠标移到行是否要高亮显示
highlightHoverRow: true
},
input: {
clearable: true
}
});
export function useTable(app: App) {
app
.use(Icon)
.use(Filter)
.use(Edit)
.use(Menu)
.use(Export)
.use(Keyboard)
.use(Validator)
// 可选组件
.use(Column)
.use(Colgroup)
.use(Grid)
.use(Tooltip)
.use(Toolbar)
.use(Pager)
.use(Form)
.use(FormItem)
.use(FormGather)
.use(Checkbox)
.use(CheckboxGroup)
.use(Radio)
.use(RadioGroup)
.use(RadioButton)
.use(Switch)
.use(Input)
.use(Select)
.use(Optgroup)
.use(Option)
.use(Textarea)
.use(Button)
.use(Modal)
.use(List)
.use(Pulldown)
// 安装表格
.use(Table);
}

View File

@@ -1,7 +1,6 @@
// import "@/utils/sso"; // import "@/utils/sso";
import { getConfig } from "@/config"; import { getConfig } from "@/config";
import NProgress from "@/utils/progress"; import NProgress from "@/utils/progress";
import { findIndex } from "lodash-unified";
import { sessionKey, type DataInfo } from "@/utils/auth"; import { sessionKey, type DataInfo } from "@/utils/auth";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
@@ -13,6 +12,7 @@ import {
} from "vue-router"; } from "vue-router";
import { import {
ascending, ascending,
getTopMenu,
initRouter, initRouter,
isOneOfArray, isOneOfArray,
getHistoryMode, getHistoryMode,
@@ -22,7 +22,7 @@ import {
formatFlatteningRoutes formatFlatteningRoutes
} from "./utils"; } from "./utils";
import { buildHierarchyTree } from "@/utils/tree"; 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"; import remainingRouter from "./modules/remaining";
@@ -46,13 +46,13 @@ Object.keys(modules).forEach(key => {
/** 导出处理后的静态路由(三级及以上的路由全部拍成二级) */ /** 导出处理后的静态路由(三级及以上的路由全部拍成二级) */
export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes( export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes(
formatFlatteningRoutes(buildHierarchyTree(ascending(routes))) formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity))))
); );
/** 用于渲染菜单,保持原始层级 */ /** 用于渲染菜单,保持原始层级 */
export const constantMenus: Array<RouteComponent> = ascending(routes).concat( export const constantMenus: Array<RouteComponent> = ascending(
...remainingRouter routes.flat(Infinity)
); ).concat(...remainingRouter);
/** 不参与菜单的路由 */ /** 不参与菜单的路由 */
export const remainingPaths = Object.keys(remainingRouter).map(v => { export const remainingPaths = Object.keys(remainingRouter).map(v => {
@@ -61,7 +61,7 @@ export const remainingPaths = Object.keys(remainingRouter).map(v => {
/** 创建路由实例 */ /** 创建路由实例 */
export const router: Router = createRouter({ export const router: Router = createRouter({
history: getHistoryMode(), history: getHistoryMode(import.meta.env.VITE_ROUTER_HISTORY),
routes: constantRoutes.concat(...(remainingRouter as any)), routes: constantRoutes.concat(...(remainingRouter as any)),
strict: true, strict: true,
scrollBehavior(to, from, savedPosition) { scrollBehavior(to, from, savedPosition) {
@@ -86,7 +86,9 @@ export function resetRouter() {
if (name && router.hasRoute(name) && meta?.backstage) { if (name && router.hasRoute(name) && meta?.backstage) {
router.removeRoute(name); router.removeRoute(name);
router.options.routes = formatTwoStageRoutes( router.options.routes = formatTwoStageRoutes(
formatFlatteningRoutes(buildHierarchyTree(ascending(routes))) formatFlatteningRoutes(
buildHierarchyTree(ascending(routes.flat(Infinity)))
)
); );
} }
}); });
@@ -96,16 +98,17 @@ export function resetRouter() {
/** 路由白名单 */ /** 路由白名单 */
const whiteList = ["/login"]; 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) { if (to.meta?.keepAlive) {
const newMatched = to.matched; handleAliveRoute(to, "add");
handleAliveRoute(newMatched, "add");
// 页面整体刷新和点击标签页刷新 // 页面整体刷新和点击标签页刷新
if (_from.name === undefined || _from.name === "Redirect") { if (_from.name === undefined || _from.name === "Redirect") {
handleAliveRoute(newMatched); handleAliveRoute(to);
} }
} }
const userInfo = storageSession.getItem<DataInfo<number>>(sessionKey); const userInfo = storageSession().getItem<DataInfo<number>>(sessionKey);
NProgress.start(); NProgress.start();
const externalLink = isUrl(to?.name as string); const externalLink = isUrl(to?.name as string);
if (!externalLink) { if (!externalLink) {
@@ -125,6 +128,10 @@ router.beforeEach((to: toRouteType, _from, next) => {
if (to.meta?.roles && !isOneOfArray(to.meta?.roles, userInfo?.roles)) { if (to.meta?.roles && !isOneOfArray(to.meta?.roles, userInfo?.roles)) {
next({ path: "/error/403" }); next({ path: "/error/403" });
} }
// 开启隐藏首页后在浏览器地址栏手动输入首页welcome路由则跳转到404页面
if (VITE_HIDE_HOME === "true" && to.fullPath === "/welcome") {
next({ path: "/error/404" });
}
if (_from?.name) { if (_from?.name) {
// name为超链接 // name为超链接
if (externalLink) { if (externalLink) {
@@ -138,29 +145,39 @@ router.beforeEach((to: toRouteType, _from, next) => {
if ( if (
usePermissionStoreHook().wholeMenus.length === 0 && usePermissionStoreHook().wholeMenus.length === 0 &&
to.path !== "/login" to.path !== "/login"
) ) {
initRouter().then((router: Router) => { initRouter().then((router: Router) => {
if (!useMultiTagsStoreHook().getMultiTagsCache) { if (!useMultiTagsStoreHook().getMultiTagsCache) {
const { path } = to; const { path } = to;
const index = findIndex(remainingRouter, v => { const route = findRouteByPath(
return v.path == path; path,
}); router.options.routes[0].children
const routes: any = );
index === -1 getTopMenu(true);
? router.options.routes[0].children
: router.options.routes;
const route = findRouteByPath(path, routes);
// query、params模式路由传参数的标签页不在此处处理 // query、params模式路由传参数的标签页不在此处处理
if (route && route.meta?.title) { if (route && route.meta?.title) {
useMultiTagsStoreHook().handleTags("push", { if (isAllEmpty(route.parentId) && route.meta?.backstage) {
path: route.path, // 此处为动态顶级路由(目录)
name: route.name, const { path, name, meta } = route.children[0];
meta: route.meta 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(); toCorrectRoute();
} }
} else { } else {

View File

@@ -2,8 +2,9 @@ export default {
path: "/error", path: "/error",
redirect: "/error/403", redirect: "/error/403",
meta: { meta: {
icon: "information-line", icon: "informationLine",
title: "异常页面", title: "异常页面",
// showLink: false,
rank: 9 rank: 9
}, },
children: [ children: [

View File

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

View File

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

View File

@@ -3,22 +3,24 @@ import {
RouteRecordRaw, RouteRecordRaw,
RouteComponent, RouteComponent,
createWebHistory, createWebHistory,
createWebHashHistory, createWebHashHistory
RouteRecordNormalized
} from "vue-router"; } from "vue-router";
import { router } from "./index"; import { router } from "./index";
import { isProxy, toRaw } from "vue"; import { isProxy, toRaw } from "vue";
import { loadEnv } from "../../build";
import { useTimeoutFn } from "@vueuse/core"; import { useTimeoutFn } from "@vueuse/core";
import { RouteConfigs } from "@/layout/types";
import { import {
isString, isString,
cloneDeep,
isAllEmpty,
intersection,
storageSession, storageSession,
isIncludeAllChildren isIncludeAllChildren
} from "@pureadmin/utils"; } from "@pureadmin/utils";
import { getConfig } from "@/config";
import { menuType } from "@/layout/types";
import { buildHierarchyTree } from "@/utils/tree"; import { buildHierarchyTree } from "@/utils/tree";
import { cloneDeep, intersection } from "lodash-unified";
import { sessionKey, type DataInfo } from "@/utils/auth"; import { sessionKey, type DataInfo } from "@/utils/auth";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { usePermissionStoreHook } from "@/store/modules/permission"; import { usePermissionStoreHook } from "@/store/modules/permission";
const IFrame = () => import("@/layout/frameView.vue"); const IFrame = () => import("@/layout/frameView.vue");
// https://cn.vitejs.dev/guide/features.html#glob-import // https://cn.vitejs.dev/guide/features.html#glob-import
@@ -27,19 +29,25 @@ const modulesRoutes = import.meta.glob("/src/views/**/*.{vue,tsx}");
// 动态路由 // 动态路由
import { getAsyncRoutes } from "@/api/routes"; import { getAsyncRoutes } from "@/api/routes";
function handRank(routeInfo: any) {
const { name, path, parentId, meta } = routeInfo;
return isAllEmpty(parentId)
? isAllEmpty(meta?.rank) ||
(meta?.rank === 0 && name !== "Home" && path !== "/")
? true
: false
: false;
}
/** 按照路由中meta下的rank等级升序来排序路由 */ /** 按照路由中meta下的rank等级升序来排序路由 */
function ascending(arr: any[]) { function ascending(arr: any[]) {
arr.forEach(v => { arr.forEach((v, index) => {
if (v?.meta?.rank === null) v.meta.rank = undefined; // 当rank不存在时根据顺序自动创建首页路由永远在第一位
if (v?.meta?.rank === 0) { if (handRank(v)) v.meta.rank = index + 2;
if (v.name !== "Home" && v.path !== "/") {
console.warn("rank only the home page can be 0");
}
}
}); });
return arr.sort( return arr.sort(
(a: { meta: { rank: number } }, b: { meta: { rank: number } }) => { (a: { meta: { rank: number } }, b: { meta: { rank: number } }) => {
return a?.meta?.rank - b?.meta?.rank; return a?.meta.rank - b?.meta.rank;
} }
); );
} }
@@ -76,7 +84,7 @@ function isOneOfArray(a: Array<string>, b: Array<string>) {
/** 从sessionStorage里取出当前登陆用户的角色roles过滤无权限的菜单 */ /** 从sessionStorage里取出当前登陆用户的角色roles过滤无权限的菜单 */
function filterNoPermissionTree(data: RouteComponent[]) { function filterNoPermissionTree(data: RouteComponent[]) {
const currentRoles = const currentRoles =
storageSession.getItem<DataInfo<number>>(sessionKey)?.roles ?? []; storageSession().getItem<DataInfo<number>>(sessionKey)?.roles ?? [];
const newTree = cloneDeep(data).filter((v: any) => const newTree = cloneDeep(data).filter((v: any) =>
isOneOfArray(v.meta?.roles, currentRoles) isOneOfArray(v.meta?.roles, currentRoles)
); );
@@ -86,30 +94,20 @@ function filterNoPermissionTree(data: RouteComponent[]) {
return filterChildrenTree(newTree); return filterChildrenTree(newTree);
} }
/** 批量删除缓存路由(keepalive) */ /** 通过指定 `key` 获取父级路径集合,默认 `key` 为 `path` */
function delAliveRoutes(delAliveRouteList: Array<RouteConfigs>) { function getParentPaths(value: string, routes: RouteRecordRaw[], key = "path") {
delAliveRouteList.forEach(route => {
usePermissionStoreHook().cacheOperate({
mode: "delete",
name: route?.name
});
});
}
/** 通过path获取父级路径 */
function getParentPaths(path: string, routes: RouteRecordRaw[]) {
// 深度遍历查找 // 深度遍历查找
function dfs(routes: RouteRecordRaw[], path: string, parents: string[]) { function dfs(routes: RouteRecordRaw[], value: string, parents: string[]) {
for (let i = 0; i < routes.length; i++) { for (let i = 0; i < routes.length; i++) {
const item = routes[i]; const item = routes[i];
// 找到path则返回父级path // 返回父级path
if (item.path === path) return parents; if (item[key] === value) return parents;
// children不存在或为空则不递归 // children不存在或为空则不递归
if (!item.children || !item.children.length) continue; if (!item.children || !item.children.length) continue;
// 往下查找时将当前path入栈 // 往下查找时将当前path入栈
parents.push(item.path); parents.push(item.path);
if (dfs(item.children, path, parents).length) return parents; if (dfs(item.children, value, parents).length) return parents;
// 深度遍历查找未找到时当前path 出栈 // 深度遍历查找未找到时当前path 出栈
parents.pop(); parents.pop();
} }
@@ -117,10 +115,10 @@ function getParentPaths(path: string, routes: RouteRecordRaw[]) {
return []; return [];
} }
return dfs(routes, path, []); return dfs(routes, value, []);
} }
/** 查找对应path的路由信息 */ /** 查找对应 `path` 的路由信息 */
function findRouteByPath(path: string, routes: RouteRecordRaw[]) { function findRouteByPath(path: string, routes: RouteRecordRaw[]) {
let res = routes.find((item: { path: string }) => item.path == path); let res = routes.find((item: { path: string }) => item.path == path);
if (res) { if (res) {
@@ -151,42 +149,66 @@ function addPathMatch() {
} }
} }
/** 初始化路由 */ /** 处理动态路由(后端返回的路由) */
function initRouter() { function handleAsyncRoutes(routeList) {
return new Promise(resolve => { if (routeList.length === 0) {
getAsyncRoutes().then(({ data }) => { usePermissionStoreHook().handleWholeMenus(routeList);
if (data.length === 0) { } else {
usePermissionStoreHook().handleWholeMenus(data); formatFlatteningRoutes(addAsyncRoutes(routeList)).map(
resolve(router); (v: RouteRecordRaw) => {
} else { // 防止重复添加路由
formatFlatteningRoutes(addAsyncRoutes(data)).map( if (
(v: RouteRecordRaw) => { router.options.routes[0].children.findIndex(
// 防止重复添加路由 value => value.path === v.path
if ( ) !== -1
router.options.routes[0].children.findIndex( ) {
value => value.path === v.path return;
) !== -1 } else {
) { // 切记将路由push到routes后还需要使用addRoute这样路由才能正常跳转
return; router.options.routes[0].children.push(v);
} else { // 最终路由进行升序
// 切记将路由push到routes后还需要使用addRoute这样路由才能正常跳转 ascending(router.options.routes[0].children);
router.options.routes[0].children.push(v); if (!router.hasRoute(v?.name)) router.addRoute(v);
// 最终路由进行升序 const flattenRouters: any = router
ascending(router.options.routes[0].children); .getRoutes()
if (!router.hasRoute(v?.name)) router.addRoute(v); .find(n => n.path === "/");
const flattenRouters: any = router router.addRoute(flattenRouters);
.getRoutes() }
.find(n => n.path === "/");
router.addRoute(flattenRouters);
}
resolve(router);
}
);
usePermissionStoreHook().handleWholeMenus(data);
} }
addPathMatch(); );
usePermissionStoreHook().handleWholeMenus(routeList);
}
addPathMatch();
}
/** 初始化路由(`new Promise` 写法防止在异步请求中造成无限循环)*/
function initRouter() {
if (getConfig()?.CachingAsyncRoutes) {
// 开启动态路由缓存本地sessionStorage
const key = "async-routes";
const asyncRouteList = storageSession().getItem(key) as any;
if (asyncRouteList && asyncRouteList?.length > 0) {
return new Promise(resolve => {
handleAsyncRoutes(asyncRouteList);
resolve(router);
});
} else {
return new Promise(resolve => {
getAsyncRoutes().then(({ data }) => {
handleAsyncRoutes(cloneDeep(data));
storageSession().setItem(key, data);
resolve(router);
});
});
}
} else {
return new Promise(resolve => {
getAsyncRoutes().then(({ data }) => {
handleAsyncRoutes(cloneDeep(data));
resolve(router);
});
}); });
}); }
} }
/** /**
@@ -209,7 +231,7 @@ function formatFlatteningRoutes(routesList: RouteRecordRaw[]) {
/** /**
* 一维数组处理成多级嵌套数组三级及以上的路由全部拍成二级keep-alive 只支持到二级缓存) * 一维数组处理成多级嵌套数组三级及以上的路由全部拍成二级keep-alive 只支持到二级缓存)
* https://github.com/xiaoxian521/vue-pure-admin/issues/67 * https://github.com/pure-admin/vue-pure-admin/issues/67
* @param routesList 处理后的一维路由菜单数组 * @param routesList 处理后的一维路由菜单数组
* @returns 返回将一维数组重新处理成规定路由的格式 * @returns 返回将一维数组重新处理成规定路由的格式
*/ */
@@ -227,34 +249,42 @@ function formatTwoStageRoutes(routesList: RouteRecordRaw[]) {
children: [] children: []
}); });
} else { } else {
newRoutesList[0].children.push({ ...v }); newRoutesList[0]?.children.push({ ...v });
} }
}); });
return newRoutesList; return newRoutesList;
} }
/** 处理缓存路由(添加、删除、刷新) */ /** 处理缓存路由(添加、删除、刷新) */
function handleAliveRoute(matched: RouteRecordNormalized[], mode?: string) { function handleAliveRoute({ name }: ToRouteType, mode?: string) {
switch (mode) { switch (mode) {
case "add": case "add":
matched.forEach(v => { usePermissionStoreHook().cacheOperate({
usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name }); mode: "add",
name
}); });
break; break;
case "delete": case "delete":
usePermissionStoreHook().cacheOperate({ usePermissionStoreHook().cacheOperate({
mode: "delete", mode: "delete",
name: matched[matched.length - 1].name name
});
break;
case "refresh":
usePermissionStoreHook().cacheOperate({
mode: "refresh",
name
}); });
break; break;
default: default:
usePermissionStoreHook().cacheOperate({ usePermissionStoreHook().cacheOperate({
mode: "delete", mode: "delete",
name: matched[matched.length - 1].name name
}); });
useTimeoutFn(() => { useTimeoutFn(() => {
matched.forEach(v => { usePermissionStoreHook().cacheOperate({
usePermissionStoreHook().cacheOperate({ mode: "add", name: v.name }); mode: "add",
name
}); });
}, 100); }, 100);
} }
@@ -290,8 +320,7 @@ function addAsyncRoutes(arrRoutes: Array<RouteRecordRaw>) {
} }
/** 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html */ /** 获取路由历史模式 https://next.router.vuejs.org/zh/guide/essentials/history-mode.html */
function getHistoryMode(): RouterHistory { function getHistoryMode(routerHistory): RouterHistory {
const routerHistory = loadEnv().VITE_ROUTER_HISTORY;
// len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1 // len为1 代表只有历史模式 为2 代表历史模式中存在base参数 https://next.router.vuejs.org/zh/api/#%E5%8F%82%E6%95%B0-1
const historyMode = routerHistory.split(","); const historyMode = routerHistory.split(",");
const leftMode = historyMode[0]; const leftMode = historyMode[0];
@@ -323,22 +352,31 @@ function hasAuth(value: string | Array<string>): boolean {
if (!value) return false; if (!value) return false;
/** 从当前路由的`meta`字段里获取按钮级别的所有自定义`code`值 */ /** 从当前路由的`meta`字段里获取按钮级别的所有自定义`code`值 */
const metaAuths = getAuths(); const metaAuths = getAuths();
if (!metaAuths) return false;
const isAuths = isString(value) const isAuths = isString(value)
? metaAuths.includes(value) ? metaAuths.includes(value)
: isIncludeAllChildren(value, metaAuths); : isIncludeAllChildren(value, metaAuths);
return isAuths ? true : false; return isAuths ? true : false;
} }
/** 获取所有菜单中的第一个菜单(顶级菜单)*/
function getTopMenu(tag = false): menuType {
const topMenu = usePermissionStoreHook().wholeMenus[0]?.children[0];
tag && useMultiTagsStoreHook().handleTags("push", topMenu);
return topMenu;
}
export { export {
hasAuth, hasAuth,
getAuths, getAuths,
ascending, ascending,
filterTree, filterTree,
initRouter, initRouter,
getTopMenu,
addPathMatch,
isOneOfArray, isOneOfArray,
getHistoryMode, getHistoryMode,
addAsyncRoutes, addAsyncRoutes,
delAliveRoutes,
getParentPaths, getParentPaths,
findRouteByPath, findRouteByPath,
handleAliveRoute, handleAliveRoute,

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,8 @@ import { defineStore } from "pinia";
import { store } from "@/store"; import { store } from "@/store";
import { cacheType } from "./types"; import { cacheType } from "./types";
import { constantMenus } from "@/router"; import { constantMenus } from "@/router";
import { useMultiTagsStoreHook } from "./multiTags";
import { debounce, getKeyList } from "@pureadmin/utils";
import { ascending, filterTree, filterNoPermissionTree } from "@/router/utils"; import { ascending, filterTree, filterNoPermissionTree } from "@/router/utils";
export const usePermissionStore = defineStore({ export const usePermissionStore = defineStore({
@@ -22,17 +24,32 @@ export const usePermissionStore = defineStore({
); );
}, },
cacheOperate({ mode, name }: cacheType) { cacheOperate({ mode, name }: cacheType) {
const delIndex = this.cachePageList.findIndex(v => v === name);
switch (mode) { switch (mode) {
case "refresh":
this.cachePageList = this.cachePageList.filter(v => v !== name);
break;
case "add": case "add":
this.cachePageList.push(name); this.cachePageList.push(name);
this.cachePageList = [...new Set(this.cachePageList)];
break; break;
case "delete": case "delete":
// eslint-disable-next-line no-case-declarations
const delIndex = this.cachePageList.findIndex(v => v === name);
delIndex !== -1 && this.cachePageList.splice(delIndex, 1); delIndex !== -1 && this.cachePageList.splice(delIndex, 1);
break; 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() { clearAllCachePage() {

View File

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

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