Browse Source

AI智能判分系统第一次提交

dengshaobo 3 weeks ago
commit
77ae31dcb1
92 changed files with 14312 additions and 0 deletions
  1. 1 0
      .env.development
  2. 1 0
      .env.production
  3. 24 0
      .gitignore
  4. 24 0
      README.md
  5. 14 0
      index.html
  6. 3192 0
      package-lock.json
  7. 34 0
      package.json
  8. BIN
      public/favicon.ico
  9. 9 0
      src/App.vue
  10. 185 0
      src/api/exam.ts
  11. 35 0
      src/api/login.ts
  12. BIN
      src/assets/bg/no_content_bg.png
  13. BIN
      src/assets/bg/table_no_data.png
  14. BIN
      src/assets/icon/abnormal_icon.png
  15. BIN
      src/assets/icon/abnormal_icon_hover.png
  16. BIN
      src/assets/icon/add_icon.png
  17. BIN
      src/assets/icon/add_icon.webp
  18. BIN
      src/assets/icon/miss_exam.png
  19. BIN
      src/assets/icon/miss_exam_hover.png
  20. BIN
      src/assets/icon/no_scan_icon.png
  21. BIN
      src/assets/icon/no_scan_icon_hover.png
  22. BIN
      src/assets/icon/refresh_default.png
  23. BIN
      src/assets/icon/scan_button_bg.png
  24. BIN
      src/assets/icon/sucess_upload.png
  25. BIN
      src/assets/icon/sucess_upload_hover.png
  26. BIN
      src/assets/icon/version.webp
  27. BIN
      src/assets/login/input_clear.webp
  28. BIN
      src/assets/login/input_eye.webp
  29. BIN
      src/assets/login/input_pass.webp
  30. BIN
      src/assets/login/input_show_pass.webp
  31. BIN
      src/assets/login/input_user.webp
  32. BIN
      src/assets/login/login_bg.webp
  33. BIN
      src/assets/login/login_bg_center.webp
  34. BIN
      src/assets/login/login_email.webp
  35. BIN
      src/assets/login/login_feishu.webp
  36. BIN
      src/assets/login/login_logo.webp
  37. BIN
      src/assets/login/login_wechat.webp
  38. 9 0
      src/assets/school/default_logo.svg
  39. 9 0
      src/assets/school/default_logo_cur.svg
  40. 332 0
      src/components/AddExam.vue
  41. 95 0
      src/components/FiltersItem.vue
  42. 125 0
      src/components/resetPassword.vue
  43. 11 0
      src/env.d.ts
  44. 24 0
      src/main.ts
  45. 86 0
      src/router/index.ts
  46. 51 0
      src/store/exam.ts
  47. 31 0
      src/store/user.ts
  48. 77 0
      src/style.css
  49. 3474 0
      src/styles/common.scss
  50. 165 0
      src/styles/element.scss
  51. 259 0
      src/styles/login.scss
  52. 10 0
      src/types/types.ts
  53. 5 0
      src/types/vue.d.ts
  54. 28 0
      src/utils/common.ts
  55. 24 0
      src/utils/jsencrypt.ts
  56. 69 0
      src/utils/request.ts
  57. 357 0
      src/utils/scanCommon.js
  58. 326 0
      src/utils/scanCommon.ts
  59. 166 0
      src/views/choice/addSchool.vue
  60. 404 0
      src/views/choice/index.vue
  61. 0 0
      src/views/choice/选择题.txt
  62. 161 0
      src/views/essay/addAdmin.vue
  63. 17 0
      src/views/essay/index.vue
  64. 0 0
      src/views/essay/作文.txt
  65. 114 0
      src/views/exam/components/aside.vue
  66. 153 0
      src/views/exam/components/chooseTemplate.vue
  67. 197 0
      src/views/exam/components/scanButton.vue
  68. 167 0
      src/views/exam/components/selectStudent.vue
  69. 371 0
      src/views/exam/components/stepItem.vue
  70. 392 0
      src/views/exam/examList.vue
  71. 24 0
      src/views/exam/index.vue
  72. 631 0
      src/views/exam/question.vue
  73. 258 0
      src/views/exam/scanDetail.vue
  74. 317 0
      src/views/exam/scanList.vue
  75. 30 0
      src/views/exam/score.vue
  76. 0 0
      src/views/exam/考试详情文件夹.txt
  77. 243 0
      src/views/fillblank/SearchMain.vue
  78. 414 0
      src/views/fillblank/addUser.vue
  79. 163 0
      src/views/fillblank/authConfig.vue
  80. 14 0
      src/views/fillblank/index.vue
  81. 183 0
      src/views/fillblank/subjectList.vue
  82. 0 0
      src/views/fillblank/填空题.txt
  83. 113 0
      src/views/layout/components/SiderBar.vue
  84. 385 0
      src/views/layout/components/header.vue
  85. 22 0
      src/views/layout/components/main.vue
  86. 20 0
      src/views/layout/index.vue
  87. 169 0
      src/views/login/login.vue
  88. 8 0
      src/vite-env.d.ts
  89. 29 0
      tsconfig.app.json
  90. 7 0
      tsconfig.json
  91. 27 0
      tsconfig.node.json
  92. 27 0
      vite.config.ts

+ 1 - 0
.env.development

@@ -0,0 +1 @@
+VITE_API_BASE_URL = ""

+ 1 - 0
.env.production

@@ -0,0 +1 @@
+VITE_API_BASE_URL = "https://admin.k12100.com/admin/"

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Logs
+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?

+ 24 - 0
README.md

@@ -0,0 +1,24 @@
+# admin_system
+
+## Project setup
+```
+npm install
+```
+
+### Compiles and hot-reloads for development
+```
+npm run dev
+```
+
+### Compiles and minifies for production
+```
+npm run build
+```
+
+### Lints and fixes files
+```
+npm run lint
+```
+
+### Customize configuration
+See [Configuration Reference](https://cli.vuejs.org/config/).

+ 14 - 0
index.html

@@ -0,0 +1,14 @@
+<!doctype html>
+<html >
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
+    <link rel="stylesheet" href="//at.alicdn.com/t/c/font_4939100_qj6r4mp3lif.css">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>慧教研智能判分系统</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+  </body>
+</html>

+ 3192 - 0
package-lock.json

@@ -0,0 +1,3192 @@
+{
+  "name": "admindata",
+  "version": "0.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "admindata",
+      "version": "0.0.0",
+      "dependencies": {
+        "@element-plus/icons-vue": "^2.3.1",
+        "@tsparticles/slim": "^3.9.1",
+        "@tsparticles/vue3": "^3.0.1",
+        "axios": "^1.9.0",
+        "element-plus": "^2.9.11",
+        "jsencrypt": "^3.5.4",
+        "pinia": "^3.0.4",
+        "vue": "^3.5.13",
+        "vue-router": "^4.5.1",
+        "vuex": "^4.1.0"
+      },
+      "devDependencies": {
+        "@tsconfig/node20": "^20.1.9",
+        "@types/node": "^22.15.26",
+        "@vitejs/plugin-vue": "^5.2.3",
+        "@vue/tsconfig": "^0.7.0",
+        "sass": "^1.89.0",
+        "sass-loader": "^16.0.5",
+        "typescript": "~5.8.3",
+        "vite": "^6.3.5",
+        "vue-tsc": "^2.2.8"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.3",
+      "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.3.tgz",
+      "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@ctrl/tinycolor": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
+      "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@element-plus/icons-vue": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz",
+      "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==",
+      "license": "MIT",
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+      "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+      "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+      "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+      "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+      "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+      "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+      "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+      "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+      "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+      "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+      "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+      "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+      "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+      "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+      "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+      "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+      "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+      "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+      "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+      "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+      "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@floating-ui/core": {
+      "version": "1.7.5",
+      "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz",
+      "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/utils": "^0.2.11"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.7.6",
+      "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz",
+      "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@floating-ui/core": "^1.7.5",
+        "@floating-ui/utils": "^0.2.11"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.11",
+      "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz",
+      "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "license": "MIT"
+    },
+    "node_modules/@parcel/watcher": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.6.tgz",
+      "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "detect-libc": "^2.0.3",
+        "is-glob": "^4.0.3",
+        "node-addon-api": "^7.0.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher-android-arm64": "2.5.6",
+        "@parcel/watcher-darwin-arm64": "2.5.6",
+        "@parcel/watcher-darwin-x64": "2.5.6",
+        "@parcel/watcher-freebsd-x64": "2.5.6",
+        "@parcel/watcher-linux-arm-glibc": "2.5.6",
+        "@parcel/watcher-linux-arm-musl": "2.5.6",
+        "@parcel/watcher-linux-arm64-glibc": "2.5.6",
+        "@parcel/watcher-linux-arm64-musl": "2.5.6",
+        "@parcel/watcher-linux-x64-glibc": "2.5.6",
+        "@parcel/watcher-linux-x64-musl": "2.5.6",
+        "@parcel/watcher-win32-arm64": "2.5.6",
+        "@parcel/watcher-win32-ia32": "2.5.6",
+        "@parcel/watcher-win32-x64": "2.5.6"
+      }
+    },
+    "node_modules/@parcel/watcher-android-arm64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
+      "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-arm64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
+      "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-x64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
+      "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-freebsd-x64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
+      "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-glibc": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
+      "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-musl": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
+      "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-glibc": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
+      "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-musl": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
+      "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-glibc": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
+      "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-musl": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
+      "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-arm64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
+      "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-ia32": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
+      "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-x64": {
+      "version": "2.5.6",
+      "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
+      "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@popperjs/core": {
+      "name": "@sxzz/popperjs-es",
+      "version": "2.11.8",
+      "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz",
+      "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/popperjs"
+      }
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
+      "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
+      "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
+      "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
+      "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
+      "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
+      "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
+      "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
+      "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
+      "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
+      "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
+      "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
+      "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
+      "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
+      "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
+      "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
+      "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
+      "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
+      "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
+      "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
+      "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
+      "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
+      "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
+      "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
+      "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
+      "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@tsconfig/node20": {
+      "version": "20.1.9",
+      "resolved": "https://registry.npmmirror.com/@tsconfig/node20/-/node20-20.1.9.tgz",
+      "integrity": "sha512-IjlTv1RsvnPtUcjTqtVsZExKVq+KQx4g5pCP5tI7rAs6Xesl2qFwSz/tPDBC4JajkL/MlezBu3gPUwqRHl+RIg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@tsparticles/basic": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/basic/-/basic-3.9.1.tgz",
+      "integrity": "sha512-ijr2dHMx0IQHqhKW3qA8tfwrR2XYbbWYdaJMQuBo2CkwBVIhZ76U+H20Y492j/NXpd1FUnt2aC0l4CEVGVGdeQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/matteobruni"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/tsparticles"
+        },
+        {
+          "type": "buymeacoffee",
+          "url": "https://www.buymeacoffee.com/matteobruni"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1",
+        "@tsparticles/move-base": "3.9.1",
+        "@tsparticles/plugin-hex-color": "3.9.1",
+        "@tsparticles/plugin-hsl-color": "3.9.1",
+        "@tsparticles/plugin-rgb-color": "3.9.1",
+        "@tsparticles/shape-circle": "3.9.1",
+        "@tsparticles/updater-color": "3.9.1",
+        "@tsparticles/updater-opacity": "3.9.1",
+        "@tsparticles/updater-out-modes": "3.9.1",
+        "@tsparticles/updater-size": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/engine": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/engine/-/engine-3.9.1.tgz",
+      "integrity": "sha512-DpdgAhWMZ3Eh2gyxik8FXS6BKZ8vyea+Eu5BC4epsahqTGY9V3JGGJcXC6lRJx6cPMAx1A0FaQAojPF3v6rkmQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/matteobruni"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/tsparticles"
+        },
+        {
+          "type": "buymeacoffee",
+          "url": "https://www.buymeacoffee.com/matteobruni"
+        }
+      ],
+      "hasInstallScript": true,
+      "license": "MIT"
+    },
+    "node_modules/@tsparticles/interaction-external-attract": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/interaction-external-attract/-/interaction-external-attract-3.9.1.tgz",
+      "integrity": "sha512-5AJGmhzM9o4AVFV24WH5vSqMBzOXEOzIdGLIr+QJf4fRh9ZK62snsusv/ozKgs2KteRYQx+L7c5V3TqcDy2upg==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/interaction-external-bounce": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/interaction-external-bounce/-/interaction-external-bounce-3.9.1.tgz",
+      "integrity": "sha512-bv05+h70UIHOTWeTsTI1AeAmX6R3s8nnY74Ea6p6AbQjERzPYIa0XY19nq/hA7+Nrg+EissP5zgoYYeSphr85A==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/interaction-external-bubble": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/interaction-external-bubble/-/interaction-external-bubble-3.9.1.tgz",
+      "integrity": "sha512-tbd8ox/1GPl+zr+KyHQVV1bW88GE7OM6i4zql801YIlCDrl9wgTDdDFGIy9X7/cwTvTrCePhrfvdkUamXIribQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/interaction-external-connect": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/interaction-external-connect/-/interaction-external-connect-3.9.1.tgz",
+      "integrity": "sha512-sq8YfUNsIORjXHzzW7/AJQtfi/qDqLnYG2qOSE1WOsog39MD30RzmiOloejOkfNeUdcGUcfsDgpUuL3UhzFUOA==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/interaction-external-grab": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/interaction-external-grab/-/interaction-external-grab-3.9.1.tgz",
+      "integrity": "sha512-QwXza+sMMWDaMiFxd8y2tJwUK6c+nNw554+/9+tEZeTTk2fCbB0IJ7p/TH6ZGWDL0vo2muK54Njv2fEey191ow==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/interaction-external-pause": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/interaction-external-pause/-/interaction-external-pause-3.9.1.tgz",
+      "integrity": "sha512-Gzv4/FeNir0U/tVM9zQCqV1k+IAgaFjDU3T30M1AeAsNGh/rCITV2wnT7TOGFkbcla27m4Yxa+Fuab8+8pzm+g==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/interaction-external-push": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/interaction-external-push/-/interaction-external-push-3.9.1.tgz",
+      "integrity": "sha512-GvnWF9Qy4YkZdx+WJL2iy9IcgLvzOIu3K7aLYJFsQPaxT8d9TF8WlpoMlWKnJID6H5q4JqQuMRKRyWH8aAKyQw==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/interaction-external-remove": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/interaction-external-remove/-/interaction-external-remove-3.9.1.tgz",
+      "integrity": "sha512-yPThm4UDWejDOWW5Qc8KnnS2EfSo5VFcJUQDWc1+Wcj17xe7vdSoiwwOORM0PmNBzdDpSKQrte/gUnoqaUMwOA==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/interaction-external-repulse": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/interaction-external-repulse/-/interaction-external-repulse-3.9.1.tgz",
+      "integrity": "sha512-/LBppXkrMdvLHlEKWC7IykFhzrz+9nebT2fwSSFXK4plEBxDlIwnkDxd3FbVOAbnBvx4+L8+fbrEx+RvC8diAw==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/interaction-external-slow": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/interaction-external-slow/-/interaction-external-slow-3.9.1.tgz",
+      "integrity": "sha512-1ZYIR/udBwA9MdSCfgADsbDXKSFS0FMWuPWz7bm79g3sUxcYkihn+/hDhc6GXvNNR46V1ocJjrj0u6pAynS1KQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/interaction-particles-attract": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/interaction-particles-attract/-/interaction-particles-attract-3.9.1.tgz",
+      "integrity": "sha512-CYYYowJuGwRLUixQcSU/48PTKM8fCUYThe0hXwQ+yRMLAn053VHzL7NNZzKqEIeEyt5oJoy9KcvubjKWbzMBLQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/interaction-particles-collisions": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/interaction-particles-collisions/-/interaction-particles-collisions-3.9.1.tgz",
+      "integrity": "sha512-ggGyjW/3v1yxvYW1IF1EMT15M6w31y5zfNNUPkqd/IXRNPYvm0Z0ayhp+FKmz70M5p0UxxPIQHTvAv9Jqnuj8w==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/interaction-particles-links": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/interaction-particles-links/-/interaction-particles-links-3.9.1.tgz",
+      "integrity": "sha512-MsLbMjy1vY5M5/hu/oa5OSRZAUz49H3+9EBMTIOThiX+a+vpl3sxc9AqNd9gMsPbM4WJlub8T6VBZdyvzez1Vg==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/move-base": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/move-base/-/move-base-3.9.1.tgz",
+      "integrity": "sha512-X4huBS27d8srpxwOxliWPUt+NtCwY+8q/cx1DvQxyqmTA8VFCGpcHNwtqiN+9JicgzOvSuaORVqUgwlsc7h4pQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/move-parallax": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/move-parallax/-/move-parallax-3.9.1.tgz",
+      "integrity": "sha512-whlOR0bVeyh6J/hvxf/QM3DqvNnITMiAQ0kro6saqSDItAVqg4pYxBfEsSOKq7EhjxNvfhhqR+pFMhp06zoCVA==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/plugin-easing-quad": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/plugin-easing-quad/-/plugin-easing-quad-3.9.1.tgz",
+      "integrity": "sha512-C2UJOca5MTDXKUTBXj30Kiqr5UyID+xrY/LxicVWWZPczQW2bBxbIbfq9ULvzGDwBTxE2rdvIB8YFKmDYO45qw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/matteobruni"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/tsparticles"
+        },
+        {
+          "type": "buymeacoffee",
+          "url": "https://www.buymeacoffee.com/matteobruni"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/plugin-hex-color": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/plugin-hex-color/-/plugin-hex-color-3.9.1.tgz",
+      "integrity": "sha512-vZgZ12AjUicJvk7AX4K2eAmKEQX/D1VEjEPFhyjbgI7A65eX72M465vVKIgNA6QArLZ1DLs7Z787LOE6GOBWsg==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/matteobruni"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/tsparticles"
+        },
+        {
+          "type": "buymeacoffee",
+          "url": "https://www.buymeacoffee.com/matteobruni"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/plugin-hsl-color": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/plugin-hsl-color/-/plugin-hsl-color-3.9.1.tgz",
+      "integrity": "sha512-jJd1iGgRwX6eeNjc1zUXiJivaqC5UE+SC2A3/NtHwwoQrkfxGWmRHOsVyLnOBRcCPgBp/FpdDe6DIDjCMO715w==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/matteobruni"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/tsparticles"
+        },
+        {
+          "type": "buymeacoffee",
+          "url": "https://www.buymeacoffee.com/matteobruni"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/plugin-rgb-color": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/plugin-rgb-color/-/plugin-rgb-color-3.9.1.tgz",
+      "integrity": "sha512-SBxk7f1KBfXeTnnklbE2Hx4jBgh6I6HOtxb+Os1gTp0oaghZOkWcCD2dP4QbUu7fVNCMOcApPoMNC8RTFcy9wQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/matteobruni"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/tsparticles"
+        },
+        {
+          "type": "buymeacoffee",
+          "url": "https://www.buymeacoffee.com/matteobruni"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/shape-circle": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/shape-circle/-/shape-circle-3.9.1.tgz",
+      "integrity": "sha512-DqZFLjbuhVn99WJ+A9ajz9YON72RtCcvubzq6qfjFmtwAK7frvQeb6iDTp6Ze9FUipluxVZWVRG4vWTxi2B+/g==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/shape-emoji": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/shape-emoji/-/shape-emoji-3.9.1.tgz",
+      "integrity": "sha512-ifvY63usuT+hipgVHb8gelBHSeF6ryPnMxAAEC1RGHhhXfpSRWMtE6ybr+pSsYU52M3G9+TF84v91pSwNrb9ZQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/shape-image": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/shape-image/-/shape-image-3.9.1.tgz",
+      "integrity": "sha512-fCA5eme8VF3oX8yNVUA0l2SLDKuiZObkijb0z3Ky0qj1HUEVlAuEMhhNDNB9E2iELTrWEix9z7BFMePp2CC7AA==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/shape-line": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/shape-line/-/shape-line-3.9.1.tgz",
+      "integrity": "sha512-wT8NSp0N9HURyV05f371cHKcNTNqr0/cwUu6WhBzbshkYGy1KZUP9CpRIh5FCrBpTev34mEQfOXDycgfG0KiLQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/shape-polygon": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/shape-polygon/-/shape-polygon-3.9.1.tgz",
+      "integrity": "sha512-dA77PgZdoLwxnliH6XQM/zF0r4jhT01pw5y7XTeTqws++hg4rTLV9255k6R6eUqKq0FPSW1/WBsBIl7q/MmrqQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/shape-square": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/shape-square/-/shape-square-3.9.1.tgz",
+      "integrity": "sha512-DKGkDnRyZrAm7T2ipqNezJahSWs6xd9O5LQLe5vjrYm1qGwrFxJiQaAdlb00UNrexz1/SA7bEoIg4XKaFa7qhQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/shape-star": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/shape-star/-/shape-star-3.9.1.tgz",
+      "integrity": "sha512-kdMJpi8cdeb6vGrZVSxTG0JIjCwIenggqk0EYeKAwtOGZFBgL7eHhF2F6uu1oq8cJAbXPujEoabnLsz6mW8XaA==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/slim": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/slim/-/slim-3.9.1.tgz",
+      "integrity": "sha512-CL5cDmADU7sDjRli0So+hY61VMbdroqbArmR9Av+c1Fisa5ytr6QD7Jv62iwU2S6rvgicEe9OyRmSy5GIefwZw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/matteobruni"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/tsparticles"
+        },
+        {
+          "type": "buymeacoffee",
+          "url": "https://www.buymeacoffee.com/matteobruni"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/basic": "3.9.1",
+        "@tsparticles/engine": "3.9.1",
+        "@tsparticles/interaction-external-attract": "3.9.1",
+        "@tsparticles/interaction-external-bounce": "3.9.1",
+        "@tsparticles/interaction-external-bubble": "3.9.1",
+        "@tsparticles/interaction-external-connect": "3.9.1",
+        "@tsparticles/interaction-external-grab": "3.9.1",
+        "@tsparticles/interaction-external-pause": "3.9.1",
+        "@tsparticles/interaction-external-push": "3.9.1",
+        "@tsparticles/interaction-external-remove": "3.9.1",
+        "@tsparticles/interaction-external-repulse": "3.9.1",
+        "@tsparticles/interaction-external-slow": "3.9.1",
+        "@tsparticles/interaction-particles-attract": "3.9.1",
+        "@tsparticles/interaction-particles-collisions": "3.9.1",
+        "@tsparticles/interaction-particles-links": "3.9.1",
+        "@tsparticles/move-parallax": "3.9.1",
+        "@tsparticles/plugin-easing-quad": "3.9.1",
+        "@tsparticles/shape-emoji": "3.9.1",
+        "@tsparticles/shape-image": "3.9.1",
+        "@tsparticles/shape-line": "3.9.1",
+        "@tsparticles/shape-polygon": "3.9.1",
+        "@tsparticles/shape-square": "3.9.1",
+        "@tsparticles/shape-star": "3.9.1",
+        "@tsparticles/updater-life": "3.9.1",
+        "@tsparticles/updater-rotate": "3.9.1",
+        "@tsparticles/updater-stroke-color": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/updater-color": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/updater-color/-/updater-color-3.9.1.tgz",
+      "integrity": "sha512-XGWdscrgEMA8L5E7exsE0f8/2zHKIqnTrZymcyuFBw2DCB6BIV+5z6qaNStpxrhq3DbIxxhqqcybqeOo7+Alpg==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/updater-life": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/updater-life/-/updater-life-3.9.1.tgz",
+      "integrity": "sha512-Oi8aF2RIwMMsjssUkCB6t3PRpENHjdZf6cX92WNfAuqXtQphr3OMAkYFJFWkvyPFK22AVy3p/cFt6KE5zXxwAA==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/updater-opacity": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/updater-opacity/-/updater-opacity-3.9.1.tgz",
+      "integrity": "sha512-w778LQuRZJ+IoWzeRdrGykPYSSaTeWfBvLZ2XwYEkh/Ss961InOxZKIpcS6i5Kp/Zfw0fS1ZAuqeHwuj///Osw==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/updater-out-modes": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/updater-out-modes/-/updater-out-modes-3.9.1.tgz",
+      "integrity": "sha512-cKQEkAwbru+hhKF+GTsfbOvuBbx2DSB25CxOdhtW2wRvDBoCnngNdLw91rs+0Cex4tgEeibkebrIKFDDE6kELg==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/updater-rotate": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/updater-rotate/-/updater-rotate-3.9.1.tgz",
+      "integrity": "sha512-9BfKaGfp28JN82MF2qs6Ae/lJr9EColMfMTHqSKljblwbpVDHte4umuwKl3VjbRt87WD9MGtla66NTUYl+WxuQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/updater-size": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/updater-size/-/updater-size-3.9.1.tgz",
+      "integrity": "sha512-3NSVs0O2ApNKZXfd+y/zNhTXSFeG1Pw4peI8e6z/q5+XLbmue9oiEwoPy/tQLaark3oNj3JU7Q903ZijPyXSzw==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/updater-stroke-color": {
+      "version": "3.9.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/updater-stroke-color/-/updater-stroke-color-3.9.1.tgz",
+      "integrity": "sha512-3x14+C2is9pZYTg9T2TiA/aM1YMq4wLdYaZDcHm3qO30DZu5oeQq0rm/6w+QOGKYY1Z3Htg9rlSUZkhTHn7eDA==",
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "3.9.1"
+      }
+    },
+    "node_modules/@tsparticles/vue3": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/@tsparticles/vue3/-/vue3-3.0.1.tgz",
+      "integrity": "sha512-BxaSZ0wtxq33SDsrqLkLWoV88Jd5BnBoYjyVhKSNzOLOesCiG8Z5WQC1QZGTez79l/gBe0xaCDF0ng1e2iKJvA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/matteobruni"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/tsparticles"
+        },
+        {
+          "type": "buymeacoffee",
+          "url": "https://www.buymeacoffee.com/matteobruni"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@tsparticles/engine": "^3.0.3",
+        "vue": "^3.3.13"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/lodash": {
+      "version": "4.17.24",
+      "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz",
+      "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
+      "license": "MIT"
+    },
+    "node_modules/@types/lodash-es": {
+      "version": "4.17.12",
+      "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+      "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/lodash": "*"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "22.19.19",
+      "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.19.tgz",
+      "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.21.0"
+      }
+    },
+    "node_modules/@types/web-bluetooth": {
+      "version": "0.0.21",
+      "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+      "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
+      "license": "MIT"
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "5.2.4",
+      "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
+      "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^5.0.0 || ^6.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@volar/language-core": {
+      "version": "2.4.15",
+      "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz",
+      "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/source-map": "2.4.15"
+      }
+    },
+    "node_modules/@volar/source-map": {
+      "version": "2.4.15",
+      "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz",
+      "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@volar/typescript": {
+      "version": "2.4.15",
+      "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz",
+      "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.15",
+        "path-browserify": "^1.0.1",
+        "vscode-uri": "^3.0.8"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.34.tgz",
+      "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.3",
+        "@vue/shared": "3.5.34",
+        "entities": "^7.0.1",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz",
+      "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.34",
+        "@vue/shared": "3.5.34"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz",
+      "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.3",
+        "@vue/compiler-core": "3.5.34",
+        "@vue/compiler-dom": "3.5.34",
+        "@vue/compiler-ssr": "3.5.34",
+        "@vue/shared": "3.5.34",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.21",
+        "postcss": "^8.5.14",
+        "source-map-js": "^1.2.1"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz",
+      "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.34",
+        "@vue/shared": "3.5.34"
+      }
+    },
+    "node_modules/@vue/compiler-vue2": {
+      "version": "2.7.16",
+      "resolved": "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
+      "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "de-indent": "^1.0.2",
+        "he": "^1.2.0"
+      }
+    },
+    "node_modules/@vue/devtools-api": {
+      "version": "6.6.4",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+      "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/devtools-kit": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
+      "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-shared": "^7.7.9",
+        "birpc": "^2.3.0",
+        "hookable": "^5.5.3",
+        "mitt": "^3.0.1",
+        "perfect-debounce": "^1.0.0",
+        "speakingurl": "^14.0.1",
+        "superjson": "^2.2.2"
+      }
+    },
+    "node_modules/@vue/devtools-shared": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
+      "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
+      "license": "MIT",
+      "dependencies": {
+        "rfdc": "^1.4.1"
+      }
+    },
+    "node_modules/@vue/language-core": {
+      "version": "2.2.12",
+      "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz",
+      "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/language-core": "2.4.15",
+        "@vue/compiler-dom": "^3.5.0",
+        "@vue/compiler-vue2": "^2.7.16",
+        "@vue/shared": "^3.5.0",
+        "alien-signals": "^1.0.3",
+        "minimatch": "^9.0.3",
+        "muggle-string": "^0.4.1",
+        "path-browserify": "^1.0.1"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vue/reactivity": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.34.tgz",
+      "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.34"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.34.tgz",
+      "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.34",
+        "@vue/shared": "3.5.34"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz",
+      "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.34",
+        "@vue/runtime-core": "3.5.34",
+        "@vue/shared": "3.5.34",
+        "csstype": "^3.2.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.34.tgz",
+      "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.34",
+        "@vue/shared": "3.5.34"
+      },
+      "peerDependencies": {
+        "vue": "3.5.34"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.34.tgz",
+      "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==",
+      "license": "MIT"
+    },
+    "node_modules/@vue/tsconfig": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.7.0.tgz",
+      "integrity": "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "typescript": "5.x",
+        "vue": "^3.4.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        },
+        "vue": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vueuse/core": {
+      "version": "14.3.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.3.0.tgz",
+      "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/web-bluetooth": "^0.0.21",
+        "@vueuse/metadata": "14.3.0",
+        "@vueuse/shared": "14.3.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/@vueuse/metadata": {
+      "version": "14.3.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.3.0.tgz",
+      "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/@vueuse/shared": {
+      "version": "14.3.0",
+      "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.3.0.tgz",
+      "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz",
+      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/alien-signals": {
+      "version": "1.0.13",
+      "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz",
+      "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/async-validator": {
+      "version": "4.2.5",
+      "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
+      "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/axios": {
+      "version": "1.16.1",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-1.16.1.tgz",
+      "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.16.0",
+        "form-data": "^4.0.5",
+        "https-proxy-agent": "^5.0.1",
+        "proxy-from-env": "^2.1.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/birpc": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz",
+      "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz",
+      "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz",
+      "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 14.16.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/copy-anything": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.0.5.tgz",
+      "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
+      "license": "MIT",
+      "dependencies": {
+        "is-what": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
+      "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+      "license": "MIT"
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.20",
+      "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz",
+      "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
+      "license": "MIT"
+    },
+    "node_modules/de-indent": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz",
+      "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/element-plus": {
+      "version": "2.14.0",
+      "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.0.tgz",
+      "integrity": "sha512-POgH+TtoreaEKWqYYAVQyE6i8rQMEFqAEublyF29dBA5yASWPLKY6EzfeqBTr2Uv26mPss4vSrMrNPyaK7LX5w==",
+      "license": "MIT",
+      "dependencies": {
+        "@ctrl/tinycolor": "^4.2.0",
+        "@element-plus/icons-vue": "^2.3.2",
+        "@floating-ui/dom": "^1.0.1",
+        "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8",
+        "@types/lodash": "^4.17.24",
+        "@types/lodash-es": "^4.17.12",
+        "@vueuse/core": "14.3.0",
+        "async-validator": "^4.2.5",
+        "dayjs": "^1.11.20",
+        "lodash": "^4.18.1",
+        "lodash-es": "^4.18.1",
+        "lodash-unified": "^1.0.3",
+        "memoize-one": "^6.0.0",
+        "normalize-wheel-es": "^1.2.0",
+        "vue-component-type-helpers": "^3.2.8"
+      },
+      "peerDependencies": {
+        "vue": "^3.3.7"
+      }
+    },
+    "node_modules/entities": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
+      "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz",
+      "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.12",
+        "@esbuild/android-arm": "0.25.12",
+        "@esbuild/android-arm64": "0.25.12",
+        "@esbuild/android-x64": "0.25.12",
+        "@esbuild/darwin-arm64": "0.25.12",
+        "@esbuild/darwin-x64": "0.25.12",
+        "@esbuild/freebsd-arm64": "0.25.12",
+        "@esbuild/freebsd-x64": "0.25.12",
+        "@esbuild/linux-arm": "0.25.12",
+        "@esbuild/linux-arm64": "0.25.12",
+        "@esbuild/linux-ia32": "0.25.12",
+        "@esbuild/linux-loong64": "0.25.12",
+        "@esbuild/linux-mips64el": "0.25.12",
+        "@esbuild/linux-ppc64": "0.25.12",
+        "@esbuild/linux-riscv64": "0.25.12",
+        "@esbuild/linux-s390x": "0.25.12",
+        "@esbuild/linux-x64": "0.25.12",
+        "@esbuild/netbsd-arm64": "0.25.12",
+        "@esbuild/netbsd-x64": "0.25.12",
+        "@esbuild/openbsd-arm64": "0.25.12",
+        "@esbuild/openbsd-x64": "0.25.12",
+        "@esbuild/openharmony-arm64": "0.25.12",
+        "@esbuild/sunos-x64": "0.25.12",
+        "@esbuild/win32-arm64": "0.25.12",
+        "@esbuild/win32-ia32": "0.25.12",
+        "@esbuild/win32-x64": "0.25.12"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
+      "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+      "license": "MIT"
+    },
+    "node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "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.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz",
+      "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "he": "bin/he"
+      }
+    },
+    "node_modules/hookable": {
+      "version": "5.5.3",
+      "resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
+      "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+      "license": "MIT"
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "6",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/immutable": {
+      "version": "5.1.5",
+      "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.5.tgz",
+      "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-what": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz",
+      "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mesqueeb"
+      }
+    },
+    "node_modules/jsencrypt": {
+      "version": "3.5.4",
+      "resolved": "https://registry.npmmirror.com/jsencrypt/-/jsencrypt-3.5.4.tgz",
+      "integrity": "sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA==",
+      "license": "MIT"
+    },
+    "node_modules/lodash": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz",
+      "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-es": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz",
+      "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+      "license": "MIT"
+    },
+    "node_modules/lodash-unified": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz",
+      "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+      "license": "MIT",
+      "peerDependencies": {
+        "@types/lodash-es": "*",
+        "lodash": "*",
+        "lodash-es": "*"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.21",
+      "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
+      "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/memoize-one": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
+      "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+      "license": "MIT"
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.9",
+      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz",
+      "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/mitt": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
+      "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+      "license": "MIT"
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/muggle-string": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz",
+      "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.12",
+      "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz",
+      "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/neo-async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz",
+      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/node-addon-api": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz",
+      "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/normalize-wheel-es": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+      "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/path-browserify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
+      "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/perfect-debounce": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+      "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz",
+      "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pinia": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz",
+      "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^7.7.7"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.5.0",
+        "vue": "^3.5.11"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pinia/node_modules/@vue/devtools-api": {
+      "version": "7.7.9",
+      "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
+      "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-kit": "^7.7.9"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.14",
+      "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.14.tgz",
+      "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+      "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"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/proxy-from-env": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+      "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz",
+      "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.18.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+      "license": "MIT"
+    },
+    "node_modules/rollup": {
+      "version": "4.60.4",
+      "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.60.4.tgz",
+      "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.60.4",
+        "@rollup/rollup-android-arm64": "4.60.4",
+        "@rollup/rollup-darwin-arm64": "4.60.4",
+        "@rollup/rollup-darwin-x64": "4.60.4",
+        "@rollup/rollup-freebsd-arm64": "4.60.4",
+        "@rollup/rollup-freebsd-x64": "4.60.4",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
+        "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
+        "@rollup/rollup-linux-arm64-gnu": "4.60.4",
+        "@rollup/rollup-linux-arm64-musl": "4.60.4",
+        "@rollup/rollup-linux-loong64-gnu": "4.60.4",
+        "@rollup/rollup-linux-loong64-musl": "4.60.4",
+        "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
+        "@rollup/rollup-linux-ppc64-musl": "4.60.4",
+        "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
+        "@rollup/rollup-linux-riscv64-musl": "4.60.4",
+        "@rollup/rollup-linux-s390x-gnu": "4.60.4",
+        "@rollup/rollup-linux-x64-gnu": "4.60.4",
+        "@rollup/rollup-linux-x64-musl": "4.60.4",
+        "@rollup/rollup-openbsd-x64": "4.60.4",
+        "@rollup/rollup-openharmony-arm64": "4.60.4",
+        "@rollup/rollup-win32-arm64-msvc": "4.60.4",
+        "@rollup/rollup-win32-ia32-msvc": "4.60.4",
+        "@rollup/rollup-win32-x64-gnu": "4.60.4",
+        "@rollup/rollup-win32-x64-msvc": "4.60.4",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/sass": {
+      "version": "1.99.0",
+      "resolved": "https://registry.npmmirror.com/sass/-/sass-1.99.0.tgz",
+      "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chokidar": "^4.0.0",
+        "immutable": "^5.1.5",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher": "^2.4.1"
+      }
+    },
+    "node_modules/sass-loader": {
+      "version": "16.0.8",
+      "resolved": "https://registry.npmmirror.com/sass-loader/-/sass-loader-16.0.8.tgz",
+      "integrity": "sha512-hcov4ZwZJIGbEuyNr9EmiTmZueyrxSToE6GOzoZnq5JM7ecRO7ttyvilPn+VmRsqiP16+VYZzVnGZj/hzZgKBA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "neo-async": "^2.6.2"
+      },
+      "engines": {
+        "node": ">= 18.12.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0",
+        "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
+        "sass": "^1.3.0",
+        "sass-embedded": "*",
+        "webpack": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@rspack/core": {
+          "optional": true
+        },
+        "node-sass": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "webpack": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/speakingurl": {
+      "version": "14.0.1",
+      "resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
+      "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/superjson": {
+      "version": "2.2.6",
+      "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz",
+      "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
+      "license": "MIT",
+      "dependencies": {
+        "copy-anything": "^4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.16",
+      "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz",
+      "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.8.3",
+      "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz",
+      "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
+      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vite": {
+      "version": "6.4.2",
+      "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz",
+      "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.25.0",
+        "fdir": "^6.4.4",
+        "picomatch": "^4.0.2",
+        "postcss": "^8.5.3",
+        "rollup": "^4.34.9",
+        "tinyglobby": "^0.2.13"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0",
+        "jiti": ">=1.21.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vscode-uri": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz",
+      "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vue": {
+      "version": "3.5.34",
+      "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.34.tgz",
+      "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.34",
+        "@vue/compiler-sfc": "3.5.34",
+        "@vue/runtime-dom": "3.5.34",
+        "@vue/server-renderer": "3.5.34",
+        "@vue/shared": "3.5.34"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-component-type-helpers": {
+      "version": "3.2.9",
+      "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.2.9.tgz",
+      "integrity": "sha512-S3BiWYaLSzHxTpln665ELSrMR9UYmrIDUmhik7nVZxmJjTKL2/a+ew1hvGxksKelivm0ujjWfG1fYOiU/2e8rA==",
+      "license": "MIT"
+    },
+    "node_modules/vue-router": {
+      "version": "4.6.4",
+      "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz",
+      "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.5.0"
+      }
+    },
+    "node_modules/vue-tsc": {
+      "version": "2.2.12",
+      "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz",
+      "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@volar/typescript": "2.4.15",
+        "@vue/language-core": "2.2.12"
+      },
+      "bin": {
+        "vue-tsc": "bin/vue-tsc.js"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.0"
+      }
+    },
+    "node_modules/vuex": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/vuex/-/vuex-4.1.0.tgz",
+      "integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.0.0-beta.11"
+      },
+      "peerDependencies": {
+        "vue": "^3.2.0"
+      }
+    }
+  }
+}

+ 34 - 0
package.json

@@ -0,0 +1,34 @@
+{
+  "name": "admindata",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.3.1",
+    "@tsparticles/slim": "^3.9.1",
+    "@tsparticles/vue3": "^3.0.1",
+    "axios": "^1.9.0",
+    "element-plus": "^2.9.11",
+    "jsencrypt": "^3.5.4",
+    "pinia": "^3.0.4",
+    "vue": "^3.5.13",
+    "vue-router": "^4.5.1",
+    "vuex": "^4.1.0"
+  },
+  "devDependencies": {
+    "@tsconfig/node20": "^20.1.9",
+    "@types/node": "^22.15.26",
+    "@vitejs/plugin-vue": "^5.2.3",
+    "@vue/tsconfig": "^0.7.0",
+    "sass": "^1.89.0",
+    "sass-loader": "^16.0.5",
+    "typescript": "~5.8.3",
+    "vite": "^6.3.5",
+    "vue-tsc": "^2.2.8"
+  }
+}

BIN
public/favicon.ico


+ 9 - 0
src/App.vue

@@ -0,0 +1,9 @@
+
+<template>
+  <router-view />
+</template>
+<script setup lang="ts">
+</script>
+<style scoped>
+
+</style>

+ 185 - 0
src/api/exam.ts

@@ -0,0 +1,185 @@
+// src/api/exam.ts  考试相关的接口
+import request from '../utils/request.ts'
+import type { ApiResponse } from '@/types/types' // 引入类型
+
+
+// 新建编辑考试接口
+export const createExamSubject = (data:any): Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/ai_home/create_exam_subject',
+    method: 'post',
+    data
+  })
+}
+
+// 获取头部筛选信息
+export const getHeadData = ():Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/ai_home/find_smart_head_data',
+    method: 'get'
+  })
+}
+
+
+// 获取选择题智能判分首页列表
+export const getExamList = (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/ai_home/query_smart_exam_page',
+    method: 'get',
+    params: data 
+  })
+}
+
+
+// 获取考试类型相关数据
+export const getExamTypeData= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/common_exam/find_all_info_under_school',
+    method: 'get',
+    params: data 
+  })
+}
+
+//删除考试接口
+export const deleteExam= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/ai_home/remove_ai_exam',
+    method: 'post',
+    data
+  })
+}
+
+//获取模版信息列表
+export const getTemplateList= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/smart_template/query_template_by_page',
+    method: 'get',
+    params: data 
+  })
+}
+
+
+//更换模板 使用模版
+export const useTemplate= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/smart_question/use_smart_template',
+    method: 'post',
+    data
+  })
+}
+
+ //获取试题列表
+ export const getSmartQuestionList= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/smart_question/find_question_tbl',
+    method: 'get',
+    params: data 
+  })
+}
+
+//试题结构 设置题目
+export const setQuestion= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/smart_question/set_question',
+    method: 'post',
+    data
+  })
+}
+
+//试题结构 查询模板信息
+export const getTemplateInfo= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/smart_template/find_ai_exam_template',
+    method: 'get',
+    params: data 
+  })
+}
+
+
+//试题结构 删除所有题目
+export const deleteAllQuestion= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/smart_question/remove_all_question',
+    method: 'post',
+    data
+  })
+}
+
+//试题结构 删除单个题目
+export const deleteSingleQuestion= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/smart_question/remove_single_question',
+    method: 'post',
+    data
+  })
+}
+
+
+//试题结构  保存题目数据
+export const saveQuestion= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/smart_question/finish_smart_question',
+    method: 'post',
+    data
+  })
+} 
+
+//扫描学生 查询是否导入了学生名单
+export const hasImportStudent= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/ai_exam_scan/has_import_student',
+    method: 'get',
+    params: data 
+  })
+}
+
+//扫描学生 查询班级名单
+export const getClassList= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/ai_exam_room/find_class',
+    method: 'get',
+    params: data 
+  })
+}
+
+
+//扫描学生 使用班级名单
+export const useClassList= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/ai_exam_room/use_sys_class_roster',
+    method: 'post',
+    data
+  })
+}
+
+//扫描学生 查询考场列表数据
+export const getExamRoomList= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/ai_exam_room/find_exam_room',
+    method: 'get',
+    params: data 
+  })
+}
+
+//扫描学生 查询考场的学生列表数据
+export const getExamRoomStudentList= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/ai_exam_room/find_scan_student_by_exam_room',
+    method: 'get',
+    params: data 
+  })
+}
+
+
+//扫描学生  删除单个未扫描的学生
+export const deleteExamRoomStudent= (data:any):Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/ai_exam_room/delete_not_scan_student',
+    method: 'post',
+    data
+  })
+}
+
+
+
+

+ 35 - 0
src/api/login.ts

@@ -0,0 +1,35 @@
+// src/api/login.ts
+import request from '../utils/request.ts'
+import type { ApiResponse } from '@/types/types' // 引入类型
+// 登录接口
+export const login = (data:any): Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/teach_login/sign_in',
+    method: 'post',
+    data
+  })
+}
+
+// 获取用户信息
+export const getUserInfo = ():Promise<ApiResponse> => {
+  return request({
+    url: '/api/v1/pc_personal/find_user_info',
+    method: 'get'
+  })
+}
+// 退出登录
+export const loginOut = (): Promise<ApiResponse>  => {
+  return request({
+    url: `/api/v1/teach_login/sign_out`,
+    method: 'post'
+  })
+}
+// 修改密码
+export const changePassWord = (data: any) => {
+  return request({
+    url: `/adminApi/v1/sys/users/updatePassword`,
+    method: 'post',
+    data
+  })
+}
+

BIN
src/assets/bg/no_content_bg.png


BIN
src/assets/bg/table_no_data.png


BIN
src/assets/icon/abnormal_icon.png


BIN
src/assets/icon/abnormal_icon_hover.png


BIN
src/assets/icon/add_icon.png


BIN
src/assets/icon/add_icon.webp


BIN
src/assets/icon/miss_exam.png


BIN
src/assets/icon/miss_exam_hover.png


BIN
src/assets/icon/no_scan_icon.png


BIN
src/assets/icon/no_scan_icon_hover.png


BIN
src/assets/icon/refresh_default.png


BIN
src/assets/icon/scan_button_bg.png


BIN
src/assets/icon/sucess_upload.png


BIN
src/assets/icon/sucess_upload_hover.png


BIN
src/assets/icon/version.webp


BIN
src/assets/login/input_clear.webp


BIN
src/assets/login/input_eye.webp


BIN
src/assets/login/input_pass.webp


BIN
src/assets/login/input_show_pass.webp


BIN
src/assets/login/input_user.webp


BIN
src/assets/login/login_bg.webp


BIN
src/assets/login/login_bg_center.webp


BIN
src/assets/login/login_email.webp


BIN
src/assets/login/login_feishu.webp


BIN
src/assets/login/login_logo.webp


BIN
src/assets/login/login_wechat.webp


+ 9 - 0
src/assets/school/default_logo.svg

@@ -0,0 +1,9 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="24" cy="24" r="22" fill="#EFEFEF"/>
+<path d="M13 18.1142C13 16.8463 13.797 15.7154 14.991 15.2889L24.3273 11.9545C25.6298 11.4894 27 12.4549 27 13.838V35H13V18.1142Z" fill="#D2D2D2"/>
+<path d="M27 18H32C33.6569 18 35 19.3431 35 21V35H27V18Z" fill="#DDDDDD"/>
+<rect x="17" y="20" width="6" height="1.5" rx="0.75" fill="#EFEFEF"/>
+<rect x="10" y="34" width="28" height="2" rx="1" fill="#D2D2D2"/>
+<rect x="17" y="23.5" width="6" height="1.5" rx="0.75" fill="#EFEFEF"/>
+<rect x="17" y="27" width="6" height="1.5" rx="0.75" fill="#EFEFEF"/>
+</svg>

+ 9 - 0
src/assets/school/default_logo_cur.svg

@@ -0,0 +1,9 @@
+<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
+<circle cx="24" cy="24" r="22" fill="#C2D2FF"/>
+<path d="M13 18.1142C13 16.8463 13.797 15.7154 14.991 15.2889L24.3273 11.9545C25.6298 11.4894 27 12.4549 27 13.838V35H13V18.1142Z" fill="#6BA3FE"/>
+<path d="M27 18H32C33.6569 18 35 19.3431 35 21V35H27V18Z" fill="#83B2FF"/>
+<rect x="17" y="20" width="6" height="1.5" rx="0.75" fill="#C2D2FF"/>
+<rect x="10" y="34" width="28" height="2" rx="1" fill="#6BA3FE"/>
+<rect x="17" y="23.5" width="6" height="1.5" rx="0.75" fill="#C2D2FF"/>
+<rect x="17" y="27" width="6" height="1.5" rx="0.75" fill="#C2D2FF"/>
+</svg>

+ 332 - 0
src/components/AddExam.vue

@@ -0,0 +1,332 @@
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    title="新增考试"
+    width="480px"
+    class="page_dialog"
+    :before-close="handleClose"
+  >
+    <el-form ref="formRef"   :model="formData"  :rules="rules"  label-width="100px">
+      <el-form-item label="考试名称:" prop="examSubjectName">
+        <el-input v-model="formData.examSubjectName" placeholder="请输入考试名称" style="width: 300px;"/>
+      </el-form-item>
+      
+      <el-form-item label="考试年级:" prop="gradeCode">
+        <el-radio-group v-model="formData.gradeCode" @change="HandleGradeChange">
+          <el-radio :value="grade.gradeCode"  v-for="grade in gradeList">{{grade.gradeName}}</el-radio>
+
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="考试科目:" prop="courseCode">
+        <el-radio-group v-model="formData.courseCode" @change="HandleCourseChange">
+          <el-radio :value="subject.courseCode"  v-for="subject in subjectList">{{subject.courseName}}</el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="HandleCancel">取消</el-button>
+        <el-button type="primary" :loading="loading" @click="HandleSubmit">确定</el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, computed, watch } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { ElMessage } from 'element-plus'
+// 1. 确保导入接口函数,请根据实际路径调整
+import { getExamTypeData,createExamSubject } from '@/api/exam' 
+// 定义 Props 和 Emits
+const props = defineProps<{
+  modelValue: boolean,
+  editData: Record<string, any> | null ,
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: boolean): void
+  (e: 'success'): void
+}>()
+
+// 弹窗显示状态的双向绑定
+const dialogVisible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+})
+
+// 表单引用
+const formRef = ref<FormInstance>()
+
+//定义用于存储下拉菜单选项的响应式数据
+const gradeList=ref<any[]>([]);
+const subjectList=ref<any[]>([]);
+// 表单数据
+const formData = reactive({
+    id:'',//考试科目列表id 不为空表示修改
+    schoolYearId:'',//学年id
+    schoolYearGradeId:'',//学年表id
+    schoolYearCode:'',//学年code
+    levelCode:'',//学段code
+    levelName:'',//学段名称
+    graduates:'',//毕业年份
+    gradeCode:'',//年级code
+    gradeName:'',//年级名称
+    examSubjectName:'',//考试科目名称
+    courseCode:'',//科目code
+    courseName:'',//科目名称
+})
+// 重置表单
+const ResetForm = () => {
+  if (formRef.value) {
+    formRef.value.resetFields()
+  }
+  Object.assign(formData, {
+    id:'',//考试科目列表id 不为空表示修改
+    schoolYearId:'',//学年id
+    schoolYearGradeId:'',//学年表id
+    schoolYearCode:'',//学年code
+    levelCode:'',//学段code
+    levelName:'',//学段名称
+    graduates:'',//毕业年份
+    gradeCode:'',//年级code
+    gradeName:'',//年级名称
+    examSubjectName:'',//考试科目名称
+    courseCode:'',//科目code
+    courseName:'',//科目名称
+  })
+}
+//监听数据变化
+watch(() => props.editData, (newData) => { 
+  if (newData) 
+  {
+    // --- 编辑模式:回填数据 ---
+    console.log('编辑模式,回填数据:', newData);
+    //编辑模式 回填数据
+    Object.assign(formData, newData)
+    console.log('回填数据:', formData)
+    // 2. 关键:根据回填的 gradeCode,手动更新 subjectList 和其他关联字段
+      // 因为 HandleGradeChange 是用户交互触发的,程序赋值不会触发它,
+      // 所以我们需要手动执行一次类似的逻辑,确保科目列表正确显示,且隐藏字段完整
+      if (formData.gradeCode && gradeList.value.length > 0) {
+        const selectedGrade = gradeList.value.find((item: any) => item.gradeCode === formData.gradeCode);
+        if (selectedGrade) {
+          subjectList.value = selectedGrade.gradeCourseVOS || [];
+          
+          // 补全隐藏字段(确保提交时数据完整)
+          formData.schoolYearId = selectedGrade.schoolYearId;
+          formData.schoolYearGradeId = selectedGrade.id;
+          formData.schoolYearCode = selectedGrade.schoolYearCode;
+          formData.levelCode = selectedGrade.levelCode;
+          const code = String(selectedGrade.levelCode);
+          formData.levelName = LEVEL_NAME_MAP[code] || '';
+          formData.graduates = selectedGrade.graduates;
+          formData.gradeName = selectedGrade.gradeName;
+
+          // 补全 courseName (防止后端没传或数据不一致)
+          if (formData.courseCode) {
+            const selectedCourse = subjectList.value.find((item: any) => item.courseCode === formData.courseCode);
+            if (selectedCourse) {
+              formData.courseName = selectedCourse.courseName;
+            }
+          }
+        }
+      }
+  }
+  else
+  {
+    //新增模式 清空表单数据
+    ResetForm()
+  }
+},{ immediate: true } // 【关键】立即执行,确保组件挂载时就能处理初始 prop
+)
+
+// 表单验证规则
+const rules = reactive<FormRules>({
+  examSubjectName: [
+    { required: true, message: '请输入考试名称', trigger: 'blur' },
+    { min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' }
+  ],
+  gradeCode: [
+    { required: true, message: '请选择考试年级', trigger: 'change' }
+  ],
+  courseCode: [
+    { required: true, message: '请选择考试科目', trigger: 'change' }
+  ]
+})
+
+const loading=ref(false);//加载状态
+
+// 关闭弹窗前的处理
+const handleClose = (done: () => void) => {
+  // 可以在这里添加确认关闭的逻辑
+  done()
+}
+
+// 获取下拉菜单选项
+const GetExamType=async()=>{
+  const res:any = await getExamTypeData({});
+  console.log("打印获取的结果",res)
+  if(res.code==200 && res.data)
+  {
+    gradeList.value=res.data.schoolGrade;
+    console.log("打印formData",formData)
+    // if(props.editData==null)
+    // {
+    //   formData.gradeCode=res.data.schoolGrade[0].gradeCode;
+    //   subjectList.value=res.data.schoolGrade[0].gradeCourseVOS || [];
+    //   formData.courseCode=res.data.schoolGrade[0].gradeCourseVOS[0].courseCode;
+    // }
+
+    // 如果当前是编辑模式,且已经回填了 formData,但 subjectList 还没更新
+    // 说明 watch(editData) 先于 GetExamType 执行了,这里需要手动补救一下
+    if (props.editData && formData.gradeCode) {
+       const selectedGrade = gradeList.value.find((item: any) => item.gradeCode === formData.gradeCode);
+       if (selectedGrade) {
+          subjectList.value = selectedGrade.gradeCourseVOS || [];
+          // 再次补全隐藏字段,确保万无一失
+          formData.schoolYearId = selectedGrade.schoolYearId;
+          formData.schoolYearGradeId = selectedGrade.id;
+          formData.schoolYearCode = selectedGrade.schoolYearCode;
+          formData.levelCode = selectedGrade.levelCode;
+          const code = String(selectedGrade.levelCode);
+          formData.levelName = LEVEL_NAME_MAP[code] || '';
+          formData.graduates = selectedGrade.graduates;
+          formData.gradeName = selectedGrade.gradeName;
+          
+          if (formData.courseCode) {
+            const selectedCourse = subjectList.value.find((item: any) => item.courseCode === formData.courseCode);
+            if (selectedCourse) {
+              formData.courseName = selectedCourse.courseName;
+            }
+          }
+       }
+    } 
+    // 如果是新增模式,自动选中第一个
+    else if (!props.editData && !formData.gradeCode && gradeList.value.length > 0) {
+       const firstGrade = gradeList.value[0];
+       subjectList.value = firstGrade.gradeCourseVOS || [];
+       formData.gradeCode = firstGrade.gradeCode;
+       formData.schoolYearId = firstGrade.schoolYearId;
+       formData.schoolYearGradeId = firstGrade.id;
+       formData.schoolYearCode = firstGrade.schoolYearCode;
+       formData.levelCode = firstGrade.levelCode;
+       const code = String(firstGrade.levelCode);
+       formData.levelName = LEVEL_NAME_MAP[code] || '';
+       formData.graduates = firstGrade.graduates;
+       formData.gradeName = firstGrade.gradeName;
+       if (subjectList.value.length > 0) {
+         formData.courseCode = subjectList.value[0].courseCode;
+         formData.courseName = subjectList.value[0].courseName;
+       }
+    }
+   
+  }
+}
+//监听弹窗打开,调用接口
+watch(dialogVisible, (val) => { 
+  if (val) 
+  {
+    //弹窗打开时获取数据
+    GetExamType()
+  }
+})
+// 学段名称映射表
+const LEVEL_NAME_MAP: Record<string, string> = {
+  '1': '小学',
+  '2': '初中',
+  '3': '高中'
+}
+
+//切换年级改变事件
+const HandleGradeChange=()=>{
+  // 在 gradeList 中找到当前选中的年级对象
+  const selectedGrade = gradeList.value.find((item: any) => item.gradeCode === formData.gradeCode);
+  console.log('选中的年级对象:', selectedGrade);
+  if (selectedGrade) {
+    // 更新科目列表
+    subjectList.value = selectedGrade.gradeCourseVOS || [];
+    formData.schoolYearId=selectedGrade.schoolYearId;//年级id
+    formData.schoolYearGradeId=selectedGrade.id;//年级表id
+    formData.schoolYearCode=selectedGrade.schoolYearCode;//年级code
+    formData.levelCode=selectedGrade.levelCode;//学段code
+    const code = String(selectedGrade.levelCode); 
+    formData.levelName = LEVEL_NAME_MAP[code] || ''; // 如果找不到对应代码,则设为空字符串
+    formData.graduates=selectedGrade.graduates;//毕业年份
+    formData.gradeName=selectedGrade.gradeName;//年级名称
+    
+    // 切换年级后,自动选中该年级的第一个科目(或者清空,视需求而定)
+    if (subjectList.value.length > 0) {
+      formData.courseCode = subjectList.value[0].courseCode;
+      formData.courseName=subjectList.value[0].courseName;
+    } else {
+      formData.courseCode = '';
+    }
+  }
+}
+
+//切换科目改变事件
+const HandleCourseChange=()=>{
+  //在gradeList 中找到当前选中的年级对象
+  const selectedCourse = subjectList.value.find((item: any) => item.courseCode === formData.courseCode);
+  console.log('选中的科目对象:', selectedCourse);
+  if(selectedCourse) 
+  {
+    // 更新科目列表
+    formData.courseName=selectedCourse.courseName;
+  }
+}
+
+// 取消按钮
+const HandleCancel = () => {
+  dialogVisible.value = false
+  ResetForm()
+}
+
+// 提交按钮
+const HandleSubmit = async () => {
+  if (!formRef.value) return
+  await formRef.value.validate()
+  console.log('提交数据:', formData)
+  // TODO: 调用接口提交数据
+  let params={
+    id:formData.id || '',//考试科目列表id 不为空表示修改
+    examType:1,//考试类型
+    schoolYearId:formData.schoolYearId,//学年id
+    schoolYearGradeId:formData.schoolYearGradeId,//学年表id
+    schoolYearCode:formData.schoolYearCode,//学年code
+    levelCode:formData.levelCode,//学段code
+    levelName:formData.levelName,//学段名称
+    graduates:formData.graduates,//毕业年份
+    gradeCode:formData.gradeCode,//年级code
+    gradeName:formData.gradeName,//年级名称
+    examSubjectName:formData.examSubjectName,//考试科目名称
+    courseCode:formData.courseCode,//科目code
+    courseName:formData.courseName,//科目名称
+  };
+  loading.value=true;
+  const res:any=await createExamSubject(params);
+  console.log("打印提交的结果",res)
+  if(res.code==200)
+  {
+    loading.value=false;
+    ElMessage.success('新增考试成功')
+    dialogVisible.value = false
+    emit('success')
+    ResetForm()
+  }
+  // ElMessage.success('新增考试成功')
+  // dialogVisible.value = false
+  // emit('success')
+  // 
+}
+
+
+</script>
+
+<style lang="scss" scoped>
+.exam-form {
+  padding-right: 20px;
+}
+</style>

+ 95 - 0
src/components/FiltersItem.vue

@@ -0,0 +1,95 @@
+<template>
+    <div class="filters_group">
+        <div class="group_item" v-for="(group,groupIndex) in data">
+            <div class="group_title">
+                {{group.label}}:
+            </div>
+            <div class="list_item" @click="HandleSelect(groupIndex,item.value)" v-for="item in group.list" :class="item.value == group.value ? 'list_item_cur' : ''">
+                {{item.label}}
+            </div>
+        </div>
+    </div>
+</template>
+
+<script lang="ts">
+export default {
+  name: 'FiltersItem',//筛选基础组件
+}
+</script>
+<script lang="ts" setup>
+import { ref, reactive, onMounted, inject,defineProps, defineEmits } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { ElMessage } from 'element-plus'
+
+export interface Props {
+  data?: Array<{ label: string;list:any[], value: any }>
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  data: () => []
+})
+// 2. 定义 Emit 事件
+const emit = defineEmits<{
+  (e: 'select', index: any,value: any ): void
+}>()
+
+const HandleSelect =  (index: any, value: any) => 
+{
+
+  emit('select', index, value)
+}
+
+onMounted(() => {
+  
+})
+
+</script>  
+<style lang="scss" scoped>
+
+.filters_group
+{
+
+}
+.group_item
+{
+  width: 100%;
+  display: flex;
+  justify-content: flex-start;
+  line-height:28px;
+  gap: 16px;
+  margin-top: 16px;
+  margin-bottom: 10px;
+  .group_title
+  {
+    width: 42px;
+    font-weight: 600;
+    font-size: 14px;
+    color:#333333;
+    // margin-right: 8px;
+  }
+
+  .list_item
+  {
+    font-weight: 400;
+    font-size: 14px;
+    color: #999999;
+    cursor: pointer;
+    padding-left: 8px;
+    padding-right: 8px;
+  }
+
+  .list_item:hover
+  {
+    color: #2E64FA;
+  }
+
+  .list_item_cur
+  {
+    color: #2E64FA;
+    background: rgba(71,113,203,0.1);
+    border-radius: 2px 2px 2px 2px;
+    
+  }
+
+}
+</style>

+ 125 - 0
src/components/resetPassword.vue

@@ -0,0 +1,125 @@
+<template>
+  <el-dialog v-model="dialogData.dialogShow" title="重置密码" width="480px">
+    <el-form :model="editPassWordData" :rules="editPassWordDataRules" ref="editPassWordForm" label-width="120px">
+      <el-form-item :label="dialogData.label+ ':'" prop="name" >
+        <el-input :value="dialogData.name" disabled/>
+      </el-form-item>
+      <el-form-item label="重置方式:" v-if="dialogData.type == '2'">
+        <el-radio-group v-model="editPassWordData.useDefault">
+          <el-radio :value="0">使用新密码</el-radio>
+          <el-radio :value="1">重置为默认密码</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="新密码:" v-if="editPassWordData.useDefault==0" prop="randomPassword">
+        <el-input v-model="editPassWordData.randomPassword" type="password" placeholder="请输入密码" />
+      </el-form-item>
+      <el-form-item label="默认密码:" v-if="dialogData.type == '2' && editPassWordData.useDefault==1">
+        <div class="default_pass">
+          <el-input value="******" disabled />
+          <p class="default_pass">默认密码:{{ dialogData.remark }}</p>
+        </div>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button class="button_border_gray cancel" @click="dialogData.dialogShow = false">取 消</el-button>
+      <el-button class="button_background" :loading="submitLoading" @click="confirmPassWord(editPassWordForm)">确 定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts">
+export default {
+  name: 'ResetPassword',
+}
+</script>
+<script lang="ts" setup>
+import { ref, reactive, onMounted, inject } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { ElMessage } from 'element-plus'
+import { resetPassWord } from '@/api/manager'
+import { resetSchoolPassWord } from '@/api/school'
+interface DialogData {
+  dialogShow: boolean,
+  type: string
+  name: string,
+  label: string,
+  id: string
+  remark: string
+}
+const dialogData = inject<DialogData>('passwordDialog', {
+  dialogShow: false,
+  type: '',
+  name: '',
+  label: '',
+  id: '',
+  remark: ''
+})
+interface EditPassWordData {
+  useDefault: number
+  randomPassword: string
+}
+const editPassWordData = reactive<EditPassWordData>({
+  useDefault: 0,
+  randomPassword: '',
+})
+const submitLoading = ref(false)
+const editPassWordForm = ref<FormInstance>()
+const editPassWordDataRules = reactive<FormRules<EditPassWordData>>({
+  randomPassword: [
+    { required: true, message: '请输入密码', trigger: 'blur' },
+    { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' },
+  ],
+})
+onMounted(() => {
+  
+})
+const confirmPassWord = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate(async(valid) => {
+    if (valid) {
+      submitLoading.value = true
+      try {
+        let res
+        if(dialogData.type == '1') {
+          res = await resetPassWord(dialogData.id, { 
+            randomPassword: editPassWordData.useDefault == 1 ? '' : editPassWordData.randomPassword 
+          })
+        }else if(dialogData.type == '2')  {
+          res = await resetSchoolPassWord({
+            schoolInfoId: dialogData.id, 
+            useDefault: editPassWordData.useDefault,
+            password: editPassWordData.useDefault == 1 ? '' : editPassWordData.randomPassword 
+          })
+        }
+        if (res.code == 200) {
+          ElMessage.success(res.msg)
+          if(editPassWordForm.value) {
+            editPassWordForm.value.resetFields()
+          }
+          dialogData.dialogShow = false
+        } else {
+          ElMessage.error(res.msg)
+        }
+      }catch {}finally {
+        submitLoading.value = false
+      }
+    }
+  })
+}
+</script>  
+<style lang="scss" scoped>
+.el-form :deep {
+  .el-form-item__label {
+    font-size: 16px;
+  }
+  .el-radio__label {
+    font-size: 16px;
+  }
+  .el-input {
+    width: 240px;
+  }
+}
+.default_pass {
+  font-size: 12px;
+}
+</style>

+ 11 - 0
src/env.d.ts

@@ -0,0 +1,11 @@
+// src/env.d.ts
+/// <reference types="vite/client" />
+
+interface ImportMetaEnv {
+  readonly VITE_API_BASE_URL: string
+  // 其他环境变量...
+}
+
+interface ImportMeta {
+  readonly env: ImportMetaEnv
+}

+ 24 - 0
src/main.ts

@@ -0,0 +1,24 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia' // 引入 Pinia
+
+import App from './App.vue'
+import router from './router' // 引入路由配置
+import ElementPlus from 'element-plus'//引入elmplus
+import 'element-plus/dist/index.css'
+import zhCn from 'element-plus/es/locale/lang/zh-cn'  // 导入中文语言包
+import * as ElIcons from '@element-plus/icons-vue'
+
+
+import './style.css'
+import './styles/common.scss'
+import './styles/element.scss'
+const app = createApp(App)
+const pinia = createPinia() // 创建实例
+Object.keys(ElIcons).forEach((key) => {
+  app.component(key, (ElIcons as Record<string, any>)[key])
+})//ele icon
+
+app.use(ElementPlus, { locale: zhCn })
+app.use(pinia) // 使用 Pinia vue3 的状态管理 替代vuex
+app.use(router) // 注册路由
+app.mount('#app')

+ 86 - 0
src/router/index.ts

@@ -0,0 +1,86 @@
+import { createRouter, createWebHistory, createWebHashHistory  } from 'vue-router'
+import type { RouteRecordRaw } from 'vue-router'
+import Layout from '@/views/layout/index.vue'
+
+// 示例路由配置
+const routes: Array<RouteRecordRaw> = [
+  {
+    path: '/',
+    name: '',
+    component: () => import('../views/login/login.vue')
+  },
+  {
+    path: '/login',
+    name: 'Login',
+    component: () => import('../views/login/login.vue')
+  },
+  //主页面
+  {
+    path: '/main',
+    name: 'main',
+    component: Layout,
+    children: [
+      {
+        path: 'choice',
+        name: 'school',
+        component: () => import('@/views/choice/index.vue')
+      },
+      {
+        path: 'fillblank',
+        name: 'manager',
+        component: () => import('@/views/fillblank/index.vue')
+      },
+      {
+        path: 'essay',
+        name: 'essay',
+        component: () => import('@/views/essay/index.vue')
+      }
+    ]
+
+  },
+  //考试详情页面
+  {
+    path: '/exam',
+    name: 'exam',
+    component: () => import('@/views/exam/index.vue'),
+    children:[
+      {
+        path: 'question',
+        name: 'question',
+        component: () => import('@/views/exam/question.vue')
+      },
+      {
+        path: 'scanList',
+        name: 'scanList',
+        component: () => import('@/views/exam/scanList.vue')
+      },
+      {
+        path: 'scanDetail',
+        name: 'scanDetail',
+        component: () => import('@/views/exam/scanDetail.vue')
+      },
+      {
+        path: 'score',
+        name: 'score',
+        component: () => import('@/views/exam/score.vue')
+      },
+      {
+        path: 'examList',
+        name: 'examList',
+        component: () => import('@/views/exam/examList.vue')
+      },
+    ]
+  },
+  {
+    path: '/:pathMatch(.*)*', // 捕获所有未匹配路径
+    name: 'NotFound',
+    component: () => import('../views/login/login.vue') // 未匹配到路由跳转到登录页面
+  }
+]
+
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes
+})
+
+export default router

+ 51 - 0
src/store/exam.ts

@@ -0,0 +1,51 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+
+// 定义考试信息的类型接口
+interface ExamInfo {
+    id: string | number // 根据实际业务调整 id 类型
+    examSubjectName: string,
+    examSubjectCode: string,
+    [key: string]: any // 允许其他未知属性
+}
+
+//考试相关的状态管理
+export const useExamStore = defineStore('exam', () => {
+    //初始化时尝试从localStorage中获取考试信息
+    const storedExam = localStorage.getItem('current_exam_info')
+    const initialExam = storedExam ? JSON.parse(storedExam) : null
+    //显式指定泛型类型为 ExamInfo | null,解决类型推断为 never 的问题
+    const currentExam = ref<ExamInfo | null>(initialExam)
+
+    //计算属性 考试id
+    const examId = computed(() => currentExam.value?.id)
+    //计算属性 考试名称
+    const examName = computed(() => currentExam.value?.examSubjectName)
+    //计算属性 考试类型
+    const examType = computed(() => currentExam.value?.examType || 1) //考试类型 1 选择题判分 2 填空题判分  3 作文题判分
+    //设置考试信息
+    const setExamInfo = (info: any) => {
+        currentExam.value = info
+        // 同步更新 localStorage,保证刷新后数据不丢失
+        if (info) {
+            localStorage.setItem('current_exam_info', JSON.stringify(info))
+        } else {
+            localStorage.removeItem('current_exam_info')
+        }
+    }
+
+    // 清空考试信息
+    const clearExamInfo = () => {
+        currentExam.value = null
+        localStorage.removeItem('current_exam_info')
+    }
+
+  return {
+    currentExam,
+    examId,
+    examName,
+    examType,
+    setExamInfo,
+    clearExamInfo
+  }
+})

+ 31 - 0
src/store/user.ts

@@ -0,0 +1,31 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+
+//用户相关的状态管理
+export const useUserStore = defineStore('user', () => {
+  // State
+  const schoolLogo = ref<string>('')
+  
+  // 如果需要管理 userInfo,也可以放在这里,但通常 userInfo 存在 localStorage 中
+  // 这里我们主要解决 header.vue 中用到的 schoolLogo
+  
+  // Getters (可选,如果需要计算属性)
+  const hasLogo = computed(() => !!schoolLogo.value)
+
+  // Actions
+  function setSchoolLogo(logo: string) {
+    schoolLogo.value = logo
+  }
+
+  function clearUserState() {
+    schoolLogo.value = ''
+    // 清除其他用户相关状态
+  }
+
+  return {
+    schoolLogo,
+    hasLogo,
+    setSchoolLogo,
+    clearUserState
+  }
+})

+ 77 - 0
src/style.css

@@ -0,0 +1,77 @@
+:root {
+
+  line-height: 1.5;
+  font-weight: 400;
+
+  color-scheme: light dark;
+  color: rgba(255, 255, 255, 0.87);
+  background-color: #242424;
+
+  font-synthesis: none;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+  font-weight: 500;
+  color: #646cff;
+  text-decoration: inherit;
+}
+a:hover {
+  color: #535bf2;
+}
+
+body {
+  margin: 0;
+  display: flex;
+  place-items: center;
+  min-width: 320px;
+  min-height: 100vh;
+}
+
+h1 {
+  font-size: 3.2em;
+  line-height: 1.1;
+}
+
+button {
+  border-radius: 8px;
+  border: 1px solid transparent;
+  padding: 0.6em 1.2em;
+  font-size: 1em;
+  font-weight: 500;
+  /* font-family: inherit; */
+  background-color: #1a1a1a;
+  cursor: pointer;
+  transition: border-color 0.25s;
+}
+button:hover {
+  border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+  outline: 4px auto -webkit-focus-ring-color;
+}
+
+.card {
+  padding: 2em;
+}
+
+#app {
+width: 100%;
+height: 100vh;
+}
+
+@media (prefers-color-scheme: light) {
+  :root {
+    color: #213547;
+    background-color: #ffffff;
+  }
+  a:hover {
+    color: #747bff;
+  }
+  button {
+    background-color: #f9f9f9;
+  }
+}

+ 3474 - 0
src/styles/common.scss

@@ -0,0 +1,3474 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+  font-family: Inter, -apple-system, BlinkMacSystemFont, PingFang SC,
+    Hiragino Sans GB, noto sans, Helvetica Neue, Helvetica,
+    Arial, sans-serif;
+}
+ul,
+li {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
+
+//页面最高层级  统一最底层背景色 #F0F4FB
+.page_layout {
+  position: relative;
+  min-height: 100%;
+  margin: 0 auto;
+  // padding-bottom: 30px;
+  overflow-y: hidden;
+  background-color: #f0f4fb;
+
+
+  .header_container 
+  {
+    width: 100%;
+    height: 64px;
+    font-size: 22px;
+    color: #fff;
+    box-sizing: border-box;
+    background-color: #2e64fa;
+  }
+
+  .main_container 
+  {
+    padding: 20px;
+    background: #F0F4FB;
+    height: calc(100vh - 65px);
+    display: flex;
+    align-items: center;
+    overflow-y: auto;
+  }
+
+}
+
+//考试详情页面
+.page_detail
+{
+  width: 100%;
+  height: 100%;
+  background-color: #f0f4fb;
+  display: flex;
+  justify-content: space-between;
+
+  //左侧导航栏
+  .left_aside
+  {
+    width: 160px;
+    height: 100%;
+    background-color: #fff;
+
+  }
+
+  //右侧主内容
+  .right_main
+  {
+    width: calc(100% - 160px);
+    height: 100%;
+    padding: 20px;
+    box-sizing: border-box;
+  }
+}
+
+#app > div {
+  height: 100%;
+}
+
+html,
+body {
+  width: 100% !important;
+  height: 100%;
+  overflow: hidden;
+  -webkit-font-smoothing: antialiased;//控制字体在 WebKit 浏览器(如 Chrome、Safari)中的抗锯齿方式。antialiased 表示使用“无锯齿”的字体渲染方式,使文字更清晰平滑,尤其适用于浅色或白色背景上的深色字体。
+  text-rendering: optimizeLegibility;//告诉浏览器优先考虑文字的可读性而不是渲染速度或几何精度
+  background: #F0F4FB;
+}
+
+// 全局滚动条样式修改
+/* 滚动条轨道区域样式 */
+::-webkit-scrollbar {
+  width: 8px;
+  height: 8px;
+  /* 设置滚动条宽度为8像素 */
+  background-color: transparent;
+}
+
+/* 滑块样式 */
+::-webkit-scrollbar-thumb {
+  background-color: #b8b8b8;
+  /* 设置滑块颜色为深灰色 */
+  border-radius: 4px;
+
+  max-height: 150px;//设置手柄最大高度
+  /* 设置滑块边角半径为4像素 */
+}
+
+/* 滚动条轨道内部空白区域样式 */
+::-webkit-scrollbar-track {
+  background-color: #f0f0f0;
+  /* 设置轨道背景色为浅灰色 */
+}
+
+/* 滚动条两端按钮样式 */
+::-webkit-scrollbar-button {
+  display: none;
+  /* 不显示按钮 */
+}
+
+/* 交叉点处的区域样式 */
+::-webkit-scrollbar-corner {
+  background-color: transparent;
+  /* 设置交叉点处的背景色为透明 */
+}
+
+/* 调整大小手柄样式 */
+::-webkit-resizer {
+  display: none;
+  /* 不显示调整大小手柄 */
+}
+
+.vxe-table {
+  // font-family: revert;
+  /* 不设置 font-family,浏览器使用默认字体 */
+}
+
+
+.page_tree {
+  width: 100%;
+  border: 1px solid rgb(238, 238, 238);
+  overflow-y: auto;
+  border-radius: 6px;
+  .el-tree--highlight-current
+    .el-tree-node.is-current
+    > .el-tree-node__content {
+    background: #e3eeff !important;
+    color: #2e64fa !important;
+    border-radius: 4px;
+    padding: 5px 4px;
+    .el-icon-caret-right {
+      color: #2e64fa !important;
+    }
+  }
+  .el-tree--highlight-current
+    .el-tree-node.is-current
+    > .el-tree-node__content {
+    background-color: red;
+  }
+  .el-tree-node .el-tree-node__content:hover {
+    background-color: #f0f2f5;
+    /* 悬浮时的背景色 */
+    border-radius: 4px;
+    padding: 5px 4px;
+  }
+  .el-tree-node .el-tree-node__content {
+    padding: 5px 4px;
+  }
+}
+//通用间隔
+.page_jg {
+  clear: both;
+  height: 12px;
+  width: 100%;
+}
+//通用间隔20px
+.page_jg_20 {
+  clear: both;
+  height: 20px;
+  width: 100%;
+}
+//通用间隔18px
+.page_jg_18 {
+  clear: both;
+  height: 18px;
+  width: 100%;
+}
+//通用间隔16px
+.page_jg_16 {
+  clear: both;
+  height: 16px;
+  width: 100%;
+}
+//通用间隔10px
+.page_jg_10 {
+  clear: both;
+  height: 10px;
+  width: 100%;
+}
+//页面表格公共样式
+.page_table {
+  .el-table {
+    border-radius: 8px;
+    border: 1px solid #f0f2f5;
+    border-bottom: 0px solid #f0f2f5;
+    //表格竖着的边框线样式
+    th.el-table__cell {
+      border-right: 1px solid #ebeef5;
+    }
+    .el-table__body-wrapper {
+      table {
+        tr {
+          td {
+            border-right: 1px solid #ebeef5;
+          }
+          td:last-child,
+          th:last-child {
+            border-right: 0px solid green;
+          }
+        }
+      }
+    }
+    .el-table__empty-block {
+      min-height: 120px;
+      background-image: url("../assets/bg/table_no_data.png");
+      background-size: 64px 72px;
+      background-position: 50% 50%;
+      background-repeat: no-repeat;
+
+      .el-table__empty-text {
+        display: none;
+      }
+    }
+  }
+  //表格加载动画字体
+  .el-loading-spinner .el-loading-text {
+    font-size: 14px;
+  }
+  .el-loading-spinner i {
+    font-size: 20px;
+    color:#2E64FA;
+  }
+  //表格里的输入框 选择框样式覆盖
+  .el-input__inner {
+    height: 36px !important;
+    line-height: 36px !important;
+  }
+  // 下拉选择框 中间箭头上下居中
+  .el-select .el-input .el-select__caret {
+    height: 36px;
+  }
+  // 下拉选择框 中间箭头上下居中
+  .el-input__icon {
+    line-height: 36px;
+  }
+  //去掉滚动条后的表格整体宽度
+  .el-table__body {
+    width: 100% !important;
+  }
+  //标题栏背景色
+  .el-table tr th.el-table__cell {
+    background-color: #f4f6f8 !important;
+    font-size: 16px;
+    color: #333;
+    font-weight: 600;
+  }
+  .el-table .cell {
+    padding: 0 5px !important;
+  }
+  .el-table thead {
+    color: #333 !important;
+    font-weight: 500;
+    font-size: 14px;
+    height: 52px;
+    .is-left {
+      padding-left: 10px;  //靠左的标题加10边距
+    }
+  }
+  .el-table .el-table__cell {
+    padding: 0;
+    height: 52px;
+    color:#666;
+  }
+
+}
+
+.table_no_bg {
+  .el-table__empty-block {
+    // background-color: red;
+    // min-height: 120px;
+    min-height: auto !important;
+    background-image: none !important;
+    background-size: 64px 72px;
+    background-position: 50% 50%;
+    background-repeat: no-repeat;
+
+    .el-table__empty-text {
+      display: block !important;
+    }
+  }
+}
+//状态
+.table_row_status {
+  // font-family:"Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "WenQuanYi Zen Hei", sans-serif;
+  font-weight: 400;
+  font-size: 14px;
+  color: #666666;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 5px;
+
+  //不计入 灰色
+  .not_included_icon 
+  {
+    margin-left: 5px;
+    width: 6px;
+    height: 6px;
+    border-radius: 50%;
+    background: #C0C4CC;
+  }
+
+  //异常的  红色
+  .abnormal_icon {
+    margin-left: 5px;
+    width: 6px;
+    height: 6px;
+    border-radius: 50%;
+    background: #fa3a2e;
+  }
+
+  //正常的  绿色的
+  .normal_icon {
+    margin-left: 5px;
+    width: 6px;
+    height: 6px;
+    background: #2bc644;
+    border-radius: 50%;
+  }
+
+  //分析管理  已发布
+  .yes_release_icon {
+    margin-left: 5px;
+    width: 6px;
+    height: 6px;
+    background: #2bc644;
+    border-radius: 50%;
+  }
+
+  //分析管理  未发布 缺考
+  .no_release_icon {
+    margin-left: 5px;
+    width: 6px;
+    height: 6px;
+    background: #fb9f34;
+    border-radius: 50%;
+  }
+
+  //分析管理  已关闭  违纪
+  .ready_close_icon {
+    margin-left: 5px;
+    width: 6px;
+    height: 6px;
+    background: #f56c6c;
+    border-radius: 50%;
+  }
+
+  //分析管理  需汇总
+  .need_summary_icon {
+    margin-left: 5px;
+    width: 6px;
+    height: 6px;
+    background: #2e64fa;
+    border-radius: 50%;
+  }
+  //分析管理  成绩中
+  .score_look_icon {
+    margin-left: 5px;
+    width: 6px;
+    height: 6px;
+    background: #995FB3;
+    border-radius: 50%;
+  }
+}
+
+
+// 分页公共样式
+.page_pagination {
+  width: 100%;
+  height: 30px;
+  margin-top: 20px;
+  margin-bottom: 2px;
+ 
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+
+  .el-pagination {
+    padding: 0 !important;
+    display: flex;
+    align-items: center;
+  }
+  .el-input--mini .el-input__inner {
+    height: 30px !important;
+    line-height: 30 !important;
+  }
+  // 上一页
+  .el-pagination .btn-prev {
+    width: 30px !important;
+    height: 30px;
+    padding: 0 !important;
+    background: #f3f3f3;
+    border-radius: 4px 4px 4px 4px;
+  }
+
+  // 下一页
+  .el-pagination .btn-next {
+    width: 30px !important;
+    height: 30px;
+    padding: 0 !important;
+    background: #f3f3f3;
+    border-radius: 4px 4px 4px 4px;
+    margin-left: 8px;
+  }
+
+  .el-pagination span:not([class*="suffix"]),
+  .el-pagination button {
+    min-width: 30px;
+  }
+
+  .el-pagination .el-pager li {
+    min-width: 30px;
+    height: 30px;
+    width: 30px;
+    line-height: 30px;
+    background: #f3f3f3;
+    border-radius: 4px 4px 4px 4px;
+    padding: 0 !important;
+    margin-left: 8px;
+    font-weight: 400;
+    font-size: 14px;
+    color: #666;
+  }
+
+  .el-pagination .el-pager li.active {
+    background: #2f51ff;
+    font-weight: 400;
+    font-size: 14px;
+    color: #ffffff;
+  }
+}
+
+
+
+//页面公共样式
+.page_item 
+{
+  // width: 80%;
+
+  min-width: 1200px;
+  margin: auto;
+  min-height: 100%;
+
+  background: #fff;
+  border-radius: 10px;
+  padding: 20px;
+  box-sizing: border-box;
+}
+
+.page_module
+{
+  width: 100%;
+  padding: 20px;
+  height: auto;
+  box-sizing: border-box;
+  background-color: #fff;
+  border-radius: 10px;
+}
+.page_tag {
+  width: calc(100% - 20px);
+  height: 100%;
+  border-radius: 0px 4px 4px 0px;
+  .el-radio-button__inner {
+    padding: 0 20px;
+    height: 36px;
+    line-height: 36px;
+    color: #666666;
+  }
+  .el-radio-button__orig-radio:checked + .el-radio-button__inner {
+    color: #fff !important;
+  }
+  .tab_item {
+    font-weight: 400;
+    font-size: 14px;
+    color:#999999;
+  }
+  .tag_item {
+    max-width: 100px;
+    white-space: nowrap;     /* 禁止换行 */
+    overflow: hidden;        /* 隐藏溢出内容 */
+    text-overflow: ellipsis; /* 溢出部分显示省略号 */
+  }
+}
+
+.page_content
+{
+    width: 100%;
+    height: calc(100% - 56px);
+    display: flex;
+    justify-content: space-between;
+    .content_table
+    {
+        width: calc(100% - 340px);
+        height: 100%;
+        .table_header
+        {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+          .header_left
+          {
+
+          }
+          .header_right
+          {
+
+            font-weight: 400;
+            font-size: 16px;
+            color: #333333;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+
+          }
+        }
+
+    }
+    .content_right
+    {
+        width: 320px;
+        height: 100%;
+        background: #FFFFFF;
+        border-radius: 10px 10px 10px 10px;
+        border: 1px solid #EBEEF5;
+
+        position: relative;
+        .right_header
+        {
+          width: 100%;
+          height: 48px;
+          line-height: 48px;
+          padding: 0 10px;
+          box-sizing: border-box;
+          border-bottom: 1px solid #EBEEF5;
+
+          display: flex;
+          justify-content: space-between;
+        }
+        .right_center
+        {
+          width: 100%;
+          height: calc(100% - 48px - 80px);
+
+          .scan_buttons
+          {
+            width: calc(100% - 40px);
+            margin: auto;
+            cursor: pointer;
+            position: relative;
+            border-radius: 10px 10px 10px 10px;
+            // border: 1px solid #EBEEF5;
+          }
+
+          /* 小屏幕笔记本的样式 常见分辨率:1366x768 */
+          @media (max-height: 600px) {
+
+            .scan_list {
+              width: calc(100% - 20px);
+              margin-left: 20px;
+              // height:calc(100% - 250px);
+              display: flex;
+              justify-content: flex-start;
+              flex-wrap: wrap;
+
+              .list_item {
+                width: calc(50% - 22px);
+                margin-right: 20px;
+                margin-bottom: 30px;
+                height: 50px;
+                font-weight: 500;
+                font-size: 14px;
+                // color: #333333;
+                text-align: center;
+
+                border-radius: 10px 10px 10px 10px;
+                border: 1px solid #EBEEF5;
+                cursor: pointer;
+                background-position: calc(100% - 10px) 10px;
+                background-repeat: no-repeat;
+                background-size: 32px 32px;
+                margin-top: -20px;
+
+                .list_item_info {
+                  width: calc(100% - 20px);
+                  margin: auto;
+                  height: 40px;
+
+
+
+                  .item_info_title {
+                    font-weight: 600;
+                    font-size: 14px;
+                    color: #333333;
+                    padding-top: 5px;
+
+                    text-align: left;
+                  }
+
+                  .item_info_number {
+                    width: 100%;
+
+                    position: relative;
+                    display: flex;
+                    justify-content: space-between;
+                    align-items: center;
+                    height: 30px;
+                    // line-height: 60px;
+
+                    // 未扫描
+                    .number_no_scan {
+                      font-weight: 600;
+                      font-size: 16px;
+                      color: #2E64FA;
+                    }
+
+                    //缺考
+                    .number_no_exam {
+                      font-weight: 600;
+                      font-size: 16px;
+                      color: #FB9F34;
+
+                    }
+
+                    //已上传 
+                    .number_uploaded {
+                      font-weight: 600;
+                      font-size: 16px;
+                      color: #2BC644;
+
+                    }
+
+                    //异常
+                    .number_abnormal {
+                      font-weight: 600;
+                      font-size: 16px;
+                      color: #F56C6C;
+
+                    }
+
+                    .number_no_icon {
+                      width: 32px;
+                      height: 32px;
+                      display: flex;
+                      justify-content: flex-start;
+                      margin-left: 20px;
+                      // background-color: red;
+                      margin-top: 5px;
+                    }
+
+                  }
+                }
+              }
+
+              .no_scan {
+                background-image: url("../assets/icon/no_scan_icon.png");
+                border: 1px solid #2E64FA;
+
+                .item_info_title {
+                  color: #2E64FA !important;
+                }
+              }
+
+              .no_hover {
+                pointer-events: none; // 这会禁用所有鼠标事件
+              }
+
+              .no_scan:hover {
+                background-color: rgba(46, 100, 250, 0.1);
+                background-image: url("../assets/icon/no_scan_icon_hover.png");
+                border-radius: 10px 10px 10px 10px;
+                border: 1px solid #2E64FA;
+                box-shadow: 4px 2px 10px #2E64FA;
+
+                .item_info_title {
+                  color: #2E64FA !important;
+                }
+              }
+
+              .no_exam {
+
+                background-image: url("../assets/icon/miss_exam.png");
+                border: 1px solid #FB9F34;
+
+                .item_info_title {
+                  color: #FB9F34 !important;
+                }
+              }
+
+              .no_exam:hover {
+
+
+                background-color: rgba(251, 159, 52, 0.1);
+                ;
+                background-image: url("../assets/icon/miss_exam_hover.png");
+                border-radius: 10px 10px 10px 10px;
+                border: 1px solid #FB9F34;
+                box-shadow: 4px 2px 10px #FB9F34;
+
+                .item_info_title {
+                  color: #FB9F34 !important;
+                }
+              }
+
+              .annormal_icon {
+                background-image: url("../assets/icon/abnormal_icon.png");
+                border: 1px solid #F56C6C;
+
+                .item_info_title {
+                  color: #F56C6C !important;
+                }
+              }
+
+              .annormal_icon:hover {
+
+
+                background-color: rgba(245, 108, 108, 0.1);
+                background-image: url("../assets/icon/abnormal_icon_hover.png");
+                border-radius: 10px 10px 10px 10px;
+                border: 1px solid #F56C6C;
+                box-shadow: 4px 2px 10px #F56C6C;
+
+                .item_info_title {
+                  color: #F56C6C !important;
+                }
+              }
+
+              .sucess_upload {
+                background-image: url("../assets/icon/sucess_upload.png");
+                border: 1px solid #2BC644;
+
+                .item_info_title {
+                  color: #2BC644 !important;
+                }
+              }
+
+              .sucess_upload:hover {
+
+
+                background-color: rgba(43, 198, 68, 0.1);
+                background-image: url("../assets/icon/sucess_upload_hover.png");
+                border-radius: 10px 10px 10px 10px;
+                border: 1px solid #2BC644;
+                box-shadow: 4px 2px 10px #2BC644;
+
+                .item_info_title {
+                  color: #2BC644 !important;
+                }
+              }
+            }
+          }
+
+          /* 屏幕高度在767-到1440之间时 常见分辨率:1600x900, 1920x1080*/
+          @media (min-height: 600px) and (max-height: 900px) {
+
+
+            .scan_list {
+              width: calc(100% - 20px);
+              margin-left: 20px;
+              // height:calc(100% - 250px)
+              // background-color: red;
+              display: flex;
+              justify-content: flex-start;
+              flex-wrap: wrap;
+              margin-top: 20px;
+
+              .list_item {
+                width: calc(50% - 22px);
+                margin-right: 20px;
+                margin-bottom: 20px;
+                height: 70px;
+                font-weight: 500;
+                font-size: 16px;
+                // color: #333333;
+                text-align: center;
+                background: #FFFFFF;
+                border-radius: 10px 10px 10px 10px;
+                border: 1px solid #EBEEF5;
+                cursor: pointer;
+                background-position: calc(100% - 10px) 10px;
+                background-repeat: no-repeat;
+                background-size: 32px 32px;
+
+                .list_item_info {
+                  width: calc(100% - 20px);
+                  margin: auto;
+                  height: 70px;
+
+
+
+                  .item_info_title {
+                    font-weight: 600;
+                    font-size: 16px;
+                    color: #333333;
+                    padding-top: 10px;
+
+                    text-align: left;
+                  }
+
+                  .item_info_number {
+                    width: 100%;
+
+                    position: relative;
+                    display: flex;
+                    justify-content: space-between;
+                    align-items: center;
+                    height: 50px;
+                    // line-height: 60px;
+
+                    // 未扫描
+                    .number_no_scan {
+                      font-weight: 600;
+                      font-size: 20px;
+                      color: #2E64FA;
+                    }
+
+                    //缺考
+                    .number_no_exam {
+                      font-weight: 600;
+                      font-size: 20px;
+                      color: #FB9F34;
+
+                    }
+
+                    //已上传 
+                    .number_uploaded {
+                      font-weight: 600;
+                      font-size: 20px;
+                      color: #2BC644;
+
+                    }
+
+                    //异常
+                    .number_abnormal {
+                      font-weight: 600;
+                      font-size: 20px;
+                      color: #F56C6C;
+
+                    }
+
+                    .number_no_icon {
+                      width: 32px;
+                      height: 32px;
+                      display: flex;
+                      justify-content: flex-start;
+                      margin-left: 20px;
+                      // background-color: red;
+                      margin-top: 5px;
+                    }
+
+                  }
+                }
+              }
+
+              .no_hover {
+                pointer-events: none; // 这会禁用所有鼠标事件
+              }
+
+              .no_scan {
+                background-image: url("../assets/icon/no_scan_icon.png");
+                border: 1px solid #2E64FA;
+
+                .item_info_title {
+                  color: #2E64FA !important;
+                }
+              }
+
+              .no_scan:hover {
+                background-color: rgba(46, 100, 250, 0.1);
+                background-image: url("../assets/icon/no_scan_icon_hover.png");
+                border-radius: 10px 10px 10px 10px;
+                border: 1px solid #2E64FA;
+                box-shadow: 4px 2px 10px #2E64FA;
+
+                .item_info_title {
+                  color: #2E64FA !important;
+                }
+              }
+
+              .no_exam {
+
+                background-image: url("../assets/icon/miss_exam.png");
+                border: 1px solid #FB9F34;
+
+                .item_info_title {
+                  color: #FB9F34 !important;
+                }
+              }
+
+              .no_exam:hover {
+
+
+                background-color: rgba(251, 159, 52, 0.1);
+                ;
+                background-image: url("../assets/icon/miss_exam_hover.png");
+                border-radius: 10px 10px 10px 10px;
+                border: 1px solid #FB9F34;
+                box-shadow: 4px 2px 10px #FB9F34;
+
+                .item_info_title {
+                  color: #FB9F34 !important;
+                }
+              }
+
+              .annormal_icon {
+                background-image: url("../assets/icon/abnormal_icon.png");
+                border: 1px solid #F56C6C;
+
+                .item_info_title {
+                  color: #F56C6C !important;
+                }
+              }
+
+              .annormal_icon:hover {
+
+
+                background-color: rgba(245, 108, 108, 0.1);
+                background-image: url("../assets/icon/abnormal_icon_hover.png");
+                border-radius: 10px 10px 10px 10px;
+                border: 1px solid #F56C6C;
+                box-shadow: 4px 2px 10px #F56C6C;
+
+                .item_info_title {
+                  color: #F56C6C !important;
+                }
+              }
+
+              .sucess_upload {
+                background-image: url("../assets/icon/sucess_upload.png");
+                border: 1px solid #2BC644;
+
+                .item_info_title {
+                  color: #2BC644 !important;
+                }
+              }
+
+              .sucess_upload:hover {
+
+
+                background-color: rgba(43, 198, 68, 0.1);
+                background-image: url("../assets/icon/sucess_upload_hover.png");
+                border-radius: 10px 10px 10px 10px;
+                border: 1px solid #2BC644;
+                box-shadow: 4px 2px 10px #2BC644;
+
+                .item_info_title {
+                  color: #2BC644 !important;
+                }
+              }
+            }
+
+
+          }
+
+          /* 当屏幕高度至少为1440px时应用的样式  常见分辨率:2560x1440*/
+          @media screen and (min-height:900px) {
+ 
+
+            .scan_list {
+              width: calc(100% - 20px);
+              margin-left: 20px;
+              // height:calc(100% - 250px)
+
+              margin-top: 40px;
+              display: flex;
+              justify-content: flex-start;
+              flex-wrap: wrap;
+
+              .list_item {
+                width: calc(50% - 22px);
+                margin-right: 20px;
+                margin-bottom: 20px;
+                height: 70px;
+                font-weight: 500;
+                font-size: 16px;
+                // color: #333333;
+                text-align: center;
+                background: #FFFFFF;
+                border-radius: 10px 10px 10px 10px;
+                border: 1px solid #EBEEF5;
+                cursor: pointer;
+                background-position: calc(100% - 10px) 10px;
+                background-repeat: no-repeat;
+                background-size: 32px 32px;
+
+                .list_item_info {
+                  width: calc(100% - 20px);
+                  margin: auto;
+                  height: 70px;
+
+
+
+                  .item_info_title {
+                    font-weight: 600;
+                    font-size: 16px;
+                    color: #333333;
+                    padding-top: 10px;
+
+                    text-align: left;
+                  }
+
+                  .item_info_number {
+                    width: 100%;
+
+                    position: relative;
+                    display: flex;
+                    justify-content: space-between;
+                    align-items: center;
+                    // height: 50px;
+                    // line-height: 60px;
+
+                    // 未扫描
+                    .number_no_scan {
+                      font-weight: 600;
+                      font-size: 20px;
+                      color: #2E64FA;
+                    }
+
+                    //缺考
+                    .number_no_exam {
+                      font-weight: 600;
+                      font-size: 20px;
+                      color: #FB9F34;
+
+                    }
+
+                    //已上传 
+                    .number_uploaded {
+                      font-weight: 600;
+                      font-size: 20px;
+                      color: #2BC644;
+
+                    }
+
+                    //异常
+                    .number_abnormal {
+                      font-weight: 600;
+                      font-size: 20px;
+                      color: #F56C6C;
+
+                    }
+
+                    .number_no_icon {
+                      width: 32px;
+                      height: 32px;
+                      display: flex;
+                      justify-content: flex-start;
+                      margin-left: 20px;
+                      // background-color: red;
+                      margin-top: 5px;
+                    }
+
+                  }
+                }
+              }
+
+              .no_hover {
+                pointer-events: none; // 这会禁用所有鼠标事件
+              }
+
+              .no_scan {
+                background-image: url("../assets/icon/no_scan_icon.png");
+                border: 1px solid #2E64FA;
+
+                .item_info_title {
+                  color: #2E64FA !important;
+                }
+              }
+
+              .no_scan:hover {
+                background-color: rgba(46, 100, 250, 0.1);
+                background-image: url("../assets/icon/no_scan_icon_hover.png");
+                border-radius: 10px 10px 10px 10px;
+                border: 1px solid #2E64FA;
+                box-shadow: 4px 2px 10px #2E64FA;
+
+                .item_info_title {
+                  color: #2E64FA !important;
+                }
+              }
+
+              .no_exam {
+
+                background-image: url("../assets/icon/miss_exam.png");
+                border: 1px solid #FB9F34;
+
+                .item_info_title {
+                  color: #FB9F34 !important;
+                }
+              }
+
+              .no_exam:hover {
+
+
+                background-color: rgba(251, 159, 52, 0.1);
+                ;
+                background-image: url("../assets/icon/miss_exam_hover.png");
+                border-radius: 10px 10px 10px 10px;
+                border: 1px solid #FB9F34;
+                box-shadow: 4px 2px 10px #FB9F34;
+
+                .item_info_title {
+                  color: #FB9F34 !important;
+                }
+              }
+
+              .annormal_icon {
+                background-image: url("../assets/icon/abnormal_icon.png");
+                border: 1px solid #F56C6C;
+
+                .item_info_title {
+                  color: #F56C6C !important;
+                }
+              }
+
+              .annormal_icon:hover {
+
+
+                background-color: rgba(245, 108, 108, 0.1);
+                background-image: url("../assets/icon/abnormal_icon_hover.png");
+                border-radius: 10px 10px 10px 10px;
+                border: 1px solid #F56C6C;
+                box-shadow: 4px 2px 10px #F56C6C;
+
+                .item_info_title {
+                  color: #F56C6C !important;
+                }
+              }
+
+              .sucess_upload {
+                background-image: url("../assets/icon/sucess_upload.png");
+                border: 1px solid #2BC644;
+
+                .item_info_title {
+                  color: #2BC644 !important;
+                }
+              }
+
+              .sucess_upload:hover {
+
+
+                background-color: rgba(43, 198, 68, 0.1);
+                background-image: url("../assets/icon/sucess_upload_hover.png");
+                border-radius: 10px 10px 10px 10px;
+                border: 1px solid #2BC644;
+                box-shadow: 4px 2px 10px #2BC644;
+
+                .item_info_title {
+                  color: #2BC644 !important;
+                }
+              }
+            }
+
+          }
+          
+        }
+
+        .right_button
+        {
+          position: absolute;
+          bottom: 0;
+          right: 0;
+          width: 100%;
+          height: 80px;
+          border-top: 1px solid #EBEEF5;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+        }
+    }
+
+}
+
+//页面底部公共样式
+.page_bottom
+{
+  width: 100%;
+
+  height: 20px;
+  font-weight: 400;
+  font-size: 14px;
+  color: #666;
+  display: flex;
+  justify-content: space-between;
+
+  .span_red
+  {
+    font-weight: 500;
+    font-size: 16px;
+    color: #F56C6C;
+  }
+
+  .bottom_left
+  {
+    display: flex;
+    justify-content: flex-start;
+    gap:12px;
+    align-items: center;
+  }
+
+  .bottom_right
+  {
+    display: flex;
+    justify-content: flex-end;
+    gap:12px;
+    align-items: center;
+  }
+
+}
+
+
+//页面表格列表 以及筛选公共样式
+.page_list{
+  margin: auto;
+  .search_content {
+    width: 100%;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    .el_button_item {
+      width: auto;
+      height: 36px !important;
+      line-height: 36px;
+      padding: 0 10px;
+      margin-left: 20px;
+      font-size: 14px !important;
+    }
+    .el-select {
+      width: 120px;
+    }
+    .el-input {
+      width: 200px;
+    }
+    .content_left {
+      display: flex;
+      gap: 10px;
+      flex-wrap: wrap;
+      width: auto;
+      // height: 36px;
+      line-height: 36px;
+      .tab_button {
+        width: auto;
+        height: 36px !important;
+        line-height: 36px;
+        padding: 0 10px;
+        // margin-right: 20px;
+        font-size: 14px !important;
+      }
+      .select_cur {
+        background:#2E64FA;
+        color:#fff;
+        border:1px solid #2E64FA;
+      }
+      .left_title {
+        width: 42px;
+        font-weight: 600;
+        font-size: 14px;
+        color: #333333;
+        margin-right: 8px;
+      }
+      
+      .grade_title {
+        font-weight: 600;
+        font-size: 16px;
+        color: #333333;
+        margin-right: 10px;
+      }
+      // 通用select
+      .select_width
+      {
+        width: 120px;
+      }
+      //通用input
+      // .input_width
+      // {
+      //   width: 230px;
+      // }
+      
+      //学年
+      .select_year
+      {
+        width: 160px;
+        margin-right: 10px;
+      }
+
+      .search_item
+      {
+          display: inline-block;
+          margin-right: 10px;
+          text-align: left;
+          color:#333;
+          .el-button {
+            margin-left: 0px;
+            min-width: 72px;
+            height: 36px;
+            line-height: 34px;
+            padding: 0 10px;
+        
+            span {
+              display: inline-flex;
+              align-items: center;
+              padding:0;
+            }
+        
+            img {
+              width: 16px;
+              height: 16px;
+              margin-right: 4px;
+              vertical-align: middle;
+              /* 使图标垂直居中 */
+            }
+          }
+
+      }
+
+    }
+
+    .content_right 
+    {
+      width: auto;
+      height: 36px;
+      line-height: 36px;
+      display: flex;
+      justify-content: flex-end;
+      gap: 10px;
+      // .el-button
+      // {
+      //   height: 36px !important;
+      //   line-height: 36px;
+      //   padding: 0 10px;
+      //   font-size: 14px !important;
+      //   margin: 0 !important;
+      // }
+
+      .el-button {
+        margin-left: 0px;
+
+        height: 36px;
+        line-height: 36px;
+        padding: 0 10px;
+    
+        span {
+          display: inline-flex;
+          align-items: center;
+          padding:0;
+        }
+    
+        img {
+          width: 16px;
+          height: 16px;
+          margin-right: 4px;
+          vertical-align: middle;
+          /* 使图标垂直居中 */
+        }
+      }
+    
+      .el-button--default {
+        color: #666;
+        font-weight: 400;
+      }
+      
+      .button_border
+      {
+
+      }
+
+      .add_button
+      {
+        border:1px solid #2e64fa;
+        background-color: #fff;
+        color: #2e64fa;
+      
+      }
+
+      .add_button:hover
+      {
+        color: #2e64fa;
+        background-color: #c0d1fe;
+        
+      }
+
+      .el_bttton_72
+      {
+        width: 72px;
+        height: 36px;
+
+      }
+      
+    }
+    
+  }
+
+  .is-active
+  {
+    color:#2e64fa !important;
+  }
+
+  .el-tabs__item{
+    font-size:16px;
+    font-weight: 600;
+    color:#666;
+  }
+  .el-tabs__header
+  {
+    margin: 0 0 16px 
+
+  }
+
+  .el-tabs__nav-wrap::after
+  {
+    height: 1px !important;
+  }
+}
+
+.dialog_warning {
+  .el-dialog__header {
+    // background-color: red !important;
+    background-image: url("../assets/icon/warning_icon.png");
+    background-size: 20px 20px;
+    background-position: 20px 50%;
+    background-repeat: no-repeat;
+    text-indent: 50px !important;
+  }
+
+  .center_align {
+    text-align: center;
+  }
+
+  .warning_footer {
+    width: calc(100% - 30px);
+    margin: auto;
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    // background-color: red;
+    height: 100%;
+  }
+}
+
+//弹窗公共样式
+.page_dialog {
+  border-radius: 8px;
+  padding: 0 !important;
+  //饿了么样式覆盖
+  .el-dialog {
+    padding: 0;
+  }
+  .el-dialog__headerbtn:focus .el-dialog__close, 
+  .el-dialog__headerbtn:hover .el-dialog__close {
+    color: #999;
+  }
+  //头部样式覆盖
+  .el-dialog__header {
+    width: 100%;
+    height: 48px;
+    line-height: 48px;
+    text-indent: 20px;
+    border-bottom: 1px solid #f5f7fa;
+    border-top-right-radius: 8px;
+    border-top-left-radius: 8px;
+    padding: 0 !important;
+    text-align: left;
+    background: #F5F7FA;
+    .el-dialog__title {
+      font-weight: 600;
+      font-size: 16px;
+      color: #333;
+    }
+    button:focus, button:focus-visible {
+      outline: none;
+    }
+    .el-dialog__headerbtn {
+      // display: none;
+      // width: 100px;
+      // height: 24px;
+      // margin-top: -8px;
+      // background-image: url("../assets/icon/close-line.png");
+      // background-position: 100% 50%;
+      // background-repeat: no-repeat;
+      // background-size: 24px 24px;
+      // // background-color: red;
+      // cursor: pointer;
+      // i {
+      //   display: none;
+      // }
+    }
+  }
+
+  //饿了么内容样式覆盖
+  .el-dialog__body {
+    font-weight: 400;
+    font-size: 16px;
+    color: #666666;
+    padding: 20px;
+    width: 100%;
+    margin: auto;
+    // min-height: 99px;
+    // display: flex;
+    // justify-content: flex-start;
+    // align-items: center;
+    .el-date-editor.el-input, .el-date-editor.el-input__wrapper {
+      width: 140px;
+    }
+    .el-form .el-form-item:last-child {
+      margin-bottom: 0;
+    }
+    .el-form-item__label {
+      font-size: 16px;
+      color: #666666;
+    }
+  }
+
+  .max_height_540 {
+    margin: 20px 0;
+    max-height: 540px;
+    overflow-y: auto;
+  }
+
+  .max_height_650 {
+    overflow-y: auto;
+    /* 小屏幕笔记本的样式 常见分辨率:1366x768 */
+    @media (max-height: 768px) {
+      max-height: 480px;
+    }
+
+    /* 中等屏幕笔记本的样式(14-15英寸) 常见分辨率:1600x900, 1920x1080*/
+    @media (min-height: 767px) {
+      max-height: 650px;
+    }
+
+    
+  }
+
+  .padding_20 {
+    // width: calc(100% - 40px);
+    padding: 20px 20px;
+  }
+
+  //个人中心 年级科目弹窗
+  .content_course_tree
+  {
+
+    width: calc(100% - 40px);
+    margin: auto;
+    display: flex;
+    justify-content:space-around;
+    .tree_item
+    {
+        width: 30%;
+        height: auto;
+       
+        .header_title
+        {
+            font-weight: 600;
+            font-size: 16px;
+            color: #333333;
+            line-height: 30px;
+        }
+
+        .item_content
+        {
+            margin-top: 10px;
+            width: 100%;
+            height: 350px;
+            border-radius: 6px;
+            border: 1px solid #eeeeee;
+            .check_all
+            {
+              width:100%;
+              margin: auto;
+              height: 34px;
+              border-bottom: 1px solid #eeeeee;
+              line-height: 34px;
+              text-indent: 4px;
+            }
+
+            .check_content
+            {
+              width: 100%;
+              height: 350px;
+              overflow: auto;
+
+
+              .el-checkbox-group
+              {
+                margin-top: 10px;
+                .el-checkbox
+                {
+                  
+                  width: 80%;
+                  text-indent: 6px;
+                  margin-bottom: 10px;
+
+                }
+              }
+
+              
+            }
+
+            .page_tree
+            {
+              border: none !important;
+              margin-top: 0px !important;
+            }
+
+            .select_item
+            {
+              width: calc(100% - 40px);
+              margin: auto;
+              line-height: 20px;
+              display: flex;
+              justify-content: space-between;
+              margin-top: 10px;
+              align-items: center;
+              
+
+              i{
+                font-size: 18px;
+                cursor: pointer;
+              }
+            }
+        }
+    
+        
+    
+    }
+   
+  }
+
+  //通用内容区域
+  .dialog_center {
+    // width: calc(100% - 40px);
+    margin: auto;
+
+    .center_header
+    {
+      width: 100%;
+      height: auto;
+      display: flex;
+      justify-content: space-between;
+      .header_left
+      {
+        display: flex;
+        justify-content: flex-start;
+        align-items: center;
+
+        .left_name
+        {
+
+          width: 120px;
+          font-size: 16px;
+          color:#666;
+          font-weight: 400;
+        }
+        .el-input__inner 
+        {
+          width: 216px;
+          height: 36px;
+          line-height: 36px;
+        }
+
+
+      }
+
+      .left_center
+      {
+        align-items: center;
+
+        
+      }
+
+      .left_count
+      {
+        color:#333;
+        font-size: 16px; 
+        font-weight: 600;
+      }
+
+      .header_right
+      {
+        display: flex;
+        justify-content: flex-end;
+      }
+
+
+    }
+
+    //弹窗 check选择框公共样式
+    .select_check_content
+    {
+      width: calc(100% - 40px);
+      margin: auto;
+      height: auto;
+      // align-items: center;
+
+      .el-checkbox-group
+      {
+        display: flex;
+        flex-wrap: wrap;
+        height: auto;
+        
+      }
+      .el-checkbox 
+      {
+        width:20%; 
+        box-sizing: border-box; /* 确保 padding 和 border 不影响宽度 */
+        // padding: 5px; /* 可选:添加一些内边距 */
+        margin-bottom: 23px;
+        margin-right: 0px;
+
+        font-size: 16px;
+        color:#333333;
+        
+      }
+
+      .el-checkbox__label
+      {
+        font-weight: 400;
+        font-size: 16px;
+        color:#333;
+      }
+      
+    }
+
+    .center_message_title 
+    {
+      font-size: 16px;
+      font-weight: 600;
+      color: #000000;
+      margin-bottom: 10px;
+    }
+
+    .center_message_content {
+      width: calc(100% - 40px);
+      margin: auto;
+
+      display: flex;
+      justify-content: space-between;
+
+      .content_lable {
+        width: 70px;
+        font-weight: 400;
+        font-size: 14px;
+        color: #666666;
+        line-height: 16px;
+        text-align: right;
+
+        padding-top: 20px;
+      }
+
+      .content_radio {
+        width: calc(100% - 70px);
+
+        padding-top: 20px;
+
+        .radio_item {
+          width: 100%;
+          height: auto;
+          display: flex;
+          justify-content: flex-start;
+          margin-bottom: 15px;
+
+          .item_radio {
+          }
+
+          .item_right {
+            .right_title {
+              font-weight: 500;
+              font-size: 16px;
+              color: #666666;
+            }
+
+            .right_message {
+              font-weight: 400;
+              font-size: 14px;
+              color: #999999;
+              margin-top: 10px;
+
+              .input_number {
+                width: 80px;
+                height: 32px;
+                margin-left: 5px;
+                margin-right: 5px;
+              }
+            }
+          }
+        }
+      }
+    }
+
+    .center_message_tishi {
+      width: calc(100% - 40px);
+
+      margin: auto;
+      margin-top: 20px;
+      font-weight: 500;
+      font-size: 16px;
+      line-height: 25px;
+      color: #333333;
+    }
+
+    //导入文件 提示信息
+    .import_message
+    {
+      font-size: 12px;
+      color:#999;
+      line-height: 20px;
+      margin-top: 10px;
+    }
+
+    .center_message_input {
+      width: calc(100% - 40px);
+      margin: auto;
+      margin-top: 20px;
+      margin-bottom: 20px;
+      font-weight: 400;
+      font-size: 16px;
+      color: #666666;
+      display: flex;
+      justify-content: flex-end;
+      align-items: center;
+
+      .el-icon-view {
+        margin-top: 4px;
+        cursor: pointer;
+      }
+
+      .el-input--small .el-input__icon {
+        height: 36px;
+        line-height: 36px;
+      }
+
+      .el-input--small .el-input__inner {
+        width: 244px;
+        height: 36px;
+        line-height: 36px;
+      }
+
+      .el-input__suffix {
+        top: 8px;
+      }
+    }
+
+    .center_message {
+      font-size: 16px;
+      color: #333;
+      padding: 15px 0;
+
+      font-weight: 400;
+      font-size: 14px;
+      color: #666666;
+      line-height: 20px;
+      text-align: left;
+    }
+
+    .score_input {
+      width: 74px !important;
+      margin-left: 8px;
+
+      .el-input__inner {
+        width: 48px !important;
+        height: 36px !important;
+        line-height: 36px !important;
+        font-size: 14px;
+        padding: 0 5px;
+        text-align: center;
+      }
+    }
+
+    .select_width_160
+    {
+      .el-input
+      {
+        width: 160px !important;
+
+      }
+      .el-input__inner
+      {
+        width: 160px !important;
+      }
+    }
+
+    .span_center
+    {
+      margin-left: 5px;
+      margin-right: 5px;
+    }
+    .input_91 {
+      width: 91px !important;
+
+
+      .el-input__inner {
+        width: 91px !important;
+        height: 36px !important;
+        line-height: 36px !important;
+        font-size: 14px;
+        padding: 0 0px;
+        text-align: center;
+      }
+    }
+
+    .list_content
+    {
+      display: flex;
+      justify-content: flex-start;
+      flex-wrap: wrap;
+      gap: 16px;
+
+      
+      .select_item
+      {
+        width: auto;
+        display: flex;
+        justify-content: flex-start;
+
+        .el-checkbox__label
+        {
+          padding: 0 0 0 5px;
+        }
+        .el-input
+        {
+          width: 120px;
+          margin-left: 5px;
+        }
+        .el-input__inner
+        {
+          width: 120px;
+          padding:0 0 0 8px !important;
+        }
+      }
+    }
+
+    .tab_item
+    {
+      // min-width:80px;
+      padding:0 10px;
+      height: 34px;
+      border:1px solid #DCDFE6;
+      line-height: 34px;
+      text-align: center;
+      border-radius: 4px;
+      font-size: 14px;
+      color:#666;
+      margin-right: 10px;
+      cursor: pointer;
+    }
+
+    .tab_active
+    {
+      background-color: #2E64FA;
+      color:#fff;
+    }
+    
+
+    .table_info {
+      font-weight: 600;
+      font-size: 16px;
+      color: #303133;
+      line-height: 24px;
+      margin-bottom: 10px;
+      display: flex;
+      justify-content: space-between;
+    }
+
+    .el-form {
+      width: 100% !important;
+
+      .el-form-item {
+        margin-bottom: 20px;
+      }
+    }
+    //弹窗 el-from-item__content
+    .el-form-item__content {
+      display: flex;
+      align-items: center;
+      font-size: 16px;
+      min-height: 40px;
+
+      .upload_file_name
+      {
+        margin-left: 10px;
+        line-height: 20px;
+      }
+
+      .el-input.is-disabled .el-input__inner
+      {
+        color:#333;
+      }
+      .score {
+        padding-left: 5px;
+      }
+
+      // 下拉选择框 中间箭头上下居中
+      .el-select .el-input .el-select__caret {
+        height: 36px;
+      }
+
+      // 下拉选择框 中间箭头上下居中
+      .el-input__icon {
+        line-height: 36px;
+      }
+
+      .el-button--small
+      {
+        height: 36px !important;
+        font-size: 14px;
+      }
+
+      .el-input {
+        width: 240px;
+        
+      }
+
+      .check_select 
+      {
+        .el-input {
+          width: 87px !important;
+        }
+      }
+
+      .el-input__inner {
+        width: 240px;
+        height: 36px;
+        line-height: 36px;
+        font-size: 14px;
+      }
+
+      .el-input--mini {
+        width: 240px;
+      }
+
+      .el-input-number {
+        width: 240px;
+      }
+
+      // 数字输入框
+      .el-input-number--mini {
+        line-height: 34px;
+      }
+
+      // 开关
+      .el-switch {
+        height: 40px;
+      }
+
+      .radio_box {
+        width: 180px;
+        height: 40px;
+      }
+
+      .radio_number_horizontal {
+        background-image: url("../assets/icon/question_number_horizontal.png");
+        background-size: 78px 30px;
+        background-repeat: no-repeat;
+        background-position: 60px 50%;
+      }
+
+      .radio_number_vertical {
+        background-image: url("../assets/icon/question_number_vertical.png");
+        background-size: 40px 40px;
+        background-repeat: no-repeat;
+        background-position: 60px 50%;
+      }
+
+      .radio_answer_horizontal {
+        background-image: url("../assets/icon/answer_direction_horizontal.png");
+        background-size: 78px 30px;
+        background-repeat: no-repeat;
+        background-position: 60px 50%;
+      }
+
+      .radio_answer_vertical {
+        background-image: url("../assets/icon/answer_direction_vertical.png");
+        background-size: 40px 40px;
+        background-repeat: no-repeat;
+        background-position: 60px 50%;
+      }
+
+      // 组
+      .el-radio-group {
+        font-size: 14px;
+        min-height: 40px;
+        line-height: 40px;
+        
+        // display: flex;
+        // align-items: center;
+        .radio_question_number_horizontal {
+          width: auto;
+          height: 40px;
+          line-height: 40px;
+          // background-image: url('../assets/icon/question_number_horizontal.png');
+          // background-size: 100% 100%;
+          // background-repeat: no-repeat;
+          // background-position: 0 0;
+
+          img {
+            width: 78px;
+            height: 30px;
+          }
+        }
+        .el-radio__label {
+          padding-left: 5px !important;
+          font-size: 16px !important;
+        }
+        
+        .el-radio {
+          margin-right: 20px !important;
+        }
+      }
+
+      .group_item {
+        line-height: 40px;
+      }
+
+      .el-input-group__append {
+        width: 26px !important;
+      }
+
+      .el-checkbox__label {
+        line-height: 36px;
+      }
+    }
+
+    .el-form-item {
+      // margin-bottom: 15px !important;
+      min-height: 40px;
+      .el-form-item__label {
+        font-size: 16px !important;
+        padding: 0 !important;
+        color:#666666;
+      }
+
+      .el-checkbox__label
+      {
+        color:#666;
+        font-size: 16px;
+      }
+    }
+
+    .label_left {
+      margin-left: 20px;
+    }
+
+    .dialog_search {
+      width: 100%;
+      height: auto;
+
+      .search_title_info {
+        font-weight: 600;
+        font-size: 14px;
+        color: #303133;
+
+        span {
+          margin-right: 20px;
+        }
+      }
+
+      .search_input {
+        .el-input-group {
+          width: auto !important;
+        }
+      }
+
+      .search_filter {
+        margin-top: 8px;
+        margin-bottom: 8px;
+      }
+    }
+
+
+
+    .el-table__header,
+    .el-table__body,
+    .el-table__foote {
+      width: 100%;
+    }
+    .el_table {
+      box-shadow: 1px 1px 1px #e4e7ed;
+      border-radius: 4px;
+    }
+    .default_pass {
+      font-size: 12px;
+      color:#666;
+    }
+  }
+  //饿了么底部按钮样式覆盖
+  .el-dialog__footer {
+    padding: 0 20px;
+    text-align: right;
+    border-top: 1px solid #e6e6e6;
+    height: 64px;
+    line-height: 64px;
+    border-radius: 0 0 8px 8px;
+    .dialog_footer {
+      padding: 0 !important;
+      margin: auto;
+      height: 64px;
+      display: flex;
+      justify-content: space-between;
+      .footer_left {
+        width: auto;
+        height: 64px;
+      }
+      .footer_right {
+        width: auto;
+        height: 64px;
+        display: flex;
+        justify-content: flex-end;
+        align-items: center;
+      }
+    }
+    .el-button {
+      width: 68px;
+      height: 36px;
+      padding: 0 !important;
+      font-size: 14px;
+    }
+    //饿了么按钮样式覆盖
+    .el-button--primary {
+      width: 68px;
+      height: 36px;
+      margin-left: 15px;
+    }
+    .el-button--default {
+      width: 68px;
+      height: 36px;
+    }
+    // 自定义特殊按钮样式
+    .no_save_button {
+      width: 82px;
+      height: 36px;
+      background: #ffffff;
+      border-radius: 4px 4px 4px 4px;
+      border: 1px solid #dcdfe6;
+    }
+    .save_button {
+      width: 68px;
+      height: 36px;
+      margin-left: 20px;
+    }
+    .cancel_button {
+      width: 68px;
+      height: 36px;
+      background: #ffffff;
+      border-radius: 4px 4px 4px 4px;
+      border: 1px solid #dcdfe6;
+      margin-left: 20px;
+    }
+  }
+  //消息提醒弹窗
+  .el-message-box__header {
+    padding: 0 !important;
+    height: 48px;
+    line-height: 48px;
+    background-color: #f5f7fa;
+    .el-message-box__title {
+      height: 48px;
+      line-height: 48px;
+      font-weight: 500;
+      font-size: 16px;
+      color: #333;
+      text-indent: 50px;
+      background-image: url("../assets/icon/warning_icon.png");
+      background-size: 20px 20px;
+      background-position: 20px 50%;
+      background-repeat: no-repeat;
+    }
+    .el-message-box__headerbtn {
+      width: 36px;
+      height: 36px;
+      margin-top: -8px;
+      background-image: url("../assets/icon/close-line.png");
+      background-position: 100% 50%;
+      background-repeat: no-repeat;
+      background-size: 24px 24px;
+      .el-icon-close {
+        display: none;
+      }
+    }
+  }
+  .el-message-box__content {
+    min-height: 55px;
+    padding: 0px 0px !important;
+    display: flex;
+    justify-content: left;
+    align-items: center;
+    .el-message-box__message {
+      padding: 20px 10px 20px 20px !important;
+      font-weight: 400;
+      font-size: 14px;
+      color: #666666;
+      line-height: 25px;
+      max-height: 250px;
+      overflow-y: auto;
+      white-space: normal;
+      word-break: break-all;
+    }
+  }
+  .el-message-box__status {
+    display: none !important;
+  }
+  .el-message-box__btns {
+    padding: 13px 20px 4px 20px;
+    border-top: 1px solid #e4e7ed;
+    .el-button {
+      width: 68px;
+      height: 36px;
+      border-radius: 4px 4px 4px 4px;
+      border: 1px solid #dcdfe6;
+      font-size: 14px;
+    }
+  }
+  .el-upload-list {
+    display: none;
+  }
+}
+
+.dialog_table 
+{
+      
+}
+
+//弹窗全屏
+.page_full_dialog
+{
+  //饿了么样式覆盖
+  .el-dialog__header {
+    display: none;
+  }
+  .el-dialog
+  {
+    border-radius: 0;
+  }
+  //饿了么内容样式覆盖
+  .el-dialog__body {
+    font-weight: 400;
+    font-size: 14px;
+    color: #666666;
+    padding: 0 !important;
+    width: 100%;
+    height: 100%;
+    margin: auto;
+    // min-height: 99px;
+    // display: flex;
+    // justify-content: flex-start;
+    // align-items: center;
+  }
+}
+
+.el-popover
+{
+  border-radius: 4px;
+}
+
+//悬浮层公共样式  成绩单表头设置使用
+.page_popover
+{
+
+  width: 100%;
+  height: auto;
+  .popover_content
+  {
+    width: auto;
+    height:calc(100% - 46px) ;
+    .vertical_checkbox_group
+    {
+      width:auto;
+      display: flex;
+      flex-direction:column;
+      flex-wrap: wrap;
+      // overflow-y: auto;
+      height: 300px;
+      .el-checkbox 
+      {
+        width:auto; 
+        // box-sizing: border-box; /* 确保 padding 和 border 不影响宽度 */
+        // padding: 5px 0px; /* 可选:添加一些内边距 */
+        margin-bottom: 10px;
+        // margin-right: 20px;
+        font-weight: 400;
+        font-size: 14px;
+        color:#666;
+      }
+      
+      
+    }
+   
+    .el-checkbox__input.is-disabled + span.el-checkbox__label
+    {
+      color:#666;
+    }
+    
+  }
+
+  .popover_button
+  {
+    width: 100%;
+    height: 36px;
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    gap: 10px;
+
+    .el-button 
+    {
+      margin-left: 0px;
+      min-width: 68px;
+      height: 36px;
+  
+      padding: 0;
+
+      font-weight: 500;
+      font-size: 14px;
+      color: #666666;
+  
+    }
+    .el-button--primary
+    {
+      color:#ffffff;
+    }
+
+  }
+}
+.el-time-panel {
+  width: 130px !important;
+}
+//抽屉弹窗公共样式
+.page_drawer {
+  //日期选择弹窗组件样式覆盖
+  .el-date-picker {
+    background-color: red;
+    width: 200px !important;
+
+    .el-picker-panel__body {
+      width: 200px !important;
+    }
+
+    .el-time-panel {
+      width: 130px !important;
+    }
+  }
+  .el-drawer__header {
+    display: none;
+  }
+
+  .drawer_content {
+    width: 100%;
+
+    .set_search_title {
+      width: 100%;
+      height: 56px;
+      line-height: 56px;
+      text-indent: 20px;
+
+      font-weight: 600;
+      font-size: 16px;
+      color: #303133;
+    }
+  }
+
+  .drawer_footer {
+    width: 360px;
+    position: fixed;
+    bottom: 0;
+    right: 0;
+  }
+
+  .drawer_title {
+    font-size: 16px;
+    color: #fff;
+
+    width: 100%;
+    height: 48px;
+    line-height: 48px;
+    text-indent: 20px;
+
+    font-weight: 600;
+    font-size: 16px;
+    color: #303133;
+
+    border-bottom: 1px solid #14191f25;
+    border-radius: 0px 0px 0px 0px;
+
+    // background-image: url("@/assets/markDetailScore/close_drawer.png");
+    // background-repeat: no-repeat;
+    // background-size: 24px 24px;
+    // background-position: calc(100% - 20px) 50%;
+    cursor: pointer;
+  }
+
+  .sel_item {
+    width: calc(100% - 40px);
+    margin: auto;
+    margin-bottom: 16px;
+
+    .selItem_title {
+      line-height: 28px;
+      font-weight: 400;
+      font-size: 16px;
+      color: #303133;
+    }
+
+    .selValItem_line {
+      margin: 0 9px;
+    }
+  }
+
+  .button_content {
+    width: 100%;
+    height: auto;
+
+    text-align: center;
+    margin-top: 20px;
+    margin-bottom: 20px;
+  }
+
+  .drawer_button {
+    width: calc(100% - 40px);
+    margin: auto;
+    font-weight: 600;
+    font-size: 14px;
+    color: #ffffff;
+    height: 36px;
+  }
+
+  .end_content {
+    width: 100%;
+    height: 64px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-top: 1px solid rgba(25, 31, 37, 0.08);
+
+    .end_button {
+      width: calc(100% - 40px);
+      height: 36px;
+      border-radius: 4px 4px 4px 4px;
+      border: 1px solid #f56c6c;
+
+      font-weight: 500;
+      font-size: 14px;
+      color: #f56c6c;
+    }
+  }
+
+  .w173 {
+    width: 231px;
+  }
+}
+
+
+
+/* 小屏幕笔记本的样式 常见分辨率:1366x768 */
+@media (max-width: 1366px) {
+  .exam_aside {
+    width: 206px;
+  }
+}
+
+/* 中等屏幕笔记本的样式(14-15英寸) 常见分辨率:1600x900, 1920x1080*/
+@media (min-width: 1367px) and (max-width: 1920px) {
+  .exam_aside {
+    width: 15%;
+  }
+}
+
+/* 大屏幕笔记本的样式(17-20英寸) 常见分辨率:2560x1440*/
+@media screen and (min-width: 1920px) {
+  .exam_aside {
+    width: 19%;
+  }
+}
+
+/* 高分辨率设备的样式 */
+@media only screen and (-webkit-min-device-pixel-ratio: 2),
+  only screen and (min-resolution: 192dpi),
+  only screen and (min-resolution: 2dppx) {
+}
+
+
+// 应用滚动条样式到三个类下的 .el-table__body-wrapper
+.page_table, .dialog_table, .area_table, .right_table, .card_table
+{
+  .el-table__body-wrapper {
+    // 滚动条样式修改
+    /* 滚动条轨道区域样式 */
+    &::-webkit-scrollbar {
+      width: 8px;
+      /* 设置滚动条宽度为8像素 */
+      height: 8px;
+      /* 设置滚动条高度为8像素 */
+      background-color: transparent;
+    }
+
+    /* 滑块样式 */
+    &::-webkit-scrollbar-thumb {
+      background-color: #b8b8b8;
+      /* 设置滑块颜色为深灰色 */
+      border-radius: 4px;
+      /* 设置滑块边角半径为4像素 */
+      min-height: 60px;//设置手柄最小高度
+    }
+
+    /* 滚动条轨道内部空白区域样式 */
+    &::-webkit-scrollbar-track {
+      background-color: #f0f0f0;
+      /* 设置轨道背景色为浅灰色 */
+    }
+
+    /* 滚动条两端按钮样式 */
+    &::-webkit-scrollbar-button {
+      display: none;
+      /* 不显示按钮 */
+    }
+
+    /* 交叉点处的区域样式 */
+    &::-webkit-scrollbar-corner {
+      background-color: transparent;
+      /* 设置交叉点处的背景色为透明 */
+    }
+
+    /* 调整大小手柄样式 */
+    &::-webkit-resizer {
+      display: none;
+      /* 不显示调整大小手柄 */
+    }
+  }
+
+  // 表格列表标题栏滚动条
+  .el-table th.gutter {
+    // display: none !important;
+    width: 8px !important;
+  }
+
+  // 表格滚动条列宽
+  .el-table colgroup col[name="gutter"] {
+    // display: ;
+    width: 8px;
+  }
+}
+
+.el-message-box__btns {
+  .el-button + .el-button {
+    margin-left: 10px!important;
+  }
+  .el-button {
+    &:hover {
+      background: none;
+      color: #606266;
+    }
+    &.active {
+      background: none;
+      color: #606266;
+    }
+    .el-button:focus-visible {
+      background: inherit;
+      border: none;
+
+    }
+    &.fouce {
+      background: inherit;
+      border: none;
+    }
+  }
+  .el-button--primary {
+    background: #2E64FA;
+    &:hover {
+      background: #2E64FA;
+      color: #fff;
+    }
+    &.active {
+      background: #2E64FA;
+      color: #fff;
+    }
+  }
+
+}
+//列表行操作
+.table_row_option {
+  width: 100%;
+  text-align: center;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  gap: 10px;
+  
+  .move_icon {
+    width: 36px;
+    height: 36px;
+    background-image: url("../assets/icon/move_icon_hover.png");
+    background-size: 20px 20px;
+    background-repeat: no-repeat;
+    background-position: 0% 50%;
+    cursor: move;
+  }
+
+  .sort_move
+  {
+    width: 36px;
+    height: 36px;
+    background-image: url("../assets/icon/sort_move.webp");
+    background-size: 20px 21px;
+    background-repeat: no-repeat;
+    background-position: 50% 50%;
+    cursor: move;
+  }
+
+  .editor_icon {
+    width: 36px;
+    height: 36px;
+    background-image: url("../assets/icon/editor_icon_hover.png");
+    background-size: 20px 20px;
+    background-repeat: no-repeat;
+    background-position: 0% 50%;
+    cursor: pointer;
+  }
+
+  .delete_icon {
+    width: 36px;
+    height: 36px;
+    background-image: url("../assets/icon/delete_icon.png");
+    background-size: 20px 20px;
+    background-repeat: no-repeat;
+    background-position: 0% 50%;
+    cursor: pointer;
+  }
+
+  .el_button_editor {
+    width: 30px;
+    height: 36px !important;
+    line-height: 36px;
+
+    font-size: 14px;
+    color: #2e64fa;
+    cursor: pointer;
+  }
+
+  .option_button_editor {
+    width: auto;
+    height: 36px !important;
+    line-height: 36px;
+
+    font-size: 14px;
+    color: #2e64fa;
+    cursor: pointer;
+  }
+
+  .editor_disable {
+    color: #bbbbbb !important;
+    cursor: not-allowed !important;
+  }
+
+  .el_button_delete {
+    width: 30px;
+    height: 36px !important;
+    line-height: 36px;
+    font-size: 14px;
+    color: #F56C6C;
+    cursor: pointer;
+  }
+
+  .button_editor {
+    width: auto;
+    height: 36px;
+    line-height: 36px;
+    margin-right: 5px;
+    font-size: 14px;
+    color: #2e64fa;
+    cursor: pointer;
+  }
+
+  .button_delete {
+    width: auto;
+    height: 36px;
+    line-height: 36px;
+    font-size: 14px;
+    color: #F56C6C;
+    cursor: pointer;
+  }
+
+  .option_delete {
+    color: #f56c6c;
+    font-size: 14px;
+    font-weight: 400;
+    cursor: pointer;
+  }
+
+  .option_editor {
+    color: #2e64fa;
+    font-size: 14px;
+    font-weight: 400;
+    cursor: pointer;
+  }
+
+  .option_add {
+    font-weight: 400;
+    font-size: 14px;
+    color: #f56c6c;
+    cursor: pointer;
+  }
+  .option_disabled
+  {
+    font-weight: 400;
+    font-size: 14px;
+    color: #C0C4CC;
+  }
+}
+
+
+
+// 表格滚动条样式通用
+.el-table__body-wrapper {
+  // 滚动条样式修改
+  /* 滚动条轨道区域样式 */
+  &::-webkit-scrollbar {
+    width: 10px;
+    /* 设置滚动条宽度为8像素 */
+    background-color: transparent;
+  }
+
+  /* 滑块样式 */
+  &::-webkit-scrollbar-thumb {
+    background-color: #b8b8b8;
+    /* 设置滑块颜色为深灰色 */
+    border-radius: 4px;
+    /* 设置滑块边角半径为4像素 */
+    min-height: 60px;//设置手柄最小高度
+  }
+
+  /* 滚动条轨道内部空白区域样式 */
+  &::-webkit-scrollbar-track {
+    background-color: #f0f0f0;
+    /* 设置轨道背景色为浅灰色 */
+  }
+
+  /* 滚动条两端按钮样式 */
+  &::-webkit-scrollbar-button {
+    display: none;
+    /* 不显示按钮 */
+  }
+
+  /* 交叉点处的区域样式 */
+  &::-webkit-scrollbar-corner {
+    background-color: transparent;
+    /* 设置交叉点处的背景色为透明 */
+  }
+
+  /* 调整大小手柄样式 */
+  &::-webkit-resizer {
+    display: none;
+    /* 不显示调整大小手柄 */
+  }
+}
+
+// 表格列表标题栏滚动条
+.el-table th.gutter {
+  // display: none !important;
+  width: 8px !important;
+}
+
+// 表格滚动条列宽
+.el-table colgroup col[name="gutter"] {
+  display: none;
+  width: 8px;
+}
+
+// 使用element-ui按钮后的 公共按钮样式  导入 导出  其他等等可追加
+.el_button {
+  .el-button .is-disabled {
+    background-color: #f5f7fa;
+    border-color: #dcdfe6;
+    color: #c0c4cc;
+    cursor: not-allowed;
+    background-color: red !important;
+  }
+
+  .el-button {
+    min-width: 72px;
+    width: auto;
+    height: 36px;
+    padding: 0 !important;
+  }
+
+  //导出
+  .export_btn {
+    width: auto;
+    height: 36px !important;
+    font-weight: 400;
+    font-size: 14px;
+    color: #666666;
+    text-indent: 20px;
+
+    background-image: url("../assets/icon/exam_exports.png");
+    background-repeat: no-repeat;
+    background-size: 16px 16px;
+    background-position: 12px 50%;
+    cursor: pointer;
+  }
+
+  .export_btn:hover {
+    color: #2e64fa;
+    background-image: url("../assets/icon/exam_export.png");
+  }
+
+  //导入
+  .import_btn {
+    width: auto;
+    height: 36px !important;
+    line-height: 36px;
+    font-weight: 400;
+    font-size: 14px;
+    color: #666666;
+    margin-left: 10px;
+    text-indent: 20px;
+    background-image: url("../assets/icon/exam_imports.png");
+    background-repeat: no-repeat;
+    background-size: 16px 16px;
+    background-position: 12px 50%;
+    cursor: pointer;
+  }
+
+  .import_btn:hover {
+    color: #2e64fa;
+    background-image: url("../assets/icon/exam_import.png");
+  }
+
+  //返回扫描
+  .back_scan_button {
+    width: 120px;
+    height: 36px !important;
+    font-weight: 400;
+    font-size: 14px;
+    color: #666666;
+    text-indent: 20px;
+
+    background-image: url("../assets/icon/exam_back.png");
+    background-repeat: no-repeat;
+    background-size: 20px 20px;
+    background-position: 17px 50%;
+    cursor: pointer;
+  }
+
+  //刷新
+  .refresh_btn {
+    width: 68px;
+    height: 36px !important;
+    font-weight: 400;
+    font-size: 14px;
+    color: #666666;
+    text-indent: 20px;
+
+    background-image: url("../assets/icon/refresh_default.png");
+    background-repeat: no-repeat;
+    background-size: 16px 16px;
+    background-position: 10px 50%;
+    cursor: pointer;
+  }
+
+  //适合屏幕
+  .fit_screen_btn {
+    width: 96px;
+    height: 36px;
+
+    font-weight: 400;
+    font-size: 14px;
+    color: #666666;
+
+    text-align: center;
+    background-image: url("../assets/icon/fit_screen_default.png");
+    background-size: 16px 16px;
+    background-position: 8px 50%;
+    background-repeat: no-repeat;
+    text-indent: 20px;
+    margin-left: 10px;
+    margin-right: 0px;
+  }
+
+  // 删除
+  .delete_btn {
+    width: 96px;
+    height: 36px !important;
+    font-weight: 400;
+    font-size: 14px;
+    color: #f56c6c;
+    text-align: center;
+    border: 1px solid #f56c6c;
+    cursor: pointer;
+    margin-left: 20px;
+  }
+
+  // 编辑
+  .editor_btn {
+    width: 96px;
+    height: 36px !important;
+    font-weight: 400;
+    font-size: 14px;
+    color: #2E64FA;
+    text-align: center;
+    border: 1px solid #2E64FA;
+    cursor: pointer;
+    margin-left: 20px;
+  }
+
+  //标记为正常
+  .marke_normal_btn {
+    width: 96px;
+    height: 36px !important;
+    font-weight: 400;
+    font-size: 14px;
+    color: #2e64fa;
+    text-align: center;
+    border: 1px solid #2e64fa;
+    cursor: pointer;
+    margin-left: 20px;
+  }
+
+  //标记为正常 禁用
+  .marke_normal_btn_disable {
+    width: 110px;
+    height: 36px !important;
+    font-weight: 400;
+    font-size: 14px;
+    color: #c0c4cc;
+    cursor: not-allowed;
+    background-image: none;
+    background-color: #ffffff;
+    border-color: #ebeef5;
+    margin-left: 20px;
+  }
+
+  // 标记为缺考
+  .marke_miss_exam_btn {
+    width: 110px;
+    height: 36px !important;
+    font-weight: 400;
+    font-size: 14px;
+    color: #2e64fa;
+    text-align: center;
+    border: 1px solid #2e64fa;
+    cursor: pointer;
+    margin-left: 20px;
+  }
+
+  //重新切割
+  .re_cut_btn {
+    width: calc(50% - 10px);
+    height: 36px !important;
+    font-weight: 400;
+    font-size: 14px;
+    color: #2e64fa;
+    text-align: center;
+    border: 1px solid #2e64fa;
+    cursor: pointer;
+  }
+
+  //灰色按钮
+  .abnormal_other_btn {
+    width: calc(50% - 10px);
+    height: 36px !important;
+    font-weight: 400;
+    font-size: 14px;
+    color: #666666;
+    text-align: center;
+    border: 1px solid #e9e9e9;
+    cursor: pointer;
+  }
+
+  //删除试卷
+  .delete_paper_btn {
+    width: calc(50% - 10px);
+    height: 36px !important;
+    font-weight: 400;
+    font-size: 14px;
+    color: #f56c6c;
+    text-align: center;
+    border: 1px solid #f56c6c;
+    cursor: pointer;
+  }
+
+  // 回收站
+  .recycle_bin_btn {
+    width: calc(100% - 40px);
+    margin: auto;
+    height: 36px !important;
+    font-weight: 400;
+    font-size: 14px;
+    color: #dcdfe6;
+    text-align: center;
+    border: 1px solid #dcdfe6;
+    cursor: pointer;
+    margin-left: 20px;
+  }
+
+  //结束批阅
+  .end_review {
+    width: 96px;
+    height: 36px;
+    background: #f56c6c;
+    border-radius: 4px 4px 4px 4px;
+    border: 0px solid #f56c6c;
+    font-size: 14px;
+    font-weight: 500;
+    color: #fff;
+  }
+
+  //处理异常卷
+  .review_abnormal {
+    width: 110px !important;
+    height: 36px;
+    background: #ffffff;
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #f56c6c;
+    font-weight: 400;
+    font-size: 14px;
+    color: #f56c6c;
+  }
+
+  .other_button_review {
+    width: 68px;
+    height: 36px;
+    background: #ffffff;
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #dcdfe6;
+    font-weight: 400;
+    font-size: 14px;
+    color: #666666;
+  }
+
+  //重新导入名单
+  .other_btn {
+    width: 120px !important;
+    height: 36px !important;
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #dcdfe6;
+    font-weight: 400;
+    font-size: 14px;
+    color: #666666;
+  }
+
+
+  //通用蓝色无背景编辑按钮
+  .editor_item
+  {
+    width: 96px;
+    height: 36px !important;
+    font-weight: 400;
+    font-size: 14px;
+    color: #2E64FA;
+    text-align: center;
+    border: 1px solid #2E64FA;
+    cursor: pointer;
+  }
+  .editor_item:hover
+  {
+    background: rgba(46,100,250,0.1);
+    color: #2E64FA;
+    border: 1px solid #2E64FA;
+  }
+  // 通用红色无背景删除
+  .delete_item 
+  {
+    width: 96px;
+    height: 36px !important;
+    font-weight: 400;
+    font-size: 14px;
+    color: #f56c6c;
+    text-align: center;
+    border: 1px solid #f56c6c;
+    cursor: pointer;
+  }
+  .delete_item:hover 
+  {
+    background: rgba(245,108,108,0.1);
+    color: #f56c6c;
+    border: 1px solid #f56c6c;
+  }
+}
+
+
+
+
+
+.pic_item_list {
+  width: 100%;
+
+  padding-top: 10px;
+  // overflow-x: auto;
+  // height: 160px;
+  display: flex;
+  justify-content: flex-start;
+
+  .draggable_move {
+    display: flex;
+
+    span {
+      display: flex;
+      justify-content: flex-start;
+      align-items: center;
+    }
+  }
+
+  .pic_item_img_detail {
+    width: 181px;
+    height: auto;
+    position: relative;
+    margin-right: 10px;
+
+    .pic_item_img_title {
+      font-weight: 500;
+      font-size: 14px;
+      color: #303133;
+      margin-bottom: 5px;
+      display: flex;
+      justify-content: space-between;
+
+      .title_left {
+        font-weight: 500;
+        font-size: 14px;
+        color: #303133;
+      }
+
+      .title_right {
+        font-weight: 400;
+        font-size: 14px;
+        color: #666666;
+      }
+    }
+
+    .pic_item_img_content {
+      width: 181px;
+      height: 128px;
+
+      // background-color: red;
+      img {
+        width: 100%;
+        height: 100%;
+
+        cursor: pointer;
+        border-radius: 5px;
+        object-fit: contain;
+      }
+    }
+
+    .button_group {
+      position: absolute;
+      display: flex;
+      display: none;
+      width: 181px;
+      height: 128px;
+      z-index: 10;
+      top: 20px;
+      left: 0;
+      background: rgba(0, 0, 0, 0.4);
+      align-items: center;
+      justify-content: center;
+      gap: 16px;
+
+      .pic_button_view {
+        width: 68px;
+        height: 34px;
+        text-align: center;
+        line-height: 34px;
+
+        cursor: pointer;
+        background-color: rgba(255, 255, 255, 1);
+        border: 1px solid rgba(220, 223, 230, 1);
+        border-radius: 4px;
+        color: #666666;
+
+        text-indent: 15px;
+        background-image: url("../assets/icon/pic_show_view.png");
+        background-size: 16px 16px;
+        background-position: 10px 50%;
+        background-repeat: no-repeat;
+        font-size: 14px;
+      }
+
+      .pic_button_delete {
+        width: 68px;
+        height: 34px;
+        text-align: center;
+        line-height: 34px;
+
+        cursor: pointer;
+        background-color: rgba(255, 255, 255, 1);
+        border: 1px solid rgba(220, 223, 230, 1);
+        border-radius: 4px;
+
+        text-indent: 10px;
+        background-image: url("../assets/icon/pic_show_delete.png");
+        background-size: 16px 16px;
+        background-position: 10px 50%;
+        background-repeat: no-repeat;
+        color: #f56c6c;
+        font-size: 14px;
+      }
+    }
+  }
+
+  .pic_item_img_detail:hover .button_group {
+    display: flex;
+  }
+}
+
+
+// 42行高表格公共样式
+.table_42 {
+  width: 100%;
+  padding-bottom: 10px;
+
+  .el-table {
+    width: 100%;
+    border-left: 1px solid #e9e9e9;
+    border-right: 1px solid #e9e9e9;
+    border-radius: 4px;
+
+    // 关键修改:确保内部单元格有右边框
+    // 注意:Element UI 的 border 属性通常会自动处理,但如果被覆盖,需手动指定
+    th.el-table__cell, 
+    td.el-table__cell {
+      border-right: 1px solid #e9e9e9 !important; // 强制添加竖线颜色
+    }
+
+    // 如果不想让最后一列显示右边框(通常做法),可以加上这个
+    // 如果希望所有列都有竖线,包括最后一列与表框之间,则注释掉下面这块
+    th.el-table__cell:last-child,
+    td.el-table__cell:last-child {
+      border-right: none !important;
+    }
+  }
+    
+  .el-table--border .no-border {
+    border-right: none !important; // 移除右边框
+  }
+
+  .el-table--border .no-border + .el-table__cell {
+    border-left: none !important; // 移除左边框
+  }
+
+  .el-table .cell {
+    padding: 0 !important;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+
+  .el-table .el-table__cell {
+    padding: 0 !important;
+    height: 42px;
+  }
+
+  .el-table tr th.el-table__cell {
+    background-color: #F0F2F5 !important;
+  }
+
+  .el-table th.el-table__cell > .cell {
+    padding: 0 !important;
+  }
+
+  .el-table thead {
+    width: 100%;
+    height: 42px !important;
+    background-color: #F0F2F5 !important;
+
+    font-weight: 500;
+    font-size: 16px;
+    color: #333;
+  }
+
+  .el-table .el-table__body tr {
+    height: 42px;
+    font-size: 14px;
+  }
+
+  .table_row_title {
+    width: 100%;
+    display: inline-block;
+    // display: block;
+    overflow: hidden;
+    /* 隐藏超出容器的内容 */
+    white-space: nowrap;
+    /* 保持文本在一行内 */
+    text-overflow: ellipsis;
+    /* 超出部分显示省略号 */
+    cursor: pointer;
+  }
+
+  .full_mark_input
+  {
+    width: 88px;
+    margin: auto;
+  }
+
+  //表格内 通用输入框
+  .input_width
+  {
+    width: calc(100% - 32px);
+    margin: auto;
+  }
+}
+
+//无数据显示
+.no_content_data
+{
+  min-height: 500px;
+  background-image: url("../assets/bg/no_content_bg.png");
+  background-repeat: no-repeat;
+  background-size:360px auto;
+  background-position: 50% 30%;
+  color: #999999;
+  justify-content: center;
+  display: flex;
+  align-items: center;
+}
+
+//按钮公共样式
+.ele_button
+{
+  .btn_delete
+  {
+    width: auto;
+    font-weight: 500;
+    font-size: 14px;
+    color: #F56C6C;
+    cursor: pointer;
+  }
+
+  .btn_editor
+  {
+    width: auto;
+    font-weight: 500;
+    font-size: 14px;
+    color: #2E64FA;
+    cursor: pointer;
+  }
+
+  .btn_disabled
+  {
+    width: auto;
+    font-weight: 500;
+    font-size: 14px;
+    color: #C0C4CC;
+    cursor: not-allowed;
+  }
+}
+
+//表格内操作按钮
+.table_row_button
+{
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  gap: 10px;
+
+}

+ 165 - 0
src/styles/element.scss

@@ -0,0 +1,165 @@
+//饿了么基础样式覆盖
+//默认高度36px
+.el-select .el-select__wrapper,
+.el-input .el-input__wrapper,
+.el-date-editor.el-input,
+.el-button {
+  height: 36px;
+}
+
+.el-button:focus  {
+  outline: none;
+}
+.el-input-group__append {
+  width: 36px;
+}
+.el-button.is-text {
+  padding: 0 5px;
+  font-weight: 400;
+}
+.el-button.is-text:not(.is-disabled):active,
+.el-button.is-text:not(.is-disabled):hover 
+{
+  background: none;
+}
+
+//按钮样式覆盖
+.el-button--primary
+{
+    background-color: #2E64FA;
+}
+.el-button--primary:hover 
+{
+    color: #2e64fa;
+    // background-color: none;
+    font-size: 14px;
+    background: #5883fb;
+    border-color: #5883fb;
+    color: #fff !important;
+} 
+
+.el-button {
+
+  &:hover {
+    // background: #2E64FA;
+    color:#666;
+  }
+  &.actiive {
+    border: none;
+  }
+}
+.el-button.button_border {
+  background: #fff;
+  border: 1px solid  #2E64FA;
+  color: #2E64FA;
+  &:hover {
+    background: none;
+  }
+}
+.el-button.button_border_gray {
+  background: #fff;
+  border: 1px solid #DCDFE6;
+  color: #BBBBBB;
+  &:hover {
+    background: none;
+  }
+  &.cancel  {
+    color: #606266;
+  }
+}
+.el-button.button_refresh {
+  min-width: 72px!important;
+  background: #fff;
+  border: 1px solid #DCDFE6;
+  color: #606266;
+  &:hover {
+    background: none;
+  }
+  &.cancel  {
+    color: #606266;
+  }
+}
+
+.el-checkbox__input.is-indeterminate .el-checkbox__inner,
+.el-checkbox__input.is-checked .el-checkbox__inner {
+  background: #2E64FA;
+  border-color: #2E64FA;
+}
+.el-checkbox__inner:hover, .el-radio__inner:hover {
+  border-color: #2E64FA;
+}
+
+.el-radio__input.is-checked .el-radio__inner {
+  background: #2E64FA;
+  border-color: #2E64FA;
+}
+.el-radio__input.is-checked+.el-radio__label {
+  color: #2E64FA;
+}
+.el-radio-button.is-active .el-radio-button__original-radio:not(:disabled)+.el-radio-button__inner {
+  background: #2E64FA;
+  border-color: #2E64FA;
+}
+.el-checkbox__input.is-checked+.el-checkbox__label {
+  color: #2E64FA;
+}
+.el-select-dropdown__item.is-selected {
+  color: #2E64FA;
+}
+.el-dropdown-menu__item:not(.is-disabled):focus, .el-dropdown-menu__item:not(.is-disabled):hover {
+  color: #2E64FA;
+}
+button:focus, button:focus-visible {
+  outline: none;
+}
+.el-picker-panel__icon-btn:focus-visible {
+  color: #2E64FA;
+}
+.el-picker-panel__icon-btn:hover {
+  color: #2E64FA;
+}
+.el-date-picker__header-label:hover {
+  color: #2E64FA;
+}
+.el-date-table td.available:hover {
+  color: #2E64FA;
+}
+.el-date-table, .el-year-table, .el-month-table  {
+  td.today .el-date-table-cell__text {
+    color: #2E64FA;
+  }
+  td .el-date-table-cell__text:hover {
+    color: #2E64FA;
+  }
+  td.current:not(.disabled) .el-date-table-cell__text {
+    background-color: #2E64FA;
+  }
+}
+.el-message-box__btns .el-button:active {
+  color: #606266;
+  border-color: #DCDFE6; 
+  background: none;
+}
+.el-message-box__btns .el-button:hover {
+  border-color: #DCDFE6;
+}
+.el-message-box__headerbtn:focus .el-message-box__close, 
+.el-message-box__headerbtn:hover .el-message-box__close {
+  color: #999;
+}
+
+.el-select__wrapper.is-focused,
+.el-input__wrapper.is-focus {
+  box-shadow: 0 0 0 1px #2E64FA inset;
+}
+
+.el-select-dropdown.is-multiple .el-select-dropdown__item.is-selected:after {
+  background: #2E64FA;
+}
+
+.el-pagination button.is-active, .el-pagination button:hover {
+  color: #2E64FA;
+}
+.el-pagination.is-background .el-pager li.is-active {
+  background: #2E64FA;
+}

+ 259 - 0
src/styles/login.scss

@@ -0,0 +1,259 @@
+.login_page
+{
+  background-image:  url('@/assets/login/login_bg.webp');
+  background-size: cover;
+  background-position:0 0;
+  width:100%;
+  height:100%;
+  .login_header
+  {
+    width: 100%;
+    height:64px;
+   
+    background: linear-gradient( 90deg, #006AE2 0%, #04A0F4 100%);
+    box-shadow: 0px 8 16px 0px rgba(172,200,243,0.1);
+    border-radius: 0px 0px 0px 0px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    .header_left
+    {
+      width: calc(100% - 300px);
+      display: flex;
+      align-items: center;
+      font-weight: 400;
+      font-size: 24px;
+      color: #FFFFFF;
+
+      img
+      {
+        width: 116px;
+        height: 32px;
+        margin-left: 40px;
+        margin-right: 5px;
+      }
+    }
+
+    .header_right
+    {
+      min-width: 300px;
+      display: flex;
+      justify-content: flex-end;
+      align-items: center;
+      .right_button
+      {
+        width: 114px;
+        height: 34px;
+        background: rgba(255,255,255,0.2);
+        border-radius: 22px 22px 22px 22px;
+        border: 1px solid #FFFFFF;
+        margin-right: 40px;
+        font-weight: 400;
+        font-size: 16px;
+        color: #FFFFFF;
+        text-align: center;
+        line-height: 34px;
+
+        cursor: pointer;
+      }
+    }
+  }
+
+  .login_content
+  {
+    width: 100%;
+    height:calc(100% - 64px);
+   
+
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background-image: url('@/assets/login/login_bg_center.webp');
+    background-size: contain;
+    background-position:10% 50%;
+    background-repeat: no-repeat;
+
+  }
+
+  .login_info
+  {
+    min-width:450px;
+    height: 562px;
+    background: #FFFFFF;
+    border-radius: 10px 10px 10px 10px;  
+
+    top:calc(50% - 281px + 32px);
+    right:13.5%;
+    position: absolute;
+    padding: 29px;
+    box-sizing: border-box;
+    .login_info_title
+    {
+      width: 100%;
+      
+      background: #FFFFFF;
+      border-radius: 10px 10px 0px 0px;
+
+      font-weight: 500;
+      font-size: 32px;
+      color: #333333;
+      line-height: 45px;
+      margin-bottom: 20px;
+    }
+
+    .login_info_message
+    {
+      font-weight: 500;
+      font-size: 16px;
+      color: #333333;
+      line-height: 40px;
+
+      margin-bottom: 10px;
+    }
+
+    .login_info_input {
+      width: 100%;
+      height: auto;
+      margin-bottom: 10px;
+      .el-form-item:last-child {
+        margin-bottom: 0;
+      }
+      .el-input__wrapper {
+        padding: 0;
+        height: 48px;
+      }
+      .el-input__inner {
+        width: 100%;
+        height: 48px;
+        line-height: 48px;
+        padding: 0;
+        text-indent: 50px;
+      }
+      .el-input .el-input__prefix {
+        position: absolute;
+        display: flex;
+        align-items: center;
+        left: 20px;
+      }
+      .el-input .el-input__suffix {
+        position: absolute;
+        right: 17px;
+        display: flex;
+        align-items: center;
+      }
+      .iconfont {
+        cursor: pointer;
+      }
+    }
+
+    .login_info_item {
+      width: 100%;
+      
+      margin-bottom: 20px;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+
+      .item_left
+      {
+        width: 50%;
+        text-align: left;
+
+
+      }
+
+      .item_right
+      {
+        width: 50%;
+        text-align: right;
+
+        font-weight: 400;
+        font-size: 12px;
+        color: rgba(25,31,37,0.72);
+      }
+    }
+
+    .login_info_button
+    {
+      width: 100%;
+      height: 53px;
+      background: #0470FF;
+      border-radius: 10px;
+      border: none;
+      font-weight: 600;
+      font-size: 16px;
+      color: #FFFFFF;
+      line-height: 53px;
+      text-align:center;
+      .el-button
+      {
+        background-color: transparent;
+        border:none;
+        color:#fff;
+        height: 52px;
+        font-size: 16px;
+        font-weight: 600;
+      }
+
+    }
+
+    .login_other_type
+    {
+
+      width: 100%;
+      height: auto;
+      margin-top: 10px;
+      .other_login_line
+      {
+        display: flex;
+        justify-content: center;
+        height: 45px;
+        align-items: center;
+        .line 
+        {
+          flex: 1;
+          height: 1px;
+          background-color: #dcdfe6;
+        }
+        
+        .text 
+        {
+          margin: 0 10px;
+
+          font-weight: 400;
+          font-size: 14px;
+          color: #999999;
+          img {
+            width: 32px;
+          }
+        }
+      }
+
+      .other_login_icon
+      {
+        display: flex;
+        justify-content: center;
+        .icon_item
+        {
+          width: auto;
+          height: auto;
+          text-align: center;
+          padding:10px 20px;
+          img
+          {
+            width: 32px;
+            height: 32px;
+          }
+
+          p
+          {
+            font-weight: 400;
+            font-size: 12px;
+            color: #909399;
+            line-height: 20px;
+          }
+        }
+      }
+    }
+  }
+}

+ 10 - 0
src/types/types.ts

@@ -0,0 +1,10 @@
+// src/types/index.ts (或者 src/api/types.ts)
+
+/**
+ * 后端统一返回结构
+ */
+export interface ApiResponse<T = any> {
+  code: number;
+  msg: string;
+  data: T;
+}

+ 5 - 0
src/types/vue.d.ts

@@ -0,0 +1,5 @@
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue'
+  const component: DefineComponent<{}, {}, any>
+  export default component
+}

+ 28 - 0
src/utils/common.ts

@@ -0,0 +1,28 @@
+/**
+ * 科目背景色映射表
+ */
+export const COURSE_COLOR_MAP: Record<string, string> = {
+  '语文': '#91CC75', // 浅绿
+  '数学': '#FAC858', // 黄色
+  '英语': '#EE6666', // 红色
+  '物理': '#817BE2', // 紫色
+  '化学': '#73C0DE', // 天蓝
+  '生物': '#3BA272', // 绿色
+  '政治': '#63A1FF', // 
+  '历史': '#FC8452', // 土黄
+  '地理': '#9A60B4', // 天蓝
+  '道德与法治': '#FF869D',
+  'default': '#C495FA' // 默认颜色
+}
+
+/**
+ * 根据科目名称获取背景色
+ * @param courseName 科目名称
+ * @returns 背景色十六进制字符串
+ */
+export const getCourseBgColor = (courseName: string): string => {
+  if (!courseName) {
+    return COURSE_COLOR_MAP['default']
+  }
+  return COURSE_COLOR_MAP[courseName] || COURSE_COLOR_MAP['default']
+}

+ 24 - 0
src/utils/jsencrypt.ts

@@ -0,0 +1,24 @@
+import JSEncrypt from 'jsencrypt'
+
+// 公钥字符串
+// 注意:实际项目中建议将公钥放在 .env 环境变量中,通过 import.meta.env.VITE_RSA_PUBLIC_KEY 获取,避免硬编码
+const PUBLIC_KEY = `MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDmkP5hUxr7x3t76s9kiVrP1acGVp3JFyPsSoZzV+FvFSc78rfLSwEhj0WkUC0O+entDZ/hnvX1pSPsY8VwXtc20n5pSuo/G1HzrFSUCmFD9HWX3RRrFTHGn+G01CQswO/2JZokJQzbTsNDUnlK/w//NYQvXhxOhF9fM/S1L73CkYPIabHmIM1Rc8uik66XlTXrb9+gDQ8Uon+QZTGo3BzxWceONYYnVZZXdFQtzvaHLI5av8LSU5MLp03faX8fYlfB5FwyBL+dQZRVyTP09pbESn30487tsa0/+0pRZ1D+4Tsc2cjIpuXEg1IKGLrs0TSX78T1fROt/JcZ/lt3Tn9xAgMBAAECggEADEAD4/PgaSQuIWVWY4cQth4p46JSe86o7/L9tb8jkR1UmlDJBxoTE09jadmAq10H2rpwljI16zk88WBTqya+1IDWio2aaIPxFLtBOyRaCpxAazMp1I6puF3iRhNHYMFXfoJ88BKv3i8PHNKS8zMeDHcxcLrVUi6iSpKeG8pPkLjEqY0eccYjTUOyh2Q8YmLLwO3+lOOYrD9TcUZ1NqsZCqOI3pzLNONYatqaPprqiuZLUcloiO7D1OJ52LtTzlP2z7/jXM4fwUZK1/9U79zMYmjJifIORY9UiildkGaKzYs3G1ZAKiLKBehf7tGzorU0+tSIF99voyjhed286fMeqQKBgQDn7wH6oadnfZL5NknEaioua4GJ0LhBeZCiaLReYs116554zzBubb4Y752DTE4ka8LKsSIcnCqNbc/b6q0Kw+yThJJ5F/nFgaCzKHfhImqtslolqW5K1UVwUZ9RdFUeeqt8w+CtcnrJs/jIswv7XOnfHm6VKd0gSlpbnAPEVua75wKBgQD+farEb4PCNsOTaLQYcy0dSDh68lBskc5k+kMYmDsUloaOQel2S9ufEEYxZgy4Y2b0U9Y8nEJByFBoKZW+Mplp+hH9OWaK/QpMylh1uwoN44jUyRAKhR211kBZMXcgbIjr49PxG261OydxIyu9ntcJApVpHK30qzR+DBRD7H8+5wKBgQDGz/hQUZXgfqIoAkNFnQO/euQ1sLbhWUWEEmDar7MTq//R6zjG0EettGi/Df/F9KGrgh+NishnJ4SQLSBcJAp9gZzVNJoklbOdH8lzMT9k2Yew1QX4G81ENJNvDVuRnvG1J2tHAuUCVcWitOhGdiT732hHcPVeIp5F/Py1pxBubQKBgQDlVvJxm90tRJTzXsQN1J2vacocYgpADRXmwfF9VJLJdu1DffqadLoymkPneIO2Fz5MqNDERj0fcxmjBPbBNHA0pPtZLEVQs8B4e1FEp43j/kztFVSzZkrj93R97KniOm0Zx3LUMViPUgO1XXCprV8z63QiCYpql27yuIf6vkHduQKBgEe/BLwVHxAxmGzt/k0t9TZYn4MMQd5nx1UWlFzI+mNkNQqNC5cLGWarazeHC2YdcaYAsSMfi11NTvnVY79EbXYomxSBoN3Y63ML7W6Cj9fInVtUF6j002sdRyfey+e2BQtYiUHb9xW6A7sf4LyLZJ64LGYIh1uqWQc2iUeUsSWQ`
+
+/**
+ * RSA 加密方法
+ * @param data 需要加密的字符串
+ * @returns 加密后的字符串
+ */
+export function encryptPassword(data: string): string {
+  if (!data) return ''
+  
+  const encryptor = new JSEncrypt()
+  // 设置公钥
+  encryptor.setPublicKey(PUBLIC_KEY)
+  
+  // 执行加密
+  const encrypted = encryptor.encrypt(data)
+  
+  // encrypt 方法可能返回 false (当加密失败时),这里做一个容错处理
+  return encrypted || ''
+}

+ 69 - 0
src/utils/request.ts

@@ -0,0 +1,69 @@
+// src/utils/request.ts
+import axios from 'axios'
+import { ElMessage } from 'element-plus'
+import type { AxiosInstance, AxiosResponse ,InternalAxiosRequestConfig} from 'axios'
+
+// 创建 axios 实例
+const service: AxiosInstance = axios.create({
+  baseURL: import.meta.env.VITE_API_BASE_URL || '', // 从环境变量中读取
+  timeout: 10000, // 超时时间
+  withCredentials: true // 是否允许携带 cookie
+})
+
+// 请求拦截器
+service.interceptors.request.use(
+  (config:InternalAxiosRequestConfig) => {
+    // 可在此添加 token 到 headers 中
+    const token = localStorage.getItem('token')
+    if (token) {
+      config.headers!['Authorization'] = `${token}`
+    }
+    return config
+  },
+  (error) => { 
+    // 请求错误处理
+    console.error('请求错误:', error)
+    return Promise.reject(error)
+  }
+)
+
+// 响应拦截器 添加泛型 <ApiResponse>,明确拦截器返回的是业务数据结构而非原始 AxiosResponse
+service.interceptors.response.use(
+  (response: AxiosResponse) => {
+    const res = response.data
+
+    // if (res.code !== 200) {
+    //   ElMessage({
+    //     message: res.msg || '请求失败',
+    //     type: 'error',
+    //     duration: 5 * 1000
+    //   })
+
+      // 特殊状态码处理,如 token 失效等
+      if (res.code === 401) {
+        // 跳转登录页
+        window.location.href = '/'
+      }
+      else
+      {
+        return res
+      }
+
+    //   return Promise.reject(res.msg || '请求失败')
+    // }
+    //这里直接返回 res (即 ApiResponse 对象),而不是 response
+    // 这样在组件中接收到的就是 { code, msg, data }
+    // return res 
+  },
+  (error) => {
+    // 网络错误、超时等处理
+    const message = error.message.includes('timeout') ? '请求超时' : '网络异常'
+    ElMessage({
+      message,
+      type: 'error'
+    })
+    return Promise.reject(error)
+  }
+)
+
+export default service

+ 357 - 0
src/utils/scanCommon.js

@@ -0,0 +1,357 @@
+import { Message } from 'element-ui';
+
+// 定义 WebSocket 和扫描客户端的通信模块
+const scanCommon = {
+    // 扫描客户端WebSocket 服务器地址
+    url: 'ws://127.0.0.1:9999',//扫描端口9999  图片服务端口9998
+
+    // WebSocket 实例
+    ws: null,
+
+    // 当前客户端连接状态
+    isOnline:false,
+    isScanned:false,//是否有扫描仪 是否可以扫描
+
+    // 心跳定时器
+    timerPing: null,
+    timerClose: null,
+    connectionInterval:null,//监听连接状态变化的定时器
+
+    // 心跳间隔时间(单位:毫秒)
+    intervalPing: 10000, // 每隔10秒发送一次ping
+    intervalClose: 5000,  // 等待5秒未收到pong则判定断开
+    isManuallyStopped:false,//是否手动停止,防止重复连接
+    /**
+     * 初始化 WebSocket 连接
+     * @param {Function} callback - 接收扫描结果的回调函数
+     */
+    init(callback) {
+        // Message({
+        //     type: 'success',
+        //     message: '扫描程序正在启动,请稍后……'
+        // });
+
+        try {
+            this.ws = new WebSocket(this.url);
+        } catch (error) {
+            // console.error('WebSocket 初始化失败:', error);
+            Message({
+                type: 'error',
+                message: '无法初始化 WebSocket 连接,请检查网络设置',
+                duration: 5000
+            });
+        //    if (callback) callback({ success: false, message: 'WebSocket 初始化失败' });
+           return;
+          
+        }
+
+        /**
+         * WebSocket 连接建立成功时触发
+         */
+        this.ws.onopen = () => {
+            this.isOnline = true;
+            // 添加状态检查
+            if (this.ws && this.ws.readyState === WebSocket.OPEN) {
+                this.ws.send("client connect success");
+            } else {
+                // console.warn('WebSocket 尚未就绪,延迟发送消息');
+                setTimeout(() => {
+                    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
+                        this.ws.send("client connect success");
+                    }
+                }, 500); // 延迟发送,等待连接真正建立
+            }
+            this.startHeartbeat(); // 启动心跳检测
+        };
+
+        /**
+         * 接收到服务端消息时触发
+         * @param {MessageEvent} evt - WebSocket 返回的消息事件对象
+         */
+        this.ws.onmessage = (evt) => {
+            // console.log("打印扫描仪返回的 evt:", evt.data);
+            if(evt.data!=null)
+            {
+                if (evt.data === "ping") 
+                {
+                    this.send("pong"); // 回复 pong
+                } else if (evt.data === "pong") 
+                {
+                    // 收到 pong 不做处理
+                } else {
+                    // try {
+                        const objectData = JSON.parse(evt.data);
+                        console.log("处理扫描结果的objectData:", objectData);
+                        if(objectData!=null)
+                        {
+                             if (objectData.code === 200) 
+                            {
+                                if(objectData.action=='connectMessage')
+                                {
+                                    Message({
+                                        type: 'success',
+                                        message: '客户端连接成功……'
+                                    });
+                                }
+                                else if(objectData.action=='getScannerList')
+                                {
+                                    //获取扫描仪结果
+                                    // console.log("打印扫描仪返回的 objectData:", objectData);
+                                   
+                                    if(objectData.data.length>0)
+                                    {
+                                        //有扫描仪连接 
+                                        this.isScanned=true;
+                                        Message({
+                                            type: 'success',
+                                            message: '扫描仪连接成功……'
+                                        });
+                                    }
+                                    else
+                                    {
+                                        //没有扫描仪连接
+                                        this.isScanned=false;
+                                    }
+
+                                }
+                                else
+                                {
+                                    
+                                    callback(objectData); // 可选:通过回调函数返回扫描结果
+                                    
+                                    
+                                }
+                                
+                                
+                                
+                            }
+                            else if (objectData.code === 509) 
+                            {
+                                // Message({
+                                //     type: 'error',
+                                //     message: '扫描仪未连接,请检查网络设置'
+                                // });
+                                callback(objectData); // 可选:通过回调函数返回扫描结果
+                            } else if (objectData.code === 502) 
+                            {
+                                // Message({
+                                //     type: 'error',
+                                //     message: '未检测到纸张或卡纸'
+                                // });
+                                callback(objectData); // 可选:通过回调函数返回扫描结果
+                                // this.ws.close(); // 关闭链接
+                            } 
+                        }
+                       
+
+                    // } catch (error) {
+                    //     console.error("解析消息失败:", error);
+                        
+                    // }
+                }
+            }
+            
+            
+
+            this.resetHeartbeat(); // 重置心跳
+        };
+
+        /**
+         * WebSocket 连接关闭时触发
+         */
+        this.ws.onclose = () => {
+            // console.log("连接关闭");
+            // if(callback) callback({success: false, message: '客户端已关闭!'})
+            this.handleOffline();
+        };
+
+        /**
+         * WebSocket 发生错误时触发
+         * @param {Event} evt - 错误事件对象
+         */
+        this.ws.onerror = (evt) => {
+            // console.error("WebSocket 错误:", evt);
+            // Message({
+            //     type: 'error',
+            //     message: 'WebSocket 发生错误,请检查扫描仪是否运行正常'
+            // });
+            // if(callback) callback({success: false, message: '客户端未启动!'})
+            // console.log("扫描客户端未启动");
+            this.isOnline=false;    
+            this.ws.close();
+        };
+    },
+
+    /**
+     * 发送消息给 WebSocket 服务端
+     * @param {string} data - 要发送的消息内容
+     */
+    send(data) {
+        if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
+
+            
+            // Message({
+            //     type: 'error',
+            //     message: '请确认扫描客户端是否已经启动',
+            //     duration: 5000
+            // });
+            return;
+        }
+
+        try {
+            // console.log("发送数据:" + data);
+            this.ws.send(data);
+        } catch (e) {
+            // console.error("发送数据失败:", e);
+            Message({
+                type: 'error',
+                message: '发送数据失败,请检查连接状态',
+                duration: 5000
+            });
+        }
+    },
+
+    /**
+     * 启动心跳机制
+     */
+    startHeartbeat() {
+        this.timerPing = setTimeout(() => {
+            if (this.ws && this.ws.readyState === WebSocket.OPEN) {
+                this.send("ping"); // 发送心跳 ping
+                this.timerClose = setTimeout(() => {
+                    if (this.ws) {
+                        this.ws.close(); // 若长时间无响应,主动断开连接
+                    }
+                    this.isOnline = false;
+                    this.handleOffline(); // 处理离线逻辑
+                }, this.intervalClose);
+            }
+        }, this.intervalPing);
+    },
+
+    /**
+     * 重置心跳定时器
+     */
+    resetHeartbeat() {
+        clearTimeout(this.timerPing);
+        clearTimeout(this.timerClose);
+        this.startHeartbeat(); // 重新启动心跳
+    },
+
+
+    /**
+     * 停止 WebSocket 连接并清除所有定时器
+     */
+    stop() {
+        // 清除心跳定时器
+        clearTimeout(this.timerPing);
+        clearTimeout(this.timerClose);
+
+        // 清除连接状态监听定时器
+        this.stopWatchingConnection();
+
+        // 关闭 WebSocket 连接
+        if (this.ws) {
+            this.ws.onclose = () => {}; // 移除原有 onclose 处理
+            this.ws.close(); // 主动关闭连接
+            this.ws = null;
+        }
+
+        // 标记为手动停止,防止后续自动重连
+        this.isManuallyStopped = true;
+
+        // 重置连接状态
+        this.isOnline = false;
+
+        // console.log('WebSocket 已被手动停止');
+    },
+
+    /**
+     * 处理设备离线逻辑,尝试自动重连
+     */
+    handleOffline() {
+
+        // 如果是手动停止,则不再尝试重连
+        if (this.isManuallyStopped) {
+            return;
+        }
+        // console.log("客户端未开启,尝试重新连接...");
+        // Message({
+        //     type: 'warning',
+        //     message: '扫描仪已断开连接,尝试重新连接...',
+        //     duration: 3000
+        // });
+        // console.log("当前连接状态",this.isOnline);
+        this.isOnline=false;   
+        // 延迟尝试重连
+        setTimeout(() => {
+            if (!this.isOnline && !this.isManuallyStopped) {
+                this.init(); // 重新初始化连接
+            }
+        }, this.intervalClose * 2);
+    },
+    /**
+     * 检查客户端是否已连接
+     * @returns {boolean}
+     */
+    isClientConnected() {
+        return this.ws && this.ws.readyState === WebSocket.OPEN;
+    },
+
+    /**
+     * 监听连接状态变化
+     * @param {Function} callback - 回调函数,接收 isOnline 布尔值参数
+     */
+    watchConnection(callback) {
+        if (this.connectionInterval) {
+            clearInterval(this.connectionInterval); // 防止重复启动
+        }
+
+        const checkStatus = () => {
+            const isOnline = this.isClientConnected();
+            //调用检测扫描仪指令
+            if(isOnline)
+            {
+                // console.log("开始检测扫描仪状态",this.isScanned);
+                // console.log("开始检查客户端状态",isOnline);
+                if(this.isScanned)
+                {
+                    //有扫描仪连接  就不发送指令了
+                   
+                }
+                else
+                {
+                    //没有扫描仪连接 实时发送指令获取状态
+                    let json={
+                        action:'getScannerList',//获取扫描仪信息
+                    };
+                    // console.log("发送指令获取扫描仪信息",json);
+                    this.ws.send(JSON.stringify(json));
+                   
+                }
+                callback(isOnline);//告诉网页 客户端打开状态
+            }
+            else
+            {
+                callback(false);//告诉网页 连接已经断开
+            }
+
+            
+        };
+
+        checkStatus(); // 立即执行一次
+        this.connectionInterval = setInterval(checkStatus, 3000); // 每3秒检测一次
+    },
+
+    /**
+     * 停止监听连接状态
+     */
+    stopWatchingConnection() {
+        if (this.connectionInterval) {
+            clearInterval(this.connectionInterval);
+            this.connectionInterval = null;
+        }
+    }
+};
+
+export default scanCommon;

+ 326 - 0
src/utils/scanCommon.ts

@@ -0,0 +1,326 @@
+import { ElMessage } from 'element-plus';
+
+// 定义扫描动作类型
+type ScanAction = 'connectMessage' | 'getScannerList' | 'scanResult' | string;
+
+// 定义接收到的消息结构
+interface ScanMessage {
+  code: number;
+  action?: ScanAction;
+  data?: any;
+  message?: string;
+}
+
+// 定义回调函数类型
+type ScanCallback = (data: ScanMessage) => void;
+type ConnectionCallback = (isOnline: boolean) => void;
+
+class ScanCommonService {
+  // 扫描客户端WebSocket 服务器地址
+  private url: string = 'ws://127.0.0.1:9999'; // 扫描端口9999 图片服务端口9998
+
+  // WebSocket 实例
+  private ws: WebSocket | null = null;
+
+  // 当前客户端连接状态
+  public isOnline: boolean = false;
+  public isScanned: boolean = false; // 是否有扫描仪 是否可以扫描
+
+  // 心跳定时器
+  private timerPing: number | null = null;
+  private timerClose: number | null = null;
+  private connectionInterval: number | null = null; // 监听连接状态变化的定时器
+
+  // 心跳间隔时间(单位:毫秒)
+  private intervalPing: number = 10000; // 每隔10秒发送一次ping
+  private intervalClose: number = 5000; // 等待5秒未收到pong则判定断开
+  
+  private isManuallyStopped: boolean = false; // 是否手动停止,防止重复连接
+
+  /**
+   * 初始化 WebSocket 连接
+   * @param callback - 接收扫描结果的回调函数
+   */
+  init(callback?: ScanCallback): void {
+    // 如果已经连接且未手动停止,不重复初始化
+    if (this.ws && this.ws.readyState === WebSocket.OPEN && !this.isManuallyStopped) {
+      return;
+    }
+
+    // 重置手动停止标志,允许重连
+    this.isManuallyStopped = false;
+
+    try {
+      this.ws = new WebSocket(this.url);
+    } catch (error) {
+      console.error('WebSocket 初始化失败:', error);
+      ElMessage({
+        type: 'error',
+        message: '无法初始化 WebSocket 连接,请检查网络设置',
+        duration: 5000
+      });
+      return;
+    }
+
+    /**
+     * WebSocket 连接建立成功时触发
+     */
+    this.ws.onopen = () => {
+      this.isOnline = true;
+      
+      // 发送连接成功消息
+      const sendConnectMsg = () => {
+        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
+          this.ws.send("client connect success");
+        }
+      };
+
+      sendConnectMsg();
+      
+      // 兜底延迟发送,防止某些极端情况下的就绪延迟
+      setTimeout(sendConnectMsg, 500);
+
+      this.startHeartbeat(); // 启动心跳检测
+    };
+
+    /**
+     * 接收到服务端消息时触发
+     */
+    this.ws.onmessage = (evt: MessageEvent) => {
+      if (evt.data != null) {
+        if (evt.data === "ping") {
+          this.send("pong"); // 回复 pong
+        } else if (evt.data === "pong") {
+          // 收到 pong 不做处理,仅用于重置心跳
+        } else {
+          try {
+            const objectData: ScanMessage = JSON.parse(evt.data);
+            console.log("处理扫描结果的objectData:", objectData);
+
+            if (objectData != null) {
+              if (objectData.code === 200) {
+                if (objectData.action === 'connectMessage') {
+                  ElMessage({
+                    type: 'success',
+                    message: '客户端连接成功……'
+                  });
+                } else if (objectData.action === 'getScannerList') {
+                  // 获取扫描仪结果
+                  if (objectData.data && Array.isArray(objectData.data) && objectData.data.length > 0) {
+                    // 有扫描仪连接 
+                    this.isScanned = true;
+                    ElMessage({
+                      type: 'success',
+                      message: '扫描仪连接成功……'
+                    });
+                  } else {
+                    // 没有扫描仪连接
+                    this.isScanned = false;
+                  }
+                } else {
+                  // 其他业务数据回调
+                  if (callback) callback(objectData);
+                }
+              } else if (objectData.code === 509) {
+                // 扫描仪未连接
+                if (callback) callback(objectData);
+              } else if (objectData.code === 502) {
+                // 未检测到纸张或卡纸
+                if (callback) callback(objectData);
+              }
+            }
+          } catch (error) {
+            console.error("解析消息失败:", error);
+          }
+        }
+      }
+      this.resetHeartbeat(); // 重置心跳
+    };
+
+    /**
+     * WebSocket 连接关闭时触发
+     */
+    this.ws.onclose = () => {
+      this.handleOffline();
+    };
+
+    /**
+     * WebSocket 发生错误时触发
+     */
+    this.ws.onerror = () => {
+      this.isOnline = false;
+      if (this.ws) {
+        this.ws.close();
+      }
+    };
+  }
+
+  /**
+   * 发送消息给 WebSocket 服务端
+   * @param data - 要发送的消息内容
+   */
+  send(data: string): void {
+    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
+      // 静默失败或根据需要提示
+      return;
+    }
+
+    try {
+      this.ws.send(data);
+    } catch (e) {
+      console.error("发送数据失败:", e);
+      ElMessage({
+        type: 'error',
+        message: '发送数据失败,请检查连接状态',
+        duration: 5000
+      });
+    }
+  }
+
+  /**
+   * 启动心跳机制
+   */
+  private startHeartbeat(): void {
+    this.clearHeartbeatTimers();
+
+    this.timerPing = window.setTimeout(() => {
+      if (this.ws && this.ws.readyState === WebSocket.OPEN) {
+        this.send("ping"); // 发送心跳 ping
+        
+        this.timerClose = window.setTimeout(() => {
+          if (this.ws) {
+            this.ws.close(); // 若长时间无响应,主动断开连接
+          }
+          this.isOnline = false;
+          this.handleOffline(); // 处理离线逻辑
+        }, this.intervalClose);
+      }
+    }, this.intervalPing);
+  }
+
+  /**
+   * 清除心跳定时器
+   */
+  private clearHeartbeatTimers(): void {
+    if (this.timerPing) {
+      clearTimeout(this.timerPing);
+      this.timerPing = null;
+    }
+    if (this.timerClose) {
+      clearTimeout(this.timerClose);
+      this.timerClose = null;
+    }
+  }
+
+  /**
+   * 重置心跳定时器
+   */
+  private resetHeartbeat(): void {
+    this.clearHeartbeatTimers();
+    this.startHeartbeat(); // 重新启动心跳
+  }
+
+  /**
+   * 停止 WebSocket 连接并清除所有定时器
+   */
+  stop(): void {
+    // 标记为手动停止,防止后续自动重连
+    this.isManuallyStopped = true;
+
+    // 清除心跳定时器
+    this.clearHeartbeatTimers();
+
+    // 清除连接状态监听定时器
+    this.stopWatchingConnection();
+
+    // 关闭 WebSocket 连接
+    if (this.ws) {
+      // 移除事件监听,防止触发 handleOffline 导致重连
+      this.ws.onclose = null;
+      this.ws.onerror = null;
+      this.ws.onmessage = null;
+      this.ws.onopen = null;
+      
+      if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
+        this.ws.close();
+      }
+      this.ws = null;
+    }
+
+    // 重置连接状态
+    this.isOnline = false;
+    this.isScanned = false;
+  }
+
+  /**
+   * 处理设备离线逻辑,尝试自动重连
+   */
+  private handleOffline(): void {
+    this.isOnline = false;
+
+    // 如果是手动停止,则不再尝试重连
+    if (this.isManuallyStopped) {
+      return;
+    }
+
+    // 延迟尝试重连
+    setTimeout(() => {
+      if (!this.isOnline && !this.isManuallyStopped) {
+        this.init(); // 重新初始化连接
+      }
+    }, this.intervalClose * 2);
+  }
+
+  /**
+   * 检查客户端是否已连接
+   * @returns {boolean}
+   */
+  isClientConnected(): boolean {
+    return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
+  }
+
+  /**
+   * 监听连接状态变化
+   * @param callback - 回调函数,接收 isOnline 布尔值参数
+   */
+  watchConnection(callback: ConnectionCallback): void {
+    if (this.connectionInterval) {
+      clearInterval(this.connectionInterval); // 防止重复启动
+    }
+
+    const checkStatus = () => {
+      const isOnline = this.isClientConnected();
+      
+      // 调用检测扫描仪指令
+      if (isOnline) {
+        if (!this.isScanned) {
+          // 没有扫描仪连接 实时发送指令获取状态
+          const json = {
+            action: 'getScannerList', // 获取扫描仪信息
+          };
+          this.ws?.send(JSON.stringify(json));
+        }
+        callback(true); // 告诉网页 客户端打开状态
+      } else {
+        callback(false); // 告诉网页 连接已经断开
+      }
+    };
+
+    checkStatus(); // 立即执行一次
+    this.connectionInterval = window.setInterval(checkStatus, 3000); // 每3秒检测一次
+  }
+
+  /**
+   * 停止监听连接状态
+   */
+  stopWatchingConnection(): void {
+    if (this.connectionInterval) {
+      clearInterval(this.connectionInterval);
+      this.connectionInterval = null;
+    }
+  }
+}
+
+// 导出单例
+const scanCommon = new ScanCommonService();
+export default scanCommon;

+ 166 - 0
src/views/choice/addSchool.vue

@@ -0,0 +1,166 @@
+<template>
+  <el-dialog v-model="schoolData.showDialog" :title="`${schoolData.tenantName} 新增学校`" width="800px">
+    <div class="page_list">
+      <div class="search_content">
+        <div class="content_left">
+          <el-select v-model="provinceSelect" placeholder="请选择" @change="SearchChange">
+            <el-option v-for="province in provincesList" :key="province.provinceCode" :label="province.provinceName" :value="province.provinceCode"></el-option>
+          </el-select>
+          <el-input v-model="schooltName" placeholder="请输入学校名称或编号" @input="SearchChange">
+            <template #append>
+            <el-button :icon="Search" @click="SearchChange" />
+            </template>
+          </el-input>
+        </div>
+      </div>
+    </div>
+    <div class="page_jg_20"></div>
+    <div class="page_table"> 
+      <el-table :data="tableData" stripe v-loading="tableLoading" element-loading-text="拼命加载中……" height="500px" ref="tableDataRef"
+        element-loading-spinner="el-icon-loading" element-loading-background="#ffffff" @selection-change="handleSelectionChange">
+          <el-table-column type="selection" align="center" width="50" />
+          <el-table-column type="index" label="序号" align="center" width="50" />
+          <el-table-column prop="tenantCode" label="账号" align="center" width="80" />
+          <el-table-column prop="tenantName" label="学校名称" align="center" />
+          <el-table-column prop="cityAndAreaName" label="区域" align="center" width="150">
+            <template #default="{ row }">
+              {{ row.cityAndAreaName ||  '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="schoolType" label="账号类型" align="center" width="80">
+            <template #default="{ row }">
+              {{ row.schoolType == 0 ? '单校版' : row.schoolType == 1 ? '联考版' :  '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="status" label="状态" align="center" width="70">
+            <template #default="{ row }">
+              <div class="table_row_status">
+                <!-- 正式 -->
+                <div class="need_summary_icon" v-if="row.status === 1 && row.type === 1"></div>
+                <!-- 试用 -->
+                <div class="yes_release_icon" v-else-if="row.status === 1 && row.type === 0"></div>
+                <!-- 冻结 -->
+                <div class="no_release_icon" v-else-if="row.status === 0"></div>
+                <!-- 状态文本 -->
+                {{ row.status === 1 ? (row.type === 1 ? '正式' : row.type === 0 ? '体验' : '-') : row.status === 0 ? '冻结' : '-' }}
+              </div>
+            </template>
+          </el-table-column>
+      </el-table>
+    </div>
+    <template #footer>
+      <el-button class="button_border_gray cancel" @click="schoolData.showDialog = false">取消</el-button>
+      <el-button class="button_background" :loading="submitLoading" @click="AddComfirm">确定</el-button>
+    </template>
+  </el-dialog>
+</template>
+<script lang="ts">
+export default {
+  name: 'AddSchool',
+}
+</script>
+<script lang="ts" setup>
+import { Search } from '@element-plus/icons-vue'
+import { ElMessage, ElTable } from 'element-plus'
+import { ref, inject, onMounted, nextTick } from 'vue'
+import { getSingleExamList, addSingleSchool } from '@/api/examination'
+import { provinceTree } from '@/api/school'
+interface SchoolData {
+  showDialog: boolean
+  tenantName: string
+  id: string
+}
+const schoolData = inject<SchoolData>('schoolData', {
+  showDialog: false,
+  tenantName: '',
+  id: ''
+})
+interface Province {
+  provinceCode: string
+  provinceName: string
+  children?: any[] // 可选字段
+}
+const provincesList = ref<Province[]>([])
+const provinceSelect = ref('0')
+const schooltName =  ref('')
+const tableData = ref([])
+const selUserList = ref([])
+const selectListId = inject<any>('selectListId', [])
+const tableDataRef = ref<InstanceType<typeof ElTable>>()
+// 提交按钮loading
+const submitLoading = ref(false)
+onMounted(() => {
+  GetProvinceTree()
+  GetSingleExamList()
+})
+// 获取省市区树列表
+const GetProvinceTree = async () => {
+  const res = await provinceTree()
+  if(res.code == 200 && res.data) {
+    const { data } = res
+    let allObj = {
+      provinceName: '全部省份',
+      provinceCode: '0'
+    }
+    provincesList.value.push(allObj)
+    provincesList.value.push(...data)
+  }
+}
+const tableLoading = ref(false)
+const GetSingleExamList = async () => {
+  try {
+    tableLoading.value = true
+    const res = await getSingleExamList({
+      queryStr: schooltName.value,
+      provinceCode: provinceSelect.value == '0' ? '' : provinceSelect.value,
+      // cityCode: '',
+      // areaCode: '',
+    })
+    if (res.code === 200) {
+      tableData.value = res.data
+      nextTick(() => {
+        tableData.value.forEach((item: any) => {
+          if(selectListId.value.includes(item.tenantCode)) {
+            tableDataRef.value?.toggleRowSelection(item, true)
+          }
+        })
+      })
+    }
+  }catch {} finally {
+    tableLoading.value = false
+  }
+}
+const SearchChange = () => {
+  GetSingleExamList()
+}
+const handleSelectionChange = (val: any) => {
+  selUserList.value = val.map((item: any) => item.id)
+}
+const emit = defineEmits(['RefTable'])
+const AddComfirm = async () => {
+  submitLoading.value = true
+  try {
+    const res = await addSingleSchool({
+      schoolParentId: schoolData.id,
+      schoolChildId: selUserList.value
+    })
+    if(res.code == 200) {
+      ElMessage.success(res.msg)
+      schoolData.showDialog = false
+      emit('RefTable', false)
+    }else {
+      ElMessage.error(res.msg)
+    }
+    submitLoading.value = false
+  } catch (error) {
+    console.log(error)
+    submitLoading.value = false
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.page_list {
+  display: flex;
+}
+</style>

+ 404 - 0
src/views/choice/index.vue

@@ -0,0 +1,404 @@
+<template>
+  <div class="page_item page_list">
+
+    <div class="search_content">
+        <div class="content_left">
+          <label class="left_title">届别:</label>
+          <el-select  v-model="params.graduates"   placeholder="选择届别" @change="GoSearch()" class="select_width" >
+            <el-option label="全部届别" value=""></el-option>
+            <el-option v-for="item in graduatesList"
+              :key="item.value"
+              :label="item.label" :value="item.value"></el-option>
+          </el-select>
+          <el-select  v-model="params.levels"   placeholder="选择学段" @change="ChangeLevels()" class="select_width" >
+            <el-option label="全部学段" value=""></el-option>
+            <el-option v-for="item in levelsList"
+              :key="item.value"
+              :label="item.label" :value="item.value"></el-option>
+          </el-select>
+          <el-input placeholder="考试名称,编号"  v-model="params.keyWord" @input="GoSearch" @change="GoSearch()" class="input_width" > 
+            <el-button @click="GoSearch()"  slot="append" icon="el-icon-search"></el-button>
+          </el-input>
+        </div>
+        <div class="content_right">
+          <el-button @click="Refresh">
+            <img src="@/assets/icon/refresh_default.png" />刷新
+          </el-button>
+          <el-button type="primary" @click="OpenAddExam" >
+            <img src="@/assets/icon/add_icon.webp" />新增考试
+          </el-button>
+        </div>
+    </div>
+    <FiltersItem :data="filtersData" @select="GetSelect"></FiltersItem>
+    <div class="content_list">
+      <div class="page_jg_16" v-if="examList.length>0"></div>
+      <div class="list_item" v-for="exam in examList">
+        <div class="item_left">
+          <div class="left_course" :style="{backgroundColor:getCourseBgColor(exam.courseName)}" >
+            {{exam.courseName}}
+          </div>
+          <div class="left_info">
+            <div class="info_title">
+              {{exam.examSubjectName}}
+            </div>
+            <div class="info_message">
+              <span>时间:{{exam.createDate}}</span>
+              <span>年级:{{exam.gradeName}}</span>
+              <span>创建人:{{exam.createdBy}}</span>
+              <span>考试编号:{{exam.examSubjectCode}}</span>
+            </div>
+          </div>
+        </div>
+        <div class="item_right">
+          <div class="ele_button right_button">
+            
+            <span @click="OpenDeleteExam(exam)" class="btn_delete" v-if="exam.canDelete==1">删除</span>
+            <span class="btn_disabled" v-else>删除</span>
+            <span @click="OpenEditorExam(exam)" class="btn_editor">编辑</span>
+            <span @click="GotoExam(exam)" class="btn_editor">进入考试</span>
+          </div>
+        </div>
+      </div>
+      <div class="no_content_data" v-if="examList.length==0">
+        暂无数据
+      </div>
+    </div>
+    <div class="page_pagination">
+      <el-pagination background
+        :page-size="pageInfo.pageSize"
+        :pager-count="11"
+        layout="prev, pager, next"
+        :total="pageInfo.total"
+        :current-page="pageInfo.pageNum" 
+        @current-change="HandlePageChange"
+      />
+    </div>
+    <AddExam ref="addExamRef" :editData="currentEditItem" v-model="showAddExam" @success="HandleSuccess" />
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive,onMounted } from 'vue'
+import FiltersItem from '@/components/FiltersItem.vue';
+import AddExam from '@/components/AddExam.vue' // 注意路径可能需要调整
+import { getHeadData,getExamList ,deleteExam} from '@/api/exam'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getCourseBgColor } from '@/utils/common' 
+import { useRouter } from 'vue-router' 
+import { useExamStore } from '@/store/exam'
+const examStore = useExamStore()
+const router = useRouter()
+const showAddExam = ref(false)
+// 新增:用于存储当前编辑的数据,null 表示新增模式
+const currentEditItem = ref<any>(null)
+
+const params = reactive({
+  graduates: '',//届别
+  levels: '',//学段
+  keyWord: '',//关键词
+  gradeCode:'',//年级code
+  courseCode:'',//科目code
+})
+const pageInfo = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  total: 0
+})//分页信息
+// 定义接口类型以增强类型安全(可选但推荐)
+interface SearchParams {
+  graduates: string;
+  levels: string;
+  keyWord: string;
+}
+// 1. 定义过滤项中列表项的类型
+interface FilterListItem {
+  label: string
+  value: string
+}
+
+// 2. 定义单个过滤条件的类型
+interface FilterItem {
+  label: string
+  list: FilterListItem[]
+  value: string
+}
+
+
+const filtersData=ref<FilterItem[]>([
+  {
+    label:'年级',
+    list:[],
+    value:'',
+  },
+  {
+    label:'科目',
+    list:[],
+    value:'',
+  }
+]);//过滤条件数据
+
+
+const graduatesList=ref<any[]>([]);//届别列表
+const levelsList=ref<any[]>([]);//学段列表
+const examList=ref<any[]>([]);//考试列表
+
+
+const ChangeLevels = () => {
+
+  GoSearch();
+}
+
+
+//获取头部筛选数据
+const GetHeadData=async ()=>{
+    try {
+    const res = await getHeadData()
+    console.log("打印头部数据res",res)
+    if (res && res.code === 200 && res.data) {
+      //届别数据
+      if (res.data.graduates) 
+      {
+        graduatesList.value = res.data.graduates
+      }
+      //学段事件
+      if (res.data.levels) 
+      {
+        levelsList.value = res.data.levels
+      }
+      //年级数据
+      if(res.data.grades)
+      {
+        const allOption = {
+          label: '全部',
+          value: '' // 通常“全部”对应的值为空字符串,以便筛选时不传参或传空
+        }
+        filtersData.value[0].list = [allOption,...res.data.grades]
+      } 
+      if(res.data.courses)
+      {
+        const allOption = {
+          label: '全部',
+          value: '' // 通常“全部”对应的值为空字符串,以便筛选时不传参或传空
+        }
+        filtersData.value[1].list = [allOption,...res.data.courses]
+      }
+    } else {
+      ElMessage.warning(res?.msg || '获取头部数据失败')
+    }
+  } catch (error) {
+    console.error('GetHeadData error:', error)
+    ElMessage.error('网络请求异常,请稍后重试')
+  }
+}
+
+//选择的过滤条件
+const GetSelect = (index: number, value: string) => {
+  console.log('选择的过滤条件:', index, value)
+  filtersData.value[index].value = value
+  if (index === 0) 
+  {
+    params.gradeCode = value
+
+    GoSearch()
+  } else if (index === 1) {
+    params.courseCode = value
+
+    GoSearch()
+  }
+}
+
+//搜索
+const GoSearch = () => { 
+  GetExamList()
+}
+
+//分页切换
+const HandlePageChange=(newPage: number)=>{
+  pageInfo.pageNum = newPage
+  GetExamList()
+  console.log('分页切换:', newPage)
+}
+//获取列表数据
+const GetExamList = async () => { 
+  console.log('搜索参数:', params)
+  const param={
+    pageNum:pageInfo.pageNum,
+    pageSize:pageInfo.pageSize,
+
+    examType:1,//考试类型  1 智能选择题判分   2 智能填空题判分  3 Ai作文
+    graduates:params.graduates,
+    levelCode:params.levels,
+    queryStr:params.keyWord,
+    gradeCode:params.gradeCode,
+    courseCode:params.courseCode
+  };
+  const res= await getExamList(param);
+  console.log("打印列表数据",res)
+  if(res.code==200)
+  {
+    examList.value=res.data.records;
+    pageInfo.total=Number(res.data.total);
+
+  }
+}
+
+//刷新列表
+const Refresh = () => { 
+  params.graduates = ''
+  params.levels = ''
+  params.keyWord = ''
+  params.gradeCode = ''
+  params.courseCode = ''
+  filtersData.value[0].value = ''
+  filtersData.value[1].value = ''
+  GoSearch()
+  GetHeadData()
+}
+
+//考试添加成功 刷新列表
+const HandleSuccess = () => {
+  console.log('考试新增成功,刷新列表')
+  GetHeadData()
+  GoSearch();
+}
+
+//新增考试
+const OpenAddExam = () => {
+  showAddExam.value = true
+  currentEditItem.value = null
+}
+
+//删除考试方法
+const OpenDeleteExam=async(item:any) => {
+  ElMessageBox.confirm('确认删除该考试吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async () => {
+    console.log('删除考试', item)
+    let param={
+      id:item.id
+    };
+    const res = await deleteExam(param)
+    if(res.code == 200) {
+      ElMessage.success("删除成功!")
+      GetHeadData()
+      GoSearch()
+    }else {
+      ElMessage.error(res.msg)
+    }
+  })
+}
+
+//编辑的时候需要把数据传到子组件里
+const OpenEditorExam=(item?:any) => {
+  console.log("编辑考试",item);
+  currentEditItem.value=item || null;//存储当前编辑的数据,null 表示新增模式
+  showAddExam.value = true
+  
+}
+
+//进入考试 把当前的考试信息存储到本地存储中
+const GotoExam=(item:any) => {
+  console.log("进入考试",item);
+
+  try {
+    // localStorage.setItem('current_exam_info', JSON.stringify(item));
+    examStore.setExamInfo(item)
+    console.log('将当前的考试信息存储到本地:', item)
+  } catch (error) {
+    console.error('存储考试信息失败:', error)
+  }
+  router.push({
+    path:'/exam/question',
+    query:{
+      id:item.id
+    }
+  })
+}
+
+// 组件挂载时获取初始数据
+onMounted(() => {
+  GetHeadData()
+  GoSearch()
+})
+</script>
+
+<style lang="scss" scoped>
+.content_list
+{
+  width: 100%;
+  min-height: calc(100vh - 338px);
+   box-sizing: border-box;
+  // height:auto;
+  // overflow-y: auto;
+  
+  .list_item
+  {
+    width: 100%;
+    height: 75px;
+    border-radius: 6px;
+    border:1px solid #DCDFE6;
+    box-sizing: border-box;
+    margin-bottom: 16px;
+
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+
+    .item_left
+    {
+      padding-left: 10px;
+      display: flex;
+      justify-content: flex-start;
+      gap: 10px;
+      .left_course
+      {
+        width: 46px;
+        height: 48px;
+        // background: #91CC75;
+        border-radius: 5px 5px 5px 5px;
+        line-height: 48px;
+        text-align: center;
+        font-weight: 500;
+        font-size: 16px;
+        color: #FFFFFF;
+      }
+
+      .left_info
+      {
+
+        .info_title
+        {
+          width: 100%;
+          font-weight: 500;
+          font-size: 18px;
+          color: #333;
+        }
+
+        .info_message
+        {
+          display: flex;
+          justify-content: flex-start;
+          gap: 16px;
+          font-weight: 400;
+          font-size: 14px;
+          color: #999;
+        }
+      }
+    }
+
+    .item_right
+    {
+      padding-right: 15px;
+      .right_button
+      {
+        display: flex;
+        justify-content: flex-end;
+        gap: 10px;
+      }
+    }
+  }
+
+}
+</style>

+ 0 - 0
src/views/choice/选择题.txt


+ 161 - 0
src/views/essay/addAdmin.vue

@@ -0,0 +1,161 @@
+<template>
+  <el-dialog v-model="dialogData.showDialog" :title="dialogData.pageType == 'add' ? '新增用户' : dialogData.pageType == 'edit' ? '编辑用户' : ''" width="560px">
+    <el-form :model="userForm" :rules="rules" ref="userFormRef">
+      <el-form-item label="姓名:" label-width="120px" prop="nickname">
+        <el-input v-model="userForm.nickname" placeholder="请输入姓名" />
+      </el-form-item>
+      <el-form-item label="账号:" label-width="120px" prop="username">
+        <el-input v-model="userForm.username" placeholder="请输入账号" />
+      </el-form-item>
+      <el-form-item label="密码:" label-width="120px" prop="passwordOfNewUser" v-if="dialogData.pageType == 'add'">
+        <el-input v-model="userForm.passwordOfNewUser" type="password" show-password placeholder="请输入密码" autocomplete="new-password" />
+      </el-form-item>
+      <el-form-item label="手机号:" label-width="120px" prop="phoneNo">
+        <el-input v-model="userForm.phoneNo" placeholder="请输入手机号" />
+      </el-form-item>
+      <el-form-item label="性别:" label-width="120px" prop="gender">
+        <el-radio-group v-model="userForm.gender">
+          <el-radio :value="2">男</el-radio>
+          <el-radio :value="1">女</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="状态:" label-width="120px" prop="status">
+        <el-radio-group v-model="userForm.status">
+          <el-radio :value="1">正常</el-radio>
+          <el-radio :value="0">封禁</el-radio>
+        </el-radio-group>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button class="button_border_gray cancel" @click="dialogData.showDialog = false">取消</el-button>
+      <el-button class="button_background" :loading="submitLoading" @click="SubmitForm(userFormRef)">确定</el-button>
+    </template>
+  </el-dialog>
+</template>
+<script lang="ts">
+export default {
+  name: 'AddAdmin',
+}
+</script>
+<script lang="ts" setup>
+import type { FormInstance, FormRules } from 'element-plus'
+import { ElMessage } from 'element-plus'
+import { ref, reactive, onMounted, inject } from 'vue'
+import { managerDetails, addManager, editManager, deleteManager } from '@/api/manager'
+interface DialogData {
+  showDialog: boolean
+  pageType: string
+  id: string
+}
+let emit = defineEmits(['update'])
+let dialogData = inject<DialogData>('dialogData', {
+  showDialog: false,
+  pageType: 'add',
+  id: ''
+})
+// 新增用户表单数据定义
+interface UserForm {
+  id: string
+  nickname: string
+  username: string
+  passwordOfNewUser: string
+  phoneNo: string
+  gender: number
+  status: number
+}
+// 新增用户表单数据初始化
+const userForm = reactive<UserForm>({
+  id: '',
+  nickname: '',
+  username: '',
+  passwordOfNewUser: '',
+  phoneNo: '',
+  gender: 0,
+  status: 1,
+})
+const userFormRef = ref<FormInstance>()
+// 表单校验规则
+const rules = reactive<FormRules<UserForm>>({
+  nickname: [
+    { required: true, message: '请输入姓名', trigger: 'blur' }
+  ],
+  username: [
+    { required: true, message: '请输入账号', trigger: 'blur' }
+  ],
+  passwordOfNewUser: [
+    { required: true, message: '请输入密码', trigger: 'blur' }
+  ],
+  phoneNo: [
+    { 
+      validator: (rule, value, callback) => {
+        if(!value) return callback() 
+        const pattern = /^1[3-9]\d{9}$/
+        if(!pattern.test(value)) {
+          callback(new Error('请输入正确的手机号'))
+        }
+        callback()
+      }, 
+      trigger: 'blur' 
+    }
+  ],
+  // gender: [
+  //   { required: true, message: '请选择性别', trigger: 'change' }
+  // ],
+  // status: [
+  //   { required: true, message: '请选择状态', trigger: 'change' }
+  // ]
+})
+onMounted(() => {
+  if (dialogData.pageType == 'edit') {
+    GetManagerDetails()
+  }
+})
+const GetManagerDetails = () => {
+  managerDetails(dialogData.id).then((res: any) => {
+    if(res.code == 200 && res.data) {
+      const { data } = res
+      userForm.id = data.id
+      userForm.nickname = data.nickname
+      userForm.username = data.username
+      userForm.passwordOfNewUser = data.passwordOfNewUser
+      userForm.phoneNo = data.phoneNo
+      userForm.gender = data.gender
+      userForm.status = data.status
+    }
+  })
+}
+const submitLoading = ref(false)
+const SubmitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate(async(valid) => {
+    if (valid) {
+      submitLoading.value = true
+      try {
+        let res
+        if(dialogData?.pageType == 'add') {
+          res = await addManager(userForm)
+        }else if(dialogData?.pageType == 'edit') {
+          res = await editManager(userForm)
+        }
+        if (res.code == 200) {
+          ElMessage.success(res.msg)
+          if(userFormRef.value) {
+            userFormRef.value.resetFields()
+          }
+          emit('update', false)
+        } else {
+          ElMessage.error(res.msg)
+        }
+        submitLoading.value = false
+      }catch {
+        submitLoading.value = false
+      }
+    }
+  })
+}
+</script>
+<style lang="scss" scoped>
+.el-input {
+  width: 320px;
+}
+</style>

+ 17 - 0
src/views/essay/index.vue

@@ -0,0 +1,17 @@
+<template>
+  <div class=" ">
+
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, onMounted, provide} from 'vue'
+
+
+
+
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 0 - 0
src/views/essay/作文.txt


+ 114 - 0
src/views/exam/components/aside.vue

@@ -0,0 +1,114 @@
+<template>
+  <div class="aside_container">
+    <div class="exam_info">
+        <div class="info_title" :title="examStore.currentExam?.examSubjectName">{{examStore.currentExam?.examSubjectName || '未选择考试'}}</div>
+        <div class="info_message">考试编号:{{examStore.currentExam?.examSubjectCode || '-'}}</div>
+    </div>
+    <div class="exam_back">
+       <div class="back_button" @click="GoBack()">
+          <i class="iconfont icon_return"></i>返回
+        </div>
+    </div>
+    <div class="exam_menu">
+        <StepItem></StepItem>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { useExamStore } from '@/store/exam'
+import { useRouter } from 'vue-router'
+import { onMounted ,ref} from 'vue';
+import StepItem from './stepItem.vue'
+// 实例化 Store
+const examStore = useExamStore()
+const router = useRouter()
+
+
+const menulist=ref<any[]>([]);//考试详情导航菜单
+
+const GoBack=()=>{
+  examStore.clearExamInfo()//清空考试信息
+  router.push('/main/choice')//返回考试列表
+}
+
+onMounted(() => {
+  
+    if (!examStore.currentExam) {
+        console.warn('当前没有选中的考试信息')
+        // 可选:如果没有数据,可以重定向回列表页或提示用户
+    }
+    
+  
+})
+</script>
+ 
+<style lang="scss" scoped>
+.aside_container
+{
+    width: 100%;
+    height: 100vh;
+}
+
+.exam_info
+{
+    width: 100%;
+    min-height: 80px;
+    background: #2E64FA;
+    color: #fff;
+    text-align: center;
+    padding:13px 5px;
+    box-sizing: border-box;
+    .info_title
+    {
+        font-weight: 500;
+        font-size: 20px;
+        color: #FFFFFF;
+        line-height: 25px;
+        display: -webkit-box;       // 将对象作为弹性伸缩盒子模型显示
+        -webkit-box-orient: vertical; // 设置伸缩盒子的子元素排列方式
+        -webkit-line-clamp: 2;      // 限制在一个块元素显示的文本的行数
+        overflow: hidden;           // 超出部分隐藏
+        text-overflow: ellipsis;    // 显示省略符号来代表被修剪的文本
+        word-break: break-all;      // 允许在单词内换行(防止长英文单词溢出)
+    }
+    .info_message
+    {
+        font-weight: 400;
+        font-size: 14px;
+        color: #FFFFFF;
+        line-height: 20px;
+        margin-top: 10px;
+    }
+}
+
+.exam_back
+{
+    width: 100%;
+    height: 60px;
+    background: #FFFFFF;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    .back_button
+    {
+        width: 136px;
+        height: 38px;
+        background: #FFFFFF;
+        border-radius: 4px 4px 4px 4px;
+        border: 1px solid #DCDFE6;
+
+        font-weight: 500;
+        font-size: 14px;
+        color: #666666;
+        text-align: center;
+        line-height: 34px;
+
+        cursor: pointer;
+    }
+}
+.exam_menu
+{
+    width: 100%;
+    height: calc(100% - 170px);
+}
+</style>

+ 153 - 0
src/views/exam/components/chooseTemplate.vue

@@ -0,0 +1,153 @@
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    title="请选择机读卡模版"
+    width="580px"
+    class="page_dialog"
+    :before-close="handleClose"
+  >
+    <div class="table_42">
+      <el-table  :data="tableData"    stripe style="width: 100%" >
+        
+        <el-table-column label=""  width="80">
+          <template v-slot="scope">
+            <el-radio  v-model="scope.row.selected"></el-radio>
+          </template>
+        </el-table-column>
+        <el-table-column  prop="templateCode"  label="模板编号" ></el-table-column>
+        <el-table-column  prop="templateName"  label="模板名称"></el-table-column>
+        <el-table-column  prop="option"  label="操作">
+          <template v-slot="scope">
+            <div class="ele_button table_row_button">
+              <span class="btn_editor">预览</span>
+            </div> 
+          </template>
+        </el-table-column>
+       
+      </el-table>
+    </div>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="HandleCancel">取消</el-button>
+        <el-button type="primary" :loading="loading" @click="HandleSubmit">确定</el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, computed, watch,onMounted } from 'vue'
+import type { ElTableColumn, FormInstance, FormRules } from 'element-plus'
+import { ElMessage } from 'element-plus'
+
+// 1. 确保导入接口函数,请根据实际路径调整
+import { getTemplateList,useTemplate } from '@/api/exam' 
+import { useExamStore } from '@/store/exam'
+
+// 实例化 Store
+const examStore = useExamStore()
+// 定义 Props 和 Emits
+const props = defineProps<{
+  modelValue: boolean,
+
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: boolean): void
+  (e: 'success'): void
+}>()
+
+// 弹窗显示状态的双向绑定
+const dialogVisible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+})
+
+// 表单引用
+const formRef = ref<FormInstance>()
+
+//定义用于存储下拉菜单选项的响应式数据
+const tableData=ref<any[]>([]);
+
+const selectedId=ref<any>();//选中的模板id
+
+const loading=ref(false);//加载状态
+
+// 考试科目 ID
+const examSubjectId = computed(() => {
+  // 替换为: return makeTemplateStore.examTempDetail?.id
+  return examStore.currentExam?.id
+})//计算属性
+
+// 关闭弹窗前的处理
+const handleClose = (done: () => void) => {
+  // 可以在这里添加确认关闭的逻辑
+  done()
+}
+
+onMounted(()=>{
+  GetTemplateList();
+})
+
+// 获取模板列表
+const GetTemplateList=async()=>{
+  let params={
+    pageNum:1,
+    pageSize:1000,
+  };
+  const res:any = await getTemplateList(params);
+  console.log("打印获取的结果",res)
+  if(res.code==200 && res.data)
+  {
+    tableData.value=res.data.records;
+    console.log("打印formData",tableData.value)
+    selectedId.value=tableData.value[0].id;
+
+   
+  }
+}
+//监听弹窗打开,调用接口
+watch(dialogVisible, (val) => { 
+  if (val) 
+  {
+    //弹窗打开时获取数据
+    GetTemplateList()
+  }
+})
+
+
+
+
+// 取消按钮
+const HandleCancel = () => {
+  dialogVisible.value = false
+
+}
+
+// 确定按钮
+const HandleSubmit= () => { 
+  console.log("确定选择模板")
+
+  console.log("examSubjectId",examSubjectId.value)
+  console.log("selectedId",selectedId.value)
+  let params={
+    examSubjectId:examSubjectId.value,//考试科目id
+    templateId:selectedId.value,//模板id
+  };
+  useTemplate(params).then(res=>{ 
+    console.log("打印模板接口返回res",res)
+    if(res.code==200)
+    {
+      ElMessage.success("使用成功");
+      dialogVisible.value = false
+      emit('success')
+    }
+  })
+}
+
+
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 197 - 0
src/views/exam/components/scanButton.vue

@@ -0,0 +1,197 @@
+<template>
+  <div class="canvas_button">
+    <!-- 使用 ref 绑定 canvas 元素 -->
+    <canvas ref="canvasRef" height="180" width="180"></canvas>
+    <div class="canvas_cr"></div>
+    <img src="../../../assets/icon/scan_button_bg.png" v-if="showButton" alt="scan button bg" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
+
+// 定义 Props
+interface Props {
+  process?: number;
+  quekao?: number;
+  yichang?: number;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  process: 10,
+  quekao: 20,
+  yichang: 40,
+});
+
+// 响应式数据
+const canvasRef = ref<HTMLCanvasElement | null>(null);
+const showButton = ref(true);
+let animationFrameId: number | null = null;
+
+// 格式化进度显示
+const formatProcess = (process: number): string | number => {
+  if (process % 1 === 0) {
+    return Math.floor(process);
+  } else {
+    return process.toFixed(2);
+  }
+};
+
+// 绘制 Canvas 核心逻辑
+const drawCanvas = () => {
+  const canvas = canvasRef.value;
+  if (!canvas) return;
+
+  const ctx = canvas.getContext('2d');
+  if (!ctx) return;
+
+  // 清除画布
+  ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+  // 保存状态
+  ctx.save();
+
+  // 1. 绘制白色圆底
+  ctx.beginPath();
+  ctx.arc(90, 90, 75, 0, 2 * Math.PI);
+  ctx.shadowColor = "#4D2EFA";
+  ctx.shadowBlur = 10;
+  ctx.fillStyle = '#ffffff';
+  ctx.fill();
+  ctx.closePath();
+  ctx.restore(); // 恢复阴影状态,避免影响后续绘制
+
+  // 2. 绘制进度环轨道 (灰色背景环)
+  ctx.save();
+  ctx.beginPath();
+  ctx.lineWidth = 10;
+  ctx.strokeStyle = '#EAF0FF';
+  ctx.arc(90, 90, 75, 0, 2 * Math.PI);
+  ctx.stroke();
+  ctx.closePath();
+  ctx.restore();
+
+  // 计算角度辅助函数 (将百分比转换为弧度,起始点为 -90度/12点钟方向)
+  const getEndAngle = (percent: number) => {
+    return ((percent / 100) * 360 - 90) * Math.PI / 180;
+  };
+
+  const startAngle = -90 * Math.PI / 180;
+  const processEnd = getEndAngle(props.process);
+  const yichangEnd = getEndAngle(props.yichang + props.process);
+  const quekaoEnd = getEndAngle(props.quekao + props.yichang + props.process);
+
+  // 3. 绿色进度环 (正常扫描)
+  ctx.save();
+  ctx.beginPath();
+  ctx.lineWidth = 10;
+  ctx.strokeStyle = '#2BC644';
+  ctx.arc(90, 90, 75, startAngle, processEnd);
+  ctx.stroke();
+  ctx.closePath();
+  ctx.restore();
+
+  // 4. 红色进度环 (异常)
+  ctx.save();
+  ctx.beginPath();
+  ctx.strokeStyle = '#F56C6C';
+  ctx.arc(90, 90, 75, processEnd, yichangEnd);
+  ctx.stroke();
+  ctx.closePath();
+  ctx.restore();
+
+  // 5. 橙色进度环 (缺考)
+  ctx.save();
+  ctx.beginPath();
+  ctx.strokeStyle = '#FB9F34';
+  ctx.arc(90, 90, 75, yichangEnd, quekaoEnd);
+  ctx.stroke();
+  ctx.closePath();
+  ctx.restore();
+
+  // 6. 绘制文字 "开始扫描"
+  ctx.save();
+  ctx.font = "bold 16px Arial"; // 建议指定字体族
+  ctx.textAlign = 'center';
+  ctx.textBaseline = 'middle'; // 优化垂直居中
+  ctx.fillStyle = '#333333';
+  ctx.fillText('开始扫描', 90, 120);
+  ctx.restore();
+
+  // 7. 绘制百分比
+  ctx.save();
+  ctx.font = "bold 32px Arial";
+  ctx.textAlign = 'center';
+  ctx.textBaseline = 'middle';
+  ctx.fillStyle = '#2E64FA';
+  ctx.fillText(`${formatProcess(props.process)}%`, 90, 90);
+  ctx.restore();
+
+  // 请求下一帧动画
+  animationFrameId = requestAnimationFrame(drawCanvas);
+};
+
+// 监听 Props 变化(可选,如果需要在数据变化时执行特定逻辑)
+watch(() => props.yichang, (newVal) => {
+  // console.log("异常数值变化了", newVal);
+});
+
+onMounted(() => {
+  // 启动动画循环
+  drawCanvas();
+});
+
+onBeforeUnmount(() => {
+  // 取消动画帧,防止内存泄漏
+  if (animationFrameId !== null) {
+    cancelAnimationFrame(animationFrameId);
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.canvas_button {
+  width: 250px;
+  height: 250px;
+  background-color: transparent;
+  position: relative;
+  display: flex; /* 修复:原本缺少 display:flex 导致 justify-content/align-items 无效 */
+  justify-content: center;
+  align-items: center;
+
+  img {
+    width: 100%;
+    height: 100%;
+    object-fit: contain; /* 保持图片比例 */
+  }
+
+  canvas {
+    position: absolute;
+    left: 35px;
+    top: 35px;
+    z-index: 999;
+  }
+
+  .canvas_cr {
+    width: 138px;
+    height: 138px;
+    border-radius: 50%;
+    border: 1px dashed #4D2EFA;
+    position: absolute;
+    left: 55px;
+    top: 55px;
+    z-index: 1000;
+    animation: identifier 10s linear infinite;
+    pointer-events: none; /* 防止遮挡点击事件 */
+  }
+
+  @keyframes identifier {
+    from {
+      transform: rotate(0deg);
+    }
+    to {
+      transform: rotate(360deg);
+    }
+  }
+}
+</style>

+ 167 - 0
src/views/exam/components/selectStudent.vue

@@ -0,0 +1,167 @@
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    title="请选择考场名单"
+    width="800px"
+    class="page_dialog"
+    :before-close="handleClose"
+  >
+    <div class="dialog_center">
+      <div class="center_message_title">请选择参加考试的班级,再点击确认按钮</div>
+    </div>
+    <div class="table_42">
+      <el-table ref="tableRef"  :data="tableData"    stripe  height="400px" >
+        
+        <el-table-column type="selection" width="50" align="center"></el-table-column>
+        <el-table-column align="center" type="index" width="100"  label="序号"></el-table-column>  
+        <el-table-column  prop="className"  label="班级名称"></el-table-column>
+        <el-table-column  prop="userTotal"  label="人数"></el-table-column>
+      </el-table>
+    </div>
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="HandleCancel">取消</el-button>
+        <el-button type="primary" :loading="loading" @click="HandleSubmit">确定</el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts" setup>
+import { ref, reactive, computed, watch,onMounted } from 'vue'
+import type { ElTableColumn, FormInstance, FormRules } from 'element-plus'
+import { ElMessage,ElTable } from 'element-plus'
+
+// 1. 确保导入接口函数,请根据实际路径调整
+import { getClassList,useClassList } from '@/api/exam' 
+import { useExamStore } from '@/store/exam'
+
+// 定义 table ref
+const tableRef = ref<InstanceType<typeof ElTable>>()
+// 实例化 Store
+const examStore = useExamStore()
+// 定义 Props 和 Emits
+const props = defineProps<{
+  modelValue: boolean,
+
+}>()
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: boolean): void
+  (e: 'success'): void
+}>()
+
+// 弹窗显示状态的双向绑定
+const dialogVisible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+})
+
+
+
+// 表单引用
+const formRef = ref<FormInstance>()
+
+//定义用于存储下拉菜单选项的响应式数据
+const tableData=ref<any[]>([]);
+
+const selectedId=ref<any>();//选中的模板id
+
+const loading=ref(false);//加载状态
+
+// 考试科目 ID
+const examSubjectId = computed(() => {
+  // 替换为: return makeTemplateStore.examTempDetail?.id
+  return examStore.currentExam?.id
+})//计算属性
+
+// 关闭弹窗前的处理
+const handleClose = (done: () => void) => {
+  // 可以在这里添加确认关闭的逻辑
+  done()
+}
+
+onMounted(()=>{
+  GetClassList();
+})
+
+// 获取模板列表
+const GetClassList=async()=>{
+  const params={
+    examSubjectId:examSubjectId.value,
+    schoolId:0,
+  };
+  const res:any = await getClassList(params);
+  console.log("打印获取的结果",res)
+  if(res.code==200 && res.data)
+  {
+    tableData.value=res.data.adminClasses;
+    console.log("打印formData",tableData.value)
+    selectedId.value=tableData.value[0].classId;
+
+   
+  }
+}
+//监听弹窗打开,调用接口
+watch(dialogVisible, (val) => { 
+  if (val) 
+  {
+    //弹窗打开时获取数据
+    GetClassList()
+  }
+})
+
+
+
+
+// 取消按钮
+const HandleCancel = () => {
+  dialogVisible.value = false
+
+}
+
+// 确定按钮
+const HandleSubmit= () => { 
+  console.log("确定选择模板")
+
+  console.log("examSubjectId",examSubjectId.value)
+  console.log("selectedId",selectedId.value)
+
+  const selectRows=tableRef.value?.getSelectionRows() || [];
+  if(selectRows.length==0)
+  {
+    ElMessage.error("请至少选择一个班级!");
+    return;
+  }
+  // 提取选中项的 id 
+  const selectedIds = selectRows.map((row: any) => row.classId)
+  console.log("选中的班级id",selectedIds)
+
+  const params={
+    examSubjectId:examSubjectId.value,
+    classIds:selectedIds,
+    reuse:0,//是否是重新使用名单 0-否 1-是
+    schoolId:0,//学校id
+  };
+  loading.value=true;
+  useClassList(params).then((res:any)=>{
+    loading.value=false;
+    if(res.code==200)
+    {
+      ElMessage.success("操作成功!");
+      dialogVisible.value = false
+      emit('success')
+    }
+    else
+    {
+      ElMessage.error(res.msg || "操作失败!");
+    }
+  })
+}
+
+
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 371 - 0
src/views/exam/components/stepItem.vue

@@ -0,0 +1,371 @@
+<template>
+  <div class="step_exam_item">
+    <div class="left_line"></div>
+    <div 
+      class="step_item" 
+      v-for="(item, index) in menuList" 
+      :key="index" 
+      :class="{ active: index === activeIndex, success: item.status === 1 }"
+      
+    >
+      <div class="step_title" @click="MenuChange(index, item)">
+        <div class="num_box">
+          <!-- 假设 el-icon-success 是全局注册的组件或 CSS 类 -->
+          <span v-if="item.status === 1" class="el-icon-success"></span>
+          <span v-else class="step_num">{{ index + 1 }}</span>
+        </div>
+        <span>{{ item.name }}</span>
+      </div>
+      <div class="step_desc" v-if="item.name=='成绩查询'">
+        <div class="menu_item" :class="childMenuPath==menu.path?'menu_item_active':''" v-for="menu in item.menu" @click="MenuSelect(menu)">
+          {{menu.name}}
+        </div>
+      </div>
+      <div v-else class="step_desc" @click="MenuChange(index, item)">
+        {{ item.desc }}
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, watch } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { useExamStore } from '@/store/exam'
+
+const examStore = useExamStore()
+// --- 响应式数据 ---
+const route = useRoute()//读取当前路由对象
+const router = useRouter()//访问全局路由方法
+
+const stepOnIndex = ref<number>(0)//步骤索引
+const childMenuPath = ref<string>('')//子级菜单路径
+const menuListRaw = ref<any[]>([]) // 用于存储计算后的菜单列表,以便 watch 能检测到变化(如果需要)
+
+// --- 计算属性 ---
+
+// 1. 考试科目 ID
+const examSubjectId = computed(() => {
+  // 替换为: return makeTemplateStore.examTempDetail?.id
+  return examStore.currentExam?.id
+})
+
+// 2. 考试状态字符串
+const examStateStr = computed(() => {
+  // 替换为: return makeTemplateStore.examStateStr
+  return examStore.currentExam?.examStateStr
+})
+
+// 3. 考试类型 1 选择题 2 填空题 3 Ai作文
+const examType=computed(() => {
+  return examStore.currentExam?.examType  // 默认为选择题
+})
+
+
+// 5. 动态菜单列表
+const menuList = computed(() => {
+  console.log('打印菜单examType', examSubjectId.value, examType.value)
+  let list: any[] = []
+  if(examType.value==1)
+  {
+    //选择题判分
+    list = [
+      { name: '试题结构', number: '1', path: 'question', desc: '建立考试试题结构、设置标答、分组等信息', status: 0 },
+      { name: '扫描学生', number: '2', path: 'scanList', desc: '扫描学生,处理扫描异常学生等', status: 0 },
+      { name: '成绩查询', number: '3', path: 'score', desc: '设置每个主观题下的划分区信息及位置', status: 0 ,
+        menu:[
+          { name: '成绩单', path: 'score' },
+          { name: '错题分析', path: 'errorAnalysis' },
+          { name: '选项明细', path: 'optionDetail' },
+          { name: '水平分布', path: 'levelDistribution' },
+          { name: '班级对比', path: 'classComparison' },
+          { name: '小题分析', path: 'questionAnalysis' },
+          { name: '分组分析', path: 'groupAnalysis' },
+          { name: '选项分析', path: 'optionAnalysis' },
+          { name: '命题分析', path: 'propositionAnalysis' },
+        ]
+      },
+    ];
+   
+  }
+  else if(examType.value==2)
+  {
+    //填空题判分
+
+  }
+  else
+  {
+    //Ai作文
+
+  }
+  
+ 
+
+  
+
+  // // 更新状态
+  // const stateStr = String(examStateStr.value || '')
+  // list.forEach((item) => {
+  //   if (stateStr.includes(item.number)) {
+  //     item.status = 1
+  //   } else {
+  //     item.status = 0
+  //   }
+  // })
+
+  return list
+})
+
+// 6. 激活的菜单索引
+const activeIndex = computed(() => {
+  const currentPath = route.path
+  
+  // 特殊处理 scanStudentHome 页面
+  if (currentPath.includes('scanStudentHome')) {
+    const stepActiveIndex = menuList.value.findIndex(item => item.name === '扫描学生')
+    return stepActiveIndex !== -1 ? stepActiveIndex : stepOnIndex.value
+  }
+  
+  // 根据路由路径找到匹配的菜单项索引
+  const pathMatchIndex = menuList.value.findIndex(item => currentPath.includes(item.path))
+  return pathMatchIndex !== -1 ? pathMatchIndex : stepOnIndex.value
+})
+
+// --- 方法 ---
+
+// 获取模板数据
+const GetThirdCardData = () => {
+  if (!examSubjectId.value) return
+
+  const param = {
+    examSubjectId: examSubjectId.value
+  }
+  
+  // 假设 $api 已全局挂载或在 utils 中引入
+  // import { examApi } from '@/api/exam'
+  ;(window as any).$api?.exam.getExamThirdCardData(param).then((res: any) => {
+    console.log('获取模板相关数据 包含第三方卡和系统卡', res)
+    if (res.code === 200) {
+      localStorage.setItem('templateData', JSON.stringify(res.data.examCardPageVOS))
+      localStorage.setItem('usedCardType', res.data.usedCardType)
+      
+      // 替换为 Store Commit/Action
+      // makeTemplateStore.setMarkType(res.data.markType)
+      // makeTemplateStore.setUsedCardType(res.data.usedCardType)
+      console.warn('请在此处调用 Store 的 action 更新 markType 和 usedCardType')
+    }
+  })
+}
+
+// // 获取考试流程状态
+// const GetExamState = () => {
+//   const param = {
+//     examSubjectId: route.query.id
+//   }
+  
+//   ;(window as any).$api?.reviewBlock.getExamFlowStatus(param).then((res: any) => {
+//     console.log('打印考试流程状态返回', res)
+//     if (res.code === 200) {
+//       const newStateStr = res.data.finished.join(',')
+//       // 替换为 Store Commit/Action
+//       // makeTemplateStore.setExamStateStr(newStateStr)
+//       console.warn('请在此处调用 Store 的 action 更新 examStateStr')
+//     }
+//   })
+// }
+
+// 菜单点击事件
+const MenuChange = (index: number, item: any) => {
+  console.log('点击菜单', index, item)
+  stepOnIndex.value = index
+  childMenuPath.value = ''
+  // 触发父组件事件
+  // 在 <script setup> 中,需要使用 defineEmits
+  router.push('/exam/'+item.path)
+}
+
+//子级菜单选择事件
+const MenuSelect = (menu: any) => { 
+  console.log('选择的子菜单', menu)
+  stepOnIndex.value = 2;
+  childMenuPath.value = menu.path;
+}
+// --- 生命周期 & 监听 ---
+
+// 定义 emits
+const emit = defineEmits(['menuChange'])
+
+// 监听 examStateStr 变化,虽然 menuList 是 computed 会自动更新,
+// 但如果原逻辑中有副作用,可以保留 watch。
+// 注意:在 Vue3 computed 中直接修改返回对象的属性是不推荐的,
+// 原代码在 computed 中修改了 menuList 内部对象的 status,这在 Vue3 中可能导致警告。
+// 更好的做法是在 computed 中返回新对象,或者将 status 的计算逻辑完全放在 computed 内部(如上所示)。
+// 因此,下面的 watch 可能不再需要,因为 menuList computed 已经包含了 status 的逻辑。
+watch(examStateStr, (newVal) => {
+  // 由于 menuList 是 computed 且依赖 examStateStr,它会自动重新计算
+  // 如果这里有其他副作用,请保留
+  console.log('考试状态发生变化', newVal)
+})
+
+onMounted(() => {
+  // 初始化 stepOnIndex
+  if (route.path.includes('scanStudentHome')) {
+    
+    
+   
+    stepOnIndex.value = 0
+   
+  }
+
+  
+})
+
+</script>
+
+<style lang="scss" scoped>
+.step_exam_item {
+  position: relative;
+  height: 100%;
+  .left_line {
+    position: absolute;
+    top: 0;
+    left: 15.55px;
+    z-index: 6;
+    height: 100%;
+    width: 0;
+    border-left: 1px dashed #C0C4CC;
+  }
+  .step_item {
+    width: 100%;
+    position: relative;
+    z-index: 7;
+    border-top-left-radius: 10px;
+    border-bottom-left-radius: 10px;
+    cursor: pointer;
+    .step_title {
+      display: flex;
+      align-items: center;
+      justify-content: flex-start;
+      width: 100%;
+      height: 34px;
+      font-size: 16px;
+      color: #333333;
+      line-height: 34px;
+      font-weight: 600;
+      border-top-left-radius: 10px;
+    }
+    .num_box {
+      width: 30px;
+      height: 100%;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+
+      .el-icon-success::before {
+        font-size: 19px;
+      }
+    }
+    .step_num {
+      display: block;
+      width: 17px;
+      height: 17px;
+      font-size: 12px;
+      text-align: center;
+      font-weight: 500;
+      line-height: 17px;
+      border-radius: 50%;
+      background-color: #B0B2B7;
+      color: #fff;
+    }
+    .step_desc {
+      padding: 0px 5px 6px 20px;
+      font-size: 12px;
+      line-height: 16px;
+      color: #999999;
+
+      .menu_item
+      {
+        font-weight: 400;
+        font-size: 14px;
+        color: #666666;
+        line-height: 35px;
+        text-indent: 10px;
+      }
+
+      .menu_item_active
+      {
+        color: #2e64fa;
+      }
+    }
+    &.success {
+      border-color: #fff;
+      .num_box > span {
+        color: #15bc83;
+      }
+    }
+    &.active {
+      border-color: #2e64fa;
+      background-color: rgba(46, 100, 250, 0.1);
+      box-shadow: inset 1px -1px 0px 1.5px #2e64fa;
+      .step_title {
+        background-color: #2e64fa;
+        color: #fff;
+      }
+      .step_num {
+        background-color: #ffff;
+        color: #2e64fa;
+      }
+      .step_desc {
+        background-color: rgba(46, 100, 250, 0.1);
+      }
+    }
+  }
+
+  /* 小屏幕笔记本的样式 常见分辨率:1366x768 */
+  @media (max-height: 768px) {
+    .step_item {
+      margin-bottom: 0px;
+      .step_title {
+        font-size: 14px;
+        line-height: 30px;
+        font-weight: 700;
+      }
+      .step_desc {
+        padding: 0px 5px 6px 20px;
+        font-size: 10px;
+        line-height: 15px;
+      }
+    }
+  }
+  /* 中等屏幕笔记本的样式(14-15英寸) 常见分辨率:1600x900, 1920x1080*/
+  @media (min-height: 769px) and (max-height: 1080px) {
+    .step_item {
+      margin-bottom: 15px;
+      .step_title {
+        font-size: 16px;
+        line-height: 30px;
+      }
+      .step_desc {
+        padding: 0px 5px 6px 20px;
+        font-size: 12px;
+        line-height: 15px;
+      }
+    }
+  }
+  /* 大屏幕笔记本的样式(17-20英寸) 常见分辨率:2560x1440*/
+  @media screen and (min-height: 1081px) {
+    .step_item {
+      margin-bottom: 25px;
+      .step_title {
+        font-size: 17px;
+        line-height: 30px;
+      }
+      .step_desc {
+        padding: 10px 5px 10px 20px;
+        font-size: 14px;
+        line-height: 15px;
+      }
+    }
+  }
+}
+</style>

+ 392 - 0
src/views/exam/examList.vue

@@ -0,0 +1,392 @@
+<template>
+    <!-- 考场名单 -->
+  <div class="page_item page_list">
+    <div class="search_content">
+        <div class="content_left">
+            <el-input placeholder="请输入学生姓名"  v-model="searchContent" @input="GoSearch" @change="GoSearch()" style="width:240px;" >
+                <el-button @click="GoSearch()"  slot="append" icon="el-icon-search"></el-button>
+            </el-input>
+        </div>
+        <div class="content_right">
+            <el-button @click="Refresh()" class="refresh_btn"><i class="iconfont icon_shuaxin"></i>刷新</el-button>
+            <el-button class="other_btn" @click="ExportExamStudentList()"  >
+                <i class="iconfont icon_export"> </i> 导出
+            </el-button>
+            <el-button @click="BackToTemplate()" class="other_btn" >重新导入名单</el-button>
+            <el-button @click="OpenAddStudent()" class="add_student" >新增学生</el-button>
+            <el-button @click="BackToScan()" type="primary" >扫描页面</el-button>
+        </div>
+    </div>
+    <div class="page_jg_20"></div>
+    <div class="table_content">
+        <div class="content_left page_table">
+            <el-table :data="examRoomList" style="width: 100%" :height="tableHeight">
+                <el-table-column align="center" width="80"  prop="examInfo" >
+                        <template v-slot="scope">
+                            <el-radio v-model="selectExamRoomCode" :value="scope.row.examRoomCode"></el-radio>
+                        </template>
+                    </el-table-column>
+                <el-table-column prop="examRoomName" label="班级" width="80" align="center">
+                </el-table-column>
+                <el-table-column prop="date" label="缺考/学生数" width="120" align="center">
+                    <template v-slot="scope">
+                        <div class="answer_input">
+                            {{scope.row.missExamNum}}/{{scope.row.totalStudentNum}}
+                        </div>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="fullScore" label="异常数" width="80" align="center">
+                    <template v-slot="scope">
+                        <div class="full_mark_input">
+                            {{scope.row.abnormalNum}}
+                        </div>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="name" label="已上传/学生数" align="center">
+                    <template v-slot="scope">
+                        <div class="full_mark_input">
+                            {{scope.row.uploadedNum}}/{{scope.row.totalStudentNum}}
+                        </div>
+                   </template>
+                </el-table-column>
+            </el-table>
+        </div>
+        <div class="content_right page_table">
+            <el-table :data="examStudentList" style="width: 100%" :height="tableHeight">
+                <el-table-column type="index" label="序号" width="100" align="center">
+                </el-table-column>
+                <el-table-column prop="studentCode" label="学号" width="120" align="center">
+
+                </el-table-column>
+                <el-table-column prop="fullScore" label="姓名" width="120" align="center">
+                    <template v-slot="scope">
+                        <div class="full_mark_input">
+                            {{scope.row.studentName}}
+                        </div>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="examCode" label="考号" width="170" align="center">
+                    <template v-slot="scope">
+                         {{scope.row.examCode}}
+                   </template>
+                </el-table-column>
+                <el-table-column prop="adminClassName" label="行政班" width="120" align="center">
+                    <template v-slot="scope">
+                        {{scope.row.adminClassName}}
+                   </template>
+                </el-table-column>
+                <el-table-column prop="teachingClassName" label="教学班" width="120" align="center">
+                </el-table-column>
+                <el-table-column  label="状态" width="130" align="center">
+                    <template v-slot="scope">
+                        <div class="table_row_scan_state"></div>
+                        <div v-if="scope.row.scannedStatus==3" class="table_row_studentStatus"><div class="que_icon"></div>缺页</div>
+                        <div v-if="scope.row.scannedStatus==0" class="table_row_studentStatus"><div class="no_scan_icon"></div>未扫描</div>
+                        <div v-if="scope.row.scannedStatus==1" class="table_row_studentStatus"><div class="upload_icon"></div>已上传</div>
+                        <div v-if="scope.row.scannedStatus==2" class="table_row_studentStatus"><div class="qk_icon"></div>缺考</div>
+                   </template>
+                </el-table-column>
+                <el-table-column prop="name" label="操作" align="center">
+                    <template v-slot="scope">
+                        <div class="table_row_option" >
+                                <div class="button_editor" @click="EditorStudent(scope.row)">
+                                    编辑
+                                </div>
+                                <!-- 逻辑 有图片的才能查看  无图片的不能查看 -->
+                                <div class="button_editor" v-if="scope.row.scanPictureVOS && scope.row.scanPictureVOS.length>0" @click="OpenImage(scope.row)">
+                                    查看
+                                </div>
+                                <div class="editor_disable" v-else>
+                                    查看
+                                </div>
+                                <div class="button_delete"  @click="ConfirmDelete(scope.row)" v-if="scope.row.scannedStatus==0">
+                                    删除
+                                </div>
+                                <div class="editor_disable" v-else>
+                                    删除
+                                </div>
+                            </div>  
+                   </template>
+                </el-table-column>
+            </el-table>
+        </div>
+    </div>
+    <div class="page_jg_20"></div>
+    <div class="page_bottom">
+        <div class="bottom_left">
+            <span>班级数:15个</span>
+            <span>学生总数:15个</span>
+            <span>缺考人数:15个</span>
+            <span>已扫描学生:<span class="span_red">15个</span></span>
+            <span>扫描进度:<span class="span_red">15个</span></span>
+        </div>
+        <div class="bottom_right">
+            当前选中班级:{{selectExamRoomName}}  学生数: {{examStudentList.length }}人
+        </div>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { getExamRoomList,getExamRoomStudentList,deleteExamRoomStudent} from '@/api/exam'
+import { useExamStore } from '@/store/exam'
+import { useRouter } from 'vue-router'
+import { onMounted, onUnmounted, ref, nextTick,computed, watch } from 'vue';
+import { ElMessageBox, ElMessage } from 'element-plus'
+// 实例化 Store
+const examStore = useExamStore()
+const router = useRouter()
+
+const examRoomList=ref([]);//考场列表
+const examStudentList=ref([]);//考场学生列表
+const selectSchoolId=ref(0);
+const searchContent=ref('');//搜索内容
+const selectExamRoomCode=ref('');//选择的考场code
+const selectExamRoomName=ref('');//选择的考场名称
+const tableHeight=ref(400);//表格高度
+const examInfo=ref({
+    classCount:0,//班级数
+    totalStudentCount:0,//学生总数
+    missCount:0,//缺考人数
+    uploadedCount:0,//已上传学生数
+    abnormalCount:0,//异常数
+    scanRate:0,//扫描进度
+});
+
+// 考试科目 ID
+const examSubjectId = computed(() => {
+  return examStore.currentExam?.id
+})//计算属性
+
+const schoolId = computed(() => {
+    return examStore.currentExam?.schoolId || 0
+})
+
+//刷新
+const Refresh = () => {
+    examRoomList.value=[];
+    examStudentList.value=[];
+  GetExamRoomList();
+}
+// 返回到扫描界面
+const BackToScan = () => {
+
+    router.push({
+        path: '/exam/scanList',
+        query: {
+            examSubjectId: examSubjectId.value,
+        },
+    });
+}
+
+// 监听选择的考场代码变化,自动更新考场名称
+watch(selectExamRoomCode, (newCode) => {
+  if (!newCode || !examRoomList.value || examRoomList.value.length === 0) {
+    selectExamRoomName.value = '';
+    return;
+  }
+
+  // 在考场列表中查找匹配的项
+  const selectedRoom = examRoomList.value.find((room: any) => room.examRoomCode === newCode);
+  
+  if (selectedRoom) {
+    selectExamRoomName.value = selectedRoom.examRoomName;
+    console.log('当前选中考场:', selectExamRoomName.value);
+    
+    // 可选:如果切换考场需要重新获取学生列表,可以在这里调用
+    GetExamRoomStudentList(); 
+  } else {
+    selectExamRoomName.value = '';
+  }
+});
+
+//搜索
+const GoSearch=()=>{ 
+    console.log('搜索内容:', searchContent.value);
+    GetExamRoomStudentList();
+}
+
+
+//删除学生
+const ConfirmDelete=(row:any)=>{
+    console.log('删除',row);
+    ElMessageBox.confirm('确认删除该学生吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+    }).then(async () => {
+        console.log('删除学生')
+        const param={
+            id:row.id,
+        };
+        const res = await deleteExamRoomStudent(param);
+        console.log('删除结果:', res);
+        if (res.code === 200) {
+            ElMessage.success('删除成功');
+            GetExamRoomStudentList();
+        }
+        else
+        {
+            ElMessage.error('删除失败!'+res.msg);
+        }
+        
+    })
+}
+//获取考场列表
+const GetExamRoomList = async () => {
+    try {
+        const params={
+            examSubjectId:examSubjectId.value,
+            schoolId:schoolId.value,
+        };
+        const res = await getExamRoomList(params);
+        if (res.code === 200) {
+            examRoomList.value = res.data.examRoomTblVOS;
+            selectExamRoomCode.value = examRoomList.value[0]?.examRoomCode;
+            GetExamRoomStudentList();
+        }
+        console.log('考场列表', res);
+    } catch (error) {
+        console.error('获取考场列表失败', error);
+    }
+};
+
+//获取扫描考场的学生列表
+const GetExamRoomStudentList = async () => {
+    try {
+        const params = {
+            examSubjectId:examSubjectId.value,
+            schoolId:schoolId.value,
+            examRoomCode:selectExamRoomCode.value,
+            studentName:searchContent.value,//学生姓名搜索
+            pageNum:1,
+            pageSize:1000,
+        };
+        console.log('获取考场学生列表参数', params); // 打印参数
+        const res = await getExamRoomStudentList(params);
+        if (res.code === 200) 
+        {
+            examStudentList.value = res.data.records;
+        }
+        console.log('获取考场学生列表成功', res);
+
+    } catch (error) {
+            console.error('获取考场学生列表失败', error);
+        return error;
+    }
+
+}
+//计算高度的函数
+const CalculateTableHeight = () => {
+  // nextTick 确保 DOM 更新后再获取尺寸
+  nextTick(() => {
+    // window.innerHeight 是浏览器可视区域高度
+
+    // 简单算法:视窗高度 - 固定占用高度
+    let computedHeight = window.innerHeight - 40 - 20 - 36 - 20 - 65;
+    
+    // 限制最小高度,防止太矮
+    if (computedHeight < 200) {
+      computedHeight = 200;
+    }
+
+    tableHeight.value = computedHeight;
+  });
+}; 
+
+onMounted(() => {
+  
+    if (!examStore.currentExam) {
+        console.warn('当前没有选中的考试信息')
+        // 可选:如果没有数据,可以重定向回列表页或提示用户
+        
+    }
+    GetExamRoomList();//获取考场列表
+
+    
+    // 初始化计算
+    CalculateTableHeight();
+  
+    // 监听窗口大小变化
+    window.addEventListener('resize', CalculateTableHeight);
+})
+// 卸载时移除监听,防止内存泄漏
+onUnmounted(() => {
+  window.removeEventListener('resize', CalculateTableHeight);
+});
+</script>
+ 
+<style lang="scss" scoped>
+.table_content
+{
+    width: 100%;
+    display: flex;
+    justify-content: space-between;
+    height: calc(100vh - 140px - 40px);
+
+    .content_left
+    {
+        width: 35%;
+        height: 100%;
+    }
+
+    .content_right
+    {
+        width: calc(65% - 20px);
+        height: 100%;
+    }
+}
+
+//状态
+.table_row_studentStatus
+{
+    font-weight: 400;
+    font-size: 14px;
+    color: #666666;
+    display: flex;
+    align-items: center;
+    justify-content:center;
+    gap: 5px;
+
+
+    //缺页
+    .que_icon
+    {
+        margin-left: 5px;
+        width: 6px;
+        height: 6px;
+        border-radius: 50%;
+        background: #F56C6C;
+    }
+
+    //异常的
+    .qk_icon
+    {
+        margin-left: 5px;
+        width: 6px;
+        height: 6px;
+        border-radius: 50%;
+        background: #FB9F34;
+        
+    }
+
+    //已上传
+    .upload_icon
+    {
+        margin-left: 5px;
+        width: 6px;
+        height: 6px;
+        background: #2BC644;
+        border-radius: 50%;
+    }
+    //未扫描
+    .no_scan_icon
+    {
+        margin-left: 5px;
+        width: 6px;
+        height: 6px;
+        background: #2E64FA;
+        border-radius: 50%;
+    }
+
+}
+</style>

+ 24 - 0
src/views/exam/index.vue

@@ -0,0 +1,24 @@
+<template>
+  <div class="page_detail">
+    <div class="left_aside">
+        <Aside></Aside>
+    </div>
+    <div class="right_main">
+        <router-view></router-view>
+    </div>
+  </div>
+</template>
+
+<script>
+import Aside from './components/aside.vue'
+
+export default {
+  components: {
+    Aside
+  }
+}
+</script>
+
+<style>
+
+</style>

+ 631 - 0
src/views/exam/question.vue

@@ -0,0 +1,631 @@
+<template>
+  <div class="detail_main">
+    <div class="question_header">
+        <div class="item_title">
+            <div class="item_step">1</div>
+            扫描标准答案:<el-button type="primary"  style="width: 88px;">开始扫描</el-button>  <span class="preview_btn">预览</span>
+            <el-button style="margin-left: 10px;width: 116px;">引用试题结构</el-button>
+        </div>
+        <div class="choose_template">
+            当前模板:{{templateName}}   <span class="choose_btn" @click="OpenChooseTemplate()">更换</span>
+        </div>
+    </div>
+    <div class="page_jg_20"></div>
+    <div class="page_module">
+        <div class="item_title">
+            <div class="item_step">2</div>
+            设置分值:
+            <div class="item_search">
+                <div class="search_name">题目从</div>
+                <el-select  v-model="queryParams.start"   placeholder="请选择"  class="select_width" >
+                    <el-option :label="number" :value="number" v-for="number in templateQuestionCount"></el-option>
+                </el-select>
+                <div class="search_name">至</div>
+                <el-select  v-model="queryParams.end"   placeholder="请选择"  class="select_width" >
+                    <el-option :label="number" :value="number" v-for="number in templateQuestionCount"></el-option>
+                </el-select>
+            </div>
+            <div class="item_search">
+                <div class="search_name">题目类型:</div>
+                <el-select  v-model="queryParams.questionType"   placeholder="请选择"  class="select_width" >
+                    <el-option label="单选题" :value="1"></el-option>
+                    <el-option label="多选题" :value="2"></el-option>
+                </el-select>
+            </div>
+            <div class="item_search">
+                <div class="search_name">每题满分:</div>
+                <el-input v-model="queryParams.fullMark" placeholder="请输入" maxlength="3" @input="(val: any) => onScoreInput(queryParams, 'fullMark', val)"   class="select_width" ></el-input>
+            </div>
+            <div class="item_search">
+                <div class="search_name">分组:</div>
+                <el-input v-model="queryParams.groupName" placeholder="请输入"  class="select_width" ></el-input>
+            </div>
+            <div class="item_search">
+                <el-button type="primary"  style="width: 68px;" @click="EnterSetQuestion()">确定</el-button>
+            </div>
+
+            <div class="delete_all">
+                <div  class="delete_btn" @click="DeleteAll()">全部删除</div>
+            </div>
+        </div>
+        <div class="page_jg_16"></div>
+        <div class="table_42">
+            <el-table :data="tableData" style="width: 100%" :height="tableHeight">
+                <el-table-column prop="questionName" label="题目名称" width="100">
+                </el-table-column>
+                <!-- 题目类型 questionType 1 单校  2多选 -->
+                <el-table-column prop="questionType" label="题目类型" width="120">
+                    <template v-slot="scope">
+                        <el-select  v-model="scope.row.questionType"  style="width: calc(100% - 32px);" @change="TypeChange(scope.row)">
+                            <el-option :value="1" label="单选题"></el-option>
+                            <el-option :value="2" label="多选题"></el-option>
+                        </el-select>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="date" label="标注答案" width="242">
+                    <template v-slot="scope">
+                        <div class="answer_input">
+                            <!-- 单选题 -->
+                            <div class="single_selected"  v-if="scope.row.questionType == 1">
+                                <el-button  v-for="(item,index) in scope.row.optionsSize" :type="scope.row.standardAnswer === answerList[index] ? 'primary': ''"  class="selected_button" @click="SingleClick(scope.row,answerList[index])">{{answerList[index]}}</el-button>
+                            </div>
+                            <!-- 多选 -->
+                            <div class="single_selected"  v-if="scope.row.questionType == 2">
+                                <el-button v-for="(item,index) in scope.row.optionsSize" :type="scope.row.standardAnswer?.includes(answerList[index]) ? 'primary': ''"  class="selected_button" @click="MultiClick(answerList[index],scope.row)">{{answerList[index]}}</el-button>
+                                
+                            </div>
+                            <!-- 判断题 -->
+                            <div class="single_selected" v-if="scope.row.questionType == 3">
+                                <el-button  v-for="(item,index) in 2" :type="scope.row.standardAnswer === tflist[index]   ? 'primary': ''" class="selected_button" @click="SingleClick(tflist[index],scope.row)">{{tflist[index]}}</el-button>
+                                
+                            </div>
+                        </div>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="fullScore" label="满分" width="120">
+                    <template v-slot="scope">
+                        <div class="full_mark_input">
+                            <el-input v-model="scope.row.fullScore" maxlength="3" @input="(val: any) => onScoreInput(scope.row, 'fullScore', val)"></el-input>
+                        </div>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="name" label="多选题给分分布">
+                    <template v-slot="scope">
+                        <div v-if="scope.row.questionType == '1' || scope.row.questionType == '3'">
+                            {{ '-' }}
+                        </div>
+                        <div v-if="scope.row.questionType == '2'">
+                            <div v-if="scope.row.multipleRule == '1'"> 
+                                全对得满分
+                                <span>,少选得{{ scope.row.multipleRuleDesc || 0 }}分,选错不得分</span>
+                            </div>
+                            <div v-if="scope.row.multipleRule == '2'">
+                                全对得满分,选对1个得{{ scope.row.multipleRuleDesc?.split(',')[0] }}分
+                                <span v-if="scope.row.multipleRuleDesc && scope.row.standardAnswer.split(',').length > 2">,选对2个得{{ scope.row.multipleRuleDesc?.split(',')[1] }}分</span>
+                                <span v-if="scope.row.multipleRuleDesc && scope.row.standardAnswer.split(',').length == 4">,选对3个得{{ scope.row.multipleRuleDesc?.split(',')[2] }}分</span>
+                            </div>
+                            <div v-if="scope.row.multipleRule == '3'">
+                                {{scope.row.multipleRuleDesc}}
+                            </div>
+                        </div>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="groupName" label="分组">
+                    <template v-slot="scope">
+                        <div class="input_width">
+                            <el-input v-model="scope.row.groupName" maxlength="3" ></el-input>
+                        </div>
+                    </template>
+                </el-table-column>
+                <el-table-column prop="name" label="操作" width="150">
+                    <template v-slot="scope">
+                        <div class="ele_button table_row_button">
+                            <span class="btn_editor">编辑</span>
+                            <span class="btn_delete" @click="DeleteSingle(scope.row)">删除</span>
+                        </div>
+                   </template>
+                </el-table-column>
+            </el-table>
+            <div class="page_info">
+                共{{tableData.length}}题   总分:{{totalScore}}分
+            </div>
+        </div>
+        <div class="page_button">
+            <el-button type="primary"  style="width: 200px;" @click="FinishQuestion()">完成</el-button>
+        </div>
+    </div>
+    <ChooseTemplate v-model="showChooseTemplate"></ChooseTemplate>
+  </div>
+</template>
+<script lang="ts" setup>
+import { useExamStore } from '@/store/exam'
+import { useRouter } from 'vue-router'
+import { onMounted,computed ,onUnmounted,ref,nextTick } from 'vue';
+import ChooseTemplate from './components/chooseTemplate.vue';
+import { ElMessageBox,ElMessage } from 'element-plus' 
+import { getSmartQuestionList,getTemplateInfo,setQuestion,saveQuestion,deleteAllQuestion,deleteSingleQuestion} from '@/api/exam'
+
+
+// 实例化 Store
+const examStore = useExamStore()
+const router = useRouter()
+
+// 定义题目数据的接口类型
+interface QuestionItem {
+  id?: string;
+  questionName?: string;
+  questionType?: number; // 1:单选, 2:多选, 3:判断
+  standardAnswer?: string;
+  fullScore?: string | number;
+  groupName?: string;
+  optionsSize?: number;
+  multipleRule?: string;
+  multipleRuleDesc?: string;
+  [key: string]: any; // 允许其他动态属性
+}
+
+const queryParams= ref({
+    start:'',
+    end:'',
+    questionType:null,
+    fullMark:'',
+    groupName:'',//分组名称
+})
+
+const templateName=ref('机读卡');
+const templateQuestionCount=ref(0);//模板题目总数量
+const showChooseTemplate=ref(false);// 弹出选择模板的弹窗
+const tableData = ref<QuestionItem[]>([])
+
+const tableHeight = ref(400)// 表格高度
+const answerList=['A','B','C','D','E','F','G','H','I','J','K']//答案列表
+const tflist=['T','F']//判断题列表
+
+
+// 考试科目 ID
+const examSubjectId = computed(() => {
+  return examStore.currentExam?.id
+})//计算属性
+
+const templateId=computed(() => {
+  return examStore.currentExam?.templateId
+})//计算属性
+
+const totalScore=computed(() => {
+  if (!tableData.value || tableData.value.length === 0) {
+    return 0;
+  }
+  
+  // 累加所有题目的 fullScore,防止值为空或非数字时出错
+  const sum = tableData.value.reduce((total, item) => {
+    // 确保 fullScore 是有效数字,如果是空字符串或 null 则视为 0
+    const score = Number(item.fullScore);
+    return total + (isNaN(score) ? 0 : score);
+  }, 0);
+
+  return sum;
+})
+
+// 通用分数输入处理函数
+// target: 需要更新的目标对象 (如 scope.row 或 queryParams)
+// key: 目标对象中的属性名 (如 'fullScore' 或 'fullMark')
+// // val: 当前输入的值
+const onScoreInput = (target: any, key: string, val: any) => {
+    // 1. 确保 val 是字符串
+    let strVal = String(val || '');
+    
+    // 2. 正则替换:只保留数字和小数点
+    strVal = strVal.replace(/[^\d.]/g, '');
+    
+    // 3. 逻辑:只允许一个小数点
+    const parts = strVal.split('.');
+    if (parts.length > 2) {
+        strVal = parts[0] + '.' + parts.slice(1).join('');
+    }
+    
+    // 4. 逻辑:限制总长度不超过3
+    if (strVal.length > 3) {
+        strVal = strVal.slice(0, 3);
+    }
+    
+    // 5. 逻辑:如果不以数字开头且不为空,防止单独输入 '.'
+    // 注意:如果允许 "0." 这种输入,则不需要此步,但通常 "0." 后面还得跟数字,这里暂时保留防止非法状态
+    if (strVal === '.') {
+        strVal = ''; 
+    }
+
+    // 6. 关键步骤:直接修改目标对象的属性
+    if (target && key) {
+        target[key] = strVal;
+    }
+};
+
+//打开选择模板弹窗
+const OpenChooseTemplate=()=>{
+    showChooseTemplate.value=true;
+}
+
+//切换题目类型
+const TypeChange=(item:any)=>{
+    console.log("打印题目类型",item)
+    
+}
+
+//单选点击事件
+const SingleClick=(item:any,answer:any)=>{
+    console.log("打印单选点击",item,answer)
+    item.standardAnswer = answer;
+    console.log(item);//打印当前标准客户题题列表
+}
+
+// 多选点击事件
+const MultiClick=(answer:any,item:any)=>{
+    console.log("打印多选点击",answer,item)
+
+            // console.log("打印当前questionTable",this.questionTable);
+
+    if(item.standardAnswer==undefined)
+    {
+        item.standardAnswer='';
+    }
+    // let answerlist=item.standardAnswer==''?[]:item.standardAnswer?.split(",");
+    let answerlist = item.standardAnswer === '' ? [] : (item.standardAnswer || '').replace(/\s+/g, '').split(',').flatMap(item => item === '' ? [] : item.split(''));
+    if (!answerlist?.includes(answer)) 
+    {
+        answerlist?.push(answer)
+
+    }
+    else 
+    {
+        answerlist?.forEach((i,index) => {
+            if (i === answer) {
+                answerlist?.splice(index,1)
+            }
+        })
+    }
+    item.standardAnswer = answerlist?.join(",")
+    //     // 给当前题选择的答案赋值
+    // for(var i=0;i<this.questionTable.length;i++)
+    // {
+    //     if(item.id==this.questionTable[i].id)
+    //     {
+    //         this.questionTable[i].standardAnswer=item.standardAnswer;
+    //     }
+    // }
+    // console.log("打印当前questionTable",this.questionTable);
+}
+
+//删除全部 需要二次弹窗确认提示
+const DeleteAll=()=>{
+    console.log("打印删除全部")
+    ElMessageBox.confirm('确认删除所有题目吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+    }).then(async () => {
+        console.log('删除考试')
+        const param={
+            id:examSubjectId.value,
+        };
+        const res = await deleteAllQuestion(param)
+        if(res.code == 200) 
+        {
+            ElMessage.success("删除成功!")
+            GetSmartQuestionList();
+        }
+        else
+        {
+            ElMessage.error(res.msg)
+        }
+    })
+}
+
+//确定完成
+const FinishQuestion=()=>{
+    console.log("打印完成")
+    ElMessageBox.confirm('确认完成吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+    }).then(async () => {
+        console.log('完成',tableData.value)
+
+        const res = await saveQuestion(tableData.value)
+        if(res.code == 200) 
+        {
+            ElMessage.success("保存成功!")
+            GetSmartQuestionList();
+        }
+        else
+        {
+            ElMessage.error(res.msg)
+        }
+    })
+}
+
+//删除单个题目 需要二次弹窗确认提示
+const DeleteSingle=(item:any)=>{
+    console.log("打印删除单个",item)
+    ElMessageBox.confirm('确认删除该题目吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+    }).then(async () => {
+        console.log('删除考试')
+        const param={
+            id:item.id,
+            examSubjectId:examSubjectId.value,
+        };
+        const res = await deleteSingleQuestion(param)
+        if(res.code == 200) 
+        {
+            ElMessage.success("删除成功!")
+            GetSmartQuestionList(); 
+        }
+        else
+        {
+            ElMessage.error(res.msg);
+        }
+    })
+}
+// 4. 计算高度的函数
+const CalculateTableHeight = () => {
+
+  // 获取父容器的高度
+  const containerHeight = window.innerHeight 
+  
+  // 减去其他固定元素的高度(需要根据实际布局调整这些数值)
+  // 例如:header(64px) + 间距(20px) + 标题区域(约50px) + 底部按钮区域(约60px) + 分页信息(56px)
+  // 建议通过浏览器控制台检查实际占用高度
+  const offset = 320 // 这是一个预估值,请根据实际 CSS 调整
+  
+  tableHeight.value = containerHeight - offset
+}
+
+//获取模板信息
+const GetTemplateInfo=()=>{
+    const params={
+        templateId:templateId.value,
+    };
+    getTemplateInfo(params).then((res) => {
+        console.log("打印获取模板信息结果",res)
+        if(res.code==200)
+        {
+            templateQuestionCount.value=res.data.totalQuestionSize;//题目数量
+            templateName.value=res.data.templateName;
+        }
+    })
+}
+
+//获取试题列表
+const GetSmartQuestionList=()=>{
+    const params={
+        examSubjectId: examSubjectId.value,
+    };
+    getSmartQuestionList(params).then((res) => {
+        console.log("打印获取试题结构列表结果",res)
+        if(res.code==200)
+        {
+            // tableData.value=res.data;
+            // 增加空值保护,确保 tableData 始终是数组
+            tableData.value = Array.isArray(res.data) ? res.data : [];
+        }
+    })
+}
+
+//确定设置题目
+const EnterSetQuestion=()=>{
+    console.log("打印提交参数",queryParams.value)
+
+    const params={
+        examSubjectId: examSubjectId.value,//考试科目id
+        questionMin: queryParams.value.start,//起始题号
+        questionMax: queryParams.value.end,//结束题号
+        questionType: queryParams.value.questionType,
+        fullScore: queryParams.value.fullMark,
+        groupName: queryParams.value.groupName,
+    };
+    setQuestion(params).then((res) => {
+        console.log("打印设置题目结果",res)
+        if(res.code==200)
+        {
+           //设置成功 更新列表
+           GetSmartQuestionList();
+        }
+    })
+}
+
+onMounted(() => {
+  
+    if (!examStore.currentExam) {
+        console.warn('当前没有选中的考试信息')
+        // 可选:如果没有数据,可以重定向回列表页或提示用户
+    }
+    GetSmartQuestionList();
+    GetTemplateInfo();
+    // 初始化计算
+    nextTick(() => {
+        CalculateTableHeight()
+    })
+
+    window.addEventListener('resize', CalculateTableHeight)
+})
+
+
+onUnmounted(() => {
+    // 移除监听,防止内存泄漏
+    window.removeEventListener('resize', CalculateTableHeight)
+})
+</script>
+ 
+<style lang="scss" scoped>
+.detail_main
+{
+    height: 100vh; /* 或 100% */
+    display: flex;
+    flex-direction: column;
+}
+.question_header
+{
+    width: 100%;
+    height: 64px;
+    line-height: 64px;
+    background: #FFFFFF;
+    border-radius: 10px 10px 10px 10px;
+    padding: 0 20px;
+    box-sizing: border-box;
+
+    display: flex;
+    justify-content: space-between;
+
+    .choose_template
+    {
+        font-weight: 500;
+        font-size: 16px;
+        color: #333333;
+
+        .choose_btn
+        {
+            font-weight: 500;
+            font-size: 16px;
+            color: #2E64FA;
+            margin-left: 10px;
+            cursor: pointer;
+        }
+
+    }
+
+}
+.item_step
+{
+    width: 20px;
+    height: 20px;
+    background: #2E64FA;
+    border-radius: 50%;
+    font-weight: 500;
+    font-size: 14px;
+    color: #FFFFFF;
+    text-align: center;
+    line-height: 20px;
+
+}
+
+.item_search
+{
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    margin-right: 10px;
+    .search_name
+    {
+        width: auto;
+        font-weight: 400;
+        font-size: 16px;
+        color: #666666;
+    }
+    
+    .select_width
+    {
+        width: 100px;
+    }
+}
+.item_title
+{
+    font-weight: 500;
+    font-size: 16px;
+    color: #333333;
+
+    display: flex;
+    justify-content: flex-start;
+    align-items: center;
+    gap: 10px;
+    position: relative;
+
+    .preview_btn
+    {
+        font-weight: 500;
+        font-size: 16px;
+        color: #2E64FA;
+        cursor: pointer;
+    }
+
+    .delete_all
+    {
+        position: absolute;
+        right: 0;
+        top: 0;
+        width: 100px;
+        height: 36px;
+        display: flex;
+        justify-content: flex-end;
+        align-items: center;
+
+        .delete_btn
+        {
+            width: 88px;
+            height: 34px;
+            line-height: 36px;
+            text-align: center;
+            border-radius: 4px 4px 4px 4px;
+            border: 1px solid #F56C6C;
+            font-weight: 500;
+            font-size: 14px;
+            color: #F56C6C;
+            cursor: pointer;
+        }
+    }
+}
+
+.page_info
+{
+    font-weight: 500;
+    font-size: 16px;
+    color: #333333;
+    text-align: right;
+    height: 56px;
+    line-height: 56px;
+}
+
+.page_button
+{
+    text-align: center;
+}
+
+//标准答案录入
+.answer_input {
+    width: 100%;
+    padding-top: 8px;
+    padding-bottom: 8px;
+    margin: auto;
+    // background-color: red;
+    display: flex;
+    justify-content: flex-start;
+    flex-wrap: wrap;
+    height: auto;
+
+    .single_selected {
+      width: auto;
+      margin: auto;
+      display: flex;
+      justify-content: flex-start;
+      flex-wrap: wrap;
+
+      gap: 8px;
+
+      .selected_button {
+        width: 32px;
+        height: 32px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        margin-bottom: 5px;
+        padding: 0 !important;
+        border-radius: 50%;
+        margin: 0;
+
+      }
+    }
+
+
+  }
+</style>

+ 258 - 0
src/views/exam/scanDetail.vue

@@ -0,0 +1,258 @@
+<template>
+    <!-- 扫描详情 -->
+  <div class="page_list">
+    
+    <div class="search_content">
+        <div class="content_left">
+            <el-select  v-model="params.batchNo"   placeholder="选择批次" @change="GoSearch()" class="select_width" >
+                <el-option label="全部批次" value=""></el-option>
+                <el-option v-for="item in batchList"
+                :key="item.value"
+                :label="item.label" :value="item.value"></el-option>
+            </el-select>
+            <el-select  v-model="params.batchNo"   placeholder="选择状态" @change="GoSearch()" class="select_width" >
+                <el-option label="全部状态" value=""></el-option>
+                <el-option v-for="item in batchList"
+                :key="item.value"
+                :label="item.label" :value="item.value"></el-option>
+            </el-select>
+            <el-input placeholder="考试名称,编号"  v-model="params.keyWord" @input="GoSearch" @change="GoSearch()" class="input_width" > 
+                <el-button @click="GoSearch()"  slot="append" icon="el-icon-search"></el-button>
+            </el-input>
+        </div>
+        <div class="content_right">
+            <el-button @click="Refresh" >
+                <i class="iconfont icon_shuaxin"></i>刷新
+            </el-button>
+            
+
+            
+            <!-- <el-button class="delete_item" type="text" @click="OpenDeleteAllDialog"  v-if="tableData.length>0">删除所有</el-button> -->
+            <el-button @click="OpenReIdentify" >重新识别</el-button>
+            <el-button @click="OpenEditExamList()" type="primary" v-if="isImportStudent">编辑考场名单</el-button>
+            <el-button @click="OpenImportStudent()" type="primary" v-else>导入考场名单</el-button>
+        </div>
+    </div>
+    <div class="page_jg_20"></div>
+    <div class="page_content" >
+        <div class="content_table">
+            <div class="table_header">
+                <div class="header_left">
+                    <div class="scan_button_header">
+                        <el-button @click="ChangeMode('list')" :type="listMode=='list'?'primary':''"><el-icon><Menu /></el-icon> 列表模式</el-button>
+                        <el-button @click="ChangeMode('pic')" :type="listMode=='pic'?'primary':''"><el-icon><Picture /></el-icon> 图片模式</el-button>
+                    </div>
+    
+                </div>
+                <div class="header_right">
+                    客户端状态:
+                    <span class="scan_state_open" v-if="scanClientStates">
+                        <i class="iconfont icon_open"></i>已打开</span>
+                    <span class="scan_state_close" v-else>
+                        <i class="iconfont icon_close"></i>未打开</span>
+                </div>
+            </div>
+            <div class="page_jg_20"></div>
+            <div class="page_table">
+                <el-table :data="tableData" style="width: 100%" :height="tableHeight">
+                    <el-table-column prop="questionName" label="序号" width="100" align="center" >
+                    </el-table-column>
+                    <el-table-column prop="questionType" label="批次" width="120" align="center">
+                    </el-table-column>
+                    <el-table-column prop="questionType" label="考号" width="120" align="center">
+                    </el-table-column>
+                    <el-table-column prop="date" label="客观题" align="center">
+                        <template v-slot="scope">
+                            
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="fullScore" label="状态" width="120" align="center">
+                        <template v-slot="scope">
+                            <div class="full_mark_input">
+                                <el-input v-model="scope.row.fullScore" maxlength="3" @input="(val: any) => onScoreInput(scope.row, 'fullScore', val)"></el-input>
+                            </div>
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="name" label="操作" width="150" align="center">
+                        <template v-slot="scope">
+                            <div class="ele_button table_row_button">
+                                <span class="btn_editor">编辑</span>
+                                <span class="btn_delete" @click="DeleteSingle(scope.row)">删除</span>
+                            </div>
+                    </template>
+                    </el-table-column>
+                </el-table>
+            </div>
+        </div>
+        <div class="content_right">
+            <div class="right_header">
+               <span> 扫描 设置</span>
+               <span>
+                识别号:
+                <el-select v-model="params.batchNo" placeholder="请选择" @change="GoSearch()" style="width: 120px;">
+                    <el-option label="全部状态" value=""></el-option>
+                    <el-option v-for="item in batchList" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                </el-select>
+               </span>
+            </div>
+            <div class="right_center">
+                <div class="scan_buttons" @click="OpenScan">
+                    <ScanButton></ScanButton>
+                </div>
+                <div class="scan_list">
+                    <div class="list_item no_scan" >
+                        <div class="list_item_info " @click="GotoDetail(0)">
+                            <div class="item_info_title">
+                                未扫描
+                            </div>
+                            <div class="item_info_number">
+                                <span class="number_no_scan">{{}}人</span>
+                                <!-- <span class="number_no_icon"><img src="../../assets/icon/no_scan_icon.png"></span> -->
+                            </div>
+                        </div>
+                    </div>
+                    <div class="list_item no_exam" >
+                        <div class="list_item_info " @click="GotoDetail(2)">
+                            <div class="item_info_title">
+                                缺考
+                            </div>
+                            <div class="item_info_number">
+                                <span class="number_no_exam">{{}}人</span>
+                                <!-- <span class="number_no_icon"><img src="../../assets/icon/miss_exam.png"></span> -->
+                            </div>
+                        </div>
+                    </div>
+                    <div class="list_item annormal_icon" >
+                        <div class="list_item_info " @click="GotoDetail(3)">
+                            <div class="item_info_title">
+                                异常
+                            </div>
+                            <div class="item_info_number">
+                                <span class="number_abnormal">{{}}份</span>
+                                <!-- <span class="number_no_icon"><img src="../../assets/icon/abnormal_icon.png"></span> -->
+                            </div>
+                        </div>
+                    </div>
+                    <div class="list_item sucess_upload" >
+                        <div class="list_item_info " @click="GotoDetail(1)">
+                            <div class="item_info_title">
+                                已上传
+                            </div>
+                            <div class="item_info_number">
+                                <span class="number_uploaded">{{}}人</span>
+                                <!-- <span class="number_no_icon"><img src="../../assets/icon/sucess_upload.png"></span> -->
+
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="right_button">
+                <el-button type="primary" @click="GoSearch()" style="width:calc(100% - 40px);">扫描完成</el-button>
+            </div>
+        </div>
+    </div>
+    <SelectStudent v-model="showSelectStudent"  @success="StudentSuccess"></SelectStudent>
+  </div>
+</template>
+<script lang="ts" setup>
+import { useExamStore } from '@/store/exam'
+import { useRouter } from 'vue-router'
+import { onMounted ,ref,computed} from 'vue';
+import ScanButton from './components/scanButton.vue'
+import SelectStudent from './components/selectStudent.vue'
+import { hasImportStudent, } from '@/api/exam'
+// 实例化 Store
+const examStore = useExamStore()
+const router = useRouter()
+
+// 考试科目 ID
+const examSubjectId = computed(() => {
+  return examStore.currentExam?.id
+})//计算属性
+
+const params=ref({
+    batchNo:'',
+    keyWord:''
+})
+
+const batchList=[];
+const listMode=ref('list');
+const scanClientStates=ref(false);//客户端状态
+const tableData=ref([]);
+
+const tableHeight=ref(500);
+
+
+const isImportStudent=ref(false);//是否导入了学生名单
+const showSelectStudent=ref(false);//是否显示选择学生名单弹窗
+
+// 打开导入学生名单
+const OpenImportStudent = () => {
+    showSelectStudent.value=true;
+}
+
+// 打开编辑考试名单
+const OpenEditExamList = () => {
+    router.push({
+        path: '/exam/examList',
+        query: {
+            examSubjectId: examSubjectId.value,
+        },
+    });
+}
+
+
+//学生名单导入成功
+const StudentSuccess = () => {
+
+    HasImportStudent();
+}
+
+
+
+//查询是否导入了学生名单
+const HasImportStudent = async () => { 
+    const params = {
+      examSubjectId: examSubjectId.value,
+      schoolId: 0,//单校 0 
+    };
+    const res = await hasImportStudent(params);
+    console.log("打印是否导入了学生名单",res);
+    if(res.code==200)
+    {
+        isImportStudent.value=res.data;
+    }
+    else{
+        isImportStudent.value=false;
+    }
+}
+
+
+onMounted(() => {
+  
+    if (!examStore.currentExam) {
+        console.warn('当前没有选中的考试信息')
+        // 可选:如果没有数据,可以重定向回列表页或提示用户
+    }
+    HasImportStudent();
+  
+})
+</script>
+ 
+<style lang="scss" scoped>
+
+.page_list
+{
+    width: 100%;
+    height: 100%;
+    padding: 20px;
+    background-color: #fff;
+    border-radius: 4px;
+    box-sizing: border-box;
+
+}
+
+
+
+</style>

+ 317 - 0
src/views/exam/scanList.vue

@@ -0,0 +1,317 @@
+<template>
+  <div class="page_list">
+    
+    <div class="search_content">
+        <div class="content_left">
+                <span class="scan_state_title">客户端状态:</span> 
+                <span class="scan_state_open" v-if="scanClientStates">
+                    <i class="iconfont icon_open"></i>已打开</span>
+                <span class="scan_state_close" v-else>
+                    <i class="iconfont icon_close"></i>未打开</span>
+            <!-- <el-select  v-model="params.batchNo"   placeholder="选择批次" @change="GoSearch()" class="select_width" >
+                <el-option label="全部批次" value=""></el-option>
+                <el-option v-for="item in batchList"
+                :key="item.value"
+                :label="item.label" :value="item.value"></el-option>
+            </el-select>
+            <el-select  v-model="params.batchNo"   placeholder="选择状态" @change="GoSearch()" class="select_width" >
+                <el-option label="全部状态" value=""></el-option>
+                <el-option v-for="item in batchList"
+                :key="item.value"
+                :label="item.label" :value="item.value"></el-option>
+            </el-select>
+            <el-input placeholder="考试名称,编号"  v-model="params.keyWord" @input="GoSearch" @change="GoSearch()" class="input_width" > 
+                <el-button @click="GoSearch()"  slot="append" icon="el-icon-search"></el-button>
+            </el-input> -->
+        </div>
+        <div class="content_right">
+            <el-button @click="Refresh" >
+                <i class="iconfont icon_shuaxin"></i>刷新
+            </el-button>
+            
+
+            
+            <!-- <el-button class="delete_item" type="text" @click="OpenDeleteAllDialog"  v-if="tableData.length>0">删除所有</el-button> -->
+            <el-button @click="OpenReIdentify" >重新识别</el-button>
+            <el-button @click="OpenEditExamList()" type="primary" v-if="isImportStudent">编辑考场名单</el-button>
+            <el-button @click="OpenImportStudent()" type="primary" v-else>导入考场名单</el-button>
+        </div>
+    </div>
+    <div class="page_jg_20"></div>
+    <div class="page_content" >
+        <div class="content_table">
+            <div class="page_table">
+                <el-table :data="tableData" style="width: 100%" :height="tableHeight">
+                    <el-table-column prop="questionName" label="序号" width="100" align="center" >
+                    </el-table-column>
+                    <el-table-column prop="questionType" label="批次" width="120" align="center">
+                    </el-table-column>
+                    <el-table-column prop="questionType" label="考号" width="120" align="center">
+                    </el-table-column>
+                    <el-table-column prop="date" label="客观题" align="center">
+                        <template v-slot="scope">
+                            
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="fullScore" label="状态" width="120" align="center">
+                        <template v-slot="scope">
+                            <div class="full_mark_input">
+                                <el-input v-model="scope.row.fullScore" maxlength="3" @input="(val: any) => onScoreInput(scope.row, 'fullScore', val)"></el-input>
+                            </div>
+                        </template>
+                    </el-table-column>
+                    <el-table-column prop="name" label="操作" width="150" align="center">
+                        <template v-slot="scope">
+                            <div class="ele_button table_row_button">
+                                <span class="btn_editor">编辑</span>
+                                <span class="btn_delete" @click="DeleteSingle(scope.row)">删除</span>
+                            </div>
+                    </template>
+                    </el-table-column>
+                </el-table>
+            </div>
+        </div>
+        <div class="content_right">
+            <div class="right_header">
+               <span> 扫描 设置</span>
+               <span>
+                识别号:
+                <el-select v-model="params.batchNo" placeholder="请选择" @change="GoSearch()" style="width: 120px;">
+                    <el-option label="全部状态" value=""></el-option>
+                    <el-option v-for="item in batchList" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                </el-select>
+               </span>
+            </div>
+            <div class="right_center">
+                <div class="scan_buttons" @click="OpenScan">
+                    <ScanButton></ScanButton>
+                </div>
+                <div class="scan_list">
+                    <div class="list_item no_scan" >
+                        <div class="list_item_info " @click="GotoDetail(0)">
+                            <div class="item_info_title">
+                                未扫描
+                            </div>
+                            <div class="item_info_number">
+                                <span class="number_no_scan">{{}}人</span>
+                                <!-- <span class="number_no_icon"><img src="../../assets/icon/no_scan_icon.png"></span> -->
+                            </div>
+                        </div>
+                    </div>
+                    <div class="list_item no_exam" >
+                        <div class="list_item_info " @click="GotoDetail(2)">
+                            <div class="item_info_title">
+                                缺考
+                            </div>
+                            <div class="item_info_number">
+                                <span class="number_no_exam">{{}}人</span>
+                                <!-- <span class="number_no_icon"><img src="../../assets/icon/miss_exam.png"></span> -->
+                            </div>
+                        </div>
+                    </div>
+                    <div class="list_item annormal_icon" >
+                        <div class="list_item_info " @click="GotoDetail(3)">
+                            <div class="item_info_title">
+                                异常
+                            </div>
+                            <div class="item_info_number">
+                                <span class="number_abnormal">{{}}份</span>
+                                <!-- <span class="number_no_icon"><img src="../../assets/icon/abnormal_icon.png"></span> -->
+                            </div>
+                        </div>
+                    </div>
+                    <div class="list_item sucess_upload" >
+                        <div class="list_item_info " @click="GotoDetail(1)">
+                            <div class="item_info_title">
+                                已上传
+                            </div>
+                            <div class="item_info_number">
+                                <span class="number_uploaded">{{}}人</span>
+                                <!-- <span class="number_no_icon"><img src="../../assets/icon/sucess_upload.png"></span> -->
+
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="right_button">
+                <el-button type="primary" @click="GoSearch()" style="width:calc(100% - 40px);">扫描完成</el-button>
+            </div>
+        </div>
+    </div>
+    <SelectStudent v-model="showSelectStudent"  @success="StudentSuccess"></SelectStudent>
+  </div>
+</template>
+<script lang="ts" setup>
+import { useExamStore } from '@/store/exam'
+import { useRouter } from 'vue-router'
+import { onMounted ,ref,computed,onUnmounted,nextTick } from 'vue';
+import ScanButton from './components/scanButton.vue'
+import SelectStudent from './components/selectStudent.vue'
+import { hasImportStudent, } from '@/api/exam'
+import scanCommon from '@/utils/scanCommon';
+
+
+// 实例化 Store
+const examStore = useExamStore()
+const router = useRouter()
+
+// 考试科目 ID
+const examSubjectId = computed(() => {
+  return examStore.currentExam?.id
+})//计算属性
+
+const params=ref({
+    batchNo:'',
+    keyWord:''
+})
+
+const batchList=[];
+const listMode=ref('list');
+const scanClientStates=ref(false);//客户端状态
+const tableData=ref([]);
+
+const tableHeight=ref(500);
+
+
+const isImportStudent=ref(false);//是否导入了学生名单
+const showSelectStudent=ref(false);//是否显示选择学生名单弹窗
+
+// 打开导入学生名单
+const OpenImportStudent = () => {
+    showSelectStudent.value=true;
+}
+
+// 打开编辑考试名单
+const OpenEditExamList = () => {
+    router.push({
+        path: '/exam/examList',
+        query: {
+            examSubjectId: examSubjectId.value,
+        },
+    });
+}
+
+
+//学生名单导入成功
+const StudentSuccess = () => {
+
+    HasImportStudent();
+}
+
+
+
+//查询是否导入了学生名单
+const HasImportStudent = async () => { 
+    const params = {
+      examSubjectId: examSubjectId.value,
+      schoolId: 0,//单校 0 
+    };
+    const res = await hasImportStudent(params);
+    console.log("打印是否导入了学生名单",res);
+    if(res.code==200)
+    {
+        isImportStudent.value=res.data;
+    }
+    else{
+        isImportStudent.value=false;
+    }
+}
+
+
+//计算高度的函数
+const CalculateTableHeight = () => {
+  // nextTick 确保 DOM 更新后再获取尺寸
+  nextTick(() => {
+    // window.innerHeight 是浏览器可视区域高度
+
+    // 简单算法:视窗高度 - 固定占用高度
+    let computedHeight = window.innerHeight - 136;
+    
+    // 限制最小高度,防止太矮
+    if (computedHeight < 200) {
+      computedHeight = 200;
+    }
+
+    tableHeight.value = computedHeight;
+  });
+}; 
+// 处理扫描结果
+const HandleScanResult = (data: any) => {
+  console.log('收到扫描数据', data);
+  // 业务逻辑...
+};
+
+onMounted(() => {
+    // 1. 初始化连接
+    scanCommon.init(HandleScanResult);
+    // 2. 监听连接状态(可选)
+    scanCommon.watchConnection((isOnline) => {
+        console.log('连接状态:', isOnline ? '在线' : '离线');
+        scanClientStates.value=isOnline;
+    });
+    if (!examStore.currentExam) {
+        console.warn('当前没有选中的考试信息')
+        // 可选:如果没有数据,可以重定向回列表页或提示用户
+    }
+    HasImportStudent();
+     // 初始化计算
+    CalculateTableHeight();
+  
+    // 监听窗口大小变化
+    window.addEventListener('resize', CalculateTableHeight);
+});
+// 卸载时移除监听,防止内存泄漏
+onUnmounted(() => {
+  window.removeEventListener('resize', CalculateTableHeight);
+  // 组件销毁时务必停止,防止内存泄漏和后台重连
+  scanCommon.stop();
+});
+</script>
+ 
+<style lang="scss" scoped>
+
+.page_list
+{
+    width: 100%;
+    height: 100%;
+    padding: 20px;
+    background-color: #fff;
+    border-radius: 4px;
+    box-sizing: border-box;
+
+}
+
+.scan_state_title
+{
+    font-size: 14px;
+    color:#333;
+    font-weight: 400;
+
+}
+.scan_state_open
+{   margin-left: 5px;
+    font-size: 14px;
+    font-weight: 400;
+    color: #2BC644;
+     i
+    {
+        margin-right: 5px;
+    }
+}
+
+.scan_state_close
+{
+    font-size: 14px;
+    font-weight: 400;
+    margin-left: 5px;
+    color:#F56C6C;
+    i
+    {
+        margin-right: 5px;
+    }
+    
+}
+
+
+</style>

+ 30 - 0
src/views/exam/score.vue

@@ -0,0 +1,30 @@
+<template>
+    <!-- 成绩查询 -->
+  <div class="aside_container">
+    
+  </div>
+</template>
+<script lang="ts" setup>
+import { useExamStore } from '@/store/exam'
+import { useRouter } from 'vue-router'
+import { onMounted ,ref} from 'vue';
+
+// 实例化 Store
+const examStore = useExamStore()
+const router = useRouter()
+
+
+onMounted(() => {
+  
+    if (!examStore.currentExam) {
+        console.warn('当前没有选中的考试信息')
+        // 可选:如果没有数据,可以重定向回列表页或提示用户
+    }
+    
+  
+})
+</script>
+ 
+<style lang="scss" scoped>
+
+</style>

+ 0 - 0
src/views/exam/考试详情文件夹.txt


+ 243 - 0
src/views/fillblank/SearchMain.vue

@@ -0,0 +1,243 @@
+<template>
+  <div class="page_item">
+    <div class="tag_group" v-if="searchList.provinceNode.length > 1">
+      <div class="tag_group_title">省份:</div>
+      <div class="tag_group_content">
+        <div class="tag_item" v-for="item in searchList.provinceNode" :key="item.provinceCode" @click="selectItem(item.provinceCode, 'provinceCode', item)" :class="item.provinceCode === searchData.provinceCode ? 'active' : ''">
+          {{ item.provinceName }}
+        </div>
+      </div>
+    </div>
+    <div class="tag_group" v-if="cityNode.length > 0">
+      <div class="tag_group_title">城市:</div>
+      <div class="tag_group_content">
+        <div class="tag_item" v-for="item in cityNode" :key="item.cityCode" @click="selectItem(item.cityCode, 'cityCode', item)" :class="item.cityCode === searchData.cityCode ? 'active' : ''">
+          {{ item.cityName }}
+        </div>
+      </div>
+    </div>
+    <div class="tag_group" v-if="districtNode.length > 0">
+      <div class="tag_group_title">区/县:</div>
+      <div class="tag_group_content">
+        <div class="tag_item" v-for="item in districtNode" :key="item.districtCode" @click="selectItem(item.districtCode, 'districtCode', item)" :class="item.districtCode === searchData.districtCode ? 'active' : ''">
+          {{ item.districtName }}
+        </div>
+      </div>
+    </div>
+    <div class="tag_group" v-if="searchList.userInfo.length > 1">
+      <div class="tag_group_title">运维:</div>
+      <div class="tag_group_content">
+        <div class="tag_item" v-for="item in searchList.userInfo" :key="item.userId" @click="selectItem(item.userId, 'userId', item)" :class="item.userId === searchData.userId ? 'active' : ''">
+          {{ item.nickname }}
+        </div>
+      </div>
+    </div>
+    <div class="tag_group" v-if="searchList.typeNode.length > 1">
+      <div class="tag_group_title">状态:</div>
+      <div class="tag_group_content">
+        <div class="tag_item" v-for="item in searchList.typeNode" :key="item.type" @click="selectItem(item.type, 'type', item)" :class="item.type === searchData.type ? 'active' : ''">
+          {{ item.typeName }}
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+export default {
+  name: 'SearchMain',
+}
+</script>
+<script lang="ts" setup>
+import { onMounted, ref, nextTick } from 'vue'
+import { getSearchList } from '@/api/school'
+const searchData = ref({
+  provinceCode: '',
+  cityCode: '',
+  districtCode: '',
+  type: '',
+  userId: '',
+})
+
+interface Province {
+  provinceCode: string;
+  provinceName: string;
+  children?: City[]; // 如果有子级城市数据的话
+}
+interface City {
+  cityCode:  string
+  cityName:  string
+  children?: District[]; // 如果有子级区县数据的话
+}
+interface District {
+  districtCode:  string
+  districtName:  string
+}
+const cityNode = ref<City[]>([])
+const districtNode = ref<District[]>([])
+
+interface TypeNode {
+  type: string;
+  typeName: string;
+}
+
+interface UserInfo {
+  userId: string;
+  nickname: string;
+}
+
+interface SearchList {
+  examTypes: any[]; 
+  provinceNode: Province[];
+  cityNode: City[];
+  districtNode: District[];
+  typeNode: TypeNode[];
+  userInfo: UserInfo[];
+}
+const searchList =  ref<SearchList>({
+  examTypes: [],
+  provinceNode: [],
+  cityNode: [],
+  districtNode: [],
+  typeNode: [],
+  userInfo: []
+})
+const emit = defineEmits(['FnSearch'])
+
+type SearchDataKey = 'provinceCode' | 'cityCode' | 'districtCode' | 'type' | 'userId'
+const selectItem = (value: string, str: SearchDataKey, item: any) => {
+  searchData.value[str] = value
+  let arr = []
+  if(str == 'provinceCode') {
+    if(value) {
+      arr = [...item.children]
+      arr.splice(0, 0, { cityCode: "", cityName: "全部" })
+      cityNode.value = arr
+      searchData.value.cityCode = ''
+      districtNode.value = []
+      searchData.value.districtCode = ''
+    }else {
+      cityNode.value = []
+      searchData.value.cityCode = ''
+      districtNode.value = []
+      searchData.value.districtCode = ''
+    }
+  }
+  if(str == 'cityCode') {
+    if(value) {
+      arr = [...item.children]
+      arr.splice(0, 0, { districtCode: "", districtName: "全部" })
+      districtNode.value = arr
+      searchData.value.districtCode = ''
+    }else {
+      districtNode.value = []
+      searchData.value.districtCode = ''
+    }
+  }
+  emit('FnSearch', searchData);
+}
+
+onMounted(() => {
+  GetSearchList()
+
+    // HandleScroll()
+    // window.addEventListener('scroll', function() {
+    //   console.log('12123123')
+    // });
+})
+
+const filterRef = ref(null)
+const isSticky = ref(false)
+let observer = null;
+const HandleScroll = () => {
+  observer = new IntersectionObserver(
+    (entries) => {
+      console.log(entries, '>>>>>>entries')
+      entries.forEach(entry => {
+        if (entry.boundingClientRect.top <= 65) {
+          isSticky.value = true;
+        } else {
+          isSticky.value = false;
+        }
+      });
+    },
+    {
+      root: null, // 使用视口作为根元素
+      threshold: 0, // 当元素顶部接触视口顶部时触发
+      rootMargin: `-${65}px 0px 0px 0px` // 相对于视口顶部的偏移
+    }
+  );
+  
+  if (filterRef.value) {
+    observer.observe(filterRef.value);
+  }
+}
+const GetSearchList = () => {
+  getSearchList().then((res: any) => {
+    console.log(res)
+    if(res.code == 200) {
+      const { data } = res
+      for(let i in data) {
+        if(i == 'provinceNode') {
+          data[i].splice(0, 0, { provinceCode: "", provinceName: "全部" })
+        }else if(i == 'cityNode') {
+          data[i].splice(0, 0, { cityCode: "", cityName: "全部" })
+        }else if(i == 'districtNode') {
+          data[i].splice(0, 0, { districtCode: "", districtName: "全部" })
+        }else if( i == 'typeNode') {
+          data[i].splice(0, 0, { type: "", typeName: "全部" })
+        }else if( i == 'userInfo') {
+          data[i].splice(0, 0, { userId: "", nickname: "全部" })
+        }
+      }
+      searchList.value = data
+      console.log(searchList.value)
+    }
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+.page_item {
+  padding: 10px 20px !important;
+}
+.tag_group{
+  padding:8px 0;
+  display: flex;
+  justify-content: flex-start;
+  .tag_group_title {
+    // width: auto;
+    width: 50px;
+    height: auto;
+    margin-right: 8px;
+    color: #303133;
+    font-size: 14px;
+    text-align: right;
+    line-height: 30px;
+    font-weight: 500;
+    letter-spacing: 0em;
+  }
+  .tag_group_content {
+    width: calc(100% - 80px);
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    .tag_item {
+      padding: 0 8px;
+      line-height: 30px;
+      border-radius: 4px;
+      font-size: 14px;
+      background: #fff;
+      color: #999999;
+      cursor: pointer;
+      &.active {
+        background: rgba(71,113,203,0.1);
+        color: #2E64FA;
+      }
+      &:hover {
+        color: #2E64FA;
+      }
+    }
+  }
+}
+</style>

+ 414 - 0
src/views/fillblank/addUser.vue

@@ -0,0 +1,414 @@
+<template>
+  <el-dialog v-model="showDialog" :title="dialogData.pageType == 'add' ? '新增用户' : dialogData.pageType == 'edit' ? '编辑用户' : ''" width="600px">
+    <el-form :model="userForm" :rules="rules" ref="userFormRef">
+      <el-form-item label="学校名称:" :label-width="formLabelWidth" prop="tenantName">
+        <el-input v-model="userForm.tenantName" placeholder="请输入学校名称" />
+      </el-form-item>
+      <el-form-item label="学校简称:" :label-width="formLabelWidth" prop="abbreviation">
+        <el-input v-model="userForm.abbreviation" placeholder="请输入学校名称" />
+      </el-form-item>
+      <el-form-item label="区域:" :label-width="formLabelWidth" required>
+        <el-form-item prop="provinceCode">
+          <el-select v-model="userForm.provinceCode" placeholder="请选择" @change="handleProvinceChange" style="width: 100px;">
+            <el-option
+              v-for="province in provinceTreeList || []"
+              :key="province.provinceCode"
+              :label="province.provinceName"
+              :value="province.provinceCode"
+            ></el-option>
+          </el-select>
+          <span class="margin_10">省</span>
+        </el-form-item>
+        <el-form-item prop="cityCode">
+          <el-select v-model="userForm.cityCode" placeholder="请选择" @change="handleCityChange" style="width: 100px;">
+            <el-option
+              v-for="city in cityTreeList || []"
+              :key="city.cityCode"
+              :label="city.cityName"
+              :value="city.cityCode"
+            ></el-option>
+          </el-select>
+          <span class="margin_10">市</span>
+        </el-form-item>
+        <el-form-item prop="areaCode">
+          <el-select v-model="userForm.areaCode" placeholder="请选择" @change="handleAreaChange" style="width: 100px;">
+            <el-option
+              v-for="district in areaList || []"
+              :key="district.districtCode"
+              :label="district.districtName"
+              :value="district.districtCode"
+            ></el-option>
+          </el-select>
+          <span class="margin_10">区/县</span>
+        </el-form-item>
+      </el-form-item>
+      <el-form-item label="账号类型:" :label-width="formLabelWidth" prop="schoolType">
+        <el-radio-group v-model="userForm.schoolType">
+          <el-radio :value="0">单校版</el-radio>
+          <el-radio :value="1">联考版</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="用户类型:" :label-width="formLabelWidth" prop="type">
+        <el-radio-group v-model="userForm.type">
+          <el-radio :value="1">正式用户</el-radio>
+          <el-radio :value="0">体验用户</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="学校logo:" :label-width="formLabelWidth">
+        <img v-if="userForm.image" :src="userForm.image" alt="" class="file_image">
+        <img v-else src="@/assets/school/default_logo.svg" alt="" class="file_image">
+        <el-upload :http-request="UploadLogo">
+          <el-button size="small" type="primary">{{ userForm.image ? '重新上传' : '上传logo'}}</el-button>
+        </el-upload>
+      </el-form-item>
+      <el-form-item label="有效时间:" :label-width="formLabelWidth" required>
+        <el-form-item prop="startDate">
+          <el-date-picker
+            v-model="userForm.startDate"
+            type="date"
+            placeholder="请选择"
+            format="YYYY/MM/DD"
+            value-format="YYYY-MM-DD"
+            :disabled-date="disabledBeforeDate"
+            :clearable="dialogData.pageType != 'edit'"
+          /><span class="margin_10">至</span>
+        </el-form-item>
+        <el-form-item prop="endDate">
+          <el-date-picker
+            v-model="userForm.endDate"
+            type="date"
+            placeholder="请选择"
+            format="YYYY/MM/DD"
+            value-format="YYYY-MM-DD"
+            :disabled-date="disabledAfterDate"
+            :clearable="dialogData.pageType != 'edit'"
+          />
+        </el-form-item>
+      </el-form-item>
+      <el-form-item label="运维负责人:" :label-width="formLabelWidth" prop="sysUserId">
+        <el-select v-model="userForm.sysUserId" placeholder="请选择" @change="handleUserChange" style="width: 320px;" multiple collapse-tags collapse-tags-tooltip>
+          <el-option
+            v-for="item in userList"
+            :key="item.id"
+            :label="item.nickname"
+            :value="item.id"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button class="button_border_gray cancel" @click="showDialog = false">取消</el-button>
+      <el-button class="button_background" :loading="submitLoading" @click="SubmitForm(userFormRef)">确定</el-button>
+    </template>
+  </el-dialog>
+</template>
+<script lang="ts">
+export default {
+  name: 'AddUser',
+}
+</script>
+<script lang="ts" setup>
+import { ref, reactive, inject, onMounted } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import { ElMessage } from 'element-plus'
+import { getUserList, addUser, uploadFile, provinceTree, getSchoolInfo, editSchoolInfo } from '@/api/school'
+interface DialogData {
+  pageType: string
+  id: string
+}
+let showDialog = inject('showDialog')
+let dialogData = inject<DialogData>('dialogData', {
+  pageType: 'add',
+  id: ''
+})
+const formLabelWidth = '120px'
+// 新增用户表单数据定义
+interface UserForm {
+  id: string
+  tenantName: string
+  abbreviation: string
+  provinceCode: string
+  provinceName: string
+  cityCode: string
+  cityName: string
+  areaCode: string
+  areaName: string
+  schoolType: number
+  type: number
+  image: string
+  startDate: string
+  endDate: string
+  // selUserList: string[]
+  sysUserId: string[]
+}
+// 新增用户表单数据初始化
+const userForm = reactive<UserForm>({
+  id: '',
+  tenantName: '',
+  abbreviation: '',
+  provinceCode: '',
+  provinceName: '',
+  cityCode: '',
+  cityName: '',
+  areaCode: '',
+  areaName: '',
+  schoolType: 0,
+  type: 1,
+  image: '',
+  startDate: '',
+  endDate: '',
+  // selUserList: [],
+  sysUserId: []
+})
+const userFormRef = ref<FormInstance>()
+// 表单校验规则
+const rules = reactive<FormRules<UserForm>>({
+  tenantName: [
+    { required: true, message: '请输入学校名称', trigger: 'blur' }
+  ],
+  // abbreviation: [
+  //   { required: true, message: '请输入学校简称', trigger: 'blur' }
+  // ],
+  provinceCode: [
+    { required: true, message: '请选择省', trigger: 'change' }
+  ],
+  cityCode: [
+    { required: true, message: '请选择市', trigger: 'change' }
+  ],
+  areaCode: [
+    { required: true, message: '请选择区/县', trigger: 'change' }
+  ],
+  schoolType: [
+    { required: true, message: '请选择账号类型', trigger: 'change' }
+  ],
+  type: [
+    { required: true, message: '请选择用户类型', trigger: 'change' }
+  ],
+  startDate: [
+    { required: true, message: '请选择开始时间', trigger: 'change' }
+  ],
+  endDate: [
+    { required: true, message: '请选择结束时间', trigger: 'change' }
+  ],
+  // sysUserId: [
+  //   { required: true, message: '请选择运维人员', trigger: 'change' }
+  // ]
+})
+interface Province {
+  provinceCode: string
+  provinceName: string
+  children?: City[] // 可选字段
+}
+
+interface City {
+  cityCode: string
+  cityName: string
+  children?: District[]
+}
+
+interface District {
+  districtCode: string
+  districtName: string
+}
+// 省份列表
+const provinceTreeList = ref<Province[]>([])
+// 市列表
+const cityTreeList = ref<City[]>([
+  {
+    cityCode: '',
+    cityName: '',
+    children: []
+  }
+])
+// 区/县列表
+const areaList = ref<District[]>([
+  {
+    districtCode: '',
+    districtName: ''
+  }
+])
+interface User {
+  id: string
+  nickname: string
+}
+// 运维人员列表
+const userList = ref<User[]>([])
+// 提交按钮loading
+const submitLoading = ref(false)
+onMounted (async () => {
+  await GetProvinceTree()
+  await GetUserList()
+  if(dialogData.pageType == 'edit') {
+    await GetUserDetail()
+  }
+})
+// 获取用户详情
+const GetUserDetail = () => {
+  getSchoolInfo(dialogData.id).then((res: any) => {
+    if(res.code == 200 && res.data) {
+      const { data } = res
+      if(data.provinceCode) {
+        let list = provinceTreeList.value.find(item => item.provinceCode == data.provinceCode)
+        cityTreeList.value = list?.children ?? []
+      }
+      if(data.cityCode) {
+        let list = cityTreeList.value.find(item => item.cityCode == data.cityCode)
+        areaList.value = list?.children ?? []
+      }
+      userForm.id = data.id
+      userForm.tenantName = data.tenantName
+      userForm.abbreviation = data.abbreviation
+      userForm.provinceCode = data.provinceCode
+      userForm.provinceName = data.provinceName
+      userForm.cityCode = data.cityCode
+      userForm.cityName = data.cityName
+      userForm.areaCode = data.areaCode
+      userForm.areaName = data.areaName
+      userForm.schoolType = data.schoolType
+      userForm.type = data.type
+      userForm.image = data.image
+      userForm.startDate = data.startDate
+      userForm.endDate = data.endDate
+      // userForm.selUserList = data.selUserList
+      userForm.sysUserId = data.sysUserId
+    }
+  })
+}
+// 获取运维人员列表
+const GetUserList = () => {
+  getUserList({}).then((res: any) => {
+    if(res.code == 200 && res.data) {
+      const { data } = res
+      userList.value = data
+    }
+  })
+}
+// 获取省市区树列表
+const GetProvinceTree = (): Promise<void> => {
+  return new Promise((resolve) => {
+    provinceTree().then((res: any) => {
+      if(res.code == 200 && res.data) {
+        const { data } = res
+        provinceTreeList.value = data
+        resolve()
+      }
+    })
+  })
+}
+// 选择省份
+const handleProvinceChange = (val: any) => {
+  userForm.provinceCode = val
+  let name = provinceTreeList.value.find(item => item.provinceCode == val)
+  userForm.provinceName = name?.provinceName ?? ''
+  let list = provinceTreeList.value.find(item => item.provinceCode == val)
+  cityTreeList.value = list?.children ?? []
+  areaList.value = []
+  userForm.cityCode = ''
+  userForm.areaCode = ''
+}
+// 选择市
+const handleCityChange = (val: any) => {
+  userForm.cityCode = val
+  let city = cityTreeList.value.find(item => item.cityCode == val)
+  userForm.cityName = city?.cityName ?? ''
+  let list = cityTreeList.value.find(item => item.cityCode == val)
+  areaList.value = list?.children ?? []
+  userForm.areaCode = ''
+}
+// 选择区/县
+const handleAreaChange = (val: any) => {
+  userForm.areaCode = val
+  let name = areaList.value.find(item => item.districtCode == val)
+  userForm.areaName = name?.districtName ?? ''
+}
+// 上传学校logo
+const UploadLogo = (file: any) => {
+  let formData = new FormData();
+  formData.append("file", file.file);
+  uploadFile(formData).then((res: any) => {
+    console.log(res)
+    if(res.code == 200 && res.data) {
+      const { data } = res
+      userForm.image = data.url
+    }
+  })
+}
+// 禁用目标日期之后的所有日期
+const disabledBeforeDate = (time) => {
+  const targetDate = userForm.endDate
+  if(!targetDate) return false
+  const end = new Date(targetDate)
+  end.setHours(23, 59, 59, 999); // 设置为当天最后一刻
+  return time.getTime() >= end.getTime();
+};
+// 禁用目标日期之前的所有日期
+const disabledAfterDate = (time) => {
+  const targetDate = userForm.startDate
+  if(!targetDate) return false
+  const start = new Date(targetDate)
+  start.setHours(0, 0, 0, 0); // 设置为当天开始
+  return time.getTime() <= start.getTime();
+};
+// 选择运维人员
+const handleUserChange = (val: any) => {
+  console.log(val)
+  let result = userList.value.filter(item => val.includes(item.id)).map(item => {
+    return { id: item.id }
+  })
+  console.log(result)
+  // userForm.selUserList = val
+  userForm.sysUserId = val
+}
+const emit = defineEmits(['AddUser'])
+// 表单提交
+const SubmitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate(async(valid) => {
+    if (valid) {
+      submitLoading.value = true
+      try {
+        let res
+        if(dialogData.pageType == 'add') {
+          res = await addUser(userForm)
+        }else if(dialogData.pageType == 'edit') {
+          res = await editSchoolInfo(userForm)
+        }
+        if (res.code == 200) {
+          ElMessage.success(res.msg)
+          if(userFormRef.value) {
+            userFormRef.value.resetFields()
+          }
+          emit('AddUser', false)
+        } else {
+          ElMessage.error(res.msg)
+        }
+        submitLoading.value = false
+      }catch {
+        submitLoading.value = false
+      }
+    }
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+.file_image {
+  width: 48px;
+  height: 48px;
+  margin-right: 10px;
+  border-radius: 50%;
+}
+.margin_10 {
+  margin: 0 10px;
+}
+.el-input {
+  width: 320px;
+}
+.el-upload .el-button {
+  width: 76px;
+  height: 32px;
+  background: #2E64FA1A;
+  color: #2E64FA;
+  border: none;
+}
+</style>

+ 163 - 0
src/views/fillblank/authConfig.vue

@@ -0,0 +1,163 @@
+<template>
+  <el-dialog v-model="authData.showDialog" :title="`${authData.schoolName} 权限管理`" width="680px">
+    <div>
+      <div class="page_tag">
+        <el-radio-group v-model="authType" @change="ChangeTab">
+          <el-radio-button value="1">开通年级</el-radio-button>
+          <el-radio-button value="2" disabled>开通模块</el-radio-button>
+        </el-radio-group>
+      </div>
+      <div class="page_jg_20"></div>
+      <div v-if="authType == '1'" class="grade_content">
+        <div v-for="(grade, index) in gradeList" :key="grade.levelCode" class="grade_level">
+          <el-checkbox v-model="checkAll[index]" :indeterminate="!checkAll[index] && selectedGrades[index]?.length > 0" :label="grade.levelName" @click="CheckAllChange(index)" />
+          <el-checkbox-group v-model="selectedGrades[index]" @change="(val: string[]) => handleGradeChange(val, index)">
+            <el-checkbox v-for="item in grade.gradeList" :key="item.gradeCode" :label="item.gradeName" :value="item.gradeCode" />
+          </el-checkbox-group>
+          <!-- <div>
+            <el-checkbox v-model="selectedGrades[index]" :label="item.gradeName" v-for="item in grade.gradeList" :key="item.gradeCode" @change="(val) =>handleGradeChange(val, item)" />
+          </div> -->
+        </div>
+      </div>
+    </div>
+    <template #footer>
+      <el-button class="button_border_gray cancel" @click="authData.showDialog = false">取消</el-button>
+      <el-button class="button_background" :loading="submitLoading" @click="SubmitForm">确定</el-button>
+    </template>
+  </el-dialog>
+</template>
+<script lang="ts">
+export default {
+  name: 'AuthConfig'
+}
+</script>
+<script lang="ts" setup>
+import { ref, inject, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getAllGrade, addGrade } from '@/api/school'
+interface AuthData {
+  showDialog: boolean
+  schoolName: string
+  tenantCode: string
+  id: string
+}
+let authData = inject<AuthData>('authData', {
+  showDialog: false,
+  schoolName: '',
+  tenantCode: '',
+  id: ''
+})
+interface GradeData {
+  gradeName: string
+  gradeCode: string
+  schoolId: string
+  gradeList: any[]
+  levelCode: string
+  levelName: string
+  levelNumder?: number 
+}
+const gradeList = ref<GradeData[]>([])
+const authType = ref('1')
+const submitLoading = ref(false)
+const checkAll = ref<boolean[]>([])
+const selectedGrades = ref<string[][]>([])
+onMounted (() => {
+  GetAllGrade()
+})
+const GetAllGrade = () => {
+  getAllGrade(authData.id).then((res: any) => {
+    if(res.code == 200 && res.data) {
+      const { data } = res
+      gradeList.value = data
+      gradeList.value.forEach((item, index) => {
+        checkAll.value[index] = item.gradeList.length ? item.gradeList.every(i => i.schoolGradeId) : false
+        if(item.gradeList.length) {
+          selectedGrades.value[index] = item.gradeList.some(i => i.schoolGradeId) ? item.gradeList.map(i => {
+            if(i.schoolGradeId) {
+              return i.gradeCode
+            }
+          }) : []
+        } else {
+          selectedGrades.value[index] = []
+        }
+      })
+    }
+  })
+}
+const ChangeTab = (val: any) => {
+  console.log(val)
+}
+const CheckAllChange = (index: number) => {
+  if(!checkAll.value[index]) {
+    selectedGrades.value[index] = gradeList.value[index].gradeList.map(item => item.gradeCode)
+  }else {
+    selectedGrades.value[index] = []
+  }
+}
+const handleGradeChange = (val: any[], index: number) => {
+  console.log(index)
+  console.log(val)
+  if(gradeList.value[index].gradeList.length == val.length) {
+    checkAll.value[index] = true
+  } else if(val.length == 0) {
+    checkAll.value[index] = false
+  } else {
+    checkAll.value[index] = false
+  }
+}
+const SubmitForm = async () => {
+  if(!authData) {
+    return
+  }
+  try {
+    submitLoading.value = true
+    let arrList = gradeList.value.map((item: any, index: number) => {
+      let arr = item.gradeList.filter((i: any) => selectedGrades.value[index].includes(i.gradeCode))
+      return {
+        levelCode: item.levelCode,
+        levelName: item.levelName,
+        levelNumder: item.levelNumder,
+        levelFirstGradeCode: item.levelFirstGradeCode,
+        gradeList: arr
+      }
+    }).filter(item => item.gradeList.length)
+    console.log(arrList)
+    const res = await addGrade({
+      tenantCode: authData.tenantCode,
+      schoolInfoId: authData.id,
+      adminLevelInfo: arrList
+    })
+    if(res.code == 200) {
+      ElMessage.success(res.msg)
+      authData.showDialog = false
+    } else {
+      ElMessage.error(res.msg)
+    }
+  } catch {} finally {
+    submitLoading.value = false
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.page_tag {
+  padding: 0 20px;
+  :deep(.el-radio-button__inner){
+    font-size: 16px !important;
+  }
+}
+.grade_content {
+  padding: 0 20px;
+  .grade_level {
+    margin-bottom: 10px;
+    > .el-checkbox :deep(.el-checkbox__label){
+      font-weight: 600;
+      color: #333;
+    }
+    :deep(.el-checkbox__label){
+      font-size: 16px !important;
+      color: #666;
+    }
+  }
+}
+</style>

+ 14 - 0
src/views/fillblank/index.vue

@@ -0,0 +1,14 @@
+<template>
+  <div class="main_container" >
+
+  </div>
+</template>
+
+<script lang="ts" setup>
+
+import { ref, watch, onMounted, onUnmounted } from 'vue'
+
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 183 - 0
src/views/fillblank/subjectList.vue

@@ -0,0 +1,183 @@
+<template>
+  <el-dialog v-model="subjectShowDialog" title="科目列表" width="600px">
+    <div class="subject_list">
+      <div v-for="tag in subjectList" :key="tag.id" class="list_item" :class="tag.canDelete == 1 ? 'list_item can_delete' : 'list_item'">
+        <span>{{ tag.courseName }}</span>
+        <img src="@/assets/icon/delete.svg" v-if="tag.canDelete" @click="delSubject(tag.id)">
+      </div>
+      <el-input ref="InputRef" v-if="inputVisible" v-model="inputValue" @keyup.enter="handleInputConfirm" @blur="InputBlur" class="input_item">
+        <template #append>
+          <img src="@/assets/icon/confirm.svg" @click="handleInputConfirm">
+        </template>
+      </el-input>
+      <div class="list_item cancel" v-if="inputVisible" @click="cancelAdd">
+        <img src="@/assets/icon/cancel.svg">
+        <span>取消</span>
+      </div>
+      <div v-else class="list_item add" @click="AddCourse">
+        <img src="@/assets/icon/add.svg">
+        <span>添加</span>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script lang="ts">
+export default {
+  name: 'SubjectList'
+}
+</script>
+<script lang="ts" setup>
+import { ref, inject, nextTick, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getAllSubject, deleteSubject, addSubject } from '@/api/school'
+let subjectShowDialog = inject('subjectShowDialog')
+interface subjectList {
+  id: number
+  courseName: string
+}
+const subjectList = ref<subjectList[]>([])
+const inputValue = ref('')
+const inputVisible = ref(false)
+onMounted(() => {
+  GetAllSubject()
+})
+// 获取所有科目
+const GetAllSubject = () => {
+  getAllSubject({}).then((res: any) => {
+    if (res.code === 200 && res.data) {
+      const { data } = res
+      subjectList.value = data
+    }
+  })
+}
+
+const InputBlur = () => {
+  if(!inputValue.value) {
+    inputVisible.value = false
+    inputValue.value = ''
+  }
+}
+const handleInputConfirm = () => {
+  if (inputValue.value) {
+    addSubject({ courseName: inputValue.value }).then((res: any) => {
+      if (res.code === 200) {
+        ElMessage.success(res.msg)
+        GetAllSubject()
+        inputVisible.value = false
+        inputValue.value = ''
+      } else {
+        ElMessage.error(res.msg)
+      }
+    })
+  } else {
+    inputVisible.value = false
+    inputValue.value = ''
+  }
+}
+const InputRef = ref<HTMLElement | null>(null)
+const AddCourse = () => {
+  inputVisible.value = true;
+  nextTick(() => {
+    (InputRef.value as HTMLInputElement).focus()
+  })
+  
+}
+const cancelAdd = () => {
+  inputVisible.value = false
+  inputValue.value = ''
+}
+const delSubject = (id: number) => {
+  deleteSubject({ ids: [id] }).then((res: any) => { 
+    if (res.code === 200) {
+      ElMessage.success(res.msg)
+      GetAllSubject()
+    } else {
+      ElMessage.error(res.msg)
+    }
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+ .el-dialog  ::deep(.el-dialog__body ) {
+  min-height: 300px !important;
+  padding: 50px!important;
+}
+.subject_list {
+  height: auto;
+  display: flex;
+  justify-content: flex-start;
+  flex-wrap: wrap;
+  gap:20px ;
+  .list_item {
+    border-radius: 4px 4px 4px 4px;
+    border: 1px solid #2E64FA;
+    width: auto;
+    min-width: 72px;
+    height: 36px;
+    line-height: 36px;
+    padding: 0 10px;
+    font-weight: 400;
+    font-size: 14px;
+    color: #2E64FA;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    &.can_delete {
+      background: rgba(46,100,250,0.1);
+    }
+    span {
+      width: 100%;
+      text-align: center;
+      // margin-right: 8px;
+    }
+    img {
+      margin-left: 8px;
+      width: 16px;
+      height: 16px;
+      cursor: pointer;
+    }
+    img:hover {
+      opacity: 0.8;
+    }
+    &.add, 
+    &.cancel {
+      background: #fff;
+      span {
+        margin-right: 0;
+      }
+      img {
+        margin-right: 5px;
+      }
+      cursor: pointer;
+    }
+    &.cancel {
+      border-color: #BBBBBB;
+      color: #BBBBBB;
+    }
+  }
+  :deep(.input_item.el-input) {
+    width: 120px;
+    height: 36px;
+    border-radius: 4px;
+    border: 1px solid #2E64FA!important;
+    .el-input__wrapper {
+      height: 34px;
+      box-shadow: none;
+      padding: 1px 5px;
+    }
+    .el-input__inner {
+      border: none;  /* 去掉边框 */
+      background-color: transparent;  /* 去掉背景色 */
+      box-shadow: none;  /* 去掉阴影 */
+    }
+    .el-input-group__append {
+      padding: 0 10px 0 0;
+      background: #fff;
+      box-shadow: none;
+      cursor: pointer;
+    }
+  }
+}
+</style>

+ 0 - 0
src/views/fillblank/填空题.txt


+ 113 - 0
src/views/layout/components/SiderBar.vue

@@ -0,0 +1,113 @@
+<template>
+  <el-aside class="siderbar_container" :width="isCollapse ? '72px' : '150px'">
+    <div @click="toggleCollapse" class="collapse_box">
+      <img v-if="isCollapse" src="@/assets/icon/open.svg" alt="">
+      <img v-else src="@/assets/icon/close.svg" alt="">
+    </div>
+    <el-menu :collapse="isCollapse" router>
+      <el-menu-item v-for="item in menuItems" :key="item.route" :index="item.route" :class="activeRoute == item.route ? 'is-active' : ''" >
+        <template #default>
+          <i class="iconfont" :class="item.icon"></i>
+          <span v-if="!isCollapse">{{ item.title }}</span>
+        </template>
+        <template #title>
+          <span v-if="isCollapse">{{ item.title }}</span>
+        </template>
+      </el-menu-item>
+    </el-menu>
+  </el-aside>
+</template>
+<script lang="ts">
+export default {
+  name: 'SiderBar'
+}
+</script>
+<script lang="ts" setup> 
+import { ref, watch, onMounted } from 'vue';
+import { useRoute } from 'vue-router'
+import router from '@/router';
+
+const route = useRoute()
+const activeRoute = ref(route.path)
+watch(() => route.path, (newVal) => {
+  activeRoute.value = newVal
+})
+
+const isCollapse = ref(false)
+interface menuItem {
+  label: string;
+  icon: string;
+  route: string;
+}
+const menuItems = ref<menuItem[]>()
+
+onMounted(() => {
+  const menuListStr = localStorage.getItem('menuList')
+  console.log(menuListStr)
+  if (menuListStr) {
+    menuItems.value = JSON.parse(menuListStr)
+    // router.push(menuItems.value[0].route)
+  }
+  console.log(menuItems.value)
+});
+const toggleCollapse = () => {
+  isCollapse.value = !isCollapse.value
+}
+</script>
+
+<style lang="scss" scoped>
+.siderbar_container {
+  background: #fff;
+  padding: 16px;
+}
+.el-menu {
+  width: 100%;
+  border: none;
+}
+.el-menu-item {
+  padding: 0;
+}
+.iconfont {
+  margin-right: 5px;
+  display: inline-block;
+  color: #666666;
+  font-size: 20px;
+}
+.collapse_box {
+  padding-left: 10px;
+  // text-align: center;
+}
+.el-menu.el-menu--vertical :deep(.el-menu-item) {
+  height: 40px;
+  margin: 10px 0;
+  padding: 0 10px;
+  border-radius: 4px;
+  .iconfont {
+    margin-right: 8px;
+    text-align: center;
+  }
+}
+.el-menu--collapse .el-menu-item span {
+  display: none;
+}
+.el-menu-item :deep(.el-menu-tooltip__trigger) {
+  padding: 0 10px;
+}
+.el-menu-item {
+  &.is-active,
+  &.is-active:hover {
+    background: #2E64FA;
+    color: #fff;
+    .iconfont {
+      color: #fff;
+    }
+  }
+  &:not(.is-active):hover {
+    background: inherit;
+    color: #2E64FA;
+    .iconfont {
+      color: #2E64FA;
+    }
+  }
+}
+</style>

+ 385 - 0
src/views/layout/components/header.vue

@@ -0,0 +1,385 @@
+<template>
+  <div class="header_container">
+    <div class="header_content">
+      <div class="header_title">
+        <img alt="" :src="schoolLogo" class="header_icon" v-if="schoolLogo" />
+        <img src="@/assets/school/default_logo.svg" alt="" class="header_icon" v-else />
+        <div class="top_title_blod">{{systemTitle}}</div>
+        <img class="version_icon" src="@/assets/icon/version.webp" alt="">
+      </div>
+      <div class="header_menu">
+        <el-menu :default-active="activeMenuName" class="el-menu-wrap" mode="horizontal" :router="true"
+          @select="menuSelect" background-color="#2E64FA" text-color="#fff" active-text-color="#fff">
+          <el-menu-item :index="item.externalLink" v-for="(item, index) in menuList" :key="index">
+            {{ item.menuName }}
+          </el-menu-item>
+        </el-menu>
+      </div>
+      <div class="header_right">
+        <el-dropdown trigger="click" @command="UserCommand" placement="bottom-end">
+          <span class="userName">
+            {{ userInfo.nickname }}
+            <el-icon class="el-icon--right">
+              <arrow-down />
+            </el-icon>
+          </span>
+          <template #dropdown>
+            <el-dropdown-menu>
+              <el-dropdown-item command="setPassWord">修改密码</el-dropdown-item>
+              <el-dropdown-item command="loginOut">退出登录</el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+      </div>
+    </div>
+    <div class="page_dialog">
+      <el-dialog v-model="dialogShow" title="修改密码" width="450px">
+        <el-form
+          :model="editPassWordData"
+          label-width="80px"
+          :rules="editPassWordDataRules"
+          ref="editPassWordForm"
+        >
+          <el-form-item label="原密码" prop="passWord">
+            <el-input v-model="editPassWordData.passWord" type="password" show-password placeholder="请输入原密码" />
+          </el-form-item>
+          <el-form-item label="新密码" prop="newPassWord">
+            <el-input v-model="editPassWordData.newPassWord" type="password" show-password placeholder="请输入新密码" />
+          </el-form-item>
+          <el-form-item label="确认密码" prop="confirmNewPassWord">
+            <el-input v-model="editPassWordData.confirmNewPassWord" type="password" show-password placeholder="请输入确认新密码" />
+          </el-form-item>
+        </el-form>
+        <template #footer>
+          <el-button class="button_border_gray cancel" @click="dialogShow = false">取 消</el-button>
+          <el-button class="button_background" :loading="submitLoading" @click="confirmPassWord(editPassWordForm)">确 定</el-button>
+        </template>
+      </el-dialog>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+
+import { ArrowDown } from '@element-plus/icons-vue'
+import { ref, reactive, computed } from 'vue'
+import { loginOut, changePassWord } from '@/api/login'
+import { ElMessage } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+// 1. 引入 Pinia store
+import { useUserStore } from '@/store/user'
+
+// 2. 初始化 store
+const userStore = useUserStore()
+
+// 修改 schoolLogo 计算属性,直接从 store 获取
+const schoolLogo = computed(() => userStore.schoolLogo)
+
+const systemTitle=ref('选择题智能判分系统');
+
+const menuList=ref([
+  {
+    menuName: '选择题智能批阅',
+    externalLink: '/main/choice'
+  },
+  {
+    menuName: '填空题AI批阅',
+    externalLink: '/main/fillblank'
+  },
+  {
+    menuName: '作文题A批阅',
+    externalLink: '/main/essay'
+  },
+])
+const activeMenuName=ref('');
+const menuSelect=(menuName: string) => {
+  activeMenuName.value = menuName
+}
+const userInfo = computed(() => {
+  const user = localStorage.getItem('userInfo')
+  if (user) {
+    return JSON.parse(user)
+  }
+  return null
+})
+const dialogShow = ref(false)
+interface EditPassWordData {
+  passWord: string
+  newPassWord: string
+  confirmNewPassWord: string
+}
+const editPassWordData = reactive<EditPassWordData>({
+  passWord: '',
+  newPassWord: '',
+  confirmNewPassWord: ''
+}) // 修改密码弹窗数据
+
+const editPassWordForm = ref<FormInstance>()
+const editPassWordDataRules = reactive<FormRules<EditPassWordData>>({
+  passWord: [
+    { required: true, message: '请输入旧密码', trigger: 'blur' }
+  ],
+  newPassWord: [
+    { required: true, message: '请输入新密码', trigger: 'blur' },
+    { min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' },
+  ],
+  confirmNewPassWord: [
+    { required: true, message: '请输入确认新密码', trigger: 'blur' }
+  ]
+})
+const UserCommand = (command: string) => {
+  console.log(command)
+  if(command == 'setPassWord') {
+    setPassWord()
+  }else if(command == 'loginOut') {
+    loginOutFn()
+  }
+}
+// 打开修改密码弹窗
+const setPassWord = () => {
+  dialogShow.value = true
+}
+// 退出登录
+const loginOutFn = async () => {
+  console.log(userInfo.value.username)
+  const res = await loginOut(userInfo.value.userid)
+  if(res.code == 200) {
+    localStorage.removeItem('token')
+    localStorage.removeItem('userInfo')
+    router.push('/login')
+  }else {
+    ElMessage.error(res.msg)
+  }
+}
+const submitLoading = ref(false)
+// 修改密码提交
+const confirmPassWord = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate(async(valid) => {
+    if (valid) {
+      submitLoading.value = true
+      try {
+        const res = await changePassWord({
+          oldPassword: editPassWordData.passWord,
+          newPassword: editPassWordData.newPassWord,
+          confirmNewPassword: editPassWordData.confirmNewPassWord,
+        })
+        if(res.code == 200) {
+          ElMessage.success(res.msg)
+          dialogShow.value = false
+          // 清除本地存储信息
+          localStorage.removeItem('token')
+          localStorage.removeItem('userInfo')
+          // 跳转登录页面
+          router.push('/login')
+        }else {
+          ElMessage.error(res.msg)
+        }
+      }catch {}finally {
+        submitLoading.value = false
+      }
+    }
+  })
+}
+</script>
+ 
+<style lang="scss" scoped>
+
+
+
+.header_content {
+  // margin: 0 auto;
+  box-sizing: border-box;
+  height: 60px;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 20px;
+
+  .header_title {
+    min-width: 430px;
+    width: auto;
+    display: inline-flex;
+    align-items: center;
+    margin-right: 60px;
+
+    .top_title_blod {
+      font-size: 26px;
+      font-weight: 500;
+      color: #fff;
+      text-align: center;
+      vertical-align: middle;
+      margin: 0 10px;
+    }
+
+    img.header_icon {
+      width: 48px;
+      height: 48px;
+      border-radius: 50%;
+    }
+
+    img.project_title {
+      width: 207px;
+      height: 27px;
+    }
+
+    img.version_icon {
+      width: 75px;
+    }
+  }
+
+  @media screen and (max-width: 1400px) {
+    padding: 0 10px;
+
+    .header_logo {
+      min-width: auto;
+      width: auto;
+      display: inline-flex;
+      align-items: center;
+      margin-right: 10px;
+
+      .top_title_blod {
+        font-size: 16px;
+        font-weight: 500;
+        color: #fff;
+        text-align: center;
+        vertical-align: middle;
+        margin: 0 10px;
+      }
+
+      img.header_icon {
+        width: 48px;
+        height: 48px;
+        border-radius: 50%;
+      }
+
+      img.project_title {
+        width: 207px;
+        height: 27px;
+      }
+
+      img.version_icon {
+        width: 75px;
+      }
+    }
+  }
+
+  .header_menu {
+    flex: 1;
+    :deep(.el-menu-wrap)
+    {
+      height: 60px;
+      border-bottom: none !important;
+
+      //覆盖菜单样式
+      .el-menu-item{
+        font-size: 18px;
+        border-bottom: 0;
+        padding: 0 20px;
+      }
+    }
+
+
+    @media screen and (max-width: 1400px) {
+      .el-menu-item {
+        padding: 0 10px;
+      }
+    }
+  }
+
+  .el-menu-wrap {
+    height: 60px;
+
+    li {
+      font-size: 18px;
+      border-bottom: 0;
+
+      &.report {
+        padding: 0 50px 0 20px;
+      }
+
+      &.is-active {
+        font-weight: bold;
+        background-color: rgba(0, 0, 0, 0.2) !important;
+      }
+    }
+
+    li {
+      position: relative;
+
+      .versions {
+        position: absolute;
+        right: -5px;
+        top: 8px;
+        width: 25px;
+        height: 15px;
+        border-radius: 25px;
+        border: 1px solid #FFFFFF;
+        box-sizing: border-box;
+        display: inline-flex;
+        justify-content: center;
+        align-items: center;
+        z-index: 12;
+
+        span {
+          display: inline-flex;
+          justify-content: center;
+          align-items: center;
+          box-sizing: border-box;
+          line-height: normal;
+          transform: scale(0.9);
+
+          &:first-child {
+            font-size: 10px;
+            margin-right: 2px;
+            line-height: 15px;
+            font-weight: normal;
+          }
+
+          &:nth-child(2) {
+            font-size: 10px;
+          }
+        }
+      }
+    }
+  }
+
+  //屏幕小于1400px
+  @media screen and (max-width: 1400px) {
+    .el-menu-wrap {
+      height: 60px;
+
+      li {
+        font-size: 14px;
+        border-bottom: 0;
+
+        &.is-active {
+          font-weight: bold;
+          background-color: rgba(0, 0, 0, 0.2) !important;
+        }
+      }
+    }
+  }
+
+  //屏幕大于1400px
+  @media screen and (min-width: 1401px) {}
+
+}
+
+.header_right {
+  width: auto;
+  line-height: 30px;
+
+  .user_name {
+    font-size: 15px;
+    color: #fff;
+    cursor: pointer;
+
+    i {
+      margin-left: 10px;
+    }
+  }
+}
+
+
+</style>

+ 22 - 0
src/views/layout/components/main.vue

@@ -0,0 +1,22 @@
+<template>
+  <div class="main_container">
+    <router-view></router-view>
+  </div>
+</template>
+
+<script>
+export default {
+  components: {
+
+  },
+  setup() {
+    return {
+    }
+  },
+}
+</script>
+
+<style scoped>
+
+
+</style>

+ 20 - 0
src/views/layout/index.vue

@@ -0,0 +1,20 @@
+<template>
+  <div class="page_layout">
+    <Header />
+    <Main />
+  </div>
+</template>
+
+<script>
+import Header from './components/header.vue'
+import Main from './components/main.vue'
+export default {
+  components: {
+    Header, Main
+  }
+}
+</script>
+
+<style>
+
+</style>

+ 169 - 0
src/views/login/login.vue

@@ -0,0 +1,169 @@
+<template>
+  <div class="login_page" >
+    <div class="login_header">
+      <div class="header_left">
+        <img src="../../assets/login/login_logo.webp" alt="">
+        <span style="margin-left: 10px;">    大数据精准教学诊断平台</span>
+      </div>
+      <div class="header_right">
+        <div class="right_button" @click="GoWebsite"> 官方网站 </div>
+      </div>
+    </div>
+    <div class="login_content" id="particles">
+      <div class="login_info">
+        <div class="login_info_title">
+          <p>HI~欢迎使用</p>
+          <p>大数据精准教学诊断平台</p>
+        </div>
+        <div class="login_info_message">后台管理系统登录</div>
+        <div class="login_info_input">
+          <el-form ref="userInfoForm" @submit.prevent="SubmitLogin">
+            <el-form-item prop="userName">
+              <el-input v-model="userName" placeholder="请输入账号" clearable @keyup.enter="SubmitLogin">
+                <template #prefix>
+                  <i class="iconfont icon_zhanghao" />
+                </template>
+              </el-input>
+            </el-form-item>
+            <el-form-item prop="passWord">
+              <el-input v-model="passWord" type="password" show-password placeholder="请输入密码" @keyup.enter="SubmitLogin">
+                <template #prefix>
+                  <i class="iconfont icon_mima" />
+                </template>
+              </el-input>
+              <!-- <el-input v-model="passWord" placeholder="请输入密码" :type="!showPass ? 'password' : 'text'">
+                <template #prefix>
+                  <i class="iconfont icon_mima" />
+                </template>
+                <template #suffix>
+                  <span @click.stop="showPass = !showPass">
+                    <i v-if="showPass" class="iconfont icon_xianshimima"></i>
+                    <i v-else class="iconfont icon_buxianshimima"></i>
+                  </span>
+                </template>
+              </el-input> -->
+            </el-form-item>
+          </el-form>
+        </div>
+        <div class="login_info_item">
+          <div class="item_left">
+            <el-checkbox v-model="remeberPassWord">记住密码</el-checkbox>
+          </div>
+          <div class="item_right">忘记密码</div>
+        </div>
+        <div class="login_info_button">
+          <el-button  style="width:100%;" @click="SubmitLogin()" :loading="loadingLogin">登 录</el-button>
+        </div>
+        <div class="login_other_type">
+          <div class="other_login_line">
+            <span class="line"></span>
+            <span class="text">其他登录方式</span>
+            <span class="line"></span>
+          </div>
+          <div class="other_login_icon">
+            <div class="icon_item">
+              <img src="../../assets/login/login_wechat.webp" alt=""></img>
+              <p>微信</p>
+            </div>
+            <div class="icon_item">
+              <img src="../../assets/login/login_feishu.webp" alt=""></img>
+              <p>钉钉</p>
+            </div>
+            <div class="icon_item">
+              <img src="../../assets/login/login_email.webp" alt=""></img>
+              <p>邮箱</p>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    
+  </div>
+</template>
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+
+import { login, getUserInfo } from '@/api/login'
+import { ElMessage } from 'element-plus' // 
+import { useRouter } from 'vue-router'
+
+// 1. 引入封装好的加密方法
+import { encryptPassword } from '@/utils/jsencrypt'
+
+const router = useRouter() // 获取路由实例
+// 响应式数据
+const userName = ref('huijiaoyan')
+const passWord = ref('123456')
+const remeberPassWord = ref(false)
+const loadingLogin = ref(false)
+const showPass = ref(false)
+
+
+onMounted(() => {
+
+  const storedUsername = localStorage.getItem('userName')
+  const storedPassword = localStorage.getItem('passWord')
+  const storedRememberMe = localStorage.getItem('rememberMe')
+
+  if (storedRememberMe === 'true') {
+    // 使用 ?? 运算符,如果获取值为 null 则默认为空字符串,解决类型冲突
+    userName.value = storedUsername ?? ''
+    passWord.value = storedPassword ?? ''
+    remeberPassWord.value = true
+  }
+})
+
+// 清空输入框
+const ClearInput = () => {
+  userName.value = ''
+}
+ const menuList = ref([])
+// 登录提交
+const SubmitLogin = () => {
+  if(!userName.value || !passWord.value) {
+    ElMessage.error('请输入账号和密码');
+    return
+  }
+
+  const params = {
+    username: userName.value.trim(),
+    password: encryptPassword(passWord.value.trim()),
+    deviceType:'PC'
+  }
+  loadingLogin.value = true
+  login(params).then((res) => {
+    console.log("打印登录接口返回res",res)
+       loadingLogin.value = false
+      if (res.code === 200) {
+        console.log("登录成功返回",res)
+        localStorage.setItem('token', res.data.tokenValue)
+        menuList.value = res.data.pcMenuVOS
+        localStorage.setItem('menuList', JSON.stringify(res.data.pcMenuVOS))
+        localStorage.setItem('permissions', JSON.stringify(res.data.permissions))
+        // 登录成功后调用获取用户信息接口
+        return getUserInfo()
+      }
+  }).then((userInfoRes)=>{
+    console.log("获取用户信息返回",userInfoRes)
+    if (userInfoRes && userInfoRes.code === 200) {
+      // 存储用户信息到 localStorage 或 Pinia
+      localStorage.setItem('userInfo', JSON.stringify(userInfoRes.data))
+      ElMessage.success('登录成功');
+      // 跳转到主页
+      router.push('/main/choice')
+    }
+  }).catch((err) => {
+      loadingLogin.value = false
+
+      console.error(err)
+  })
+}
+
+// 去往官网
+const GoWebsite = () => {
+  window.open('https://www.k12100.com/website')
+}
+</script>
+<style lang="scss">
+@use "../../styles/login.scss" as *;
+</style>

+ 8 - 0
src/vite-env.d.ts

@@ -0,0 +1,8 @@
+declare module '*.css' {
+  const content: Record<string, string>
+  export default content
+}
+declare module '*.scss' {
+  const content: Record<string, string>
+  export default content
+}

+ 29 - 0
tsconfig.app.json

@@ -0,0 +1,29 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "composite": true,
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+    /* 类型声明 */
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"]
+    },
+     /* 其他推荐选项 */
+    "noEmit": true,
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "erasableSyntaxOnly": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true
+  },
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.d.ts",
+    "src/**/*.tsx",
+    "src/**/*.vue"
+  ]
+}

+ 7 - 0
tsconfig.json

@@ -0,0 +1,7 @@
+{
+  "files": [],
+  "references": [
+    { "path": "./tsconfig.app.json" },
+    { "path": "./tsconfig.node.json" }
+  ],
+}

+ 27 - 0
tsconfig.node.json

@@ -0,0 +1,27 @@
+{
+  "extends": "@tsconfig/node20/tsconfig.json", // 或 @tsconfig/node18
+  "compilerOptions": {
+    "composite": true,
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+    "target": "ES2022",
+    "lib": ["ES2023"],
+    "module": "ESNext",
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "erasableSyntaxOnly": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 27 - 0
vite.config.ts

@@ -0,0 +1,27 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import path from 'path' // 引入 Node.js 的 path 模块
+// https://vite.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+  resolve: {
+    alias: {
+      '@': path.resolve(__dirname, './src') // 配置 @ 指向 src 目录
+    },
+    extensions: ['.ts', '.js', '.vue', '.json'] // 添加这一行
+  },
+  server: {
+    host: '0.0.0.0',//允许所有ip访问
+    port: 8088,//默认端口
+    proxy:{
+     
+      '/api':{
+        target:'https://dev3.k12100.net/teaching/api/',//测试环境
+        // target:'https://www.k12100.com/teaching/api/',//正式环境
+        changeOrigin:true,
+        rewrite:path => path.replace(/^\/api/, '')
+      }
+    },
+    
+  }//配置代理
+})