Linhanmic 5 månader sedan
incheckning
aabbc1a383
59 ändrade filer med 7141 tillägg och 0 borttagningar
  1. 2 0
      .env
  2. 24 0
      .gitignore
  3. 3 0
      .vscode/extensions.json
  4. 5 0
      README.md
  5. 13 0
      index.html
  6. 2437 0
      package-lock.json
  7. 27 0
      package.json
  8. 0 0
      public/favicon.ico
  9. 1 0
      public/vite.svg
  10. 37 0
      src/App.vue
  11. 44 0
      src/api/Admin.js
  12. 66 0
      src/api/Open.js
  13. 87 0
      src/api/Resources.js
  14. 155 0
      src/api/Teacher.js
  15. 85 0
      src/api/Users.js
  16. 0 0
      src/assets/images
  17. 1 0
      src/assets/vue.svg
  18. 27 0
      src/components/AdminMenu.vue
  19. 81 0
      src/components/CollapsibleSection.vue
  20. 57 0
      src/components/ConfirmDialog.vue
  21. 60 0
      src/components/FileItem.vue
  22. 59 0
      src/components/FileUpload.vue
  23. 28 0
      src/components/Footer.vue
  24. 143 0
      src/components/Header.vue
  25. 50 0
      src/components/Pagination.vue
  26. 84 0
      src/components/SearchBar.vue
  27. 31 0
      src/components/TeacherCard.vue
  28. 93 0
      src/components/UploadModal.vue
  29. 30 0
      src/components/UserMenu.vue
  30. 40 0
      src/composables/useAuth.js
  31. 39 0
      src/composables/usePagination.js
  32. 18 0
      src/composables/useSearch.js
  33. 49 0
      src/layouts/Admin.Layout.vue
  34. 26 0
      src/layouts/DefaultLayout.vue
  35. 12 0
      src/main.js
  36. 115 0
      src/router/index.js
  37. 17 0
      src/store/index.js
  38. 68 0
      src/store/modules/auth.js
  39. 72 0
      src/store/modules/files.js
  40. 67 0
      src/store/modules/teachers.js
  41. 156 0
      src/styles/components.css
  42. 92 0
      src/styles/main.css
  43. 40 0
      src/styles/variables.css
  44. 34 0
      src/utils/request.js
  45. 19 0
      src/utils/validators.js
  46. 192 0
      src/views/AdminAccount.vue
  47. 188 0
      src/views/AdminFileManagement.vue
  48. 180 0
      src/views/ChangePassword.vue
  49. 262 0
      src/views/EditProfile.vue
  50. 206 0
      src/views/ForgotPassword.vue
  51. 77 0
      src/views/Home.vue
  52. 172 0
      src/views/Login.vue
  53. 95 0
      src/views/PaperDetail.vue
  54. 159 0
      src/views/PaperUpload.vue
  55. 255 0
      src/views/Profile.vue
  56. 260 0
      src/views/Register.vue
  57. 170 0
      src/views/SharedLibrary.vue
  58. 305 0
      src/views/TeacherDetail.vue
  59. 26 0
      vite.config.js

+ 2 - 0
.env

@@ -0,0 +1,2 @@
+VITE_APP_API_BASE_URL=http://10.113.233.180:8080
+

+ 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?

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar"]
+}

+ 5 - 0
README.md

@@ -0,0 +1,5 @@
+# Vue 3 + Vite
+
+This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>教师团队管理系统</title>
+  <link rel="icon" href="/favicon.ico">
+</head>
+<body>
+  <div id="app"></div>
+  <script type="module" src="/src/main.js"></script>
+</body>
+</html>

+ 2437 - 0
package-lock.json

@@ -0,0 +1,2437 @@
+{
+  "name": "teacher-management-system",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "teacher-management-system",
+      "version": "1.0.0",
+      "dependencies": {
+        "axios": "^0.27.2",
+        "joi": "^17.13.3",
+        "joi-browser": "^13.4.0",
+        "lucide-vue-next": "^0.445.0",
+        "pinia": "^2.2.2",
+        "vue": "^3.2.37",
+        "vue-router": "^4.4.5",
+        "vuex": "^4.1.0"
+      },
+      "devDependencies": {
+        "@vitejs/plugin-vue": "^3.0.1",
+        "eslint": "^8.20.0",
+        "eslint-plugin-vue": "^9.2.0",
+        "vite": "^3.0.1"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.24.8",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.24.7",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.25.6",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.25.6"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.25.6",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.24.8",
+        "@babel/helper-validator-identifier": "^7.24.7",
+        "to-fast-properties": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
+      "integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz",
+      "integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+      "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eslint-visitor-keys": "^3.3.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+      }
+    },
+    "node_modules/@eslint-community/regexpp": {
+      "version": "4.11.1",
+      "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.11.1.tgz",
+      "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@eslint/eslintrc": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+      "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^6.12.4",
+        "debug": "^4.3.2",
+        "espree": "^9.6.0",
+        "globals": "^13.19.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "minimatch": "^3.1.2",
+        "strip-json-comments": "^3.1.1"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint/js": {
+      "version": "8.57.1",
+      "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.57.1.tgz",
+      "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@hapi/hoek": {
+      "version": "9.3.0",
+      "resolved": "https://registry.npmmirror.com/@hapi/hoek/-/hoek-9.3.0.tgz",
+      "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@hapi/topo": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmmirror.com/@hapi/topo/-/topo-5.1.0.tgz",
+      "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@hapi/hoek": "^9.0.0"
+      }
+    },
+    "node_modules/@humanwhocodes/config-array": {
+      "version": "0.13.0",
+      "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
+      "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==",
+      "deprecated": "Use @eslint/config-array instead",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanwhocodes/object-schema": "^2.0.3",
+        "debug": "^4.3.1",
+        "minimatch": "^3.0.5"
+      },
+      "engines": {
+        "node": ">=10.10.0"
+      }
+    },
+    "node_modules/@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.22"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@humanwhocodes/object-schema": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz",
+      "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
+      "deprecated": "Use @eslint/object-schema instead",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.0",
+      "license": "MIT"
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@sideway/address": {
+      "version": "4.1.5",
+      "resolved": "https://registry.npmmirror.com/@sideway/address/-/address-4.1.5.tgz",
+      "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@hapi/hoek": "^9.0.0"
+      }
+    },
+    "node_modules/@sideway/formula": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/@sideway/formula/-/formula-3.0.1.tgz",
+      "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@sideway/pinpoint": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
+      "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@ungap/structured-clone": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
+      "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/@vitejs/plugin-vue": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-3.2.0.tgz",
+      "integrity": "sha512-E0tnaL4fr+qkdCNxJ+Xd0yM31UwMkQje76fsDVBBUCoGOUPexu2VDUYHL8P4CwV+zMvWw6nlRw19OnRKmYAJpw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^3.0.0",
+        "vue": "^3.2.25"
+      }
+    },
+    "node_modules/@vue/compiler-core": {
+      "version": "3.5.6",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.25.3",
+        "@vue/shared": "3.5.6",
+        "entities": "^4.5.0",
+        "estree-walker": "^2.0.2",
+        "source-map-js": "^1.2.0"
+      }
+    },
+    "node_modules/@vue/compiler-dom": {
+      "version": "3.5.6",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-core": "3.5.6",
+        "@vue/shared": "3.5.6"
+      }
+    },
+    "node_modules/@vue/compiler-sfc": {
+      "version": "3.5.6",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.25.3",
+        "@vue/compiler-core": "3.5.6",
+        "@vue/compiler-dom": "3.5.6",
+        "@vue/compiler-ssr": "3.5.6",
+        "@vue/shared": "3.5.6",
+        "estree-walker": "^2.0.2",
+        "magic-string": "^0.30.11",
+        "postcss": "^8.4.47",
+        "source-map-js": "^1.2.0"
+      }
+    },
+    "node_modules/@vue/compiler-ssr": {
+      "version": "3.5.6",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.6",
+        "@vue/shared": "3.5.6"
+      }
+    },
+    "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/reactivity": {
+      "version": "3.5.6",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/shared": "3.5.6"
+      }
+    },
+    "node_modules/@vue/runtime-core": {
+      "version": "3.5.6",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.6",
+        "@vue/shared": "3.5.6"
+      }
+    },
+    "node_modules/@vue/runtime-dom": {
+      "version": "3.5.6",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/reactivity": "3.5.6",
+        "@vue/runtime-core": "3.5.6",
+        "@vue/shared": "3.5.6",
+        "csstype": "^3.1.3"
+      }
+    },
+    "node_modules/@vue/server-renderer": {
+      "version": "3.5.6",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-ssr": "3.5.6",
+        "@vue/shared": "3.5.6"
+      },
+      "peerDependencies": {
+        "vue": "3.5.6"
+      }
+    },
+    "node_modules/@vue/shared": {
+      "version": "3.5.6",
+      "license": "MIT"
+    },
+    "node_modules/acorn": {
+      "version": "8.12.1",
+      "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.12.1.tgz",
+      "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true,
+      "license": "Python-2.0"
+    },
+    "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": "0.27.2",
+      "resolved": "https://registry.npmmirror.com/axios/-/axios-0.27.2.tgz",
+      "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.14.9",
+        "form-data": "^4.0.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/boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "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/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.3",
+      "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
+      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.1.3",
+      "license": "MIT"
+    },
+    "node_modules/debug": {
+      "version": "4.3.7",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
+      "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "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/doctrine": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz",
+      "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "esutils": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/entities": {
+      "version": "4.5.0",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.15.18.tgz",
+      "integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/android-arm": "0.15.18",
+        "@esbuild/linux-loong64": "0.15.18",
+        "esbuild-android-64": "0.15.18",
+        "esbuild-android-arm64": "0.15.18",
+        "esbuild-darwin-64": "0.15.18",
+        "esbuild-darwin-arm64": "0.15.18",
+        "esbuild-freebsd-64": "0.15.18",
+        "esbuild-freebsd-arm64": "0.15.18",
+        "esbuild-linux-32": "0.15.18",
+        "esbuild-linux-64": "0.15.18",
+        "esbuild-linux-arm": "0.15.18",
+        "esbuild-linux-arm64": "0.15.18",
+        "esbuild-linux-mips64le": "0.15.18",
+        "esbuild-linux-ppc64le": "0.15.18",
+        "esbuild-linux-riscv64": "0.15.18",
+        "esbuild-linux-s390x": "0.15.18",
+        "esbuild-netbsd-64": "0.15.18",
+        "esbuild-openbsd-64": "0.15.18",
+        "esbuild-sunos-64": "0.15.18",
+        "esbuild-windows-32": "0.15.18",
+        "esbuild-windows-64": "0.15.18",
+        "esbuild-windows-arm64": "0.15.18"
+      }
+    },
+    "node_modules/esbuild-android-64": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz",
+      "integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-android-arm64": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz",
+      "integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-darwin-64": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz",
+      "integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-darwin-arm64": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz",
+      "integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-freebsd-64": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz",
+      "integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-freebsd-arm64": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz",
+      "integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-32": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz",
+      "integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-64": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz",
+      "integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-arm": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz",
+      "integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-arm64": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz",
+      "integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-mips64le": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz",
+      "integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-ppc64le": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz",
+      "integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-riscv64": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz",
+      "integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-linux-s390x": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz",
+      "integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-netbsd-64": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz",
+      "integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-openbsd-64": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz",
+      "integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-sunos-64": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz",
+      "integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-windows-32": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz",
+      "integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-windows-64": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz",
+      "integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/esbuild-windows-arm64": {
+      "version": "0.15.18",
+      "resolved": "https://registry.npmmirror.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz",
+      "integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/eslint": {
+      "version": "8.57.1",
+      "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz",
+      "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.6.1",
+        "@eslint/eslintrc": "^2.1.4",
+        "@eslint/js": "8.57.1",
+        "@humanwhocodes/config-array": "^0.13.0",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@nodelib/fs.walk": "^1.2.8",
+        "@ungap/structured-clone": "^1.2.0",
+        "ajv": "^6.12.4",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.2",
+        "debug": "^4.3.2",
+        "doctrine": "^3.0.0",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^7.2.2",
+        "eslint-visitor-keys": "^3.4.3",
+        "espree": "^9.6.1",
+        "esquery": "^1.4.2",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^6.0.1",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "globals": "^13.19.0",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "is-path-inside": "^3.0.3",
+        "js-yaml": "^4.1.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.4.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3",
+        "strip-ansi": "^6.0.1",
+        "text-table": "^0.2.0"
+      },
+      "bin": {
+        "eslint": "bin/eslint.js"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-plugin-vue": {
+      "version": "9.28.0",
+      "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.28.0.tgz",
+      "integrity": "sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.4.0",
+        "globals": "^13.24.0",
+        "natural-compare": "^1.4.0",
+        "nth-check": "^2.1.1",
+        "postcss-selector-parser": "^6.0.15",
+        "semver": "^7.6.3",
+        "vue-eslint-parser": "^9.4.3",
+        "xml-name-validator": "^4.0.0"
+      },
+      "engines": {
+        "node": "^14.17.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "7.2.2",
+      "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz",
+      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/espree": {
+      "version": "9.6.1",
+      "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz",
+      "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "acorn": "^8.9.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^3.4.1"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/esquery": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz",
+      "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "estraverse": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "2.0.2",
+      "license": "MIT"
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fastq": {
+      "version": "1.17.1",
+      "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.17.1.tgz",
+      "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/file-entry-cache": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+      "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flat-cache": "^3.0.4"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/flat-cache": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz",
+      "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.3",
+        "rimraf": "^3.0.2"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.1.tgz",
+      "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.9",
+      "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.9.tgz",
+      "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+      "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.0",
+      "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz",
+      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "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==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/glob": {
+      "version": "7.2.3",
+      "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
+      "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+      "deprecated": "Glob versions prior to v9 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/globals": {
+      "version": "13.24.0",
+      "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz",
+      "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "type-fest": "^0.20.2"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.0.tgz",
+      "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+      "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/is-core-module": {
+      "version": "2.15.1",
+      "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.15.1.tgz",
+      "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "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",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-path-inside": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz",
+      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/joi": {
+      "version": "17.13.3",
+      "resolved": "https://registry.npmmirror.com/joi/-/joi-17.13.3.tgz",
+      "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@hapi/hoek": "^9.3.0",
+        "@hapi/topo": "^5.1.0",
+        "@sideway/address": "^4.1.5",
+        "@sideway/formula": "^3.0.1",
+        "@sideway/pinpoint": "^2.0.0"
+      }
+    },
+    "node_modules/joi-browser": {
+      "version": "13.4.0",
+      "resolved": "https://registry.npmmirror.com/joi-browser/-/joi-browser-13.4.0.tgz",
+      "integrity": "sha512-TfzJd2JaJ/lg/gU+q5j9rLAjnfUNF9DUmXTP9w+GfmG79LjFOXFeM7hIFuXCBcZCivUDFwd9l1btTV9rhHumtQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "node_modules/levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lucide-vue-next": {
+      "version": "0.445.0",
+      "resolved": "https://registry.npmmirror.com/lucide-vue-next/-/lucide-vue-next-0.445.0.tgz",
+      "integrity": "sha512-+JWAiyLliw+hprRsxv+Og/Vba3VoCyPLK1GHyd/1nYrVH0bySlPDdobbv2DxhMfGe+3y3yOOk0aSCligFy3Vkg==",
+      "license": "ISC",
+      "peerDependencies": {
+        "vue": ">=3.0.1"
+      }
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.11",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0"
+      }
+    },
+    "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": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "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==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.7",
+      "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/natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nth-check": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz",
+      "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "boolbase": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/nth-check?sponsor=1"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/optionator": {
+      "version": "0.9.4",
+      "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
+      "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.0",
+      "license": "ISC"
+    },
+    "node_modules/pinia": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.2.2.tgz",
+      "integrity": "sha512-ja2XqFWZC36mupU4z1ZzxeTApV7DOw44cV4dhQ9sGwun+N89v/XP7+j7q6TanS1u1tdbK4r+1BUx7heMaIdagA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.3",
+        "vue-demi": "^0.14.10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.4.0",
+        "typescript": ">=4.4.4",
+        "vue": "^2.6.14 || ^3.3.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        },
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pinia/node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.4.47",
+      "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.7",
+        "picocolors": "^1.1.0",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "6.1.2",
+      "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+      "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/resolve": {
+      "version": "1.22.8",
+      "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz",
+      "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-core-module": "^2.13.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.0.4.tgz",
+      "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rimraf": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz",
+      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+      "deprecated": "Rimraf versions prior to v4 are no longer supported",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "2.79.1",
+      "resolved": "https://registry.npmmirror.com/rollup/-/rollup-2.79.1.tgz",
+      "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.6.3",
+      "resolved": "https://registry.npmmirror.com/semver/-/semver-7.6.3.tgz",
+      "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/text-table": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
+      "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/to-fast-properties": {
+      "version": "2.0.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/type-fest": {
+      "version": "0.20.2",
+      "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz",
+      "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vite": {
+      "version": "3.2.11",
+      "resolved": "https://registry.npmmirror.com/vite/-/vite-3.2.11.tgz",
+      "integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.15.9",
+        "postcss": "^8.4.18",
+        "resolve": "^1.22.1",
+        "rollup": "^2.79.1"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      },
+      "peerDependencies": {
+        "@types/node": ">= 14",
+        "less": "*",
+        "sass": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue": {
+      "version": "3.5.6",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/compiler-dom": "3.5.6",
+        "@vue/compiler-sfc": "3.5.6",
+        "@vue/runtime-dom": "3.5.6",
+        "@vue/server-renderer": "3.5.6",
+        "@vue/shared": "3.5.6"
+      },
+      "peerDependencies": {
+        "typescript": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vue-eslint-parser": {
+      "version": "9.4.3",
+      "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
+      "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.4",
+        "eslint-scope": "^7.1.1",
+        "eslint-visitor-keys": "^3.3.0",
+        "espree": "^9.3.1",
+        "esquery": "^1.4.0",
+        "lodash": "^4.17.21",
+        "semver": "^7.3.6"
+      },
+      "engines": {
+        "node": "^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/mysticatea"
+      },
+      "peerDependencies": {
+        "eslint": ">=6.0.0"
+      }
+    },
+    "node_modules/vue-router": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.4.5.tgz",
+      "integrity": "sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "vue": "^3.2.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"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/word-wrap": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz",
+      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/xml-name-validator": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+      "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    }
+  }
+}

+ 27 - 0
package.json

@@ -0,0 +1,27 @@
+{
+  "name": "teacher-management-system",
+  "version": "1.0.0",
+  "private": true,
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore"
+  },
+  "dependencies": {
+    "axios": "^0.27.2",
+    "joi": "^17.13.3",
+    "joi-browser": "^13.4.0",
+    "lucide-vue-next": "^0.445.0",
+    "pinia": "^2.2.2",
+    "vue": "^3.2.37",
+    "vue-router": "^4.4.5",
+    "vuex": "^4.1.0"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-vue": "^3.0.1",
+    "eslint": "^8.20.0",
+    "eslint-plugin-vue": "^9.2.0",
+    "vite": "^3.0.1"
+  }
+}

+ 0 - 0
public/favicon.ico


+ 1 - 0
public/vite.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

+ 37 - 0
src/App.vue

@@ -0,0 +1,37 @@
+<template>
+  <div id="app">
+    <Header />
+    <main>
+      <router-view></router-view>
+    </main>
+    <Footer />
+  </div>
+</template>
+
+<script setup>
+import { useAuthStore } from './store/modules/auth';
+import Header from './components/Header.vue';
+import Footer from './components/Footer.vue';
+
+const authStore = useAuthStore();
+
+
+</script>
+
+<style>
+#app {
+  font-family: 'Helvetica Neue', Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  display: flex;
+  flex-direction: column;
+  min-height: 100vh;
+  background-color: var(--background-color);
+  color: var(--text-color);
+}
+
+main {
+  flex: 1;
+  padding: var(--spacing-4);
+}
+</style>

+ 44 - 0
src/api/Admin.js

@@ -0,0 +1,44 @@
+import request from '@/utils/request';
+
+class AdminApi {
+  constructor() {
+    this.basePath = '/admin';
+  }
+
+  async deleteAccount(id) {
+    return request({
+      url: `${this.basePath}/${id}`,
+      method: 'DELETE'
+    });
+  }
+
+  async getAccounts(page, pageSize = 12, name) {
+    return request({
+      url: `${this.basePath}/page`,
+      method: 'GET',
+      params: {
+        page,
+        pageSize,
+        ...(name && { name })
+      }
+    });
+  }
+
+  async updateAccountStatus(status, id) {
+    return request({
+      url: `${this.basePath}/status/${status}`,
+      method: 'POST',
+      params: { id }
+    });
+  }
+
+  // 你可以继续添加其他方法...
+}
+
+const adminApi = new AdminApi();
+
+export const deleteAccount = (id) => adminApi.deleteAccount(id);
+export const getAccounts = (page, pageSize, name) => adminApi.getAccounts(page, pageSize, name);
+export const updateAccountStatus = (status, id) => adminApi.updateAccountStatus(status, id);
+
+export default adminApi;

+ 66 - 0
src/api/Open.js

@@ -0,0 +1,66 @@
+import request from '@/utils/request';
+
+class OpenApi {
+  constructor() {
+    this.basePath = '/open';
+  }
+
+  async getAwards(page, pageSize = 12, name) {
+    return request({
+      url: `${this.basePath}/awards/page`,
+      method: 'GET',
+      params: {
+        page,
+        pageSize,
+        ...(name && { name }),
+      },
+    });
+  }
+
+  
+
+  async getHomepageData(page, pageSize = 12, name) {
+    return request({
+      url: `${this.basePath}/page`,
+      method: 'GET',
+      params: {
+        page,
+        pageSize,
+        ...(name && { name }),
+      },
+    });
+  }
+
+  async getThesisPage(page, pageSize = 12, name) {
+    return request({
+      url: `${this.basePath}/thesis/page`,
+      method: 'GET',
+      params: {
+        page,
+        pageSize,
+        ...(name && { name }),
+      },
+    });
+  }
+
+  async getWorksPage(page, pageSize = 12, name) {
+    return request({
+      url: `${this.basePath}/work/page`,
+      method: 'GET',
+      params: {
+        page,
+        pageSize,
+        ...(name && { name }),
+      },
+    });
+  }
+}
+
+const openApi = new OpenApi();
+
+export const getAwards = (page, pageSize, name) => openApi.getAwards(page, pageSize, name);
+export const getHomepageData = (page, pageSize, name) => openApi.getHomepageData(page, pageSize, name);
+export const getThesisPage = (page, pageSize, name) => openApi.getThesisPage(page, pageSize, name);
+export const getWorksPage = (page, pageSize, name) => openApi.getWorksPage(page, pageSize, name);
+
+export default openApi;

+ 87 - 0
src/api/Resources.js

@@ -0,0 +1,87 @@
+import request from '@/utils/request';
+
+class ResourceApi {
+  constructor() {
+    this.basePath = '/resources';
+  }
+
+  async deleteResourceById(id) {
+    try {
+      return await request({
+        url: `${this.basePath}/${id}`,
+        method: 'DELETE'
+      });
+    } catch (error) {
+      this.handleRequestError(error, '删除资源');
+    }
+  }
+
+  async getResourceById(id) {
+    try {
+      return await request({
+        url: `${this.basePath}/${id}`,
+        method: 'GET'
+      });
+    } catch (error) {
+      this.handleRequestError(error, '获取资源');
+    }
+  }
+
+  async updateResourceById(id, data) {
+    try {
+      return await request({
+        url: `${this.basePath}/${id}`,
+        method: 'PUT',
+        data
+      });
+    } catch (error) {
+      this.handleRequestError(error, '更新资源');
+    }
+  }
+
+  async getResourcesPage(page, pageSize = 12, name) {
+    try {
+      return await request({
+        url: `${this.basePath}/page`,
+        method: 'GET',
+        params: { page, pageSize, name }
+      });
+    } catch (error) {
+      this.handleRequestError(error, '获取资源分页');
+    }
+  }
+
+  async uploadResource(data) {
+    try {
+      return await request({
+        url: this.basePath,
+        method: 'POST',
+        data
+      });
+    } catch (error) {
+      this.handleRequestError(error, '上传资源');
+    }
+  }
+
+  handleRequestError(error, action = '请求') {
+    if (error.response) {
+      console.error(`${action}时服务器返回错误:`, error.response.data, `状态码: ${error.response.status}`);
+    } else if (error.request) {
+      console.error(`${action}时未收到服务器响应:`, error.request);
+    } else {
+      console.error(`${action}时发生错误:`, error.message);
+    }
+    throw error;
+  }
+}
+
+const resourceApi = new ResourceApi();
+
+export const deleteResourceById = (id) => resourceApi.deleteResourceById(id);
+export const getResourceById = (id) => resourceApi.getResourceById(id);
+export const updateResourceById = (id, data) => resourceApi.updateResourceById(id, data);
+export const getResourcesPage = (page, pageSize, name) => resourceApi.getResourcesPage(page, pageSize, name);
+export const uploadResource = (data) => resourceApi.uploadResource(data);
+
+export default resourceApi;
+

+ 155 - 0
src/api/Teacher.js

@@ -0,0 +1,155 @@
+import request from '@/utils/request';
+
+class TeacherApi {
+  constructor() {
+    this.basePath = '/teacher';
+  }
+
+  // Awards 相关方法
+  async deleteAwardById(id) {
+    return request({
+      url: `${this.basePath}/awards/${id}`,
+      method: 'DELETE'
+    });
+  }
+
+  async getAwardById(id) {
+    return request({
+      url: `${this.basePath}/awards/${id}`,
+      method: 'GET'
+    });
+  }
+
+  async updateAwardById(id, data) {
+    return request({
+      url: `${this.basePath}/awards/${id}`,
+      method: 'PUT',
+      data
+    });
+  }
+
+  async uploadAward(data) {
+    return request({
+      url: `${this.basePath}/awards`,
+      method: 'POST',
+      data
+    });
+  }
+
+  // Teachers 相关方法
+  async deleteTeacherById(id) {
+    return request({
+      url: `${this.basePath}/${id}`,
+      method: 'DELETE'
+    });
+  }
+
+  async getTeacherById(id) {
+    return request({
+      url: `${this.basePath}/${id}`,
+      method: 'GET'
+    });
+  }
+
+  async updateTeacherById(id, data) {
+    return request({
+      url: `${this.basePath}/${id}`,
+      method: 'PUT',
+      data
+    });
+  }
+
+  async createTeacher(data) {
+    return request({
+      url: this.basePath,
+      method: 'POST',
+      data
+    });
+  }
+
+  // Theses 相关方法
+  async deleteThesisById(id) {
+    return request({
+      url: `${this.basePath}/thesis/${id}`,
+      method: 'DELETE'
+    });
+  }
+
+  async getThesisById(id) {
+    return request({
+      url: `${this.basePath}/thesis/${id}`,
+      method: 'GET'
+    });
+  }
+
+  async updateThesisById(id, data) {
+    return request({
+      url: `${this.basePath}/thesis/${id}`,
+      method: 'PUT',
+      data
+    });
+  }
+
+  async uploadThesis(data) {
+    return request({
+      url: `${this.basePath}/thesis`,
+      method: 'POST',
+      data
+    });
+  }
+
+  // Works 相关方法
+  async deleteWorkById(id) {
+    return request({
+      url: `${this.basePath}/work/${id}`,
+      method: 'DELETE'
+    });
+  }
+
+  async getWorkById(id) {
+    return request({
+      url: `${this.basePath}/work/${id}`,
+      method: 'GET'
+    });
+  }
+
+  async updateWorkById(id, data) {
+    return request({
+      url: `${this.basePath}/work/${id}`,
+      method: 'PUT',
+      data
+    });
+  }
+
+  async uploadWork(data) {
+    return request({
+      url: `${this.basePath}/work`,
+      method: 'POST',
+      data
+    });
+  }
+}
+
+const teacherApi = new TeacherApi();
+
+export const deleteAwardById = (id) => teacherApi.deleteAwardById(id);
+export const getAwardById = (id) => teacherApi.getAwardById(id);
+export const updateAwardById = (id, data) => teacherApi.updateAwardById(id, data);
+export const uploadAward = (data) => teacherApi.uploadAward(data);
+
+export const deleteTeacherById = (id) => teacherApi.deleteTeacherById(id);
+export const getTeacherById = (id) => teacherApi.getTeacherById(id);
+export const updateTeacherById = (id, data) => teacherApi.updateTeacherById(id, data);
+export const createTeacher = (data) => teacherApi.createTeacher(data);
+
+export const deleteThesisById = (id) => teacherApi.deleteThesisById(id);
+export const getThesisById = (id) => teacherApi.getThesisById(id);
+export const updateThesisById = (id, data) => teacherApi.updateThesisById(id, data);
+export const uploadThesis = (data) => teacherApi.uploadThesis(data);
+
+export const deleteWorkById = (id) => teacherApi.deleteWorkById(id);
+export const getWorkById = (id) => teacherApi.getWorkById(id);
+export const updateWorkById = (id, data) => teacherApi.updateWorkById(id, data);
+export const uploadWork = (data) => teacherApi.uploadWork(data);
+
+export default teacherApi;

+ 85 - 0
src/api/Users.js

@@ -0,0 +1,85 @@
+import request from '@/utils/request';
+
+class UserApi {
+  constructor() {
+    this.basePath = '/user';
+  }
+
+  async userLoginPost(data) {
+    return request({
+      url: `${this.basePath}/login`,
+      method: 'POST',
+      data
+    });
+  }
+
+  async userRegisterPost(data) {
+    return request({
+      url: `${this.basePath}/register`,
+      method: 'POST',
+      data
+    });
+  }
+
+  async userLogoutPost(token) {
+    return request({
+      url: `${this.basePath}/logout`,
+      method: 'POST',
+      headers: { token }
+    });
+  }
+
+  async userForgetPost(data) {
+    return request({
+      url: `${this.basePath}/forget`,
+      method: 'POST',
+      data
+    });
+  }
+
+  async userPasswordPut(data) {
+    return request({
+      url: `${this.basePath}/password`,
+      method: 'PUT',
+      data
+    });
+  }
+
+  async userDownloadGet(id, status) {
+    if (typeof id !== 'number' || typeof status !== 'number') {
+      throw new Error('Invalid parameters: id and status must be numbers');
+    }
+    return request({
+      url: `${this.basePath}/download?id=${id}&status=${status}`,
+      method: 'GET'
+    });
+  }
+
+  async userUploadPost(file) {
+    if (!(file instanceof File)) {
+      throw new Error('Invalid parameter: file must be a File object');
+    }
+
+    const formData = new FormData();
+    formData.append('file', file);
+
+    return request({
+      url: `${this.basePath}/upload`,
+      method: 'POST',
+      data: formData
+    });
+  }
+}
+
+const userApi = new UserApi();
+
+export const userLoginPost = (data) => userApi.userLoginPost(data);
+export const userRegisterPost = (data) => userApi.userRegisterPost(data);
+export const userLogoutPost = (token) => userApi.userLogoutPost(token);
+export const userForgetPost = (data) => userApi.userForgetPost(data);
+export const userPasswordPut = (data) => userApi.userPasswordPut(data);
+export const userDownloadGet = (id, status) => userApi.userDownloadGet(id, status);
+export const userUploadPost = (file) => userApi.userUploadPost(file);
+
+export default userApi;
+

+ 0 - 0
src/assets/images


+ 1 - 0
src/assets/vue.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 27 - 0
src/components/AdminMenu.vue

@@ -0,0 +1,27 @@
+<template>
+    <div class="relative" @mouseleave="isOpen = false">
+      <div @mouseenter="isOpen = true" class="flex items-center cursor-pointer">
+        <img src="/admin-avatar.png" alt="Admin" class="w-8 h-8 rounded-full mr-2" />
+        <span class="text-gray-800">管理员</span>
+      </div>
+      <div v-if="isOpen" class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg z-10">
+        <router-link to="/admin/change-password" class="block px-4 py-2 text-gray-800 hover:bg-gray-100">修改密码</router-link>
+        <a @click="logout" class="block px-4 py-2 text-gray-800 hover:bg-gray-100 cursor-pointer">登出</a>
+      </div>
+    </div>
+  </template>
+  
+  <script setup>
+  import { ref } from 'vue';
+  import { useStore } from 'vuex';
+  import { useRouter } from 'vue-router';
+  
+  const store = useStore();
+  const router = useRouter();
+  const isOpen = ref(false);
+  
+  const logout = () => {
+    store.dispatch('auth/logout');
+    router.push('/');
+  };
+  </script>

+ 81 - 0
src/components/CollapsibleSection.vue

@@ -0,0 +1,81 @@
+<template>
+    <div class="border rounded-md mb-4">
+      <button
+        @click="toggleCollapse"
+        class="flex justify-between items-center w-full px-4 py-2 text-left text-gray-700 font-medium bg-gray-100 hover:bg-gray-200 focus:outline-none"
+      >
+        <span>{{ title }}</span>
+        <svg
+          class="w-5 h-5 transform transition-transform duration-200"
+          :class="{ 'rotate-180': !isCollapsed }"
+          fill="none"
+          stroke-linecap="round"
+          stroke-linejoin="round"
+          stroke-width="2"
+          viewBox="0 0 24 24"
+          stroke="currentColor"
+        >
+          <path d="M19 9l-7 7-7-7"></path>
+        </svg>
+      </button>
+      <transition
+        name="collapse"
+        @enter="startTransition"
+        @after-enter="endTransition"
+        @before-leave="startTransition"
+        @after-leave="endTransition"
+      >
+        <div v-show="!isCollapsed" class="overflow-hidden">
+          <div class="p-4">
+            <slot></slot>
+          </div>
+        </div>
+      </transition>
+    </div>
+  </template>
+  
+  <script setup>
+  import { ref } from 'vue';
+  
+  const props = defineProps({
+    title: {
+      type: String,
+      required: true,
+    },
+    initiallyCollapsed: {
+      type: Boolean,
+      default: true,
+    },
+  });
+  
+  const isCollapsed = ref(props.initiallyCollapsed);
+  
+  const toggleCollapse = () => {
+    isCollapsed.value = !isCollapsed.value;
+  };
+  
+  const startTransition = (el) => {
+    el.style.height = 'auto';
+    const height = el.scrollHeight;
+    el.style.height = '0px';
+    el.offsetHeight; // force reflow
+    el.style.height = height + 'px';
+  };
+  
+  const endTransition = (el) => {
+    el.style.height = '';
+  };
+  </script>
+  
+  <style scoped>
+  .collapse-enter-active,
+  .collapse-leave-active {
+    transition: height 0.3s ease-out;
+    overflow: hidden;
+  }
+  
+  .collapse-enter-from,
+  .collapse-leave-to {
+    height: 0;
+  }
+  </style>

+ 57 - 0
src/components/ConfirmDialog.vue

@@ -0,0 +1,57 @@
+<template>
+    <div v-if="isOpen" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
+      <div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
+        <div class="mt-3 text-center">
+          <h3 class="text-lg leading-6 font-medium text-gray-900">{{ title }}</h3>
+          <div class="mt-2 px-7 py-3">
+            <p class="text-sm text-gray-500">
+              {{ message }}
+            </p>
+          </div>
+          <div class="items-center px-4 py-3">
+            <button
+              @click="confirm"
+              class="px-4 py-2 bg-blue-500 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-300"
+            >
+              确认
+            </button>
+            <button
+              @click="cancel"
+              class="mt-3 px-4 py-2 bg-gray-300 text-gray-800 text-base font-medium rounded-md w-full shadow-sm hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-300"
+            >
+              取消
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+</template>
+
+<script setup>
+import { defineProps, defineEmits } from 'vue';
+
+const props = defineProps({
+  isOpen: {
+    type: Boolean,
+    required: true,
+  },
+  title: {
+    type: String,
+    required: true,
+  },
+  message: {
+    type: String,
+    required: true,
+  },
+});
+
+const emit = defineEmits(['confirm', 'cancel']);
+
+const confirm = () => {
+  emit('confirm');
+};
+
+const cancel = () => {
+  emit('cancel');
+};
+</script>

+ 60 - 0
src/components/FileItem.vue

@@ -0,0 +1,60 @@
+<template>
+    <div class="file-item">
+      <div class="file-info">
+        <h3>{{ file.name }}</h3>
+        <p>类型: {{ file.type }}</p>
+        <p>大小: {{ (file.size / 1024 / 1024).toFixed(2) }} MB</p>
+        <p v-if="file.visibility === 'public'">公开文件</p>
+        <p v-else>私密文件</p>
+      </div>
+      <div class="file-actions">
+        <button @click="downloadFile(file.id, file.status)">下载</button>
+      </div>
+    </div>
+  </template>
+  
+  <script>
+  export default {
+    name: 'FileItem',
+    props: {
+      file: {
+        type: Object,
+        required: true
+      }
+    },
+    methods: {
+      downloadFile(id, status) {
+        this.$emit('download', id, status);
+      }
+    }
+  };
+  </script>
+  
+  <style scoped>
+  .file-item {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 1rem;
+    border: 1px solid #eee;
+    margin-bottom: 1rem;
+    border-radius: 8px;
+  }
+  
+  .file-info h3 {
+    margin-bottom: 0.5rem;
+  }
+  
+  .file-actions button {
+    padding: 0.5rem 1rem;
+    background-color: #007bff;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+  }
+  
+  .file-actions button:hover {
+    background-color: #0056b3;
+  }
+  </style>

+ 59 - 0
src/components/FileUpload.vue

@@ -0,0 +1,59 @@
+<template>
+    <div class="mt-4">
+      <label class="block text-sm font-medium text-gray-700">
+        {{ label }}
+      </label>
+      <div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
+        <div class="space-y-1 text-center">
+          <svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
+            <path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
+          </svg>
+          <div class="flex text-sm text-gray-600">
+            <label for="file-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
+              <span>上传文件</span>
+              <input id="file-upload" name="file-upload" type="file" class="sr-only" @change="handleFileUpload" :accept="acceptedFileTypes">
+            </label>
+            <p class="pl-1">或拖放文件</p>
+          </div>
+          <p class="text-xs text-gray-500">
+            {{ fileTypesMessage }}
+          </p>
+        </div>
+      </div>
+      <p v-if="fileName" class="mt-2 text-sm text-gray-500">
+        已选择文件: {{ fileName }}
+      </p>
+    </div>
+  </template>
+  
+  <script setup>
+  import { ref, defineProps, defineEmits } from 'vue';
+  
+  const props = defineProps({
+    label: {
+      type: String,
+      default: '文件上传',
+    },
+    acceptedFileTypes: {
+      type: String,
+      default: '*',
+    },
+    fileTypesMessage: {
+      type: String,
+      default: '支持的文件类型: PNG, JPG, PDF, DOC, DOCX 最大 10MB',
+    },
+  });
+  
+  const emit = defineEmits(['file-selected']);
+  
+  const fileName = ref('');
+  
+  const handleFileUpload = (event) => {
+    const file = event.target.files[0];
+    if (file) {
+      fileName.value = file.name;
+      emit('file-selected', file);
+    }
+  };
+  </script>
+  

+ 28 - 0
src/components/Footer.vue

@@ -0,0 +1,28 @@
+
+<template>
+  <footer class="bg-morandi-gray text-text-color py-6">
+    <div class="container mx-auto px-4">
+      <div class="text-center">
+        <p>&copy; 2024 教师团队系统. 保留所有权利.</p>
+      </div>
+    </div>
+  </footer>
+</template>
+
+<script setup>
+// No changes needed in the script
+</script>
+
+<style scoped>
+footer {
+  position: relative;
+  width: 100%;
+  background-color: var(--morandi-gray);
+  color: var(--text-color);
+}
+
+.container {
+  max-width: 1200px;
+  margin: 0 auto;
+}
+</style>

+ 143 - 0
src/components/Header.vue

@@ -0,0 +1,143 @@
+<template>
+  <header class="header">
+    <nav class="container header-nav">
+      <div class="logo">
+        <span class="logo-text">教师团队管理系统</span>
+      </div>
+      <div class="menu">
+        <router-link to="/" class="menu-item">首页</router-link>
+        <router-link to="/shared-library" class="menu-item">共享资料库</router-link>
+        <template v-if="isAdmin">
+          <router-link to="/admin/accounts" class="menu-item">账号管理</router-link>
+          <router-link to="/admin/files" class="menu-item">资料库管理</router-link>
+        </template>
+      </div>
+      <div class="auth">
+        <template v-if="!isLoggedIn">
+          <router-link to="/login" class="auth-link">登录</router-link>
+          <router-link to="/register" class="auth-link">注册</router-link>
+        </template>
+        <template v-else>
+          <UserMenu v-if="!isAdmin" />
+          <AdminMenu v-else />
+        </template>
+      </div>
+    </nav>
+  </header>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+import UserMenu from './UserMenu.vue';
+import AdminMenu from './AdminMenu.vue';
+import { useAuthStore } from '@/store/modules/auth';
+
+const store = useAuthStore();
+const isLoggedIn = computed(() => store.isAuthenticated);
+const isAdmin = computed(() => store.isAdmin);
+</script>
+
+<style scoped>
+.header {
+  background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
+  box-shadow: var(--shadow-md);
+  padding: 1rem 0;
+  position: relative;
+  overflow: hidden;
+}
+
+.header::before {
+  content: '';
+  position: absolute;
+  top: -50%;
+  left: -50%;
+  width: 200%;
+  height: 200%;
+  background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 70%);
+  animation: shimmer 15s infinite linear;
+  pointer-events: none;
+}
+
+@keyframes shimmer {
+  0% { transform: rotate(0deg); }
+  100% { transform: rotate(360deg); }
+}
+
+.header-nav {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 0 1.5rem;
+  position: relative;
+  z-index: 1;
+}
+
+.logo-text {
+  font-size: 1.5rem;
+  color: var(--light-text-color);
+  font-weight: bold;
+  letter-spacing: 1px;
+  text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.menu {
+  display: flex;
+  gap: 1.5rem;
+}
+
+.menu-item, .auth-link {
+  color: var(--light-text-color);
+  text-decoration: none;
+  padding: 0.5rem 0.75rem;
+  border-radius: var(--border-radius-base);
+  transition: all var(--transition-base);
+  position: relative;
+  overflow: hidden;
+}
+
+.menu-item::after, .auth-link::after {
+  content: '';
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  height: 2px;
+  background-color: var(--light-text-color);
+  transform: scaleX(0);
+  transition: transform var(--transition-base);
+}
+
+.menu-item:hover::after, .auth-link:hover::after {
+  transform: scaleX(1);
+}
+
+.menu-item:hover, .auth-link:hover {
+  background-color: rgba(255, 255, 255, 0.1);
+}
+
+.auth {
+  display: flex;
+  gap: 1rem;
+}
+
+@media (max-width: 768px) {
+  .header-nav {
+    flex-direction: column;
+    align-items: flex-start;
+  }
+
+  .menu, .auth {
+    margin-top: 1rem;
+  }
+
+  .menu {
+    flex-wrap: wrap;
+  }
+}
+</style>
+
+
+
+

+ 50 - 0
src/components/Pagination.vue

@@ -0,0 +1,50 @@
+<template>
+  <div class="flex justify-center items-center space-x-4 bg-white shadow-md py-4 px-6 rounded-lg">
+    <button 
+      @click="onPageChange(currentPage - 1)" 
+      :disabled="currentPage === 1"
+      class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary-dark disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
+    >
+      上一页
+    </button>
+    <span class="px-4 py-2 bg-gray-100 text-primary font-semibold rounded-md">
+      {{ currentPage }} / {{ totalPages }}
+    </span>
+    <button 
+      @click="onPageChange(currentPage + 1)" 
+      :disabled="currentPage === totalPages"
+      class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary-dark disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-300 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
+    >
+      下一页
+    </button>
+  </div>
+</template>
+
+<script setup>
+import { defineProps, defineEmits, computed } from 'vue';
+
+const props = defineProps({
+  currentPage: {
+    type: Number,
+    required: true
+  },
+  totalItems: {
+    type: Number,
+    required: true
+  },
+  itemsPerPage: {
+    type: Number,
+    required: true
+  }
+});
+
+const emit = defineEmits(['page-change']);
+
+const totalPages = computed(() => Math.ceil(props.totalItems / props.itemsPerPage));
+
+const onPageChange = (page) => {
+  if (page >= 1 && page <= totalPages.value) {
+    emit('page-change', page);
+  }
+};
+</script>

+ 84 - 0
src/components/SearchBar.vue

@@ -0,0 +1,84 @@
+<script setup>
+import { ref } from 'vue'
+import { Search } from 'lucide-vue-next'
+
+const searchQuery = ref('')
+const emit = defineEmits(['search'])
+
+const handleSearch = () => {
+  emit('search', searchQuery.value)
+}
+</script>
+
+<template>
+  <div class="search-container">
+    <input
+      type="text"
+      v-model="searchQuery"
+      @keyup.enter="handleSearch"
+      placeholder="搜索教师"
+      class="search-input"
+    />
+    <button @click="handleSearch" class="search-button" aria-label="搜索">
+      <Search class="search-icon" />
+    </button>
+  </div>
+</template>
+
+<style scoped>
+.search-container {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  max-width: 600px;
+  height: 36px;
+  margin: 0 auto;
+  background-color: #ffffff;
+  border-radius: 18px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.search-input {
+  flex-grow: 1;
+  height: 100%;
+  padding: 0 16px;
+  font-size: 16px;
+  color: #333;
+  border: none;
+  background: transparent;
+  border-radius: 18px 0 0 18px;
+  cursor: text;
+}
+
+.search-input::placeholder {
+  color: #999;
+  opacity: 1;
+}
+
+.search-input:focus {
+  outline: none;
+}
+
+.search-button {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 36px;
+  height: 36px;
+  background-color: #4285f4;
+  border: none;
+  border-radius: 0 18px 18px 0;
+  cursor: pointer;
+  transition: background-color 0.2s ease;
+}
+
+.search-button:hover {
+  background-color: #3367d6;
+}
+
+.search-icon {
+  width: 20px;
+  height: 20px;
+  color: white;
+}
+</style>

+ 31 - 0
src/components/TeacherCard.vue

@@ -0,0 +1,31 @@
+<template>
+  <div class="bg-white shadow-lg rounded-lg overflow-hidden transition-all duration-300 hover:shadow-xl transform hover:-translate-y-2">
+    <div class="relative pb-2/3">
+      <img :src="teacher.profileImage" :alt="teacher.name" class="absolute h-full w-full object-cover" />
+      <div class="absolute inset-0 bg-gradient-to-t from-black to-transparent opacity-60"></div>
+      <div class="absolute bottom-0 left-0 p-4 text-white">
+        <h3 class="text-xl font-semibold mb-1">{{ teacher.name }}</h3>
+        <p class="text-sm opacity-80">{{ teacher.researchField }}</p>
+      </div>
+    </div>
+    <div class="p-4">
+      <router-link 
+        :to="{ name: 'TeacherDetail', params: { id: teacher.id } }" 
+        class="block w-full text-center bg-primary text-white px-4 py-2 rounded-md hover:bg-primary-dark transition-colors duration-300"
+      >
+        查看详情
+      </router-link>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { defineProps } from 'vue';
+
+defineProps({
+  teacher: {
+    type: Object,
+    required: true,
+  },
+});
+</script>

+ 93 - 0
src/components/UploadModal.vue

@@ -0,0 +1,93 @@
+<template>
+    <div v-if="show" class="modal">
+      <div class="modal-content">
+        <h2>上传文件</h2>
+        <input type="file" @change="handleFileSelect" />
+        <div v-if="selectedFile">
+          <p>已选择文件: {{ selectedFile.name }}</p>
+          <p>文件大小: {{ (selectedFile.size / 1024 / 1024).toFixed(2) }} MB</p>
+        </div>
+        <button @click="uploadFile" :disabled="!selectedFile">上传</button>
+        <button @click="closeModal">取消</button>
+      </div>
+    </div>
+  </template>
+  
+  <script>
+  export default {
+    name: 'UploadModal',
+    props: {
+      show: {
+        type: Boolean,
+        required: true
+      }
+    },
+    data() {
+      return {
+        selectedFile: null
+      };
+    },
+    methods: {
+      handleFileSelect(event) {
+        this.selectedFile = event.target.files[0];
+      },
+      async uploadFile() {
+        if (this.selectedFile) {
+          const formData = new FormData();
+          formData.append('file', this.selectedFile);
+  
+          this.$emit('upload', formData);
+          this.selectedFile = null;
+        }
+      },
+      closeModal() {
+        this.$emit('close');
+      }
+    }
+  };
+  </script>
+  
+  <style scoped>
+  .modal {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100vw;
+    height: 100vh;
+    background-color: rgba(0, 0, 0, 0.5);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+  }
+  
+  .modal-content {
+    background-color: white;
+    padding: 2rem;
+    border-radius: 8px;
+    width: 400px;
+    text-align: center;
+  }
+  
+  .modal-content h2 {
+    margin-bottom: 1rem;
+  }
+  
+  .modal-content button {
+    margin-top: 1rem;
+    padding: 0.5rem 1rem;
+    background-color: #007bff;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+  }
+  
+  .modal-content button:hover {
+    background-color: #0056b3;
+  }
+  
+  .modal-content button:disabled {
+    background-color: #ccc;
+    cursor: not-allowed;
+  }
+  </style>

+ 30 - 0
src/components/UserMenu.vue

@@ -0,0 +1,30 @@
+<template>
+    <div class="relative" @mouseleave="isOpen = false">
+      <div @mouseenter="isOpen = true" class="flex items-center cursor-pointer">
+        <img :src="user.profileImage" :alt="user.name" class="w-8 h-8 rounded-full mr-2" />
+        <span class="text-gray-800">{{ user.name }}</span>
+      </div>
+      <div v-if="isOpen" class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg z-10">
+        <router-link to="/profile" class="block px-4 py-2 text-gray-800 hover:bg-gray-100">个人资料</router-link>
+        <router-link to="/change-password" class="block px-4 py-2 text-gray-800 hover:bg-gray-100">修改密码</router-link>
+        <a @click="logout" class="block px-4 py-2 text-gray-800 hover:bg-gray-100 cursor-pointer">登出</a>
+      </div>
+    </div>
+  </template>
+  
+  <script setup>
+  import { ref } from 'vue';
+  import { useStore } from 'vuex';
+  import { useRouter } from 'vue-router';
+  
+  const store = useStore();
+  const router = useRouter();
+  const isOpen = ref(false);
+  
+  const user = store.getters['auth/user'];
+  
+  const logout = () => {
+    store.dispatch('auth/logout');
+    router.push('/');
+  };
+  </script>

+ 40 - 0
src/composables/useAuth.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { Resources } from '../../api'
+
+export const useFileStore = defineStore('files', {
+  state: () => ({
+    sharedFiles: [],
+    totalPages: 1,
+    currentPage: 1,
+  }),
+  actions: {
+    async fetchSharedFiles(page = 1, search = '') {
+      try {
+        const response = await Resources.getSharedResources({ page, search })
+        this.sharedFiles = response.data.resources
+        this.totalPages = response.data.totalPages
+        this.currentPage = page
+      } catch (error) {
+        console.error('Failed to fetch shared files:', error)
+      }
+    },
+    async uploadFile(fileData) {
+      try {
+        await Resources.uploadResource(fileData)
+        // Refresh the file list after upload
+        await this.fetchSharedFiles(this.currentPage)
+      } catch (error) {
+        console.error('Failed to upload file:', error)
+      }
+    },
+    async deleteFile(id) {
+      try {
+        await Resources.deleteResource(id)
+        // Refresh the file list after deletion
+        await this.fetchSharedFiles(this.currentPage)
+      } catch (error) {
+        console.error('Failed to delete file:', error)
+      }
+    },
+  },
+})

+ 39 - 0
src/composables/usePagination.js

@@ -0,0 +1,39 @@
+import { ref, computed } from 'vue'
+
+export function usePagination(fetchFunction, itemsPerPage = 10) {
+  const currentPage = ref(1)
+  const totalPages = ref(1)
+
+  const setPage = (page) => {
+    currentPage.value = page
+    fetchFunction(page)
+  }
+
+  const nextPage = () => {
+    if (currentPage.value < totalPages.value) {
+      setPage(currentPage.value + 1)
+    }
+  }
+
+  const prevPage = () => {
+    if (currentPage.value > 1) {
+      setPage(currentPage.value - 1)
+    }
+  }
+
+  const paginationInfo = computed(() => ({
+    currentPage: currentPage.value,
+    totalPages: totalPages.value,
+    hasPrev: currentPage.value > 1,
+    hasNext: currentPage.value < totalPages.value,
+  }))
+
+  return {
+    currentPage,
+    totalPages,
+    setPage,
+    nextPage,
+    prevPage,
+    paginationInfo,
+  }
+}

+ 18 - 0
src/composables/useSearch.js

@@ -0,0 +1,18 @@
+import { ref } from 'vue'
+
+export function useSearch(searchFunction, delay = 300) {
+  const searchQuery = ref('')
+  let timeoutId = null
+
+  const handleSearch = () => {
+    clearTimeout(timeoutId)
+    timeoutId = setTimeout(() => {
+      searchFunction(searchQuery.value)
+    }, delay)
+  }
+
+  return {
+    searchQuery,
+    handleSearch,
+  }
+}

+ 49 - 0
src/layouts/Admin.Layout.vue

@@ -0,0 +1,49 @@
+<template>
+  <div class="admin-layout">
+    <header>
+      <AdminHeader />
+    </header>
+    <div class="admin-content">
+      <aside>
+        <AdminSidebar />
+      </aside>
+      <main>
+        <slot></slot>
+      </main>
+    </div>
+    <footer>
+      <Footer />
+    </footer>
+  </div>
+</template>
+
+<script>
+import AdminHeader from '@/components/AdminHeader.vue'
+import AdminSidebar from '@/components/AdminSidebar.vue'
+import Footer from '@/components/Footer.vue'
+
+export default {
+  components: {
+    AdminHeader,
+    AdminSidebar,
+    Footer
+  }
+}
+</script>
+
+<style scoped>
+.admin-content {
+  display: flex;
+}
+
+aside {
+  width: 250px;
+  background-color: var(--background-color);
+  padding: var(--spacing-medium);
+}
+
+main {
+  flex-grow: 1;
+  padding: var(--spacing-medium);
+}
+</style>

+ 26 - 0
src/layouts/DefaultLayout.vue

@@ -0,0 +1,26 @@
+<template>
+  <div class="default-layout">
+    <header>
+      <Header />
+    </header>
+    <main>
+      <slot></slot>
+    </main>
+    <footer>
+      <Footer />
+    </footer>
+  </div>
+</template>
+
+<script>
+import Header from '@/components/Header.vue'
+import Footer from '@/components/Footer.vue'
+
+export default {
+  components: {
+    Header,
+    Footer
+  }
+}
+</script>
+

+ 12 - 0
src/main.js

@@ -0,0 +1,12 @@
+import { createApp } from 'vue';
+import { createPinia } from 'pinia';
+import App from './App.vue';
+import router from './router'; // 直接从 ./router 导入已经配置好的 router
+import './styles/main.css';
+
+const app = createApp(App);
+const pinia = createPinia();
+
+app.use(pinia);
+app.use(router); // 直接使用从 ./router 导入的 router
+app.mount('#app');

+ 115 - 0
src/router/index.js

@@ -0,0 +1,115 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import Home from '../views/Home.vue'
+import Login from '../views/Login.vue'
+import Register from '../views/Register.vue'
+import ForgotPassword from '../views/ForgotPassword.vue'
+import Profile from '../views/Profile.vue'
+import EditProfile from '../views/EditProfile.vue'
+import ChangePassword from '../views/ChangePassword.vue'
+import TeacherDetail from '../views/TeacherDetail.vue'
+import PaperDetail from '../views/PaperDetail.vue'
+import PaperUpload from '../views/PaperUpload.vue'
+import SharedLibrary from '../views/SharedLibrary.vue'
+import AdminAccount from '../views/AdminAccount.vue'
+import AdminFileManagement from '../views/AdminFileManagement.vue'
+
+const routes = [
+  {
+    path: '/',
+    name: 'Home',
+    component: Home
+  },
+  {
+    path: '/login',
+    name: 'Login',
+    component: Login
+  },
+  {
+    path: '/register',
+    name: 'Register',
+    component: Register
+  },
+  {
+    path: '/forgot-password',
+    name: 'ForgotPassword',
+    component: ForgotPassword
+  },
+  {
+    path: '/shared-library',
+    name: 'SharedLibrary',
+    component: SharedLibrary
+  },
+  {
+    path: '/teacher/:id',
+    name: 'TeacherDetail',
+    component: TeacherDetail,
+    props: true
+  },
+  {
+    path: '/paper/:id',
+    name: 'PaperDetail',
+    component: PaperDetail,
+    props: true
+  },
+  {
+    path: '/profile',
+    name: 'Profile',
+    component: Profile,
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/editprofile',
+    name: 'Editprofile',
+    component: EditProfile,
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/change-password',
+    name: 'ChangePassword',
+    component: ChangePassword,
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/paper-upload',
+    name: 'PaperUpload',
+    component: PaperUpload,
+    meta: { requiresAuth: true }
+  },
+  {
+    path: '/admin/accounts',
+    name: 'AdminAccount',
+    component: AdminAccount,
+    meta: { requiresAuth: true, requiresAdmin: true }
+  },
+  {
+    path: '/admin/files',
+    name: 'AdminFileManagement',
+    component: AdminFileManagement,
+    meta: { requiresAuth: true, requiresAdmin: true }
+  }
+]
+
+const router = createRouter({
+  history: createWebHistory(),
+  routes
+})
+
+// Navigation guard
+router.beforeEach((to, from, next) => {
+  const isAuthenticated = false // This should be replaced with actual auth check
+  const isAdmin = false // This should be replaced with actual admin check
+
+  if (to.matched.some(record => record.meta.requiresAuth)) {
+    if (!isAuthenticated) {
+      next({ name: 'Login', query: { redirect: to.fullPath } })
+    } else if (to.matched.some(record => record.meta.requiresAdmin) && !isAdmin) {
+      next({ name: 'Home' }) // Redirect non-admin users
+    } else {
+      next()
+    }
+  } else {
+    next()
+  }
+})
+
+export default router

+ 17 - 0
src/store/index.js

@@ -0,0 +1,17 @@
+// store/index.js
+
+import { createPinia } from 'pinia';
+
+// 导入各个模块的 store
+import { useAuthStore } from './modules/auth';
+import { useTeachersStore } from './modules/teachers';
+import { useFilesStore } from './modules/files';
+
+// 创建 pinia 实例
+const pinia = createPinia();
+
+// 导出 pinia 实例
+export default pinia;
+
+// 导出各个模块的 store 以便在组件中使用
+export { useAuthStore, useTeachersStore, useFilesStore };

+ 68 - 0
src/store/modules/auth.js

@@ -0,0 +1,68 @@
+import { defineStore } from 'pinia';
+import { userLoginPost, userLogoutPost } from '../../api/Users';
+import { userRegisterPost, userForgetPost, userPasswordPut } from '../../api/Users';
+
+export const useAuthStore = defineStore('auth', {
+  state: () => ({
+    user: null,
+    isAuthenticated: false,
+    isAdmin: false,
+  }),
+  actions: {
+    async login(username, password) {
+      try {
+        const response = await userLoginPost({ username, password });
+        this.user = response.data;
+        this.isAuthenticated = true;
+        this.isAdmin = this.user.role === 'admin';
+        return true;
+      } catch (error) {
+        this.handleError('登录失败', error);
+        return false;
+      }
+    },
+    async logout() {
+      try {
+        await userLogoutPost();
+        this.resetAuthState();
+      } catch (error) {
+        this.handleError('登出失败', error);
+      }
+    },
+    async register(userData) {
+      try {
+        await userRegisterPost(userData);
+        return true;
+      } catch (error) {
+        this.handleError('注册失败', error);
+        return false;
+      }
+    },
+    async forgotPassword(userData) {
+      try {
+        await userForgetPost(userData);
+        return true;
+      } catch (error) {
+        this.handleError('密码重置请求失败', error);
+        return false;
+      }
+    },
+    async changePassword(oldPassword, newPassword) {
+      try {
+        await userPasswordPut({ oldPassword, newPassword });
+        return true;
+      } catch (error) {
+        this.handleError('密码更改失败', error);
+        return false;
+      }
+    },
+    resetAuthState() {
+      this.user = null;
+      this.isAuthenticated = false;
+      this.isAdmin = false;
+    },
+    handleError(message, error) {
+      console.error(message, error);
+    },
+  },
+});

+ 72 - 0
src/store/modules/files.js

@@ -0,0 +1,72 @@
+import { defineStore } from 'pinia'
+import { getResourcesPage, uploadResource, deleteResourceById } from '../../api/Resources'
+
+export const useFilesStore = defineStore('files', {
+  state: () => ({
+    sharedFiles: [],
+    totalPages: 1,
+    currentPage: 1,
+    isLoading: false,
+    error: null,
+  }),
+  getters: {
+    getFileById: (state) => (id) => {
+      return state.sharedFiles.find(file => file.id === id)
+    },
+    isLastPage: (state) => state.currentPage === state.totalPages,
+  },
+  actions: {
+    async fetchSharedFiles(page = 1, name = '') {
+      this.isLoading = true
+      this.error = null
+      try {
+        const response = await getResourcesPage(page, 12, name) // Assuming pageSize is 12
+        this.sharedFiles = response.data.resources
+        this.totalPages = response.data.totalPages
+        this.currentPage = page
+      } catch (error) {
+        console.error('Failed to fetch shared files:', error)
+        this.error = 'Failed to fetch shared files. Please try again.'
+      } finally {
+        this.isLoading = false
+      }
+    },
+    async uploadFile(fileData) {
+      this.isLoading = true
+      this.error = null
+      try {
+        const response = await uploadResource(fileData)
+        // Add the new file to the list if it's returned by the API
+        if (response) {
+          this.sharedFiles.unshift(response)
+        } else {
+          // If the API doesn't return the new file, refresh the whole list
+          await this.fetchSharedFiles(1)
+        }
+      } catch (error) {
+        console.error('Failed to upload file:', error)
+        this.error = 'Failed to upload file. Please try again.'
+      } finally {
+        this.isLoading = false
+      }
+    },
+    async deleteFile(id) {
+      this.isLoading = true
+      this.error = null
+      try {
+        await deleteResourceById(id)
+        // Remove the file from the list
+        this.sharedFiles = this.sharedFiles.filter(file => file.id !== id)
+        // If the current page is empty and it's not the first page, fetch the previous page
+        if (this.sharedFiles.length === 0 && this.currentPage > 1) {
+          await this.fetchSharedFiles(this.currentPage - 1)
+        }
+      } catch (error) {
+        console.error('Failed to delete file:', error)
+        this.error = 'Failed to delete file. Please try again.'
+      } finally {
+        this.isLoading = false
+      }
+    },
+  },
+})

+ 67 - 0
src/store/modules/teachers.js

@@ -0,0 +1,67 @@
+import { defineStore } from 'pinia'
+import * as OpenApi from '../../api/Open'
+import * as TeacherApi from '../../api/Teacher'
+
+export const useTeachersStore = defineStore('teachers', {
+  state: () => ({
+    teachers: [],
+    currentTeacher: null,
+    totalPages: 1,
+    currentPage: 1,
+    isLoading: false,
+    error: null,
+  }),
+  getters: {
+    isLastPage: (state) => state.currentPage === state.totalPages,
+  },
+  actions: {
+    async fetchTeachers(page = 1, pageSize = 12, name = '') {
+      this.isLoading = true
+      this.error = null
+      try {
+        const response = await OpenApi.default.getHomepageData(page, pageSize = 12, name)
+        this.teachers = response.data.records
+        this.totalPages = response.data.pages
+        this.currentPage = page
+      } catch (error) {
+        console.error('获取教师列表失败:', error)
+        this.error = '获取教师列表失败。请重试。'
+      } finally {
+        this.isLoading = false
+      }
+    },
+    async fetchTeacherDetails(id) {
+      this.isLoading = true
+      this.error = null
+      try {
+        const response = await TeacherApi.getTeacherDetails(id)
+        this.currentTeacher = response
+      } catch (error) {
+        console.error('获取教师详情失败:', error)
+        this.error = '获取教师详情失败。请重试。'
+      } finally {
+        this.isLoading = false
+      }
+    },
+    async updateTeacherProfile(id, profileData) {
+      this.isLoading = true
+      this.error = null
+      try {
+        const updatedTeacher = await TeacherApi.updateProfile(id, profileData)
+        if (this.currentTeacher && this.currentTeacher.id === id) {
+          this.currentTeacher = updatedTeacher
+        }
+        const index = this.teachers.findIndex(t => t.id === id)
+        if (index !== -1) {
+          this.teachers[index] = updatedTeacher
+        }
+      } catch (error) {
+        console.error('Failed to update teacher profile:', error)
+        this.error = 'Failed to update teacher profile. Please try again.'
+      } finally {
+        this.isLoading = false
+      }
+    },
+    
+  },
+})

+ 156 - 0
src/styles/components.css

@@ -0,0 +1,156 @@
+@import 'variables.css';
+
+/* Header */
+.header {
+  background-color: var(--light-text-color);
+  box-shadow: var(--shadow-base);
+  padding: var(--spacing-4) 0;
+}
+
+.header-nav {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.header-nav a {
+  color: var(--text-color);
+  margin-right: var(--spacing-4);
+  transition: color var(--transition-base);
+}
+
+.header-nav a:hover {
+  color: var(--primary-color);
+}
+
+/* Footer */
+.footer {
+  background-color: var(--text-color);
+  color: var(--light-text-color);
+  padding: var(--spacing-8) 0;
+  margin-top: var(--spacing-8);
+}
+
+/* Pagination */
+.pagination {
+  display: flex;
+  justify-content: center;
+  margin-top: var(--spacing-8);
+}
+
+.pagination button {
+  margin: 0 var(--spacing-2);
+}
+
+/* Search Bar */
+.search-bar {
+  margin-bottom: var(--spacing-6);
+}
+
+.search-bar input {
+  width: 100%;
+  padding: var(--spacing-3);
+  font-size: var(--font-size-base);
+  border-radius: var(--border-radius-full);
+  border: 1px solid var(--border-color);
+  transition: box-shadow var(--transition-base);
+}
+
+.search-bar input:focus {
+  outline: none;
+  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5);
+}
+
+/* Teacher Card */
+.teacher-card {
+  background-color: var(--light-text-color);
+  border-radius: var(--border-radius-lg);
+  box-shadow: var(--shadow-md);
+  overflow: hidden;
+  transition: transform var(--transition-base), box-shadow var(--transition-base);
+}
+
+.teacher-card:hover {
+  transform: translateY(-5px);
+  box-shadow: var(--shadow-lg);
+}
+
+.teacher-card img {
+  width: 100%;
+  height: 200px;
+  object-fit: cover;
+}
+
+.teacher-card-content {
+  padding: var(--spacing-4);
+}
+
+/* User Menu */
+.user-menu {
+  position: relative;
+}
+
+.user-menu-trigger {
+  cursor: pointer;
+}
+
+.user-menu-dropdown {
+  position: absolute;
+  top: 100%;
+  right: 0;
+  background-color: var(--light-text-color);
+  border: 1px solid var(--border-color);
+  border-radius: var(--border-radius-base);
+  padding: var(--spacing-2);
+  box-shadow: var(--shadow-md);
+}
+
+.user-menu-dropdown a {
+  display: block;
+  padding: var(--spacing-2);
+}
+
+/* File Upload */
+.file-upload {
+  border: 2px dashed var(--border-color);
+  border-radius: var(--border-radius-lg);
+  padding: var(--spacing-6);
+  text-align: center;
+  cursor: pointer;
+  transition: border-color var(--transition-base);
+}
+
+.file-upload:hover {
+  border-color: var(--primary-color);
+}
+
+/* Confirm Dialog */
+.confirm-dialog-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.confirm-dialog {
+  background-color: var(--light-text-color);
+  border-radius: var(--border-radius-lg);
+  padding: var(--spacing-6);
+  max-width: 400px;
+  width: 100%;
+}
+
+.confirm-dialog-actions {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: var(--spacing-4);
+}
+
+.confirm-dialog-actions button {
+  margin-left: var(--spacing-2);
+}

+ 92 - 0
src/styles/main.css

@@ -0,0 +1,92 @@
+@import 'variables.css';
+@import 'components.css';
+
+/* Reset and base styles */
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: 'Helvetica Neue', Arial, sans-serif;
+  line-height: 1.6;
+  color: var(--text-color);
+  background-color: var(--background-color);
+}
+
+/* Typography */
+h1, h2, h3, h4, h5, h6 {
+  margin-bottom: var(--spacing-4);
+  font-weight: bold;
+  line-height: 1.2;
+  color: var(--text-color);
+}
+
+h1 { font-size: var(--font-size-3xl); }
+h2 { font-size: var(--font-size-2xl); }
+h3 { font-size: var(--font-size-xl); }
+
+p { margin-bottom: var(--spacing-4); }
+
+/* Links */
+a {
+  color: var(--primary-color);
+  text-decoration: none;
+  transition: color var(--transition-base);
+}
+
+a:hover {
+  color: var(--primary-dark);
+}
+
+/* Buttons */
+.btn {
+  display: inline-block;
+  padding: var(--spacing-2) var(--spacing-4);
+  background-color: var(--primary-color);
+  color: var(--light-text-color);
+  border: none;
+  border-radius: var(--border-radius-base);
+  font-size: var(--font-size-base);
+  cursor: pointer;
+  transition: background-color var(--transition-base);
+}
+
+.btn:hover {
+  background-color: var(--primary-dark);
+}
+
+/* Forms */
+input, textarea, select {
+  width: 100%;
+  padding: var(--spacing-2);
+  margin-bottom: var(--spacing-4);
+  border: 1px solid var(--border-color);
+  border-radius: var(--border-radius-base);
+  font-size: var(--font-size-base);
+  background-color: var(--light-text-color);
+}
+
+/* Utility classes */
+.container {
+  max-width: 1200px;
+  margin: 0 auto;
+  padding: 0 var(--spacing-4);
+}
+
+.text-center { text-align: center; }
+.text-right { text-align: right; }
+.text-left { text-align: left; }
+
+.mt-1 { margin-top: var(--spacing-1); }
+.mt-2 { margin-top: var(--spacing-2); }
+.mt-4 { margin-top: var(--spacing-4); }
+.mt-6 { margin-top: var(--spacing-6); }
+
+.mb-1 { margin-bottom: var(--spacing-1); }
+.mb-2 { margin-bottom: var(--spacing-2); }
+.mb-4 { margin-bottom: var(--spacing-4); }
+.mb-6 { margin-bottom: var(--spacing-6); }
+
+.hidden { display: none; }

+ 40 - 0
src/styles/variables.css

@@ -0,0 +1,40 @@
+:root {
+  /* 新的现代配色方案 */
+  --primary-color: #3498db;
+  --primary-dark: #2980b9;
+  --secondary-color: #2ecc71;
+  --background-color: #f5f7fa;
+  --text-color: #34495e;
+  --light-text-color: #ffffff;
+  --border-color: #bdc3c7;
+
+  /* 保持其他变量不变 */
+  --font-size-xs: 0.75rem;
+  --font-size-sm: 0.875rem;
+  --font-size-base: 1rem;
+  --font-size-lg: 1.125rem;
+  --font-size-xl: 1.25rem;
+  --font-size-2xl: 1.5rem;
+  --font-size-3xl: 1.875rem;
+
+  --spacing-1: 0.25rem;
+  --spacing-2: 0.5rem;
+  --spacing-3: 0.75rem;
+  --spacing-4: 1rem;
+  --spacing-6: 1.5rem;
+  --spacing-8: 2rem;
+
+  --border-radius-sm: 0.125rem;
+  --border-radius-base: 0.25rem;
+  --border-radius-md: 0.375rem;
+  --border-radius-lg: 0.5rem;
+  --border-radius-full: 9999px;
+
+  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+  --shadow-base: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+
+  --transition-base: 0.2s ease-in-out;
+  --transition-slow: 0.3s ease-in-out;
+}

+ 34 - 0
src/utils/request.js

@@ -0,0 +1,34 @@
+import axios from 'axios'
+
+const request = axios.create({
+  // baseURL: import.meta.env.VITE_APP_API_BASE_URL, // 使用环境变量设置基础URL
+  baseURL: '/api', // 使用环境变量设置基础URL
+  timeout: 5000,
+})
+
+request.interceptors.request.use(
+  (config) => {
+    const token = localStorage.getItem('token')
+    if (token) {
+      config.headers['token'] = token // 修改为 'token',而不是 'Authorization'
+    }
+    return config
+  },
+  (error) => {
+    return Promise.reject(error)
+  }
+)
+
+request.interceptors.response.use(
+  (response) => response.data, // 直接返回响应的数据部分
+  (error) => {
+    if (error.response && error.response.status === 401) {
+      // 处理未授权访问(例如,重定向到登录页面)
+      console.log('Unauthorized access, redirecting to login...')
+      // 这里可以添加重定向逻辑
+    }
+    return Promise.reject(error)
+  }
+)
+
+export default request

+ 19 - 0
src/utils/validators.js

@@ -0,0 +1,19 @@
+export const required = (value) => !!value || '此字段是必填的'
+
+export const email = (value) => {
+  const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+  return pattern.test(value) || '请输入有效的电子邮件地址'
+}
+
+export const minLength = (min) => (value) => {
+  return value.length >= min || `最少需要 ${min} 个字符`
+}
+
+export const phoneNumber = (value) => {
+  const pattern = /^1[3-9]\d{9}$/
+  return pattern.test(value) || '请输入有效的手机号码'
+}
+
+export const passwordMatch = (password, confirmPassword) => {
+  return password === confirmPassword || '两次输入的密码不一致'
+}

+ 192 - 0
src/views/AdminAccount.vue

@@ -0,0 +1,192 @@
+<template>
+  <div class="admin-account">
+    <h1>账号管理</h1>
+
+    <div class="search-bar">
+      <input v-model="searchQuery" @input="handleSearch" placeholder="搜索用户名或姓名...">
+    </div>
+
+    <table class="account-table">
+      <thead>
+        <tr>
+          <th>用户名</th>
+          <th>姓名</th>
+          <th>角色</th>
+          <th>状态</th>
+          <th>操作</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr v-for="account in accounts" :key="account.id">
+          <td>{{ account.username }}</td>
+          <td>{{ account.name }}</td>
+          <td>{{ account.role }}</td>
+          <td>{{ account.isActive ? '启用' : '禁用' }}</td>
+          <td>
+            <button @click="toggleAccountStatus(account)">
+              {{ account.isActive ? '禁用' : '启用' }}
+            </button>
+            <button @click="deleteAccount(account)" class="delete-btn">删除</button>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+
+    <Pagination 
+      :total="totalAccounts" 
+      :per-page="perPage" 
+      :current-page="currentPage" 
+      @page-changed="handlePageChange"
+    />
+
+    <ConfirmDialog
+      v-if="showDeleteConfirm"
+      :message="'确定要删除账号 ' + (accountToDelete ? accountToDelete.username : '') + ' 吗?'"
+      @confirm="confirmDelete"
+      @cancel="cancelDelete"
+    />
+  </div>
+</template>
+
+<script>
+import { ref} from 'vue';
+import Pagination from '@/components/Pagination.vue';
+import ConfirmDialog from '@/components/ConfirmDialog.vue';
+import { getAccounts, updateAccountStatus, deleteAccount } from '@/api/Admin'; // 继续解构导入
+
+export default {
+  name: 'AdminAccount',
+  components: {
+    Pagination,
+    ConfirmDialog
+  },
+  setup() {
+    const accounts = ref([]);
+    const searchQuery = ref('');
+    const currentPage = ref(1);
+    const pageSize = ref(10);
+    const totalAccounts = ref(0);
+    const showDeleteConfirm = ref(false);
+    const accountToDelete = ref(null);
+
+    const fetchAccounts = async () => {
+      try {
+        const response = await getAccounts(currentPage.value, pageSize.value, searchQuery.value);
+        accounts.value = response.data;
+        totalAccounts.value = response.total;
+      } catch (error) {
+        console.error('Failed to fetch accounts:', error);
+      }
+    };
+
+    const handleSearch = () => {
+      currentPage.value = 1;
+      fetchAccounts();
+    };
+
+    const handlePageChange = (page) => {
+      currentPage.value = page;
+      fetchAccounts();
+    };
+
+    const toggleAccountStatus = async (account) => {
+      try {
+        const newStatus = account.status === 1 ? 0 : 1;
+        await updateAccountStatus(newStatus, account.id);
+        account.status = newStatus;
+        alert(`账号 ${account.name} ${newStatus === 1 ? '已启用' : '已禁用'}。`);
+      } catch (error) {
+        console.error('Failed to toggle account status:', error);
+        alert('操作失败,请重试。');
+      }
+    };
+
+    const removeAccount = (account) => {
+      accountToDelete.value = account;
+      showDeleteConfirm.value = true;
+    };
+
+    const confirmDelete = async () => {
+      try {
+        await deleteAccount(accountToDelete.value.id);
+        await fetchAccounts();
+        alert('账号删除成功!');
+      } catch (error) {
+        console.error('Failed to delete account:', error);
+        alert('账号删除失败,请重试。');
+      } finally {
+        showDeleteConfirm.value = false;
+        accountToDelete.value = null;
+      }
+    };
+
+    const cancelDelete = () => {
+      showDeleteConfirm.value = false;
+      accountToDelete.value = null;
+    };
+
+   
+
+    return {
+      accounts,
+      searchQuery,
+      currentPage,
+      pageSize,
+      totalAccounts,
+      showDeleteConfirm,
+      accountToDelete,
+      handleSearch,
+      handlePageChange,
+      toggleAccountStatus,
+      removeAccount,
+      confirmDelete,
+      cancelDelete
+    };
+  }
+};
+
+</script>
+<style scoped>
+.admin-account {
+  max-width: 1000px;
+  margin: 0 auto;
+  padding: 20px;
+}
+
+.search-bar {
+  margin-bottom: 20px;
+}
+
+.search-bar input {
+  width: 100%;
+  padding: 8px;
+  font-size: 16px;
+}
+
+.account-table {
+  width: 100%;
+  border-collapse: collapse;
+}
+
+.account-table th, .account-table td {
+  border: 1px solid #ddd;
+  padding: 8px;
+  text-align: left;
+}
+
+.account-table th {
+  background-color: #f2f2f2;
+}
+
+button {
+  padding: 5px 10px;
+  margin-right: 5px;
+  cursor: pointer;
+}
+
+.delete-btn {
+  background-color: #f44336;
+  color: white;
+  border: none;
+}
+</style>

+ 188 - 0
src/views/AdminFileManagement.vue

@@ -0,0 +1,188 @@
+<template>
+    <div class="admin-file">
+      <h1>共享资料库管理</h1>
+  
+      <div class="search-bar">
+        <input v-model="searchQuery" @input="handleSearch" placeholder="搜索文件名...">
+      </div>
+  
+      <table class="file-table">
+        <thead>
+          <tr>
+            <th>文件名</th>
+            <th>上传人</th>
+            <th>上传日期</th>
+            <th>可见性</th>
+            <th>操作</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr v-for="file in files" :key="file.id">
+            <td>{{ file.name }}</td>
+            <td>{{ file.uploader }}</td>
+            <td>{{ formatDate(file.uploadDate) }}</td>
+            <td>{{ file.visibility === 'public' ? '所有用户可见' : '仅登录用户可见' }}</td>
+            <td>
+              <button @click="viewFile(file)">查看</button>
+              <button @click="deleteFile(file)" class="delete-btn">删除</button>
+            </td>
+          </tr>
+        </tbody>
+      </table>
+  
+      <Pagination 
+        :total="totalFiles" 
+        :per-page="perPage" 
+        :current-page="currentPage" 
+        @page-changed="handlePageChange"
+      />
+  
+      <ConfirmDialog
+        v-if="showDeleteConfirm"
+        :message="'确定要删除文件 ' + (fileToDelete ? fileToDelete.name : '') + ' 吗?'"
+        @confirm="confirmDelete"
+        @cancel="cancelDelete"
+      />
+    </div>
+  </template>
+  
+  <script>
+  import { ref} from 'vue';
+  import Pagination from '@/components/Pagination.vue';
+  import ConfirmDialog from '@/components/ConfirmDialog.vue';
+  import { getResourcesPage, deleteResourceById } from '@/api/Resources';
+  
+  export default {
+    name: 'AdminFile',
+    components: {
+      Pagination,
+      ConfirmDialog
+    },
+    setup() {
+      const resources = ref([]);
+      const searchQuery = ref('');
+      const currentPage = ref(1);
+      const pageSize = ref(10);
+      const totalFiles = ref(0);
+      const showDeleteConfirm = ref(false);
+      const fileToDelete = ref(null);
+  
+      const fetchResources = async () => {
+        try {
+          const response = await getResourcesPage(currentPage.value, pageSize.value, searchQuery.value);
+          resources.value = response.data;
+          totalFiles.value = response.total;
+        } catch (error) {
+          console.error('Failed to fetch resources:', error);
+        }
+      };
+  
+      const handleSearch = () => {
+        currentPage.value = 1;
+        fetchResources();
+      };
+  
+      const handlePageChange = (page) => {
+        currentPage.value = page;
+        fetchResources();
+      };
+  
+      const viewFile = (file) => {
+        // Implement file viewing logic
+        console.log('Viewing file:', file);
+      };
+  
+      const deleteResource = (file) => {
+        fileToDelete.value = file;
+        showDeleteConfirm.value = true;
+      };
+  
+      const confirmDelete = async () => {
+        try {
+          await deleteResourceById(fileToDelete.value.id);
+          await fetchResources();
+          alert('文件删除成功!');
+        } catch (error) {
+          console.error('Failed to delete resource:', error);
+          alert('文件删除失败,请重试。');
+        } finally {
+          showDeleteConfirm.value = false;
+          fileToDelete.value = null;
+        }
+      };
+  
+      const cancelDelete = () => {
+        showDeleteConfirm.value = false;
+        fileToDelete.value = null;
+      };
+  
+      const formatDate = (date) => {
+        return new Date(date).toLocaleDateString();
+      };
+  
+      
+  
+      return {
+        resources,
+        searchQuery,
+        currentPage,
+        pageSize,
+        totalFiles,
+        showDeleteConfirm,
+        fileToDelete,
+        handleSearch,
+        handlePageChange,
+        viewFile,
+        deleteResource,
+        confirmDelete,
+        cancelDelete,
+        formatDate
+      };
+    }
+  };
+  </script>
+  
+  <style scoped>
+  .admin-file {
+    max-width: 1000px;
+    margin: 0 auto;
+    padding: 20px;
+  }
+  
+  .search-bar {
+    margin-bottom: 20px;
+  }
+  
+  .search-bar input {
+    width: 100%;
+    padding: 8px;
+    font-size: 16px;
+  }
+  
+  .file-table {
+    width: 100%;
+    border-collapse: collapse;
+  }
+  
+  .file-table th, .file-table td {
+    border: 1px solid #ddd;
+    padding: 8px;
+    text-align: left;
+  }
+  
+  .file-table th {
+    background-color: #f2f2f2;
+  }
+  
+  button {
+    padding: 5px 10px;
+    margin-right: 5px;
+    cursor: pointer;
+  }
+  
+  .delete-btn {
+    background-color: #f44336;
+    color: white;
+    border: none;
+  }
+  </style>

+ 180 - 0
src/views/ChangePassword.vue

@@ -0,0 +1,180 @@
+<template>
+  <div class="change-password-container">
+    <div class="change-password-card">
+      <h2 class="change-password-title">修改密码</h2>
+      <form @submit.prevent="handleChangePassword" class="change-password-form">
+        <div class="form-group">
+          <label for="oldPassword">旧密码</label>
+          <input
+            id="oldPassword"
+            v-model="oldPassword"
+            type="password"
+            required
+            placeholder="请输入旧密码"
+          />
+        </div>
+        <div class="form-group">
+          <label for="newPassword">新密码</label>
+          <input
+            id="newPassword"
+            v-model="newPassword"
+            type="password"
+            required
+            placeholder="请输入新密码"
+          />
+        </div>
+        <div class="form-group">
+          <label for="confirmNewPassword">确认新密码</label>
+          <input
+            id="confirmNewPassword"
+            v-model="confirmNewPassword"
+            type="password"
+            required
+            placeholder="请再次输入新密码"
+          />
+        </div>
+        <button type="submit" class="change-password-button">修改密码</button>
+      </form>
+      <div class="links">
+        <router-link to="/profile">返回个人中心</router-link>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { userPasswordPut } from '@/api/Users';
+
+export default {
+  name: 'ChangePassword',
+  setup() {
+    const router = useRouter();
+    const oldPassword = ref('');
+    const newPassword = ref('');
+    const confirmNewPassword = ref('');
+
+    const handleChangePassword = async () => {
+      if (newPassword.value !== confirmNewPassword.value) {
+        alert('新密码和确认新密码不匹配');
+        return;
+      }
+      try {
+        await userPasswordPut({
+          oldPassword: oldPassword.value,
+          newPassword: newPassword.value
+        });
+        alert('密码修改成功');
+        router.push('/profile');
+      } catch (error) {
+        alert('修改密码失败:' + error.message);
+      }
+    };
+
+    return {
+      oldPassword,
+      newPassword,
+      confirmNewPassword,
+      handleChangePassword
+    };
+  }
+};
+</script>
+
+<style scoped>
+.change-password-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100vh;
+  padding: var(--spacing-6);
+  box-sizing: border-box;
+}
+
+.change-password-card {
+  background-color: var(--light-text-color);
+  border-radius: var(--border-radius-lg);
+  box-shadow: var(--shadow-lg);
+  padding: var(--spacing-8);
+  width: 100%;
+  max-width: 400px;
+  animation: fadeIn 0.5s ease-out;
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; transform: translateY(-20px); }
+  to { opacity: 1; transform: translateY(0); }
+}
+
+.change-password-title {
+  color: var(--primary-color);
+  font-size: var(--font-size-2xl);
+  margin-bottom: var(--spacing-6);
+  text-align: center;
+}
+
+.change-password-form {
+  display: flex;
+  flex-direction: column;
+  gap: var(--spacing-4);
+}
+
+.form-group {
+  display: flex;
+  flex-direction: column;
+}
+
+.form-group label {
+  margin-bottom: var(--spacing-2);
+  color: var(--text-color);
+  font-weight: bold;
+}
+
+.form-group input {
+  padding: var(--spacing-3);
+  border: 1px solid var(--border-color);
+  border-radius: var(--border-radius-base);
+  font-size: var(--font-size-base);
+  transition: all var(--transition-base);
+}
+
+.form-group input:focus {
+  outline: none;
+  border-color: var(--primary-color);
+  box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
+}
+
+.change-password-button {
+  background-color: var(--primary-color);
+  color: var(--light-text-color);
+  border: none;
+  border-radius: var(--border-radius-base);
+  padding: var(--spacing-3);
+  font-size: var(--font-size-base);
+  cursor: pointer;
+  transition: background-color var(--transition-base);
+}
+
+.change-password-button:hover {
+  background-color: var(--primary-dark);
+}
+
+.links {
+  margin-top: var(--spacing-6);
+  text-align: center;
+}
+
+.links a {
+  color: var(--primary-color);
+  text-decoration: none;
+  font-size: var(--font-size-sm);
+  transition: color var(--transition-base);
+}
+
+.links a:hover {
+  color: var(--primary-dark);
+  text-decoration: underline;
+}
+</style>
+  

+ 262 - 0
src/views/EditProfile.vue

@@ -0,0 +1,262 @@
+<template>
+    <div class="edit-profile">
+      <h1>编辑个人资料</h1>
+      
+      <!-- 基本信息部分 -->
+      <div class="basic-info">
+        <img :src="teacher.image" alt="Profile Image" class="profile-image">
+        <div>
+          <label for="name">姓名:</label>
+          <input type="text" v-model="teacher.name" id="name">
+          
+          <label for="email">邮箱:</label>
+          <input type="email" v-model="teacher.email" id="email">
+          
+          <label for="phoneNumber">电话:</label>
+          <input type="tel" v-model="teacher.phoneNumber" id="phoneNumber">
+          
+          <label for="profile">简介:</label>
+          <textarea v-model="teacher.profile" id="profile"></textarea>
+          
+          <label for="undergraduateCourse">本科课程:</label>
+          <input type="text" v-model="teacher.undergraduateCourse" id="undergraduateCourse">
+          
+          <label for="graduateCourse">研究生课程:</label>
+          <input type="text" v-model="teacher.graduateCourse" id="graduateCourse">
+          
+          <label for="researchField">研究领域:</label>
+          <textarea v-model="teacher.researchField.information" id="researchField"></textarea>
+        </div>
+      </div>
+  
+      <!-- 专利信息 -->
+      <CollapsibleSection title="专利信息">
+        <div v-for="(patent, index) in teacher.patent" :key="index" class="patent-item">
+          <label>专利号:</label>
+          <input type="text" v-model="patent.patentNum">
+          <label>授权号:</label>
+          <input type="text" v-model="patent.authorizationNum">
+          <label>证书号:</label>
+          <input type="text" v-model="patent.certificateNum">
+          <label>日期:</label>
+          <input type="date" v-model="patent.date">
+        </div>
+      </CollapsibleSection>
+  
+      <!-- 获奖信息 -->
+      <CollapsibleSection title="获奖信息">
+        <div v-for="(award, index) in awards" :key="index" class="award-item">
+          <label>获奖名称:</label>
+          <input type="text" v-model="award.name">
+          <label>时间:</label>
+          <input type="date" v-model="award.time">
+          <label>图片链接:</label>
+          <input type="url" v-model="award.image">
+        </div>
+      </CollapsibleSection>
+  
+      <!-- 论文信息 -->
+      <CollapsibleSection title="论文信息">
+        <div v-for="(thesis, index) in theses" :key="index" class="thesis-item">
+          <label>论文名称:</label>
+          <input type="text" v-model="thesis.name">
+          <label>发表时间:</label>
+          <input type="date" v-model="thesis.time">
+          <label>网站链接:</label>
+          <input type="url" v-model="thesis.website">
+          <label>图片链接:</label>
+          <input type="url" v-model="thesis.image">
+          <label>文件链接:</label>
+          <input type="url" v-model="thesis.file">
+        </div>
+      </CollapsibleSection>
+  
+      <!-- 著作信息 -->
+      <CollapsibleSection title="著作信息">
+        <div v-for="(work, index) in works" :key="index" class="work-item">
+          <label>著作名称:</label>
+          <input type="text" v-model="work.name">
+          <label>出版社:</label>
+          <input type="text" v-model="work.press">
+          <label>出版时间:</label>
+          <input type="date" v-model="work.time">
+          <label>图片链接:</label>
+          <input type="url" v-model="work.image">
+          <label>文件链接:</label>
+          <input type="url" v-model="work.file">
+        </div>
+      </CollapsibleSection>
+  
+      <!-- 保存按钮 -->
+      
+    <button @click="saveProfile" class="save-button">保存</button>
+  </div>
+</template>
+
+<script>
+import { ref, watchEffect } from 'vue';
+import { useRouter } from 'vue-router';
+import { updateTeacherById, getTeacherById } from '@/api/Teacher';
+import { useAuthStore } from '@/store/modules/auth';
+
+export default {
+  name: 'EditProfile',
+  setup() {
+    const router = useRouter();
+    const authStore = useAuthStore();
+    
+    const teacher = ref({
+      name: '',
+      email: '',
+      phoneNumber: '',
+      undergraduateCourse: '',
+      graduateCourse: '',
+      profile: '',
+      image: '',
+      researchField: {}
+    });
+
+    const teacherId = authStore.user.id;
+    const token = authStore.token;
+
+    // 使用 watchEffect 来监控 teacherId 的变化并获取教师数据
+    watchEffect(async () => {
+      try {
+        const response = await getTeacherById(teacherId, token);
+        teacher.value = response;
+      } catch (error) {
+        console.error('获取教师信息失败:', error);
+      }
+    });
+
+    const saveProfile = async () => {
+      try {
+        await updateTeacherById(teacherId, { ...teacher.value });
+        router.push({ name: 'Profile' }); // 返回个人资料页面
+      } catch (error) {
+        console.error('保存个人资料失败:', error);
+      }
+    };
+
+    return {
+      teacher,
+      saveProfile
+    };
+  }
+}
+</script>
+
+<style scoped>
+.edit-profile {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+  background: #f9f9f9;
+  border-radius: 8px;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+}
+
+h1 {
+  text-align: center;
+  color: #333;
+  margin-bottom: 20px;
+}
+
+.basic-info {
+  display: flex;
+  align-items: center;
+  margin-bottom: 30px;
+}
+
+.profile-image {
+  width: 100px;
+  height: 100px;
+  border-radius: 50%;
+  margin-right: 20px;
+  border: 2px solid #007bff;
+}
+
+.basic-info div {
+  flex: 1;
+}
+
+label {
+  display: block;
+  font-weight: bold;
+  margin-bottom: 5px;
+  color: #555;
+}
+
+input[type="text"],
+input[type="email"],
+input[type="tel"],
+input[type="url"],
+textarea {
+  width: 100%;
+  padding: 10px;
+  margin-bottom: 15px;
+  border: 1px solid #ccc;
+  border-radius: 5px;
+  font-size: 14px;
+  transition: border-color 0.3s;
+}
+
+input[type="text"]:focus,
+input[type="email"]:focus,
+input[type="tel"]:focus,
+input[type="url"]:focus,
+textarea:focus {
+  border-color: #007bff;
+  outline: none;
+}
+
+.CollapsibleSection {
+  margin-bottom: 20px;
+  border: 1px solid #e0e0e0;
+  border-radius: 5px;
+  background: #fff;
+}
+
+.CollapsibleSection h2 {
+  cursor: pointer;
+  padding: 10px;
+  background: #f1f1f1;
+  margin: 0;
+  border-bottom: 1px solid #e0e0e0;
+  color: #333;
+}
+
+.CollapsibleSection-content {
+  padding: 10px;
+}
+
+.award-item,
+.thesis-item,
+.work-item,
+.patent-item {
+  margin-bottom: 15px;
+  padding: 10px;
+  border: 1px solid #e0e0e0;
+  border-radius: 5px;
+  background: #f9f9f9;
+}
+
+.save-button {
+  display: block;
+  width: 100%;
+  padding: 10px;
+  background: #28a745;
+  color: white;
+  border: none;
+  border-radius: 5px;
+  font-size: 16px;
+  cursor: pointer;
+  transition: background 0.3s;
+}
+
+.save-button:hover {
+  background: #218838;
+}
+</style>
+
+  

+ 206 - 0
src/views/ForgotPassword.vue

@@ -0,0 +1,206 @@
+<template>
+  <div class="forgot-password-container">
+    <div class="forgot-password-card">
+      <h2 class="forgot-password-title">忘记密码</h2>
+      <form @submit.prevent="handleForgotPassword" class="forgot-password-form">
+        <div class="form-group">
+          <label for="name">姓名</label>
+          <input
+            id="name"
+            v-model="name"
+            type="text"
+            required
+            placeholder="请输入姓名"
+          />
+        </div>
+        <div class="form-group">
+          <label for="username">用户名</label>
+          <input
+            id="username"
+            v-model="username"
+            type="text"
+            required
+            placeholder="请输入用户名"
+          />
+        </div>
+        <div class="form-group">
+          <label for="phone">手机号</label>
+          <input
+            id="phone"
+            v-model="phone"
+            type="tel"
+            required
+            placeholder="请输入手机号"
+          />
+        </div>
+        <div class="form-group">
+          <label for="newPassword">新密码</label>
+          <input
+            id="newPassword"
+            v-model="newPassword"
+            type="password"
+            required
+            placeholder="请输入新密码"
+          />
+        </div>
+        <div class="form-group">
+          <label for="confirmNewPassword">确认新密码</label>
+          <input
+            id="confirmNewPassword"
+            v-model="confirmNewPassword"
+            type="password"
+            required
+            placeholder="请再次输入新密码"
+          />
+        </div>
+        <button type="submit" class="forgot-password-button">提交</button>
+      </form>
+      <div class="links">
+        <router-link to="/login">记起密码了?返回登录</router-link>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { userForgetPost } from '@/api/Users';
+
+export default {
+  name: 'ForgotPassword',
+  setup() {
+    const router = useRouter();
+    const name = ref('');
+    const username = ref('');
+    const phone = ref('');
+    const newPassword = ref('');
+    const confirmNewPassword = ref('');
+
+    const handleForgotPassword = async () => {
+      if (newPassword.value !== confirmNewPassword.value) {
+        alert('新密码和确认新密码不匹配');
+        return;
+      }
+      try {
+        await userForgetPost({
+          name: name.value,
+          username: username.value,
+          phoneNumber: phone.value,
+          newPassword: newPassword.value
+        });
+        alert('密码重置请求已提交,请等待管理员审核');
+        router.push('/login');
+      } catch (error) {
+        alert('提交失败:' + error.message);
+      }
+    };
+
+    return {
+      name,
+      username,
+      phone,
+      newPassword,
+      confirmNewPassword,
+      handleForgotPassword
+    };
+  }
+};
+</script>
+
+<style scoped>
+.forgot-password-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100vh;
+  padding: var(--spacing-6);
+  box-sizing: border-box;
+}
+
+.forgot-password-card {
+  background-color: var(--light-text-color);
+  border-radius: var(--border-radius-lg);
+  box-shadow: var(--shadow-lg);
+  padding: var(--spacing-8);
+  width: 100%;
+  max-width: 400px;
+  animation: fadeIn 0.5s ease-out;
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; transform: translateY(-20px); }
+  to { opacity: 1; transform: translateY(0); }
+}
+
+.forgot-password-title {
+  color: var(--primary-color);
+  font-size: var(--font-size-2xl);
+  margin-bottom: var(--spacing-6);
+  text-align: center;
+}
+
+.forgot-password-form {
+  display: flex;
+  flex-direction: column;
+  gap: var(--spacing-4);
+}
+
+.form-group {
+  display: flex;
+  flex-direction: column;
+}
+
+.form-group label {
+  margin-bottom: var(--spacing-2);
+  color: var(--text-color);
+  font-weight: bold;
+}
+
+.form-group input {
+  padding: var(--spacing-3);
+  border: 1px solid var(--border-color);
+  border-radius: var(--border-radius-base);
+  font-size: var(--font-size-base);
+  transition: all var(--transition-base);
+}
+
+.form-group input:focus {
+  outline: none;
+  border-color: var(--primary-color);
+  box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
+}
+
+.forgot-password-button {
+  background-color: var(--primary-color);
+  color: var(--light-text-color);
+  border: none;
+  border-radius: var(--border-radius-base);
+  padding: var(--spacing-3);
+  font-size: var(--font-size-base);
+  cursor: pointer;
+  transition: background-color var(--transition-base);
+}
+
+.forgot-password-button:hover {
+  background-color: var(--primary-dark);
+}
+
+.links {
+  margin-top: var(--spacing-6);
+  text-align: center;
+}
+
+.links a {
+  color: var(--primary-color);
+  text-decoration: none;
+  font-size: var(--font-size-sm);
+  transition: color var(--transition-base);
+}
+
+.links a:hover {
+  color: var(--primary-dark);
+  text-decoration: underline;
+}
+</style>
+  

+ 77 - 0
src/views/Home.vue

@@ -0,0 +1,77 @@
+<template>
+  <div class="home bg-background-color min-h-screen pb-24">
+    <div class="container mx-auto px-4 py-8">
+      <h1 class="text-3xl font-bold text-center text-primary mb-8">师资队伍</h1>
+
+      <div class="w-full max-w-2xl mx-auto mb-12">
+        <SearchBar @search="handleSearch" />
+      </div>
+
+      <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8 mb-16">
+        <TeacherCard
+          v-for="teacher in teachersStore.teachers"
+          :key="teacher.id"
+          :teacher="teacher"
+          @click="goToTeacherDetail(teacher.id)"
+        />
+      </div>
+    </div>
+
+    <div class="fixed bottom-0 left-0 right-0 bg-white shadow-md">
+      <div class="container mx-auto px-4 py-4">
+        <Pagination
+          :totalItems="teachersStore.totalTeachers"
+          :currentPage="teachersStore.currentPage"
+          :itemsPerPage="pageSize"
+          @page-change="handlePageChange"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, watchEffect } from 'vue';
+import { useRouter } from 'vue-router';
+import { useTeachersStore } from '@/store/modules/teachers';
+import SearchBar from '@/components/SearchBar.vue';
+import TeacherCard from '@/components/TeacherCard.vue';
+import Pagination from '@/components/Pagination.vue';
+
+const router = useRouter();
+const teachersStore = useTeachersStore();
+const pageSize = ref(12);
+const searchQuery = ref('');
+
+const totalTeachers = computed(() => teachersStore.totalTeachers || teachersStore.teachers.length);
+
+const fetchTeachers = () => {
+  teachersStore.fetchTeachers(teachersStore.currentPage, searchQuery.value);
+};
+
+const handleSearch = (query) => {
+  searchQuery.value = query;
+  teachersStore.currentPage = 1;
+  fetchTeachers();
+};
+
+const handlePageChange = (page) => {
+  teachersStore.currentPage = page;
+  fetchTeachers();
+};
+
+const goToTeacherDetail = (teacherId) => {
+  router.push({ name: 'TeacherDetail', params: { id: teacherId } });
+};
+
+// 使用 watchEffect 替代 onMounted
+watchEffect(() => {
+  fetchTeachers();
+});
+</script>
+
+<style scoped>
+.home {
+  padding-bottom: 80px;
+}
+</style>

+ 172 - 0
src/views/Login.vue

@@ -0,0 +1,172 @@
+<template>
+  <div class="login-container">
+    <div class="login-card">
+      <h2 class="login-title">欢迎登录</h2>
+      <form @submit.prevent="handleLogin" class="login-form">
+        <div class="form-group">
+          <label for="username">用户名</label>
+          <input
+            id="username"
+            v-model="username"
+            type="text"
+            required
+            placeholder="请输入您的用户名"
+          />
+        </div>
+        <div class="form-group">
+          <label for="password">密码</label>
+          <input
+            id="password"
+            v-model="password"
+            type="password"
+            required
+            placeholder="请输入您的密码"
+          />
+        </div>
+        <button type="submit" class="login-button">登录</button>
+      </form>
+      <div class="links">
+        <router-link to="/register">注册新账号</router-link>
+        <router-link to="/forgot-password">忘记密码</router-link>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { userLoginPost } from '@/api/Users';
+import { useAuthStore } from '@/store/modules/auth';
+
+export default {
+  name: 'Login',
+  setup() {
+    const router = useRouter();
+    const authStore = useAuthStore();
+    const username = ref('');
+    const password = ref('');
+
+    const handleLogin = async () => {
+      try {
+        const response = await userLoginPost({ username: username.value, password: password.value });
+        localStorage.setItem('token', response.token);
+        authStore.setUser(response.user);
+        authStore.login();
+        
+        if (response.user.role === 'admin') {
+          router.push('/admin');
+        } else {
+          router.push('/');
+        }
+      } catch (error) {
+        console.error('Login failed:', error);
+        alert('登录失败:' + (error.response?.data?.message || error.message));
+      }
+    };
+
+    return {
+      username,
+      password,
+      handleLogin
+    };
+  }
+};
+</script>
+
+<style scoped>
+.login-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100vh; /* 使用 height 而不是 min-height */
+  padding: var(--spacing-6);
+  box-sizing: border-box; /* 确保 padding 不会增加元素的总高度 */
+}
+
+.login-card {
+  background-color: var(--light-text-color);
+  border-radius: var(--border-radius-lg);
+  box-shadow: var(--shadow-lg);
+  padding: var(--spacing-8);
+  width: 100%;
+  max-width: 400px;
+  animation: fadeIn 0.5s ease-out;
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; transform: translateY(-20px); }
+  to { opacity: 1; transform: translateY(0); }
+}
+
+.login-title {
+  color: var(--primary-color);
+  font-size: var(--font-size-2xl);
+  margin-bottom: var(--spacing-6);
+  text-align: center;
+}
+
+.login-form {
+  display: flex;
+  flex-direction: column;
+  gap: var(--spacing-4);
+}
+
+.form-group {
+  display: flex;
+  flex-direction: column;
+}
+
+.form-group label {
+  margin-bottom: var(--spacing-2);
+  color: var(--text-color);
+  font-weight: bold;
+}
+
+.form-group input {
+  padding: var(--spacing-3);
+  border: 1px solid var(--border-color);
+  border-radius: var(--border-radius-base);
+  font-size: var(--font-size-base);
+  transition: all var(--transition-base);
+}
+
+.form-group input:focus {
+  outline: none;
+  border-color: var(--primary-color);
+  box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
+}
+
+.login-button {
+  background-color: var(--primary-color);
+  color: var(--light-text-color);
+  border: none;
+  border-radius: var(--border-radius-base);
+  padding: var(--spacing-3);
+  font-size: var(--font-size-base);
+  cursor: pointer;
+  transition: background-color var(--transition-base);
+}
+
+.login-button:hover {
+  background-color: var(--primary-dark);
+}
+
+.links {
+  display: flex;
+  justify-content: space-between;
+  margin-top: var(--spacing-6);
+}
+
+.links a {
+  color: var(--primary-color);
+  text-decoration: none;
+  font-size: var(--font-size-sm);
+  transition: color var(--transition-base);
+}
+
+.links a:hover {
+  color: var(--primary-dark);
+  text-decoration: underline;
+}
+</style>

+ 95 - 0
src/views/PaperDetail.vue

@@ -0,0 +1,95 @@
+<template>
+    <div class="paper-detail">
+      <h1>{{ paper.name }}</h1>
+      <img :src="paper.image" :alt="paper.name" class="paper-image">
+      
+      <div class="paper-info">
+        <p><strong>发表刊物:</strong> {{ paper.journal }}</p>
+        <p><strong>发表时间:</strong> {{ paper.time }}</p>
+        <p><strong>作者:</strong> {{ paper.authors.join(', ') }}</p>
+        <p><strong>摘要:</strong> {{ paper.abstract }}</p>
+        <a :href="paper.website" target="_blank" class="paper-link">查看原文</a>
+      </div>
+  
+      <div v-if="paper.file" class="paper-file">
+        <h2>论文文件</h2>
+        <a :href="paper.file" download>下载论文</a>
+      </div>
+  
+      <div class="paper-citations" v-if="paper.citations && paper.citations.length">
+        <h2>引用</h2>
+        <ul>
+          <li v-for="citation in paper.citations" :key="citation">{{ citation }}</li>
+        </ul>
+      </div>
+    </div>
+  </template>
+  
+  <script>
+import { ref} from 'vue';
+import { useRoute } from 'vue-router';
+import { getThesisById } from '@/api/Teacher';
+
+export default {
+  name: 'PaperDetail',
+  setup() {
+    const route = useRoute();
+    const paperId = ref(route.params.id);
+    const paperDetails = ref({});
+
+    const fetchPaperDetails = async () => {
+      try {
+        paperDetails.value = await getThesisById(paperId.value);
+      } catch (error) {
+        console.error('Failed to fetch paper details:', error);
+      }
+    };
+
+   
+
+    return {
+      paperDetails
+    };
+  }
+};
+</script>
+  
+  <style scoped>
+  .paper-detail {
+    max-width: 800px;
+    margin: 0 auto;
+    padding: 20px;
+  }
+  
+  .paper-image {
+    max-width: 100%;
+    height: auto;
+    margin-bottom: 20px;
+  }
+  
+  .paper-info {
+    margin-bottom: 20px;
+  }
+  
+  .paper-link {
+    display: inline-block;
+    margin-top: 10px;
+    padding: 10px 15px;
+    background-color: #4CAF50;
+    color: white;
+    text-decoration: none;
+    border-radius: 4px;
+  }
+  
+  .paper-file {
+    margin-top: 20px;
+  }
+  
+  .paper-citations {
+    margin-top: 20px;
+  }
+  
+  h1, h2 {
+    margin-bottom: 15px;
+  }
+  </style>

+ 159 - 0
src/views/PaperUpload.vue

@@ -0,0 +1,159 @@
+<template>
+    <div class="paper-upload">
+      <h1>上传论文</h1>
+      <form @submit.prevent="handleSubmit">
+        <div class="form-group">
+          <label for="title">论文标题</label>
+          <input type="text" id="title" v-model="paper.title" required>
+        </div>
+  
+        <div class="form-group">
+          <label for="journal">发表刊物</label>
+          <input type="text" id="journal" v-model="paper.journal" required>
+        </div>
+  
+        <div class="form-group">
+          <label for="publicationDate">发表日期</label>
+          <input type="date" id="publicationDate" v-model="paper.publicationDate" required>
+        </div>
+  
+        <div class="form-group">
+          <label for="abstract">摘要</label>
+          <textarea id="abstract" v-model="paper.abstract" rows="4" required></textarea>
+        </div>
+  
+        <div class="form-group">
+          <label for="authors">作者 (用逗号分隔)</label>
+          <input type="text" id="authors" v-model="paper.authors" required>
+        </div>
+  
+        <div class="form-group">
+          <label for="paperFile">论文文件</label>
+          <input type="file" id="paperFile" @change="handleFileChange" accept=".pdf,.doc,.docx" required>
+        </div>
+  
+        <div class="form-group">
+          <label for="paperImage">论文封面图片</label>
+          <input type="file" id="paperImage" @change="handleImageChange" accept="image/*" required>
+        </div>
+  
+        <button type="submit" :disabled="isUploading">上传论文</button>
+      </form>
+  
+      <div v-if="isUploading" class="upload-progress">
+        正在上传...{{ uploadProgress }}%
+      </div>
+    </div>
+  </template>
+  
+  <script>
+import { ref, reactive } from 'vue';
+import { useRouter } from 'vue-router';
+import { uploadThesis } from '@/api/Teacher';
+
+export default {
+  name: 'PaperUpload',
+  setup() {
+    const router = useRouter();
+    const paper = reactive({
+      name: '',
+      website: '',
+      time: '',
+      abstract: '',
+      authors: '',
+      file: null,
+      image: null
+    });
+    const isUploading = ref(false);
+    const uploadProgress = ref(0);
+
+    const handleFileChange = (event) => {
+      paper.file = event.target.files[0];
+    };
+
+    const handleImageChange = (event) => {
+      paper.image = event.target.files[0];
+    };
+
+    const uploadPaper = async () => {
+      isUploading.value = true;
+      try {
+        const formData = new FormData();
+        for (const key in paper) {
+          if (key === 'authors') {
+            formData.append(key, paper[key].split(',').map(author => author.trim()));
+          } else {
+            formData.append(key, paper[key]);
+          }
+        }
+
+        await uploadThesis(formData, (progressEvent) => {
+          uploadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total);
+        });
+
+        alert('论文上传成功!');
+        router.push('/profile'); // 假设上传成功后跳转到个人资料页面
+      } catch (error) {
+        console.error('论文上传失败:', error);
+        alert('论文上传失败,请重试。');
+      } finally {
+        isUploading.value = false;
+      }
+    };
+
+    return {
+      paper,
+      isUploading,
+      uploadProgress,
+      handleFileChange,
+      handleImageChange,
+      uploadPaper
+    };
+  }
+};
+</script>
+  
+  <style scoped>
+  .paper-upload {
+    max-width: 600px;
+    margin: 0 auto;
+    padding: 20px;
+  }
+  
+  .form-group {
+    margin-bottom: 20px;
+  }
+  
+  label {
+    display: block;
+    margin-bottom: 5px;
+  }
+  
+  input[type="text"],
+  input[type="date"],
+  textarea {
+    width: 100%;
+    padding: 8px;
+    border: 1px solid #ddd;
+    border-radius: 4px;
+  }
+  
+  button {
+    padding: 10px 20px;
+    background-color: #4CAF50;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    cursor: pointer;
+  }
+  
+  button:disabled {
+    background-color: #ddd;
+    cursor: not-allowed;
+  }
+  
+  .upload-progress {
+    margin-top: 20px;
+    text-align: center;
+  }
+  </style>

+ 255 - 0
src/views/Profile.vue

@@ -0,0 +1,255 @@
+<template>
+  <div class="profile">
+    <h1>个人资料</h1>
+    
+    <!-- 基本信息部分 -->
+    <div class="basic-info">
+      <img :src="teacher.image" alt="Profile Image" class="profile-image">
+      <div>
+        <h2>{{ teacher.name }}</h2>
+        <p>{{ teacher.email }}</p>
+        <p>{{ teacher.phoneNumber }}</p>
+        <p>简介: {{ teacher.profile }}</p>
+        <p>本科课程: {{ teacher.undergraduateCourse }}</p>
+        <p>研究生课程: {{ teacher.graduateCourse }}</p>
+      </div>
+    </div>
+
+    <!-- 研究领域 -->
+    <CollapsibleSection title="研究领域">
+      <p>{{ teacher.researchField?.information }}</p>
+    </CollapsibleSection>
+
+    <!-- 专利信息 -->
+    <CollapsibleSection title="专利信息">
+      <div v-for="patent in teacher.patent" :key="patent.patentNum">
+        <p>专利号: {{ patent.patentNum }}</p>
+        <p>授权号: {{ patent.authorizationNum }}</p>
+        <p>证书号: {{ patent.certificateNum }}</p>
+        <p>日期: {{ patent.date }}</p>
+      </div>
+    </CollapsibleSection>
+
+    <!-- 获奖信息 -->
+    <CollapsibleSection title="获奖信息">
+      <div v-for="award in awards" :key="award.id" class="award-item">
+        <h4>{{ award.name }}</h4>
+        <p>时间: {{ award.time }}</p>
+        <img :src="award.image" alt="Award Image" class="award-image">
+      </div>
+      <Pagination
+        :total="awardTotal"
+        :current-page="awardPage"
+        :page-size="pageSize"
+        @page-change="handleAwardPageChange"
+      />
+    </CollapsibleSection>
+
+    <!-- 论文信息 -->
+    <CollapsibleSection title="论文信息">
+      <div v-for="thesis in theses" :key="thesis.id" class="thesis-item">
+        <h4>{{ thesis.name }}</h4>
+        <p>发表时间: {{ thesis.time }}</p>
+        <a :href="thesis.website" target="_blank">查看论文</a>
+        <img :src="thesis.image" alt="Paper Image" class="thesis-image">
+        <a :href="thesis.file" target="_blank">下载文件</a>
+      </div>
+      <Pagination
+        :total="thesisTotal"
+        :current-page="thesisPage"
+        :page-size="pageSize"
+        @page-change="handleThesisPageChange"
+      />
+      <router-link to="/upload-thesis" class="upload-button">上传论文</router-link>
+    </CollapsibleSection>
+
+    <!-- 著作信息 -->
+    <CollapsibleSection title="著作信息">
+      <div v-for="work in works" :key="work.id" class="work-item">
+        <h4>{{ work.name }}</h4>
+        <p>出版社: {{ work.press }}</p>
+        <p>出版时间: {{ work.time }}</p>
+        <img :src="work.image" alt="Work Image" class="work-image">
+        <a :href="work.file" target="_blank">下载文件</a>
+      </div>
+      <Pagination
+        :total="workTotal"
+        :current-page="workPage"
+        :page-size="pageSize"
+        @page-change="handleWorkPageChange"
+      />
+    </CollapsibleSection>
+
+    
+
+    <!-- 编辑按钮 -->
+    <button @click="editProfile" class="edit-button">编辑资料</button>
+  </div>
+</template>
+
+<script>
+import { ref, watchEffect } from 'vue';
+import { useRouter } from 'vue-router';
+import CollapsibleSection from '@/components/CollapsibleSection.vue';
+import Pagination from '@/components/Pagination.vue';
+import { getTeacherById, getAwardById, getThesisById, getWorkById } from '@/api/Teacher';
+import { useAuthStore } from '@/store/modules/auth';
+
+export default {
+  name: 'Profile',
+  components: {
+    CollapsibleSection,
+    Pagination
+  },
+  setup() {
+    const router = useRouter();
+    const authStore = useAuthStore();
+    
+    const teacher = ref({});
+    const awards = ref([]);
+    const theses = ref([]);
+    const works = ref([]);
+
+    const pageSize = ref(5);
+    const awardPage = ref(1);
+    const thesisPage = ref(1);
+    const workPage = ref(1);
+
+    const awardTotal = ref(0);
+    const thesisTotal = ref(0);
+    const workTotal = ref(0);
+
+    const teacherId = authStore.user.id;
+    const token = authStore.token;
+
+    const fetchTeacherData = async () => {
+      try {
+        teacher.value = await getTeacherById(teacherId, token);
+      } catch (error) {
+        console.error('获取教师信息失败:', error);
+      }
+    };
+
+    const fetchAwards = async () => {
+      try {
+        const response = await getAwardById(teacherId, token);
+        awards.value = response.records || [];
+        awardTotal.value = response.total || 0;
+      } catch (error) {
+        console.error('获取获奖信息失败:', error);
+      }
+    };
+
+    const fetchTheses = async () => {
+      try {
+        const response = await getThesisById(teacherId, token);
+        theses.value = response.records || [];
+        thesisTotal.value = response.total || 0;
+      } catch (error) {
+        console.error('获取论文信息失败:', error);
+      }
+    };
+
+    const fetchWorks = async () => {
+      try {
+        const response = await getWorkById(teacherId, token);
+        works.value = response.records || [];
+        workTotal.value = response.total || 0;
+      } catch (error) {
+        console.error('获取著作信息失败:', error);
+      }
+    };
+
+    const handleAwardPageChange = (page) => {
+      awardPage.value = page;
+      fetchAwards();
+    };
+
+    const handleThesisPageChange = (page) => {
+      thesisPage.value = page;
+      fetchTheses();
+    };
+
+    const handleWorkPageChange = (page) => {
+      workPage.value = page;
+      fetchWorks();
+    };
+
+    const editProfile = () => {
+      router.push({ name: 'EditProfile' });
+    };
+
+    
+    watchEffect(() => {
+      if (teacherId && token) {
+        fetchTeacherData();
+        fetchAwards();
+        fetchTheses();
+        fetchWorks();
+      }
+    });
+
+    return {
+      teacher,
+      awards,
+      theses,
+      works,
+      awardPage,
+      thesisPage,
+      workPage,
+      pageSize,
+      awardTotal,
+      thesisTotal,
+      workTotal,
+      handleAwardPageChange,
+      handleThesisPageChange,
+      handleWorkPageChange,
+      editProfile
+    };
+  }
+}
+</script>
+
+<style scoped>
+.profile {
+  padding: 20px;
+}
+
+.basic-info {
+  display: flex;
+  align-items: flex-start;
+  margin-bottom: 20px;
+}
+
+.profile-image {
+  width: 100px;
+  height: 100px;
+  border-radius: 50%;
+  margin-right: 20px;
+}
+
+.award-item, .thesis-item, .work-item, .software-item {
+  margin-bottom: 10px;
+}
+
+.award-image, .thesis-image, .work-image {
+  max-width: 200px;
+  margin-top: 10px;
+}
+
+.edit-button, .upload-button {
+  margin-top: 20px;
+  padding: 10px 20px;
+  background-color: #4CAF50;
+  color: white;
+  border: none;
+  cursor: pointer;
+  text-decoration: none;
+  display: inline-block;
+}
+
+.edit-button:hover, .upload-button:hover {
+  background-color: #45a049;
+}
+</style>
+  

+ 260 - 0
src/views/Register.vue

@@ -0,0 +1,260 @@
+<template>
+  <div class="register-container">
+    <div class="register-card">
+      <h2 class="register-title">注册新账号</h2>
+      <form @submit.prevent="handleRegister" class="register-form">
+        <div class="form-group">
+          <label for="username">用户名</label>
+          <input
+            id="username"
+            v-model="username"
+            type="text"
+            required
+            placeholder="请输入用户名"
+          />
+        </div>
+        <div class="form-group">
+          <label for="name">姓名</label>
+          <input
+            id="name"
+            v-model="name"
+            type="text"
+            required
+            placeholder="请输入姓名"
+          />
+        </div>
+        <div class="form-group">
+          <label for="password">密码</label>
+          <input
+            id="password"
+            v-model="password"
+            type="password"
+            required
+            placeholder="请输入密码"
+          />
+        </div>
+        <div class="form-group">
+          <label for="confirmPassword">确认密码</label>
+          <input
+            id="confirmPassword"
+            v-model="confirmPassword"
+            type="password"
+            required
+            placeholder="请再次输入密码"
+          />
+        </div>
+        <div class="form-group">
+          <label for="phone">手机号</label>
+          <input
+            id="phone"
+            v-model="phone"
+            type="tel"
+            required
+            placeholder="请输入手机号"
+          />
+        </div>
+        <div class="form-group captcha-group">
+          <label for="captcha">验证码</label>
+          <input
+            id="captcha"
+            v-model="captcha"
+            type="text"
+            required
+            placeholder="请输入验证码"
+          />
+          <div class="captcha-image" @click="refreshCaptcha" v-html="captchaImage"></div>
+        </div>
+        <button type="submit" class="register-button">注册</button>
+      </form>
+      <div class="links">
+        <router-link to="/login">已有账号?立即登录</router-link>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { userRegisterPost } from '@/api/Users';
+
+export default {
+  name: 'Register',
+  setup() {
+    const router = useRouter();
+    const username = ref('');
+    const name = ref('');
+    const password = ref('');
+    const confirmPassword = ref('');
+    const phone = ref('');
+    const captcha = ref('');
+    const captchaImage = ref('');
+
+    const generateCaptcha = () => {
+      // 生成随机验证码的逻辑
+      const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+      let result = '';
+      for (let i = 0; i < 4; i++) {
+        result += characters.charAt(Math.floor(Math.random() * characters.length));
+      }
+      return result;
+    };
+
+    const refreshCaptcha = () => {
+      const newCaptcha = generateCaptcha();
+      captchaImage.value = `<svg width="100" height="30" xmlns="http://www.w3.org/2000/svg">
+        <text x="10" y="25" font-family="Arial" font-size="20" fill="black">${newCaptcha}</text>
+      </svg>`;
+    };
+
+    refreshCaptcha(); // 初始化验证码
+
+    const handleRegister = async () => {
+      if (password.value !== confirmPassword.value) {
+        alert('密码和确认密码不匹配');
+        return;
+      }
+      // 这里应该添加验证码检查的逻辑
+      try {
+        await userRegisterPost({
+          username: username.value,
+          name: name.value,
+          password: password.value,
+          confirmPassword:password.value,
+          phoneNumber: phone.value,
+          captcha: captcha.value
+        });
+        alert('注册申请已提交,请等待管理员审核');
+        router.push('/login');
+      } catch (error) {
+        alert('注册失败:' + error.message);
+      }
+    };
+
+    return {
+      username,
+      name,
+      password,
+      confirmPassword,
+      phone,
+      captcha,
+      captchaImage,
+      handleRegister,
+      refreshCaptcha
+    };
+  }
+};
+</script>
+
+<style scoped>
+.register-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100vh;
+  padding: var(--spacing-6);
+  box-sizing: border-box;
+}
+
+.register-card {
+  background-color: var(--light-text-color);
+  border-radius: var(--border-radius-lg);
+  box-shadow: var(--shadow-lg);
+  padding: var(--spacing-8);
+  width: 100%;
+  max-width: 400px;
+  animation: fadeIn 0.5s ease-out;
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; transform: translateY(-20px); }
+  to { opacity: 1; transform: translateY(0); }
+}
+
+.register-title {
+  color: var(--primary-color);
+  font-size: var(--font-size-2xl);
+  margin-bottom: var(--spacing-6);
+  text-align: center;
+}
+
+.register-form {
+  display: flex;
+  flex-direction: column;
+  gap: var(--spacing-4);
+}
+
+.form-group {
+  display: flex;
+  flex-direction: column;
+}
+
+.form-group label {
+  margin-bottom: var(--spacing-2);
+  color: var(--text-color);
+  font-weight: bold;
+}
+
+.form-group input {
+  padding: var(--spacing-3);
+  border: 1px solid var(--border-color);
+  border-radius: var(--border-radius-base);
+  font-size: var(--font-size-base);
+  transition: all var(--transition-base);
+}
+
+.form-group input:focus {
+  outline: none;
+  border-color: var(--primary-color);
+  box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
+}
+
+.captcha-group {
+  display: flex;
+  align-items: center;
+}
+
+.captcha-group input {
+  flex: 1;
+  margin-right: var(--spacing-2);
+}
+
+.captcha-image {
+  cursor: pointer;
+  background-color: #f0f0f0;
+  padding: 5px;
+  border-radius: var(--border-radius-base);
+}
+
+.register-button {
+  background-color: var(--primary-color);
+  color: var(--light-text-color);
+  border: none;
+  border-radius: var(--border-radius-base);
+  padding: var(--spacing-3);
+  font-size: var(--font-size-base);
+  cursor: pointer;
+  transition: background-color var(--transition-base);
+}
+
+.register-button:hover {
+  background-color: var(--primary-dark);
+}
+
+.links {
+  margin-top: var(--spacing-6);
+  text-align: center;
+}
+
+.links a {
+  color: var(--primary-color);
+  text-decoration: none;
+  font-size: var(--font-size-sm);
+  transition: color var(--transition-base);
+}
+
+.links a:hover {
+  color: var(--primary-dark);
+  text-decoration: underline;
+}
+</style>

+ 170 - 0
src/views/SharedLibrary.vue

@@ -0,0 +1,170 @@
+<template>
+  <div class="shared-library">
+    <h1>共享资料库</h1>
+    
+    <div class="search-bar">
+      <input v-model="searchQuery" @input="handleSearch" placeholder="搜索文件名...">
+    </div>
+
+    <div v-if="isTeacher" class="upload-section">
+      <button @click="showUploadModal = true">上传文件</button>
+    </div>
+
+    <div class="file-list">
+      <h2>所有用户可见</h2>
+      <FileItem 
+        v-for="file in publicFiles" 
+        :key="file.id" 
+        :file="file"
+        @download="downloadFile"
+      />
+
+      <h2 v-if="isLoggedIn">仅登录用户可见</h2>
+      <FileItem 
+        v-for="file in privateFiles" 
+        :key="file.id" 
+        :file="file"
+        @download="downloadFile"
+      />
+    </div>
+
+    <Pagination 
+      :total="totalFiles" 
+      :per-page="perPage" 
+      :current-page="currentPage" 
+      @page-changed="handlePageChange"
+    />
+
+    <UploadModal 
+      v-if="showUploadModal" 
+      @close="showUploadModal = false"
+      @upload="handleFileUpload"
+    />
+  </div>
+</template>
+
+<script>
+import { ref, computed} from 'vue';
+import { useAuthStore } from '@/store/modules/auth';
+import FileItem from '@/components/FileItem.vue';
+import Pagination from '@/components/Pagination.vue';
+import UploadModal from '@/components/UploadModal.vue';
+import { getResourcesPage, uploadResource } from '@/api/Resources';
+import { userDownloadGet } from '@/api/Users';
+
+export default {
+  name: 'SharedLibrary',
+  components: {
+    FileItem,
+    Pagination,
+    UploadModal
+  },
+  setup() {
+    const authStore = useAuthStore();
+    const searchQuery = ref('');
+    const resources = ref([]);
+    const currentPage = ref(1);
+    const pageSize = ref(10);
+    const showUploadModal = ref(false);
+
+    const isLoggedIn = computed(() => authStore.getIsLoggedIn);
+    const isTeacher = computed(() => authStore.getIsTeacher);
+
+    const publicFiles = computed(() => 
+      resources.value.filter(file => file.visibility === 'public')
+    );
+    
+    const privateFiles = computed(() => 
+      isLoggedIn.value ? resources.value.filter(file => file.visibility === 'private') : []
+    );
+
+    const totalFiles = computed(() => resources.value.length);
+
+    const handleSearch = () => {
+      fetchResources();
+    };
+
+    const handlePageChange = (page) => {
+      currentPage.value = page;
+      fetchResources();
+    };
+
+    const handleFileUpload = async (fileData) => {
+      try {
+        await uploadResource(fileData);
+        showUploadModal.value = false;
+        fetchResources();
+      } catch (error) {
+        console.error('Failed to upload file:', error);
+      }
+    };
+
+    const fetchResources = async () => {
+      try {
+        const response = await getResourcesPage(currentPage.value, pageSize.value, searchQuery.value);
+        resources.value = response.data;
+      } catch (error) {
+        console.error('Failed to fetch resources:', error);
+      }
+    };
+
+    const downloadFile = async (id, status) => {
+      try {
+        await userDownloadGet(id, status);
+        // 处理文件下载
+      } catch (error) {
+        console.error('Failed to download file:', error);
+      }
+    };
+
+  
+
+    return {
+      searchQuery,
+      publicFiles,
+      privateFiles,
+      totalFiles,
+      currentPage,
+      pageSize,
+      isLoggedIn,
+      isTeacher,
+      showUploadModal,
+      handleSearch,
+      handlePageChange,
+      handleFileUpload,
+      downloadFile
+    };
+  }
+};
+</script>
+
+
+<style scoped>
+.shared-library {
+  max-width: 1000px;
+  margin: 0 auto;
+  padding: 20px;
+}
+
+.search-bar {
+  margin-bottom: 20px;
+}
+
+.search-bar input {
+  width: 100%;
+  padding: 10px;
+  font-size: 16px;
+}
+
+.upload-section {
+  margin-bottom: 20px;
+}
+
+.file-list {
+  margin-bottom: 20px;
+}
+
+h1, h2 {
+  margin-bottom: 15px;
+}
+</style>

+ 305 - 0
src/views/TeacherDetail.vue

@@ -0,0 +1,305 @@
+<template>
+  <div class="teacher-detail">
+    <header class="teacher-header">
+      <img :src="teacher.image" :alt="teacher.name" class="profile-image">
+      <h1>{{ teacher.name }}</h1>
+    </header>
+    
+    <section class="basic-info">
+      <h2>基本信息</h2>
+      <p><strong>简介:</strong> {{ teacher.profile }}</p>
+      <p><strong>本科课程:</strong> {{ teacher.undergraduateCourse }}</p>
+      <p><strong>研究生课程:</strong> {{ teacher.graduateCourse }}</p>
+      <p><strong>研究领域:</strong> {{ teacher.researchField?.information }}</p>
+      <p><strong>邮箱:</strong> {{ teacher.email }}</p>
+      <p><strong>电话:</strong> {{ teacher.phoneNumber }}</p>
+    </section>
+
+    <section class="additional-info">
+      <CollapsibleSection title="专利" :expanded="expandedSections.patents">
+        <div v-for="patent in teacher.patent" :key="patent.patentNum" class="info-item">
+          <p><strong>专利号:</strong> {{ patent.patentNum }}</p>
+          <p><strong>授权号:</strong> {{ patent.authorizationNum }}</p>
+          <p><strong>证书号:</strong> {{ patent.certificateNum }}</p>
+          <p><strong>日期:</strong> {{ patent.date }}</p>
+        </div>
+        <Pagination
+          :current-page="patentPage"
+          :total="patentTotal"
+          :page-size="pageSize"
+          @page-change="handlePatentPageChange"
+        />
+      </CollapsibleSection>
+
+      <CollapsibleSection title="获奖" :expanded="expandedSections.awards">
+        <div v-for="award in awards" :key="award.id" class="info-item">
+          <p><strong>名称:</strong> {{ award.name }}</p>
+          <p><strong>时间:</strong> {{ award.time }}</p>
+          <img :src="award.image" :alt="award.name" class="info-image">
+        </div>
+        <Pagination
+          :current-page="awardPage"
+          :total="awardTotal"
+          :page-size="pageSize"
+          @page-change="handleAwardPageChange"
+        />
+      </CollapsibleSection>
+
+      <CollapsibleSection title="论文" :expanded="expandedSections.theses">
+        <div v-for="thesis in theses" :key="thesis.id" class="info-item">
+          <p><strong>名称:</strong> {{ thesis.name }}</p>
+          <p><strong>发表时间:</strong> {{ thesis.time }}</p>
+          <a :href="thesis.website" target="_blank" class="info-link">查看详情</a>
+          <img :src="thesis.image" :alt="thesis.name" class="info-image">
+          <a :href="thesis.file" target="_blank" class="info-link">下载文件</a>
+        </div>
+        <Pagination
+          :current-page="thesisPage"
+          :total="thesisTotal"
+          :page-size="pageSize"
+          @page-change="handleThesisPageChange"
+        />
+      </CollapsibleSection>
+
+      <CollapsibleSection title="著作" :expanded="expandedSections.works">
+        <div v-for="work in works" :key="work.id" class="info-item">
+          <p><strong>名称:</strong> {{ work.name }}</p>
+          <p><strong>出版社:</strong> {{ work.press }}</p>
+          <p><strong>出版时间:</strong> {{ work.time }}</p>
+          <img :src="work.image" :alt="work.name" class="info-image">
+          <a :href="work.file" target="_blank" class="info-link">下载文件</a>
+        </div>
+        <Pagination
+          :current-page="workPage"
+          :total="workTotal"
+          :page-size="pageSize"
+          @page-change="handleWorkPageChange"
+        />
+      </CollapsibleSection>
+    </section>
+  </div>
+</template>
+
+<script>
+import { ref, reactive, watch } from 'vue';
+import { useRoute } from 'vue-router';
+import { getTeacherById, getAwardById, getThesisById, getWorkById } from '@/api/Teacher';
+import CollapsibleSection from '@/components/CollapsibleSection.vue';
+import Pagination from '@/components/Pagination.vue';
+
+export default {
+  name: 'TeacherDetail',
+  components: {
+    CollapsibleSection,
+    Pagination
+  },
+  setup() {
+    const route = useRoute();
+    const teacherId = ref(route.params.id);
+    
+    const teacher = ref({});
+    const awards = ref([]);
+    const theses = ref([]);
+    const works = ref([]);
+    
+    const pageSize = ref(10);
+    const awardPage = ref(1);
+    const awardTotal = ref(0);
+    const thesisPage = ref(1);
+    const thesisTotal = ref(0);
+    const workPage = ref(1);
+    const workTotal = ref(0);
+    const patentPage = ref(1);
+    const patentTotal = ref(0);
+
+    const expandedSections = reactive({
+      patents: false,
+      awards: false,
+      theses: false,
+      works: false
+    });
+
+    const fetchTeacherData = async () => {
+      try {
+        teacher.value = await getTeacherById(teacherId.value);
+        patentTotal.value = teacher.value.patent ? teacher.value.patent.length : 0;
+      } catch (error) {
+        console.error('获取教师信息失败:', error);
+      }
+    };
+
+    const fetchAwards = async () => {
+      try {
+        const response = await getAwardById(teacherId.value, awardPage.value, pageSize.value);
+        awards.value = response.records;
+        awardTotal.value = response.total;
+      } catch (error) {
+        console.error('获取获奖信息失败:', error);
+      }
+    };
+
+    const fetchTheses = async () => {
+      try {
+        const response = await getThesisById(teacherId.value, thesisPage.value, pageSize.value);
+        theses.value = response.records;
+        thesisTotal.value = response.total;
+      } catch (error) {
+        console.error('获取论文信息失败:', error);
+      }
+    };
+
+    const fetchWorks = async () => {
+      try {
+        const response = await getWorkById(teacherId.value, workPage.value, pageSize.value);
+        works.value = response.records;
+        workTotal.value = response.total;
+      } catch (error) {
+        console.error('获取著作信息失败:', error);
+      }
+    };
+
+    const handleAwardPageChange = (page) => {
+      awardPage.value = page;
+      fetchAwards();
+    };
+
+    const handleThesisPageChange = (page) => {
+      thesisPage.value = page;
+      fetchTheses();
+    };
+
+    const handleWorkPageChange = (page) => {
+      workPage.value = page;
+      fetchWorks();
+    };
+
+    const handlePatentPageChange = (page) => {
+      patentPage.value = page;
+      teacher.value.patent = teacher.value.patent.slice((page - 1) * pageSize.value, page * pageSize.value);
+    };
+
+    watch(teacherId, () => {
+      fetchTeacherData();
+      fetchAwards();
+      fetchTheses();
+      fetchWorks();
+    }, { immediate: true });
+
+    return {
+      teacher,
+      awards,
+      theses,
+      works,
+      pageSize,
+      awardPage,
+      awardTotal,
+      thesisPage,
+      thesisTotal,
+      workPage,
+      workTotal,
+      patentPage,
+      patentTotal,
+      expandedSections,
+      handleAwardPageChange,
+      handleThesisPageChange,
+      handleWorkPageChange,
+      handlePatentPageChange
+    };
+  }
+};
+</script>
+
+<style scoped>
+.teacher-detail {
+  max-width: 1000px;
+  margin: 0 auto;
+  padding: 2rem;
+  background-color: #f8f9fa;
+  border-radius: 10px;
+  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+}
+
+.teacher-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 2rem;
+}
+
+.profile-image {
+  width: 150px;
+  height: 150px;
+  object-fit: cover;
+  border-radius: 50%;
+  margin-right: 2rem;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+h1 {
+  font-size: 2.5rem;
+  color: #2c3e50;
+  margin: 0;
+}
+
+h2 {
+  font-size: 1.8rem;
+  color: #34495e;
+  margin-top: 0;
+  margin-bottom: 1rem;
+  border-bottom: 2px solid #3498db;
+  padding-bottom: 0.5rem;
+}
+
+.basic-info, .additional-info {
+  background-color: #ffffff;
+  padding: 1.5rem;
+  border-radius: 8px;
+  margin-bottom: 2rem;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+}
+
+.info-item {
+  margin-bottom: 1.5rem;
+  padding-bottom: 1.5rem;
+  border-bottom: 1px solid #e0e0e0;
+}
+
+.info-item:last-child {
+  border-bottom: none;
+}
+
+.info-image {
+  max-width: 100%;
+  height: auto;
+  border-radius: 4px;
+  margin-top: 1rem;
+}
+
+.info-link {
+  display: inline-block;
+  margin-top: 0.5rem;
+  color: #3498db;
+  text-decoration: none;
+  transition: color 0.3s ease;
+}
+
+.info-link:hover {
+  color: #2980b9;
+}
+
+p {
+  margin-bottom: 0.5rem;
+  line-height: 1.6;
+}
+
+strong {
+  color: #2c3e50;
+}
+
+.pagination {
+  display: flex;
+  justify-content: center;
+  margin-top: 1.5rem;
+}
+
+/* Add any additional styles for the CollapsibleSection and Pagination components here */
+</style>

+ 26 - 0
vite.config.js

@@ -0,0 +1,26 @@
+import { defineConfig } from 'vite';
+import vue from '@vitejs/plugin-vue';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+// 获取当前文件的目录名
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+export default defineConfig({
+  plugins: [vue()],
+  resolve: {
+    alias: {
+      '@': path.resolve(__dirname, './src'),
+    },
+  },
+  server: {
+    proxy: {
+      '/api': {
+        target: 'http://10.113.233.180:8080',
+        changeOrigin: true,
+        rewrite: (path) => path.replace(/^\/api/, '')
+      }
+    }
+  }   
+});