Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f27d6cd9e | ||
|
|
4b435d0e0f | ||
|
|
872e0bbd5b | ||
|
|
b6859d7920 | ||
|
|
6e02ae14a0 | ||
|
|
c28066fb1f | ||
|
|
d9ab1b1198 |
21
.dockerignore
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
.eslintcache
|
||||||
|
report.html
|
||||||
|
|
||||||
|
yarn.lock
|
||||||
|
npm-debug.log*
|
||||||
|
.pnpm-error.log*
|
||||||
|
.pnpm-debug.log
|
||||||
|
tests/**/coverage/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
VITE_PORT = 8848
|
VITE_PORT = 8848
|
||||||
|
|
||||||
# 开发环境读取配置文件路径
|
# 开发环境读取配置文件路径
|
||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = ./
|
||||||
|
|
||||||
# 开发环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
# 开发环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||||
VITE_ROUTER_HISTORY = "hash"
|
VITE_ROUTER_HISTORY = "hash"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# 线上环境平台打包路径
|
# 线上环境平台打包路径
|
||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = ./
|
||||||
|
|
||||||
# 线上环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
# 线上环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||||
VITE_ROUTER_HISTORY = "hash"
|
VITE_ROUTER_HISTORY = "hash"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# 预发布也需要生产环境的行为
|
# 预发布也需要生产环境的行为
|
||||||
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
|
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
|
||||||
NODE_ENV=production
|
# NODE_ENV = development
|
||||||
|
|
||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = ./
|
||||||
|
|
||||||
# 预发布环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
# 预发布环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||||
VITE_ROUTER_HISTORY = "hash"
|
VITE_ROUTER_HISTORY = "hash"
|
||||||
|
|||||||
1
.vscode/extensions.json
vendored
@@ -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",
|
||||||
|
|||||||
12
.vscode/vue3.0.code-snippets
vendored
@@ -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"
|
||||||
],
|
],
|
||||||
|
|||||||
6
.vscode/vue3.2.code-snippets
vendored
@@ -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
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"Vue3.3+defineOptions快速生成模板": {
|
||||||
|
"scope": "vue",
|
||||||
|
"prefix": "Vue3.3+",
|
||||||
|
"body": [
|
||||||
|
"<script setup lang='ts'>",
|
||||||
|
"defineOptions({",
|
||||||
|
"\tname: ''",
|
||||||
|
"})",
|
||||||
|
"</script>\n",
|
||||||
|
"<template>",
|
||||||
|
"\t<div>test</div>",
|
||||||
|
"</template>\n",
|
||||||
|
"<style lang='scss' scoped>\n",
|
||||||
|
"</style>",
|
||||||
|
"$2"
|
||||||
|
],
|
||||||
|
"description": "Vue3.3+defineOptions快速生成模板"
|
||||||
|
}
|
||||||
|
}
|
||||||
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM node:16-alpine as build-stage
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
RUN corepack enable
|
||||||
|
RUN corepack prepare pnpm@7.32.1 --activate
|
||||||
|
|
||||||
|
RUN npm config set registry https://registry.npmmirror.com
|
||||||
|
|
||||||
|
COPY .npmrc package.json pnpm-lock.yaml ./
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM nginx:stable-alpine as production-stage
|
||||||
|
|
||||||
|
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -15,39 +15,22 @@ The simplified version is based on the shelf extracted from [vue-pure-admin](htt
|
|||||||
|
|
||||||
## 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://pure-admin.github.io/pure-admin-doc)
|
|
||||||
|
|
||||||
## Preview
|
## Preview
|
||||||
|
|
||||||
- [Click me to view the preview station](https://pure-admin-thin.netlify.app/#/login)
|
- [Click me to view the preview station](https://pure-admin-thin.netlify.app/#/login)
|
||||||
|
|
||||||
## Usage
|
## Maintainer
|
||||||
|
|
||||||
### Installation dependencies
|
[xiaoxian521](https://github.com/xiaoxian521)
|
||||||
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
### Install a package
|
|
||||||
|
|
||||||
pnpm add packageName
|
|
||||||
|
|
||||||
### Uninstall a package
|
|
||||||
|
|
||||||
pnpm remove packageName
|
|
||||||
|
|
||||||
I think you should fork the project first to develop, so that you can pull the update synchronously when I update! ! !
|
|
||||||
|
|
||||||
## Supporting video tutorial
|
|
||||||
|
|
||||||
bilibili: https://www.bilibili.com/video/BV1534y1S7HV/
|
|
||||||
|
|
||||||
## ⚠️ 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/pure-admin/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)
|
[MIT © 2020-present, pure-admin](./LICENSE)
|
||||||
|
|||||||
39
README.md
@@ -10,58 +10,31 @@
|
|||||||
|
|
||||||
## 版本选择
|
## 版本选择
|
||||||
|
|
||||||
当前是非国际化版本哦,如果您需要国际化版本 [请点击](https://github.com/pure-admin/pure-admin-thin/tree/i18n)
|
当前是非国际化版本,如果您需要国际化版本 [请点击](https://github.com/pure-admin/pure-admin-thin/tree/i18n)
|
||||||
|
|
||||||
## 配套视频
|
## 配套视频
|
||||||
|
|
||||||
- [点我查看教程](https://www.bilibili.com/video/BV1kg411v7QT)
|
- [点我查看教程](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://pure-admin.github.io/pure-admin-doc)
|
|
||||||
|
|
||||||
## 预览
|
## 预览
|
||||||
|
|
||||||
- [点我查看预览站](https://pure-admin-thin.netlify.app/#/login)
|
- [查看预览](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` 交流群
|
|
||||||
|
|
||||||
[点击去加入](https://yiming_chang.gitee.io/pure-admin-doc/pages/support/#qq-%E4%BA%A4%E6%B5%81%E7%BE%A4)
|
|
||||||
|
|
||||||
## 用法
|
|
||||||
|
|
||||||
### 安装依赖
|
|
||||||
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
### 安装一个包
|
|
||||||
|
|
||||||
pnpm add 包名
|
|
||||||
|
|
||||||
### 卸载一个包
|
|
||||||
|
|
||||||
pnpm remove 包名
|
|
||||||
|
|
||||||
我认为你应该先 `fork` 项目去开发,以便我更新时您可以同步拉取更新!!!
|
|
||||||
|
|
||||||
## ⚠️ 注意
|
## ⚠️ 注意
|
||||||
|
|
||||||
- 精简版不接受任何 `issues` 和 `pr`,如果有问题请到完整版 [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) 去提,谢谢!!!
|
- 精简版不接受任何 `issues` 和 `pr`,如果有问题请到完整版 [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) 去提,谢谢!
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
原则上不收取任何费用及版权,可以放心使用,不过如需二次开源(比如用此平台二次开发并开源)请联系作者获取许可!
|
原则上不收取任何费用及版权,可商用,不过如需二次开源(比如用此平台二次开发并开源,要求前端代码必须开源免费)请联系作者获取许可!(免费,走个记录而已)
|
||||||
|
|
||||||
[MIT © 2020-present, pure-admin](./LICENSE)
|
[MIT © 2020-present, pure-admin](./LICENSE)
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ const include = [
|
|||||||
"pinia",
|
"pinia",
|
||||||
"js-cookie",
|
"js-cookie",
|
||||||
"sortablejs",
|
"sortablejs",
|
||||||
|
"pinyin-pro",
|
||||||
"@vueuse/core",
|
"@vueuse/core",
|
||||||
"@pureadmin/utils",
|
"@pureadmin/utils",
|
||||||
"responsive-storage",
|
"responsive-storage"
|
||||||
"element-resize-detector"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
88
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pure-admin-thin",
|
"name": "pure-admin-thin",
|
||||||
"version": "4.1.0",
|
"version": "4.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
|
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
|
||||||
@@ -13,10 +13,10 @@
|
|||||||
"typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
|
"typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
|
||||||
"svgo": "svgo -f src/assets/svg -o src/assets/svg",
|
"svgo": "svgo -f src/assets/svg -o src/assets/svg",
|
||||||
"cloc": "NODE_OPTIONS=--max-old-space-size=4096 cloc . --exclude-dir=node_modules --exclude-lang=YAML",
|
"cloc": "NODE_OPTIONS=--max-old-space-size=4096 cloc . --exclude-dir=node_modules --exclude-lang=YAML",
|
||||||
"clean:cache": "rm -rf node_modules && rm -rf .eslintcache && pnpm install",
|
"clean:cache": "rimraf node_modules && rimraf .eslintcache && pnpm install",
|
||||||
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
|
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
|
||||||
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
|
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
|
||||||
"lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
|
"lint: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",
|
||||||
@@ -30,91 +30,90 @@
|
|||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@pureadmin/descriptions": "^1.1.1",
|
"@pureadmin/descriptions": "^1.1.1",
|
||||||
"@pureadmin/table": "^2.1.0",
|
"@pureadmin/table": "^2.3.2",
|
||||||
"@pureadmin/utils": "^1.8.9",
|
"@pureadmin/utils": "^1.9.6",
|
||||||
"@vueuse/core": "^10.1.2",
|
"@vueuse/core": "^10.2.0",
|
||||||
"@vueuse/motion": "2.0.0-beta.12",
|
"@vueuse/motion": "^2.0.0",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.8",
|
||||||
"echarts": "^5.4.2",
|
"echarts": "^5.4.2",
|
||||||
"element-plus": "^2.3.4",
|
"element-plus": "^2.3.7",
|
||||||
"element-resize-detector": "^1.2.4",
|
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"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.36",
|
"pinia": "^2.1.4",
|
||||||
"qs": "^6.11.1",
|
"pinyin-pro": "^3.15.2",
|
||||||
|
"qs": "^6.11.2",
|
||||||
"responsive-storage": "^2.2.0",
|
"responsive-storage": "^2.2.0",
|
||||||
"sortablejs": "^1.15.0",
|
"sortablejs": "^1.15.0",
|
||||||
"vue": "^3.3.1",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "^4.1.6",
|
"vue-router": "^4.2.2",
|
||||||
"vue-types": "^5.0.2"
|
"vue-types": "^5.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.6.3",
|
"@commitlint/cli": "^17.6.6",
|
||||||
"@commitlint/config-conventional": "^17.6.3",
|
"@commitlint/config-conventional": "^17.6.6",
|
||||||
"@iconify-icons/ep": "^1.2.11",
|
"@iconify-icons/ep": "^1.2.12",
|
||||||
"@iconify-icons/ri": "^1.2.7",
|
"@iconify-icons/ri": "^1.2.9",
|
||||||
"@iconify/vue": "^4.1.1",
|
"@iconify/vue": "^4.1.1",
|
||||||
"@pureadmin/theme": "^3.0.0",
|
"@pureadmin/theme": "^3.1.0",
|
||||||
"@types/element-resize-detector": "1.1.3",
|
|
||||||
"@types/js-cookie": "^3.0.3",
|
"@types/js-cookie": "^3.0.3",
|
||||||
"@types/mockjs": "^1.0.7",
|
"@types/mockjs": "^1.0.7",
|
||||||
"@types/node": "^18.15.12",
|
"@types/node": "^20.3.1",
|
||||||
"@types/nprogress": "0.2.0",
|
"@types/nprogress": "0.2.0",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@types/sortablejs": "^1.15.1",
|
"@types/sortablejs": "^1.15.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.5",
|
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
||||||
"@typescript-eslint/parser": "^5.59.5",
|
"@typescript-eslint/parser": "^5.60.0",
|
||||||
"@vitejs/plugin-vue": "^4.2.2",
|
"@vitejs/plugin-vue": "^4.2.3",
|
||||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||||
"@vue/eslint-config-prettier": "^7.1.0",
|
"@vue/eslint-config-prettier": "^7.1.0",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"cloc": "^2.11.0",
|
"cloc": "^2.11.0",
|
||||||
"cssnano": "^6.0.1",
|
"cssnano": "^6.0.1",
|
||||||
"eslint": "^8.40.0",
|
"eslint": "^8.43.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-vue": "^9.12.0",
|
"eslint-plugin-vue": "^9.15.1",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^13.2.2",
|
"lint-staged": "^13.2.2",
|
||||||
"picocolors": "^1.0.0",
|
"picocolors": "^1.0.0",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.24",
|
||||||
"postcss-html": "^1.5.0",
|
"postcss-html": "^1.5.0",
|
||||||
"postcss-import": "^15.1.0",
|
"postcss-import": "^15.1.0",
|
||||||
"postcss-scss": "^4.0.6",
|
"postcss-scss": "^4.0.6",
|
||||||
"prettier": "^2.8.7",
|
"prettier": "^2.8.8",
|
||||||
"pretty-quick": "3.1.1",
|
"pretty-quick": "^3.1.3",
|
||||||
"rimraf": "^5.0.0",
|
"rimraf": "^5.0.1",
|
||||||
"rollup-plugin-visualizer": "^5.9.0",
|
"rollup-plugin-visualizer": "^5.9.2",
|
||||||
"sass": "^1.62.1",
|
"sass": "^1.63.6",
|
||||||
"sass-loader": "^13.2.2",
|
"sass-loader": "^13.3.2",
|
||||||
"stylelint": "^15.6.1",
|
"stylelint": "^15.9.0",
|
||||||
"stylelint-config-html": "^1.1.0",
|
"stylelint-config-html": "^1.1.0",
|
||||||
"stylelint-config-recess-order": "^4.0.0",
|
"stylelint-config-recess-order": "^4.2.0",
|
||||||
"stylelint-config-recommended": "^12.0.0",
|
"stylelint-config-recommended": "^12.0.0",
|
||||||
"stylelint-config-recommended-scss": "^11.0.0",
|
"stylelint-config-recommended-scss": "^12.0.0",
|
||||||
"stylelint-config-recommended-vue": "^1.4.0",
|
"stylelint-config-recommended-vue": "^1.4.0",
|
||||||
"stylelint-config-standard": "^33.0.0",
|
"stylelint-config-standard": "^33.0.0",
|
||||||
"stylelint-config-standard-scss": "^9.0.0",
|
"stylelint-config-standard-scss": "^9.0.0",
|
||||||
"stylelint-order": "^6.0.3",
|
"stylelint-order": "^6.0.3",
|
||||||
"stylelint-prettier": "^3.0.0",
|
"stylelint-prettier": "^3.0.0",
|
||||||
"stylelint-scss": "^5.0.0",
|
"stylelint-scss": "^5.0.1",
|
||||||
"svgo": "^3.0.2",
|
"svgo": "^3.0.2",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
"terser": "^5.17.1",
|
"terser": "^5.18.1",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "5.0.4",
|
||||||
"vite": "^4.3.5",
|
"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": "^2.1.1",
|
"vite-plugin-remove-console": "^2.1.1",
|
||||||
"vite-svg-loader": "^4.0.0",
|
"vite-svg-loader": "^4.0.0",
|
||||||
"vue-eslint-parser": "^9.2.1",
|
"vue-eslint-parser": "^9.3.1",
|
||||||
"vue-tsc": "^1.6.4"
|
"vue-tsc": "^1.8.1"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"peerDependencyRules": {
|
"peerDependencyRules": {
|
||||||
@@ -126,6 +125,7 @@
|
|||||||
},
|
},
|
||||||
"allowedDeprecatedVersions": {
|
"allowedDeprecatedVersions": {
|
||||||
"sourcemap-codec": "*",
|
"sourcemap-codec": "*",
|
||||||
|
"w3c-hr-time": "*",
|
||||||
"stable": "*"
|
"stable": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
3523
pnpm-lock.yaml
generated
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"Version": "4.1.0",
|
"Version": "4.5.0",
|
||||||
"Title": "PureAdmin",
|
"Title": "PureAdmin",
|
||||||
"FixedHeader": true,
|
"FixedHeader": true,
|
||||||
"HiddenSideBar": false,
|
"HiddenSideBar": false,
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
BIN
src/assets/user.jpg
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
@@ -12,6 +12,7 @@ import type {
|
|||||||
|
|
||||||
const dialogStore = ref<Array<DialogOptions>>([]);
|
const dialogStore = ref<Array<DialogOptions>>([]);
|
||||||
|
|
||||||
|
/** 打开弹框 */
|
||||||
const addDialog = (options: DialogOptions) => {
|
const addDialog = (options: DialogOptions) => {
|
||||||
const open = () =>
|
const open = () =>
|
||||||
dialogStore.value.push(Object.assign(options, { visible: true }));
|
dialogStore.value.push(Object.assign(options, { visible: true }));
|
||||||
@@ -24,16 +25,40 @@ const addDialog = (options: DialogOptions) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 关闭弹框 */
|
||||||
const closeDialog = (options: DialogOptions, index: number, args?: any) => {
|
const closeDialog = (options: DialogOptions, index: number, args?: any) => {
|
||||||
dialogStore.value.splice(index, 1);
|
dialogStore.value.splice(index, 1);
|
||||||
options.closeCallBack && options.closeCallBack({ options, index, args });
|
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 = () => {
|
const closeAllDialog = () => {
|
||||||
dialogStore.value = [];
|
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);
|
const ReDialog = withInstall(reDialog);
|
||||||
|
|
||||||
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };
|
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };
|
||||||
export { ReDialog, dialogStore, addDialog, closeDialog, closeAllDialog };
|
export {
|
||||||
|
ReDialog,
|
||||||
|
dialogStore,
|
||||||
|
addDialog,
|
||||||
|
closeDialog,
|
||||||
|
updateDialog,
|
||||||
|
closeAllDialog
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
|
||||||
import { isFunction } from "@pureadmin/utils";
|
|
||||||
import {
|
import {
|
||||||
type DialogOptions,
|
closeDialog,
|
||||||
type ButtonProps,
|
|
||||||
type EventType,
|
|
||||||
dialogStore,
|
dialogStore,
|
||||||
closeDialog
|
type EventType,
|
||||||
|
type ButtonProps,
|
||||||
|
type DialogOptions
|
||||||
} from "./index";
|
} 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(() => {
|
const footerButtons = computed(() => {
|
||||||
return (options: DialogOptions) => {
|
return (options: DialogOptions) => {
|
||||||
@@ -47,11 +51,22 @@ const footerButtons = computed(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fullscreenClass = computed(() => {
|
||||||
|
return [
|
||||||
|
"el-icon",
|
||||||
|
"el-dialog__close",
|
||||||
|
"-translate-x-2",
|
||||||
|
"cursor-pointer",
|
||||||
|
"hover:!text-[red]"
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
function eventsCallBack(
|
function eventsCallBack(
|
||||||
event: EventType,
|
event: EventType,
|
||||||
options: DialogOptions,
|
options: DialogOptions,
|
||||||
index: number
|
index: number
|
||||||
) {
|
) {
|
||||||
|
fullscreen.value = options?.fullscreen ?? false;
|
||||||
if (options?.[event] && isFunction(options?.[event])) {
|
if (options?.[event] && isFunction(options?.[event])) {
|
||||||
return options?.[event]({ options, index });
|
return options?.[event]({ options, index });
|
||||||
}
|
}
|
||||||
@@ -69,25 +84,49 @@ function handleClose(
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
|
class="pure-dialog"
|
||||||
v-for="(options, index) in dialogStore"
|
v-for="(options, index) in dialogStore"
|
||||||
:key="index"
|
:key="index"
|
||||||
v-bind="options"
|
v-bind="options"
|
||||||
v-model="options.visible"
|
v-model="options.visible"
|
||||||
@opened="eventsCallBack('open', options, index)"
|
:fullscreen="fullscreen ? true : options?.fullscreen ? true : false"
|
||||||
@close="handleClose(options, index)"
|
@close="handleClose(options, index)"
|
||||||
|
@opened="eventsCallBack('open', options, index)"
|
||||||
@openAutoFocus="eventsCallBack('openAutoFocus', options, index)"
|
@openAutoFocus="eventsCallBack('openAutoFocus', options, index)"
|
||||||
@closeAutoFocus="eventsCallBack('closeAutoFocus', options, index)"
|
@closeAutoFocus="eventsCallBack('closeAutoFocus', options, index)"
|
||||||
>
|
>
|
||||||
<!-- header -->
|
<!-- header -->
|
||||||
<template
|
<template
|
||||||
v-if="options?.headerRenderer"
|
v-if="options?.fullscreenIcon || options?.headerRenderer"
|
||||||
#header="{ close, titleId, titleClass }"
|
#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
|
<component
|
||||||
|
v-else
|
||||||
:is="options?.headerRenderer({ close, titleId, titleClass })"
|
:is="options?.headerRenderer({ close, titleId, titleClass })"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<!-- default -->
|
|
||||||
<component
|
<component
|
||||||
v-bind="options?.props"
|
v-bind="options?.props"
|
||||||
:is="options.contentRenderer({ options, index })"
|
:is="options.contentRenderer({ options, index })"
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ type DialogProps = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
/** `Dialog` 的宽度,默认 `50%` */
|
/** `Dialog` 的宽度,默认 `50%` */
|
||||||
width?: string | number;
|
width?: string | number;
|
||||||
/** 是否为全屏 `Dialog`,默认 `false` */
|
/** 是否为全屏 `Dialog`(会一直处于全屏状态,除非弹框关闭),默认 `false`,`fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
|
||||||
fullscreen?: boolean;
|
fullscreen?: boolean;
|
||||||
|
/** 是否显示全屏操作图标,默认 `false`,`fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
|
||||||
|
fullscreenIcon?: boolean;
|
||||||
/** `Dialog CSS` 中的 `margin-top` 值,默认 `15vh` */
|
/** `Dialog CSS` 中的 `margin-top` 值,默认 `15vh` */
|
||||||
top?: string;
|
top?: string;
|
||||||
/** 是否需要遮罩层,默认 `true` */
|
/** 是否需要遮罩层,默认 `true` */
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
|
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
|
||||||
import { delay, getKeyList, cloneDeep } from "@pureadmin/utils";
|
|
||||||
import { defineComponent, ref, computed, type PropType, nextTick } from "vue";
|
import { defineComponent, ref, computed, type PropType, nextTick } from "vue";
|
||||||
|
import {
|
||||||
|
delay,
|
||||||
|
cloneDeep,
|
||||||
|
isBoolean,
|
||||||
|
isFunction,
|
||||||
|
getKeyList
|
||||||
|
} from "@pureadmin/utils";
|
||||||
|
|
||||||
import Sortable from "sortablejs";
|
import Sortable from "sortablejs";
|
||||||
import DragIcon from "./svg/drag.svg?component";
|
import DragIcon from "./svg/drag.svg?component";
|
||||||
@@ -37,8 +43,13 @@ export default defineComponent({
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const checkAll = ref(true);
|
const checkAll = ref(true);
|
||||||
const isIndeterminate = ref(false);
|
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");
|
let checkColumnList = getKeyList(cloneDeep(props?.columns), "label");
|
||||||
const checkedColumns = ref(checkColumnList);
|
const checkedColumns = ref(getKeyList(cloneDeep(filterColumns), "label"));
|
||||||
const dynamicColumns = ref(cloneDeep(props?.columns));
|
const dynamicColumns = ref(cloneDeep(props?.columns));
|
||||||
|
|
||||||
const getDropdownItemStyle = computed(() => {
|
const getDropdownItemStyle = computed(() => {
|
||||||
@@ -120,7 +131,7 @@ export default defineComponent({
|
|||||||
dynamicColumns.value = cloneDeep(props?.columns);
|
dynamicColumns.value = cloneDeep(props?.columns);
|
||||||
checkColumnList = [];
|
checkColumnList = [];
|
||||||
checkColumnList = await getKeyList(cloneDeep(props?.columns), "label");
|
checkColumnList = await getKeyList(cloneDeep(props?.columns), "label");
|
||||||
checkedColumns.value = checkColumnList;
|
checkedColumns.value = getKeyList(cloneDeep(filterColumns), "label");
|
||||||
}
|
}
|
||||||
|
|
||||||
const dropdown = {
|
const dropdown = {
|
||||||
@@ -200,9 +211,13 @@ export default defineComponent({
|
|||||||
|
|
||||||
return () => (
|
return () => (
|
||||||
<>
|
<>
|
||||||
<div {...attrs} class="w-[99/100] mt-6 p-2 bg-bg_color">
|
<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">
|
<div class="flex justify-between w-full h-[60px] p-4">
|
||||||
<p class="font-bold truncate">{props.title}</p>
|
{slots?.title ? (
|
||||||
|
slots.title()
|
||||||
|
) : (
|
||||||
|
<p class="font-bold truncate">{props.title}</p>
|
||||||
|
)}
|
||||||
<div class="flex items-center justify-around">
|
<div class="flex items-center justify-around">
|
||||||
{slots?.buttons ? (
|
{slots?.buttons ? (
|
||||||
<div class="flex mr-4">{slots.buttons()}</div>
|
<div class="flex mr-4">{slots.buttons()}</div>
|
||||||
@@ -245,6 +260,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
<el-popover
|
<el-popover
|
||||||
v-slots={reference}
|
v-slots={reference}
|
||||||
|
placement="bottom-start"
|
||||||
popper-style={{ padding: 0 }}
|
popper-style={{ padding: 0 }}
|
||||||
width="160"
|
width="160"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
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) {
|
||||||
@@ -7,7 +7,9 @@ export const auth: Directive = {
|
|||||||
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']\""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
33
src/directives/copy/index.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { message } from "@/utils/message";
|
||||||
|
import { useEventListener } from "@vueuse/core";
|
||||||
|
import { copyTextToClipboard } from "@pureadmin/utils";
|
||||||
|
import type { Directive, DirectiveBinding } from "vue";
|
||||||
|
|
||||||
|
interface CopyEl extends HTMLElement {
|
||||||
|
copyValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 文本复制指令(默认双击复制) */
|
||||||
|
export const copy: Directive = {
|
||||||
|
mounted(el: CopyEl, binding: DirectiveBinding) {
|
||||||
|
const { value } = binding;
|
||||||
|
if (value) {
|
||||||
|
el.copyValue = value;
|
||||||
|
const arg = binding.arg ?? "dblclick";
|
||||||
|
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
|
||||||
|
useEventListener(el, arg, () => {
|
||||||
|
const success = copyTextToClipboard(el.copyValue);
|
||||||
|
success
|
||||||
|
? message("复制成功", { type: "success" })
|
||||||
|
: message("复制失败", { type: "error" });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'[Directive: copy]: need value! Like v-copy="modelValue"'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updated(el: CopyEl, binding: DirectiveBinding) {
|
||||||
|
el.copyValue = binding.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Directive, type DirectiveBinding, type VNode } from "vue";
|
|
||||||
import elementResizeDetectorMaker from "element-resize-detector";
|
|
||||||
import type { Erd } from "element-resize-detector";
|
|
||||||
import { emitter } from "@/utils/mitt";
|
|
||||||
|
|
||||||
const erd: Erd = elementResizeDetectorMaker({
|
|
||||||
strategy: "scroll"
|
|
||||||
});
|
|
||||||
|
|
||||||
export const resize: Directive = {
|
|
||||||
mounted(el: HTMLElement, binding?: DirectiveBinding, vnode?: VNode) {
|
|
||||||
erd.listenTo(el, elem => {
|
|
||||||
const width = elem.offsetWidth;
|
|
||||||
const height = elem.offsetHeight;
|
|
||||||
if (binding?.instance) {
|
|
||||||
emitter.emit("resize", { detail: { width, height } });
|
|
||||||
} else {
|
|
||||||
vnode.el.dispatchEvent(
|
|
||||||
new CustomEvent("resize", { detail: { width, height } })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
unmounted(el: HTMLElement) {
|
|
||||||
erd.uninstall(el);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
export * from "./auth";
|
export * from "./auth";
|
||||||
export * from "./elResizeDetector";
|
export * from "./copy";
|
||||||
|
export * from "./longpress";
|
||||||
|
export * from "./optimize";
|
||||||
|
|||||||
63
src/directives/longpress/index.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { useEventListener } from "@vueuse/core";
|
||||||
|
import type { Directive, DirectiveBinding } from "vue";
|
||||||
|
import { subBefore, subAfter, isFunction } from "@pureadmin/utils";
|
||||||
|
|
||||||
|
export const longpress: Directive = {
|
||||||
|
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||||
|
const cb = binding.value;
|
||||||
|
if (cb && isFunction(cb)) {
|
||||||
|
let timer = null;
|
||||||
|
let interTimer = null;
|
||||||
|
let num = 500;
|
||||||
|
let interNum = null;
|
||||||
|
const isInter = binding?.arg?.includes(":") ?? false;
|
||||||
|
|
||||||
|
if (isInter) {
|
||||||
|
num = Number(subBefore(binding.arg, ":"));
|
||||||
|
interNum = Number(subAfter(binding.arg, ":"));
|
||||||
|
} else if (binding.arg) {
|
||||||
|
num = Number(binding.arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
if (interTimer) {
|
||||||
|
clearInterval(interTimer);
|
||||||
|
interTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDownInter = (ev: PointerEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (interTimer === null) {
|
||||||
|
interTimer = setInterval(() => cb(), interNum);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDown = (ev: PointerEvent) => {
|
||||||
|
clear();
|
||||||
|
ev.preventDefault();
|
||||||
|
if (timer === null) {
|
||||||
|
timer = isInter
|
||||||
|
? setTimeout(() => {
|
||||||
|
cb();
|
||||||
|
onDownInter(ev);
|
||||||
|
}, num)
|
||||||
|
: setTimeout(() => cb(), num);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
|
||||||
|
useEventListener(el, "pointerdown", onDown);
|
||||||
|
useEventListener(el, "pointerup", clear);
|
||||||
|
useEventListener(el, "pointerleave", clear);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'[Directive: longpress]: need callback and callback must be a function! Like v-longpress="callback"'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
55
src/directives/optimize/index.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
isFunction,
|
||||||
|
isObject,
|
||||||
|
isArray,
|
||||||
|
debounce,
|
||||||
|
throttle
|
||||||
|
} from "@pureadmin/utils";
|
||||||
|
import { useEventListener } from "@vueuse/core";
|
||||||
|
import type { Directive, DirectiveBinding } from "vue";
|
||||||
|
|
||||||
|
/** 防抖(v-optimize或v-optimize:debounce)、节流(v-optimize:throttle)指令 */
|
||||||
|
export const optimize: Directive = {
|
||||||
|
mounted(el: HTMLElement, binding: DirectiveBinding) {
|
||||||
|
const { value } = binding;
|
||||||
|
const optimizeType = binding.arg ?? "debounce";
|
||||||
|
const type = ["debounce", "throttle"].find(t => t === optimizeType);
|
||||||
|
if (type) {
|
||||||
|
if (value && value.event && isFunction(value.fn)) {
|
||||||
|
let params = value?.params;
|
||||||
|
if (params) {
|
||||||
|
if (isArray(params) || isObject(params)) {
|
||||||
|
params = isObject(params) ? Array.of(params) : params;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"[Directive: optimize]: `params` must be an array or object"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
|
||||||
|
useEventListener(
|
||||||
|
el,
|
||||||
|
value.event,
|
||||||
|
type === "debounce"
|
||||||
|
? debounce(
|
||||||
|
params ? () => value.fn(...params) : value.fn,
|
||||||
|
value?.timeout ?? 200,
|
||||||
|
value?.immediate ?? false
|
||||||
|
)
|
||||||
|
: throttle(
|
||||||
|
params ? () => value.fn(...params) : value.fn,
|
||||||
|
value?.timeout ?? 1000
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"[Directive: optimize]: `event` and `fn` are required, and `fn` must be a function"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
"[Directive: optimize]: only `debounce` and `throttle` are supported"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -15,6 +15,7 @@ const {
|
|||||||
onPanel,
|
onPanel,
|
||||||
pureApp,
|
pureApp,
|
||||||
username,
|
username,
|
||||||
|
userAvatar,
|
||||||
avatarsStyle,
|
avatarsStyle,
|
||||||
toggleSideBar
|
toggleSideBar
|
||||||
} = useNav();
|
} = useNav();
|
||||||
@@ -46,10 +47,7 @@ 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>
|
||||||
|
|||||||
@@ -22,19 +22,31 @@ notices.value.map(v => (noticesNum.value += v.list.length));
|
|||||||
</span>
|
</span>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
<el-tabs :stretch="true" v-model="activeKey" class="dropdown-tabs">
|
<el-tabs
|
||||||
<template v-for="item in notices" :key="item.key">
|
:stretch="true"
|
||||||
<el-tab-pane
|
v-model="activeKey"
|
||||||
:label="`${item.name}(${item.list.length})`"
|
class="dropdown-tabs"
|
||||||
:name="`${item.key}`"
|
:style="{ width: notices.length === 0 ? '200px' : '330px' }"
|
||||||
>
|
>
|
||||||
<el-scrollbar max-height="330px">
|
<el-empty
|
||||||
<div class="noticeList-container">
|
v-if="notices.length === 0"
|
||||||
<NoticeList :list="item.list" />
|
description="暂无消息"
|
||||||
</div>
|
:image-size="60"
|
||||||
</el-scrollbar>
|
/>
|
||||||
</el-tab-pane>
|
<span v-else>
|
||||||
</template>
|
<template v-for="item in notices" :key="item.key">
|
||||||
|
<el-tab-pane
|
||||||
|
:label="`${item.name}(${item.list.length})`"
|
||||||
|
:name="`${item.key}`"
|
||||||
|
>
|
||||||
|
<el-scrollbar max-height="330px">
|
||||||
|
<div class="noticeList-container">
|
||||||
|
<NoticeList :list="item.list" />
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-tab-pane>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
@@ -46,8 +58,9 @@ notices.value.map(v => (noticesNum.value += v.list.length));
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 60px;
|
width: 40px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
margin-right: 10px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.header-notice-icon {
|
.header-notice-icon {
|
||||||
@@ -56,8 +69,6 @@ notices.value.map(v => (noticesNum.value += v.list.length));
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-tabs {
|
.dropdown-tabs {
|
||||||
width: 330px;
|
|
||||||
|
|
||||||
.noticeList-container {
|
.noticeList-container {
|
||||||
padding: 15px 24px 0;
|
padding: 15px 24px 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue";
|
|
||||||
import { emitter } from "@/utils/mitt";
|
import { emitter } from "@/utils/mitt";
|
||||||
import { onClickOutside } from "@vueuse/core";
|
import { onClickOutside } from "@vueuse/core";
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount } from "vue";
|
||||||
import Close from "@iconify-icons/ep/close";
|
import Close from "@iconify-icons/ep/close";
|
||||||
|
|
||||||
const target = ref(null);
|
const target = ref(null);
|
||||||
@@ -27,8 +27,15 @@ onClickOutside(target, (event: any) => {
|
|||||||
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>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -13,16 +27,15 @@
|
|||||||
<mdiKeyboardEsc class="icon" />
|
<mdiKeyboardEsc class="icon" />
|
||||||
关闭
|
关闭
|
||||||
</span>
|
</span>
|
||||||
|
<p
|
||||||
|
v-if="device !== 'mobile' && props.total > 0"
|
||||||
|
class="search-footer-total"
|
||||||
|
>
|
||||||
|
共{{ props.total }}项
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
|
|
||||||
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
|
|
||||||
import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
|
|
||||||
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.search-footer {
|
.search-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -40,5 +53,10 @@ import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
|
|||||||
box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff,
|
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>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { match } from "pinyin-pro";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { cloneDeep } from "@pureadmin/utils";
|
|
||||||
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 { ref, computed, shallowRef } from "vue";
|
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 { usePermissionStoreHook } from "@/store/modules/permission";
|
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||||
import Search from "@iconify-icons/ep/search";
|
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([]);
|
||||||
@@ -59,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;
|
||||||
@@ -82,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;
|
||||||
@@ -91,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,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 */
|
||||||
@@ -126,41 +143,56 @@ onKeyStroke("ArrowDown", handleDown);
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
top="5vh"
|
top="5vh"
|
||||||
|
class="pure-search-dialog"
|
||||||
v-model="show"
|
v-model="show"
|
||||||
:width="device === 'mobile' ? '80vw' : '50vw'"
|
:show-close="false"
|
||||||
|
:width="device === 'mobile' ? '80vw' : '40vw'"
|
||||||
:before-close="handleClose"
|
:before-close="handleClose"
|
||||||
|
:style="{
|
||||||
|
borderRadius: '6px'
|
||||||
|
}"
|
||||||
|
append-to-body
|
||||||
@opened="inputRef.focus()"
|
@opened="inputRef.focus()"
|
||||||
@closed="inputRef.blur()"
|
@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>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
import { useResizeObserver } from "@vueuse/core";
|
||||||
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
|
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
|
||||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||||
|
import { ref, computed, getCurrentInstance, onMounted } from "vue";
|
||||||
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
|
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
|
||||||
import Bookmark2Line from "@iconify-icons/ri/bookmark-2-line";
|
import Bookmark2Line from "@iconify-icons/ri/bookmark-2-line";
|
||||||
|
|
||||||
@@ -23,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 => {
|
||||||
@@ -54,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-item(56px+56px=112px)高度加上下margin(8px+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 ?? Bookmark2Line)" />
|
@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>
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,6 @@ watch($storage, ({ layout }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
dataThemeChange();
|
|
||||||
/* 初始化项目配置 */
|
/* 初始化项目配置 */
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
settings.greyVal &&
|
settings.greyVal &&
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<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 LogoutCircleRLine from "@iconify-icons/ri/logout-circle-r-line";
|
||||||
@@ -13,25 +14,21 @@ const menuRef = ref();
|
|||||||
const {
|
const {
|
||||||
route,
|
route,
|
||||||
title,
|
title,
|
||||||
routers,
|
|
||||||
logout,
|
logout,
|
||||||
backTopMenu,
|
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>
|
||||||
@@ -48,8 +45,7 @@ watch(
|
|||||||
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"
|
||||||
@@ -66,10 +62,7 @@ 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>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { title } = useNav();
|
const { title } = useNav();
|
||||||
const topPath = getTopMenu().path;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -18,7 +17,7 @@ const topPath = getTopMenu().path;
|
|||||||
key="props.collapse"
|
key="props.collapse"
|
||||||
:title="title"
|
:title="title"
|
||||||
class="sidebar-logo-link"
|
class="sidebar-logo-link"
|
||||||
:to="topPath"
|
:to="getTopMenu()?.path ?? '/'"
|
||||||
>
|
>
|
||||||
<img src="/logo.svg" alt="logo" />
|
<img src="/logo.svg" alt="logo" />
|
||||||
<span class="sidebar-title">{{ title }}</span>
|
<span class="sidebar-title">{{ title }}</span>
|
||||||
@@ -28,7 +27,7 @@ const topPath = getTopMenu().path;
|
|||||||
key="expand"
|
key="expand"
|
||||||
:title="title"
|
:title="title"
|
||||||
class="sidebar-logo-link"
|
class="sidebar-logo-link"
|
||||||
:to="topPath"
|
:to="getTopMenu()?.path ?? '/'"
|
||||||
>
|
>
|
||||||
<img src="/logo.svg" alt="logo" />
|
<img src="/logo.svg" alt="logo" />
|
||||||
<span class="sidebar-title">{{ title }}</span>
|
<span class="sidebar-title">{{ title }}</span>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import extraIcon from "./extraIcon.vue";
|
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";
|
||||||
@@ -16,12 +17,11 @@ const defaultActive = ref(null);
|
|||||||
const {
|
const {
|
||||||
route,
|
route,
|
||||||
device,
|
device,
|
||||||
routers,
|
|
||||||
logout,
|
logout,
|
||||||
onPanel,
|
onPanel,
|
||||||
menuSelect,
|
|
||||||
resolvePath,
|
resolvePath,
|
||||||
username,
|
username,
|
||||||
|
userAvatar,
|
||||||
getDivStyle,
|
getDivStyle,
|
||||||
avatarsStyle
|
avatarsStyle
|
||||||
} = useNav();
|
} = useNav();
|
||||||
@@ -30,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(() => {
|
||||||
@@ -64,7 +63,6 @@ watch(
|
|||||||
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"
|
||||||
@@ -97,10 +95,7 @@ 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>
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { emitter } from "@/utils/mitt";
|
|||||||
import SidebarItem from "./sidebarItem.vue";
|
import SidebarItem from "./sidebarItem.vue";
|
||||||
import leftCollapse from "./leftCollapse.vue";
|
import leftCollapse from "./leftCollapse.vue";
|
||||||
import { useNav } from "@/layout/hooks/useNav";
|
import { useNav } from "@/layout/hooks/useNav";
|
||||||
import { storageLocal } from "@pureadmin/utils";
|
|
||||||
import { responsiveStorageNameSpace } from "@/config";
|
import { 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(
|
||||||
@@ -18,8 +18,7 @@ const showLogo = ref(
|
|||||||
)?.showLogo ?? true
|
)?.showLogo ?? true
|
||||||
);
|
);
|
||||||
|
|
||||||
const { routers, device, pureApp, isCollapse, menuSelect, toggleSideBar } =
|
const { device, pureApp, isCollapse, menuSelect, toggleSideBar } = useNav();
|
||||||
useNav();
|
|
||||||
|
|
||||||
const subMenuData = ref([]);
|
const subMenuData = ref([]);
|
||||||
|
|
||||||
@@ -33,7 +32,13 @@ const loading = computed(() =>
|
|||||||
pureApp.layout === "mix" ? false : menuData.value.length === 0 ? true : false
|
pureApp.layout === "mix" ? false : menuData.value.length === 0 ? true : false
|
||||||
);
|
);
|
||||||
|
|
||||||
function getSubMenuData(path: string) {
|
const defaultActive = computed(() =>
|
||||||
|
!isAllEmpty(route.meta?.activePath) ? route.meta.activePath : route.path
|
||||||
|
);
|
||||||
|
|
||||||
|
function getSubMenuData() {
|
||||||
|
let path = "";
|
||||||
|
path = defaultActive.value;
|
||||||
subMenuData.value = [];
|
subMenuData.value = [];
|
||||||
// path的上级路由组成的数组
|
// path的上级路由组成的数组
|
||||||
const parentPathArr = getParentPaths(
|
const parentPathArr = getParentPaths(
|
||||||
@@ -49,22 +54,27 @@ 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");
|
||||||
if (route.path.includes("/redirect")) return;
|
});
|
||||||
getSubMenuData(route.path);
|
|
||||||
menuSelect(route.path, routers);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -83,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"
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ 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, isAllEmpty } from "@pureadmin/utils";
|
|
||||||
import { handleAliveRoute, getTopMenu } from "@/router/utils";
|
import { handleAliveRoute, getTopMenu } from "@/router/utils";
|
||||||
import { useSettingStoreHook } from "@/store/modules/settings";
|
import { useSettingStoreHook } from "@/store/modules/settings";
|
||||||
|
import { useResizeObserver, useFullscreen } from "@vueuse/core";
|
||||||
|
import { isEqual, isAllEmpty, debounce } from "@pureadmin/utils";
|
||||||
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
||||||
import { ref, watch, unref, toRaw, nextTick, onBeforeMount } from "vue";
|
import { ref, watch, unref, toRaw, nextTick, onBeforeUnmount } from "vue";
|
||||||
import { useResizeObserver, useDebounceFn, useFullscreen } from "@vueuse/core";
|
|
||||||
|
|
||||||
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
|
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
|
||||||
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
|
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
|
||||||
@@ -48,24 +48,26 @@ const tabDom = ref();
|
|||||||
const containerDom = ref();
|
const containerDom = ref();
|
||||||
const scrollbarDom = ref();
|
const scrollbarDom = ref();
|
||||||
const isShowArrow = ref(false);
|
const isShowArrow = ref(false);
|
||||||
const topPath = getTopMenu().path;
|
const topPath = getTopMenu()?.path;
|
||||||
const { VITE_HIDE_HOME } = import.meta.env;
|
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 = async (index: number): Promise<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];
|
||||||
@@ -76,9 +78,6 @@ const moveToView = async (index: number): Promise<void> => {
|
|||||||
? scrollbarDom.value?.offsetWidth
|
? scrollbarDom.value?.offsetWidth
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// 获取视图更新后dom
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
// 已有标签页总长度(包含溢出部分)
|
// 已有标签页总长度(包含溢出部分)
|
||||||
const tabDomWidth = tabDom.value ? tabDom.value?.offsetWidth : 0;
|
const tabDomWidth = tabDom.value ? tabDom.value?.offsetWidth : 0;
|
||||||
|
|
||||||
@@ -133,31 +132,29 @@ const handleScroll = (offset: number): void => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function dynamicRouteTag(value: string, parentPath: string): void {
|
function dynamicRouteTag(value: string): void {
|
||||||
const hasValue = multiTags.value.some(item => {
|
const hasValue = multiTags.value.some(item => {
|
||||||
return item.path === value;
|
return item.path === value;
|
||||||
});
|
});
|
||||||
|
|
||||||
function concatPath(arr: object[], value: string, parentPath: string) {
|
function concatPath(arr: object[], value: string) {
|
||||||
if (!hasValue) {
|
if (!hasValue) {
|
||||||
arr.forEach((arrItem: any) => {
|
arr.forEach((arrItem: any) => {
|
||||||
const pathConcat = parentPath + arrItem.path;
|
if (arrItem.path === value || arrItem.path === value) {
|
||||||
if (arrItem.path === value || pathConcat === value) {
|
|
||||||
useMultiTagsStoreHook().handleTags("push", {
|
useMultiTagsStoreHook().handleTags("push", {
|
||||||
path: value,
|
path: value,
|
||||||
parentPath: `/${parentPath.split("/")[1]}`,
|
|
||||||
meta: arrItem.meta,
|
meta: arrItem.meta,
|
||||||
name: arrItem.name
|
name: arrItem.name
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (arrItem.children && arrItem.children.length > 0) {
|
if (arrItem.children && arrItem.children.length > 0) {
|
||||||
concatPath(arrItem.children, value, parentPath);
|
concatPath(arrItem.children, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
concatPath(router.options.routes as any, value, parentPath);
|
concatPath(router.options.routes as any, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 刷新路由 */
|
/** 刷新路由 */
|
||||||
@@ -167,7 +164,7 @@ function onFresh() {
|
|||||||
path: "/redirect" + fullPath,
|
path: "/redirect" + fullPath,
|
||||||
query
|
query
|
||||||
});
|
});
|
||||||
handleAliveRoute(route as toRouteType, "refresh");
|
handleAliveRoute(route as ToRouteType, "refresh");
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteDynamicTag(obj: any, current: any, tag?: string) {
|
function deleteDynamicTag(obj: any, current: any, tag?: string) {
|
||||||
@@ -240,7 +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);
|
handleAliveRoute(route as ToRouteType);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClickDrop(key, item, selectRoute?: RouteConfigs) {
|
function onClickDrop(key, item, selectRoute?: RouteConfigs) {
|
||||||
@@ -288,7 +285,7 @@ function onClickDrop(key, item, selectRoute?: RouteConfigs) {
|
|||||||
length: multiTags.value.length
|
length: multiTags.value.length
|
||||||
});
|
});
|
||||||
router.push(topPath);
|
router.push(topPath);
|
||||||
handleAliveRoute(route as toRouteType);
|
handleAliveRoute(route as ToRouteType);
|
||||||
break;
|
break;
|
||||||
case 6:
|
case 6:
|
||||||
// 整体页面全屏
|
// 整体页面全屏
|
||||||
@@ -463,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;
|
||||||
|
|
||||||
// 根据当前路由初始化操作标签页的禁用状态
|
// 根据当前路由初始化操作标签页的禁用状态
|
||||||
@@ -481,32 +488,25 @@ 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();
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(isFullscreen, () => {
|
|
||||||
tagsViews[6].icon = Fullscreen;
|
|
||||||
tagsViews[6].text = "全屏";
|
|
||||||
});
|
|
||||||
|
|
||||||
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>
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ 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 { getTopMenu } from "@/router/utils";
|
||||||
import { useGlobal } from "@pureadmin/utils";
|
import { useGlobal } from "@pureadmin/utils";
|
||||||
import { computed, CSSProperties } from "vue";
|
|
||||||
import { useRouter, useRoute } from "vue-router";
|
import { useRouter, useRoute } from "vue-router";
|
||||||
import { router, remainingPaths } from "@/router";
|
import { router, remainingPaths } from "@/router";
|
||||||
|
import { computed, type CSSProperties } from "vue";
|
||||||
import { useAppStoreHook } from "@/store/modules/app";
|
import { useAppStoreHook } from "@/store/modules/app";
|
||||||
import { useUserStoreHook } from "@/store/modules/user";
|
import { useUserStoreHook } from "@/store/modules/user";
|
||||||
import { usePermissionStoreHook } from "@/store/modules/permission";
|
import { usePermissionStoreHook } from "@/store/modules/permission";
|
||||||
@@ -70,7 +71,7 @@ export function useNav() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function backTopMenu() {
|
function backTopMenu() {
|
||||||
router.push(getTopMenu().path);
|
router.push(getTopMenu()?.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPanel() {
|
function onPanel() {
|
||||||
@@ -96,38 +97,13 @@ export function useNav() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function menuSelect(indexPath: string, routers): void {
|
function menuSelect(indexPath: string) {
|
||||||
if (wholeMenus.value.length === 0) return;
|
if (wholeMenus.value.length === 0 || isRemaining(indexPath)) return;
|
||||||
if (isRemaining(indexPath)) return;
|
emitter.emit("changLayoutRoute", indexPath);
|
||||||
let parentPath = "";
|
|
||||||
const parentPathIndex = indexPath.lastIndexOf("/");
|
|
||||||
if (parentPathIndex > 0) {
|
|
||||||
parentPath = indexPath.slice(0, parentPathIndex);
|
|
||||||
}
|
|
||||||
/** 找到当前路由的信息 */
|
|
||||||
function findCurrentRoute(indexPath: string, routes) {
|
|
||||||
if (!routes) return console.error(errorInfo);
|
|
||||||
return routes.map(item => {
|
|
||||||
if (item.path === indexPath) {
|
|
||||||
if (item.redirect) {
|
|
||||||
findCurrentRoute(item.redirect, item.children);
|
|
||||||
} else {
|
|
||||||
/** 切换左侧菜单 通知标签页 */
|
|
||||||
emitter.emit("changLayoutRoute", {
|
|
||||||
indexPath,
|
|
||||||
parentPath
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (item.children) findCurrentRoute(indexPath, item.children);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
findCurrentRoute(indexPath, routers);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 判断路径是否参与菜单 */
|
/** 判断路径是否参与菜单 */
|
||||||
function isRemaining(path: string): boolean {
|
function isRemaining(path: string) {
|
||||||
return remainingPaths.includes(path);
|
return remainingPaths.includes(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +126,7 @@ export function useNav() {
|
|||||||
isCollapse,
|
isCollapse,
|
||||||
pureApp,
|
pureApp,
|
||||||
username,
|
username,
|
||||||
|
userAvatar,
|
||||||
avatarsStyle,
|
avatarsStyle,
|
||||||
tooltipEffect
|
tooltipEffect
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,12 +3,21 @@ import "animate.css";
|
|||||||
// 引入 src/components/ReIcon/src/offlineIcon.ts 文件中所有使用addIcon添加过的本地图标
|
// 引入 src/components/ReIcon/src/offlineIcon.ts 文件中所有使用addIcon添加过的本地图标
|
||||||
import "@/components/ReIcon/src/offlineIcon";
|
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";
|
||||||
@@ -18,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();
|
||||||
@@ -70,10 +80,10 @@ function toggle(device: string, bool: boolean) {
|
|||||||
// 判断是否可自动关闭菜单栏
|
// 判断是否可自动关闭菜单栏
|
||||||
let isAutoCloseSidebar = true;
|
let isAutoCloseSidebar = true;
|
||||||
|
|
||||||
// 监听容器
|
useResizeObserver(appWrapperRef, entries => {
|
||||||
emitter.on("resize", ({ detail }) => {
|
|
||||||
if (isMobile) return;
|
if (isMobile) return;
|
||||||
const { width } = detail;
|
const entry = entries[0];
|
||||||
|
const { width } = entry.contentRect;
|
||||||
width <= 760 ? setTheme("vertical") : setTheme(useAppStoreHook().layout);
|
width <= 760 ? setTheme("vertical") : setTheme(useAppStoreHook().layout);
|
||||||
/** width app-wrapper类容器宽度
|
/** width app-wrapper类容器宽度
|
||||||
* 0 < width <= 760 隐藏侧边栏
|
* 0 < width <= 760 隐藏侧边栏
|
||||||
@@ -88,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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,6 +113,10 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
useDataThemeChange().dataThemeChange();
|
||||||
|
});
|
||||||
|
|
||||||
const layoutHeader = defineComponent({
|
const layoutHeader = defineComponent({
|
||||||
render() {
|
render() {
|
||||||
return h(
|
return h(
|
||||||
@@ -134,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' &&
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export const routerArrays: Array<RouteConfigs> =
|
|||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
path: "/welcome",
|
path: "/welcome",
|
||||||
parentPath: "/",
|
|
||||||
meta: {
|
meta: {
|
||||||
title: "首页",
|
title: "首页",
|
||||||
icon: "homeFilled"
|
icon: "homeFilled"
|
||||||
@@ -25,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;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
formatFlatteningRoutes
|
formatFlatteningRoutes
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
import { buildHierarchyTree } from "@/utils/tree";
|
import { buildHierarchyTree } from "@/utils/tree";
|
||||||
import { isUrl, openLink, storageSession } from "@pureadmin/utils";
|
import { isUrl, openLink, storageSession, isAllEmpty } from "@pureadmin/utils";
|
||||||
|
|
||||||
import remainingRouter from "./modules/remaining";
|
import remainingRouter from "./modules/remaining";
|
||||||
|
|
||||||
@@ -46,13 +46,13 @@ Object.keys(modules).forEach(key => {
|
|||||||
|
|
||||||
/** 导出处理后的静态路由(三级及以上的路由全部拍成二级) */
|
/** 导出处理后的静态路由(三级及以上的路由全部拍成二级) */
|
||||||
export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes(
|
export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes(
|
||||||
formatFlatteningRoutes(buildHierarchyTree(ascending(routes)))
|
formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity))))
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 用于渲染菜单,保持原始层级 */
|
/** 用于渲染菜单,保持原始层级 */
|
||||||
export const constantMenus: Array<RouteComponent> = ascending(routes).concat(
|
export const constantMenus: Array<RouteComponent> = ascending(
|
||||||
...remainingRouter
|
routes.flat(Infinity)
|
||||||
);
|
).concat(...remainingRouter);
|
||||||
|
|
||||||
/** 不参与菜单的路由 */
|
/** 不参与菜单的路由 */
|
||||||
export const remainingPaths = Object.keys(remainingRouter).map(v => {
|
export const remainingPaths = Object.keys(remainingRouter).map(v => {
|
||||||
@@ -86,7 +86,9 @@ export function resetRouter() {
|
|||||||
if (name && router.hasRoute(name) && meta?.backstage) {
|
if (name && router.hasRoute(name) && meta?.backstage) {
|
||||||
router.removeRoute(name);
|
router.removeRoute(name);
|
||||||
router.options.routes = formatTwoStageRoutes(
|
router.options.routes = formatTwoStageRoutes(
|
||||||
formatFlatteningRoutes(buildHierarchyTree(ascending(routes)))
|
formatFlatteningRoutes(
|
||||||
|
buildHierarchyTree(ascending(routes.flat(Infinity)))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -98,7 +100,7 @@ const whiteList = ["/login"];
|
|||||||
|
|
||||||
const { VITE_HIDE_HOME } = import.meta.env;
|
const { VITE_HIDE_HOME } = import.meta.env;
|
||||||
|
|
||||||
router.beforeEach((to: toRouteType, _from, next) => {
|
router.beforeEach((to: ToRouteType, _from, next) => {
|
||||||
if (to.meta?.keepAlive) {
|
if (to.meta?.keepAlive) {
|
||||||
handleAliveRoute(to, "add");
|
handleAliveRoute(to, "add");
|
||||||
// 页面整体刷新和点击标签页刷新
|
// 页面整体刷新和点击标签页刷新
|
||||||
@@ -154,14 +156,26 @@ router.beforeEach((to: toRouteType, _from, next) => {
|
|||||||
getTopMenu(true);
|
getTopMenu(true);
|
||||||
// query、params模式路由传参数的标签页不在此处处理
|
// query、params模式路由传参数的标签页不在此处处理
|
||||||
if (route && route.meta?.title) {
|
if (route && route.meta?.title) {
|
||||||
useMultiTagsStoreHook().handleTags("push", {
|
if (isAllEmpty(route.parentId) && route.meta?.backstage) {
|
||||||
path: route.path,
|
// 此处为动态顶级路由(目录)
|
||||||
name: route.name,
|
const { path, name, meta } = route.children[0];
|
||||||
meta: route.meta
|
useMultiTagsStoreHook().handleTags("push", {
|
||||||
});
|
path,
|
||||||
|
name,
|
||||||
|
meta
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const { path, name, meta } = route;
|
||||||
|
useMultiTagsStoreHook().handleTags("push", {
|
||||||
|
path,
|
||||||
|
name,
|
||||||
|
meta
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
router.push(to.fullPath);
|
// 确保动态路由完全加入路由列表并且不影响静态路由(注意:动态路由刷新时router.beforeEach可能会触发两次,第一次触发动态路由还未完全添加,第二次动态路由才完全添加到路由列表,如果需要在router.beforeEach做一些判断可以在to.name存在的条件下去判断,这样就只会触发一次)
|
||||||
|
if (isAllEmpty(to.name)) router.push(to.fullPath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
toCorrectRoute();
|
toCorrectRoute();
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ function formatTwoStageRoutes(routesList: RouteRecordRaw[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 处理缓存路由(添加、删除、刷新) */
|
/** 处理缓存路由(添加、删除、刷新) */
|
||||||
function handleAliveRoute({ name }: toRouteType, mode?: string) {
|
function handleAliveRoute({ name }: ToRouteType, mode?: string) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case "add":
|
case "add":
|
||||||
usePermissionStoreHook().cacheOperate({
|
usePermissionStoreHook().cacheOperate({
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { defineStore } from "pinia";
|
|||||||
import { store } from "@/store";
|
import { store } from "@/store";
|
||||||
import { cacheType } from "./types";
|
import { cacheType } from "./types";
|
||||||
import { constantMenus } from "@/router";
|
import { constantMenus } from "@/router";
|
||||||
import { getKeyList } from "@pureadmin/utils";
|
|
||||||
import { useMultiTagsStoreHook } from "./multiTags";
|
import { useMultiTagsStoreHook } from "./multiTags";
|
||||||
|
import { debounce, getKeyList } from "@pureadmin/utils";
|
||||||
import { ascending, filterTree, filterNoPermissionTree } from "@/router/utils";
|
import { ascending, filterTree, filterNoPermissionTree } from "@/router/utils";
|
||||||
|
|
||||||
export const usePermissionStore = defineStore({
|
export const usePermissionStore = defineStore({
|
||||||
@@ -37,7 +37,7 @@ export const usePermissionStore = defineStore({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
/** 监听缓存页面是否存在于标签页,不存在则删除 */
|
/** 监听缓存页面是否存在于标签页,不存在则删除 */
|
||||||
(() => {
|
debounce(() => {
|
||||||
let cacheLength = this.cachePageList.length;
|
let cacheLength = this.cachePageList.length;
|
||||||
const nameList = getKeyList(useMultiTagsStoreHook().multiTags, "name");
|
const nameList = getKeyList(useMultiTagsStoreHook().multiTags, "name");
|
||||||
while (cacheLength > 0) {
|
while (cacheLength > 0) {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export type appType = {
|
|||||||
|
|
||||||
export type multiType = {
|
export type multiType = {
|
||||||
path: string;
|
path: string;
|
||||||
parentPath: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
meta: any;
|
meta: any;
|
||||||
query?: object;
|
query?: object;
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ html.dark {
|
|||||||
&:hover {
|
&:hover {
|
||||||
color: rgb(255 255 255 / 85%) !important;
|
color: rgb(255 255 255 / 85%) !important;
|
||||||
background-color: rgb(255 255 255 / 12%);
|
background-color: rgb(255 255 255 / 12%);
|
||||||
|
|
||||||
|
.pure-dialog-svg {
|
||||||
|
color: rgb(255 255 255 / 85%) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,4 +107,35 @@ html.dark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 自定义菜单搜索样式 */
|
||||||
|
.pure-search-dialog {
|
||||||
|
.el-dialog__footer {
|
||||||
|
box-shadow: 0 -1px 0 0 #555a64, 0 -3px 6px 0 rgb(69 98 155 / 12%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-footer {
|
||||||
|
.search-footer-item {
|
||||||
|
color: rgb(235 235 235 / 60%);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ReSegmented 组件 */
|
||||||
|
.pure-segmented {
|
||||||
|
color: rgb(255 255 255 / 65%);
|
||||||
|
background-color: #000;
|
||||||
|
|
||||||
|
.pure-segmented-item-selected {
|
||||||
|
background-color: #1f1f1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pure-segmented-item-disabled {
|
||||||
|
color: rgb(255 255 255 / 25%);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pure-dialog {
|
||||||
|
.pure-dialog-svg {
|
||||||
|
color: var(--el-color-info);
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__headerbtn {
|
||||||
|
top: 20px;
|
||||||
|
right: 14px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 全局覆盖element-plus的el-dialog、el-drawer、el-message-box、el-notification组件右上角关闭图标的样式,表现更鲜明 */
|
/* 全局覆盖element-plus的el-dialog、el-drawer、el-message-box、el-notification组件右上角关闭图标的样式,表现更鲜明 */
|
||||||
.el-dialog__headerbtn,
|
.el-dialog__headerbtn,
|
||||||
.el-message-box__headerbtn {
|
.el-message-box__headerbtn {
|
||||||
@@ -94,6 +107,10 @@
|
|||||||
color: rgb(0 0 0 / 88%) !important;
|
color: rgb(0 0 0 / 88%) !important;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: rgb(0 0 0 / 6%);
|
background-color: rgb(0 0 0 / 6%);
|
||||||
|
|
||||||
|
.pure-dialog-svg {
|
||||||
|
color: rgb(0 0 0 / 88%) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,3 +148,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 自定义菜单搜索样式 */
|
||||||
|
.pure-search-dialog {
|
||||||
|
.el-dialog__header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__body {
|
||||||
|
padding-top: 12px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input__inner {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__footer {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
box-shadow: 0 -1px 0 0 #e0e3e8, 0 -3px 6px 0 rgb(69 98 155 / 12%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class PureHttp {
|
|||||||
async (config: PureHttpRequestConfig): Promise<any> => {
|
async (config: PureHttpRequestConfig): Promise<any> => {
|
||||||
// 开启进度条动画
|
// 开启进度条动画
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
// 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
|
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
|
||||||
if (typeof config.beforeRequestCallback === "function") {
|
if (typeof config.beforeRequestCallback === "function") {
|
||||||
config.beforeRequestCallback(config);
|
config.beforeRequestCallback(config);
|
||||||
return config;
|
return config;
|
||||||
@@ -123,7 +123,7 @@ class PureHttp {
|
|||||||
const $config = response.config;
|
const $config = response.config;
|
||||||
// 关闭进度条动画
|
// 关闭进度条动画
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
// 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
|
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
|
||||||
if (typeof $config.beforeResponseCallback === "function") {
|
if (typeof $config.beforeResponseCallback === "function") {
|
||||||
$config.beforeResponseCallback(response);
|
$config.beforeResponseCallback(response);
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -159,7 +159,7 @@ class PureHttp {
|
|||||||
...axiosConfig
|
...axiosConfig
|
||||||
} as PureHttpRequestConfig;
|
} as PureHttpRequestConfig;
|
||||||
|
|
||||||
// 单独处理自定义请求/响应回掉
|
// 单独处理自定义请求/响应回调
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
PureHttp.axiosInstance
|
PureHttp.axiosInstance
|
||||||
.request(config)
|
.request(config)
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
import type { Emitter } from "mitt";
|
import type { Emitter } from "mitt";
|
||||||
import mitt from "mitt";
|
import mitt from "mitt";
|
||||||
|
|
||||||
|
/** 全局公共事件需要在此处添加类型 */
|
||||||
type Events = {
|
type Events = {
|
||||||
resize: {
|
|
||||||
detail: {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
openPanel: string;
|
openPanel: string;
|
||||||
tagViewsChange: string;
|
tagViewsChange: string;
|
||||||
tagViewsShowModel: string;
|
tagViewsShowModel: string;
|
||||||
logoChange: boolean;
|
logoChange: boolean;
|
||||||
changLayoutRoute: {
|
changLayoutRoute: string;
|
||||||
indexPath: string;
|
|
||||||
parentPath: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const emitter: Emitter<Events> = mitt<Events>();
|
export const emitter: Emitter<Events> = mitt<Events>();
|
||||||
|
|||||||
@@ -28,8 +28,7 @@
|
|||||||
"element-plus/global",
|
"element-plus/global",
|
||||||
"@pureadmin/table/volar",
|
"@pureadmin/table/volar",
|
||||||
"@pureadmin/descriptions/volar"
|
"@pureadmin/descriptions/volar"
|
||||||
],
|
]
|
||||||
"typeRoots": ["./types", "./node_modules/@types/"]
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"mock/*.ts",
|
"mock/*.ts",
|
||||||
|
|||||||
93
types/global.d.ts
vendored
@@ -7,7 +7,6 @@ import type {
|
|||||||
import type { ECharts } from "echarts";
|
import type { ECharts } from "echarts";
|
||||||
import type { IconifyIcon } from "@iconify/vue";
|
import type { IconifyIcon } from "@iconify/vue";
|
||||||
import type { TableColumns } from "@pureadmin/table";
|
import type { TableColumns } from "@pureadmin/table";
|
||||||
import { type RouteComponent, type RouteLocationNormalized } from "vue-router";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局类型声明,无需引入直接在 `.vue` 、`.ts` 、`.tsx` 文件使用即可获得类型提示
|
* 全局类型声明,无需引入直接在 `.vue` 、`.ts` 、`.tsx` 文件使用即可获得类型提示
|
||||||
@@ -150,98 +149,6 @@ declare global {
|
|||||||
tags?: Array<any>;
|
tags?: Array<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* `src/router` 文件夹里的类型声明
|
|
||||||
*/
|
|
||||||
interface toRouteType extends RouteLocationNormalized {
|
|
||||||
meta: {
|
|
||||||
roles: Array<string>;
|
|
||||||
keepAlive?: boolean;
|
|
||||||
dynamicLevel?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 完整子路由配置表
|
|
||||||
*/
|
|
||||||
interface RouteChildrenConfigsTable {
|
|
||||||
/** 子路由地址 `必填` */
|
|
||||||
path: string;
|
|
||||||
/** 路由名字(对应不要重复,和当前组件的`name`保持一致)`必填` */
|
|
||||||
name?: string;
|
|
||||||
/** 路由重定向 `可选` */
|
|
||||||
redirect?: string;
|
|
||||||
/** 按需加载组件 `可选` */
|
|
||||||
component?: RouteComponent;
|
|
||||||
meta?: {
|
|
||||||
/** 菜单名称(兼容国际化、非国际化,如何用国际化的写法就必须在根目录的`locales`文件夹下对应添加) `必填` */
|
|
||||||
title: string;
|
|
||||||
/** 菜单图标 `可选` */
|
|
||||||
icon?: string | FunctionalComponent | IconifyIcon;
|
|
||||||
/** 菜单名称右侧的额外图标 */
|
|
||||||
extraIcon?: string | FunctionalComponent | IconifyIcon;
|
|
||||||
/** 是否在菜单中显示(默认`true`)`可选` */
|
|
||||||
showLink?: boolean;
|
|
||||||
/** 是否显示父级菜单 `可选` */
|
|
||||||
showParent?: boolean;
|
|
||||||
/** 页面级别权限设置 `可选` */
|
|
||||||
roles?: Array<string>;
|
|
||||||
/** 按钮级别权限设置 `可选` */
|
|
||||||
auths?: Array<string>;
|
|
||||||
/** 路由组件缓存(开启 `true`、关闭 `false`)`可选` */
|
|
||||||
keepAlive?: boolean;
|
|
||||||
/** 内嵌的`iframe`链接 `可选` */
|
|
||||||
frameSrc?: string;
|
|
||||||
/** `iframe`页是否开启首次加载动画(默认`true`)`可选` */
|
|
||||||
frameLoading?: boolean;
|
|
||||||
/** 页面加载动画(有两种形式,一种直接采用vue内置的`transitions`动画,另一种是使用`animate.css`写进、离场动画)`可选` */
|
|
||||||
transition?: {
|
|
||||||
/**
|
|
||||||
* @description 当前路由动画效果
|
|
||||||
* @see {@link https://next.router.vuejs.org/guide/advanced/transitions.html#transitions}
|
|
||||||
* @see animate.css {@link https://animate.style}
|
|
||||||
*/
|
|
||||||
name?: string;
|
|
||||||
/** 进场动画 */
|
|
||||||
enterTransition?: string;
|
|
||||||
/** 离场动画 */
|
|
||||||
leaveTransition?: string;
|
|
||||||
};
|
|
||||||
// 是否不添加信息到标签页,(默认`false`)
|
|
||||||
hiddenTag?: boolean;
|
|
||||||
/** 动态路由可打开的最大数量 `可选` */
|
|
||||||
dynamicLevel?: number;
|
|
||||||
};
|
|
||||||
/** 子路由配置项 */
|
|
||||||
children?: Array<RouteChildrenConfigsTable>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description 整体路由配置表(包括完整子路由)
|
|
||||||
*/
|
|
||||||
interface RouteConfigsTable {
|
|
||||||
/** 路由地址 `必填` */
|
|
||||||
path: string;
|
|
||||||
/** 路由名字(保持唯一)`可选` */
|
|
||||||
name?: string;
|
|
||||||
/** `Layout`组件 `可选` */
|
|
||||||
component?: RouteComponent;
|
|
||||||
/** 路由重定向 `可选` */
|
|
||||||
redirect?: string;
|
|
||||||
meta?: {
|
|
||||||
/** 菜单名称(兼容国际化、非国际化,如何用国际化的写法就必须在根目录的`locales`文件夹下对应添加)`必填` */
|
|
||||||
title: string;
|
|
||||||
/** 菜单图标 `可选` */
|
|
||||||
icon?: string | FunctionalComponent | IconifyIcon;
|
|
||||||
/** 是否在菜单中显示(默认`true`)`可选` */
|
|
||||||
showLink?: boolean;
|
|
||||||
/** 菜单升序排序,值越高排的越后(只针对顶级路由)`可选` */
|
|
||||||
rank?: number;
|
|
||||||
};
|
|
||||||
/** 子路由配置项 */
|
|
||||||
children?: Array<RouteChildrenConfigsTable>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 平台里所有组件实例都能访问到的全局属性对象的类型声明
|
* 平台里所有组件实例都能访问到的全局属性对象的类型声明
|
||||||
*/
|
*/
|
||||||
|
|||||||
4
types/index.d.ts
vendored
@@ -41,6 +41,10 @@ type DeepPartial<T> = {
|
|||||||
[P in keyof T]?: DeepPartial<T[P]>;
|
[P in keyof T]?: DeepPartial<T[P]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||||
|
|
||||||
|
type Exclusive<T, U> = (Without<T, U> & U) | (Without<U, T> & T);
|
||||||
|
|
||||||
type TimeoutHandle = ReturnType<typeof setTimeout>;
|
type TimeoutHandle = ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
type IntervalHandle = ReturnType<typeof setInterval>;
|
type IntervalHandle = ReturnType<typeof setInterval>;
|
||||||
|
|||||||
105
types/router.d.ts
vendored
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// 全局路由类型声明
|
||||||
|
|
||||||
|
import { type RouteComponent, type RouteLocationNormalized } from "vue-router";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface ToRouteType extends RouteLocationNormalized {
|
||||||
|
meta: CustomizeRouteMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 完整子路由的`meta`配置表
|
||||||
|
*/
|
||||||
|
interface CustomizeRouteMeta {
|
||||||
|
/** 菜单名称(兼容国际化、非国际化,如何用国际化的写法就必须在根目录的`locales`文件夹下对应添加) `必填` */
|
||||||
|
title: string;
|
||||||
|
/** 菜单图标 `可选` */
|
||||||
|
icon?: string | FunctionalComponent | IconifyIcon;
|
||||||
|
/** 菜单名称右侧的额外图标 */
|
||||||
|
extraIcon?: string | FunctionalComponent | IconifyIcon;
|
||||||
|
/** 是否在菜单中显示(默认`true`)`可选` */
|
||||||
|
showLink?: boolean;
|
||||||
|
/** 是否显示父级菜单 `可选` */
|
||||||
|
showParent?: boolean;
|
||||||
|
/** 页面级别权限设置 `可选` */
|
||||||
|
roles?: Array<string>;
|
||||||
|
/** 按钮级别权限设置 `可选` */
|
||||||
|
auths?: Array<string>;
|
||||||
|
/** 路由组件缓存(开启 `true`、关闭 `false`)`可选` */
|
||||||
|
keepAlive?: boolean;
|
||||||
|
/** 内嵌的`iframe`链接 `可选` */
|
||||||
|
frameSrc?: string;
|
||||||
|
/** `iframe`页是否开启首次加载动画(默认`true`)`可选` */
|
||||||
|
frameLoading?: boolean;
|
||||||
|
/** 页面加载动画(有两种形式,一种直接采用vue内置的`transitions`动画,另一种是使用`animate.css`写进、离场动画)`可选` */
|
||||||
|
transition?: {
|
||||||
|
/**
|
||||||
|
* @description 当前路由动画效果
|
||||||
|
* @see {@link https://next.router.vuejs.org/guide/advanced/transitions.html#transitions}
|
||||||
|
* @see animate.css {@link https://animate.style}
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
/** 进场动画 */
|
||||||
|
enterTransition?: string;
|
||||||
|
/** 离场动画 */
|
||||||
|
leaveTransition?: string;
|
||||||
|
};
|
||||||
|
// 是否不添加信息到标签页,(默认`false`)
|
||||||
|
hiddenTag?: boolean;
|
||||||
|
/** 动态路由可打开的最大数量 `可选` */
|
||||||
|
dynamicLevel?: number;
|
||||||
|
/** 将某个菜单激活
|
||||||
|
* (主要用于通过`query`或`params`传参的路由,当它们通过配置`showLink: false`后不在菜单中显示,就不会有任何菜单高亮,
|
||||||
|
* 而通过设置`activePath`指定激活菜单即可获得高亮,`activePath`为指定激活菜单的`path`)
|
||||||
|
*/
|
||||||
|
activePath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 完整子路由配置表
|
||||||
|
*/
|
||||||
|
interface RouteChildrenConfigsTable {
|
||||||
|
/** 子路由地址 `必填` */
|
||||||
|
path: string;
|
||||||
|
/** 路由名字(对应不要重复,和当前组件的`name`保持一致)`必填` */
|
||||||
|
name?: string;
|
||||||
|
/** 路由重定向 `可选` */
|
||||||
|
redirect?: string;
|
||||||
|
/** 按需加载组件 `可选` */
|
||||||
|
component?: RouteComponent;
|
||||||
|
meta?: CustomizeRouteMeta;
|
||||||
|
/** 子路由配置项 */
|
||||||
|
children?: Array<RouteChildrenConfigsTable>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 整体路由配置表(包括完整子路由)
|
||||||
|
*/
|
||||||
|
interface RouteConfigsTable {
|
||||||
|
/** 路由地址 `必填` */
|
||||||
|
path: string;
|
||||||
|
/** 路由名字(保持唯一)`可选` */
|
||||||
|
name?: string;
|
||||||
|
/** `Layout`组件 `可选` */
|
||||||
|
component?: RouteComponent;
|
||||||
|
/** 路由重定向 `可选` */
|
||||||
|
redirect?: string;
|
||||||
|
meta?: {
|
||||||
|
/** 菜单名称(兼容国际化、非国际化,如何用国际化的写法就必须在根目录的`locales`文件夹下对应添加)`必填` */
|
||||||
|
title: string;
|
||||||
|
/** 菜单图标 `可选` */
|
||||||
|
icon?: string | FunctionalComponent | IconifyIcon;
|
||||||
|
/** 是否在菜单中显示(默认`true`)`可选` */
|
||||||
|
showLink?: boolean;
|
||||||
|
/** 菜单升序排序,值越高排的越后(只针对顶级路由)`可选` */
|
||||||
|
rank?: number;
|
||||||
|
};
|
||||||
|
/** 子路由配置项 */
|
||||||
|
children?: Array<RouteChildrenConfigsTable>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://router.vuejs.org/zh/guide/advanced/meta.html#typescript
|
||||||
|
declare module "vue-router" {
|
||||||
|
interface RouteMeta extends CustomizeRouteMeta {}
|
||||||
|
}
|
||||||