完成上传下载连接,公告管理与详情页面,求种区页面,轮播图折扣显示,修改部分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)}>
- ← 返回公告区
+ <button className="back-button" onClick={handleBack}>
+ ← 返回
</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}>
- ← 返回求种区
+ ← 返回求助区
</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">×</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