Compare commits

...

7 Commits

Author SHA1 Message Date
xiaoxian521
1f27d6cd9e release: update 4.5.0 2023-06-26 12:32:11 +08:00
xiaoxian521
4b435d0e0f chore: 同步完整版代码 2023-06-19 12:04:37 +08:00
xiaoxian521
872e0bbd5b chore: 同步完整版代码 2023-06-15 18:38:39 +08:00
xiaoxian521
b6859d7920 release: update 4.4.0 2023-06-14 16:29:45 +08:00
xiaoxian521
6e02ae14a0 chore: 同步完整版代码 2023-06-07 11:40:26 +08:00
xiaoxian521
c28066fb1f feat: 添加vscode-docker插件 2023-06-05 23:02:54 +08:00
xiaoxian521
d9ab1b1198 release: update 4.3.0 2023-06-05 15:44:49 +08:00
56 changed files with 2826 additions and 2069 deletions

21
.dockerignore Normal file
View File

@@ -0,0 +1,21 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.eslintcache
report.html
yarn.lock
npm-debug.log*
.pnpm-error.log*
.pnpm-debug.log
tests/**/coverage/
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
tsconfig.tsbuildinfo

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
# 预发布也需要生产环境的行为 # 预发布也需要生产环境的行为
# https://cn.vitejs.dev/guide/env-and-mode.html#modes # https://cn.vitejs.dev/guide/env-and-mode.html#modes
NODE_ENV=production # NODE_ENV = development
VITE_PUBLIC_PATH = / VITE_PUBLIC_PATH = ./
# 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数" # 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash" VITE_ROUTER_HISTORY = "hash"

View File

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

View File

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

View File

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

20
.vscode/vue3.3.code-snippets vendored Normal file
View File

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

20
Dockerfile Normal file
View File

@@ -0,0 +1,20 @@
FROM node:16-alpine as build-stage
WORKDIR /app
RUN corepack enable
RUN corepack prepare pnpm@7.32.1 --activate
RUN npm config set registry https://registry.npmmirror.com
COPY .npmrc package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

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

View File

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

View File

@@ -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"
]; ];
/** /**

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{ {
"Version": "4.1.0", "Version": "4.5.0",
"Title": "PureAdmin", "Title": "PureAdmin",
"FixedHeader": true, "FixedHeader": true,
"HiddenSideBar": false, "HiddenSideBar": false,

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src/assets/user.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

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

View File

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

View File

@@ -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` */

View File

@@ -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"

View File

@@ -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']\""
);
} }
} }
}; };

View File

@@ -0,0 +1,33 @@
import { message } from "@/utils/message";
import { useEventListener } from "@vueuse/core";
import { copyTextToClipboard } from "@pureadmin/utils";
import type { Directive, DirectiveBinding } from "vue";
interface CopyEl extends HTMLElement {
copyValue: string;
}
/** 文本复制指令(默认双击复制) */
export const copy: Directive = {
mounted(el: CopyEl, binding: DirectiveBinding) {
const { value } = binding;
if (value) {
el.copyValue = value;
const arg = binding.arg ?? "dblclick";
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
useEventListener(el, arg, () => {
const success = copyTextToClipboard(el.copyValue);
success
? message("复制成功", { type: "success" })
: message("复制失败", { type: "error" });
});
} else {
throw new Error(
'[Directive: copy]: need value! Like v-copy="modelValue"'
);
}
},
updated(el: CopyEl, binding: DirectiveBinding) {
el.copyValue = binding.value;
}
};

View File

@@ -1,27 +0,0 @@
import { Directive, type DirectiveBinding, type VNode } from "vue";
import elementResizeDetectorMaker from "element-resize-detector";
import type { Erd } from "element-resize-detector";
import { emitter } from "@/utils/mitt";
const erd: Erd = elementResizeDetectorMaker({
strategy: "scroll"
});
export const resize: Directive = {
mounted(el: HTMLElement, binding?: DirectiveBinding, vnode?: VNode) {
erd.listenTo(el, elem => {
const width = elem.offsetWidth;
const height = elem.offsetHeight;
if (binding?.instance) {
emitter.emit("resize", { detail: { width, height } });
} else {
vnode.el.dispatchEvent(
new CustomEvent("resize", { detail: { width, height } })
);
}
});
},
unmounted(el: HTMLElement) {
erd.uninstall(el);
}
};

View File

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

View File

@@ -0,0 +1,63 @@
import { useEventListener } from "@vueuse/core";
import type { Directive, DirectiveBinding } from "vue";
import { subBefore, subAfter, isFunction } from "@pureadmin/utils";
export const longpress: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const cb = binding.value;
if (cb && isFunction(cb)) {
let timer = null;
let interTimer = null;
let num = 500;
let interNum = null;
const isInter = binding?.arg?.includes(":") ?? false;
if (isInter) {
num = Number(subBefore(binding.arg, ":"));
interNum = Number(subAfter(binding.arg, ":"));
} else if (binding.arg) {
num = Number(binding.arg);
}
const clear = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
if (interTimer) {
clearInterval(interTimer);
interTimer = null;
}
};
const onDownInter = (ev: PointerEvent) => {
ev.preventDefault();
if (interTimer === null) {
interTimer = setInterval(() => cb(), interNum);
}
};
const onDown = (ev: PointerEvent) => {
clear();
ev.preventDefault();
if (timer === null) {
timer = isInter
? setTimeout(() => {
cb();
onDownInter(ev);
}, num)
: setTimeout(() => cb(), num);
}
};
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
useEventListener(el, "pointerdown", onDown);
useEventListener(el, "pointerup", clear);
useEventListener(el, "pointerleave", clear);
} else {
throw new Error(
'[Directive: longpress]: need callback and callback must be a function! Like v-longpress="callback"'
);
}
}
};

View File

@@ -0,0 +1,55 @@
import {
isFunction,
isObject,
isArray,
debounce,
throttle
} from "@pureadmin/utils";
import { useEventListener } from "@vueuse/core";
import type { Directive, DirectiveBinding } from "vue";
/** 防抖v-optimize或v-optimize:debounce、节流v-optimize:throttle指令 */
export const optimize: Directive = {
mounted(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding;
const optimizeType = binding.arg ?? "debounce";
const type = ["debounce", "throttle"].find(t => t === optimizeType);
if (type) {
if (value && value.event && isFunction(value.fn)) {
let params = value?.params;
if (params) {
if (isArray(params) || isObject(params)) {
params = isObject(params) ? Array.of(params) : params;
} else {
throw new Error(
"[Directive: optimize]: `params` must be an array or object"
);
}
}
// Register using addEventListener on mounted, and removeEventListener automatically on unmounted
useEventListener(
el,
value.event,
type === "debounce"
? debounce(
params ? () => value.fn(...params) : value.fn,
value?.timeout ?? 200,
value?.immediate ?? false
)
: throttle(
params ? () => value.fn(...params) : value.fn,
value?.timeout ?? 1000
)
);
} else {
throw new Error(
"[Directive: optimize]: `event` and `fn` are required, and `fn` must be a function"
);
}
} else {
throw new Error(
"[Directive: optimize]: only `debounce` and `throttle` are supported"
);
}
}
};

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,17 @@
<script setup lang="ts">
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
import { useNav } from "@/layout/hooks/useNav";
import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
const props = withDefaults(defineProps<{ total: number }>(), {
total: 0
});
const { device } = useNav();
</script>
<template> <template>
<div class="search-footer text-[#333] dark:text-white"> <div class="search-footer text-[#333] dark:text-white">
<span class="search-footer-item"> <span class="search-footer-item">
@@ -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>

View File

@@ -1,13 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { match } from "pinyin-pro";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { cloneDeep } from "@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>

View File

@@ -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-item56px+56px=112px高度加上下margin8px+8px=16px
return scrollTop > innerHeight.value ? scrollTop - innerHeight.value : 0;
}
onMounted(() => {
resizeResult();
});
defineExpose({ handleScroll });
</script> </script>
<template> <template>
<div class="result"> <div ref="resultRef" class="result">
<template v-for="item in options" :key="item.path"> <div
<div v-for="(item, index) in options"
class="result-item dark:bg-[#1d1d1d]" :key="item.path"
:style="itemStyle(item)" :ref="'resultItemRef' + index"
@click="handleTo" class="result-item dark:bg-[#1d1d1d]"
@mouseenter="handleMouse(item)" :style="itemStyle(item)"
> @click="handleTo"
<component :is="useRenderIcon(item.meta?.icon ?? 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>

View File

@@ -218,7 +218,6 @@ watch($storage, ({ layout }) => {
}); });
onBeforeMount(() => { onBeforeMount(() => {
dataThemeChange();
/* 初始化项目配置 */ /* 初始化项目配置 */
nextTick(() => { nextTick(() => {
settings.greyVal && settings.greyVal &&

View File

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

View File

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

View File

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

View File

@@ -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"

View File

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

View File

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

View File

@@ -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' &&

View File

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

View File

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

View File

@@ -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({

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -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
View 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 {}
}