完成上传下载连接,公告管理与详情页面,求种区页面,轮播图折扣显示,修改部分bug

Change-Id: I86fc294e32911cb3426a8b16f90aca371f975c11
diff --git a/package-lock.json b/package-lock.json
index 556ed37..a27a56c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
       "version": "0.1.0",
       "dependencies": {
         "@testing-library/dom": "^10.4.0",
+        "antd": "^5.25.3",
         "axios": "^1.8.4",
         "buffer": "^6.0.3",
         "https-browserify": "^1.0.0",
@@ -73,6 +74,103 @@
         "node": ">=6.0.0"
       }
     },
+    "node_modules/@ant-design/colors": {
+      "version": "7.2.1",
+      "resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-7.2.1.tgz",
+      "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/fast-color": "^2.0.6"
+      }
+    },
+    "node_modules/@ant-design/cssinjs": {
+      "version": "1.23.0",
+      "resolved": "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-1.23.0.tgz",
+      "integrity": "sha512-7GAg9bD/iC9ikWatU9ym+P9ugJhi/WbsTWzcKN6T4gU0aehsprtke1UAaaSxxkjjmkJb3llet/rbUSLPgwlY4w==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.1",
+        "@emotion/hash": "^0.8.0",
+        "@emotion/unitless": "^0.7.5",
+        "classnames": "^2.3.1",
+        "csstype": "^3.1.3",
+        "rc-util": "^5.35.0",
+        "stylis": "^4.3.4"
+      },
+      "peerDependencies": {
+        "react": ">=16.0.0",
+        "react-dom": ">=16.0.0"
+      }
+    },
+    "node_modules/@ant-design/cssinjs-utils": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmmirror.com/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz",
+      "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/cssinjs": "^1.21.0",
+        "@babel/runtime": "^7.23.2",
+        "rc-util": "^5.38.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@ant-design/fast-color": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmmirror.com/@ant-design/fast-color/-/fast-color-2.0.6.tgz",
+      "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.7"
+      },
+      "engines": {
+        "node": ">=8.x"
+      }
+    },
+    "node_modules/@ant-design/icons": {
+      "version": "5.6.1",
+      "resolved": "https://registry.npmmirror.com/@ant-design/icons/-/icons-5.6.1.tgz",
+      "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^7.0.0",
+        "@ant-design/icons-svg": "^4.4.0",
+        "@babel/runtime": "^7.24.8",
+        "classnames": "^2.2.6",
+        "rc-util": "^5.31.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "peerDependencies": {
+        "react": ">=16.0.0",
+        "react-dom": ">=16.0.0"
+      }
+    },
+    "node_modules/@ant-design/icons-svg": {
+      "version": "4.4.2",
+      "resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
+      "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
+      "license": "MIT"
+    },
+    "node_modules/@ant-design/react-slick": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-1.1.2.tgz",
+      "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.4",
+        "classnames": "^2.2.5",
+        "json2mq": "^0.2.0",
+        "resize-observer-polyfill": "^1.5.1",
+        "throttle-debounce": "^5.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0"
+      }
+    },
     "node_modules/@babel/code-frame": {
       "version": "7.27.1",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -2340,6 +2438,18 @@
         "postcss-selector-parser": "^6.0.10"
       }
     },
+    "node_modules/@emotion/hash": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz",
+      "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
+      "license": "MIT"
+    },
+    "node_modules/@emotion/unitless": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.7.5.tgz",
+      "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
+      "license": "MIT"
+    },
     "node_modules/@eslint-community/eslint-utils": {
       "version": "4.7.0",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
@@ -3330,6 +3440,155 @@
         "node": ">= 8"
       }
     },
+    "node_modules/@rc-component/async-validator": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.0.4.tgz",
+      "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.4"
+      },
+      "engines": {
+        "node": ">=14.x"
+      }
+    },
+    "node_modules/@rc-component/color-picker": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/@rc-component/color-picker/-/color-picker-2.0.1.tgz",
+      "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/fast-color": "^2.0.6",
+        "@babel/runtime": "^7.23.6",
+        "classnames": "^2.2.6",
+        "rc-util": "^5.38.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/context": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmmirror.com/@rc-component/context/-/context-1.4.0.tgz",
+      "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "rc-util": "^5.27.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/mini-decimal": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz",
+      "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      }
+    },
+    "node_modules/@rc-component/mutate-observer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz",
+      "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.0",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.24.4"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/portal": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/@rc-component/portal/-/portal-1.1.2.tgz",
+      "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.0",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.24.4"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/qrcode": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/@rc-component/qrcode/-/qrcode-1.0.0.tgz",
+      "integrity": "sha512-L+rZ4HXP2sJ1gHMGHjsg9jlYBX/SLN2D6OxP9Zn3qgtpMWtO2vUfxVFwiogHpAIqs54FnALxraUy/BCO1yRIgg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.7",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.38.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/tour": {
+      "version": "1.15.1",
+      "resolved": "https://registry.npmmirror.com/@rc-component/tour/-/tour-1.15.1.tgz",
+      "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.0",
+        "@rc-component/portal": "^1.0.0-9",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.24.4"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/@rc-component/trigger": {
+      "version": "2.2.6",
+      "resolved": "https://registry.npmmirror.com/@rc-component/trigger/-/trigger-2.2.6.tgz",
+      "integrity": "sha512-/9zuTnWwhQ3S3WT1T8BubuFTT46kvnXgaERR9f4BTKyn61/wpf/BvbImzYBubzJibU707FxwbKszLlHjcLiv1Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.23.2",
+        "@rc-component/portal": "^1.1.0",
+        "classnames": "^2.3.2",
+        "rc-motion": "^2.0.0",
+        "rc-resize-observer": "^1.3.1",
+        "rc-util": "^5.44.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
     "node_modules/@remix-run/router": {
       "version": "1.23.0",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/@remix-run/router/-/router-1.23.0.tgz",
@@ -4930,6 +5189,71 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
+    "node_modules/antd": {
+      "version": "5.25.3",
+      "resolved": "https://registry.npmmirror.com/antd/-/antd-5.25.3.tgz",
+      "integrity": "sha512-tBBcAFRjmWM3sitxrL/FEbQL+MTQntYY5bGa5c1ZZZHXWCynkhS3Ch/gy25mGMUY1M/9Uw3pH029v/RGht1x3w==",
+      "license": "MIT",
+      "dependencies": {
+        "@ant-design/colors": "^7.2.1",
+        "@ant-design/cssinjs": "^1.23.0",
+        "@ant-design/cssinjs-utils": "^1.1.3",
+        "@ant-design/fast-color": "^2.0.6",
+        "@ant-design/icons": "^5.6.1",
+        "@ant-design/react-slick": "~1.1.2",
+        "@babel/runtime": "^7.26.0",
+        "@rc-component/color-picker": "~2.0.1",
+        "@rc-component/mutate-observer": "^1.1.0",
+        "@rc-component/qrcode": "~1.0.0",
+        "@rc-component/tour": "~1.15.1",
+        "@rc-component/trigger": "^2.2.6",
+        "classnames": "^2.5.1",
+        "copy-to-clipboard": "^3.3.3",
+        "dayjs": "^1.11.11",
+        "rc-cascader": "~3.34.0",
+        "rc-checkbox": "~3.5.0",
+        "rc-collapse": "~3.9.0",
+        "rc-dialog": "~9.6.0",
+        "rc-drawer": "~7.2.0",
+        "rc-dropdown": "~4.2.1",
+        "rc-field-form": "~2.7.0",
+        "rc-image": "~7.12.0",
+        "rc-input": "~1.8.0",
+        "rc-input-number": "~9.5.0",
+        "rc-mentions": "~2.20.0",
+        "rc-menu": "~9.16.1",
+        "rc-motion": "^2.9.5",
+        "rc-notification": "~5.6.4",
+        "rc-pagination": "~5.1.0",
+        "rc-picker": "~4.11.3",
+        "rc-progress": "~4.0.0",
+        "rc-rate": "~2.13.1",
+        "rc-resize-observer": "^1.4.3",
+        "rc-segmented": "~2.7.0",
+        "rc-select": "~14.16.8",
+        "rc-slider": "~11.1.8",
+        "rc-steps": "~6.0.1",
+        "rc-switch": "~4.1.0",
+        "rc-table": "~7.50.5",
+        "rc-tabs": "~15.6.1",
+        "rc-textarea": "~1.10.0",
+        "rc-tooltip": "~6.4.0",
+        "rc-tree": "~5.13.1",
+        "rc-tree-select": "~5.27.0",
+        "rc-upload": "~4.9.0",
+        "rc-util": "^5.44.4",
+        "scroll-into-view-if-needed": "^3.1.0",
+        "throttle-debounce": "^5.0.2"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/ant-design"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
     "node_modules/any-promise": {
       "version": "1.3.0",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/any-promise/-/any-promise-1.3.0.tgz",
@@ -6011,6 +6335,12 @@
       "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
       "license": "MIT"
     },
+    "node_modules/classnames": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz",
+      "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+      "license": "MIT"
+    },
     "node_modules/clean-css": {
       "version": "5.3.3",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/clean-css/-/clean-css-5.3.3.tgz",
@@ -6285,6 +6615,12 @@
       "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
       "license": "MIT"
     },
+    "node_modules/compute-scroll-into-view": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
+      "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
+      "license": "MIT"
+    },
     "node_modules/concat-map": {
       "version": "0.0.1",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/concat-map/-/concat-map-0.0.1.tgz",
@@ -6349,6 +6685,15 @@
       "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
       "license": "MIT"
     },
+    "node_modules/copy-to-clipboard": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmmirror.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
+      "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
+      "license": "MIT",
+      "dependencies": {
+        "toggle-selection": "^1.0.6"
+      }
+    },
     "node_modules/core-js": {
       "version": "3.42.0",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/core-js/-/core-js-3.42.0.tgz",
@@ -6842,6 +7187,12 @@
       "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
       "license": "MIT"
     },
+    "node_modules/csstype": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
+      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+      "license": "MIT"
+    },
     "node_modules/damerau-levenshtein": {
       "version": "1.0.8",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -6923,6 +7274,12 @@
         "url": "https://github.com/sponsors/kossnocorp"
       }
     },
+    "node_modules/dayjs": {
+      "version": "1.11.13",
+      "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
+      "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
+      "license": "MIT"
+    },
     "node_modules/debug": {
       "version": "4.4.0",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/debug/-/debug-4.4.0.tgz",
@@ -12117,6 +12474,15 @@
       "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
       "license": "MIT"
     },
+    "node_modules/json2mq": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/json2mq/-/json2mq-0.2.0.tgz",
+      "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
+      "license": "MIT",
+      "dependencies": {
+        "string-convert": "^0.2.0"
+      }
+    },
     "node_modules/json5": {
       "version": "2.2.3",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/json5/-/json5-2.2.3.tgz",
@@ -15084,6 +15450,618 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/rc-cascader": {
+      "version": "3.34.0",
+      "resolved": "https://registry.npmmirror.com/rc-cascader/-/rc-cascader-3.34.0.tgz",
+      "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.25.7",
+        "classnames": "^2.3.1",
+        "rc-select": "~14.16.2",
+        "rc-tree": "~5.13.0",
+        "rc-util": "^5.43.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-checkbox": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmmirror.com/rc-checkbox/-/rc-checkbox-3.5.0.tgz",
+      "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.25.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-collapse": {
+      "version": "3.9.0",
+      "resolved": "https://registry.npmmirror.com/rc-collapse/-/rc-collapse-3.9.0.tgz",
+      "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "2.x",
+        "rc-motion": "^2.3.4",
+        "rc-util": "^5.27.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-dialog": {
+      "version": "9.6.0",
+      "resolved": "https://registry.npmmirror.com/rc-dialog/-/rc-dialog-9.6.0.tgz",
+      "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "@rc-component/portal": "^1.0.0-8",
+        "classnames": "^2.2.6",
+        "rc-motion": "^2.3.0",
+        "rc-util": "^5.21.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-drawer": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmmirror.com/rc-drawer/-/rc-drawer-7.2.0.tgz",
+      "integrity": "sha512-9lOQ7kBekEJRdEpScHvtmEtXnAsy+NGDXiRWc2ZVC7QXAazNVbeT4EraQKYwCME8BJLa8Bxqxvs5swwyOepRwg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.23.9",
+        "@rc-component/portal": "^1.1.1",
+        "classnames": "^2.2.6",
+        "rc-motion": "^2.6.1",
+        "rc-util": "^5.38.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-dropdown": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmmirror.com/rc-dropdown/-/rc-dropdown-4.2.1.tgz",
+      "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "^2.2.6",
+        "rc-util": "^5.44.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.11.0",
+        "react-dom": ">=16.11.0"
+      }
+    },
+    "node_modules/rc-field-form": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmmirror.com/rc-field-form/-/rc-field-form-2.7.0.tgz",
+      "integrity": "sha512-hgKsCay2taxzVnBPZl+1n4ZondsV78G++XVsMIJCAoioMjlMQR9YwAp7JZDIECzIu2Z66R+f4SFIRrO2DjDNAA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.0",
+        "@rc-component/async-validator": "^5.0.3",
+        "rc-util": "^5.32.2"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-image": {
+      "version": "7.12.0",
+      "resolved": "https://registry.npmmirror.com/rc-image/-/rc-image-7.12.0.tgz",
+      "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.2",
+        "@rc-component/portal": "^1.0.2",
+        "classnames": "^2.2.6",
+        "rc-dialog": "~9.6.0",
+        "rc-motion": "^2.6.2",
+        "rc-util": "^5.34.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-input": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmmirror.com/rc-input/-/rc-input-1.8.0.tgz",
+      "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.1",
+        "classnames": "^2.2.1",
+        "rc-util": "^5.18.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.0.0",
+        "react-dom": ">=16.0.0"
+      }
+    },
+    "node_modules/rc-input-number": {
+      "version": "9.5.0",
+      "resolved": "https://registry.npmmirror.com/rc-input-number/-/rc-input-number-9.5.0.tgz",
+      "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "@rc-component/mini-decimal": "^1.0.1",
+        "classnames": "^2.2.5",
+        "rc-input": "~1.8.0",
+        "rc-util": "^5.40.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-mentions": {
+      "version": "2.20.0",
+      "resolved": "https://registry.npmmirror.com/rc-mentions/-/rc-mentions-2.20.0.tgz",
+      "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.22.5",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "^2.2.6",
+        "rc-input": "~1.8.0",
+        "rc-menu": "~9.16.0",
+        "rc-textarea": "~1.10.0",
+        "rc-util": "^5.34.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-menu": {
+      "version": "9.16.1",
+      "resolved": "https://registry.npmmirror.com/rc-menu/-/rc-menu-9.16.1.tgz",
+      "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "2.x",
+        "rc-motion": "^2.4.3",
+        "rc-overflow": "^1.3.1",
+        "rc-util": "^5.27.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-motion": {
+      "version": "2.9.5",
+      "resolved": "https://registry.npmmirror.com/rc-motion/-/rc-motion-2.9.5.tgz",
+      "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.1",
+        "classnames": "^2.2.1",
+        "rc-util": "^5.44.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-notification": {
+      "version": "5.6.4",
+      "resolved": "https://registry.npmmirror.com/rc-notification/-/rc-notification-5.6.4.tgz",
+      "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "2.x",
+        "rc-motion": "^2.9.0",
+        "rc-util": "^5.20.1"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-overflow": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmmirror.com/rc-overflow/-/rc-overflow-1.4.1.tgz",
+      "integrity": "sha512-3MoPQQPV1uKyOMVNd6SZfONi+f3st0r8PksexIdBTeIYbMX0Jr+k7pHEDvsXtR4BpCv90/Pv2MovVNhktKrwvw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.1",
+        "classnames": "^2.2.1",
+        "rc-resize-observer": "^1.0.0",
+        "rc-util": "^5.37.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-pagination": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmmirror.com/rc-pagination/-/rc-pagination-5.1.0.tgz",
+      "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.3.2",
+        "rc-util": "^5.38.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-picker": {
+      "version": "4.11.3",
+      "resolved": "https://registry.npmmirror.com/rc-picker/-/rc-picker-4.11.3.tgz",
+      "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.24.7",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "^2.2.1",
+        "rc-overflow": "^1.3.2",
+        "rc-resize-observer": "^1.4.0",
+        "rc-util": "^5.43.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "date-fns": ">= 2.x",
+        "dayjs": ">= 1.x",
+        "luxon": ">= 3.x",
+        "moment": ">= 2.x",
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      },
+      "peerDependenciesMeta": {
+        "date-fns": {
+          "optional": true
+        },
+        "dayjs": {
+          "optional": true
+        },
+        "luxon": {
+          "optional": true
+        },
+        "moment": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/rc-progress": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmmirror.com/rc-progress/-/rc-progress-4.0.0.tgz",
+      "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.2.6",
+        "rc-util": "^5.16.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-rate": {
+      "version": "2.13.1",
+      "resolved": "https://registry.npmmirror.com/rc-rate/-/rc-rate-2.13.1.tgz",
+      "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.2.5",
+        "rc-util": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-resize-observer": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmmirror.com/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz",
+      "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.20.7",
+        "classnames": "^2.2.1",
+        "rc-util": "^5.44.1",
+        "resize-observer-polyfill": "^1.5.1"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-segmented": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmmirror.com/rc-segmented/-/rc-segmented-2.7.0.tgz",
+      "integrity": "sha512-liijAjXz+KnTRVnxxXG2sYDGd6iLL7VpGGdR8gwoxAXy2KglviKCxLWZdjKYJzYzGSUwKDSTdYk8brj54Bn5BA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.1",
+        "classnames": "^2.2.1",
+        "rc-motion": "^2.4.4",
+        "rc-util": "^5.17.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.0.0",
+        "react-dom": ">=16.0.0"
+      }
+    },
+    "node_modules/rc-select": {
+      "version": "14.16.8",
+      "resolved": "https://registry.npmmirror.com/rc-select/-/rc-select-14.16.8.tgz",
+      "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "@rc-component/trigger": "^2.1.1",
+        "classnames": "2.x",
+        "rc-motion": "^2.0.1",
+        "rc-overflow": "^1.3.1",
+        "rc-util": "^5.16.1",
+        "rc-virtual-list": "^3.5.2"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-dom": "*"
+      }
+    },
+    "node_modules/rc-slider": {
+      "version": "11.1.8",
+      "resolved": "https://registry.npmmirror.com/rc-slider/-/rc-slider-11.1.8.tgz",
+      "integrity": "sha512-2gg/72YFSpKP+Ja5AjC5DPL1YnV8DEITDQrcc1eASrUYjl0esptaBVJBh5nLTXCCp15eD8EuGjwezVGSHhs9tQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.2.5",
+        "rc-util": "^5.36.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-steps": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmmirror.com/rc-steps/-/rc-steps-6.0.1.tgz",
+      "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.16.7",
+        "classnames": "^2.2.3",
+        "rc-util": "^5.16.1"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-switch": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmmirror.com/rc-switch/-/rc-switch-4.1.0.tgz",
+      "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.21.0",
+        "classnames": "^2.2.1",
+        "rc-util": "^5.30.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-table": {
+      "version": "7.50.5",
+      "resolved": "https://registry.npmmirror.com/rc-table/-/rc-table-7.50.5.tgz",
+      "integrity": "sha512-FDZu8aolhSYd3v9KOc3lZOVAU77wmRRu44R0Wfb8Oj1dXRUsloFaXMSl6f7yuWZUxArJTli7k8TEOX2mvhDl4A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "@rc-component/context": "^1.4.0",
+        "classnames": "^2.2.5",
+        "rc-resize-observer": "^1.1.0",
+        "rc-util": "^5.44.3",
+        "rc-virtual-list": "^3.14.2"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-tabs": {
+      "version": "15.6.1",
+      "resolved": "https://registry.npmmirror.com/rc-tabs/-/rc-tabs-15.6.1.tgz",
+      "integrity": "sha512-/HzDV1VqOsUWyuC0c6AkxVYFjvx9+rFPKZ32ejxX0Uc7QCzcEjTA9/xMgv4HemPKwzBNX8KhGVbbumDjnj92aA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.2",
+        "classnames": "2.x",
+        "rc-dropdown": "~4.2.0",
+        "rc-menu": "~9.16.0",
+        "rc-motion": "^2.6.2",
+        "rc-resize-observer": "^1.0.0",
+        "rc-util": "^5.34.1"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-textarea": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmmirror.com/rc-textarea/-/rc-textarea-1.10.0.tgz",
+      "integrity": "sha512-ai9IkanNuyBS4x6sOL8qu/Ld40e6cEs6pgk93R+XLYg0mDSjNBGey6/ZpDs5+gNLD7urQ14po3V6Ck2dJLt9SA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "^2.2.1",
+        "rc-input": "~1.8.0",
+        "rc-resize-observer": "^1.0.0",
+        "rc-util": "^5.27.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-tooltip": {
+      "version": "6.4.0",
+      "resolved": "https://registry.npmmirror.com/rc-tooltip/-/rc-tooltip-6.4.0.tgz",
+      "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.11.2",
+        "@rc-component/trigger": "^2.0.0",
+        "classnames": "^2.3.1",
+        "rc-util": "^5.44.3"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-tree": {
+      "version": "5.13.1",
+      "resolved": "https://registry.npmmirror.com/rc-tree/-/rc-tree-5.13.1.tgz",
+      "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.10.1",
+        "classnames": "2.x",
+        "rc-motion": "^2.0.1",
+        "rc-util": "^5.16.1",
+        "rc-virtual-list": "^3.5.1"
+      },
+      "engines": {
+        "node": ">=10.x"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-dom": "*"
+      }
+    },
+    "node_modules/rc-tree-select": {
+      "version": "5.27.0",
+      "resolved": "https://registry.npmmirror.com/rc-tree-select/-/rc-tree-select-5.27.0.tgz",
+      "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.25.7",
+        "classnames": "2.x",
+        "rc-select": "~14.16.2",
+        "rc-tree": "~5.13.0",
+        "rc-util": "^5.43.0"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-dom": "*"
+      }
+    },
+    "node_modules/rc-upload": {
+      "version": "4.9.2",
+      "resolved": "https://registry.npmmirror.com/rc-upload/-/rc-upload-4.9.2.tgz",
+      "integrity": "sha512-nHx+9rbd1FKMiMRYsqQ3NkXUv7COHPBo3X1Obwq9SWS6/diF/A0aJ5OHubvwUAIDs+4RMleljV0pcrNUc823GQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "classnames": "^2.2.5",
+        "rc-util": "^5.2.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-util": {
+      "version": "5.44.4",
+      "resolved": "https://registry.npmmirror.com/rc-util/-/rc-util-5.44.4.tgz",
+      "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.18.3",
+        "react-is": "^18.2.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
+    "node_modules/rc-util/node_modules/react-is": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
+      "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+      "license": "MIT"
+    },
+    "node_modules/rc-virtual-list": {
+      "version": "3.18.6",
+      "resolved": "https://registry.npmmirror.com/rc-virtual-list/-/rc-virtual-list-3.18.6.tgz",
+      "integrity": "sha512-TQ5SsutL3McvWmmxqQtMIbfeoE3dGjJrRSfKekgby7WQMpPIFvv4ghytp5Z0s3D8Nik9i9YNOCqHBfk86AwgAA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.20.0",
+        "classnames": "^2.2.6",
+        "rc-resize-observer": "^1.0.0",
+        "rc-util": "^5.36.0"
+      },
+      "engines": {
+        "node": ">=8.x"
+      },
+      "peerDependencies": {
+        "react": ">=16.9.0",
+        "react-dom": ">=16.9.0"
+      }
+    },
     "node_modules/react": {
       "version": "19.1.0",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/react/-/react-19.1.0.tgz",
@@ -17280,6 +18258,12 @@
       "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
       "license": "MIT"
     },
+    "node_modules/resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+      "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+      "license": "MIT"
+    },
     "node_modules/resolve": {
       "version": "1.22.10",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/resolve/-/resolve-1.22.10.tgz",
@@ -17700,6 +18684,15 @@
       "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
       "license": "MIT"
     },
+    "node_modules/scroll-into-view-if-needed": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
+      "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
+      "license": "MIT",
+      "dependencies": {
+        "compute-scroll-into-view": "^3.0.2"
+      }
+    },
     "node_modules/select-hose": {
       "version": "2.0.0",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/select-hose/-/select-hose-2.0.0.tgz",
@@ -18336,6 +19329,12 @@
         "safe-buffer": "~5.2.0"
       }
     },
+    "node_modules/string-convert": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz",
+      "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
+      "license": "MIT"
+    },
     "node_modules/string-length": {
       "version": "4.0.2",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/string-length/-/string-length-4.0.2.tgz",
@@ -18626,6 +19625,12 @@
         "postcss": "^8.2.15"
       }
     },
+    "node_modules/stylis": {
+      "version": "4.3.6",
+      "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.6.tgz",
+      "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
+      "license": "MIT"
+    },
     "node_modules/sucrase": {
       "version": "3.35.0",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/sucrase/-/sucrase-3.35.0.tgz",
@@ -19179,6 +20184,15 @@
       "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==",
       "license": "MIT"
     },
+    "node_modules/throttle-debounce": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
+      "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.22"
+      }
+    },
     "node_modules/thunky": {
       "version": "1.1.0",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/thunky/-/thunky-1.1.0.tgz",
@@ -19203,6 +20217,12 @@
         "node": ">=8.0"
       }
     },
+    "node_modules/toggle-selection": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmmirror.com/toggle-selection/-/toggle-selection-1.0.6.tgz",
+      "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
+      "license": "MIT"
+    },
     "node_modules/toidentifier": {
       "version": "1.0.1",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/toidentifier/-/toidentifier-1.0.1.tgz",
diff --git a/package.json b/package.json
index 866a01f..8091f3c 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
   ],
   "dependencies": {
     "@testing-library/dom": "^10.4.0",
+    "antd": "^5.25.3",
     "axios": "^1.8.4",
     "buffer": "^6.0.3",
     "https-browserify": "^1.0.0",
diff --git a/src/api/announcement.js b/src/api/announcement.js
new file mode 100644
index 0000000..6213d10
--- /dev/null
+++ b/src/api/announcement.js
@@ -0,0 +1,28 @@
+import { api } from './auth';
+
+// 获取所有公告
+export const getAnnouncements = () => {
+  return api.get('/announcement/list');
+};
+
+
+export const postAnnouncement = (data) => {
+  // 创建 FormData 对象
+  const formData = new FormData();
+  formData.append('title', data.title);
+  formData.append('content', data.content);
+
+  return api.post('/announcement/create', formData);
+}
+
+
+// 获取最新公告
+export const getLatestAnnouncements = () => {
+  return api.get('/announcement/latest');
+};
+
+// 获取公告详情
+export const getAnnouncementDetail = (id) => {
+  return api.get(`/announcement/${id}`);
+};
+
diff --git a/src/api/announcement.test.js b/src/api/announcement.test.js
new file mode 100644
index 0000000..bae9482
--- /dev/null
+++ b/src/api/announcement.test.js
@@ -0,0 +1,139 @@
+import MockAdapter from 'axios-mock-adapter';
+import { api } from './auth';
+import { getAnnouncements, getLatestAnnouncements, getAnnouncementDetail,postAnnouncement } from './announcement';
+
+describe('公告API', () => {
+  let mockAxios;
+
+  beforeEach(() => {
+    mockAxios = new MockAdapter(api);
+  });
+
+  afterEach(() => {
+    mockAxios.restore();
+  });
+
+describe('postAnnouncement - 发布公告', () => {
+  it('应该成功发布公告', async () => {
+    const mockData = {
+      title: '测试公告',
+      content: '测试内容'
+    };
+    
+    const mockResponse = {
+      code: 200,
+      message: '公告发布成功',
+      data: {
+        id: 123,
+        title: mockData.title,
+        content: mockData.content,
+        createTime: new Date().toISOString()
+      }
+    };
+  
+
+    mockAxios.onPost('/announcement/create').reply(200, mockResponse);
+
+    const response = await postAnnouncement(mockData);
+    expect(response.data).toEqual(mockResponse);
+    expect(response.data.code).toBe(200);
+    expect(response.data.data.title).toBe(mockData.title);
+    expect(response.data.data.content).toBe(mockData.content);
+  });
+});
+
+  describe('getAnnouncements - 获取所有公告', () => {
+    it('应该成功获取公告列表', async () => {
+      const mockResponse = {
+        code: 200,
+        data: {
+          announcements: [
+            { id: 1, title: '公告1', content: '内容1', createTime: '2023-01-01T00:00:00Z' },
+            { id: 2, title: '公告2', content: '内容2', createTime: '2023-01-01T00:00:00Z' }
+          ]
+        }
+      };
+      
+      mockAxios.onGet('/announcement/list').reply(200, mockResponse);
+
+      const response = await getAnnouncements();
+      expect(response.data).toEqual(mockResponse);
+      expect(response.data.data.announcements).toHaveLength(2);
+    });
+
+    
+  });
+
+  describe('getLatestAnnouncements - 获取最新公告', () => {
+    it('应该成功获取最新公告', async () => {
+      const mockResponse = {
+        code: 200,
+        data: {
+          announcements: [
+            { id: 1, title: '最新公告', content: '最新内容' }
+          ]
+        }
+      };
+      
+      mockAxios.onGet('/announcement/latest').reply(200, mockResponse);
+
+      const response = await getLatestAnnouncements();
+      expect(response.data).toEqual(mockResponse);
+      expect(response.data.data.announcements).toHaveLength(1);
+    });
+
+    it('应该正确处理服务器错误', async () => {
+      mockAxios.onGet('/announcement/latest').reply(500);
+      
+      await expect(getLatestAnnouncements()).rejects.toThrow();
+    });
+  });
+
+  describe('getAnnouncementDetail - 获取公告详情', () => {
+    it('应该成功获取公告详情', async () => {
+      const announcementId = 123;
+      const mockResponse = {
+        code: 200,
+        data: {
+          announcement: {
+            id: announcementId,
+            title: '详细公告',
+            content: '详细内容',
+            createTime: '2023-01-01T00:00:00Z'
+          }
+        }
+      };
+      
+      mockAxios.onGet(`/announcement/${announcementId}`).reply(200, mockResponse);
+
+      const response = await getAnnouncementDetail(announcementId);
+      expect(response.data).toEqual(mockResponse);
+      expect(response.data.data.announcement.id).toBe(announcementId);
+    });
+
+    it('应该处理公告不存在的情况', async () => {
+      const announcementId = 999;
+      const mockResponse = {
+        code: 404,
+        message: '公告不存在'
+      };
+      
+      mockAxios.onGet(`/announcement/${announcementId}`).reply(404, mockResponse);
+
+      try {
+        await getAnnouncementDetail(announcementId);
+    } catch (error) {
+        expect(error.response.status).toBe(404);
+        expect(error.response.data.code).toBe(404);
+        expect(error.response.data.message).toBe('公告不存在');
+    }
+    });
+
+    it('应该正确处理网络错误', async () => {
+      const announcementId = 456;
+      mockAxios.onGet(`/announcement/${announcementId}`).networkError();
+      
+      await expect(getAnnouncementDetail(announcementId)).rejects.toThrow();
+    });
+  });
+});
\ No newline at end of file
diff --git a/src/api/helpComment.js b/src/api/helpComment.js
index c1d00c0..2ff9ade 100644
--- a/src/api/helpComment.js
+++ b/src/api/helpComment.js
@@ -1,14 +1,14 @@
 import { api } from './auth';

 

-export const likePostComment = (commentId) => {

+export const likeHelpPostComment = (commentId) => {

   return api.post(`/help/comments/${commentId}/like`);

 };

 

-export const getCommentReplies = (commentId) => {

+export const getHelpCommentReplies = (commentId) => {

   return api.get(`/help/comments/${commentId}/replies`);

 };

 

-export const addCommentReply = (commentId, replyData) => {

+export const addHelpCommentReply = (commentId, replyData) => {

   const formData = new FormData();

   formData.append('authorId', replyData.authorId);

   formData.append('content', replyData.content);

@@ -21,7 +21,7 @@
   return api.post(`/help/comments/${commentId}/replies`, formData);

 };

 

-export const deleteComment = (commentId, authorId) => {

+export const deleteHelpComment = (commentId, authorId) => {

   return api.delete(`/help/comments/${commentId}`, {

     params: { authorId }

   });

diff --git a/src/api/helpComment.test.js b/src/api/helpComment.test.js
index a4a7d99..99efd8c 100644
--- a/src/api/helpComment.test.js
+++ b/src/api/helpComment.test.js
@@ -1,6 +1,6 @@
 import MockAdapter from 'axios-mock-adapter';

 import { api } from './auth'; // 添加api导入

-import { likePostComment, getCommentReplies, addCommentReply, deleteComment } from './helpComment';

+import { likeHelpPostComment, getHelpCommentReplies, addHelpCommentReply, deleteHelpComment } from './helpComment';

 

 describe('求助帖评论API', () => {

   let mockAxios;

@@ -13,27 +13,27 @@
     mockAxios.restore();

   });

 

-  describe('likePostComment - 点赞求助帖评论', () => {

+  describe('likeHelpPostComment - 点赞求助帖评论', () => {

     it('应该成功发送点赞请求', async () => {

       const mockResponse = { code: 200, message: '点赞成功' };

       mockAxios.onPost('/help/comments/123/like').reply(200, mockResponse);

 

-      const response = await likePostComment('123');

+      const response = await likeHelpPostComment('123');

       expect(response.data).toEqual(mockResponse);

     });

   });

 

-  describe('getCommentReplies - 获取评论回复', () => {

+  describe('getHelpCommentReplies - 获取评论回复', () => {

     it('应该返回正确的回复数据结构', async () => {

       const mockData = [{ id: '1', content: '回复内容' }];

       mockAxios.onGet('/help/comments/456/replies').reply(200, { code: 200, data: mockData });

 

-      const response = await getCommentReplies('456');

+      const response = await getHelpCommentReplies('456');

       expect(response.data.data).toEqual(mockData);

     });

   });

 

-  describe('addCommentReply - 添加评论回复', () => {

+  describe('addHelpCommentReply - 添加评论回复', () => {

     it('应该正确发送回复内容(无图片)', async () => {

       const commentId = '789';

       const replyData = {

@@ -49,7 +49,7 @@
         return [200, { code: 200 }];

       });

 

-      const response = await addCommentReply(commentId, replyData);

+      const response = await addHelpCommentReply(commentId, replyData);

       expect(response.status).toBe(200);

     });

     it('应该正确处理带图片的回复', async () => {

@@ -66,11 +66,11 @@
         return [200, { code: 200 }];

       });

 

-      const response = await addCommentReply(commentId, replyData);

+      const response = await addHelpCommentReply(commentId, replyData);

       expect(response.status).toBe(200);

     });

   });

-  describe('deleteComment - 删除评论', () => {

+  describe('deleteHelpComment - 删除评论', () => {

     it('应该正确发送删除请求', async () => {

       const commentId = '101112';

       const authorId = 'user1';

@@ -80,7 +80,7 @@
         return [200, { code: 200 }];

       });

 

-      const response = await deleteComment(commentId, authorId);

+      const response = await deleteHelpComment(commentId, authorId);

       expect(response.status).toBe(200);

     });

   });

diff --git a/src/api/helpPost.js b/src/api/helpPost.js
index 92d6402..68b37d9 100644
--- a/src/api/helpPost.js
+++ b/src/api/helpPost.js
@@ -1,7 +1,7 @@
 // src/api/helpPost.js

 import { api } from './auth'; // 复用已有的axios实例

 

-export const createPost = (title, content, authorId, selectedImage) => {

+export const createHelpPost = (title, content, authorId, selectedImage) => {

   // 创建 FormData 对象

   const formData = new FormData();

   formData.append('title', title);

@@ -16,44 +16,36 @@
   return api.post('/help/posts', formData);

 };

 

-export const getPosts = (page = 1, size = 5) => {

+export const getHelpPosts = (page = 1, size = 5) => {

   return api.get('/help/posts', {

     params: { page, size }

   });

 };

 

-export const getPostDetail = (postId) => {

+export const getHelpPostDetail = (postId) => {

   return api.get(`/help/posts/${postId}`);

 };

 

-export const likePost = (postId, data) => {

+export const likeHelpPost = (postId, data) => {

   return api.post(`/help/posts/${postId}/like`, null, {

     params: data

   });

 };

 

-export const addPostComment = (postId, commentData) => {

-  // 创建FormData对象来处理文件上传

-  const formData = new FormData();

-  formData.append('authorId', commentData.authorId);

-  formData.append('content', commentData.content);

+export const addHelpPostComment = (postId, commentData) => {

   

-  // 如果有图片,添加到formData

-  if (commentData.commentImage) {

-    formData.append('image', commentData.commentImage);

-  }

 

-  return api.post(`/help/posts/${postId}/comments`, formData);

+  return api.post(`/help/posts/${postId}/comments`, commentData);

 };

 

-export const deletePost = (postId, authorId) => {

+export const deleteHelpPost = (postId, authorId) => {

   return api.delete(`/help/posts/${postId}`, {

     params: { authorId }

   });

 };

 

 

-export const searchPosts = (keyword, page = 1, size = 5) => {

+export const searchHelpPosts = (keyword, page = 1, size = 5) => {

   return api.get('/help/posts/search', {

     params: { keyword, page, size }

   });

diff --git a/src/api/helpPost.test.js b/src/api/helpPost.test.js
index b445b4d..53e9e01 100644
--- a/src/api/helpPost.test.js
+++ b/src/api/helpPost.test.js
@@ -1,6 +1,6 @@
 import MockAdapter from 'axios-mock-adapter';

 import { api } from './auth'; // 添加api导入

-import { createPost, getPosts, getPostDetail, likePost, addPostComment,deletePost } from './helpPost';

+import { createHelpPost, getHelpPosts, getHelpPostDetail, likeHelpPost, addHelpPostComment,deleteHelpPost,searchHelpPosts } from './helpPost';

 

 describe('求助帖API', () => {

   let mockAxios;

@@ -13,7 +13,7 @@
     mockAxios.restore();

   });

 

-  describe('createPost - 创建求助帖', () => {

+  describe('createHelpPost - 创建求助帖', () => {

     it('应该正确发送无图片帖子数据', async () => {

       const postData = {

         title: '测试标题',

@@ -30,7 +30,7 @@
         return [201, { code: 201 }];

       });

 

-      const response = await createPost(postData.title, postData.content, postData.authorId);

+      const response = await createHelpPost(postData.title, postData.content, postData.authorId);

       expect(response.status).toBe(201);

     });

   });

@@ -48,7 +48,7 @@
       return [201, { code: 201 }];

     });

 

-    const response = await createPost(

+    const response = await createHelpPost(

       postData.title, 

       postData.content, 

       postData.authorId, 

@@ -58,7 +58,7 @@
   });

 

 

-  describe('getPosts - 获取求助帖列表', () => {

+  describe('getHelpPosts - 获取求助帖列表', () => {

     it('应该支持分页参数', async () => {

       const page = 2, size = 10;

       mockAxios.onGet('/help/posts', { params: { page, size } }).reply(200, {

@@ -66,57 +66,21 @@
         data: []

       });

 

-      const response = await getPosts(page, size);

+      const response = await getHelpPosts(page, size);

       expect(response.status).toBe(200);

     });

   });

 

-  describe('likePost - 点赞求助帖', () => {

+  describe('likeHelpPost - 点赞求助帖', () => {

     it('应该正确发送点赞请求', async () => {

       mockAxios.onPost('/help/posts/post123/like').reply(200, { code: 200 });

-      const response = await likePost('post123');

+      const response = await likeHelpPost('post123');

       expect(response.status).toBe(200);

     });

   });

 

-  describe('addPostComment - 添加帖子评论', () => {

-    it('应该正确发送评论数据(无图片)', async () => {

-      const postId = 'post456';

-      const commentData = {

-        authorId: 'user1',

-        content: '测试评论'

-      };

-      

-      mockAxios.onPost(`/help/posts/${postId}/comments`).reply(config => {

-        const data = config.data;

-        expect(data.get('authorId')).toBe(commentData.authorId);

-        expect(data.get('content')).toBe(commentData.content);

-        expect(data.has('image')).toBe(false);

-        return [200, { code: 200 }];

-      });

-      

-      const response = await addPostComment('post456', commentData);

-      expect(response.status).toBe(200);

-    });

-    it('应该正确处理带图片的评论', async () => {

-      const postId = 'post456';

-      const commentData = {

-        authorId: 'user1',

-        content: '测试评论',

-        commentImage: new File(['content'], 'comment.jpg')

-      };

-      

-      mockAxios.onPost(`/help/posts/${postId}/comments`).reply(config => {

-        const data = config.data;

-        expect(data.get('image')).toBeInstanceOf(File);

-        return [200, { code: 200 }];

-      });

-

-      const response = await addPostComment(postId, commentData);

-      expect(response.status).toBe(200);

-    });

-  });

-  describe('deletePost - 删除帖子', () => {

+  

+  describe('deleteHelpPost - 删除帖子', () => {

     it('应该正确发送删除请求', async () => {

       const postId = 'post789';

       const authorId = 'user1';

@@ -126,8 +90,28 @@
         return [200, { code: 200 }];

       });

 

-      const response = await deletePost(postId, authorId);

+      const response = await deleteHelpPost(postId, authorId);

       expect(response.status).toBe(200);

     });

   });

+  describe('searchHelpPosts - 搜索求助帖', () => {

+      it('应该正确发送搜索请求', async () => {

+        const keyword = '测试';

+        const page = 1, size = 5;

+        

+        mockAxios.onGet('/help/posts/search', {

+          params: { keyword, page, size }

+        }).reply(200, {

+          code: 200,

+          data: {

+            records: [],

+            total: 0

+          }

+        });

+  

+        const response = await searchHelpPosts(keyword, page, size);

+        expect(response.status).toBe(200);

+        expect(response.data.data).toBeDefined();

+      });

+    });

 });
\ No newline at end of file
diff --git a/src/api/personal.test.js b/src/api/personal.test.js
new file mode 100644
index 0000000..3a929e8
--- /dev/null
+++ b/src/api/personal.test.js
@@ -0,0 +1,354 @@
+// src/api/__tests__/personal.test.js
+import { 
+  getUserInfo, 
+  formatFileSize, 
+  getDownloadQuota, 
+  getDownloadProgress, 
+  getUserTorrents, 
+  deleteTorrent, 
+  generateInviteCode, 
+  getUserInviteCodes, 
+  exchangeUpload, 
+  updatePassword 
+} from './personal';
+import { api } from './auth';
+
+// 模拟整个 auth 模块
+jest.mock('./auth');
+
+describe('Personal API', () => {
+  beforeEach(() => {
+    // 在每个测试前重置模拟
+    jest.clearAllMocks();
+  });
+
+  describe('getUserInfo', () => {
+    it('should return formatted user info when API call succeeds', async () => {
+      const mockUserData = {
+        username: 'testuser',
+        level: 3,
+        registTime: '2023-01-01T00:00:00Z',
+        magicPoints: 100,
+        upload: 1024,
+        download: 512,
+        shareRate: 2.0
+      };
+
+      api.get.mockResolvedValue({
+        data: {
+          code: 200,
+          data: mockUserData,
+          message: 'success'
+        }
+      });
+
+      const result = await getUserInfo();
+      
+      expect(api.get).toHaveBeenCalledWith('/user/userInfo');
+      expect(result).toEqual({
+        username: 'testuser',
+        level: 3,
+        registTime: '2023-01-01',
+        magicPoints: 100,
+        upload: 1024,
+        download: 512,
+        shareRate: '2.00'
+      });
+    });
+
+    it('should throw error when API call fails', async () => {
+      api.get.mockResolvedValue({
+        data: {
+          code: 500,
+          message: 'Internal server error'
+        }
+      });
+
+      await expect(getUserInfo()).rejects.toThrow('Internal server error');
+    });
+  });
+
+  describe('formatFileSize', () => {
+    it('should format bytes correctly', () => {
+      expect(formatFileSize(500)).toBe('500 B');
+      expect(formatFileSize(1024)).toBe('1.00 KB');
+      expect(formatFileSize(2048)).toBe('2.00 KB');
+      expect(formatFileSize(1024 * 1024)).toBe('1.00 MB');
+      expect(formatFileSize(1024 * 1024 * 2.5)).toBe('2.50 MB');
+      expect(formatFileSize(1024 * 1024 * 1024)).toBe('1.00 GB');
+      expect(formatFileSize(1024 * 1024 * 1024 * 3.7)).toBe('3.70 GB');
+    });
+  });
+
+  describe('getDownloadQuota', () => {
+    it('should return download quota data when API call succeeds', async () => {
+      const mockData = {
+        total: 1073741824, // 1GB in bytes
+        used: 536870912,   // 512MB
+        remaining: 536870912
+      };
+
+      api.get.mockResolvedValue({
+        data: {
+          code: 200,
+          data: mockData,
+          message: 'success'
+        }
+      });
+
+      const result = await getDownloadQuota();
+      
+      expect(api.get).toHaveBeenCalledWith('/user/allowDownload');
+      expect(result).toEqual(mockData);
+    });
+
+    it('should throw error when API call fails', async () => {
+      api.get.mockResolvedValue({
+        data: {
+          code: 403,
+          message: 'Forbidden'
+        }
+      });
+
+      await expect(getDownloadQuota()).rejects.toThrow('Forbidden');
+    });
+  });
+
+  describe('getDownloadProgress', () => {
+    it('should return download progress when API call succeeds', async () => {
+      const mockProgresses = [
+        { id: 1, name: 'file1', progress: 50 },
+        { id: 2, name: 'file2', progress: 75 }
+      ];
+
+      api.get.mockResolvedValue({
+        data: {
+          code: 200,
+          data: { progresses: mockProgresses },
+          message: 'success'
+        }
+      });
+
+      const result = await getDownloadProgress();
+      
+      expect(api.get).toHaveBeenCalledWith('/torrent/getProgress');
+      expect(result).toEqual(mockProgresses);
+    });
+
+    it('should throw error when API call fails', async () => {
+      api.get.mockResolvedValue({
+        data: {
+          code: 404,
+          message: 'Not found'
+        }
+      });
+
+      await expect(getDownloadProgress()).rejects.toThrow('Not found');
+    });
+  });
+
+  describe('getUserTorrents', () => {
+    it('should return user torrents with pagination when API call succeeds', async () => {
+      const mockResponse = {
+        records: [
+          { 
+            torrent: { id: 1, name: 'torrent1', size: 1024 },
+            downloadCount: 10,
+            formattedSize: '1 KB'
+          }
+        ],
+        total: 1
+      };
+
+      api.get.mockResolvedValue({
+        data: {
+          code: 200,
+          data: mockResponse,
+          message: 'success'
+        }
+      });
+
+      const result = await getUserTorrents(1, 5);
+      
+      expect(api.get).toHaveBeenCalledWith('/torrent/get/torrentMyself', {
+        params: { page: 1, size: 5 }
+      });
+      expect(result).toEqual({
+        records: [
+          {
+            id: 1,
+            name: 'torrent1',
+            size: 1024,
+            downloadCount: 10,
+            formattedSize: '1 KB'
+          }
+        ],
+        total: 1
+      });
+    });
+
+    it('should throw error when API call fails', async () => {
+      api.get.mockResolvedValue({
+        data: {
+          code: 500,
+          message: 'Server error'
+        }
+      });
+
+      await expect(getUserTorrents()).rejects.toThrow('Server error');
+    });
+  });
+
+  describe('deleteTorrent', () => {
+    it('should successfully delete torrent when API call succeeds', async () => {
+      const mockResponse = {
+        code: 200,
+        message: 'Deleted successfully'
+      };
+
+      api.delete.mockResolvedValue({
+        data: mockResponse
+      });
+
+      const result = await deleteTorrent(123);
+      
+      expect(api.delete).toHaveBeenCalledWith('/torrent/deleteTorrent/123');
+      expect(result).toEqual(mockResponse);
+    });
+
+    it('should throw error when API call fails', async () => {
+      api.delete.mockResolvedValue({
+        data: {
+          code: 404,
+          message: 'Torrent not found'
+        }
+      });
+
+      await expect(deleteTorrent(123)).rejects.toThrow('Torrent not found');
+    });
+  });
+
+  describe('generateInviteCode', () => {
+    it('should return generated invite code when API call succeeds', async () => {
+      const mockCode = 'ABCD-EFGH-IJKL';
+
+      api.post.mockResolvedValue({
+        data: {
+          code: 200,
+          data: { inviteCode: mockCode },
+          message: 'success'
+        }
+      });
+
+      const result = await generateInviteCode();
+      
+      expect(api.post).toHaveBeenCalledWith('/invitecode/generate');
+      expect(result).toBe(mockCode);
+    });
+
+    it('should throw error when API call fails', async () => {
+      api.post.mockResolvedValue({
+        data: {
+          code: 403,
+          message: 'Permission denied'
+        }
+      });
+
+      await expect(generateInviteCode()).rejects.toThrow('Permission denied');
+    });
+  });
+
+  describe('getUserInviteCodes', () => {
+    it('should return user invite codes when API call succeeds', async () => {
+      const mockCodes = ['CODE1', 'CODE2'];
+
+      api.get.mockResolvedValue({
+        data: {
+          code: 200,
+          data: { inviteCode: mockCodes },
+          message: 'success'
+        }
+      });
+
+      const result = await getUserInviteCodes();
+      
+      expect(api.get).toHaveBeenCalledWith('/invitecode/userInviteCode');
+      expect(result).toEqual(mockCodes);
+    });
+
+    it('should throw error when API call fails', async () => {
+      api.get.mockResolvedValue({
+        data: {
+          code: 500,
+          message: 'Server error'
+        }
+      });
+
+      await expect(getUserInviteCodes()).rejects.toThrow('Server error');
+    });
+  });
+
+  describe('exchangeUpload', () => {
+    it('should successfully exchange magic points for upload when API call succeeds', async () => {
+      const mockResponse = {
+        code: 200,
+        message: 'Exchange successful'
+      };
+
+      api.post.mockResolvedValue({
+        data: mockResponse
+      });
+
+      const result = await exchangeUpload(100);
+      
+      expect(api.post).toHaveBeenCalledWith('/user/exchangeUpload', {
+        magicPoint: 100
+      });
+      expect(result).toEqual(mockResponse);
+    });
+
+    it('should throw error when API call fails', async () => {
+      api.post.mockResolvedValue({
+        data: {
+          code: 400,
+          message: 'Not enough points'
+        }
+      });
+
+      await expect(exchangeUpload(100)).rejects.toThrow('Not enough points');
+    });
+  });
+
+  describe('updatePassword', () => {
+    it('should successfully update password when API call succeeds', async () => {
+      const mockResponse = {
+        code: 200,
+        message: 'Password updated'
+      };
+
+      api.put.mockResolvedValue({
+        data: mockResponse
+      });
+
+      const result = await updatePassword('oldPass', 'newPass');
+      
+      expect(api.put).toHaveBeenCalledWith('/user/password', {
+        oldPassword: 'oldPass',
+        newPassword: 'newPass'
+      });
+      expect(result).toEqual(mockResponse);
+    });
+
+    it('should throw error when API call fails', async () => {
+      api.put.mockResolvedValue({
+        data: {
+          code: 401,
+          message: 'Invalid old password'
+        }
+      });
+
+      await expect(updatePassword('wrongPass', 'newPass'))
+        .rejects.toThrow('Invalid old password');
+    });
+  });
+});
\ No newline at end of file
diff --git a/src/api/requestComment.js b/src/api/requestComment.js
new file mode 100644
index 0000000..f7e9b43
--- /dev/null
+++ b/src/api/requestComment.js
@@ -0,0 +1,28 @@
+import { api } from './auth';
+
+export const likeRequestPostComment = (commentId) => {
+  return api.post(`/request/comments/${commentId}/like`);
+};
+
+export const getRequestCommentReplies = (commentId) => {
+  return api.get(`/request/comments/${commentId}/replies`);
+};
+
+export const addRequestCommentReply = (commentId, replyData) => {
+  const formData = new FormData();
+  formData.append('authorId', replyData.authorId);
+  formData.append('content', replyData.content);
+
+  // 如果有图片,添加到formData
+  if (replyData.image) {
+    formData.append('image', replyData.image);
+  }
+
+  return api.post(`/request/comments/${commentId}/replies`, formData);
+};
+
+export const deleteRequestComment = (commentId, authorId) => {
+  return api.delete(`/request/comments/${commentId}`, {
+    params: { authorId }
+  });
+};
\ No newline at end of file
diff --git a/src/api/requestComment.test.js b/src/api/requestComment.test.js
new file mode 100644
index 0000000..2f50a3c
--- /dev/null
+++ b/src/api/requestComment.test.js
@@ -0,0 +1,97 @@
+import MockAdapter from 'axios-mock-adapter';
+import { api } from './auth';
+import {
+  likeRequestPostComment,
+  getRequestCommentReplies,
+  addRequestCommentReply,
+  deleteRequestComment
+} from './requestComment';
+
+describe('求种帖评论API', () => {
+  let mockAxios;
+
+  beforeEach(() => {
+    mockAxios = new MockAdapter(api);
+  });
+
+  afterEach(() => {
+    mockAxios.restore();
+  });
+
+  describe('likeRequestPostComment - 点赞求种帖评论', () => {
+    it('应该成功发送点赞请求', async () => {
+      const commentId = 'reqc123';
+      const mockResponse = { code: 200, message: '点赞成功' };
+      mockAxios.onPost(`/request/comments/${commentId}/like`).reply(200, mockResponse);
+
+      const response = await likeRequestPostComment(commentId);
+      expect(response.data).toEqual(mockResponse);
+    });
+  });
+
+  describe('getRequestCommentReplies - 获取评论回复', () => {
+    it('应该返回正确的回复数据结构', async () => {
+      const commentId = 'reqc456';
+      const mockData = [{ id: '1', content: '求种回复内容' }];
+      mockAxios.onGet(`/request/comments/${commentId}/replies`)
+        .reply(200, { code: 200, data: mockData });
+
+      const response = await getRequestCommentReplies(commentId);
+      expect(response.data.data).toEqual(mockData);
+    });
+  });
+
+  describe('addRequestCommentReply - 添加评论回复', () => {
+    it('应该正确发送回复内容(无图片)', async () => {
+      const commentId = 'reqc789';
+      const replyData = {
+        authorId: 'user1',
+        content: '测试求种回复'
+      };
+      
+      mockAxios.onPost(`/request/comments/${commentId}/replies`).reply(config => {
+        const data = config.data;
+        expect(data.get('authorId')).toBe(replyData.authorId);
+        expect(data.get('content')).toBe(replyData.content);
+        expect(data.has('image')).toBe(false);
+        return [200, { code: 200 }];
+      });
+
+      const response = await addRequestCommentReply(commentId, replyData);
+      expect(response.status).toBe(200);
+    });
+
+    it('应该正确处理带图片的回复', async () => {
+      const commentId = 'reqc789';
+      const replyData = {
+        authorId: 'user1',
+        content: '测试求种回复',
+        image: new File(['content'], 'reply.jpg')
+      };
+      
+      mockAxios.onPost(`/request/comments/${commentId}/replies`).reply(config => {
+        const data = config.data;
+        expect(data.get('image')).toBeInstanceOf(File);
+        return [200, { code: 200 }];
+      });
+
+      const response = await addRequestCommentReply(commentId, replyData);
+      expect(response.status).toBe(200);
+    });
+  });
+
+  describe('deleteRequestComment - 删除评论', () => {
+    it('应该正确发送删除请求', async () => {
+      const commentId = 'reqc101';
+      const authorId = 'user1';
+      
+      mockAxios.onDelete(`/request/comments/${commentId}`).reply(config => {
+        expect(config.params).toEqual({ authorId });
+        return [200, { code: 200 }];
+      });
+
+      const response = await deleteRequestComment(commentId, authorId);
+      expect(response.status).toBe(200);
+    });
+  });
+});
\ No newline at end of file
diff --git a/src/api/requestPost.js b/src/api/requestPost.js
new file mode 100644
index 0000000..224e3e9
--- /dev/null
+++ b/src/api/requestPost.js
@@ -0,0 +1,52 @@
+// src/api/requestPost.js
+import { api } from './auth'; // 复用已有的axios实例
+
+export const createRequestPost = (title, content, authorId, selectedImage) => {
+  // 创建 FormData 对象
+  const formData = new FormData();
+  formData.append('title', title);
+  formData.append('content', content);
+  formData.append('authorId', authorId);
+  
+  // 如果有图片,添加到 FormData
+  if (selectedImage) {
+    formData.append('image', selectedImage);
+  }
+
+  return api.post('/request/posts', formData);
+};
+
+export const getRequestPosts = (page = 1, size = 5) => {
+  return api.get('/request/posts', {
+    params: { page, size }
+  });
+};
+
+export const getRequestPostDetail = (postId) => {
+  return api.get(`/request/posts/${postId}`);
+};
+
+export const likeRequestPost = (postId, data) => {
+  return api.post(`/request/posts/${postId}/like`, null, {
+    params: data
+  });
+};
+
+export const addRequestPostComment = (postId, commentData) => {
+  
+
+  return api.post(`/request/posts/${postId}/comments`, commentData);
+};
+
+export const deleteRequestPost = (postId, authorId) => {
+  return api.delete(`/request/posts/${postId}`, {
+    params: { authorId }
+  });
+};
+
+
+export const searchRequestPosts = (keyword, page = 1, size = 5) => {
+  return api.get('/request/posts/search', {
+    params: { keyword, page, size }
+  });
+};
\ No newline at end of file
diff --git a/src/api/requestPost.test.js b/src/api/requestPost.test.js
new file mode 100644
index 0000000..a3bccc6
--- /dev/null
+++ b/src/api/requestPost.test.js
@@ -0,0 +1,147 @@
+import MockAdapter from 'axios-mock-adapter';
+import { api } from './auth';
+import {
+  createRequestPost,
+  getRequestPosts,
+  getRequestPostDetail,
+  likeRequestPost,
+  addRequestPostComment,
+  deleteRequestPost,
+  searchRequestPosts
+} from './requestPost';
+
+describe('求种帖API', () => {
+  let mockAxios;
+
+  beforeEach(() => {
+    mockAxios = new MockAdapter(api);
+  });
+
+  afterEach(() => {
+    mockAxios.restore();
+  });
+
+  describe('createRequestPost - 创建求种帖', () => {
+    it('应该正确发送无图片帖子数据', async () => {
+      const postData = {
+        title: '求种测试标题',
+        content: '求种测试内容',
+        authorId: 'user123'
+      };
+      
+      mockAxios.onPost('/request/posts').reply(config => {
+        const data = config.data;
+        expect(data.get('title')).toBe(postData.title);
+        expect(data.get('content')).toBe(postData.content);
+        expect(data.get('authorId')).toBe(postData.authorId);
+        expect(data.has('image')).toBe(false);
+        return [201, { code: 201 }];
+      });
+
+      const response = await createRequestPost(
+        postData.title,
+        postData.content,
+        postData.authorId
+      );
+      expect(response.status).toBe(201);
+    });
+
+    it('应该正确处理带图片的帖子', async () => {
+      const postData = {
+        title: '求种测试标题',
+        content: '求种测试内容',
+        authorId: 'user123',
+        selectedImage: new File(['content'], 'request.jpg')
+      };
+      
+      mockAxios.onPost('/request/posts').reply(config => {
+        const data = config.data;
+        expect(data.get('image')).toBeInstanceOf(File);
+        return [201, { code: 201 }];
+      });
+
+      const response = await createRequestPost(
+        postData.title,
+        postData.content,
+        postData.authorId,
+        postData.selectedImage
+      );
+      expect(response.status).toBe(201);
+    });
+  });
+
+  describe('getRequestPosts - 获取求种帖列表', () => {
+    it('应该支持分页参数', async () => {
+      const page = 2, size = 10;
+      mockAxios.onGet('/request/posts', { params: { page, size } })
+        .reply(200, {
+          code: 200,
+          data: {
+            records: [],
+            total: 0
+          }
+        });
+
+      const response = await getRequestPosts(page, size);
+      expect(response.status).toBe(200);
+      expect(response.data.data).toBeDefined();
+    });
+  });
+
+  describe('getRequestPostDetail - 获取求种帖详情', () => {
+    it('应该正确获取帖子详情', async () => {
+      const postId = 'req123';
+      mockAxios.onGet(`/request/posts/${postId}`)
+        .reply(200, {
+          code: 200,
+          data: {
+            post: {
+              id: postId,
+              title: '测试求种帖'
+            }
+          }
+        });
+
+      const response = await getRequestPostDetail(postId);
+      expect(response.status).toBe(200);
+      expect(response.data.data.post.id).toBe(postId);
+    });
+  });
+
+
+
+  describe('deleteRequestPost - 删除求种帖', () => {
+    it('应该正确发送删除请求', async () => {
+      const postId = 'req101';
+      const authorId = 'user1';
+      
+      mockAxios.onDelete(`/request/posts/${postId}`, {
+        params: { authorId }
+      }).reply(200, { code: 200 });
+
+      const response = await deleteRequestPost(postId, authorId);
+      expect(response.status).toBe(200);
+    });
+  });
+
+  describe('searchRequestPosts - 搜索求种帖', () => {
+    it('应该正确发送搜索请求', async () => {
+      const keyword = '测试';
+      const page = 1, size = 5;
+      
+      mockAxios.onGet('/request/posts/search', {
+        params: { keyword, page, size }
+      }).reply(200, {
+        code: 200,
+        data: {
+          records: [],
+          total: 0
+        }
+      });
+
+      const response = await searchRequestPosts(keyword, page, size);
+      expect(response.status).toBe(200);
+      expect(response.data.data).toBeDefined();
+    });
+  });
+});
\ No newline at end of file
diff --git a/src/api/torrent.js b/src/api/torrent.js
index 8a00e54..4a623d7 100644
--- a/src/api/torrent.js
+++ b/src/api/torrent.js
@@ -1,26 +1,74 @@
 // src/api/torrent.js

 import { api } from './auth'; // 复用已有的axios实例

 

-export const createTorrent = (torrentData, file) => {

+/**

+ * 创建并上传一个种子

+ * @param {File} file 种子文件 (.torrent)

+ * @param {Object} bodyObj 包含种子信息的对象,格式如下:

+ * {

+ *   torrentName: "测试下载",

+ *   description: "A high-quality 1080p version of Example Movie.",

+ *   category: "电影",

+ *   region: "USA",

+ *   resolution: "1080p",

+ *   subtitle: "English",

+ *   filePath: "D:/大学/大三_下/torrentFrom/[电影天堂www.dytt89.com]两杆大烟枪BD中英双字.mp4.torrent"

+ * }

+ */

+export async function createTorrent(file, bodyObj) {

   const formData = new FormData();

-  const token = localStorage.getItem('token');

+  formData.append('file', file);

 

-  // 添加文件

-  if (file) {

-    formData.append('file', file);

+  // 关键修改:将JSON字符串转换为Blob对象,并设置正确的Content-Type

+  formData.append('body', new Blob([JSON.stringify(bodyObj)], {

+    type: 'application/json'

+  }));

+

+  try {

+    const response = await api.post('/torrent', formData, {

+      headers: {

+      }

+    });

+    return response.data;

+  } catch (error) {

+    console.error('上传种子失败:', error);

+    throw error;

   }

-  // 通过这个方式就可以指定 ContentType 了

-  formData.append('body', JSON.stringify(torrentData))

-

-  console.log(formData);

-  return api.post('/torrent', formData, {

-    headers: {

-      'Content-Type': 'multipart/form-data',

-      'Authorization': `${token}`

-    },

-  });

 };

 

+/**

+ * 下载种子文件

+ * @param {number} torrentId 种子ID

+ * @param {string} downloadPath 下载路径

+ */

+export async function downloadTorrent(torrentId, downloadPath) {

+  try {

+    const response = await api.get(`/torrent/downloadTorrent`, {

+      params: {

+        id: torrentId,

+        downloadPath: downloadPath

+      }

+    });

+    return response.data;

+  } catch (error) {

+    console.error('下载种子失败:', error);

+    throw error;

+  }

+}

+

+/**

+ * 获取下载进度

+ */

+export async function getDownloadProgress() {

+  try {

+    const response = await api.get('/torrent/getProgress');

+    return response.data;

+  } catch (error) {

+    console.error('获取下载进度失败:', error);

+    throw error;

+  }

+}

+

 export const getTorrents = (page = 1, size = 5) => {

   return api.get('/torrent', {

     params: { page, size }

@@ -54,6 +102,61 @@
   return api.post(`/torrent/${torrentId}/comments`, commentData);

 };

 

+export const deleteTorrent = (torrentId) => {

+  return api.delete(`/torrent/deleteTorrent/${torrentId}`);

+};

+

+// // 上传种子接口

+// export const uploadTorrent = async (torrentFile, torrentData, onProgress) => {

+//   // 1. 基础验证

+//   if (!torrentFile || !torrentFile.name.endsWith('.torrent')) {

+//     throw new Error('请选择有效的.torrent文件');

+//   }

+

+//   // 2. 构建FormData

+//   const formData = new FormData();

+//   formData.append('file', torrentFile);

+  

+//   // 确保JSON内容使用正确的Content-Type

+//   const metadataBlob = new Blob(

+//     [JSON.stringify(torrentData)], 

+//     { type: 'application/json' }

+//   );

+//   formData.append('body', metadataBlob);

+

+//   // 3. 获取认证token(根据你的实际存储方式调整)

+//   const token = localStorage.getItem('token') || '';

+

+//   try {

+//     const response = await api.post('/api/torrents', formData, {

+//       headers: {

+//         'Authorization': `Bearer ${token}`,

+//       },

+//       onUploadProgress: (progressEvent) => {

+//         if (onProgress && progressEvent.total) {

+//           const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);

+//           onProgress(percent);

+//         }

+//       }

+//     });

+

+//     return response.data;

+//   } catch (error) {

+//     console.error('上传失败:', error);

+    

+//     // 增强错误处理

+//     let errorMessage = '上传失败';

+//     if (error.response) {

+//       errorMessage = error.response.data?.message || 

+//                      `服务器错误: ${error.response.status}`;

+//     } else if (error.request) {

+//       errorMessage = '网络错误,请检查连接';

+//     }

+

+//     throw new Error(errorMessage);

+//   }

+// };

+

 export const searchTorrents = (keyword, page = 1, size = 5) => {

   return api.get('/torrent/search', {

     params: { keyword, page, size }

diff --git a/src/api/torrent.test.js b/src/api/torrent.test.js
index 6515bdc..6cd481e 100644
--- a/src/api/torrent.test.js
+++ b/src/api/torrent.test.js
@@ -22,22 +22,6 @@
     localStorage.clear();

   });

 

-  describe('createTorrent - 创建种子', () => {

-    it('应该正确发送包含文件和数据的表单请求', async () => {

-      const mockFile = new File(['test'], 'test.torrent');

-      const torrentData = { name: '测试种子', description: '测试描述' };

-      const mockResponse = { code: 200, message: '创建成功' };

-  

-      mockAxios.onPost('/torrent').reply((config) => {

-        expect(config.headers['Content-Type']).toBe('multipart/form-data');

-        expect(config.headers['Authorization']).toBe('Bearer test-token'); // 修改为包含Bearer

-        return [200, mockResponse];

-      });

-  

-      const response = await createTorrent(torrentData, mockFile);

-      expect(response.data).toEqual(mockResponse);

-    });

-  });

 

   describe('getTorrents - 获取种子列表', () => {

     it('应该发送带分页参数的请求', async () => {

diff --git a/src/components/Administer.css b/src/components/Administer.css
index 1599a3e..786e3dc 100644
--- a/src/components/Administer.css
+++ b/src/components/Administer.css
@@ -167,4 +167,60 @@
   padding: 10px;
   background: #f5f5f5;
   border-radius: 4px;
+}
+
+/* 公告表单样式 */
+.announcement-form {
+  padding: 20px;
+  background: #f5f5f5;
+  border-radius: 8px;
+  margin-bottom: 20px;
+}
+
+.announcement-form .form-group {
+  margin-bottom: 15px;
+}
+
+.announcement-form label {
+  display: block;
+  margin-bottom: 5px;
+  font-weight: bold;
+}
+
+.announcement-form input[type="text"],
+.announcement-form textarea {
+  width: 100%;
+  padding: 8px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+}
+
+.announcement-form textarea {
+  min-height: 100px;
+}
+
+/* 公告列表样式 */
+.announcement-list-container {
+  margin-top: 20px;
+}
+
+.announcement-table {
+  width: 100%;
+  border-collapse: collapse;
+}
+
+.announcement-table th,
+.announcement-table td {
+  padding: 12px;
+  text-align: left;
+  border-bottom: 1px solid #ddd;
+}
+
+.announcement-table th {
+  background-color: #f2f2f2;
+  font-weight: bold;
+}
+
+.announcement-table tr:hover {
+  background-color: #f9f9f9;
 }
\ No newline at end of file
diff --git a/src/components/Administer.jsx b/src/components/Administer.jsx
index 35ae428..0f915e3 100644
--- a/src/components/Administer.jsx
+++ b/src/components/Administer.jsx
@@ -10,6 +10,7 @@
   addDiscount,
   deleteDiscount
 } from '../api/administer';
+import {postAnnouncement, getAnnouncements} from '../api/announcement'; 
 import DatePicker from 'react-datepicker';
 import 'react-datepicker/dist/react-datepicker.css';
 
@@ -22,13 +23,18 @@
   const [searchKey, setSearchKey] = useState('');
   const [loading, setLoading] = useState(false);
   const [error, setError] = useState(null);
+  const [announcements, setAnnouncements] = useState([]); // 存储公告列表
+  const [newAnnouncement, setNewAnnouncement] = useState({
+    title: '',
+    content: ''
+  });
   const [newDiscount, setNewDiscount] = useState({
     name: '',
     discountType: 'FREE'
   });
   const [startDate, setStartDate] = useState(new Date());
   const [endDate, setEndDate] = useState(new Date());
-  const [activeTab, setActiveTab] = useState('users'); // 'users' 或 'discounts'
+  const [activeTab, setActiveTab] = useState('users'); // 'users' 或 'discounts','announcements'
 
 const fetchAllUsers = async () => {
   setLoading(true);
@@ -208,15 +214,7 @@
     }
   };
 
-  // 初始化加载数据
-  useEffect(() => {
-    if (activeTab === 'users') {
-      fetchAllUsers();
-    } else {
-      fetchAllDiscounts();
-      fetchCurrentDiscount();
-    }
-  }, [activeTab]);
+  
 
   // 格式化分享率为百分比
   const formatShareRate = (rate) => {
@@ -244,6 +242,54 @@
     }
   };
 
+  // 获取所有公告
+  const fetchAllAnnouncements = async () => {
+  setLoading(true);
+  setError(null);
+  try {
+    const announcements = await getAnnouncements();
+    // 确保总是设置为数组
+    setAnnouncements(Array.isArray(announcements) ? announcements : []);
+  } catch (err) {
+    setError('获取公告列表失败: ' + err.message);
+    console.error(err);
+  } finally {
+    setLoading(false);
+  }
+};
+
+  // 发布新公告
+  const handlePostAnnouncement = async () => {
+    if (!newAnnouncement.title || !newAnnouncement.content) {
+      setError('请填写公告标题和内容');
+      return;
+    }
+
+    try {
+      await postAnnouncement(newAnnouncement);
+      setNewAnnouncement({ title: '', content: '' });
+      fetchAllAnnouncements();
+      setError(null);
+    } catch (err) {
+      setError('发布公告失败: ' + err.message);
+      console.error(err);
+    }
+  };
+
+  // 初始化加载数据
+  useEffect(() => {
+    if (activeTab === 'users') {
+      fetchAllUsers();
+    } else if (activeTab === 'discounts') {
+      fetchAllDiscounts();
+      fetchCurrentDiscount();
+    } else if (activeTab === 'announcements') {
+      fetchAllAnnouncements();
+    }
+  }, [activeTab]);
+
+  
+
 
   return (
     <div className="administer-container">
@@ -263,6 +309,12 @@
         >
           折扣管理
         </button>
+        <button 
+          className={`tab-button ${activeTab === 'announcements' ? 'active' : ''}`}
+          onClick={() => setActiveTab('announcements')}
+        >
+          公告管理
+        </button>
       </div>
 
       {activeTab === 'users' ? (
@@ -335,116 +387,174 @@
                 </table>
             </div>
         </>
-    ) : (
-        /* 新增的折扣管理部分 */
-        <>
-          {/* 当前活动折扣 */}
-          <div className="current-discount-section">
-            <h3>当前活动折扣</h3>
-            {currentDiscount ? (
-              <div className="current-discount-card">
-                <p><strong>名称:</strong> {currentDiscount.name}</p>
-                <p><strong>类型:</strong> {translateDiscountType(currentDiscount.discountType)}</p>
-                <p><strong>时间:</strong> {formatDateTime(currentDiscount.startTime)} 至 {formatDateTime(currentDiscount.endTime)}</p>
-                <p><strong>状态:</strong> {currentDiscount.status}</p>
-              </div>
-            ) : (
-              <p>当前没有进行中的折扣</p>
-            )}
-          </div>
+      ) : activeTab === 'discounts' ? (
+          /* 新增的折扣管理部分 */
+          <>
+            {/* 当前活动折扣 */}
+            <div className="current-discount-section">
+              <h3>当前活动折扣</h3>
+              {currentDiscount ? (
+                <div className="current-discount-card">
+                  <p><strong>名称:</strong> {currentDiscount.name}</p>
+                  <p><strong>类型:</strong> {translateDiscountType(currentDiscount.discountType)}</p>
+                  <p><strong>时间:</strong> {formatDateTime(currentDiscount.startTime)} 至 {formatDateTime(currentDiscount.endTime)}</p>
+                  <p><strong>状态:</strong> {currentDiscount.status}</p>
+                </div>
+              ) : (
+                <p>当前没有进行中的折扣</p>
+              )}
+            </div>
 
-          {/* 添加新折扣表单 */}
-          <div className="add-discount-form">
-            <h3>添加新折扣</h3>
-            <div className="form-group">
-              <label>折扣名称:</label>
-              <input
-                type="text"
-                value={newDiscount.name}
-                onChange={(e) => setNewDiscount({...newDiscount, name: e.target.value})}
-              />
-            </div>
-            <div className="form-group">
-                <label>开始时间:</label>
-                <DatePicker
-                  selected={startDate}
-                  onChange={(date) => setStartDate(date)}
-                  showTimeSelect
-                  timeFormat="HH:mm"
-                  timeIntervals={1} // 1分钟间隔
-                  dateFormat="yyyy-MM-dd HH:mm"
-                  minDate={new Date()}
-                  placeholderText="选择开始日期和时间"
+            {/* 添加新折扣表单 */}
+            <div className="add-discount-form">
+              <h3>添加新折扣</h3>
+              <div className="form-group">
+                <label>折扣名称:</label>
+                <input
+                  type="text"
+                  value={newDiscount.name}
+                  onChange={(e) => setNewDiscount({...newDiscount, name: e.target.value})}
                 />
-            </div>
-            <div className="form-group">
-                  <label>结束时间:</label>
+              </div>
+              <div className="form-group">
+                  <label>开始时间:</label>
                   <DatePicker
-                    selected={endDate}
-                    onChange={(date) => setEndDate(date)}
+                    selected={startDate}
+                    onChange={(date) => setStartDate(date)}
                     showTimeSelect
                     timeFormat="HH:mm"
                     timeIntervals={1} // 1分钟间隔
                     dateFormat="yyyy-MM-dd HH:mm"
-                    minDate={startDate}
-                    placeholderText="选择结束日期和时间"
+                    minDate={new Date()}
+                    placeholderText="选择开始日期和时间"
                   />
+              </div>
+              <div className="form-group">
+                    <label>结束时间:</label>
+                    <DatePicker
+                      selected={endDate}
+                      onChange={(date) => setEndDate(date)}
+                      showTimeSelect
+                      timeFormat="HH:mm"
+                      timeIntervals={1} // 1分钟间隔
+                      dateFormat="yyyy-MM-dd HH:mm"
+                      minDate={startDate}
+                      placeholderText="选择结束日期和时间"
+                    />
+              </div>
+              <div className="form-group">
+                <label>折扣类型:</label>
+                <select
+                  value={newDiscount.discountType}
+                  onChange={(e) => setNewDiscount({...newDiscount, discountType: e.target.value})}
+                >
+                  <option value="FREE">全部免费</option>
+                  <option value="HALF">半价下载</option>
+                  <option value="DOUBLE">双倍上传</option>
+                </select>
+              </div>
+              <button 
+                  onClick={(e) => {
+                      e.preventDefault(); // 确保没有阻止默认行为
+                      handleAddDiscount();
+                  }}
+                  >
+                  添加折扣
+              </button>
+            </div>
+
+            {/* 所有折扣列表 */}
+            <div className="discount-list-container">
+              <h3>所有折扣计划</h3>
+              <table className="discount-table">
+                <thead>
+                  <tr>
+                    <th>ID</th>
+                    <th>名称</th>
+                    <th>开始时间</th>
+                    <th>结束时间</th>
+                    <th>类型</th>
+                    <th>创建时间</th>
+                    <th>状态</th>
+                    <th>操作</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  {discounts.map(discount => (
+                    <tr key={discount.id}>
+                      <td>{discount.id}</td>
+                      <td>{discount.name}</td>
+                      <td>{formatDateTime(discount.startTime)}</td>
+                      <td>{formatDateTime(discount.endTime)}</td>
+                      <td>{translateDiscountType(discount.discountType)}</td>
+                      <td>{formatDateTime(discount.createTime)}</td>
+                      <td>{discount.status || '未知'}</td>
+                      <td>
+                        <button 
+                          onClick={() => handleDeleteDiscount(discount.id)}
+                          className="delete-button"
+                        >
+                          删除
+                        </button>
+                      </td>
+                    </tr>
+                  ))}
+                </tbody>
+              </table>
+            </div>
+          </>
+        ) : (
+        /* 新增的公告管理部分 */
+        <>
+          {/* 发布新公告表单 */}
+          <div className="announcement-form">
+            <h3>发布新公告</h3>
+            <div className="form-group">
+              <label>公告标题:</label>
+              <input
+                type="text"
+                value={newAnnouncement.title}
+                onChange={(e) => setNewAnnouncement({
+                  ...newAnnouncement,
+                  title: e.target.value
+                })}
+                placeholder="输入公告标题"
+              />
             </div>
             <div className="form-group">
-              <label>折扣类型:</label>
-              <select
-                value={newDiscount.discountType}
-                onChange={(e) => setNewDiscount({...newDiscount, discountType: e.target.value})}
-              >
-                <option value="FREE">全部免费</option>
-                <option value="HALF">半价下载</option>
-                <option value="DOUBLE">双倍上传</option>
-              </select>
+              <label>公告内容:</label>
+              <textarea
+                value={newAnnouncement.content}
+                onChange={(e) => setNewAnnouncement({
+                  ...newAnnouncement,
+                  content: e.target.value
+                })}
+                rows="5"
+                placeholder="输入公告内容"
+              />
             </div>
-            <button 
-                onClick={(e) => {
-                    e.preventDefault(); // 确保没有阻止默认行为
-                    handleAddDiscount();
-                }}
-                >
-                添加折扣
+            <button onClick={handlePostAnnouncement}>
+              发布公告
             </button>
           </div>
 
-          {/* 所有折扣列表 */}
-          <div className="discount-list-container">
-            <h3>所有折扣计划</h3>
-            <table className="discount-table">
+          {/* 所有公告列表 */}
+          <div className="announcement-list-container">
+            <h3>所有公告</h3>
+            <table className="announcement-table">
               <thead>
                 <tr>
-                  <th>ID</th>
-                  <th>名称</th>
-                  <th>开始时间</th>
-                  <th>结束时间</th>
-                  <th>类型</th>
-                  <th>创建时间</th>
-                  <th>状态</th>
-                  <th>操作</th>
+                  <th>标题</th>
+                  <th>内容</th>
+                  <th>发布时间</th>
                 </tr>
               </thead>
               <tbody>
-                {discounts.map(discount => (
-                  <tr key={discount.id}>
-                    <td>{discount.id}</td>
-                    <td>{discount.name}</td>
-                    <td>{formatDateTime(discount.startTime)}</td>
-                    <td>{formatDateTime(discount.endTime)}</td>
-                    <td>{translateDiscountType(discount.discountType)}</td>
-                    <td>{formatDateTime(discount.createTime)}</td>
-                    <td>{discount.status || '未知'}</td>
-                    <td>
-                      <button 
-                        onClick={() => handleDeleteDiscount(discount.id)}
-                        className="delete-button"
-                      >
-                        删除
-                      </button>
-                    </td>
+                {announcements.map(announcement => (
+                  <tr key={announcement.id}>
+                    <td>{announcement.title}</td>
+                    <td>{announcement.content}</td>
+                    <td>{formatDateTime(announcement.createTime)}</td>
                   </tr>
                 ))}
               </tbody>
diff --git a/src/components/Administer.test.jsx b/src/components/Administer.test.jsx
index 5a452fa..7f446ed 100644
--- a/src/components/Administer.test.jsx
+++ b/src/components/Administer.test.jsx
@@ -3,193 +3,122 @@
 import { MemoryRouter } from 'react-router-dom';
 import '@testing-library/jest-dom';
 import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
 import Administer from './Administer';
 
-// Mock axios
-jest.mock('axios');
-
-// Mock localStorage
-const localStorageMock = {
-  getItem: jest.fn(),
-  setItem: jest.fn(),
-  removeItem: jest.fn(),
-  clear: jest.fn(),
-};
-global.localStorage = localStorageMock;
-
 describe('Administer Component', () => {
-  beforeEach(() => {
-    localStorageMock.getItem.mockReturnValue('test-token');
-    jest.clearAllMocks();
+  let mock;
+
+  beforeAll(() => {
+    mock = new MockAdapter(axios);
+    localStorage.setItem('token', 'test-token');
   });
 
-  const mockUsers = [
-    {
-      username: 'user1',
-      authority: 'USER',
-      registTime: '2023-01-01T00:00:00',
-      lastLogin: '2023-05-01T00:00:00',
-      upload: 1000,
-      download: 500,
-      shareRate: 2.0,
-      magicPoints: 100
-    },
-    {
-      username: 'admin1',
-      authority: 'ADMIN',
-      registTime: '2023-01-15T00:00:00',
-      lastLogin: '2023-05-10T00:00:00',
-      upload: 5000,
-      download: 1000,
-      shareRate: 5.0,
-      magicPoints: 500
-    }
-  ];
+  afterEach(() => {
+    mock.reset();
+  });
 
-  const mockDiscounts = [
-    {
-      id: 1,
-      name: '五一活动',
-      discountType: 'FREE',
-      startTime: '2023-05-01T00:00:00',
-      endTime: '2023-05-07T23:59:59',
-      createTime: '2023-04-25T10:00:00',
-      status: '已过期'
-    },
-    {
-      id: 2,
-      name: '端午节活动',
-      discountType: 'HALF',
-      startTime: '2023-06-10T00:00:00',
-      endTime: '2023-06-15T23:59:59',
-      createTime: '2023-05-30T10:00:00',
-      status: '已过期'
-    }
-  ];
+  afterAll(() => {
+    mock.restore();
+  });
 
-  const mockCurrentDiscount = {
-    id: 3,
-    name: '国庆活动',
-    discountType: 'DOUBLE',
-    startTime: '2023-10-01T00:00:00',
-    endTime: '2023-10-07T23:59:59',
-    createTime: '2023-09-25T10:00:00',
-    status: '进行中'
-  };
+  test('renders user management tab by default', async () => {
+    mock.onGet('http://localhost:8088/user/allUser').reply(200, {
+      code: 200,
+      data: { data: [] }
+    });
 
-  const renderAdminister = () => {
-    return render(
+    render(
       <MemoryRouter>
         <Administer />
       </MemoryRouter>
     );
-  };
 
-  test('renders Administer component with user tab by default', async () => {
-    axios.get.mockResolvedValueOnce({
-      data: {
-        code: 200,
-        data: { data: mockUsers }
-      }
-    });
-
-    renderAdminister();
-
-    expect(screen.getByText('系统管理')).toBeInTheDocument();
     expect(screen.getByText('用户管理')).toBeInTheDocument();
     expect(screen.getByText('折扣管理')).toBeInTheDocument();
-
-    await waitFor(() => {
-      expect(screen.getByText('user1')).toBeInTheDocument();
-    });
-    
-    expect(screen.getByText('admin1')).toBeInTheDocument();
+    expect(screen.getByText('公告管理')).toBeInTheDocument();
   });
 
-  test('switches between user and discount tabs', async () => {
-    axios.get
-      .mockResolvedValueOnce({
-        data: {
-          code: 200,
-          data: { data: mockUsers }
-        }
-      })
-      .mockResolvedValueOnce({
-        data: {
-          code: 200,
-          data: { data: mockDiscounts }
-        }
-      })
-      .mockResolvedValueOnce({
-        data: {
-          code: 200,
-          data: { data: mockCurrentDiscount }
-        }
-      });
+  test('fetches and displays users', async () => {
+    const mockUsers = [
+      {
+        username: 'testuser',
+        authority: 'USER',
+        registTime: '2023-01-01',
+        lastLogin: '2023-05-01',
+        upload: 1000,
+        download: 500,
+        shareRate: 2.0,
+        magicPoints: 100
+      }
+    ];
 
-    renderAdminister();
+    mock.onGet('http://localhost:8088/user/allUser').reply(200, {
+      code: 200,
+      data: { data: mockUsers }
+    });
+
+    render(
+      <MemoryRouter>
+        <Administer />
+      </MemoryRouter>
+    );
 
     await waitFor(() => {
-      expect(screen.getByText('user1')).toBeInTheDocument();
+      expect(screen.getByText('testuser')).toBeInTheDocument();
     });
+  });
+
+  test('handles user search', async () => {
+    const mockUsers = [
+      {
+        username: 'searchuser',
+        authority: 'USER'
+      }
+    ];
+
+    mock.onGet('http://localhost:8088/user/searchUser').reply(200, {
+      code: 200,
+      data: { data: mockUsers }
+    });
+
+    render(
+      <MemoryRouter>
+        <Administer />
+      </MemoryRouter>
+    );
+
+    fireEvent.change(screen.getByPlaceholderText('输入用户名搜索'), {
+      target: { value: 'search' }
+    });
+    fireEvent.click(screen.getByText('搜索'));
+
+    await waitFor(() => {
+      expect(screen.getByText('searchuser')).toBeInTheDocument();
+    });
+  });
+
+  test('switches between tabs', async () => {
+    mock.onGet('http://localhost:8088/user/allUser').reply(200, {
+      code: 200,
+      data: { data: [] }
+    });
+
+    mock.onGet('http://localhost:8088/discount/all').reply(200, {
+      code: 200,
+      data: { data: [] }
+    });
+
+    render(
+      <MemoryRouter>
+        <Administer />
+      </MemoryRouter>
+    );
 
     fireEvent.click(screen.getByText('折扣管理'));
 
     await waitFor(() => {
-      expect(screen.getByText('五一活动')).toBeInTheDocument();
-    });
-    
-    expect(screen.getByText('国庆活动')).toBeInTheDocument();
-  });
-
- 
-
-  test('changes user authority', async () => {
-    axios.get.mockResolvedValueOnce({
-      data: {
-        code: 200,
-        data: { data: mockUsers }
-      }
-    });
-    axios.put.mockResolvedValueOnce({
-      data: {
-        code: 200,
-        message: '修改用户权限成功'
-      }
-    });
-
-    renderAdminister();
-
-    await waitFor(() => {
-      expect(screen.getByText('user1')).toBeInTheDocument();
-    });
-
-    const selectElement = screen.getAllByRole('combobox')[0];
-    fireEvent.change(selectElement, { target: { value: 'ADMIN' } });
-
-    await waitFor(() => {
-      expect(axios.put).toHaveBeenCalled();
-    });
-    
-    expect(axios.put).toHaveBeenCalledWith(
-      expect.stringContaining('/user/changeAuthority'),
-      {
-        changeUsername: 'user1',
-        authority: 'ADMIN'
-      },
-      expect.any(Object)
-    );
-  });
-
-
-
-  test('shows error messages', async () => {
-    axios.get.mockRejectedValueOnce(new Error('Network Error'));
-
-    renderAdminister();
-
-    await waitFor(() => {
-      expect(screen.getByText(/获取用户列表失败/)).toBeInTheDocument();
+      expect(screen.getByText('添加新折扣')).toBeInTheDocument();
     });
   });
 });
\ No newline at end of file
diff --git a/src/components/AnnouncementDetail.jsx b/src/components/AnnouncementDetail.jsx
index 8e90672..ac3b92c 100644
--- a/src/components/AnnouncementDetail.jsx
+++ b/src/components/AnnouncementDetail.jsx
@@ -1,12 +1,19 @@
-import React from 'react';

-import { useNavigate, useLocation } from 'react-router-dom';

+import React, { useEffect, useState } from 'react';

+import { useNavigate, useLocation, useParams } from 'react-router-dom';

+import { getAnnouncementDetail } from '../api/announcement';

+import { message } from 'antd';

 import './AnnouncementDetail.css';

 

 const AnnouncementDetail = () => {

   const navigate = useNavigate();

-const location = useLocation();

+  const { id } = useParams();

+  const [announcement, setAnnouncement] = useState(null);

+  const [loading, setLoading] = useState(true);

+  const [error, setError] = useState('');

+  const location = useLocation();

   const { state } = useLocation();

-  const announcement = state?.announcement;

+  

+  

   const handleBack = () => {

     const fromTab = location.state?.fromTab; // 从跳转时传递的 state 中获取

     if (fromTab) {

@@ -15,16 +22,36 @@
         navigate(-1); // 保底策略

     }

 }

+  useEffect(() => {

+    const fetchAnnouncement = async () => {

+      try {

+        setLoading(true);

+        const response = await getAnnouncementDetail(id);

+        if (response.data.code === 200) {

+          setAnnouncement(response.data.data.announcement);

+        } else {

+          setError(response.data.message || '获取公告详情失败');

+        }

+      } catch (err) {

+        setError('获取公告详情失败');

+        message.error('获取公告详情失败');

+      } finally {

+        setLoading(false);

+      }

+    };

+

+    fetchAnnouncement();

+  }, [id]);

   

     

 

-  if (!announcement) {

+  if (error || !announcement) {

     return (

       <div className="announcement-container">

-        <button className="back-button" onClick={() => navigate(-1)}>

-          &larr; 返回公告区

+        <button className="back-button" onClick={handleBack}>

+          &larr; 返回

         </button>

-        <div className="error-message">公告加载失败,请返回重试</div>

+        <div className="error-message">{error || '公告不存在'}</div>

       </div>

     );

   }

@@ -39,9 +66,7 @@
         <div className="announcement-header">

           <h1>{announcement.title}</h1>

           <div className="announcement-meta">

-            <span className="category-badge">{announcement.category}</span>

-            <span>发布人:{announcement.author}</span>

-            <span>发布日期:{announcement.date}</span>

+            <span>发布日期:{announcement.createTime}</span>

           </div>

         </div>

 

diff --git a/src/components/AnnouncementDetail.test.jsx b/src/components/AnnouncementDetail.test.jsx
new file mode 100644
index 0000000..7f44ecd
--- /dev/null
+++ b/src/components/AnnouncementDetail.test.jsx
@@ -0,0 +1,116 @@
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { MemoryRouter, Route, Routes, useNavigate, useParams, useLocation } from 'react-router-dom';
+import AnnouncementDetail from './AnnouncementDetail';
+import * as announcementApi from '../api/announcement';
+
+// Mock API 模块
+jest.mock('../api/announcement');
+
+// Mock 路由钩子
+const mockNavigate = jest.fn();
+jest.mock('react-router-dom', () => ({
+  ...jest.requireActual('react-router-dom'),
+  useNavigate: () => mockNavigate,
+  useParams: jest.fn(),
+  useLocation: jest.fn(),
+}));
+
+describe('AnnouncementDetail 组件', () => {
+  const mockAnnouncement = {
+    id: 1,
+    title: '测试公告标题',
+    content: '这是测试公告内容\n这是第二行内容',
+    
+    createTime: '2023-01-01T00:00:00Z',
+    
+  };
+
+  beforeEach(() => {
+    // 重置 mock 函数
+    mockNavigate.mockClear();
+    
+    // 设置模拟的 API 响应
+    announcementApi.getAnnouncementDetail.mockResolvedValue({
+      data: {
+        code: 200,
+        data: {
+          announcement: mockAnnouncement
+        }
+      }
+    });
+
+    // 模拟 useParams
+    useParams.mockReturnValue({ id: '1' });
+
+    // 模拟 useLocation
+    useLocation.mockReturnValue({
+      state: { fromTab: 'announcement' }
+    });
+  });
+
+  const renderComponent = () => {
+    return render(
+      <MemoryRouter initialEntries={['/announcement/1']}>
+        <Routes>
+          <Route path="/announcement/:id" element={<AnnouncementDetail />} />
+          {/* 添加一个模拟的公告列表路由用于导航测试 */}
+          <Route path="/dashboard/announcement" element={<div>公告列表</div>} />
+        </Routes>
+      </MemoryRouter>
+    );
+  };
+
+  it('应该正确加载和显示公告详情', async () => {
+    renderComponent();
+
+    
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('测试公告标题')).toBeInTheDocument();
+      expect(screen.getByText('这是测试公告内容')).toBeInTheDocument();
+      expect(screen.getByText('这是第二行内容')).toBeInTheDocument();
+    });
+  });
+
+  it('应该处理公告加载失败的情况', async () => {
+    // 模拟 API 返回错误
+    announcementApi.getAnnouncementDetail.mockResolvedValue({
+      data: {
+        code: 404,
+        message: '公告不存在'
+      }
+    });
+
+    renderComponent();
+
+    await waitFor(() => {
+      expect(screen.getByText('公告不存在')).toBeInTheDocument();
+    });
+  });
+
+  it('应该能够返回公告区', async () => {
+    renderComponent();
+  
+    await waitFor(() => {
+      expect(screen.getByText('测试公告标题')).toBeInTheDocument();
+    });
+  
+    fireEvent.click(screen.getByRole('button', { name: /返回/i }));
+  
+    // 检查是否导航回公告区
+    expect(mockNavigate).toHaveBeenCalledWith('/dashboard/announcement');
+  });
+
+  it('应该显示加载错误时的UI', async () => {
+    // 模拟 API 抛出错误
+    announcementApi.getAnnouncementDetail.mockRejectedValue(new Error('网络错误'));
+
+    renderComponent();
+
+    await waitFor(() => {
+      expect(screen.getByText('获取公告详情失败')).toBeInTheDocument();
+    });
+  });
+});
\ No newline at end of file
diff --git a/src/components/Dashboard.css b/src/components/Dashboard.css
index a6b3a5a..d3a5471 100644
--- a/src/components/Dashboard.css
+++ b/src/components/Dashboard.css
@@ -717,6 +717,154 @@
   background-color: #218838;

 }

 

+/* 下载模态框样式 */

+.modal-overlay {

+    position: fixed;

+    top: 0;

+    left: 0;

+    right: 0;

+    bottom: 0;

+    background: rgba(0, 0, 0, 0.5);

+    z-index: 1000;

+    display: flex;

+    justify-content: center;

+    align-items: center;

+}

+

+.download-modal {

+    background: white;

+    padding: 20px;

+    border-radius: 8px;

+    width: 500px;

+    max-width: 90%;

+    position: relative;

+}

+

+.download-modal .close-btn {

+    position: absolute;

+    top: 10px;

+    right: 10px;

+    background: none;

+    border: none;

+    font-size: 20px;

+    cursor: pointer;

+}

+

+.download-modal .form-group {

+    margin-bottom: 15px;

+}

+

+.download-modal .form-group label {

+    display: block;

+    margin-bottom: 5px;

+}

+

+.download-modal .form-group input {

+    width: 100%;

+    padding: 8px;

+    border: 1px solid #ddd;

+    border-radius: 4px;

+}

+

+.progress-container {

+    margin: 15px 0;

+    background: #f0f0f0;

+    border-radius: 4px;

+    height: 20px;

+    overflow: hidden;

+}

+

+.progress-bar {

+    height: 100%;

+    background: #1890ff;

+    transition: width 0.3s;

+    color: white;

+    text-align: center;

+    font-size: 12px;

+    line-height: 20px;

+}

+

+.modal-actions {

+    display: flex;

+    justify-content: flex-end;

+    gap: 10px;

+    margin-top: 20px;

+}

+

+.modal-actions button {

+    padding: 8px 16px;

+    border-radius: 4px;

+    cursor: pointer;

+}

+

+.modal-actions button:first-child {

+    background: #f5f5f5;

+    border: 1px solid #d9d9d9;

+}

+

+.modal-actions button:last-child {

+    background: #1890ff;

+    color: white;

+    border: none;

+}

+

+.modal-actions button:disabled {

+    background: #d9d9d9;

+    cursor: not-allowed;

+    opacity: 0.7;

+}

+

+/* 在Dashboard.css中添加以下样式 */

+

+.resource-actions {

+  display: flex;

+  gap: 10px;

+  margin-left: auto;

+}

+

+.delete-btn {

+  padding: 8px 15px;

+  background-color: #ff4d4f;

+  color: white;

+  border: none;

+  border-radius: 4px;

+  cursor: pointer;

+  transition: background-color 0.3s;

+}

+

+.delete-btn:hover {

+  background-color: #ff7875;

+}

+

+.delete-btn:disabled {

+  background-color: #d9d9d9;

+  cursor: not-allowed;

+}

+

+.carousel-image {

+  padding: 1rem;

+  background: linear-gradient(to right, #f7b733, #fc4a1a);

+  color: white;

+  border-radius: 10px;

+  height: 180px;

+  display: flex;

+  flex-direction: column;

+  justify-content: center;

+  align-items: center;

+  font-size: 18px;

+  transition: all 0.5s ease-in-out;

+}

+

+.carousel-image h3 {

+  font-size: 22px;

+  margin: 0;

+}

+

+.carousel-image p {

+  margin: 4px 0;

+}

+

+

 /* 平台名称样式 */

 .platform-name {

   flex: 1;

diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx
index f8f5dd3..40bd19d 100644
--- a/src/components/Dashboard.jsx
+++ b/src/components/Dashboard.jsx
@@ -1,9 +1,13 @@
 import React, {useEffect, useState} from 'react';

 import {useNavigate, useLocation, useParams} from 'react-router-dom';  

-import {createTorrent, getTorrents, searchTorrents} from '../api/torrent';

+import {createTorrent, getTorrents, downloadTorrent, getDownloadProgress, deleteTorrent, searchTorrents} from '../api/torrent';

 import './Dashboard.css';

-import {createPost, getPosts, getPostDetail, searchPosts} from '../api/helpPost';

-import { getUserInfo, isAdmin } from '../api/auth';

+import {createHelpPost, getHelpPosts, getHelpPostDetail, searchHelpPosts} from '../api/helpPost';

+import {createRequestPost, getRequestPosts, getRequestPostDetail, searchRequestPosts} from '../api/requestPost';

+import { message } from 'antd'; // 用于显示提示消息

+import { getAnnouncements,getLatestAnnouncements,getAnnouncementDetail } from '../api/announcement'; 

+import { getAllDiscounts } from '../api/administer'; 

+   import { getUserInfo, isAdmin } from '../api/auth';

 import { api } from '../api/auth'; 

 

 

@@ -36,6 +40,11 @@
     const [currentPage, setCurrentPage] = useState(1);

     const [totalPages, setTotalPages] = useState(1);

     const [likedPosts,setLikedPosts] = useState({});

+    const [announcements, setAnnouncements] = useState([]);

+    const [carouselDiscounts, setCarouselDiscounts] = useState([]);

+    const [requestLoading, setRequestLoading] = useState(false);

+    const [requestError, setRequestError] = useState(null);

+    const [requestPosts, setRequestPosts] = useState([]);

 

 

     // 添加状态

@@ -46,6 +55,13 @@
     const [filteredResources, setFilteredResources] = useState(torrentPosts);

     const [isAdmin, setIsAdmin] = useState(false);

 

+    // 在组件状态中添加

+    const [showDownloadModal, setShowDownloadModal] = useState(false);

+    const [selectedTorrent, setSelectedTorrent] = useState(null);

+    const [downloadProgress, setDownloadProgress] = useState(0);

+    const [isDownloading, setIsDownloading] = useState(false);

+    const [downloadPath, setDownloadPath] = useState('D:/studies/ptPlatform/torrent');

+

     // 新增搜索状态

     const [announcementSearch, setAnnouncementSearch] = useState('');

     const [shareSearch, setShareSearch] = useState('');

@@ -66,150 +82,54 @@
         });

     };

 

-    //公告区

-    const [announcements] = useState([

-        {

-            id: 1,

-            title: '系统维护与更新',

-            content: '2023-10-15 02:00-06:00将进行系统维护升级,期间网站将无法访问。本次更新包含:\n1. 数据库服务器迁移\n2. 安全补丁更新\n3. CDN节点优化\n\n请提前做好下载安排。',

-            author: '系统管理员',

-            date: '2023-10-10',

-            excerpt: '2023-10-15 02:00-06:00将进行系统维护,期间无法访问',

-            category: '系统'

-        },

-        {

-            id: 2,

-            title: '资源上新',

-            content: '最新热门电影《奥本海默》4K REMUX资源已上线,包含:\n- 4K HDR版本 (56.8GB)\n- 1080P标准版 (12.3GB)\n- 中英双语字幕\n\n欢迎下载保种!',

-            author: '资源组',

-            date: '2023-10-08',

-            excerpt: '最新热门电影《奥本海默》4K资源已上线',

-            category: '资源'

-        },

-        {

-            id: 3,

-            title: '积分规则调整',

-            content: '自11月1日起,上传资源积分奖励提升20%,具体规则如下:\n- 上传电影资源:每GB 10积分\n- 上传电视剧资源:每GB 8积分\n- 上传动漫资源:每GB 6积分\n\n感谢大家的支持与贡献!',

-            author: '管理员',

-            date: '2023-10-05',

-            excerpt: '自11月1日起,上传资源积分奖励提升20%',

-            category: '公告'

-        },

-        {

-            id: 4,

-            title: '违规处理公告',

-            content: '用户user123因发布虚假资源已被封禁,相关资源已删除。请大家遵守社区规则,维护良好的分享环境。',

-            author: '管理员',

-            date: '2023-10-03',

-            excerpt: '用户user123因发布虚假资源已被封禁',

-            category: '违规'

-        },

-        {

-            id: 5,

-            title: '节日活动',

-            content: '国庆期间所有资源下载积分减半,活动时间:2023年10月1日至2023年10月7日。',

-            author: '活动组',

-            date: '2023-09-30',

-            excerpt: '国庆期间所有资源下载积分减半',

-            category: '活动'

-        },

-        {

-            id: 6,

-            title: '客户端更新',

-            content: 'PT客户端v2.5.0已发布,修复多个BUG,新增资源搜索功能。请尽快更新到最新版本以获得更好的使用体验。',

-            author: '开发组',

-            date: '2023-09-28',

-            excerpt: 'PT客户端v2.5.0已发布,修复多个BUG',

-            category: '更新'

-        },

-        // 其他公告...

-    ]);

-

-    // 公告区搜索处理

-    const handleSearchAnnouncement = (e) => {

-        setAnnouncementSearch(e.target.value);

-    };

-

-    // 修改后的搜索函数

-    const handleSearchShare = async () => {

-        try {

-            setTorrentLoading(true);

-            const response = await searchTorrents(shareSearch, 1);

-            if (response.data.code === 200) {

-                setTorrentPosts(response.data.data.records);

-                const total = response.data.data.total;

-                setTotalPages(Math.ceil(total / 5));

-                setCurrentPage(1);

-            } else {

-                setTorrentError(response.data.message || '搜索失败');

-            }

-        } catch (err) {

-            setTorrentError(err.message || '搜索失败');

-        } finally {

-            setTorrentLoading(false);

-        }

-    };

-

-    const handleResetShareSearch = async () => {

-        setShareSearch('');

-        setSelectedFilters(

-            Object.keys(filterCategories).reduce((acc, category) => {

-                acc[category] = 'all';

-                return acc;

-            }, {})

-        );

-        await fetchTorrentPosts(1, true);

-    };

-

-    // 求种区搜索处理

-    const handleSearchRequest = (e) => {

-        setRequestSearch(e.target.value);

-    };

-

-    // 添加搜索函数

-    const handleSearchHelp = async () => {

-        try {

-        setHelpLoading(true);

-        const response = await searchPosts(helpSearch, currentPage);

-        if (response.data.code === 200) {

-            const postsWithCounts = await Promise.all(

-            response.data.data.records.map(async (post) => {

-                try {

-                const detailResponse = await getPostDetail(post.id);

-                if (detailResponse.data.code === 200) {

-                    return {

-                    ...post,

-                    replyCount: detailResponse.data.data.post.replyCount || 0,

-                    isLiked: false

-                    };

-                }

-                return post;

-                } catch (err) {

-                console.error(`获取帖子${post.id}详情失败:`, err);

-                return post;

-                }

-            })

-            );

-            setHelpPosts(postsWithCounts);

-            setTotalPages(Math.ceil(response.data.data.total / 5));

-        } else {

-            setHelpError(response.data.message || '搜索失败');

-        }

-        } catch (err) {

-        setHelpError(err.message || '搜索失败');

-        } finally {

-        setHelpLoading(false);

-        }

-    };

     

-    // 添加重置搜索函数

-    const handleResetHelpSearch = async () => {

-        setHelpSearch('');

-        await fetchHelpPosts(1); // 重置到第一页

+

+    //公告区

+    // 添加获取公告的方法

+    const fetchAnnouncements = async () => {

+    try {

+        const response = await getLatestAnnouncements();

+        setAnnouncements(response.data.data.announcements || []);

+    } catch (error) {

+        console.error('获取公告失败:', error);

+    }

     };

 

+    useEffect(() => {

+        if (activeTab === 'announcement') {

+            fetchAnnouncements();

+            fetchDiscountsForCarousel();

+        }

+    }, [activeTab]);

 

+    const fetchDiscountsForCarousel = async () => {

+        try {

+            const all = await getAllDiscounts();

+            console.log("返回的折扣数据:", all);

+            const now = new Date();

 

+            // ⚠️ 使用 Date.parse 确保兼容 ISO 格式

+            const ongoing = all.filter(d =>

+            Date.parse(d.startTime) <= now.getTime() && Date.parse(d.endTime) >= now.getTime()

+            );

+

+            const upcoming = all

+            .filter(d => Date.parse(d.startTime) > now.getTime())

+            .sort((a, b) => Date.parse(a.startTime) - Date.parse(b.startTime));

+

+            const selected = [...ongoing.slice(0, 3)];

+

+            while (selected.length < 3 && upcoming.length > 0) {

+            selected.push(upcoming.shift());

+            }

+

+            setCarouselDiscounts(selected);

+        } catch (e) {

+            console.error("获取折扣失败:", e);

+        }

+    };

+

+    // 修改handleAnnouncementClick函数中的state传递,移除不必要的字段

     const handleAnnouncementClick = (announcement, e) => {

         if (!e.target.closest('.exclude-click')) {

             navigate(`/announcement/${announcement.id}`, {

@@ -223,6 +143,129 @@
         }

     };

 

+    

+   // 公告区搜索处理

+    const handleSearchAnnouncement = (e) => {

+        setAnnouncementSearch(e.target.value);

+    };

+

+    // 修改后的搜索函数

+    const handleSearchShare = async () => {

+        try {

+            setTorrentLoading(true);

+            const response = await searchTorrents(shareSearch, 1);

+            if (response.data.code === 200) {

+                setTorrentPosts(response.data.data.records);

+                const total = response.data.data.total;

+                setTotalPages(Math.ceil(total / 5));

+                setCurrentPage(1);

+            } else {

+                setTorrentError(response.data.message || '搜索失败');

+            }

+        } catch (err) {

+            setTorrentError(err.message || '搜索失败');

+        } finally {

+            setTorrentLoading(false);

+        }

+    };

+

+    const handleResetShareSearch = async () => {

+        setShareSearch('');

+        setSelectedFilters(

+            Object.keys(filterCategories).reduce((acc, category) => {

+                acc[category] = 'all';

+                return acc;

+            }, {})

+        );

+        await fetchTorrentPosts(1, true);

+    };

+

+    // 添加搜索函数

+    const handleSearchRequest = async () => {

+        try {

+        setRequestLoading(true);

+        const response = await searchRequestPosts(requestSearch, currentPage);

+        if (response.data.code === 200) {

+            const postsWithCounts = await Promise.all(

+            response.data.data.records.map(async (post) => {

+                try {

+                const detailResponse = await getRequestPostDetail(post.id);

+                if (detailResponse.data.code === 200) {

+                    return {

+                    ...post,

+                    replyCount: detailResponse.data.data.post.replyCount || 0,

+                    isLiked: false

+                    };

+                }

+                return post;

+                } catch (err) {

+                console.error(`获取帖子${post.id}详情失败:`, err);

+                return post;

+                }

+            })

+            );

+            setRequestPosts(postsWithCounts);

+            setTotalPages(Math.ceil(response.data.data.total / 5));

+        } else {

+            setRequestError(response.data.message || '搜索失败');

+        }

+        } catch (err) {

+        setRequestError(err.message || '搜索失败');

+        } finally {

+        setRequestLoading(false);

+        }

+    };

+    

+    // 添加重置搜索函数

+    const handleResetRequestSearch = async () => {

+        setRequestSearch('');

+        await fetchRequestPosts(1); // 重置到第一页

+ };

+

+   // 添加搜索函数

+    const handleSearchHelp = async () => {

+        try {

+        setHelpLoading(true);

+        const response = await searchHelpPosts(helpSearch, currentPage);

+        if (response.data.code === 200) {

+            const postsWithCounts = await Promise.all(

+            response.data.data.records.map(async (post) => {

+                try {

+                const detailResponse = await getHelpPostDetail(post.id);

+                if (detailResponse.data.code === 200) {

+                    return {

+                    ...post,

+                    replyCount: detailResponse.data.data.post.replyCount || 0,

+                    isLiked: false

+                    };

+                }

+                return post;

+                } catch (err) {

+                console.error(`获取帖子${post.id}详情失败:`, err);

+                return post;

+                }

+            })

+            );

+            setHelpPosts(postsWithCounts);

+            setTotalPages(Math.ceil(response.data.data.total / 5));

+        } else {

+            setHelpError(response.data.message || '搜索失败');

+        }

+        } catch (err) {

+        setHelpError(err.message || '搜索失败');

+        } finally {

+        setHelpLoading(false);

+        }

+    };

+    

+    // 添加重置搜索函数

+    const handleResetHelpSearch = async () => {

+        setHelpSearch('');

+        await fetchHelpPosts(1); // 重置到第一页

+ };

+

+

+

 

     //资源区

     const handleFileChange = (e) => {

@@ -243,7 +286,10 @@
                 subtitle: uploadData.subtitle

             };

 

-            await createTorrent(torrentData, uploadData.file);

+            await createTorrent(uploadData.file, torrentData, (progress) => {

+                console.log(`上传进度: ${progress}%`);

+                // 这里可以添加进度条更新逻辑

+                });

 

             // 上传成功处理

             setShowUploadModal(false);

@@ -269,11 +315,164 @@
         }

     };

 

-    const handlePostSubmit = async (e) => {

+    // 处理下载按钮点击

+    const handleDownloadClick = (torrent, e) => {

+        e.stopPropagation();

+        setSelectedTorrent(torrent);

+        setShowDownloadModal(true);

+    };

+

+    // 执行下载

+    const handleDownload = async () => {

+    if (!selectedTorrent || !downloadPath) return;

+

+    setIsDownloading(true);

+    setDownloadProgress(0);

+

+    try {

+        // 标准化路径

+        const cleanPath = downloadPath

+        .replace(/\\/g, '/')  // 统一使用正斜杠

+        .replace(/\/+/g, '/') // 去除多余斜杠

+        .trim();

+

+        // 确保路径以斜杠结尾

+        const finalPath = cleanPath.endsWith('/') ? cleanPath : cleanPath + '/';

+

+        // 发起下载请求

+        await downloadTorrent(selectedTorrent.id, finalPath);

+        

+        // 开始轮询进度

+        const interval = setInterval(async () => {

+            try {

+                const res = await getDownloadProgress();

+                const progresses = res.data.progresses;

+                

+                if (progresses) {

+                    // 使用完整的 torrent 文件路径作为键

+                    const torrentPath = selectedTorrent.filePath.replace(/\\/g, '/');

+                    const torrentHash = selectedTorrent.hash;

+                    // 查找匹配的进度

+                    let foundProgress = null;

+                    for (const [key, value] of Object.entries(progresses)) {

+                        const normalizedKey = key.replace(/\\/g, '/');

+                        if (normalizedKey.includes(selectedTorrent.hash) || 

+                            normalizedKey.includes(selectedTorrent.torrentName)) {

+                            foundProgress = value;

+                            break;

+                        }

+                    }

+                    if (foundProgress !== null) {

+                        const newProgress = Math.round(foundProgress * 100);

+                        setDownloadProgress(newProgress);

+                        

+                        // 检查是否下载完成

+                        if (newProgress >= 100) {

+                        clearInterval(interval);

+                        setIsDownloading(false);

+                        message.success('下载完成!');

+                        setTimeout(() => setShowDownloadModal(false), 2000);

+                        }

+                    } else {

+                        console.log('当前下载进度:', progresses); // 添加日志

+                    }

+                    }

+                } catch (error) {

+                    console.error('获取进度失败:', error);

+                    // 如果获取进度失败但文件已存在,也视为完成

+                    const filePath = `${finalPath}${selectedTorrent.torrentName || 'downloaded_file'}`;

+                    try {

+                    const exists = await checkFileExists(filePath);

+                    if (exists) {

+                        clearInterval(interval);

+                        setDownloadProgress(100);

+                        setIsDownloading(false);

+                        message.success('下载完成!');

+                        setTimeout(() => setShowDownloadModal(false), 2000);

+                    }

+                    } catch (e) {

+                    console.error('文件检查失败:', e);

+                    }

+                }

+                }, 2000);

+

+        return () => clearInterval(interval);

+    } catch (error) {

+        setIsDownloading(false);

+        if (error.response && error.response.status === 409) {

+        message.error('分享率不足,无法下载此资源');

+        } else {

+        message.error('下载失败: ' + (error.message || '未知错误'));

+        }

+    }

+    };

+

+    const checkFileExists = async (filePath) => {

+        try {

+            // 这里需要根据您的实际环境实现文件存在性检查

+            // 如果是Electron应用,可以使用Node.js的fs模块

+            // 如果是纯前端,可能需要通过API请求后端检查

+            return true; // 暂时返回true,实际实现需要修改

+        } catch (e) {

+            console.error('检查文件存在性失败:', e);

+            return false;

+        }

+    };

+

+    const handleDeleteTorrent = async (torrentId, e) => {

+        e.stopPropagation(); // 阻止事件冒泡,避免触发资源项的点击事件

+        

+        try {

+            // 确认删除

+            if (!window.confirm('确定要删除这个种子吗?此操作不可撤销!')) {

+            return;

+            }

+            

+            // 调用删除API

+            await deleteTorrent(torrentId);

+            

+            // 删除成功后刷新列表

+            message.success('种子删除成功');

+            await fetchTorrentPosts(currentPage);

+        } catch (error) {

+            console.error('删除种子失败:', error);

+            message.error('删除种子失败: ' + (error.response?.data?.message || error.message));

+        }

+        };

+

+        const handleRequestPostSubmit = async (e) => {

         e.preventDefault();

         try {

           const username = localStorage.getItem('username');

-          const response = await createPost(

+          const response = await createRequestPost(

+            postTitle,

+            postContent,

+            username,

+            selectedImage

+          );

+          

+          if (response.data.code === 200) {

+            // 刷新帖子列表

+           

+            await fetchRequestPosts(currentPage);

+            // 重置表单

+            setShowPostModal(false);

+            setPostTitle('');

+            setPostContent('');

+            setSelectedImage(null);

+          } else {

+            setHelpError(response.data.message || '发帖失败');

+          }

+        } catch (err) {

+          setHelpError(err.message || '发帖失败');

+        }

+      };

+

+    const handleHelpPostSubmit = async (e) => {

+        e.preventDefault();

+        try {

+          const username = localStorage.getItem('username');

+          const response = await createHelpPost(

             postTitle,

             postContent,

             username,

@@ -283,6 +482,7 @@
           if (response.data.code === 200) {

             // 刷新帖子列表

             await fetchHelpPosts(currentPage);

+            

             // 重置表单

             setShowPostModal(false);

             setPostTitle('');

@@ -339,6 +539,42 @@
         }

     }, [activeTab]);

 

+const fetchRequestPosts = async (page = 1) => {

+        setRequestLoading(true);

+        try {

+          const response = await getRequestPosts(page);

+          if (response.data.code === 200) {

+            const postsWithCounts = await Promise.all(

+              response.data.data.records.map(async (post) => {

+                try {

+                  const detailResponse = await getRequestPostDetail(post.id);

+                  if (detailResponse.data.code === 200) {

+                    return {

+                      ...post,

+                      replyCount: detailResponse.data.data.post.replyCount || 0,

+                      isLiked: false // 根据需要添加其他字段

+                    };

+                  }

+                  return post; // 如果获取详情失败,返回原始帖子数据

+                } catch (err) {

+                  console.error(`获取帖子${post.id}详情失败:`, err);

+                  return post;

+                }

+              })

+            );

+            setRequestPosts(postsWithCounts);

+            setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条

+            setCurrentPage(page);

+          } else {

+            setRequestError(response.data.message || '获取求助帖失败');

+          }

+        } catch (err) {

+          setRequestError(err.message || '获取求助帖失败');

+        } finally {

+          setRequestLoading(false);

+        }

+      };

+

     const handleImageUpload = (e) => {

         setSelectedImage(e.target.files[0]);

     };

@@ -347,12 +583,12 @@
     const fetchHelpPosts = async (page = 1) => {

         setHelpLoading(true);

         try {

-          const response = await getPosts(page);

+          const response = await getHelpPosts(page);

           if (response.data.code === 200) {

             const postsWithCounts = await Promise.all(

               response.data.data.records.map(async (post) => {

                 try {

-                  const detailResponse = await getPostDetail(post.id);

+                  const detailResponse = await getHelpPostDetail(post.id);

                   if (detailResponse.data.code === 200) {

                     return {

                       ...post,

@@ -382,8 +618,8 @@
 

 

     useEffect(() => {

-        if (activeTab === 'help') {

-            fetchHelpPosts(currentPage);

+        if (activeTab === 'request') {

+            fetchRequestPosts(currentPage);

         }

     }, [activeTab, currentPage]); // 添加 currentPage 作为依赖

 

@@ -541,7 +777,10 @@
     useEffect(() => {

         if (activeTab === 'announcement') {

             const timer = setInterval(() => {

-                setCurrentSlide(prev => (prev + 1) % 3); // 3张轮播图循环

+                setCurrentSlide(prev => {

+                    const count = carouselDiscounts.length || 1;

+                    return (prev + 1) % count;

+                });

             }, 3000);

             return () => clearInterval(timer);

         }

@@ -551,7 +790,9 @@
         if (activeTab === 'help') {

             fetchHelpPosts();

         }

-    }, [activeTab]);

+    }, [activeTab, currentPage]); // 添加 currentPage 作为依赖

+

+    

 

     const renderContent = () => {

         switch (activeTab) {

@@ -575,22 +816,30 @@
                             </button>

                         </div>

                         {/* 轮播图区域 */}

-                        <div className="carousel-container">

-                            <div className={`carousel-slide ${currentSlide === 0 ? 'active' : ''}`}>

-                                <div className="carousel-image gray-bg">促销活动1</div>

+                       <div className="carousel-container">

+                        {carouselDiscounts.length === 0 ? (

+                            <div className="carousel-slide active">

+                            <div className="carousel-image gray-bg">暂无折扣活动</div>

                             </div>

-                            <div className={`carousel-slide ${currentSlide === 1 ? 'active' : ''}`}>

-                                <div className="carousel-image gray-bg">促销活动2</div>

+                        ) : (

+                            carouselDiscounts.map((discount, index) => (

+                            <div key={index} className={`carousel-slide ${currentSlide === index ? 'active' : ''}`}>

+                                <div className="carousel-image gray-bg">

+                                <h3>{discount.type}</h3>

+                                <p>{discount.name}</p>

+                                <p>{new Date(discount.startTime).toLocaleDateString()} ~ {new Date(discount.endTime).toLocaleDateString()}</p>

+                                </div>

                             </div>

-                            <div className={`carousel-slide ${currentSlide === 2 ? 'active' : ''}`}>

-                                <div className="carousel-image gray-bg">促销活动3</div>

-                            </div>

-                            <div className="carousel-dots">

-                                <span className={`dot ${currentSlide === 0 ? 'active' : ''}`}></span>

-                                <span className={`dot ${currentSlide === 1 ? 'active' : ''}`}></span>

-                                <span className={`dot ${currentSlide === 2 ? 'active' : ''}`}></span>

-                            </div>

+                            ))

+                        )}

+                        <div className="carousel-dots">

+                            {carouselDiscounts.map((_, index) => (

+                            <span key={index} className={`dot ${currentSlide === index ? 'active' : ''}`}></span>

+                            ))}

                         </div>

+                        </div>

+

+

 

                         {/* 公告区块区域 */}

                         <div className="announcement-grid">

@@ -601,9 +850,8 @@
                                     onClick={(e) => handleAnnouncementClick(announcement, e)}

                                 >

                                     <h3>{announcement.title}</h3>

-                                    <p>{announcement.excerpt}</p>

+                                    <p>{announcement.content.substring(0, 100)}...</p>

                                     <div className="announcement-footer exclude-click">

-                                        <span>{announcement.author}</span>

                                         <span>{announcement.date}</span>

                                     </div>

                                 </div>

@@ -845,13 +1093,19 @@
                                     </div>

                                     <button

                                         className="download-btn"

-                                        onClick={(e) => {

-                                            e.stopPropagation();

-                                            // 下载逻辑

-                                        }}

+                                        onClick={(e) => handleDownloadClick(torrent, e)}

                                     >

                                         立即下载

                                     </button>

+                                    {/* 添加删除按钮 - 只有管理员或发布者可见 */}

+                                    {(userInfo?.isAdmin || userInfo?.name === torrent.username) && (

+                                        <button

+                                        className="delete-btn"

+                                        onClick={(e) => handleDeleteTorrent(torrent.id, e)}

+                                        >

+                                        删除

+                                        </button>

+                                    )}

                                 </div>

                             ))}

                         </div>

@@ -889,11 +1143,11 @@
             case 'request':

                 return (

                     <div className="content-area" data-testid="request-section">

-                        {/* 求种区搜索框 */}

+                        {/* 求助区搜索框 */}

                         <div className="section-search-container">

                             <input

                                 type="text"

-                                placeholder="搜索求种..."

+                                placeholder="搜索求助..."

                                 value={requestSearch}

                                 onChange={(e) => setRequestSearch(e.target.value)}

                                 className="section-search-input"

@@ -905,51 +1159,160 @@
                             >

                                 搜索

                             </button>

+                            <button 

+                                className="reset-button"

+                                onClick={handleResetRequestSearch}

+                                style={{marginLeft: '10px'}}

+                            >

+                                重置

+                            </button>

                         </div>

+

+                         {/* 新增发帖按钮 */}

+                        <div className="post-header">

+                            <button

+                                className="create-post-btn"

+                                onClick={() => setShowPostModal(true)}

+                            >

+                                + 发帖求助

+                            </button>

+                        </div>

+

+                        {/* 加载状态和错误提示 */}

+                        {requestLoading && <div className="loading">加载中...</div>}

+                        {requestError && <div className="error">{helpError}</div>}

                         {/* 求种区帖子列表 */}

                         <div className="request-list">

-                            {[

-                                {

-                                    id: 1,

-                                    title: '求《药屋少女的呢喃》第二季全集',

-                                    content: '求1080P带中文字幕版本,最好是内嵌字幕不是外挂的',

-                                    author: '动漫爱好者',

-                                    authorAvatar: 'https://via.placeholder.com/40',

-                                    date: '2023-10-15',

-                                    likeCount: 24,

-                                    commentCount: 8

-                                },

-                                {

-                                    id: 2,

-                                    title: '求《奥本海默》IMAX版',

-                                    content: '最好是原盘或者高码率的版本,谢谢各位大佬',

-                                    author: '电影收藏家',

-                                    authorAvatar: 'https://via.placeholder.com/40',

-                                    date: '2023-10-14',

-                                    likeCount: 15,

-                                    commentCount: 5

-                                }

-                            ].map(post => (

+                            {requestPosts.map(post => (

                                 <div

                                     key={post.id}

-                                    className="request-post"

+                                    className={`request-post ${post.isSolved ? 'solved' : ''}`}

                                     onClick={() => navigate(`/request/${post.id}`)}

                                 >

                                     <div className="post-header">

-                                        <img src={post.authorAvatar} alt={post.author} className="post-avatar"/>

-                                        <div className="post-author">{post.author}</div>

-                                        <div className="post-date">{post.date}</div>

+                                        <img

+                                            src={post.authorAvatar || 'https://via.placeholder.com/40'}

+                                            alt={post.authorId}

+                                            className="post-avatar"

+                                        />

+                                        <div className="post-author">{post.authorId}</div>

+                                        <div className="post-date">

+                                            {new Date(post.createTime).toLocaleDateString()}

+                                        </div>

+                                        {post.isSolved && <span className="solved-badge">已解决</span>}

                                     </div>

                                     <h3 className="post-title">{post.title}</h3>

                                     <p className="post-content">{post.content}</p>

                                     <div className="post-stats">

-                                        <span className="post-likes">👍 {post.likeCount}</span>

-                                        <span className="post-comments">💬 {post.commentCount}</span>

+                                        <span className="post-likes">👍 {post.likeCount || 0}</span>

+                                        <span className="post-comments">💬 {post.replyCount || 0}</span>

                                     </div>

                                 </div>

                             ))}

                         </div>

+                        {/* 在帖子列表后添加分页控件 */}

+                        <div className="pagination">

+                            <button

+                                onClick={() => fetchRequestPosts(currentPage - 1)}

+                                disabled={currentPage === 1}

+                            >

+                                上一页

+                            </button>

+

+                            {Array.from({length: totalPages}, (_, i) => i + 1).map(page => (

+                                <button

+                                    key={page}

+                                    onClick={() => fetchRequestPosts(page)}

+                                    className={currentPage === page ? 'active' : ''}

+                                >

+                                    {page}

+                                </button>

+                            ))}

+

+                            <button

+                                onClick={() => fetchRequestPosts(currentPage + 1)}

+                                disabled={currentPage === totalPages}

+                            >

+                                下一页

+                            </button>

+                        </div>

+                        {/* 新增发帖弹窗 */}

+                        {showPostModal && (

+                            <div className="post-modal-overlay">

+                                <div className="post-modal">

+                                    <h3>发布求种帖</h3>

+                                    <button

+                                        className="modal-close-btn"

+                                        onClick={() => setShowPostModal(false)}

+                                    >

+                                        ×

+                                    </button>

+

+                                    <form onSubmit={handleRequestPostSubmit}>

+                                        <div className="form-group">

+                                            <label>帖子标题</label>

+                                            <input

+                                                type="text"

+                                                value={postTitle}

+                                                onChange={(e) => setPostTitle(e.target.value)}

+                                                placeholder="请输入标题"

+                                                required

+                                            />

+                                        </div>

+

+                                        <div className="form-group">

+                                            <label>帖子内容</label>

+                                            <textarea

+                                                value={postContent}

+                                                onChange={(e) => setPostContent(e.target.value)}

+                                                placeholder="详细描述你的问题"

+                                                required

+                                            />

+                                        </div>

+

+                                        <div className="form-group">

+                                            <label>上传图片</label>

+                                            <div className="upload-image-btn">

+                                                <input

+                                                    type="file"

+                                                    id="image-upload"

+                                                    accept="image/*"

+                                                    onChange={handleImageUpload}

+                                                    style={{display: 'none'}}

+                                                />

+                                                <label htmlFor="image-upload">

+                                                    {selectedImage ? '已选择图片' : '选择图片'}

+                                                </label>

+                                                {selectedImage && (

+                                                    <span className="image-name">{selectedImage.name}</span>

+                                                )}

+                                            </div>

+                                        </div>

+

+                                        <div className="form-actions">

+                                            <button

+                                                type="button"

+                                                className="cancel-btn"

+                                                onClick={() => setShowPostModal(false)}

+                                            >

+                                                取消

+                                            </button>

+                                            <button

+                                                type="submit"

+                                                className="submit-btn"

+                                            >

+                                                确认发帖

+                                            </button>

+                                        </div>

+                                    </form>

+                                </div>

+                            </div>

+                        )}

                     </div>

+                    

+                

+

+                    

                 );

             // 在Dashboard.jsx的renderContent函数中修改case 'help'部分

             case 'help':

@@ -1063,7 +1426,7 @@
                                         ×

                                     </button>

 

-                                    <form onSubmit={handlePostSubmit}>

+                                    <form onSubmit={handleHelpPostSubmit}>

                                         <div className="form-group">

                                             <label>帖子标题</label>

                                             <input

@@ -1128,6 +1491,7 @@
             default:

                 return <div className="content-area" data-testid="default-section">公告区内容</div>;

         }

+        

     };

 

     if (loading) return <div className="loading">加载中...</div>;

@@ -1198,6 +1562,62 @@
 

             {/* 内容区 */}

             {renderContent()}

+            {/* 下载模态框 - 添加在这里 */}

+            {showDownloadModal && selectedTorrent && (

+                <div className="modal-overlay">

+                    <div className="download-modal">

+                        <h3>下载 {selectedTorrent.torrentName}</h3>

+                        <button 

+                            className="close-btn"

+                            onClick={() => !isDownloading && setShowDownloadModal(false)}

+                            disabled={isDownloading}

+                        >

+                            ×

+                        </button>

+                        

+                        <div className="form-group">

+                            <label>下载路径:</label>

+                            <input

+                                type="text"

+                                value={downloadPath}

+                                onChange={(e) => {

+                                    // 实时格式化显示

+                                    let path = e.target.value

+                                        .replace(/\t/g, '')

+                                        .replace(/\\/g, '/')

+                                        .replace(/\s+/g, ' ');

+                                    setDownloadPath(path);

+                                }}

+                                disabled={isDownloading}

+                                placeholder="例如: D:/downloads/"

+                            />

+                        </div>

+                        

+                        {isDownloading && (

+                            <div className="progress-container">

+                                <div className="progress-bar" style={{ width: `${downloadProgress}%` }}>

+                                    {downloadProgress}%

+                                </div>

+                            </div>

+                        )}

+                        

+                        <div className="modal-actions">

+                            <button

+                                onClick={() => !isDownloading && setShowDownloadModal(false)}

+                                disabled={isDownloading}

+                            >

+                                取消

+                            </button>

+                            <button

+                                onClick={handleDownload}

+                                disabled={isDownloading || !downloadPath}

+                            >

+                                {isDownloading ? '下载中...' : '开始下载'}

+                            </button>

+                        </div>

+                    </div>

+                </div>

+            )}

         </div>

     );

 };

diff --git a/src/components/HelpDetail.css b/src/components/HelpDetail.css
index 94d791b..34e1363 100644
--- a/src/components/HelpDetail.css
+++ b/src/components/HelpDetail.css
@@ -554,4 +554,33 @@
   border-radius: 4px;

   padding: 8px 16px;

   cursor: pointer;

-}
\ No newline at end of file
+}

+

+.post-image-container {

+  width: 100%;

+  max-width: 500px; /* 最大宽度 */

+  margin: 10px 0;

+}

+

+.post-image {

+  width: 100%;

+  height: auto;

+  max-height: 400px; /* 最大高度 */

+  object-fit: contain; /* 保持比例完整显示图片 */

+  border-radius: 4px;

+}

+

+.comment-image-container {

+  width: 100%;

+  max-width: 500px; /* 最大宽度 */

+  margin: 10px 0;

+}

+

+.comment-image {

+  width: 100%;

+  height: auto;

+  max-height: 400px; /* 最大高度 */

+  object-fit: contain; /* 保持比例完整显示图片 */

+  border-radius: 4px;

+}

+

diff --git a/src/components/HelpDetail.jsx b/src/components/HelpDetail.jsx
index 7e7b7c8..1e35aec 100644
--- a/src/components/HelpDetail.jsx
+++ b/src/components/HelpDetail.jsx
@@ -1,16 +1,16 @@
 import React, { useState, useEffect, useRef } from 'react';

 import { useParams, useNavigate, useLocation } from 'react-router-dom';

 import { 

-  getPostDetail,

-  addPostComment,

-  likePost,

-  deletePost

+  getHelpPostDetail,

+  addHelpPostComment,

+  likeHelpPost,

+  deleteHelpPost

 } from '../api/helpPost';

 import {

-  likePostComment,

+  likeHelpPostComment,

   getCommentReplies,

-  addCommentReply,

-  deleteComment

+  addHelpCommentReply,

+  deleteHelpComment

 } from '../api/helpComment';

 import './HelpDetail.css';

 

@@ -136,7 +136,7 @@
    const fetchPostDetail = async () => {

       try {

         setLoading(true);

-        const response = await getPostDetail(id);

+        const response = await getHelpPostDetail(id);

         console.log('API Response:', JSON.parse(JSON.stringify(response.data.data.comments))); // 深度拷贝避免Proxy影响

         setPost(response.data.data.post);

         setComments(response.data.data.comments);

@@ -154,7 +154,7 @@
     // 点赞帖子

     const handleLikePost = async () => {

       try {

-        await likePost(id);

+        await likeHelpPost(id);

         setPost(prev => ({

           ...prev,

           likeCount: prev.likeCount + 1

@@ -169,7 +169,7 @@
       if (window.confirm('确定要删除这个帖子吗?所有评论也将被删除!')) {

         try {

           const username = localStorage.getItem('username');

-          await deletePost(postId, username);

+          await deleteHelpPost(postId, username);

           navigate('/dashboard/help'); // 删除成功后返回求助区

         } catch (err) {

           setError('删除失败: ' + (err.response?.data?.message || err.message));

@@ -183,16 +183,21 @@
       

       try {

         const username = localStorage.getItem('username');

-        const response = await addPostComment(id, {

-          content: newComment,

-          authorId: username

-        });

+        const formData = new FormData();

+        formData.append('content', newComment);

+        formData.append('authorId', username);

+        if (commentImage) {

+          formData.append('image', commentImage);

+        }

+

+        const response = await addHelpPostComment(id, formData);

     

         // 修改这里的响应处理逻辑

         if (response.data && response.data.code === 200) {

           await fetchPostDetail();

           

           setNewComment('');

+          setCommentImage(null); // 清空评论图片

         } else {

           setError(response.data.message || '评论失败');

         }

@@ -204,7 +209,7 @@
     

     const handleLikeComment = async (commentId) => {

       try {

-        await likePostComment(commentId);

+        await likeHelpPostComment(commentId);

         

         // 递归更新评论点赞数

         const updateComments = (comments) => {

@@ -236,7 +241,7 @@
       if (window.confirm('确定要删除这条评论吗?')) {

         try {

           const username = localStorage.getItem('username');

-          await deleteComment(commentId, username);

+          await deleteHelpComment(commentId, username);

           await fetchPostDetail(); // 刷新评论列表

         } catch (err) {

           setError('删除失败: ' + (err.response?.data?.message || err.message));

@@ -264,7 +269,7 @@
     

       try {

         const username = localStorage.getItem('username');

-        const response = await addCommentReply(replyModal.replyingTo, {

+        const response = await addHelpCommentReply(replyModal.replyingTo, {

           authorId: username,

           content: replyContent,

           image: replyImage

diff --git a/src/components/HelpDetail.test.jsx b/src/components/HelpDetail.test.jsx
index 6364be2..2e917e7 100644
--- a/src/components/HelpDetail.test.jsx
+++ b/src/components/HelpDetail.test.jsx
@@ -53,7 +53,7 @@
     mockNavigate.mockClear();

     

     // 设置模拟的 API 响应

-    helpPostApi.getPostDetail.mockResolvedValue({

+    helpPostApi.getHelpPostDetail.mockResolvedValue({

       data: {

         code: 200,

         data: {

@@ -111,29 +111,33 @@
     fireEvent.click(screen.getByText(/点赞 \(5\)/));

 

     await waitFor(() => {

-      expect(helpPostApi.likePost).toHaveBeenCalledWith('1');

+      expect(helpPostApi.likeHelpPost).toHaveBeenCalledWith('1');

     });

   });

 

   it('应该能够提交评论', async () => {

-    renderComponent();

+  renderComponent();

 

-    await waitFor(() => {

-      expect(screen.getByText('测试求助帖')).toBeInTheDocument();

-    });

-

-    const commentInput = screen.getByPlaceholderText('写下你的评论...');

-    fireEvent.change(commentInput, { target: { value: '新评论' } });

-    fireEvent.click(screen.getByText('发表评论'));

-

-    await waitFor(() => {

-      expect(helpPostApi.addPostComment).toHaveBeenCalledWith('1', {

-        content: '新评论',

-        authorId: 'testuser'

-      });

-    });

+  await waitFor(() => {

+    expect(screen.getByText('测试求助帖')).toBeInTheDocument();

   });

 

+  const commentInput = screen.getByPlaceholderText('写下你的评论...');

+  fireEvent.change(commentInput, { target: { value: '新评论' } });

+  fireEvent.click(screen.getByText('发表评论'));

+

+  await waitFor(() => {

+    expect(helpPostApi.addHelpPostComment).toHaveBeenCalled();

+    

+    const calledArgs = helpPostApi.addHelpPostComment.mock.calls[0];

+    expect(calledArgs[0]).toBe('1'); // postId

+    const formData = calledArgs[1];

+    

+    expect(formData instanceof FormData).toBe(true);

+    expect(formData.get('authorId')).toBe('testuser');

+    expect(formData.get('content')).toBe('新评论');

+  });

+});

   it('应该能够点赞评论', async () => {

     renderComponent();

 

@@ -144,7 +148,7 @@
     fireEvent.click(screen.getAllByText(/👍 \(2\)/)[0]);

 

     await waitFor(() => {

-      expect(helpCommentApi.likePostComment).toHaveBeenCalledWith('c1');

+      expect(helpCommentApi.likeHelpPostComment).toHaveBeenCalledWith('c1');

     });

   });

 

diff --git a/src/components/RequestDetail.jsx b/src/components/RequestDetail.jsx
index 022b0cb..c138419 100644
--- a/src/components/RequestDetail.jsx
+++ b/src/components/RequestDetail.jsx
@@ -1,111 +1,355 @@
-import React, { useState } from 'react';

-import { useParams, useNavigate,useLocation } from 'react-router-dom';

+import React, { useState, useEffect, useRef } from 'react';

+import { useParams, useNavigate, useLocation } from 'react-router-dom';

+import { 

+  getRequestPostDetail,

+  addRequestPostComment,

+  likeRequestPost,

+  deleteRequestPost

+} from '../api/requestPost';

+import {

+  likeRequestPostComment,

+  getCommentReplies,

+  addRequestCommentReply,

+  deleteRequestComment

+} from '../api/requestComment';

 import './RequestDetail.css';

 

 const RequestDetail = () => {

   const { id } = useParams();

   const navigate = useNavigate();

   const location = useLocation();

-  

-  // 模拟数据

-  const [post, setPost] = useState({

-    id: 1,

-    title: '求《药屋少女的呢喃》第二季全集',

-    content: '求1080P带中文字幕版本,最好是内嵌字幕不是外挂的。\n\n希望有热心大佬能分享,可以给积分奖励!',

-    author: '动漫爱好者',

-    authorAvatar: 'https://via.placeholder.com/40',

-    date: '2023-10-15',

-    likeCount: 24,

-    isLiked: false,

-    isFavorited: false

-  });

-

-  const [comments, setComments] = useState([

-    {

-      id: 1,

-      type: 'text',

-      author: '资源达人',

-      authorAvatar: 'https://via.placeholder.com/40',

-      content: '我有第1-5集,需要的话可以私聊',

-      date: '2023-10-15 14:30',

-      likeCount: 5

-    },

-    {

-      id: 2,

-      type: 'torrent',

-      title: '药屋少女的呢喃第二季第8集',

-      size: '1.2GB',

-      author: '种子分享者',

-      authorAvatar: 'https://via.placeholder.com/40',

-      date: '2023-10-16 09:15',

-      likeCount: 8

-    }

-  ]);

-

+  const fileInputRef = useRef(null);

+  const [post, setPost] = useState(null);

+  const [comments, setComments] = useState([]);

+  const [loading, setLoading] = useState(true);

+  const [error, setError] = useState(null);

   const [newComment, setNewComment] = useState('');

+  const [replyContent, setReplyContent] = useState('');

+  const [replyImage, setReplyImage] = useState([]);

+  const [commentImage, setCommentImage] = useState([]);

+  const [expandedReplies, setExpandedReplies] = useState({}); // 记录哪些评论的回复是展开的

+  const [loadingReplies, setLoadingReplies] = useState({});

+  const [setReplyingTo] = useState(null);

+  

 

-  const handleLikePost = () => {

-    setPost(prev => ({

-      ...prev,

-      likeCount: prev.isLiked ? prev.likeCount - 1 : prev.likeCount + 1,

-      isLiked: !prev.isLiked

-    }));

+  const [activeReplyId, setActiveReplyId] = useState(null);

+    const [replyModal, setReplyModal] = useState({

+      visible: false,

+      replyingTo: null,

+      replyingToUsername: '',

+      isReply: false

+    });

+  

+    // 确保openReplyModal接收username参数

+  const openReplyModal = (commentId, username) => {

+    setReplyModal({

+      visible: true,

+      replyingTo: commentId,

+      replyingToUsername: username,  // 确保这里接收username

+      isReply: false

+    });

+  };

+  

+    // 关闭回复弹窗

+    const closeReplyModal = () => {

+      setReplyModal({

+        visible: false,

+        replyingTo: null,

+        replyingToUsername: '',

+        isReply: false

+      });

+      setReplyContent('');

+    };

+

+    const Comment = ({ comment, onLike, onReply, onDelete, isReply = false }) => {

+      return (

+        <div className={`comment-container ${isReply ? "is-reply" : ""}`}>

+          <div className="comment-item">

+            <div className="comment-avatar">

+              {(comment.authorId || "?").charAt(0)} {/* 修复点 */}

+            </div>

+            <div className="comment-content">

+              <div className="comment-header">

+                <span className="comment-user">{comment.authorId || "匿名用户"}</span>

+                {comment.replyTo && (

+                  <span className="reply-to">回复 @{comment.replyTo}</span>

+                )}

+                <span className="comment-time">

+                  {new Date(comment.createTime).toLocaleString()}

+                </span>

+              </div>

+              <p className="comment-text">{comment.content}</p>

+              {/* 添加评论图片展示 */}

+              {comment.imageUrl && (

+                <div className="comment-image-container">

+                  <img 

+                    src={`http://localhost:8088${comment.imageUrl}`} 

+                    alt="评论图片" 

+                    className="comment-image"

+                    onClick={() => window.open(comment.imageUrl, '_blank')}

+                  />

+                </div>

+              )}

+              <div className="comment-actions">

+                <button onClick={() => onLike(comment.id)}>

+                  👍 ({comment.likeCount || 0})

+                </button>

+                <button onClick={() => onReply(comment.id, comment.authorId)}>

+                  回复

+                </button>

+                {comment.authorId === localStorage.getItem('username') && (

+                  <button 

+                    className="delete-comment-btn"

+                    onClick={() => onDelete(comment.id)}

+                  >

+                    删除

+                  </button>

+                )}

+              </div>

+            </div>

+          </div>

+        </div>

+      );

+    };

+

+      // 递归渲染评论组件

+  const renderComment = (comment, depth = 0) => {

+    return (

+      <div key={comment.id} style={{ marginLeft: `${depth * 30}px` }}>

+        <Comment

+          comment={comment}

+          onLike={handleLikeComment}

+          onReply={openReplyModal}

+          isReply={depth > 0}

+          onDelete={handleDeleteComment}

+        />

+        

+        {/* 递归渲染所有回复 */}

+        {comment.replies && comment.replies.map(reply => 

+          renderComment(reply, depth + 1)

+        )}

+      </div>

+    );

   };

 

-  const handleFavoritePost = () => {

-    setPost(prev => ({

-      ...prev,

-      isFavorited: !prev.isFavorited

-    }));

-  };

 

-  const handleCommentSubmit = (e) => {

-    e.preventDefault();

-    if (!newComment.trim()) return;

+   const fetchPostDetail = async () => {

+      try {

+        setLoading(true);

+        const response = await getRequestPostDetail(id);

+        console.log('API Response:', JSON.parse(JSON.stringify(response.data.data.comments))); // 深度拷贝避免Proxy影响

+        setPost(response.data.data.post);

+        setComments(response.data.data.comments);

+      } catch (err) {

+        setError(err.response?.data?.message || '获取帖子详情失败');

+      } finally {

+        setLoading(false);

+      }

+    };

+  

+    useEffect(() => {

+      fetchPostDetail();

+    }, [id]);

+  

+    // 点赞帖子

+    const handleLikePost = async () => {

+      try {

+        await likeRequestPost(id);

+        setPost(prev => ({

+          ...prev,

+          likeCount: prev.likeCount + 1

+        }));

+      } catch (err) {

+        setError('点赞失败: ' + (err.response?.data?.message || err.message));

+      }

+    };

+

+     // 添加删除处理函数

+     const handleDeletePost = async (postId) => {

+      if (window.confirm('确定要删除这个帖子吗?所有评论也将被删除!')) {

+        try {

+          const username = localStorage.getItem('username');

+          await deleteRequestPost(postId, username);

+          navigate('/dashboard/request'); // 删除成功后返回求助区

+        } catch (err) {

+          setError('删除失败: ' + (err.response?.data?.message || err.message));

+        }

+      }

+    };

+  

+    const handleCommentSubmit = async (e) => {

+      e.preventDefault();

+      if (!newComment.trim()) return;

+      

+      try {

+        const username = localStorage.getItem('username');

+        const formData = new FormData();

+        formData.append('content', newComment);

+        formData.append('authorId', username);

+        if (commentImage) {

+          formData.append('image', commentImage);

+        }

+

+        const response = await addRequestPostComment(id, formData);

     

-    const newCommentObj = {

-      id: comments.length + 1,

-      type: 'text',

-      author: '当前用户',

-      authorAvatar: 'https://via.placeholder.com/40',

-      content: newComment,

-      date: new Date().toLocaleString(),

-      likeCount: 0

+        // 修改这里的响应处理逻辑

+        if (response.data && response.data.code === 200) {

+          await fetchPostDetail();

+          

+          setNewComment('');

+          setCommentImage(null); // 清空评论图片

+        } else {

+          setError(response.data.message || '评论失败');

+        }

+      } catch (err) {

+        setError('评论失败: ' + (err.response?.data?.message || err.message));

+      }

     };

     

-    setComments([...comments, newCommentObj]);

-    setNewComment('');

-  };

-

-  const handleDownloadTorrent = (commentId) => {

-

-

     

-    console.log('下载种子', commentId);

-    // 实际下载逻辑

+    const handleLikeComment = async (commentId) => {

+      try {

+        await likeRequestPostComment(commentId);

+        

+        // 递归更新评论点赞数

+        const updateComments = (comments) => {

+          return comments.map(comment => {

+            // 当前评论匹配

+            if (comment.id === commentId) {

+              return { ...comment, likeCount: comment.likeCount + 1 };

+            }

+            

+            // 递归处理回复

+            if (comment.replies && comment.replies.length > 0) {

+              return {

+                ...comment,

+                replies: updateComments(comment.replies)

+              };

+            }

+            

+            return comment;

+          });

+        };

+    

+        setComments(prev => updateComments(prev));

+      } catch (err) {

+        setError('点赞失败: ' + (err.response?.data?.message || err.message));

+      }

+    };

+

+    const handleDeleteComment = async (commentId) => {

+      if (window.confirm('确定要删除这条评论吗?')) {

+        try {

+          const username = localStorage.getItem('username');

+          await deleteRequestComment(commentId, username);

+          await fetchPostDetail(); // 刷新评论列表

+        } catch (err) {

+          setError('删除失败: ' + (err.response?.data?.message || err.message));

+        }

+      }

+    };

+

+

+    // 修改startReply函数

+    const startReply = (commentId) => {

+      if (activeReplyId === commentId) {

+        // 如果点击的是已经激活的回复按钮,则关闭

+        setActiveReplyId(null);

+        setReplyingTo(null);

+      } else {

+        // 否则打开新的回复框

+        setActiveReplyId(commentId);

+        setReplyingTo(commentId);

+      }

+    };

+  

+    const handleReplySubmit = async (e) => {

+      e.preventDefault();

+      if (!replyContent.trim()) return;

+    

+      try {

+        const username = localStorage.getItem('username');

+        const response = await addRequestCommentReply(replyModal.replyingTo, {

+          authorId: username,

+          content: replyContent,

+          image: replyImage

+        });

+    

+        console.log('回复响应:', response.data); // 调试

+        

+        if (response.data && response.data.code === 200) {

+          await fetchPostDetail();

+          setReplyContent('');

+          closeReplyModal();

+        }

+      } catch (err) {

+        console.error('回复错误:', err);

+        setError('回复失败: ' + (err.response?.data?.message || err.message));

+      }

+    };

+  

+  

+    // 返回按钮

+    const handleBack = () => {

+      const fromTab = location.state?.fromTab || 'share';

+      navigate(`/dashboard/request`);

+    };

+  

+    

+

+  const handleMarkSolved = () => {

+    // TODO: 实现标记为已解决的功能

+    setPost(prev => ({

+      ...prev,

+      isSolved: !prev.isSolved

+    }));

   };

 

-  const handleBack = () => {

-    const fromTab = location.state?.fromTab; // 从跳转时传递的 state 中获取

-    if (fromTab) {

-      navigate(`/dashboard/${fromTab}`); // 明确返回对应标签页

-    } else {

-      navigate(-1); // 保底策略

-    }

-  }

+  // const handleImageUpload = (e) => {

+  //   const files = Array.from(e.target.files);

+  //   const newImages = files.map(file => URL.createObjectURL(file));

+  //   setImages(prev => [...prev, ...newImages]);

+  // };

+

+  // const handleRemoveImage = (index) => {

+  //   setImages(prev => prev.filter((_, i) => i !== index));

+  // };

+

+  

+

+  if (loading) return <div className="loading">加载中...</div>;

+  if (error) return <div className="error">{error}</div>;

+  if (!post) return <div className="error">帖子不存在</div>;

 

   return (

     <div className="request-detail-container">

       <button className="back-button" onClick={handleBack}>

-        &larr; 返回求种区

+        &larr; 返回求助区

       </button>

       

-      <div className="request-post">

+      <div className={`request-post ${post.isSolved ? 'solved' : ''}`}>

         <div className="post-header">

-          <img src={post.authorAvatar} alt={post.author} className="post-avatar" />

+          <img 

+            src={post.authorAvatar || 'https://via.placeholder.com/40'} 

+            alt={post.authorId} 

+            className="post-avatar" 

+          />

           <div className="post-meta">

-            <div className="post-author">{post.author}</div>

-            <div className="post-date">{post.date}</div>

+            <div className="post-author">{post.authorId}</div>

+            <div className="post-date">

+              {new Date(post.createTime).toLocaleString()}

+              </div>

+          </div>

+          {post.isSolved && <span ClassName="solved-badge">已解决</span>}

+          <div classname="delete-post">

+            {post.authorId === localStorage.getItem('username') && (

+              <button 

+                className="delete-button"

+                onClick={() => handleDeletePost(post.id)}

+              >

+                删除帖子

+              </button>

+            )}

           </div>

         </div>

         

@@ -115,6 +359,21 @@
           {post.content.split('\n').map((para, i) => (

             <p key={i}>{para}</p>

           ))}

+          {/* 添加帖子图片展示 */}

+          {post.imageUrl && (

+            <div className="post-image-container">

+              <img 

+                src={`http://localhost:8088${post.imageUrl}`} 

+                alt="帖子图片" 

+                className="post-image"

+                // onError={(e) => {

+                //   e.target.onerror = null; 

+                //   e.target.src = 'https://via.placeholder.com/400x300?text=图片加载失败';

+                //   console.error('图片加载失败:', post.imageUrl);

+                // }}

+              />

+            </div>

+          )}

         </div>

         

         <div className="post-actions">

@@ -125,74 +384,86 @@
             👍 点赞 ({post.likeCount})

           </button>

           <button 

-            className={`favorite-button ${post.isFavorited ? 'favorited' : ''}`}

-            onClick={handleFavoritePost}

+            className={`solve-button ${post.isSolved ? 'solved' : ''}`}

+            onClick={handleMarkSolved}

           >

-            {post.isFavorited ? '★ 已收藏' : '☆ 收藏'}

+            {post.isSolved ? '✓ 已解决' : '标记为已解决'}

           </button>

         </div>

       </div>

       

-      <div className="comments-section">

-        <h2>回应 ({comments.length})</h2>

+       <div className="comments-section">

+        <h2>评论 ({post.replyCount})</h2>

         

         <form onSubmit={handleCommentSubmit} className="comment-form">

           <textarea

             value={newComment}

             onChange={(e) => setNewComment(e.target.value)}

-            placeholder="写下你的回应..."

+            placeholder="写下你的评论..."

             rows="3"

             required

           />

-          <div className="form-actions">

-            <button type="submit" className="submit-comment">发表文字回应</button>

-            <button 

-              type="button" 

-              className="submit-torrent"

-              onClick={() => console.log('打开种子上传对话框')}

-            >

-              上传种子回应

-            </button>

+          <button type="submit">发表评论</button>

+

+          {/* 图片上传部分 */}

+          <div className="form-group">

+            <div className="upload-image-btn">

+              <input 

+                type="file" 

+                accept="image/*" 

+                onChange={(e) => setCommentImage(e.target.files[0])} 

+                data-testid="comment-image-input"  

+              />

+            </div>

           </div>

         </form>

         

+        

         <div className="comment-list">

-          {comments.map(comment => (

-            <div key={comment.id} className={`comment-item ${comment.type}`}>

-              <img 

-                src={comment.authorAvatar} 

-                alt={comment.author} 

-                className="comment-avatar"

-              />

-              

-              <div className="comment-content">

-                <div className="comment-header">

-                  <span className="comment-author">{comment.author}</span>

-                  <span className="comment-date">{comment.date}</span>

-                </div>

-                

-                {comment.type === 'text' ? (

-                  <p className="comment-text">{comment.content}</p>

-                ) : (

-                  <div className="torrent-comment">

-                    <span className="torrent-title">{comment.title}</span>

-                    <span className="torrent-size">{comment.size}</span>

-                    <button 

-                      className="download-torrent"

-                      onClick={() => handleDownloadTorrent(comment.id)}

-                    >

-                      立即下载

-                    </button>

-                  </div>

-                )}

-                

-                <button className="comment-like">

-                  👍 ({comment.likeCount})

-                </button>

-              </div>

-            </div>

-          ))}

+          {comments.map(comment => renderComment(comment))}

         </div>

+

+        {replyModal.visible && (

+          <div className="reply-modal-overlay">

+            <div className="reply-modal">

+              <div className="modal-header">

+                <h3>回复 @{replyModal.replyingToUsername}</h3>

+                <button onClick={closeReplyModal} className="close-modal">&times;</button>

+              </div>

+              <form onSubmit={handleReplySubmit}>

+                <textarea

+                  value={replyContent}

+                  onChange={(e) => setReplyContent(e.target.value)}

+                  placeholder={`回复 @${replyModal.replyingToUsername}...`}

+                  rows="5"

+                  autoFocus

+                  required

+                />

+

+                {/* 图片上传部分 */}

+                <div className="form-group">

+                  <div className="upload-image-btn">

+                    <input 

+                      type="file" 

+                      accept="image/*" 

+                      onChange={(e) => setReplyImage(e.target.files[0])} 

+                    />

+                  </div>

+                </div>

+

+                <div className="modal-actions">

+                  <button type="button" onClick={closeReplyModal} className="cancel-btn">

+                    取消

+                  </button>

+                  <button type="submit" className="submit-btn">

+                    发送回复

+                  </button>

+                </div>

+              </form>

+            </div>

+          </div>

+        )}

+

       </div>

     </div>

   );

diff --git a/src/components/RequestDetail.test.jsx b/src/components/RequestDetail.test.jsx
new file mode 100644
index 0000000..8ec34c2
--- /dev/null
+++ b/src/components/RequestDetail.test.jsx
@@ -0,0 +1,209 @@
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import RequestDetail from './RequestDetail';
+import { isFormData } from 'axios';
+
+const mockNavigate = jest.fn();
+
+
+// 明确 mock API 模块
+jest.mock('../api/requestPost', () => ({
+  getRequestPostDetail: jest.fn(),
+  addRequestPostComment: jest.fn(),
+  likeRequestPost: jest.fn(),
+  deleteRequestPost: jest.fn()
+}));
+
+jest.mock('../api/requestComment', () => ({
+  likeRequestPostComment: jest.fn(),
+  getRequestCommentReplies: jest.fn(),
+  addRequestCommentReply: jest.fn(),
+  deleteRequestComment: jest.fn()
+}));
+
+jest.mock('react-router-dom', () => ({
+  ...jest.requireActual('react-router-dom'),
+  useNavigate: () => mockNavigate,
+  useLocation: jest.fn()
+}));
+
+// 导入 mock 后的 API
+import * as requestPostApi from '../api/requestPost';
+import * as requestCommentApi from '../api/requestComment';
+
+describe('RequestDetail 组件', () => {
+  const mockPost = {
+    id: '1',
+    title: '测试求种帖',
+    content: '这是一个测试求种内容',
+    authorId: 'user1',
+    createTime: '2023-01-01T00:00:00Z',
+    likeCount: 5,
+    replyCount: 3,
+    isSolved: false,
+  };
+
+  const mockComments = [
+    {
+      id: 'c1',
+      content: '测试评论1',
+      authorId: 'user2',
+      createTime: '2023-01-01T01:00:00Z',
+      likeCount: 2,
+      replies: [
+        {
+          id: 'c1r1',
+          content: '测试回复1',
+          authorId: 'user3',
+          createTime: '2023-01-01T02:00:00Z',
+          likeCount: 1,
+        }
+      ]
+    }
+  ];
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+    
+    requestPostApi.getRequestPostDetail.mockResolvedValue({
+      data: {
+        code: 200,
+        data: {
+          post: mockPost,
+          comments: mockComments,
+        }
+      }
+    });
+
+    // 模拟 useLocation
+    require('react-router-dom').useLocation.mockReturnValue({
+      state: { fromTab: 'request' }
+    });
+
+    // 模拟 localStorage
+    Storage.prototype.getItem = jest.fn((key) => {
+      if (key === 'username') return 'testuser';
+      return null;
+    });
+  });
+
+  const renderComponent = () => {
+    return render(
+      <MemoryRouter initialEntries={['/request/1']}>
+        <Routes>
+          <Route path="/request/:id" element={<RequestDetail />} />
+          {/* 添加一个模拟的求助列表路由用于导航测试 */}
+          <Route path="/dashboard/request" element={<div>求助列表</div>} />
+        </Routes>
+      </MemoryRouter>
+    );
+  };
+
+  it('应该正确加载和显示求助帖详情', async () => {
+    renderComponent();
+
+    // 检查加载状态
+    expect(screen.getByText('加载中...')).toBeInTheDocument();
+
+    // 增加超时时间
+    await waitFor(() => {
+        expect(screen.getByText(mockPost.title)).toBeInTheDocument();
+    }, { timeout: 3000 });
+
+    // 等待数据加载完成
+     await waitFor(() => {
+      expect(screen.getByText(mockPost.title)).toBeInTheDocument();
+      expect(screen.getByText(mockPost.content)).toBeInTheDocument();
+      expect(screen.getByText(mockPost.authorId)).toBeInTheDocument();
+    });
+  });
+  
+  it('应该能够点赞帖子', async () => {
+    renderComponent();
+
+    await waitFor(() => {
+      expect(screen.getByText(mockPost.title)).toBeInTheDocument();
+    });
+
+    const likeButton = screen.getByRole('button', { name: /点赞 \(\d+\)/ });
+    fireEvent.click(likeButton);
+
+    await waitFor(() => {
+      expect(requestPostApi.likeRequestPost).toHaveBeenCalledWith('1');
+    });
+  });
+
+  it('应该能够提交评论', async () => {
+    renderComponent();
+
+    await waitFor(() => {
+      expect(screen.getByText(mockPost.title)).toBeInTheDocument();
+    });
+
+    const commentInput = screen.getByPlaceholderText('写下你的评论...');
+    fireEvent.change(commentInput, { target: { value: '新评论' } });
+    
+    const submitButton = screen.getByRole('button', { name: '发表评论' });
+    fireEvent.click(submitButton);
+
+    await waitFor(() => {
+      expect(requestPostApi.addRequestPostComment).toHaveBeenCalled();
+    });
+  });
+
+  it('应该能够点赞评论', async () => {
+    renderComponent();
+
+    await waitFor(() => {
+      expect(screen.getByText(mockComments[0].content)).toBeInTheDocument();
+    });
+
+    const likeButtons = screen.getAllByRole('button', { name: /👍 \(\d+\)/ });
+    fireEvent.click(likeButtons[0]);
+
+    await waitFor(() => {
+      expect(requestCommentApi.likeRequestPostComment).toHaveBeenCalledWith('c1');
+    });
+  });
+
+  it('应该能够打开和关闭回复模态框', async () => {
+    renderComponent();
+
+    await waitFor(() => {
+      expect(screen.getByText(mockComments[0].content)).toBeInTheDocument();
+    });
+
+    // 点击回复按钮
+    const replyButtons = screen.getAllByRole('button', { name: '回复' });
+    fireEvent.click(replyButtons[0]);
+
+    // 检查模态框是否打开
+    await waitFor(() => {
+      expect(screen.getByText(`回复 @${mockComments[0].authorId}`)).toBeInTheDocument();
+    });
+
+    // 点击关闭按钮
+    const closeButton = screen.getByRole('button', { name: '×' });
+    fireEvent.click(closeButton);
+
+    // 检查模态框是否关闭
+    await waitFor(() => {
+      expect(screen.queryByText(`回复 @${mockComments[0].authorId}`)).not.toBeInTheDocument();
+    });
+  });
+
+  it('应该能够返回求助区', async () => {
+    renderComponent();
+  
+    await waitFor(() => {
+      expect(screen.getByText(mockPost.title)).toBeInTheDocument();
+    });
+  
+    const backButton = screen.getByRole('button', { name: /返回/i });
+    fireEvent.click(backButton);
+  
+    expect(mockNavigate).toHaveBeenCalledWith('/dashboard/request');
+
+  });
+});
\ No newline at end of file
diff --git "a/\345\255\230\345\234\250\347\232\204\351\227\256\351\242\230.txt" "b/\345\255\230\345\234\250\347\232\204\351\227\256\351\242\230.txt"
index ca235ec..335c27b 100644
--- "a/\345\255\230\345\234\250\347\232\204\351\227\256\351\242\230.txt"
+++ "b/\345\255\230\345\234\250\347\232\204\351\227\256\351\242\230.txt"
@@ -1,2 +1,13 @@
-1.添加torrent表字段,添加点赞数、评论数

-2.列表
\ No newline at end of file
+1.把上传下载接口连接好ok

+2.搜索框位置修改,功能完成(包括前端和后端)ok

+3.分享区,根据类型字幕等进行筛选ok

+4.个人中心添加一个下载进度展示的条,可能每十秒请求一次数据ok

+5.公告区前后端ok

+6.个人中心新增的魔力值兑换量之类的接口都接好ok

+7.求种区,前后端

+8.下载进度条合上之后才能测试

+9。管理员区里的发布公告,新开一个公告管理,完成

+10.轮播图实现折扣展示ok

+11.图片展示固定比例,(暂时解决,未找到其他bug,已修复主评论不能上传图片bug)

+12.看不到其他人上传的图片(需要云服务器),暂且搁置

+a
\ No newline at end of file