Ver Fonte

项目搭建

qianduan há 7 meses atrás
commit
64ad4b29fa
100 ficheiros alterados com 7876 adições e 0 exclusões
  1. 15 0
      .eslintrc.cjs
  2. 30 0
      .gitignore
  3. 8 0
      .prettierrc.json
  4. 7 0
      .vscode/extensions.json
  5. 39 0
      README.md
  6. 1 0
      env.d.ts
  7. 13 0
      index.html
  8. 53 0
      package.json
  9. 3723 0
      pnpm-lock.yaml
  10. BIN
      public/favicon.ico
  11. 17 0
      src/App.vue
  12. 36 0
      src/api/hook-demo/use-fetch-select.ts
  13. 26 0
      src/api/hook-demo/use-fullscreen-loading.ts
  14. 27 0
      src/api/login/index.ts
  15. 14 0
      src/api/login/types/login.ts
  16. 37 0
      src/api/table/index.ts
  17. 31 0
      src/api/table/types/table.ts
  18. 0 0
      src/assets/error-page/403.svg
  19. 0 0
      src/assets/error-page/404.svg
  20. BIN
      src/assets/layouts/logo-text-1.png
  21. BIN
      src/assets/layouts/logo-text-2.png
  22. BIN
      src/assets/layouts/logo.png
  23. BIN
      src/assets/login/close-eyes.png
  24. BIN
      src/assets/login/face.png
  25. BIN
      src/assets/login/hand-down-left.png
  26. BIN
      src/assets/login/hand-down-right.png
  27. BIN
      src/assets/login/hand-up-left.png
  28. BIN
      src/assets/login/hand-up-right.png
  29. 58 0
      src/components/Notify/NotifyList.vue
  30. 66 0
      src/components/Notify/data.ts
  31. 95 0
      src/components/Notify/index.vue
  32. 103 0
      src/components/Screenfull/index.vue
  33. 54 0
      src/components/SearchMenu/SearchFooter.vue
  34. 202 0
      src/components/SearchMenu/SearchModal.vue
  35. 110 0
      src/components/SearchMenu/SearchResult.vue
  36. 29 0
      src/components/SearchMenu/index.vue
  37. 29 0
      src/components/SvgIcon/index.vue
  38. 50 0
      src/components/ThemeSwitch/index.vue
  39. 55 0
      src/config/layouts.ts
  40. 28 0
      src/config/route.ts
  41. 15 0
      src/config/white-list.ts
  42. 20 0
      src/constants/app-key.ts
  43. 13 0
      src/constants/cache-key.ts
  44. 7 0
      src/directives/index.ts
  45. 17 0
      src/directives/permission/index.ts
  46. 11 0
      src/hooks/useDevice.ts
  47. 49 0
      src/hooks/useFetchSelect.ts
  48. 35 0
      src/hooks/useFullscreenLoading.ts
  49. 16 0
      src/hooks/useLayoutMode.ts
  50. 41 0
      src/hooks/usePagination.ts
  51. 48 0
      src/hooks/useRouteListener.ts
  52. 57 0
      src/hooks/useTheme.ts
  53. 23 0
      src/hooks/useTitle.ts
  54. 236 0
      src/hooks/useWatermark.ts
  55. 7 0
      src/icons/index.ts
  56. 1 0
      src/icons/svg/404.svg
  57. 1 0
      src/icons/svg/bug.svg
  58. 1 0
      src/icons/svg/component.svg
  59. 1 0
      src/icons/svg/dashboard.svg
  60. 1 0
      src/icons/svg/fullscreen-exit.svg
  61. 1 0
      src/icons/svg/fullscreen.svg
  62. 1 0
      src/icons/svg/keyboard-down.svg
  63. 1 0
      src/icons/svg/keyboard-enter.svg
  64. 1 0
      src/icons/svg/keyboard-esc.svg
  65. 1 0
      src/icons/svg/keyboard-up.svg
  66. 1 0
      src/icons/svg/link.svg
  67. 1 0
      src/icons/svg/lock.svg
  68. 1 0
      src/icons/svg/menu.svg
  69. 1 0
      src/icons/svg/search.svg
  70. 5 0
      src/icons/svg/unocss.svg
  71. 170 0
      src/layouts/LeftMode.vue
  72. 111 0
      src/layouts/LeftTopMode.vue
  73. 75 0
      src/layouts/TopMode.vue
  74. 49 0
      src/layouts/components/AppMain.vue
  75. 65 0
      src/layouts/components/Breadcrumb/index.vue
  76. 67 0
      src/layouts/components/CompConsumer/index.ts
  77. 18 0
      src/layouts/components/Footer/index.vue
  78. 36 0
      src/layouts/components/Hamburger/index.vue
  79. 83 0
      src/layouts/components/Logo/index.vue
  80. 133 0
      src/layouts/components/NavigationBar/index.vue
  81. 45 0
      src/layouts/components/RightPanel/index.vue
  82. 103 0
      src/layouts/components/Settings/SelectLayoutMode.vue
  83. 86 0
      src/layouts/components/Settings/index.vue
  84. 97 0
      src/layouts/components/Sidebar/SidebarItem.vue
  85. 18 0
      src/layouts/components/Sidebar/SidebarItemLink.vue
  86. 156 0
      src/layouts/components/Sidebar/index.vue
  87. 155 0
      src/layouts/components/TagsView/ScrollPane.vue
  88. 257 0
      src/layouts/components/TagsView/index.vue
  89. 7 0
      src/layouts/components/index.ts
  90. 52 0
      src/layouts/hooks/useResize.ts
  91. 70 0
      src/layouts/index.vue
  92. 32 0
      src/main.ts
  93. 9 0
      src/plugins/element-plus-icon/index.ts
  94. 7 0
      src/plugins/element-plus/index.ts
  95. 10 0
      src/plugins/index.ts
  96. 66 0
      src/plugins/vxe-table/index.ts
  97. 69 0
      src/router/helper.ts
  98. 193 0
      src/router/index.ts
  99. 62 0
      src/router/permission.ts
  100. 5 0
      src/store/index.ts

+ 15 - 0
.eslintrc.cjs

@@ -0,0 +1,15 @@
+/* eslint-env node */
+require('@rushstack/eslint-patch/modern-module-resolution')
+
+module.exports = {
+  root: true,
+  'extends': [
+    'plugin:vue/vue3-essential',
+    'eslint:recommended',
+    '@vue/eslint-config-typescript',
+    '@vue/eslint-config-prettier/skip-formatting'
+  ],
+  parserOptions: {
+    ecmaVersion: 'latest'
+  }
+}

+ 30 - 0
.gitignore

@@ -0,0 +1,30 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*.tsbuildinfo

+ 8 - 0
.prettierrc.json

@@ -0,0 +1,8 @@
+{
+  "$schema": "https://json.schemastore.org/prettierrc",
+  "semi": false,
+  "tabWidth": 2,
+  "singleQuote": true,
+  "printWidth": 100,
+  "trailingComma": "none"
+}

+ 7 - 0
.vscode/extensions.json

@@ -0,0 +1,7 @@
+{
+  "recommendations": [
+    "Vue.volar",
+    "dbaeumer.vscode-eslint",
+    "esbenp.prettier-vscode"
+  ]
+}

+ 39 - 0
README.md

@@ -0,0 +1,39 @@
+# baozhida-panel-module
+
+This template should help get you started developing with Vue 3 in Vite.
+
+## Recommended IDE Setup
+
+[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
+
+## Type Support for `.vue` Imports in TS
+
+TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vitejs.dev/config/).
+
+## Project Setup
+
+```sh
+pnpm install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+pnpm dev
+```
+
+### Type-Check, Compile and Minify for Production
+
+```sh
+pnpm build
+```
+
+### Lint with [ESLint](https://eslint.org/)
+
+```sh
+pnpm lint
+```

+ 1 - 0
env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <link rel="icon" href="/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Vite App</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 53 - 0
package.json

@@ -0,0 +1,53 @@
+{
+  "name": "baozhida-panel-module",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "run-p type-check \"build-only {@}\" --",
+    "preview": "vite preview",
+    "build-only": "vite build",
+    "type-check": "vue-tsc --build --force",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
+    "format": "prettier --write src/"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.3.1",
+    "axios": "^1.7.2",
+    "dayjs": "^1.11.12",
+    "element-plus": "^2.7.8",
+    "js-cookie": "^3.0.5",
+    "lodash-es": "^4.17.21",
+    "mitt": "^3.0.1",
+    "normalize.css": "^8.0.1",
+    "nprogress": "^0.2.0",
+    "path-to-regexp": "^7.1.0",
+    "pinia": "^2.1.7",
+    "sass": "^1.77.8",
+    "screenfull": "^6.0.2",
+    "vue": "^3.4.29",
+    "vue-router": "^4.3.3",
+    "vxe-table": "^4.6.18",
+    "vxe-table-plugin-element": "4.0.4",
+    "xe-utils": "3.5.28"
+  },
+  "devDependencies": {
+    "@rushstack/eslint-patch": "^1.8.0",
+    "@tsconfig/node20": "^20.1.4",
+    "@types/node": "^20.14.5",
+    "@vitejs/plugin-vue": "^5.0.5",
+    "@vue/eslint-config-prettier": "^9.0.0",
+    "@vue/eslint-config-typescript": "^13.0.0",
+    "@vue/tsconfig": "^0.5.1",
+    "eslint": "^8.57.0",
+    "eslint-plugin-vue": "^9.23.0",
+    "npm-run-all2": "^6.2.0",
+    "path-browserify": "^1.0.1",
+    "prettier": "^3.2.5",
+    "typescript": "~5.4.0",
+    "unocss": "^0.61.9",
+    "vite": "^5.3.1",
+    "vue-tsc": "^2.0.21"
+  }
+}

+ 3723 - 0
pnpm-lock.yaml

@@ -0,0 +1,3723 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    dependencies:
+      '@element-plus/icons-vue':
+        specifier: ^2.3.1
+        version: 2.3.1(vue@3.4.35(typescript@5.4.5))
+      axios:
+        specifier: ^1.7.2
+        version: 1.7.2
+      dayjs:
+        specifier: ^1.11.12
+        version: 1.11.12
+      element-plus:
+        specifier: ^2.7.8
+        version: 2.7.8(vue@3.4.35(typescript@5.4.5))
+      js-cookie:
+        specifier: ^3.0.5
+        version: 3.0.5
+      lodash-es:
+        specifier: ^4.17.21
+        version: 4.17.21
+      mitt:
+        specifier: ^3.0.1
+        version: 3.0.1
+      normalize.css:
+        specifier: ^8.0.1
+        version: 8.0.1
+      nprogress:
+        specifier: ^0.2.0
+        version: 0.2.0
+      path-to-regexp:
+        specifier: ^7.1.0
+        version: 7.1.0
+      pinia:
+        specifier: ^2.1.7
+        version: 2.2.0(typescript@5.4.5)(vue@3.4.35(typescript@5.4.5))
+      sass:
+        specifier: ^1.77.8
+        version: 1.77.8
+      screenfull:
+        specifier: ^6.0.2
+        version: 6.0.2
+      vue:
+        specifier: ^3.4.29
+        version: 3.4.35(typescript@5.4.5)
+      vue-router:
+        specifier: ^4.3.3
+        version: 4.4.1(vue@3.4.35(typescript@5.4.5))
+      vxe-table:
+        specifier: ^4.6.18
+        version: 4.6.18(vue@3.4.35(typescript@5.4.5))
+      vxe-table-plugin-element:
+        specifier: 4.0.4
+        version: 4.0.4(vxe-table@4.6.18(vue@3.4.35(typescript@5.4.5)))
+      xe-utils:
+        specifier: 3.5.28
+        version: 3.5.28
+    devDependencies:
+      '@rushstack/eslint-patch':
+        specifier: ^1.8.0
+        version: 1.10.4
+      '@tsconfig/node20':
+        specifier: ^20.1.4
+        version: 20.1.4
+      '@types/node':
+        specifier: ^20.14.5
+        version: 20.14.13
+      '@vitejs/plugin-vue':
+        specifier: ^5.0.5
+        version: 5.1.1(vite@5.3.5(@types/node@20.14.13)(sass@1.77.8))(vue@3.4.35(typescript@5.4.5))
+      '@vue/eslint-config-prettier':
+        specifier: ^9.0.0
+        version: 9.0.0(eslint@8.57.0)(prettier@3.3.3)
+      '@vue/eslint-config-typescript':
+        specifier: ^13.0.0
+        version: 13.0.0(eslint-plugin-vue@9.27.0(eslint@8.57.0))(eslint@8.57.0)(typescript@5.4.5)
+      '@vue/tsconfig':
+        specifier: ^0.5.1
+        version: 0.5.1
+      eslint:
+        specifier: ^8.57.0
+        version: 8.57.0
+      eslint-plugin-vue:
+        specifier: ^9.23.0
+        version: 9.27.0(eslint@8.57.0)
+      npm-run-all2:
+        specifier: ^6.2.0
+        version: 6.2.2
+      path-browserify:
+        specifier: ^1.0.1
+        version: 1.0.1
+      prettier:
+        specifier: ^3.2.5
+        version: 3.3.3
+      typescript:
+        specifier: ~5.4.0
+        version: 5.4.5
+      unocss:
+        specifier: ^0.61.9
+        version: 0.61.9(postcss@8.4.40)(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.13)(sass@1.77.8))
+      vite:
+        specifier: ^5.3.1
+        version: 5.3.5(@types/node@20.14.13)(sass@1.77.8)
+      vue-tsc:
+        specifier: ^2.0.21
+        version: 2.0.29(typescript@5.4.5)
+
+packages:
+
+  '@ampproject/remapping@2.3.0':
+    resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==, tarball: https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.3.0.tgz}
+    engines: {node: '>=6.0.0'}
+
+  '@antfu/install-pkg@0.1.1':
+    resolution: {integrity: sha512-LyB/8+bSfa0DFGC06zpCEfs89/XoWZwws5ygEa5D+Xsm3OfI+aXQ86VgVG7Acyef+rSZ5HE7J8rrxzrQeM3PjQ==, tarball: https://registry.npmmirror.com/@antfu/install-pkg/-/install-pkg-0.1.1.tgz}
+
+  '@antfu/utils@0.7.10':
+    resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==, tarball: https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz}
+
+  '@babel/code-frame@7.24.7':
+    resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==, tarball: https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.24.7.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/compat-data@7.25.2':
+    resolution: {integrity: sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==, tarball: https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.25.2.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/core@7.25.2':
+    resolution: {integrity: sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==, tarball: https://registry.npmmirror.com/@babel/core/-/core-7.25.2.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/generator@7.25.0':
+    resolution: {integrity: sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==, tarball: https://registry.npmmirror.com/@babel/generator/-/generator-7.25.0.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-annotate-as-pure@7.24.7':
+    resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==, tarball: https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-compilation-targets@7.25.2':
+    resolution: {integrity: sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==, tarball: https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-create-class-features-plugin@7.25.0':
+    resolution: {integrity: sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ==, tarball: https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0
+
+  '@babel/helper-member-expression-to-functions@7.24.8':
+    resolution: {integrity: sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==, tarball: https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-module-imports@7.24.7':
+    resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==, tarball: https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-module-transforms@7.25.2':
+    resolution: {integrity: sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==, tarball: https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0
+
+  '@babel/helper-optimise-call-expression@7.24.7':
+    resolution: {integrity: sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==, tarball: https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-plugin-utils@7.24.8':
+    resolution: {integrity: sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==, tarball: https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-replace-supers@7.25.0':
+    resolution: {integrity: sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==, tarball: https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0
+
+  '@babel/helper-simple-access@7.24.7':
+    resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==, tarball: https://registry.npmmirror.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-skip-transparent-expression-wrappers@7.24.7':
+    resolution: {integrity: sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==, tarball: https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-string-parser@7.24.8':
+    resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==, tarball: https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-validator-identifier@7.24.7':
+    resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==, tarball: https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-validator-option@7.24.8':
+    resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==, tarball: https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helpers@7.25.0':
+    resolution: {integrity: sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==, tarball: https://registry.npmmirror.com/@babel/helpers/-/helpers-7.25.0.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/highlight@7.24.7':
+    resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==, tarball: https://registry.npmmirror.com/@babel/highlight/-/highlight-7.24.7.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/parser@7.25.3':
+    resolution: {integrity: sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==, tarball: https://registry.npmmirror.com/@babel/parser/-/parser-7.25.3.tgz}
+    engines: {node: '>=6.0.0'}
+    hasBin: true
+
+  '@babel/plugin-syntax-jsx@7.24.7':
+    resolution: {integrity: sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==, tarball: https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0-0
+
+  '@babel/plugin-syntax-typescript@7.24.7':
+    resolution: {integrity: sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==, tarball: https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0-0
+
+  '@babel/plugin-transform-modules-commonjs@7.24.8':
+    resolution: {integrity: sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==, tarball: https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0-0
+
+  '@babel/plugin-transform-typescript@7.25.2':
+    resolution: {integrity: sha512-lBwRvjSmqiMYe/pS0+1gggjJleUJi7NzjvQ1Fkqtt69hBa/0t1YuW/MLQMAPixfwaQOHUXsd6jeU3Z+vdGv3+A==, tarball: https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.2.tgz}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0-0
+
+  '@babel/preset-typescript@7.24.7':
+    resolution: {integrity: sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==, tarball: https://registry.npmmirror.com/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0-0
+
+  '@babel/template@7.25.0':
+    resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==, tarball: https://registry.npmmirror.com/@babel/template/-/template-7.25.0.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/traverse@7.25.3':
+    resolution: {integrity: sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==, tarball: https://registry.npmmirror.com/@babel/traverse/-/traverse-7.25.3.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/types@7.25.2':
+    resolution: {integrity: sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==, tarball: https://registry.npmmirror.com/@babel/types/-/types-7.25.2.tgz}
+    engines: {node: '>=6.9.0'}
+
+  '@ctrl/tinycolor@3.6.1':
+    resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==, tarball: https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz}
+    engines: {node: '>=10'}
+
+  '@element-plus/icons-vue@2.3.1':
+    resolution: {integrity: sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==, tarball: https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz}
+    peerDependencies:
+      vue: ^3.2.0
+
+  '@esbuild/aix-ppc64@0.21.5':
+    resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==, tarball: https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [ppc64]
+    os: [aix]
+
+  '@esbuild/android-arm64@0.21.5':
+    resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==, tarball: https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [android]
+
+  '@esbuild/android-arm@0.21.5':
+    resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==, tarball: https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [android]
+
+  '@esbuild/android-x64@0.21.5':
+    resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==, tarball: https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [android]
+
+  '@esbuild/darwin-arm64@0.21.5':
+    resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==, tarball: https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@esbuild/darwin-x64@0.21.5':
+    resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==, tarball: https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@esbuild/freebsd-arm64@0.21.5':
+    resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==, tarball: https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-x64@0.21.5':
+    resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==, tarball: https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/linux-arm64@0.21.5':
+    resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==, tarball: https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@esbuild/linux-arm@0.21.5':
+    resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==, tarball: https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [linux]
+
+  '@esbuild/linux-ia32@0.21.5':
+    resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==, tarball: https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [linux]
+
+  '@esbuild/linux-loong64@0.21.5':
+    resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==, tarball: https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [loong64]
+    os: [linux]
+
+  '@esbuild/linux-mips64el@0.21.5':
+    resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==, tarball: https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [mips64el]
+    os: [linux]
+
+  '@esbuild/linux-ppc64@0.21.5':
+    resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==, tarball: https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@esbuild/linux-riscv64@0.21.5':
+    resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==, tarball: https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@esbuild/linux-s390x@0.21.5':
+    resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==, tarball: https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [s390x]
+    os: [linux]
+
+  '@esbuild/linux-x64@0.21.5':
+    resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==, tarball: https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [linux]
+
+  '@esbuild/netbsd-x64@0.21.5':
+    resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==, tarball: https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/openbsd-x64@0.21.5':
+    resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==, tarball: https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@esbuild/sunos-x64@0.21.5':
+    resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==, tarball: https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [sunos]
+
+  '@esbuild/win32-arm64@0.21.5':
+    resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==, tarball: https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@esbuild/win32-ia32@0.21.5':
+    resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==, tarball: https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@esbuild/win32-x64@0.21.5':
+    resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==, tarball: https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [win32]
+
+  '@eslint-community/eslint-utils@4.4.0':
+    resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==, tarball: https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+    peerDependencies:
+      eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+  '@eslint-community/regexpp@4.11.0':
+    resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==, tarball: https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz}
+    engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+  '@eslint/eslintrc@2.1.4':
+    resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==, tarball: https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  '@eslint/js@8.57.0':
+    resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==, tarball: https://registry.npmmirror.com/@eslint/js/-/js-8.57.0.tgz}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  '@floating-ui/core@1.6.5':
+    resolution: {integrity: sha512-8GrTWmoFhm5BsMZOTHeGD2/0FLKLQQHvO/ZmQga4tKempYRLz8aqJGqXVuQgisnMObq2YZ2SgkwctN1LOOxcqA==, tarball: https://registry.npmmirror.com/@floating-ui/core/-/core-1.6.5.tgz}
+
+  '@floating-ui/dom@1.6.8':
+    resolution: {integrity: sha512-kx62rP19VZ767Q653wsP1XZCGIirkE09E0QUGNYTM/ttbbQHqcGPdSfWFxUyyNLc/W6aoJRBajOSXhP6GXjC0Q==, tarball: https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.6.8.tgz}
+
+  '@floating-ui/utils@0.2.5':
+    resolution: {integrity: sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==, tarball: https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.5.tgz}
+
+  '@humanwhocodes/config-array@0.11.14':
+    resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==, tarball: https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz}
+    engines: {node: '>=10.10.0'}
+    deprecated: Use @eslint/config-array instead
+
+  '@humanwhocodes/module-importer@1.0.1':
+    resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, tarball: https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz}
+    engines: {node: '>=12.22'}
+
+  '@humanwhocodes/object-schema@2.0.3':
+    resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==, tarball: https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz}
+    deprecated: Use @eslint/object-schema instead
+
+  '@iconify/types@2.0.0':
+    resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==, tarball: https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz}
+
+  '@iconify/utils@2.1.29':
+    resolution: {integrity: sha512-wCcTsmlJvTi1VWBgcJ7HeuWlh7gLGWY7L9HmbgMfjOfsoo7DADemB2Nqnrw1KvCdEAxLL5wTMBAOP5BesFrtng==, tarball: https://registry.npmmirror.com/@iconify/utils/-/utils-2.1.29.tgz}
+
+  '@jridgewell/gen-mapping@0.3.5':
+    resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==, tarball: https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz}
+    engines: {node: '>=6.0.0'}
+
+  '@jridgewell/resolve-uri@3.1.2':
+    resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, tarball: https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz}
+    engines: {node: '>=6.0.0'}
+
+  '@jridgewell/set-array@1.2.1':
+    resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==, tarball: https://registry.npmmirror.com/@jridgewell/set-array/-/set-array-1.2.1.tgz}
+    engines: {node: '>=6.0.0'}
+
+  '@jridgewell/sourcemap-codec@1.5.0':
+    resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==, tarball: https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz}
+
+  '@jridgewell/trace-mapping@0.3.25':
+    resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==, tarball: https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz}
+
+  '@nodelib/fs.scandir@2.1.5':
+    resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, tarball: https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz}
+    engines: {node: '>= 8'}
+
+  '@nodelib/fs.stat@2.0.5':
+    resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, tarball: https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz}
+    engines: {node: '>= 8'}
+
+  '@nodelib/fs.walk@1.2.8':
+    resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, tarball: https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz}
+    engines: {node: '>= 8'}
+
+  '@pkgr/core@0.1.1':
+    resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==, tarball: https://registry.npmmirror.com/@pkgr/core/-/core-0.1.1.tgz}
+    engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+
+  '@polka/url@1.0.0-next.25':
+    resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==, tarball: https://registry.npmmirror.com/@polka/url/-/url-1.0.0-next.25.tgz}
+
+  '@rollup/pluginutils@5.1.0':
+    resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==, tarball: https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz}
+    engines: {node: '>=14.0.0'}
+    peerDependencies:
+      rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
+    peerDependenciesMeta:
+      rollup:
+        optional: true
+
+  '@rollup/rollup-android-arm-eabi@4.19.1':
+    resolution: {integrity: sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==, tarball: https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.1.tgz}
+    cpu: [arm]
+    os: [android]
+
+  '@rollup/rollup-android-arm64@4.19.1':
+    resolution: {integrity: sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==, tarball: https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.1.tgz}
+    cpu: [arm64]
+    os: [android]
+
+  '@rollup/rollup-darwin-arm64@4.19.1':
+    resolution: {integrity: sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==, tarball: https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.1.tgz}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@rollup/rollup-darwin-x64@4.19.1':
+    resolution: {integrity: sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==, tarball: https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.1.tgz}
+    cpu: [x64]
+    os: [darwin]
+
+  '@rollup/rollup-linux-arm-gnueabihf@4.19.1':
+    resolution: {integrity: sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.1.tgz}
+    cpu: [arm]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-arm-musleabihf@4.19.1':
+    resolution: {integrity: sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.1.tgz}
+    cpu: [arm]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-arm64-gnu@4.19.1':
+    resolution: {integrity: sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.1.tgz}
+    cpu: [arm64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-arm64-musl@4.19.1':
+    resolution: {integrity: sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.1.tgz}
+    cpu: [arm64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-powerpc64le-gnu@4.19.1':
+    resolution: {integrity: sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.1.tgz}
+    cpu: [ppc64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-riscv64-gnu@4.19.1':
+    resolution: {integrity: sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.1.tgz}
+    cpu: [riscv64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-s390x-gnu@4.19.1':
+    resolution: {integrity: sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.1.tgz}
+    cpu: [s390x]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-x64-gnu@4.19.1':
+    resolution: {integrity: sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.1.tgz}
+    cpu: [x64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-x64-musl@4.19.1':
+    resolution: {integrity: sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==, tarball: https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.1.tgz}
+    cpu: [x64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-win32-arm64-msvc@4.19.1':
+    resolution: {integrity: sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==, tarball: https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.1.tgz}
+    cpu: [arm64]
+    os: [win32]
+
+  '@rollup/rollup-win32-ia32-msvc@4.19.1':
+    resolution: {integrity: sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==, tarball: https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.1.tgz}
+    cpu: [ia32]
+    os: [win32]
+
+  '@rollup/rollup-win32-x64-msvc@4.19.1':
+    resolution: {integrity: sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==, tarball: https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz}
+    cpu: [x64]
+    os: [win32]
+
+  '@rushstack/eslint-patch@1.10.4':
+    resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==, tarball: https://registry.npmmirror.com/@rushstack/eslint-patch/-/eslint-patch-1.10.4.tgz}
+
+  '@sxzz/popperjs-es@2.11.7':
+    resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==, tarball: https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz}
+
+  '@tsconfig/node20@20.1.4':
+    resolution: {integrity: sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==, tarball: https://registry.npmmirror.com/@tsconfig/node20/-/node20-20.1.4.tgz}
+
+  '@types/estree@1.0.5':
+    resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==, tarball: https://registry.npmmirror.com/@types/estree/-/estree-1.0.5.tgz}
+
+  '@types/lodash-es@4.17.12':
+    resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==, tarball: https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz}
+
+  '@types/lodash@4.17.7':
+    resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==, tarball: https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.7.tgz}
+
+  '@types/node@20.14.13':
+    resolution: {integrity: sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==, tarball: https://registry.npmmirror.com/@types/node/-/node-20.14.13.tgz}
+
+  '@types/web-bluetooth@0.0.16':
+    resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==, tarball: https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz}
+
+  '@typescript-eslint/eslint-plugin@7.18.0':
+    resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==, tarball: https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz}
+    engines: {node: ^18.18.0 || >=20.0.0}
+    peerDependencies:
+      '@typescript-eslint/parser': ^7.0.0
+      eslint: ^8.56.0
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@typescript-eslint/parser@7.18.0':
+    resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==, tarball: https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-7.18.0.tgz}
+    engines: {node: ^18.18.0 || >=20.0.0}
+    peerDependencies:
+      eslint: ^8.56.0
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@typescript-eslint/scope-manager@7.18.0':
+    resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==, tarball: https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz}
+    engines: {node: ^18.18.0 || >=20.0.0}
+
+  '@typescript-eslint/type-utils@7.18.0':
+    resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==, tarball: https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz}
+    engines: {node: ^18.18.0 || >=20.0.0}
+    peerDependencies:
+      eslint: ^8.56.0
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@typescript-eslint/types@7.18.0':
+    resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==, tarball: https://registry.npmmirror.com/@typescript-eslint/types/-/types-7.18.0.tgz}
+    engines: {node: ^18.18.0 || >=20.0.0}
+
+  '@typescript-eslint/typescript-estree@7.18.0':
+    resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==, tarball: https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz}
+    engines: {node: ^18.18.0 || >=20.0.0}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@typescript-eslint/utils@7.18.0':
+    resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==, tarball: https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-7.18.0.tgz}
+    engines: {node: ^18.18.0 || >=20.0.0}
+    peerDependencies:
+      eslint: ^8.56.0
+
+  '@typescript-eslint/visitor-keys@7.18.0':
+    resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==, tarball: https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz}
+    engines: {node: ^18.18.0 || >=20.0.0}
+
+  '@ungap/structured-clone@1.2.0':
+    resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==, tarball: https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz}
+
+  '@unocss/astro@0.61.9':
+    resolution: {integrity: sha512-adOXz4itYHxqhvQgJHlEU58EHDTtY2qrcEPVmQVk4qI1W+ezQV6nQMQvti8mS/HbFw3MOJhIY1MlJoZK36/cyw==, tarball: https://registry.npmmirror.com/@unocss/astro/-/astro-0.61.9.tgz}
+    peerDependencies:
+      vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0
+    peerDependenciesMeta:
+      vite:
+        optional: true
+
+  '@unocss/cli@0.61.9':
+    resolution: {integrity: sha512-W5pN2cOKAOkeKKXMqsGD/J7dpEAmxODtOH2Afjk41qsjqUlzGlUbmgG9PjAz7TDHrAmvuf3nvmMeeT3fii2UFg==, tarball: https://registry.npmmirror.com/@unocss/cli/-/cli-0.61.9.tgz}
+    engines: {node: '>=14'}
+    hasBin: true
+
+  '@unocss/config@0.61.9':
+    resolution: {integrity: sha512-ATvZEFMQiW3/oUaaplVMBYuagEELtnLbHSYH4pUGbJ5MALAfV98mZRyk4FkKkYoMYqWLGdCylzpgMPFDOuFQlQ==, tarball: https://registry.npmmirror.com/@unocss/config/-/config-0.61.9.tgz}
+    engines: {node: '>=14'}
+
+  '@unocss/core@0.61.9':
+    resolution: {integrity: sha512-2W1YZQIWXcueGdbXU/ZCqn/8yQhWk8e8kAHFkVlbc9rictkd2UmPB9nIZ8Ii1tMwt6F0TT6vfHbLJEGCV08o2g==, tarball: https://registry.npmmirror.com/@unocss/core/-/core-0.61.9.tgz}
+
+  '@unocss/extractor-arbitrary-variants@0.61.9':
+    resolution: {integrity: sha512-ii42/hKbhgeBBOy86729t6/HeGmxUcHM8FprPeb/v/DfYsCkjDvMYVynX3FN/K5pR2WV+HHp6TQS7GbTmRIN0g==, tarball: https://registry.npmmirror.com/@unocss/extractor-arbitrary-variants/-/extractor-arbitrary-variants-0.61.9.tgz}
+
+  '@unocss/inspector@0.61.9':
+    resolution: {integrity: sha512-kUcQ/h8/yAfkqL2eCGVFyB0IGSPdR0dx2HH4V+mdSMfd8yKFR/BQys3mBvqZwSZu5a0+iisFHHq9wr+/I5DtHQ==, tarball: https://registry.npmmirror.com/@unocss/inspector/-/inspector-0.61.9.tgz}
+
+  '@unocss/postcss@0.61.9':
+    resolution: {integrity: sha512-HuFE/TUH6tt5f/AwiKNhQ/FO/lvFeW0JHPkx9SCURcKKoD3rpJUbhTqVv7c0zlCVQnRFX0hxpimoetp5Dh8qdA==, tarball: https://registry.npmmirror.com/@unocss/postcss/-/postcss-0.61.9.tgz}
+    engines: {node: '>=14'}
+    peerDependencies:
+      postcss: ^8.4.21
+
+  '@unocss/preset-attributify@0.61.9':
+    resolution: {integrity: sha512-AHlEF7PiIBz1jHZZ62+AZ1u5ITrPNL/mgN8XyKwocoAr9HH8aQ3xzUgIuEX6vfV4a8rTdawffY99BQ12msePWQ==, tarball: https://registry.npmmirror.com/@unocss/preset-attributify/-/preset-attributify-0.61.9.tgz}
+
+  '@unocss/preset-icons@0.61.9':
+    resolution: {integrity: sha512-5XZO511ksu3EVwpV2nIZKa5NzyJAb+JARKaUpQIXssHUVdRKk5nJYr1XtrpBDLgB6VEf/1skViLEa1bpOUI5Wg==, tarball: https://registry.npmmirror.com/@unocss/preset-icons/-/preset-icons-0.61.9.tgz}
+
+  '@unocss/preset-mini@0.61.9':
+    resolution: {integrity: sha512-qhagWfdM7ytRWf4wFfrAcdeCUCVD9wDVrM+9evAmuOnMXWEiVZCjfwhjjFu+8lM7g+38n+gi7VcrNuTiZ8fHBA==, tarball: https://registry.npmmirror.com/@unocss/preset-mini/-/preset-mini-0.61.9.tgz}
+
+  '@unocss/preset-tagify@0.61.9':
+    resolution: {integrity: sha512-E+54+uSe+btOnQDlh8XjDUXhwxJd6/TL/8Rdl+7Pg6m+JNXudEt7xOd81L/KlDPD2tYYH9g/dQUaDN5aJyfRPQ==, tarball: https://registry.npmmirror.com/@unocss/preset-tagify/-/preset-tagify-0.61.9.tgz}
+
+  '@unocss/preset-typography@0.61.9':
+    resolution: {integrity: sha512-ZDoRViHtzI1Ny0sZyjajeCGEdFQCBn5CeIYgxO/KCpN107KTGLnYfoabv0gHtj/qaeAh30obeOMxZaIuxYoW3Q==, tarball: https://registry.npmmirror.com/@unocss/preset-typography/-/preset-typography-0.61.9.tgz}
+
+  '@unocss/preset-uno@0.61.9':
+    resolution: {integrity: sha512-N4R/BCMphrHvAMZ+qgR/FPoh724uXDuZ/1DEGuirUQJMg7makqrI6czL+P99q1bP8nWzxWEXiRXnKKLiyD9pJw==, tarball: https://registry.npmmirror.com/@unocss/preset-uno/-/preset-uno-0.61.9.tgz}
+
+  '@unocss/preset-web-fonts@0.61.9':
+    resolution: {integrity: sha512-fjQv74+FiAvGJM5vSLkD15Taku0cbi5F7qAr5T85EIQOpUB1fiH2kPoXIOT1WS2lKbQZh6pNGBxLrbBRgnVPew==, tarball: https://registry.npmmirror.com/@unocss/preset-web-fonts/-/preset-web-fonts-0.61.9.tgz}
+
+  '@unocss/preset-wind@0.61.9':
+    resolution: {integrity: sha512-AzbjJrNL9Rb2BzTiREyssd8v7KFVVLERQ/PNILGzo6yYelYMl4AhKXZ3jgxWEsIABArVa3UkGBigG4h/L+2JHA==, tarball: https://registry.npmmirror.com/@unocss/preset-wind/-/preset-wind-0.61.9.tgz}
+
+  '@unocss/reset@0.61.9':
+    resolution: {integrity: sha512-A1KtJiFgLM0N3FqJ9r5M3mVULcwsn+14tq5WkvSPF9ik3zQeJh8/NhxKdJImWClwBOzn795NQFXXFB70Ja+2RA==, tarball: https://registry.npmmirror.com/@unocss/reset/-/reset-0.61.9.tgz}
+
+  '@unocss/rule-utils@0.61.9':
+    resolution: {integrity: sha512-54Hw0nF+3ga70ETo3kes4He62wdsB4dHMgEiD/DEmJzyVY3ZuG/sIVAgkxjMQDo5w4SSYU/Ys1QaY+IQmeJHFQ==, tarball: https://registry.npmmirror.com/@unocss/rule-utils/-/rule-utils-0.61.9.tgz}
+    engines: {node: '>=14'}
+
+  '@unocss/scope@0.61.9':
+    resolution: {integrity: sha512-a9/vdg7YTFZEnJSaJBh/GqkLokYh3ZjEd3gHUxl/TZDSkGOz3WnkR2h+lgaLZm9MJ7RlSvJxYP8ySezH7jU1Pw==, tarball: https://registry.npmmirror.com/@unocss/scope/-/scope-0.61.9.tgz}
+
+  '@unocss/transformer-attributify-jsx-babel@0.61.9':
+    resolution: {integrity: sha512-+fojHVJhA2MVd3VTCjlEKXf8Vnoy4N+lEl0CrYOD+im44sH5CWogm0RWs9rbeemy1uel6NI1wkP4xTfIA4vEgQ==, tarball: https://registry.npmmirror.com/@unocss/transformer-attributify-jsx-babel/-/transformer-attributify-jsx-babel-0.61.9.tgz}
+
+  '@unocss/transformer-attributify-jsx@0.61.9':
+    resolution: {integrity: sha512-tKZpZ64Lr6/CX96PhDtKEsqWDo1qjtswEulzIDLxpS90SMyann3azTs6mSuOwGbkbwc4gaJe6H38eCNos0ZqHg==, tarball: https://registry.npmmirror.com/@unocss/transformer-attributify-jsx/-/transformer-attributify-jsx-0.61.9.tgz}
+
+  '@unocss/transformer-compile-class@0.61.9':
+    resolution: {integrity: sha512-jezMpssFJGIaZNE/rw5U+9Rk1RoDrZqXZokRkqt4tamEn1SiXjRMPWoE/hLg5Kw4oybxwCXTuAk2OsD+kTb7iA==, tarball: https://registry.npmmirror.com/@unocss/transformer-compile-class/-/transformer-compile-class-0.61.9.tgz}
+
+  '@unocss/transformer-directives@0.61.9':
+    resolution: {integrity: sha512-e4uIbHYdAYJSVpvxOv6kAsyI18X3gHkBsmBYWcUlPLVv+8tYo4eZtc0rn6ZvpiLzkFywG9e9cmpqVQwOR6pBVg==, tarball: https://registry.npmmirror.com/@unocss/transformer-directives/-/transformer-directives-0.61.9.tgz}
+
+  '@unocss/transformer-variant-group@0.61.9':
+    resolution: {integrity: sha512-iewADYlY0LoeCb80E/4feHVSCKHl+QzGH4xUvW0zU85evMqNOa0/t0dCIoEG22wr/9piyEsg6OdHprZ2QliYqg==, tarball: https://registry.npmmirror.com/@unocss/transformer-variant-group/-/transformer-variant-group-0.61.9.tgz}
+
+  '@unocss/vite@0.61.9':
+    resolution: {integrity: sha512-hP/sL9rq1DvVCbSSx05m+bwYqen1nHm9tW6elKFkfV7X5jBUywu24WRq551NZI33KmgHA525ApX++DSWye+0uw==, tarball: https://registry.npmmirror.com/@unocss/vite/-/vite-0.61.9.tgz}
+    peerDependencies:
+      vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0
+
+  '@vitejs/plugin-vue@5.1.1':
+    resolution: {integrity: sha512-sDckXxlHpMsjRQbAH9WanangrfrblsOd3pNifePs+FOHjJg1jfWq5L/P0PsBRndEt3nmdUnmvieP8ULDeX5AvA==, tarball: https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.1.1.tgz}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    peerDependencies:
+      vite: ^5.0.0
+      vue: ^3.2.25
+
+  '@volar/language-core@2.4.0-alpha.18':
+    resolution: {integrity: sha512-JAYeJvYQQROmVRtSBIczaPjP3DX4QW1fOqW1Ebs0d3Y3EwSNRglz03dSv0Dm61dzd0Yx3WgTW3hndDnTQqgmyg==, tarball: https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.0-alpha.18.tgz}
+
+  '@volar/source-map@2.4.0-alpha.18':
+    resolution: {integrity: sha512-MTeCV9MUwwsH0sNFiZwKtFrrVZUK6p8ioZs3xFzHc2cvDXHWlYN3bChdQtwKX+FY2HG6H3CfAu1pKijolzIQ8g==, tarball: https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.0-alpha.18.tgz}
+
+  '@volar/typescript@2.4.0-alpha.18':
+    resolution: {integrity: sha512-sXh5Y8sqGUkgxpMWUGvRXggxYHAVxg0Pa1C42lQZuPDrW6vHJPR0VCK8Sr7WJsAW530HuNQT/ZIskmXtxjybMQ==, tarball: https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.0-alpha.18.tgz}
+
+  '@vue/compiler-core@3.4.35':
+    resolution: {integrity: sha512-gKp0zGoLnMYtw4uS/SJRRO7rsVggLjvot3mcctlMXunYNsX+aRJDqqw/lV5/gHK91nvaAAlWFgdVl020AW1Prg==, tarball: https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.4.35.tgz}
+
+  '@vue/compiler-dom@3.4.35':
+    resolution: {integrity: sha512-pWIZRL76/oE/VMhdv/ovZfmuooEni6JPG1BFe7oLk5DZRo/ImydXijoZl/4kh2406boRQ7lxTYzbZEEXEhj9NQ==, tarball: https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.4.35.tgz}
+
+  '@vue/compiler-sfc@3.4.35':
+    resolution: {integrity: sha512-xacnRS/h/FCsjsMfxBkzjoNxyxEyKyZfBch/P4vkLRvYJwe5ChXmZZrj8Dsed/752H2Q3JE8kYu9Uyha9J6PgA==, tarball: https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.4.35.tgz}
+
+  '@vue/compiler-ssr@3.4.35':
+    resolution: {integrity: sha512-7iynB+0KB1AAJKk/biENTV5cRGHRdbdaD7Mx3nWcm1W8bVD6QmnH3B4AHhQQ1qZHhqFwzEzMwiytXm3PX1e60A==, tarball: https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.4.35.tgz}
+
+  '@vue/compiler-vue2@2.7.16':
+    resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==, tarball: https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz}
+
+  '@vue/devtools-api@6.6.3':
+    resolution: {integrity: sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==, tarball: https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.3.tgz}
+
+  '@vue/eslint-config-prettier@9.0.0':
+    resolution: {integrity: sha512-z1ZIAAUS9pKzo/ANEfd2sO+v2IUalz7cM/cTLOZ7vRFOPk5/xuRKQteOu1DErFLAh/lYGXMVZ0IfYKlyInuDVg==, tarball: https://registry.npmmirror.com/@vue/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz}
+    peerDependencies:
+      eslint: '>= 8.0.0'
+      prettier: '>= 3.0.0'
+
+  '@vue/eslint-config-typescript@13.0.0':
+    resolution: {integrity: sha512-MHh9SncG/sfqjVqjcuFLOLD6Ed4dRAis4HNt0dXASeAuLqIAx4YMB1/m2o4pUKK1vCt8fUvYG8KKX2Ot3BVZTg==, tarball: https://registry.npmmirror.com/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz}
+    engines: {node: ^18.18.0 || >=20.0.0}
+    peerDependencies:
+      eslint: ^8.56.0
+      eslint-plugin-vue: ^9.0.0
+      typescript: '>=4.7.4'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@vue/language-core@2.0.29':
+    resolution: {integrity: sha512-o2qz9JPjhdoVj8D2+9bDXbaI4q2uZTHQA/dbyZT4Bj1FR9viZxDJnLcKVHfxdn6wsOzRgpqIzJEEmSSvgMvDTQ==, tarball: https://registry.npmmirror.com/@vue/language-core/-/language-core-2.0.29.tgz}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  '@vue/reactivity@3.4.35':
+    resolution: {integrity: sha512-Ggtz7ZZHakriKioveJtPlStYardwQH6VCs9V13/4qjHSQb/teE30LVJNrbBVs4+aoYGtTQKJbTe4CWGxVZrvEw==, tarball: https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.4.35.tgz}
+
+  '@vue/runtime-core@3.4.35':
+    resolution: {integrity: sha512-D+BAjFoWwT5wtITpSxwqfWZiBClhBbR+bm0VQlWYFOadUUXFo+5wbe9ErXhLvwguPiLZdEF13QAWi2vP3ZD5tA==, tarball: https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.4.35.tgz}
+
+  '@vue/runtime-dom@3.4.35':
+    resolution: {integrity: sha512-yGOlbos+MVhlS5NWBF2HDNgblG8e2MY3+GigHEyR/dREAluvI5tuUUgie3/9XeqhPE4LF0i2wjlduh5thnfOqw==, tarball: https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.4.35.tgz}
+
+  '@vue/server-renderer@3.4.35':
+    resolution: {integrity: sha512-iZ0e/u9mRE4T8tNhlo0tbA+gzVkgv8r5BX6s1kRbOZqfpq14qoIvCZ5gIgraOmYkMYrSEZgkkojFPr+Nyq/Mnw==, tarball: https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.4.35.tgz}
+    peerDependencies:
+      vue: 3.4.35
+
+  '@vue/shared@3.4.35':
+    resolution: {integrity: sha512-hvuhBYYDe+b1G8KHxsQ0diDqDMA8D9laxWZhNAjE83VZb5UDaXl9Xnz7cGdDSyiHM90qqI/CyGMcpBpiDy6VVQ==, tarball: https://registry.npmmirror.com/@vue/shared/-/shared-3.4.35.tgz}
+
+  '@vue/tsconfig@0.5.1':
+    resolution: {integrity: sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==, tarball: https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.5.1.tgz}
+
+  '@vueuse/core@9.13.0':
+    resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==, tarball: https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz}
+
+  '@vueuse/metadata@9.13.0':
+    resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==, tarball: https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz}
+
+  '@vueuse/shared@9.13.0':
+    resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==, tarball: https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz}
+
+  acorn-jsx@5.3.2:
+    resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, tarball: https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz}
+    peerDependencies:
+      acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+  acorn@8.12.1:
+    resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==, tarball: https://registry.npmmirror.com/acorn/-/acorn-8.12.1.tgz}
+    engines: {node: '>=0.4.0'}
+    hasBin: true
+
+  ajv@6.12.6:
+    resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==, tarball: https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz}
+
+  ansi-regex@5.0.1:
+    resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, tarball: https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz}
+    engines: {node: '>=8'}
+
+  ansi-styles@3.2.1:
+    resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==, tarball: https://registry.npmmirror.com/ansi-styles/-/ansi-styles-3.2.1.tgz}
+    engines: {node: '>=4'}
+
+  ansi-styles@4.3.0:
+    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==, tarball: https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz}
+    engines: {node: '>=8'}
+
+  ansi-styles@6.2.1:
+    resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==, tarball: https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz}
+    engines: {node: '>=12'}
+
+  anymatch@3.1.3:
+    resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==, tarball: https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz}
+    engines: {node: '>= 8'}
+
+  argparse@2.0.1:
+    resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, tarball: https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz}
+
+  array-union@2.1.0:
+    resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==, tarball: https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz}
+    engines: {node: '>=8'}
+
+  async-validator@4.2.5:
+    resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==, tarball: https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz}
+
+  asynckit@0.4.0:
+    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==, tarball: https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz}
+
+  axios@1.7.2:
+    resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==, tarball: https://registry.npmmirror.com/axios/-/axios-1.7.2.tgz}
+
+  balanced-match@1.0.2:
+    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, tarball: https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz}
+
+  binary-extensions@2.3.0:
+    resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==, tarball: https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz}
+    engines: {node: '>=8'}
+
+  boolbase@1.0.0:
+    resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==, tarball: https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz}
+
+  brace-expansion@1.1.11:
+    resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==, tarball: https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz}
+
+  brace-expansion@2.0.1:
+    resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==, tarball: https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz}
+
+  braces@3.0.3:
+    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, tarball: https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz}
+    engines: {node: '>=8'}
+
+  browserslist@4.23.2:
+    resolution: {integrity: sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==, tarball: https://registry.npmmirror.com/browserslist/-/browserslist-4.23.2.tgz}
+    engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+    hasBin: true
+
+  bundle-require@5.0.0:
+    resolution: {integrity: sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==, tarball: https://registry.npmmirror.com/bundle-require/-/bundle-require-5.0.0.tgz}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+    peerDependencies:
+      esbuild: '>=0.18'
+
+  cac@6.7.14:
+    resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==, tarball: https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz}
+    engines: {node: '>=8'}
+
+  callsites@3.1.0:
+    resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, tarball: https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz}
+    engines: {node: '>=6'}
+
+  caniuse-lite@1.0.30001646:
+    resolution: {integrity: sha512-dRg00gudiBDDTmUhClSdv3hqRfpbOnU28IpI1T6PBTLWa+kOj0681C8uML3PifYfREuBrVjDGhL3adYpBT6spw==, tarball: https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001646.tgz}
+
+  chalk@2.4.2:
+    resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==, tarball: https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz}
+    engines: {node: '>=4'}
+
+  chalk@4.1.2:
+    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==, tarball: https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz}
+    engines: {node: '>=10'}
+
+  chokidar@3.6.0:
+    resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==, tarball: https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz}
+    engines: {node: '>= 8.10.0'}
+
+  color-convert@1.9.3:
+    resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==, tarball: https://registry.npmmirror.com/color-convert/-/color-convert-1.9.3.tgz}
+
+  color-convert@2.0.1:
+    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, tarball: https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz}
+    engines: {node: '>=7.0.0'}
+
+  color-name@1.1.3:
+    resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==, tarball: https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz}
+
+  color-name@1.1.4:
+    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, tarball: https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz}
+
+  colorette@2.0.20:
+    resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==, tarball: https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz}
+
+  combined-stream@1.0.8:
+    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==, tarball: https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz}
+    engines: {node: '>= 0.8'}
+
+  computeds@0.0.1:
+    resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==, tarball: https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz}
+
+  concat-map@0.0.1:
+    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, tarball: https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz}
+
+  confbox@0.1.7:
+    resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==, tarball: https://registry.npmmirror.com/confbox/-/confbox-0.1.7.tgz}
+
+  consola@3.2.3:
+    resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==, tarball: https://registry.npmmirror.com/consola/-/consola-3.2.3.tgz}
+    engines: {node: ^14.18.0 || >=16.10.0}
+
+  convert-source-map@2.0.0:
+    resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, tarball: https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz}
+
+  cross-spawn@7.0.3:
+    resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==, tarball: https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz}
+    engines: {node: '>= 8'}
+
+  css-tree@2.3.1:
+    resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==, tarball: https://registry.npmmirror.com/css-tree/-/css-tree-2.3.1.tgz}
+    engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
+  cssesc@3.0.0:
+    resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==, tarball: https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz}
+    engines: {node: '>=4'}
+    hasBin: true
+
+  csstype@3.1.3:
+    resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==, tarball: https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz}
+
+  dayjs@1.11.12:
+    resolution: {integrity: sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==, tarball: https://registry.npmmirror.com/dayjs/-/dayjs-1.11.12.tgz}
+
+  de-indent@1.0.2:
+    resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==, tarball: https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz}
+
+  debug@4.3.6:
+    resolution: {integrity: sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==, tarball: https://registry.npmmirror.com/debug/-/debug-4.3.6.tgz}
+    engines: {node: '>=6.0'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
+  deep-is@0.1.4:
+    resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, tarball: https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz}
+
+  defu@6.1.4:
+    resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==, tarball: https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz}
+
+  delayed-stream@1.0.0:
+    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==, tarball: https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz}
+    engines: {node: '>=0.4.0'}
+
+  destr@2.0.3:
+    resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==, tarball: https://registry.npmmirror.com/destr/-/destr-2.0.3.tgz}
+
+  dir-glob@3.0.1:
+    resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==, tarball: https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz}
+    engines: {node: '>=8'}
+
+  doctrine@3.0.0:
+    resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==, tarball: https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz}
+    engines: {node: '>=6.0.0'}
+
+  dom-zindex@1.0.4:
+    resolution: {integrity: sha512-PNk7u71TJ1C9Lwjjp5nNuQcVWuECFMmr9kZAwi2UbgWUM7jXdTCe4O4x5bhLUa07jpcZUVA5Du3ho7/FXzS9Ng==, tarball: https://registry.npmmirror.com/dom-zindex/-/dom-zindex-1.0.4.tgz}
+
+  duplexer@0.1.2:
+    resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==, tarball: https://registry.npmmirror.com/duplexer/-/duplexer-0.1.2.tgz}
+
+  electron-to-chromium@1.5.4:
+    resolution: {integrity: sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==, tarball: https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz}
+
+  element-plus@2.7.8:
+    resolution: {integrity: sha512-h6dx2XihAbQaud0v+6O7Fy0b0G3YNplNVH7QnK3csTcvQd4y4raiyMRQpf9EKbRbTMdNrFsqAZrs9ok9DMcJHg==, tarball: https://registry.npmmirror.com/element-plus/-/element-plus-2.7.8.tgz}
+    peerDependencies:
+      vue: ^3.2.0
+
+  entities@4.5.0:
+    resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==, tarball: https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz}
+    engines: {node: '>=0.12'}
+
+  esbuild@0.21.5:
+    resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==, tarball: https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz}
+    engines: {node: '>=12'}
+    hasBin: true
+
+  escalade@3.1.2:
+    resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==, tarball: https://registry.npmmirror.com/escalade/-/escalade-3.1.2.tgz}
+    engines: {node: '>=6'}
+
+  escape-html@1.0.3:
+    resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==, tarball: https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz}
+
+  escape-string-regexp@1.0.5:
+    resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==, tarball: https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz}
+    engines: {node: '>=0.8.0'}
+
+  escape-string-regexp@4.0.0:
+    resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, tarball: https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz}
+    engines: {node: '>=10'}
+
+  eslint-config-prettier@9.1.0:
+    resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==, tarball: https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz}
+    hasBin: true
+    peerDependencies:
+      eslint: '>=7.0.0'
+
+  eslint-plugin-prettier@5.2.1:
+    resolution: {integrity: sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==, tarball: https://registry.npmmirror.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz}
+    engines: {node: ^14.18.0 || >=16.0.0}
+    peerDependencies:
+      '@types/eslint': '>=8.0.0'
+      eslint: '>=8.0.0'
+      eslint-config-prettier: '*'
+      prettier: '>=3.0.0'
+    peerDependenciesMeta:
+      '@types/eslint':
+        optional: true
+      eslint-config-prettier:
+        optional: true
+
+  eslint-plugin-vue@9.27.0:
+    resolution: {integrity: sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==, tarball: https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.27.0.tgz}
+    engines: {node: ^14.17.0 || >=16.0.0}
+    peerDependencies:
+      eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0
+
+  eslint-scope@7.2.2:
+    resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==, tarball: https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  eslint-visitor-keys@3.4.3:
+    resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==, tarball: https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  eslint@8.57.0:
+    resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==, tarball: https://registry.npmmirror.com/eslint/-/eslint-8.57.0.tgz}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+    hasBin: true
+
+  espree@9.6.1:
+    resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==, tarball: https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  esquery@1.6.0:
+    resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==, tarball: https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz}
+    engines: {node: '>=0.10'}
+
+  esrecurse@4.3.0:
+    resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, tarball: https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz}
+    engines: {node: '>=4.0'}
+
+  estraverse@5.3.0:
+    resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, tarball: https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz}
+    engines: {node: '>=4.0'}
+
+  estree-walker@2.0.2:
+    resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==, tarball: https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz}
+
+  esutils@2.0.3:
+    resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, tarball: https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz}
+    engines: {node: '>=0.10.0'}
+
+  execa@5.1.1:
+    resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==, tarball: https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz}
+    engines: {node: '>=10'}
+
+  fast-deep-equal@3.1.3:
+    resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, tarball: https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz}
+
+  fast-diff@1.3.0:
+    resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==, tarball: https://registry.npmmirror.com/fast-diff/-/fast-diff-1.3.0.tgz}
+
+  fast-glob@3.3.2:
+    resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==, tarball: https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.2.tgz}
+    engines: {node: '>=8.6.0'}
+
+  fast-json-stable-stringify@2.1.0:
+    resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, tarball: https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz}
+
+  fast-levenshtein@2.0.6:
+    resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, tarball: https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz}
+
+  fastq@1.17.1:
+    resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==, tarball: https://registry.npmmirror.com/fastq/-/fastq-1.17.1.tgz}
+
+  file-entry-cache@6.0.1:
+    resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==, tarball: https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz}
+    engines: {node: ^10.12.0 || >=12.0.0}
+
+  fill-range@7.1.1:
+    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==, tarball: https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz}
+    engines: {node: '>=8'}
+
+  find-up@5.0.0:
+    resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, tarball: https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz}
+    engines: {node: '>=10'}
+
+  flat-cache@3.2.0:
+    resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==, tarball: https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz}
+    engines: {node: ^10.12.0 || >=12.0.0}
+
+  flatted@3.3.1:
+    resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==, tarball: https://registry.npmmirror.com/flatted/-/flatted-3.3.1.tgz}
+
+  follow-redirects@1.15.6:
+    resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==, tarball: https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.6.tgz}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+
+  form-data@4.0.0:
+    resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==, tarball: https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz}
+    engines: {node: '>= 6'}
+
+  fs.realpath@1.0.0:
+    resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==, tarball: https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz}
+
+  fsevents@2.3.3:
+    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, tarball: https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz}
+    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+    os: [darwin]
+
+  gensync@1.0.0-beta.2:
+    resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, tarball: https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz}
+    engines: {node: '>=6.9.0'}
+
+  get-stream@6.0.1:
+    resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==, tarball: https://registry.npmmirror.com/get-stream/-/get-stream-6.0.1.tgz}
+    engines: {node: '>=10'}
+
+  get-tsconfig@4.7.6:
+    resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==, tarball: https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.7.6.tgz}
+
+  glob-parent@5.1.2:
+    resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, tarball: https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz}
+    engines: {node: '>= 6'}
+
+  glob-parent@6.0.2:
+    resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, tarball: https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz}
+    engines: {node: '>=10.13.0'}
+
+  glob@7.2.3:
+    resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==, tarball: https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz}
+    deprecated: Glob versions prior to v9 are no longer supported
+
+  globals@11.12.0:
+    resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==, tarball: https://registry.npmmirror.com/globals/-/globals-11.12.0.tgz}
+    engines: {node: '>=4'}
+
+  globals@13.24.0:
+    resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==, tarball: https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz}
+    engines: {node: '>=8'}
+
+  globby@11.1.0:
+    resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==, tarball: https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz}
+    engines: {node: '>=10'}
+
+  graphemer@1.4.0:
+    resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==, tarball: https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz}
+
+  gzip-size@6.0.0:
+    resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==, tarball: https://registry.npmmirror.com/gzip-size/-/gzip-size-6.0.0.tgz}
+    engines: {node: '>=10'}
+
+  has-flag@3.0.0:
+    resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==, tarball: https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz}
+    engines: {node: '>=4'}
+
+  has-flag@4.0.0:
+    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==, tarball: https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz}
+    engines: {node: '>=8'}
+
+  he@1.2.0:
+    resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==, tarball: https://registry.npmmirror.com/he/-/he-1.2.0.tgz}
+    hasBin: true
+
+  human-signals@2.1.0:
+    resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==, tarball: https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz}
+    engines: {node: '>=10.17.0'}
+
+  ignore@5.3.1:
+    resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==, tarball: https://registry.npmmirror.com/ignore/-/ignore-5.3.1.tgz}
+    engines: {node: '>= 4'}
+
+  immutable@4.3.7:
+    resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==, tarball: https://registry.npmmirror.com/immutable/-/immutable-4.3.7.tgz}
+
+  import-fresh@3.3.0:
+    resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==, tarball: https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz}
+    engines: {node: '>=6'}
+
+  importx@0.4.3:
+    resolution: {integrity: sha512-x6E6OxmWq/SUaj7wDeDeSjyHP+rMUbEaqJ5fw0uEtC/FTX9ocxNMFJ+ONnpJIsRpFz3ya6qJAK4orwSKqw0BSQ==, tarball: https://registry.npmmirror.com/importx/-/importx-0.4.3.tgz}
+
+  imurmurhash@0.1.4:
+    resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, tarball: https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz}
+    engines: {node: '>=0.8.19'}
+
+  inflight@1.0.6:
+    resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==, tarball: https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz}
+    deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
+
+  inherits@2.0.4:
+    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, tarball: https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz}
+
+  is-binary-path@2.1.0:
+    resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==, tarball: https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz}
+    engines: {node: '>=8'}
+
+  is-extglob@2.1.1:
+    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, tarball: https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz}
+    engines: {node: '>=0.10.0'}
+
+  is-glob@4.0.3:
+    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, tarball: https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz}
+    engines: {node: '>=0.10.0'}
+
+  is-number@7.0.0:
+    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, tarball: https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz}
+    engines: {node: '>=0.12.0'}
+
+  is-path-inside@3.0.3:
+    resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==, tarball: https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz}
+    engines: {node: '>=8'}
+
+  is-stream@2.0.1:
+    resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==, tarball: https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz}
+    engines: {node: '>=8'}
+
+  isexe@2.0.0:
+    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, tarball: https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz}
+
+  jiti@1.21.6:
+    resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==, tarball: https://registry.npmmirror.com/jiti/-/jiti-1.21.6.tgz}
+    hasBin: true
+
+  jiti@2.0.0-beta.2:
+    resolution: {integrity: sha512-c+PHQZakiQuMKbnhvrjZUvrK6E/AfmTOf4P+E3Y4FNVHcNMX9e/XrnbEvO+m4wS6ZjsvhHh/POQTlfy8uXFc0A==, tarball: https://registry.npmmirror.com/jiti/-/jiti-2.0.0-beta.2.tgz}
+    hasBin: true
+
+  js-cookie@3.0.5:
+    resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==, tarball: https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz}
+    engines: {node: '>=14'}
+
+  js-tokens@4.0.0:
+    resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, tarball: https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz}
+
+  js-yaml@4.1.0:
+    resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==, tarball: https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz}
+    hasBin: true
+
+  jsesc@2.5.2:
+    resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==, tarball: https://registry.npmmirror.com/jsesc/-/jsesc-2.5.2.tgz}
+    engines: {node: '>=4'}
+    hasBin: true
+
+  json-buffer@3.0.1:
+    resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, tarball: https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz}
+
+  json-parse-even-better-errors@3.0.2:
+    resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==, tarball: https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz}
+    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+
+  json-schema-traverse@0.4.1:
+    resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, tarball: https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz}
+
+  json-stable-stringify-without-jsonify@1.0.1:
+    resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, tarball: https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz}
+
+  json5@2.2.3:
+    resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, tarball: https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz}
+    engines: {node: '>=6'}
+    hasBin: true
+
+  keyv@4.5.4:
+    resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, tarball: https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz}
+
+  kolorist@1.8.0:
+    resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==, tarball: https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz}
+
+  levn@0.4.1:
+    resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, tarball: https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz}
+    engines: {node: '>= 0.8.0'}
+
+  load-tsconfig@0.2.5:
+    resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==, tarball: https://registry.npmmirror.com/load-tsconfig/-/load-tsconfig-0.2.5.tgz}
+    engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+
+  local-pkg@0.5.0:
+    resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==, tarball: https://registry.npmmirror.com/local-pkg/-/local-pkg-0.5.0.tgz}
+    engines: {node: '>=14'}
+
+  locate-path@6.0.0:
+    resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, tarball: https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz}
+    engines: {node: '>=10'}
+
+  lodash-es@4.17.21:
+    resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==, tarball: https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz}
+
+  lodash-unified@1.0.3:
+    resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==, tarball: https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz}
+    peerDependencies:
+      '@types/lodash-es': '*'
+      lodash: '*'
+      lodash-es: '*'
+
+  lodash.merge@4.6.2:
+    resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==, tarball: https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz}
+
+  lodash@4.17.21:
+    resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==, tarball: https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz}
+
+  lru-cache@5.1.1:
+    resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, tarball: https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz}
+
+  magic-string@0.30.11:
+    resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==, tarball: https://registry.npmmirror.com/magic-string/-/magic-string-0.30.11.tgz}
+
+  mdn-data@2.0.30:
+    resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==, tarball: https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.30.tgz}
+
+  memoize-one@6.0.0:
+    resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==, tarball: https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz}
+
+  memorystream@0.3.1:
+    resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==, tarball: https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz}
+    engines: {node: '>= 0.10.0'}
+
+  merge-stream@2.0.0:
+    resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==, tarball: https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz}
+
+  merge2@1.4.1:
+    resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==, tarball: https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz}
+    engines: {node: '>= 8'}
+
+  micromatch@4.0.7:
+    resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==, tarball: https://registry.npmmirror.com/micromatch/-/micromatch-4.0.7.tgz}
+    engines: {node: '>=8.6'}
+
+  mime-db@1.52.0:
+    resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, tarball: https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz}
+    engines: {node: '>= 0.6'}
+
+  mime-types@2.1.35:
+    resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==, tarball: https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz}
+    engines: {node: '>= 0.6'}
+
+  mimic-fn@2.1.0:
+    resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==, tarball: https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz}
+    engines: {node: '>=6'}
+
+  minimatch@3.1.2:
+    resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==, tarball: https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz}
+
+  minimatch@9.0.5:
+    resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==, tarball: https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz}
+    engines: {node: '>=16 || 14 >=14.17'}
+
+  mitt@3.0.1:
+    resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==, tarball: https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz}
+
+  mlly@1.7.1:
+    resolution: {integrity: sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==, tarball: https://registry.npmmirror.com/mlly/-/mlly-1.7.1.tgz}
+
+  mrmime@2.0.0:
+    resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==, tarball: https://registry.npmmirror.com/mrmime/-/mrmime-2.0.0.tgz}
+    engines: {node: '>=10'}
+
+  ms@2.1.2:
+    resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==, tarball: https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz}
+
+  muggle-string@0.4.1:
+    resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==, tarball: https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz}
+
+  nanoid@3.3.7:
+    resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==, tarball: https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+
+  natural-compare@1.4.0:
+    resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, tarball: https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz}
+
+  node-fetch-native@1.6.4:
+    resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==, tarball: https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.4.tgz}
+
+  node-releases@2.0.18:
+    resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==, tarball: https://registry.npmmirror.com/node-releases/-/node-releases-2.0.18.tgz}
+
+  normalize-path@3.0.0:
+    resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==, tarball: https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz}
+    engines: {node: '>=0.10.0'}
+
+  normalize-wheel-es@1.2.0:
+    resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==, tarball: https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz}
+
+  normalize.css@8.0.1:
+    resolution: {integrity: sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==, tarball: https://registry.npmmirror.com/normalize.css/-/normalize.css-8.0.1.tgz}
+
+  npm-normalize-package-bin@3.0.1:
+    resolution: {integrity: sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==, tarball: https://registry.npmmirror.com/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz}
+    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+
+  npm-run-all2@6.2.2:
+    resolution: {integrity: sha512-Q+alQAGIW7ZhKcxLt8GcSi3h3ryheD6xnmXahkMRVM5LYmajcUrSITm8h+OPC9RYWMV2GR0Q1ntTUCfxaNoOJw==, tarball: https://registry.npmmirror.com/npm-run-all2/-/npm-run-all2-6.2.2.tgz}
+    engines: {node: ^14.18.0 || ^16.13.0 || >=18.0.0, npm: '>= 8'}
+    hasBin: true
+
+  npm-run-path@4.0.1:
+    resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==, tarball: https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz}
+    engines: {node: '>=8'}
+
+  nprogress@0.2.0:
+    resolution: {integrity: sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==, tarball: https://registry.npmmirror.com/nprogress/-/nprogress-0.2.0.tgz}
+
+  nth-check@2.1.1:
+    resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==, tarball: https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz}
+
+  ofetch@1.3.4:
+    resolution: {integrity: sha512-KLIET85ik3vhEfS+3fDlc/BAZiAp+43QEC/yCo5zkNoY2YaKvNkOaFr/6wCFgFH1kuYQM5pMNi0Tg8koiIemtw==, tarball: https://registry.npmmirror.com/ofetch/-/ofetch-1.3.4.tgz}
+
+  once@1.4.0:
+    resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, tarball: https://registry.npmmirror.com/once/-/once-1.4.0.tgz}
+
+  onetime@5.1.2:
+    resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==, tarball: https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz}
+    engines: {node: '>=6'}
+
+  optionator@0.9.4:
+    resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, tarball: https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz}
+    engines: {node: '>= 0.8.0'}
+
+  p-limit@3.1.0:
+    resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, tarball: https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz}
+    engines: {node: '>=10'}
+
+  p-locate@5.0.0:
+    resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, tarball: https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz}
+    engines: {node: '>=10'}
+
+  parent-module@1.0.1:
+    resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, tarball: https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz}
+    engines: {node: '>=6'}
+
+  path-browserify@1.0.1:
+    resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==, tarball: https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz}
+
+  path-exists@4.0.0:
+    resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, tarball: https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz}
+    engines: {node: '>=8'}
+
+  path-is-absolute@1.0.1:
+    resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==, tarball: https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz}
+    engines: {node: '>=0.10.0'}
+
+  path-key@3.1.1:
+    resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, tarball: https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz}
+    engines: {node: '>=8'}
+
+  path-to-regexp@7.1.0:
+    resolution: {integrity: sha512-ZToe+MbUF4lBqk6dV8GKot4DKfzrxXsplOddH8zN3YK+qw9/McvP7+4ICjZvOne0jQhN4eJwHsX6tT0Ns19fvw==, tarball: https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-7.1.0.tgz}
+    engines: {node: '>=16'}
+
+  path-type@4.0.0:
+    resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==, tarball: https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz}
+    engines: {node: '>=8'}
+
+  pathe@1.1.2:
+    resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==, tarball: https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz}
+
+  perfect-debounce@1.0.0:
+    resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==, tarball: https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz}
+
+  picocolors@1.0.1:
+    resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==, tarball: https://registry.npmmirror.com/picocolors/-/picocolors-1.0.1.tgz}
+
+  picomatch@2.3.1:
+    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==, tarball: https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz}
+    engines: {node: '>=8.6'}
+
+  pidtree@0.6.0:
+    resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==, tarball: https://registry.npmmirror.com/pidtree/-/pidtree-0.6.0.tgz}
+    engines: {node: '>=0.10'}
+    hasBin: true
+
+  pinia@2.2.0:
+    resolution: {integrity: sha512-iPrIh26GMqfpUlMOGyxuDowGmYousTecbTHFwT0xZ1zJvh23oQ+Cj99ZoPQA1TnUPhU6AuRPv6/drkTCJ0VHQA==, tarball: https://registry.npmmirror.com/pinia/-/pinia-2.2.0.tgz}
+    peerDependencies:
+      '@vue/composition-api': ^1.4.0
+      typescript: '>=4.4.4'
+      vue: ^2.6.14 || ^3.3.0
+    peerDependenciesMeta:
+      '@vue/composition-api':
+        optional: true
+      typescript:
+        optional: true
+
+  pkg-types@1.1.3:
+    resolution: {integrity: sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==, tarball: https://registry.npmmirror.com/pkg-types/-/pkg-types-1.1.3.tgz}
+
+  postcss-selector-parser@6.1.1:
+    resolution: {integrity: sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==, tarball: https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz}
+    engines: {node: '>=4'}
+
+  postcss@8.4.40:
+    resolution: {integrity: sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==, tarball: https://registry.npmmirror.com/postcss/-/postcss-8.4.40.tgz}
+    engines: {node: ^10 || ^12 || >=14}
+
+  prelude-ls@1.2.1:
+    resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, tarball: https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz}
+    engines: {node: '>= 0.8.0'}
+
+  prettier-linter-helpers@1.0.0:
+    resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==, tarball: https://registry.npmmirror.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz}
+    engines: {node: '>=6.0.0'}
+
+  prettier@3.3.3:
+    resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==, tarball: https://registry.npmmirror.com/prettier/-/prettier-3.3.3.tgz}
+    engines: {node: '>=14'}
+    hasBin: true
+
+  proxy-from-env@1.1.0:
+    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==, tarball: https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz}
+
+  punycode@2.3.1:
+    resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, tarball: https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz}
+    engines: {node: '>=6'}
+
+  queue-microtask@1.2.3:
+    resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, tarball: https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz}
+
+  read-package-json-fast@3.0.2:
+    resolution: {integrity: sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==, tarball: https://registry.npmmirror.com/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz}
+    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+
+  readdirp@3.6.0:
+    resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==, tarball: https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz}
+    engines: {node: '>=8.10.0'}
+
+  resolve-from@4.0.0:
+    resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, tarball: https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz}
+    engines: {node: '>=4'}
+
+  resolve-pkg-maps@1.0.0:
+    resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==, tarball: https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz}
+
+  reusify@1.0.4:
+    resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==, tarball: https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz}
+    engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+  rimraf@3.0.2:
+    resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==, tarball: https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz}
+    deprecated: Rimraf versions prior to v4 are no longer supported
+    hasBin: true
+
+  rollup@4.19.1:
+    resolution: {integrity: sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==, tarball: https://registry.npmmirror.com/rollup/-/rollup-4.19.1.tgz}
+    engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+    hasBin: true
+
+  run-parallel@1.2.0:
+    resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==, tarball: https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz}
+
+  sass@1.77.8:
+    resolution: {integrity: sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==, tarball: https://registry.npmmirror.com/sass/-/sass-1.77.8.tgz}
+    engines: {node: '>=14.0.0'}
+    hasBin: true
+
+  screenfull@6.0.2:
+    resolution: {integrity: sha512-AQdy8s4WhNvUZ6P8F6PB21tSPIYKniic+Ogx0AacBMjKP1GUHN2E9URxQHtCusiwxudnCKkdy4GrHXPPJSkCCw==, tarball: https://registry.npmmirror.com/screenfull/-/screenfull-6.0.2.tgz}
+    engines: {node: ^14.13.1 || >=16.0.0}
+
+  semver@6.3.1:
+    resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, tarball: https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz}
+    hasBin: true
+
+  semver@7.6.3:
+    resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==, tarball: https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz}
+    engines: {node: '>=10'}
+    hasBin: true
+
+  shebang-command@2.0.0:
+    resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, tarball: https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz}
+    engines: {node: '>=8'}
+
+  shebang-regex@3.0.0:
+    resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, tarball: https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz}
+    engines: {node: '>=8'}
+
+  shell-quote@1.8.1:
+    resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==, tarball: https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.1.tgz}
+
+  signal-exit@3.0.7:
+    resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==, tarball: https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz}
+
+  sirv@2.0.4:
+    resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==, tarball: https://registry.npmmirror.com/sirv/-/sirv-2.0.4.tgz}
+    engines: {node: '>= 10'}
+
+  slash@3.0.0:
+    resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==, tarball: https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz}
+    engines: {node: '>=8'}
+
+  source-map-js@1.2.0:
+    resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==, tarball: https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.0.tgz}
+    engines: {node: '>=0.10.0'}
+
+  strip-ansi@6.0.1:
+    resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, tarball: https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz}
+    engines: {node: '>=8'}
+
+  strip-final-newline@2.0.0:
+    resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==, tarball: https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz}
+    engines: {node: '>=6'}
+
+  strip-json-comments@3.1.1:
+    resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, tarball: https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz}
+    engines: {node: '>=8'}
+
+  supports-color@5.5.0:
+    resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==, tarball: https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz}
+    engines: {node: '>=4'}
+
+  supports-color@7.2.0:
+    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==, tarball: https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz}
+    engines: {node: '>=8'}
+
+  synckit@0.9.1:
+    resolution: {integrity: sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==, tarball: https://registry.npmmirror.com/synckit/-/synckit-0.9.1.tgz}
+    engines: {node: ^14.18.0 || >=16.0.0}
+
+  text-table@0.2.0:
+    resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==, tarball: https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz}
+
+  to-fast-properties@2.0.0:
+    resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==, tarball: https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz}
+    engines: {node: '>=4'}
+
+  to-regex-range@5.0.1:
+    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, tarball: https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz}
+    engines: {node: '>=8.0'}
+
+  totalist@3.0.1:
+    resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==, tarball: https://registry.npmmirror.com/totalist/-/totalist-3.0.1.tgz}
+    engines: {node: '>=6'}
+
+  ts-api-utils@1.3.0:
+    resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==, tarball: https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz}
+    engines: {node: '>=16'}
+    peerDependencies:
+      typescript: '>=4.2.0'
+
+  tslib@2.6.3:
+    resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==, tarball: https://registry.npmmirror.com/tslib/-/tslib-2.6.3.tgz}
+
+  tsx@4.16.4:
+    resolution: {integrity: sha512-E0EDobc7FtOxdNtG0ZQWztLa9PK/TqC5QvdV0heyIMJySwcJ8vyvziOznzO1MIM2IDfacLGBgfiCUmba6mvI7Q==, tarball: https://registry.npmmirror.com/tsx/-/tsx-4.16.4.tgz}
+    engines: {node: '>=18.0.0'}
+    hasBin: true
+
+  type-check@0.4.0:
+    resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, tarball: https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz}
+    engines: {node: '>= 0.8.0'}
+
+  type-fest@0.20.2:
+    resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==, tarball: https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz}
+    engines: {node: '>=10'}
+
+  typescript@5.4.5:
+    resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==, tarball: https://registry.npmmirror.com/typescript/-/typescript-5.4.5.tgz}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+  ufo@1.5.4:
+    resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==, tarball: https://registry.npmmirror.com/ufo/-/ufo-1.5.4.tgz}
+
+  unconfig@0.5.5:
+    resolution: {integrity: sha512-VQZ5PT9HDX+qag0XdgQi8tJepPhXiR/yVOkn707gJDKo31lGjRilPREiQJ9Z6zd/Ugpv6ZvO5VxVIcatldYcNQ==, tarball: https://registry.npmmirror.com/unconfig/-/unconfig-0.5.5.tgz}
+
+  undici-types@5.26.5:
+    resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==, tarball: https://registry.npmmirror.com/undici-types/-/undici-types-5.26.5.tgz}
+
+  unocss@0.61.9:
+    resolution: {integrity: sha512-D7nEObT1lhCUwXU5MoQ2Msh5S5g1EHVVSqDNM2ODs6dqWSboDCsRTPZQiyQmV9vCobrjYcvAFno9ZAgO7pvurw==, tarball: https://registry.npmmirror.com/unocss/-/unocss-0.61.9.tgz}
+    engines: {node: '>=14'}
+    peerDependencies:
+      '@unocss/webpack': 0.61.9
+      vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0 || ^5.0.0-0
+    peerDependenciesMeta:
+      '@unocss/webpack':
+        optional: true
+      vite:
+        optional: true
+
+  update-browserslist-db@1.1.0:
+    resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==, tarball: https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz}
+    hasBin: true
+    peerDependencies:
+      browserslist: '>= 4.21.0'
+
+  uri-js@4.4.1:
+    resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, tarball: https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz}
+
+  util-deprecate@1.0.2:
+    resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, tarball: https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz}
+
+  vite@5.3.5:
+    resolution: {integrity: sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==, tarball: https://registry.npmmirror.com/vite/-/vite-5.3.5.tgz}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    hasBin: true
+    peerDependencies:
+      '@types/node': ^18.0.0 || >=20.0.0
+      less: '*'
+      lightningcss: ^1.21.0
+      sass: '*'
+      stylus: '*'
+      sugarss: '*'
+      terser: ^5.4.0
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+      less:
+        optional: true
+      lightningcss:
+        optional: true
+      sass:
+        optional: true
+      stylus:
+        optional: true
+      sugarss:
+        optional: true
+      terser:
+        optional: true
+
+  vscode-uri@3.0.8:
+    resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==, tarball: https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.0.8.tgz}
+
+  vue-demi@0.14.10:
+    resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==, tarball: https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz}
+    engines: {node: '>=12'}
+    hasBin: true
+    peerDependencies:
+      '@vue/composition-api': ^1.0.0-rc.1
+      vue: ^3.0.0-0 || ^2.6.0
+    peerDependenciesMeta:
+      '@vue/composition-api':
+        optional: true
+
+  vue-eslint-parser@9.4.3:
+    resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==, tarball: https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz}
+    engines: {node: ^14.17.0 || >=16.0.0}
+    peerDependencies:
+      eslint: '>=6.0.0'
+
+  vue-router@4.4.1:
+    resolution: {integrity: sha512-njTLt/6gYGgIhv+U8nc5J6JpJpntFgy4fptRJ9Dp2qWQRo/PekB5DbKRYRPt0kM6feXysPKl7A5BjOmOJL5Ttw==, tarball: https://registry.npmmirror.com/vue-router/-/vue-router-4.4.1.tgz}
+    peerDependencies:
+      vue: ^3.2.0
+
+  vue-tsc@2.0.29:
+    resolution: {integrity: sha512-MHhsfyxO3mYShZCGYNziSbc63x7cQ5g9kvijV7dRe1TTXBRLxXyL0FnXWpUF1xII2mJ86mwYpYsUmMwkmerq7Q==, tarball: https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.0.29.tgz}
+    hasBin: true
+    peerDependencies:
+      typescript: '>=5.0.0'
+
+  vue@3.4.35:
+    resolution: {integrity: sha512-+fl/GLmI4GPileHftVlCdB7fUL4aziPcqTudpTGXCT8s+iZWuOCeNEB5haX6Uz2IpRrbEXOgIFbe+XciCuGbNQ==, tarball: https://registry.npmmirror.com/vue/-/vue-3.4.35.tgz}
+    peerDependencies:
+      typescript: '*'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  vxe-table-plugin-element@4.0.4:
+    resolution: {integrity: sha512-TKhx4Bi45kdXRa9PyjdDtOjQuA9We+ASDdZez/d3DB7p2tFvBwVNnyvwYHT+kJntAeq0pOchmhsYhZeVADMAuw==, tarball: https://registry.npmmirror.com/vxe-table-plugin-element/-/vxe-table-plugin-element-4.0.4.tgz}
+    peerDependencies:
+      vxe-table: ^4.5.0
+
+  vxe-table@4.6.18:
+    resolution: {integrity: sha512-CE3gMWH9eKMwPvs1qS/FS61DdcH5RwfDOu39pUTiMCEiunde/uuktWDNaT4TMhpkbid7yybA8JP4gsflAg/1cw==, tarball: https://registry.npmmirror.com/vxe-table/-/vxe-table-4.6.18.tgz}
+    peerDependencies:
+      vue: ^3.2.28
+
+  which@2.0.2:
+    resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, tarball: https://registry.npmmirror.com/which/-/which-2.0.2.tgz}
+    engines: {node: '>= 8'}
+    hasBin: true
+
+  word-wrap@1.2.5:
+    resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==, tarball: https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz}
+    engines: {node: '>=0.10.0'}
+
+  wrappy@1.0.2:
+    resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, tarball: https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz}
+
+  xe-utils@3.5.28:
+    resolution: {integrity: sha512-oeLLJ0b54QdOSSgYQ9TiKW/xAGrc9r0weCA/5UfyGdm3n3js4cNOuuf9Tml7UwgBQpl4uWMbMwUZKLh2yqPF3A==, tarball: https://registry.npmmirror.com/xe-utils/-/xe-utils-3.5.28.tgz}
+
+  xml-name-validator@4.0.0:
+    resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==, tarball: https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz}
+    engines: {node: '>=12'}
+
+  yallist@3.1.1:
+    resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, tarball: https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz}
+
+  yocto-queue@0.1.0:
+    resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, tarball: https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz}
+    engines: {node: '>=10'}
+
+snapshots:
+
+  '@ampproject/remapping@2.3.0':
+    dependencies:
+      '@jridgewell/gen-mapping': 0.3.5
+      '@jridgewell/trace-mapping': 0.3.25
+
+  '@antfu/install-pkg@0.1.1':
+    dependencies:
+      execa: 5.1.1
+      find-up: 5.0.0
+
+  '@antfu/utils@0.7.10': {}
+
+  '@babel/code-frame@7.24.7':
+    dependencies:
+      '@babel/highlight': 7.24.7
+      picocolors: 1.0.1
+
+  '@babel/compat-data@7.25.2': {}
+
+  '@babel/core@7.25.2':
+    dependencies:
+      '@ampproject/remapping': 2.3.0
+      '@babel/code-frame': 7.24.7
+      '@babel/generator': 7.25.0
+      '@babel/helper-compilation-targets': 7.25.2
+      '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2)
+      '@babel/helpers': 7.25.0
+      '@babel/parser': 7.25.3
+      '@babel/template': 7.25.0
+      '@babel/traverse': 7.25.3
+      '@babel/types': 7.25.2
+      convert-source-map: 2.0.0
+      debug: 4.3.6
+      gensync: 1.0.0-beta.2
+      json5: 2.2.3
+      semver: 6.3.1
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/generator@7.25.0':
+    dependencies:
+      '@babel/types': 7.25.2
+      '@jridgewell/gen-mapping': 0.3.5
+      '@jridgewell/trace-mapping': 0.3.25
+      jsesc: 2.5.2
+
+  '@babel/helper-annotate-as-pure@7.24.7':
+    dependencies:
+      '@babel/types': 7.25.2
+
+  '@babel/helper-compilation-targets@7.25.2':
+    dependencies:
+      '@babel/compat-data': 7.25.2
+      '@babel/helper-validator-option': 7.24.8
+      browserslist: 4.23.2
+      lru-cache: 5.1.1
+      semver: 6.3.1
+
+  '@babel/helper-create-class-features-plugin@7.25.0(@babel/core@7.25.2)':
+    dependencies:
+      '@babel/core': 7.25.2
+      '@babel/helper-annotate-as-pure': 7.24.7
+      '@babel/helper-member-expression-to-functions': 7.24.8
+      '@babel/helper-optimise-call-expression': 7.24.7
+      '@babel/helper-replace-supers': 7.25.0(@babel/core@7.25.2)
+      '@babel/helper-skip-transparent-expression-wrappers': 7.24.7
+      '@babel/traverse': 7.25.3
+      semver: 6.3.1
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/helper-member-expression-to-functions@7.24.8':
+    dependencies:
+      '@babel/traverse': 7.25.3
+      '@babel/types': 7.25.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/helper-module-imports@7.24.7':
+    dependencies:
+      '@babel/traverse': 7.25.3
+      '@babel/types': 7.25.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/helper-module-transforms@7.25.2(@babel/core@7.25.2)':
+    dependencies:
+      '@babel/core': 7.25.2
+      '@babel/helper-module-imports': 7.24.7
+      '@babel/helper-simple-access': 7.24.7
+      '@babel/helper-validator-identifier': 7.24.7
+      '@babel/traverse': 7.25.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/helper-optimise-call-expression@7.24.7':
+    dependencies:
+      '@babel/types': 7.25.2
+
+  '@babel/helper-plugin-utils@7.24.8': {}
+
+  '@babel/helper-replace-supers@7.25.0(@babel/core@7.25.2)':
+    dependencies:
+      '@babel/core': 7.25.2
+      '@babel/helper-member-expression-to-functions': 7.24.8
+      '@babel/helper-optimise-call-expression': 7.24.7
+      '@babel/traverse': 7.25.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/helper-simple-access@7.24.7':
+    dependencies:
+      '@babel/traverse': 7.25.3
+      '@babel/types': 7.25.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/helper-skip-transparent-expression-wrappers@7.24.7':
+    dependencies:
+      '@babel/traverse': 7.25.3
+      '@babel/types': 7.25.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/helper-string-parser@7.24.8': {}
+
+  '@babel/helper-validator-identifier@7.24.7': {}
+
+  '@babel/helper-validator-option@7.24.8': {}
+
+  '@babel/helpers@7.25.0':
+    dependencies:
+      '@babel/template': 7.25.0
+      '@babel/types': 7.25.2
+
+  '@babel/highlight@7.24.7':
+    dependencies:
+      '@babel/helper-validator-identifier': 7.24.7
+      chalk: 2.4.2
+      js-tokens: 4.0.0
+      picocolors: 1.0.1
+
+  '@babel/parser@7.25.3':
+    dependencies:
+      '@babel/types': 7.25.2
+
+  '@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.25.2)':
+    dependencies:
+      '@babel/core': 7.25.2
+      '@babel/helper-plugin-utils': 7.24.8
+
+  '@babel/plugin-syntax-typescript@7.24.7(@babel/core@7.25.2)':
+    dependencies:
+      '@babel/core': 7.25.2
+      '@babel/helper-plugin-utils': 7.24.8
+
+  '@babel/plugin-transform-modules-commonjs@7.24.8(@babel/core@7.25.2)':
+    dependencies:
+      '@babel/core': 7.25.2
+      '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2)
+      '@babel/helper-plugin-utils': 7.24.8
+      '@babel/helper-simple-access': 7.24.7
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/plugin-transform-typescript@7.25.2(@babel/core@7.25.2)':
+    dependencies:
+      '@babel/core': 7.25.2
+      '@babel/helper-annotate-as-pure': 7.24.7
+      '@babel/helper-create-class-features-plugin': 7.25.0(@babel/core@7.25.2)
+      '@babel/helper-plugin-utils': 7.24.8
+      '@babel/helper-skip-transparent-expression-wrappers': 7.24.7
+      '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.25.2)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/preset-typescript@7.24.7(@babel/core@7.25.2)':
+    dependencies:
+      '@babel/core': 7.25.2
+      '@babel/helper-plugin-utils': 7.24.8
+      '@babel/helper-validator-option': 7.24.8
+      '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2)
+      '@babel/plugin-transform-modules-commonjs': 7.24.8(@babel/core@7.25.2)
+      '@babel/plugin-transform-typescript': 7.25.2(@babel/core@7.25.2)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/template@7.25.0':
+    dependencies:
+      '@babel/code-frame': 7.24.7
+      '@babel/parser': 7.25.3
+      '@babel/types': 7.25.2
+
+  '@babel/traverse@7.25.3':
+    dependencies:
+      '@babel/code-frame': 7.24.7
+      '@babel/generator': 7.25.0
+      '@babel/parser': 7.25.3
+      '@babel/template': 7.25.0
+      '@babel/types': 7.25.2
+      debug: 4.3.6
+      globals: 11.12.0
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/types@7.25.2':
+    dependencies:
+      '@babel/helper-string-parser': 7.24.8
+      '@babel/helper-validator-identifier': 7.24.7
+      to-fast-properties: 2.0.0
+
+  '@ctrl/tinycolor@3.6.1': {}
+
+  '@element-plus/icons-vue@2.3.1(vue@3.4.35(typescript@5.4.5))':
+    dependencies:
+      vue: 3.4.35(typescript@5.4.5)
+
+  '@esbuild/aix-ppc64@0.21.5':
+    optional: true
+
+  '@esbuild/android-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/android-arm@0.21.5':
+    optional: true
+
+  '@esbuild/android-x64@0.21.5':
+    optional: true
+
+  '@esbuild/darwin-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/darwin-x64@0.21.5':
+    optional: true
+
+  '@esbuild/freebsd-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/freebsd-x64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-arm@0.21.5':
+    optional: true
+
+  '@esbuild/linux-ia32@0.21.5':
+    optional: true
+
+  '@esbuild/linux-loong64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-mips64el@0.21.5':
+    optional: true
+
+  '@esbuild/linux-ppc64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-riscv64@0.21.5':
+    optional: true
+
+  '@esbuild/linux-s390x@0.21.5':
+    optional: true
+
+  '@esbuild/linux-x64@0.21.5':
+    optional: true
+
+  '@esbuild/netbsd-x64@0.21.5':
+    optional: true
+
+  '@esbuild/openbsd-x64@0.21.5':
+    optional: true
+
+  '@esbuild/sunos-x64@0.21.5':
+    optional: true
+
+  '@esbuild/win32-arm64@0.21.5':
+    optional: true
+
+  '@esbuild/win32-ia32@0.21.5':
+    optional: true
+
+  '@esbuild/win32-x64@0.21.5':
+    optional: true
+
+  '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)':
+    dependencies:
+      eslint: 8.57.0
+      eslint-visitor-keys: 3.4.3
+
+  '@eslint-community/regexpp@4.11.0': {}
+
+  '@eslint/eslintrc@2.1.4':
+    dependencies:
+      ajv: 6.12.6
+      debug: 4.3.6
+      espree: 9.6.1
+      globals: 13.24.0
+      ignore: 5.3.1
+      import-fresh: 3.3.0
+      js-yaml: 4.1.0
+      minimatch: 3.1.2
+      strip-json-comments: 3.1.1
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/js@8.57.0': {}
+
+  '@floating-ui/core@1.6.5':
+    dependencies:
+      '@floating-ui/utils': 0.2.5
+
+  '@floating-ui/dom@1.6.8':
+    dependencies:
+      '@floating-ui/core': 1.6.5
+      '@floating-ui/utils': 0.2.5
+
+  '@floating-ui/utils@0.2.5': {}
+
+  '@humanwhocodes/config-array@0.11.14':
+    dependencies:
+      '@humanwhocodes/object-schema': 2.0.3
+      debug: 4.3.6
+      minimatch: 3.1.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@humanwhocodes/module-importer@1.0.1': {}
+
+  '@humanwhocodes/object-schema@2.0.3': {}
+
+  '@iconify/types@2.0.0': {}
+
+  '@iconify/utils@2.1.29':
+    dependencies:
+      '@antfu/install-pkg': 0.1.1
+      '@antfu/utils': 0.7.10
+      '@iconify/types': 2.0.0
+      debug: 4.3.6
+      kolorist: 1.8.0
+      local-pkg: 0.5.0
+      mlly: 1.7.1
+    transitivePeerDependencies:
+      - supports-color
+
+  '@jridgewell/gen-mapping@0.3.5':
+    dependencies:
+      '@jridgewell/set-array': 1.2.1
+      '@jridgewell/sourcemap-codec': 1.5.0
+      '@jridgewell/trace-mapping': 0.3.25
+
+  '@jridgewell/resolve-uri@3.1.2': {}
+
+  '@jridgewell/set-array@1.2.1': {}
+
+  '@jridgewell/sourcemap-codec@1.5.0': {}
+
+  '@jridgewell/trace-mapping@0.3.25':
+    dependencies:
+      '@jridgewell/resolve-uri': 3.1.2
+      '@jridgewell/sourcemap-codec': 1.5.0
+
+  '@nodelib/fs.scandir@2.1.5':
+    dependencies:
+      '@nodelib/fs.stat': 2.0.5
+      run-parallel: 1.2.0
+
+  '@nodelib/fs.stat@2.0.5': {}
+
+  '@nodelib/fs.walk@1.2.8':
+    dependencies:
+      '@nodelib/fs.scandir': 2.1.5
+      fastq: 1.17.1
+
+  '@pkgr/core@0.1.1': {}
+
+  '@polka/url@1.0.0-next.25': {}
+
+  '@rollup/pluginutils@5.1.0(rollup@4.19.1)':
+    dependencies:
+      '@types/estree': 1.0.5
+      estree-walker: 2.0.2
+      picomatch: 2.3.1
+    optionalDependencies:
+      rollup: 4.19.1
+
+  '@rollup/rollup-android-arm-eabi@4.19.1':
+    optional: true
+
+  '@rollup/rollup-android-arm64@4.19.1':
+    optional: true
+
+  '@rollup/rollup-darwin-arm64@4.19.1':
+    optional: true
+
+  '@rollup/rollup-darwin-x64@4.19.1':
+    optional: true
+
+  '@rollup/rollup-linux-arm-gnueabihf@4.19.1':
+    optional: true
+
+  '@rollup/rollup-linux-arm-musleabihf@4.19.1':
+    optional: true
+
+  '@rollup/rollup-linux-arm64-gnu@4.19.1':
+    optional: true
+
+  '@rollup/rollup-linux-arm64-musl@4.19.1':
+    optional: true
+
+  '@rollup/rollup-linux-powerpc64le-gnu@4.19.1':
+    optional: true
+
+  '@rollup/rollup-linux-riscv64-gnu@4.19.1':
+    optional: true
+
+  '@rollup/rollup-linux-s390x-gnu@4.19.1':
+    optional: true
+
+  '@rollup/rollup-linux-x64-gnu@4.19.1':
+    optional: true
+
+  '@rollup/rollup-linux-x64-musl@4.19.1':
+    optional: true
+
+  '@rollup/rollup-win32-arm64-msvc@4.19.1':
+    optional: true
+
+  '@rollup/rollup-win32-ia32-msvc@4.19.1':
+    optional: true
+
+  '@rollup/rollup-win32-x64-msvc@4.19.1':
+    optional: true
+
+  '@rushstack/eslint-patch@1.10.4': {}
+
+  '@sxzz/popperjs-es@2.11.7': {}
+
+  '@tsconfig/node20@20.1.4': {}
+
+  '@types/estree@1.0.5': {}
+
+  '@types/lodash-es@4.17.12':
+    dependencies:
+      '@types/lodash': 4.17.7
+
+  '@types/lodash@4.17.7': {}
+
+  '@types/node@20.14.13':
+    dependencies:
+      undici-types: 5.26.5
+
+  '@types/web-bluetooth@0.0.16': {}
+
+  '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)':
+    dependencies:
+      '@eslint-community/regexpp': 4.11.0
+      '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/scope-manager': 7.18.0
+      '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/visitor-keys': 7.18.0
+      eslint: 8.57.0
+      graphemer: 1.4.0
+      ignore: 5.3.1
+      natural-compare: 1.4.0
+      ts-api-utils: 1.3.0(typescript@5.4.5)
+    optionalDependencies:
+      typescript: 5.4.5
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5)':
+    dependencies:
+      '@typescript-eslint/scope-manager': 7.18.0
+      '@typescript-eslint/types': 7.18.0
+      '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.4.5)
+      '@typescript-eslint/visitor-keys': 7.18.0
+      debug: 4.3.6
+      eslint: 8.57.0
+    optionalDependencies:
+      typescript: 5.4.5
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/scope-manager@7.18.0':
+    dependencies:
+      '@typescript-eslint/types': 7.18.0
+      '@typescript-eslint/visitor-keys': 7.18.0
+
+  '@typescript-eslint/type-utils@7.18.0(eslint@8.57.0)(typescript@5.4.5)':
+    dependencies:
+      '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.4.5)
+      '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.4.5)
+      debug: 4.3.6
+      eslint: 8.57.0
+      ts-api-utils: 1.3.0(typescript@5.4.5)
+    optionalDependencies:
+      typescript: 5.4.5
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/types@7.18.0': {}
+
+  '@typescript-eslint/typescript-estree@7.18.0(typescript@5.4.5)':
+    dependencies:
+      '@typescript-eslint/types': 7.18.0
+      '@typescript-eslint/visitor-keys': 7.18.0
+      debug: 4.3.6
+      globby: 11.1.0
+      is-glob: 4.0.3
+      minimatch: 9.0.5
+      semver: 7.6.3
+      ts-api-utils: 1.3.0(typescript@5.4.5)
+    optionalDependencies:
+      typescript: 5.4.5
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/utils@7.18.0(eslint@8.57.0)(typescript@5.4.5)':
+    dependencies:
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+      '@typescript-eslint/scope-manager': 7.18.0
+      '@typescript-eslint/types': 7.18.0
+      '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.4.5)
+      eslint: 8.57.0
+    transitivePeerDependencies:
+      - supports-color
+      - typescript
+
+  '@typescript-eslint/visitor-keys@7.18.0':
+    dependencies:
+      '@typescript-eslint/types': 7.18.0
+      eslint-visitor-keys: 3.4.3
+
+  '@ungap/structured-clone@1.2.0': {}
+
+  '@unocss/astro@0.61.9(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.13)(sass@1.77.8))':
+    dependencies:
+      '@unocss/core': 0.61.9
+      '@unocss/reset': 0.61.9
+      '@unocss/vite': 0.61.9(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.13)(sass@1.77.8))
+    optionalDependencies:
+      vite: 5.3.5(@types/node@20.14.13)(sass@1.77.8)
+    transitivePeerDependencies:
+      - rollup
+      - supports-color
+
+  '@unocss/cli@0.61.9(rollup@4.19.1)':
+    dependencies:
+      '@ampproject/remapping': 2.3.0
+      '@rollup/pluginutils': 5.1.0(rollup@4.19.1)
+      '@unocss/config': 0.61.9
+      '@unocss/core': 0.61.9
+      '@unocss/preset-uno': 0.61.9
+      cac: 6.7.14
+      chokidar: 3.6.0
+      colorette: 2.0.20
+      consola: 3.2.3
+      fast-glob: 3.3.2
+      magic-string: 0.30.11
+      pathe: 1.1.2
+      perfect-debounce: 1.0.0
+    transitivePeerDependencies:
+      - rollup
+      - supports-color
+
+  '@unocss/config@0.61.9':
+    dependencies:
+      '@unocss/core': 0.61.9
+      unconfig: 0.5.5
+    transitivePeerDependencies:
+      - supports-color
+
+  '@unocss/core@0.61.9': {}
+
+  '@unocss/extractor-arbitrary-variants@0.61.9':
+    dependencies:
+      '@unocss/core': 0.61.9
+
+  '@unocss/inspector@0.61.9':
+    dependencies:
+      '@unocss/core': 0.61.9
+      '@unocss/rule-utils': 0.61.9
+      gzip-size: 6.0.0
+      sirv: 2.0.4
+
+  '@unocss/postcss@0.61.9(postcss@8.4.40)':
+    dependencies:
+      '@unocss/config': 0.61.9
+      '@unocss/core': 0.61.9
+      '@unocss/rule-utils': 0.61.9
+      css-tree: 2.3.1
+      fast-glob: 3.3.2
+      magic-string: 0.30.11
+      postcss: 8.4.40
+    transitivePeerDependencies:
+      - supports-color
+
+  '@unocss/preset-attributify@0.61.9':
+    dependencies:
+      '@unocss/core': 0.61.9
+
+  '@unocss/preset-icons@0.61.9':
+    dependencies:
+      '@iconify/utils': 2.1.29
+      '@unocss/core': 0.61.9
+      ofetch: 1.3.4
+    transitivePeerDependencies:
+      - supports-color
+
+  '@unocss/preset-mini@0.61.9':
+    dependencies:
+      '@unocss/core': 0.61.9
+      '@unocss/extractor-arbitrary-variants': 0.61.9
+      '@unocss/rule-utils': 0.61.9
+
+  '@unocss/preset-tagify@0.61.9':
+    dependencies:
+      '@unocss/core': 0.61.9
+
+  '@unocss/preset-typography@0.61.9':
+    dependencies:
+      '@unocss/core': 0.61.9
+      '@unocss/preset-mini': 0.61.9
+
+  '@unocss/preset-uno@0.61.9':
+    dependencies:
+      '@unocss/core': 0.61.9
+      '@unocss/preset-mini': 0.61.9
+      '@unocss/preset-wind': 0.61.9
+      '@unocss/rule-utils': 0.61.9
+
+  '@unocss/preset-web-fonts@0.61.9':
+    dependencies:
+      '@unocss/core': 0.61.9
+      ofetch: 1.3.4
+
+  '@unocss/preset-wind@0.61.9':
+    dependencies:
+      '@unocss/core': 0.61.9
+      '@unocss/preset-mini': 0.61.9
+      '@unocss/rule-utils': 0.61.9
+
+  '@unocss/reset@0.61.9': {}
+
+  '@unocss/rule-utils@0.61.9':
+    dependencies:
+      '@unocss/core': 0.61.9
+      magic-string: 0.30.11
+
+  '@unocss/scope@0.61.9': {}
+
+  '@unocss/transformer-attributify-jsx-babel@0.61.9':
+    dependencies:
+      '@babel/core': 7.25.2
+      '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2)
+      '@babel/preset-typescript': 7.24.7(@babel/core@7.25.2)
+      '@unocss/core': 0.61.9
+    transitivePeerDependencies:
+      - supports-color
+
+  '@unocss/transformer-attributify-jsx@0.61.9':
+    dependencies:
+      '@unocss/core': 0.61.9
+
+  '@unocss/transformer-compile-class@0.61.9':
+    dependencies:
+      '@unocss/core': 0.61.9
+
+  '@unocss/transformer-directives@0.61.9':
+    dependencies:
+      '@unocss/core': 0.61.9
+      '@unocss/rule-utils': 0.61.9
+      css-tree: 2.3.1
+
+  '@unocss/transformer-variant-group@0.61.9':
+    dependencies:
+      '@unocss/core': 0.61.9
+
+  '@unocss/vite@0.61.9(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.13)(sass@1.77.8))':
+    dependencies:
+      '@ampproject/remapping': 2.3.0
+      '@rollup/pluginutils': 5.1.0(rollup@4.19.1)
+      '@unocss/config': 0.61.9
+      '@unocss/core': 0.61.9
+      '@unocss/inspector': 0.61.9
+      '@unocss/scope': 0.61.9
+      '@unocss/transformer-directives': 0.61.9
+      chokidar: 3.6.0
+      fast-glob: 3.3.2
+      magic-string: 0.30.11
+      vite: 5.3.5(@types/node@20.14.13)(sass@1.77.8)
+    transitivePeerDependencies:
+      - rollup
+      - supports-color
+
+  '@vitejs/plugin-vue@5.1.1(vite@5.3.5(@types/node@20.14.13)(sass@1.77.8))(vue@3.4.35(typescript@5.4.5))':
+    dependencies:
+      vite: 5.3.5(@types/node@20.14.13)(sass@1.77.8)
+      vue: 3.4.35(typescript@5.4.5)
+
+  '@volar/language-core@2.4.0-alpha.18':
+    dependencies:
+      '@volar/source-map': 2.4.0-alpha.18
+
+  '@volar/source-map@2.4.0-alpha.18': {}
+
+  '@volar/typescript@2.4.0-alpha.18':
+    dependencies:
+      '@volar/language-core': 2.4.0-alpha.18
+      path-browserify: 1.0.1
+      vscode-uri: 3.0.8
+
+  '@vue/compiler-core@3.4.35':
+    dependencies:
+      '@babel/parser': 7.25.3
+      '@vue/shared': 3.4.35
+      entities: 4.5.0
+      estree-walker: 2.0.2
+      source-map-js: 1.2.0
+
+  '@vue/compiler-dom@3.4.35':
+    dependencies:
+      '@vue/compiler-core': 3.4.35
+      '@vue/shared': 3.4.35
+
+  '@vue/compiler-sfc@3.4.35':
+    dependencies:
+      '@babel/parser': 7.25.3
+      '@vue/compiler-core': 3.4.35
+      '@vue/compiler-dom': 3.4.35
+      '@vue/compiler-ssr': 3.4.35
+      '@vue/shared': 3.4.35
+      estree-walker: 2.0.2
+      magic-string: 0.30.11
+      postcss: 8.4.40
+      source-map-js: 1.2.0
+
+  '@vue/compiler-ssr@3.4.35':
+    dependencies:
+      '@vue/compiler-dom': 3.4.35
+      '@vue/shared': 3.4.35
+
+  '@vue/compiler-vue2@2.7.16':
+    dependencies:
+      de-indent: 1.0.2
+      he: 1.2.0
+
+  '@vue/devtools-api@6.6.3': {}
+
+  '@vue/eslint-config-prettier@9.0.0(eslint@8.57.0)(prettier@3.3.3)':
+    dependencies:
+      eslint: 8.57.0
+      eslint-config-prettier: 9.1.0(eslint@8.57.0)
+      eslint-plugin-prettier: 5.2.1(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.3.3)
+      prettier: 3.3.3
+    transitivePeerDependencies:
+      - '@types/eslint'
+
+  '@vue/eslint-config-typescript@13.0.0(eslint-plugin-vue@9.27.0(eslint@8.57.0))(eslint@8.57.0)(typescript@5.4.5)':
+    dependencies:
+      '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
+      '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.4.5)
+      eslint: 8.57.0
+      eslint-plugin-vue: 9.27.0(eslint@8.57.0)
+      vue-eslint-parser: 9.4.3(eslint@8.57.0)
+    optionalDependencies:
+      typescript: 5.4.5
+    transitivePeerDependencies:
+      - supports-color
+
+  '@vue/language-core@2.0.29(typescript@5.4.5)':
+    dependencies:
+      '@volar/language-core': 2.4.0-alpha.18
+      '@vue/compiler-dom': 3.4.35
+      '@vue/compiler-vue2': 2.7.16
+      '@vue/shared': 3.4.35
+      computeds: 0.0.1
+      minimatch: 9.0.5
+      muggle-string: 0.4.1
+      path-browserify: 1.0.1
+    optionalDependencies:
+      typescript: 5.4.5
+
+  '@vue/reactivity@3.4.35':
+    dependencies:
+      '@vue/shared': 3.4.35
+
+  '@vue/runtime-core@3.4.35':
+    dependencies:
+      '@vue/reactivity': 3.4.35
+      '@vue/shared': 3.4.35
+
+  '@vue/runtime-dom@3.4.35':
+    dependencies:
+      '@vue/reactivity': 3.4.35
+      '@vue/runtime-core': 3.4.35
+      '@vue/shared': 3.4.35
+      csstype: 3.1.3
+
+  '@vue/server-renderer@3.4.35(vue@3.4.35(typescript@5.4.5))':
+    dependencies:
+      '@vue/compiler-ssr': 3.4.35
+      '@vue/shared': 3.4.35
+      vue: 3.4.35(typescript@5.4.5)
+
+  '@vue/shared@3.4.35': {}
+
+  '@vue/tsconfig@0.5.1': {}
+
+  '@vueuse/core@9.13.0(vue@3.4.35(typescript@5.4.5))':
+    dependencies:
+      '@types/web-bluetooth': 0.0.16
+      '@vueuse/metadata': 9.13.0
+      '@vueuse/shared': 9.13.0(vue@3.4.35(typescript@5.4.5))
+      vue-demi: 0.14.10(vue@3.4.35(typescript@5.4.5))
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - vue
+
+  '@vueuse/metadata@9.13.0': {}
+
+  '@vueuse/shared@9.13.0(vue@3.4.35(typescript@5.4.5))':
+    dependencies:
+      vue-demi: 0.14.10(vue@3.4.35(typescript@5.4.5))
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - vue
+
+  acorn-jsx@5.3.2(acorn@8.12.1):
+    dependencies:
+      acorn: 8.12.1
+
+  acorn@8.12.1: {}
+
+  ajv@6.12.6:
+    dependencies:
+      fast-deep-equal: 3.1.3
+      fast-json-stable-stringify: 2.1.0
+      json-schema-traverse: 0.4.1
+      uri-js: 4.4.1
+
+  ansi-regex@5.0.1: {}
+
+  ansi-styles@3.2.1:
+    dependencies:
+      color-convert: 1.9.3
+
+  ansi-styles@4.3.0:
+    dependencies:
+      color-convert: 2.0.1
+
+  ansi-styles@6.2.1: {}
+
+  anymatch@3.1.3:
+    dependencies:
+      normalize-path: 3.0.0
+      picomatch: 2.3.1
+
+  argparse@2.0.1: {}
+
+  array-union@2.1.0: {}
+
+  async-validator@4.2.5: {}
+
+  asynckit@0.4.0: {}
+
+  axios@1.7.2:
+    dependencies:
+      follow-redirects: 1.15.6
+      form-data: 4.0.0
+      proxy-from-env: 1.1.0
+    transitivePeerDependencies:
+      - debug
+
+  balanced-match@1.0.2: {}
+
+  binary-extensions@2.3.0: {}
+
+  boolbase@1.0.0: {}
+
+  brace-expansion@1.1.11:
+    dependencies:
+      balanced-match: 1.0.2
+      concat-map: 0.0.1
+
+  brace-expansion@2.0.1:
+    dependencies:
+      balanced-match: 1.0.2
+
+  braces@3.0.3:
+    dependencies:
+      fill-range: 7.1.1
+
+  browserslist@4.23.2:
+    dependencies:
+      caniuse-lite: 1.0.30001646
+      electron-to-chromium: 1.5.4
+      node-releases: 2.0.18
+      update-browserslist-db: 1.1.0(browserslist@4.23.2)
+
+  bundle-require@5.0.0(esbuild@0.21.5):
+    dependencies:
+      esbuild: 0.21.5
+      load-tsconfig: 0.2.5
+
+  cac@6.7.14: {}
+
+  callsites@3.1.0: {}
+
+  caniuse-lite@1.0.30001646: {}
+
+  chalk@2.4.2:
+    dependencies:
+      ansi-styles: 3.2.1
+      escape-string-regexp: 1.0.5
+      supports-color: 5.5.0
+
+  chalk@4.1.2:
+    dependencies:
+      ansi-styles: 4.3.0
+      supports-color: 7.2.0
+
+  chokidar@3.6.0:
+    dependencies:
+      anymatch: 3.1.3
+      braces: 3.0.3
+      glob-parent: 5.1.2
+      is-binary-path: 2.1.0
+      is-glob: 4.0.3
+      normalize-path: 3.0.0
+      readdirp: 3.6.0
+    optionalDependencies:
+      fsevents: 2.3.3
+
+  color-convert@1.9.3:
+    dependencies:
+      color-name: 1.1.3
+
+  color-convert@2.0.1:
+    dependencies:
+      color-name: 1.1.4
+
+  color-name@1.1.3: {}
+
+  color-name@1.1.4: {}
+
+  colorette@2.0.20: {}
+
+  combined-stream@1.0.8:
+    dependencies:
+      delayed-stream: 1.0.0
+
+  computeds@0.0.1: {}
+
+  concat-map@0.0.1: {}
+
+  confbox@0.1.7: {}
+
+  consola@3.2.3: {}
+
+  convert-source-map@2.0.0: {}
+
+  cross-spawn@7.0.3:
+    dependencies:
+      path-key: 3.1.1
+      shebang-command: 2.0.0
+      which: 2.0.2
+
+  css-tree@2.3.1:
+    dependencies:
+      mdn-data: 2.0.30
+      source-map-js: 1.2.0
+
+  cssesc@3.0.0: {}
+
+  csstype@3.1.3: {}
+
+  dayjs@1.11.12: {}
+
+  de-indent@1.0.2: {}
+
+  debug@4.3.6:
+    dependencies:
+      ms: 2.1.2
+
+  deep-is@0.1.4: {}
+
+  defu@6.1.4: {}
+
+  delayed-stream@1.0.0: {}
+
+  destr@2.0.3: {}
+
+  dir-glob@3.0.1:
+    dependencies:
+      path-type: 4.0.0
+
+  doctrine@3.0.0:
+    dependencies:
+      esutils: 2.0.3
+
+  dom-zindex@1.0.4: {}
+
+  duplexer@0.1.2: {}
+
+  electron-to-chromium@1.5.4: {}
+
+  element-plus@2.7.8(vue@3.4.35(typescript@5.4.5)):
+    dependencies:
+      '@ctrl/tinycolor': 3.6.1
+      '@element-plus/icons-vue': 2.3.1(vue@3.4.35(typescript@5.4.5))
+      '@floating-ui/dom': 1.6.8
+      '@popperjs/core': '@sxzz/popperjs-es@2.11.7'
+      '@types/lodash': 4.17.7
+      '@types/lodash-es': 4.17.12
+      '@vueuse/core': 9.13.0(vue@3.4.35(typescript@5.4.5))
+      async-validator: 4.2.5
+      dayjs: 1.11.12
+      escape-html: 1.0.3
+      lodash: 4.17.21
+      lodash-es: 4.17.21
+      lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21)
+      memoize-one: 6.0.0
+      normalize-wheel-es: 1.2.0
+      vue: 3.4.35(typescript@5.4.5)
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+
+  entities@4.5.0: {}
+
+  esbuild@0.21.5:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.21.5
+      '@esbuild/android-arm': 0.21.5
+      '@esbuild/android-arm64': 0.21.5
+      '@esbuild/android-x64': 0.21.5
+      '@esbuild/darwin-arm64': 0.21.5
+      '@esbuild/darwin-x64': 0.21.5
+      '@esbuild/freebsd-arm64': 0.21.5
+      '@esbuild/freebsd-x64': 0.21.5
+      '@esbuild/linux-arm': 0.21.5
+      '@esbuild/linux-arm64': 0.21.5
+      '@esbuild/linux-ia32': 0.21.5
+      '@esbuild/linux-loong64': 0.21.5
+      '@esbuild/linux-mips64el': 0.21.5
+      '@esbuild/linux-ppc64': 0.21.5
+      '@esbuild/linux-riscv64': 0.21.5
+      '@esbuild/linux-s390x': 0.21.5
+      '@esbuild/linux-x64': 0.21.5
+      '@esbuild/netbsd-x64': 0.21.5
+      '@esbuild/openbsd-x64': 0.21.5
+      '@esbuild/sunos-x64': 0.21.5
+      '@esbuild/win32-arm64': 0.21.5
+      '@esbuild/win32-ia32': 0.21.5
+      '@esbuild/win32-x64': 0.21.5
+
+  escalade@3.1.2: {}
+
+  escape-html@1.0.3: {}
+
+  escape-string-regexp@1.0.5: {}
+
+  escape-string-regexp@4.0.0: {}
+
+  eslint-config-prettier@9.1.0(eslint@8.57.0):
+    dependencies:
+      eslint: 8.57.0
+
+  eslint-plugin-prettier@5.2.1(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.3.3):
+    dependencies:
+      eslint: 8.57.0
+      prettier: 3.3.3
+      prettier-linter-helpers: 1.0.0
+      synckit: 0.9.1
+    optionalDependencies:
+      eslint-config-prettier: 9.1.0(eslint@8.57.0)
+
+  eslint-plugin-vue@9.27.0(eslint@8.57.0):
+    dependencies:
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+      eslint: 8.57.0
+      globals: 13.24.0
+      natural-compare: 1.4.0
+      nth-check: 2.1.1
+      postcss-selector-parser: 6.1.1
+      semver: 7.6.3
+      vue-eslint-parser: 9.4.3(eslint@8.57.0)
+      xml-name-validator: 4.0.0
+    transitivePeerDependencies:
+      - supports-color
+
+  eslint-scope@7.2.2:
+    dependencies:
+      esrecurse: 4.3.0
+      estraverse: 5.3.0
+
+  eslint-visitor-keys@3.4.3: {}
+
+  eslint@8.57.0:
+    dependencies:
+      '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
+      '@eslint-community/regexpp': 4.11.0
+      '@eslint/eslintrc': 2.1.4
+      '@eslint/js': 8.57.0
+      '@humanwhocodes/config-array': 0.11.14
+      '@humanwhocodes/module-importer': 1.0.1
+      '@nodelib/fs.walk': 1.2.8
+      '@ungap/structured-clone': 1.2.0
+      ajv: 6.12.6
+      chalk: 4.1.2
+      cross-spawn: 7.0.3
+      debug: 4.3.6
+      doctrine: 3.0.0
+      escape-string-regexp: 4.0.0
+      eslint-scope: 7.2.2
+      eslint-visitor-keys: 3.4.3
+      espree: 9.6.1
+      esquery: 1.6.0
+      esutils: 2.0.3
+      fast-deep-equal: 3.1.3
+      file-entry-cache: 6.0.1
+      find-up: 5.0.0
+      glob-parent: 6.0.2
+      globals: 13.24.0
+      graphemer: 1.4.0
+      ignore: 5.3.1
+      imurmurhash: 0.1.4
+      is-glob: 4.0.3
+      is-path-inside: 3.0.3
+      js-yaml: 4.1.0
+      json-stable-stringify-without-jsonify: 1.0.1
+      levn: 0.4.1
+      lodash.merge: 4.6.2
+      minimatch: 3.1.2
+      natural-compare: 1.4.0
+      optionator: 0.9.4
+      strip-ansi: 6.0.1
+      text-table: 0.2.0
+    transitivePeerDependencies:
+      - supports-color
+
+  espree@9.6.1:
+    dependencies:
+      acorn: 8.12.1
+      acorn-jsx: 5.3.2(acorn@8.12.1)
+      eslint-visitor-keys: 3.4.3
+
+  esquery@1.6.0:
+    dependencies:
+      estraverse: 5.3.0
+
+  esrecurse@4.3.0:
+    dependencies:
+      estraverse: 5.3.0
+
+  estraverse@5.3.0: {}
+
+  estree-walker@2.0.2: {}
+
+  esutils@2.0.3: {}
+
+  execa@5.1.1:
+    dependencies:
+      cross-spawn: 7.0.3
+      get-stream: 6.0.1
+      human-signals: 2.1.0
+      is-stream: 2.0.1
+      merge-stream: 2.0.0
+      npm-run-path: 4.0.1
+      onetime: 5.1.2
+      signal-exit: 3.0.7
+      strip-final-newline: 2.0.0
+
+  fast-deep-equal@3.1.3: {}
+
+  fast-diff@1.3.0: {}
+
+  fast-glob@3.3.2:
+    dependencies:
+      '@nodelib/fs.stat': 2.0.5
+      '@nodelib/fs.walk': 1.2.8
+      glob-parent: 5.1.2
+      merge2: 1.4.1
+      micromatch: 4.0.7
+
+  fast-json-stable-stringify@2.1.0: {}
+
+  fast-levenshtein@2.0.6: {}
+
+  fastq@1.17.1:
+    dependencies:
+      reusify: 1.0.4
+
+  file-entry-cache@6.0.1:
+    dependencies:
+      flat-cache: 3.2.0
+
+  fill-range@7.1.1:
+    dependencies:
+      to-regex-range: 5.0.1
+
+  find-up@5.0.0:
+    dependencies:
+      locate-path: 6.0.0
+      path-exists: 4.0.0
+
+  flat-cache@3.2.0:
+    dependencies:
+      flatted: 3.3.1
+      keyv: 4.5.4
+      rimraf: 3.0.2
+
+  flatted@3.3.1: {}
+
+  follow-redirects@1.15.6: {}
+
+  form-data@4.0.0:
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      mime-types: 2.1.35
+
+  fs.realpath@1.0.0: {}
+
+  fsevents@2.3.3:
+    optional: true
+
+  gensync@1.0.0-beta.2: {}
+
+  get-stream@6.0.1: {}
+
+  get-tsconfig@4.7.6:
+    dependencies:
+      resolve-pkg-maps: 1.0.0
+
+  glob-parent@5.1.2:
+    dependencies:
+      is-glob: 4.0.3
+
+  glob-parent@6.0.2:
+    dependencies:
+      is-glob: 4.0.3
+
+  glob@7.2.3:
+    dependencies:
+      fs.realpath: 1.0.0
+      inflight: 1.0.6
+      inherits: 2.0.4
+      minimatch: 3.1.2
+      once: 1.4.0
+      path-is-absolute: 1.0.1
+
+  globals@11.12.0: {}
+
+  globals@13.24.0:
+    dependencies:
+      type-fest: 0.20.2
+
+  globby@11.1.0:
+    dependencies:
+      array-union: 2.1.0
+      dir-glob: 3.0.1
+      fast-glob: 3.3.2
+      ignore: 5.3.1
+      merge2: 1.4.1
+      slash: 3.0.0
+
+  graphemer@1.4.0: {}
+
+  gzip-size@6.0.0:
+    dependencies:
+      duplexer: 0.1.2
+
+  has-flag@3.0.0: {}
+
+  has-flag@4.0.0: {}
+
+  he@1.2.0: {}
+
+  human-signals@2.1.0: {}
+
+  ignore@5.3.1: {}
+
+  immutable@4.3.7: {}
+
+  import-fresh@3.3.0:
+    dependencies:
+      parent-module: 1.0.1
+      resolve-from: 4.0.0
+
+  importx@0.4.3:
+    dependencies:
+      bundle-require: 5.0.0(esbuild@0.21.5)
+      debug: 4.3.6
+      esbuild: 0.21.5
+      jiti: 2.0.0-beta.2
+      jiti-v1: jiti@1.21.6
+      pathe: 1.1.2
+      pkg-types: 1.1.3
+      tsx: 4.16.4
+    transitivePeerDependencies:
+      - supports-color
+
+  imurmurhash@0.1.4: {}
+
+  inflight@1.0.6:
+    dependencies:
+      once: 1.4.0
+      wrappy: 1.0.2
+
+  inherits@2.0.4: {}
+
+  is-binary-path@2.1.0:
+    dependencies:
+      binary-extensions: 2.3.0
+
+  is-extglob@2.1.1: {}
+
+  is-glob@4.0.3:
+    dependencies:
+      is-extglob: 2.1.1
+
+  is-number@7.0.0: {}
+
+  is-path-inside@3.0.3: {}
+
+  is-stream@2.0.1: {}
+
+  isexe@2.0.0: {}
+
+  jiti@1.21.6: {}
+
+  jiti@2.0.0-beta.2: {}
+
+  js-cookie@3.0.5: {}
+
+  js-tokens@4.0.0: {}
+
+  js-yaml@4.1.0:
+    dependencies:
+      argparse: 2.0.1
+
+  jsesc@2.5.2: {}
+
+  json-buffer@3.0.1: {}
+
+  json-parse-even-better-errors@3.0.2: {}
+
+  json-schema-traverse@0.4.1: {}
+
+  json-stable-stringify-without-jsonify@1.0.1: {}
+
+  json5@2.2.3: {}
+
+  keyv@4.5.4:
+    dependencies:
+      json-buffer: 3.0.1
+
+  kolorist@1.8.0: {}
+
+  levn@0.4.1:
+    dependencies:
+      prelude-ls: 1.2.1
+      type-check: 0.4.0
+
+  load-tsconfig@0.2.5: {}
+
+  local-pkg@0.5.0:
+    dependencies:
+      mlly: 1.7.1
+      pkg-types: 1.1.3
+
+  locate-path@6.0.0:
+    dependencies:
+      p-locate: 5.0.0
+
+  lodash-es@4.17.21: {}
+
+  lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21):
+    dependencies:
+      '@types/lodash-es': 4.17.12
+      lodash: 4.17.21
+      lodash-es: 4.17.21
+
+  lodash.merge@4.6.2: {}
+
+  lodash@4.17.21: {}
+
+  lru-cache@5.1.1:
+    dependencies:
+      yallist: 3.1.1
+
+  magic-string@0.30.11:
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.5.0
+
+  mdn-data@2.0.30: {}
+
+  memoize-one@6.0.0: {}
+
+  memorystream@0.3.1: {}
+
+  merge-stream@2.0.0: {}
+
+  merge2@1.4.1: {}
+
+  micromatch@4.0.7:
+    dependencies:
+      braces: 3.0.3
+      picomatch: 2.3.1
+
+  mime-db@1.52.0: {}
+
+  mime-types@2.1.35:
+    dependencies:
+      mime-db: 1.52.0
+
+  mimic-fn@2.1.0: {}
+
+  minimatch@3.1.2:
+    dependencies:
+      brace-expansion: 1.1.11
+
+  minimatch@9.0.5:
+    dependencies:
+      brace-expansion: 2.0.1
+
+  mitt@3.0.1: {}
+
+  mlly@1.7.1:
+    dependencies:
+      acorn: 8.12.1
+      pathe: 1.1.2
+      pkg-types: 1.1.3
+      ufo: 1.5.4
+
+  mrmime@2.0.0: {}
+
+  ms@2.1.2: {}
+
+  muggle-string@0.4.1: {}
+
+  nanoid@3.3.7: {}
+
+  natural-compare@1.4.0: {}
+
+  node-fetch-native@1.6.4: {}
+
+  node-releases@2.0.18: {}
+
+  normalize-path@3.0.0: {}
+
+  normalize-wheel-es@1.2.0: {}
+
+  normalize.css@8.0.1: {}
+
+  npm-normalize-package-bin@3.0.1: {}
+
+  npm-run-all2@6.2.2:
+    dependencies:
+      ansi-styles: 6.2.1
+      cross-spawn: 7.0.3
+      memorystream: 0.3.1
+      minimatch: 9.0.5
+      pidtree: 0.6.0
+      read-package-json-fast: 3.0.2
+      shell-quote: 1.8.1
+
+  npm-run-path@4.0.1:
+    dependencies:
+      path-key: 3.1.1
+
+  nprogress@0.2.0: {}
+
+  nth-check@2.1.1:
+    dependencies:
+      boolbase: 1.0.0
+
+  ofetch@1.3.4:
+    dependencies:
+      destr: 2.0.3
+      node-fetch-native: 1.6.4
+      ufo: 1.5.4
+
+  once@1.4.0:
+    dependencies:
+      wrappy: 1.0.2
+
+  onetime@5.1.2:
+    dependencies:
+      mimic-fn: 2.1.0
+
+  optionator@0.9.4:
+    dependencies:
+      deep-is: 0.1.4
+      fast-levenshtein: 2.0.6
+      levn: 0.4.1
+      prelude-ls: 1.2.1
+      type-check: 0.4.0
+      word-wrap: 1.2.5
+
+  p-limit@3.1.0:
+    dependencies:
+      yocto-queue: 0.1.0
+
+  p-locate@5.0.0:
+    dependencies:
+      p-limit: 3.1.0
+
+  parent-module@1.0.1:
+    dependencies:
+      callsites: 3.1.0
+
+  path-browserify@1.0.1: {}
+
+  path-exists@4.0.0: {}
+
+  path-is-absolute@1.0.1: {}
+
+  path-key@3.1.1: {}
+
+  path-to-regexp@7.1.0: {}
+
+  path-type@4.0.0: {}
+
+  pathe@1.1.2: {}
+
+  perfect-debounce@1.0.0: {}
+
+  picocolors@1.0.1: {}
+
+  picomatch@2.3.1: {}
+
+  pidtree@0.6.0: {}
+
+  pinia@2.2.0(typescript@5.4.5)(vue@3.4.35(typescript@5.4.5)):
+    dependencies:
+      '@vue/devtools-api': 6.6.3
+      vue: 3.4.35(typescript@5.4.5)
+      vue-demi: 0.14.10(vue@3.4.35(typescript@5.4.5))
+    optionalDependencies:
+      typescript: 5.4.5
+
+  pkg-types@1.1.3:
+    dependencies:
+      confbox: 0.1.7
+      mlly: 1.7.1
+      pathe: 1.1.2
+
+  postcss-selector-parser@6.1.1:
+    dependencies:
+      cssesc: 3.0.0
+      util-deprecate: 1.0.2
+
+  postcss@8.4.40:
+    dependencies:
+      nanoid: 3.3.7
+      picocolors: 1.0.1
+      source-map-js: 1.2.0
+
+  prelude-ls@1.2.1: {}
+
+  prettier-linter-helpers@1.0.0:
+    dependencies:
+      fast-diff: 1.3.0
+
+  prettier@3.3.3: {}
+
+  proxy-from-env@1.1.0: {}
+
+  punycode@2.3.1: {}
+
+  queue-microtask@1.2.3: {}
+
+  read-package-json-fast@3.0.2:
+    dependencies:
+      json-parse-even-better-errors: 3.0.2
+      npm-normalize-package-bin: 3.0.1
+
+  readdirp@3.6.0:
+    dependencies:
+      picomatch: 2.3.1
+
+  resolve-from@4.0.0: {}
+
+  resolve-pkg-maps@1.0.0: {}
+
+  reusify@1.0.4: {}
+
+  rimraf@3.0.2:
+    dependencies:
+      glob: 7.2.3
+
+  rollup@4.19.1:
+    dependencies:
+      '@types/estree': 1.0.5
+    optionalDependencies:
+      '@rollup/rollup-android-arm-eabi': 4.19.1
+      '@rollup/rollup-android-arm64': 4.19.1
+      '@rollup/rollup-darwin-arm64': 4.19.1
+      '@rollup/rollup-darwin-x64': 4.19.1
+      '@rollup/rollup-linux-arm-gnueabihf': 4.19.1
+      '@rollup/rollup-linux-arm-musleabihf': 4.19.1
+      '@rollup/rollup-linux-arm64-gnu': 4.19.1
+      '@rollup/rollup-linux-arm64-musl': 4.19.1
+      '@rollup/rollup-linux-powerpc64le-gnu': 4.19.1
+      '@rollup/rollup-linux-riscv64-gnu': 4.19.1
+      '@rollup/rollup-linux-s390x-gnu': 4.19.1
+      '@rollup/rollup-linux-x64-gnu': 4.19.1
+      '@rollup/rollup-linux-x64-musl': 4.19.1
+      '@rollup/rollup-win32-arm64-msvc': 4.19.1
+      '@rollup/rollup-win32-ia32-msvc': 4.19.1
+      '@rollup/rollup-win32-x64-msvc': 4.19.1
+      fsevents: 2.3.3
+
+  run-parallel@1.2.0:
+    dependencies:
+      queue-microtask: 1.2.3
+
+  sass@1.77.8:
+    dependencies:
+      chokidar: 3.6.0
+      immutable: 4.3.7
+      source-map-js: 1.2.0
+
+  screenfull@6.0.2: {}
+
+  semver@6.3.1: {}
+
+  semver@7.6.3: {}
+
+  shebang-command@2.0.0:
+    dependencies:
+      shebang-regex: 3.0.0
+
+  shebang-regex@3.0.0: {}
+
+  shell-quote@1.8.1: {}
+
+  signal-exit@3.0.7: {}
+
+  sirv@2.0.4:
+    dependencies:
+      '@polka/url': 1.0.0-next.25
+      mrmime: 2.0.0
+      totalist: 3.0.1
+
+  slash@3.0.0: {}
+
+  source-map-js@1.2.0: {}
+
+  strip-ansi@6.0.1:
+    dependencies:
+      ansi-regex: 5.0.1
+
+  strip-final-newline@2.0.0: {}
+
+  strip-json-comments@3.1.1: {}
+
+  supports-color@5.5.0:
+    dependencies:
+      has-flag: 3.0.0
+
+  supports-color@7.2.0:
+    dependencies:
+      has-flag: 4.0.0
+
+  synckit@0.9.1:
+    dependencies:
+      '@pkgr/core': 0.1.1
+      tslib: 2.6.3
+
+  text-table@0.2.0: {}
+
+  to-fast-properties@2.0.0: {}
+
+  to-regex-range@5.0.1:
+    dependencies:
+      is-number: 7.0.0
+
+  totalist@3.0.1: {}
+
+  ts-api-utils@1.3.0(typescript@5.4.5):
+    dependencies:
+      typescript: 5.4.5
+
+  tslib@2.6.3: {}
+
+  tsx@4.16.4:
+    dependencies:
+      esbuild: 0.21.5
+      get-tsconfig: 4.7.6
+    optionalDependencies:
+      fsevents: 2.3.3
+
+  type-check@0.4.0:
+    dependencies:
+      prelude-ls: 1.2.1
+
+  type-fest@0.20.2: {}
+
+  typescript@5.4.5: {}
+
+  ufo@1.5.4: {}
+
+  unconfig@0.5.5:
+    dependencies:
+      '@antfu/utils': 0.7.10
+      defu: 6.1.4
+      importx: 0.4.3
+    transitivePeerDependencies:
+      - supports-color
+
+  undici-types@5.26.5: {}
+
+  unocss@0.61.9(postcss@8.4.40)(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.13)(sass@1.77.8)):
+    dependencies:
+      '@unocss/astro': 0.61.9(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.13)(sass@1.77.8))
+      '@unocss/cli': 0.61.9(rollup@4.19.1)
+      '@unocss/core': 0.61.9
+      '@unocss/extractor-arbitrary-variants': 0.61.9
+      '@unocss/postcss': 0.61.9(postcss@8.4.40)
+      '@unocss/preset-attributify': 0.61.9
+      '@unocss/preset-icons': 0.61.9
+      '@unocss/preset-mini': 0.61.9
+      '@unocss/preset-tagify': 0.61.9
+      '@unocss/preset-typography': 0.61.9
+      '@unocss/preset-uno': 0.61.9
+      '@unocss/preset-web-fonts': 0.61.9
+      '@unocss/preset-wind': 0.61.9
+      '@unocss/reset': 0.61.9
+      '@unocss/transformer-attributify-jsx': 0.61.9
+      '@unocss/transformer-attributify-jsx-babel': 0.61.9
+      '@unocss/transformer-compile-class': 0.61.9
+      '@unocss/transformer-directives': 0.61.9
+      '@unocss/transformer-variant-group': 0.61.9
+      '@unocss/vite': 0.61.9(rollup@4.19.1)(vite@5.3.5(@types/node@20.14.13)(sass@1.77.8))
+    optionalDependencies:
+      vite: 5.3.5(@types/node@20.14.13)(sass@1.77.8)
+    transitivePeerDependencies:
+      - postcss
+      - rollup
+      - supports-color
+
+  update-browserslist-db@1.1.0(browserslist@4.23.2):
+    dependencies:
+      browserslist: 4.23.2
+      escalade: 3.1.2
+      picocolors: 1.0.1
+
+  uri-js@4.4.1:
+    dependencies:
+      punycode: 2.3.1
+
+  util-deprecate@1.0.2: {}
+
+  vite@5.3.5(@types/node@20.14.13)(sass@1.77.8):
+    dependencies:
+      esbuild: 0.21.5
+      postcss: 8.4.40
+      rollup: 4.19.1
+    optionalDependencies:
+      '@types/node': 20.14.13
+      fsevents: 2.3.3
+      sass: 1.77.8
+
+  vscode-uri@3.0.8: {}
+
+  vue-demi@0.14.10(vue@3.4.35(typescript@5.4.5)):
+    dependencies:
+      vue: 3.4.35(typescript@5.4.5)
+
+  vue-eslint-parser@9.4.3(eslint@8.57.0):
+    dependencies:
+      debug: 4.3.6
+      eslint: 8.57.0
+      eslint-scope: 7.2.2
+      eslint-visitor-keys: 3.4.3
+      espree: 9.6.1
+      esquery: 1.6.0
+      lodash: 4.17.21
+      semver: 7.6.3
+    transitivePeerDependencies:
+      - supports-color
+
+  vue-router@4.4.1(vue@3.4.35(typescript@5.4.5)):
+    dependencies:
+      '@vue/devtools-api': 6.6.3
+      vue: 3.4.35(typescript@5.4.5)
+
+  vue-tsc@2.0.29(typescript@5.4.5):
+    dependencies:
+      '@volar/typescript': 2.4.0-alpha.18
+      '@vue/language-core': 2.0.29(typescript@5.4.5)
+      semver: 7.6.3
+      typescript: 5.4.5
+
+  vue@3.4.35(typescript@5.4.5):
+    dependencies:
+      '@vue/compiler-dom': 3.4.35
+      '@vue/compiler-sfc': 3.4.35
+      '@vue/runtime-dom': 3.4.35
+      '@vue/server-renderer': 3.4.35(vue@3.4.35(typescript@5.4.5))
+      '@vue/shared': 3.4.35
+    optionalDependencies:
+      typescript: 5.4.5
+
+  vxe-table-plugin-element@4.0.4(vxe-table@4.6.18(vue@3.4.35(typescript@5.4.5))):
+    dependencies:
+      vxe-table: 4.6.18(vue@3.4.35(typescript@5.4.5))
+
+  vxe-table@4.6.18(vue@3.4.35(typescript@5.4.5)):
+    dependencies:
+      dom-zindex: 1.0.4
+      vue: 3.4.35(typescript@5.4.5)
+      xe-utils: 3.5.28
+
+  which@2.0.2:
+    dependencies:
+      isexe: 2.0.0
+
+  word-wrap@1.2.5: {}
+
+  wrappy@1.0.2: {}
+
+  xe-utils@3.5.28: {}
+
+  xml-name-validator@4.0.0: {}
+
+  yallist@3.1.1: {}
+
+  yocto-queue@0.1.0: {}

BIN
public/favicon.ico


+ 17 - 0
src/App.vue

@@ -0,0 +1,17 @@
+<script lang="ts" setup>
+import { useTheme } from "@/hooks/useTheme"
+import { ElNotification } from "element-plus"
+// 将 Element Plus 的语言设置为中文
+import zhCn from "element-plus/es/locale/lang/zh-cn"
+
+const { initTheme } = useTheme()
+
+/** 初始化主题 */
+initTheme()
+</script>
+
+<template>
+  <el-config-provider :locale="zhCn">
+    <router-view />
+  </el-config-provider>
+</template>

+ 36 - 0
src/api/hook-demo/use-fetch-select.ts

@@ -0,0 +1,36 @@
+/** 模拟接口响应数据 */
+const SELECT_RESPONSE_DATA = {
+  code: 0,
+  data: [
+    {
+      label: "苹果",
+      value: 1
+    },
+    {
+      label: "香蕉",
+      value: 2
+    },
+    {
+      label: "橘子",
+      value: 3,
+      disabled: true
+    }
+  ],
+  message: "获取 Select 数据成功"
+}
+
+/** 模拟接口 */
+export function getSelectDataApi() {
+  return new Promise<typeof SELECT_RESPONSE_DATA>((resolve, reject) => {
+    // 模拟接口响应时间 2s
+    setTimeout(() => {
+      // 模拟接口调用成功
+      if (Math.random() < 0.8) {
+        resolve(SELECT_RESPONSE_DATA)
+      } else {
+        // 模拟接口调用出错
+        reject(new Error("接口发生错误"))
+      }
+    }, 2000)
+  })
+}

+ 26 - 0
src/api/hook-demo/use-fullscreen-loading.ts

@@ -0,0 +1,26 @@
+/** 模拟接口响应数据 */
+const SUCCESS_RESPONSE_DATA = {
+  code: 0,
+  data: {
+    list: [] as number[]
+  },
+  message: "获取成功"
+}
+
+/** 模拟请求接口成功 */
+export function getSuccessApi(list: number[]) {
+  return new Promise<typeof SUCCESS_RESPONSE_DATA>((resolve) => {
+    setTimeout(() => {
+      resolve({ ...SUCCESS_RESPONSE_DATA, data: { list } })
+    }, 1000)
+  })
+}
+
+/** 模拟请求接口失败 */
+export function getErrorApi() {
+  return new Promise((_resolve, reject) => {
+    setTimeout(() => {
+      reject(new Error("发生错误"))
+    }, 1000)
+  })
+}

+ 27 - 0
src/api/login/index.ts

@@ -0,0 +1,27 @@
+import { request } from "@/utils/service"
+import type * as Login from "./types/login"
+
+/** 获取登录验证码 */
+export function getLoginCodeApi() {
+  return request<Login.LoginCodeResponseData>({
+    url: "login/code",
+    method: "get"
+  })
+}
+
+/** 登录并返回 Token */
+export function loginApi(data: Login.LoginRequestData) {
+  return request<Login.LoginResponseData>({
+    url: "users/login",
+    method: "post",
+    data
+  })
+}
+
+/** 获取用户详情 */
+export function getUserInfoApi() {
+  return request<Login.UserInfoResponseData>({
+    url: "users/info",
+    method: "get"
+  })
+}

+ 14 - 0
src/api/login/types/login.ts

@@ -0,0 +1,14 @@
+export interface LoginRequestData {
+  /** admin 或 editor */
+  username: "admin" | "editor"
+  /** 密码 */
+  password: string
+  /** 验证码 */
+  code: string
+}
+
+export type LoginCodeResponseData = ApiResponseData<string>
+
+export type LoginResponseData = ApiResponseData<{ token: string }>
+
+export type UserInfoResponseData = ApiResponseData<{ username: string; roles: string[] }>

+ 37 - 0
src/api/table/index.ts

@@ -0,0 +1,37 @@
+import { request } from "@/utils/service"
+import type * as Table from "./types/table"
+
+/** 增 */
+export function createTableDataApi(data: Table.CreateOrUpdateTableRequestData) {
+  return request({
+    url: "table",
+    method: "post",
+    data
+  })
+}
+
+/** 删 */
+export function deleteTableDataApi(id: string) {
+  return request({
+    url: `table/${id}`,
+    method: "delete"
+  })
+}
+
+/** 改 */
+export function updateTableDataApi(data: Table.CreateOrUpdateTableRequestData) {
+  return request({
+    url: "table",
+    method: "put",
+    data
+  })
+}
+
+/** 查 */
+export function getTableDataApi(params: Table.TableRequestData) {
+  return request<Table.TableResponseData>({
+    url: "table",
+    method: "get",
+    params
+  })
+}

+ 31 - 0
src/api/table/types/table.ts

@@ -0,0 +1,31 @@
+export interface CreateOrUpdateTableRequestData {
+  id?: string
+  username: string
+  password?: string
+}
+
+export interface TableRequestData {
+  /** 当前页码 */
+  currentPage: number
+  /** 查询条数 */
+  size: number
+  /** 查询参数:用户名 */
+  username?: string
+  /** 查询参数:手机号 */
+  phone?: string
+}
+
+export interface TableData {
+  createTime: string
+  email: string
+  id: string
+  phone: string
+  roles: string
+  status: boolean
+  username: string
+}
+
+export type TableResponseData = ApiResponseData<{
+  list: TableData[]
+  total: number
+}>

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
src/assets/error-page/403.svg


Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
src/assets/error-page/404.svg


BIN
src/assets/layouts/logo-text-1.png


BIN
src/assets/layouts/logo-text-2.png


BIN
src/assets/layouts/logo.png


BIN
src/assets/login/close-eyes.png


BIN
src/assets/login/face.png


BIN
src/assets/login/hand-down-left.png


BIN
src/assets/login/hand-down-right.png


BIN
src/assets/login/hand-up-left.png


BIN
src/assets/login/hand-up-right.png


+ 58 - 0
src/components/Notify/NotifyList.vue

@@ -0,0 +1,58 @@
+<script lang="ts" setup>
+import { type ListItem } from "./data"
+
+interface Props {
+  list: ListItem[]
+}
+
+const props = defineProps<Props>()
+</script>
+
+<template>
+  <el-empty v-if="props.list.length === 0" />
+  <el-card v-else v-for="(item, index) in props.list" :key="index" shadow="never" class="card-container">
+    <template #header>
+      <div class="card-header">
+        <div>
+          <span>
+            <span class="card-title">{{ item.title }}</span>
+            <el-tag v-if="item.extra" :type="item.status" effect="plain" size="small">{{ item.extra }}</el-tag>
+          </span>
+          <div class="card-time">{{ item.datetime }}</div>
+        </div>
+        <div v-if="item.avatar" class="card-avatar">
+          <img :src="item.avatar" width="34" />
+        </div>
+      </div>
+    </template>
+    <div class="card-body">
+      {{ item.description ?? "No Data" }}
+    </div>
+  </el-card>
+</template>
+
+<style lang="scss" scoped>
+.card-container {
+  margin-bottom: 10px;
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    .card-title {
+      font-weight: bold;
+      margin-right: 10px;
+    }
+    .card-time {
+      font-size: 12px;
+      color: var(--el-text-color-secondary);
+    }
+    .card-avatar {
+      display: flex;
+      align-items: center;
+    }
+  }
+  .card-body {
+    font-size: 12px;
+  }
+}
+</style>

+ 66 - 0
src/components/Notify/data.ts

@@ -0,0 +1,66 @@
+export interface ListItem {
+  avatar?: string
+  title: string
+  datetime?: string
+  description?: string
+  status?: "primary" | "success" | "info" | "warning" | "danger"
+  extra?: string
+}
+
+export const notifyData: ListItem[] = [
+  {
+    avatar: "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
+    title: "V3 Admin Vite 上线啦",
+    datetime: "一年前",
+    description:
+      "一个免费开源的中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus、Pinia 和 Vite 等主流技术"
+  },
+  {
+    avatar: "https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png",
+    title: "V3 Admin 上线啦",
+    datetime: "两年前",
+    description: "一个中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus 和 Pinia"
+  }
+]
+
+export const messageData: ListItem[] = [
+  {
+    avatar: "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
+    title: "来自楚门的世界",
+    description: "如果再也不能见到你,祝你早安、午安和晚安",
+    datetime: "1998-06-05"
+  },
+  {
+    avatar: "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
+    title: "来自大话西游",
+    description: "如果非要在这份爱上加上一个期限,我希望是一万年",
+    datetime: "1995-02-04"
+  },
+  {
+    avatar: "https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png",
+    title: "来自龙猫",
+    description: "心存善意,定能途遇天使",
+    datetime: "1988-04-16"
+  }
+]
+
+export const todoData: ListItem[] = [
+  {
+    title: "任务名称",
+    description: "这家伙很懒,什么都没留下",
+    extra: "未开始",
+    status: "info"
+  },
+  {
+    title: "任务名称",
+    description: "这家伙很懒,什么都没留下",
+    extra: "进行中",
+    status: "primary"
+  },
+  {
+    title: "任务名称",
+    description: "这家伙很懒,什么都没留下",
+    extra: "已超时",
+    status: "danger"
+  }
+]

+ 95 - 0
src/components/Notify/index.vue

@@ -0,0 +1,95 @@
+<script lang="ts" setup>
+import { ref, computed } from "vue"
+import { ElMessage } from "element-plus"
+import { Bell } from "@element-plus/icons-vue"
+import NotifyList from "./NotifyList.vue"
+import { type ListItem, notifyData, messageData, todoData } from "./data"
+
+type TabName = "通知" | "消息" | "待办"
+
+interface DataItem {
+  name: TabName
+  type: "primary" | "success" | "warning" | "danger" | "info"
+  list: ListItem[]
+}
+
+/** 角标当前值 */
+const badgeValue = computed(() => {
+  return data.value.reduce((sum, item) => sum + item.list.length, 0)
+})
+/** 角标最大值 */
+const badgeMax = 99
+/** 面板宽度 */
+const popoverWidth = 350
+/** 当前 Tab */
+const activeName = ref<TabName>("通知")
+/** 所有数据 */
+const data = ref<DataItem[]>([
+  // 通知数据
+  {
+    name: "通知",
+    type: "primary",
+    list: notifyData
+  },
+  // 消息数据
+  {
+    name: "消息",
+    type: "danger",
+    list: messageData
+  },
+  // 待办数据
+  {
+    name: "待办",
+    type: "warning",
+    list: todoData
+  }
+])
+
+const handleHistory = () => {
+  ElMessage.success(`跳转到${activeName.value}历史页面`)
+}
+</script>
+
+<template>
+  <div class="notify">
+    <el-popover placement="bottom" :width="popoverWidth" trigger="click">
+      <template #reference>
+        <el-badge :value="badgeValue" :max="badgeMax" :hidden="badgeValue === 0">
+          <el-tooltip effect="dark" content="消息通知" placement="bottom">
+            <el-icon :size="20">
+              <Bell />
+            </el-icon>
+          </el-tooltip>
+        </el-badge>
+      </template>
+      <template #default>
+        <el-tabs v-model="activeName" class="demo-tabs" stretch>
+          <el-tab-pane v-for="(item, index) in data" :name="item.name" :key="index">
+            <template #label>
+              {{ item.name }}
+              <el-badge :value="item.list.length" :max="badgeMax" :type="item.type" />
+            </template>
+            <el-scrollbar height="400px">
+              <NotifyList :list="item.list" />
+            </el-scrollbar>
+          </el-tab-pane>
+        </el-tabs>
+        <div class="notify-history">
+          <el-button link @click="handleHistory">查看{{ activeName }}历史</el-button>
+        </div>
+      </template>
+    </el-popover>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.notify {
+  margin-right: 10px;
+}
+
+.notify-history {
+  text-align: center;
+  padding-top: 12px;
+  border-top: 1px solid var(--el-border-color);
+}
+</style>

+ 103 - 0
src/components/Screenfull/index.vue

@@ -0,0 +1,103 @@
+<script lang="ts" setup>
+import { computed, ref, watchEffect } from "vue"
+import { ElMessage } from "element-plus"
+import screenfull from "screenfull"
+
+interface Props {
+  /** 全屏的元素,默认是 html */
+  element?: string
+  /** 打开全屏提示语 */
+  openTips?: string
+  /** 关闭全屏提示语 */
+  exitTips?: string
+  /** 是否只针对内容区 */
+  content?: boolean
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  element: "html",
+  openTips: "全屏",
+  exitTips: "退出全屏",
+  content: false
+})
+
+//#region 全屏
+const isFullscreen = ref<boolean>(false)
+const fullscreenTips = computed(() => {
+  return isFullscreen.value ? props.exitTips : props.openTips
+})
+const fullscreenSvgName = computed(() => {
+  return isFullscreen.value ? "fullscreen-exit" : "fullscreen"
+})
+const handleFullscreenClick = () => {
+  const dom = document.querySelector(props.element) || undefined
+  screenfull.isEnabled ? screenfull.toggle(dom) : ElMessage.warning("您的浏览器无法工作")
+}
+const handleFullscreenChange = () => {
+  isFullscreen.value = screenfull.isFullscreen
+  // 退出全屏时清除所有的 class
+  isFullscreen.value || (document.body.className = "")
+}
+watchEffect((onCleanup) => {
+  // 挂载组件时自动执行
+  screenfull.isEnabled && screenfull.on("change", handleFullscreenChange)
+  // 卸载组件时自动执行
+  onCleanup(() => {
+    screenfull.isEnabled && screenfull.off("change", handleFullscreenChange)
+  })
+})
+//#endregion
+
+//#region 内容区
+const isContentLarge = ref<boolean>(false)
+const contentLargeTips = computed(() => {
+  return isContentLarge.value ? "内容区复原" : "内容区放大"
+})
+const contentLargeSvgName = computed(() => {
+  return isContentLarge.value ? "fullscreen-exit" : "fullscreen"
+})
+const handleContentLargeClick = () => {
+  isContentLarge.value = !isContentLarge.value
+  // 内容区放大时,将不需要的组件隐藏
+  document.body.className = isContentLarge.value ? "content-large" : ""
+}
+const handleContentFullClick = () => {
+  // 取消内容区放大
+  isContentLarge.value && handleContentLargeClick()
+  // 内容区全屏时,将不需要的组件隐藏
+  document.body.className = "content-full"
+  // 开启全屏
+  handleFullscreenClick()
+}
+//#endregion
+</script>
+
+<template>
+  <div>
+    <!-- 全屏 -->
+    <el-tooltip v-if="!content" effect="dark" :content="fullscreenTips" placement="bottom">
+      <SvgIcon :name="fullscreenSvgName" @click="handleFullscreenClick" />
+    </el-tooltip>
+    <!-- 内容区 -->
+    <el-dropdown v-else :disabled="isFullscreen">
+      <SvgIcon :name="contentLargeSvgName" />
+      <template #dropdown>
+        <el-dropdown-menu>
+          <!-- 内容区放大 -->
+          <el-dropdown-item @click="handleContentLargeClick">{{ contentLargeTips }}</el-dropdown-item>
+          <!-- 内容区全屏 -->
+          <el-dropdown-item @click="handleContentFullClick">内容区全屏</el-dropdown-item>
+        </el-dropdown-menu>
+      </template>
+    </el-dropdown>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.svg-icon {
+  font-size: 20px;
+  &:focus {
+    outline: none;
+  }
+}
+</style>

+ 54 - 0
src/components/SearchMenu/SearchFooter.vue

@@ -0,0 +1,54 @@
+<script lang="ts" setup>
+import { useDevice } from "@/hooks/useDevice"
+
+interface Props {
+  total: number
+}
+
+const props = defineProps<Props>()
+
+const { isMobile } = useDevice()
+</script>
+
+<template>
+  <div class="search-footer">
+    <template v-if="!isMobile">
+      <span class="search-footer-item">
+        <SvgIcon name="keyboard-enter" />
+        <span>确认</span>
+      </span>
+      <span class="search-footer-item">
+        <SvgIcon name="keyboard-up" />
+        <SvgIcon name="keyboard-down" />
+        <span>切换</span>
+      </span>
+      <span class="search-footer-item">
+        <SvgIcon name="keyboard-esc" />
+        <span>关闭</span>
+      </span>
+    </template>
+    <span class="search-footer-total">共 {{ props.total }} 项</span>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.search-footer {
+  display: flex;
+  color: var(--el-text-color-secondary);
+  font-size: 14px;
+  &-item {
+    display: flex;
+    align-items: center;
+    margin-right: 12px;
+    .svg-icon {
+      margin-right: 5px;
+      padding: 2px;
+      font-size: 20px;
+      background-color: var(--el-fill-color);
+    }
+  }
+  &-total {
+    margin: 0 0 0 auto;
+  }
+}
+</style>

+ 202 - 0
src/components/SearchMenu/SearchModal.vue

@@ -0,0 +1,202 @@
+<script lang="ts" setup>
+import { computed, ref, shallowRef } from "vue"
+import { type RouteRecordName, type RouteRecordRaw, useRouter } from "vue-router"
+import { usePermissionStore } from "@/store/modules/permission"
+import SearchResult from "./SearchResult.vue"
+import SearchFooter from "./SearchFooter.vue"
+import { ElMessage, ElScrollbar } from "element-plus"
+import { cloneDeep, debounce } from "lodash-es"
+import { useDevice } from "@/hooks/useDevice"
+import { isExternal } from "@/utils/validate"
+
+/** 控制 modal 显隐 */
+const modelValue = defineModel<boolean>({ required: true })
+
+const router = useRouter()
+const { isMobile } = useDevice()
+
+const inputRef = ref<HTMLInputElement | null>(null)
+const scrollbarRef = ref<InstanceType<typeof ElScrollbar> | null>(null)
+const searchResultRef = ref<InstanceType<typeof SearchResult> | null>(null)
+
+const keyword = ref<string>("")
+const resultList = shallowRef<RouteRecordRaw[]>([])
+const activeRouteName = ref<RouteRecordName | undefined>(undefined)
+/** 是否按下了上键或下键(用于解决和 mouseenter 事件的冲突) */
+const isPressUpOrDown = ref<boolean>(false)
+
+/** 控制搜索对话框宽度 */
+const modalWidth = computed(() => (isMobile.value ? "80vw" : "40vw"))
+/** 树形菜单 */
+const menusData = computed(() => cloneDeep(usePermissionStore().routes))
+
+/** 搜索(防抖) */
+const handleSearch = debounce(() => {
+  const flatMenusData = flatTree(menusData.value)
+  resultList.value = flatMenusData.filter((menu) =>
+    keyword.value ? menu.meta?.title?.toLocaleLowerCase().includes(keyword.value.toLocaleLowerCase().trim()) : false
+  )
+  // 默认选中搜索结果的第一项
+  const length = resultList.value?.length
+  activeRouteName.value = length > 0 ? resultList.value[0].name : undefined
+}, 500)
+
+/** 将树形菜单扁平化为一维数组,用于菜单搜索 */
+const flatTree = (arr: RouteRecordRaw[], result: RouteRecordRaw[] = []) => {
+  arr.forEach((item) => {
+    result.push(item)
+    item.children && flatTree(item.children, result)
+  })
+  return result
+}
+
+/** 关闭搜索对话框 */
+const handleClose = () => {
+  modelValue.value = false
+  // 延时处理防止用户看到重置数据的操作
+  setTimeout(() => {
+    keyword.value = ""
+    resultList.value = []
+  }, 200)
+}
+
+/** 根据下标位置进行滚动 */
+const scrollTo = (index: number) => {
+  if (!searchResultRef.value) return
+  const scrollTop = searchResultRef.value.getScrollTop(index)
+  // 手动控制 el-scrollbar 滚动条滚动,设置滚动条到顶部的距离
+  scrollbarRef.value?.setScrollTop(scrollTop)
+}
+
+/** 键盘上键 */
+const handleUp = () => {
+  isPressUpOrDown.value = true
+  const { length } = resultList.value
+  if (length === 0) return
+  // 获取该 name 在菜单中第一次出现的位置
+  const index = resultList.value.findIndex((item) => item.name === activeRouteName.value)
+  // 如果已处在顶部
+  if (index === 0) {
+    const bottomName = resultList.value[length - 1].name
+    // 如果顶部和底部的 bottomName 相同,且长度大于 1,就再跳一个位置(可解决遇到首尾两个相同 name 导致的上键不能生效的问题)
+    if (activeRouteName.value === bottomName && length > 1) {
+      activeRouteName.value = resultList.value[length - 2].name
+      scrollTo(length - 2)
+    } else {
+      // 跳转到底部
+      activeRouteName.value = bottomName
+      scrollTo(length - 1)
+    }
+  } else {
+    activeRouteName.value = resultList.value[index - 1].name
+    scrollTo(index - 1)
+  }
+}
+
+/** 键盘下键 */
+const handleDown = () => {
+  isPressUpOrDown.value = true
+  const { length } = resultList.value
+  if (length === 0) return
+  // 获取该 name 在菜单中最后一次出现的位置(可解决遇到连续两个相同 name 导致的下键不能生效的问题)
+  const index = resultList.value.map((item) => item.name).lastIndexOf(activeRouteName.value)
+  // 如果已处在底部
+  if (index === length - 1) {
+    const topName = resultList.value[0].name
+    // 如果底部和顶部的 topName 相同,且长度大于 1,就再跳一个位置(可解决遇到首尾两个相同 name 导致的下键不能生效的问题)
+    if (activeRouteName.value === topName && length > 1) {
+      activeRouteName.value = resultList.value[1].name
+      scrollTo(1)
+    } else {
+      // 跳转到顶部
+      activeRouteName.value = topName
+      scrollTo(0)
+    }
+  } else {
+    activeRouteName.value = resultList.value[index + 1].name
+    scrollTo(index + 1)
+  }
+}
+
+/** 键盘回车键 */
+const handleEnter = () => {
+  const { length } = resultList.value
+  if (length === 0) return
+  const name = activeRouteName.value
+  const path = resultList.value.find((item) => item.name === name)?.path
+  if (path && isExternal(path)) {
+    window.open(path, "_blank", "noopener, noreferrer")
+    return
+  }
+  if (!name) {
+    ElMessage.warning("无法通过搜索进入该菜单,请为对应的路由设置唯一的 Name")
+    return
+  }
+  try {
+    router.push({ name })
+  } catch {
+    ElMessage.error("该菜单有必填的动态参数,无法通过搜索进入")
+    return
+  }
+  handleClose()
+}
+
+/** 释放上键或下键 */
+const handleReleaseUpOrDown = () => {
+  isPressUpOrDown.value = false
+}
+</script>
+
+<template>
+  <el-dialog
+    v-model="modelValue"
+    @opened="inputRef?.focus()"
+    @closed="inputRef?.blur()"
+    @keydown.up="handleUp"
+    @keydown.down="handleDown"
+    @keydown.enter="handleEnter"
+    @keyup.up.down="handleReleaseUpOrDown"
+    :before-close="handleClose"
+    :width="modalWidth"
+    top="5vh"
+    class="search-modal__private"
+    append-to-body
+  >
+    <el-input ref="inputRef" v-model="keyword" @input="handleSearch" placeholder="搜索菜单" size="large" clearable>
+      <template #prefix>
+        <SvgIcon name="search" />
+      </template>
+    </el-input>
+    <el-empty v-if="resultList.length === 0" description="暂无搜索结果" :image-size="100" />
+    <template v-else>
+      <p>搜索结果</p>
+      <el-scrollbar ref="scrollbarRef" max-height="40vh" always>
+        <SearchResult
+          ref="searchResultRef"
+          v-model="activeRouteName"
+          :list="resultList"
+          :isPressUpOrDown="isPressUpOrDown"
+          @click="handleEnter"
+        />
+      </el-scrollbar>
+    </template>
+    <template #footer>
+      <SearchFooter :total="resultList.length" />
+    </template>
+  </el-dialog>
+</template>
+
+<style lang="scss">
+.search-modal__private {
+  .svg-icon {
+    font-size: 18px;
+  }
+  .el-dialog__header {
+    display: none;
+  }
+  .el-dialog__footer {
+    border-top: 1px solid var(--el-border-color);
+    padding: var(--el-dialog-padding-primary);
+  }
+}
+</style>

+ 110 - 0
src/components/SearchMenu/SearchResult.vue

@@ -0,0 +1,110 @@
+<script lang="ts" setup>
+import { getCurrentInstance, onBeforeMount, onBeforeUnmount, onMounted, ref } from "vue"
+import { type RouteRecordName, type RouteRecordRaw } from "vue-router"
+
+interface Props {
+  list: RouteRecordRaw[]
+  isPressUpOrDown: boolean
+}
+
+/** 选中的菜单 */
+const modelValue = defineModel<RouteRecordName | undefined>({ required: true })
+const props = defineProps<Props>()
+
+const instance = getCurrentInstance()
+const scrollbarHeight = ref<number>(0)
+
+/** 菜单的样式 */
+const itemStyle = (item: RouteRecordRaw) => {
+  const flag = item.name === modelValue.value
+  return {
+    background: flag ? "var(--el-color-primary)" : "",
+    color: flag ? "#ffffff" : ""
+  }
+}
+
+/** 鼠标移入 */
+const handleMouseenter = (item: RouteRecordRaw) => {
+  // 如果上键或下键与 mouseenter 事件同时生效,则以上下键为准,不执行该函数的赋值逻辑
+  if (props.isPressUpOrDown) return
+  modelValue.value = item.name
+}
+
+/** 计算滚动可视区高度 */
+const getScrollbarHeight = () => {
+  // el-scrollbar max-height="40vh"
+  scrollbarHeight.value = Number((window.innerHeight * 0.4).toFixed(1))
+}
+
+/** 根据下标计算到顶部的距离 */
+const getScrollTop = (index: number) => {
+  const currentInstance = instance?.proxy?.$refs[`resultItemRef${index}`] as HTMLDivElement[]
+  if (!currentInstance) return 0
+  const currentRef = currentInstance[0]
+  const scrollTop = currentRef.offsetTop + 128 // 128 = 两个 result-item (56 + 56 = 112)高度与上下 margin(8 + 8 = 16)大小之和
+  return scrollTop > scrollbarHeight.value ? scrollTop - scrollbarHeight.value : 0
+}
+
+/** 在组件挂载前添加窗口大小变化事件监听器 */
+onBeforeMount(() => {
+  window.addEventListener("resize", getScrollbarHeight)
+})
+
+/** 在组件挂载时立即计算滚动可视区高度 */
+onMounted(() => {
+  getScrollbarHeight()
+})
+
+/** 在组件卸载前移除窗口大小变化事件监听器 */
+onBeforeUnmount(() => {
+  window.removeEventListener("resize", getScrollbarHeight)
+})
+
+defineExpose({ getScrollTop })
+</script>
+
+<template>
+  <!-- 外层 div 不能删除,是用来接收父组件 click 事件的 -->
+  <div>
+    <div
+      v-for="(item, index) in list"
+      :key="index"
+      :ref="`resultItemRef${index}`"
+      class="result-item"
+      :style="itemStyle(item)"
+      @mouseenter="handleMouseenter(item)"
+    >
+      <SvgIcon v-if="item.meta?.svgIcon" :name="item.meta.svgIcon" />
+      <component v-else-if="item.meta?.elIcon" :is="item.meta.elIcon" class="el-icon" />
+      <span class="result-item-title">
+        {{ item.meta?.title }}
+      </span>
+      <SvgIcon v-if="modelValue && modelValue === item.name" name="keyboard-enter" />
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.result-item {
+  display: flex;
+  align-items: center;
+  height: 56px;
+  padding: 0 15px;
+  margin-top: 8px;
+  border: 1px solid var(--el-border-color);
+  border-radius: 4px;
+  cursor: pointer;
+  .svg-icon {
+    min-width: 1em;
+    font-size: 18px;
+  }
+  .el-icon {
+    width: 1em;
+    font-size: 18px;
+  }
+  &-title {
+    flex: 1;
+    margin-left: 12px;
+  }
+}
+</style>

+ 29 - 0
src/components/SearchMenu/index.vue

@@ -0,0 +1,29 @@
+<script lang="ts" setup>
+import { ref } from "vue"
+import SearchModal from "./SearchModal.vue"
+
+/** 控制 modal 显隐 */
+const modalVisible = ref<boolean>(false)
+/** 打开 modal */
+const handleOpen = () => {
+  modalVisible.value = true
+}
+</script>
+
+<template>
+  <div>
+    <el-tooltip effect="dark" content="搜索菜单" placement="bottom">
+      <SvgIcon name="search" @click="handleOpen" />
+    </el-tooltip>
+    <SearchModal v-model="modalVisible" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.svg-icon {
+  font-size: 20px;
+  &:focus {
+    outline: none;
+  }
+}
+</style>

+ 29 - 0
src/components/SvgIcon/index.vue

@@ -0,0 +1,29 @@
+<script lang="ts" setup>
+import { computed } from "vue"
+
+interface Props {
+  prefix?: string
+  name: string
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  prefix: "icon"
+})
+
+const symbolId = computed(() => `#${props.prefix}-${props.name}`)
+</script>
+
+<template>
+  <svg class="svg-icon" aria-hidden="true">
+    <use :href="symbolId" />
+  </svg>
+</template>
+
+<style lang="scss" scoped>
+.svg-icon {
+  width: 1em;
+  height: 1em;
+  fill: currentColor;
+  overflow: hidden;
+}
+</style>

+ 50 - 0
src/components/ThemeSwitch/index.vue

@@ -0,0 +1,50 @@
+<script lang="ts" setup>
+import { type ThemeName, useTheme } from "@/hooks/useTheme"
+import { MagicStick } from "@element-plus/icons-vue"
+
+const { themeList, activeThemeName, setTheme } = useTheme()
+
+const handleChangeTheme = ({ clientX, clientY }: MouseEvent, themeName: ThemeName) => {
+  const maxRadius = Math.hypot(
+    Math.max(clientX, window.innerWidth - clientX),
+    Math.max(clientY, window.innerHeight - clientY)
+  )
+  const style = document.documentElement.style
+  style.setProperty("--v3-theme-x", clientX + "px")
+  style.setProperty("--v3-theme-y", clientY + "px")
+  style.setProperty("--v3-theme-r", maxRadius + "px")
+  const handler = () => {
+    setTheme(themeName)
+  }
+  // @ts-expect-error
+  document.startViewTransition ? document.startViewTransition(handler) : handler()
+}
+</script>
+
+<template>
+  <el-dropdown trigger="click">
+    <div>
+      <el-tooltip effect="dark" content="主题模式" placement="bottom">
+        <el-icon :size="20">
+          <MagicStick />
+        </el-icon>
+      </el-tooltip>
+    </div>
+    <template #dropdown>
+      <el-dropdown-menu>
+        <el-dropdown-item
+          v-for="(theme, index) in themeList"
+          :key="index"
+          :disabled="activeThemeName === theme.name"
+          @click="
+            (e: MouseEvent) => {
+              handleChangeTheme(e, theme.name)
+            }
+          "
+        >
+          <span>{{ theme.title }}</span>
+        </el-dropdown-item>
+      </el-dropdown-menu>
+    </template>
+  </el-dropdown>
+</template>

+ 55 - 0
src/config/layouts.ts

@@ -0,0 +1,55 @@
+import { getConfigLayout } from "@/utils/cache/local-storage"
+import { LayoutModeEnum } from "@/constants/app-key"
+
+/** 项目配置类型 */
+export interface LayoutSettings {
+  /** 是否显示 Settings Panel */
+  showSettings: boolean
+  /** 布局模式 */
+  layoutMode: LayoutModeEnum
+  /** 是否显示标签栏 */
+  showTagsView: boolean
+  /** 是否显示 Logo */
+  showLogo: boolean
+  /** 是否固定 Header */
+  fixedHeader: boolean
+  /** 是否显示页脚 Footer */
+  showFooter: boolean
+  /** 是否显示消息通知 */
+  showNotify: boolean
+  /** 是否显示切换主题按钮 */
+  showThemeSwitch: boolean
+  /** 是否显示全屏按钮 */
+  showScreenfull: boolean
+  /** 是否显示搜索按钮 */
+  showSearchMenu: boolean
+  /** 是否缓存标签栏 */
+  cacheTagsView: boolean
+  /** 开启系统水印 */
+  showWatermark: boolean
+  /** 是否显示灰色模式 */
+  showGreyMode: boolean
+  /** 是否显示色弱模式 */
+  showColorWeakness: boolean
+}
+
+/** 默认配置 */
+const defaultSettings: LayoutSettings = {
+  layoutMode: LayoutModeEnum.Left,
+  showSettings: true,
+  showTagsView: true,
+  fixedHeader: true,
+  showFooter: true,
+  showLogo: true,
+  showNotify: true,
+  showThemeSwitch: true,
+  showScreenfull: true,
+  showSearchMenu: true,
+  cacheTagsView: false,
+  showWatermark: true,
+  showGreyMode: false,
+  showColorWeakness: false
+}
+
+/** 项目配置 */
+export const layoutSettings: LayoutSettings = { ...defaultSettings, ...getConfigLayout() }

+ 28 - 0
src/config/route.ts

@@ -0,0 +1,28 @@
+/** 路由配置 */
+interface RouteSettings {
+  /**
+   * 是否开启动态路由功能?
+   * 1. 开启后需要后端配合,在查询用户详情接口返回当前用户可以用来判断并加载动态路由的字段(该项目用的是角色 roles 字段)
+   * 2. 假如项目不需要根据不同的用户来显示不同的页面,则应该将 dynamic: false
+   */
+  dynamic: boolean
+  /** 当动态路由功能关闭时:
+   * 1. 应该将所有路由都写到常驻路由里面(表明所有登录的用户能访问的页面都是一样的)
+   * 2. 系统自动给当前登录用户赋值一个没有任何作用的默认角色
+   */
+  defaultRoles: Array<string>
+  /**
+   * 是否开启三级及其以上路由缓存功能?
+   * 1. 开启后会进行路由降级(把三级及其以上的路由转化为二级路由)
+   * 2. 由于都会转成二级路由,所以二级及其以上路由有内嵌子路由将会失效
+   */
+  thirdLevelRouteCache: boolean
+}
+
+const routeSettings: RouteSettings = {
+  dynamic: true,
+  defaultRoles: ["DEFAULT_ROLE"],
+  thirdLevelRouteCache: false
+}
+
+export default routeSettings

+ 15 - 0
src/config/white-list.ts

@@ -0,0 +1,15 @@
+import { type RouteLocationNormalized } from "vue-router"
+
+/** 免登录白名单(匹配路由 path) */
+const whiteListByPath: string[] = ["/login"]
+
+/** 免登录白名单(匹配路由 name) */
+const whiteListByName: string[] = []
+
+/** 判断是否在白名单 */
+const isWhiteList = (to: RouteLocationNormalized) => {
+  // path 和 name 任意一个匹配上即可
+  return whiteListByPath.indexOf(to.path) !== -1 || whiteListByName.indexOf(to.name as any) !== -1
+}
+
+export default isWhiteList

+ 20 - 0
src/constants/app-key.ts

@@ -0,0 +1,20 @@
+/** 设备类型 */
+export enum DeviceEnum {
+  Mobile,
+  Desktop
+}
+
+/** 布局模式 */
+export enum LayoutModeEnum {
+  Left = "left",
+  Top = "top",
+  LeftTop = "left-top"
+}
+
+/** 侧边栏打开状态常量 */
+export const SIDEBAR_OPENED = "opened"
+/** 侧边栏关闭状态常量 */
+export const SIDEBAR_CLOSED = "closed"
+
+export type SidebarOpened = typeof SIDEBAR_OPENED
+export type SidebarClosed = typeof SIDEBAR_CLOSED

+ 13 - 0
src/constants/cache-key.ts

@@ -0,0 +1,13 @@
+const SYSTEM_NAME = "v3-admin-vite"
+
+/** 缓存数据时用到的 Key */
+class CacheKey {
+  static readonly TOKEN = `${SYSTEM_NAME}-token-key`
+  static readonly CONFIG_LAYOUT = `${SYSTEM_NAME}-config-layout-key`
+  static readonly SIDEBAR_STATUS = `${SYSTEM_NAME}-sidebar-status-key`
+  static readonly ACTIVE_THEME_NAME = `${SYSTEM_NAME}-active-theme-name-key`
+  static readonly VISITED_VIEWS = `${SYSTEM_NAME}-visited-views-key`
+  static readonly CACHED_VIEWS = `${SYSTEM_NAME}-cached-views-key`
+}
+
+export default CacheKey

+ 7 - 0
src/directives/index.ts

@@ -0,0 +1,7 @@
+import { type App } from "vue"
+import { permission } from "./permission"
+
+/** 挂载自定义指令 */
+export function loadDirectives(app: App) {
+  app.directive("permission", permission)
+}

+ 17 - 0
src/directives/permission/index.ts

@@ -0,0 +1,17 @@
+import { type Directive } from "vue"
+import { useUserStoreHook } from "@/store/modules/user"
+
+/** 权限指令,和权限判断函数 checkPermission 功能类似 */
+export const permission: Directive = {
+  mounted(el, binding) {
+    const { value: permissionRoles } = binding
+    const { roles } = useUserStoreHook()
+    if (Array.isArray(permissionRoles) && permissionRoles.length > 0) {
+      const hasPermission = roles.some((role) => permissionRoles.includes(role))
+      // hasPermission || (el.style.display = "none") // 隐藏
+      hasPermission || el.parentNode?.removeChild(el) // 销毁
+    } else {
+      throw new Error(`need roles! Like v-permission="['admin','editor']"`)
+    }
+  }
+}

+ 11 - 0
src/hooks/useDevice.ts

@@ -0,0 +1,11 @@
+import { computed } from "vue"
+import { useAppStore } from "@/store/modules/app"
+import { DeviceEnum } from "@/constants/app-key"
+
+const appStore = useAppStore()
+const isMobile = computed(() => appStore.device === DeviceEnum.Mobile)
+const isDesktop = computed(() => appStore.device === DeviceEnum.Desktop)
+
+export function useDevice() {
+  return { isMobile, isDesktop }
+}

+ 49 - 0
src/hooks/useFetchSelect.ts

@@ -0,0 +1,49 @@
+import { ref, onMounted } from "vue"
+
+type OptionValue = string | number
+
+/** Select 需要的数据格式 */
+interface SelectOption {
+  value: OptionValue
+  label: string
+  disabled?: boolean
+}
+
+/** 接口响应格式 */
+type ApiData = ApiResponseData<SelectOption[]>
+
+/** 入参格式,暂时只需要传递 api 函数即可 */
+interface FetchSelectProps {
+  api: () => Promise<ApiData>
+}
+
+export function useFetchSelect(props: FetchSelectProps) {
+  const { api } = props
+
+  const loading = ref<boolean>(false)
+  const options = ref<SelectOption[]>([])
+  const value = ref<OptionValue>("")
+
+  /** 调用接口获取数据 */
+  const loadData = () => {
+    loading.value = true
+    options.value = []
+    api()
+      .then((res) => {
+        options.value = res.data
+      })
+      .finally(() => {
+        loading.value = false
+      })
+  }
+
+  onMounted(() => {
+    loadData()
+  })
+
+  return {
+    loading,
+    options,
+    value
+  }
+}

+ 35 - 0
src/hooks/useFullscreenLoading.ts

@@ -0,0 +1,35 @@
+import { type LoadingOptions, ElLoading } from "element-plus"
+
+const defaultOptions = {
+  lock: true,
+  text: "加载中..."
+}
+
+interface LoadingInstance {
+  close: () => void
+}
+
+interface UseFullscreenLoading {
+  <T extends (...args: any[]) => ReturnType<T>>(
+    fn: T,
+    options?: LoadingOptions
+  ): (...args: Parameters<T>) => Promise<ReturnType<T>>
+}
+
+/**
+ * 传入一个函数 fn,在它执行周期内,加上「全屏」loading
+ * @param fn 要执行的函数
+ * @param options LoadingOptions
+ * @returns 返回一个新的函数,该函数返回一个 Promise
+ */
+export const useFullscreenLoading: UseFullscreenLoading = (fn, options = {}) => {
+  let loadingInstance: LoadingInstance
+  return async (...args) => {
+    try {
+      loadingInstance = ElLoading.service({ ...defaultOptions, ...options })
+      return await fn(...args)
+    } finally {
+      loadingInstance?.close()
+    }
+  }
+}

+ 16 - 0
src/hooks/useLayoutMode.ts

@@ -0,0 +1,16 @@
+import { computed } from "vue"
+import { useSettingsStore } from "@/store/modules/settings"
+import { LayoutModeEnum } from "@/constants/app-key"
+
+const settingsStore = useSettingsStore()
+const isLeft = computed(() => settingsStore.layoutMode === LayoutModeEnum.Left)
+const isTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.Top)
+const isLeftTop = computed(() => settingsStore.layoutMode === LayoutModeEnum.LeftTop)
+
+const setLayoutMode = (mode: LayoutModeEnum) => {
+  settingsStore.layoutMode = mode
+}
+
+export function useLayoutMode() {
+  return { isLeft, isTop, isLeftTop, setLayoutMode }
+}

+ 41 - 0
src/hooks/usePagination.ts

@@ -0,0 +1,41 @@
+import { reactive } from "vue"
+
+interface DefaultPaginationData {
+  total: number
+  currentPage: number
+  pageSizes: number[]
+  pageSize: number
+  layout: string
+}
+
+interface PaginationData {
+  total?: number
+  currentPage?: number
+  pageSizes?: number[]
+  pageSize?: number
+  layout?: string
+}
+
+/** 默认的分页参数 */
+const defaultPaginationData: DefaultPaginationData = {
+  total: 0,
+  currentPage: 1,
+  pageSizes: [10, 20, 50],
+  pageSize: 10,
+  layout: "total, sizes, prev, pager, next, jumper"
+}
+
+export function usePagination(initialPaginationData: PaginationData = {}) {
+  /** 合并分页参数 */
+  const paginationData = reactive({ ...defaultPaginationData, ...initialPaginationData })
+  /** 改变当前页码 */
+  const handleCurrentChange = (value: number) => {
+    paginationData.currentPage = value
+  }
+  /** 改变页面大小 */
+  const handleSizeChange = (value: number) => {
+    paginationData.pageSize = value
+  }
+
+  return { paginationData, handleCurrentChange, handleSizeChange }
+}

+ 48 - 0
src/hooks/useRouteListener.ts

@@ -0,0 +1,48 @@
+import { onBeforeUnmount } from "vue"
+import mitt, { type Handler } from "mitt"
+import { type RouteLocationNormalized } from "vue-router"
+
+/** 回调函数的类型 */
+type Callback = (route: RouteLocationNormalized) => void
+
+const emitter = mitt()
+const key = Symbol("ROUTE_CHANGE")
+let latestRoute: RouteLocationNormalized
+
+/** 设置最新的路由信息,触发路由变化事件 */
+export const setRouteChange = (to: RouteLocationNormalized) => {
+  // 触发事件
+  emitter.emit(key, to)
+  // 缓存最新的路由信息
+  latestRoute = to
+}
+
+/** 单独监听路由会浪费渲染性能,使用发布订阅模式去进行分发管理 */
+export function useRouteListener() {
+  /** 回调函数集合 */
+  const callbackList: Callback[] = []
+
+  /** 监听路由变化(可以选择立即执行) */
+  const listenerRouteChange = (callback: Callback, immediate = false) => {
+    // 缓存回调函数
+    callbackList.push(callback)
+    // 监听事件
+    emitter.on(key, callback as Handler)
+    // 可以选择立即执行一次回调函数
+    immediate && latestRoute && callback(latestRoute)
+  }
+
+  /** 移除路由变化事件监听器 */
+  const removeRouteListener = (callback: Callback) => {
+    emitter.off(key, callback as Handler)
+  }
+
+  /** 组件销毁前移除监听器 */
+  onBeforeUnmount(() => {
+    for (let i = 0; i < callbackList.length; i++) {
+      removeRouteListener(callbackList[i])
+    }
+  })
+
+  return { listenerRouteChange, removeRouteListener }
+}

+ 57 - 0
src/hooks/useTheme.ts

@@ -0,0 +1,57 @@
+import { ref, watchEffect } from "vue"
+import { getActiveThemeName, setActiveThemeName } from "@/utils/cache/local-storage"
+
+const DEFAULT_THEME_NAME = "normal"
+type DefaultThemeName = typeof DEFAULT_THEME_NAME
+
+/** 注册的主题名称, 其中 DefaultThemeName 是必填的 */
+export type ThemeName = DefaultThemeName | "dark" | "dark-blue"
+
+interface ThemeList {
+  title: string
+  name: ThemeName
+}
+
+/** 主题列表 */
+const themeList: ThemeList[] = [
+  {
+    title: "默认",
+    name: DEFAULT_THEME_NAME
+  },
+  {
+    title: "黑暗",
+    name: "dark"
+  },
+  {
+    title: "深蓝",
+    name: "dark-blue"
+  }
+]
+
+/** 正在应用的主题名称 */
+const activeThemeName = ref<ThemeName>(getActiveThemeName() || DEFAULT_THEME_NAME)
+
+/** 设置主题 */
+const setTheme = (value: ThemeName) => {
+  activeThemeName.value = value
+}
+
+/** 在 html 根元素上挂载 class */
+const setHtmlRootClassName = (value: ThemeName) => {
+  document.documentElement.className = value
+}
+
+/** 初始化 */
+const initTheme = () => {
+  // watchEffect 来收集副作用
+  watchEffect(() => {
+    const value = activeThemeName.value
+    setHtmlRootClassName(value)
+    setActiveThemeName(value)
+  })
+}
+
+/** 主题 hook */
+export function useTheme() {
+  return { themeList, activeThemeName, initTheme, setTheme }
+}

+ 23 - 0
src/hooks/useTitle.ts

@@ -0,0 +1,23 @@
+import { ref, watch } from "vue"
+
+/** 项目标题 */
+const VITE_APP_TITLE = import.meta.env.VITE_APP_TITLE ?? "V3 Admin Vite"
+
+/** 动态标题 */
+const dynamicTitle = ref<string>("")
+
+/** 设置标题 */
+const setTitle = (title?: string) => {
+  dynamicTitle.value = title ? `${VITE_APP_TITLE} | ${title}` : VITE_APP_TITLE
+}
+
+/** 监听标题变化 */
+watch(dynamicTitle, (value, oldValue) => {
+  if (document && value !== oldValue) {
+    document.title = value
+  }
+})
+
+export function useTitle() {
+  return { setTitle }
+}

+ 236 - 0
src/hooks/useWatermark.ts

@@ -0,0 +1,236 @@
+import { type Ref, onBeforeUnmount, ref } from "vue"
+import { debounce } from "lodash-es"
+
+type Observer = {
+  watermarkElMutationObserver?: MutationObserver
+  parentElMutationObserver?: MutationObserver
+  parentElResizeObserver?: ResizeObserver
+}
+
+type DefaultConfig = typeof defaultConfig
+
+/** 默认配置 */
+const defaultConfig = {
+  /** 防御(默认开启,能防御水印被删除或隐藏,但可能会有性能损耗) */
+  defense: true,
+  /** 文本颜色 */
+  color: "#c0c4cc",
+  /** 文本透明度 */
+  opacity: 0.5,
+  /** 文本字体大小 */
+  size: 16,
+  /** 文本字体 */
+  family: "serif",
+  /** 文本倾斜角度 */
+  angle: -20,
+  /** 一处水印所占宽度(数值越大水印密度越低) */
+  width: 300,
+  /** 一处水印所占高度(数值越大水印密度越低) */
+  height: 200
+}
+
+/** body 元素 */
+const bodyEl = ref<HTMLElement>(document.body)
+
+/**
+ * 创建水印
+ * 1. 可以选择传入挂载水印的容器元素,默认是 body
+ * 2. 做了水印防御,能有效防御别人打开控制台删除或隐藏水印
+ */
+export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
+  /** 备份文本 */
+  let backupText: string
+  /** 最终配置 */
+  let mergeConfig: DefaultConfig
+  /** 水印元素 */
+  let watermarkEl: HTMLElement | null = null
+  /** 观察器 */
+  const observer: Observer = {
+    watermarkElMutationObserver: undefined,
+    parentElMutationObserver: undefined,
+    parentElResizeObserver: undefined
+  }
+
+  /** 设置水印 */
+  const setWatermark = (text: string, config: Partial<DefaultConfig> = {}) => {
+    if (!parentEl.value) {
+      console.warn("请在 DOM 挂载完成后再调用 setWatermark 方法设置水印")
+      return
+    }
+    // 备份文本
+    backupText = text
+    // 合并配置
+    mergeConfig = { ...defaultConfig, ...config }
+    // 创建或更新水印元素
+    watermarkEl ? updateWatermarkEl() : createWatermarkEl()
+    // 监听水印元素和容器元素的变化
+    addElListener(parentEl.value)
+  }
+
+  /** 创建水印元素 */
+  const createWatermarkEl = () => {
+    const isBody = parentEl.value!.tagName.toLowerCase() === bodyEl.value.tagName.toLowerCase()
+    const watermarkElPosition = isBody ? "fixed" : "absolute"
+    const parentElPosition = isBody ? "" : "relative"
+    watermarkEl = document.createElement("div")
+    watermarkEl.style.pointerEvents = "none"
+    watermarkEl.style.top = "0"
+    watermarkEl.style.left = "0"
+    watermarkEl.style.position = watermarkElPosition
+    watermarkEl.style.zIndex = "99999"
+    const { clientWidth, clientHeight } = parentEl.value!
+    updateWatermarkEl({ width: clientWidth, height: clientHeight })
+    // 设置水印容器为相对定位
+    parentEl.value!.style.position = parentElPosition
+    // 将水印元素添加到水印容器中
+    parentEl.value!.appendChild(watermarkEl)
+  }
+
+  /** 更新水印元素 */
+  const updateWatermarkEl = (
+    options: Partial<{
+      width: number
+      height: number
+    }> = {}
+  ) => {
+    if (!watermarkEl) return
+    backupText && (watermarkEl.style.background = `url(${createBase64()}) left top repeat`)
+    options.width && (watermarkEl.style.width = `${options.width}px`)
+    options.height && (watermarkEl.style.height = `${options.height}px`)
+  }
+
+  /** 创建 base64 图片 */
+  const createBase64 = () => {
+    const { color, opacity, size, family, angle, width, height } = mergeConfig
+    const canvasEl = document.createElement("canvas")
+    canvasEl.width = width
+    canvasEl.height = height
+    const ctx = canvasEl.getContext("2d")
+    if (ctx) {
+      ctx.fillStyle = color
+      ctx.globalAlpha = opacity
+      ctx.font = `${size}px ${family}`
+      ctx.rotate((Math.PI / 180) * angle)
+      ctx.fillText(backupText, 0, height / 2)
+    }
+    return canvasEl.toDataURL()
+  }
+
+  /** 清除水印 */
+  const clearWatermark = () => {
+    if (!parentEl.value || !watermarkEl) return
+    // 移除对水印元素和容器元素的监听
+    removeListener()
+    // 移除水印元素
+    try {
+      parentEl.value.removeChild(watermarkEl)
+    } catch {
+      // 比如在无防御情况下,用户打开控制台删除了这个元素
+      console.warn("水印元素已不存在,请重新创建")
+    } finally {
+      watermarkEl = null
+    }
+  }
+
+  /** 刷新水印(防御时调用) */
+  const updateWatermark = debounce(() => {
+    clearWatermark()
+    createWatermarkEl()
+    addElListener(parentEl.value!)
+  }, 100)
+
+  /** 监听水印元素和容器元素的变化(DOM 变化 & DOM 大小变化) */
+  const addElListener = (targetNode: HTMLElement) => {
+    // 判断是否开启防御
+    if (mergeConfig.defense) {
+      // 防止重复添加监听
+      if (!observer.watermarkElMutationObserver && !observer.parentElMutationObserver) {
+        // 监听 DOM 变化
+        addMutationListener(targetNode)
+      }
+    } else {
+      // 无防御时不需要 mutation 监听
+      removeListener("mutation")
+    }
+    // 防止重复添加监听
+    if (!observer.parentElResizeObserver) {
+      // 监听 DOM 大小变化
+      addResizeListener(targetNode)
+    }
+  }
+
+  /** 移除对水印元素和容器元素的监听,传参可指定要移除哪个监听,不传默认移除全部监听 */
+  const removeListener = (kind: "mutation" | "resize" | "all" = "all") => {
+    // 移除 mutation 监听
+    if (kind === "mutation" || kind === "all") {
+      observer.watermarkElMutationObserver?.disconnect()
+      observer.watermarkElMutationObserver = undefined
+      observer.parentElMutationObserver?.disconnect()
+      observer.parentElMutationObserver = undefined
+    }
+    // 移除 resize 监听
+    if (kind === "resize" || kind === "all") {
+      observer.parentElResizeObserver?.disconnect()
+      observer.parentElResizeObserver = undefined
+    }
+  }
+
+  /** 监听 DOM 变化 */
+  const addMutationListener = (targetNode: HTMLElement) => {
+    // 当观察到变动时执行的回调
+    const mutationCallback = debounce((mutationList: MutationRecord[]) => {
+      // 水印的防御(防止用户手动删除水印元素或通过 CSS 隐藏水印)
+      mutationList.forEach(
+        debounce((mutation: MutationRecord) => {
+          switch (mutation.type) {
+            case "attributes":
+              mutation.target === watermarkEl && updateWatermark()
+              break
+            case "childList":
+              mutation.removedNodes.forEach((item) => {
+                item === watermarkEl && targetNode.appendChild(watermarkEl)
+              })
+              break
+          }
+        }, 100)
+      )
+    }, 100)
+    // 创建观察器实例并传入回调
+    observer.watermarkElMutationObserver = new MutationObserver(mutationCallback)
+    observer.parentElMutationObserver = new MutationObserver(mutationCallback)
+    // 以上述配置开始观察目标节点
+    observer.watermarkElMutationObserver.observe(watermarkEl!, {
+      // 观察目标节点属性是否变动,默认为 true
+      attributes: true,
+      // 观察目标子节点是否有添加或者删除,默认为 false
+      childList: false,
+      // 是否拓展到观察所有后代节点,默认为 false
+      subtree: false
+    })
+    observer.parentElMutationObserver.observe(targetNode, {
+      attributes: false,
+      childList: true,
+      subtree: false
+    })
+  }
+
+  /** 监听 DOM 大小变化 */
+  const addResizeListener = (targetNode: HTMLElement) => {
+    // 当 targetNode 元素大小变化时去更新整个水印的大小
+    const resizeCallback = debounce(() => {
+      const { clientWidth, clientHeight } = targetNode
+      updateWatermarkEl({ width: clientWidth, height: clientHeight })
+    }, 500)
+    // 创建一个观察器实例并传入回调
+    observer.parentElResizeObserver = new ResizeObserver(resizeCallback)
+    // 开始观察目标节点
+    observer.parentElResizeObserver.observe(targetNode)
+  }
+
+  /** 在组件卸载前移除水印以及各种监听 */
+  onBeforeUnmount(() => {
+    clearWatermark()
+  })
+
+  return { setWatermark, clearWatermark }
+}

+ 7 - 0
src/icons/index.ts

@@ -0,0 +1,7 @@
+import { type App } from "vue"
+import SvgIcon from "@/components/SvgIcon/index.vue" // Svg Component
+// import "virtual:svg-icons-register"
+
+export function loadSvg(app: App) {
+  app.component("SvgIcon", SvgIcon)
+}

+ 1 - 0
src/icons/svg/404.svg

@@ -0,0 +1 @@
+<svg t="1651119499039" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9021" width="200" height="200"><path d="M512 720m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" p-id="9022"></path><path d="M480 416v184c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V416c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8z" p-id="9023"></path><path d="M955.7 856l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48z m-783.5-27.9L512 239.9l339.8 588.2H172.2z" p-id="9024"></path></svg>

+ 1 - 0
src/icons/svg/bug.svg

@@ -0,0 +1 @@
+<svg t="1651119031318" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8881" width="200" height="200"><path d="M940 512H792V412c76.8 0 139-62.2 139-139 0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 34.8-28.2 63-63 63H232c-34.8 0-63-28.2-63-63 0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 76.8 62.2 139 139 139v100H84c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h148v96c0 6.5 0.2 13 0.7 19.3C164.1 728.6 116 796.7 116 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-44.2 23.9-82.9 59.6-103.7 6 17.2 13.6 33.6 22.7 49 24.3 41.5 59 76.2 100.5 100.5S460.5 960 512 960s99.8-13.9 141.3-38.2c41.5-24.3 76.2-59 100.5-100.5 9.1-15.5 16.7-31.9 22.7-49C812.1 793.1 836 831.8 836 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-79.3-48.1-147.4-116.7-176.7 0.4-6.4 0.7-12.8 0.7-19.3v-96h148c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM716 680c0 36.8-9.7 72-27.8 102.9-17.7 30.3-43 55.6-73.3 73.3-20.1 11.8-42 20-64.9 24.3V484c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v396.5c-22.9-4.3-44.8-12.5-64.9-24.3-30.3-17.7-55.6-43-73.3-73.3C317.7 752 308 716.8 308 680V412h408v268z" p-id="8882"></path><path d="M304 280h56c4.4 0 8-3.6 8-8 0-28.3 5.9-53.2 17.1-73.5 10.6-19.4 26-34.8 45.4-45.4C450.9 142 475.7 136 504 136h16c28.3 0 53.2 5.9 73.5 17.1 19.4 10.6 34.8 26 45.4 45.4C650 218.9 656 243.7 656 272c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-40-8.8-76.7-25.9-108.1-17.2-31.5-42.5-56.8-74-74C596.7 72.8 560 64 520 64h-16c-40 0-76.7 8.8-108.1 25.9-31.5 17.2-56.8 42.5-74 74C304.8 195.3 296 232 296 272c0 4.4 3.6 8 8 8z" p-id="8883"></path></svg>

+ 1 - 0
src/icons/svg/component.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1672728665955" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3482" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16"><path d="M64 64h384v384H64V64z m0 512h384v384H64V576z m512 0h384v384H576V576z m192-128c106.039 0 192-85.961 192-192S874.039 64 768 64s-192 85.961-192 192 85.961 192 192 192z" p-id="3483"></path></svg>

+ 1 - 0
src/icons/svg/dashboard.svg

@@ -0,0 +1 @@
+<svg t="1651118937898" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8601" width="200" height="200"><path d="M924.8 385.6c-22.6-53.4-54.9-101.3-96-142.4-41.1-41.1-89-73.4-142.4-96C631.1 123.8 572.5 112 512 112s-119.1 11.8-174.4 35.2c-53.4 22.6-101.3 54.9-142.4 96-41.1 41.1-73.4 89-96 142.4C75.8 440.9 64 499.5 64 560c0 132.7 58.3 257.7 159.9 343.1l1.7 1.4c5.8 4.8 13.1 7.5 20.6 7.5h531.7c7.5 0 14.8-2.7 20.6-7.5l1.7-1.4C901.7 817.7 960 692.7 960 560c0-60.5-11.9-119.1-35.2-174.4zM761.4 836H262.6C184.5 765.5 140 665.6 140 560c0-99.4 38.7-192.8 109-263 70.3-70.3 163.7-109 263-109 99.4 0 192.8 38.7 263 109 70.3 70.3 109 163.7 109 263 0 105.6-44.5 205.5-122.6 276z" p-id="8602"></path><path d="M623.5 421.5c-3.1-3.1-8.2-3.1-11.3 0L527.7 506c-18.7-5-39.4-0.2-54.1 14.5-21.9 21.9-21.9 57.3 0 79.2 21.9 21.9 57.3 21.9 79.2 0 14.7-14.7 19.5-35.4 14.5-54.1l84.5-84.5c3.1-3.1 3.1-8.2 0-11.3l-28.3-28.3zM490 320h44c4.4 0 8-3.6 8-8v-80c0-4.4-3.6-8-8-8h-44c-4.4 0-8 3.6-8 8v80c0 4.4 3.6 8 8 8zM750 538v44c0 4.4 3.6 8 8 8h80c4.4 0 8-3.6 8-8v-44c0-4.4-3.6-8-8-8h-80c-4.4 0-8 3.6-8 8zM762.7 340.8l-31.1-31.1c-3.1-3.1-8.2-3.1-11.3 0l-56.6 56.6c-3.1 3.1-3.1 8.2 0 11.3l31.1 31.1c3.1 3.1 8.2 3.1 11.3 0l56.6-56.6c3.1-3.1 3.1-8.2 0-11.3zM304.1 309.7c-3.1-3.1-8.2-3.1-11.3 0l-31.1 31.1c-3.1 3.1-3.1 8.2 0 11.3l56.6 56.6c3.1 3.1 8.2 3.1 11.3 0l31.1-31.1c3.1-3.1 3.1-8.2 0-11.3l-56.6-56.6zM262 530h-80c-4.4 0-8 3.6-8 8v44c0 4.4 3.6 8 8 8h80c4.4 0 8-3.6 8-8v-44c0-4.4-3.6-8-8-8z" p-id="8603"></path></svg>

+ 1 - 0
src/icons/svg/fullscreen-exit.svg

@@ -0,0 +1 @@
+<svg t="1661153147729" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3352" width="200" height="200"><path d="M704 864v-96c0-54.4 41.6-96 96-96h96c19.2 0 32-12.8 32-32s-12.8-32-32-32h-96c-89.6 0-160 70.4-160 160v96c0 19.2 12.8 32 32 32s32-12.8 32-32z m-64-704v96c0 89.6 70.4 160 160 160h96c19.2 0 32-12.8 32-32s-12.8-32-32-32h-96c-54.4 0-96-41.6-96-96v-96c0-19.2-12.8-32-32-32s-32 12.8-32 32z m-256 704v-96c0-89.6-70.4-160-160-160h-96c-19.2 0-32 12.8-32 32s12.8 32 32 32h96c54.4 0 96 41.6 96 96v96c0 19.2 12.8 32 32 32s32-12.8 32-32z m-64-704v96c0 54.4-41.6 96-96 96h-96c-19.2 0-32 12.8-32 32s12.8 32 32 32h96c89.6 0 160-70.4 160-160v-96c0-19.2-12.8-32-32-32s-32 12.8-32 32z" p-id="3353"></path></svg>

+ 1 - 0
src/icons/svg/fullscreen.svg

@@ -0,0 +1 @@
+<svg t="1661151768669" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3212" width="200" height="200"><path d="M192 384v-96c0-54.4 41.6-96 96-96h96c19.2 0 32-12.8 32-32s-12.8-32-32-32h-96c-89.6 0-160 70.4-160 160v96c0 19.2 12.8 32 32 32s32-12.8 32-32z m-64 256v96c0 89.6 70.4 160 160 160h96c19.2 0 32-12.8 32-32s-12.8-32-32-32h-96c-54.4 0-96-41.6-96-96v-96c0-19.2-12.8-32-32-32s-32 12.8-32 32z m768-256v-96c0-89.6-70.4-160-160-160h-96c-19.2 0-32 12.8-32 32s12.8 32 32 32h96c54.4 0 96 41.6 96 96v96c0 19.2 12.8 32 32 32s32-12.8 32-32z m-64 256v96c0 54.4-41.6 96-96 96h-96c-19.2 0-32 12.8-32 32s12.8 32 32 32h96c89.6 0 160-70.4 160-160v-96c0-19.2-12.8-32-32-32s-32 12.8-32 32z" p-id="3213"></path></svg>

+ 1 - 0
src/icons/svg/keyboard-down.svg

@@ -0,0 +1 @@
+<svg width="15" height="15" aria-label="Arrow down" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M7.5 3.5v8M10.5 8.5l-3 3-3-3"></path></g></svg>

+ 1 - 0
src/icons/svg/keyboard-enter.svg

@@ -0,0 +1 @@
+<svg width="15" height="15" aria-label="Enter key" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M12 3.53088v3c0 1-1 2-2 2H4M7 11.53088l-3-3 3-3"></path></g></svg>

+ 1 - 0
src/icons/svg/keyboard-esc.svg

@@ -0,0 +1 @@
+<svg width="15" height="15" aria-label="Escape key" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M13.6167 8.936c-.1065.3583-.6883.962-1.4875.962-.7993 0-1.653-.9165-1.653-2.1258v-.5678c0-1.2548.7896-2.1016 1.653-2.1016.8634 0 1.3601.4778 1.4875 1.0724M9 6c-.1352-.4735-.7506-.9219-1.46-.8972-.7092.0246-1.344.57-1.344 1.2166s.4198.8812 1.3445.9805C8.465 7.3992 8.968 7.9337 9 8.5c.032.5663-.454 1.398-1.4595 1.398C6.6593 9.898 6 9 5.963 8.4851m-1.4748.5368c-.2635.5941-.8099.876-1.5443.876s-1.7073-.6248-1.7073-2.204v-.4603c0-1.0416.721-2.131 1.7073-2.131.9864 0 1.6425 1.031 1.5443 2.2492h-2.956"></path></g></svg>

+ 1 - 0
src/icons/svg/keyboard-up.svg

@@ -0,0 +1 @@
+<svg width="15" height="15" aria-label="Arrow up" role="img"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M7.5 11.5v-8M10.5 6.5l-3-3-3 3"></path></g></svg>

+ 1 - 0
src/icons/svg/link.svg

@@ -0,0 +1 @@
+<svg t="1651118878747" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8461" width="200" height="200"><path d="M574 665.4c-3.1-3.1-8.2-3.1-11.3 0L446.5 781.6c-53.8 53.8-144.6 59.5-204 0-59.5-59.5-53.8-150.2 0-204l116.2-116.2c3.1-3.1 3.1-8.2 0-11.3l-39.8-39.8c-3.1-3.1-8.2-3.1-11.3 0L191.4 526.5c-84.6 84.6-84.6 221.5 0 306s221.5 84.6 306 0l116.2-116.2c3.1-3.1 3.1-8.2 0-11.3L574 665.4zM832.6 191.4c-84.6-84.6-221.5-84.6-306 0L410.3 307.6c-3.1 3.1-3.1 8.2 0 11.3l39.7 39.7c3.1 3.1 8.2 3.1 11.3 0l116.2-116.2c53.8-53.8 144.6-59.5 204 0 59.5 59.5 53.8 150.2 0 204L665.3 562.6c-3.1 3.1-3.1 8.2 0 11.3l39.8 39.8c3.1 3.1 8.2 3.1 11.3 0l116.2-116.2c84.5-84.6 84.5-221.5 0-306.1z" p-id="8462"></path><path d="M610.1 372.3c-3.1-3.1-8.2-3.1-11.3 0L372.3 598.7c-3.1 3.1-3.1 8.2 0 11.3l39.6 39.6c3.1 3.1 8.2 3.1 11.3 0l226.4-226.4c3.1-3.1 3.1-8.2 0-11.3l-39.5-39.6z" p-id="8463"></path></svg>

+ 1 - 0
src/icons/svg/lock.svg

@@ -0,0 +1 @@
+<svg t="1651119007904" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8741" width="200" height="200"><path d="M832 464h-68V240c0-70.7-57.3-128-128-128H388c-70.7 0-128 57.3-128 128v224h-68c-17.7 0-32 14.3-32 32v384c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32zM332 240c0-30.9 25.1-56 56-56h248c30.9 0 56 25.1 56 56v224H332V240z m460 600H232V536h560v304z" p-id="8742"></path><path d="M484 701v53c0 4.4 3.6 8 8 8h40c4.4 0 8-3.6 8-8v-53c12.1-8.7 20-22.9 20-39 0-26.5-21.5-48-48-48s-48 21.5-48 48c0 16.1 7.9 30.3 20 39z" p-id="8743"></path></svg>

+ 1 - 0
src/icons/svg/menu.svg

@@ -0,0 +1 @@
+<svg t="1651750906395" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9162" width="200" height="200"><path d="M904 158H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM904 582H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h496c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM904 794H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM904 370H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8z" p-id="9163"></path></svg>

+ 1 - 0
src/icons/svg/search.svg

@@ -0,0 +1 @@
+<svg t="1691398959507" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2431" width="200" height="200"><path d="M862.609 816.955L726.44 680.785l-0.059-0.056a358.907 358.907 0 0 0 56.43-91.927c18.824-44.507 28.369-91.767 28.369-140.467 0-48.701-9.545-95.96-28.369-140.467-18.176-42.973-44.19-81.56-77.319-114.689-33.13-33.129-71.717-59.144-114.69-77.32-44.507-18.825-91.767-28.37-140.467-28.37-48.701 0-95.96 9.545-140.467 28.37-42.973 18.176-81.56 44.19-114.689 77.32-33.13 33.129-59.144 71.717-77.32 114.689-18.825 44.507-28.37 91.767-28.37 140.467 0 48.7 9.545 95.96 28.37 140.467 18.176 42.974 44.19 81.561 77.32 114.69 33.129 33.129 71.717 59.144 114.689 77.319 44.507 18.824 91.767 28.369 140.467 28.369 48.7 0 95.96-9.545 140.467-28.369 32.78-13.864 62.997-32.303 90.197-54.968 0.063 0.064 0.122 0.132 0.186 0.195l136.169 136.17c6.25 6.25 14.438 9.373 22.628 9.373 8.188 0 16.38-3.125 22.627-9.372 12.496-12.496 12.496-32.758 0-45.254z m-412.274-69.466c-79.907 0-155.031-31.118-211.534-87.62-56.503-56.503-87.62-131.627-87.62-211.534s31.117-155.031 87.62-211.534c56.502-56.503 131.626-87.62 211.534-87.62s155.031 31.117 211.534 87.62c56.502 56.502 87.62 131.626 87.62 211.534s-31.118 155.031-87.62 211.534c-56.503 56.502-131.627 87.62-211.534 87.62z" p-id="2432"></path></svg>

+ 5 - 0
src/icons/svg/unocss.svg

@@ -0,0 +1,5 @@
+<svg width="220" height="220" viewBox="0 0 220 220" xmlns="http://www.w3.org/2000/svg">
+<path d="M117.444 167.888C117.444 140.273 139.83 117.888 167.444 117.888V117.888C195.058 117.888 217.444 140.273 217.444 167.888V167.888C217.444 195.502 195.058 217.888 167.444 217.888V217.888C139.83 217.888 117.444 195.502 117.444 167.888V167.888Z"/>
+<path d="M117.444 53C117.444 25.3858 139.83 3 167.444 3V3C195.058 3 217.444 25.3858 217.444 53V98C217.444 100.761 215.205 103 212.444 103H122.444C119.683 103 117.444 100.761 117.444 98V53Z"/>
+<path d="M102 167.888C102 195.502 79.6142 217.888 52 217.888V217.888C24.3858 217.888 2 195.502 2 167.888L2.00001 122.888C2.00001 120.126 4.23859 117.888 7.00001 117.888L97 117.888C99.7614 117.888 102 120.126 102 122.888L102 167.888Z"/>
+</svg>

+ 170 - 0
src/layouts/LeftMode.vue

@@ -0,0 +1,170 @@
+<script lang="ts" setup>
+import { computed } from "vue"
+import { storeToRefs } from "pinia"
+import { useAppStore } from "@/store/modules/app"
+import { useSettingsStore } from "@/store/modules/settings"
+import { AppMain, NavigationBar, Sidebar, TagsView } from "./components"
+import { useDevice } from "@/hooks/useDevice"
+
+const { isMobile } = useDevice()
+const appStore = useAppStore()
+const settingsStore = useSettingsStore()
+const { showTagsView, fixedHeader } = storeToRefs(settingsStore)
+
+/** 定义计算属性 layoutClasses,用于控制布局的类名 */
+const layoutClasses = computed(() => {
+  return {
+    hideSidebar: !appStore.sidebar.opened,
+    openSidebar: appStore.sidebar.opened,
+    withoutAnimation: appStore.sidebar.withoutAnimation,
+    mobile: isMobile.value
+  }
+})
+
+/** 用于处理点击 mobile 端侧边栏遮罩层的事件 */
+const handleClickOutside = () => {
+  appStore.closeSidebar(false)
+}
+</script>
+
+<template>
+  <div :class="layoutClasses" class="app-wrapper">
+    <!-- mobile 端侧边栏遮罩层 -->
+    <div v-if="layoutClasses.mobile && layoutClasses.openSidebar" class="drawer-bg" @click="handleClickOutside" />
+    <!-- 左侧边栏 -->
+    <Sidebar class="sidebar-container" />
+    <!-- 主容器 -->
+    <div :class="{ hasTagsView: showTagsView }" class="main-container">
+      <!-- 头部导航栏和标签栏 -->
+      <div :class="{ 'fixed-header': fixedHeader }" class="layout-header">
+        <NavigationBar />
+        <TagsView v-show="showTagsView" />
+      </div>
+      <!-- 页面主体内容 -->
+      <AppMain class="app-main" />
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import "@/styles/mixins.scss";
+$transition-time: 0.35s;
+
+.app-wrapper {
+  @extend %clearfix;
+  position: relative;
+  width: 100%;
+}
+
+.drawer-bg {
+  background-color: rgba(0, 0, 0, 0.3);
+  width: 100%;
+  top: 0;
+  height: 100%;
+  position: absolute;
+  z-index: 999;
+}
+
+.sidebar-container {
+  background-color: var(--v3-sidebar-menu-bg-color);
+  transition: width $transition-time;
+  width: var(--v3-sidebar-width) !important;
+  height: 100%;
+  position: fixed;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 1001;
+  overflow: hidden;
+  border-right: var(--v3-sidebar-border-right);
+}
+
+.main-container {
+  min-height: 100%;
+  transition: margin-left $transition-time;
+  margin-left: var(--v3-sidebar-width);
+  position: relative;
+}
+
+.fixed-header {
+  position: fixed !important;
+  top: 0;
+  right: 0;
+  z-index: 9;
+  width: calc(100% - var(--v3-sidebar-width));
+  transition: width $transition-time;
+}
+
+.layout-header {
+  position: relative;
+  z-index: 9;
+  background-color: var(--v3-header-bg-color);
+  box-shadow: var(--v3-header-box-shadow);
+  border-bottom: var(--v3-header-border-bottom);
+}
+
+.app-main {
+  min-height: calc(100vh - var(--v3-navigationbar-height));
+  position: relative;
+  overflow: hidden;
+}
+
+.fixed-header + .app-main {
+  padding-top: var(--v3-navigationbar-height);
+  height: 100vh;
+  overflow: auto;
+}
+
+.hasTagsView {
+  .app-main {
+    min-height: calc(100vh - var(--v3-header-height));
+  }
+  .fixed-header + .app-main {
+    padding-top: var(--v3-header-height);
+  }
+}
+
+.hideSidebar {
+  .sidebar-container {
+    width: var(--v3-sidebar-hide-width) !important;
+  }
+  .main-container {
+    margin-left: var(--v3-sidebar-hide-width);
+  }
+  .fixed-header {
+    width: calc(100% - var(--v3-sidebar-hide-width));
+  }
+}
+
+// 适配 mobile 端
+.mobile {
+  .sidebar-container {
+    transition: transform $transition-time;
+    width: var(--v3-sidebar-width) !important;
+  }
+  .main-container {
+    margin-left: 0px;
+  }
+  .fixed-header {
+    width: 100%;
+  }
+  &.openSidebar {
+    position: fixed;
+    top: 0;
+  }
+  &.hideSidebar {
+    .sidebar-container {
+      pointer-events: none;
+      transition-duration: 0.3s;
+      transform: translate3d(calc(0px - var(--v3-sidebar-width)), 0, 0);
+    }
+  }
+}
+
+.withoutAnimation {
+  .sidebar-container,
+  .main-container {
+    transition: none;
+  }
+}
+</style>

+ 111 - 0
src/layouts/LeftTopMode.vue

@@ -0,0 +1,111 @@
+<script lang="ts" setup>
+import { computed } from "vue"
+import { storeToRefs } from "pinia"
+import { useAppStore } from "@/store/modules/app"
+import { useSettingsStore } from "@/store/modules/settings"
+import { AppMain, NavigationBar, Sidebar, TagsView, Logo } from "./components"
+
+const appStore = useAppStore()
+const settingsStore = useSettingsStore()
+const { showTagsView, showLogo } = storeToRefs(settingsStore)
+
+/** 定义计算属性 layoutClasses,用于控制布局的类名 */
+const layoutClasses = computed(() => {
+  return {
+    hideSidebar: !appStore.sidebar.opened
+  }
+})
+</script>
+
+<template>
+  <div :class="layoutClasses" class="app-wrapper">
+    <!-- 头部导航栏和标签栏 -->
+    <div class="fixed-header layout-header">
+      <Logo v-if="showLogo" :collapse="false" class="logo" />
+      <div class="content">
+        <NavigationBar />
+        <TagsView v-show="showTagsView" />
+      </div>
+    </div>
+    <!-- 主容器 -->
+    <div :class="{ hasTagsView: showTagsView }" class="main-container">
+      <!-- 左侧边栏 -->
+      <Sidebar class="sidebar-container" />
+      <!-- 页面主体内容 -->
+      <AppMain class="app-main" />
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import "@/styles/mixins.scss";
+$transition-time: 0.35s;
+
+.app-wrapper {
+  @extend %clearfix;
+  width: 100%;
+}
+
+.fixed-header {
+  position: fixed;
+  top: 0;
+  z-index: 1002;
+  width: 100%;
+  display: flex;
+  .logo {
+    width: var(--v3-sidebar-width);
+  }
+  .content {
+    flex: 1;
+    position: relative;
+  }
+}
+
+.layout-header {
+  background-color: var(--v3-header-bg-color);
+  box-shadow: var(--v3-header-box-shadow);
+  border-bottom: var(--v3-header-border-bottom);
+}
+
+.main-container {
+  min-height: 100%;
+}
+
+.sidebar-container {
+  transition: width $transition-time;
+  width: var(--v3-sidebar-width) !important;
+  height: 100%;
+  position: fixed;
+  left: 0;
+  z-index: 1001;
+  overflow: hidden;
+  border-right: var(--v3-sidebar-border-right);
+  padding-top: var(--v3-navigationbar-height);
+}
+
+.app-main {
+  transition: padding-left $transition-time;
+  padding-top: var(--v3-navigationbar-height);
+  padding-left: var(--v3-sidebar-width);
+  height: 100vh;
+  overflow: auto;
+}
+
+.hideSidebar {
+  .sidebar-container {
+    width: var(--v3-sidebar-hide-width) !important;
+  }
+  .app-main {
+    padding-left: var(--v3-sidebar-hide-width);
+  }
+}
+
+.hasTagsView {
+  .sidebar-container {
+    padding-top: var(--v3-header-height);
+  }
+  .app-main {
+    padding-top: var(--v3-header-height);
+  }
+}
+</style>

+ 75 - 0
src/layouts/TopMode.vue

@@ -0,0 +1,75 @@
+<script lang="ts" setup>
+import { storeToRefs } from "pinia"
+import { useSettingsStore } from "@/store/modules/settings"
+import { AppMain, NavigationBar, TagsView, Logo } from "./components"
+
+const settingsStore = useSettingsStore()
+const { showTagsView, showLogo } = storeToRefs(settingsStore)
+</script>
+
+<template>
+  <div class="app-wrapper">
+    <!-- 头部导航栏和标签栏 -->
+    <div class="fixed-header layout-header">
+      <div class="content">
+        <Logo v-if="showLogo" :collapse="false" class="logo" />
+        <NavigationBar class="navigation-bar" />
+      </div>
+      <TagsView v-show="showTagsView" />
+    </div>
+    <!-- 主容器 -->
+    <div :class="{ hasTagsView: showTagsView }" class="main-container">
+      <!-- 页面主体内容 -->
+      <AppMain class="app-main" />
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import "@/styles/mixins.scss";
+$transition-time: 0.35s;
+
+.app-wrapper {
+  @extend %clearfix;
+  width: 100%;
+}
+
+.fixed-header {
+  position: fixed;
+  top: 0;
+  z-index: 1002;
+  width: 100%;
+  .logo {
+    width: var(--v3-sidebar-width);
+  }
+  .content {
+    display: flex;
+    .navigation-bar {
+      flex: 1;
+    }
+  }
+}
+
+.layout-header {
+  background-color: var(--v3-header-bg-color);
+  box-shadow: var(--v3-header-box-shadow);
+  border-bottom: var(--v3-header-border-bottom);
+}
+
+.main-container {
+  min-height: 100%;
+}
+
+.app-main {
+  transition: padding-left $transition-time;
+  padding-top: var(--v3-navigationbar-height);
+  height: 100vh;
+  overflow: auto;
+}
+
+.hasTagsView {
+  .app-main {
+    padding-top: var(--v3-header-height);
+  }
+}
+</style>

+ 49 - 0
src/layouts/components/AppMain.vue

@@ -0,0 +1,49 @@
+<script lang="ts" setup>
+import { useTagsViewStore } from "@/store/modules/tags-view"
+import { useSettingsStore } from "@/store/modules/settings"
+import Footer from "./Footer/index.vue"
+
+const tagsViewStore = useTagsViewStore()
+const settingsStore = useSettingsStore()
+</script>
+
+<template>
+  <section class="app-main">
+    <div class="app-scrollbar">
+      <!-- key 采用 route.path 和 route.fullPath 有着不同的效果,大多数时候 path 更通用 -->
+      <router-view v-slot="{ Component, route }">
+        <transition name="el-fade-in" mode="out-in">
+          <keep-alive :include="tagsViewStore.cachedViews">
+            <component :is="Component" :key="route.path" class="app-container-grow" />
+          </keep-alive>
+        </transition>
+      </router-view>
+      <!-- 页脚 -->
+      <Footer v-if="settingsStore.showFooter" />
+    </div>
+    <!-- 返回顶部 -->
+    <el-backtop />
+    <!-- 返回顶部(固定 Header 情况下) -->
+    <el-backtop target=".app-scrollbar" />
+  </section>
+</template>
+
+<style lang="scss" scoped>
+@import "@/styles/mixins.scss";
+
+.app-main {
+  width: 100%;
+  display: flex;
+}
+
+.app-scrollbar {
+  flex-grow: 1;
+  overflow: auto;
+  @extend %scrollbar;
+  display: flex;
+  flex-direction: column;
+  .app-container-grow {
+    flex-grow: 1;
+  }
+}
+</style>

+ 65 - 0
src/layouts/components/Breadcrumb/index.vue

@@ -0,0 +1,65 @@
+<script lang="ts" setup>
+import { ref } from "vue"
+import { type RouteLocationMatched, useRoute, useRouter } from "vue-router"
+import { useRouteListener } from "@/hooks/useRouteListener"
+import { compile } from "path-to-regexp"
+
+const route = useRoute()
+const router = useRouter()
+const { listenerRouteChange } = useRouteListener()
+
+/** 定义响应式数据 breadcrumbs,用于存储面包屑导航信息 */
+const breadcrumbs = ref<RouteLocationMatched[]>([])
+
+/** 获取面包屑导航信息 */
+const getBreadcrumb = () => {
+  breadcrumbs.value = route.matched.filter((item) => item.meta?.title && item.meta?.breadcrumb !== false)
+}
+
+/** 编译路由路径 */
+const pathCompile = (path: string) => {
+  const toPath = compile(path)
+  return toPath(route.params)
+}
+
+/** 处理面包屑导航点击事件 */
+const handleLink = (item: RouteLocationMatched) => {
+  const { redirect, path } = item
+  if (redirect) {
+    router.push(redirect as string)
+    return
+  }
+  router.push(pathCompile(path))
+}
+
+/** 监听路由变化,更新面包屑导航信息 */
+listenerRouteChange((route) => {
+  if (route.path.startsWith("/redirect/")) return
+  getBreadcrumb()
+}, true)
+</script>
+
+<template>
+  <el-breadcrumb>
+    <el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="item.path">
+      <span v-if="item.redirect === 'noRedirect' || index === breadcrumbs.length - 1" class="no-redirect">
+        {{ item.meta.title }}
+      </span>
+      <a v-else @click.prevent="handleLink(item)">
+        {{ item.meta.title }}
+      </a>
+    </el-breadcrumb-item>
+  </el-breadcrumb>
+</template>
+
+<style lang="scss" scoped>
+.el-breadcrumb {
+  line-height: var(--v3-navigationbar-height);
+  .no-redirect {
+    color: var(--el-text-color-placeholder);
+  }
+  a {
+    font-weight: normal;
+  }
+}
+</style>

+ 67 - 0
src/layouts/components/CompConsumer/index.ts

@@ -0,0 +1,67 @@
+import { type VNode, cloneVNode, createVNode, defineComponent, h, KeepAlive } from "vue"
+import { useRoute } from "vue-router"
+import { useTagsViewStore } from "@/store/modules/tags-view"
+
+interface CompConsumerProps {
+  component: VNode
+}
+
+/** 定义 compMap 对象,用于存储路由名称和对应的组件 */
+const compMap = new Map<string, VNode>()
+
+/**
+ * CompConsumer 组件
+ * 用法:替换 <keep-alive> 标签以及内部代码,变成:<CompConsumer :component="Component" />
+ * 优点:缓存路由时只需写路由 Name,无需再写组件 Name
+ * 缺点:当路由表有动态路由匹配时(指向同一个组件),会出现复用组件的情况(例如修改 /info/1 时 /info/2 也会跟着改变)
+ */
+export const CompConsumer = defineComponent(
+  (props: CompConsumerProps) => {
+    const tagsViewStore = useTagsViewStore()
+    const route = useRoute()
+    return () => {
+      // 获取传入的组件
+      const component = props.component
+      // 判断当前是否包含 name,如果不包含 name,那就直接处理掉 name
+      if (!route.name) return component
+      // 获取当前组件的名称
+      const compName = (component.type as any)?.name
+      // 获取当前路由的名称
+      const routeName = route.name as string
+      let Comp: VNode
+      // 检查 compMap 中是否已经存在对应的组件
+      if (compMap.has(routeName)) {
+        // 如果存在,则直接使用该组件进行渲染
+        Comp = compMap.get(routeName)!
+      } else {
+        // 如果不存在,则克隆传入的组件并创建一个新的组件,将其添加到 compMap 中
+        const node = cloneVNode(component)
+        if (compName && compName === routeName) {
+          ;(node.type as any).name = `__${compName}__CUSTOM_NAME`
+        }
+        // @ts-expect-error this is VNode
+        Comp = defineComponent({
+          name: routeName,
+          setup() {
+            return () => node
+          }
+        })
+        compMap.set(routeName, Comp)
+      }
+      // 使用 createVNode 函数创建一个 KeepAlive 组件,并缓存 cachedViews 数组中对应的组件
+      return createVNode(
+        KeepAlive,
+        {
+          include: tagsViewStore.cachedViews
+        },
+        {
+          default: () => h(Comp)
+        }
+      )
+    }
+  },
+  {
+    name: "CompConsumer",
+    props: ["component"]
+  }
+)

+ 18 - 0
src/layouts/components/Footer/index.vue

@@ -0,0 +1,18 @@
+<script lang="ts" setup>
+const VITE_APP_TITLE = import.meta.env.VITE_APP_TITLE
+</script>
+
+<template>
+  <footer class="layout-footer">{{ VITE_APP_TITLE }}</footer>
+</template>
+
+<style lang="scss" scoped>
+.layout-footer {
+  width: 100%;
+  min-height: 50px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: var(--el-text-color-placeholder);
+}
+</style>

+ 36 - 0
src/layouts/components/Hamburger/index.vue

@@ -0,0 +1,36 @@
+<script lang="ts" setup>
+import { Expand, Fold } from "@element-plus/icons-vue"
+
+interface Props {
+  isActive?: boolean
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  isActive: false
+})
+
+/** Vue 3.3+ defineEmits 语法 */
+const emit = defineEmits<{
+  toggleClick: []
+}>()
+
+const toggleClick = () => {
+  emit("toggleClick")
+}
+</script>
+
+<template>
+  <div @click="toggleClick">
+    <el-icon :size="20" class="icon">
+      <Fold v-if="props.isActive" />
+      <Expand v-else />
+    </el-icon>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.icon {
+  vertical-align: middle;
+  color: var(--v3-hamburger-text-color);
+}
+</style>

+ 83 - 0
src/layouts/components/Logo/index.vue

@@ -0,0 +1,83 @@
+<script lang="ts" setup>
+import { useLayoutMode } from "@/hooks/useLayoutMode"
+import logo from "@/assets/layouts/logo.png?url"
+import logoText1 from "@/assets/layouts/logo-text-1.png?url"
+import logoText2 from "@/assets/layouts/logo-text-2.png?url"
+
+interface Props {
+  collapse?: boolean
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  collapse: true
+})
+
+const { isLeft, isTop } = useLayoutMode()
+</script>
+
+<template>
+  <div class="layout-logo-container" :class="{ collapse: props.collapse, 'layout-mode-top': isTop }">
+    <transition name="layout-logo-fade">
+      <router-link v-if="props.collapse" key="collapse" to="/">
+        <img :src="logo" class="layout-logo" />
+      </router-link>
+      <router-link v-else key="expand" to="/">
+        <div class="layout-logo-text">
+          <img class="latout_card" src="@/assets/layouts/logo.png" />
+          <span :style="{ color: (!isLeft ? '#000' : '#fff') }">宝智达</span>
+        </div>
+      </router-link>
+    </transition>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.layout-logo-container {
+  position: relative;
+  width: 100%;
+  height: var(--v3-header-height);
+  line-height: var(--v3-header-height);
+  text-align: center;
+  overflow: hidden;
+
+  .layout-logo {
+    display: none;
+  }
+
+  .layout-logo-text {
+    display: flex;
+    align-items: center;
+    height: 100%;
+    vertical-align: middle;
+    justify-content: center;
+
+    span {
+      margin-left: 10px;
+      font-size: 25px;
+    }
+  }
+}
+
+.latout_card {
+  width: 32px;
+  height: 32px;
+}
+
+.layout-mode-top {
+  height: var(--v3-navigationbar-height);
+  line-height: var(--v3-navigationbar-height);
+}
+
+.collapse {
+  .layout-logo {
+    width: 32px;
+    height: 32px;
+    vertical-align: middle;
+    display: inline-block;
+  }
+
+  .layout-logo-text {
+    display: none;
+  }
+}
+</style>

+ 133 - 0
src/layouts/components/NavigationBar/index.vue

@@ -0,0 +1,133 @@
+<script lang="ts" setup>
+import { useRouter } from "vue-router"
+import { storeToRefs } from "pinia"
+import { useAppStore } from "@/store/modules/app"
+import { useSettingsStore } from "@/store/modules/settings"
+import { useUserStore } from "@/store/modules/user"
+import Hamburger from "../Hamburger/index.vue"
+import Breadcrumb from "../Breadcrumb/index.vue"
+import Sidebar from "../Sidebar/index.vue"
+import Notify from "@/components/Notify/index.vue"
+import ThemeSwitch from "@/components/ThemeSwitch/index.vue"
+import Screenfull from "@/components/Screenfull/index.vue"
+import SearchMenu from "@/components/SearchMenu/index.vue"
+import { useDevice } from "@/hooks/useDevice"
+import { useLayoutMode } from "@/hooks/useLayoutMode"
+
+const { isMobile } = useDevice()
+const { isTop } = useLayoutMode()
+const router = useRouter()
+const appStore = useAppStore()
+const userStore = useUserStore()
+const settingsStore = useSettingsStore()
+const { showNotify, showThemeSwitch, showScreenfull, showSearchMenu } = storeToRefs(settingsStore)
+
+/** 切换侧边栏 */
+const toggleSidebar = () => {
+  appStore.toggleSidebar(false)
+}
+
+/** 登出 */
+const logout = () => {
+  userStore.logout()
+  router.push("/login")
+}
+</script>
+
+<template>
+  <div class="navigation-bar">
+    <Hamburger v-if="!isTop || isMobile" :is-active="appStore.sidebar.opened" class="hamburger"
+      @toggle-click="toggleSidebar" />
+    <Breadcrumb v-if="!isTop || isMobile" class="breadcrumb" />
+    <Sidebar v-if="isTop && !isMobile" class="sidebar" />
+    <div class="right-menu">
+      <SearchMenu v-if="showSearchMenu" class="right-menu-item" />
+      <Screenfull v-if="showScreenfull" class="right-menu-item" />
+      <ThemeSwitch v-if="showThemeSwitch" class="right-menu-item" />
+      <!-- <Notify v-if="showNotify" class="right-menu-item" /> -->
+      <el-dropdown class="right-menu-item">
+        <div class="right-menu-avatar">
+          <el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" :size="30" />
+          <span>{{ userStore.username }}</span>
+        </div>
+        <template #dropdown>
+          <el-dropdown-menu>
+            <el-dropdown-item divided @click="logout">
+              <span style="display: block">退出登录</span>
+            </el-dropdown-item>
+          </el-dropdown-menu>
+        </template>
+      </el-dropdown>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.navigation-bar {
+  height: var(--v3-navigationbar-height);
+  overflow: hidden;
+  color: var(--v3-navigationbar-text-color);
+  display: flex;
+  justify-content: space-between;
+
+  .hamburger {
+    display: flex;
+    align-items: center;
+    height: 100%;
+    padding: 0 15px;
+    cursor: pointer;
+  }
+
+  .breadcrumb {
+    flex: 1;
+
+    // 参考 Bootstrap 的响应式设计将宽度设置为 576
+    @media screen and (max-width: 576px) {
+      display: none;
+    }
+  }
+
+  .sidebar {
+    flex: 1;
+    // 设置 min-width 是为了让 Sidebar 里的 el-menu 宽度自适应
+    min-width: 0px;
+
+    :deep(.el-menu) {
+      background-color: transparent;
+    }
+
+    :deep(.el-sub-menu) {
+      &.is-active {
+        .el-sub-menu__title {
+          color: var(--el-color-primary) !important;
+        }
+      }
+    }
+  }
+
+  .right-menu {
+    margin-right: 10px;
+    height: 100%;
+    display: flex;
+    align-items: center;
+
+    .right-menu-item {
+      padding: 0 10px;
+      cursor: pointer;
+
+      .right-menu-avatar {
+        display: flex;
+        align-items: center;
+
+        .el-avatar {
+          margin-right: 10px;
+        }
+
+        span {
+          font-size: 16px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 45 - 0
src/layouts/components/RightPanel/index.vue

@@ -0,0 +1,45 @@
+<script lang="ts" setup>
+import { ref } from "vue"
+import { Setting } from "@element-plus/icons-vue"
+
+interface Props {
+  buttonTop?: number
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  buttonTop: 350
+})
+
+const buttonTopCss = props.buttonTop + "px"
+const show = ref(false)
+</script>
+
+<template>
+  <div class="handle-button" @click="show = true">
+    <el-icon :size="24">
+      <Setting />
+    </el-icon>
+  </div>
+  <el-drawer v-model="show" size="300px" :with-header="false">
+    <slot />
+  </el-drawer>
+</template>
+
+<style lang="scss" scoped>
+.handle-button {
+  width: 48px;
+  height: 48px;
+  background-color: var(--v3-rightpanel-button-bg-color);
+  position: fixed;
+  top: v-bind(buttonTopCss);
+  right: 0;
+  border-radius: 6px 0 0 6px;
+  z-index: 10;
+  cursor: pointer;
+  pointer-events: auto;
+  color: #ffffff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+</style>

+ 103 - 0
src/layouts/components/Settings/SelectLayoutMode.vue

@@ -0,0 +1,103 @@
+<script lang="ts" setup>
+import { useLayoutMode } from "@/hooks/useLayoutMode"
+import { LayoutModeEnum } from "@/constants/app-key"
+
+const { isLeft, isTop, isLeftTop, setLayoutMode } = useLayoutMode()
+</script>
+
+<template>
+  <div class="select-layout-mode">
+    <el-tooltip content="左侧模式">
+      <el-container class="layout-mode left" :class="{ active: isLeft }" @click="setLayoutMode(LayoutModeEnum.Left)">
+        <el-aside />
+        <el-container>
+          <el-header />
+          <el-main />
+        </el-container>
+      </el-container>
+    </el-tooltip>
+    <el-tooltip content="顶部模式">
+      <el-container class="layout-mode top" :class="{ active: isTop }" @click="setLayoutMode(LayoutModeEnum.Top)">
+        <el-header />
+        <el-main />
+      </el-container>
+    </el-tooltip>
+    <el-tooltip content="混合模式">
+      <el-container
+        class="layout-mode left-top"
+        :class="{ active: isLeftTop }"
+        @click="setLayoutMode(LayoutModeEnum.LeftTop)"
+      >
+        <el-header />
+        <el-container>
+          <el-aside />
+          <el-main />
+        </el-container>
+      </el-container>
+    </el-tooltip>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.select-layout-mode {
+  display: flex;
+  justify-content: space-between;
+}
+
+.layout-mode {
+  width: 60px;
+  flex-grow: 0;
+  overflow: hidden;
+  cursor: pointer;
+  border-radius: 6px;
+  border: 2px solid transparent;
+  &:hover {
+    border: 2px solid var(--el-color-primary);
+  }
+}
+
+.active {
+  border: 2px solid var(--el-color-primary);
+}
+
+.el-header {
+  height: 12px;
+}
+
+.el-aside {
+  width: 16px;
+}
+
+.left {
+  .el-header {
+    background-color: var(--el-fill-color-darker);
+  }
+  .el-aside {
+    background-color: var(--el-color-primary);
+  }
+  .el-main {
+    background-color: var(--el-fill-color-lighter);
+  }
+}
+
+.top {
+  .el-header {
+    background-color: var(--el-color-primary);
+  }
+  .el-main {
+    background-color: var(--el-fill-color-lighter);
+  }
+}
+
+.left-top {
+  .el-header {
+    background-color: var(--el-fill-color-darker);
+  }
+  .el-aside {
+    background-color: var(--el-color-primary);
+  }
+  .el-main {
+    background-color: var(--el-fill-color-lighter);
+  }
+}
+</style>

+ 86 - 0
src/layouts/components/Settings/index.vue

@@ -0,0 +1,86 @@
+<script lang="ts" setup>
+import { watchEffect } from "vue"
+import { storeToRefs } from "pinia"
+import { useSettingsStore } from "@/store/modules/settings"
+import { useLayoutMode } from "@/hooks/useLayoutMode"
+import { resetConfigLayout } from "@/utils"
+import SelectLayoutMode from "./SelectLayoutMode.vue"
+import { Refresh } from "@element-plus/icons-vue"
+
+const { isLeft } = useLayoutMode()
+const settingsStore = useSettingsStore()
+
+/** 使用 storeToRefs 将提取的属性保持其响应性 */
+const {
+  showTagsView,
+  showLogo,
+  fixedHeader,
+  showFooter,
+  showNotify,
+  showThemeSwitch,
+  showScreenfull,
+  showSearchMenu,
+  cacheTagsView,
+  showWatermark,
+  showGreyMode,
+  showColorWeakness
+} = storeToRefs(settingsStore)
+
+/** 定义 switch 设置项 */
+const switchSettings = {
+  显示标签栏: showTagsView,
+  "显示 Logo": showLogo,
+  "固定 Header": fixedHeader,
+  "显示页脚 Footer": showFooter,
+  显示消息通知: showNotify,
+  显示切换主题按钮: showThemeSwitch,
+  显示全屏按钮: showScreenfull,
+  显示搜索按钮: showSearchMenu,
+  是否缓存标签栏: cacheTagsView,
+  开启系统水印: showWatermark,
+  显示灰色模式: showGreyMode,
+  显示色弱模式: showColorWeakness
+}
+
+/** 非左侧模式时,Header 都是 fixed 布局 */
+watchEffect(() => {
+  !isLeft.value && (fixedHeader.value = true)
+})
+</script>
+
+<template>
+  <div class="setting-container">
+    <h4>布局配置</h4>
+    <SelectLayoutMode />
+    <el-divider />
+    <h4>功能配置</h4>
+    <div class="setting-item" v-for="(settingValue, settingName, index) in switchSettings" :key="index">
+      <span class="setting-name">{{ settingName }}</span>
+      <el-switch v-model="settingValue.value" :disabled="!isLeft && settingName === '固定 Header'" />
+    </div>
+    <el-button type="danger" :icon="Refresh" @click="resetConfigLayout">重 置</el-button>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+@import "@/styles/mixins.scss";
+
+.setting-container {
+  padding: 20px;
+  .setting-item {
+    font-size: 14px;
+    color: var(--el-text-color-regular);
+    padding: 5px 0;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    .setting-name {
+      @extend %ellipsis;
+    }
+  }
+  .el-button {
+    margin-top: 40px;
+    width: 100%;
+  }
+}
+</style>

+ 97 - 0
src/layouts/components/Sidebar/SidebarItem.vue

@@ -0,0 +1,97 @@
+<script lang="ts" setup>
+import { computed } from "vue"
+import { type RouteRecordRaw } from "vue-router"
+import SidebarItemLink from "./SidebarItemLink.vue"
+import { isExternal } from "@/utils/validate"
+import path from "path-browserify"
+
+interface Props {
+  item: RouteRecordRaw
+  basePath?: string
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  basePath: ""
+})
+
+/** 是否始终显示根菜单 */
+const alwaysShowRootMenu = computed(() => props.item.meta?.alwaysShow)
+
+/** 显示的子菜单 */
+const showingChildren = computed(() => {
+  return props.item.children?.filter((child) => !child.meta?.hidden) ?? []
+})
+
+/** 显示的子菜单数量 */
+const showingChildNumber = computed(() => {
+  return showingChildren.value.length
+})
+
+/** 唯一的子菜单项 */
+const theOnlyOneChild = computed(() => {
+  const number = showingChildNumber.value
+  switch (true) {
+    case number > 1:
+      return null
+    case number === 1:
+      return showingChildren.value[0]
+    default:
+      return { ...props.item, path: "" }
+  }
+})
+
+/** 解析路径 */
+const resolvePath = (routePath: string) => {
+  switch (true) {
+    case isExternal(routePath):
+      return routePath
+    case isExternal(props.basePath):
+      return props.basePath
+    default:
+      return path.resolve(props.basePath, routePath)
+  }
+}
+</script>
+
+<template>
+  <template v-if="!alwaysShowRootMenu && theOnlyOneChild && !theOnlyOneChild.children">
+    <SidebarItemLink v-if="theOnlyOneChild.meta" :to="resolvePath(theOnlyOneChild.path)">
+      <el-menu-item :index="resolvePath(theOnlyOneChild.path)">
+        <SvgIcon v-if="theOnlyOneChild.meta.svgIcon" :name="theOnlyOneChild.meta.svgIcon" />
+        <component v-else-if="theOnlyOneChild.meta.elIcon" :is="theOnlyOneChild.meta.elIcon" class="el-icon" />
+        <template v-if="theOnlyOneChild.meta.title" #title>
+          {{ theOnlyOneChild.meta.title }}
+        </template>
+      </el-menu-item>
+    </SidebarItemLink>
+  </template>
+  <el-sub-menu v-else :index="resolvePath(props.item.path)" teleported>
+    <template #title>
+      <SvgIcon v-if="props.item.meta?.svgIcon" :name="props.item.meta.svgIcon" />
+      <component v-else-if="props.item.meta?.elIcon" :is="props.item.meta.elIcon" class="el-icon" />
+      <span v-if="props.item.meta?.title">{{ props.item.meta.title }}</span>
+    </template>
+    <template v-if="props.item.children">
+      <SidebarItem
+        v-for="child in showingChildren"
+        :key="child.path"
+        :item="child"
+        :base-path="resolvePath(child.path)"
+      />
+    </template>
+  </el-sub-menu>
+</template>
+
+<style lang="scss" scoped>
+.svg-icon {
+  min-width: 1em;
+  margin-right: 12px;
+  font-size: 18px;
+}
+
+.el-icon {
+  width: 1em !important;
+  margin-right: 12px !important;
+  font-size: 18px;
+}
+</style>

+ 18 - 0
src/layouts/components/Sidebar/SidebarItemLink.vue

@@ -0,0 +1,18 @@
+<script lang="ts" setup>
+import { isExternal } from "@/utils/validate"
+
+interface Props {
+  to: string
+}
+
+const props = defineProps<Props>()
+</script>
+
+<template>
+  <a v-if="isExternal(props.to)" :href="props.to" target="_blank" rel="noopener">
+    <slot />
+  </a>
+  <router-link v-else :to="props.to">
+    <slot />
+  </router-link>
+</template>

+ 156 - 0
src/layouts/components/Sidebar/index.vue

@@ -0,0 +1,156 @@
+<script lang="ts" setup>
+import { computed } from "vue"
+import { useRoute } from "vue-router"
+import { useAppStore } from "@/store/modules/app"
+import { usePermissionStore } from "@/store/modules/permission"
+import { useSettingsStore } from "@/store/modules/settings"
+import SidebarItem from "./SidebarItem.vue"
+import Logo from "../Logo/index.vue"
+import { useDevice } from "@/hooks/useDevice"
+import { useLayoutMode } from "@/hooks/useLayoutMode"
+import { getCssVariableValue } from "@/utils"
+
+const v3SidebarMenuBgColor = getCssVariableValue("--v3-sidebar-menu-bg-color")
+const v3SidebarMenuTextColor = getCssVariableValue("--v3-sidebar-menu-text-color")
+const v3SidebarMenuActiveTextColor = getCssVariableValue("--v3-sidebar-menu-active-text-color")
+
+const { isMobile } = useDevice()
+const { isLeft, isTop } = useLayoutMode()
+const route = useRoute()
+const appStore = useAppStore()
+const permissionStore = usePermissionStore()
+const settingsStore = useSettingsStore()
+
+const activeMenu = computed(() => {
+  const {
+    meta: { activeMenu },
+    path
+  } = route
+  return activeMenu ? activeMenu : path
+})
+const noHiddenRoutes = computed(() => permissionStore.routes.filter((item) => !item.meta?.hidden))
+const isCollapse = computed(() => !appStore.sidebar.opened)
+const isLogo = computed(() => isLeft.value && settingsStore.showLogo)
+const backgroundColor = computed(() => (isLeft.value ? v3SidebarMenuBgColor : undefined))
+const textColor = computed(() => (isLeft.value ? v3SidebarMenuTextColor : undefined))
+const activeTextColor = computed(() => (isLeft.value ? v3SidebarMenuActiveTextColor : undefined))
+const sidebarMenuItemHeight = computed(() => {
+  return !isTop.value ? "var(--v3-sidebar-menu-item-height)" : "var(--v3-navigationbar-height)"
+})
+const sidebarMenuHoverBgColor = computed(() => {
+  return !isTop.value ? "var(--v3-sidebar-menu-hover-bg-color)" : "transparent"
+})
+const tipLineWidth = computed(() => {
+  return !isTop.value ? "2px" : "0px"
+})
+// 当为顶部模式时隐藏垂直滚动条
+const hiddenScrollbarVerticalBar = computed(() => {
+  return isTop.value ? "none" : "block"
+})
+</script>
+
+<template>
+  <div :class="{ 'has-logo': isLogo }">
+    <Logo v-if="isLogo" :collapse="isCollapse" />
+    <el-scrollbar wrap-class="scrollbar-wrapper">
+      <el-menu
+        :default-active="activeMenu"
+        :collapse="isCollapse && !isTop"
+        :background-color="backgroundColor"
+        :text-color="textColor"
+        :active-text-color="activeTextColor"
+        :unique-opened="true"
+        :collapse-transition="false"
+        :mode="isTop && !isMobile ? 'horizontal' : 'vertical'"
+      >
+        <SidebarItem v-for="route in noHiddenRoutes" :key="route.path" :item="route" :base-path="route.path" />
+      </el-menu>
+    </el-scrollbar>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+%tip-line {
+  &::before {
+    content: "";
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: v-bind(tipLineWidth);
+    height: 100%;
+    background-color: var(--v3-sidebar-menu-tip-line-bg-color);
+  }
+}
+
+.has-logo {
+  .el-scrollbar {
+    // 多 1% 是为了在左侧模式时侧边栏最底部不显示 1px 左右的白色线条
+    height: calc(101% - var(--v3-header-height));
+  }
+}
+
+.el-scrollbar {
+  // 多 1% 是为了在顶部模式时防止垂直滚动
+  height: 101%;
+  :deep(.scrollbar-wrapper) {
+    // 限制水平宽度
+    overflow-x: hidden !important;
+    .el-scrollbar__view {
+      height: 100%;
+    }
+  }
+  // 滚动条
+  :deep(.el-scrollbar__bar) {
+    &.is-horizontal {
+      // 隐藏水平滚动条
+      display: none;
+    }
+    &.is-vertical {
+      // 当为顶部模式时隐藏垂直滚动条
+      display: v-bind(hiddenScrollbarVerticalBar);
+    }
+  }
+}
+
+.el-menu {
+  border: none;
+  min-height: 100%;
+  width: 100% !important;
+}
+
+.el-menu--horizontal {
+  height: v-bind(sidebarMenuItemHeight);
+}
+
+:deep(.el-menu-item),
+:deep(.el-sub-menu__title),
+:deep(.el-sub-menu .el-menu-item),
+:deep(.el-menu--horizontal .el-menu-item) {
+  height: v-bind(sidebarMenuItemHeight);
+  line-height: v-bind(sidebarMenuItemHeight);
+  &.is-active,
+  &:hover {
+    background-color: v-bind(sidebarMenuHoverBgColor);
+  }
+}
+
+:deep(.el-sub-menu) {
+  &.is-active {
+    > .el-sub-menu__title {
+      color: v-bind(activeTextColor) !important;
+    }
+  }
+}
+
+:deep(.el-menu-item.is-active) {
+  @extend %tip-line;
+}
+
+.el-menu--collapse {
+  :deep(.el-sub-menu.is-active) {
+    .el-sub-menu__title {
+      @extend %tip-line;
+    }
+  }
+}
+</style>

+ 155 - 0
src/layouts/components/TagsView/ScrollPane.vue

@@ -0,0 +1,155 @@
+<script lang="ts" setup>
+import { ref, nextTick } from "vue"
+import { RouterLink, useRoute } from "vue-router"
+import { useSettingsStore } from "@/store/modules/settings"
+import { useRouteListener } from "@/hooks/useRouteListener"
+import Screenfull from "@/components/Screenfull/index.vue"
+import { ElScrollbar } from "element-plus"
+import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue"
+
+interface Props {
+  tagRefs: InstanceType<typeof RouterLink>[]
+}
+
+const props = defineProps<Props>()
+
+const route = useRoute()
+const settingsStore = useSettingsStore()
+const { listenerRouteChange } = useRouteListener()
+
+/** 滚动条组件元素的引用 */
+const scrollbarRef = ref<InstanceType<typeof ElScrollbar>>()
+/** 滚动条内容元素的引用 */
+const scrollbarContentRef = ref<HTMLDivElement>()
+
+/** 当前滚动条距离左边的距离 */
+let currentScrollLeft = 0
+/** 每次滚动距离 */
+const translateDistance = 200
+
+/** 滚动时触发 */
+const scroll = ({ scrollLeft }: { scrollLeft: number }) => {
+  currentScrollLeft = scrollLeft
+}
+
+/** 鼠标滚轮滚动时触发 */
+const wheelScroll = ({ deltaY }: WheelEvent) => {
+  if (/^-/.test(deltaY.toString())) {
+    scrollTo("left")
+  } else {
+    scrollTo("right")
+  }
+}
+
+/** 获取可能需要的宽度 */
+const getWidth = () => {
+  /** 可滚动内容的长度 */
+  const scrollbarContentRefWidth = scrollbarContentRef.value!.clientWidth
+  /** 滚动可视区宽度 */
+  const scrollbarRefWidth = scrollbarRef.value!.wrapRef!.clientWidth
+  /** 最后剩余可滚动的宽度 */
+  const lastDistance = scrollbarContentRefWidth - scrollbarRefWidth - currentScrollLeft
+
+  return { scrollbarContentRefWidth, scrollbarRefWidth, lastDistance }
+}
+
+/** 左右滚动 */
+const scrollTo = (direction: "left" | "right", distance: number = translateDistance) => {
+  let scrollLeft = 0
+  const { scrollbarContentRefWidth, scrollbarRefWidth, lastDistance } = getWidth()
+  // 没有横向滚动条,直接结束
+  if (scrollbarRefWidth > scrollbarContentRefWidth) return
+  if (direction === "left") {
+    scrollLeft = Math.max(0, currentScrollLeft - distance)
+  } else {
+    scrollLeft = Math.min(currentScrollLeft + distance, currentScrollLeft + lastDistance)
+  }
+  scrollbarRef.value!.setScrollLeft(scrollLeft)
+}
+
+/** 移动到目标位置 */
+const moveTo = () => {
+  const tagRefs = props.tagRefs
+  for (let i = 0; i < tagRefs.length; i++) {
+    // @ts-ignore
+    if (route.path === tagRefs[i].$props.to.path) {
+      // @ts-ignore
+      const el: HTMLElement = tagRefs[i].$el
+      const offsetWidth = el.offsetWidth
+      const offsetLeft = el.offsetLeft
+      const { scrollbarRefWidth } = getWidth()
+      // 当前 tag 在可视区域左边时
+      if (offsetLeft < currentScrollLeft) {
+        const distance = currentScrollLeft - offsetLeft
+        scrollTo("left", distance)
+        return
+      }
+      // 当前 tag 在可视区域右边时
+      const width = scrollbarRefWidth + currentScrollLeft - offsetWidth
+      if (offsetLeft > width) {
+        const distance = offsetLeft - width
+        scrollTo("right", distance)
+        return
+      }
+    }
+  }
+}
+
+/** 监听路由变化,移动到目标位置 */
+listenerRouteChange(() => {
+  nextTick(moveTo)
+})
+</script>
+
+<template>
+  <div class="scroll-container">
+    <el-icon class="arrow left" @click="scrollTo('left')">
+      <ArrowLeft />
+    </el-icon>
+    <el-scrollbar ref="scrollbarRef" @wheel.passive="wheelScroll" @scroll="scroll">
+      <div ref="scrollbarContentRef" class="scrollbar-content">
+        <slot />
+      </div>
+    </el-scrollbar>
+    <el-icon class="arrow right" @click="scrollTo('right')">
+      <ArrowRight />
+    </el-icon>
+    <Screenfull v-if="settingsStore.showScreenfull" :content="true" class="screenfull" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.scroll-container {
+  height: 100%;
+  user-select: none;
+  display: flex;
+  justify-content: space-between;
+  .arrow {
+    width: 40px;
+    height: 100%;
+    font-size: 18px;
+    cursor: pointer;
+    &.left {
+      box-shadow: 5px 0 5px -6px var(--el-border-color-darker);
+    }
+    &.right {
+      box-shadow: -5px 0 5px -6px var(--el-border-color-darker);
+    }
+  }
+  .el-scrollbar {
+    flex: 1;
+    // 防止换行(超出宽度时,显示滚动条)
+    white-space: nowrap;
+    .scrollbar-content {
+      display: inline-block;
+    }
+  }
+  .screenfull {
+    width: 40px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    cursor: pointer;
+  }
+}
+</style>

+ 257 - 0
src/layouts/components/TagsView/index.vue

@@ -0,0 +1,257 @@
+<script lang="ts" setup>
+import { getCurrentInstance, onMounted, ref, watch } from "vue"
+import { type RouteLocationNormalizedLoaded, type RouteRecordRaw, RouterLink, useRoute, useRouter } from "vue-router"
+import { type TagView, useTagsViewStore } from "@/store/modules/tags-view"
+import { usePermissionStore } from "@/store/modules/permission"
+import { useRouteListener } from "@/hooks/useRouteListener"
+import path from "path-browserify"
+import ScrollPane from "./ScrollPane.vue"
+import { Close } from "@element-plus/icons-vue"
+
+const instance = getCurrentInstance()
+const router = useRouter()
+const route = useRoute()
+const tagsViewStore = useTagsViewStore()
+const permissionStore = usePermissionStore()
+const { listenerRouteChange } = useRouteListener()
+
+/** 标签页组件元素的引用数组 */
+const tagRefs = ref<InstanceType<typeof RouterLink>[]>([])
+
+/** 右键菜单的状态 */
+const visible = ref(false)
+/** 右键菜单的 top 位置 */
+const top = ref(0)
+/** 右键菜单的 left 位置 */
+const left = ref(0)
+/** 当前正在右键操作的标签页 */
+const selectedTag = ref<TagView>({})
+/** 固定的标签页 */
+let affixTags: TagView[] = []
+
+/** 判断标签页是否激活 */
+const isActive = (tag: TagView) => {
+  return tag.path === route.path
+}
+
+/** 判断标签页是否固定 */
+const isAffix = (tag: TagView) => {
+  return tag.meta?.affix
+}
+
+/** 筛选出固定标签页 */
+const filterAffixTags = (routes: RouteRecordRaw[], basePath = "/") => {
+  const tags: TagView[] = []
+  routes.forEach((route) => {
+    if (isAffix(route)) {
+      const tagPath = path.resolve(basePath, route.path)
+      tags.push({
+        fullPath: tagPath,
+        path: tagPath,
+        name: route.name,
+        meta: { ...route.meta }
+      })
+    }
+    if (route.children) {
+      const childTags = filterAffixTags(route.children, route.path)
+      tags.push(...childTags)
+    }
+  })
+  return tags
+}
+
+/** 初始化标签页 */
+const initTags = () => {
+  affixTags = filterAffixTags(permissionStore.routes)
+  for (const tag of affixTags) {
+    // 必须含有 name 属性
+    tag.name && tagsViewStore.addVisitedView(tag)
+  }
+}
+
+/** 添加标签页 */
+const addTags = (route: RouteLocationNormalizedLoaded) => {
+  if (route.name) {
+    tagsViewStore.addVisitedView(route)
+    tagsViewStore.addCachedView(route)
+  }
+}
+
+/** 刷新当前正在右键操作的标签页 */
+const refreshSelectedTag = (view: TagView) => {
+  tagsViewStore.delCachedView(view)
+  router.replace({ path: "/redirect" + view.path, query: view.query })
+}
+
+/** 关闭当前正在右键操作的标签页 */
+const closeSelectedTag = (view: TagView) => {
+  tagsViewStore.delVisitedView(view)
+  tagsViewStore.delCachedView(view)
+  isActive(view) && toLastView(tagsViewStore.visitedViews, view)
+}
+
+/** 关闭其他标签页 */
+const closeOthersTags = () => {
+  const fullPath = selectedTag.value.fullPath
+  if (fullPath !== route.path && fullPath !== undefined) {
+    router.push(fullPath)
+  }
+  tagsViewStore.delOthersVisitedViews(selectedTag.value)
+  tagsViewStore.delOthersCachedViews(selectedTag.value)
+}
+
+/** 关闭所有标签页 */
+const closeAllTags = (view: TagView) => {
+  tagsViewStore.delAllVisitedViews()
+  tagsViewStore.delAllCachedViews()
+  if (affixTags.some((tag) => tag.path === route.path)) return
+  toLastView(tagsViewStore.visitedViews, view)
+}
+
+/** 跳转到最后一个标签页 */
+const toLastView = (visitedViews: TagView[], view: TagView) => {
+  const latestView = visitedViews.slice(-1)[0]
+  const fullPath = latestView?.fullPath
+  if (fullPath !== undefined) {
+    router.push(fullPath)
+  } else {
+    // 如果 TagsView 全部被关闭了,则默认重定向到主页
+    if (view.name === "Dashboard") {
+      // 重新加载主页
+      router.push({ path: "/redirect" + view.path, query: view.query })
+    } else {
+      router.push("/")
+    }
+  }
+}
+
+/** 打开右键菜单面板 */
+const openMenu = (tag: TagView, e: MouseEvent) => {
+  const menuMinWidth = 105
+  // 当前组件距离浏览器左端的距离
+  const offsetLeft = instance!.proxy!.$el.getBoundingClientRect().left
+  // 当前组件宽度
+  const offsetWidth = instance!.proxy!.$el.offsetWidth
+  // 面板的最大左边距
+  const maxLeft = offsetWidth - menuMinWidth
+  // 面板距离鼠标指针的距离
+  const left15 = e.clientX - offsetLeft + 15
+  left.value = left15 > maxLeft ? maxLeft : left15
+  top.value = e.clientY
+  // 显示面板
+  visible.value = true
+  // 更新当前正在右键操作的标签页
+  selectedTag.value = tag
+}
+
+/** 关闭右键菜单面板 */
+const closeMenu = () => {
+  visible.value = false
+}
+
+watch(visible, (value) => {
+  value ? document.body.addEventListener("click", closeMenu) : document.body.removeEventListener("click", closeMenu)
+})
+
+onMounted(() => {
+  initTags()
+  /** 监听路由变化 */
+  listenerRouteChange(async (route) => {
+    addTags(route)
+  }, true)
+})
+</script>
+
+<template>
+  <div class="tags-view-container">
+    <ScrollPane class="tags-view-wrapper" :tag-refs="tagRefs">
+      <router-link
+        ref="tagRefs"
+        v-for="tag in tagsViewStore.visitedViews"
+        :key="tag.path"
+        :class="{ active: isActive(tag) }"
+        class="tags-view-item"
+        :to="{ path: tag.path, query: tag.query }"
+        @click.middle="!isAffix(tag) && closeSelectedTag(tag)"
+        @contextmenu.prevent="openMenu(tag, $event)"
+      >
+        {{ tag.meta?.title }}
+        <el-icon v-if="!isAffix(tag)" :size="12" @click.prevent.stop="closeSelectedTag(tag)">
+          <Close />
+        </el-icon>
+      </router-link>
+    </ScrollPane>
+    <ul v-show="visible" class="contextmenu" :style="{ left: left + 'px', top: top + 'px' }">
+      <li @click="refreshSelectedTag(selectedTag)">刷新</li>
+      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li>
+      <li @click="closeOthersTags">关闭其它</li>
+      <li @click="closeAllTags(selectedTag)">关闭所有</li>
+    </ul>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.tags-view-container {
+  height: var(--v3-tagsview-height);
+  width: 100%;
+  color: var(--v3-tagsview-text-color);
+  overflow: hidden;
+  .tags-view-wrapper {
+    .tags-view-item {
+      display: inline-block;
+      position: relative;
+      cursor: pointer;
+      height: 26px;
+      line-height: 26px;
+      border: 1px solid var(--v3-tagsview-tag-border-color);
+      border-radius: var(--v3-tagsview-tag-border-radius);
+      background-color: var(--v3-tagsview-tag-bg-color);
+      padding: 0 8px;
+      font-size: 12px;
+      margin-left: 5px;
+      margin-top: 4px;
+      &:first-of-type {
+        margin-left: 5px;
+      }
+      &:last-of-type {
+        margin-right: 5px;
+      }
+      &.active {
+        background-color: var(--v3-tagsview-tag-active-bg-color);
+        color: var(--v3-tagsview-tag-active-text-color);
+        border-color: var(--v3-tagsview-tag-active-border-color);
+      }
+      .el-icon {
+        margin: 0 2px;
+        vertical-align: middle;
+        border-radius: 50%;
+        &:hover {
+          background-color: var(--v3-tagsview-tag-icon-hover-bg-color);
+          color: var(--v3-tagsview-tag-icon-hover-color);
+        }
+      }
+    }
+  }
+  .contextmenu {
+    margin: 0;
+    z-index: 3000;
+    position: absolute;
+    list-style-type: none;
+    padding: 5px 0;
+    border-radius: 4px;
+    font-size: 12px;
+    color: var(--v3-tagsview-contextmenu-text-color);
+    background-color: var(--v3-tagsview-contextmenu-bg-color);
+    box-shadow: var(--v3-tagsview-contextmenu-box-shadow);
+    li {
+      margin: 0;
+      padding: 7px 16px;
+      cursor: pointer;
+      &:hover {
+        color: var(--v3-tagsview-contextmenu-hover-text-color);
+        background-color: var(--v3-tagsview-contextmenu-hover-bg-color);
+      }
+    }
+  }
+}
+</style>

+ 7 - 0
src/layouts/components/index.ts

@@ -0,0 +1,7 @@
+export { default as AppMain } from "./AppMain.vue"
+export { default as NavigationBar } from "./NavigationBar/index.vue"
+export { default as Settings } from "./Settings/index.vue"
+export { default as Sidebar } from "./Sidebar/index.vue"
+export { default as TagsView } from "./TagsView/index.vue"
+export { default as RightPanel } from "./RightPanel/index.vue"
+export { default as Logo } from "./Logo/index.vue"

+ 52 - 0
src/layouts/hooks/useResize.ts

@@ -0,0 +1,52 @@
+import { onBeforeMount, onMounted, onBeforeUnmount } from "vue"
+import { useAppStore } from "@/store/modules/app"
+import { useRouteListener } from "@/hooks/useRouteListener"
+import { DeviceEnum } from "@/constants/app-key"
+
+/** 参考 Bootstrap 的响应式设计将最大移动端宽度设置为 992 */
+const MAX_MOBILE_WIDTH = 992
+
+/** 根据浏览器宽度变化,变换 Layout 布局 */
+export default () => {
+  const appStore = useAppStore()
+  const { listenerRouteChange } = useRouteListener()
+
+  /** 用于判断当前设备是否为移动端 */
+  const _isMobile = () => {
+    const rect = document.body.getBoundingClientRect()
+    return rect.width - 1 < MAX_MOBILE_WIDTH
+  }
+
+  /** 用于处理窗口大小变化事件 */
+  const _resizeHandler = () => {
+    if (!document.hidden) {
+      const isMobile = _isMobile()
+      appStore.toggleDevice(isMobile ? DeviceEnum.Mobile : DeviceEnum.Desktop)
+      isMobile && appStore.closeSidebar(true)
+    }
+  }
+  /** 监听路由变化,根据设备类型调整布局 */
+  listenerRouteChange(() => {
+    if (appStore.device === DeviceEnum.Mobile && appStore.sidebar.opened) {
+      appStore.closeSidebar(false)
+    }
+  })
+
+  /** 在组件挂载前添加窗口大小变化事件监听器 */
+  onBeforeMount(() => {
+    window.addEventListener("resize", _resizeHandler)
+  })
+
+  /** 在组件挂载后根据窗口大小判断设备类型并调整布局 */
+  onMounted(() => {
+    if (_isMobile()) {
+      appStore.toggleDevice(DeviceEnum.Mobile)
+      appStore.closeSidebar(true)
+    }
+  })
+
+  /** 在组件卸载前移除窗口大小变化事件监听器 */
+  onBeforeUnmount(() => {
+    window.removeEventListener("resize", _resizeHandler)
+  })
+}

+ 70 - 0
src/layouts/index.vue

@@ -0,0 +1,70 @@
+<script lang="ts" setup>
+import { computed, watchEffect } from "vue"
+import { storeToRefs } from "pinia"
+import { useSettingsStore } from "@/store/modules/settings"
+import useResize from "./hooks/useResize"
+import { useWatermark } from "@/hooks/useWatermark"
+import { useDevice } from "@/hooks/useDevice"
+import { useLayoutMode } from "@/hooks/useLayoutMode"
+import LeftMode from "./LeftMode.vue"
+import TopMode from "./TopMode.vue"
+import LeftTopMode from "./LeftTopMode.vue"
+import { Settings, RightPanel } from "./components"
+import { getCssVariableValue, setCssVariableValue } from "@/utils"
+
+/** Layout 布局响应式 */
+useResize()
+
+const { setWatermark, clearWatermark } = useWatermark()
+const { isMobile } = useDevice()
+const { isLeft, isTop, isLeftTop } = useLayoutMode()
+const settingsStore = useSettingsStore()
+const { showSettings, showTagsView, showWatermark, showGreyMode, showColorWeakness } = storeToRefs(settingsStore)
+
+const classes = computed(() => {
+  return {
+    showGreyMode: showGreyMode.value,
+    showColorWeakness: showColorWeakness.value
+  }
+})
+
+//#region 隐藏标签栏时删除其高度,是为了让 Logo 组件高度和 Header 区域高度始终一致
+const cssVariableName = "--v3-tagsview-height"
+const v3TagsviewHeight = getCssVariableValue(cssVariableName)
+watchEffect(() => {
+  showTagsView.value
+    ? setCssVariableValue(cssVariableName, v3TagsviewHeight)
+    : setCssVariableValue(cssVariableName, "0px")
+})
+//#endregion
+
+/** 开启或关闭系统水印 */
+// watchEffect(() => {
+//   showWatermark.value ? setWatermark(import.meta.env.VITE_APP_TITLE) : clearWatermark()
+// })
+</script>
+
+<template>
+  <div :class="classes">
+    <!-- 左侧模式 -->
+    <LeftMode v-if="isLeft || isMobile" />
+    <!-- 顶部模式 -->
+    <TopMode v-else-if="isTop" />
+    <!-- 混合模式 -->
+    <LeftTopMode v-else-if="isLeftTop" />
+    <!-- 右侧设置面板 -->
+    <RightPanel v-if="showSettings">
+      <Settings />
+    </RightPanel>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.showGreyMode {
+  filter: grayscale(1);
+}
+
+.showColorWeakness {
+  filter: invert(0.8);
+}
+</style>

+ 32 - 0
src/main.ts

@@ -0,0 +1,32 @@
+// core
+import { createApp } from "vue"
+import App from "@/App.vue"
+import store from "@/store"
+import router from "@/router"
+import "@/router/permission"
+// load
+import { loadSvg } from "@/icons"
+import { loadPlugins } from "@/plugins"
+import { loadDirectives } from "@/directives"
+// css
+// import "uno.css"
+import "normalize.css"
+import "element-plus/dist/index.css"
+import "element-plus/theme-chalk/dark/css-vars.css"
+import "vxe-table/lib/style.css"
+import "vxe-table-plugin-element/dist/style.css"
+import "@/styles/index.scss"
+
+const app = createApp(App)
+
+/** 加载插件 */
+loadPlugins(app)
+/** 加载全局 SVG */
+loadSvg(app)
+/** 加载自定义指令 */
+loadDirectives(app)
+
+app.use(store).use(router)
+router.isReady().then(() => {
+  app.mount("#app")
+})

+ 9 - 0
src/plugins/element-plus-icon/index.ts

@@ -0,0 +1,9 @@
+import { type App } from "vue"
+import * as ElementPlusIconsVue from "@element-plus/icons-vue"
+
+export function loadElementPlusIcon(app: App) {
+  /** 注册所有 Element Plus Icon */
+  for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+    app.component(key, component)
+  }
+}

+ 7 - 0
src/plugins/element-plus/index.ts

@@ -0,0 +1,7 @@
+import { type App } from "vue"
+import ElementPlus from "element-plus"
+
+export function loadElementPlus(app: App) {
+  /** Element Plus 组件完整引入 */
+  app.use(ElementPlus)
+}

+ 10 - 0
src/plugins/index.ts

@@ -0,0 +1,10 @@
+import { type App } from "vue"
+import { loadElementPlus } from "./element-plus"
+import { loadElementPlusIcon } from "./element-plus-icon"
+import { loadVxeTable } from "./vxe-table"
+
+export function loadPlugins(app: App) {
+  loadElementPlus(app)
+  loadElementPlusIcon(app)
+  loadVxeTable(app)
+}

+ 66 - 0
src/plugins/vxe-table/index.ts

@@ -0,0 +1,66 @@
+import { type App } from "vue"
+// https://vxetable.cn/#/table/start/install
+import VXETable from "vxe-table"
+// https://github.com/x-extends/vxe-table-plugin-element
+import VXETablePluginElement from "vxe-table-plugin-element"
+
+VXETable.use(VXETablePluginElement)
+
+/** 全局默认参数 */
+VXETable.setConfig({
+  /** 全局尺寸 */
+  size: "medium",
+  /** 全局 zIndex 起始值,如果项目的的 z-index 样式值过大时就需要跟随设置更大,避免被遮挡 */
+  zIndex: 9999,
+  /** 版本号,对于某些带数据缓存的功能有用到,上升版本号可以用于重置数据 */
+  version: 0,
+  /** 全局 loading 提示内容,如果为 null 则不显示文本 */
+  loadingText: null,
+  table: {
+    showHeader: true,
+    showOverflow: "tooltip",
+    showHeaderOverflow: "tooltip",
+    autoResize: true,
+    // stripe: false,
+    border: "inner",
+    // round: false,
+    emptyText: "暂无数据",
+    rowConfig: {
+      isHover: true,
+      isCurrent: true,
+      // 行数据的唯一主键字段名
+      keyField: "_VXE_ID"
+    },
+    columnConfig: {
+      resizable: false
+    },
+    align: "center",
+    headerAlign: "center"
+  },
+  pager: {
+    // size: "medium",
+    /** 配套的样式 */
+    perfect: false,
+    pageSize: 10,
+    pagerCount: 7,
+    pageSizes: [10, 20, 50],
+    layouts: ["Total", "PrevJump", "PrevPage", "Number", "NextPage", "NextJump", "Sizes", "FullJump"]
+  },
+  modal: {
+    minWidth: 500,
+    minHeight: 400,
+    lockView: true,
+    mask: true,
+    // duration: 3000,
+    // marginSize: 20,
+    dblclickZoom: false,
+    showTitleOverflow: true,
+    transfer: true,
+    draggable: false
+  }
+})
+
+export function loadVxeTable(app: App) {
+  /** Vxe Table 组件完整引入 */
+  app.use(VXETable)
+}

+ 69 - 0
src/router/helper.ts

@@ -0,0 +1,69 @@
+import {
+  type Router,
+  type RouteRecordNormalized,
+  type RouteRecordRaw,
+  createRouter,
+  createWebHashHistory,
+  createWebHistory
+} from "vue-router"
+import { cloneDeep, omit } from "lodash-es"
+
+/** 路由模式 */
+export const history =
+  import.meta.env.VITE_ROUTER_HISTORY === "hash"
+    ? createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH)
+    : createWebHistory(import.meta.env.VITE_PUBLIC_PATH)
+
+/** 路由降级(把三级及其以上的路由转化为二级路由) */
+export const flatMultiLevelRoutes = (routes: RouteRecordRaw[]) => {
+  const routesMirror = cloneDeep(routes)
+  routesMirror.forEach((route) => {
+    // 如果路由是三级及其以上路由,对其进行降级处理
+    isMultipleRoute(route) && promoteRouteLevel(route)
+  })
+  return routesMirror
+}
+
+/** 判断路由层级是否大于 2 */
+const isMultipleRoute = (route: RouteRecordRaw) => {
+  const children = route.children
+  if (children?.length) {
+    // 只要有一个子路由的 children 长度大于 0,就说明是三级及其以上路由
+    return children.some((child) => child.children?.length)
+  }
+  return false
+}
+
+/** 生成二级路由 */
+const promoteRouteLevel = (route: RouteRecordRaw) => {
+  // 创建 router 实例是为了获取到当前传入的 route 的所有路由信息
+  let router: Router | null = createRouter({
+    history,
+    routes: [route]
+  })
+  const routes = router.getRoutes()
+  // 在 addToChildren 函数中使用上面获取到的路由信息来更新 route 的 children
+  addToChildren(routes, route.children || [], route)
+  router = null
+  // 转为二级路由后,去除所有子路由中的 children
+  route.children = route.children?.map((item) => omit(item, "children") as RouteRecordRaw)
+}
+
+/** 将给定的子路由添加到指定的路由模块中 */
+const addToChildren = (routes: RouteRecordNormalized[], children: RouteRecordRaw[], routeModule: RouteRecordRaw) => {
+  children.forEach((child) => {
+    const route = routes.find((item) => item.name === child.name)
+    if (route) {
+      // 初始化 routeModule 的 children
+      routeModule.children = routeModule.children || []
+      // 如果 routeModule 的 children 属性中不包含该路由,则将其添加进去
+      if (!routeModule.children.includes(route)) {
+        routeModule.children.push(route)
+      }
+      // 如果该子路由还有自己的子路由,则递归调用此函数将它们也添加进去
+      if (child.children?.length) {
+        addToChildren(routes, child.children, routeModule)
+      }
+    }
+  })
+}

+ 193 - 0
src/router/index.ts

@@ -0,0 +1,193 @@
+import { type RouteRecordRaw, createRouter } from "vue-router"
+import { history, flatMultiLevelRoutes } from "./helper"
+import routeSettings from "@/config/route"
+
+const Layouts = () => import("@/layouts/index.vue")
+
+/**
+ * 常驻路由
+ * 除了 redirect/403/404/login 等隐藏页面,其他页面建议设置 Name 属性
+ */
+export const constantRoutes: RouteRecordRaw[] = [
+  {
+    path: "/redirect",
+    component: Layouts,
+    meta: {
+      hidden: true
+    },
+    children: [
+      {
+        path: ":path(.*)",
+        component: () => import("@/views/redirect/index.vue")
+      }
+    ]
+  },
+  {
+    path: "/403",
+    component: () => import("@/views/error-page/403.vue"),
+    meta: {
+      hidden: true
+    }
+  },
+  {
+    path: "/404",
+    component: () => import("@/views/error-page/404.vue"),
+    meta: {
+      hidden: true
+    },
+    alias: "/:pathMatch(.*)*"
+  },
+  {
+    path: "/login",
+    component: () => import("@/views/login/index.vue"),
+    meta: {
+      hidden: true
+    }
+  },
+  {
+    path: "/",
+    component: Layouts,
+    redirect: "/dashboard",
+    children: [
+      {
+        path: "dashboard",
+        component: () => import("@/views/dashboard/index.vue"),
+        name: "Dashboard",
+        meta: {
+          title: "首页",
+          elIcon: "HomeFilled",
+          affix: true
+        }
+      }
+    ]
+  },
+  {
+    path: "/table",
+    component: Layouts,
+    redirect: "/table/element-plus",
+    name: "Table",
+    meta: {
+      title: "表格",
+      elIcon: "Grid"
+    },
+    children: [
+      {
+        path: "element-plus",
+        component: () => import("@/views/table/element-plus/index.vue"),
+        name: "ElementPlus",
+        meta: {
+          title: "Element Plus",
+          keepAlive: true
+        }
+      },
+      {
+        path: "vxe-table",
+        component: () => import("@/views/table/vxe-table/index.vue"),
+        name: "VxeTable",
+        meta: {
+          title: "Vxe Table",
+          keepAlive: true
+        }
+      }
+    ]
+  },
+  {
+    path: "/hook-demo",
+    component: Layouts,
+    redirect: "/hook-demo/use-fetch-select",
+    name: "HookDemo",
+    meta: {
+      title: "Hook",
+      elIcon: "Tools",
+      alwaysShow: true
+    },
+    children: [
+      {
+        path: "use-fetch-select",
+        component: () => import("@/views/hook-demo/use-fetch-select.vue"),
+        name: "UseFetchSelect",
+        meta: {
+          title: "useFetchSelect"
+        }
+      },
+      {
+        path: "use-fullscreen-loading",
+        component: () => import("@/views/hook-demo/use-fullscreen-loading.vue"),
+        name: "UseFullscreenLoading",
+        meta: {
+          title: "useFullscreenLoading"
+        }
+      },
+      {
+        path: "use-watermark",
+        component: () => import("@/views/hook-demo/use-watermark.vue"),
+        name: "UseWatermark",
+        meta: {
+          title: "useWatermark"
+        }
+      }
+    ]
+  }
+]
+
+/**
+ * 动态路由
+ * 用来放置有权限 (Roles 属性) 的路由
+ * 必须带有 Name 属性
+ */
+export const dynamicRoutes: RouteRecordRaw[] = [
+  {
+    path: "/permission",
+    component: Layouts,
+    redirect: "/permission/page",
+    name: "Permission",
+    meta: {
+      title: "权限",
+      svgIcon: "lock",
+      roles: ["admin", "editor"], // 可以在根路由中设置角色
+      alwaysShow: true // 将始终显示根菜单
+    },
+    children: [
+      {
+        path: "page",
+        component: () => import("@/views/permission/page.vue"),
+        name: "PagePermission",
+        meta: {
+          title: "页面级",
+          roles: ["admin"] // 或者在子导航中设置角色
+        }
+      },
+      {
+        path: "directive",
+        component: () => import("@/views/permission/directive.vue"),
+        name: "DirectivePermission",
+        meta: {
+          title: "按钮级" // 如果未设置角色,则表示:该页面不需要权限,但会继承根路由的角色
+        }
+      }
+    ]
+  }
+]
+
+const router = createRouter({
+  history,
+  routes: routeSettings.thirdLevelRouteCache ? flatMultiLevelRoutes(constantRoutes) : constantRoutes
+})
+
+/** 重置路由 */
+export function resetRouter() {
+  // 注意:所有动态路由路由必须带有 Name 属性,否则可能会不能完全重置干净
+  try {
+    router.getRoutes().forEach((route) => {
+      const { name, meta }: any = route
+      if (name && meta.roles?.length) {
+        router.hasRoute(name) && router.removeRoute(name)
+      }
+    })
+  } catch {
+    // 强制刷新浏览器也行,只是交互体验不是很好
+    window.location.reload()
+  }
+}
+
+export default router

+ 62 - 0
src/router/permission.ts

@@ -0,0 +1,62 @@
+import router from "@/router"
+import { useUserStoreHook } from "@/store/modules/user"
+import { usePermissionStoreHook } from "@/store/modules/permission"
+import { ElMessage } from "element-plus"
+import { setRouteChange } from "@/hooks/useRouteListener"
+import { useTitle } from "@/hooks/useTitle"
+import { getToken } from "@/utils/cache/cookies"
+import routeSettings from "@/config/route"
+import isWhiteList from "@/config/white-list"
+import NProgress from "nprogress"
+import "nprogress/nprogress.css"
+
+const { setTitle } = useTitle()
+NProgress.configure({ showSpinner: false })
+
+router.beforeEach(async (to, _from, next) => {
+  NProgress.start()
+  const userStore = useUserStoreHook()
+  const permissionStore = usePermissionStoreHook()
+  const token = getToken()
+
+  // 如果没有登陆
+  if (!token) {
+    // 如果在免登录的白名单中,则直接进入
+    if (isWhiteList(to)) return next()
+    // 其他没有访问权限的页面将被重定向到登录页面
+    return next("/login")
+  }
+
+  // 如果已经登录,并准备进入 Login 页面,则重定向到主页
+  if (to.path === "/login") {
+    return next({ path: "/" })
+  }
+
+  // 如果用户已经获得其权限角色
+  if (userStore.roles.length !== 0) return next()
+
+  // 否则要重新获取权限角色
+  try {
+    await userStore.getInfo()
+    // 注意:角色必须是一个数组! 例如: ["admin"] 或 ["developer", "editor"]
+    const roles = userStore.roles
+    // 生成可访问的 Routes
+    routeSettings.dynamic ? permissionStore.setRoutes(roles) : permissionStore.setAllRoutes()
+    // 将 "有访问权限的动态路由" 添加到 Router 中
+    permissionStore.addRoutes.forEach((route) => router.addRoute(route))
+    // 确保添加路由已完成
+    // 设置 replace: true, 因此导航将不会留下历史记录
+    next({ ...to, replace: true })
+  } catch (err: any) {
+    // 过程中发生任何错误,都直接重置 Token,并重定向到登录页面
+    userStore.resetToken()
+    ElMessage.error(err.message || "路由守卫过程发生错误")
+    next("/login")
+  }
+})
+
+router.afterEach((to) => {
+  setRouteChange(to)
+  setTitle(to.meta.title)
+  NProgress.done()
+})

+ 5 - 0
src/store/index.ts

@@ -0,0 +1,5 @@
+import { createPinia } from "pinia"
+
+const store = createPinia()
+
+export default store

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff