luoy 1 nedēļu atpakaļ
revīzija
a6a8c5ba84
100 mainītis faili ar 10243 papildinājumiem un 0 dzēšanām
  1. 26 0
      .gitignore
  2. 24 0
      LICENSE
  3. 4 0
      README.md
  4. 21 0
      build.sh
  5. 8 0
      jsconfig.json
  6. 3162 0
      package-lock.json
  7. 42 0
      package.json
  8. 6 0
      postcss.config.cjs
  9. 3 0
      run.sh
  10. 153 0
      src/assets/base.css
  11. 98 0
      src/assets/grids.css
  12. 1078 0
      src/assets/table.css
  13. 311 0
      src/assets/tailwind.css
  14. 206 0
      src/components/anti-ddos/ADInstanceObjectsBox.vue
  15. 92 0
      src/components/common/BandwidthSizeCapacityBox.vue
  16. 22 0
      src/components/common/BandwidthSizeCapacityView.vue
  17. 26 0
      src/components/common/BaseComponents.vue
  18. 28 0
      src/components/common/BitsVar.vue
  19. 26 0
      src/components/common/BytesVar.vue
  20. 40 0
      src/components/common/CSRFToken.vue
  21. 12 0
      src/components/common/CardComment.vue
  22. 13 0
      src/components/common/CardDivider.vue
  23. 149 0
      src/components/common/CardRow.vue
  24. 9 0
      src/components/common/CardRowGroup.vue
  25. 86 0
      src/components/common/CardView.vue
  26. 12 0
      src/components/common/Cards.vue
  27. 97 0
      src/components/common/ChartColumnsGrid.vue
  28. 20 0
      src/components/common/CodeLabel.vue
  29. 11 0
      src/components/common/CodeLabelPlain.vue
  30. 62 0
      src/components/common/ColumnsGrid.vue
  31. 340 0
      src/components/common/ComboBox.vue
  32. 31 0
      src/components/common/CopyToClipboard.vue
  33. 69 0
      src/components/common/CountriesSelector.vue
  34. 222 0
      src/components/common/DatetimeInput.vue
  35. 11 0
      src/components/common/DefinitionTable.vue
  36. 10 0
      src/components/common/Dot.vue
  37. 56 0
      src/components/common/DownloadLink.vue
  38. 48 0
      src/components/common/FileTextarea.vue
  39. 20 0
      src/components/common/FirstMenu.vue
  40. 23 0
      src/components/common/GreyLabel.vue
  41. 370 0
      src/components/common/HealthCheckConfigBox.vue
  42. 48 0
      src/components/common/JSPage.vue
  43. 67 0
      src/components/common/Keyword.vue
  44. 14 0
      src/components/common/LabelOn.vue
  45. 26 0
      src/components/common/LinkIcon.vue
  46. 15 0
      src/components/common/LinkPopup.vue
  47. 35 0
      src/components/common/LinkRed.vue
  48. 98 0
      src/components/common/ListTable.vue
  49. 17 0
      src/components/common/LoadingMessage.vue
  50. 10 0
      src/components/common/MaskWarning.vue
  51. 61 0
      src/components/common/MenuItem.vue
  52. 63 0
      src/components/common/MoreItems.vue
  53. 26 0
      src/components/common/MoreOptionsAngle.vue
  54. 31 0
      src/components/common/MoreOptionsIndicator.vue
  55. 32 0
      src/components/common/MoreOptionsTbody.vue
  56. 57 0
      src/components/common/NavButton.vue
  57. 14 0
      src/components/common/NavButtons.vue
  58. 203 0
      src/components/common/NavTab.vue
  59. 167 0
      src/components/common/Navbar.vue
  60. 172 0
      src/components/common/NetworkAddressesBox.vue
  61. 17 0
      src/components/common/NetworkAddressesView.vue
  62. 24 0
      src/components/common/NodeLogRow.vue
  63. 16 0
      src/components/common/NotFoundBox.vue
  64. 20 0
      src/components/common/NotFoundInfo.vue
  65. 11 0
      src/components/common/OptionalLabel.vue
  66. 73 0
      src/components/common/PageSizeSelector.vue
  67. 69 0
      src/components/common/Pager.vue
  68. 13 0
      src/components/common/PlusButton.vue
  69. 28 0
      src/components/common/PopTipComponent.vue
  70. 23 0
      src/components/common/PopupIcon.vue
  71. 13 0
      src/components/common/PrimaryButton.vue
  72. 38 0
      src/components/common/PriorCheckbox.vue
  73. 13 0
      src/components/common/ProWarningLabel.vue
  74. 68 0
      src/components/common/ProvincesSelector.vue
  75. 10 0
      src/components/common/RaquoItem.vue
  76. 47 0
      src/components/common/RequestVariablesDescriber.vue
  77. 57 0
      src/components/common/SearchBox.vue
  78. 24 0
      src/components/common/SecondMenu.vue
  79. 13 0
      src/components/common/SecondaryButton.vue
  80. 89 0
      src/components/common/SizeCapacityBox.vue
  81. 22 0
      src/components/common/SizeCapacityView.vue
  82. 65 0
      src/components/common/SortArrow.vue
  83. 130 0
      src/components/common/SourceCodeBox.vue
  84. 9 0
      src/components/common/SourceCodeHighlighter.vue
  85. 50 0
      src/components/common/ThemeSpans.vue
  86. 110 0
      src/components/common/TimeDurationBox.vue
  87. 29 0
      src/components/common/TimeDurationText.vue
  88. 13 0
      src/components/common/TinyBasicLabel.vue
  89. 20 0
      src/components/common/TipIcon.vue
  90. 49 0
      src/components/common/TipMessageBox.vue
  91. 31 0
      src/components/common/URLPatternLabels.vue
  92. 193 0
      src/components/common/URLPatternsBox.vue
  93. 165 0
      src/components/common/ValuesBox.vue
  94. 42 0
      src/components/common/ViewLink.vue
  95. 40 0
      src/components/common/WarningMessage.vue
  96. 11 0
      src/components/form/BinaryCheckbox.vue
  97. 62 0
      src/components/form/Editor.vue
  98. 137 0
      src/components/form/GetForm.vue
  99. 183 0
      src/components/form/PostForm.vue
  100. 13 0
      src/components/form/SubmitButton.vue

+ 26 - 0
.gitignore

@@ -0,0 +1,26 @@
+# Logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# FlexCDN
+*_plus.*

+ 24 - 0
LICENSE

@@ -0,0 +1,24 @@
+BSD 2-Clause License
+
+Copyright (c) 2024, tjbrains
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 4 - 0
README.md

@@ -0,0 +1,4 @@
+# FlexCDN用户系统前端源码
+
+参考文档:
+* [用户系统界面开发](https://flexcdn.cn/docs/developer/user-front)

+ 21 - 0
build.sh

@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+
+MAIN_PROJECT_DIR="../CloudUser/web/views/@default"
+npm install
+npm run build
+if [ -d "${MAIN_PROJECT_DIR}" ]; then
+	rm -rf "${MAIN_PROJECT_DIR:?}"/*
+else
+	echo "'${MAIN_PROJECT_DIR}' not exists"
+fi
+
+npm run build
+cp  -r -f "./dist/views/"* "${MAIN_PROJECT_DIR}/"
+
+if [ -d "${MAIN_PROJECT_DIR}" ]; then
+	echo "*" > "${MAIN_PROJECT_DIR}/.gitignore"
+fi
+
+rm -rf "./dist/views"
+
+echo "[done]"

+ 8 - 0
jsconfig.json

@@ -0,0 +1,8 @@
+{
+  "compilerOptions": {
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+  "exclude": ["node_modules", "dist"]
+}

+ 3162 - 0
package-lock.json

@@ -0,0 +1,3162 @@
+{
+  "name": "clouduserfront",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "clouduserfront",
+      "version": "1.0.0",
+      "dependencies": {
+        "@codemirror/commands": "^6.6.0",
+        "@codemirror/lang-javascript": "^6.2.2",
+        "@codemirror/lang-json": "^6.0.1",
+        "@codemirror/lang-yaml": "^6.1.1",
+        "@codemirror/state": "^6.4.1",
+        "@codemirror/view": "^6.28.4",
+        "@headlessui/vue": "^1.7.22",
+        "@heroicons/vue": "^2.1.4",
+        "@primevue/themes": "^4.0.0",
+        "@tailwindcss/container-queries": "^0.1.1",
+        "axios": "^1.7.2",
+        "codemirror": "^6.0.1",
+        "copy-to-clipboard": "^3.3.3",
+        "echarts": "^5.5.1",
+        "md5": "^2.3.0",
+        "primeicons": "^7.0.0",
+        "primevue": "^4.0.5",
+        "quill": "^2.0.2",
+        "sortablejs": "^1.15.2",
+        "tailwindcss-primeui": "^0.3.1",
+        "vue": "^3.4.35",
+        "vue-router": "^4.4.0"
+      },
+      "devDependencies": {
+        "@vitejs/plugin-vue": "^5.0.4",
+        "autoprefixer": "^10.4.19",
+        "postcss": "^8.4.38",
+        "tailwindcss": "^3.4.4",
+        "vite": "^5.2.0"
+      }
+    },
+    "node_modules/@alloc/quick-lru": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.24.8",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
+      "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.24.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
+      "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.25.3",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
+      "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
+      "dependencies": {
+        "@babel/types": "^7.25.2"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.25.2",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
+      "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.24.8",
+        "@babel/helper-validator-identifier": "^7.24.7",
+        "to-fast-properties": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@codemirror/autocomplete": {
+      "version": "6.17.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.17.0.tgz",
+      "integrity": "sha512-fdfj6e6ZxZf8yrkMHUSJJir7OJkHkZKaOZGzLWIYp2PZ3jd+d+UjG8zVPqJF6d3bKxkhvXTPan/UZ1t7Bqm0gA==",
+      "dependencies": {
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.17.0",
+        "@lezer/common": "^1.0.0"
+      },
+      "peerDependencies": {
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "@lezer/common": "^1.0.0"
+      }
+    },
+    "node_modules/@codemirror/commands": {
+      "version": "6.6.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz",
+      "integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==",
+      "dependencies": {
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/state": "^6.4.0",
+        "@codemirror/view": "^6.27.0",
+        "@lezer/common": "^1.1.0"
+      }
+    },
+    "node_modules/@codemirror/lang-javascript": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz",
+      "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==",
+      "dependencies": {
+        "@codemirror/autocomplete": "^6.0.0",
+        "@codemirror/language": "^6.6.0",
+        "@codemirror/lint": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.17.0",
+        "@lezer/common": "^1.0.0",
+        "@lezer/javascript": "^1.0.0"
+      }
+    },
+    "node_modules/@codemirror/lang-json": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz",
+      "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==",
+      "dependencies": {
+        "@codemirror/language": "^6.0.0",
+        "@lezer/json": "^1.0.0"
+      }
+    },
+    "node_modules/@codemirror/lang-yaml": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.1.tgz",
+      "integrity": "sha512-HV2NzbK9bbVnjWxwObuZh5FuPCowx51mEfoFT9y3y+M37fA3+pbxx4I7uePuygFzDsAmCTwQSc/kXh/flab4uw==",
+      "dependencies": {
+        "@codemirror/autocomplete": "^6.0.0",
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@lezer/common": "^1.2.0",
+        "@lezer/highlight": "^1.2.0",
+        "@lezer/yaml": "^1.0.0"
+      }
+    },
+    "node_modules/@codemirror/language": {
+      "version": "6.10.2",
+      "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz",
+      "integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==",
+      "dependencies": {
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.23.0",
+        "@lezer/common": "^1.1.0",
+        "@lezer/highlight": "^1.0.0",
+        "@lezer/lr": "^1.0.0",
+        "style-mod": "^4.0.0"
+      }
+    },
+    "node_modules/@codemirror/lint": {
+      "version": "6.8.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.1.tgz",
+      "integrity": "sha512-IZ0Y7S4/bpaunwggW2jYqwLuHj0QtESf5xcROewY6+lDNwZ/NzvR4t+vpYgg9m7V8UXLPYqG+lu3DF470E5Oxg==",
+      "dependencies": {
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "crelt": "^1.0.5"
+      }
+    },
+    "node_modules/@codemirror/search": {
+      "version": "6.5.6",
+      "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz",
+      "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==",
+      "dependencies": {
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0",
+        "crelt": "^1.0.5"
+      }
+    },
+    "node_modules/@codemirror/state": {
+      "version": "6.4.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz",
+      "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A=="
+    },
+    "node_modules/@codemirror/view": {
+      "version": "6.29.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.29.1.tgz",
+      "integrity": "sha512-7r+DlO/QFwPqKp73uq5mmrS4TuLPUVotbNOKYzN3OLP5ScrOVXcm4g13/48b6ZXGhdmzMinzFYqH0vo+qihIkQ==",
+      "dependencies": {
+        "@codemirror/state": "^6.4.0",
+        "style-mod": "^4.1.0",
+        "w3c-keyname": "^2.2.4"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@headlessui/vue": {
+      "version": "1.7.22",
+      "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.22.tgz",
+      "integrity": "sha512-Hoffjoolq1rY+LOfJ+B/OvkhuBXXBFgd8oBlN+l1TApma2dB0En0ucFZrwQtb33SmcCqd32EQd0y07oziXWNYg==",
+      "dependencies": {
+        "@tanstack/vue-virtual": "^3.0.0-beta.60"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
+    "node_modules/@heroicons/vue": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.1.5.tgz",
+      "integrity": "sha512-IpqR72sFqFs55kyKfFS7tN+Ww6odFNeH/7UxycIOrlVYfj4WUGAdzQtLBnJspucSeqWFQsKM0g0YrgU655BEfA==",
+      "peerDependencies": {
+        "vue": ">= 3"
+      }
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
+      "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
+      "dependencies": {
+        "@jridgewell/set-array": "^1.2.1",
+        "@jridgewell/sourcemap-codec": "^1.4.10",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/set-array": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+      "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.25",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+      "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@lezer/common": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz",
+      "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ=="
+    },
+    "node_modules/@lezer/highlight": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz",
+      "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==",
+      "dependencies": {
+        "@lezer/common": "^1.0.0"
+      }
+    },
+    "node_modules/@lezer/javascript": {
+      "version": "1.4.17",
+      "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.17.tgz",
+      "integrity": "sha512-bYW4ctpyGK+JMumDApeUzuIezX01H76R1foD6LcRX224FWfyYit/HYxiPGDjXXe/wQWASjCvVGoukTH68+0HIA==",
+      "dependencies": {
+        "@lezer/common": "^1.2.0",
+        "@lezer/highlight": "^1.1.3",
+        "@lezer/lr": "^1.3.0"
+      }
+    },
+    "node_modules/@lezer/json": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz",
+      "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==",
+      "dependencies": {
+        "@lezer/common": "^1.2.0",
+        "@lezer/highlight": "^1.0.0",
+        "@lezer/lr": "^1.0.0"
+      }
+    },
+    "node_modules/@lezer/lr": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
+      "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
+      "dependencies": {
+        "@lezer/common": "^1.0.0"
+      }
+    },
+    "node_modules/@lezer/yaml": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.3.tgz",
+      "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==",
+      "dependencies": {
+        "@lezer/common": "^1.2.0",
+        "@lezer/highlight": "^1.0.0",
+        "@lezer/lr": "^1.4.0"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@primeuix/styled": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.0.5.tgz",
+      "integrity": "sha512-pVoGn/uPkVm/DyF3TR3EmH/pL/dP4nR42FcYbVduFq9VfO3KVeOEqvcCULHXos66RZO9MCbCFUoLy6ctf9GUGQ==",
+      "dependencies": {
+        "@primeuix/utils": "^0.0.5"
+      },
+      "engines": {
+        "node": ">=12.11.0"
+      }
+    },
+    "node_modules/@primeuix/utils": {
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.0.5.tgz",
+      "integrity": "sha512-ntUiUgtRtkF8KuaxHffzhYxQxoXk6LAPHm7CVlFjdqS8Rx8xRkLkZVyo84E+pO2hcNFkOGVP/GxHhQ2s94O8zA==",
+      "engines": {
+        "node": ">=12.11.0"
+      }
+    },
+    "node_modules/@primevue/core": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.0.5.tgz",
+      "integrity": "sha512-DUCslDA93eUOVW0A1I3yoZgRLI4zmI2++loZQXbUF5jaXCwKiAza14+iyUU+cWH27VSq+jQnCEP9QJtPZiJJ0w==",
+      "dependencies": {
+        "@primeuix/styled": "^0.0.5",
+        "@primeuix/utils": "^0.0.5"
+      },
+      "engines": {
+        "node": ">=12.11.0"
+      },
+      "peerDependencies": {
+        "vue": "^3.0.0"
+      }
+    },
+    "node_modules/@primevue/icons": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.0.5.tgz",
+      "integrity": "sha512-ZxR9W1wlAE2fTtUhrHyeMx5t0jNyAgxDcHPm0cNXpX8q1XF95rSM/qb48QKXIBDBrJ/xs57BcyCNADP/VDPY4g==",
+      "dependencies": {
+        "@primeuix/utils": "^0.0.5",
+        "@primevue/core": "4.0.5"
+      },
+      "engines": {
+        "node": ">=12.11.0"
+      }
+    },
+    "node_modules/@primevue/themes": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.0.2.tgz",
+      "integrity": "sha512-DGrdFOWRUb8/qDX+Hjkg0SkU2hvo6EmEE5/fA/+NSlLAemj5fcR8r3wo9jhFMIU0QVNl17jZycuL5taU0H72BA==",
+      "dependencies": {
+        "@primeuix/styled": "^0.0.5"
+      },
+      "engines": {
+        "node": ">=12.11.0"
+      }
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.1.tgz",
+      "integrity": "sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.1.tgz",
+      "integrity": "sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.1.tgz",
+      "integrity": "sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.1.tgz",
+      "integrity": "sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.1.tgz",
+      "integrity": "sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.1.tgz",
+      "integrity": "sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.1.tgz",
+      "integrity": "sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.1.tgz",
+      "integrity": "sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.1.tgz",
+      "integrity": "sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.1.tgz",
+      "integrity": "sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.1.tgz",
+      "integrity": "sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.1.tgz",
+      "integrity": "sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.1.tgz",
+      "integrity": "sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.1.tgz",
+      "integrity": "sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.1.tgz",
+      "integrity": "sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz",
+      "integrity": "sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@tailwindcss/container-queries": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz",
+      "integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==",
+      "peerDependencies": {
+        "tailwindcss": ">=3.2.0"
+      }
+    },
+    "node_modules/@tanstack/virtual-core": {
+      "version": "3.8.4",
+      "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.8.4.tgz",
+      "integrity": "sha512-iO5Ujgw3O1yIxWDe9FgUPNkGjyT657b1WNX52u+Wv1DyBFEpdCdGkuVaky0M3hHFqNWjAmHWTn4wgj9rTr7ZQg==",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      }
+    },
+    "node_modules/@tanstack/vue-virtual": {
+      "version": "3.8.4",
+      "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.8.4.tgz",
+      "integrity": "sha512-4Pq8odunHQPsTg2iE2yzWdzYed/8LySy2knxqJYkaNOQRXbqJ7O/Owpoon8ZM9L+jLL1faM5TVHV0eJxm68q8A==",
+      "dependencies": {
+        "@tanstack/virtual-core": "3.8.4"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/tannerlinsley"
+      },
+      "peerDependencies": {
+        "vue": "^2.7.0 || ^3.0.0"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
+      "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+      "dev": true
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.1.tgz",
+      "integrity": "sha512-sDckXxlHpMsjRQbAH9WanangrfrblsOd3pNifePs+FOHjJg1jfWq5L/P0PsBRndEt3nmdUnmvieP8ULDeX5AvA==",
+      "dev": true,
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.4.35",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.35.tgz",
+      "integrity": "sha512-gKp0zGoLnMYtw4uS/SJRRO7rsVggLjvot3mcctlMXunYNsX+aRJDqqw/lV5/gHK91nvaAAlWFgdVl020AW1Prg==",
+      "dependencies": {
+        "@babel/parser": "^7.24.7",
+        "@vue/shared": "3.4.35",
+        "entities": "^4.5.0",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.0"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.4.35",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.35.tgz",
+      "integrity": "sha512-pWIZRL76/oE/VMhdv/ovZfmuooEni6JPG1BFe7oLk5DZRo/ImydXijoZl/4kh2406boRQ7lxTYzbZEEXEhj9NQ==",
+      "dependencies": {
+        "@vue/compiler-core": "3.4.35",
+        "@vue/shared": "3.4.35"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.4.35",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.35.tgz",
+      "integrity": "sha512-xacnRS/h/FCsjsMfxBkzjoNxyxEyKyZfBch/P4vkLRvYJwe5ChXmZZrj8Dsed/752H2Q3JE8kYu9Uyha9J6PgA==",
+      "dependencies": {
+        "@babel/parser": "^7.24.7",
+        "@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.10",
+        "postcss": "^8.4.40",
+        "source-map-js": "^1.2.0"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.4.35",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.35.tgz",
+      "integrity": "sha512-7iynB+0KB1AAJKk/biENTV5cRGHRdbdaD7Mx3nWcm1W8bVD6QmnH3B4AHhQQ1qZHhqFwzEzMwiytXm3PX1e60A==",
+      "dependencies": {
+        "@vue/compiler-dom": "3.4.35",
+        "@vue/shared": "3.4.35"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.3",
+      "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz",
+      "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw=="
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.4.35",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.35.tgz",
+      "integrity": "sha512-Ggtz7ZZHakriKioveJtPlStYardwQH6VCs9V13/4qjHSQb/teE30LVJNrbBVs4+aoYGtTQKJbTe4CWGxVZrvEw==",
+      "dependencies": {
+        "@vue/shared": "3.4.35"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.4.35",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.35.tgz",
+      "integrity": "sha512-D+BAjFoWwT5wtITpSxwqfWZiBClhBbR+bm0VQlWYFOadUUXFo+5wbe9ErXhLvwguPiLZdEF13QAWi2vP3ZD5tA==",
+      "dependencies": {
+        "@vue/reactivity": "3.4.35",
+        "@vue/shared": "3.4.35"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.4.35",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.35.tgz",
+      "integrity": "sha512-yGOlbos+MVhlS5NWBF2HDNgblG8e2MY3+GigHEyR/dREAluvI5tuUUgie3/9XeqhPE4LF0i2wjlduh5thnfOqw==",
+      "dependencies": {
+        "@vue/reactivity": "3.4.35",
+        "@vue/runtime-core": "3.4.35",
+        "@vue/shared": "3.4.35",
+        "csstype": "^3.1.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.4.35",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.35.tgz",
+      "integrity": "sha512-iZ0e/u9mRE4T8tNhlo0tbA+gzVkgv8r5BX6s1kRbOZqfpq14qoIvCZ5gIgraOmYkMYrSEZgkkojFPr+Nyq/Mnw==",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.4.35",
+        "@vue/shared": "3.4.35"
+      },
+      "peerDependencies": {
+        "vue": "3.4.35"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.4.35",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.35.tgz",
+      "integrity": "sha512-hvuhBYYDe+b1G8KHxsQ0diDqDMA8D9laxWZhNAjE83VZb5UDaXl9Xnz7cGdDSyiHM90qqI/CyGMcpBpiDy6VVQ=="
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+      "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/arg": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+      "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+    },
+    "node_modules/autoprefixer": {
+      "version": "10.4.19",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
+      "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "browserslist": "^4.23.0",
+        "caniuse-lite": "^1.0.30001599",
+        "fraction.js": "^4.3.7",
+        "normalize-range": "^0.1.2",
+        "picocolors": "^1.0.0",
+        "postcss-value-parser": "^4.2.0"
+      },
+      "bin": {
+        "autoprefixer": "bin/autoprefixer"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/axios": {
+      "version": "1.7.4",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
+      "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
+      "dependencies": {
+        "follow-redirects": "^1.15.6",
+        "form-data": "^4.0.0",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.23.2",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz",
+      "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "caniuse-lite": "^1.0.30001640",
+        "electron-to-chromium": "^1.4.820",
+        "node-releases": "^2.0.14",
+        "update-browserslist-db": "^1.1.0"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/camelcase-css": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+      "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001690",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
+      "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/charenc": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
+      "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/chokidar/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/codemirror": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
+      "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
+      "dependencies": {
+        "@codemirror/autocomplete": "^6.0.0",
+        "@codemirror/commands": "^6.0.0",
+        "@codemirror/language": "^6.0.0",
+        "@codemirror/lint": "^6.0.0",
+        "@codemirror/search": "^6.0.0",
+        "@codemirror/state": "^6.0.0",
+        "@codemirror/view": "^6.0.0"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/commander": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+      "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/copy-anything": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz",
+      "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "is-what": "^3.14.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/copy-to-clipboard": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
+      "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
+      "dependencies": {
+        "toggle-selection": "^1.0.6"
+      }
+    },
+    "node_modules/crelt": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
+      "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/crypt": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
+      "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/didyoumean": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+      "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="
+    },
+    "node_modules/dlv": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+      "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="
+    },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
+    },
+    "node_modules/echarts": {
+      "version": "5.5.1",
+      "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.1.tgz",
+      "integrity": "sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==",
+      "dependencies": {
+        "tslib": "2.3.0",
+        "zrender": "5.6.0"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.3.tgz",
+      "integrity": "sha512-QNdYSS5i8D9axWp/6XIezRObRHqaav/ur9z1VzCDUCH1XIFOr9WQk5xmgunhsTpjjgDy3oLxO/WMOVZlpUQrlA==",
+      "dev": true
+    },
+    "node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
+    },
+    "node_modules/entities": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+      "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/errno": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
+      "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "prr": "~1.0.1"
+      },
+      "bin": {
+        "errno": "cli.js"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "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"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
+      "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
+      "dev": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
+    },
+    "node_modules/eventemitter3": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+      "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
+    },
+    "node_modules/fast-diff": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+      "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
+      "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fastq": {
+      "version": "1.17.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+      "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.6",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+      "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/foreground-child": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz",
+      "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==",
+      "dependencies": {
+        "cross-spawn": "^7.0.0",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fraction.js": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+      "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+      "dev": true,
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "type": "patreon",
+        "url": "https://github.com/sponsors/rawify"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "hasInstallScript": true,
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/glob": {
+      "version": "10.4.5",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+      "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/image-size": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
+      "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "bin": {
+        "image-size": "bin/image-size.js"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-buffer": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
+    },
+    "node_modules/is-core-module": {
+      "version": "2.15.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
+      "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-what": {
+      "version": "3.14.1",
+      "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz",
+      "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+    },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
+    "node_modules/jiti": {
+      "version": "1.21.6",
+      "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
+      "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
+      "bin": {
+        "jiti": "bin/jiti.js"
+      }
+    },
+    "node_modules/less": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz",
+      "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "copy-anything": "^2.0.1",
+        "parse-node-version": "^1.0.1",
+        "tslib": "^2.3.0"
+      },
+      "bin": {
+        "lessc": "bin/lessc"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "optionalDependencies": {
+        "errno": "^0.1.1",
+        "graceful-fs": "^4.1.2",
+        "image-size": "~0.5.0",
+        "make-dir": "^2.1.0",
+        "mime": "^1.4.1",
+        "needle": "^3.1.0",
+        "source-map": "~0.6.0"
+      }
+    },
+    "node_modules/lilconfig": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
+      "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
+    },
+    "node_modules/lodash-es": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
+    },
+    "node_modules/lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
+    },
+    "node_modules/lodash.isequal": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+      "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
+    },
+    "node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.11",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
+      "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0"
+      }
+    },
+    "node_modules/make-dir": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+      "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "pify": "^4.0.1",
+        "semver": "^5.6.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/md5": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
+      "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
+      "dependencies": {
+        "charenc": "0.0.2",
+        "crypt": "0.0.2",
+        "is-buffer": "~1.1.6"
+      }
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "dependencies": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.7",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/needle": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz",
+      "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "dependencies": {
+        "iconv-lite": "^0.6.3",
+        "sax": "^1.2.4"
+      },
+      "bin": {
+        "needle": "bin/needle"
+      },
+      "engines": {
+        "node": ">= 4.4.x"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.18",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
+      "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
+      "dev": true
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/normalize-range": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+      "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-hash": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+      "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
+      "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw=="
+    },
+    "node_modules/parchment": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
+      "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A=="
+    },
+    "node_modules/parse-node-version": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
+      "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+    },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
+      "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pify": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+      "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/pirates": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
+      "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.4.40",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
+      "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "nanoid": "^3.3.7",
+        "picocolors": "^1.0.1",
+        "source-map-js": "^1.2.0"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-import": {
+      "version": "15.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+      "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+      "dependencies": {
+        "postcss-value-parser": "^4.0.0",
+        "read-cache": "^1.0.0",
+        "resolve": "^1.1.7"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.0.0"
+      }
+    },
+    "node_modules/postcss-js": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+      "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+      "dependencies": {
+        "camelcase-css": "^2.0.1"
+      },
+      "engines": {
+        "node": "^12 || ^14 || >= 16"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/postcss/"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4.21"
+      }
+    },
+    "node_modules/postcss-load-config": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+      "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "lilconfig": "^3.0.0",
+        "yaml": "^2.3.4"
+      },
+      "engines": {
+        "node": ">= 14"
+      },
+      "peerDependencies": {
+        "postcss": ">=8.0.9",
+        "ts-node": ">=9.0.0"
+      },
+      "peerDependenciesMeta": {
+        "postcss": {
+          "optional": true
+        },
+        "ts-node": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss-load-config/node_modules/lilconfig": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
+      "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antonk52"
+      }
+    },
+    "node_modules/postcss-nested": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+      "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "postcss-selector-parser": "^6.1.1"
+      },
+      "engines": {
+        "node": ">=12.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.2.14"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz",
+      "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+    },
+    "node_modules/primeicons": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
+      "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="
+    },
+    "node_modules/primevue": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.0.5.tgz",
+      "integrity": "sha512-MALszGIZ5SnEQy1XeZLBFhpMXQ1OS7D1U7H+l/JAX5U46RQ1vufo7NAiWbbV5/ADjPGw4uLplqMQxujkksNY2g==",
+      "dependencies": {
+        "@primeuix/styled": "^0.0.5",
+        "@primeuix/utils": "^0.0.5",
+        "@primevue/core": "4.0.5",
+        "@primevue/icons": "4.0.5"
+      },
+      "engines": {
+        "node": ">=12.11.0"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+    },
+    "node_modules/prr": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
+      "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ]
+    },
+    "node_modules/quill": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.2.tgz",
+      "integrity": "sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==",
+      "dependencies": {
+        "eventemitter3": "^5.0.1",
+        "lodash-es": "^4.17.21",
+        "parchment": "^3.0.0",
+        "quill-delta": "^5.1.0"
+      },
+      "engines": {
+        "npm": ">=8.2.3"
+      }
+    },
+    "node_modules/quill-delta": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
+      "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
+      "dependencies": {
+        "fast-diff": "^1.3.0",
+        "lodash.clonedeep": "^4.5.0",
+        "lodash.isequal": "^4.5.0"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      }
+    },
+    "node_modules/read-cache": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+      "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+      "dependencies": {
+        "pify": "^2.3.0"
+      }
+    },
+    "node_modules/read-cache/node_modules/pify": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+      "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.8",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
+      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+      "dependencies": {
+        "is-core-module": "^2.13.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.1.tgz",
+      "integrity": "sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==",
+      "dev": true,
+      "dependencies": {
+        "@types/estree": "1.0.5"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "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.2"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/sax": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
+      "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
+      "dev": true,
+      "optional": true,
+      "peer": true
+    },
+    "node_modules/semver": {
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "bin": {
+        "semver": "bin/semver"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/sortablejs": {
+      "version": "1.15.2",
+      "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.2.tgz",
+      "integrity": "sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA=="
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true,
+      "optional": true,
+      "peer": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
+      "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/string-width-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/style-mod": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
+      "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
+    },
+    "node_modules/sucrase": {
+      "version": "3.35.0",
+      "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
+      "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.2",
+        "commander": "^4.0.0",
+        "glob": "^10.3.10",
+        "lines-and-columns": "^1.1.6",
+        "mz": "^2.7.0",
+        "pirates": "^4.0.1",
+        "ts-interface-checker": "^0.1.9"
+      },
+      "bin": {
+        "sucrase": "bin/sucrase",
+        "sucrase-node": "bin/sucrase-node"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tailwindcss": {
+      "version": "3.4.7",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
+      "integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
+      "dependencies": {
+        "@alloc/quick-lru": "^5.2.0",
+        "arg": "^5.0.2",
+        "chokidar": "^3.5.3",
+        "didyoumean": "^1.2.2",
+        "dlv": "^1.1.3",
+        "fast-glob": "^3.3.0",
+        "glob-parent": "^6.0.2",
+        "is-glob": "^4.0.3",
+        "jiti": "^1.21.0",
+        "lilconfig": "^2.1.0",
+        "micromatch": "^4.0.5",
+        "normalize-path": "^3.0.0",
+        "object-hash": "^3.0.0",
+        "picocolors": "^1.0.0",
+        "postcss": "^8.4.23",
+        "postcss-import": "^15.1.0",
+        "postcss-js": "^4.0.1",
+        "postcss-load-config": "^4.0.1",
+        "postcss-nested": "^6.0.1",
+        "postcss-selector-parser": "^6.0.11",
+        "resolve": "^1.22.2",
+        "sucrase": "^3.32.0"
+      },
+      "bin": {
+        "tailwind": "lib/cli.js",
+        "tailwindcss": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/tailwindcss-primeui": {
+      "version": "0.3.4",
+      "resolved": "https://registry.npmjs.org/tailwindcss-primeui/-/tailwindcss-primeui-0.3.4.tgz",
+      "integrity": "sha512-5+Qfoe5Kpq2Iwrd6umBUb3rQH6b7+pL4jxJUId0Su5agUM6TwCyH5Pyl9R0y3QQB3IRuTxBNmeS11B41f+30zw==",
+      "peerDependencies": {
+        "tailwindcss": ">=3.1.0"
+      }
+    },
+    "node_modules/thenify": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+      "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+      "dependencies": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "node_modules/thenify-all": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+      "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
+      "dependencies": {
+        "thenify": ">= 3.1.0 < 4"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/to-fast-properties": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+      "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/toggle-selection": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
+      "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
+    },
+    "node_modules/ts-interface-checker": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
+      "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
+    },
+    "node_modules/tslib": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+      "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
+      "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "dependencies": {
+        "escalade": "^3.1.2",
+        "picocolors": "^1.0.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+    },
+    "node_modules/vite": {
+      "version": "5.3.5",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",
+      "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
+      "dev": true,
+      "dependencies": {
+        "esbuild": "^0.21.3",
+        "postcss": "^8.4.39",
+        "rollup": "^4.13.0"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "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
+        }
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.4.35",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.35.tgz",
+      "integrity": "sha512-+fl/GLmI4GPileHftVlCdB7fUL4aziPcqTudpTGXCT8s+iZWuOCeNEB5haX6Uz2IpRrbEXOgIFbe+XciCuGbNQ==",
+      "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/shared": "3.4.35"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.0.tgz",
+      "integrity": "sha512-HB+t2p611aIZraV2aPSRNXf0Z/oLZFrlygJm+sZbdJaW6lcFqEDQwnzUBXn+DApw+/QzDU/I9TeWx9izEjTmsA==",
+      "dependencies": {
+        "@vue/devtools-api": "^6.5.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
+    "node_modules/w3c-keyname": {
+      "version": "2.2.8",
+      "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+      "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/yaml": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
+      "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
+      "bin": {
+        "yaml": "bin.mjs"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/zrender": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.0.tgz",
+      "integrity": "sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==",
+      "dependencies": {
+        "tslib": "2.3.0"
+      }
+    }
+  }
+}

+ 42 - 0
package.json

@@ -0,0 +1,42 @@
+{
+  "name": "clouduserfront",
+  "private": true,
+  "version": "1.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build -l warn",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@codemirror/commands": "^6.6.0",
+    "@codemirror/lang-javascript": "^6.2.2",
+    "@codemirror/lang-json": "^6.0.1",
+    "@codemirror/lang-yaml": "^6.1.1",
+    "@codemirror/state": "^6.4.1",
+    "@codemirror/view": "^6.28.4",
+    "@headlessui/vue": "^1.7.22",
+    "@heroicons/vue": "^2.1.4",
+    "@primevue/themes": "^4.0.0",
+    "@tailwindcss/container-queries": "^0.1.1",
+    "axios": "^1.7.2",
+    "codemirror": "^6.0.1",
+    "copy-to-clipboard": "^3.3.3",
+    "echarts": "^5.5.1",
+    "md5": "^2.3.0",
+    "primeicons": "^7.0.0",
+    "primevue": "^4.0.5",
+    "quill": "^2.0.2",
+    "sortablejs": "^1.15.2",
+    "tailwindcss-primeui": "^0.3.1",
+    "vue": "^3.4.35",
+    "vue-router": "^4.4.0"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^5.0.4",
+    "autoprefixer": "^10.4.19",
+    "postcss": "^8.4.38",
+    "tailwindcss": "^3.4.4",
+    "vite": "^5.2.0"
+  }
+}

+ 6 - 0
postcss.config.cjs

@@ -0,0 +1,6 @@
+module.exports = {
+	plugins: {
+		tailwindcss: {},
+		autoprefixer: {},
+	}
+}

+ 3 - 0
run.sh

@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+npm run dev -- --host 0.0.0.0 --port 5174

+ 153 - 0
src/assets/base.css

@@ -0,0 +1,153 @@
+:root {
+
+}
+
+html {
+    font-feature-settings: "cv02", "cv03", "cv04", "cv11";
+    font-family: Inter var, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+    font-size: 14px;
+    --p-primary-color: #1E65FF !important;
+}
+
+body {
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+    font-weight: 400;
+    margin: 0;
+    min-height: 100%;
+    overflow-x: hidden;
+    overflow-y: auto
+}
+
+a {
+    cursor: pointer;
+}
+
+a.link, a.active, a.link .pi, a.active .pi, a.link .icon, a.active .icon {
+    color: var(--p-primary-color) !important;
+    text-decoration: none;
+}
+
+a.link:hover {
+    color: var(--p-primary-700) !important;
+}
+
+a.small {
+    font-size: 0.9em;
+}
+
+h1:first-child, h2:first-child, h3:first-child, h4:first-child, h5:first-child, h6:first-child {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+form table {
+    margin-top: 0 !important;
+}
+
+.width5 {
+    width: 5rem;
+}
+
+/** table **/
+tr.gray {
+    color: var(--p-gray-300) !important;
+}
+
+.name-column {
+    min-width: 10em;
+}
+
+.value-column {
+    min-width: 8em;
+    text-align: center !important;
+}
+
+.value-column .p-datatable-column-header-content {
+    display: inline-block;
+}
+
+.normal-value-column {
+    min-width: 8em;
+}
+
+.fixed-value-column {
+    width: 8em;
+}
+
+.bool-column {
+    min-width: 6em;
+    text-align: center !important;
+}
+
+.bool-column .p-datatable-column-header-content {
+    display: inline-block;
+}
+
+.status-column {
+    min-width: 6em;
+    text-align: center;
+}
+
+.ip-column {
+    min-width: 10em;
+}
+
+.date-column {
+    min-width: 7.6em;
+}
+
+.datetime-column {
+    min-width: 12em;
+}
+
+.title-column {
+    min-width: 12em;
+}
+
+.domain-column {
+    min-width: 14em;
+}
+
+.url-column {
+    min-width: 14em;
+}
+
+.email-column {
+    min-width: 14em;
+}
+
+.user-column {
+    min-width: 10em;
+}
+
+.mobile-column {
+    min-width: 10em;
+}
+
+.sort-column {
+    width: 3em;
+}
+
+.checkbox-column {
+    width: 3em;
+}
+
+.fixed-status-column {
+    width: 6em;
+    text-align: center;
+    white-space: nowrap;
+}
+
+.description-column {
+    min-width: 14em;
+}
+
+/** list **/
+ol:not(.p-breadcrumb-list) {
+    @apply !list-decimal;
+
+    li {
+        @apply ml-6 leading-7;
+    }
+}

+ 98 - 0
src/assets/grids.css

@@ -0,0 +1,98 @@
+.grid.counter-chart {
+	margin-top: 1em !important;
+
+	.column {
+		margin-bottom: 1em;
+		padding-bottom: 1em;
+		font-size: 0.85em;
+		text-align: center;
+		position: relative;
+		border: 1px var(--p-content-border-color) solid;
+		border-right: 0;
+
+		div.value {
+            margin-top: 0.8em;
+            margin-bottom: 0.6em;
+			font-weight: normal;
+
+			span {
+				font-size: 1.5em;
+				margin-right: 0.2em;
+			}
+		}
+	}
+
+	.column.with-border {
+		border-right: 1px var(--p-content-border-color) solid;
+	}
+
+	h4, h5 {
+        color: var(--p-text-muted-color);
+		position: relative;
+
+		a {
+			position: absolute;
+			right: 1em;
+			font-size: 1.26em;
+			display: none;
+		}
+
+		font-size: 1.0em;
+		text-align: left;
+		margin-left: 1em;
+	}
+
+	.column:hover {
+		background: rgba(0, 0, 0, .03) !important;
+
+		a {
+			display: inline;
+		}
+	}
+}
+
+.grid.chart-grid {
+	margin-top: 0;
+
+	.column {
+		margin-bottom: 1em;
+		border: 1px var(--p-content-border-color) solid;
+		border-right: 0;
+
+		.p-divider {
+			margin-top: 0;
+		}
+
+		.menu {
+			line-height:3em;
+			margin-left: 1em;
+
+			a {
+				margin-right: 1em;
+			}
+
+			a.active {
+				color: var(--p-primary-color);
+			}
+		}
+
+		h4, h5 {
+			font-size: 1.0em;
+			line-height: 3em;
+			margin-left: 1em;
+
+			span {
+				font-size: 0.8em;
+				color: grey;
+			}
+		}
+
+		.chart-box {
+			margin: 0 1em 1em 1em;
+		}
+	}
+
+	.column.with-border {
+		border-right: 1px var(--p-content-border-color) solid;
+	}
+}

+ 1078 - 0
src/assets/table.css

@@ -0,0 +1,1078 @@
+/*!
+ * # Semantic UI 2.5.0 - Table
+ * http://github.com/semantic-org/semantic-ui/
+ *
+ *
+ * Released under the MIT license
+ * http://opensource.org/licenses/MIT
+ *
+ */
+
+
+/*******************************
+             Table
+*******************************/
+
+
+/* Prototype */
+.ui.table {
+    width: 100%;
+    /** background: #FFFFFF; **/
+    margin: 1em 0em;
+    border: 1px solid var(--p-content-border-color);
+    box-shadow: none;
+    border-radius: 0.28571429rem;
+    text-align: left;
+    /** color: rgba(0, 0, 0, 0.87); **/
+    border-collapse: separate;
+    border-spacing: 0px;
+}
+
+.ui.table:first-child {
+    margin-top: 0em;
+}
+
+.ui.table:last-child {
+    margin-bottom: 0em;
+}
+
+
+/*******************************
+             Parts
+*******************************/
+
+
+/* Table Content */
+.ui.table th,
+.ui.table td {
+    transition: background 0.1s ease, color 0.1s ease;
+}
+
+/* Headers */
+.ui.table thead {
+    box-shadow: none;
+}
+
+.ui.table thead th {
+    cursor: auto;
+    background: #F9FAFB;
+    text-align: inherit;
+    /**color: rgba(0, 0, 0, 0.87);**/
+    padding: 0.92857143em 0.78571429em;
+    vertical-align: inherit;
+    font-style: none;
+    font-weight: 500;
+    text-transform: none;
+    border-bottom: 1px solid rgba(34, 36, 38, 0.1);
+    border-left: none;
+}
+
+.ui.table thead tr > th:first-child {
+    border-left: none;
+}
+
+.ui.table thead tr:first-child > th:first-child {
+    border-radius: 0.28571429rem 0em 0em 0em;
+}
+
+.ui.table thead tr:first-child > th:last-child {
+    border-radius: 0em 0.28571429rem 0em 0em;
+}
+
+.ui.table thead tr:first-child > th:only-child {
+    border-radius: 0.28571429rem 0.28571429rem 0em 0em;
+}
+
+/* Footer */
+.ui.table tfoot {
+    box-shadow: none;
+}
+
+.ui.table tfoot th {
+    cursor: auto;
+    border-top: 1px solid rgba(34, 36, 38, 0.15);
+    background: #F9FAFB;
+    text-align: inherit;
+    /**color: rgba(0, 0, 0, 0.87);**/
+    padding: 0.78571429em 0.78571429em;
+    vertical-align: middle;
+    font-style: normal;
+    font-weight: normal;
+    text-transform: none;
+}
+
+.ui.table tfoot tr > th:first-child {
+    border-left: none;
+}
+
+.ui.table tfoot tr:first-child > th:first-child {
+    border-radius: 0em 0em 0em 0.28571429rem;
+}
+
+.ui.table tfoot tr:first-child > th:last-child {
+    border-radius: 0em 0em 0.28571429rem 0em;
+}
+
+.ui.table tfoot tr:first-child > th:only-child {
+    border-radius: 0em 0em 0.28571429rem 0.28571429rem;
+}
+
+/* Table Row */
+.ui.table tr td {
+    border-top: 1px solid var(--p-content-border-color);
+}
+
+.ui.table tr:first-child td {
+    border-top: none;
+}
+
+/* Repeated tbody */
+.ui.table tbody + tbody tr:first-child td {
+    border-top: 1px solid var(--p-content-border-color);
+}
+
+/* Table Cells */
+.ui.table td {
+    padding: 0.78571429em 0.78571429em;
+    text-align: inherit;
+}
+
+/* Icons */
+.ui.table > .icon {
+    vertical-align: baseline;
+}
+
+.ui.table > .icon:only-child {
+    margin: 0em;
+}
+
+/* Responsive */
+@media only screen and (max-width: 767px) {
+    .ui.table:not(.unstackable) {
+        width: 100%;
+    }
+
+    .ui.table:not(.unstackable) tbody,
+    .ui.table:not(.unstackable) tr,
+    .ui.table:not(.unstackable) tr > th,
+    .ui.table:not(.unstackable) tr > td {
+        width: auto !important;
+        display: block !important;
+    }
+
+    .ui.table:not(.unstackable) {
+        padding: 0em;
+    }
+
+    .ui.table:not(.unstackable) thead {
+        display: block;
+    }
+
+    .ui.table:not(.unstackable) tfoot {
+        display: block;
+    }
+
+    .ui.table:not(.unstackable) tr {
+        padding-top: 1em;
+        padding-bottom: 1em;
+        box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.1) inset !important;
+    }
+
+    .ui.table:not(.unstackable) tr > th,
+    .ui.table:not(.unstackable) tr > td {
+        background: none;
+        border: none !important;
+        padding: 0.25em 0.75em !important;
+        box-shadow: none !important;
+    }
+
+    .ui.table:not(.unstackable) th:first-child,
+    .ui.table:not(.unstackable) td:first-child {
+        font-weight: 500;
+    }
+
+    /* Definition Table */
+    .ui.definition.table:not(.unstackable) thead th:first-child {
+        box-shadow: none !important;
+    }
+}
+
+
+/*******************************
+            Coupling
+*******************************/
+
+
+/* UI Image */
+.ui.table th .image,
+.ui.table th .image img,
+.ui.table td .image,
+.ui.table td .image img {
+    max-width: none;
+}
+
+
+/*******************************
+             Types
+*******************************/
+
+
+/*--------------
+    Complex
+---------------*/
+
+.ui.structured.table {
+    border-collapse: collapse;
+}
+
+.ui.structured.table thead th {
+    border-left: none;
+    border-right: none;
+}
+
+.ui.structured.sortable.table thead th {
+    border-left: 1px solid var(--p-content-border-color);
+    border-right: 1px solid var(--p-content-border-color);
+}
+
+.ui.structured.basic.table th {
+    border-left: none;
+    border-right: none;
+}
+
+.ui.structured.celled.table tr th,
+.ui.structured.celled.table tr td {
+    border-left: 1px solid var(--p-content-border-color);
+    border-right: 1px solid var(--p-content-border-color);
+}
+
+/*--------------
+   Definition
+---------------*/
+
+.ui.definition.table > thead:not(.full-width) th:first-child {
+    pointer-events: none;
+    background: transparent;
+    font-weight: normal;
+    box-shadow: -1px -1px 0px 1px #FFFFFF;
+
+    @apply text-gray-600  dark:text-gray-500;
+}
+
+.ui.definition.table tfoot:not(.full-width) th:first-child {
+    pointer-events: none;
+    background: transparent;
+    font-weight: normal;
+    box-shadow: 1px 1px 0px 1px #FFFFFF;
+
+    @apply text-gray-600 dark:text-gray-500;
+}
+
+/* Remove Border */
+.ui.celled.definition.table thead:not(.full-width) th:first-child {
+    box-shadow: 0px -1px 0px 1px #FFFFFF;
+}
+
+.ui.celled.definition.table tfoot:not(.full-width) th:first-child {
+    box-shadow: 0px 1px 0px 1px #FFFFFF;
+}
+
+/* Highlight Defining Column */
+.ui.definition.table > tr > td:first-child:not(.ignored), .ui.definition.table > tbody > tr > td:first-child:not(.ignored), .ui.definition.table > tbody > tr > td.definition, .ui.definition.table > tr > td.definition {
+    background: rgba(0, 0, 0, 0.03);
+    font-weight: 500;
+    text-transform: '';
+    box-shadow: '';
+    text-align: '';
+    font-size: 1em;
+    padding-left: '';
+    padding-right: '';
+
+    @apply text-gray-600 dark:text-gray-500;
+}
+
+/* Fix 2nd Column */
+.ui.definition.table thead:not(.full-width) th:nth-child(2) {
+    border-left: 1px solid var(--p-content-border-color);
+}
+
+.ui.definition.table tfoot:not(.full-width) th:nth-child(2) {
+    border-left: 1px solid var(--p-content-border-color);
+}
+
+.ui.definition.table > tr > td:nth-child(2), .ui.definition.table > tbody > tr > td:nth-child(2) {
+    border-left: 1px solid var(--p-content-border-color);
+}
+
+
+/*******************************
+             States
+*******************************/
+
+
+/*--------------
+    Positive
+---------------*/
+
+.ui.table tr.positive,
+.ui.table td.positive {
+    box-shadow: 0px 0px 0px #A3C293 inset;
+}
+
+.ui.table tr.positive,
+.ui.table td.positive {
+    background: #FCFFF5 !important;
+    color: #2C662D !important;
+}
+
+/*--------------
+     Negative
+---------------*/
+
+.ui.table tr.negative,
+.ui.table td.negative {
+    box-shadow: 0px 0px 0px #E0B4B4 inset;
+}
+
+.ui.table tr.negative,
+.ui.table td.negative {
+    background: #FFF6F6 !important;
+    color: #9F3A38 !important;
+}
+
+/*--------------
+      Error
+---------------*/
+
+.ui.table tr.error,
+.ui.table td.error {
+    box-shadow: 0px 0px 0px #E0B4B4 inset;
+}
+
+.ui.table tr.error,
+.ui.table td.error {
+    background: #FFF6F6 !important;
+    color: #9F3A38 !important;
+}
+
+.dark {
+    .ui.table tr.error,
+    .ui.table td.error {
+        background: none !important;
+    }
+}
+
+/*--------------
+     Warning
+---------------*/
+
+.ui.table tr.warning,
+.ui.table td.warning {
+    box-shadow: 0px 0px 0px #C9BA9B inset;
+}
+
+.ui.table tr.warning,
+.ui.table td.warning {
+    background: #FFFAF3 !important;
+    color: #573A08 !important;
+}
+
+/*--------------
+     Active
+---------------*/
+
+.ui.table tr.active,
+.ui.table td.active {
+    box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.87) inset;
+}
+
+.ui.table tr.active,
+.ui.table td.active {
+    background: #E0E0E0 !important;
+    color: rgba(0, 0, 0, 0.87) !important;
+}
+
+/*--------------
+     Disabled
+---------------*/
+
+.ui.table tr.disabled td,
+.ui.table tr td.disabled,
+.ui.table tr.disabled:hover,
+.ui.table tr:hover td.disabled {
+    pointer-events: none;
+    color: rgba(40, 40, 40, 0.3);
+}
+
+
+/*******************************
+          Variations
+*******************************/
+
+
+/*--------------
+    Stackable
+---------------*/
+
+@media only screen and (max-width: 991px) {
+    .ui[class*="tablet stackable"].table,
+    .ui[class*="tablet stackable"].table tbody,
+    .ui[class*="tablet stackable"].table tr,
+    .ui[class*="tablet stackable"].table tr > th,
+    .ui[class*="tablet stackable"].table tr > td {
+        width: 100% !important;
+        display: block !important;
+    }
+
+    .ui[class*="tablet stackable"].table {
+        padding: 0em;
+    }
+
+    .ui[class*="tablet stackable"].table thead {
+        display: block;
+    }
+
+    .ui[class*="tablet stackable"].table tfoot {
+        display: block;
+    }
+
+    .ui[class*="tablet stackable"].table tr {
+        padding-top: 1em;
+        padding-bottom: 1em;
+        box-shadow: 0px -1px 0px 0px rgba(0, 0, 0, 0.1) inset !important;
+    }
+
+    .ui[class*="tablet stackable"].table tr > th,
+    .ui[class*="tablet stackable"].table tr > td {
+        background: none;
+        border: none !important;
+        padding: 0.25em 0.75em;
+        box-shadow: none !important;
+    }
+
+    /* Definition Table */
+    .ui.definition[class*="tablet stackable"].table thead th:first-child {
+        box-shadow: none !important;
+    }
+}
+
+/*--------------
+ Text Alignment
+---------------*/
+
+.ui.table[class*="left aligned"],
+.ui.table [class*="left aligned"] {
+    text-align: left;
+}
+
+.ui.table[class*="center aligned"],
+.ui.table [class*="center aligned"] {
+    text-align: center;
+}
+
+.ui.table[class*="right aligned"],
+.ui.table [class*="right aligned"] {
+    text-align: right;
+}
+
+/*------------------
+ Vertical Alignment
+------------------*/
+
+.ui.table[class*="top aligned"],
+.ui.table [class*="top aligned"] {
+    vertical-align: top;
+}
+
+.ui.table[class*="middle aligned"],
+.ui.table [class*="middle aligned"] {
+    vertical-align: middle;
+}
+
+.ui.table[class*="bottom aligned"],
+.ui.table [class*="bottom aligned"] {
+    vertical-align: bottom;
+}
+
+/*--------------
+    Collapsing
+---------------*/
+
+.ui.table th.collapsing,
+.ui.table td.collapsing {
+    width: 1px;
+    white-space: nowrap;
+}
+
+/*--------------
+     Fixed
+---------------*/
+
+.ui.fixed.table {
+    table-layout: fixed;
+}
+
+.ui.fixed.table th,
+.ui.fixed.table td {
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+
+/*--------------
+   Selectable
+---------------*/
+
+.ui.selectable.table tbody tr:hover,
+.ui.table tbody tr td.selectable:hover {
+    @apply bg-gray-50;
+    color: rgba(0, 0, 0, 0.95) !important;
+}
+
+.dark {
+    .ui.selectable.table tbody tr:hover,
+    .ui.table tbody tr td.selectable:hover {
+        @apply bg-gray-900;
+        color: #ffffff !important;
+    }
+}
+
+/* Selectable Cell Link */
+.ui.table tbody tr td.selectable {
+    padding: 0em;
+}
+
+.ui.table tbody tr td.selectable > a:not(.ui) {
+    display: block;
+    color: inherit;
+    padding: 0.78571429em 0.78571429em;
+}
+
+/* Other States */
+.ui.selectable.table tr.error:hover,
+.ui.table tr td.selectable.error:hover,
+.ui.selectable.table tr:hover td.error {
+    background: #ffe7e7 !important;
+    color: #943634 !important;
+}
+
+.dark {
+    .ui.selectable.table tr.error:hover,
+    .ui.table tr td.selectable.error:hover,
+    .ui.selectable.table tr:hover td.error {
+        @apply bg-amber-50;
+    }
+}
+
+.ui.selectable.table tr.warning:hover,
+.ui.table tr td.selectable.warning:hover,
+.ui.selectable.table tr:hover td.warning {
+    background: #fff4e4 !important;
+    color: #493107 !important;
+}
+
+.ui.selectable.table tr.active:hover,
+.ui.table tr td.selectable.active:hover,
+.ui.selectable.table tr:hover td.active {
+    background: #E0E0E0 !important;
+    color: rgba(0, 0, 0, 0.87) !important;
+}
+
+.ui.selectable.table tr.positive:hover,
+.ui.table tr td.selectable.positive:hover,
+.ui.selectable.table tr:hover td.positive {
+    background: #f7ffe6 !important;
+    color: #275b28 !important;
+}
+
+.ui.selectable.table tr.negative:hover,
+.ui.table tr td.selectable.negative:hover,
+.ui.selectable.table tr:hover td.negative {
+    background: #ffe7e7 !important;
+    color: #943634 !important;
+}
+
+/*--------------
+     Striped
+---------------*/
+
+
+/* Table Striping */
+.ui.striped.table > tr:nth-child(2n),
+.ui.striped.table tbody tr:nth-child(2n) {
+    background-color: rgba(0, 0, 50, 0.02);
+}
+
+/* Stripes */
+.dark {
+    .ui.striped.table > tr:nth-child(2n),
+    .ui.striped.table tbody tr:nth-child(2n) {
+        background-color: rgba(255, 255, 255, 0.05);
+    }
+}
+
+/* Allow striped active hover */
+.ui.striped.selectable.selectable.selectable.table tbody tr.active:hover {
+    background: #EFEFEF !important;
+    color: rgba(0, 0, 0, 0.95) !important;
+}
+
+/*--------------
+   Single Line
+---------------*/
+
+.ui.table[class*="single line"],
+.ui.table [class*="single line"] {
+    white-space: nowrap;
+}
+
+.ui.table[class*="single line"],
+.ui.table [class*="single line"] {
+    white-space: nowrap;
+}
+
+/*--------------
+  Column Count
+---------------*/
+
+
+/* Grid Based */
+.ui.one.column.table td {
+    width: 100%;
+}
+
+.ui.two.column.table td {
+    width: 50%;
+}
+
+.ui.three.column.table td {
+    width: 33.33333333%;
+}
+
+.ui.four.column.table td {
+    width: 25%;
+}
+
+.ui.five.column.table td {
+    width: 20%;
+}
+
+.ui.six.column.table td {
+    width: 16.66666667%;
+}
+
+.ui.seven.column.table td {
+    width: 14.28571429%;
+}
+
+.ui.eight.column.table td {
+    width: 12.5%;
+}
+
+.ui.nine.column.table td {
+    width: 11.11111111%;
+}
+
+.ui.ten.column.table td {
+    width: 10%;
+}
+
+.ui.eleven.column.table td {
+    width: 9.09090909%;
+}
+
+.ui.twelve.column.table td {
+    width: 8.33333333%;
+}
+
+.ui.thirteen.column.table td {
+    width: 7.69230769%;
+}
+
+.ui.fourteen.column.table td {
+    width: 7.14285714%;
+}
+
+.ui.fifteen.column.table td {
+    width: 6.66666667%;
+}
+
+.ui.sixteen.column.table td {
+    width: 6.25%;
+}
+
+/* Column Width */
+.ui.table th.one.wide,
+.ui.table td.one.wide {
+    width: 6.25%;
+}
+
+.ui.table th.two.wide,
+.ui.table td.two.wide {
+    width: 12.5%;
+}
+
+.ui.table th.three.wide,
+.ui.table td.three.wide {
+    width: 18.75%;
+}
+
+.ui.table th.four.wide,
+.ui.table td.four.wide {
+    width: 25%;
+}
+
+.ui.table th.five.wide,
+.ui.table td.five.wide {
+    width: 31.25%;
+}
+
+.ui.table th.six.wide,
+.ui.table td.six.wide {
+    width: 37.5%;
+}
+
+.ui.table th.seven.wide,
+.ui.table td.seven.wide {
+    width: 43.75%;
+}
+
+.ui.table th.eight.wide,
+.ui.table td.eight.wide {
+    width: 50%;
+}
+
+.ui.table th.nine.wide,
+.ui.table td.nine.wide {
+    width: 56.25%;
+}
+
+.ui.table th.ten.wide,
+.ui.table td.ten.wide {
+    width: 62.5%;
+}
+
+.ui.table th.eleven.wide,
+.ui.table td.eleven.wide {
+    width: 68.75%;
+}
+
+.ui.table th.twelve.wide,
+.ui.table td.twelve.wide {
+    width: 75%;
+}
+
+.ui.table th.thirteen.wide,
+.ui.table td.thirteen.wide {
+    width: 81.25%;
+}
+
+.ui.table th.fourteen.wide,
+.ui.table td.fourteen.wide {
+    width: 87.5%;
+}
+
+.ui.table th.fifteen.wide,
+.ui.table td.fifteen.wide {
+    width: 93.75%;
+}
+
+.ui.table th.sixteen.wide,
+.ui.table td.sixteen.wide {
+    width: 100%;
+}
+
+/*--------------
+    Sortable
+---------------*/
+
+.ui.sortable.table thead th {
+    cursor: pointer;
+    white-space: nowrap;
+    border-left: 1px solid var(--p-content-border-color);
+    color: rgba(0, 0, 0, 0.87);
+}
+
+.ui.sortable.table thead th:first-child {
+    border-left: none;
+}
+
+.ui.sortable.table thead th.sorted,
+.ui.sortable.table thead th.sorted:hover {
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+}
+
+.ui.sortable.table thead th:after {
+    display: none;
+    font-style: normal;
+    font-weight: normal;
+    text-decoration: inherit;
+    content: '';
+    height: 1em;
+    width: auto;
+    opacity: 0.8;
+    margin: 0em 0em 0em 0.5em;
+    font-family: 'Icons';
+}
+
+.ui.sortable.table thead th.ascending:after {
+    content: '\f0d8';
+}
+
+.ui.sortable.table thead th.descending:after {
+    content: '\f0d7';
+}
+
+/* Hover */
+.ui.sortable.table th.disabled:hover {
+    cursor: auto;
+    color: rgba(40, 40, 40, 0.3);
+}
+
+.ui.sortable.table thead th:hover {
+    background: rgba(0, 0, 0, 0.05);
+    color: rgba(0, 0, 0, 0.8);
+}
+
+/* Sorted */
+.ui.sortable.table thead th.sorted {
+    background: rgba(0, 0, 0, 0.05);
+    color: rgba(0, 0, 0, 0.95);
+}
+
+.ui.sortable.table thead th.sorted:after {
+    display: inline-block;
+}
+
+/* Sorted Hover */
+.ui.sortable.table thead th.sorted:hover {
+    background: rgba(0, 0, 0, 0.05);
+    color: rgba(0, 0, 0, 0.95);
+}
+
+/* Inverted */
+.dark {
+    .ui.sortable.table thead th.sorted {
+        background: rgba(255, 255, 255, 0.15) linear-gradient(transparent, rgba(0, 0, 0, 0.05));
+        color: #ffffff;
+    }
+
+    .ui.sortable.table thead th:hover {
+        background: rgba(255, 255, 255, 0.08) linear-gradient(transparent, rgba(0, 0, 0, 0.05));
+        color: #ffffff;
+    }
+
+    .ui.sortable.table thead th {
+        border-left-color: transparent;
+        border-right-color: transparent;
+    }
+}
+
+/*--------------
+    Inverted
+---------------*/
+
+
+/* Text Color */
+.dark {
+    .ui.table.definition {
+        color: rgba(255, 255, 255, 0.9);
+    }
+
+    .ui.table {
+        border: 1px solid rgba(255, 255, 255, 0.1) !important;
+    }
+
+    .ui.table th {
+        background-color: rgba(0, 0, 0, 0.15);
+        border-color: rgba(255, 255, 255, 0.1) !important;
+        color: rgba(255, 255, 255, 0.9) !important;
+    }
+
+    .ui.table tr td {
+        border-color: rgba(255, 255, 255, 0.1) !important;
+    }
+
+    .ui.table tr.disabled td,
+    .ui.table tr td.disabled,
+    .ui.table tr.disabled:hover td,
+    .ui.table tr:hover td.disabled {
+        pointer-events: none;
+        color: rgba(225, 225, 225, 0.3);
+    }
+}
+
+/* Definition */
+.dark {
+    .ui.definition.table tfoot:not(.full-width) th:first-child,
+    .ui.definition.table > thead:not(.full-width) > tr > th:first-child {
+        background: #FFFFFF;
+    }
+
+    .ui.definition.table > tr > td:first-child, .ui.definition.table > tbody > tr > td:first-child {
+        background: rgba(255, 255, 255, 0.02);
+        color: #ffffff;
+    }
+}
+
+/*--------------
+   Collapsing
+---------------*/
+
+.ui.collapsing.table {
+    width: auto;
+}
+
+/*--------------
+      Basic
+---------------*/
+
+.ui.basic.table {
+    background: transparent;
+    border: 1px solid rgba(34, 36, 38, 0.15);
+    box-shadow: none;
+}
+
+.ui.basic.table thead,
+.ui.basic.table tfoot {
+    box-shadow: none;
+}
+
+.ui.basic.table th {
+    background: transparent;
+    border-left: none;
+}
+
+.ui.basic.table tbody tr {
+    border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+.ui.basic.table td {
+    background: transparent;
+}
+
+.ui.basic.striped.table tbody tr:nth-child(2n) {
+    background-color: rgba(0, 0, 0, 0.05) !important;
+}
+
+/* Very Basic */
+.ui[class*="very basic"].table {
+    border: none;
+}
+
+.ui[class*="very basic"].table:not(.sortable):not(.striped) th,
+.ui[class*="very basic"].table:not(.sortable):not(.striped) td {
+    padding: '';
+}
+
+.ui[class*="very basic"].table:not(.sortable):not(.striped) th:first-child,
+.ui[class*="very basic"].table:not(.sortable):not(.striped) td:first-child {
+    padding-left: 0em;
+}
+
+.ui[class*="very basic"].table:not(.sortable):not(.striped) th:last-child,
+.ui[class*="very basic"].table:not(.sortable):not(.striped) td:last-child {
+    padding-right: 0em;
+}
+
+.ui[class*="very basic"].table:not(.sortable):not(.striped) thead tr:first-child th {
+    padding-top: 0em;
+}
+
+/*--------------
+     Celled
+---------------*/
+
+.ui.celled.table tr th,
+.ui.celled.table tr td {
+    border-left: 1px solid var(--p-content-border-color);
+}
+
+.ui.celled.table tr th:first-child,
+.ui.celled.table tr td:first-child {
+    border-left: none;
+}
+
+/*--------------
+     Padded
+---------------*/
+
+.ui.padded.table th {
+    padding-left: 1em;
+    padding-right: 1em;
+}
+
+.ui.padded.table th,
+.ui.padded.table td {
+    padding: 1em 1em;
+}
+
+/* Very */
+.ui[class*="very padded"].table th {
+    padding-left: 1.5em;
+    padding-right: 1.5em;
+}
+
+.ui[class*="very padded"].table td {
+    padding: 1.5em 1.5em;
+}
+
+/*--------------
+     Compact
+---------------*/
+
+.ui.compact.table th {
+    padding-left: 0.7em;
+    padding-right: 0.7em;
+}
+
+.ui.compact.table td {
+    padding: 0.5em 0.7em;
+}
+
+/* Very */
+.ui[class*="very compact"].table th {
+    padding-left: 0.6em;
+    padding-right: 0.6em;
+}
+
+.ui[class*="very compact"].table td {
+    padding: 0.4em 0.6em;
+}
+
+/*--------------
+      Sizes
+---------------*/
+
+
+/* Small */
+.ui.small.table {
+    font-size: 0.9em;
+}
+
+/* Standard */
+.ui.table {
+    font-size: 1em;
+}
+
+/* Large */
+.ui.large.table {
+    font-size: 1.1em;
+}
+
+
+/*******************************
+         Site Overrides
+*******************************/
+
+.ui.table tbody[style*="display: none;"], .ui.table tr[style*="display: none;"], .ui.table tr > td[style*="display: none;"], .ui.table tr > th[style*="display: none;"] {
+    display: none !important;
+}
+
+
+table.definition > tbody > tr > td:first-child, table.definition > tr > td:first-child {
+    width: 10em;
+    min-width: 10em;
+}

+ 311 - 0
src/assets/tailwind.css

@@ -0,0 +1,311 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+.fields.inline {
+    @apply flex flex-row flex-wrap gap-4 items-center;
+}
+
+.field {
+    label {
+        @apply text-gray-700;
+
+        em {
+            font-style: normal;
+            font-size: 0.9em;
+        }
+    }
+
+    p:not(.comment) {
+        @apply mt-2 text-gray-500;
+    }
+
+    .input {
+        @apply mt-1 block w-full;
+    }
+}
+
+.label-group {
+    @apply flex flex-wrap gap-2 items-center;
+}
+
+p.comment {
+    @apply my-2 text-gray-500  dark:text-gray-300;
+}
+
+td.text-center, th.text-center {
+    @apply !text-center;
+
+    .p-datatable-column-header-content {
+        display: flow;
+    }
+}
+
+h1 {
+    @apply text-6xl;
+}
+
+h2 {
+    @apply text-5xl;
+}
+
+h3 {
+    @apply text-4xl;
+}
+
+h4 {
+    @apply text-3xl;
+}
+
+h5 {
+    @apply text-2xl;
+}
+
+h5.group-header {
+    @apply !mt-8 !mb-4;
+}
+
+h5 a {
+    font-size: 14px;
+    @apply ml-2;
+    color: var(--p-primary-color);
+}
+
+h5 span, h5 a span {
+    font-size: 14px;
+}
+
+h5 a:hover {
+    color: var(--p-primary-700);
+}
+
+h6 {
+    @apply text-xl;
+}
+
+header {
+    @apply text-2xl;
+}
+
+.pi-remove::before {
+    content: "\e90b"
+}
+
+.icon.circle:before {
+    content: "\e9dc";
+}
+
+.icon.remove::before, .icon.close::before {
+    content: "\e90b"
+}
+
+.icon.pencil::before {
+    content: "\e942"
+}
+
+.icon.expand::before {
+    content: "\ea0a";
+}
+
+.icon.setting::before {
+    content: "\e94a";
+}
+
+.icon.info::before {
+    content: "\e923";
+}
+
+.icon.info.circle::before {
+    content: "\e924";
+}
+
+.icon.home::before {
+    content: "\e925";
+}
+
+.icon.arrow.left::before {
+    content: "\e91a";
+}
+
+.icon.arrow.right::before {
+    content: "\e91b";
+}
+
+.icon.angle.down::before {
+    content: "\e930";
+}
+
+.icon.angle.up::before {
+    content: "\e933";
+}
+
+.icon.search::before {
+    content: "\e908";
+}
+
+.icon.bars::before {
+    content: "\e91d";
+}
+
+.icon.expand:before {
+    content: "\ea0a";
+}
+
+.icon.copy:before {
+    content: "\e957";
+}
+
+.icon.database:before {
+    content: "\e9e3";
+}
+
+.icon.download:before {
+    content: "\e956";
+}
+
+.icon.home:before {
+    content: "\e925";
+}
+
+.icon.user:before {
+    content: "\e939";
+}
+
+.icon.cloud:before {
+    content: "\e945";
+}
+
+.icon.bell:before {
+    content: "\e97c";
+}
+
+.icon.file:before {
+    content: "\e958";
+}
+
+.icon.sign.out:before {
+    content: "\e971";
+}
+
+.icon.dashboard:before {
+    content: "\ea37";
+}
+
+.icon.clone:before {
+    content: "\e955";
+}
+
+.icon.address.book:before {
+    content: "\ea2a";
+}
+
+.icon.ticket:before {
+    content: "\e9a9";
+}
+
+.icon.dollar:before {
+    content: "\e96b";
+}
+
+.icon.shield:before {
+    content: "\e9b9";
+}
+
+.icon.globe:before {
+    content: "\e94f";
+}
+
+.icon.paper.plane:before {
+    content: "\e9ca";
+}
+
+.icon.puzzle.piece:before {
+    content: "\e918";
+}
+
+.icon.magnet:before {
+    content: "\e9b9";
+}
+
+.icon.plus:before {
+    content: "\e90d";
+}
+
+.icon.check:before {
+    content: "\e909";
+}
+
+.icon.chart.area:before {
+    content: "\e98b";
+}
+
+.icon.table:before {
+    content: "\e969";
+}
+
+.icon.tag:before {
+    content: "\e95e";
+}
+
+.icon {
+    font-family: 'primeicons';
+    speak: none;
+    font-style: normal;
+    font-weight: normal;
+    font-variant: normal;
+    text-transform: none;
+    line-height: 1;
+    display: inline-block;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+}
+
+.icon:before {
+    --webkit-backface-visibility: hidden;
+    backface-visibility: hidden;
+}
+
+.icon.small {
+    font-size: 12px;
+}
+
+.pi.small {
+    font-size: 12px;
+}
+
+.pi.tiny {
+    font-size: 0.7em;
+}
+
+.pi.red, .icon.red {
+    @apply !text-red-500;
+}
+
+.pi.green, .icon.green {
+    @apply !text-green-500;
+}
+
+.pi.gray, .pi.grey, .icon.gray, .icon.grey {
+    @apply text-gray-400;
+}
+
+.card {
+    @apply block p-6 bg-white border border-gray-200 rounded-lg shadow hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700
+}
+
+.card.green {
+    @apply text-green-500 border-t-green-500 border-t-2;
+}
+
+.card.red {
+    @apply text-red-500 border-t-red-500 border-t-2;
+}
+
+/** span **/
+span.small {
+    font-size: 0.9em;
+}
+
+code-label {
+    @apply bg-gray-100 py-1 px-2 rounded-md;
+}

+ 206 - 0
src/components/anti-ddos/ADInstanceObjectsBox.vue

@@ -0,0 +1,206 @@
+<script setup>
+
+import "@/lib/array.js"
+import useUtils from "@/utils/utils.js"
+import {inject, onMounted, ref} from "vue"
+import TLabel from "@/components/ui/TLabel.vue";
+import JSPage from "@/components/common/JSPage.vue";
+import PlusButton from "@/components/common/PlusButton.vue";
+import ListTable from "@/components/common/ListTable.vue";
+import TColumn from "@/components/ui/TColumn.vue";
+import DefinitionTable from "@/components/common/DefinitionTable.vue";
+
+const utils = useUtils()
+
+const Tea = inject("$Tea")
+
+const props = defineProps(["v-objects", "v-user-id"])
+
+const serverPageRef = ref()
+const {userId, objects, objectCodes, isAdding, servers, serversIsLoading} = (function () {
+	let objects = props.vObjects
+	if (objects == null) {
+		objects = []
+	}
+
+	let objectCodes = []
+	objects.forEach(function (v) {
+		objectCodes.push(v.code)
+	})
+
+	return {
+		userId: ref(props.vUserId),
+		objects: ref(objects),
+		objectCodes: ref(objectCodes),
+		isAdding: ref(true),
+
+		servers: ref([]),
+		serversIsLoading: ref(false)
+	}
+})()
+
+const add = function () {
+	isAdding.value = true
+}
+
+const cancel = function () {
+	isAdding.value = false
+}
+
+const remove = function (index) {
+	utils.confirm("确定要删除此防护对象吗?", function () {
+		objects.value.$remove(index)
+		notifyChange()
+	})
+}
+
+const removeObjectCode = function (objectCode) {
+	let index = -1
+	objectCodes.value.forEach(function (v, k) {
+		if (objectCode == v) {
+			index = k
+		}
+	})
+	if (index >= 0) {
+		objects.value.$remove(index)
+		notifyChange()
+	}
+}
+
+const getUserServers = function (page) {
+	serversIsLoading.value = true
+	Tea.action(".userServers")
+		.post()
+		.params({
+			userId: userId.value,
+			page: page,
+			pageSize: 5
+		})
+		.success(function (resp) {
+			servers.value = resp.data.servers
+
+			serverPageRef.value.updateMax(resp.data.page.max)
+			serversIsLoading.value = false
+		})
+		.error(function () {
+			serversIsLoading.value = false
+		})
+}
+
+const changeServerPage = function (page) {
+	getUserServers(page)
+}
+
+const selectServerObject = function (server) {
+	if (existObjectCode("server:" + server.id)) {
+		return
+	}
+
+	objects.value.push({
+		"type": "server",
+		"code": "server:" + server.id,
+
+		"id": server.id,
+		"name": server.name
+	})
+	notifyChange()
+}
+
+const notifyChange = function () {
+	let newObjectCodes = []
+	objects.value.forEach(function (v) {
+		newObjectCodes.push(v.code)
+	})
+	objectCodes.value = newObjectCodes
+}
+
+const existObjectCode = function (objectCode) {
+	let found = false
+	objects.value.forEach(function (v) {
+		if (v.code == objectCode) {
+			found = true
+		}
+	})
+	return found
+}
+
+onMounted(() => {
+	getUserServers(1)
+})
+
+</script>
+
+<template>
+	<div>
+		<input type="hidden" name="objectCodesJSON" :value="JSON.stringify(objectCodes)"/>
+
+		<!-- 已有对象 -->
+		<div>
+			<div v-if="objects.length == 0"><span class="grey">暂时还没有设置任何防护对象。</span></div>
+			<div v-if="objects.length > 0">
+				<DefinitionTable>
+					<tbody>
+					<tr>
+						<td class="title">已选中防护对象</td>
+						<td>
+							<div class="label-group">
+								<TLabel outlined="" v-for="(object, index) in objects">
+									<span v-if="object.type == 'server'">网站:{{ object.name }}</span>
+									&nbsp;
+									<a href="" title="删除" @click.prevent="remove(index)"><i class="pi pi-remove small"></i></a>
+								</TLabel>
+							</div>
+						</td>
+					</tr>
+					</tbody>
+				</DefinitionTable>
+			</div>
+		</div>
+		<div class="margin"></div>
+
+		<!-- 添加表单 -->
+		<div v-if="isAdding">
+			<DefinitionTable>
+				<tbody>
+				<tr>
+					<td class="title">对象类型</td>
+					<td>网站</td>
+				</tr>
+				<!-- 网站列表 -->
+				<tr>
+					<td>网站列表</td>
+					<td>
+						<span v-if="serversIsLoading">加载中...</span>
+						<div v-if="!serversIsLoading && servers.length == 0">暂时还没有可选的网站。</div>
+						<div v-show="!serversIsLoading && servers.length > 0">
+							<ListTable :items="servers">
+								<TColumn header="网站名称">
+									<template #body="{data: server}">
+										{{ server.name }}
+									</template>
+								</TColumn>
+								<TColumn header="操作" class="one op">
+									<template #body="{data: server}">
+										<a href="" @click.prevent="selectServerObject(server)" v-if="!existObjectCode('server:' + server.id)">选中</a>
+										<a href="" @click.prevent="removeObjectCode('server:' + server.id)" v-else><span class="red">取消</span></a>
+									</template>
+								</TColumn>
+							</ListTable>
+						</div>
+
+						<JSPage ref="serverPageRef" @change="changeServerPage"></JSPage>
+					</td>
+				</tr>
+				</tbody>
+			</DefinitionTable>
+		</div>
+
+		<!-- 添加按钮 -->
+		<div v-if="!isAdding">
+			<PlusButton size="small" type="button" @click.prevent="add"></PlusButton>
+		</div>
+	</div>
+</template>
+
+<style scoped>
+</style>

+ 92 - 0
src/components/common/BandwidthSizeCapacityBox.vue

@@ -0,0 +1,92 @@
+<script setup>
+import {ref, watch} from "vue";
+import TSelect from "@/components/ui/TSelect.vue";
+import TOption from "@/components/ui/TOption.vue";
+import TInputText from "@/components/ui/TInputText.vue";
+
+const props = defineProps(["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"])
+const emits = defineEmits(["change"])
+
+const {capacity, countString, vSize, vMaxlength, supportedUnits} = (function () {
+	let v = props.vValue
+	if (v == null) {
+		v = {
+			count: props.vCount,
+			unit: props.vUnit
+		}
+	}
+	if (v.unit == null || v.unit.length == 0) {
+		v.unit = "mb"
+	}
+
+	if (typeof (v["count"]) != "number") {
+		v["count"] = -1
+	}
+
+	let vSize = props.size
+	if (vSize == null) {
+		vSize = 6
+	}
+
+	let vMaxlength = props.maxlength
+	if (vMaxlength == null) {
+		vMaxlength = 10
+	}
+
+	let supportedUnits = props.vSupportedUnits
+	if (supportedUnits == null) {
+		supportedUnits = []
+	}
+
+	return {
+		capacity: ref(v),
+		countString: ref((v.count >= 0) ? v.count.toString() : ""),
+		vSize: ref(vSize),
+		vMaxlength: ref(vMaxlength),
+		supportedUnits: ref(supportedUnits)
+	}
+})()
+
+const change = function () {
+	emits("change", capacity.value)
+}
+
+watch(countString, function (newValue) {
+	let value = newValue.trim()
+	if (value.length == 0) {
+		capacity.value.count = -1
+		change()
+		return
+	}
+	let count = parseInt(value)
+	if (!isNaN(count)) {
+		capacity.value.count = count
+	}
+	change()
+})
+
+</script>
+
+<template>
+	<div class="ui fields inline">
+		<input type="hidden" :name="vName" :value="JSON.stringify(capacity)"/>
+		<div class="ui field">
+			<TInputText type="text" v-model="countString" :maxlength="vMaxlength" :size="vSize"/>
+		</div>
+		<div class="ui field">
+			<TSelect v-model="capacity.unit" @change="change">
+				<TOption value="b" v-if="supportedUnits.length == 0 || supportedUnits.$contains('b')">Bps</TOption>
+				<TOption value="kb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('kb')">Kbps</TOption>
+				<TOption value="mb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('mb')">Mbps</TOption>
+				<TOption value="gb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('gb')">Gbps</TOption>
+				<TOption value="tb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('tb')">Tbps</TOption>
+				<TOption value="pb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('pb')">Pbps</TOption>
+				<TOption value="eb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('eb')">Ebps</TOption>
+			</TSelect>
+		</div>
+	</div>
+</template>
+
+<style scoped>
+
+</style>

+ 22 - 0
src/components/common/BandwidthSizeCapacityView.vue

@@ -0,0 +1,22 @@
+<script setup>
+const props = defineProps( ["v-value"])
+const {capacity} = (function () {
+	let capacity = Object.assign({}, props.vValue)
+	if (capacity != null && capacity.count > 0 && typeof capacity.unit === "string") {
+		capacity.unit = capacity.unit[0].toUpperCase() + capacity.unit.substring(1) + "ps"
+	}
+	return {
+		capacity: capacity
+	}
+})()
+</script>
+
+<template>
+<span>
+	<span v-if="capacity != null && capacity.count > 0">{{capacity.count}}{{capacity.unit}}</span>
+</span>
+</template>
+
+<style scoped>
+
+</style>

+ 26 - 0
src/components/common/BaseComponents.vue

@@ -0,0 +1,26 @@
+<script setup>
+import TToast from "@/components/ui/TToast.vue";
+import TConfirmDialog from "@/components/ui/TConfirmDialog.vue";
+import TDialogDynamic from "@/components/ui/TDialogDynamic.vue";
+import InstantMessageToast from "@/components/messages/InstantMessageToast.vue";
+</script>
+
+<template>
+	<div>
+		<TConfirmDialog></TConfirmDialog>
+		<TToast>
+			<template #message="slotProps">
+				{{ slotProps.message.detail }}
+			</template>
+		</TToast>
+		<InstantMessageToast></InstantMessageToast>
+		<TDialogDynamic></TDialogDynamic>
+	</div>
+</template>
+
+<style>
+.p-confirmdialog {
+	min-width: 20rem;
+	max-width: 30rem;
+}
+</style>

+ 28 - 0
src/components/common/BitsVar.vue

@@ -0,0 +1,28 @@
+<script setup>
+import useUtils from "@/utils/utils.js";
+
+const utils = useUtils()
+
+const props = defineProps(["v-bits"])
+const {format} = (function () {
+	let bits = props.vBits
+	if (typeof bits != "number") {
+		bits = 0
+	}
+	let format = utils.splitFormat(utils.formatBits(bits))
+	return {
+		format: format
+	}
+})()
+
+</script>
+
+<template>
+	<var class="normal">
+		<span>{{format[0]}}</span>{{format[1]}}
+	</var>
+</template>
+
+<style scoped>
+
+</style>

+ 26 - 0
src/components/common/BytesVar.vue

@@ -0,0 +1,26 @@
+<script setup>
+import useUtils from "@/utils/utils.js";
+
+const utils = useUtils()
+const props = defineProps(["v-bytes"])
+const {format} = (function () {
+	let bytes = props.vBytes
+	if (typeof bytes != "number") {
+		bytes = 0
+	}
+	let format = utils.splitFormat(utils.formatBytes(bytes))
+	return {
+		format: format
+	}
+})()
+</script>
+
+<template>
+	<var class="normal">
+		<span>{{ format[0] }}</span>{{ format[1] }}
+	</var>
+</template>
+
+<style scoped>
+
+</style>

+ 40 - 0
src/components/common/CSRFToken.vue

@@ -0,0 +1,40 @@
+<script setup>
+import {inject, onMounted, ref} from "vue";
+
+const Tea = inject("$Tea")
+
+const token = ref("")
+const tokenInputRe = ref()
+const emits = defineEmits(["update:modelValue"])
+
+const refreshToken = function () {
+	Tea.action("/csrf/token")
+		.get()
+		.success(function (resp) {
+			token.value = resp.data.token
+			emits("update:modelValue", token.value)
+		})
+}
+
+onMounted(() => {
+	refreshToken()
+
+	tokenInputRe.value.form.addEventListener("submit", function () {
+		refreshToken()
+	})
+
+	// 自动刷新
+	setInterval(function () {
+		refreshToken()
+	}, 10 * 60 * 1000)
+})
+
+</script>
+
+<template>
+	<input type="hidden" name="csrfToken" :value="token" ref="tokenInputRe"/>
+</template>
+
+<style scoped>
+
+</style>

+ 12 - 0
src/components/common/CardComment.vue

@@ -0,0 +1,12 @@
+<script setup>
+</script>
+
+<template>
+	<p class="comment">
+		<slot></slot>
+	</p>
+</template>
+
+<style scoped lang="postcss">
+
+</style>

+ 13 - 0
src/components/common/CardDivider.vue

@@ -0,0 +1,13 @@
+<script setup>
+import TDivider from "@/components/ui/TDivider.vue";
+</script>
+
+<template>
+	<div class="col-span-full">
+		<TDivider></TDivider>
+	</div>
+</template>
+
+<style scoped lang="postcss">
+
+</style>

+ 149 - 0
src/components/common/CardRow.vue

@@ -0,0 +1,149 @@
+<script setup>
+import {inject, onMounted, ref, watchEffect} from "vue";
+
+const utils = inject("$utils")
+
+const props = defineProps({
+	name: {type: String, default: ""},
+	aliasName: {type: String, default: ""},
+	value: {type: String, default: ""},
+	comment: {type: String, default: ""},
+	colspan: {default: ""},
+	full: {type: Boolean, default: false},
+	newRow: {type: Boolean, default: false},
+	tip: {type: String, default: ""},
+	groupItem: {type: Boolean, default: false},
+	required: {type: Boolean, default: false},
+	noHover: {type: Boolean, default: false}
+})
+
+const valueRef = ref()
+
+// custom class
+const customClass = ref({})
+const calculateCustomClass = () => {
+	customClass.value = {}
+
+	if (props.colspan == "full") {
+		customClass.value["col-span-full"] = true
+	} else if (typeof props.colspan === "string" && props.colspan.match(/^\d+$/)) {
+		customClass.value["col-span-" + props.colspan] = true
+	} else if (typeof props.colspan === "number") {
+		customClass.value["col-span-" + props.colspan] = true
+	}
+
+	if (props.full) {
+		customClass.value["col-span-full"] = true
+	}
+
+	if (props.newRow) {
+		customClass.value["col-start-1"] = true
+	}
+
+	if (props.groupItem) {
+		customClass.value["!border-l-gray-100 dark:!border-l-gray-800"] = true
+	}
+
+	if (props.noHover) {
+		customClass.value["no-hover"] = true
+	}
+}
+
+watchEffect(() => {
+	calculateCustomClass()
+})
+
+// tip
+const showTip = (tip) => {
+	utils.popupTip(tip)
+}
+
+// label
+const processLabels = () => {
+	valueRef.value.querySelectorAll(".p-chip").forEach(label => {
+		if (!label.className.match(/\b(small|large|normal|tiny)\b/)) {
+			label.className += " small"
+		}
+	})
+}
+
+// lifecycle
+onMounted(() => {
+	calculateCustomClass()
+
+	// label
+	processLabels()
+	setTimeout(() => processLabels())
+})
+
+</script>
+
+<template>
+	<div class="row border-l-[1px] border-l-transparent" :class="customClass">
+		<slot name="start"></slot>
+		<div class="name" v-if="(name != null && name.length > 0) || $slots.name">
+			<div>
+				{{ name }}
+				<slot name="name"></slot>
+			</div>
+			<div class="alias-name" v-if="aliasName.length > 0">({{ aliasName }})</div>
+			<span v-if="required">&nbsp;*</span>
+			<a href="" v-if="tip.length > 0" @click.prevent="showTip(tip)" v-tooltip.top="'点击看帮助'"><i class="icon pi pi-question-circle text-gray-400"></i></a>
+		</div>
+		<div class="value" ref="valueRef">{{ value }}
+			<slot name="value"></slot>
+		</div>
+		<p class="comment" v-if="comment.length > 0 || $slots.comment">{{ comment }}
+			<slot name="comment"></slot>
+		</p>
+		<slot></slot>
+		<slot name="end"></slot>
+	</div>
+</template>
+
+<style scoped lang="postcss">
+.row {
+	@apply leading-6 px-4 py-2;
+	word-break: break-word;
+
+	&:not(.no-hover):hover {
+		@apply !border-l-transparent bg-gray-50 dark:bg-gray-800 rounded-lg;
+
+		.comment, :deep(.comment) {
+			@apply dark:border-l-gray-500;
+		}
+	}
+}
+
+.name {
+	color: var(--p-text-muted-color);
+	@apply flex items-center gap-1;
+
+	:deep(em) {
+		@apply ml-2 text-sm not-italic text-gray-500 font-normal;
+	}
+
+	a {
+		@apply flex;
+
+		i:hover {
+			color: var(--p-primary-700);
+		}
+	}
+
+	.alias-name {
+		@apply text-sm text-gray-500 dark:text-gray-500 font-normal;
+	}
+}
+
+.value {
+	font-size: 0.95em;
+	@apply mt-1.5;
+}
+
+.comment, :deep(.comment) {
+	font-size: 0.95em;
+	@apply !mt-1.5 border-l-2 dark:border-l-gray-800 pl-2;
+}
+
+</style>

+ 9 - 0
src/components/common/CardRowGroup.vue

@@ -0,0 +1,9 @@
+<script setup></script>
+
+<template>
+	<slot></slot>
+</template>
+
+<style scoped lang="postcss">
+
+</style>

+ 86 - 0
src/components/common/CardView.vue

@@ -0,0 +1,86 @@
+<script setup>
+
+import TDivider from "@/components/ui/TDivider.vue";
+
+const props = defineProps({
+	header: {type: String, default: ""},
+	cols: {type: [Number, String], default: 0},
+	gap: {type: [Number, String], default: -1}
+})
+
+</script>
+
+<template>
+	<div>
+		<div class="card-box" v-bind="$attrs">
+			<div class="header" v-if="(header && header.length > 0) || $slots.header">{{ header }}
+				<slot name="header"></slot>
+			</div>
+			<div class="rows" :class="{'mt-0': (header && header.length > 0) || $slots.header, ['fixed-cols grid-cols-' + cols]:(cols > 0), ['fixed-gap gap-y-' + gap]:(gap > -1)  }">
+				<slot></slot>
+			</div>
+			<div class="footer" v-if="$slots.footer">
+				<TDivider></TDivider>
+				<slot name="footer"></slot>
+			</div>
+		</div>
+	</div>
+</template>
+
+<style scoped lang="postcss">
+.card-box {
+	@apply border-[1px] p-2 rounded-lg;
+	border-color: var(--p-content-border-color);
+
+	&:hover {
+		border-left-color: var(--p-primary-color);
+		border-bottom-left-radius: 0;
+		border-top-left-radius: 0;
+	}
+}
+
+.rows {
+	@apply grid;
+}
+
+.rows:not(.fixed-cols) {
+	@apply grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5;
+}
+
+.rows:not(.fixed-gap) {
+	@apply gap-y-3;
+}
+
+.header {
+	font-weight: 400;
+	color: #333;
+	font-size: 1.05em;
+	@apply px-4 mb-4 text-xl;
+}
+
+.dark .header {
+	@apply text-gray-300;
+}
+
+.footer {
+	@apply px-4;
+}
+
+.footer .comment, :deep(.footer .comment) {
+	font-size: 0.95em;
+	@apply !mt-1.5 border-l-2 dark:border-l-gray-800 pl-2;
+}
+
+:deep(a) {
+	color: var(--p-primary-color);
+}
+
+:deep(a:hover) {
+	color: var(--p-primary-700);
+}
+</style>
+<style lang="postcss">
+.dark .card-box a:hover {
+	color: var(--p-primary-300);
+}
+</style>

+ 12 - 0
src/components/common/Cards.vue

@@ -0,0 +1,12 @@
+<script setup>
+</script>
+
+<template>
+	<div class="grid gap-4">
+		<slot></slot>
+	</div>
+</template>
+
+<style scoped lang="postcss">
+
+</style>

+ 97 - 0
src/components/common/ChartColumnsGrid.vue

@@ -0,0 +1,97 @@
+<script setup>
+import {onMounted, onUnmounted, onUpdated, ref} from "vue";
+import "@/assets/grids.css"
+
+const rootRef = ref()
+
+const {columns, totalElements} = (function () {
+	return {
+		columns: ref("4"),
+		totalElements: ref(0)
+	}
+})()
+
+const calculateColumns = function () {
+	let w = window.innerWidth
+	let columns = Math.floor(w / 500)
+	if (columns == 0) {
+		columns = 1
+	}
+
+	let columnElements = rootRef.value.getElementsByClassName("column")
+	if (columnElements.length == 0) {
+		return "one"
+	}
+	let maxColumns = columnElements.length
+	if (columns > maxColumns) {
+		columns = maxColumns
+	}
+
+	// 添加右侧边框
+	for (let index = 0; index < columnElements.length; index++) {
+		let el = columnElements[index]
+		el.className = el.className.replace("with-border", "")
+		if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) {
+			el.className += " with-border"
+		}
+	}
+
+	switch (columns) {
+		case 1:
+			return "1"
+		case 2:
+			return "2"
+		case 3:
+			return "3"
+		case 4:
+			return "4"
+		case 5:
+			return "5"
+		case 6:
+			return "6"
+		case 7:
+			return "7"
+		case 8:
+			return "8"
+		case 9:
+			return "9"
+		case 10:
+			return "10"
+		default:
+			return "10"
+	}
+}
+
+const resizeListener = function () {
+	columns.value = calculateColumns()
+}
+
+onMounted(() => {
+	columns.value = calculateColumns()
+
+	window.addEventListener("resize", resizeListener)
+})
+
+onUnmounted(() => {
+	window.removeEventListener("resize", resizeListener)
+})
+
+onUpdated(() => {
+	let _totalElements = rootRef.value.getElementsByClassName("column").length
+	if (_totalElements == totalElements.value) {
+		return
+	}
+	totalElements.value = _totalElements
+	calculateColumns()
+})
+
+</script>
+
+<template>
+	<div class="grid chart-grid" :class="'grid-cols-' + columns" ref="rootRef">
+		<slot></slot>
+	</div>
+</template>
+
+<style scoped lang="postcss">
+</style>

+ 20 - 0
src/components/common/CodeLabel.vue

@@ -0,0 +1,20 @@
+<script setup>
+import TLabel from "@/components/ui/TLabel.vue";
+
+const emits = defineEmits(["click"])
+
+const click = (...allArgs) => {
+	emits("click", ...allArgs)
+}
+
+</script>
+
+<template>
+	<TLabel class="text-xs" @click.prevent="click"><slot></slot></TLabel>
+</template>
+
+<style scoped>
+.p-chip {
+	padding: 0.1em 0.8em;
+}
+</style>

+ 11 - 0
src/components/common/CodeLabelPlain.vue

@@ -0,0 +1,11 @@
+<script setup>
+import TLabel from "@/components/ui/TLabel.vue";
+</script>
+
+<template>
+	<TLabel outlined="" size="tiny"><slot></slot></TLabel>
+</template>
+
+<style scoped lang="postcss">
+
+</style>

+ 62 - 0
src/components/common/ColumnsGrid.vue

@@ -0,0 +1,62 @@
+<script setup>
+import "@/assets/grids.css"
+import {onMounted, onUnmounted, ref} from "vue";
+
+const columns = ref(4)
+const rootRef = ref()
+
+const calculateColumns = function () {
+	let w = window.innerWidth
+	let columns = Math.floor(w / 250)
+	if (columns == 0) {
+		columns = 1
+	}
+
+	let columnElements = rootRef.value.getElementsByClassName("column")
+	if (columnElements.length == 0) {
+		return
+	}
+	let maxColumns = columnElements.length
+	if (columns > maxColumns) {
+		columns = maxColumns
+	}
+
+	// 添加右侧边框
+	for (let index = 0; index < columnElements.length; index++) {
+		let el = columnElements[index]
+		el.className = el.className.replace("with-border", "")
+		if (index % columns == columns - 1 || index == columnElements.length - 1 /** 最后一个 **/) {
+			el.className += " with-border"
+		}
+	}
+	if (columns > 10) {
+		columns = 10
+	}
+
+	return columns
+}
+
+const resizeListener = function () {
+	columns.value = calculateColumns()
+}
+
+onMounted(() => {
+	columns.value = calculateColumns()
+
+	window.addEventListener("resize", resizeListener)
+})
+
+onUnmounted(() => {
+	window.removeEventListener("resize", resizeListener)
+})
+
+</script>
+
+<template>
+	<div ref="rootRef" class="grid" :class="'grid-cols-' + columns + ' counter-chart'">
+		<slot></slot>
+	</div>
+</template>
+
+<style scoped>
+</style>

+ 340 - 0
src/components/common/ComboBox.vue

@@ -0,0 +1,340 @@
+<script setup>
+// TODO 实现键盘上下移动选择
+
+import "@/lib/array.js"
+
+// data-url 和 data-key 成对出现
+import {inject, onMounted, ref} from "vue";
+import useUtils from "@/utils/utils.js";
+import TInputText from "@/components/ui/TInputText.vue";
+import TListBox from "@/components/ui/TListBox.vue";
+import TLabel from "@/components/ui/TLabel.vue";
+
+const utils = useUtils()
+const Tea = inject("$Tea")
+
+const props = defineProps([
+	"name", "title", "placeholder", "size", "v-items", "v-value",
+	"data-url", // 数据源URL
+	"data-key", // 数据源中数据的键名
+	"data-search", // 是否启用动态搜索,如果值为on或true,则表示启用
+	"width"
+])
+const emits = defineEmits(["change"])
+const searchBox = ref()
+const selectedLabel = ref()
+const menu = ref()
+
+onMounted(() => {
+	if (dataURL.value.length > 0) {
+		search("")
+	}
+
+	// 设定菜单宽度
+	if (searchBox.value != null) {
+		let inputWidth = searchBox.value.offsetWidth
+		if (inputWidth != null && inputWidth > 0) {
+			menu.value.style.width = inputWidth + "px"
+		} else if (styleWidth.value.length > 0) {
+			menu.value.style.width = styleWidth.value
+		}
+	}
+})
+
+const search = function (keyword) {
+	// 从URL中获取选项数据
+	let dataUrl = dataURL.value
+	let dataKey = props.dataKey
+
+	let requestId = Math.random()
+	urlRequestId.value = requestId
+
+	Tea.action(dataUrl)
+		.params({
+			keyword: (keyword == null) ? "" : keyword
+		})
+		.post()
+		.success(function (resp) {
+			if (requestId != urlRequestId.value) {
+				return
+			}
+
+			if (resp.data != null) {
+				if (typeof (resp.data[dataKey]) == "object") {
+					let newItems = formatItems(resp.data[dataKey])
+					allItems.value = newItems
+					items.value = newItems.$copy()
+
+					if (isInitial.value) {
+						isInitial.value = false
+						if (props.vValue != null) {
+							newItems.forEach(function (v) {
+								if (v.value == props.vValue) {
+									selectedItem.value = v
+								}
+							})
+						}
+					}
+				}
+			}
+		})
+}
+
+const formatItems = function (items) {
+	items.forEach(function (v) {
+		if (v.value == null) {
+			v.value = v.id
+		}
+	})
+	return items
+}
+
+const reset = function () {
+	selectedItem.value = null
+	change()
+	hoverIndex.value = 0
+
+	setTimeout(function () {
+		if (searchBox) {
+			searchBox.value.focus()
+		}
+	})
+}
+
+const clear = function () {
+	if (isInitial.value) {
+		selectedItem.value = null
+		change()
+		hoverIndex.value = 0
+	}
+}
+
+const changeKeyword = function () {
+	let shouldSearch = dataURL.value.length > 0 && (props.dataSearch == "on" || props.dataSearch == "true")
+
+	hoverIndex.value = 0
+	if (keyword.value.length == 0) {
+		if (shouldSearch) {
+			search(keyword.value)
+		} else {
+			items.value = allItems.value.$copy()
+		}
+		return
+	}
+
+	if (shouldSearch) {
+		search(keyword.value)
+	} else {
+		items.value = allItems.value.$copy().filter(function (v) {
+			if (v.fullname != null && v.fullname.length > 0 && utils.match(v.fullname, keyword.value)) {
+				return true
+			}
+			return utils.match(v.name, keyword.value)
+		})
+	}
+}
+
+const selectItem = function (item) {
+	selectedItem.value = item
+	change()
+	hoverIndex.value = 0
+	keyword.value = ""
+	changeKeyword()
+}
+
+const confirm = function () {
+	if (items.value.length > hoverIndex.value) {
+		selectItem(items.value[hoverIndex.value])
+	}
+}
+
+const show = function (e) {
+	visible.value = true
+
+	// 不要重置hoverIndex,以便焦点可以在输入框和可选项之间切换
+}
+
+const hide = function () {
+	hideTimer.value = setTimeout(function () {
+		visible.value = false
+	}, 500)
+}
+
+const downItem = function () {
+	hoverIndex.value++
+	if (hoverIndex.value > items.value.length - 1) {
+		hoverIndex.value = 0
+	}
+	focusItem()
+}
+
+const upItem = function () {
+	hoverIndex.value--
+	if (hoverIndex.value < 0) {
+		hoverIndex.value = 0
+	}
+	focusItem()
+}
+
+const focusItem = function () {
+	if (hoverIndex.value < items.value.length) {
+		//itemRef.value[hoverIndex.value].focus() // TODO
+		setTimeout(function () {
+			searchBox.value.focus()
+			if (hideTimer.value != null) {
+				clearTimeout(hideTimer.value)
+				hideTimer.value = null
+			}
+		})
+	}
+}
+
+const change = function () {
+	emits("change", selectedItem.value)
+
+	setTimeout(function () {
+		if (selectedLabel.value != null) {
+			selectedLabel.value.$el.focus()
+		}
+	})
+}
+
+const submitForm = function (event) {
+	if (event.target.tagName != "A") {
+		return
+	}
+	let parentBox = selectedLabel.value.$el.parentNode
+	while (true) {
+		parentBox = parentBox.parentNode
+		if (parentBox == null || parentBox.tagName == "BODY") {
+			return
+		}
+		if (parentBox.tagName == "FORM") {
+			parentBox.submit()
+			break
+		}
+	}
+}
+
+const setDataURL = function (newDataURL) {
+	dataURL.value = newDataURL
+}
+
+const reloadData = function () {
+	search("")
+}
+
+const {
+	allItems,
+	items,
+	selectedItem,
+	keyword,
+	visible,
+	hideTimer,
+	hoverIndex,
+	styleWidth,
+	isInitial,
+	dataURL,
+	urlRequestId
+} = (function () {
+	let items = props.vItems
+	if (items == null || !(items instanceof Array)) {
+		items = []
+	}
+	items = formatItems(items)
+
+	// 当前选中项
+	let selectedItem = null
+	if (props.vValue != null) {
+		items.forEach(function (v) {
+			if (v.value == props.vValue) {
+				selectedItem = v
+			}
+		})
+	}
+
+	let width = props.width
+	if (width == null || width.length == 0) {
+		width = "11em"
+	} else {
+		if (/\d+$/.test(width)) {
+			width += "em"
+		}
+	}
+
+	// data url
+	let dataURL = ""
+	if (typeof props.dataUrl == "string" && props.dataUrl.length > 0) {
+		dataURL = props.dataUrl
+	}
+
+	return {
+		allItems: ref(items), // 原始的所有的items
+		items: ref(items.$copy()), // 候选的items
+		selectedItem: ref(selectedItem), // 选中的item
+		keyword: ref(""),
+		visible: ref(false),
+		hideTimer: ref(null),
+		hoverIndex: ref(0),
+		styleWidth: ref(width),
+
+		isInitial: ref(true),
+		dataURL: ref(dataURL),
+		urlRequestId: ref(0) // 记录URL请求ID,防止并行冲突
+	}
+})()
+
+const onListItemChange = (e) => {
+	selectItem(items.value.$find((k, v) => {
+		return v.value == e.value
+	}))
+}
+
+defineExpose({
+	clear,
+	setDataURL,
+	reloadData
+})
+</script>
+
+<template>
+	<div style="display: inline; background: white" class="combo-box relative">
+		<!-- 搜索框 -->
+		<div v-if="selectedItem == null">
+			<TInputText type="text" v-model="keyword" :placeholder="placeholder" :size="size"
+						:style="{'width': styleWidth}"
+						@input="changeKeyword" @focus="show" @blur="hide" @keyup.enter="confirm()"
+						@keypress.enter.prevent="1" ref="searchBox" @keydown.down.prevent="downItem"
+						@keydown.up.prevent="upItem"/>
+		</div>
+
+		<!-- 当前选中 -->
+		<div v-if="selectedItem != null">
+			<input type="hidden" :name="name" :value="selectedItem.value"/>
+			<TLabel ref="selectedLabel" outlined=""><span><span
+				v-if="title != null && title.length > 0">{{ title }}:</span>{{ selectedItem.name }}</span>&nbsp;
+				<a href="" title="清除" @click.prevent="reset"><i class="pi pi-remove small"></i></a>
+			</TLabel>
+		</div>
+
+		<!-- 菜单 -->
+		<div v-show="selectedItem == null && items.length > 0 && visible" ref="menu">
+			<div class="absolute bg-white z-50">
+				<TListBox :options="items" @change="onListItemChange"></TListBox>
+			</div>
+
+			<!--<div class="ui menu vertical small narrow-scrollbar" ref="menu">
+				<a href="" v-for="(item, index) in items" ref="itemRef" class="item"
+				   :class="{active: index == hoverIndex, blue: index == hoverIndex}" @click.prevent="selectItem(item)"
+				   style="line-height: 1.4">
+					<span v-if="item.fullname != null && item.fullname.length > 0">{{ item.fullname }}</span>
+					<span v-else>{{ item.name }}</span>
+				</a>
+			</div>-->
+		</div>
+	</div>
+</template>
+
+<style scoped>
+
+</style>

+ 31 - 0
src/components/common/CopyToClipboard.vue

@@ -0,0 +1,31 @@
+<script setup>
+
+import copy from "copy-to-clipboard"
+import {inject} from "vue";
+
+const utils = inject("$utils")
+
+const props = defineProps({
+	"text": {
+		type: String,
+		default: ""
+	},
+	tooltip: {
+		type: String,
+		default: ""
+	}
+})
+
+const execCopy = function () {
+	copy(props.text)
+	utils.successToast("已复制到剪切板")
+}
+
+</script>
+
+<template>
+	<a href="" v-tooltip.bottom="tooltip ? tooltip : '点击拷贝到剪切板'" @click.prevent="execCopy"><i class="pi pi-copy small"></i></a>
+</template>
+
+<style scoped>
+</style>

+ 69 - 0
src/components/common/CountriesSelector.vue

@@ -0,0 +1,69 @@
+<script setup>
+import {ref} from "vue";
+import "@/lib/array.js"
+import useUtils from "@/utils/utils.js";
+import TLabel from "@/components/ui/TLabel.vue";
+import TDivider from "@/components/ui/TDivider.vue";
+import PlusButton from "@/components/common/PlusButton.vue";
+
+const props = defineProps({
+	"v-countries": Array
+})
+const emits = defineEmits(["update:modelValue"])
+
+let oldCountries = props.vCountries
+if (oldCountries == null) {
+	oldCountries = []
+}
+let oldCountryIds = oldCountries.$map(function (k, v) {
+	return v.id
+})
+
+const countries = ref(oldCountries)
+const countryIds = ref(oldCountryIds)
+
+const utils = useUtils()
+
+const add = function () {
+	let countryStringIds = countryIds.value.map(function (v) {
+		return v.toString()
+	})
+	utils.popup("/ui/selectCountriesPopup?countryIds=" + countryStringIds.join(","), {
+		width: "48em",
+		callback: function (resp) {
+			countries.value = resp.data.countries
+			change()
+		}
+	})
+}
+
+const remove = function (index) {
+	countries.value.$remove(index)
+	change()
+}
+
+const change = function () {
+	countryIds.value = countries.value.$map(function (k, v) {
+		return v.id
+	})
+	emits("update:modelValue", JSON.stringify(countryIds.value))
+}
+</script>
+
+<template>
+	<div>
+		<input type="hidden" name="countryIdsJSON" :value="JSON.stringify(countryIds)"/>
+		<div v-if="countries.length > 0" style="margin-bottom: 0.5em" class="flex flex-wrap gap-2">
+			<TLabel v-for="(country, index) in countries" outlined="">{{ country.name }}
+				<a href="" title="删除" @click.prevent="remove(index)"><i class="pi pi-remove"></i></a></TLabel>
+			<TDivider></TDivider>
+		</div>
+		<div>
+			<PlusButton size="small" type="button" @click.prevent="add"></PlusButton>
+		</div>
+	</div>
+</template>
+
+<style scoped>
+
+</style>

+ 222 - 0
src/components/common/DatetimeInput.vue

@@ -0,0 +1,222 @@
+<script setup>
+
+import useUtils from "@/utils/utils.js"
+import {onMounted, ref} from "vue"
+import TInputText from "@/components/ui/TInputText.vue";
+import TDatePicker from "@/components/ui/TDatePicker.vue";
+import "@/lib/date.js"
+
+const utils = useUtils()
+
+const props = defineProps(["v-name", "v-timestamp"])
+
+const leadingZero = function (s, l) {
+	s = s.toString()
+	if (l <= s.length) {
+		return s
+	}
+	for (let i = 0; i < l - s.length; i++) {
+		s = "0" + s
+	}
+	return s
+}
+
+const {timestamp, day, hour, minute, second, hasDayError, hasHourError, hasMinuteError, hasSecondError} = (function () {
+	let timestamp = props.vTimestamp
+	if (timestamp != null) {
+		timestamp = parseInt(timestamp)
+		if (isNaN(timestamp)) {
+			timestamp = 0
+		}
+	} else {
+		timestamp = 0
+	}
+
+	let day = ""
+	let hour = ""
+	let minute = ""
+	let second = ""
+
+	if (timestamp > 0) {
+		let date = new Date()
+		date.setTime(timestamp * 1000)
+
+		let year = date.getFullYear().toString()
+		let month = leadingZero((date.getMonth() + 1).toString(), 2)
+		day = year + "-" + month + "-" + leadingZero(date.getDate().toString(), 2)
+
+		hour = leadingZero(date.getHours().toString(), 2)
+		minute = leadingZero(date.getMinutes().toString(), 2)
+		second = leadingZero(date.getSeconds().toString(), 2)
+	}
+
+	return {
+		timestamp: ref(timestamp),
+		day: ref(day),
+		hour: ref(hour),
+		minute: ref(minute),
+		second: ref(second),
+
+		hasDayError: ref(false),
+		hasHourError: ref(false),
+		hasMinuteError: ref(false),
+		hasSecondError: ref(false)
+	}
+})()
+
+const change = function () {
+	// day
+	if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(day.value)) {
+		hasDayError.value = true
+		timestamp.value = 0
+		return
+	}
+	let pieces = day.value.split("-")
+	let year = parseInt(pieces[0])
+
+	let month = parseInt(pieces[1])
+	if (month < 1 || month > 12) {
+		hasDayError.value = true
+		return
+	}
+
+	let _day = parseInt(pieces[2])
+	if (_day < 1 || _day > 32) {
+		hasDayError.value = true
+		return
+	}
+
+	hasDayError.value = false
+
+	// hour
+	if (!/^\d+$/.test(hour.value)) {
+		hasHourError.value = true
+		return
+	}
+	let _hour = parseInt(hour.value)
+	if (isNaN(_hour)) {
+		hasHourError.value = true
+		return
+	}
+	if (_hour < 0 || _hour >= 24) {
+		hasHourError.value = true
+		return
+	}
+	hasHourError.value = false
+
+	// minute
+	if (!/^\d+$/.test(minute.value)) {
+		hasMinuteError.value = true
+		return
+	}
+	let _minute = parseInt(minute.value)
+	if (isNaN(_minute)) {
+		hasMinuteError.value = true
+		return
+	}
+	if (_minute < 0 || _minute >= 60) {
+		hasMinuteError.value = true
+		return
+	}
+	hasMinuteError.value = false
+
+	// second
+	if (!/^\d+$/.test(second.value)) {
+		hasSecondError.value = true
+		return
+	}
+	let _second = parseInt(second.value)
+	if (isNaN(_second)) {
+		hasSecondError.value = true
+		return
+	}
+	if (_second < 0 || _second >= 60) {
+		hasSecondError.value = true
+		return
+	}
+	hasSecondError.value = false
+
+	let date = new Date(year, month - 1, _day, _hour, _minute, _second)
+	timestamp.value = Math.floor(date.getTime() / 1000)
+}
+
+const resultTimestamp = function () {
+	return timestamp.value
+}
+
+const nextYear = function () {
+	let date = new Date()
+	date.setFullYear(date.getFullYear() + 1)
+	day.value = date.getFullYear() + "-" + leadingZero(date.getMonth() + 1, 2) + "-" + leadingZero(date.getDate(), 2)
+	hour.value = leadingZero(date.getHours(), 2)
+	minute.value = leadingZero(date.getMinutes(), 2)
+	second.value = leadingZero(date.getSeconds(), 2)
+	change()
+}
+
+const nextDays = function (days) {
+	let date = new Date()
+	date.setTime(date.getTime() + days * 86400 * 1000)
+	day.value = date.getFullYear() + "-" + leadingZero(date.getMonth() + 1, 2) + "-" + leadingZero(date.getDate(), 2)
+	hour.value = leadingZero(date.getHours(), 2)
+	minute.value = leadingZero(date.getMinutes(), 2)
+	second.value = leadingZero(date.getSeconds(), 2)
+	change()
+}
+
+const nextHours = function (hours) {
+	let date = new Date()
+	date.setTime(date.getTime() + hours * 3600 * 1000)
+	day.value = date.getFullYear() + "-" + leadingZero(date.getMonth() + 1, 2) + "-" + leadingZero(date.getDate(), 2)
+	hour.value = leadingZero(date.getHours(), 2)
+	minute.value = leadingZero(date.getMinutes(), 2)
+	second.value = leadingZero(date.getSeconds(), 2)
+	change()
+}
+
+defineExpose({
+	resultTimestamp
+})
+
+onMounted(() => {
+	if (timestamp.value < 0) {
+		day.value = new Date().format("Y-m-d")
+		hour.value = "23"
+		minute.value = "59"
+		second.value = "59"
+	}
+	change()
+})
+
+</script>
+
+<template>
+	<div>
+		<input type="hidden" :name="vName" :value="timestamp"/>
+		<div class="flex items-center gap-3" style="padding: 0; margin:0">
+			<div class="ui field" :class="{error: hasDayError}">
+				<TDatePicker type="text" v-model="day" placeholder="YYYY-MM-DD" style="width:8.2em" maxlength="10" @change="change"/>
+			</div>
+			<div class="ui field" :class="{error: hasHourError}">
+				<TInputText type="text" v-model="hour" maxlength="2" style="width:3.6em" placeholder="时" @input="change"/>
+			</div>
+			<div class="ui field">:</div>
+			<div class="ui field" :class="{error: hasMinuteError}">
+				<TInputText type="text" v-model="minute" maxlength="2" style="width:3.6em" placeholder="分" @input="change"/>
+			</div>
+			<div class="ui field">:</div>
+			<div class="ui field" :class="{error: hasSecondError}">
+				<TInputText type="text" v-model="second" maxlength="2" style="width:3.6em" placeholder="秒" @input="change"/>
+			</div>
+		</div>
+		<p class="comment">常用时间:<a href="" @click.prevent="nextHours(1)"> &nbsp;1小时&nbsp; </a>
+			<span class="disabled">|</span> <a href="" @click.prevent="nextDays(1)"> &nbsp;1天&nbsp; </a>
+			<span class="disabled">|</span> <a href="" @click.prevent="nextDays(3)"> &nbsp;3天&nbsp; </a>
+			<span class="disabled">|</span> <a href="" @click.prevent="nextDays(7)"> &nbsp;1周&nbsp; </a>
+			<span class="disabled">|</span> <a href="" @click.prevent="nextDays(30)"> &nbsp;30天&nbsp; </a>
+			<span class="disabled">|</span> <a href="" @click.prevent="nextYear()"> &nbsp;1年&nbsp; </a></p>
+	</div>
+</template>
+
+<style scoped>
+</style>

+ 11 - 0
src/components/common/DefinitionTable.vue

@@ -0,0 +1,11 @@
+<script setup>
+</script>
+
+<template>
+	<table class="ui table selectable definition">
+		<slot></slot>
+	</table>
+</template>
+
+<style scoped lang="postcss">
+</style>

+ 10 - 0
src/components/common/Dot.vue

@@ -0,0 +1,10 @@
+<script setup>
+</script>
+
+<template>
+	<span style="display: inline-block; padding-bottom: 3px"><i class="pi pi-circle-fill gray tiny"></i></span>
+</template>
+
+<style scoped lang="postcss">
+
+</style>

+ 56 - 0
src/components/common/DownloadLink.vue

@@ -0,0 +1,56 @@
+<script setup>
+import {inject, onMounted, ref} from "vue";
+
+const Tea = inject("$Tea")
+
+const props = defineProps(["v-element", "v-file", "v-value"])
+
+const composeURL = function () {
+	let text = ""
+	if (props.vValue != null) {
+		text = props.vValue
+	} else {
+		let e = document.getElementById(props.vElement)
+		if (e == null) {
+			// 不提示错误,因为此时可能页面未加载完整
+			return
+		}
+		text = e.innerText
+		if (text == null) {
+			text = e.textContent
+		}
+	}
+	return Tea.url("/ui/download", {
+		file: file.value,
+		text: text
+	})
+}
+
+const {file, url} = (function () {
+	let filename = props.vFile
+	if (filename == null || filename.length == 0) {
+		filename = "unknown-file"
+	}
+	return {
+		file: ref(filename),
+		url: ref(composeURL())
+	}
+})()
+
+onMounted(() => {
+	setTimeout(() => {
+		url.value = composeURL()
+	}, 1000)
+})
+
+</script>
+
+<template>
+	<a :href="url" target="_blank" style="font-weight: normal">
+		<slot></slot>
+	</a>
+</template>
+
+<style scoped>
+
+</style>

+ 48 - 0
src/components/common/FileTextarea.vue

@@ -0,0 +1,48 @@
+<script setup>
+import {ref} from "vue";
+import TTextarea from "@/components/ui/TTextarea.vue";
+
+const props = defineProps(["value"])
+const emits = defineEmits(["update:modelValue"])
+
+const {realValue} = (function () {
+	let value = props.value
+	if (typeof value != "string") {
+		value = ""
+	}
+	return {
+		realValue: ref(value)
+	}
+})()
+
+const textarea = ref()
+
+const dragover = function () {
+}
+
+const drop = function (e) {
+	e.dataTransfer.items[0].getAsFile().text().then(function (data) {
+		setValue(data)
+	})
+}
+const setValue = function (value) {
+	realValue.value = value
+	emits("update:modelValue", value)
+}
+
+defineExpose({
+	focus() {
+		textarea.value.$el.focus()
+	}
+})
+
+
+</script>
+
+<template>
+	<TTextarea @drop.prevent="drop" @dragover.prevent="dragover" ref="textarea" v-model="realValue" v-bind="$attrs"></TTextarea>
+</template>
+
+<style scoped>
+
+</style>

+ 20 - 0
src/components/common/FirstMenu.vue

@@ -0,0 +1,20 @@
+<script setup>
+import TDivider from "@/components/ui/TDivider.vue";
+
+const props = defineProps(["hideDivider"])
+</script>
+
+<template>
+	<div>
+		<div class="mt-1 flex flex-wrap gap-4 items-center">
+			<slot></slot>
+		</div>
+		<TDivider v-if="hideDivider !== true"></TDivider>
+	</div>
+</template>
+
+<style scoped>
+:deep(.active) {
+	color: var(--p-primary-color);
+}
+</style>

+ 23 - 0
src/components/common/GreyLabel.vue

@@ -0,0 +1,23 @@
+<script setup>
+// 灰色的Label
+import {ref} from "vue";
+import TLabel from "@/components/ui/TLabel.vue";
+
+const props = defineProps(["color"])
+let color = "gray"
+if (props.color != null && props.color.length > 0) {
+	color = props.color
+}
+
+const labelColor = ref(color)
+</script>
+
+<template>
+	<TLabel outlined="" severity="secondary" :class="labelColor" size="tiny">
+		<slot></slot>
+	</TLabel>
+</template>
+
+<style scoped>
+
+</style>

+ 370 - 0
src/components/common/HealthCheckConfigBox.vue

@@ -0,0 +1,370 @@
+<script setup>
+
+import "@/lib/array.js"
+import {inject, ref, watch} from "vue"
+import TSelect from "@/components/ui/TSelect.vue";
+import CodeLabel from "@/components/common/CodeLabel.vue";
+import TOption from "@/components/ui/TOption.vue";
+import TInputText from "@/components/ui/TInputText.vue";
+import TCheckboxBinary from "@/components/ui/TCheckboxBinary.vue";
+import TimeDurationBox from "@/components/common/TimeDurationBox.vue";
+import ValuesBox from "@/components/common/ValuesBox.vue";
+import TDivider from "@/components/ui/TDivider.vue";
+import MoreOptionsAngle from "@/components/common/MoreOptionsAngle.vue";
+import TLabel from "@/components/ui/TLabel.vue";
+import DefinitionTable from "@/components/common/DefinitionTable.vue";
+
+const Tea = inject("$Tea")
+
+const props = defineProps(["v-health-check-config", "v-check-domain-url", "v-is-plus"])
+
+const {
+	healthCheck,
+	advancedVisible,
+	urlProtocol,
+	urlHost,
+	urlPort,
+	urlRequestURI,
+	urlIsEditing,
+	hostErr
+} = (function () {
+	let healthCheckConfig = props.vHealthCheckConfig
+	let urlProtocol = "http"
+	let urlPort = ""
+	let urlRequestURI = "/"
+	let urlHost = ""
+
+	if (healthCheckConfig == null) {
+		healthCheckConfig = {
+			isOn: false,
+			url: "",
+			interval: {count: 60, unit: "second"},
+			statusCodes: [200],
+			timeout: {count: 10, unit: "second"},
+			countTries: 3,
+			tryDelay: {count: 100, unit: "ms"},
+			autoDown: true,
+			countUp: 1,
+			countDown: 3,
+			userAgent: "",
+			onlyBasicRequest: true,
+			accessLogIsOn: true
+		}
+		setTimeout(function () {
+			changeURL()
+		}, 500)
+	} else {
+		try {
+			let url = new URL(healthCheckConfig.url)
+			urlProtocol = url.protocol.substring(0, url.protocol.length - 1)
+
+			// 域名
+			urlHost = url.host
+			if (urlHost == "%24%7Bhost%7D") {
+				urlHost = "${host}"
+			}
+			let colonIndex = urlHost.indexOf(":")
+			if (colonIndex > 0) {
+				urlHost = urlHost.substring(0, colonIndex)
+			}
+
+			urlPort = url.port
+			urlRequestURI = url.pathname
+			if (url.search.length > 0) {
+				urlRequestURI += url.search
+			}
+		} catch (e) {
+		}
+
+		if (healthCheckConfig.statusCodes == null) {
+			healthCheckConfig.statusCodes = [200]
+		}
+		if (healthCheckConfig.interval == null) {
+			healthCheckConfig.interval = {count: 60, unit: "second"}
+		}
+		if (healthCheckConfig.timeout == null) {
+			healthCheckConfig.timeout = {count: 10, unit: "second"}
+		}
+		if (healthCheckConfig.tryDelay == null) {
+			healthCheckConfig.tryDelay = {count: 100, unit: "ms"}
+		}
+		if (healthCheckConfig.countUp == null || healthCheckConfig.countUp < 1) {
+			healthCheckConfig.countUp = 1
+		}
+		if (healthCheckConfig.countDown == null || healthCheckConfig.countDown < 1) {
+			healthCheckConfig.countDown = 3
+		}
+	}
+
+	return {
+		healthCheck: ref(healthCheckConfig),
+		advancedVisible: ref(false),
+		urlProtocol: ref(urlProtocol),
+		urlHost: ref(urlHost),
+		urlPort: ref(urlPort),
+		urlRequestURI: ref(urlRequestURI),
+		urlIsEditing: ref(healthCheckConfig.url.length == 0),
+
+		hostErr: ref("")
+	}
+})()
+
+const showAdvanced = function () {
+	advancedVisible.value = !advancedVisible.value
+}
+
+const changeURL = function () {
+	let _urlHost = urlHost.value
+	if (_urlHost.length == 0) {
+		_urlHost = "${host}"
+	}
+	healthCheck.value.url = urlProtocol.value + "://" + _urlHost + ((urlPort.value.length > 0) ? ":" + urlPort.value : "") + urlRequestURI.value
+}
+
+const changeStatus = function (values) {
+	healthCheck.value.statusCodes = values.$map(function (k, v) {
+		let status = parseInt(v)
+		if (isNaN(status)) {
+			return 0
+		} else {
+			return status
+		}
+	})
+}
+
+const onChangeURLHost = function () {
+	let checkDomainURL = props.vCheckDomainUrl
+	if (checkDomainURL == null || checkDomainURL.length == 0) {
+		return
+	}
+
+	Tea.action(checkDomainURL)
+		.params({host: urlHost.value})
+		.success(function (resp) {
+			if (!resp.data.isOk) {
+				hostErr.value = "在当前集群中找不到此域名,可能会影响健康检查结果。"
+			} else {
+				hostErr.value = ""
+			}
+		})
+		.post()
+}
+
+const editURL = function () {
+	urlIsEditing.value = !urlIsEditing.value
+}
+
+watch(urlRequestURI, function () {
+	if (urlRequestURI.value.length > 0 && urlRequestURI.value[0] != "/") {
+		urlRequestURI.value = "/" + urlRequestURI.value
+	}
+	changeURL()
+})
+
+watch(urlPort, function (v) {
+	let port = parseInt(v)
+	if (!isNaN(port)) {
+		urlPort.value = port.toString()
+	} else {
+		urlPort.value = ""
+	}
+	changeURL()
+})
+
+watch(urlProtocol, function () {
+	changeURL()
+})
+
+watch(urlHost, function () {
+	changeURL()
+	hostErr.value = ""
+})
+
+watch(() => healthCheck.value.countTries, function (v) {
+	let count = parseInt(v)
+	if (!isNaN(count)) {
+		healthCheck.value.countTries = count
+	} else {
+		healthCheck.value.countTries = 0
+	}
+})
+
+watch(() => healthCheck.value.countUp, function (v) {
+	let count = parseInt(v)
+	if (!isNaN(count)) {
+		healthCheck.value.countUp = count
+	} else {
+		healthCheck.value.countUp = 0
+	}
+})
+
+watch(() => healthCheck.value.countDown, function (v) {
+	let count = parseInt(v)
+	if (!isNaN(count)) {
+		healthCheck.value.countDown = count
+	} else {
+		healthCheck.value.countDown = 0
+	}
+})
+
+
+</script>
+
+<template>
+	<div>
+		<input type="hidden" name="healthCheckJSON" :value="JSON.stringify(healthCheck)"/>
+		<DefinitionTable>
+			<tbody>
+			<tr>
+				<td class="title">启用健康检查</td>
+				<td>
+					<TCheckboxBinary value="1" v-model="healthCheck.isOn"/>
+					<p class="comment">通过访问节点上的网站URL来确定节点是否健康。</p>
+				</td>
+			</tr>
+			</tbody>
+			<tbody v-show="healthCheck.isOn">
+			<tr>
+				<td>检测URL *</td>
+				<td>
+					<div v-if="healthCheck.url.length > 0">
+						<TLabel outlined="">{{ healthCheck.url }}</TLabel> &nbsp;
+						<a href="" @click.prevent="editURL"><span class="small">修改 <i class="pi" :class="{'pi-angle-down': !urlIsEditing, 'pi-angle-up': urlIsEditing}"></i></span></a>
+					</div>
+					<div v-show="urlIsEditing" class="mt-2">
+						<table class="ui table definition">
+							<tr>
+								<td class="title">协议</td>
+								<td>
+									<TSelect v-model="urlProtocol">
+										<TOption value="http">http://</TOption>
+										<TOption value="https">https://</TOption>
+									</TSelect>
+								</td>
+							</tr>
+							<tr>
+								<td>域名</td>
+								<td>
+									<TInputText type="text" v-model="urlHost" @change="onChangeURLHost" class="w-full"/>
+									<p class="comment"><span v-if="hostErr.length > 0" class="red">{{ hostErr }}</span>已经部署到当前集群的一个域名;如果为空则使用节点IP作为域名。<span class="red" v-if="urlProtocol == 'https' && urlHost.length == 0">如果协议是https,这里必须填写一个已经设置了SSL证书的域名。</span>
+									</p>
+								</td>
+							</tr>
+							<tr>
+								<td>端口</td>
+								<td>
+									<TInputText type="text" maxlength="5" style="width:5.4em" placeholder="端口" v-model="urlPort"/>
+									<p class="comment">域名或者IP的端口,可选项,默认为80/443。</p>
+								</td>
+							</tr>
+							<tr>
+								<td>RequestURI</td>
+								<td>
+									<TInputText type="text" v-model="urlRequestURI" placeholder="/" class="w-full"/>
+									<p class="comment">请求的路径,可以带参数,可选项。</p>
+								</td>
+							</tr>
+						</table>
+						<TDivider></TDivider>
+						<p class="comment" v-if="healthCheck.url.length > 0">拼接后的检测URL:
+							<CodeLabel>{{ healthCheck.url }}</CodeLabel>
+							,其中${host}指的是域名。
+						</p>
+					</div>
+				</td>
+			</tr>
+			<tr>
+				<td>检测时间间隔</td>
+				<td>
+					<TimeDurationBox :v-value="healthCheck.interval"></TimeDurationBox>
+					<p class="comment">两次检查之间的间隔。</p>
+				</td>
+			</tr>
+			<tr>
+				<td>自动上/下线<span v-if="vIsPlus">IP</span></td>
+				<td>
+					<TCheckboxBinary value="1" v-model="healthCheck.autoDown"/>
+					<p class="comment">选中后系统会根据健康检查的结果自动标记<span v-if="vIsPlus">节点IP</span><span v-else>节点</span>的上线/下线状态,并可能自动同步DNS设置。<span v-if="!vIsPlus">注意:免费版的只能整体上下线整个节点,商业版的可以下线单个IP。</span>
+					</p>
+				</td>
+			</tr>
+			<tr v-show="healthCheck.autoDown">
+				<td>连续上线次数</td>
+				<td>
+					<TInputText type="text" v-model="healthCheck.countUp" style="width:5em" maxlength="6"/>
+					<p class="comment">连续{{ healthCheck.countUp }}次检查成功后自动恢复上线。</p>
+				</td>
+			</tr>
+			<tr v-show="healthCheck.autoDown">
+				<td>连续下线次数</td>
+				<td>
+					<TInputText type="text" v-model="healthCheck.countDown" style="width:5em" maxlength="6"/>
+					<p class="comment">连续{{ healthCheck.countDown }}次检查失败后自动下线。</p>
+				</td>
+			</tr>
+			</tbody>
+			<tbody v-show="healthCheck.isOn">
+			<tr>
+				<td colspan="2">
+					<MoreOptionsAngle @change="showAdvanced"></MoreOptionsAngle>
+				</td>
+			</tr>
+			</tbody>
+			<tbody v-show="advancedVisible && healthCheck.isOn">
+			<tr>
+				<td>允许的状态码</td>
+				<td>
+					<ValuesBox :values="healthCheck.statusCodes" maxlength="3" @change="changeStatus"></ValuesBox>
+					<p class="comment">允许检测URL返回的状态码列表。</p>
+				</td>
+			</tr>
+			<tr>
+				<td>超时时间</td>
+				<td>
+					<TimeDurationBox :v-value="healthCheck.timeout"></TimeDurationBox>
+					<p class="comment">读取检测URL超时时间。</p>
+				</td>
+			</tr>
+			<tr>
+				<td>连续尝试次数</td>
+				<td>
+					<TInputText type="text" v-model="healthCheck.countTries" style="width: 5em" maxlength="2"/>
+					<p class="comment">如果读取检测URL失败后需要再次尝试的次数。</p>
+				</td>
+			</tr>
+			<tr>
+				<td>每次尝试间隔</td>
+				<td>
+					<TimeDurationBox :v-value="healthCheck.tryDelay"></TimeDurationBox>
+					<p class="comment">如果读取检测URL失败后再次尝试时的间隔时间。</p>
+				</td>
+			</tr>
+			<tr>
+				<td>终端信息<em>(User-Agent)</em></td>
+				<td>
+					<TInputText type="text" v-model="healthCheck.userAgent" maxlength="200" class="w-full"/>
+					<p class="comment">发送到服务器的User-Agent值,不填写表示使用默认值。</p>
+				</td>
+			</tr>
+			<tr>
+				<td>只基础请求</td>
+				<td>
+					<TCheckboxBinary v-model="healthCheck.onlyBasicRequest"></TCheckboxBinary>
+					<p class="comment">只做基础的请求,不处理反向代理(不检查源站)、WAF等。</p>
+				</td>
+			</tr>
+			<tr>
+				<td>记录访问日志</td>
+				<td>
+					<TCheckboxBinary v-model="healthCheck.accessLogIsOn"></TCheckboxBinary>
+					<p class="comment">记录健康检查的访问日志。</p>
+				</td>
+			</tr>
+			</tbody>
+		</DefinitionTable>
+		<div class="margin"></div>
+	</div>
+</template>
+
+<style scoped>
+</style>

+ 48 - 0
src/components/common/JSPage.vue

@@ -0,0 +1,48 @@
+<script setup>
+
+import {ref} from "vue"
+
+const props = defineProps(["v-max"])
+
+const emits = defineEmits(["change"])
+
+const {max, page} = (function () {
+	let max = props.vMax
+	if (max == null) {
+		max = 0
+	}
+	return {
+		max: ref(max),
+		page: ref(1)
+	}
+})()
+
+const updateMax = function (maxValue) {
+	max.value = maxValue
+}
+
+const selectPage = function (pageValue) {
+	page.value = pageValue
+	emits("change", page.value)
+}
+
+defineExpose({
+	updateMax,
+	selectPage
+})
+
+</script>
+
+<template>
+	<div>
+		<div class="flex flex-wrap gap-2 items-center my-2" v-if="max > 1">
+			<a href="" v-for="i in max" :class="{active: i == page}" @click.prevent="selectPage(i)">{{ i }}</a>
+		</div>
+	</div>
+</template>
+
+<style scoped>
+a:not(.active) {
+	color: var(--p-text-color) !important;
+}
+</style>

+ 67 - 0
src/components/common/Keyword.vue

@@ -0,0 +1,67 @@
+<script setup>
+import {onMounted, ref} from "vue";
+
+const props = defineProps(["v-word"])
+const textRef = ref()
+const textValue = ref("")
+
+onMounted(() => {
+	let word = props.vWord
+	if (word == null) {
+		word = ""
+	} else {
+		word = word.replace(/\)/g, "\\)")
+		word = word.replace(/\(/g, "\\(")
+		word = word.replace(/\+/g, "\\+")
+		word = word.replace(/\^/g, "\\^")
+		word = word.replace(/\$/g, "\\$")
+		word = word.replace(/\?/g, "\\?")
+		word = word.replace(/\*/g, "\\*")
+		word = word.replace(/\[/g, "\\[")
+		word = word.replace(/{/g, "\\{")
+		word = word.replace(/\./g, "\\.")
+	}
+
+	let text = textRef.value.textContent
+	if (typeof text !== "string") {
+		return
+	}
+	if (word.length > 0) {
+		let m = []  // replacement => tmp
+		let tmpIndex = 0
+		text = text.replaceAll(new RegExp("(" + word + ")", "ig"), function (replacement) {
+			tmpIndex++
+			let s = "<span style=\"border: 1px #ccc dashed; color: #ef4d58\">" + encodeHTML(replacement) + "</span>"
+			let tmpKey = "$TMP__KEY__" + tmpIndex.toString() + "$"
+			m.push([tmpKey, s])
+			return tmpKey
+		})
+		text = encodeHTML(text)
+
+		m.forEach(function (r) {
+			text = text.replace(r[0], r[1])
+		})
+
+	} else {
+		text = encodeHTML(text)
+	}
+
+	textValue.value = text
+})
+
+const encodeHTML = function (s) {
+	s = s.replace(/&/g, "&amp;")
+	s = s.replace(/</g, "&lt;")
+	s = s.replace(/>/g, "&gt;")
+	s = s.replace(/"/g, "&quot;")
+	return s
+}
+</script>
+
+<template>
+	<span><span style="display: none" ref="textRef"><slot></slot></span><span v-html="textValue"></span></span>
+</template>
+
+<style scoped>
+
+</style>

+ 14 - 0
src/components/common/LabelOn.vue

@@ -0,0 +1,14 @@
+<script setup>
+const props = defineProps({
+	"vIsOn": Boolean
+})
+
+</script>
+
+<template>
+	<div><span v-if="vIsOn" class="green">已启用</span><span v-if="!vIsOn" class="red">已停用</span></div>
+</template>
+
+<style scoped>
+
+</style>

+ 26 - 0
src/components/common/LinkIcon.vue

@@ -0,0 +1,26 @@
+<script setup>
+// 使用Icon的链接方式
+
+const props = defineProps(["href", "title", "target", "size"])
+
+const {vTitle, realSize} = (function () {
+	let realSize = props.size
+	if (realSize == null || realSize.length == 0) {
+		realSize = "small"
+	}
+
+	return {
+		vTitle: (props.title == null) ? "点击打开链接" : props.title,
+		realSize: realSize
+	}
+})()
+
+</script>
+
+<template>
+	<span><slot></slot>&nbsp;<a :href="href" v-tooltip.bottom="vTitle" class="link grey" :target="target"><i class="pi pi-link" :class="realSize"></i></a></span>
+</template>
+
+<style scoped>
+
+</style>

+ 15 - 0
src/components/common/LinkPopup.vue

@@ -0,0 +1,15 @@
+<script setup>
+// 会弹出窗口的链接
+const props = defineProps(["title"])
+
+</script>
+
+<template>
+	<a href="" :title="title">
+		<slot></slot>
+	</a>
+</template>
+
+<style scoped>
+
+</style>

+ 35 - 0
src/components/common/LinkRed.vue

@@ -0,0 +1,35 @@
+<script setup>
+// 带有下划虚线的链接
+
+import {ref} from "vue";
+
+const props = defineProps(["href", "title"])
+const emits = defineEmits(["click.prevent"])
+const {vHref} = (function () {
+	let href = props.href
+	if (href == null) {
+		href = ""
+	}
+	return {
+		vHref: ref(href)
+	}
+})()
+
+const clickPrevent = function (...allArgs) {
+	emits("click.prevent", ...allArgs)
+
+	if (vHref.value.length > 0) {
+		// TODO 在弹窗中就直接弹出新的弹窗
+		window.location = vHref.value
+	}
+}
+
+</script>
+
+<template>
+	<a :href="vHref" :title="title" style="border-bottom: 1px #db2828 dashed" @click.prevent="clickPrevent"><span class="red"><slot></slot></span></a>
+</template>
+
+<style scoped>
+
+</style>

+ 98 - 0
src/components/common/ListTable.vue

@@ -0,0 +1,98 @@
+<script setup>
+import TDataTable from "@/components/ui/TDataTable.vue";
+import {onMounted, onUnmounted, ref} from "vue";
+
+const emits = defineEmits(["sort"])
+const props = defineProps(["items", "sortId"])
+
+const onRowReorder = e => {
+	if (e == null || !(e.value instanceof Array)) {
+		return
+	}
+
+	let idField = "id"
+	if (typeof props.sortId === "string" && props.sortId.length > 0) {
+		idField = props.sortId
+	}
+	const ids = e.value.map(v => v[idField])
+	emits("sort", ids)
+
+	props.items.length = 0
+	props.items.push(...e.value)
+}
+
+const rootRef = ref()
+let container
+const scrollListener = () => {
+	updateColumnClass()
+}
+
+onMounted(() => {
+	container = rootRef.value.$el.querySelector(".p-datatable-table-container")
+	container.addEventListener("scroll", scrollListener)
+	setTimeout(() => updateColumnClass())
+
+	window.addEventListener("resize", scrollListener)
+})
+
+onUnmounted(() => {
+	container.removeEventListener("scroll", scrollListener)
+	window.removeEventListener("resize", scrollListener)
+})
+
+const updateColumnClass = () => {
+	container.querySelectorAll(".p-datatable-frozen-column").forEach(columnEl => {
+		columnEl.className = columnEl.className.replace(/(left|right)/, "").trim()
+		const cssText = columnEl.style.cssText
+
+		if (cssText.indexOf("left") >= 0) {
+			if (container.scrollLeft > 0) {
+				columnEl.className += " left"
+			}
+		} else {
+			if (container.scrollWidth > container.offsetWidth && container.scrollLeft + container.offsetWidth < container.scrollWidth - 2) {
+				columnEl.className += " right"
+			}
+		}
+	})
+}
+
+</script>
+
+<template>
+	<TDataTable dataKey="id" :items="items" tableClass="ui table selectable celled" scrollable @row-reorder="onRowReorder" ref="rootRef">
+		<slot></slot>
+	</TDataTable>
+</template>
+
+<style lang="postcss" scoped>
+:deep(.p-datatable-frozen-column.right) {
+	box-shadow: -4px 0 8px -1px rgb(0 0 0 / 0.1);
+	border-right: 1px solid rgba(34, 36, 38, 0.15) !important;
+
+}
+
+:deep(.p-datatable-frozen-column.left) {
+	box-shadow: 4px 0 8px -1px rgb(0 0 0 / 0.1);
+	border-left: 1px solid rgba(34, 36, 38, 0.15) !important;
+	border-right: 1px solid rgba(34, 36, 38, 0.15) !important;
+}
+
+:deep(.p-datatable-table-container) {
+	scrollbar-width: auto;
+}
+
+:deep(.p-datatable-tbody > tr > td) {
+	border-color: inherit;
+	border-bottom: 0;
+}
+
+:deep(tr:hover td) {
+	@apply !bg-gray-50 dark:!bg-gray-700;
+}
+
+:deep(th.p-datatable-frozen-column.right), :deep(th.p-datatable-frozen-column.left) {
+	background: var(--p-datatable-header-cell-background) !important;
+}
+
+</style>

+ 17 - 0
src/components/common/LoadingMessage.vue

@@ -0,0 +1,17 @@
+<script setup>
+import TMessage from "@/components/ui/TMessage.vue";
+import TLoading from "@/components/ui/TLoading.vue";
+</script>
+
+<template>
+	<TMessage>
+		<div class="flex items-center gap-2">
+			<TLoading style="width: 1.4em; height: 1.4em"></TLoading>
+			<slot></slot>
+		</div>
+	</TMessage>
+</template>
+
+<style scoped lang="postcss">
+
+</style>

+ 10 - 0
src/components/common/MaskWarning.vue

@@ -0,0 +1,10 @@
+<script setup>
+</script>
+
+<template>
+	<span class="red">为了安全起见,此项数据保存后将不允许在界面查看完整明文,为避免忘记,请自行记录原始数据。</span>
+</template>
+
+<style scoped>
+
+</style>

+ 61 - 0
src/components/common/MenuItem.vue

@@ -0,0 +1,61 @@
+<script setup>
+import {inject, ref} from "vue";
+
+const Tea = inject("$Tea")
+
+const props = defineProps(["href", "active", "code"])
+const emits = defineEmits(["click"])
+const popupState = inject("$popupState", null)
+
+const click = (e) => {
+	emits("click", e)
+}
+
+let active = props.active
+if (typeof (active) == "undefined") {
+	var itemCode = ""
+
+	if (popupState != null) {
+		if (typeof popupState.ctx.firstMenuItem === "string") {
+			itemCode = popupState.ctx.firstMenuItem
+		}
+	} else {
+		if (typeof (window.X_VIEW_CTX.firstMenuItem) === "string") {
+			itemCode = window.X_VIEW_CTX.firstMenuItem
+		}
+	}
+	if (itemCode != null && itemCode.length > 0 && props.code != null && props.code.length > 0) {
+		if (itemCode.indexOf(",") > 0) {
+			active = itemCode.split(",").$contains(props.code)
+		} else {
+			active = (itemCode == props.code)
+		}
+	}
+}
+
+let href = (props.href == null) ? "" : props.href
+if (typeof (href) == "string" && href.length > 0 && href.startsWith(".")) {
+	let qIndex = href.indexOf("?")
+	if (qIndex >= 0) {
+		href = Tea.url(href.substring(0, qIndex)) + href.substring(qIndex)
+	} else {
+		href = Tea.url(href)
+	}
+}
+
+const vHref = ref(href)
+const vActive = ref(active)
+
+</script>
+
+<template>
+	<a :href="vHref" class="flex flex-row items-center" :class="{active:vActive}" @click="click">
+		<slot></slot>
+	</a>
+</template>
+
+<style scoped lang="postcss">
+a {
+	color: var(--p-text-muted-color);
+}
+</style>

+ 63 - 0
src/components/common/MoreItems.vue

@@ -0,0 +1,63 @@
+<script>
+export default {
+	inheritAttrs: false,
+}
+</script>
+<script setup>
+import {onMounted, ref} from "vue";
+import TLabel from "@/components/ui/TLabel.vue";
+
+const rootRef = ref()
+const count = ref(0)
+
+const visible = ref(false)
+const toggle = () => {
+	visible.value = !visible.value
+}
+
+onMounted(() => {
+	let countNodes = 0
+	rootRef.value.childNodes.forEach(node => {
+		if (node.tagName != null) {
+			countNodes ++
+		}
+	})
+	count.value = countNodes
+})
+</script>
+
+<template>
+	<div class="items" :class="{'show-more' : visible, 'p-4 border dark:border-gray-700 bg-gray-50 dark:bg-gray-700 rounded-xl absolute': visible}" v-bind="$attrs" ref="rootRef">
+		<slot></slot>
+		<a class="more-link" v-if="count > 0" href="" @click.prevent="toggle">
+			<TLabel class="outlined">
+				<span v-if="!visible">更多({{ count - 1}}) <i class="pi pi-angle-double-right"></i></span>
+				<span v-if="visible"><i class="pi pi-angle-double-left"></i> 收起</span>
+			</TLabel>
+		</a>
+	</div>
+</template>
+
+<style scoped lang="postcss">
+.items:not(.show-more) > *:not(.more-link) {
+	@apply hidden;
+}
+
+.items > *:not(.more-link):first-child {
+	@apply flex;
+}
+
+.more-link {
+	.p-chip {
+		@apply border-dashed;
+	}
+
+	span {
+		@apply flex items-center gap-1 text-gray-500 dark:text-gray-200;
+
+		i {
+			@apply text-sm text-gray-400 dark:text-gray-200;
+		}
+	}
+}
+</style>

+ 26 - 0
src/components/common/MoreOptionsAngle.vue

@@ -0,0 +1,26 @@
+<script setup>
+
+import {ref} from "vue"
+
+const emits = defineEmits(["change"])
+
+const {isVisible} = (function () {
+	return {
+		isVisible: ref(false)
+	}
+})()
+
+const show = function () {
+	isVisible.value = !isVisible.value
+	emits("change", isVisible.value)
+}
+
+
+</script>
+
+<template>
+	<a href="" @click.prevent="show()"><span v-if="!isVisible">更多选项</span><span v-if="isVisible">收起选项</span><i class="pi" :class="{'pi-angle-down':!isVisible, 'pi-angle-up':isVisible}"></i></a>
+</template>
+
+<style scoped>
+</style>

+ 31 - 0
src/components/common/MoreOptionsIndicator.vue

@@ -0,0 +1,31 @@
+<script setup>
+
+import {inject, ref} from "vue";
+
+const popupState = inject("$popupState", null)
+const emits = defineEmits(["change", "update:modelValue"])
+const visible = ref(false)
+
+const changeVisible = function () {
+	visible.value = !visible.value
+
+	if (popupState != null) {
+		popupState.ctx.moreOptionsVisible = visible.value
+	} else {
+		window.X_VIEW_CTX.moreOptionsVisible = visible.value
+	}
+	emits("change", visible.value)
+	emits("update:modelValue", visible.value)
+}
+
+</script>
+
+<template>
+	<a href="" style="font-weight: normal" @click.prevent="changeVisible()" class="flex items-center gap-1">
+		<slot><span v-if="!visible">更多选项</span><span v-if="visible">收起选项</span></slot>
+		<i class="pi" :class="{'pi-angle-down':!visible, 'pi-angle-up':visible}"></i> </a>
+</template>
+
+<style scoped>
+
+</style>

+ 32 - 0
src/components/common/MoreOptionsTbody.vue

@@ -0,0 +1,32 @@
+<script setup>
+
+import {ref} from "vue"
+
+const emits = defineEmits(["change"])
+
+const {isVisible} = (function () {
+	return {
+		isVisible: ref(false)
+	}
+})()
+
+const show = function () {
+	isVisible.value = !isVisible.value
+	emits("change", isVisible.value)
+}
+
+
+</script>
+
+<template>
+	<tbody>
+	<tr>
+		<td colspan="2">
+			<a href="" @click.prevent="show()"><span v-if="!isVisible">更多选项</span><span v-if="isVisible">收起选项</span><i class="pi" :class="{'pi-angle-down':!isVisible, 'pi-angle-up':isVisible}"></i></a>
+		</td>
+	</tr>
+	</tbody>
+</template>
+
+<style scoped>
+</style>

+ 57 - 0
src/components/common/NavButton.vue

@@ -0,0 +1,57 @@
+<script setup>
+import TButton from "@/components/ui/TButton.vue";
+
+const props = defineProps({
+	primary: {
+		type: Boolean,
+		default: false
+	},
+	actionCreate: {
+		type: Boolean,
+		default: false
+	},
+	actionReload: {
+		type: Boolean,
+		default: false
+	},
+	actionDelete: {
+		type: Boolean,
+		default: false
+	},
+	actionStop: {
+		type: Boolean,
+		default: false
+	},
+	actionPlay: {
+		type: Boolean,
+		default: false
+	},
+	danger: {
+		type: Boolean,
+		default: false
+	},
+	small: {
+		type: Boolean,
+		default: false
+	},
+	icon: {
+		type: String
+	}
+})
+</script>
+
+<template>
+	<TButton :outlined="!primary" :danger="danger" :size="small ? 'small' : null">
+		<i v-if="actionCreate" class="pi pi-pen-to-square" :class="{small: small}"></i>
+		<i v-if="actionReload" class="pi pi-refresh" :class="{small: small}"></i>
+		<i v-if="actionDelete" class="pi pi-trash" :class="{small: small}"></i>
+		<i v-if="actionStop" class="pi pi-stop" :class="{small: small}"></i>
+		<i v-if="actionPlay" class="pi pi-play" :class="{small: small}"></i>
+		<i v-if="icon" :class="{[icon]:true, small: small}"></i>
+		<slot></slot>
+	</TButton>
+</template>
+
+<style scoped lang="postcss">
+
+</style>

+ 14 - 0
src/components/common/NavButtons.vue

@@ -0,0 +1,14 @@
+<script setup>
+import TDivider from "@/components/ui/TDivider.vue";
+</script>
+
+<template>
+	<div>
+		<div class="flex gap-2 items-center"><slot></slot></div>
+		<TDivider></TDivider>
+	</div>
+</template>
+
+<style scoped lang="postcss">
+
+</style>

+ 203 - 0
src/components/common/NavTab.vue

@@ -0,0 +1,203 @@
+<script setup>
+import TTab from "@/components/ui/TTab.vue";
+import {inject, onBeforeMount, onMounted, ref} from "vue";
+
+const Tea = inject("$Tea")
+const props = defineProps({
+	value: {
+		type: String,
+		default: ""
+	},
+	href: {
+		type: String,
+		default: ""
+	},
+	target: {
+		type: String,
+		default: ""
+	},
+	name: {
+		type: String,
+		default: ""
+	},
+	icon: {
+		type: String,
+		default: ""
+	},
+	separator: {
+		type: Boolean,
+		default: false
+	},
+	disabled: {
+		type: Boolean,
+		default: false
+	},
+	back: {
+		type: Boolean,
+		default: false
+	},
+	static: {
+		type: Boolean,
+		default: false
+	},
+	externalLink: {
+		type: Boolean,
+		default: false
+	},
+	active: {
+		type: Boolean,
+		default: false
+	},
+
+	// 是否为概览
+	overview: {
+		type: Boolean,
+		default: false
+	},
+
+	// 特殊动作
+	actionCreate: {
+		type: Boolean,
+		default: false
+	},
+	actionCreateLink: {
+		type: Boolean,
+		default: false
+	},
+	actionUpload: {
+		type: Boolean,
+		default: false
+	},
+	actionSetting: {
+		type: Boolean,
+		default: false
+	},
+	actionCart: {
+		type: Boolean,
+		default: false
+	}
+})
+const realHref = ref()
+
+const navbar = inject("$navbar")
+
+const isStaticTab = () => {
+	return props.static || props.separator || props.disabled || props.actionCreate
+}
+
+const clickTab = (e) => {
+	if (props.externalLink) {
+		setTimeout(() => {
+			navbar.exposed.goPrevTab()
+		})
+		return
+	}
+
+	if (e.target.tagName == "A") {
+		const href = e.target.getAttribute("href")
+		if (href != null && href.length > 0 && href != "#") {
+			const targetValue = e.target.getAttribute("target")
+			if (targetValue == "_blank") {
+				setTimeout(() => {
+					navbar.exposed.goPrevTab()
+				})
+				return
+			}
+			return
+		}
+	}
+
+	if (isStaticTab()) {
+		setTimeout(() => {
+			navbar.exposed.goPrevTab()
+		})
+		return
+	}
+}
+
+const convertHref = () => {
+	let href = (props.href == null) ? "" : props.href
+	if (typeof (href) == "string" && href.length > 0 && !href.match(/^(http|https|ftp):\/\//i)) {
+		let qIndex = href.indexOf("?")
+		if (qIndex >= 0) {
+			href = Tea.url(href.substring(0, qIndex).replace(/\/+$/, "")) + href.substring(qIndex)
+		} else {
+			href = Tea.url(href.replace(/\/+$/, ""))
+		}
+	}
+	return href
+}
+
+const calculateTabValue = () => {
+	if (props.back) {
+		return "TAB_BACK"
+	}
+	if (props.separator) {
+		return "TAB_SEPARATOR"
+	}
+
+	if (props.value == null || props.value.length == 0) {
+		if (props.name != null && props.name.length > 0) {
+			return props.name
+		}
+
+		if (props.href != null && props.href.length > 0) {
+			return props.href
+		}
+
+		return "TAB_UNDEFINED"
+	}
+
+	return props.value
+}
+const tabValue = ref(calculateTabValue())
+
+const calculateTarget = () => {
+	if (props.target && props.target.length > 0) {
+		return props.target
+	}
+
+	if (props.externalLink) {
+		return "_blank"
+	}
+
+	return undefined
+}
+
+onBeforeMount(() => {
+	realHref.value = convertHref()
+})
+
+onMounted(() => {
+	if (props.active) {
+		navbar.exposed.goTab(calculateTabValue())
+	}
+})
+
+</script>
+
+<template>
+	<TTab :disabled="separator || disabled" :value="tabValue" @click="clickTab($event)" :as="(isStaticTab()) ? 'SPAN' : 'A'" :href="realHref" :target="calculateTarget()" class="flex">
+		<span v-if="!separator" class="flex items-center gap-2">
+			<span v-if="back"><i class="pi pi-arrow-left"></i></span>
+			<i v-if="icon.length > 0" :class="icon"></i>
+			<i v-if="actionCreate || actionCreateLink" class="pi pi-pen-to-square"></i>
+			<i v-if="actionUpload" class="pi pi-upload"></i>
+			<i v-if="actionSetting" class="pi pi-cog"></i>
+			<i v-if="actionCart" class="pi pi-shopping-cart"></i>
+			<i class="pi pi-external-link" v-if="externalLink"></i>
+			<span>
+				<span v-if="name.length > 0">{{ name }}</span>
+				<span v-else-if="overview">概览</span>
+			</span>
+			<span v-if="$slots.name"><slot name="name"></slot></span>
+		</span>
+		<span v-if="separator">|</span>
+	</TTab>
+</template>
+
+<style scoped lang="postcss">
+.p-tab.p-disabled {
+	@apply px-2;
+}
+</style>

+ 167 - 0
src/components/common/Navbar.vue

@@ -0,0 +1,167 @@
+<script setup>
+import TTabView from "@/components/ui/TTabView.vue";
+import TTabs from "@/components/ui/TTabs.vue";
+import {getCurrentInstance, provide, ref, watch} from "vue";
+import TBreadcrumb from "@/components/ui/TBreadcrumb.vue";
+
+const props = defineProps({
+	activeTab: {
+		type: String,
+		default: ""
+	},
+	disableSpacing: {
+		type: Boolean,
+		default: false
+	},
+	contentName: {
+		type: String,
+		default: ""
+	},
+	parentItems: {
+		type: Array,
+		default: []
+	},
+	secondary: {
+		type: Boolean,
+		default: false
+	}
+})
+const activeTabRef = ref(props.activeTab)
+watch(() => props.activeTab, () => {
+	activeTabRef.value = props.activeTab
+})
+
+let tabHistory = [props.activeTab]
+const changeTab = tabValue => {
+	tabHistory.push(tabValue.value)
+}
+
+// get refer information
+let referURL = ""
+let referName = ""
+if (window.location.search.length > 0) {
+	window.location.search.substring(1).split("&").forEach(param => {
+		const index = param.indexOf("=")
+		if (index > 0) {
+			switch (param.substring(0, index)) {
+				case "REFER":
+					referURL = window.decodeURIComponent(param.substring(index + 1))
+					break
+				case "REFERNAME":
+					referName = window.decodeURIComponent(param.substring(index + 1))
+					break
+			}
+		}
+	})
+}
+
+const navItems = ref([])
+if (props.parentItems.length > 0) {
+	navItems.value.push({
+		name: props.parentItems[0][0],
+		isHome: true,
+		url: props.parentItems[0][1]
+	})
+
+	if (props.parentItems.length > 1) {
+		props.parentItems.slice(1).forEach(item => {
+			navItems.value.push({
+				name: item[0],
+				url: item[1]
+			})
+		})
+	}
+
+	if (props.contentName.length > 0) {
+		navItems.value.push({
+			name: props.contentName,
+			disabled: !props.secondary,
+			class: props.secondary ? "font-medium" : ""
+		})
+	}
+}
+
+const instance = getCurrentInstance()
+provide("$navbar", instance)
+
+defineExpose({
+	goPrevTab() {
+		tabHistory.pop()
+		activeTabRef.value = tabHistory[tabHistory.length - 1]
+	},
+	goTab(tab) {
+		tabHistory.push(tab)
+		activeTabRef.value = tab
+	}
+})
+</script>
+
+<template>
+	<div class="wrapper" :class="{'no-spacing': disableSpacing, secondary: secondary}">
+		<div v-if="(contentName.length > 0) && navItems.length > 0">
+			<TBreadcrumb :items="navItems" v-if="navItems.length > 0 && !secondary" class="!pl-0">
+				<template #separator>
+					<i class="pi pi-angle-right"></i>
+				</template>
+				<template #itemicon="{item}">
+					<svg v-if="item.isHome" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
+						<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/>
+					</svg>
+				</template>
+			</TBreadcrumb>
+			<div class="relative" :class="{'pt-4' : navItems.length == 0}">
+				<span class="name-box">
+					<a :href="(referURL.length > 0) ? referURL : navItems[navItems.length - 2].url" class="flex py-2 pr-4 z-10 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md" v-tooltip.top="'返回' + ((referName.length > 0) ? referName : ( (referURL.length == 0) ? navItems[navItems.length - 2].name : '' ))"><i class="pi pi-arrow-left"></i></a>
+					<span>{{ contentName }}</span>
+				</span>
+				<div class="flex items-center gap-2 md:absolute md:top-0 md:right-2 md:bottom-0">
+					<div class="flex flex-wrap gap-4">
+						<slot name="buttons"></slot>
+					</div>
+				</div>
+			</div>
+		</div>
+		<TTabView v-if="$slots.tabs" v-model="activeTabRef" @change="changeTab">
+			<TTabs>
+				<slot name="tabs"></slot>
+			</TTabs>
+		</TTabView>
+	</div>
+</template>
+
+<style scoped lang="postcss">
+.wrapper {
+	&:not(.secondary) {
+		@apply pb-4 -mt-2;
+	}
+
+	&.no-spacing {
+		@apply pb-0;
+	}
+
+	.name-box {
+		color: #333;
+		@apply flex items-center text-2xl mb-4 dark:text-gray-300;
+	}
+
+	&.secondary {
+		--font-size: 0.95em;
+
+		font-size: var(--font-size);
+		@apply pb-4 -mt-3;
+
+		:deep(i) {
+			font-size: var(--font-size);
+		}
+
+		.name-box {
+			@apply text-xl mt-7 mb-2;
+		}
+
+		:deep(.p-tab:not(.p-disabled)) {
+			padding-left: 0.9em;
+			padding-right: 0.9em;
+		}
+	}
+}
+</style>

+ 172 - 0
src/components/common/NetworkAddressesBox.vue

@@ -0,0 +1,172 @@
+<script setup>
+import {ref, watch} from "vue";
+import useUtils from "@/utils/utils.js";
+import "@/lib/array.js"
+import TLabel from "@/components/ui/TLabel.vue";
+import TDivider from "@/components/ui/TDivider.vue";
+
+const props = defineProps(["v-server-type", "v-addresses", "v-protocol", "v-name", "v-from", "v-support-range", "v-url", "minPort", "maxPort"])
+const utils = useUtils()
+const emits = defineEmits(["change", "update:modelValue"])
+
+const {addresses, protocol, name, from, isEditing} = (function () {
+	let addresses = props.vAddresses
+	if (addresses == null) {
+		addresses = []
+	}
+	let protocol = props.vProtocol
+	if (protocol == null) {
+		protocol = ""
+	}
+
+	let name = props.vName
+	if (name == null) {
+		name = "addresses"
+	}
+
+	let from = props.vFrom
+	if (from == null) {
+		from = ""
+	}
+
+	return {
+		addresses: ref(addresses),
+		protocol: ref(protocol),
+		name: ref(name),
+		from: ref(from),
+		isEditing: ref(false)
+	}
+})()
+setTimeout(() => change())
+
+let tlsProtocolName = ""
+
+watch(() => props.vServerType, () => {
+	addresses.value = []
+})
+
+watch(() => props.vAddresses, () => {
+	if (props.vAddresses != null) {
+		addresses.value = props.vAddresses
+	}
+})
+
+const addAddr = function () {
+	isEditing.value = true
+
+	window.UPDATING_ADDR = null
+
+	let url = props.vUrl
+	if (url == null) {
+		url = "/servers/addPortPopup"
+	}
+
+	utils.popup(url + "?serverType=" + props.vServerType + "&protocol=" + protocol.value + "&from=" + from.value + "&supportRange=" + (supportRange() ? 1 : 0) + "&minPort=" + (props.minPort ? props.minPort : 0) + "&maxPort=" + (props.maxPort ? props.maxPort : 0), {
+		callback: function (resp) {
+			var addr = resp.data.address
+			if (addresses.value.$find(function (k, v) {
+				return addr.host == v.host && addr.portRange == v.portRange && addr.protocol == v.protocol
+			}) != null) {
+				utils.warn("要添加的网络地址已经存在")
+				return
+			}
+			addresses.value.push(addr)
+			if (["https", "https4", "https6"].$contains(addr.protocol)) {
+				tlsProtocolName = "HTTPS"
+			} else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) {
+				tlsProtocolName = "TLS"
+			}
+
+			// 发送事件
+			change()
+		}
+	})
+}
+
+const removeAddr = function (index) {
+	addresses.value.$remove(index);
+
+	// 发送事件
+	change()
+}
+
+const updateAddr = function (index, addr) {
+	window.UPDATING_ADDR = addr
+
+	let url = props.vUrl
+	if (url == null) {
+		url = "/servers/addPortPopup"
+	}
+
+	utils.popup(url + "?serverType=" + props.vServerType + "&protocol=" + protocol.value + "&from=" + from.value + "&supportRange=" + (supportRange() ? 1 : 0) + "&minPort=" + (props.minPort ? props.minPort : 0) + "&maxPort=" + (props.maxPort ? props.maxPort : 0), {
+		callback: function (resp) {
+			var addr = resp.data.address
+			addresses.value[index] = addr
+
+			if (["https", "https4", "https6"].$contains(addr.protocol)) {
+				tlsProtocolName = "HTTPS"
+			} else if (["tls", "tls4", "tls6"].$contains(addr.protocol)) {
+				tlsProtocolName = "TLS"
+			}
+
+			// 发送事件
+			change()
+		}
+	})
+
+	// 发送事件
+	change()
+}
+const supportRange = function () {
+	return props.vSupportRange || (props.vServerType == "tcpProxy" || props.vServerType == "udpProxy")
+}
+const edit = function () {
+	isEditing.value = true
+}
+
+const change = () => {
+	emits("change", addresses.value)
+	emits("update:modelValue", JSON.stringify(addresses.value))
+}
+
+defineExpose({
+	tlsProtocolName
+})
+
+</script>
+
+<template>
+	<div>
+		<input type="hidden" :name="name" :value="JSON.stringify(addresses)"/>
+		<div v-show="!isEditing">
+			<div v-if="addresses.length > 0" class="flex flex-wrap gap-2 items-center">
+				<TLabel v-for="(addr, index) in addresses" outlined="">
+					{{ addr.protocol }}://<span v-if="addr.host.length > 0">{{ addr.host.quoteIP() }}</span><span
+					v-if="addr.host.length == 0">*</span>:<span
+					v-if="addr.portRange.indexOf('-')<0">{{ addr.portRange }}</span><span v-else class="italic">{{ addr.portRange }}</span>
+				</TLabel>
+				&nbsp; &nbsp; <a href="" @click.prevent="edit" style="font-size: 0.9em">[修改]</a>
+			</div>
+		</div>
+		<div v-show="isEditing || addresses.length == 0">
+			<div v-if="addresses.length > 0" class="label-group">
+				<TLabel v-for="(addr, index) in addresses" outlined="">
+					{{ addr.protocol }}://<span v-if="addr.host.length > 0">{{ addr.host.quoteIP() }}</span><span
+					v-if="addr.host.length == 0">*</span>:<span
+					v-if="addr.portRange.indexOf('-')<0">{{ addr.portRange }}</span><span v-else class="italic">{{ addr.portRange }}</span>&nbsp;
+					<a href="" @click.prevent="updateAddr(index, addr)" title="修改"><i
+						class="pi pi-pencil small"></i></a>&nbsp;
+					<a href="" @click.prevent="removeAddr(index)" title="删除"><i class="pi pi-remove"></i></a>
+				</TLabel>
+				<TDivider></TDivider>
+			</div>
+			<div>
+				<a href="" @click.prevent="addAddr()">[添加端口绑定]</a>
+			</div>
+		</div>
+	</div>
+</template>
+
+<style scoped>
+
+</style>

+ 17 - 0
src/components/common/NetworkAddressesView.vue

@@ -0,0 +1,17 @@
+<script setup>
+import TLabel from "@/components/ui/TLabel.vue";
+
+const props = defineProps(["v-addresses"])
+</script>
+
+<template>
+	<div class="flex flex-wrap gap-2">
+		<TLabel v-if="vAddresses != null" v-for="addr in vAddresses" outlined="">
+			<span>{{addr.protocol}}://<span v-if="addr.host.length > 0">{{addr.host.quoteIP()}}</span><span v-else>*</span>:{{addr.portRange}}</span>
+		</TLabel>
+	</div>
+</template>
+
+<style scoped>
+
+</style>

+ 24 - 0
src/components/common/NodeLogRow.vue

@@ -0,0 +1,24 @@
+<script setup>
+import Keyword from "@/components/common/Keyword.vue";
+import TTag from "@/components/ui/TTag.vue";
+
+const props = defineProps(["v-log", "v-keyword"])
+const {log, keyword} = (function () {
+	return {
+		log: props.vLog,
+		keyword: props.vKeyword
+	}
+})()
+</script>
+
+<template>
+	<div class="p-0 m-0">
+		<span :class="{red:log.level == 'error', orange:log.level == 'warning', green: log.level == 'success'}"><span v-if="!log.isToday">[{{ log.createdTime }}]</span><strong v-if="log.isToday">[{{ log.createdTime }}]</strong><Keyword :v-word="keyword">[{{ log.tag }}]{{ log.description }}</Keyword></span> &nbsp;
+		<TTag info v-if="log.count > 1" :class="{red:log.level == 'error', orange:log.level == 'warning'}">共{{ log.count }}条</TTag>
+		<span v-if="log.server != null && log.server.id > 0"><a :href="'/servers/server?serverId=' + log.server.id"><TTag>{{ log.server.name }}</TTag></a></span>
+	</div>
+</template>
+
+<style scoped>
+
+</style>

+ 16 - 0
src/components/common/NotFoundBox.vue

@@ -0,0 +1,16 @@
+<script setup>
+const props = defineProps(["message"])
+</script>
+
+<template>
+	<div style="text-align: center; margin-top: 5em;">
+		<div style="font-size: 2em; margin-bottom: 1em"><i class="pi pi-exclamation-triangle !text-4xl text-gray-400"></i></div>
+		<p class="comment">{{ message }}
+			<slot></slot>
+		</p>
+	</div>
+</template>
+
+<style scoped>
+
+</style>

+ 20 - 0
src/components/common/NotFoundInfo.vue

@@ -0,0 +1,20 @@
+<script setup>
+import TMessage from "@/components/ui/TMessage.vue";
+
+const props = defineProps({
+	disableSpacing: {
+		type: Boolean,
+		default: false
+	}
+})
+</script>
+
+<template>
+	<TMessage :class="{'mt-4': !disableSpacing}">
+		<slot></slot>
+	</TMessage>
+</template>
+
+<style scoped lang="postcss">
+
+</style>

+ 11 - 0
src/components/common/OptionalLabel.vue

@@ -0,0 +1,11 @@
+<script setup>
+// 可选标签
+</script>
+
+<template>
+	<em><span class="grey">(可选)</span></em>
+</template>
+
+<style scoped>
+
+</style>

+ 73 - 0
src/components/common/PageSizeSelector.vue

@@ -0,0 +1,73 @@
+<script setup>
+
+import {ref, watch} from "vue"
+import TSelect from "@/components/ui/TSelect.vue";
+import TOption from "@/components/ui/TOption.vue";
+
+const {pageSize} = (function () {
+	let query = window.location.search
+	let pageSize = 10
+	if (query.length > 0) {
+		query = query.substr(1)
+		let params = query.split("&")
+		params.forEach(function (v) {
+			let pieces = v.split("=")
+			if (pieces.length == 2 && pieces[0] == "pageSize") {
+				let pageSizeString = pieces[1]
+				if (pageSizeString.match(/^\d+$/)) {
+					pageSize = parseInt(pageSizeString, 10)
+					if (isNaN(pageSize) || pageSize < 1) {
+						pageSize = 10
+					}
+				}
+			}
+		})
+	}
+	return {
+		pageSize: ref(pageSize)
+	}
+})()
+
+const changePageSize = function (size) {
+	let url = window.location.toString();
+	url = url.replace(/page=\d+/g, "page=1")
+	if (url.indexOf("pageSize") > 0) {
+		url = url.replace(/pageSize=\d+/g, "pageSize=" + size)
+	} else {
+		if (url.indexOf("?") > 0) {
+			let anchorIndex = url.indexOf("#")
+			if (anchorIndex < 0) {
+				url += "&pageSize=" + size;
+			} else {
+				url = url.substring(0, anchorIndex) + "&pageSize=" + size + url.substr(anchorIndex);
+			}
+		} else {
+			url += "?pageSize=" + size;
+		}
+	}
+	window.location = url;
+};
+
+watch(pageSize, function () {
+	changePageSize(pageSize.value)
+})
+
+</script>
+
+<template>
+	<TSelect v-model="pageSize">
+		<TOption value="10">10条</TOption>
+		<TOption value="20">20条</TOption>
+		<TOption value="30">30条</TOption>
+		<TOption value="40">40条</TOption>
+		<TOption value="50">50条</TOption>
+		<TOption value="60">60条</TOption>
+		<TOption value="70">70条</TOption>
+		<TOption value="80">80条</TOption>
+		<TOption value="90">90条</TOption>
+		<TOption value="100">100条</TOption>
+	</TSelect>
+</template>
+
+<style scoped>
+</style>

+ 69 - 0
src/components/common/Pager.vue

@@ -0,0 +1,69 @@
+<script setup>
+import TPaginator from "@/components/ui/TPaginator.vue";
+import {inject, ref, watch} from "vue";
+
+const Tea = inject("$Tea")
+const props = defineProps(["data"])
+const popupState = inject("$popupState", null)
+
+const key = ref()
+
+watch(() => props.data, (v) => {
+	key.value = v.current + "_" + v.size
+})
+
+const onChange = (page) => {
+	let currentPage = page.page
+	if (props.data) {
+		if (currentPage == props.data.current && page.rows == props.data.size) {
+			return
+		}
+	}
+
+	props.data.current = currentPage
+
+	let url = Tea.currentURL()
+	if (url.indexOf("?") < 0) {
+		url = url + "?page=" + currentPage + "&pageSize=" + page.rows
+	} else if (url.indexOf("?page=") > 0) {
+		url = url.replace(/\?page=(\d+)?/g, "?page=" + currentPage)
+	} else if (url.indexOf("&page=") > 0) {
+		url = url.replace(/&page=(\d+)?/g, "&page=" + currentPage)
+	} else {
+		url += "&page=" + currentPage
+	}
+
+	if (url.indexOf("?pageSize=") > 0) {
+		url = url.replace(/\?pageSize=(\d+)?/g, "?pageSize=" + page.rows)
+	} else if (url.indexOf("&pageSize=") > 0) {
+		url = url.replace(/&pageSize=(\d+)?/g, "&pageSize=" + page.rows)
+	} else {
+		url += "&pageSize=" + page.rows
+	}
+
+	if (popupState) {
+		Tea.reloadViewData(url, {
+			onDone: () => {
+				if (popupState != null) {
+					const dialogs = document.querySelectorAll(".p-dialog-content")
+					if (dialogs.length > 0) {
+						dialogs[dialogs.length - 1].scrollTop = 0
+					}
+				}
+			}
+		})
+	} else {
+		window.location.href = url
+	}
+}
+
+</script>
+
+<template>
+	<TPaginator :total="data.total" :rows="data.size" :page="data.current" :offset="data.offset"
+				@change="onChange" :key="key" :alwaysShow="false"></TPaginator>
+</template>
+
+<style scoped>
+
+</style>

+ 13 - 0
src/components/common/PlusButton.vue

@@ -0,0 +1,13 @@
+<script setup>
+import TButton from "@/components/ui/TButton.vue";
+</script>
+
+<template>
+	<TButton type="button" icon="pi pi-plus" rounded secondary="" class="plus-button"></TButton>
+</template>
+
+<style scoped lang="postcss">
+table.selectable tr:hover td .plus-button {
+	@apply bg-white border-gray-300 dark:bg-gray-800 dark:border-gray-700;
+}
+</style>

+ 28 - 0
src/components/common/PopTipComponent.vue

@@ -0,0 +1,28 @@
+<script setup>
+import {inject, onMounted, ref} from "vue";
+
+const contentHTML = ref("")
+
+const props = defineProps(["footer"])
+
+const rootRef = ref()
+const dialogRef = inject("dialogRef")
+
+onMounted(() => {
+	contentHTML.value = dialogRef.value.data.content
+})
+
+</script>
+
+<template>
+	<div ref="rootRef">
+		<header class="header">系统帮助</header>
+		<div class="leading-8">
+			<div v-html="contentHTML"></div>
+		</div>
+	</div>
+</template>
+
+<style scoped>
+
+</style>

+ 23 - 0
src/components/common/PopupIcon.vue

@@ -0,0 +1,23 @@
+<script setup>
+import useUtils from "@/utils/utils.js";
+
+const utils = useUtils()
+const props = defineProps(["title", "href", "height"])
+const clickPrevent = function () {
+	if (props.href != null && props.href.length > 0) {
+		utils.popup(props.href, {
+			height: props.height
+		})
+	}
+}
+
+</script>
+
+<template>
+	<span><slot></slot>&nbsp;<a href="" :title="title" @click.prevent="clickPrevent"><i
+		class="pi pi-expand small"></i></a></span>
+</template>
+
+<style scoped>
+
+</style>

+ 13 - 0
src/components/common/PrimaryButton.vue

@@ -0,0 +1,13 @@
+<script setup>
+import TButton from "@/components/ui/TButton.vue";
+</script>
+
+<template>
+	<TButton>
+		<slot></slot>
+	</TButton>
+</template>
+
+<style scoped>
+
+</style>

+ 38 - 0
src/components/common/PriorCheckbox.vue

@@ -0,0 +1,38 @@
+<script setup>
+
+import {ref, watch} from "vue";
+import TSwitch from "@/components/ui/TSwitch.vue";
+
+const props = defineProps(["v-config", "description"])
+
+const {isPrior, realDescription} = (function () {
+	let description = props.description
+	if (description == null) {
+		description = "打开后可以覆盖父级或子级配置"
+	}
+	return {
+		isPrior: ref(props.vConfig.isPrior),
+		realDescription: ref(description)
+	}
+})()
+
+watch(isPrior, function (v) {
+	props.vConfig.isPrior = v
+})
+</script>
+
+<template>
+	<tbody>
+	<tr :class="{active:isPrior}">
+		<td class="title">打开独立配置</td>
+		<td>
+			<TSwitch v-model="isPrior"></TSwitch>
+			<p class="comment"><strong v-if="isPrior">[已打开]</strong> {{ realDescription }}。</p>
+		</td>
+	</tr>
+	</tbody>
+</template>
+
+<style scoped lang="postcss">
+
+</style>

+ 13 - 0
src/components/common/ProWarningLabel.vue

@@ -0,0 +1,13 @@
+<script setup>
+
+// 提醒设置项为专业设置
+
+</script>
+
+<template>
+	<span class="flex flex-wrap items-center gap-2 my-2"><i class="pi pi-exclamation-circle yellow"></i>注意:通常不需要修改;如要修改,请在专家指导下进行。</span>
+</template>
+
+<style scoped>
+
+</style>

+ 68 - 0
src/components/common/ProvincesSelector.vue

@@ -0,0 +1,68 @@
+<script setup>
+import "@/lib/array.js"
+import {ref} from "vue";
+import useUtils from "@/utils/utils.js";
+import TLabel from "@/components/ui/TLabel.vue";
+import TDivider from "@/components/ui/TDivider.vue";
+import PlusButton from "@/components/common/PlusButton.vue";
+
+const props = defineProps({
+	"v-provinces": Array
+})
+const emits = defineEmits(["update:modelValue"])
+const utils = useUtils()
+
+let propProvinces = props.vProvinces
+if (propProvinces == null) {
+	propProvinces = []
+}
+let propProvinceIds = propProvinces.$map(function (k, v) {
+	return v.id
+})
+
+const provinces = ref(propProvinces)
+const provinceIds = ref(propProvinceIds)
+
+const add = function () {
+	let provinceStringIds = provinceIds.value.map(function (v) {
+		return v.toString()
+	})
+	utils.popup("/ui/selectProvincesPopup?provinceIds=" + provinceStringIds.join(","), {
+		width: "48em",
+		callback: function (resp) {
+			provinces.value = resp.data.provinces
+			change()
+		}
+	})
+}
+const remove = function (index) {
+	provinces.value.$remove(index)
+	change()
+}
+const change = function () {
+	provinceIds.value = provinces.value.$map(function (k, v) {
+		return v.id
+	})
+	emits("update:modelValue", JSON.stringify(provinceIds.value))
+}
+</script>
+
+<template>
+	<div>
+		<input type="hidden" name="provinceIdsJSON" :value="JSON.stringify(provinceIds)"/>
+		<div v-if="provinces.length > 0" style="margin-bottom: 0.5em" class="flex flex-wrap gap-2">
+			<TLabel v-for="(province, index) in provinces" outlined="">{{ province.name }} <a href=""
+																							  title="删除"
+																							  @click.prevent="remove(index)"><i
+				class="pi pi-remove"></i></a></TLabel>
+			<TDivider></TDivider>
+		</div>
+		<div>
+			<PlusButton size="small" type="button" @click.prevent="add"></PlusButton>
+		</div>
+	</div>
+</template>
+
+<style scoped>
+
+</style>

+ 10 - 0
src/components/common/RaquoItem.vue

@@ -0,0 +1,10 @@
+<script setup>
+</script>
+
+<template>
+	<span class="disabled" style="padding: 0">&raquo;</span>
+</template>
+
+<style scoped>
+
+</style>

+ 47 - 0
src/components/common/RequestVariablesDescriber.vue

@@ -0,0 +1,47 @@
+<script setup>
+
+import {ref} from "vue"
+import CodeLabel from "@/components/common/CodeLabel.vue";
+
+const {vars} = (function () {
+	return {
+		vars: ref([])
+	}
+})()
+
+const update = function (variablesString) {
+	vars.value = []
+	variablesString.replace(/\${.+?}/g, function (v) {
+		let def = findVar(v)
+		if (def == null) {
+			return v
+		}
+		vars.value.push(def)
+	})
+}
+
+const findVar = function (name) {
+	let def = null
+	window.REQUEST_VARIABLES.forEach(function (v) {
+		if (v.code == name) {
+			def = v
+		}
+	})
+	return def
+}
+
+
+defineExpose({
+	update
+})
+
+</script>
+
+<template>
+<span>
+	<span v-for="(v, index) in vars"><CodeLabel :title="v.description">{{ v.code }}</CodeLabel> - {{ v.name }}<span v-if="index < vars.length-1">;</span></span>
+</span>
+</template>
+
+<style scoped>
+</style>

+ 57 - 0
src/components/common/SearchBox.vue

@@ -0,0 +1,57 @@
+<script setup>
+
+import {ref} from "vue"
+import TInputText from "@/components/ui/TInputText.vue";
+import TInputGroup from "@/components/ui/TInputGroup.vue";
+import TInputGroupAddon from "@/components/ui/TInputGroupAddon.vue";
+
+const props = defineProps(["placeholder", "width"])
+
+const valueRef = ref()
+const emits = defineEmits(["input", "change"])
+
+const {realWidth, realValue} = (function () {
+	let width = props.width
+	if (width == null) {
+		width = "14em"
+	}
+	return {
+		realWidth: ref(width),
+		realValue: ref("")
+	}
+})()
+
+const onInput = function () {
+	emits("input", {value: realValue.value})
+	emits("change", {value: realValue.value})
+}
+
+const clearValue = function () {
+	realValue.value = ""
+	focus()
+	onInput()
+}
+
+const focus = function () {
+	valueRef.value.focus()
+}
+
+defineExpose({
+	focus
+})
+
+</script>
+
+<template>
+	<div>
+		<TInputGroup>
+			<TInputText type="text" :placeholder="placeholder" :style="{width: realWidth}" @input="onInput" v-model="realValue" ref="valueRef"/>
+			<TInputGroupAddon v-if="realValue.length > 0">
+				<a href="" @click.prevent="clearValue"><i class="icon remove"></i></a>
+			</TInputGroupAddon>
+		</TInputGroup>
+	</div>
+</template>
+
+<style scoped>
+</style>

+ 24 - 0
src/components/common/SecondMenu.vue

@@ -0,0 +1,24 @@
+<script setup>
+import TDivider from "@/components/ui/TDivider.vue";
+
+const props = defineProps(["class"])
+const className = props["class"]
+
+</script>
+
+<template>
+	<div class="mt-1 flex flex-wrap gap-4 items-center" :class="className" style="font-size: 0.95em">
+		<slot></slot>
+	</div>
+	<TDivider></TDivider>
+</template>
+
+<style scoped>
+:deep(.item) {
+	color: var(--p-text-muted-color);
+}
+
+:deep(.active) {
+	color: var(--p-primary-color);
+}
+</style>

+ 13 - 0
src/components/common/SecondaryButton.vue

@@ -0,0 +1,13 @@
+<script setup>
+import TButton from "@/components/ui/TButton.vue";
+</script>
+
+<template>
+	<TButton secondary="">
+		<slot></slot>
+	</TButton>
+</template>
+
+<style scoped>
+
+</style>

+ 89 - 0
src/components/common/SizeCapacityBox.vue

@@ -0,0 +1,89 @@
+<script setup>
+import {ref, watch} from "vue";
+import TSelect from "@/components/ui/TSelect.vue";
+import TOption from "@/components/ui/TOption.vue";
+import TInputText from "@/components/ui/TInputText.vue";
+
+const props = defineProps(["v-name", "v-value", "v-count", "v-unit", "size", "maxlength", "v-supported-units"])
+const emits = defineEmits(["change"])
+
+const {capacity, countString, vSize, vMaxlength, supportedUnits} = (function () {
+	let v = props.vValue
+	if (v == null) {
+		v = {
+			count: props.vCount,
+			unit: props.vUnit
+		}
+	}
+	if (typeof (v["count"]) != "number") {
+		v["count"] = -1
+	}
+
+	let vSize = props.size
+	if (vSize == null) {
+		vSize = 6
+	}
+
+	let vMaxlength = props.maxlength
+	if (vMaxlength == null) {
+		vMaxlength = 10
+	}
+
+	let supportedUnits = props.vSupportedUnits
+	if (supportedUnits == null) {
+		supportedUnits = []
+	}
+
+	return {
+		capacity: ref(v),
+		countString: ref((v.count >= 0) ? v.count.toString() : ""),
+		vSize: ref(vSize),
+		vMaxlength: ref(vMaxlength),
+		supportedUnits: ref(supportedUnits)
+	}
+})()
+
+const change = function () {
+	emits("change", capacity.value)
+}
+
+watch(countString, function (newValue) {
+	let value = newValue.trim()
+	if (value.length == 0) {
+		capacity.value.count = -1
+		change()
+		return
+	}
+	let count = parseInt(value)
+	if (!isNaN(count)) {
+		capacity.value.count = count
+	}
+	change()
+})
+
+</script>
+
+<template>
+	<div class="ui fields inline">
+		<input type="hidden" :name="vName" :value="JSON.stringify(capacity)"/>
+		<div class="ui field">
+			<TInputText type="text" v-model="countString" :maxlength="vMaxlength" :size="vSize"/>
+		</div>
+		<div class="ui field">
+			<TSelect v-model="capacity.unit" @change="change">
+				<TOption value="byte" v-if="supportedUnits.length == 0 || supportedUnits.$contains('byte')">字节
+				</TOption>
+				<TOption value="kb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('kb')">KiB</TOption>
+				<TOption value="mb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('mb')">MiB</TOption>
+				<TOption value="gb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('gb')">GiB</TOption>
+				<TOption value="tb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('tb')">TiB</TOption>
+				<TOption value="pb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('pb')">PiB</TOption>
+				<TOption value="eb" v-if="supportedUnits.length == 0 || supportedUnits.$contains('eb')">EiB</TOption>
+			</TSelect>
+		</div>
+	</div>
+</template>
+
+<style scoped>
+
+</style>

+ 22 - 0
src/components/common/SizeCapacityView.vue

@@ -0,0 +1,22 @@
+<script setup>
+import useUtils from "@/utils/utils.js";
+
+const utils = useUtils()
+
+const props = defineProps(["v-default-text", "v-value"])
+const composeCapacity = function (capacity) {
+	return utils.convertSizeCapacityToString(capacity)
+}
+
+</script>
+
+<template>
+	<div>
+		<span v-if="vValue != null && vValue.count > 0">{{ composeCapacity(vValue) }}</span>
+		<span v-else>{{ vDefaultText }}</span>
+	</div>
+</template>
+
+<style scoped>
+
+</style>

+ 65 - 0
src/components/common/SortArrow.vue

@@ -0,0 +1,65 @@
+<script setup>
+// 排序使用的箭头
+
+import {ref} from "vue";
+
+const props = defineProps(["name"])
+
+const {order, url, iconTitle} = (function () {
+	let url = window.location.toString()
+	let order = ""
+	let iconTitle = ""
+	let newArgs = []
+	if (window.location.search != null && window.location.search.length > 0) {
+		let queryString = window.location.search.substring(1)
+		let pieces = queryString.split("&")
+		pieces.forEach(function (v) {
+			let eqIndex = v.indexOf("=")
+			if (eqIndex > 0) {
+				let argName = v.substring(0, eqIndex)
+				let argValue = v.substring(eqIndex + 1)
+				if (argName == props.name) {
+					order = argValue
+				} else if (argName != "page" && argValue != "asc" && argValue != "desc") {
+					newArgs.push(v)
+				}
+			} else {
+				newArgs.push(v)
+			}
+		})
+	}
+	if (order == "asc") {
+		newArgs.push(props.name + "=desc")
+		iconTitle = "当前正序排列"
+	} else if (order == "desc") {
+		newArgs.push(props.name + "=asc")
+		iconTitle = "当前倒序排列"
+	} else {
+		newArgs.push(props.name + "=desc")
+		iconTitle = "当前正序排列"
+	}
+
+	let qIndex = url.indexOf("?")
+	if (qIndex > 0) {
+		url = url.substring(0, qIndex) + "?" + newArgs.join("&")
+	} else {
+		url = url + "?" + newArgs.join("&")
+	}
+
+	return {
+		order: ref(order),
+		url: ref(url),
+		iconTitle: ref(iconTitle)
+	}
+})()
+
+</script>
+
+<template>
+	<a :href="url" v-tooltip.top="iconTitle"><i class="pi small"
+												:class="{'pi-arrow-down': order == 'asc', 'pi-arrow-up': order == 'desc', 'pi-arrow-down grey': order == '' || order == null}"></i></a>
+</template>
+
+<style scoped>
+
+</style>

+ 130 - 0
src/components/common/SourceCodeBox.vue

@@ -0,0 +1,130 @@
+<script setup>
+// TODO 重新实现  height, name,支持bash、php、python等
+
+import {onMounted, ref} from "vue";
+import {basicSetup, EditorView} from "codemirror"
+import {keymap} from "@codemirror/view"
+import {yaml} from "@codemirror/lang-yaml"
+import {javascript} from "@codemirror/lang-javascript"
+import {json} from "@codemirror/lang-json"
+import {indentWithTab} from "@codemirror/commands"
+
+const props = defineProps(["name", "type", "id", "readOnly", "width", "height", "focus", "type"])
+const emits = defineEmits(["change"])
+const valueRef = ref("")
+
+const height = ref(5)
+if (props.height > 0) {
+	height.value = props.height
+}
+
+const editorRef = ref()
+const textRef = ref()
+onMounted(() => {
+	let styleSpec = {}
+	let htmlClassName = document.querySelector("html").className
+	if (typeof htmlClassName === "string" && htmlClassName.indexOf("dark") >= 0) {
+		styleSpec = {
+			"span.ͼl, span.ͼg, span.ͼc": {
+				color: "var(--p-blue-400)"
+			},
+			"span.ͼe": {
+				color: "var(--p-red-400)"
+			},
+			"span.ͼb": {
+				color: "var(--p-pink-400)"
+			},
+			".cm-selectionBackground, ::selection": {
+				background: "rgba(0, 0, 0, 0.6)"
+			},
+			".cm-selectionLayer": {
+				background: "rgba(0, 0, 0, 0.6)"
+			},
+			".cm-layer-above": {
+				background: "rgba(0, 0, 0, 0.6)"
+			},
+
+			"&": {
+				color: "var(--p-gray-200)",
+				backgroundColor: "#034"
+			},
+			".cm-content": {
+				caretColor: "#0e9"
+			},
+			"&.cm-focused .cm-cursor": {
+				borderLeftColor: "#0e9"
+			},
+			"&.cm-focused .cm-selectionBackground": {
+				backgroundColor: "#074"
+			},
+			".cm-gutters": {
+				backgroundColor: "#045",
+				color: "#ddd",
+				border: "none"
+			}
+		}
+	}
+
+	const extensions = [
+		basicSetup,
+		keymap.of([indentWithTab]),
+		EditorView.updateListener.of((v) => {
+				valueRef.value = v.view.state.doc.toString()
+				emits("change", valueRef.value)
+			}
+		),
+		EditorView.lineWrapping,
+		EditorView.baseTheme({
+			".cm-content": {
+				lineHeight: "1.8em"
+			},
+			...styleSpec
+		})
+	]
+
+	let readOnly = false
+	if (props.readOnly || (typeof props.readOnly === "string")) {
+		readOnly = true
+		extensions.push(EditorView.editable.of(false))
+	}
+
+	switch (props.type) {
+		case "text/yaml":
+			extensions.push(yaml())
+			break
+		case "application/json":
+			extensions.push(json())
+			break
+		case "application/javascript":
+			extensions.push(javascript())
+			break
+		case "text/javascript":
+			extensions.push(javascript())
+			break
+	}
+
+	valueRef.value = textRef.value.textContent
+	if (!readOnly && valueRef.value.length == 0) {
+		valueRef.value = "\n\n\n\n"
+	}
+	new EditorView({
+		doc: valueRef.value,
+		extensions: extensions,
+		parent: editorRef.value
+	})
+
+})
+
+</script>
+
+<template>
+	<input :name="name" type="hidden" v-model="valueRef"/>
+	<div ref="editorRef"></div>
+	<div class="hidden" ref="textRef" :id="id">
+		<slot></slot>
+	</div>
+</template>
+
+<style scoped>
+
+</style>

+ 9 - 0
src/components/common/SourceCodeHighlighter.vue

@@ -0,0 +1,9 @@
+<script setup>
+import SourceCodeBox from "@/components/common/SourceCodeBox.vue";
+
+const props = defineProps(["code"])
+</script>
+
+<template>
+	<SourceCodeBox readOnly>{{ code }}</SourceCodeBox>
+</template>

+ 50 - 0
src/components/common/ThemeSpans.vue

@@ -0,0 +1,50 @@
+<script setup>
+import {colors} from "@/lib/theme.js"
+import {inject} from "vue";
+
+const Tea = inject("$Tea")
+const utils = inject("$utils")
+
+const currentTheme =  window.X_VIEW_DATA ? window.X_VIEW_DATA.teaTheme : null
+
+const changeTheme = (color, e) => {
+	let target = e.target
+	if (target.className.indexOf("inner-span") >= 0) {
+		target = target.parentNode
+	}
+	target.style.border = '2px var(--p-' + color + '-500) solid'
+
+	Tea.action("/ui/theme")
+		.post()
+		.params({theme: color})
+		.success(function (resp) {
+			utils.successToast("界面风格已切换", null, () => {
+				utils.reload()
+			})
+		})
+}
+</script>
+
+<template>
+	<div class="grid grid-cols-4 gap-2">
+		<span class="color-span cursor-pointer" v-for="color in colors" :title="color" @click="changeTheme(color, $event)" :style="{border: (currentTheme == color) ? '2px var(--p-' + color + '-500) solid' : '2px white solid'}">
+			<span class="inner-span inline-block" :style="'background-color: var(--p-' + color + '-500)'"></span>
+		</span>
+	</div>
+</template>
+
+<style scoped lang="postcss">
+.color-span {
+	border-radius: 50%;
+	width: 2em;
+	height: 2em;
+	padding-top: 0.145em;
+	padding-left: 0.145em;
+}
+
+.inner-span {
+	width: 1.4em;
+	height: 1.4em;
+	border-radius: 50%;
+}
+</style>

+ 110 - 0
src/components/common/TimeDurationBox.vue

@@ -0,0 +1,110 @@
+<script setup>
+import {onMounted, ref, watch} from "vue";
+import TInputText from "@/components/ui/TInputText.vue";
+import TSelect from "@/components/ui/TSelect.vue";
+import TOption from "@/components/ui/TOption.vue";
+
+const props = defineProps(["v-name", "v-value", "v-count", "v-unit", "placeholder", "v-min-unit", "maxlength"])
+const emits = defineEmits(["change"])
+
+const {duration, countString, units, realMaxLength} = (function () {
+	let v = props.vValue
+	if (v == null) {
+		v = {
+			count: props.vCount,
+			unit: props.vUnit
+		}
+	}
+	if (typeof (v["count"]) != "number") {
+		v["count"] = -1
+	}
+
+	let minUnit = props.vMinUnit
+	let units = [
+		{
+			code: "ms",
+			name: "毫秒"
+		},
+		{
+			code: "second",
+			name: "秒"
+		},
+		{
+			code: "minute",
+			name: "分钟"
+		},
+		{
+			code: "hour",
+			name: "小时"
+		},
+		{
+			code: "day",
+			name: "天"
+		}
+	]
+	let minUnitIndex = -1
+	if (minUnit != null && typeof minUnit == "string" && minUnit.length > 0) {
+		for (let i = 0; i < units.length; i++) {
+			if (units[i].code == minUnit) {
+				minUnitIndex = i
+				break
+			}
+		}
+	}
+	if (minUnitIndex > -1) {
+		units = units.slice(minUnitIndex)
+	}
+
+	let maxLength = parseInt(props.maxlength)
+	if (isNaN(maxLength) || maxLength <= 0) {
+		maxLength = 6
+	}
+
+	return {
+		duration: ref(v),
+		countString: ref((v.count >= 0) ? v.count.toString() : ""),
+		units: ref(units),
+		realMaxLength: ref(maxLength)
+	}
+})()
+
+const change = function () {
+	emits("change", duration.value)
+}
+
+watch(countString, function (newValue) {
+	let value = newValue.trim()
+	if (value.length == 0) {
+		duration.value.count = -1
+		return
+	}
+	let count = parseInt(value)
+	if (!isNaN(count)) {
+		duration.value.count = count
+	}
+	change()
+})
+
+onMounted(() => {
+	change()
+})
+
+</script>
+
+<template>
+	<div class="ui fields inline" style="padding-bottom: 0; margin-bottom: 0">
+		<input type="hidden" :name="vName" :value="JSON.stringify(duration)"/>
+		<div class="ui field">
+			<TInputText type="text" v-model="countString" :maxlength="realMaxLength" :size="realMaxLength" :placeholder="placeholder"/>
+		</div>
+		<div class="ui field">
+			<TSelect v-model="duration.unit" @change="change">
+				<TOption v-for="unit in units" :value="unit.code">{{ unit.name }}</TOption>
+			</TSelect>
+		</div>
+	</div>
+</template>
+
+<style scoped>
+
+</style>

+ 29 - 0
src/components/common/TimeDurationText.vue

@@ -0,0 +1,29 @@
+<script setup>
+const props = defineProps(["v-value"])
+
+const unitName = function (unit) {
+	switch (unit) {
+		case "ms":
+			return "毫秒"
+		case "second":
+			return "秒"
+		case "minute":
+			return "分钟"
+		case "hour":
+			return "小时"
+		case "day":
+			return "天"
+	}
+}
+
+</script>
+
+<template>
+<span>
+	{{ vValue.count }} {{ unitName(vValue.unit) }}
+</span>
+</template>
+
+<style scoped>
+
+</style>

+ 13 - 0
src/components/common/TinyBasicLabel.vue

@@ -0,0 +1,13 @@
+<script setup>
+import TLabel from "@/components/ui/TLabel.vue";
+</script>
+
+<template>
+	<TLabel size="tiny" outlined="">
+		<slot></slot>
+	</TLabel>
+</template>
+
+<style scoped>
+
+</style>

+ 20 - 0
src/components/common/TipIcon.vue

@@ -0,0 +1,20 @@
+<script setup>
+// 小提示
+
+import useUtils from "@/utils/utils.js";
+
+const props = defineProps(["content"])
+const utils = useUtils()
+
+const showTip = function () {
+	utils.popupTip(props.content)
+}
+</script>
+
+<template>
+	<a href="" title="查看帮助" @click.prevent="showTip" v-tooltip.top="'点击查看帮助'"><i class="pi pi-question-circle gray"></i></a>
+</template>
+
+<style scoped>
+
+</style>

+ 49 - 0
src/components/common/TipMessageBox.vue

@@ -0,0 +1,49 @@
+<script setup>
+// 信息提示窗口
+
+import {inject, onMounted, ref} from "vue";
+import TMessage from "@/components/ui/TMessage.vue";
+
+const Tea = inject("$Tea")
+
+const props = defineProps(["code"])
+const {visible} = (function () {
+	return {
+		visible: ref(false)
+	}
+})()
+
+onMounted(() => {
+	Tea.action("/ui/showTip")
+		.params({
+			code: props.code
+		})
+		.success(function (resp) {
+			visible.value = resp.data.visible
+		})
+		.post()
+})
+
+const close = function () {
+	visible.value = false
+	Tea.action("/ui/hideTip")
+		.params({
+			code: props.code
+		})
+		.post()
+}
+
+</script>
+
+<template>
+	<TMessage class="mb-4" icon="pi pi-info-circle" v-if="visible">
+		<div class="absolute -mt-3 left-10 right-0">
+			<slot></slot>
+			<a href="" title="取消" @click.prevent="close" class="absolute right-4"><i class="pi pi-remove" ></i></a>
+		</div>
+	</TMessage>
+</template>
+
+<style scoped lang="postcss">
+
+</style>

+ 31 - 0
src/components/common/URLPatternLabels.vue

@@ -0,0 +1,31 @@
+<script setup>
+import TLabel from "@/components/ui/TLabel.vue";
+
+const props = defineProps(["modelValue"])
+</script>
+
+<template>
+	<div class="label-group">
+		<TLabel outlined="" v-for="pattern in props.modelValue">
+			<span v-if="pattern.type == 'images'">
+				<span class="gray">[常见图片]</span>
+			</span>
+			<span v-if="pattern.type == 'audios'">
+				<span class="gray">[常见音频]</span>
+			</span>
+			<span v-if="pattern.type == 'videos'">
+				<span class="gray">[常见视频]</span>
+			</span>
+			<span v-if="pattern.type == 'wildcard'">
+				<span class="gray">[通配符]</span> {{ pattern.pattern }}
+			</span>
+			<span v-if="pattern.type == 'regexp'">
+				<span class="gray">[正则表达式]</span> {{ pattern.pattern }}
+			</span>
+		</TLabel>
+	</div>
+</template>
+
+<style scoped lang="postcss">
+
+</style>

+ 193 - 0
src/components/common/URLPatternsBox.vue

@@ -0,0 +1,193 @@
+<script setup>
+
+import "@/lib/array.js"
+import useUtils from "@/utils/utils.js"
+import {ref, watch} from "vue"
+import TSelect from "@/components/ui/TSelect.vue";
+import TOption from "@/components/ui/TOption.vue";
+import TInputText from "@/components/ui/TInputText.vue";
+import TButton from "@/components/ui/TButton.vue";
+import TipIcon from "@/components/common/TipIcon.vue";
+import TLabel from "@/components/ui/TLabel.vue";
+import PlusButton from "@/components/common/PlusButton.vue";
+import TDivider from "@/components/ui/TDivider.vue";
+
+const utils = useUtils()
+
+const props = defineProps(["modelValue"])
+
+const patternInputRef = ref()
+const emits = defineEmits(["input", "update:modelValue"])
+
+const {patterns, isAdding, addingPattern, editingIndex, patternIsInvalid, windowIsSmall} = (function () {
+	let patterns = []
+	if (props.modelValue != null) {
+		patterns = props.modelValue
+	}
+
+	return {
+		patterns: ref(patterns),
+		isAdding: ref(false),
+
+		addingPattern: ref({"type": "wildcard", "pattern": ""}),
+		editingIndex: ref(-1),
+
+		patternIsInvalid: ref(false),
+
+		windowIsSmall: ref(window.innerWidth)
+	}
+})()
+
+const add = function () {
+	isAdding.value = true
+	setTimeout(function () {
+		patternInputRef.value.focus()
+	})
+}
+
+const edit = function (index) {
+	isAdding.value = true
+	editingIndex.value = index
+	addingPattern.value = {
+		type: patterns.value[index].type,
+		pattern: patterns.value[index].pattern
+	}
+}
+
+const confirm = function () {
+	if (requireURL(addingPattern.value.type)) {
+		let pattern = addingPattern.value.pattern.trim()
+		if (pattern.length == 0) {
+			utils.warn("请输入URL", function () {
+				patternInputRef.value.focus()
+			})
+			return
+		}
+	}
+	if (editingIndex.value < 0) {
+		patterns.value.push({
+			type: addingPattern.value.type,
+			pattern: addingPattern.value.pattern
+		})
+	} else {
+		patterns.value[editingIndex.value].type = addingPattern.value.type
+		patterns.value[editingIndex.value].pattern = addingPattern.value.pattern
+	}
+	notifyChange()
+	cancel()
+}
+
+const remove = function (index) {
+	patterns.value.$remove(index)
+	cancel()
+	notifyChange()
+}
+
+const cancel = function () {
+	isAdding.value = false
+	addingPattern.value = {"type": "wildcard", "pattern": ""}
+	editingIndex.value = -1
+}
+
+const patternTypeName = function (patternType) {
+	switch (patternType) {
+		case "wildcard":
+			return "通配符"
+		case "regexp":
+			return "正则"
+		case "images":
+			return "常见图片文件"
+		case "audios":
+			return "常见音频文件"
+		case "videos":
+			return "常见视频文件"
+	}
+	return ""
+}
+
+const notifyChange = function () {
+	emits("input", patterns.value)
+	emits("update:modelValue", patterns.value)
+}
+
+const changePattern = function () {
+	patternIsInvalid.value = false
+	let pattern = addingPattern.value.pattern
+	switch (addingPattern.value.type) {
+		case "wildcard":
+			if (pattern.indexOf("?") >= 0) {
+				patternIsInvalid.value = true
+			}
+			break
+		case "regexp":
+			if (pattern.indexOf("?") >= 0) {
+				let pieces = pattern.split("?")
+				for (let i = 0; i < pieces.length - 1; i++) {
+					if (pieces[i].length == 0 || pieces[i][pieces[i].length - 1] != "\\") {
+						patternIsInvalid.value = true
+					}
+				}
+			}
+			break
+	}
+}
+
+const requireURL = function (patternType) {
+	return patternType == "wildcard" || patternType == "regexp"
+}
+
+watch(() => addingPattern.value.type, () => {
+	if (patternInputRef.value != null) {
+		setTimeout(() => {
+			patternInputRef.value.focus()
+		})
+	}
+})
+</script>
+
+<template>
+	<div>
+		<div v-show="patterns.length > 0" class="label-group">
+			<TLabel outlined="" v-for="(pattern, index) in patterns" :class="{blue: index == editingIndex, disabled: isAdding && index != editingIndex}">
+				<span class="grey" style="font-weight: normal">[{{ patternTypeName(pattern.type) }}]</span>
+				<span>{{ pattern.pattern }}</span> &nbsp;
+				<a href="" title="修改" @click.prevent="edit(index)"><i class="pi pi-pencil tiny"></i></a> &nbsp;
+				<a href="" title="删除" @click.prevent="remove(index)"><i class="pi pi-remove small"></i></a>
+			</TLabel>
+			<TDivider></TDivider>
+		</div>
+		<div v-show="isAdding" style="margin-top: 0.5em">
+			<div class="ui fields inline">
+				<div class="ui field">
+					<TSelect v-model="addingPattern.type">
+						<TOption value="wildcard">通配符</TOption>
+						<TOption value="regexp">正则表达式</TOption>
+						<TOption value="images">常见图片</TOption>
+						<TOption value="audios">常见音频</TOption>
+						<TOption value="videos">常见视频</TOption>
+					</TSelect>
+				</div>
+				<div class="ui field" v-show="addingPattern.type == 'wildcard' || addingPattern.type ==  'regexp'">
+					<TInputText type="text" :placeholder="(addingPattern.type == 'wildcard') ? '可以使用星号(*)通配符,不区分大小写' : '可以使用正则表达式,不区分大小写'" v-model="addingPattern.pattern" @input="changePattern" size="36" ref="patternInputRef" @keyup.enter="confirm()" @keypress.enter.prevent="1" spellcheck="false"/>
+					<p class="comment" v-if="patternIsInvalid">
+						<span class="red" style="font-weight: normal"><span v-if="addingPattern.type == 'wildcard'">通配符</span><span v-if="addingPattern.type == 'regexp'">正则表达式</span>中不能包含问号(?)及问号以后的内容。</span>
+					</p>
+				</div>
+				<div class="ui field" style="padding-left: 0" v-show="addingPattern.type == 'wildcard' || addingPattern.type ==  'regexp'">
+					<TipIcon content="通配符示例:<br/>单个路径开头:/hello/world/*<br/>单个路径结尾:*/hello/world<br/>包含某个路径:*/article/*<br/>某个域名下的所有URL:*example.com/*<br/>忽略某个扩展名:*.js" v-if="addingPattern.type == 'wildcard'"></TipIcon>
+					<TipIcon content="正则表达式示例:<br/>单个路径开头:^/hello/world<br/>单个路径结尾:/hello/world$<br/>包含某个路径:/article/<br/>匹配某个数字路径:/article/(\\d+)<br/>某个域名下的所有URL:^(http|https)://example.com/" v-if="addingPattern.type == 'regexp'"></TipIcon>
+				</div>
+				<div class="ui field">
+					<TButton secondary="" size="small" :class="{disabled:patternIsInvalid}" type="button" @click.prevent="confirm">确定</TButton> &nbsp;
+					<a href="" title="取消" @click.prevent="cancel"><i class="pi pi-remove small"></i></a>
+				</div>
+			</div>
+		</div>
+		<div v-if=!isAdding style="margin-top: 0.5em">
+			<PlusButton size="small" type="button" @click.prevent="add"></PlusButton>
+		</div>
+	</div>
+</template>
+
+<style scoped>
+</style>

+ 165 - 0
src/components/common/ValuesBox.vue

@@ -0,0 +1,165 @@
+<script setup>
+import TButton from "@/components/ui/TButton.vue";
+import TInputText from "@/components/ui/TInputText.vue";
+import TLabel from "@/components/ui/TLabel.vue";
+import TDivider from "@/components/ui/TDivider.vue";
+import {ref} from "vue";
+import useUtils from "@/utils/utils.js";
+import PlusButton from "@/components/common/PlusButton.vue";
+
+const utils = useUtils()
+
+const props = defineProps(["values", "v-values", "size", "maxlength", "name", "placeholder", "v-allow-empty", "validator"])
+
+const {realValues, isUpdating, isAdding, index, value, isEditing} = (function () {
+	let values = props.values;
+	if (values == null) {
+		values = [];
+	}
+
+	if (props.vValues != null && typeof props.vValues == "object") {
+		values = props.vValues
+	}
+
+	return {
+		"realValues": ref(values),
+		"isUpdating": ref(false),
+		"isAdding": ref(false),
+		"index": ref(0),
+		"value": ref(""),
+		isEditing: ref(false)
+	}
+})()
+
+const emits = defineEmits(["change"])
+
+const valueRef = ref()
+
+const create = function () {
+	isAdding.value = true;
+	setTimeout(function () {
+		valueRef.value.focus();
+	}, 200);
+}
+
+const update = function (_index) {
+	cancel()
+	isUpdating.value = true;
+	index.value = _index;
+	value.value = realValues.value[_index];
+	setTimeout(function () {
+		valueRef.value.focus();
+	}, 200);
+}
+
+const confirm = function () {
+	if (value.value.length == 0) {
+		if (typeof (props.vAllowEmpty) != "boolean" || !props.vAllowEmpty) {
+			return
+		}
+	}
+
+	// validate
+	if (typeof (props.validator) == "function") {
+		let resp = props.validator.call(valueRef, value.value)
+		if (typeof resp == "object") {
+			if (typeof resp.isOk == "boolean" && !resp.isOk) {
+				if (typeof resp.message == "string") {
+					utils.warn(resp.message, function () {
+						valueRef.value.focus();
+					})
+				}
+				return
+			}
+		}
+	}
+
+	if (isUpdating.value) {
+		realValues.value[index.value] = value.value
+	} else {
+		realValues.value.push(value.value);
+	}
+	cancel()
+	emits("change", realValues.value)
+}
+
+const remove = function (index) {
+	realValues.value.$remove(index)
+	emits("change", realValues.value)
+}
+
+const cancel = function () {
+	isUpdating.value = false;
+	isAdding.value = false;
+	value.value = "";
+}
+
+const updateAll = function (values) {
+	realValues.value = values
+}
+
+const addValue = function (v) {
+	realValues.value.push(v)
+}
+
+const startEditing = function () {
+	isEditing.value = !isEditing.value
+}
+
+const allValues = function () {
+	return realValues.value
+}
+
+defineExpose({
+	updateAll,
+	addValue,
+	allValues
+})
+
+</script>
+
+<template>
+	<div>
+		<div v-show="!isEditing && realValues.length > 0" class="label-group">
+			<TLabel v-for="(value, index) in realValues" class="outlined">
+				<span v-if="value.toString().length > 0">{{ value }}</span>
+				<span v-if="value.toString().length == 0" class="disabled">[空]</span>
+			</TLabel>
+			<a href="" @click.prevent="startEditing" style="font-size: 0.8em; margin-left: 0.2em">[修改]</a>
+		</div>
+		<div v-show="isEditing || realValues.length == 0">
+			<div v-if="realValues.length > 0" class="label-group">
+				<TLabel v-for="(value, _index) in realValues" :class="{blue:_index == index && isUpdating}" outlined="">
+					<span v-if="value.toString().length > 0">{{ value }}</span>
+					<span v-if="value.toString().length == 0" class="disabled">[空]</span>
+					<input type="hidden" :name="name" :value="value"/>
+					&nbsp; <a href="" @click.prevent="update(_index)" title="修改"><i class="pi pi-pencil small"></i></a>&nbsp;
+					<a href="" @click.prevent="remove(_index)" title="删除"><i class="pi pi-times small"></i></a>
+				</TLabel>
+				<TDivider></TDivider>
+			</div>
+			<!-- 添加|修改 -->
+			<div v-if="isAdding || isUpdating">
+				<div class="ui fields inline">
+					<div class="ui field">
+						<TInputText type="text" :size="size" :maxlength="maxlength" :placeholder="placeholder"
+									v-model="value" ref="valueRef" @keyup.enter="confirm()" @keypress.enter.prevent="1"/>
+					</div>
+					<div class="ui field">
+						<TButton secondary="" size="small" type="button" @click.prevent="confirm()">确定</TButton>
+					</div>
+					<div class="ui field">
+						<a href="" @click.prevent="cancel()" title="取消"><i class="pi pi-times small"></i></a>
+					</div>
+				</div>
+			</div>
+			<div v-if="!isAdding && !isUpdating">
+				<PlusButton size="small" type="button" @click.prevent="create()"></PlusButton>
+			</div>
+		</div>
+	</div>
+</template>
+
+<style scoped>
+
+</style>

+ 42 - 0
src/components/common/ViewLink.vue

@@ -0,0 +1,42 @@
+<script setup>
+import {inject} from "vue";
+
+const Tea = inject("$Tea")
+const popupState = inject("$popupState", null)
+
+const props = defineProps({
+	href: String,
+	active: Boolean
+})
+
+const onClick = (e) => {
+	if (popupState == null) {
+		return
+	}
+
+	let url = ""
+	if (typeof props.href == "string") {
+		url = props.href
+	}
+
+	if (/^(http|https):\/\//i.test(url)) {
+		return
+	}
+
+	e.preventDefault()
+
+	Tea.reloadViewData(url)
+
+	// TODO 如果是多个不同路径URL之间相互跳转,则以新弹窗的形式打开
+}
+</script>
+
+<template>
+	<a :href="href" @click="onClick" :class="{active: active}">
+		<slot></slot>
+	</a>
+</template>
+
+<style scoped>
+
+</style>

+ 40 - 0
src/components/common/WarningMessage.vue

@@ -0,0 +1,40 @@
+<script setup>
+import TMessage from "@/components/ui/TMessage.vue";
+
+const props = defineProps({
+	disableSpacing: {
+		type: Boolean,
+		default: false
+	}
+})
+</script>
+
+<template>
+	<div class="mb-4" :class="{'mt-4': !disableSpacing}">
+		<TMessage severity="warn" icon="pi pi-exclamation-triangle">
+			<div class="content">
+				<slot></slot>
+			</div>
+		</TMessage>
+	</div>
+</template>
+
+<style scoped>
+:deep(.p-message-content) {
+	position: relative;
+}
+
+:deep(.p-message-content a) {
+	color: var(--p-primary-color);
+}
+
+:deep(.p-message-content a:hover) {
+	color: var(--p-primary-700);
+}
+
+:deep(.p-message-content .pi.pi-remove), :deep(.p-message-content .icon.remove) {
+	position: absolute;
+	right: 1em;
+	top: 0.9em;
+}
+</style>

+ 11 - 0
src/components/form/BinaryCheckbox.vue

@@ -0,0 +1,11 @@
+<script setup>
+import TCheckbox from "@/components/ui/TCheckbox.vue";
+</script>
+
+<template>
+	<TCheckbox binary></TCheckbox>
+</template>
+
+<style scoped>
+
+</style>

+ 62 - 0
src/components/form/Editor.vue

@@ -0,0 +1,62 @@
+<script setup>
+import Editor from "primevue/editor"
+import {ref} from "vue";
+
+// reference: https://quilljs.com/docs/modules/toolbar/
+/**
+ * 	['bold', 'italic', 'underline', 'strike'],
+ * 					[{'header': 1}, {'header': 2}],
+ * 					[{'list': 'ordered'}, {'list': 'bullet'}],
+ * 					[{'indent': '-1'}, {'indent': '+1'}],
+ * 					[{'direction': 'rtl'}],
+ * 					[{'size': ['small', false, 'large', 'huge']}],
+ * 					[{'header': [1, 2, 3, 4, 5, 6, false]}],
+ * 					[{'color': []}, {'background': []}],
+ * 					[{'font': []}],
+ * 					[{'align': []}],
+ * 					['link', 'image'],
+ * 					['clean']
+ */
+
+
+
+const props = defineProps({
+	name: String,
+	modelValue: {
+		type: String,
+		default: ""
+	}
+})
+
+const emits = defineEmits(["update:modelValue"])
+const value = ref(props.modelValue)
+
+const onLoad = ({instance}) => {
+	instance.setContents(instance.clipboard.convert({
+		html: props.modelValue
+	}))
+}
+
+const onChange = (v) => {
+	value.value = v
+	emits("update:modelValue", v)
+}
+</script>
+
+<template>
+	<Editor editorStyle="height: 20em" @load="onLoad" @update:modelValue="onChange">
+		<template #toolbar>
+        <span class="ql-formats">
+            <button v-tooltip.bottom="'Bold'" class="ql-bold"></button>
+            <button v-tooltip.bottom="'Italic'" class="ql-italic"></button>
+            <button v-tooltip.bottom="'Underline'" class="ql-underline"></button>
+			<button v-tooltip.bottom="'Strike'" class="ql-strike"></button>
+        </span>
+		</template>
+	</Editor>
+	<input type="hidden" :name="name" :value="value"/>
+</template>
+
+<style scoped>
+
+</style>

+ 137 - 0
src/components/form/GetForm.vue

@@ -0,0 +1,137 @@
+<script setup>
+import {inject, ref} from "vue";
+import useUtils from "@/utils/utils.js";
+
+const props = defineProps(["data", "action", "method"])
+const Tea = inject("$Tea")
+const popupState = inject("$popupState", null)
+const utils = useUtils()
+const emits = defineEmits(["error", "done"])
+const formRef = ref()
+
+const onSubmit = (e) => {
+	if (!popupState) {
+		return
+	}
+
+	let url = props.action
+	if (typeof url !== "string" || url.length === 0) {
+		if (!popupState) {
+			url = window.location.pathname
+		} else {
+			url = popupState.action.path
+		}
+	}
+
+	let data = props.data
+
+	// submit form if 'data' property undefined
+	if (data === undefined) {
+		if (popupState) {
+			data = {}
+			const formData = new FormData(formRef.value)
+			formData.forEach((value, key) => {
+				data[key] = value
+			})
+		}
+	}
+
+	if (typeof data === "object") {
+		const params = Tea.serialize(data)
+		if (params.length > 0) {
+			if (url.indexOf("?") > 0) {
+				url += "&" + params
+			} else {
+				url += "?" + params
+			}
+		}
+	}
+
+	// submit form if not in popup
+	if (!popupState) {
+		window.location.href = url
+
+		e.preventDefault()
+		return
+	}
+
+	if (e != null) {
+		e.preventDefault()
+	}
+
+	if (typeof props.method === "string" && props.method.length > 0 && props.method.toLowerCase() != "get") {
+		console.error("'GetForm' says: invalid method: '" + props.method + "'")
+		return
+	}
+
+	// TODO 显示加载进度条
+
+	const actionObject = Tea.action(url)
+	actionObject
+		.header("X-From-Front", "1")
+		.error((resp) => {
+			console.error(resp.message)
+			emits("error", resp)
+		})
+		.done(() => {
+			emits("done")
+		})
+		.timeout(30)
+		.success((resp) => {
+			popupState.url = url
+
+			if (resp.data != null && typeof resp.data === "object") {
+				let ctx = window.X_VIEW_CTX
+				if (popupState) {
+					ctx = popupState.ctx
+				}
+				for (const k in resp.data) {
+					ctx[k] = resp.data[k]
+				}
+			}
+		})
+		.alert((message, callback, success) => {
+			if (typeof message === "string" && message.length > 0) {
+				if (success) {
+					utils.success(message, () => {
+						if (typeof callback === "function") {
+							callback()
+						} else {
+							if (props.refresh || props.refresh === "") {
+								Tea.reload()
+							}
+						}
+					})
+				} else {
+					utils.warn(message, () => {
+						if (typeof callback === "function") {
+							callback()
+						} else {
+							if (props.refresh || props.refresh === "") {
+								Tea.reload()
+							}
+						}
+					})
+				}
+			}
+		})
+		.get()
+}
+
+defineExpose({
+	submit(e) {
+		onSubmit(e)
+	}
+})
+
+</script>
+
+<template>
+	<form method="get" class="mb-4" @submit="onSubmit" ref="formRef">
+		<slot></slot>
+	</form>
+</template>
+
+<style scoped>
+
+</style>

+ 183 - 0
src/components/form/PostForm.vue

@@ -0,0 +1,183 @@
+<script setup>
+// 常用属性:
+//   action - 动作或URL
+//   data - 绑定的数据
+//   method - 请求方法
+//   timeout - 超时时间(秒数)
+//   refresh <boolean> - 成功后自动刷新页面
+//
+// 常用事件:
+//   @before - 提交前触发
+//   @success - 成功时触发
+//   @fail - 失败时触发
+//   @error - 错误时触发
+//   @done - 完成时触发(几乎一定会触发)
+//   @progress - 上传进度
+
+// TODO 实现 data-tea-confirm(confirm="确认语")
+
+import {getCurrentInstance, inject, provide, ref} from "vue";
+import useUtils from "@/utils/utils.js";
+import CSRFToken from "@/components/common/CSRFToken.vue";
+
+const props = defineProps(["action", "data", "method", "timeout", "refresh", "onSuccess", "onFail", "onProgress", "disableCSRF"])
+const emits = defineEmits(["before", "progress", "success", "fail", "error", "done"])
+const disableCSRF = ref(props.disableCSRF)
+if (typeof disableCSRF.value === "string") {
+	disableCSRF.value = true
+}
+
+let autoData = false
+let realData = ref(props.data)
+if (props.data === undefined) {
+	autoData = true
+	realData.value = {
+		csrfToken: ""
+	}
+}
+
+provide("postFormData", realData.value)
+provide("$postForm", getCurrentInstance())
+
+const Tea = inject("$Tea")
+const utils = useUtils()
+
+const formRef = ref()
+
+const onSubmit = e => {
+	submit()
+}
+
+const submit = () => {
+	if (typeof props.method === "string" && props.method.length > 0 && props.method.toLowerCase() != "post") {
+		console.error("'PostForm' says: invalid method: '" + props.method + "'")
+		return
+	}
+
+	// emit 'before' event
+	emits("before")
+
+	let action = props.action
+	if (typeof action !== "string") {
+		action = "$"
+	}
+
+	const actionObject = Tea.action(action)
+	if (!autoData) {
+		let data = props.data
+		if (data == null || typeof data !== "object") {
+			data = {}
+		}
+		actionObject.params(data)
+	} else {
+		const formData = new FormData(formRef.value)
+		for (const k in realData.value) {
+			const v = realData.value[k]
+
+			// process files
+			if (v instanceof Array) {
+				let processed = false
+				for (const v2 of v) {
+					if (v2 instanceof File) {
+						formData.append(k, v2)
+						processed = true
+					}
+				}
+				if (processed) {
+					continue
+				}
+			}
+
+			formData.set(k, v)
+		}
+		actionObject.form(formData)
+	}
+
+	// apply timeout
+	if (typeof props.timeout === "number") {
+		actionObject.timeout(props.timeout)
+	} else if (typeof props.timeout === "string") {
+		const numberTimeout = parseFloat(props.timeout)
+		if (!isNaN(numberTimeout)) {
+			actionObject.timeout(numberTimeout)
+		}
+	}
+
+	// success function
+	if (typeof props.onSuccess === "function") {
+		actionObject.success(resp => {
+			emits("success", resp)
+		})
+	}
+
+	// fail function
+	if (typeof props.onFail === "function") {
+		actionObject.fail(resp => {
+			emits("fail", resp)
+		})
+	}
+
+	// progress function
+	if (typeof props.onProgress === "function") {
+		actionObject.progress((loaded, total, event) => {
+			emits("progress", loaded, total, event)
+		})
+	}
+
+	actionObject
+		.error((resp) => {
+			console.error(resp.message)
+			emits("error", resp)
+		})
+		.done(() => {
+			emits("done")
+		})
+		.alert((message, callback, success) => {
+			if (typeof message === "string" && message.length > 0) {
+				if (success) {
+					utils.success(message, () => {
+						if (typeof callback === "function") {
+							callback()
+						} else {
+							if (props.refresh || props.refresh === "") {
+								Tea.reload()
+							}
+						}
+					})
+				} else {
+					utils.warn(message, () => {
+						if (typeof callback === "function") {
+							callback()
+						} else {
+							if (props.refresh || props.refresh === "") {
+								Tea.reload()
+							}
+						}
+					})
+				}
+			}
+		})
+		.post()
+}
+
+defineExpose({
+	get rawForm() {
+		return formRef.value
+	},
+	submit() {
+		setTimeout(submit)
+	}
+})
+</script>
+
+<template>
+	<form @submit.prevent="onSubmit" method="post" ref="formRef">
+		<slot></slot>
+
+		<CSRFToken v-if="realData != null && !disableCSRF" v-model="realData.csrfToken"></CSRFToken>
+	</form>
+</template>
+
+<style scoped>
+
+</style>

+ 13 - 0
src/components/form/SubmitButton.vue

@@ -0,0 +1,13 @@
+<script setup>
+import TButton from "../ui/TButton.vue";
+</script>
+
+<template>
+	<TButton label="保存" type="submit" ref="rootRef">
+		<slot></slot>
+	</TButton>
+</template>
+
+<style scoped>
+
+</style>

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels