Compare commits

...

39 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
xiaoxian521
bbe23ce0a2 release: update 3.8.7 2022-11-28 01:49:35 +08:00
xiaoxian521
68492ec362 docs: update 2022-11-27 18:05:55 +08:00
xiaoxian521
bc3b199860 release: update 3.8.6 2022-11-27 17:40:35 +08:00
xiaoxian521
4407dd5d10 perf: 无需安装 @vue/runtime-core,兼容 element-plus 的组件 volar 提示 2022-11-27 09:56:19 +08:00
xiaoxian521
b3cbdd6e87 release: update 3.8.5 2022-11-27 00:33:24 +08:00
xiaoxian521
b5f0ca52ce perf: 移除 @pureadmin/components , 打包大小未启用压缩前减少 0.48 MB , 首屏请求减少 2.3 MB 的资源,请务必升级哦 2022-11-26 21:23:43 +08:00
xiaoxian521
6110be29a1 release: update 3.8.0 2022-11-26 19:14:08 +08:00
xiaoxian521
d53496e495 chore: 同步完整版代码 2022-11-11 11:53:54 +08:00
147 changed files with 7760 additions and 4512 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

5
.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

@@ -1,8 +1,8 @@
# 项目本地运行端口号 # 平台本地运行端口号
VITE_PORT = 8848 VITE_PORT = 8848
# 开发环境读取配置文件路径 # 开发环境读取配置文件路径
VITE_PUBLIC_PATH = / VITE_PUBLIC_PATH = ./
# 开发环境路由历史模式 # 开发环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash" VITE_ROUTER_HISTORY = "hash"

View File

@@ -1,12 +1,9 @@
# 线上环境项目打包路径 # 线上环境平台打包路径
VITE_PUBLIC_PATH = / VITE_PUBLIC_PATH = ./
# 线上环境路由历史模式 # 线上环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash" VITE_ROUTER_HISTORY = "hash"
# 是否为打包后的文件提供传统浏览器兼容性支持 支持 true 不支持 false
VITE_LEGACY = false
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false # 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN = false VITE_CDN = false

View File

@@ -1,19 +1,16 @@
# 预发布也需要生产环境的行为 # 预发布也需要生产环境的行为
# 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参数"
VITE_ROUTER_HISTORY = "hash" VITE_ROUTER_HISTORY = "hash"
# 是否为打包后的文件提供传统浏览器兼容性支持 支持 true 不支持 false
VITE_LEGACY = false
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false # 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN = true 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,6 +1,7 @@
public public
dist dist
*.d.ts *.d.ts
/src/assets
package.json package.json
.eslintrc.js .eslintrc.js
.prettierrc.js .prettierrc.js

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 Lite version is based on the shelf extracted from [vue-pure-admin](https://github.com/xiaoxian521/vue-pure-admin), including the main functions, which is more suitable for actual project development, and the packaged size is lower than ` 3MB`, and will permanently sync the full version of the code. After enabling `brotli` compression and `cdn` to replace the native library mode, the package size is less than `500kb` 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,61 +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) 提炼出的架子,包含主体功能,更适合实际项目开发,打包后的大小低于 `3MB`,并且会永久同步完整版的代码。开启 `brotli` 压缩和 `cdn` 替换本地库模式后,打包大小低于 `500kb` 精简版是基于 [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/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

@@ -1,12 +1,12 @@
/** 处理环境变量 */ /** 处理环境变量 */
const warpperEnv = (envConf: Recordable): ViteEnv => { const warpperEnv = (envConf: Recordable): ViteEnv => {
/** 此处为默认值,无需修改 */ /** 此处为默认值 */
const ret: ViteEnv = { const ret: ViteEnv = {
VITE_PORT: 8848, VITE_PORT: 8848,
VITE_PUBLIC_PATH: "", VITE_PUBLIC_PATH: "",
VITE_ROUTER_HISTORY: "", VITE_ROUTER_HISTORY: "",
VITE_LEGACY: false,
VITE_CDN: false, VITE_CDN: false,
VITE_HIDE_HOME: "false",
VITE_COMPRESSION: "none" VITE_COMPRESSION: "none"
}; };
@@ -28,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(

31
build/optimize.ts Normal file
View File

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

View File

@@ -2,7 +2,6 @@ import { cdn } from "./cdn";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import { viteBuildInfo } from "./info"; import { viteBuildInfo } from "./info";
import svgLoader from "vite-svg-loader"; import svgLoader from "vite-svg-loader";
import legacy from "@vitejs/plugin-legacy";
import vueJsx from "@vitejs/plugin-vue-jsx"; import vueJsx from "@vitejs/plugin-vue-jsx";
import { viteMockServe } from "vite-plugin-mock"; import { viteMockServe } from "vite-plugin-mock";
import { configCompressPlugin } from "./compress"; import { configCompressPlugin } from "./compress";
@@ -10,12 +9,10 @@ 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(
command: string, command: string,
VITE_LEGACY: boolean,
VITE_CDN: boolean, VITE_CDN: boolean,
VITE_COMPRESSION: ViteCompression VITE_COMPRESSION: ViteCompression
) { ) {
@@ -27,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(),
@@ -35,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组件化支持
@@ -59,13 +48,6 @@ export function getPluginsList(
`, `,
logger: false logger: false
}), }),
// 是否为打包后的文件提供传统浏览器兼容性支持
VITE_LEGACY
? legacy({
targets: ["ie >= 11"],
additionalLegacyPolyfills: ["regenerator-runtime/runtime"]
})
: null,
// 打包分析 // 打包分析
lifecycle === "report" lifecycle === "report"
? visualizer({ open: true, brotliSize: true, filename: "report.html" }) ? visualizer({ open: true, brotliSize: true, filename: "report.html" })

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.6.3", "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",
@@ -11,11 +11,12 @@
"preview": "vite preview", "preview": "vite preview",
"preview:build": "pnpm build && vite preview", "preview:build": "pnpm build && vite preview",
"typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck", "typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
"svgo": "svgo -f src/assets/svg -o src/assets/svg",
"cloc": "NODE_OPTIONS=--max-old-space-size=4096 cloc . --exclude-dir=node_modules --exclude-lang=YAML", "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 && npm cache clean --force && 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",
@@ -28,109 +29,107 @@
"not op_mini all" "not op_mini all"
], ],
"dependencies": { "dependencies": {
"@ctrl/tinycolor": "^3.4.1", "@pureadmin/descriptions": "^1.1.1",
"@pureadmin/components": "^1.1.0", "@pureadmin/table": "^2.3.2",
"@pureadmin/descriptions": "^1.1.0", "@pureadmin/utils": "^1.9.6",
"@pureadmin/table": "^1.2.0", "@vueuse/core": "^10.2.0",
"@pureadmin/utils": "^1.1.5", "@vueuse/motion": "^2.0.0",
"@vueuse/core": "^9.4.0",
"@vueuse/motion": "^2.0.0-beta.12",
"@vueuse/shared": "^9.4.0",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"axios": "^1.1.3", "axios": "^1.4.0",
"dayjs": "^1.11.4", "dayjs": "^1.11.8",
"echarts": "^5.3.3", "echarts": "^5.4.2",
"element-plus": "^2.2.16", "element-plus": "^2.3.7",
"element-resize-detector": "^1.2.3", "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.21", "pinia": "^2.1.4",
"qs": "^6.11.0", "pinyin-pro": "^3.15.2",
"resize-observer-polyfill": "^1.5.1", "qs": "^6.11.2",
"responsive-storage": "^2.1.0", "responsive-storage": "^2.2.0",
"vue": "^3.2.40", "sortablejs": "^1.15.0",
"vue-router": "^4.1.6", "vue": "^3.3.4",
"vue-types": "^4.2.1", "vue-router": "^4.2.2",
"vxe-table": "^4.3.2", "vue-types": "^5.1.0"
"xe-utils": "^3.5.6"
}, },
"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": "^3.2.1", "@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/mockjs": "^1.0.7",
"@types/lodash": "^4.14.180", "@types/node": "^20.3.1",
"@types/lodash-es": "^4.17.6",
"@types/mockjs": "1.0.3",
"@types/node": "14.14.14",
"@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.10.2", "@types/sortablejs": "^1.15.1",
"@typescript-eslint/parser": "^5.10.2", "@typescript-eslint/eslint-plugin": "^5.60.0",
"@vitejs/plugin-legacy": "^2.2.0", "@typescript-eslint/parser": "^5.60.0",
"@vitejs/plugin-vue": "^3.1.2", "@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^2.0.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": "^10.0.0", "@vue/eslint-config-typescript": "^11.0.3",
"@vue/runtime-core": "^3.2.40", "autoprefixer": "^10.4.14",
"autoprefixer": "^10.4.13", "cloc": "^2.11.0",
"cloc": "^2.10.0", "cssnano": "^6.0.1",
"cssnano": "^5.1.14", "eslint": "^8.43.0",
"eslint": "^8.8.0", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-vue": "^9.15.1",
"eslint-plugin-vue": "^8.4.1", "husky": "^8.0.3",
"font-awesome": "^4.7.0", "lint-staged": "^13.2.2",
"husky": "^7.0.4",
"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.53.0", "sass": "^1.63.6",
"sass-loader": "^13.0.2", "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": "^6.0.0", "stylelint-config-recommended": "^12.0.0",
"stylelint-config-standard": "^24.0.0", "stylelint-config-recommended-scss": "^12.0.0",
"stylelint-order": "^5.0.0", "stylelint-config-recommended-vue": "^1.4.0",
"tailwindcss": "^3.2.3", "stylelint-config-standard": "^33.0.0",
"terser": "^5.15.1", "stylelint-config-standard-scss": "^9.0.0",
"typescript": "^4.7.4", "stylelint-order": "^6.0.3",
"unplugin-vue-define-options": "0.7.3", "stylelint-prettier": "^3.0.0",
"vite": "^3.1.8", "stylelint-scss": "^5.0.1",
"svgo": "^3.0.2",
"tailwindcss": "^3.3.2",
"terser": "^5.18.1",
"typescript": "5.0.4",
"vite": "^4.3.9",
"vite-plugin-cdn-import": "^0.3.5", "vite-plugin-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.2.0", "vite-plugin-remove-console": "^2.1.1",
"vite-svg-loader": "^3.6.0", "vite-svg-loader": "^4.0.0",
"vue-eslint-parser": "^8.2.0", "vue-eslint-parser": "^9.3.1",
"vue-tsc": "^0.40.13" "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"
} }

5837
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.6.3", "Version": "4.5.0",
"Title": "PureAdmin", "Title": "PureAdmin",
"FixedHeader": true, "FixedHeader": true,
"HiddenSideBar": false, "HiddenSideBar": false,
@@ -14,5 +14,9 @@
"SidebarStatus": true, "SidebarStatus": true,
"EpThemeColor": "#409EFF", "EpThemeColor": "#409EFF",
"ShowLogo": true, "ShowLogo": true,
"ShowModel": "smart" "ShowModel": "smart",
"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.

View File

@@ -1 +1 @@
<svg t="1636193306629" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1847" width="32" height="32"><path d="M410.558481 0.10861C410.558481 211.083075 109.682285 361.860579 109.682285 633.656511c0 174.943176 134.703259 316.787527 300.876196 316.787527s300.876197-141.817198 300.876197-316.787527C711.407525 361.751969 410.558481 210.974465 410.558481 0.10861z" fill="#386BF3" p-id="1848"></path><path d="M613.468671 73.664572c0 211.055922-300.876197 361.914883-300.876196 633.547901 0 174.943176 134.703259 316.787527 300.876196 316.787527s300.876197-141.817198 300.876197-316.787527c-0.054305-271.633018-300.876197-422.491979-300.876197-633.547901z" fill="#C3D2FB" p-id="1849"></path><path d="M312.592475 707.212473c0-183.713414 137.635722-312.171612 226.72288-441.390078 81.701694 106.111739 172.119322 218.740063 172.119323 367.725506a309.755045 309.755045 0 0 1-291.074166 316.516003 323.114046 323.114046 0 0 1-107.768037-242.851431z" fill="#303F5B" p-id="1850"></path></svg> <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>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 712 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2.88 18.054a35.897 35.897 0 0 1 8.531-16.32.8.8 0 0 1 1.178 0c.166.18.304.332.413.455a35.897 35.897 0 0 1 8.118 15.865c-2.141.451-4.34.747-6.584.874l-2.089 4.178a.5.5 0 0 1-.894 0l-2.089-4.178a44.019 44.019 0 0 1-6.584-.874zm6.698-1.123l1.157.066L12 19.527l1.265-2.53 1.157-.066a42.137 42.137 0 0 0 4.227-.454A33.913 33.913 0 0 0 12 4.09a33.913 33.913 0 0 0-6.649 12.387c1.395.222 2.805.374 4.227.454zM12 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> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2.88 18.054a35.897 35.897 0 0 1 8.531-16.32.8.8 0 0 1 1.178 0c.166.18.304.332.413.455a35.897 35.897 0 0 1 8.118 15.865c-2.141.451-4.34.747-6.584.874l-2.089 4.178a.5.5 0 0 1-.894 0l-2.089-4.178a44.019 44.019 0 0 1-6.584-.874zm6.698-1.123 1.157.066L12 19.527l1.265-2.53 1.157-.066a42.137 42.137 0 0 0 4.227-.454A33.913 33.913 0 0 0 12 4.09a33.913 33.913 0 0 0-6.649 12.387c1.395.222 2.805.374 4.227.454zM12 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>

Before

Width:  |  Height:  |  Size: 608 B

After

Width:  |  Height:  |  Size: 588 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.38 2.019a7.5 7.5 0 1 0 10.6 10.6C21.662 17.854 17.316 22 12.001 22 6.477 22 2 17.523 2 12c0-5.315 4.146-9.661 9.38-9.981z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.38 2.019a7.5 7.5 0 1 0 10.6 10.6C21.662 17.854 17.316 22 12.001 22 6.477 22 2 17.523 2 12c0-5.315 4.146-9.661 9.38-9.981z"/></svg>

Before

Width:  |  Height:  |  Size: 263 B

After

Width:  |  Height:  |  Size: 263 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85 1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"/></svg>

Before

Width:  |  Height:  |  Size: 480 B

After

Width:  |  Height:  |  Size: 480 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--ant-design" width="20" height="20" preserveAspectRatio="xMidYMid meet" viewBox="0 0 1024 1024"><path fill="currentColor" d="M864 170h-60c-4.4 0-8 3.6-8 8v518H310v-73c0-6.7-7.8-10.5-13-6.3l-141.9 112a8 8 0 0 0 0 12.6l141.9 112c5.3 4.2 13 .4 13-6.3v-75h498c35.3 0 64-28.7 64-64V178c0-4.4-3.6-8-8-8z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="true" class="iconify iconify--ant-design" viewBox="0 0 1024 1024"><path fill="currentColor" d="M864 170h-60c-4.4 0-8 3.6-8 8v518H310v-73c0-6.7-7.8-10.5-13-6.3l-141.9 112a8 8 0 0 0 0 12.6l141.9 112c5.3 4.2 13 .4 13-6.3v-75h498c35.3 0 64-28.7 64-64V178c0-4.4-3.6-8-8-8z"/></svg>

Before

Width:  |  Height:  |  Size: 448 B

After

Width:  |  Height:  |  Size: 352 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" class="re-screen" color="#00000073" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16"><g fill="currentColor"><path d="M3.5 4H1V3h2V1h1v2.5l-.5.5zM13 3V1h-1v2.5l.5.5H15V3h-2zm-1 9.5V15h1v-2h2v-1h-2.5l-.5.5zM1 12v1h2v2h1v-2.5l-.5-.5H1zm11-1.5l-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5v5zM10 7H6v2h4V7z"></path></g></svg> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 4H1V3h2V1h1v2.5l-.5.5zM13 3V1h-1v2.5l.5.5H15V3h-2zm-1 9.5V15h1v-2h2v-1h-2.5l-.5.5zM1 12v1h2v2h1v-2.5l-.5-.5H1zm11-1.5-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5v5zM10 7H6v2h4V7z"/></svg>

Before

Width:  |  Height:  |  Size: 452 B

After

Width:  |  Height:  |  Size: 348 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" class="re-screen" color="#00000073" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16"><g fill="currentColor"><path d="M3 12h10V4H3v8zm2-6h6v4H5V6zM2 6H1V2.5l.5-.5H5v1H2v3zm13-3.5V6h-1V3h-3V2h3.5l.5.5zM14 10h1v3.5l-.5.5H11v-1h3v-3zM2 13h3v1H1.5l-.5-.5V10h1v3z"></path></g></svg> <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" aria-hidden="true" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3 12h10V4H3v8zm2-6h6v4H5V6zM2 6H1V2.5l.5-.5H5v1H2v3zm13-3.5V6h-1V3h-3V2h3.5l.5.5zM14 10h1v3.5l-.5.5H11v-1h3v-3zM2 13h3v1H1.5l-.5-.5V10h1v3z"/></svg>

Before

Width:  |  Height:  |  Size: 421 B

After

Width:  |  Height:  |  Size: 318 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--mdi" width="20" height="20" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1V7m10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2Z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" aria-hidden="true" class="iconify iconify--mdi" viewBox="0 0 24 24"><path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1V7m10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2Z"/></svg>

Before

Width:  |  Height:  |  Size: 477 B

After

Width:  |  Height:  |  Size: 381 B

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,80 +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";
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);
// 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);
@@ -31,7 +30,6 @@ const getConfig = (key?: string): ServerConfigs => {
export const getServerConfig = async (app: App): Promise<undefined> => { export const getServerConfig = async (app: App): Promise<undefined> => {
app.config.globalProperties.$config = getConfig(); app.config.globalProperties.$config = getConfig();
return axios({ return axios({
baseURL: "",
method: "get", method: "get",
url: `${VITE_PUBLIC_PATH}serverConfig.json` url: `${VITE_PUBLIC_PATH}serverConfig.json`
}) })
@@ -44,8 +42,6 @@ export const getServerConfig = async (app: App): Promise<undefined> => {
// 设置全局配置 // 设置全局配置
setConfig($config); setConfig($config);
} }
// 设置全局baseURL
app.config.globalProperties.$baseUrl = $config.baseURL;
return $config; return $config;
}) })
.catch(() => { .catch(() => {
@@ -53,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,68 +2,65 @@
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 { templateRef } from "@vueuse/core"; import Bell from "@iconify-icons/ep/bell";
import { Tabs, TabPane } from "@pureadmin/components";
const dropdownDom = templateRef<ElRef | null>("dropdownDom", null); const noticesNum = ref(0);
const activeName = ref(noticesData[0].name);
const notices = ref(noticesData); const notices = ref(noticesData);
const activeKey = ref(noticesData[0].key);
let noticesNum = ref(0); notices.value.map(v => (noticesNum.value += v.list.length));
notices.value.forEach(notice => {
noticesNum.value += notice.list.length;
});
function tabClick() {
(dropdownDom as any).value.handleOpen();
}
</script> </script>
<template> <template>
<el-dropdown ref="dropdownDom" trigger="click" placement="bottom-end"> <el-dropdown trigger="click" placement="bottom-end">
<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>
<Tabs <el-tabs
centered :stretch="true"
v-model="activeKey"
class="dropdown-tabs" class="dropdown-tabs"
v-model:activeName="activeName" :style="{ width: notices.length === 0 ? '200px' : '330px' }"
@tabClick="tabClick"
> >
<template v-for="item in notices" :key="item.key"> <el-empty
<TabPane :tab="`${item.name}(${item.list.length})`"> v-if="notices.length === 0"
<el-scrollbar max-height="330px"> description="暂无消息"
<div class="noticeList-container"> :image-size="60"
<NoticeList :list="item.list" /> />
</div> <span v-else>
</el-scrollbar> <template v-for="item in notices" :key="item.key">
</TabPane> <el-tab-pane
</template> :label="`${item.name}(${item.list.length})`"
</Tabs> :name="`${item.key}`"
>
<el-scrollbar max-height="330px">
<div class="noticeList-container">
<NoticeList :list="item.list" />
</div>
</el-scrollbar>
</el-tab-pane>
</template>
</span>
</el-tabs>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
</template> </template>
<style>
.ant-tabs-dropdown {
z-index: 2900 !important;
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
.dropdown-badge { .dropdown-badge {
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 {
@@ -72,30 +69,20 @@ function tabClick() {
} }
.dropdown-tabs { .dropdown-tabs {
width: 336px; .noticeList-container {
background-color: #fff; padding: 15px 24px 0;
box-shadow: 0 2px 8px rgb(0 0 0 / 15%); }
border-radius: 4px;
:deep(.el-tabs__header) { :deep(.el-tabs__header) {
margin: 0; margin: 0;
} }
:deep(.el-tabs__nav-scroll) {
display: flex;
justify-content: center;
}
:deep(.el-tabs__nav-wrap)::after { :deep(.el-tabs__nav-wrap)::after {
height: 1px; height: 1px;
} }
:deep(.noticeList-container) { :deep(.el-tabs__nav-wrap) {
padding: 15px 24px 0 24px; padding: 0 36px;
} }
} }
:deep(.ant-tabs-nav) {
margin-bottom: 0;
}
</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(() => {
@@ -24,11 +28,11 @@ function hoverTitle() {
function hoverDescription(event, description) { function hoverDescription(event, description) {
// currentWidth 为文本在页面中所占的宽度创建标签加入到页面获取currentWidth ,最后在移除 // currentWidth 为文本在页面中所占的宽度创建标签加入到页面获取currentWidth ,最后在移除
let tempTag = document.createElement("span"); const tempTag = document.createElement("span");
tempTag.innerText = description; tempTag.innerText = description;
tempTag.className = "getDescriptionWidth"; tempTag.className = "getDescriptionWidth";
document.querySelector("body").appendChild(tempTag); document.querySelector("body").appendChild(tempTag);
let currentWidth = ( const currentWidth = (
document.querySelector(".getDescriptionWidth") as HTMLSpanElement document.querySelector(".getDescriptionWidth") as HTMLSpanElement
).offsetWidth; ).offsetWidth;
document.querySelector(".getDescriptionWidth").remove(); document.querySelector(".getDescriptionWidth").remove();
@@ -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";
let 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>
@@ -21,11 +45,13 @@ emitter.on("openPanel", () => {
<div ref="target" class="right-panel bg-bg_color"> <div ref="target" class="right-panel bg-bg_color">
<div class="right-panel-items"> <div class="right-panel-items">
<div class="project-configuration"> <div class="project-configuration">
<h3 class="dark:text-white">项目配置</h3> <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,21 +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 lang="ts" setup>
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
.search-footer { .search-footer {
display: flex; display: flex;
@@ -37,5 +53,10 @@ import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.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 lang="ts" setup> <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 { useNav } from "@/layout/hooks/useNav"; import { useNav } from "@/layout/hooks/useNav";
import { deleteChildren } from "@pureadmin/utils"; 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 lang="ts" setup> <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 lang="ts" setup> <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,20 +6,8 @@ import {
reactive, reactive,
computed, computed,
nextTick, nextTick,
useCssModule onBeforeMount
} from "vue"; } from "vue";
import { getConfig } from "@/config";
import { useRouter } from "vue-router";
import panel from "../panel/index.vue";
import { emitter } from "@/utils/mitt";
import { resetRouter } from "@/router";
import { templateRef } from "@vueuse/core";
import { removeToken } from "@/utils/auth";
import { routerArrays } from "@/layout/types";
import { useNav } from "@/layout/hooks/useNav";
import { useAppStoreHook } from "@/store/modules/app";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import { import {
useDark, useDark,
debounce, debounce,
@@ -27,23 +15,34 @@ import {
storageLocal, storageLocal,
storageSession storageSession
} from "@pureadmin/utils"; } from "@pureadmin/utils";
import { getConfig } from "@/config";
import { useRouter } from "vue-router";
import panel from "../panel/index.vue";
import { emitter } from "@/utils/mitt";
import { resetRouter } from "@/router";
import { removeToken } from "@/utils/auth";
import { routerArrays } from "@/layout/types";
import { useNav } from "@/layout/hooks/useNav";
import { useAppStoreHook } from "@/store/modules/app";
import { toggleTheme } from "@pureadmin/theme/dist/browser-utils"; import { toggleTheme } from "@pureadmin/theme/dist/browser-utils";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
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 = templateRef<HTMLElement | null>("mixRef", null); const mixRef = ref();
const verticalRef = templateRef<HTMLElement | null>("verticalRef", null); const verticalRef = ref();
const horizontalRef = templateRef<HTMLElement | null>("horizontalRef", null); const horizontalRef = ref();
const { const {
body,
dataTheme, dataTheme,
layoutTheme, layoutTheme,
themeColors, themeColors,
@@ -54,8 +53,8 @@ const {
/* body添加layout属性作用于src/style/sidebar.scss */ /* body添加layout属性作用于src/style/sidebar.scss */
if (unref(layoutTheme)) { if (unref(layoutTheme)) {
let layout = unref(layoutTheme).layout; const layout = unref(layoutTheme).layout;
let theme = unref(layoutTheme).theme; const theme = unref(layoutTheme).theme;
toggleTheme({ toggleTheme({
scopeName: `layout-theme-${theme}` scopeName: `layout-theme-${theme}`
}); });
@@ -119,13 +118,13 @@ const weekChange = (value): void => {
}; };
const tagsChange = () => { const tagsChange = () => {
let showVal = settings.tabsVal; const showVal = settings.tabsVal;
storageConfigureChange("hideTabs", showVal); storageConfigureChange("hideTabs", showVal);
emitter.emit("tagViewsChange", showVal as unknown as string); emitter.emit("tagViewsChange", showVal as unknown as string);
}; };
const multiTagsCacheChange = () => { const multiTagsCacheChange = () => {
let multiTagsCache = settings.multiTagsCache; const multiTagsCache = settings.multiTagsCache;
storageConfigureChange("multiTagsCache", multiTagsCache); storageConfigureChange("multiTagsCache", multiTagsCache);
useMultiTagsStoreHook().multiTagsCacheChange(multiTagsCache); useMultiTagsStoreHook().multiTagsCacheChange(multiTagsCache);
}; };
@@ -133,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);
@@ -161,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 => {
@@ -220,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>
@@ -245,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')"
> >
@@ -258,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')"
> >
@@ -274,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')"
> >
@@ -303,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>
@@ -384,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"
@@ -394,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%);
@@ -452,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;
} }
} }
} }
@@ -484,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;
} }
} }
} }
@@ -498,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;
@@ -512,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

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { isEqual } from "lodash-unified"; import { isEqual } from "@pureadmin/utils";
import { ref, watch, onMounted, toRaw } from "vue"; import { ref, watch, onMounted, toRaw } from "vue";
import { getParentPaths, findRouteByPath } from "@/router/utils"; import { getParentPaths, findRouteByPath } from "@/router/utils";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
@@ -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,24 +1,28 @@
<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();
let defaultActive = ref(null); 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,15 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import path from "path"; import path from "path";
import { childrenType } from "../../types"; import { getConfig } from "@/config";
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,
@@ -21,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 => {
@@ -41,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",
@@ -59,23 +50,68 @@ const getMenuTextStyle = computed(() => {
}; };
}); });
const getsubMenuIconStyle = computed((): CSSProperties => {
return {
display: "flex",
justifyContent: "center",
alignItems: "center",
margin:
layout.value === "horizontal"
? "0 5px 0 0"
: isCollapse.value
? "0 auto"
: "0 5px 0 0"
};
});
const getSubTextStyle = computed((): CSSProperties => { const getSubTextStyle = computed((): CSSProperties => {
return { if (!isCollapse.value) {
width: !isCollapse.value ? "210px" : "", return {
display: "inline-block", width: "210px",
overflow: "hidden", display: "inline-block",
textOverflow: "ellipsis" 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 getSpanStyle = computed(() => { const expandCloseIcon = computed(() => {
if (!getConfig()?.MenuArrowIconNoTransition) return "";
return { return {
overflow: "hidden", "expand-close-icon": useRenderIcon(EpArrowDown),
textOverflow: "ellipsis" "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元素
@@ -98,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;
@@ -127,93 +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 v-else ref="subMenu" :index="resolvePath(props.item.path)"> <el-sub-menu
v-else
ref="subMenu"
v-bind="expandCloseIcon"
:index="resolvePath(props.item.path)"
>
<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))"
/> />
@@ -221,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

@@ -4,22 +4,23 @@ import { useRoute } from "vue-router";
import { emitter } from "@/utils/mitt"; 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 type { StorageConfigs } from "/#/index";
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();
let subMenuData = ref([]); const subMenuData = ref([]);
const menuData = computed(() => { const menuData = computed(() => {
return pureApp.layout === "mix" && device.value !== "mobile" return pureApp.layout === "mix" && device.value !== "mobile"
@@ -27,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,
@@ -42,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"
@@ -72,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"
@@ -92,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,
@@ -40,23 +47,27 @@ const {
const tabDom = ref(); const tabDom = ref();
const containerDom = ref(); const containerDom = ref();
const scrollbarDom = ref(); const scrollbarDom = ref();
let 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) => {
let 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,14 +162,13 @@ 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) {
// 存放被删除的缓存路由 const valueIndex: number = multiTags.value.findIndex((item: any) => {
let delAliveRouteList = [];
let valueIndex: number = multiTags.value.findIndex((item: any) => {
if (item.query) { if (item.query) {
if (item.path === obj.path) { if (item.path === obj.path) {
return item.query === obj.query; return item.query === obj.query;
@@ -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") {
@@ -197,12 +211,8 @@ function deleteDynamicTag(obj: any, current: any, tag?: string) {
// 从当前匹配到的路径中删除 // 从当前匹配到的路径中删除
spliceRoute(valueIndex, 1); spliceRoute(valueIndex, 1);
} }
let 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,16 +341,16 @@ function disabledMenus(value: boolean) {
}); });
} }
/** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是首页,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */ /** 检查当前右键的菜单两边是否存在别的菜单,如果左侧的菜单是顶级菜单,则不显示关闭左侧标签页,如果右侧没有菜单,则不显示关闭右侧标签页 */
function showMenuModel( function showMenuModel(
currentPath: string, currentPath: string,
query: object = {}, query: object = {},
refresh = false refresh = false
) { ) {
let allRoute = multiTags.value; const allRoute = multiTags.value;
let 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,12 +1,16 @@
import { computed } from "vue"; 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";
const errorInfo = "当前路由配置不正确,请检查配置"; const errorInfo = "当前路由配置不正确,请检查配置";
@@ -14,6 +18,19 @@ export function useNav() {
const route = useRoute(); const route = useRoute();
const pureApp = useAppStoreHook(); const pureApp = useAppStoreHook();
const routers = useRouter().options.routes; const routers = useRouter().options.routes;
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(() => {
@@ -53,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() {
@@ -80,37 +97,13 @@ export function useNav() {
} }
} }
function menuSelect(indexPath: string, routers): void { function menuSelect(indexPath: string) {
if (isRemaining(indexPath)) return; if (wholeMenus.value.length === 0 || isRemaining(indexPath)) return;
let parentPath = ""; emitter.emit("changLayoutRoute", indexPath);
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);
} }
@@ -122,8 +115,9 @@ export function useNav() {
logout, logout,
routers, routers,
$storage, $storage,
backHome, backTopMenu,
onPanel, onPanel,
getDivStyle,
changeTitle, changeTitle,
toggleSideBar, toggleSideBar,
menuSelect, menuSelect,
@@ -132,6 +126,8 @@ export function useNav() {
isCollapse, isCollapse,
pureApp, pureApp,
username, username,
avatarsStyle userAvatar,
avatarsStyle,
tooltipEffect
}; };
} }

View File

@@ -9,13 +9,26 @@ import {
getCurrentInstance getCurrentInstance
} from "vue"; } from "vue";
import { tagsViewsType } from "../types"; import { tagsViewsType } from "../types";
import { isEqual } from "lodash-unified";
import type { StorageConfigs } from "/#/index";
import { useEventListener } from "@vueuse/core"; import { useEventListener } from "@vueuse/core";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
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();
@@ -33,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;
@@ -47,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,
@@ -105,15 +121,14 @@ export function useTags() {
]); ]);
function conditionHandle(item, previous, next) { function conditionHandle(item, previous, next) {
if ( if (isBoolean(route?.meta?.showLink) && route?.meta?.showLink === false) {
Object.keys(route.query).length === 0 && if (Object.keys(route.query).length > 0) {
Object.keys(route.params).length === 0 return isEqual(route.query, item.query) ? previous : next;
) { } else {
return route.path === item.path ? previous : next; return isEqual(route.params, item.params) ? previous : next;
} else if (Object.keys(route.query).length > 0) { }
return isEqual(route.query, item.query) ? previous : next;
} else { } else {
return isEqual(route.params, item.params) ? previous : next; return route.path === item.path ? previous : next;
} }
} }
@@ -188,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;
let { 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,21 +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 "@pureadmin/components/dist/index.css";
import "@pureadmin/components/dist/theme.css";
import "@pureadmin/components/dist/dark.scss";
// 导入字体图标 // 导入字体图标
import "./assets/iconfont/iconfont.js"; import "./assets/iconfont/iconfont.js";
import "./assets/iconfont/iconfont.css"; import "./assets/iconfont/iconfont.css";
@@ -56,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);
}

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