Merge remote-tracking branch 'upstream/main' into merge_1
# Conflicts: # README.md
							
								
								
									
										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
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
# 预发布也需要生产环境的行为
 | 
			
		||||
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
 | 
			
		||||
NODE_ENV=production
 | 
			
		||||
# NODE_ENV = development
 | 
			
		||||
 | 
			
		||||
VITE_PUBLIC_PATH = /
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -4,6 +4,7 @@ dist
 | 
			
		||||
dist-ssr
 | 
			
		||||
*.local
 | 
			
		||||
.eslintcache
 | 
			
		||||
.stylelintcache
 | 
			
		||||
report.html
 | 
			
		||||
 | 
			
		||||
yarn.lock
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -3,6 +3,7 @@
 | 
			
		||||
    "christian-kohler.path-intellisense",
 | 
			
		||||
    "vscode-icons-team.vscode-icons",
 | 
			
		||||
    "davidanson.vscode-markdownlint",
 | 
			
		||||
    "ms-azuretools.vscode-docker",
 | 
			
		||||
    "stylelint.vscode-stylelint",
 | 
			
		||||
    "bradlc.vscode-tailwindcss",
 | 
			
		||||
    "dbaeumer.vscode-eslint",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								.vscode/vue3.0.code-snippets
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -1,19 +1,19 @@
 | 
			
		||||
{
 | 
			
		||||
  "Vue3.0快速生成模板": {
 | 
			
		||||
    "scope": "vue",
 | 
			
		||||
    "prefix": "Vue3.0",
 | 
			
		||||
    "body": [
 | 
			
		||||
      "<template>",
 | 
			
		||||
      "\t<div>\n",
 | 
			
		||||
      "\t</div>",
 | 
			
		||||
      "\t<div>test</div>",
 | 
			
		||||
      "</template>\n",
 | 
			
		||||
      "<script lang='ts'>",
 | 
			
		||||
      "export default {",
 | 
			
		||||
      "\tsetup(){",
 | 
			
		||||
      "\t\treturn{\n\n\t\t}",
 | 
			
		||||
      "\t},",
 | 
			
		||||
      "\tsetup() {",
 | 
			
		||||
      "\t\treturn {}",
 | 
			
		||||
      "\t}",
 | 
			
		||||
      "}",
 | 
			
		||||
      "</script>\n",
 | 
			
		||||
      "<style scoped>\n",
 | 
			
		||||
      "<style lang='scss' scoped>\n",
 | 
			
		||||
      "</style>",
 | 
			
		||||
      "$2"
 | 
			
		||||
    ],
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										6
									
								
								.vscode/vue3.2.code-snippets
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -1,14 +1,14 @@
 | 
			
		||||
{
 | 
			
		||||
  "Vue3.2+快速生成模板": {
 | 
			
		||||
    "scope": "vue",
 | 
			
		||||
    "prefix": "Vue3.2+",
 | 
			
		||||
    "body": [
 | 
			
		||||
      "<script setup lang='ts'>",
 | 
			
		||||
      "</script>\n",
 | 
			
		||||
      "<template>",
 | 
			
		||||
      "\t<div>\n",
 | 
			
		||||
      "\t</div>",
 | 
			
		||||
      "\t<div>test</div>",
 | 
			
		||||
      "</template>\n",
 | 
			
		||||
      "<style scoped>\n",
 | 
			
		||||
      "<style lang='scss' scoped>\n",
 | 
			
		||||
      "</style>",
 | 
			
		||||
      "$2"
 | 
			
		||||
    ],
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								.vscode/vue3.3.code-snippets
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,20 @@
 | 
			
		||||
{
 | 
			
		||||
  "Vue3.3+defineOptions快速生成模板": {
 | 
			
		||||
    "scope": "vue",
 | 
			
		||||
    "prefix": "Vue3.3+",
 | 
			
		||||
    "body": [
 | 
			
		||||
      "<script setup lang='ts'>",
 | 
			
		||||
      "defineOptions({",
 | 
			
		||||
      "\tname: ''",
 | 
			
		||||
      "})",
 | 
			
		||||
      "</script>\n",
 | 
			
		||||
      "<template>",
 | 
			
		||||
      "\t<div>test</div>",
 | 
			
		||||
      "</template>\n",
 | 
			
		||||
      "<style lang='scss' scoped>\n",
 | 
			
		||||
      "</style>",
 | 
			
		||||
      "$2"
 | 
			
		||||
    ],
 | 
			
		||||
    "description": "Vue3.3+defineOptions快速生成模板"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,20 @@
 | 
			
		||||
FROM node:16-alpine as build-stage
 | 
			
		||||
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
RUN corepack enable
 | 
			
		||||
RUN corepack prepare pnpm@7.32.1 --activate
 | 
			
		||||
 | 
			
		||||
RUN npm config set registry https://registry.npmmirror.com
 | 
			
		||||
 | 
			
		||||
COPY .npmrc package.json pnpm-lock.yaml ./
 | 
			
		||||
RUN pnpm install --frozen-lockfile
 | 
			
		||||
 | 
			
		||||
COPY . .
 | 
			
		||||
RUN pnpm build
 | 
			
		||||
 | 
			
		||||
FROM nginx:stable-alpine as production-stage
 | 
			
		||||
 | 
			
		||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
 | 
			
		||||
EXPOSE 80
 | 
			
		||||
 | 
			
		||||
CMD ["nginx", "-g", "daemon off;"]
 | 
			
		||||
@ -15,39 +15,22 @@ The simplified version is based on the shelf extracted from [vue-pure-admin](htt
 | 
			
		||||
 | 
			
		||||
## Docs
 | 
			
		||||
 | 
			
		||||
- [Click me to view the domestic documentation site](https://yiming_chang.gitee.io/pure-admin-doc)
 | 
			
		||||
- [Click me to view foreign document site](https://pure-admin.github.io/pure-admin-doc)
 | 
			
		||||
- [documentation site](https://yiming_chang.gitee.io/pure-admin-doc)
 | 
			
		||||
 | 
			
		||||
## Preview
 | 
			
		||||
 | 
			
		||||
- [Click me to view the preview station](https://pure-admin-thin.netlify.app/#/login)
 | 
			
		||||
 | 
			
		||||
## Usage
 | 
			
		||||
## Maintainer
 | 
			
		||||
 | 
			
		||||
### Installation dependencies
 | 
			
		||||
 | 
			
		||||
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/
 | 
			
		||||
[xiaoxian521](https://github.com/xiaoxian521)
 | 
			
		||||
 | 
			
		||||
## ⚠️ Attention
 | 
			
		||||
 | 
			
		||||
- The Lite version does not accept any issues and prs. If you have any questions, please go to the full version https://github.com/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
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										13
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@ -10,21 +10,20 @@
 | 
			
		||||
 | 
			
		||||
## 版本选择
 | 
			
		||||
 | 
			
		||||
当前是非国际化版本哦,如果您需要国际化版本 [请点击](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)
 | 
			
		||||
- [点我查看 UI 设计](https://www.bilibili.com/video/BV17g411T7rq)
 | 
			
		||||
 | 
			
		||||
## 配套文档
 | 
			
		||||
## 配套保姆级文档
 | 
			
		||||
 | 
			
		||||
- [点我查看国内文档站](https://yiming_chang.gitee.io/pure-admin-doc)
 | 
			
		||||
- [点我查看国外文档站](https://pure-admin.github.io/pure-admin-doc)
 | 
			
		||||
- [查看文档](https://yiming_chang.gitee.io/pure-admin-doc)
 | 
			
		||||
 | 
			
		||||
## 预览
 | 
			
		||||
 | 
			
		||||
- [点我查看预览站](https://pure-admin-thin.netlify.app/#/login)
 | 
			
		||||
- [查看预览](https://pure-admin-thin.netlify.app/#/login)
 | 
			
		||||
 | 
			
		||||
## 维护者
 | 
			
		||||
 | 
			
		||||
@ -86,10 +85,10 @@ pnpm remove 包名
 | 
			
		||||
 | 
			
		||||
## ⚠️ 注意
 | 
			
		||||
 | 
			
		||||
- 精简版不接受任何 `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)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										67
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "pure-admin-thin",
 | 
			
		||||
  "version": "4.1.0",
 | 
			
		||||
  "version": "4.3.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
 | 
			
		||||
@ -13,10 +13,10 @@
 | 
			
		||||
    "typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
 | 
			
		||||
    "svgo": "svgo -f src/assets/svg -o src/assets/svg",
 | 
			
		||||
    "cloc": "NODE_OPTIONS=--max-old-space-size=4096 cloc . --exclude-dir=node_modules --exclude-lang=YAML",
 | 
			
		||||
    "clean:cache": "rm -rf node_modules && rm -rf .eslintcache && pnpm install",
 | 
			
		||||
    "clean:cache": "rimraf node_modules && rimraf .eslintcache && pnpm install",
 | 
			
		||||
    "lint:eslint": "eslint --cache --max-warnings 0  \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
 | 
			
		||||
    "lint:prettier": "prettier --write  \"src/**/*.{js,ts,json,tsx,css,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:pretty": "pretty-quick --staged",
 | 
			
		||||
    "lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
 | 
			
		||||
@ -30,69 +30,69 @@
 | 
			
		||||
  ],
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@pureadmin/descriptions": "^1.1.1",
 | 
			
		||||
    "@pureadmin/table": "^2.1.0",
 | 
			
		||||
    "@pureadmin/utils": "^1.8.9",
 | 
			
		||||
    "@pureadmin/table": "^2.2.0",
 | 
			
		||||
    "@pureadmin/utils": "^1.9.3",
 | 
			
		||||
    "@vueuse/core": "^10.1.2",
 | 
			
		||||
    "@vueuse/motion": "2.0.0-beta.12",
 | 
			
		||||
    "@vueuse/motion": "^2.0.0",
 | 
			
		||||
    "animate.css": "^4.1.1",
 | 
			
		||||
    "axios": "^1.4.0",
 | 
			
		||||
    "dayjs": "^1.11.7",
 | 
			
		||||
    "dayjs": "^1.11.8",
 | 
			
		||||
    "echarts": "^5.4.2",
 | 
			
		||||
    "element-plus": "^2.3.4",
 | 
			
		||||
    "element-plus": "^2.3.6",
 | 
			
		||||
    "element-resize-detector": "^1.2.4",
 | 
			
		||||
    "js-cookie": "^3.0.5",
 | 
			
		||||
    "mitt": "^3.0.0",
 | 
			
		||||
    "mockjs": "^1.1.0",
 | 
			
		||||
    "nprogress": "^0.2.0",
 | 
			
		||||
    "path": "^0.12.7",
 | 
			
		||||
    "pinia": "^2.0.36",
 | 
			
		||||
    "qs": "^6.11.1",
 | 
			
		||||
    "pinia": "^2.1.3",
 | 
			
		||||
    "qs": "^6.11.2",
 | 
			
		||||
    "responsive-storage": "^2.2.0",
 | 
			
		||||
    "sortablejs": "^1.15.0",
 | 
			
		||||
    "vue": "^3.3.1",
 | 
			
		||||
    "vue-router": "^4.1.6",
 | 
			
		||||
    "vue-types": "^5.0.2"
 | 
			
		||||
    "vue": "^3.3.4",
 | 
			
		||||
    "vue-router": "^4.2.2",
 | 
			
		||||
    "vue-types": "^5.0.3"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@commitlint/cli": "^17.6.3",
 | 
			
		||||
    "@commitlint/config-conventional": "^17.6.3",
 | 
			
		||||
    "@commitlint/cli": "^17.6.5",
 | 
			
		||||
    "@commitlint/config-conventional": "^17.6.5",
 | 
			
		||||
    "@iconify-icons/ep": "^1.2.11",
 | 
			
		||||
    "@iconify-icons/ri": "^1.2.7",
 | 
			
		||||
    "@iconify-icons/ri": "^1.2.8",
 | 
			
		||||
    "@iconify/vue": "^4.1.1",
 | 
			
		||||
    "@pureadmin/theme": "^3.0.0",
 | 
			
		||||
    "@types/element-resize-detector": "1.1.3",
 | 
			
		||||
    "@types/js-cookie": "^3.0.3",
 | 
			
		||||
    "@types/mockjs": "^1.0.7",
 | 
			
		||||
    "@types/node": "^18.15.12",
 | 
			
		||||
    "@types/node": "^20.2.5",
 | 
			
		||||
    "@types/nprogress": "0.2.0",
 | 
			
		||||
    "@types/qs": "^6.9.7",
 | 
			
		||||
    "@types/sortablejs": "^1.15.1",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^5.59.5",
 | 
			
		||||
    "@typescript-eslint/parser": "^5.59.5",
 | 
			
		||||
    "@vitejs/plugin-vue": "^4.2.2",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^5.59.8",
 | 
			
		||||
    "@typescript-eslint/parser": "^5.59.8",
 | 
			
		||||
    "@vitejs/plugin-vue": "^4.2.3",
 | 
			
		||||
    "@vitejs/plugin-vue-jsx": "^3.0.1",
 | 
			
		||||
    "@vue/eslint-config-prettier": "^7.1.0",
 | 
			
		||||
    "@vue/eslint-config-typescript": "^11.0.3",
 | 
			
		||||
    "autoprefixer": "^10.4.14",
 | 
			
		||||
    "cloc": "^2.11.0",
 | 
			
		||||
    "cssnano": "^6.0.1",
 | 
			
		||||
    "eslint": "^8.40.0",
 | 
			
		||||
    "eslint": "^8.42.0",
 | 
			
		||||
    "eslint-plugin-prettier": "^4.2.1",
 | 
			
		||||
    "eslint-plugin-vue": "^9.12.0",
 | 
			
		||||
    "eslint-plugin-vue": "^9.14.1",
 | 
			
		||||
    "husky": "^8.0.3",
 | 
			
		||||
    "lint-staged": "^13.2.2",
 | 
			
		||||
    "picocolors": "^1.0.0",
 | 
			
		||||
    "postcss": "^8.4.23",
 | 
			
		||||
    "postcss": "^8.4.24",
 | 
			
		||||
    "postcss-html": "^1.5.0",
 | 
			
		||||
    "postcss-import": "^15.1.0",
 | 
			
		||||
    "postcss-scss": "^4.0.6",
 | 
			
		||||
    "prettier": "^2.8.7",
 | 
			
		||||
    "pretty-quick": "3.1.1",
 | 
			
		||||
    "rimraf": "^5.0.0",
 | 
			
		||||
    "prettier": "^2.8.8",
 | 
			
		||||
    "pretty-quick": "^3.1.3",
 | 
			
		||||
    "rimraf": "^5.0.1",
 | 
			
		||||
    "rollup-plugin-visualizer": "^5.9.0",
 | 
			
		||||
    "sass": "^1.62.1",
 | 
			
		||||
    "sass-loader": "^13.2.2",
 | 
			
		||||
    "stylelint": "^15.6.1",
 | 
			
		||||
    "sass-loader": "^13.3.1",
 | 
			
		||||
    "stylelint": "^15.6.3",
 | 
			
		||||
    "stylelint-config-html": "^1.1.0",
 | 
			
		||||
    "stylelint-config-recess-order": "^4.0.0",
 | 
			
		||||
    "stylelint-config-recommended": "^12.0.0",
 | 
			
		||||
@ -105,16 +105,16 @@
 | 
			
		||||
    "stylelint-scss": "^5.0.0",
 | 
			
		||||
    "svgo": "^3.0.2",
 | 
			
		||||
    "tailwindcss": "^3.3.2",
 | 
			
		||||
    "terser": "^5.17.1",
 | 
			
		||||
    "typescript": "^5.0.4",
 | 
			
		||||
    "vite": "^4.3.5",
 | 
			
		||||
    "terser": "^5.17.7",
 | 
			
		||||
    "typescript": "5.0.4",
 | 
			
		||||
    "vite": "^4.3.9",
 | 
			
		||||
    "vite-plugin-cdn-import": "^0.3.5",
 | 
			
		||||
    "vite-plugin-compression": "^0.5.1",
 | 
			
		||||
    "vite-plugin-mock": "^2.9.6",
 | 
			
		||||
    "vite-plugin-remove-console": "^2.1.1",
 | 
			
		||||
    "vite-svg-loader": "^4.0.0",
 | 
			
		||||
    "vue-eslint-parser": "^9.2.1",
 | 
			
		||||
    "vue-tsc": "^1.6.4"
 | 
			
		||||
    "vue-eslint-parser": "^9.3.0",
 | 
			
		||||
    "vue-tsc": "^1.6.5"
 | 
			
		||||
  },
 | 
			
		||||
  "pnpm": {
 | 
			
		||||
    "peerDependencyRules": {
 | 
			
		||||
@ -126,6 +126,7 @@
 | 
			
		||||
    },
 | 
			
		||||
    "allowedDeprecatedVersions": {
 | 
			
		||||
      "sourcemap-codec": "*",
 | 
			
		||||
      "w3c-hr-time": "*",
 | 
			
		||||
      "stable": "*"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2660
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						@ -1,5 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
  "Version": "4.1.0",
 | 
			
		||||
  "Version": "4.3.0",
 | 
			
		||||
  "Title": "PureAdmin",
 | 
			
		||||
  "FixedHeader": true,
 | 
			
		||||
  "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 addDialog = (options: DialogOptions) => {
 | 
			
		||||
  const open = () =>
 | 
			
		||||
    dialogStore.value.push(Object.assign(options, { visible: true }));
 | 
			
		||||
@ -24,16 +25,40 @@ const addDialog = (options: DialogOptions) => {
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** 关闭弹框 */
 | 
			
		||||
const closeDialog = (options: DialogOptions, index: number, args?: any) => {
 | 
			
		||||
  dialogStore.value.splice(index, 1);
 | 
			
		||||
  options.closeCallBack && options.closeCallBack({ options, index, args });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @description 更改弹框自身属性值
 | 
			
		||||
 * @param value 属性值
 | 
			
		||||
 * @param key 属性,默认`title`
 | 
			
		||||
 * @param index 弹框索引(默认`0`,代表只有一个弹框,对于嵌套弹框要改哪个弹框的属性值就把该弹框索引赋给`index`)
 | 
			
		||||
 */
 | 
			
		||||
const updateDialog = (value: any, key = "title", index = 0) => {
 | 
			
		||||
  dialogStore.value[index][key] = value;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** 关闭所有弹框 */
 | 
			
		||||
const closeAllDialog = () => {
 | 
			
		||||
  dialogStore.value = [];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** 千万别忘了在下面这三处引入并注册下,放心注册,不使用`addDialog`调用就不会被挂载
 | 
			
		||||
 * https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L4
 | 
			
		||||
 * https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L13
 | 
			
		||||
 * https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L18
 | 
			
		||||
 */
 | 
			
		||||
const ReDialog = withInstall(reDialog);
 | 
			
		||||
 | 
			
		||||
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };
 | 
			
		||||
export { ReDialog, dialogStore, addDialog, closeDialog, closeAllDialog };
 | 
			
		||||
export {
 | 
			
		||||
  ReDialog,
 | 
			
		||||
  dialogStore,
 | 
			
		||||
  addDialog,
 | 
			
		||||
  closeDialog,
 | 
			
		||||
  updateDialog,
 | 
			
		||||
  closeAllDialog
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,17 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from "vue";
 | 
			
		||||
import { isFunction } from "@pureadmin/utils";
 | 
			
		||||
import {
 | 
			
		||||
  type DialogOptions,
 | 
			
		||||
  type ButtonProps,
 | 
			
		||||
  type EventType,
 | 
			
		||||
  closeDialog,
 | 
			
		||||
  dialogStore,
 | 
			
		||||
  closeDialog
 | 
			
		||||
  type EventType,
 | 
			
		||||
  type ButtonProps,
 | 
			
		||||
  type DialogOptions
 | 
			
		||||
} from "./index";
 | 
			
		||||
import { ref, computed } from "vue";
 | 
			
		||||
import { isFunction } from "@pureadmin/utils";
 | 
			
		||||
import Fullscreen from "@iconify-icons/ri/fullscreen-fill";
 | 
			
		||||
import ExitFullscreen from "@iconify-icons/ri/fullscreen-exit-fill";
 | 
			
		||||
 | 
			
		||||
const fullscreen = ref(false);
 | 
			
		||||
 | 
			
		||||
const footerButtons = computed(() => {
 | 
			
		||||
  return (options: DialogOptions) => {
 | 
			
		||||
@ -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(
 | 
			
		||||
  event: EventType,
 | 
			
		||||
  options: DialogOptions,
 | 
			
		||||
  index: number
 | 
			
		||||
) {
 | 
			
		||||
  fullscreen.value = options?.fullscreen ?? false;
 | 
			
		||||
  if (options?.[event] && isFunction(options?.[event])) {
 | 
			
		||||
    return options?.[event]({ options, index });
 | 
			
		||||
  }
 | 
			
		||||
@ -69,25 +84,49 @@ function handleClose(
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <el-dialog
 | 
			
		||||
    class="pure-dialog"
 | 
			
		||||
    v-for="(options, index) in dialogStore"
 | 
			
		||||
    :key="index"
 | 
			
		||||
    v-bind="options"
 | 
			
		||||
    v-model="options.visible"
 | 
			
		||||
    @opened="eventsCallBack('open', options, index)"
 | 
			
		||||
    :fullscreen="fullscreen ? true : options?.fullscreen ? true : false"
 | 
			
		||||
    @close="handleClose(options, index)"
 | 
			
		||||
    @opened="eventsCallBack('open', options, index)"
 | 
			
		||||
    @openAutoFocus="eventsCallBack('openAutoFocus', options, index)"
 | 
			
		||||
    @closeAutoFocus="eventsCallBack('closeAutoFocus', options, index)"
 | 
			
		||||
  >
 | 
			
		||||
    <!-- header -->
 | 
			
		||||
    <template
 | 
			
		||||
      v-if="options?.headerRenderer"
 | 
			
		||||
      v-if="options?.fullscreenIcon || options?.headerRenderer"
 | 
			
		||||
      #header="{ close, titleId, titleClass }"
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        v-if="options?.fullscreenIcon"
 | 
			
		||||
        class="flex items-center justify-between"
 | 
			
		||||
      >
 | 
			
		||||
        <span :id="titleId" :class="titleClass">{{ options?.title }}</span>
 | 
			
		||||
        <i
 | 
			
		||||
          v-if="!options?.fullscreen"
 | 
			
		||||
          :class="fullscreenClass"
 | 
			
		||||
          @click="fullscreen = !fullscreen"
 | 
			
		||||
        >
 | 
			
		||||
          <IconifyIconOffline
 | 
			
		||||
            class="pure-dialog-svg"
 | 
			
		||||
            :icon="
 | 
			
		||||
              options?.fullscreen
 | 
			
		||||
                ? ExitFullscreen
 | 
			
		||||
                : fullscreen
 | 
			
		||||
                ? ExitFullscreen
 | 
			
		||||
                : Fullscreen
 | 
			
		||||
            "
 | 
			
		||||
          />
 | 
			
		||||
        </i>
 | 
			
		||||
      </div>
 | 
			
		||||
      <component
 | 
			
		||||
        v-else
 | 
			
		||||
        :is="options?.headerRenderer({ close, titleId, titleClass })"
 | 
			
		||||
      />
 | 
			
		||||
    </template>
 | 
			
		||||
    <!-- default -->
 | 
			
		||||
    <component
 | 
			
		||||
      v-bind="options?.props"
 | 
			
		||||
      :is="options.contentRenderer({ options, index })"
 | 
			
		||||
 | 
			
		||||
@ -15,8 +15,10 @@ type DialogProps = {
 | 
			
		||||
  title?: string;
 | 
			
		||||
  /** `Dialog` 的宽度,默认 `50%` */
 | 
			
		||||
  width?: string | number;
 | 
			
		||||
  /** 是否为全屏 `Dialog`,默认 `false` */
 | 
			
		||||
  /** 是否为全屏 `Dialog`(会一直处于全屏状态,除非弹框关闭),默认 `false`,`fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
 | 
			
		||||
  fullscreen?: boolean;
 | 
			
		||||
  /** 是否显示全屏操作图标,默认 `false`,`fullscreen` 和 `fullscreenIcon` 都传时只有 `fullscreen` 会生效 */
 | 
			
		||||
  fullscreenIcon?: boolean;
 | 
			
		||||
  /** `Dialog CSS` 中的 `margin-top` 值,默认 `15vh` */
 | 
			
		||||
  top?: string;
 | 
			
		||||
  /** 是否需要遮罩层,默认 `true` */
 | 
			
		||||
 | 
			
		||||
@ -200,9 +200,13 @@ export default defineComponent({
 | 
			
		||||
 | 
			
		||||
    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">
 | 
			
		||||
            <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">
 | 
			
		||||
              {slots?.buttons ? (
 | 
			
		||||
                <div class="flex mr-4">{slots.buttons()}</div>
 | 
			
		||||
@ -245,6 +249,7 @@ export default defineComponent({
 | 
			
		||||
 | 
			
		||||
              <el-popover
 | 
			
		||||
                v-slots={reference}
 | 
			
		||||
                placement="bottom-start"
 | 
			
		||||
                popper-style={{ padding: 0 }}
 | 
			
		||||
                width="160"
 | 
			
		||||
                trigger="click"
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,7 @@ const {
 | 
			
		||||
  onPanel,
 | 
			
		||||
  pureApp,
 | 
			
		||||
  username,
 | 
			
		||||
  userAvatar,
 | 
			
		||||
  avatarsStyle,
 | 
			
		||||
  toggleSideBar
 | 
			
		||||
} = useNav();
 | 
			
		||||
@ -46,10 +47,7 @@ const {
 | 
			
		||||
      <!-- 退出登录 -->
 | 
			
		||||
      <el-dropdown trigger="click">
 | 
			
		||||
        <span class="el-dropdown-link navbar-bg-hover select-none">
 | 
			
		||||
          <img
 | 
			
		||||
            src="https://avatars.githubusercontent.com/u/44761321?v=4"
 | 
			
		||||
            :style="avatarsStyle"
 | 
			
		||||
          />
 | 
			
		||||
          <img :src="userAvatar" :style="avatarsStyle" />
 | 
			
		||||
          <p v-if="username" class="dark:text-white">{{ username }}</p>
 | 
			
		||||
        </span>
 | 
			
		||||
        <template #dropdown>
 | 
			
		||||
 | 
			
		||||
@ -22,19 +22,31 @@ notices.value.map(v => (noticesNum.value += v.list.length));
 | 
			
		||||
    </span>
 | 
			
		||||
    <template #dropdown>
 | 
			
		||||
      <el-dropdown-menu>
 | 
			
		||||
        <el-tabs :stretch="true" v-model="activeKey" class="dropdown-tabs">
 | 
			
		||||
          <template v-for="item in notices" :key="item.key">
 | 
			
		||||
            <el-tab-pane
 | 
			
		||||
              :label="`${item.name}(${item.list.length})`"
 | 
			
		||||
              :name="`${item.key}`"
 | 
			
		||||
            >
 | 
			
		||||
              <el-scrollbar max-height="330px">
 | 
			
		||||
                <div class="noticeList-container">
 | 
			
		||||
                  <NoticeList :list="item.list" />
 | 
			
		||||
                </div>
 | 
			
		||||
              </el-scrollbar>
 | 
			
		||||
            </el-tab-pane>
 | 
			
		||||
          </template>
 | 
			
		||||
        <el-tabs
 | 
			
		||||
          :stretch="true"
 | 
			
		||||
          v-model="activeKey"
 | 
			
		||||
          class="dropdown-tabs"
 | 
			
		||||
          :style="{ width: notices.length === 0 ? '200px' : '330px' }"
 | 
			
		||||
        >
 | 
			
		||||
          <el-empty
 | 
			
		||||
            v-if="notices.length === 0"
 | 
			
		||||
            description="暂无消息"
 | 
			
		||||
            :image-size="60"
 | 
			
		||||
          />
 | 
			
		||||
          <span v-else>
 | 
			
		||||
            <template v-for="item in notices" :key="item.key">
 | 
			
		||||
              <el-tab-pane
 | 
			
		||||
                :label="`${item.name}(${item.list.length})`"
 | 
			
		||||
                :name="`${item.key}`"
 | 
			
		||||
              >
 | 
			
		||||
                <el-scrollbar max-height="330px">
 | 
			
		||||
                  <div class="noticeList-container">
 | 
			
		||||
                    <NoticeList :list="item.list" />
 | 
			
		||||
                  </div>
 | 
			
		||||
                </el-scrollbar>
 | 
			
		||||
              </el-tab-pane>
 | 
			
		||||
            </template>
 | 
			
		||||
          </span>
 | 
			
		||||
        </el-tabs>
 | 
			
		||||
      </el-dropdown-menu>
 | 
			
		||||
    </template>
 | 
			
		||||
@ -46,8 +58,9 @@ notices.value.map(v => (noticesNum.value += v.list.length));
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  width: 60px;
 | 
			
		||||
  width: 40px;
 | 
			
		||||
  height: 48px;
 | 
			
		||||
  margin-right: 10px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
 | 
			
		||||
  .header-notice-icon {
 | 
			
		||||
@ -56,8 +69,6 @@ notices.value.map(v => (noticesNum.value += v.list.length));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-tabs {
 | 
			
		||||
  width: 330px;
 | 
			
		||||
 | 
			
		||||
  .noticeList-container {
 | 
			
		||||
    padding: 15px 24px 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,17 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
 | 
			
		||||
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
 | 
			
		||||
import { useNav } from "@/layout/hooks/useNav";
 | 
			
		||||
import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
 | 
			
		||||
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{ total: number }>(), {
 | 
			
		||||
  total: 0
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { device } = useNav();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="search-footer text-[#333] dark:text-white">
 | 
			
		||||
    <span class="search-footer-item">
 | 
			
		||||
@ -13,16 +27,15 @@
 | 
			
		||||
      <mdiKeyboardEsc class="icon" />
 | 
			
		||||
      关闭
 | 
			
		||||
    </span>
 | 
			
		||||
    <p
 | 
			
		||||
      v-if="device !== 'mobile' && props.total > 0"
 | 
			
		||||
      class="search-footer-total"
 | 
			
		||||
    >
 | 
			
		||||
      共{{ props.total }}项
 | 
			
		||||
    </p>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import ArrowUpLine from "@iconify-icons/ri/arrow-up-line";
 | 
			
		||||
import ArrowDownLine from "@iconify-icons/ri/arrow-down-line";
 | 
			
		||||
import mdiKeyboardEsc from "@/assets/svg/keyboard_esc.svg?component";
 | 
			
		||||
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.search-footer {
 | 
			
		||||
  display: flex;
 | 
			
		||||
@ -40,5 +53,10 @@ import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
 | 
			
		||||
    box-shadow: inset 0 -2px #cdcde6, inset 0 0 1px 1px #fff,
 | 
			
		||||
      0 1px 2px 1px #1e235a66;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .search-footer-total {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    right: 20px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ import { useNav } from "@/layout/hooks/useNav";
 | 
			
		||||
import { ref, computed, shallowRef } from "vue";
 | 
			
		||||
import { useDebounceFn, onKeyStroke } from "@vueuse/core";
 | 
			
		||||
import { usePermissionStoreHook } from "@/store/modules/permission";
 | 
			
		||||
import Search from "@iconify-icons/ep/search";
 | 
			
		||||
import Search from "@iconify-icons/ri/search-line";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  /** 弹窗显隐 */
 | 
			
		||||
@ -24,6 +24,8 @@ const props = withDefaults(defineProps<Props>(), {});
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
 | 
			
		||||
const keyword = ref("");
 | 
			
		||||
const scrollbarRef = ref();
 | 
			
		||||
const resultRef = ref();
 | 
			
		||||
const activePath = ref("");
 | 
			
		||||
const inputRef = ref<HTMLInputElement | null>(null);
 | 
			
		||||
const resultOptions = shallowRef([]);
 | 
			
		||||
@ -82,6 +84,11 @@ function handleClose() {
 | 
			
		||||
  }, 200);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function scrollTo(index) {
 | 
			
		||||
  const scrollTop = resultRef.value.handleScroll(index);
 | 
			
		||||
  scrollbarRef.value.setScrollTop(scrollTop);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** key up */
 | 
			
		||||
function handleUp() {
 | 
			
		||||
  const { length } = resultOptions.value;
 | 
			
		||||
@ -91,8 +98,10 @@ function handleUp() {
 | 
			
		||||
  );
 | 
			
		||||
  if (index === 0) {
 | 
			
		||||
    activePath.value = resultOptions.value[length - 1].path;
 | 
			
		||||
    scrollTo(resultOptions.value.length - 1);
 | 
			
		||||
  } else {
 | 
			
		||||
    activePath.value = resultOptions.value[index - 1].path;
 | 
			
		||||
    scrollTo(index - 1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -108,6 +117,7 @@ function handleDown() {
 | 
			
		||||
  } else {
 | 
			
		||||
    activePath.value = resultOptions.value[index + 1].path;
 | 
			
		||||
  }
 | 
			
		||||
  scrollTo(index + 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** key enter */
 | 
			
		||||
@ -126,41 +136,56 @@ onKeyStroke("ArrowDown", handleDown);
 | 
			
		||||
<template>
 | 
			
		||||
  <el-dialog
 | 
			
		||||
    top="5vh"
 | 
			
		||||
    class="pure-search-dialog"
 | 
			
		||||
    v-model="show"
 | 
			
		||||
    :width="device === 'mobile' ? '80vw' : '50vw'"
 | 
			
		||||
    :show-close="false"
 | 
			
		||||
    :width="device === 'mobile' ? '80vw' : '40vw'"
 | 
			
		||||
    :before-close="handleClose"
 | 
			
		||||
    :style="{
 | 
			
		||||
      borderRadius: '6px'
 | 
			
		||||
    }"
 | 
			
		||||
    append-to-body
 | 
			
		||||
    @opened="inputRef.focus()"
 | 
			
		||||
    @closed="inputRef.blur()"
 | 
			
		||||
  >
 | 
			
		||||
    <el-input
 | 
			
		||||
      ref="inputRef"
 | 
			
		||||
      size="large"
 | 
			
		||||
      v-model="keyword"
 | 
			
		||||
      clearable
 | 
			
		||||
      placeholder="请输入关键词搜索"
 | 
			
		||||
      placeholder="搜索菜单"
 | 
			
		||||
      @input="handleSearch"
 | 
			
		||||
    >
 | 
			
		||||
      <template #prefix>
 | 
			
		||||
        <span class="el-input__icon">
 | 
			
		||||
          <IconifyIconOffline :icon="Search" />
 | 
			
		||||
        </span>
 | 
			
		||||
        <IconifyIconOffline
 | 
			
		||||
          :icon="Search"
 | 
			
		||||
          class="text-primary w-[24px] h-[24px]"
 | 
			
		||||
        />
 | 
			
		||||
      </template>
 | 
			
		||||
    </el-input>
 | 
			
		||||
    <div class="search-result-container">
 | 
			
		||||
      <el-empty v-if="resultOptions.length === 0" description="暂无搜索结果" />
 | 
			
		||||
      <SearchResult
 | 
			
		||||
        v-else
 | 
			
		||||
        v-model:value="activePath"
 | 
			
		||||
        :options="resultOptions"
 | 
			
		||||
        @click="handleEnter"
 | 
			
		||||
      />
 | 
			
		||||
      <el-scrollbar ref="scrollbarRef" max-height="calc(90vh - 140px)">
 | 
			
		||||
        <el-empty
 | 
			
		||||
          v-if="resultOptions.length === 0"
 | 
			
		||||
          description="暂无搜索结果"
 | 
			
		||||
        />
 | 
			
		||||
        <SearchResult
 | 
			
		||||
          v-else
 | 
			
		||||
          ref="resultRef"
 | 
			
		||||
          v-model:value="activePath"
 | 
			
		||||
          :options="resultOptions"
 | 
			
		||||
          @click="handleEnter"
 | 
			
		||||
        />
 | 
			
		||||
      </el-scrollbar>
 | 
			
		||||
    </div>
 | 
			
		||||
    <template #footer>
 | 
			
		||||
      <SearchFooter />
 | 
			
		||||
      <SearchFooter :total="resultOptions.length" />
 | 
			
		||||
    </template>
 | 
			
		||||
  </el-dialog>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.search-result-container {
 | 
			
		||||
  margin-top: 20px;
 | 
			
		||||
  margin-top: 12px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,8 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from "vue";
 | 
			
		||||
import { useResizeObserver } from "@vueuse/core";
 | 
			
		||||
import { useEpThemeStoreHook } from "@/store/modules/epTheme";
 | 
			
		||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
 | 
			
		||||
import { ref, computed, getCurrentInstance, onMounted } from "vue";
 | 
			
		||||
import enterOutlined from "@/assets/svg/enter_outlined.svg?component";
 | 
			
		||||
import Bookmark2Line from "@iconify-icons/ri/bookmark-2-line";
 | 
			
		||||
 | 
			
		||||
@ -23,8 +24,11 @@ interface Emits {
 | 
			
		||||
  (e: "enter"): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const resultRef = ref();
 | 
			
		||||
const innerHeight = ref();
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {});
 | 
			
		||||
const emit = defineEmits<Emits>();
 | 
			
		||||
const instance = getCurrentInstance()!;
 | 
			
		||||
 | 
			
		||||
const itemStyle = computed(() => {
 | 
			
		||||
  return item => {
 | 
			
		||||
@ -54,22 +58,46 @@ async function handleMouse(item) {
 | 
			
		||||
function handleTo() {
 | 
			
		||||
  emit("enter");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function resizeResult() {
 | 
			
		||||
  // el-scrollbar max-height="calc(90vh - 140px)"
 | 
			
		||||
  innerHeight.value = window.innerHeight - window.innerHeight / 10 - 140;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
useResizeObserver(resultRef, () => {
 | 
			
		||||
  resizeResult();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function handleScroll(index: number) {
 | 
			
		||||
  const curInstance = instance?.proxy?.$refs[`resultItemRef${index}`];
 | 
			
		||||
  if (!curInstance) return 0;
 | 
			
		||||
  const curRef = curInstance[0] as ElRef;
 | 
			
		||||
  const scrollTop = curRef.offsetTop + 128; // 128 两个result-item(56px+56px=112px)高度加上下margin(8px+8px=16px)
 | 
			
		||||
  return scrollTop > innerHeight.value ? scrollTop - innerHeight.value : 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  resizeResult();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
defineExpose({ handleScroll });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="result">
 | 
			
		||||
    <template v-for="item in options" :key="item.path">
 | 
			
		||||
      <div
 | 
			
		||||
        class="result-item dark:bg-[#1d1d1d]"
 | 
			
		||||
        :style="itemStyle(item)"
 | 
			
		||||
        @click="handleTo"
 | 
			
		||||
        @mouseenter="handleMouse(item)"
 | 
			
		||||
      >
 | 
			
		||||
        <component :is="useRenderIcon(item.meta?.icon ?? Bookmark2Line)" />
 | 
			
		||||
        <span class="result-item-title">{{ item.meta?.title }}</span>
 | 
			
		||||
        <enterOutlined />
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
  <div ref="resultRef" class="result">
 | 
			
		||||
    <div
 | 
			
		||||
      v-for="(item, index) in options"
 | 
			
		||||
      :key="item.path"
 | 
			
		||||
      :ref="'resultItemRef' + index"
 | 
			
		||||
      class="result-item dark:bg-[#1d1d1d]"
 | 
			
		||||
      :style="itemStyle(item)"
 | 
			
		||||
      @click="handleTo"
 | 
			
		||||
      @mouseenter="handleMouse(item)"
 | 
			
		||||
    >
 | 
			
		||||
      <component :is="useRenderIcon(item.meta?.icon ?? Bookmark2Line)" />
 | 
			
		||||
      <span class="result-item-title">{{ item.meta?.title }}</span>
 | 
			
		||||
      <enterOutlined />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -218,7 +218,6 @@ watch($storage, ({ layout }) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onBeforeMount(() => {
 | 
			
		||||
  dataThemeChange();
 | 
			
		||||
  /* 初始化项目配置 */
 | 
			
		||||
  nextTick(() => {
 | 
			
		||||
    settings.greyVal &&
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@ const {
 | 
			
		||||
  onPanel,
 | 
			
		||||
  menuSelect,
 | 
			
		||||
  username,
 | 
			
		||||
  userAvatar,
 | 
			
		||||
  avatarsStyle
 | 
			
		||||
} = useNav();
 | 
			
		||||
 | 
			
		||||
@ -66,10 +67,7 @@ watch(
 | 
			
		||||
      <!-- 退出登录 -->
 | 
			
		||||
      <el-dropdown trigger="click">
 | 
			
		||||
        <span class="el-dropdown-link navbar-bg-hover">
 | 
			
		||||
          <img
 | 
			
		||||
            src="https://avatars.githubusercontent.com/u/44761321?v=4"
 | 
			
		||||
            :style="avatarsStyle"
 | 
			
		||||
          />
 | 
			
		||||
          <img :src="userAvatar" :style="avatarsStyle" />
 | 
			
		||||
          <p v-if="username" class="dark:text-white">{{ username }}</p>
 | 
			
		||||
        </span>
 | 
			
		||||
        <template #dropdown>
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,6 @@ const props = defineProps({
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const { title } = useNav();
 | 
			
		||||
const topPath = getTopMenu().path;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
@ -18,7 +17,7 @@ const topPath = getTopMenu().path;
 | 
			
		||||
        key="props.collapse"
 | 
			
		||||
        :title="title"
 | 
			
		||||
        class="sidebar-logo-link"
 | 
			
		||||
        :to="topPath"
 | 
			
		||||
        :to="getTopMenu()?.path ?? '/'"
 | 
			
		||||
      >
 | 
			
		||||
        <img src="/logo.svg" alt="logo" />
 | 
			
		||||
        <span class="sidebar-title">{{ title }}</span>
 | 
			
		||||
@ -28,7 +27,7 @@ const topPath = getTopMenu().path;
 | 
			
		||||
        key="expand"
 | 
			
		||||
        :title="title"
 | 
			
		||||
        class="sidebar-logo-link"
 | 
			
		||||
        :to="topPath"
 | 
			
		||||
        :to="getTopMenu()?.path ?? '/'"
 | 
			
		||||
      >
 | 
			
		||||
        <img src="/logo.svg" alt="logo" />
 | 
			
		||||
        <span class="sidebar-title">{{ title }}</span>
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@ const {
 | 
			
		||||
  menuSelect,
 | 
			
		||||
  resolvePath,
 | 
			
		||||
  username,
 | 
			
		||||
  userAvatar,
 | 
			
		||||
  getDivStyle,
 | 
			
		||||
  avatarsStyle
 | 
			
		||||
} = useNav();
 | 
			
		||||
@ -97,10 +98,7 @@ watch(
 | 
			
		||||
      <!-- 退出登录 -->
 | 
			
		||||
      <el-dropdown trigger="click">
 | 
			
		||||
        <span class="el-dropdown-link navbar-bg-hover select-none">
 | 
			
		||||
          <img
 | 
			
		||||
            src="https://avatars.githubusercontent.com/u/44761321?v=4"
 | 
			
		||||
            :style="avatarsStyle"
 | 
			
		||||
          />
 | 
			
		||||
          <img :src="userAvatar" :style="avatarsStyle" />
 | 
			
		||||
          <p v-if="username" class="dark:text-white">{{ username }}</p>
 | 
			
		||||
        </span>
 | 
			
		||||
        <template #dropdown>
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,7 @@ const tabDom = ref();
 | 
			
		||||
const containerDom = ref();
 | 
			
		||||
const scrollbarDom = ref();
 | 
			
		||||
const isShowArrow = ref(false);
 | 
			
		||||
const topPath = getTopMenu().path;
 | 
			
		||||
const topPath = getTopMenu()?.path;
 | 
			
		||||
const { VITE_HIDE_HOME } = import.meta.env;
 | 
			
		||||
const { isFullscreen, toggle } = useFullscreen();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import { storeToRefs } from "pinia";
 | 
			
		||||
import { getConfig } from "@/config";
 | 
			
		||||
import { emitter } from "@/utils/mitt";
 | 
			
		||||
import { routeMetaType } from "../types";
 | 
			
		||||
import userAvatar from "@/assets/user.jpg";
 | 
			
		||||
import { getTopMenu } from "@/router/utils";
 | 
			
		||||
import { useGlobal } from "@pureadmin/utils";
 | 
			
		||||
import { computed, CSSProperties } from "vue";
 | 
			
		||||
@ -70,7 +71,7 @@ export function useNav() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function backTopMenu() {
 | 
			
		||||
    router.push(getTopMenu().path);
 | 
			
		||||
    router.push(getTopMenu()?.path);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function onPanel() {
 | 
			
		||||
@ -150,6 +151,7 @@ export function useNav() {
 | 
			
		||||
    isCollapse,
 | 
			
		||||
    pureApp,
 | 
			
		||||
    username,
 | 
			
		||||
    userAvatar,
 | 
			
		||||
    avatarsStyle,
 | 
			
		||||
    tooltipEffect
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,15 @@ import { useLayout } from "./hooks/useLayout";
 | 
			
		||||
import { useAppStoreHook } from "@/store/modules/app";
 | 
			
		||||
import { useSettingStoreHook } from "@/store/modules/settings";
 | 
			
		||||
import { deviceDetection, useDark, useGlobal } from "@pureadmin/utils";
 | 
			
		||||
import { h, reactive, computed, onMounted, defineComponent } from "vue";
 | 
			
		||||
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
 | 
			
		||||
import {
 | 
			
		||||
  h,
 | 
			
		||||
  reactive,
 | 
			
		||||
  computed,
 | 
			
		||||
  onMounted,
 | 
			
		||||
  onBeforeMount,
 | 
			
		||||
  defineComponent
 | 
			
		||||
} from "vue";
 | 
			
		||||
 | 
			
		||||
import navbar from "./components/navbar.vue";
 | 
			
		||||
import tag from "./components/tag/index.vue";
 | 
			
		||||
@ -88,11 +96,12 @@ emitter.on("resize", ({ detail }) => {
 | 
			
		||||
      toggle("desktop", false);
 | 
			
		||||
      isAutoCloseSidebar = false;
 | 
			
		||||
    }
 | 
			
		||||
  } else if (width > 990) {
 | 
			
		||||
    if (!set.sidebar.isClickCollapse) {
 | 
			
		||||
      toggle("desktop", true);
 | 
			
		||||
      isAutoCloseSidebar = true;
 | 
			
		||||
    }
 | 
			
		||||
  } else if (width > 990 && !set.sidebar.isClickCollapse) {
 | 
			
		||||
    toggle("desktop", true);
 | 
			
		||||
    isAutoCloseSidebar = true;
 | 
			
		||||
  } else {
 | 
			
		||||
    toggle("desktop", false);
 | 
			
		||||
    isAutoCloseSidebar = false;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -102,6 +111,10 @@ onMounted(() => {
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onBeforeMount(() => {
 | 
			
		||||
  useDataThemeChange().dataThemeChange();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const layoutHeader = defineComponent({
 | 
			
		||||
  render() {
 | 
			
		||||
    return h(
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,7 @@ import {
 | 
			
		||||
  formatFlatteningRoutes
 | 
			
		||||
} from "./utils";
 | 
			
		||||
import { buildHierarchyTree } from "@/utils/tree";
 | 
			
		||||
import { isUrl, openLink, storageSession } from "@pureadmin/utils";
 | 
			
		||||
import { isUrl, openLink, storageSession, isAllEmpty } from "@pureadmin/utils";
 | 
			
		||||
 | 
			
		||||
import remainingRouter from "./modules/remaining";
 | 
			
		||||
 | 
			
		||||
@ -46,13 +46,13 @@ Object.keys(modules).forEach(key => {
 | 
			
		||||
 | 
			
		||||
/** 导出处理后的静态路由(三级及以上的路由全部拍成二级) */
 | 
			
		||||
export const constantRoutes: Array<RouteRecordRaw> = formatTwoStageRoutes(
 | 
			
		||||
  formatFlatteningRoutes(buildHierarchyTree(ascending(routes)))
 | 
			
		||||
  formatFlatteningRoutes(buildHierarchyTree(ascending(routes.flat(Infinity))))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
/** 用于渲染菜单,保持原始层级 */
 | 
			
		||||
export const constantMenus: Array<RouteComponent> = ascending(routes).concat(
 | 
			
		||||
  ...remainingRouter
 | 
			
		||||
);
 | 
			
		||||
export const constantMenus: Array<RouteComponent> = ascending(
 | 
			
		||||
  routes.flat(Infinity)
 | 
			
		||||
).concat(...remainingRouter);
 | 
			
		||||
 | 
			
		||||
/** 不参与菜单的路由 */
 | 
			
		||||
export const remainingPaths = Object.keys(remainingRouter).map(v => {
 | 
			
		||||
@ -86,7 +86,9 @@ export function resetRouter() {
 | 
			
		||||
    if (name && router.hasRoute(name) && meta?.backstage) {
 | 
			
		||||
      router.removeRoute(name);
 | 
			
		||||
      router.options.routes = formatTwoStageRoutes(
 | 
			
		||||
        formatFlatteningRoutes(buildHierarchyTree(ascending(routes)))
 | 
			
		||||
        formatFlatteningRoutes(
 | 
			
		||||
          buildHierarchyTree(ascending(routes.flat(Infinity)))
 | 
			
		||||
        )
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
@ -154,11 +156,22 @@ router.beforeEach((to: toRouteType, _from, next) => {
 | 
			
		||||
            getTopMenu(true);
 | 
			
		||||
            // query、params模式路由传参数的标签页不在此处处理
 | 
			
		||||
            if (route && route.meta?.title) {
 | 
			
		||||
              useMultiTagsStoreHook().handleTags("push", {
 | 
			
		||||
                path: route.path,
 | 
			
		||||
                name: route.name,
 | 
			
		||||
                meta: route.meta
 | 
			
		||||
              });
 | 
			
		||||
              if (isAllEmpty(route.parentId) && route.meta?.backstage) {
 | 
			
		||||
                // 此处为动态顶级路由(目录)
 | 
			
		||||
                const { path, name, meta } = route.children[0];
 | 
			
		||||
                useMultiTagsStoreHook().handleTags("push", {
 | 
			
		||||
                  path,
 | 
			
		||||
                  name,
 | 
			
		||||
                  meta
 | 
			
		||||
                });
 | 
			
		||||
              } else {
 | 
			
		||||
                const { path, name, meta } = route;
 | 
			
		||||
                useMultiTagsStoreHook().handleTags("push", {
 | 
			
		||||
                  path,
 | 
			
		||||
                  name,
 | 
			
		||||
                  meta
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          router.push(to.fullPath);
 | 
			
		||||
 | 
			
		||||
@ -79,6 +79,10 @@ html.dark {
 | 
			
		||||
      &:hover {
 | 
			
		||||
        color: rgb(255 255 255 / 85%) !important;
 | 
			
		||||
        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组件右上角关闭图标的样式,表现更鲜明 */
 | 
			
		||||
.el-dialog__headerbtn,
 | 
			
		||||
.el-message-box__headerbtn {
 | 
			
		||||
@ -94,6 +107,10 @@
 | 
			
		||||
      color: rgb(0 0 0 / 88%) !important;
 | 
			
		||||
      text-decoration: none;
 | 
			
		||||
      background-color: rgb(0 0 0 / 6%);
 | 
			
		||||
 | 
			
		||||
      .pure-dialog-svg {
 | 
			
		||||
        color: rgb(0 0 0 / 88%) !important;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -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> => {
 | 
			
		||||
        // 开启进度条动画
 | 
			
		||||
        NProgress.start();
 | 
			
		||||
        // 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
 | 
			
		||||
        // 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
 | 
			
		||||
        if (typeof config.beforeRequestCallback === "function") {
 | 
			
		||||
          config.beforeRequestCallback(config);
 | 
			
		||||
          return config;
 | 
			
		||||
@ -123,7 +123,7 @@ class PureHttp {
 | 
			
		||||
        const $config = response.config;
 | 
			
		||||
        // 关闭进度条动画
 | 
			
		||||
        NProgress.done();
 | 
			
		||||
        // 优先判断post/get等方法是否传入回掉,否则执行初始化设置等回掉
 | 
			
		||||
        // 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
 | 
			
		||||
        if (typeof $config.beforeResponseCallback === "function") {
 | 
			
		||||
          $config.beforeResponseCallback(response);
 | 
			
		||||
          return response.data;
 | 
			
		||||
@ -159,7 +159,7 @@ class PureHttp {
 | 
			
		||||
      ...axiosConfig
 | 
			
		||||
    } as PureHttpRequestConfig;
 | 
			
		||||
 | 
			
		||||
    // 单独处理自定义请求/响应回掉
 | 
			
		||||
    // 单独处理自定义请求/响应回调
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      PureHttp.axiosInstance
 | 
			
		||||
        .request(config)
 | 
			
		||||
 | 
			
		||||
@ -28,8 +28,7 @@
 | 
			
		||||
      "element-plus/global",
 | 
			
		||||
      "@pureadmin/table/volar",
 | 
			
		||||
      "@pureadmin/descriptions/volar"
 | 
			
		||||
    ],
 | 
			
		||||
    "typeRoots": ["./types", "./node_modules/@types/"]
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "include": [
 | 
			
		||||
    "mock/*.ts",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								types/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						@ -41,6 +41,10 @@ type DeepPartial<T> = {
 | 
			
		||||
  [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 IntervalHandle = ReturnType<typeof setInterval>;
 | 
			
		||||
 | 
			
		||||