Merge "用户中心 商城 登陆注册前端"
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 0000000..1766c19
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1,2891 @@
+#root {
+ max-width: 100%;
+ margin: 0;
+ padding: 0;
+}
+
+/* 覆盖默认样式,适配Mantine组件 */
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.mantine-Image-root {
+ border-radius: 8px;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
+
+.mantine-Card-root {
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
diff --git a/src/App.jsx b/src/App.jsx
new file mode 100644
index 0000000..b04728a
--- /dev/null
+++ b/src/App.jsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
+import Home from './pages/Home';
+import AdminPage from './pages/AdminPage';
+import TorrentDetail from './components/torrentdetail';
+import TorrentDetailhelp from './components/torrentdetailhelp';
+import TorrentDetailcomplain from './components/torrentdetailcomplain';
+import TorrentList from './components/torrentlist';
+import UploadTorrent from './components/upload';
+import Navbar from './components/Navbar';
+import TorrentDetailmanage from './pages/managetorrentdetail';
+import RecommendAll from './components/RecommendAll';
+import UserAuth from './pages/UserAuth';
+import 'antd/dist/reset.css'; // Ant Design 默认样式
+import './App.css';
+import MainPage from './pages/MainPage';
+import Friend from './pages/Friend';
+import Community from './pages/Community';
+import UserCenter from './pages/UserCenter111';
+import UploadTorrentFull from './components/upload-full';
+import ShopPage from './pages/ShopPage';
+
+// 定义橙色主题(使用 Ant Design 的 orange-6 色值)
+const orangeTheme = {
+ token: {
+ colorPrimary: '#fa8c16', // orange-6
+ borderRadius: 4, // 可选:圆角大小
+ },
+};
+
+function App() {
+ return (
+ <div className="App">
+ {/* <div className="container mx-auto p-4"> */}
+ <Routes>
+ {/* <Route path="/" element={<Home />} /> */}
+ <Route path="/home" element={<MainPage />} />
+ <Route path="/admin" element={<AdminPage />} />
+ <Route path="/admin/:id" element={<TorrentDetailmanage />} />
+ <Route path="/process/:id" element={<TorrentDetailhelp />} />
+ <Route path="/complain-process/:id" element={<TorrentDetailcomplain />} />
+ <Route path="/torrents" element={<TorrentList />} />
+ <Route path="/upload" element={<UploadTorrent />} />
+ <Route path="/torrent/:id" element={<TorrentDetail />} />
+ <Route path="/recommend" element={<RecommendAll />} />
+ <Route path="/" element={<UserAuth />} />
+ <Route path="/Community" element={<Community />} />
+ <Route path="/friend" element={<Friend />} />
+ <Route path="/usercenter" element={<UserCenter />} />
+ <Route path="/uploadfull/:requestid" element={<UploadTorrentFull/>}/>
+ <Route path="/shop" element={<ShopPage />} />
+ </Routes>
+ {/* </div> */}
+ </div>
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/src/Modal.css b/src/Modal.css
new file mode 100644
index 0000000..c37a106
--- /dev/null
+++ b/src/Modal.css
@@ -0,0 +1,118 @@
+/* 模态框遮罩层 */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5); /* 半透明黑色背景 */
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000; /* 确保在最上层 */
+}
+
+/* 模态框内容 */
+.modal-content {
+ background-color: white;
+ border-radius: 12px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+ width: 90%;
+ max-width: 500px; /* 最大宽度 */
+ overflow: hidden;
+ animation: fadeInUp 0.3s ease-out; /* 淡入动画 */
+}
+
+/* 模态框头部 */
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 20px;
+ background-color: #e6f0ff; /* 浅蓝色背景 */
+ border-bottom: 1px solid #d0e3ff;
+}
+
+.modal-header h3 {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: #0066cc; /* 蓝色标题 */
+}
+
+.close-btn {
+ background: none;
+ border: none;
+ font-size: 24px;
+ cursor: pointer;
+ color: #666;
+}
+
+.close-btn:hover {
+ color: #000;
+}
+
+/* 模态框主体 */
+.modal-body {
+ padding: 20px;
+}
+
+.modal-body p {
+ margin: 0;
+ font-size: 16px;
+ color: #333;
+}
+
+.highlight {
+ font-weight: 600;
+ color: #0066cc; /* 蓝色高亮 */
+}
+
+/* 模态框底部 */
+.modal-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ padding: 16px 20px;
+ background-color: #f5f5f5;
+}
+
+/* 按钮样式 */
+.btn-cancel {
+ padding: 8px 16px;
+ border: 1px solid #ccc;
+ background: white;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.btn-cancel:hover {
+ background: #f0f0f0;
+}
+
+.btn-confirm {
+ padding: 8px 16px;
+ background-color: #0066cc; /* 蓝色按钮 */
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.btn-confirm:hover {
+ background-color: #0052a3; /* 深蓝色 */
+}
+
+/* 动画 */
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
\ No newline at end of file
diff --git a/src/TorrentDetail.css b/src/TorrentDetail.css
new file mode 100644
index 0000000..d854a7c
--- /dev/null
+++ b/src/TorrentDetail.css
@@ -0,0 +1,48 @@
+.page-wrapper {
+ padding: 0px 24px 24px 24px;
+ background-color: #ffffff;
+ min-height: 100vh;
+}
+
+.custom-card {
+ background-color: #fafae7;
+ border: 1px solid #ffd591 !important;
+ border-radius: 12px !important;
+ box-shadow: 0 4px 12px rgba(255, 153, 0, 0.1);
+}
+
+.custom-table .ant-table-thead > tr > th {
+ background-color: #fff1b8 !important;
+ color: #a15c00;
+ font-weight: bold;
+}
+
+.info-card {
+ background-color: #fffefc;
+ border: 1px solid #ffe58f;
+ border-radius: 16px !important;
+ box-shadow: 0 6px 16px rgba(255, 140, 0, 0.15);
+ padding: 24px;
+ transition: box-shadow 0.3s ease;
+}
+
+.info-card:hover {
+ box-shadow: 0 10px 24px rgba(255, 140, 0, 0.25);
+}
+
+.info-title {
+ color: #d46b08;
+ font-weight: 700;
+ margin-bottom: 24px;
+}
+
+.custom-descriptions .ant-descriptions-item-label {
+ background-color: #fff7e6 !important;
+ color: #a15c00 !important;
+ font-size: 16px;
+}
+
+.custom-descriptions .ant-descriptions-item-content {
+ font-size: 15px;
+ color: #595959;
+}
diff --git a/src/__mocks__/axios.js b/src/__mocks__/axios.js
new file mode 100644
index 0000000..1e2caac
--- /dev/null
+++ b/src/__mocks__/axios.js
@@ -0,0 +1,4 @@
+// __mocks__/axios.js
+const axios = jest.createMockFromModule('axios');
+
+export default axios;
\ No newline at end of file
diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx
new file mode 100644
index 0000000..a381989
--- /dev/null
+++ b/src/components/Navbar.jsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import { Link, useNavigate, useLocation } from 'react-router-dom';
+import { Layout, Menu, Avatar } from 'antd';
+import {
+ HomeOutlined,
+ CloudUploadOutlined,
+ ThunderboltOutlined,
+ UsergroupAddOutlined
+} from '@ant-design/icons';
+
+const { Header } = Layout;
+
+const Navbar = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ // 从localStorage获取用户数据
+ const userData = JSON.parse(localStorage.getItem('user')) || {};
+ const { username, image } = userData;
+
+ // 点击头像或用户名跳转到首页
+ const handleUserClick = () => {
+ navigate('/usercenter');
+ };
+
+ return (
+ <Header
+ style={{
+ backgroundColor: '#f5be42',
+ boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
+ padding: '0 40px',
+ width: '100%', // 确保宽度扩展到100%
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between'
+ }}
+ >
+ {/* Logo */}
+ <div style={{ color: '#fff', fontSize: '20px', fontWeight: 'bold' }}>
+ <Link to="/" style={{ color: '#fff', textDecoration: 'none' }}>
+ 社交互动平台
+ </Link>
+ </div>
+
+ {/* Menu */}
+ <Menu
+ mode="horizontal"
+ theme="dark"
+ selectedKeys={[location.pathname]}
+ style={{
+ backgroundColor: 'transparent',
+ flex: 1,
+ justifyContent: 'center',
+ borderBottom: 'none'
+ }}
+ >
+ <Menu.Item key="/mainpage" icon={<HomeOutlined />}>
+ <Link to="/home" style={{ color: '#fff' }}>首页</Link>
+ </Menu.Item>
+ <Menu.Item key="/" icon={<HomeOutlined />}>
+ <Link to="/community" style={{ color: '#fff' }}>社区</Link>
+ </Menu.Item>
+ <Menu.Item key="/torrents" icon={<ThunderboltOutlined />}>
+ <Link to="/torrents" style={{ color: '#fff' }}>种子</Link>
+ </Menu.Item>
+ <Menu.Item key="/upload" icon={<CloudUploadOutlined />}>
+ <Link to="/upload" style={{ color: '#fff' }}>上传种子</Link>
+ </Menu.Item>
+ <Menu.Item key="/friend" icon={<UsergroupAddOutlined />}>
+ <Link to="/friend" style={{ color: '#fff' }}>好友</Link>
+ </Menu.Item>
+ <Menu.Item key="/shop" icon={<UsergroupAddOutlined />}>
+ <Link to="/shop" style={{ color: '#fff' }}>商城</Link>
+ </Menu.Item>
+ </Menu>
+
+ {/* 用户名和头像 - 添加点击事件 */}
+ <div
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ gap: '8px',
+ cursor: 'pointer' // 添加指针样式表示可点击
+ }}
+ onClick={handleUserClick}
+ >
+ <Avatar
+ src={image}
+ size="default"
+ style={{ backgroundColor: '#f56a00' }}
+ />
+ <span style={{ color: '#fff' }}>{username}</span>
+ </div>
+ </Header>
+ );
+};
+
+export default Navbar;
\ No newline at end of file
diff --git a/src/components/RecommendAll.jsx b/src/components/RecommendAll.jsx
new file mode 100644
index 0000000..330c433
--- /dev/null
+++ b/src/components/RecommendAll.jsx
@@ -0,0 +1,83 @@
+import React, { useEffect, useState } from 'react';
+import '../torrentlist.css';
+
+const RecommendAll = () => {
+ const [torrents, setTorrents] = useState([]);
+ const storedUser = localStorage.getItem('user');
+ //let currentUserId = null; // 初始化为 null
+ let currentUserId = null; // 初始化为 null
+
+ if (storedUser) {
+ try {
+ const parsedUser = JSON.parse(storedUser);
+ currentUserId = parsedUser.userid; // 直接赋值
+ } catch (error) {
+ console.error('解析用户数据失败:', error);
+ // 可以在这里处理 JSON 解析错误(如数据损坏)
+ }
+ } else {
+ console.log('用户未登录');
+ }
+
+ // 现在 currentUserId 可以在后续逻辑中使用
+ console.log('当前用户ID:', currentUserId);
+
+
+ useEffect(() => {
+ const fetchAll = async () => {
+ const res = await fetch(`http://localhost:8080/recommend/list?userId=${currentUserId}`);
+ //const res = await fetch('http://localhost:8080/torrent/list');
+ const data = await res.json();
+ setTorrents(data);
+ };
+ fetchAll();
+ }, []);
+
+ const formatFileSize = (bytes) => {
+ if (!bytes) return '0 Bytes';
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+ return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
+ };
+
+ return (
+ <div className="torrents-container">
+ <h2 style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 16 }}>全部推荐种子</h2>
+ <div className="torrents1-grid">
+ {torrents.map(torrent => (
+ <div key={torrent.torrentid} className="torrent-card">
+ <div className="cover">
+ {torrent.coverImagePath ? (
+ <img src={torrent.coverImagePath} alt="封面" className="cover-image" />
+ ) : (
+ <div className="no-cover">无封面</div>
+ )}
+ </div>
+
+ <div className="info">
+ <h3 className="title" title={torrent.filename}>
+ {torrent.torrentTitle || torrent.filename}
+ </h3>
+ <p className="description" title={torrent.description}>
+ {torrent.description || '暂无描述'}
+ </p>
+
+ <div className="details">
+ <span>大小: {formatFileSize(torrent.torrentSize)}</span>
+ <span>上传时间: {new Date(torrent.uploadTime).toLocaleDateString()}</span>
+ <span>下载次数: {torrent.downloadCount}</span>
+ </div>
+
+ <div className="actions">
+ <button className="btn btn-download">下载</button>
+ <a href={`/torrent/${torrent.torrentid}`} className="btn btn-detail">详情</a>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+};
+
+export default RecommendAll;
diff --git a/src/components/RecommendPreview.jsx b/src/components/RecommendPreview.jsx
new file mode 100644
index 0000000..5eb45d3
--- /dev/null
+++ b/src/components/RecommendPreview.jsx
@@ -0,0 +1,92 @@
+import React, { useEffect, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import '../RecommendPreview.css';
+
+const RecommendPreview = () => {
+
+ const [recommendList, setRecommendList] = useState([]);
+ const navigate = useNavigate();
+
+ const storedUser = localStorage.getItem('user');
+ //let currentUserId = null; // 初始化为 null
+ let currentUserId = null; // 初始化为 null
+
+ if (storedUser) {
+ try {
+ const parsedUser = JSON.parse(storedUser);
+ currentUserId = parsedUser.userid; // 直接赋值
+ } catch (error) {
+ console.error('解析用户数据失败:', error);
+ // 可以在这里处理 JSON 解析错误(如数据损坏)
+ }
+ } else {
+ console.log('用户未登录');
+ }
+
+ // 现在 currentUserId 可以在后续逻辑中使用
+ console.log('当前用户ID:', currentUserId);
+
+ useEffect(() => {
+ const fetchRecommendList = async () => {
+ try {
+ const res = await fetch(`http://localhost:8080/recommend/list?userId=${currentUserId}`);
+ //const res = await fetch('http://localhost:8080/torrent/list');
+ const data = await res.json();
+ console.log('推荐列表数据:', data);
+ setRecommendList(data.slice(0, 8)); // 只展示前8个
+ } catch (err) {
+ console.error('推荐获取失败:', err);
+ }
+ };
+ fetchRecommendList();
+ }, []);
+
+ const formatFileSize = (bytes) => {
+ if (!bytes) return '0 Bytes';
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
+ return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
+ };
+
+ const formatDate = (dateString) => {
+ const date = new Date(dateString);
+ return date.toLocaleDateString();
+ };
+
+ return (
+ <div className="recommend-horizontal-container">
+ <div className="recommend-header">
+ <h2>为你推荐</h2>
+ <button className="more-btn" onClick={() => navigate('/recommend')}>
+ 更多
+ </button>
+ </div>
+
+ <div className="recommend-horizontal-grid">
+ {recommendList.map((torrent) => (
+ <div key={torrent.torrentid} className="horizontal-card">
+ {torrent.coverImagePath ? (
+ <img src={torrent.coverImagePath} alt="封面" className="card-cover" />
+ ) : (
+ <div className="card-no-cover">无封面</div>
+ )}
+ <div className="card-info">
+ <div className="card-title" title={torrent.torrentTitle || torrent.filename}>
+ {torrent.torrentTitle || torrent.filename}
+ </div>
+ <div className="card-description">
+ {torrent.description || '暂无描述'}
+ </div>
+ <div className="card-meta">
+ <span>{formatDate(torrent.uploadTime)}</span>
+ <span>{formatFileSize(torrent.torrentSize)}</span>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+};
+
+export default RecommendPreview;
diff --git a/src/components/Torrentdetail.jsx b/src/components/Torrentdetail.jsx
new file mode 100644
index 0000000..a955b7b
--- /dev/null
+++ b/src/components/Torrentdetail.jsx
@@ -0,0 +1,206 @@
+import React, { useEffect, useState } from 'react';
+import { useParams, useNavigate } from 'react-router-dom'; // 添加 useNavigate
+import axios from 'axios';
+import {
+ Row,
+ Col,
+ Card,
+ Descriptions,
+ Table,
+ Typography,
+ Spin,
+ Tag,
+ Button, // 添加 Button 组件
+} from 'antd';
+import { Image as AntdImage } from 'antd';
+import '../TorrentDetail.css'; // 引入样式
+
+const { Title, Text } = Typography;
+
+const TorrentDetail = () => {
+ const { id } = useParams();
+ const navigate = useNavigate(); // 获取导航函数
+ const [torrent, setTorrent] = useState(null);
+ const [seeders, setSeeders] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [torrentLoading, setTorrentLoading] = useState(true);
+ const [seedersLoading, setSeedersLoading] = useState(false);
+
+ useEffect(() => {
+ const fetchTorrentDetails = async () => {
+ try {
+ setTorrentLoading(true);
+ const torrentRes = await axios.get(`http://localhost:8080/torrent/${id}`);
+ setTorrent(torrentRes.data);
+
+ setSeedersLoading(true);
+ const seedersRes = await axios.get(`http://localhost:8080/torrent/${torrentRes.data.infoHash}/seeders`);
+ setSeeders(seedersRes.data);
+ } catch (err) {
+ console.error('获取数据失败', err);
+ } finally {
+ setTorrentLoading(false);
+ setSeedersLoading(false);
+ setLoading(false);
+ }
+ };
+
+ fetchTorrentDetails();
+ }, [id]);
+
+ const formatSize = (bytes) => {
+ if (bytes === 0) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ };
+
+ const formatSpeed = (bytesPerSec) => {
+ if (bytesPerSec < 1024) return bytesPerSec.toFixed(2) + ' B/s';
+ if (bytesPerSec < 1024 * 1024) return (bytesPerSec / 1024).toFixed(2) + ' KB/s';
+ return (bytesPerSec / (1024 * 1024)).toFixed(2) + ' MB/s';
+ };
+
+ const columns = [
+ {
+ title: '用户名',
+ dataIndex: 'username',
+ key: 'username',
+ align: 'center',
+ render: (text) => <Tag color="orange">{text}</Tag>,
+ },
+ {
+ title: '已上传',
+ dataIndex: 'uploaded',
+ key: 'uploaded',
+ align: 'center',
+ render: (text) => <Text>{formatSize(text)}</Text>,
+ },
+ {
+ title: '上传速度',
+ dataIndex: 'uploadSpeed',
+ key: 'uploadSpeed',
+ align: 'center',
+ render: (text) => <Text>{formatSpeed(text)}</Text>,
+ },
+ {
+ title: '已下载',
+ dataIndex: 'downloaded',
+ key: 'downloaded',
+ align: 'center',
+ render: (text) => <Text>{formatSize(text)}</Text>,
+ },
+ {
+ title: '下载速度',
+ dataIndex: 'downloadSpeed',
+ key: 'downloadSpeed',
+ align: 'center',
+ render: (text) => text > 0 ? <Text>{formatSpeed(text)}</Text> : <Text>-</Text>,
+ },
+ {
+ title: '客户端',
+ dataIndex: 'client',
+ key: 'client',
+ align: 'center',
+ },
+ {
+ title: '最后活动',
+ dataIndex: 'lastEvent',
+ key: 'lastEvent',
+ align: 'center',
+ },
+ ];
+
+ if (loading) return <div className="page-wrapper"><Spin size="large" /></div>;
+
+ return (
+ <div className="page-wrapper">
+ {/* 添加返回按钮 */}
+ <div className="mb-4">
+ <Button
+ type="primary"
+ onClick={() => navigate(-1)} // 返回上一页
+ style={{ marginBottom: '16px' }}
+ >
+ 返回列表
+ </Button>
+ </div>
+
+ <Row gutter={[16, 16]}>
+ <Col xs={24} md={8}>
+ <Card bordered={false} className="custom-card h-full">
+ {torrent.coverImagePath ? (
+ <AntdImage
+ src={torrent.coverImagePath}
+ alt="Torrent Cover"
+ className="w-full h-64 object-cover rounded"
+ placeholder={
+ <div className="w-full h-64 flex items-center justify-center bg-gray-100">
+ <Spin size="small" />
+ </div>
+ }
+ preview={false}
+ />
+ ) : (
+ <div className="w-full h-64 flex items-center justify-center bg-gray-100 rounded">
+ <Text type="secondary">无封面图片</Text>
+ </div>
+ )}
+ </Card>
+ </Col>
+
+ <Col xs={24} md={16}>
+ <Card className="info-card">
+ <Title level={1} className="info-title">
+ {torrent?.torrentTitle || '加载中...'}
+ </Title>
+
+ <Descriptions
+ bordered
+ column={{ xs: 1, sm: 2 }}
+ size="middle"
+ className="custom-descriptions"
+ labelStyle={{ fontWeight: 'bold', color: '#a15c00', fontSize: '16px' }}
+ contentStyle={{ fontSize: '15px' }}
+ >
+ <Descriptions.Item label="简介">{torrent.description || '暂无简介'}</Descriptions.Item>
+ <Descriptions.Item label="上传人">{torrent.uploader_id || '未知用户'}</Descriptions.Item>
+ <Descriptions.Item label="上传时间">{new Date(torrent.uploadTime).toLocaleString()}</Descriptions.Item>
+ <Descriptions.Item label="文件大小"><Text>{formatSize(torrent.torrentSize)}</Text></Descriptions.Item>
+ <Descriptions.Item label="下载数"><Text>{torrent.downloadCount || 0}</Text></Descriptions.Item>
+ <Descriptions.Item label="做种数"><Text>{seeders.length}</Text></Descriptions.Item>
+ <Descriptions.Item label="文件分辨率">{torrent.dpi || '未知'}</Descriptions.Item>
+ <Descriptions.Item label="文件字幕">{torrent.caption || '无'}</Descriptions.Item>
+ <Descriptions.Item label="最后做种时间">
+ {torrent.lastseed ? new Date(torrent.lastseed).toLocaleString() : '暂无'}
+ </Descriptions.Item>
+ </Descriptions>
+ </Card>
+ </Col>
+ </Row>
+
+ <Card className="custom-card mt-4">
+ <Title level={4} style={{ color: '#d46b08' }}>当前做种用户 ({seeders.length})</Title>
+ {seedersLoading ? (
+ <div className="p-4 text-center"><Spin size="small" /></div>
+ ) : seeders.length > 0 ? (
+ <Table
+ columns={columns}
+ dataSource={seeders}
+ rowKey={(record, index) => index}
+ pagination={false}
+ size="small"
+ className="custom-table"
+ />
+ ) : (
+ <div className="p-4 text-center text-gray-500 bg-gray-50 rounded">
+ 当前没有用户在做种
+ </div>
+ )}
+ </Card>
+ </div>
+ );
+};
+
+export default TorrentDetail;
\ No newline at end of file
diff --git a/src/components/torrentdetailcomplain.jsx b/src/components/torrentdetailcomplain.jsx
new file mode 100644
index 0000000..10bbd62
--- /dev/null
+++ b/src/components/torrentdetailcomplain.jsx
@@ -0,0 +1,346 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate, useLocation } from 'react-router-dom';
+import { useSearchParams } from 'react-router-dom';
+import {
+ Descriptions,
+ Table,
+ Button,
+ Modal,
+ Image,
+ message,
+ Spin,
+ Input,
+ Select,
+ Pagination,
+ Space,
+ Card
+} from 'antd';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import axios from 'axios';
+
+const { confirm } = Modal;
+const { Option } = Select;
+
+//const { confirm } = Modal;
+
+const TorrentDetailcomplain = () => {
+ const { id } = useParams(); // 从URL获取种子ID
+ const navigate = useNavigate(); // 用于返回上一页
+ const [torrent, setTorrent] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ //const [searchParams] = useSearchParams();
+ const location = useLocation(); // 添加这行获取location对象
+ // 在组件状态中添加
+ // 获取参数并设置默认值
+ //const [manageId, setManageId] = useState(parseInt(searchParams.get('manageid')) || 1);
+ // 正确从location.state获取参数
+ const { duser, torrentid } = location.state || {};
+ const [rewardUserId, setRewardUserId] = useState(duser);
+ const [creditAmount, setCreditAmount] = useState(3);
+ const [torrentId, setTorrentId] = useState(torrentid); // 使用state中的torrentid或URL中的id
+
+ console.log('Torrent ID:', torrentId); // 调试输出
+ console.log('Reward User ID:', rewardUserId); // 调试输出
+ const currentUserId = 1; // 示例,实际应从认证系统获取
+
+
+ // 格式化日期
+ const formatDate = (dateString) => {
+ if (!dateString) return '未知';
+ const date = new Date(dateString);
+ return date.toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ };
+
+ // 处理删除种子
+ const handleDeleteTorrent = async (torrentId) => {
+ if (!currentUserId) {
+ message.warning('请先登录');
+ return;
+ }
+
+ confirm({
+ title: '确认删除',
+ icon: <ExclamationCircleOutlined />,
+ content: '确定要删除这个种子吗?此操作不可恢复!',
+ onOk: async () => {
+ try {
+ await axios.delete(`http://localhost:8080/torrent/delete/${torrentId}`, {
+ params: { userid: currentUserId }
+ });
+ // 成功删除后,更新状态或返回上一页
+ setTorrent(null); // 清空当前种子详情
+ navigate(-1); // 返回上一页
+ message.success('种子删除成功');
+ } catch (err) {
+ console.error('删除种子失败', err);
+ if (err.response && err.response.status === 403) {
+ message.error('无权删除此种子');
+ } else {
+ message.error('删除种子失败');
+ }
+ }
+ }
+ });
+ };
+
+
+ const handleDownloadTorrent = async (torrentId) => {
+ if (!currentUserId) {
+ message.warning('请先登录');
+ return;
+ }
+
+ setIsLoading(true); // 开始加载
+ try {
+ // 使用axios发送带有参数的GET请求
+ // const response = await axios.get(`http://localhost:8080/torrent/download/${torrentId}`, {
+ // params: { userId: currentUserId }, // 正确添加请求参数
+ // responseType: 'blob' // 重要:指定响应类型为blob以处理文件下载
+ // });
+
+ // // 创建下载链接
+ // const url = window.URL.createObjectURL(new Blob([response.data]));
+ // const link = document.createElement('a');
+ // link.href = url;
+ // //link.setAttribute('download', 'torrent_file.torrent'); // 可以设置为动态文件名
+ // document.body.appendChild(link);
+ // link.click();
+ // document.body.removeChild(link);
+ // window.URL.revokeObjectURL(url);
+ open(`http://localhost:8080/torrent/download/${torrentId}?userId=${currentUserId}`, '_blank');
+
+ message.success('种子下载开始');
+ } catch (err) {
+ console.error('下载种子失败', err);
+ if (err.response?.status === 404) {
+ message.error('种子不存在');
+ } else {
+ message.error('下载失败: ' + (err.response?.data?.message || err.message));
+ }
+ } finally {
+ setIsLoading(false); // 结束加载
+ }
+ };
+
+
+ // 格式化文件大小
+ const formatFileSize = (bytes) => {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ };
+
+ // 在组件函数内部添加这个函数
+ const getPromotionName = (promotionId) => {
+ const promotionMap = {
+ 1: '上传加倍',
+ 2: '下载减半',
+ 3: '免费下载',
+ 0: '无促销'
+ };
+
+ return promotionMap[promotionId] || '未知促销';
+ };
+
+ // 获取种子详情
+ useEffect(() => {
+ const fetchTorrentDetail = async () => {
+ try {
+ const response = await axios.get(`http://localhost:8080/torrent/${torrentId}`);
+ if (response.status === 200) {
+ setTorrent(response.data);
+ } else {
+ setError('获取种子详情失败');
+ }
+ } catch (err) {
+ console.error('获取种子详情失败:', err);
+ if (err.response) {
+ if (err.response.status === 404) {
+ setError('种子不存在');
+ } else {
+ setError('获取种子详情失败: ' + err.response.data);
+ }
+ } else {
+ setError('网络错误,请稍后重试');
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchTorrentDetail();
+ }, [id]);
+
+ // 奖励保种积分处理函数(修正版)
+ const handleAddCredit = async (torrentId) => {
+ if (!currentUserId) { // 需要定义manageUserId
+ message.warning('缺少必要权限');
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ const params = new URLSearchParams();
+ params.append('manageid', currentUserId);
+ params.append('userid', rewardUserId);
+ params.append('credit', creditAmount);
+
+ const response = await axios.post(`http://localhost:8080/torrent/deducecredit`, params.toString(), {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }
+ });
+
+ if (response.data.success !== false) {
+ message.success(`成功扣除 ${creditAmount} 保种积分`);
+ } else {
+ message.error(response.data.message || '奖励积分失败');
+ }
+ } catch (err) {
+ console.error('奖励积分失败', err);
+ if (err.response?.status === 500) {
+ message.error('服务器错误: ' + (err.response.data?.message || '未知错误'));
+ } else {
+ message.error('奖励积分失败: ' + (err.response?.data?.message || err.message));
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ console.log('Torrent Detail:', torrent);
+
+ // 返回上一页
+ const handleBack = () => {
+ navigate(-1); // 返回上一页
+ };
+
+ // 如果正在加载
+ if (loading) {
+ return (
+ <div className="flex justify-center items-center h-96">
+ <Spin size="large" tip="加载中..." />
+ </div>
+ );
+ }
+
+ // 如果有错误
+ if (error) {
+ return (
+ <div className="p-6">
+ <Card>
+ <div className="text-center p-6">
+ <ExclamationCircleOutlined className="text-2xl text-red-500 mb-4" />
+ <h3 className="text-lg font-medium text-red-600 mb-2">错误</h3>
+ <p className="text-gray-600">{error}</p>
+ <Button type="primary" onClick={handleBack} className="mt-4">
+ 返回
+ </Button>
+ </div>
+ </Card>
+ </div>
+ );
+ }
+
+ // 如果种子不存在
+ if (!torrent) {
+ return (
+ <div className="p-6">
+ <Card>
+ <div className="text-center p-6">
+ <ExclamationCircleOutlined className="text-2xl text-yellow-500 mb-4" />
+ <h3 className="text-lg font-medium text-yellow-600 mb-2">种子不存在</h3>
+ <p className="text-gray-600">抱歉,您查找的种子不存在或已被删除。</p>
+ <Button type="primary" onClick={handleBack} className="mt-4">
+ 返回
+ </Button>
+ </div>
+ </Card>
+ </div>
+ );
+ }
+
+ // 渲染种子详情
+ return (
+ <div className="p-6">
+ <Card
+ title="种子详情"
+ extra={
+ <Button type="primary" onClick={handleBack}>
+ 返回列表
+ </Button>
+ }
+ >
+ {/* 使用Descriptions组件展示详情 */}
+ <Descriptions bordered column={1}>
+ <Descriptions.Item label="ID">{torrent.torrentid}</Descriptions.Item>
+ {/* 新增的封面图片展示 */}
+ {torrent.coverImagePath && (
+ <Descriptions.Item label="封面图片">
+ <Image
+ src={torrent.coverImagePath}
+ alt="种子封面"
+ width={200} // 设置图片宽度
+ placeholder={
+ <div className="w-48 h-32 bg-gray-200 flex items-center justify-center">
+ 加载中...
+ </div>
+ }
+ />
+ </Descriptions.Item>
+ )}
+ <Descriptions.Item label="文件名">{torrent.filename}</Descriptions.Item>
+ <Descriptions.Item label="大小">{formatFileSize(torrent.torrentSize)}</Descriptions.Item>
+ <Descriptions.Item label="上传者ID">{torrent.uploader_id}</Descriptions.Item>
+ <Descriptions.Item label="上传时间">{formatDate(torrent.uploadTime)}</Descriptions.Item>
+ <Descriptions.Item label="下载次数">{torrent.downloadCount}</Descriptions.Item>
+ <Descriptions.Item label="促销">
+ {getPromotionName(torrent.promotionid)}
+ </Descriptions.Item>
+ <Descriptions.Item label="描述">
+ {torrent.description || '无描述'}
+ </Descriptions.Item>
+ </Descriptions>
+
+
+ {/* 操作按钮区域 */}
+ <div className="mt-6 flex justify-end space-x-4">
+ <Button
+ danger
+ onClick={() => handleDeleteTorrent(torrent.torrentid)}
+ loading={isLoading}
+ >
+ 删除
+ </Button>
+ <Button
+ type="primary"
+ onClick={() => handleDownloadTorrent(torrent.torrentid)}
+ loading={isLoading}
+ >
+ 下载
+ </Button>
+ {/* 新增的奖励保种积分按钮 */}
+ <Button
+ type="default"
+ onClick={() => handleAddCredit(torrent.torrentid)}
+ loading={isLoading}
+ >
+ 扣除保种积分
+ </Button>
+ </div>
+ </Card>
+ </div>
+ );
+};
+
+export default TorrentDetailcomplain;
\ No newline at end of file
diff --git a/src/components/torrentdetailhelp.jsx b/src/components/torrentdetailhelp.jsx
new file mode 100644
index 0000000..8019e4c
--- /dev/null
+++ b/src/components/torrentdetailhelp.jsx
@@ -0,0 +1,353 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate, useLocation, useSearchParams } from 'react-router-dom'; // 添加 useLocation
+import {
+ Descriptions,
+ Table,
+ Button,
+ Modal,
+ Image,
+ message,
+ Spin,
+ Input,
+ Select,
+ Pagination,
+ Space,
+ Card
+} from 'antd';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import axios from 'axios';
+
+const { confirm } = Modal;
+const { Option } = Select;
+
+//const { confirm } = Modal;
+
+const TorrentDetailhelp = () => {
+ // 从URL获取种子ID
+ const navigate = useNavigate(); // 用于返回上一页
+ const [torrent, setTorrent] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [searchParams] = useSearchParams();
+ // 在组件状态中添加
+ // 获取参数并设置默认值
+ //const [manageId, setManageId] = useState(parseInt(searchParams.get('manageid')) || 1);
+ // const [rewardUserId, setRewardUserId] = useState(parseInt(searchParams.get('userid')) || 1);
+ // const [creditAmount, setCreditAmount] = useState(parseInt(searchParams.get('credit')) || 1);
+ const location = useLocation();
+
+ // 从路由state获取参数(可能为空)
+ const { loaduser, money, torrentid, helpedid } = location.state || {};
+
+ // 设置默认值
+ const [rewardUserId, setUserId] = useState(loaduser || null);
+ const [creditAmount, setCreditAmount] = useState(money || 1); // 默认1积分
+ const [torrentId, setTorrentId] = useState(torrentid); // 默认值为路由传递的torrentid
+ const [helpedId, setHelpedId] = useState(helpedid || null); // 新增的帮助者ID
+
+
+ const currentUserId = 1; // 示例,实际应从认证系统获取
+
+
+ // 格式化日期
+ const formatDate = (dateString) => {
+ if (!dateString) return '未知';
+ const date = new Date(dateString);
+ return date.toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ };
+
+ console.log("Helped ID:", helpedId);
+
+ // 处理删除种子
+ const handleDeleteTorrent = async (torrentId) => {
+ if (!currentUserId) {
+ message.warning('请先登录');
+ return;
+ }
+
+ confirm({
+ title: '确认删除',
+ icon: <ExclamationCircleOutlined />,
+ content: '确定要删除这个种子吗?此操作不可恢复!',
+ onOk: async () => {
+ try {
+ await axios.delete(`http://localhost:8080/torrent/delete/${torrentId}`, {
+ params: { userid: currentUserId }
+ });
+ // 成功删除后,更新状态或返回上一页
+ setTorrent(null); // 清空当前种子详情
+ navigate(-1); // 返回上一页
+ message.success('种子删除成功');
+ } catch (err) {
+ console.error('删除种子失败', err);
+ if (err.response && err.response.status === 403) {
+ message.error('无权删除此种子');
+ } else {
+ message.error('删除种子失败');
+ }
+ }
+ }
+ });
+ };
+
+
+ const handleDownloadTorrent = async (torrentId) => {
+ if (!currentUserId) {
+ message.warning('请先登录');
+ return;
+ }
+
+ setIsLoading(true); // 开始加载
+ try {
+ // 使用axios发送带有参数的GET请求
+ // const response = await axios.get(`http://localhost:8080/torrent/download/${torrentId}`, {
+ // params: { userId: currentUserId }, // 正确添加请求参数
+ // responseType: 'blob' // 重要:指定响应类型为blob以处理文件下载
+ // });
+
+ // // 创建下载链接
+ // const url = window.URL.createObjectURL(new Blob([response.data]));
+ // const link = document.createElement('a');
+ // link.href = url;
+ // //link.setAttribute('download', 'torrent_file.torrent'); // 可以设置为动态文件名
+ // document.body.appendChild(link);
+ // link.click();
+ // document.body.removeChild(link);
+ // window.URL.revokeObjectURL(url);
+ open(`http://localhost:8080/torrent/download/${torrentId}?userId=${currentUserId}`, '_blank');
+
+ message.success('种子下载开始');
+ } catch (err) {
+ console.error('下载种子失败', err);
+ if (err.response?.status === 404) {
+ message.error('种子不存在');
+ } else {
+ message.error('下载失败: ' + (err.response?.data?.message || err.message));
+ }
+ } finally {
+ setIsLoading(false); // 结束加载
+ }
+ };
+
+
+ // 格式化文件大小
+ const formatFileSize = (bytes) => {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ };
+
+ // 在组件函数内部添加这个函数
+ const getPromotionName = (promotionId) => {
+ const promotionMap = {
+ 1: '上传加倍',
+ 2: '下载减半',
+ 3: '免费下载',
+ 0: '无促销'
+ };
+
+ return promotionMap[promotionId] || '未知促销';
+ };
+
+ // 获取种子详情
+ useEffect(() => {
+ const fetchTorrentDetail = async () => {
+ try {
+ const response = await axios.get(`http://localhost:8080/torrent/${torrentid}`);
+ if (response.status === 200) {
+ setTorrent(response.data);
+ } else {
+ setError('获取种子详情失败');
+ }
+ } catch (err) {
+ console.error('获取种子详情失败:', err);
+ if (err.response) {
+ if (err.response.status === 404) {
+ setError('种子不存在');
+ } else {
+ setError('获取种子详情失败: ' + err.response.data);
+ }
+ } else {
+ setError('网络错误,请稍后重试');
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchTorrentDetail();
+ }, [torrentid]);
+
+ // 奖励保种积分处理函数(修正版)
+ const handleAddCredit = async (torrentId) => {
+ if (!currentUserId) { // 需要定义manageUserId
+ message.warning('缺少必要权限');
+ return;
+ }
+
+ setIsLoading(true);
+ try {
+ const params = new URLSearchParams();
+ params.append('manageid', currentUserId);
+ params.append('userid', rewardUserId);
+ params.append('credit', creditAmount);
+ params.append('helpedId', helpedId);
+
+ const response = await axios.post(`http://localhost:8080/torrent/addcredit`, params.toString(), {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ }
+ });
+
+ if (response.data.success !== false) {
+ message.success(`成功奖励 ${creditAmount} 保种积分`);
+ } else {
+ message.error(response.data.message || '奖励积分失败');
+ }
+ } catch (err) {
+ console.error('奖励积分失败', err);
+ if (err.response?.status === 500) {
+ message.error('服务器错误: ' + (err.response.data?.message || '未知错误'));
+ } else {
+ message.error('奖励积分失败: ' + (err.response?.data?.message || err.message));
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ console.log('Torrent Detail:', torrent);
+
+ // 返回上一页
+ const handleBack = () => {
+ navigate(-1); // 返回上一页
+ };
+
+ // 如果正在加载
+ if (loading) {
+ return (
+ <div className="flex justify-center items-center h-96">
+ <Spin size="large" tip="加载中..." />
+ </div>
+ );
+ }
+
+ // 如果有错误
+ if (error) {
+ return (
+ <div className="p-6">
+ <Card>
+ <div className="text-center p-6">
+ <ExclamationCircleOutlined className="text-2xl text-red-500 mb-4" />
+ <h3 className="text-lg font-medium text-red-600 mb-2">错误</h3>
+ <p className="text-gray-600">{error}</p>
+ <Button type="primary" onClick={handleBack} className="mt-4">
+ 返回
+ </Button>
+ </div>
+ </Card>
+ </div>
+ );
+ }
+
+ // 如果种子不存在
+ if (!torrent) {
+ return (
+ <div className="p-6">
+ <Card>
+ <div className="text-center p-6">
+ <ExclamationCircleOutlined className="text-2xl text-yellow-500 mb-4" />
+ <h3 className="text-lg font-medium text-yellow-600 mb-2">种子不存在</h3>
+ <p className="text-gray-600">抱歉,您查找的种子不存在或已被删除。</p>
+ <Button type="primary" onClick={handleBack} className="mt-4">
+ 返回
+ </Button>
+ </div>
+ </Card>
+ </div>
+ );
+ }
+
+ // 渲染种子详情
+ return (
+ <div className="p-6">
+ <Card
+ title="种子详情"
+ extra={
+ <Button type="primary" onClick={handleBack}>
+ 返回列表
+ </Button>
+ }
+ >
+ {/* 使用Descriptions组件展示详情 */}
+ <Descriptions bordered column={1}>
+ <Descriptions.Item label="ID">{torrent.torrentid}</Descriptions.Item>
+ {/* 新增的封面图片展示 */}
+ {torrent.coverImagePath && (
+ <Descriptions.Item label="封面图片">
+ <Image
+ src={torrent.coverImagePath}
+ alt="种子封面"
+ width={200} // 设置图片宽度
+ placeholder={
+ <div className="w-48 h-32 bg-gray-200 flex items-center justify-center">
+ 加载中...
+ </div>
+ }
+ />
+ </Descriptions.Item>
+ )}
+ <Descriptions.Item label="文件名">{torrent.filename}</Descriptions.Item>
+ <Descriptions.Item label="大小">{formatFileSize(torrent.torrentSize)}</Descriptions.Item>
+ <Descriptions.Item label="上传者ID">{torrent.uploader_id}</Descriptions.Item>
+ <Descriptions.Item label="上传时间">{formatDate(torrent.uploadTime)}</Descriptions.Item>
+ <Descriptions.Item label="下载次数">{torrent.downloadCount}</Descriptions.Item>
+ <Descriptions.Item label="促销">
+ {getPromotionName(torrent.promotionid)}
+ </Descriptions.Item>
+ <Descriptions.Item label="描述">
+ {torrent.description || '无描述'}
+ </Descriptions.Item>
+ </Descriptions>
+
+
+ {/* 操作按钮区域 */}
+ <div className="mt-6 flex justify-end space-x-4">
+ <Button
+ danger
+ onClick={() => handleDeleteTorrent(torrent.torrentid)}
+ loading={isLoading}
+ >
+ 删除
+ </Button>
+ <Button
+ type="primary"
+ onClick={() => handleDownloadTorrent(torrent.torrentid)}
+ loading={isLoading}
+ >
+ 下载
+ </Button>
+ {/* 新增的奖励保种积分按钮 */}
+ <Button
+ type="default"
+ onClick={() => handleAddCredit(torrent.torrentid)}
+ loading={isLoading}
+ >
+ 奖励保种积分
+ </Button>
+ </div>
+ </Card>
+ </div>
+ );
+};
+
+export default TorrentDetailhelp;
\ No newline at end of file
diff --git a/src/components/torrentlist.jsx b/src/components/torrentlist.jsx
new file mode 100644
index 0000000..c760999
--- /dev/null
+++ b/src/components/torrentlist.jsx
@@ -0,0 +1,902 @@
+import { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+//import { Layout, Menu, Button, Radio,Input, } from 'antd';
+import { Layout, Input, Button, Radio, Spin, message, Modal, Pagination } from 'antd';
+const { Header, Content, Sider } = Layout;
+import '../filter.css';
+import '../torrentlist.css';
+import '../complain.css';
+import axios from 'axios';
+import Navbar from './Navbar';
+import {createComplain} from '../api/complain'; // 假设举报API在这个路径
+
+// 常量配置集中管理
+const FILTER_OPTIONS = {
+ // 通用选项
+ common: {
+ resolution: [
+ { value: '720p', label: '720p' },
+ { value: '1080p', label: '1080p' },
+ { value: '2K', label: '2K' },
+ { value: '4K', label: '4K' },
+ { value: '8K', label: '8K' },
+ { value: '其他', label: '其他' },
+ ],
+ region: {
+ movie: [
+ { value: '大陆', label: '大陆' },
+ { value: '港台', label: '港台' },
+ { value: '欧美', label: '欧美' },
+ { value: '日韩', label: '日韩' },
+ { value: '其他', label: '其他' },
+ ],
+ variety: [
+ { value: '大陆', label: '大陆' },
+ { value: '港台', label: '港台' },
+ { value: '欧美', label: '欧美' },
+ { value: '日韩', label: '日韩' },
+ { value: '其他', label: '其他' },
+ ],
+ sports: [
+ { value: '亚洲', label: '亚洲' },
+ { value: '欧洲', label: '欧洲' },
+ { value: '美洲', label: '美洲' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ genre: {
+ movie: [
+ { value: '动作', label: '动作' },
+ { value: '喜剧', label: '喜剧' },
+ { value: '爱情', label: '爱情' },
+ { value: '科幻', label: '科幻' },
+ { value: '恐怖', label: '恐怖' },
+ { value: '冒险', label: '冒险' },
+ { value: '历史', label: '历史' },
+ { value: '悬疑', label: '悬疑' },
+ { value: '其他', label: '其他' },
+ ],
+ music: [
+ { value: '流行', label: '流行' },
+ { value: '摇滚', label: '摇滚' },
+ { value: '电子', label: '电子' },
+ { value: '古典', label: '古典' },
+ { value: '爵士', label: '爵士' },
+ { value: '民谣', label: '民谣' },
+ { value: '说唱', label: '说唱' },
+ { value: '其他', label: '其他' },
+ ],
+ anime: [
+ { value: '新番连载', label: '新番连载' },
+ { value: '剧场版', label: '剧场版' },
+ { value: 'OVA', label: 'OVA' },
+ { value: '完结动漫', label: '完结动漫' },
+ { value: '其他', label: '其他' },
+ ],
+ game: [
+ { value: '角色扮演', label: '角色扮演' },
+ { value: '射击', label: '射击' },
+ { value: '冒险', label: '冒险' },
+ { value: '策略', label: '策略' },
+ { value: '体育', label: '体育' },
+ { value: '桌面游戏', label: '桌面游戏' },
+ { value: '其他', label: '其他' },
+ ],
+ variety: [
+ { value: '真人秀', label: '真人秀' },
+ { value: '选秀', label: '选秀' },
+ { value: '访谈', label: '访谈' },
+ { value: '音乐', label: '音乐' },
+ { value: '游戏', label: '游戏' },
+ { value: '其他', label: '其他' },
+ ],
+ learning: [
+ { value: '计算机', label: '计算机' },
+ { value: '软件', label: '软件' },
+ { value: '人文', label: '人文' },
+ { value: '外语', label: '外语' },
+ { value: '理工科', label: '理工科' },
+ { value: '其他', label: '其他' },
+ ],
+ sports: [
+ { value: '足球', label: '足球' },
+ { value: '篮球', label: '篮球' },
+ { value: '网球', label: '网球' },
+ { value: '乒乓球', label: '乒乓球' },
+ { value: '羽毛球', label: '羽毛球' },
+ { value: '其他', label: '其他' },
+ ],
+ // 其他类型...
+ }
+ },
+
+ // 分类特定选项
+ categories: {
+ 1: { // 电影
+ name: '电影',
+ filters: [
+ { id: 'resolution', label: '分辨率', type: 'select' },
+ {
+ id: 'codec_format', label: '编码格式', type: 'select',
+ options: [
+ { value: 'H.264', label: 'H.264' },
+ { value: 'H.265', label: 'H.265' },
+ { value: 'AV1', label: 'AV1' },
+ { value: 'VC1', label: 'VC1' },
+ { value: 'X264', label: 'X264' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ { id: 'region', label: '地区', type: 'select' },
+ { id: 'genre', label: '类型', type: 'select' }
+ ]
+ },
+ 2: { // 电视剧
+ name: '剧集',
+ filters: [
+ {
+ id: 'region', label: '地区', type: 'select',
+ options: [
+ { value: '大陆', label: '大陆' },
+ { value: '港台', label: '港台' },
+ { value: '欧美', label: '欧美' },
+ { value: '日韩', label: '日韩' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'format', label: '分辨率', type: 'select',
+ options: [
+ { value: '720p', label: '720p' },
+ { value: '1080p', label: '1080p' },
+ { value: '2K', label: '2K' },
+ { value: '4K', label: '4K' },
+ { value: '8K', label: '8K' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'genre', label: '类型', type: 'select',
+ options: [
+ { value: '真人秀', label: '真人秀' },
+ { value: '选秀', label: '选秀' },
+ { value: '访谈', label: '访谈' },
+ { value: '游戏', label: '游戏' },
+ { value: '音乐', label: '音乐' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 3: { // 音乐
+ name: '音乐',
+ filters: [
+ {
+ id: 'genre', label: '类型', type: 'select',
+ options: [
+ { value: '专辑', label: '专辑' },
+ { value: '单曲', label: '单曲' },
+ { value: 'EP', label: 'EP' },
+ { value: '现场', label: '现场' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'style', label: '风格', type: 'select',
+ options: [
+ { value: '流行', label: '流行' },
+ { value: '摇滚', label: '摇滚' },
+ { value: '电子', label: '电子' },
+ { value: '古典', label: '古典' },
+ { value: '爵士', label: '爵士' },
+ { value: '民谣', label: '民谣' },
+ { value: '说唱', label: '说唱' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'format', label: '格式', type: 'select',
+ options: [
+ { value: 'MP3', label: 'MP3' },
+ { value: 'FLAC', label: 'FLAC' },
+ { value: 'WAV', label: 'WAV' },
+ { value: 'AAC', label: 'AAC' },
+ { value: 'OGG', label: 'OGG' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 4: { // 动漫
+ name: '动漫',
+ filters: [
+ {
+ id: 'genre', label: '类型', type: 'select',
+ options: [
+ { value: '新番连载', label: '新番连载' },
+ { value: '剧场版', label: '剧场版' },
+ { value: 'OVA', label: 'OVA' },
+ { value: '完结动漫', label: '完结动漫' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'format', label: '格式', type: 'select',
+ options: [
+ { value: 'ZIP', label: 'ZIP' },
+ { value: 'RAR', label: 'RAR' },
+ { value: '7Z', label: '7Z' },
+ { value: 'MKV', label: 'MKV' },
+ { value: 'MP4', label: 'MP4' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'resolution', label: '分辨率', type: 'select',
+ options: [
+ { value: '720p', label: '720p' },
+ { value: '1080p', label: '1080p' },
+ { value: '2K', label: '2K' },
+ { value: '4K', label: '4K' },
+ { value: '8K', label: '8K' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 5: { // 游戏
+ name: '游戏',
+ filters: [
+ {
+ id: 'platform', label: '平台', type: 'select',
+ options: [
+ { value: 'PC', label: 'PC' },
+ { value: 'PS5', label: 'PS5' },
+ { value: 'Xbox', label: 'Xbox' },
+ { value: 'Switch', label: 'Switch' },
+ { value: '手机', label: '手机' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'genre', label: '类型', type: 'select',
+ options: [
+ { value: '角色扮演', label: '角色扮演' },
+ { value: '射击', label: '射击' },
+ { value: '冒险', label: '冒险' },
+ { value: '策略', label: '策略' },
+ { value: '体育', label: '体育' },
+ { value: '桌面游戏', label: '桌面游戏' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'data_format', label: '数据类型', type: 'select',
+ options: [
+ { value: '压缩包', label: '压缩包' },
+ { value: '补丁', label: '补丁' },
+ { value: '安装包', label: '安装包' },
+ { value: 'nds', label: 'nds' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'language', label: '语言', type: 'select',
+ options: [
+ { value: '中文', label: '中文' },
+ { value: '英文', label: '英文' },
+ { value: '日文', label: '日文' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 6: { // 综艺
+ name: '综艺',
+ filters: [
+ {
+ id: 'is_mainland', label: '是否大陆综艺', type: 'select',
+ options: [
+ { value: 'true', label: '是' },
+ { value: 'false', label: ' 不是' },
+ ]
+ },
+ {
+ id: 'format', label: '分辨率', type: 'select',
+ options: [
+ { value: '720p', label: '720p' },
+ { value: '1080p', label: '1080p' },
+ { value: '2K', label: '2K' },
+ { value: '4K', label: '4K' },
+ { value: '8K', label: '8K' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'genre', label: '类型', type: 'select',
+ options: [
+ { value: '真人秀', label: '真人秀' },
+ { value: '选秀', label: '选秀' },
+ { value: '访谈', label: '访谈' },
+ { value: '游戏', label: '游戏' },
+ { value: '音乐', label: '音乐' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 7: { // 体育
+ name: '体育',
+ filters: [
+ {
+ id: 'genre', label: '体育类型', type: 'select',
+ options: [
+ { value: '足球', label: '足球' },
+ { value: '篮球', label: '篮球' },
+ { value: '网球', label: '网球' },
+ { value: '乒乓球', label: '乒乓球' },
+ { value: '羽毛球', label: '羽毛球' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'event_type', label: '赛事类型', type: 'select',
+ options: [
+ { value: '足球', label: '足球' },
+ { value: '篮球', label: '篮球' },
+ { value: '网球', label: '网球' },
+ { value: '乒乓球', label: '乒乓球' },
+ { value: '羽毛球', label: '羽毛球' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'format', label: '分辨率', type: 'select',
+ options: [
+ { value: '720p', label: '720p' },
+ { value: '1080p', label: '1080p' },
+ { value: '2K', label: '2K' },
+ { value: '4K', label: '4K' },
+ { value: '8K', label: '8K' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 8: { // 软件
+ name: '软件',
+ filters: [
+ {
+ id: 'platform', label: '平台', type: 'select',
+ options: [
+ { value: 'Windows', label: 'Windows' },
+ { value: 'Mac', label: 'Mac' },
+ { value: 'Linux', label: 'Linux' },
+ { value: 'Android', label: 'Android' },
+ { value: 'iOS', label: 'iOS' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'format', label: '格式', type: 'select',
+ options: [
+ { value: 'EXE', label: 'EXE' },
+ { value: 'DMG', label: 'DMG' },
+ { value: '光盘镜像', label: '光盘镜像' },
+ { value: 'APK', label: 'APK' },
+ { value: 'IPA', label: 'IPA' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'genre', label: '类型', type: 'select',
+ options: [
+ { value: '系统软件', label: '系统软件' },
+ { value: '应用软件', label: '应用软件' },
+ { value: '游戏软件', label: '游戏软件' },
+ { value: '驱动程序', label: '驱动程序' },
+ { value: '办公软件', label: '办公软件' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ ]
+ },
+ 9: { // 学习
+ name: '学习',
+ filters: [
+ {
+ id: 'genre', label: '类型', type: 'select',
+ options: [
+ { value: '计算机', label: '计算机' },
+ { value: '软件', label: '软件' },
+ { value: '人文', label: '人文' },
+ { value: '外语', label: '外语' },
+ { value: '理工科', label: '理工科' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'format', label: '格式', type: 'select',
+ options: [
+ { value: 'PDF', label: 'PDF' },
+ { value: 'EPUB', label: 'EPUB' },
+ { value: '视频', label: '视频' },
+ { value: '音频', label: '音频' },
+ { value: 'PPT', label: 'PPT' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 10: { // 纪录片
+ name: '纪录片',
+ filters: [
+ {
+ id: 'source', label: '视频源', type: 'select',
+ options: [
+ { value: 'CCTV', label: 'CCTV' },
+ { value: '卫视', label: '卫视' },
+ { value: '国家地理', label: '国家地理' },
+ { value: 'BBC', label: 'BBC' },
+ { value: 'Discovery', label: 'Discovery' },
+ { value: '其他', label: '其他' },
+ ]
+ },
+ {
+ id: 'format', label: '格式', type: 'select',
+ options: [
+ { value: '720p', label: '720p' },
+ { value: '1080p', label: '1080p' },
+ { value: '2K', label: '2K' },
+ { value: '4K', label: '4K' },
+ { value: '8K', label: '8K' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ },
+ 11: { // 其他
+ name: '其他',
+ filters: [
+ {
+ id: 'gener', label: '类型', type: 'select',
+ options: [
+ { value: '电子书', label: '电子书' },
+ { value: '视频', label: '视频' },
+ { value: 'MP3', label: 'MP3' },
+ { value: '图片', label: '图片' },
+ { value: '其他', label: '其他' },
+ ]
+ }
+ ]
+ }
+ // 其他分类配置...
+ }
+};
+
+
+
+function TorrentList() {
+ const [torrents, setTorrents] = useState([]);
+ const [categories, setCategories] = useState([]);
+ const [selectedCategory, setSelectedCategory] = useState("");
+ const [filters, setFilters] = useState({});
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ //const userId = localStorage.getItem('userId'); // 假设用户ID存储在localStorage中
+ const userId = 1; // 确保是数字类型
+ const [usernames, setUsernames] = useState({});
+ const [searchKeyword, setSearchKeyword] = useState('');
+ // [新增] 分页相关状态
+ const [currentPage, setCurrentPage] = useState(1); // 当前页码
+ const [itemsPerPage, setItemsPerPage] = useState(10); // 每页显示的项目数
+ const [totalItems, setTotalItems] = useState(0); // 确保这行存在
+const [isReportModalVisible, setIsReportModalVisible] = useState(false);
+const [currentTorrentId, setCurrentTorrentId] = useState(null);
+const [reportContent, setReportContent] = useState('');
+const [currentTorrent, setCurrentTorrent] = useState({});
+const[currentTorrentUploaderId, setCurrentTorrentUploaderId] = useState(null);
+ // 获取所有分类
+ useEffect(() => {
+ const fetchCategories = async () => {
+ try {
+ const res = await axios.get('http://localhost:8080/categories');
+ setCategories(res.data);
+ } catch (err) {
+ console.error('加载分类失败', err);
+ setError('加载分类失败,请稍后重试');
+ }
+ };
+ fetchCategories();
+ }, []);
+
+
+// 获取分类筛选配置
+const getCategoryFilters = (categoryId) => {
+
+ const category = FILTER_OPTIONS.categories[categoryId];
+ if (!category) return [];
+
+ return category.filters.map(filter => {
+ // 自动填充通用选项
+ if (filter.id === 'resolution' && !filter.options) {
+ return { ...filter, options: FILTER_OPTIONS.common.resolution };
+ }
+ if (filter.id === 'region' && !filter.options) {
+ const regionType = categoryId === 8 ? 'sports' : 'movie';
+ return { ...filter, options: FILTER_OPTIONS.common.region[regionType] };
+ }
+ if (filter.id === 'genre' && !filter.options) {
+ const genreType = categoryId === 3 ? 'music' : 'movie';
+ return { ...filter, options: FILTER_OPTIONS.common.genre[genreType] };
+ }
+ return filter;
+ });
+};
+
+// 显示举报模态框
+const showReportModal = (torrentId) => {
+ setCurrentTorrentId(torrentId);
+ setReportContent('');
+ setIsReportModalVisible(true);
+};
+
+// 处理举报提交
+const handleReportSubmit = async () => {
+ if (!reportContent.trim()) {
+ message.error('请输入举报内容');
+ return;
+ }
+
+ try {
+ const currentTorrentData = torrents.find(t => t.torrentid === currentTorrentId);
+ const duser = currentTorrentData ? currentTorrentData.uploader_id : null;
+ const complainData = {
+ puse: userId, // 举报人ID
+ duser: duser, // 被举报人ID
+ content: reportContent,
+ torrentid: currentTorrentId,
+
+ };
+ console.log('举报数据:', complainData)
+
+ await createComplain(complainData);
+ message.success('举报提交成功');
+ setIsReportModalVisible(false);
+ } catch (error) {
+ console.error('举报提交失败:', error);
+ message.error('举报提交失败,请稍后重试');
+ }
+ };
+
+
+
+// 格式化日期显示
+const formatDate = (dateString) => {
+ const options = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' };
+ return new Date(dateString).toLocaleString('zh-CN', options);
+};
+
+// 获取促销方式名称
+const getPromotionName = (promotionId) => {
+ switch (promotionId) {
+ case 1: return '上传加倍';
+ case 2: return '下载免费';
+ case 3: return '下载减半';
+ default: return '没有促销';
+ }
+};
+
+ // 搜索种子
+ const handleSearch = async () => {
+ if (!searchKeyword.trim()) {
+ fetchAllTorrents();
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+ try {
+ const res = await axios.get(`http://localhost:8080/torrent/search`, {
+ params: { keyword: searchKeyword },
+ });
+ setTorrents(res.data);
+ //setCurrentPage(1); // 搜索后重置为第一页
+ } catch (err) {
+ console.error('搜索失败', err);
+ setError('搜索失败,请稍后重试');
+ message.error('搜索失败');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // 获取种子数据
+ // [修改] 获取种子数据
+ useEffect(() => {
+ const fetchTorrents = async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ let url = selectedCategory
+ ? `http://localhost:8080/torrent/listByCategorywithfilter?categoryid=${selectedCategory}`
+ : `http://localhost:8080/torrent/list`;
+
+ // [修改] 添加筛选参数(不再需要page和limit参数)
+ const params = new URLSearchParams();
+ Object.entries(filters).forEach(([key, value]) => {
+ if (value) params.append(key, value);
+ });
+
+ // [修改] 只有当有筛选参数时才添加
+ if (params.toString()) {
+ const separator = selectedCategory ? '&' : '?';
+ url += separator + params.toString();
+ }
+
+ const res = await axios.get(url);
+ setTorrents(res.data); // [修改] 存储所有数据,不再分页
+ setTotalItems(res.data.length); // [新增] 设置总数据量
+ } catch (err) {
+ console.error('获取种子失败', err);
+ setError('获取种子列表失败,请稍后重试');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const timer = setTimeout(fetchTorrents, 300);
+ return () => clearTimeout(timer);
+ }, [selectedCategory, filters]); // [注意] 依赖项不变
+
+ // [新增] 前端分页处理函数
+ const paginateData = (data, currentPage, itemsPerPage) => {
+ const startIndex = (currentPage - 1) * itemsPerPage;
+ const endIndex = startIndex + itemsPerPage;
+ return data.slice(startIndex, endIndex);
+ };
+
+ const currentTorrents = paginateData(torrents, currentPage, itemsPerPage);
+ useEffect(() => {
+ const fetchUsernames = async () => {
+ if (torrents.length === 0) return;
+
+ const usernamePromises = torrents.map(async (torrent) => {
+ if (torrent.uploader_id && !usernames[torrent.uploader_id]) {
+ try {
+ const response = await fetch(`http://localhost:8080/torrent/${torrent.uploader_id}/username`);
+ if (response.ok) {
+ const username = await response.text();
+ return { [torrent.uploader_id]: username };
+ }
+ } catch (error) {
+ console.error(`Failed to fetch username for uploader_id ${torrent.uploader_id}:`, error);
+ }
+ }
+ return {};
+ });
+
+ const results = await Promise.all(usernamePromises);
+ const mergedUsernames = results.reduce((acc, curr) => ({ ...acc, ...curr }), {});
+ setUsernames((prev) => ({ ...prev, ...mergedUsernames }));
+ };
+
+ fetchUsernames();
+ }, [torrents]);
+
+
+ // 切换分类时重置筛选条件
+ const handleCategoryChange = (categoryId) => {
+ setSelectedCategory(categoryId);
+ setFilters({});
+ };
+
+ // 格式化文件大小
+ const formatFileSize = (bytes) => {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ };
+
+ // 处理筛选条件变化
+ const handleFilterChange = (e) => {
+ const { name, value } = e.target;
+ setFilters(prev => ({ ...prev, [name]: value }));
+ };
+
+
+
+ // 下载种子
+ const handleDownload = (torrentId) => {
+ //window.open(`http://localhost:8080/torrent/download/${torrentId}`, '_blank');
+ window.open(`http://localhost:8080/torrent/download/${torrentId}?userId=${userId}`, '_blank');
+
+ };
+
+ // 获取当前分类的筛选配置
+ const currentFilters = getCategoryFilters(selectedCategory);
+
+ return (
+ <div className="p-4 max-w-7xl mx-auto">
+ <Navbar />
+ {/* <h1 className="text-2xl font-bold mb-6">种子列表</h1> */}
+
+ {/* 搜索框 */}
+ <div className="mb-4 flex items-center">
+ <Input
+ placeholder="搜索种子..."
+ value={searchKeyword}
+ onChange={(e) => setSearchKeyword(e.target.value)}
+ style={{ width: 300 }}
+ onPressEnter={handleSearch}
+ />
+ <Button
+ type="primary"
+ onClick={handleSearch}
+ style={{ marginLeft: 8 }}
+ >
+ 搜索
+ </Button>
+ </div>
+
+ <div className="filter-container">
+ <div className="filter-container">
+ <div className="filter-row">
+ <label className="filter-label">选择分类:</label>
+ <Radio.Group
+ onChange={(e) => handleCategoryChange(e.target.value)}
+ value={selectedCategory}
+ className="flex space-x-2" // 添加 flex 布局
+ >
+ <Radio.Button className="custom-radio-btn" value="">
+ 全部分类
+ </Radio.Button>
+ {categories.map(cat => (
+ <Radio.Button key={cat.categoryid} className="custom-radio-btn" value={cat.categoryid}>
+ {cat.category_name}
+ </Radio.Button>
+ ))}
+ </Radio.Group>
+ </div>
+
+ </div>
+
+ {currentFilters.length > 0 &&
+ currentFilters.map(filter => (
+ <div key={filter.id} className="filter-row">
+ <label className="filter-label">{filter.label}:</label>
+ <Radio.Group
+ onChange={(e) => handleFilterChange({ target: { name: filter.id, value: e.target.value } })}
+ value={filters[filter.id] || ''}
+ >
+ <Radio.Button className="custom-radio-btn" value="">全部</Radio.Button>
+ {filter.options.map(option => (
+ <Radio.Button key={option.value} className="custom-radio-btn" value={option.value}>
+ {option.label}
+ </Radio.Button>
+ ))}
+ </Radio.Group>
+ </div>
+ ))}
+ </div>
+
+
+ {/* 错误提示 */}
+ {error && (
+ <div className="mb-4 p-3 bg-red-100 text-red-700 rounded border border-red-200">
+ {error}
+ </div>
+ )}
+
+
+ {isLoading ? (
+ <div className="loading-container">
+ <div className="spinner"></div>
+ </div>
+ ) : (
+ <div className="torrents-container">
+ {torrents.length > 0 ? (
+ <div className="torrents-grid">
+ {currentTorrents.map(torrent => (
+ <div key={torrent.torrentid} className="torrent-card">
+ <div className="cover">
+ {torrent.coverImagePath ? (
+ <img
+ src={torrent.coverImagePath}
+ alt="封面"
+ className="cover-image"
+ />
+ ) : (
+ <div className="no-cover">无封面</div>
+ )}
+ </div>
+
+ <div className="info">
+ <h3 className="title" title={torrent.filename}>
+ {torrent.filename}
+ </h3>
+ <p className="description" title={torrent.description}>
+ {torrent.description || '暂无描述'}
+ </p>
+
+ <div className="details">
+ <span>大小: {formatFileSize(torrent.torrentSize)}</span>
+ <span>上传者: {usernames[torrent.uploader_id]}</span>
+ <span>上传时间: {new Date(torrent.uploadTime).toLocaleDateString()}</span>
+ <span>下载次数: {torrent.downloadCount}</span>
+ <span>促销: {getPromotionName(torrent.promotionid)}</span>
+ </div>
+
+ <div className="actions">
+ <button
+ onClick={() => handleDownload(torrent.torrentid)}
+ className="btn btn-download"
+ >
+ 下载
+ </button>
+ <Link
+ to={`/torrent/${torrent.torrentid}`}
+ className="btn btn-detail"
+ >
+ 详情
+ </Link>
+ </div>
+
+ {/* 新增举报按钮 */}
+
+<button
+ className="report-btn"
+ onClick={() => showReportModal(torrent.torrentid)}
+>
+ 举报
+</button>
+
+ {/* 添加举报模态框 */}
+
+
+ </div>
+ </div>
+ ))}
+ </div>
+ ) : (
+ <div className="no-data">没有找到符合条件的种子</div>
+ )}
+ </div>
+ )}
+ <Modal
+ title="举报内容"
+ open={isReportModalVisible}
+ onOk={handleReportSubmit}
+ onCancel={() => setIsReportModalVisible(false)}
+ okText="提交"
+ cancelText="取消"
+ >
+ <p>您正在举报种子ID: {currentTorrentId}</p>
+ <Input.TextArea
+ rows={4}
+ value={reportContent}
+ onChange={(e) => setReportContent(e.target.value)}
+ placeholder="请输入举报原因..."
+ />
+ </Modal>
+ {/* // [新增] 分页组件 */}
+ {totalItems > 0 && (
+ <div className="pagination-container mt-6 flex justify-center">
+ <Pagination
+ current={currentPage}
+ pageSize={itemsPerPage}
+ total={totalItems}
+ onChange={(page) => setCurrentPage(page)} // [新增] 页码变化处理
+ showSizeChanger={false} // [可选] 是否显示每页条数选择器
+ showTotal={(total) => `共 ${total} 条记录`} // [可选] 显示总条数
+ />
+ </div>
+ )}
+
+ </div>
+ );
+}
+
+export default TorrentList;
\ No newline at end of file
diff --git a/src/components/torrentmanage.jsx b/src/components/torrentmanage.jsx
new file mode 100644
index 0000000..4756b35
--- /dev/null
+++ b/src/components/torrentmanage.jsx
@@ -0,0 +1,443 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import {
+ Table,
+ Button,
+ Modal,
+ Image,
+ message,
+ Spin,
+ Input,
+ Select,
+ Pagination,
+ Space
+} from 'antd';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import axios from 'axios';
+
+const { confirm } = Modal;
+const { Option } = Select;
+
+const TorrentManagement = () => {
+ // 状态管理
+ const [torrents, setTorrents] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [selectedTorrentId, setSelectedTorrentId] = useState(null);
+ const [promotionOptions, setPromotionOptions] = useState([
+ { value: 1, label: '上传加倍' },
+ { value: 2, label: '下载减半' },
+ { value: 3, label: '免费下载' },
+ { value: 0, label: '无促销' }
+ ]);
+ const [selectedPromotion, setSelectedPromotion] = useState(null);
+ const [showPromotionWarning, setShowPromotionWarning] = useState(false);
+ const [currentUserId, setCurrentUserId] = useState(null);
+ const [applyPromotionsLoading, setApplyPromotionsLoading] = useState(false);
+ const [usernames, setUsernames] = useState({});
+ const [searchKeyword, setSearchKeyword] = useState('');
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize, setPageSize] = useState(10);
+ const navigate = useNavigate(); // 用于导航到详情页
+
+ // 获取当前用户ID
+ useEffect(() => {
+ const userId = 1; // 示例,实际从认证系统获取
+ setCurrentUserId(userId ? parseInt(userId) : null);
+ }, []);
+
+ // 获取所有种子数据
+ useEffect(() => {
+ fetchAllTorrents();
+ }, [searchKeyword]);
+
+ // 获取所有种子数据的函数
+ const fetchAllTorrents = async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ const res = await axios.get('http://localhost:8080/torrent/list');
+ setTorrents(res.data);
+ setCurrentPage(1); // 重置为第一页
+ } catch (err) {
+ console.error('获取种子失败', err);
+ setError('获取种子列表失败,请稍后重试');
+ message.error('获取种子列表失败');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ console.log('当前种子列表:', torrents);
+
+ // 在组件加载时,批量获取所有 uploader_id 对应的 username
+ useEffect(() => {
+ const fetchUsernames = async () => {
+ if (torrents.length === 0) return;
+
+ const usernamePromises = torrents.map(async (torrent) => {
+ if (torrent.uploader_id && !usernames[torrent.uploader_id]) {
+ try {
+ const response = await fetch(`http://localhost:8080/torrent/${torrent.uploader_id}/username`);
+ if (response.ok) {
+ const username = await response.text();
+ return { [torrent.uploader_id]: username };
+ }
+ } catch (error) {
+ console.error(`Failed to fetch username for uploader_id ${torrent.uploader_id}:`, error);
+ }
+ }
+ return {};
+ });
+
+ const results = await Promise.all(usernamePromises);
+ const mergedUsernames = results.reduce((acc, curr) => ({ ...acc, ...curr }), {});
+ setUsernames((prev) => ({ ...prev, ...mergedUsernames }));
+ };
+
+ fetchUsernames();
+ }, [torrents]);
+
+ // 处理删除种子
+ const handleDeleteTorrent = async (torrentId) => {
+ if (!currentUserId) {
+ message.warning('请先登录');
+ return;
+ }
+
+ confirm({
+ title: '确认删除',
+ icon: <ExclamationCircleOutlined />,
+ content: '确定要删除这个种子吗?此操作不可恢复!',
+ onOk: async () => {
+ try {
+ await axios.delete(`http://localhost:8080/torrent/delete/${torrentId}`, {
+ params: { userid: currentUserId }
+ });
+ setTorrents(torrents.filter(torrent => torrent.torrentid !== torrentId));
+ message.success('种子删除成功');
+ } catch (err) {
+ console.error('删除种子失败', err);
+ if (err.response && err.response.status === 403) {
+ message.error('无权删除此种子');
+ } else {
+ message.error('删除种子失败');
+ }
+ }
+ }
+ });
+ };
+
+ // 搜索种子
+ const handleSearch = async () => {
+ if (!searchKeyword.trim()) {
+ fetchAllTorrents();
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+ try {
+ const res = await axios.get(`http://localhost:8080/torrent/search`, {
+ params: { keyword: searchKeyword },
+ });
+ setTorrents(res.data);
+ setCurrentPage(1); // 搜索后重置为第一页
+ } catch (err) {
+ console.error('搜索失败', err);
+ setError('搜索失败,请稍后重试');
+ message.error('搜索失败');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // 处理修改促销方式
+ const handlePromotionChange = (torrentId, newPromotion) => {
+ setSelectedTorrentId(torrentId);
+ setSelectedPromotion(newPromotion);
+ setShowPromotionWarning(true);
+ };
+
+ // 确认修改促销方式
+ const confirmPromotionChange = async () => {
+ if (selectedTorrentId && selectedPromotion !== null) {
+ try {
+ await axios.post('http://localhost:8080/torrent/setPromotion', null, {
+ params: {
+ userid: currentUserId,
+ torrentId: selectedTorrentId,
+ promotionId: selectedPromotion
+ }
+ });
+ setTorrents(torrents.map(torrent =>
+ torrent.torrentid === selectedTorrentId
+ ? { ...torrent, promotionid: selectedPromotion }
+ : torrent
+ ));
+ setShowPromotionWarning(false);
+ message.success('促销方式修改成功');
+ } catch (err) {
+ console.error('修改促销方式失败', err);
+ if (err.response && err.response.status === 403) {
+ message.error('无权修改此种子的促销方式');
+ } else {
+ message.error('修改促销方式失败');
+ }
+ }
+ }
+ };
+
+ // 取消修改促销方式
+ const cancelPromotionChange = () => {
+ setShowPromotionWarning(false);
+ setSelectedTorrentId(null);
+ setSelectedPromotion(null);
+ };
+
+ // 触发检查(应用促销规则)
+ const handleApplyPromotions = async () => {
+ if (!currentUserId) {
+ message.warning('请先登录');
+ return;
+ }
+
+ setApplyPromotionsLoading(true);
+ try {
+ const res = await axios.post('http://localhost:8080/torrent/applyPromotions', null, {
+ params: { userid: currentUserId }
+ });
+
+ if (res.data.success) {
+ message.success(res.data.message);
+ fetchAllTorrents(); // 刷新种子列表
+ }
+ } catch (err) {
+ console.error('应用促销规则失败', err);
+ if (err.response && err.response.status === 403) {
+ message.error('无权执行此操作');
+ } else {
+ message.error('应用促销规则失败');
+ }
+ } finally {
+ setApplyPromotionsLoading(false);
+ }
+ };
+
+ // 分页数据计算
+ const getCurrentPageData = () => {
+ const start = (currentPage - 1) * pageSize;
+ const end = start + pageSize;
+ return torrents.slice(start, end);
+ };
+
+ // 页码变化处理
+ const handlePageChange = (page) => {
+ setCurrentPage(page);
+ };
+
+ // 每页条数变化处理
+ const handlePageSizeChange = (current, size) => {
+ setPageSize(size);
+ setCurrentPage(1); // 重置为第一页
+ };
+
+ // 格式化日期
+ const formatDate = (dateString) => {
+ const options = { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' };
+ return new Date(dateString).toLocaleString('zh-CN', options);
+ };
+
+ // 格式化文件大小
+ const formatFileSize = (bytes) => {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ };
+
+ // 获取促销方式名称
+ const getPromotionName = (promotionId) => {
+ if (promotionId === null) return '无促销';
+ const option = promotionOptions.find(opt => opt.value === promotionId);
+ return option ? option.label : '未知促销';
+ };
+
+ const handleViewDetails = (torrentId) => {
+ navigate(`/admin/${torrentId}`); // 使用已定义的 navigate 变量
+ };
+
+ return (
+ <div className="p-4 max-w-7xl mx-auto">
+ <h1 className="text-2xl font-bold mb-6">种子管理</h1>
+
+ {/* 搜索框 */}
+ <div className="mb-4 flex items-center">
+ <Input
+ placeholder="搜索种子..."
+ value={searchKeyword}
+ onChange={(e) => setSearchKeyword(e.target.value)}
+ style={{ width: 300 }}
+ onPressEnter={handleSearch}
+ />
+ <Button
+ type="primary"
+ onClick={handleSearch}
+ style={{ marginLeft: 8 }}
+ >
+ 搜索
+ </Button>
+ </div>
+
+ {/* 右上角按钮 */}
+ <Button
+ type="primary"
+ loading={applyPromotionsLoading}
+ onClick={handleApplyPromotions}
+ style={{ marginBottom: 16 }}
+ >
+ 触发检查
+ </Button>
+
+ {/* 加载状态 */}
+ {isLoading && <Spin size="large" style={{ display: 'block', margin: '100px auto' }} />}
+
+ {/* 错误提示 */}
+ {error && <div className="mb-4 p-3 bg-red-100 text-red-700 rounded border border-red-200">{error}</div>}
+
+ {/* 种子列表表格 */}
+ {!isLoading && !error && (
+ <>
+ <Table
+ columns={[
+ {
+ title: '封面',
+ dataIndex: 'coverImagePath',
+ key: 'coverImagePath',
+ render: (text) => text ? (
+ <Image
+ src={text}
+ width={50}
+ height={50}
+ preview={{ maskClosable: true }}
+ />
+ ) : (
+ <div className="w-16 h-16 bg-gray-200 flex items-center justify-center">无封面</div>
+ )
+ },
+ {
+ title: '名称',
+ dataIndex: 'filename',
+ key: 'filename'
+ },
+ {
+ title: '描述',
+ dataIndex: 'description',
+ key: 'description'
+ },
+ {
+ title: '大小',
+ dataIndex: 'torrentSize',
+ key: 'torrentSize',
+ render: (size) => formatFileSize(size)
+ },
+ {
+ title: '上传者',
+ dataIndex: 'uploader_id',
+ key: 'uploader_id',
+ render: (id) => usernames[id] || id
+ },
+ {
+ title: '上传时间',
+ dataIndex: 'uploadTime',
+ key: 'uploadTime',
+ render: (time) => formatDate(time)
+ },
+ {
+ title: '下载次数',
+ dataIndex: 'downloadCount',
+ key: 'downloadCount'
+ },
+ {
+ title: '促销',
+ dataIndex: 'promotionid',
+ key: 'promotionid',
+ render: (id) => getPromotionName(id)
+ },
+ {
+ title: '操作',
+ key: 'action',
+ render: (_, record) => (
+ <Space>
+ <Button
+ danger
+ onClick={() => handleDeleteTorrent(record.torrentid)}
+ loading={isLoading}
+ >
+ 删除
+ </Button>
+ <Select
+ value={record.promotionid}
+ onChange={(value) => handlePromotionChange(record.torrentid, value)}
+ style={{ width: 120 }}
+ disabled={isLoading}
+ >
+ {promotionOptions.map(option => (
+ <Option key={option.value} value={option.value}>{option.label}</Option>
+ ))}
+ </Select>
+ <Button
+ type="primary"
+ size="small"
+ onClick={() => handleViewDetails(record.torrentid)} // 使用处理函数
+ >
+ 查看详情
+ </Button>
+ </Space>
+ )
+ }
+ ]}
+ dataSource={getCurrentPageData()}
+ rowKey="torrentid"
+ pagination={false}
+ loading={isLoading}
+ />
+
+ {/* 分页控件 */}
+ {torrents.length > 0 && (
+ <div style={{ marginTop: 16, textAlign: 'center' }}>
+ <Pagination
+ current={currentPage}
+ pageSize={pageSize}
+ total={torrents.length}
+ onChange={handlePageChange}
+ onShowSizeChange={handlePageSizeChange}
+ showSizeChanger
+ showTotal={(total) => `共 ${total} 条记录`}
+ pageSizeOptions={['10', '20', '50']}
+ />
+ </div>
+ )}
+ </>
+ )}
+
+ {/* 促销方式修改确认弹窗 */}
+ <Modal
+ title="确认修改促销方式"
+ open={showPromotionWarning}
+ onOk={confirmPromotionChange}
+ onCancel={cancelPromotionChange}
+ okText="确认"
+ cancelText="取消"
+ >
+ <p>
+ 您确定要将种子 ID 为
+ <span className="font-bold">{selectedTorrentId}</span> 的促销方式修改为
+ <span className="font-bold">「{getPromotionName(selectedPromotion)}」</span> 吗?
+ </p>
+ </Modal>
+ </div>
+ );
+};
+
+export default TorrentManagement;
\ No newline at end of file
diff --git a/src/components/upload-full.jsx b/src/components/upload-full.jsx
new file mode 100644
index 0000000..bb68fab
--- /dev/null
+++ b/src/components/upload-full.jsx
@@ -0,0 +1,2005 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import "../upload.css";
+import "../uploadtorrent.css";
+import { Form, Input, Select, Button, Typography, Space, message,Switch } from 'antd';
+import { useParams, useNavigate, useLocation, useSearchParams } from 'react-router-dom'; // 添加 useLocation
+import { InboxOutlined } from '@ant-design/icons';
+const { TextArea } = Input;
+const { Text } = Typography;
+
+
+function UploadTorrentFull() {
+ const [userid,setuserid] = useState(1);
+ const [title, setTitle] = useState('');
+ const [description, setDescription] = useState('');
+ const [categoryId, setCategoryId] = useState('');
+ const [file, setFile] = useState(null);
+ const [categories, setCategories] = useState([]);
+ const [showSuccess, setShowSuccess] = useState(false);
+ const [form] = Form.useForm();
+ const location = useLocation(); // 添加这行获取location对象
+ // 在组件状态中添加
+ // 获取参数并设置默认值
+ //const [manageId, setManageId] = useState(parseInt(searchParams.get('manageid')) || 1);
+ // 正确从location.state获取参数
+ const {requestid} = location.state || {};
+
+ // 通用参数
+ const [dpi, setDpi] = useState('');
+ const [caption, setCaption] = useState('');
+ const [region, setRegion] = useState('');
+ const [year, setYear] = useState('');
+ const [genre, setGenre] = useState('');
+ const [format, setFormat] = useState('');
+ const [resolution, setResolution] = useState('');
+ const [coverImage, setCoverImage] = useState(null);
+
+ // 特殊参数
+ const [codecFormat, setCodecFormat] = useState('');
+ const [platform, setPlatform] = useState('');
+ const [language, setLanguage] = useState('');
+ const [eventType, setEventType] = useState('');
+ const [source, setSource] = useState('');
+ const [style, setStyle] = useState('');
+ const [dataType, setdataType] = useState('');
+ const [isMainland, setIsMainland] = useState(false);
+
+ // 根据分类显示不同的表单字段
+ const [showMovieFields, setShowMovieFields] = useState(false);
+ const [showMusicFields, setShowMusicFields] = useState(false);
+ const [showGameFields, setShowGameFields] = useState(false);
+ const [showTvFields, setShowTvFields] = useState(false);
+ const [showAnimeFields, setShowAnimeFields] = useState(false);
+ const [showlearningFields, setShowlearningFields] = useState(false);
+ const [showsoftwareFields, setShowsoftwareFields] = useState(false);
+ const [showvarietyFields, setShowvarietyFields] = useState(false);
+ const [showsportsFields, setShowsportsFields] = useState(false);
+ const [showdocFields, setShowdocFields] = useState(false);
+ const [showotherFields, setShowotherFields] = useState(false);
+ // 其他分类字段...
+ // 编码格式选项
+ const codecFormats = [
+ { value: 'H.264', label: 'H.264' },
+ { value: 'H.265', label: 'H.265' },
+ { value: 'AV1', label: 'AV1' },
+ { value: 'VP9', label: 'VP9' },
+ { value: 'VC1', label: 'VC1' },
+ { value: 'X264', label: 'X264' },
+ ];
+ const regions = [
+ { value: '大陆', label: '大陆' },
+ { value: '港台', label: '港台' },
+ { value: '欧美', label: '欧美' },
+ { value: '日韩', label: '日韩' },
+ { value: '其他', label: '其他' },
+ ];
+ const genres = [
+ { value: '动作', label: '动作' },
+ { value: '喜剧', label: '喜剧' },
+ { value: '爱情', label: '爱情' },
+ { value: '科幻', label: '科幻' },
+ { value: '恐怖', label: '恐怖' },
+ { value: '动作', label: '动作' },
+ { value: '冒险', label: '冒险' },
+ { value: '历史', label: '历史' },
+ { value: '悬疑', label: '悬疑' },
+ { value: '其他', label: '其他' },
+ ];
+ const resolutions = [
+ { value: '720p', label: '720p' },
+ { value: '1080p', label: '1080p' },
+ { value: '2K', label: '2K' },
+ { value: '4K', label: '4K' },
+ { value: '8K', label: '8K' },
+ { value: '其他', label: '其他' },
+ ];
+
+
+ const eventTypes = [
+ { value: '足球', label: '足球' },
+ { value: '篮球', label: '篮球' },
+ { value: '网球', label: '网球' },
+ { value: '乒乓球', label: '乒乓球' },
+ { value: '羽毛球', label: '羽毛球' },
+ ]
+ const styles = [
+ { value: '大陆综艺', label: '大陆综艺' },
+ { value: '日韩综艺', label: '日韩综艺' },
+ { value: '欧美综艺', label: '欧美综艺' },
+ { value: '其他', label: '其他' },
+ ]
+ const platforms = [
+ { value: 'PC', label: 'PC' },
+ { value: 'PS5', label: 'PS5' },
+ { value: 'Xbox', label: 'Xbox' },
+ { value: 'Switch', label: 'Switch' },
+ { value: '手机', label: '手机' },
+ { value: '其他', label: '其他' },
+ ]
+ const gamegenres = [
+ { value: '角色扮演', label: '角色扮演' },
+ { value: '射击', label: '射击' },
+ { value: '冒险', label: '冒险' },
+ { value: '策略', label: '策略' },
+ { value: '体育', label: '体育' },
+ { value: '桌面游戏', label: '桌面游戏'},
+ { value: '其他', label: '其他' },
+ ]
+ const dataTypes = [
+ { value: '压缩包', label: '压缩包' },
+ { value: '补丁', label: '补丁' },
+ { value: '安装包', label: '安装包' },
+ { value: 'nds', label: 'nds' },
+ { value: '其他', label: '其他' },
+ ]
+ const languages = [
+ { value: '中文', label: '中文' },
+ { value: '英文', label: '英文' },
+ { value: '日文', label: '日文' },
+ { value: '其他', label: '其他' },
+ ]
+ const musicgenres = [
+ { value: '专辑', label: '专辑' },
+ { value: '单曲', label: '单曲' },
+ { value: 'EP', label: 'EP' },
+ { value: '现场', label: '现场' },
+ { value: '其他', label: '其他' },
+ ]
+ const musicstyles = [
+ { value: '流行', label: '流行' },
+ { value: '摇滚', label: '摇滚' },
+ { value: '电子', label: '电子' },
+ { value: '古典', label: '古典' },
+ { value: '爵士', label: '爵士' },
+ { value: '民谣', label: '民谣' },
+ { value: '说唱', label: '说唱' },
+ { value: '其他', label: '其他' },
+ ]
+ const musicformats = [
+ { value: 'MP3', label: 'MP3' },
+ { value: 'FLAC', label: 'FLAC' },
+ { value: 'WAV', label: 'WAV' },
+ { value: 'AAC', label: 'AAC' },
+ { value: 'OGG', label: 'OGG' },
+ { value: '其他', label: '其他' },
+ ]
+ const anigenres = [
+ { value: '新番连载', label: '新番连载' },
+ { value: '剧场版', label: '剧场版' },
+ { value: 'OVA', label: 'OVA' },
+ { value: '完结动漫', label: '完结动漫' },
+ { value: '其他', label: '其他' },
+ ]
+ const animeformats = [
+ { value: 'ZIP', label: 'ZIP' },
+ { value: 'RAR', label: 'RAR' },
+ { value: '7Z', label: '7Z' },
+ { value: 'MKV', label: 'MKV' },
+ { value: 'MP4', label: 'MP4' },
+ { value: '其他', label: '其他' },
+ ]
+ const varietygenres = [
+ { value: '真人秀', label: '真人秀' },
+ { value: '选秀', label: '选秀' },
+ { value: '访谈', label: '访谈' },
+ { value: '游戏', label: '游戏' },
+ { value: '音乐', label: '音乐' },
+ { value: '其他', label: '其他' },
+ ]
+ const sportsgenres = [
+ { value: '足球', label: '足球' },
+ { value: '篮球', label: '篮球' },
+ { value: '网球', label: '网球' },
+ { value: '乒乓球', label: '乒乓球' },
+ { value: '羽毛球', label: '羽毛球' },
+ { value: '其他', label: '其他' },
+ ]
+ const softwaregenres = [
+ { value: '系统软件', label: '系统软件' },
+ { value: '应用软件', label: '应用软件' },
+ { value: '游戏软件', label: '游戏软件' },
+ { value: '驱动程序', label: '驱动程序' },
+ { value: '办公软件', label: '办公软件' },
+ { value: '其他', label: '其他' },
+ ]
+ const softwareplatforms = [
+ { value: 'Windows', label: 'Windows' },
+ { value: 'Mac', label: 'Mac' },
+ { value: 'Linux', label: 'Linux' },
+ { value: 'Android', label: 'Android' },
+ { value: 'iOS', label: 'iOS' },
+ { value: '其他', label: '其他' },
+ ]
+ const softwareformats = [
+ { value: 'EXE', label: 'EXE' },
+ { value: 'DMG', label: 'DMG' },
+ { value: '光盘镜像', label: '光盘镜像' },
+ { value: 'APK', label: 'APK' },
+ { value: 'IPA', label: 'IPA' },
+ { value: '其他', label: '其他' },
+ ]
+ const learninggenres = [
+ { value: '计算机', label: '计算机' },
+ { value: '软件', label: '软件' },
+ { value: '人文', label: '人文' },
+ { value: '外语', label: '外语' },
+ { value: '理工类', label: '理工类' },
+ { value: '其他', label: '其他' },
+ ]
+ const learningformats = [
+ { value: 'PDF', label: 'PDF' },
+ { value: 'EPUB', label: 'EPUB' },
+ { value: '视频', label: '视频' },
+ { value: '音频', label: '音频' },
+ { value: 'PPT', label: 'PPT' },
+ { value: '其他', label: '其他' },
+ ]
+ const sourceTypes = [
+ { value: 'CCTV', label: 'CCTV' },
+ { value: '卫视', label: '卫视' },
+ { value: '国家地理', label: '国家地理' },
+ { value: 'BBC', label: 'BBC' },
+ { value: 'Discovery', label: 'Discovery' },
+ { value: '其他', label: '其他' },
+ ]
+ const othergenres = [
+ { value: '电子书', label: '电子书' },
+ { value: '视频', label: '视频' },
+ { value: 'MP3', label: 'MP3' },
+ { value: '图片', label: '图片' },
+ { value: '其他', label: '其他' },
+ ]
+ useEffect(() => {
+ axios.get('http://localhost:8080/categories')
+ .then(res => setCategories(res.data))
+ .catch(err => console.error('加载分类失败', err));
+ }, []);
+
+ // 根据选择的分类显示不同的表单字段
+ useEffect(() => {
+ setShowMovieFields(categoryId === '1');
+ setShowMusicFields(categoryId === '3');
+ setShowGameFields(categoryId === '5');
+ setShowTvFields(categoryId === '2');
+ setShowAnimeFields(categoryId === '4');
+ setShowlearningFields(categoryId === '9');
+ setShowsoftwareFields(categoryId === '8');
+ setShowvarietyFields(categoryId === '6');
+ setShowsportsFields(categoryId === '7');
+ setShowdocFields(categoryId === '10');
+ setShowotherFields(categoryId === '11');
+ // 其他分类...
+ }, [categoryId]);
+
+
+ const handleSubmit = async (e) => {
+ //e.preventDefault();
+ if (!coverImage) {
+ alert('请选择一个 图片 文件');
+ return;
+ }
+ if (!file) {
+ alert('请选择一个 .torrent 文件');
+ return;
+ }
+
+ if (!categoryId) {
+ alert('请选择分类');
+ return;
+ }
+
+
+ const formData = new FormData();
+ formData.append('userid', userid);
+ formData.append('file', file);
+ // Add cover image if it exists
+ formData.append('coverImage', coverImage);
+ formData.append('title', title);
+ formData.append('description', description);
+ formData.append('categoryId', categoryId);
+ formData.append('requestId', requestid);
+
+
+ // 通用参数
+ if (dpi) formData.append('dpi', dpi);
+ if (caption) formData.append('caption', caption);
+ if (region) formData.append('region', region);
+ if (year) formData.append('year', year);
+ if (genre) formData.append('genre', genre);
+ if (format) formData.append('format', format);
+ if (resolution) formData.append('resolution', resolution);
+
+ // 特殊参数
+ if (codecFormat) formData.append('codecFormat', codecFormat);
+ if (platform) formData.append('platform', platform);
+ if (language) formData.append('language', language);
+ if (eventType) formData.append('eventType', eventType);
+ if (source) formData.append('source', source);
+ if (style) formData.append('style', style);
+ if (dataType) formData.append('dataType', dataType);
+ formData.append('isMainland', isMainland.toString());
+
+ try {
+ const response = await axios.post('http://localhost:8080/torrent/fullrequest', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ responseType: 'blob',
+ });
+
+ // 创建下载链接
+ const url = window.URL.createObjectURL(new Blob([response.data]));
+ const link = document.createElement('a');
+ link.href = url;
+ link.setAttribute('download', file.name);
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+
+ // 显示成功提示
+ setShowSuccess(true);
+ // 清空表单
+ setTitle('');
+ setDescription('');
+ setCategoryId('');
+ setFile(null);
+ // 清空其他字段...
+ } catch (err) {
+ console.error('上传失败', err.response?.data || err.message);
+ alert(err.response?.data || '上传失败,请检查后端是否启动');
+ }
+ };
+
+// return (
+// <div className="max-w-2xl mx-auto mt-10 p-6 bg-white shadow rounded">
+// <h2 className="text-2xl font-bold mb-6 text-orange-800 text-center border-b-2 border-orange-200 pb-3">上传种子</h2>
+// <form onSubmit={handleSubmit} className="space-y-6">
+// {/* 封面图片 - 大型上传区域 */}
+// <div className="form-group text-center">
+// <label className="upload-area text-orange-800 font-medium cursor-pointer p-8 border-2 border-dashed border-orange-300 rounded-lg hover:border-orange-400 transition-colors">
+// <svg xmlns="http://www.w3.org/2000/svg" className="mx-auto h-12 w-12 text-orange-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+// <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
+// </svg>
+// <p className="mt-2 text-sm">点击或拖拽上传封面图片</p>
+// <input
+// type="file"
+// accept="image/*"
+// onChange={(e) => setCoverImage(e.target.files[0])}
+// className="hidden"
+// id="cover-image-upload"
+// />
+// </label>
+// <input
+// type="file"
+// accept="image/*"
+// onChange={(e) => setCoverImage(e.target.files[0])}
+// className="sr-only"
+// id="cover-image-upload-real"
+// />
+// </div>
+
+// {/* 种子文件 - 大型上传区域 */}
+// <div className="form-group text-center">
+// <label className="upload-area text-orange-800 font-medium cursor-pointer p-8 border-2 border-dashed border-orange-300 rounded-lg hover:border-orange-400 transition-colors">
+// <svg xmlns="http://www.w3.org/2000/svg" className="mx-auto h-12 w-12 text-orange-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+// <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
+// </svg>
+// <p className="mt-2 text-sm">点击或拖拽上传种子文件(.torrent)</p>
+// <input
+// type="file"
+// accept=".torrent"
+// onChange={(e) => setFile(e.target.files[0])}
+// className="hidden"
+// id="torrent-file-upload"
+// required
+// />
+// </label>
+// <input
+// type="file"
+// accept=".torrent"
+// onChange={(e) => setFile(e.target.files[0])}
+// className="sr-only"
+// id="torrent-file-upload-real"
+// required
+// />
+// </div>
+
+// {/* 标题 */}
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">标题</label>
+// <input
+// type="text"
+// placeholder="输入种子标题"
+// value={title}
+// onChange={(e) => setTitle(e.target.value)}
+// className="form-control text-center"
+// required
+// />
+// </div>
+
+// {/* 描述 */}
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">描述</label>
+// <textarea
+// placeholder="输入种子描述"
+// value={description}
+// onChange={(e) => setDescription(e.target.value)}
+// className="form-control text-center"
+// rows={3}
+// />
+// </div>
+
+// {/* 分类 - 美化下拉框 */}
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">分类</label>
+// <div className="relative">
+// <select
+// value={categoryId}
+// onChange={(e) => setCategoryId(e.target.value)}
+// className="custom-select"
+// required
+// >
+// <option value="">请选择分类</option>
+// {categories.map(cat => (
+// <option key={cat.categoryid} value={cat.categoryid}>{cat.category_name}</option>
+// ))}
+// </select>
+// <div className="select-arrow">
+// <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-orange-500" viewBox="0 0 20 20" fill="currentColor">
+// <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
+// </svg>
+// </div>
+// </div>
+// </div>
+
+// {/* 动态字段 - 电影 */}
+// {showMovieFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">字幕/说明</label>
+// <input
+// type="text"
+// placeholder="输入字幕/说明"
+// value={caption}
+// onChange={(e) => setCaption(e.target.value)}
+// className="form-control text-center"
+// />
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">地区</label>
+// <div className="relative">
+// <select
+// value={region}
+// onChange={(e) => setRegion(e.target.value)}
+// className="custom-select"
+// >
+// <option value="">请选择地区</option>
+// {regions.map((region) => (
+// <option key={region.value} value={region.value}>
+// {region.label}
+// </option>
+// ))}
+// </select>
+// <div className="select-arrow">
+// <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-orange-500" viewBox="0 0 20 20" fill="currentColor">
+// <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
+// </svg>
+// </div>
+// </div>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">年份</label>
+// <input
+// type="text"
+// placeholder="输入年份"
+// value={year}
+// onChange={(e) => setYear(e.target.value)}
+// className="form-control text-center"
+// />
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">类型</label>
+// <div className="relative">
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="custom-select"
+// >
+// <option value="">请选择类型</option>
+// {genres.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// <div className="select-arrow">
+// <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-orange-500" viewBox="0 0 20 20" fill="currentColor">
+// <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
+// </svg>
+// </div>
+// </div>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">编码格式</label>
+// <div className="relative">
+// <select
+// value={codecFormat}
+// onChange={(e) => setCodecFormat(e.target.value)}
+// className="custom-select"
+// >
+// <option value="">请选择编码格式</option>
+// {codecFormats.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// <div className="select-arrow">
+// <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-orange-500" viewBox="0 0 20 20" fill="currentColor">
+// <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
+// </svg>
+// </div>
+// </div>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">分辨率</label>
+// <div className="relative">
+// <select
+// value={resolution}
+// onChange={(e) => setResolution(e.target.value)}
+// className="custom-select"
+// >
+// <option value="">请选择分辨率</option>
+// {resolutions.map((resolution) => (
+// <option key={resolution.value} value={resolution.value}>
+// {resolution.label}
+// </option>
+// ))}
+// </select>
+// <div className="select-arrow">
+// <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-orange-500" viewBox="0 0 20 20" fill="currentColor">
+// <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
+// </svg>
+// </div>
+// </div>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 剧集 */}
+// {showTvFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">地区</label>
+// <select
+// value={region}
+// onChange={(e) => setRegion(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择地区</option>
+// {regions.map((region) => (
+// <option key={region.value} value={region.value}>
+// {region.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {resolutions.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {genres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 游戏 */}
+// {showGameFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">平台</label>
+// <select
+// value={platform}
+// onChange={(e) => setPlatform(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择平台</option>
+// {platforms.map((platform) => (
+// <option key={platform.value} value={platform.value}>
+// {platform.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {gamegenres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">语言</label>
+// <select
+// value={language}
+// onChange={(e) => setLanguage(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择语言</option>
+// {languages.map((language) => (
+// <option key={language.value} value={language.value}>
+// {language.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">数据类型</label>
+// <select
+// value={dataType}
+// onChange={(e) => setdataType(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择数据类型</option>
+// {dataTypes.map((dataType) => (
+// <option key={dataType.value} value={dataType.value}>
+// {dataType.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 综艺 */}
+// {showvarietyFields && (
+// <>
+// <div className="form-group">
+// <label className="flex items-center">
+// <input
+// type="checkbox"
+// checked={isMainland}
+// onChange={(e) => setIsMainland(e.target.checked)}
+// className="mr-2"
+// />
+// 是否大陆综艺
+// </label>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={style}
+// onChange={(e) => setStyle(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {varietygenres.map((style) => (
+// <option key={style.value} value={style.value}>
+// {style.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {resolutions.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 动漫 */}
+// {showAnimeFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {anigenres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {animeformats.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">分辨率</label>
+// <select
+// value={resolution}
+// onChange={(e) => setResolution(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择分辨率</option>
+// {resolutions.map((resolution) => (
+// <option key={resolution.value} value={resolution.value}>
+// {resolution.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 学习 */}
+// {showlearningFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {learninggenres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {learningformats.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 软件 */}
+// {showsoftwareFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">平台</label>
+// <select
+// value={platform}
+// onChange={(e) => setPlatform(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择平台</option>
+// {softwareplatforms.map((platform) => (
+// <option key={platform.value} value={platform.value}>
+// {platform.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {softwaregenres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {softwareformats.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 体育 */}
+// {showsportsFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {sportsgenres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {resolutions.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">赛事类型</label>
+// <select
+// value={eventType}
+// onChange={(e) => setEventType(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择赛事类型</option>
+// {eventTypes.map((eventType) => (
+// <option key={eventType.value} value={eventType.value}>
+// {eventType.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 纪录片 */}
+// {showdocFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">年份</label>
+// <input
+// type="text"
+// placeholder="如 1999, 2020"
+// value={year}
+// onChange={(e) => setYear(e.target.value)}
+// className="form-control"
+// />
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">视频源</label>
+// <select
+// value={source}
+// onChange={(e) => setSource(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择视频源</option>
+// {sourceTypes.map((source) => (
+// <option key={source.value} value={source.value}>
+// {source.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {resolutions.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 音乐 */}
+// {showMusicFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {musicgenres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">地区</label>
+// <select
+// value={region}
+// onChange={(e) => setRegion(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择地区</option>
+// {regions.map((region) => (
+// <option key={region.value} value={region.value}>
+// {region.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">风格</label>
+// <select
+// value={style}
+// onChange={(e) => setStyle(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择风格</option>
+// {musicstyles.map((style) => (
+// <option key={style.value} value={style.value}>
+// {style.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {musicformats.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 其他 */}
+// {showotherFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {othergenres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+
+// {/* 其他动态字段... (保持相同的美化样式) */}
+// {/* 这里省略了其他动态字段的代码,但它们都使用相同的自定义下拉框样式 */}
+
+// {/* 提交按钮 - 居中 */}
+// <div className="text-center">
+// <button
+// type="submit"
+// className="submit-btn"
+// >
+// 上传种子
+// </button>
+// </div>
+
+// {showSuccess && (
+// <div className="success-message text-center">
+// 上传成功!
+// </div>
+// )}
+// </form>
+// </div>
+// );
+// }
+return (
+ <div className="container">
+ <div className="card">
+ <h2 className="title">上传种子</h2>
+ <Form
+ layout="vertical"
+ onFinish={handleSubmit}
+ className="form"
+ >
+ {/* 封面图片 - 自定义上传区域 */}
+<Form.Item
+ name="coverImage"
+ label={<span style={{ fontSize: '19px' , fontWeight: 'bold'}}>封面图片</span>}
+ rules={[{ required: true, message: '请上传封面图片' }]}
+>
+ <div
+ className="uploadArea"
+ onClick={() => document.getElementById('cover-image-upload').click()}
+ >
+ <InboxOutlined className="uploadIcon" />
+ <p className="uploadText">点击或拖拽上传封面图片</p>
+ <input
+ type="file"
+ accept="image/*"
+ onChange={(e) => {
+ setCoverImage(e.target.files[0]);
+ form.setFieldsValue({ coverImage: e.target.files[0] });
+ }}
+ className="hiddenInput"
+ id="cover-image-upload"
+ />
+ </div>
+</Form.Item>
+
+{/* 种子文件 - 自定义上传区域 */}
+<Form.Item
+ name="torrentFile"
+ label={<span style={{ fontSize: '19px' , fontWeight: 'bold'}}>种子文件</span>}
+ rules={[{ required: true, message: '请上传.torrent文件' }]}
+>
+ <div
+ className="uploadArea"
+ onClick={() => document.getElementById('torrent-file-upload').click()}
+ >
+ <InboxOutlined className="uploadIcon" />
+ <p className="uploadText">点击或拖拽上传种子文件(.torrent)</p>
+ <input
+ type="file"
+ accept=".torrent"
+ onChange={(e) => {
+ setFile(e.target.files[0]);
+ form.setFieldsValue({ torrentFile: e.target.files[0] });
+ }}
+ className="hiddenInput"
+ id="torrent-file-upload"
+ />
+ </div>
+</Form.Item>
+
+ {/* 标题 */}
+ <Form.Item
+ name="title"
+ label={<span style={{ fontSize: '19px' , fontWeight: 'bold'}}>标题</span>} // 直接包裹label
+ rules={[{ required: true, message: '请输入标题' }]}
+ //labelCol={{ style: { fontSize: '30px !important ' } }} // ✅ 调整 Label 字体
+ >
+ <Input
+ placeholder="输入种子标题"
+ value={title}
+ onChange={(e) => {
+ setTitle(e.target.value);
+ form.setFieldsValue({ title: e.target.value });
+ }}
+ //style={styles.input}
+ style={{ fontSize: '19px' }} // ✅ 调整 Input 字体
+ />
+ </Form.Item>
+
+ {/* 描述 */}
+ <Form.Item
+ name="description"
+ label={<span style={{ fontSize: '19px' , fontWeight: 'bold'}}>描述</span>}
+ >
+ <TextArea
+ placeholder="输入种子描述"
+ value={description}
+ onChange={(e) => {
+ setDescription(e.target.value);
+ form.setFieldsValue({ description: e.target.value });
+ }}
+ rows={3}
+ style={styles.textArea}
+ />
+ </Form.Item>
+
+ {/* 分类 - 美化下拉框 */}
+ <Form.Item
+ name="categoryId"
+ label={<span style={{ fontSize: '19px' , fontWeight: 'bold' }}>分类</span>}
+ rules={[{ required: true, message: '请选择分类' }]}
+ >
+ <Select
+ placeholder="请选择分类"
+ value={categoryId}
+ onChange={(value) => {
+ const stringValue = String(value);
+ setCategoryId(stringValue);
+ console.log('Selected category ID:', stringValue);
+ form.setFieldsValue({ categoryId: value });
+ // 触发分类变化后的逻辑
+ setShowMovieFields(value === '1');
+ setShowTvFields(value === '2');
+ setShowAnimeFields(value === '4');
+ setShowGameFields(value === '5');
+ setShowvarietyFields(value === '6');
+ setShowMusicFields(value === '3');
+ setShowlearningFields(value === '9');
+ setShowsoftwareFields(value === '8');
+ setShowsportsFields(value === '7');
+ setShowdocFields(value === '10');
+ setShowotherFields(value === '11');
+ }}
+ style={styles.select}
+ >
+ <Option value="">请选择分类</Option>
+ {categories.map(cat => (
+ <Option key={cat.categoryid} value={cat.categoryid}>{cat.category_name}</Option>
+ ))}
+ </Select>
+ </Form.Item>
+
+ {/* 动态字段 - 电影 */}
+ {showMovieFields && (
+ <>
+ <Form.Item
+ name="caption"
+ label="字幕/说明"
+ rules={[{ required: true, message: '请输入字幕/说明' }]}
+ >
+ <Input
+ placeholder="输入字幕/说明"
+ value={caption}
+ onChange={(e) => {
+ setCaption(e.target.value);
+ form.setFieldsValue({ caption: e.target.value });
+ }}
+ className="text-center"
+ />
+ </Form.Item>
+ <Form.Item
+ name="region"
+ label="地区"
+ >
+ <Select
+ placeholder="请选择地区"
+ value={region}
+ onChange={(value) => {
+ setRegion(value);
+ form.setFieldsValue({ region: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择地区</Option>
+ {regions.map((region) => (
+ <Option key={region.value} value={region.value}>
+ {region.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="year"
+ label="年份"
+ >
+ <Input
+ placeholder="输入年份"
+ value={year}
+ onChange={(e) => {
+ setYear(e.target.value);
+ form.setFieldsValue({ year: e.target.value });
+ }}
+ className="text-center"
+ />
+ </Form.Item>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {genres.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="codecFormat"
+ label="编码格式"
+ >
+ <Select
+ placeholder="请选择编码格式"
+ value={codecFormat}
+ onChange={(value) => {
+ setCodecFormat(value);
+ form.setFieldsValue({ codecFormat: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择编码格式</Option>
+ {codecFormats.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="resolution"
+ label="分辨率"
+ >
+ <Select
+ placeholder="请选择分辨率"
+ value={resolution}
+ onChange={(value) => {
+ setResolution(value);
+ form.setFieldsValue({ resolution: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择分辨率</Option>
+ {resolutions.map((resolution) => (
+ <Option key={resolution.value} value={resolution.value}>
+ {resolution.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 剧集 */}
+ {showTvFields && (
+ <>
+ <Form.Item
+ name="region"
+ label="地区"
+ >
+ <Select
+ placeholder="请选择地区"
+ value={region}
+ onChange={(value) => {
+ setRegion(value);
+ form.setFieldsValue({ region: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择地区</Option>
+ {regions.map((region) => (
+ <Option key={region.value} value={region.value}>
+ {region.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {resolutions.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {genres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 游戏 */}
+ {showGameFields && (
+ <>
+ <Form.Item
+ name="platform"
+ label="平台"
+ >
+ <Select
+ placeholder="请选择平台"
+ value={platform}
+ onChange={(value) => {
+ setPlatform(value);
+ form.setFieldsValue({ platform: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择平台</Option>
+ {platforms.map((platform) => (
+ <Option key={platform.value} value={platform.value}>
+ {platform.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {gamegenres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="language"
+ label="语言"
+ >
+ <Select
+ placeholder="请选择语言"
+ value={language}
+ onChange={(value) => {
+ setLanguage(value);
+ form.setFieldsValue({ language: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择语言</Option>
+ {languages.map((language) => (
+ <Option key={language.value} value={language.value}>
+ {language.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="dataType"
+ label="数据类型"
+ >
+ <Select
+ placeholder="请选择数据类型"
+ value={dataType}
+ onChange={(value) => {
+ setDataType(value);
+ form.setFieldsValue({ dataType: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择数据类型</Option>
+ {dataTypes.map((dataType) => (
+ <Option key={dataType.value} value={dataType.value}>
+ {dataType.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 综艺 */}
+ {showvarietyFields && (
+ <>
+ <Form.Item
+ name="isMainland"
+ label="是否大陆综艺"
+ valuePropName="checked"
+ >
+ <Switch
+ checked={isMainland}
+ onChange={(checked) => {
+ setIsMainland(checked);
+ form.setFieldsValue({ isMainland: checked });
+ }}
+ />
+ </Form.Item>
+ <Form.Item
+ name="style"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={style}
+ onChange={(value) => {
+ setStyle(value);
+ form.setFieldsValue({ style: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {varietygenres.map((style) => (
+ <Option key={style.value} value={style.value}>
+ {style.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {resolutions.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 动漫 */}
+ {showAnimeFields && (
+ <>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {anigenres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {animeformats.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="resolution"
+ label="分辨率"
+ >
+ <Select
+ placeholder="请选择分辨率"
+ value={resolution}
+ onChange={(value) => {
+ setResolution(value);
+ form.setFieldsValue({ resolution: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择分辨率</Option>
+ {resolutions.map((resolution) => (
+ <Option key={resolution.value} value={resolution.value}>
+ {resolution.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 学习 */}
+ {showlearningFields && (
+ <>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {learninggenres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {learningformats.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 软件 */}
+ {showsoftwareFields && (
+ <>
+ <Form.Item
+ name="platform"
+ label="平台"
+ >
+ <Select
+ placeholder="请选择平台"
+ value={platform}
+ onChange={(value) => {
+ setPlatform(value);
+ form.setFieldsValue({ platform: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择平台</Option>
+ {softwareplatforms.map((platform) => (
+ <Option key={platform.value} value={platform.value}>
+ {platform.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {softwaregenres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {softwareformats.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 体育 */}
+ {showsportsFields && (
+ <>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {sportsgenres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {resolutions.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="eventType"
+ label="赛事类型"
+ >
+ <Select
+ placeholder="请选择赛事类型"
+ value={eventType}
+ onChange={(value) => {
+ setEventType(value);
+ form.setFieldsValue({ eventType: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择赛事类型</Option>
+ {eventTypes.map((eventType) => (
+ <Option key={eventType.value} value={eventType.value}>
+ {eventType.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 纪录片 */}
+ {showdocFields && (
+ <>
+ <Form.Item
+ name="year"
+ label="年份"
+ >
+ <Input
+ placeholder="如 1999, 2020"
+ value={year}
+ onChange={(e) => {
+ setYear(e.target.value);
+ form.setFieldsValue({ year: e.target.value });
+ }}
+ className="w-full"
+ />
+ </Form.Item>
+ <Form.Item
+ name="source"
+ label="视频源"
+ >
+ <Select
+ placeholder="请选择视频源"
+ value={source}
+ onChange={(value) => {
+ setSource(value);
+ form.setFieldsValue({ source: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择视频源</Option>
+ {sourceTypes.map((source) => (
+ <Option key={source.value} value={source.value}>
+ {source.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {resolutions.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 音乐 */}
+ {showMusicFields && (
+ <>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {musicgenres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="region"
+ label="地区"
+ >
+ <Select
+ placeholder="请选择地区"
+ value={region}
+ onChange={(value) => {
+ setRegion(value);
+ form.setFieldsValue({ region: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择地区</Option>
+ {regions.map((region) => (
+ <Option key={region.value} value={region.value}>
+ {region.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="style"
+ label="风格"
+ >
+ <Select
+ placeholder="请选择风格"
+ value={style}
+ onChange={(value) => {
+ setStyle(value);
+ form.setFieldsValue({ style: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择风格</Option>
+ {musicstyles.map((style) => (
+ <Option key={style.value} value={style.value}>
+ {style.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {musicformats.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 其他 */}
+ {showotherFields && (
+ <>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {othergenres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+ {/* 提交按钮 */}
+ <Form.Item>
+ <Button
+ type="primary"
+ htmlType="submit"
+ className="submitButton"
+ onMouseOver={(e) => e.target.style.backgroundColor = '#ff7f00'}
+ onMouseOut={(e) => e.target.style.backgroundColor = '#ff8c00'}
+ >
+ 上传种子
+ </Button>
+ </Form.Item>
+
+ {showSuccess && (
+ <div className="successMessage">
+ 上传成功!
+ </div>
+ )}
+ </Form>
+ </div>
+ </div>
+ );
+}
+
+export default UploadTorrentFull;
+
diff --git a/src/components/upload.jsx b/src/components/upload.jsx
new file mode 100644
index 0000000..c297fa8
--- /dev/null
+++ b/src/components/upload.jsx
@@ -0,0 +1,2002 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import "../upload.css";
+import "../uploadtorrent.css";
+import { Form, Input, Select, Button, Typography, Space, message,Switch } from 'antd';
+import { InboxOutlined } from '@ant-design/icons';
+const { TextArea } = Input;
+const { Text } = Typography;
+import Navbar from './Navbar';
+
+
+function UploadTorrent() {
+ const [userid,setuserid] = useState(1);
+ const [title, setTitle] = useState('');
+ const [description, setDescription] = useState('');
+ const [categoryId, setCategoryId] = useState('');
+ const [file, setFile] = useState(null);
+ const [categories, setCategories] = useState([]);
+ const [showSuccess, setShowSuccess] = useState(false);
+ const [form] = Form.useForm();
+
+ // 通用参数
+ const [dpi, setDpi] = useState('');
+ const [caption, setCaption] = useState('');
+ const [region, setRegion] = useState('');
+ const [year, setYear] = useState('');
+ const [genre, setGenre] = useState('');
+ const [format, setFormat] = useState('');
+ const [resolution, setResolution] = useState('');
+ const [coverImage, setCoverImage] = useState(null);
+
+ // 特殊参数
+ const [codecFormat, setCodecFormat] = useState('');
+ const [platform, setPlatform] = useState('');
+ const [language, setLanguage] = useState('');
+ const [eventType, setEventType] = useState('');
+ const [source, setSource] = useState('');
+ const [style, setStyle] = useState('');
+ const [dataType, setdataType] = useState('');
+ const [isMainland, setIsMainland] = useState(false);
+
+ // 根据分类显示不同的表单字段
+ const [showMovieFields, setShowMovieFields] = useState(false);
+ const [showMusicFields, setShowMusicFields] = useState(false);
+ const [showGameFields, setShowGameFields] = useState(false);
+ const [showTvFields, setShowTvFields] = useState(false);
+ const [showAnimeFields, setShowAnimeFields] = useState(false);
+ const [showlearningFields, setShowlearningFields] = useState(false);
+ const [showsoftwareFields, setShowsoftwareFields] = useState(false);
+ const [showvarietyFields, setShowvarietyFields] = useState(false);
+ const [showsportsFields, setShowsportsFields] = useState(false);
+ const [showdocFields, setShowdocFields] = useState(false);
+ const [showotherFields, setShowotherFields] = useState(false);
+ // 其他分类字段...
+ // 编码格式选项
+ const codecFormats = [
+ { value: 'H.264', label: 'H.264' },
+ { value: 'H.265', label: 'H.265' },
+ { value: 'AV1', label: 'AV1' },
+ { value: 'VP9', label: 'VP9' },
+ { value: 'VC1', label: 'VC1' },
+ { value: 'X264', label: 'X264' },
+ ];
+ const regions = [
+ { value: '大陆', label: '大陆' },
+ { value: '港台', label: '港台' },
+ { value: '欧美', label: '欧美' },
+ { value: '日韩', label: '日韩' },
+ { value: '其他', label: '其他' },
+ ];
+ const genres = [
+ { value: '动作', label: '动作' },
+ { value: '喜剧', label: '喜剧' },
+ { value: '爱情', label: '爱情' },
+ { value: '科幻', label: '科幻' },
+ { value: '恐怖', label: '恐怖' },
+ { value: '动作', label: '动作' },
+ { value: '冒险', label: '冒险' },
+ { value: '历史', label: '历史' },
+ { value: '悬疑', label: '悬疑' },
+ { value: '其他', label: '其他' },
+ ];
+ const resolutions = [
+ { value: '720p', label: '720p' },
+ { value: '1080p', label: '1080p' },
+ { value: '2K', label: '2K' },
+ { value: '4K', label: '4K' },
+ { value: '8K', label: '8K' },
+ { value: '其他', label: '其他' },
+ ];
+
+
+ const eventTypes = [
+ { value: '足球', label: '足球' },
+ { value: '篮球', label: '篮球' },
+ { value: '网球', label: '网球' },
+ { value: '乒乓球', label: '乒乓球' },
+ { value: '羽毛球', label: '羽毛球' },
+ ]
+ const styles = [
+ { value: '大陆综艺', label: '大陆综艺' },
+ { value: '日韩综艺', label: '日韩综艺' },
+ { value: '欧美综艺', label: '欧美综艺' },
+ { value: '其他', label: '其他' },
+ ]
+ const platforms = [
+ { value: 'PC', label: 'PC' },
+ { value: 'PS5', label: 'PS5' },
+ { value: 'Xbox', label: 'Xbox' },
+ { value: 'Switch', label: 'Switch' },
+ { value: '手机', label: '手机' },
+ { value: '其他', label: '其他' },
+ ]
+ const gamegenres = [
+ { value: '角色扮演', label: '角色扮演' },
+ { value: '射击', label: '射击' },
+ { value: '冒险', label: '冒险' },
+ { value: '策略', label: '策略' },
+ { value: '体育', label: '体育' },
+ { value: '桌面游戏', label: '桌面游戏'},
+ { value: '其他', label: '其他' },
+ ]
+ const dataTypes = [
+ { value: '压缩包', label: '压缩包' },
+ { value: '补丁', label: '补丁' },
+ { value: '安装包', label: '安装包' },
+ { value: 'nds', label: 'nds' },
+ { value: '其他', label: '其他' },
+ ]
+ const languages = [
+ { value: '中文', label: '中文' },
+ { value: '英文', label: '英文' },
+ { value: '日文', label: '日文' },
+ { value: '其他', label: '其他' },
+ ]
+ const musicgenres = [
+ { value: '专辑', label: '专辑' },
+ { value: '单曲', label: '单曲' },
+ { value: 'EP', label: 'EP' },
+ { value: '现场', label: '现场' },
+ { value: '其他', label: '其他' },
+ ]
+ const musicstyles = [
+ { value: '流行', label: '流行' },
+ { value: '摇滚', label: '摇滚' },
+ { value: '电子', label: '电子' },
+ { value: '古典', label: '古典' },
+ { value: '爵士', label: '爵士' },
+ { value: '民谣', label: '民谣' },
+ { value: '说唱', label: '说唱' },
+ { value: '其他', label: '其他' },
+ ]
+ const musicformats = [
+ { value: 'MP3', label: 'MP3' },
+ { value: 'FLAC', label: 'FLAC' },
+ { value: 'WAV', label: 'WAV' },
+ { value: 'AAC', label: 'AAC' },
+ { value: 'OGG', label: 'OGG' },
+ { value: '其他', label: '其他' },
+ ]
+ const anigenres = [
+ { value: '新番连载', label: '新番连载' },
+ { value: '剧场版', label: '剧场版' },
+ { value: 'OVA', label: 'OVA' },
+ { value: '完结动漫', label: '完结动漫' },
+ { value: '其他', label: '其他' },
+ ]
+ const animeformats = [
+ { value: 'ZIP', label: 'ZIP' },
+ { value: 'RAR', label: 'RAR' },
+ { value: '7Z', label: '7Z' },
+ { value: 'MKV', label: 'MKV' },
+ { value: 'MP4', label: 'MP4' },
+ { value: '其他', label: '其他' },
+ ]
+ const varietygenres = [
+ { value: '真人秀', label: '真人秀' },
+ { value: '选秀', label: '选秀' },
+ { value: '访谈', label: '访谈' },
+ { value: '游戏', label: '游戏' },
+ { value: '音乐', label: '音乐' },
+ { value: '其他', label: '其他' },
+ ]
+ const sportsgenres = [
+ { value: '足球', label: '足球' },
+ { value: '篮球', label: '篮球' },
+ { value: '网球', label: '网球' },
+ { value: '乒乓球', label: '乒乓球' },
+ { value: '羽毛球', label: '羽毛球' },
+ { value: '其他', label: '其他' },
+ ]
+ const softwaregenres = [
+ { value: '系统软件', label: '系统软件' },
+ { value: '应用软件', label: '应用软件' },
+ { value: '游戏软件', label: '游戏软件' },
+ { value: '驱动程序', label: '驱动程序' },
+ { value: '办公软件', label: '办公软件' },
+ { value: '其他', label: '其他' },
+ ]
+ const softwareplatforms = [
+ { value: 'Windows', label: 'Windows' },
+ { value: 'Mac', label: 'Mac' },
+ { value: 'Linux', label: 'Linux' },
+ { value: 'Android', label: 'Android' },
+ { value: 'iOS', label: 'iOS' },
+ { value: '其他', label: '其他' },
+ ]
+ const softwareformats = [
+ { value: 'EXE', label: 'EXE' },
+ { value: 'DMG', label: 'DMG' },
+ { value: '光盘镜像', label: '光盘镜像' },
+ { value: 'APK', label: 'APK' },
+ { value: 'IPA', label: 'IPA' },
+ { value: '其他', label: '其他' },
+ ]
+ const learninggenres = [
+ { value: '计算机', label: '计算机' },
+ { value: '软件', label: '软件' },
+ { value: '人文', label: '人文' },
+ { value: '外语', label: '外语' },
+ { value: '理工类', label: '理工类' },
+ { value: '其他', label: '其他' },
+ ]
+ const learningformats = [
+ { value: 'PDF', label: 'PDF' },
+ { value: 'EPUB', label: 'EPUB' },
+ { value: '视频', label: '视频' },
+ { value: '音频', label: '音频' },
+ { value: 'PPT', label: 'PPT' },
+ { value: '其他', label: '其他' },
+ ]
+ const sourceTypes = [
+ { value: 'CCTV', label: 'CCTV' },
+ { value: '卫视', label: '卫视' },
+ { value: '国家地理', label: '国家地理' },
+ { value: 'BBC', label: 'BBC' },
+ { value: 'Discovery', label: 'Discovery' },
+ { value: '其他', label: '其他' },
+ ]
+ const othergenres = [
+ { value: '电子书', label: '电子书' },
+ { value: '视频', label: '视频' },
+ { value: 'MP3', label: 'MP3' },
+ { value: '图片', label: '图片' },
+ { value: '其他', label: '其他' },
+ ]
+ useEffect(() => {
+ axios.get('http://localhost:8080/categories')
+ .then(res => setCategories(res.data))
+ .catch(err => console.error('加载分类失败', err));
+ }, []);
+
+ // 根据选择的分类显示不同的表单字段
+ useEffect(() => {
+ setShowMovieFields(categoryId === '1');
+ setShowMusicFields(categoryId === '3');
+ setShowGameFields(categoryId === '5');
+ setShowTvFields(categoryId === '2');
+ setShowAnimeFields(categoryId === '4');
+ setShowlearningFields(categoryId === '9');
+ setShowsoftwareFields(categoryId === '8');
+ setShowvarietyFields(categoryId === '6');
+ setShowsportsFields(categoryId === '7');
+ setShowdocFields(categoryId === '10');
+ setShowotherFields(categoryId === '11');
+ // 其他分类...
+ }, [categoryId]);
+
+
+
+ const handleSubmit = async (e) => {
+ //e.preventDefault();
+ if (!coverImage) {
+ alert('请选择一个 图片 文件');
+ return;
+ }
+ if (!file) {
+ alert('请选择一个 .torrent 文件');
+ return;
+ }
+
+ if (!categoryId) {
+ alert('请选择分类');
+ return;
+ }
+
+
+ const formData = new FormData();
+ formData.append('userid', userid);
+ formData.append('file', file);
+ // Add cover image if it exists
+ formData.append('coverImage', coverImage);
+ formData.append('title', title);
+ formData.append('description', description);
+ formData.append('categoryId', categoryId);
+
+
+ // 通用参数
+ if (dpi) formData.append('dpi', dpi);
+ if (caption) formData.append('caption', caption);
+ if (region) formData.append('region', region);
+ if (year) formData.append('year', year);
+ if (genre) formData.append('genre', genre);
+ if (format) formData.append('format', format);
+ if (resolution) formData.append('resolution', resolution);
+
+ // 特殊参数
+ if (codecFormat) formData.append('codecFormat', codecFormat);
+ if (platform) formData.append('platform', platform);
+ if (language) formData.append('language', language);
+ if (eventType) formData.append('eventType', eventType);
+ if (source) formData.append('source', source);
+ if (style) formData.append('style', style);
+ if (dataType) formData.append('dataType', dataType);
+ formData.append('isMainland', isMainland.toString());
+
+ try {
+ const response = await axios.post('http://localhost:8080/torrent/upload', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ responseType: 'blob',
+ });
+
+ // 创建下载链接
+ const url = window.URL.createObjectURL(new Blob([response.data]));
+ const link = document.createElement('a');
+ link.href = url;
+ link.setAttribute('download', file.name);
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+
+ // 显示成功提示
+ setShowSuccess(true);
+ // 清空表单
+ setTitle('');
+ setDescription('');
+ setCategoryId('');
+ setFile(null);
+ // 清空其他字段...
+ } catch (err) {
+ console.error('上传失败', err.response?.data || err.message);
+ alert(err.response?.data || '上传失败,请检查后端是否启动');
+ }
+ };
+
+// return (
+// <div className="max-w-2xl mx-auto mt-10 p-6 bg-white shadow rounded">
+// <h2 className="text-2xl font-bold mb-6 text-orange-800 text-center border-b-2 border-orange-200 pb-3">上传种子</h2>
+// <form onSubmit={handleSubmit} className="space-y-6">
+// {/* 封面图片 - 大型上传区域 */}
+// <div className="form-group text-center">
+// <label className="upload-area text-orange-800 font-medium cursor-pointer p-8 border-2 border-dashed border-orange-300 rounded-lg hover:border-orange-400 transition-colors">
+// <svg xmlns="http://www.w3.org/2000/svg" className="mx-auto h-12 w-12 text-orange-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+// <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
+// </svg>
+// <p className="mt-2 text-sm">点击或拖拽上传封面图片</p>
+// <input
+// type="file"
+// accept="image/*"
+// onChange={(e) => setCoverImage(e.target.files[0])}
+// className="hidden"
+// id="cover-image-upload"
+// />
+// </label>
+// <input
+// type="file"
+// accept="image/*"
+// onChange={(e) => setCoverImage(e.target.files[0])}
+// className="sr-only"
+// id="cover-image-upload-real"
+// />
+// </div>
+
+// {/* 种子文件 - 大型上传区域 */}
+// <div className="form-group text-center">
+// <label className="upload-area text-orange-800 font-medium cursor-pointer p-8 border-2 border-dashed border-orange-300 rounded-lg hover:border-orange-400 transition-colors">
+// <svg xmlns="http://www.w3.org/2000/svg" className="mx-auto h-12 w-12 text-orange-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+// <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
+// </svg>
+// <p className="mt-2 text-sm">点击或拖拽上传种子文件(.torrent)</p>
+// <input
+// type="file"
+// accept=".torrent"
+// onChange={(e) => setFile(e.target.files[0])}
+// className="hidden"
+// id="torrent-file-upload"
+// required
+// />
+// </label>
+// <input
+// type="file"
+// accept=".torrent"
+// onChange={(e) => setFile(e.target.files[0])}
+// className="sr-only"
+// id="torrent-file-upload-real"
+// required
+// />
+// </div>
+
+// {/* 标题 */}
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">标题</label>
+// <input
+// type="text"
+// placeholder="输入种子标题"
+// value={title}
+// onChange={(e) => setTitle(e.target.value)}
+// className="form-control text-center"
+// required
+// />
+// </div>
+
+// {/* 描述 */}
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">描述</label>
+// <textarea
+// placeholder="输入种子描述"
+// value={description}
+// onChange={(e) => setDescription(e.target.value)}
+// className="form-control text-center"
+// rows={3}
+// />
+// </div>
+
+// {/* 分类 - 美化下拉框 */}
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">分类</label>
+// <div className="relative">
+// <select
+// value={categoryId}
+// onChange={(e) => setCategoryId(e.target.value)}
+// className="custom-select"
+// required
+// >
+// <option value="">请选择分类</option>
+// {categories.map(cat => (
+// <option key={cat.categoryid} value={cat.categoryid}>{cat.category_name}</option>
+// ))}
+// </select>
+// <div className="select-arrow">
+// <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-orange-500" viewBox="0 0 20 20" fill="currentColor">
+// <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
+// </svg>
+// </div>
+// </div>
+// </div>
+
+// {/* 动态字段 - 电影 */}
+// {showMovieFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">字幕/说明</label>
+// <input
+// type="text"
+// placeholder="输入字幕/说明"
+// value={caption}
+// onChange={(e) => setCaption(e.target.value)}
+// className="form-control text-center"
+// />
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">地区</label>
+// <div className="relative">
+// <select
+// value={region}
+// onChange={(e) => setRegion(e.target.value)}
+// className="custom-select"
+// >
+// <option value="">请选择地区</option>
+// {regions.map((region) => (
+// <option key={region.value} value={region.value}>
+// {region.label}
+// </option>
+// ))}
+// </select>
+// <div className="select-arrow">
+// <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-orange-500" viewBox="0 0 20 20" fill="currentColor">
+// <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
+// </svg>
+// </div>
+// </div>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">年份</label>
+// <input
+// type="text"
+// placeholder="输入年份"
+// value={year}
+// onChange={(e) => setYear(e.target.value)}
+// className="form-control text-center"
+// />
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">类型</label>
+// <div className="relative">
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="custom-select"
+// >
+// <option value="">请选择类型</option>
+// {genres.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// <div className="select-arrow">
+// <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-orange-500" viewBox="0 0 20 20" fill="currentColor">
+// <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
+// </svg>
+// </div>
+// </div>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">编码格式</label>
+// <div className="relative">
+// <select
+// value={codecFormat}
+// onChange={(e) => setCodecFormat(e.target.value)}
+// className="custom-select"
+// >
+// <option value="">请选择编码格式</option>
+// {codecFormats.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// <div className="select-arrow">
+// <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-orange-500" viewBox="0 0 20 20" fill="currentColor">
+// <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
+// </svg>
+// </div>
+// </div>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800 text-center">分辨率</label>
+// <div className="relative">
+// <select
+// value={resolution}
+// onChange={(e) => setResolution(e.target.value)}
+// className="custom-select"
+// >
+// <option value="">请选择分辨率</option>
+// {resolutions.map((resolution) => (
+// <option key={resolution.value} value={resolution.value}>
+// {resolution.label}
+// </option>
+// ))}
+// </select>
+// <div className="select-arrow">
+// <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-orange-500" viewBox="0 0 20 20" fill="currentColor">
+// <path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
+// </svg>
+// </div>
+// </div>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 剧集 */}
+// {showTvFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">地区</label>
+// <select
+// value={region}
+// onChange={(e) => setRegion(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择地区</option>
+// {regions.map((region) => (
+// <option key={region.value} value={region.value}>
+// {region.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {resolutions.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {genres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 游戏 */}
+// {showGameFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">平台</label>
+// <select
+// value={platform}
+// onChange={(e) => setPlatform(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择平台</option>
+// {platforms.map((platform) => (
+// <option key={platform.value} value={platform.value}>
+// {platform.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {gamegenres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">语言</label>
+// <select
+// value={language}
+// onChange={(e) => setLanguage(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择语言</option>
+// {languages.map((language) => (
+// <option key={language.value} value={language.value}>
+// {language.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">数据类型</label>
+// <select
+// value={dataType}
+// onChange={(e) => setdataType(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择数据类型</option>
+// {dataTypes.map((dataType) => (
+// <option key={dataType.value} value={dataType.value}>
+// {dataType.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 综艺 */}
+// {showvarietyFields && (
+// <>
+// <div className="form-group">
+// <label className="flex items-center">
+// <input
+// type="checkbox"
+// checked={isMainland}
+// onChange={(e) => setIsMainland(e.target.checked)}
+// className="mr-2"
+// />
+// 是否大陆综艺
+// </label>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={style}
+// onChange={(e) => setStyle(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {varietygenres.map((style) => (
+// <option key={style.value} value={style.value}>
+// {style.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {resolutions.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 动漫 */}
+// {showAnimeFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {anigenres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {animeformats.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">分辨率</label>
+// <select
+// value={resolution}
+// onChange={(e) => setResolution(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择分辨率</option>
+// {resolutions.map((resolution) => (
+// <option key={resolution.value} value={resolution.value}>
+// {resolution.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 学习 */}
+// {showlearningFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {learninggenres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {learningformats.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 软件 */}
+// {showsoftwareFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">平台</label>
+// <select
+// value={platform}
+// onChange={(e) => setPlatform(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择平台</option>
+// {softwareplatforms.map((platform) => (
+// <option key={platform.value} value={platform.value}>
+// {platform.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {softwaregenres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {softwareformats.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 体育 */}
+// {showsportsFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {sportsgenres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {resolutions.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">赛事类型</label>
+// <select
+// value={eventType}
+// onChange={(e) => setEventType(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择赛事类型</option>
+// {eventTypes.map((eventType) => (
+// <option key={eventType.value} value={eventType.value}>
+// {eventType.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 纪录片 */}
+// {showdocFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">年份</label>
+// <input
+// type="text"
+// placeholder="如 1999, 2020"
+// value={year}
+// onChange={(e) => setYear(e.target.value)}
+// className="form-control"
+// />
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">视频源</label>
+// <select
+// value={source}
+// onChange={(e) => setSource(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择视频源</option>
+// {sourceTypes.map((source) => (
+// <option key={source.value} value={source.value}>
+// {source.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {resolutions.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 音乐 */}
+// {showMusicFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {musicgenres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">地区</label>
+// <select
+// value={region}
+// onChange={(e) => setRegion(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择地区</option>
+// {regions.map((region) => (
+// <option key={region.value} value={region.value}>
+// {region.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">风格</label>
+// <select
+// value={style}
+// onChange={(e) => setStyle(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择风格</option>
+// {musicstyles.map((style) => (
+// <option key={style.value} value={style.value}>
+// {style.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// <div className="form-group">
+// <label className="form-label text-orange-800">格式</label>
+// <select
+// value={format}
+// onChange={(e) => setFormat(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择格式</option>
+// {musicformats.map((format) => (
+// <option key={format.value} value={format.value}>
+// {format.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+// {/* 动态字段 - 其他 */}
+// {showotherFields && (
+// <>
+// <div className="form-group">
+// <label className="form-label text-orange-800">类型</label>
+// <select
+// value={genre}
+// onChange={(e) => setGenre(e.target.value)}
+// className="form-control"
+// >
+// <option value="">请选择类型</option>
+// {othergenres.map((genre) => (
+// <option key={genre.value} value={genre.value}>
+// {genre.label}
+// </option>
+// ))}
+// </select>
+// </div>
+// </>
+// )}
+
+
+// {/* 其他动态字段... (保持相同的美化样式) */}
+// {/* 这里省略了其他动态字段的代码,但它们都使用相同的自定义下拉框样式 */}
+
+// {/* 提交按钮 - 居中 */}
+// <div className="text-center">
+// <button
+// type="submit"
+// className="submit-btn"
+// >
+// 上传种子
+// </button>
+// </div>
+
+// {showSuccess && (
+// <div className="success-message text-center">
+// 上传成功!
+// </div>
+// )}
+// </form>
+// </div>
+// );
+// }
+return (
+ <div>
+ <Navbar style={{ width: '100%' }}/>
+ <div className="container">
+ <div className="card">
+ <h2 className="title">上传种子</h2>
+ <Form
+ layout="vertical"
+ onFinish={handleSubmit}
+ className="form"
+ >
+ {/* 封面图片 - 自定义上传区域 */}
+<Form.Item
+ name="coverImage"
+ label={<span style={{ fontSize: '19px' , fontWeight: 'bold'}}>封面图片</span>}
+ rules={[{ required: true, message: '请上传封面图片' }]}
+>
+ <div
+ className="uploadArea"
+ onClick={() => document.getElementById('cover-image-upload').click()}
+ >
+ <InboxOutlined className="uploadIcon" />
+ <p className="uploadText">点击或拖拽上传封面图片</p>
+ <input
+ type="file"
+ accept="image/*"
+ onChange={(e) => {
+ setCoverImage(e.target.files[0]);
+ form.setFieldsValue({ coverImage: e.target.files[0] });
+ }}
+ className="hiddenInput"
+ id="cover-image-upload"
+ />
+ </div>
+</Form.Item>
+
+{/* 种子文件 - 自定义上传区域 */}
+<Form.Item
+ name="torrentFile"
+ label={<span style={{ fontSize: '19px' , fontWeight: 'bold'}}>种子文件</span>}
+ rules={[{ required: true, message: '请上传.torrent文件' }]}
+>
+ <div
+ className="uploadArea"
+ onClick={() => document.getElementById('torrent-file-upload').click()}
+ >
+ <InboxOutlined className="uploadIcon" />
+ <p className="uploadText">点击或拖拽上传种子文件(.torrent)</p>
+ <input
+ type="file"
+ accept=".torrent"
+ onChange={(e) => {
+ setFile(e.target.files[0]);
+ form.setFieldsValue({ torrentFile: e.target.files[0] });
+ }}
+ className="hiddenInput"
+ id="torrent-file-upload"
+ />
+ </div>
+</Form.Item>
+
+ {/* 标题 */}
+ <Form.Item
+ name="title"
+ label={<span style={{ fontSize: '19px' , fontWeight: 'bold'}}>标题</span>} // 直接包裹label
+ rules={[{ required: true, message: '请输入标题' }]}
+ //labelCol={{ style: { fontSize: '30px !important ' } }} // ✅ 调整 Label 字体
+ >
+ <Input
+ placeholder="输入种子标题"
+ value={title}
+ onChange={(e) => {
+ setTitle(e.target.value);
+ form.setFieldsValue({ title: e.target.value });
+ }}
+ //style={styles.input}
+ style={{ fontSize: '19px' }} // ✅ 调整 Input 字体
+ />
+ </Form.Item>
+
+ {/* 描述 */}
+ <Form.Item
+ name="description"
+ label={<span style={{ fontSize: '19px' , fontWeight: 'bold'}}>描述</span>}
+ >
+ <TextArea
+ placeholder="输入种子描述"
+ value={description}
+ onChange={(e) => {
+ setDescription(e.target.value);
+ form.setFieldsValue({ description: e.target.value });
+ }}
+ rows={3}
+ style={styles.textArea}
+ />
+ </Form.Item>
+
+ {/* 分类 - 美化下拉框 */}
+ <Form.Item
+ name="categoryId"
+ label={<span style={{ fontSize: '19px' , fontWeight: 'bold' }}>分类</span>}
+ rules={[{ required: true, message: '请选择分类' }]}
+ >
+ <Select
+ placeholder="请选择分类"
+ value={categoryId}
+ onChange={(value) => {
+ const stringValue = String(value);
+ setCategoryId(stringValue);
+ console.log('Selected category ID:', stringValue);
+ form.setFieldsValue({ categoryId: value });
+ // 触发分类变化后的逻辑
+ setShowMovieFields(value === '1');
+ setShowTvFields(value === '2');
+ setShowAnimeFields(value === '4');
+ setShowGameFields(value === '5');
+ setShowvarietyFields(value === '6');
+ setShowMusicFields(value === '3');
+ setShowlearningFields(value === '9');
+ setShowsoftwareFields(value === '8');
+ setShowsportsFields(value === '7');
+ setShowdocFields(value === '10');
+ setShowotherFields(value === '11');
+ }}
+ style={styles.select}
+ >
+ <Option value="">请选择分类</Option>
+ {categories.map(cat => (
+ <Option key={cat.categoryid} value={cat.categoryid}>{cat.category_name}</Option>
+ ))}
+ </Select>
+ </Form.Item>
+
+ {/* 动态字段 - 电影 */}
+ {showMovieFields && (
+ <>
+ <Form.Item
+ name="caption"
+ label="字幕/说明"
+ rules={[{ required: true, message: '请输入字幕/说明' }]}
+ >
+ <Input
+ placeholder="输入字幕/说明"
+ value={caption}
+ onChange={(e) => {
+ setCaption(e.target.value);
+ form.setFieldsValue({ caption: e.target.value });
+ }}
+ className="text-center"
+ />
+ </Form.Item>
+ <Form.Item
+ name="region"
+ label="地区"
+ >
+ <Select
+ placeholder="请选择地区"
+ value={region}
+ onChange={(value) => {
+ setRegion(value);
+ form.setFieldsValue({ region: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择地区</Option>
+ {regions.map((region) => (
+ <Option key={region.value} value={region.value}>
+ {region.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="year"
+ label="年份"
+ >
+ <Input
+ placeholder="输入年份"
+ value={year}
+ onChange={(e) => {
+ setYear(e.target.value);
+ form.setFieldsValue({ year: e.target.value });
+ }}
+ className="text-center"
+ />
+ </Form.Item>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {genres.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="codecFormat"
+ label="编码格式"
+ >
+ <Select
+ placeholder="请选择编码格式"
+ value={codecFormat}
+ onChange={(value) => {
+ setCodecFormat(value);
+ form.setFieldsValue({ codecFormat: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择编码格式</Option>
+ {codecFormats.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="resolution"
+ label="分辨率"
+ >
+ <Select
+ placeholder="请选择分辨率"
+ value={resolution}
+ onChange={(value) => {
+ setResolution(value);
+ form.setFieldsValue({ resolution: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择分辨率</Option>
+ {resolutions.map((resolution) => (
+ <Option key={resolution.value} value={resolution.value}>
+ {resolution.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 剧集 */}
+ {showTvFields && (
+ <>
+ <Form.Item
+ name="region"
+ label="地区"
+ >
+ <Select
+ placeholder="请选择地区"
+ value={region}
+ onChange={(value) => {
+ setRegion(value);
+ form.setFieldsValue({ region: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择地区</Option>
+ {regions.map((region) => (
+ <Option key={region.value} value={region.value}>
+ {region.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {resolutions.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {genres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 游戏 */}
+ {showGameFields && (
+ <>
+ <Form.Item
+ name="platform"
+ label="平台"
+ >
+ <Select
+ placeholder="请选择平台"
+ value={platform}
+ onChange={(value) => {
+ setPlatform(value);
+ form.setFieldsValue({ platform: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择平台</Option>
+ {platforms.map((platform) => (
+ <Option key={platform.value} value={platform.value}>
+ {platform.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {gamegenres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="language"
+ label="语言"
+ >
+ <Select
+ placeholder="请选择语言"
+ value={language}
+ onChange={(value) => {
+ setLanguage(value);
+ form.setFieldsValue({ language: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择语言</Option>
+ {languages.map((language) => (
+ <Option key={language.value} value={language.value}>
+ {language.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="dataType"
+ label="数据类型"
+ >
+ <Select
+ placeholder="请选择数据类型"
+ value={dataType}
+ onChange={(value) => {
+ setDataType(value);
+ form.setFieldsValue({ dataType: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择数据类型</Option>
+ {dataTypes.map((dataType) => (
+ <Option key={dataType.value} value={dataType.value}>
+ {dataType.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 综艺 */}
+ {showvarietyFields && (
+ <>
+ <Form.Item
+ name="isMainland"
+ label="是否大陆综艺"
+ valuePropName="checked"
+ >
+ <Switch
+ checked={isMainland}
+ onChange={(checked) => {
+ setIsMainland(checked);
+ form.setFieldsValue({ isMainland: checked });
+ }}
+ />
+ </Form.Item>
+ <Form.Item
+ name="style"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={style}
+ onChange={(value) => {
+ setStyle(value);
+ form.setFieldsValue({ style: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {varietygenres.map((style) => (
+ <Option key={style.value} value={style.value}>
+ {style.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {resolutions.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 动漫 */}
+ {showAnimeFields && (
+ <>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {anigenres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {animeformats.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="resolution"
+ label="分辨率"
+ >
+ <Select
+ placeholder="请选择分辨率"
+ value={resolution}
+ onChange={(value) => {
+ setResolution(value);
+ form.setFieldsValue({ resolution: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择分辨率</Option>
+ {resolutions.map((resolution) => (
+ <Option key={resolution.value} value={resolution.value}>
+ {resolution.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 学习 */}
+ {showlearningFields && (
+ <>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {learninggenres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {learningformats.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 软件 */}
+ {showsoftwareFields && (
+ <>
+ <Form.Item
+ name="platform"
+ label="平台"
+ >
+ <Select
+ placeholder="请选择平台"
+ value={platform}
+ onChange={(value) => {
+ setPlatform(value);
+ form.setFieldsValue({ platform: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择平台</Option>
+ {softwareplatforms.map((platform) => (
+ <Option key={platform.value} value={platform.value}>
+ {platform.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {softwaregenres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {softwareformats.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 体育 */}
+ {showsportsFields && (
+ <>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {sportsgenres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {resolutions.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="eventType"
+ label="赛事类型"
+ >
+ <Select
+ placeholder="请选择赛事类型"
+ value={eventType}
+ onChange={(value) => {
+ setEventType(value);
+ form.setFieldsValue({ eventType: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择赛事类型</Option>
+ {eventTypes.map((eventType) => (
+ <Option key={eventType.value} value={eventType.value}>
+ {eventType.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 纪录片 */}
+ {showdocFields && (
+ <>
+ <Form.Item
+ name="year"
+ label="年份"
+ >
+ <Input
+ placeholder="如 1999, 2020"
+ value={year}
+ onChange={(e) => {
+ setYear(e.target.value);
+ form.setFieldsValue({ year: e.target.value });
+ }}
+ className="w-full"
+ />
+ </Form.Item>
+ <Form.Item
+ name="source"
+ label="视频源"
+ >
+ <Select
+ placeholder="请选择视频源"
+ value={source}
+ onChange={(value) => {
+ setSource(value);
+ form.setFieldsValue({ source: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择视频源</Option>
+ {sourceTypes.map((source) => (
+ <Option key={source.value} value={source.value}>
+ {source.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {resolutions.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 音乐 */}
+ {showMusicFields && (
+ <>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {musicgenres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="region"
+ label="地区"
+ >
+ <Select
+ placeholder="请选择地区"
+ value={region}
+ onChange={(value) => {
+ setRegion(value);
+ form.setFieldsValue({ region: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择地区</Option>
+ {regions.map((region) => (
+ <Option key={region.value} value={region.value}>
+ {region.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="style"
+ label="风格"
+ >
+ <Select
+ placeholder="请选择风格"
+ value={style}
+ onChange={(value) => {
+ setStyle(value);
+ form.setFieldsValue({ style: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择风格</Option>
+ {musicstyles.map((style) => (
+ <Option key={style.value} value={style.value}>
+ {style.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ <Form.Item
+ name="format"
+ label="格式"
+ >
+ <Select
+ placeholder="请选择格式"
+ value={format}
+ onChange={(value) => {
+ setFormat(value);
+ form.setFieldsValue({ format: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择格式</Option>
+ {musicformats.map((format) => (
+ <Option key={format.value} value={format.value}>
+ {format.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+
+ {/* 动态字段 - 其他 */}
+ {showotherFields && (
+ <>
+ <Form.Item
+ name="genre"
+ label="类型"
+ >
+ <Select
+ placeholder="请选择类型"
+ value={genre}
+ onChange={(value) => {
+ setGenre(value);
+ form.setFieldsValue({ genre: value });
+ }}
+ className="w-full"
+ >
+ <Option value="">请选择类型</Option>
+ {othergenres.map((genre) => (
+ <Option key={genre.value} value={genre.value}>
+ {genre.label}
+ </Option>
+ ))}
+ </Select>
+ </Form.Item>
+ </>
+ )}
+ {/* 提交按钮 */}
+ <Form.Item>
+ <Button
+ type="primary"
+ htmlType="submit"
+ className="submitButton"
+ onMouseOver={(e) => e.target.style.backgroundColor = '#ff7f00'}
+ onMouseOut={(e) => e.target.style.backgroundColor = '#ff8c00'}
+ >
+ 上传种子
+ </Button>
+ </Form.Item>
+
+ {showSuccess && (
+ <div className="successMessage">
+ 上传成功!
+ </div>
+ )}
+ </Form>
+ </div>
+ </div>
+ </div>
+ );
+}
+
+export default UploadTorrent;
+
diff --git a/src/introGuide.js b/src/introGuide.js
new file mode 100644
index 0000000..bcfe58e
--- /dev/null
+++ b/src/introGuide.js
@@ -0,0 +1,43 @@
+import introJs from 'intro.js';
+
+export const startIntroGuide = () => {
+ introJs().setOptions({
+ steps: [
+ {
+ intro: '欢迎加入我们,这是一个优质的种子资源分享社区。让我们快速了解一下主要功能,帮助您更好地使用我们的平台。'
+ },
+ {
+ element: document.querySelector('#torrent-activity'),
+ intro: '这里是首页,包括近期活动速递,资源推荐,您可以浏览最新活动,参加领取奖励,提升您的等级!'
+ },
+ {
+ element: document.querySelector('#community-posts'),
+ intro: '这里是社区交流中心,您可以发布帖子、评论互动、发布需求贴悬赏求助,还能查看我的帖子!'
+ },
+ {
+ element: document.querySelector('#torrent-list'),
+ intro: '这里是全站种子列表,您可以按分类浏览、搜索特定资源、查看详细信息和下载种子文件。所有优质资源都在这里!'
+ },
+ {
+ element: document.querySelector('#torrent-upload'),
+ intro: '这是快速上传种子入口,操作简单易上手,快来上传你的第一个资源!'
+ },
+ {
+ element: document.querySelector('#community-friends'),
+ intro: '这里是好友聊天区,您可以添加和管理好友、畅聊分享!'
+ },
+ {
+ element: document.querySelector('#torrent-shop'),
+ intro: '用您的积分兑换个人装饰、上传量和邀请码,提升使用体验,邀请更多好友。'
+ },
+ {
+ element: document.querySelector('#help-button'),
+ intro: '小贴士:您可以随时点击右上角的帮助按钮重新查看引导和查看文档。祝您使用愉快!'
+ }
+ ],
+ nextLabel: '下一步',
+ prevLabel: '上一步',
+ skipLabel: '跳过',
+ doneLabel: '完成'
+ }).start();
+};
diff --git a/src/main.jsx b/src/main.jsx
new file mode 100644
index 0000000..62c9250
--- /dev/null
+++ b/src/main.jsx
@@ -0,0 +1,11 @@
+// src/main.jsx 或 src/index.jsx
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+import App from './App';
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+ <BrowserRouter>
+ <App />
+ </BrowserRouter>
+);
diff --git a/src/pages/HelpPage.css b/src/pages/HelpPage.css
new file mode 100644
index 0000000..6809108
--- /dev/null
+++ b/src/pages/HelpPage.css
@@ -0,0 +1,78 @@
+.help-page-wrapper {
+ min-height: 100vh;
+ background-color: #fff7e6; /* 淡橙色背景 */
+ padding: 3rem 1rem; /* 上下3rem,左右1rem */
+ box-sizing: border-box;
+}
+
+.help-container {
+ max-width: 960px; /* 大约是4xl宽度 */
+ margin: 0 auto;
+ background: white;
+ border: 4px solid black;
+ border-radius: 0.5rem;
+ box-shadow: 0 0 10px rgb(0 0 0 / 0.1);
+ padding: 2rem;
+ box-sizing: border-box;
+}
+
+.help-title {
+ font-size: 2rem;
+ font-weight: 700;
+ text-align: center;
+ margin-bottom: 1.5rem;
+ border-bottom: 2px solid black;
+ padding-bottom: 1rem;
+}
+
+.help-content {
+ max-width: none; /* 让内容最大宽度不限制 */
+ margin: 0 auto;
+ padding: 0 2rem; /* 左右边距2rem */
+ box-sizing: border-box;
+}
+
+/* markdown 内容排版,配合 rehype-highlight 样式 */
+.help-content p {
+ margin: 1rem 0;
+ line-height: 1.6;
+}
+
+.help-content h1 {
+ font-size: 1.5rem;
+ margin: 2rem 0 1rem;
+ font-weight: 700;
+ border-bottom: 1px solid #ccc;
+ padding-bottom: 0.5rem;
+}
+
+.help-content h2 {
+ font-size: 1.25rem;
+ margin: 1.5rem 0 1rem;
+ font-weight: 600;
+}
+
+.help-content code {
+ background-color: #f3f4f6;
+ padding: 0.2rem 0.4rem;
+ border-radius: 0.25rem;
+ font-family: monospace;
+ font-size: 0.9rem;
+}
+
+.help-content pre {
+ background-color: #f3f4f6;
+ padding: 1rem;
+ border-radius: 0.5rem;
+ overflow-x: auto;
+ margin: 1rem 0;
+ border: 1px solid #d1d5db;
+}
+
+.help-content blockquote {
+ border-left: 4px solid #9ca3af;
+ padding-left: 1rem;
+ font-style: italic;
+ color: #6b7280;
+ margin: 1rem 0;
+}
diff --git a/src/pages/HelpPage.jsx b/src/pages/HelpPage.jsx
new file mode 100644
index 0000000..ec5161b
--- /dev/null
+++ b/src/pages/HelpPage.jsx
@@ -0,0 +1,47 @@
+import React, { useEffect, useState } from 'react';
+import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
+import rehypeHighlight from 'rehype-highlight';
+import './HelpPage.css'; // 引入样式
+import Navbar from '../components/Navbar'; // 导航栏组件
+
+export default function HelpPage() {
+ const [markdown, setMarkdown] = useState('');
+
+ useEffect(() => {
+ fetch('/help.md')
+ .then(res => {
+ if (!res.ok) throw new Error('Markdown 文件加载失败');
+ return res.text();
+ })
+ .then(text => setMarkdown(text))
+ .catch(() => setMarkdown('# 帮助文档加载失败,请稍后再试。'));
+ }, []);
+
+ return (
+ <div>
+ <Navbar />
+ <div className="help-page-wrapper">
+ <div className="help-container">
+ <h1 className="help-title">📚 帮助文档</h1>
+ <div className="help-content">
+ <ReactMarkdown
+ remarkPlugins={[remarkGfm]}
+ rehypePlugins={[rehypeHighlight]}
+ components={{
+ h1: ({ node, ...props }) => <h1 {...props} />,
+ h2: ({ node, ...props }) => <h2 {...props} />,
+ p: ({ node, ...props }) => <p {...props} />,
+ code: ({ node, ...props }) => <code {...props} />,
+ pre: ({ node, ...props }) => <pre {...props} />,
+ blockquote: ({ node, ...props }) => <blockquote {...props} />,
+ }}
+ >
+ {markdown}
+ </ReactMarkdown>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/src/pages/Home.css b/src/pages/Home.css
new file mode 100644
index 0000000..8510372
--- /dev/null
+++ b/src/pages/Home.css
@@ -0,0 +1,20 @@
+.home-layout {
+ min-height: 100vh;
+ background-color: #f0f2f5;
+}
+
+.home-header {
+ background-color: #001529;
+ padding: 0 24px;
+ height: 64px;
+}
+
+.home-content {
+ padding: 24px 0;
+}
+
+.post-card {
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
\ No newline at end of file
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
new file mode 100644
index 0000000..437feda
--- /dev/null
+++ b/src/pages/Home.jsx
@@ -0,0 +1,74 @@
+// pages/Home.js
+import React, { useState, useEffect } from 'react';
+import { getActivityPreviews, getFullActivities } from '../api/activity';
+import RecommendAll from '../components/RecommendAll'; // 假设你有一个推荐预览组件
+import RecommendPreview from '../components/RecommendPreview';
+import Navbar from '../components/Navbar'; // 导航栏组件
+
+const Home = () => {
+ const [activityPreviews, setActivityPreviews] = useState([]);
+ const [fullActivities, setFullActivities] = useState([]);
+ const [selectedActivityId, setSelectedActivityId] = useState(null);
+
+ useEffect(() => {
+ getActivityPreviews().then(res => setActivityPreviews(res.data));
+ getFullActivities().then(res => setFullActivities(res.data));
+ }, []);
+
+ const selectedActivity = fullActivities.find(
+ activity => activity.activityid === selectedActivityId
+ );
+
+ return (
+ <div>
+ <Navbar className="fixed top-0 left-0 w-full z-50" /> {/* 导航栏组件 */}
+ <h1 className="text-3xl font-bold text-center mb-4">社交互动平台 - 首页</h1>
+
+ {/* 活动区域 */}
+ <div className="bg-white p-4 rounded shadow mb-8">
+ <h2 className="text-xl font-semibold mb-4">活动预览</h2>
+ {!selectedActivity ? (
+ <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
+ {activityPreviews.map(activity => (
+ <div key={activity.activityid} className="border p-3 rounded shadow">
+ <h3 className="text-lg font-medium mb-2">{activity.title}</h3>
+ <img
+ src={activity.photo}
+ alt={activity.title}
+ className="w-full h-40 object-cover mb-2 rounded"
+ />
+ <button
+ className="bg-blue-500 text-white px-3 py-1 rounded hover:bg-blue-600"
+ onClick={() => setSelectedActivityId(activity.activityid)}
+ >
+ 查看详情
+ </button>
+ </div>
+ ))}
+ </div>
+ ) : (
+ <div className="p-4 border rounded shadow">
+ <button
+ className="mb-4 text-blue-600 underline"
+ onClick={() => setSelectedActivityId(null)}
+ >
+ ← 返回列表
+ </button>
+ <h3 className="text-2xl font-bold mb-2">{selectedActivity.title}</h3>
+ <img
+ src={selectedActivity.photo}
+ alt={selectedActivity.title}
+ className="w-full h-60 object-cover rounded mb-4"
+ />
+ <p className="mb-2"><strong>内容:</strong>{selectedActivity.content}</p>
+ <p className="mb-2"><strong>时间:</strong>{selectedActivity.time}</p>
+ <p className="mb-2"><strong>奖励:</strong>{selectedActivity.award}</p>
+ </div>
+ )}
+ </div>
+ <RecommendPreview />
+ </div>
+ );
+};
+
+export default Home;
\ No newline at end of file
diff --git a/src/pages/MainPage.css b/src/pages/MainPage.css
new file mode 100644
index 0000000..91325c9
--- /dev/null
+++ b/src/pages/MainPage.css
@@ -0,0 +1,63 @@
+/* src/pages/MainPage.css */
+
+.main-page-wrapper {
+ min-height: 100vh;
+ background-color: #f0f2f5;
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+}
+
+.main-page-content {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 32px 16px;
+}
+
+/* 欢迎区 */
+.main-page-header {
+ margin-bottom: 32px;
+ text-align: center;
+}
+
+.main-page-header h1 {
+ font-size: 32px;
+ color: #333;
+ margin-bottom: 8px;
+}
+
+.main-page-subtitle {
+ font-size: 16px;
+ color: #666;
+}
+
+/* 卡片样式通用区 */
+.card-section {
+ background: #fff;
+ padding: 24px;
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+ margin-bottom: 32px;
+}
+
+.section-title {
+ font-size: 22px;
+ font-weight: 600;
+ margin-bottom: 20px;
+ border-bottom: 1px solid #eaeaea;
+ padding-bottom: 10px;
+}
+
+/* 推荐资源占位样式 */
+.placeholder-box {
+ height: 150px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 2px dashed #ccc;
+ border-radius: 8px;
+ background-color: #fafafa;
+}
+
+.placeholder-text {
+ color: #999;
+ font-size: 16px;
+}
\ No newline at end of file
diff --git a/src/pages/MainPage.jsx b/src/pages/MainPage.jsx
new file mode 100644
index 0000000..bf82c2e
--- /dev/null
+++ b/src/pages/MainPage.jsx
@@ -0,0 +1,41 @@
+// src/pages/MainPage.jsx
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
+import Navbar from '../components/Navbar';
+import ActivityBoard from '../components/ActivityBoard';
+import './MainPage.css';
+import RecommendPreview from'../components/RecommendPreview'; // 引入推荐预览样式
+
+const MainPage = () => {
+ const navigate = useNavigate();
+ const currentUser = {
+ id: 2,
+ username: '测试用户',
+ };
+
+ return (
+ <div className="main-page-wrapper">
+ {/* 顶部导航栏 */}
+ <Navbar />
+
+ <main className="main-page-content">
+
+ {/* 公告区域 */}
+ <section className="card-section">
+ <ActivityBoard />
+ </section>
+
+ {/* 推荐下载资源预留区域 */}
+ <section className="card-section">
+ {/* <h2 className="section-title">📥 推荐下载资源</h2>
+ <div className="placeholder-box"> */}
+ {/* <p className="placeholder-text">这里将展示为你推荐的种子资源,敬请期待~</p> */}
+ <RecommendPreview/>
+ {/* </div> */}
+ </section>
+ </main>
+ </div>
+ );
+};
+
+export default MainPage;
diff --git a/src/pages/managetorrentdetail.jsx b/src/pages/managetorrentdetail.jsx
new file mode 100644
index 0000000..029d06d
--- /dev/null
+++ b/src/pages/managetorrentdetail.jsx
@@ -0,0 +1,287 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import {
+ Descriptions,
+ Table,
+ Button,
+ Modal,
+ Image,
+ message,
+ Spin,
+ Input,
+ Select,
+ Pagination,
+ Space,
+ Card
+} from 'antd';
+import { ExclamationCircleOutlined } from '@ant-design/icons';
+import axios from 'axios';
+import '../torrentdetailmanage.css'; // 引入样式
+
+const { confirm } = Modal;
+const { Option } = Select;
+
+//const { confirm } = Modal;
+
+const TorrentDetailmanage = () => {
+ const { id } = useParams(); // 从URL获取种子ID
+ const navigate = useNavigate(); // 用于返回上一页
+ const [torrent, setTorrent] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+
+ console.log('Torrent ID:', id);
+
+ const currentUserId = 1; // 示例,实际应从认证系统获取
+
+
+ // 格式化日期
+ const formatDate = (dateString) => {
+ if (!dateString) return '未知';
+ const date = new Date(dateString);
+ return date.toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ };
+
+ // 处理删除种子
+ const handleDeleteTorrent = async (torrentId) => {
+ if (!currentUserId) {
+ message.warning('请先登录');
+ return;
+ }
+
+ confirm({
+ title: '确认删除',
+ icon: <ExclamationCircleOutlined />,
+ content: '确定要删除这个种子吗?此操作不可恢复!',
+ onOk: async () => {
+ try {
+ await axios.delete(`http://localhost:8080/torrent/delete/${torrentId}`, {
+ params: { userid: currentUserId }
+ });
+ // 成功删除后,更新状态或返回上一页
+ setTorrent(null); // 清空当前种子详情
+ navigate(-1); // 返回上一页
+ message.success('种子删除成功');
+ } catch (err) {
+ console.error('删除种子失败', err);
+ if (err.response && err.response.status === 403) {
+ message.error('无权删除此种子');
+ } else {
+ message.error('删除种子失败');
+ }
+ }
+ }
+ });
+ };
+
+
+const handleDownloadTorrent = async (torrentId) => {
+ if (!currentUserId) {
+ message.warning('请先登录');
+ return;
+ }
+
+ setIsLoading(true); // 开始加载
+ try {
+ // 使用axios发送带有参数的GET请求
+ // const response = await axios.get(`http://localhost:8080/torrent/download/${torrentId}`, {
+ // params: { userId: currentUserId }, // 正确添加请求参数
+ // responseType: 'blob' // 重要:指定响应类型为blob以处理文件下载
+ // });
+
+ // // 创建下载链接
+ // const url = window.URL.createObjectURL(new Blob([response.data]));
+ // const link = document.createElement('a');
+ // link.href = url;
+ // //link.setAttribute('download', 'torrent_file.torrent'); // 可以设置为动态文件名
+ // document.body.appendChild(link);
+ // link.click();
+ // document.body.removeChild(link);
+ // window.URL.revokeObjectURL(url);
+ open(`http://localhost:8080/torrent/download/${torrentId}?userId=${currentUserId}`, '_blank');
+
+ message.success('种子下载开始');
+ } catch (err) {
+ console.error('下载种子失败', err);
+ if (err.response?.status === 404) {
+ message.error('种子不存在');
+ } else {
+ message.error('下载失败: ' + (err.response?.data?.message || err.message));
+ }
+ } finally {
+ setIsLoading(false); // 结束加载
+ }
+};
+
+
+ // 格式化文件大小
+ const formatFileSize = (bytes) => {
+ if (bytes === 0) return '0 Bytes';
+ const k = 1024;
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ };
+
+ // 在组件函数内部添加这个函数
+const getPromotionName = (promotionId) => {
+ const promotionMap = {
+ 1: '上传加倍',
+ 2: '下载减半',
+ 3: '免费下载',
+ 0: '无促销'
+ };
+
+ return promotionMap[promotionId] || '未知促销';
+};
+
+ // 获取种子详情
+ useEffect(() => {
+ const fetchTorrentDetail = async () => {
+ try {
+ const response = await axios.get(`http://localhost:8080/torrent/${id}`);
+ if (response.status === 200) {
+ setTorrent(response.data);
+ } else {
+ setError('获取种子详情失败');
+ }
+ } catch (err) {
+ console.error('获取种子详情失败:', err);
+ if (err.response) {
+ if (err.response.status === 404) {
+ setError('种子不存在');
+ } else {
+ setError('获取种子详情失败: ' + err.response.data);
+ }
+ } else {
+ setError('网络错误,请稍后重试');
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchTorrentDetail();
+ }, [id]);
+
+ console.log('Torrent Detail:', torrent);
+
+ // 返回上一页
+ const handleBack = () => {
+ navigate(-1); // 返回上一页
+ };
+
+ // 如果正在加载
+ if (loading) {
+ return (
+ <div className="flex justify-center items-center h-96">
+ <Spin size="large" tip="加载中..." />
+ </div>
+ );
+ }
+
+ // 如果有错误
+ if (error) {
+ return (
+ <div className="p-6">
+ <Card>
+ <div className="text-center p-6">
+ <ExclamationCircleOutlined className="text-2xl text-red-500 mb-4" />
+ <h3 className="text-lg font-medium text-red-600 mb-2">错误</h3>
+ <p className="text-gray-600">{error}</p>
+ <Button type="primary" onClick={handleBack} className="mt-4">
+ 返回
+ </Button>
+ </div>
+ </Card>
+ </div>
+ );
+ }
+
+ // 如果种子不存在
+ if (!torrent) {
+ return (
+ <div className="p-6">
+ <Card>
+ <div className="text-center p-6">
+ <ExclamationCircleOutlined className="text-2xl text-yellow-500 mb-4" />
+ <h3 className="text-lg font-medium text-yellow-600 mb-2">种子不存在</h3>
+ <p className="text-gray-600">抱歉,您查找的种子不存在或已被删除。</p>
+ <Button type="primary" onClick={handleBack} className="mt-4">
+ 返回
+ </Button>
+ </div>
+ </Card>
+ </div>
+ );
+ }
+
+return (
+ <div className="torrent-detail-page">
+ <Card
+ className="torrent1-card"
+ title={<div className="torrent-title">种子详情</div>}
+ extra={
+ <Button type="primary" onClick={handleBack}>
+ 返回列表
+ </Button>
+ }
+ >
+ <Descriptions bordered column={1} size="middle">
+ <Descriptions.Item label="ID">{torrent.torrentid}</Descriptions.Item>
+
+ {torrent.coverImagePath && (
+ <Descriptions.Item label="封面图片">
+ <Image
+ className="torrent-cover-image"
+ src={torrent.coverImagePath}
+ alt="种子封面"
+ width={260}
+ height={160}
+ placeholder={
+ <div className="w-60 h-40 bg-gray-200 flex items-center justify-center">
+ 加载中...
+ </div>
+ }
+ />
+ </Descriptions.Item>
+ )}
+
+ <Descriptions.Item label="文件名">{torrent.filename}</Descriptions.Item>
+ <Descriptions.Item label="大小">{formatFileSize(torrent.torrentSize)}</Descriptions.Item>
+ <Descriptions.Item label="上传者ID">{torrent.uploader_id}</Descriptions.Item>
+ <Descriptions.Item label="上传时间">{formatDate(torrent.uploadTime)}</Descriptions.Item>
+ <Descriptions.Item label="下载次数">{torrent.downloadCount}</Descriptions.Item>
+ <Descriptions.Item label="促销">{getPromotionName(torrent.promotionid)}</Descriptions.Item>
+ <Descriptions.Item label="描述">{torrent.description || '无描述'}</Descriptions.Item>
+ </Descriptions>
+
+ <div className="torrent-buttons">
+ <Button
+ danger
+ onClick={() => handleDeleteTorrent(torrent.torrentid)}
+ loading={isLoading}
+ >
+ 删除
+ </Button>
+ <Button
+ type="primary"
+ onClick={() => handleDownloadTorrent(torrent.torrentid)}
+ loading={isLoading}
+ >
+ 下载
+ </Button>
+ </div>
+ </Card>
+ </div>
+);
+};
+
+export default TorrentDetailmanage;
\ No newline at end of file
diff --git a/src/test/TorrentDetail.test.jsx b/src/test/TorrentDetail.test.jsx
new file mode 100644
index 0000000..a540a81
--- /dev/null
+++ b/src/test/TorrentDetail.test.jsx
@@ -0,0 +1,91 @@
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import axios from 'axios';
+import TorrentDetail from '../components/Torrentdetail';
+import { vi } from 'vitest';
+
+
+beforeAll(() => {
+ Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation((query) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
+});
+
+
+// 模拟 axios
+vi.mock('axios');
+
+describe('TorrentDetail Page', () => {
+ const mockTorrent = {
+ torrentTitle: '测试种子',
+ uploader_id: 'uploader123',
+ description: '这是一个测试种子描述',
+ uploadTime: '2024-06-01T12:00:00Z',
+ torrentSize: 104857600,
+ downloadCount: 10,
+ dpi: '1080p',
+ caption: '简体中文',
+ lastseed: '2024-06-05T14:00:00Z',
+ coverImagePath: 'http://example.com/cover.jpg',
+ infoHash: 'abc123'
+ };
+
+ const mockSeeders = [
+ {
+ username: 'Seeder1',
+ uploaded: 204857600,
+ uploadSpeed: 1048576,
+ downloaded: 102400,
+ downloadSpeed: 0,
+ client: 'qBittorrent',
+ lastEvent: '2024-06-06T11:22:00Z'
+ }
+ ];
+
+ beforeEach(() => {
+ axios.get.mockImplementation((url) => {
+ if (url.includes('/torrent/abc123/seeders')) {
+ return Promise.resolve({ data: mockSeeders });
+ }
+ if (url.includes('/torrent/123')) {
+ return Promise.resolve({ data: mockTorrent });
+ }
+ return Promise.reject(new Error('not found'));
+ });
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ test('renders torrent detail and seeders correctly', async () => {
+ render(
+ <MemoryRouter initialEntries={['/torrent/123']}>
+ <Routes>
+ <Route path="/torrent/:id" element={<TorrentDetail />} />
+ </Routes>
+ </MemoryRouter>
+ );
+
+ expect(document.querySelector('.ant-spin')).toBeInTheDocument();
+
+
+ await waitFor(() => {
+ expect(screen.getByText('测试种子')).toBeInTheDocument();
+ expect(screen.getByText('uploader123')).toBeInTheDocument();
+ expect(screen.getByText(/1080p/)).toBeInTheDocument();
+ expect(screen.getByText('Seeder1')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/test/TorrentDetailhelp.test.jsx b/src/test/TorrentDetailhelp.test.jsx
new file mode 100644
index 0000000..8dd2ea5
--- /dev/null
+++ b/src/test/TorrentDetailhelp.test.jsx
@@ -0,0 +1,348 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { MemoryRouter, useParams, useNavigate, useLocation } from 'react-router-dom';
+import TorrentDetailcomplain from '../components/TorrentDetailcomplain';
+import axios from 'axios';
+
+// 模拟 axios
+vi.mock('axios');
+
+// 模拟 react-router-dom 的 hooks
+vi.mock('react-router-dom', async () => {
+ const actual = await vi.importActual('react-router-dom');
+ return {
+ ...actual,
+ useParams: vi.fn(),
+ useNavigate: vi.fn(),
+ useLocation: vi.fn(),
+ };
+});
+
+describe('TorrentDetailcomplain 组件', () => {
+ const mockTorrent = {
+ torrentid: 1,
+ filename: '测试种子文件',
+ torrentSize: 1024 * 1024 * 10, // 10MB
+ uploader_id: 123,
+ uploadTime: '2023-01-01T12:00:00',
+ downloadCount: 100,
+ promotionid: 1,
+ description: '这是一个测试种子',
+ coverImagePath: 'http://example.com/cover.jpg',
+ };
+
+ const mockNavigate = vi.fn();
+ const mockLocation = {
+ state: {
+ duser: 456,
+ torrentid: 1,
+ },
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ // 设置模拟的路由 hooks
+ vi.mocked(useParams).mockReturnValue({ id: '1' });
+ vi.mocked(useNavigate).mockReturnValue(mockNavigate);
+ vi.mocked(useLocation).mockReturnValue(mockLocation);
+
+ // 模拟 axios.get 返回种子详情
+ axios.get.mockResolvedValueOnce({ data: mockTorrent });
+ });
+
+ it('应该正确渲染而不崩溃', async () => {
+ render(
+ <MemoryRouter>
+ <TorrentDetailcomplain />
+ </MemoryRouter>
+ );
+
+ // 等待数据加载
+ await waitFor(() => {
+ expect(screen.getByText('种子详情')).toBeInTheDocument();
+ });
+
+ // 验证一些基本元素是否存在
+ expect(screen.getByText('ID: 1')).toBeInTheDocument();
+ expect(screen.getByText('文件名: 测试种子文件')).toBeInTheDocument();
+ expect(screen.getByText('大小: 10.00 MB')).toBeInTheDocument();
+ expect(screen.getByText('上传者ID: 123')).toBeInTheDocument();
+ expect(screen.getByText('上传时间: 2023/01/01 12:00')).toBeInTheDocument();
+ expect(screen.getByText('下载次数: 100')).toBeInTheDocument();
+ expect(screen.getByText('促销: 上传加倍')).toBeInTheDocument();
+ expect(screen.getByText('描述: 这是一个测试种子')).toBeInTheDocument();
+ });
+
+ it('应该加载种子详情数据', async () => {
+ render(
+ <MemoryRouter>
+ <TorrentDetailcomplain />
+ </MemoryRouter>
+ );
+
+ // 等待数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/torrent/1');
+ });
+ });
+
+ it('应该正确显示封面图片', async () => {
+ render(
+ <MemoryRouter>
+ <TorrentDetailcomplain />
+ </MemoryRouter>
+ );
+
+ // 等待数据加载
+ await waitFor(() => {
+ expect(screen.getByText('种子详情')).toBeInTheDocument();
+ });
+
+ // 验证封面图片是否渲染
+ const image = screen.getByAltText('种子封面');
+ expect(image).toBeInTheDocument();
+ expect(image).toHaveAttribute('src', 'http://example.com/cover.jpg');
+ });
+
+ it('应该正确处理删除种子按钮点击', async () => {
+ // 模拟 confirm 对话框
+ vi.mocked(axios.delete).mockResolvedValueOnce({});
+
+ render(
+ <MemoryRouter>
+ <TorrentDetailcomplain />
+ </MemoryRouter>
+ );
+
+ // 等待数据加载
+ await waitFor(() => {
+ expect(screen.getByText('种子详情')).toBeInTheDocument();
+ });
+
+ // 点击删除按钮
+ const deleteButton = screen.getByText('删除');
+ userEvent.click(deleteButton);
+
+ // 验证 confirm 对话框是否被调用
+ // 注意:由于我们使用了 antd 的 Modal.confirm,我们需要检查 axios.delete 是否被调用
+ // 这里我们直接验证 axios.delete 是否被调用
+ await waitFor(() => {
+ expect(axios.delete).toHaveBeenCalledWith('http://localhost:8080/torrent/delete/1', {
+ params: { userid: 1 },
+ });
+ });
+
+ // 验证导航是否被调用
+ await waitFor(() => {
+ expect(mockNavigate).toHaveBeenCalledWith(-1);
+ });
+
+ // 验证成功消息是否被调用
+ expect(vi.mocked(message.success)).toHaveBeenCalledWith('种子删除成功');
+ });
+
+ it('应该正确处理下载种子按钮点击', async () => {
+ render(
+ <MemoryRouter>
+ <TorrentDetailcomplain />
+ </MemoryRouter>
+ );
+
+ // 等待数据加载
+ await waitFor(() => {
+ expect(screen.getByText('种子详情')).toBeInTheDocument();
+ });
+
+ // 点击下载按钮
+ const downloadButton = screen.getByText('下载');
+ userEvent.click(downloadButton);
+
+ // 验证 axios.get 是否被调用(注意:我们实际上使用了 window.open)
+ // 由于我们直接使用了 window.open,我们需要验证它是否被调用
+ // 这里我们无法直接验证 window.open,但可以验证没有错误发生
+ // 我们可以验证组件没有崩溃
+ expect(screen.getByText('种子详情')).toBeInTheDocument();
+ });
+
+ it('应该正确处理扣除保种积分按钮点击', async () => {
+ // 模拟 axios.post
+ vi.mocked(axios.post).mockResolvedValueOnce({ data: { success: true } });
+
+ render(
+ <MemoryRouter>
+ <TorrentDetailcomplain />
+ </MemoryRouter>
+ );
+
+ // 等待数据加载
+ await waitFor(() => {
+ expect(screen.getByText('种子详情')).toBeInTheDocument();
+ });
+
+ // 点击扣除保种积分按钮
+ const creditButton = screen.getByText('扣除保种积分');
+ userEvent.click(creditButton);
+
+ // 验证 axios.post 是否被调用
+ await waitFor(() => {
+ expect(axios.post).toHaveBeenCalledWith(
+ 'http://localhost:8080/torrent/deducecredit',
+ 'manageid=1&userid=456&credit=3',
+ {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ }
+ );
+ });
+
+ // 验证成功消息是否被调用
+ expect(vi.mocked(message.success)).toHaveBeenCalledWith('成功扣除 3 保种积分');
+ });
+
+ it('应该正确处理删除种子失败的情况', async () => {
+ // 模拟 axios.delete 失败
+ vi.mocked(axios.delete).mockRejectedValueOnce({
+ response: {
+ status: 403,
+ data: '无权删除此种子',
+ },
+ });
+
+ render(
+ <MemoryRouter>
+ <TorrentDetailcomplain />
+ </MemoryRouter>
+ );
+
+ // 等待数据加载
+ await waitFor(() => {
+ expect(screen.getByText('种子详情')).toBeInTheDocument();
+ });
+
+ // 点击删除按钮
+ const deleteButton = screen.getByText('删除');
+ userEvent.click(deleteButton);
+
+ // 验证 axios.delete 是否被调用
+ await waitFor(() => {
+ expect(axios.delete).toHaveBeenCalledWith('http://localhost:8080/torrent/delete/1', {
+ params: { userid: 1 },
+ });
+ });
+
+ // 验证错误消息是否被调用
+ expect(vi.mocked(message.error)).toHaveBeenCalledWith('无权删除此种子');
+ });
+
+ it('应该正确处理下载种子失败的情况', async () => {
+ // 模拟 window.open 失败(我们无法直接模拟 window.open,所以这个测试可能有限)
+ // 由于我们直接使用了 window.open,我们无法直接测试它的失败情况
+ // 我们可以测试组件在点击按钮后没有崩溃
+ render(
+ <MemoryRouter>
+ <TorrentDetailcomplain />
+ </MemoryRouter>
+ );
+
+ // 等待数据加载
+ await waitFor(() => {
+ expect(screen.getByText('种子详情')).toBeInTheDocument();
+ });
+
+ // 点击下载按钮
+ const downloadButton = screen.getByText('下载');
+ userEvent.click(downloadButton);
+
+ // 验证组件没有崩溃
+ expect(screen.getByText('种子详情')).toBeInTheDocument();
+ });
+
+ it('应该正确处理扣除保种积分失败的情况', async () => {
+ // 模拟 axios.post 失败
+ vi.mocked(axios.post).mockRejectedValueOnce({
+ response: {
+ status: 500,
+ data: { message: '服务器错误' },
+ },
+ });
+
+ render(
+ <MemoryRouter>
+ <TorrentDetailcomplain />
+ </MemoryRouter>
+ );
+
+ // 等待数据加载
+ await waitFor(() => {
+ expect(screen.getByText('种子详情')).toBeInTheDocument();
+ });
+
+ // 点击扣除保种积分按钮
+ const creditButton = screen.getByText('扣除保种积分');
+ userEvent.click(creditButton);
+
+ // 验证 axios.post 是否被调用
+ await waitFor(() => {
+ expect(axios.post).toHaveBeenCalledWith(
+ 'http://localhost:8080/torrent/deducecredit',
+ 'manageid=1&userid=456&credit=3',
+ {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ }
+ );
+ });
+
+ // 验证错误消息是否被调用
+ expect(vi.mocked(message.error)).toHaveBeenCalledWith('服务器错误: 服务器错误');
+ });
+
+ it('应该正确处理种子不存在的情况', async () => {
+ // 模拟 axios.get 返回 404
+ axios.get.mockRejectedValueOnce({
+ response: {
+ status: 404,
+ },
+ });
+
+ render(
+ <MemoryRouter>
+ <TorrentDetailcomplain />
+ </MemoryRouter>
+ );
+
+ // 等待错误状态
+ await waitFor(() => {
+ expect(screen.queryByText('种子详情')).not.toBeInTheDocument();
+ });
+
+ // 验证错误消息是否显示
+ expect(screen.getByText('种子不存在')).toBeInTheDocument();
+ expect(screen.getByText('抱歉,您查找的种子不存在或已被删除。')).toBeInTheDocument();
+ });
+
+ it('应该正确处理网络错误的情况', async () => {
+ // 模拟 axios.get 网络错误
+ axios.get.mockRejectedValueOnce({
+ message: '网络错误',
+ });
+
+ render(
+ <MemoryRouter>
+ <TorrentDetailcomplain />
+ </MemoryRouter>
+ );
+
+ // 等待错误状态
+ await waitFor(() => {
+ expect(screen.queryByText('种子详情')).not.toBeInTheDocument();
+ });
+
+ // 验证错误消息是否显示
+ expect(screen.getByText('网络错误,请稍后重试')).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/src/test/TorrentList.search.test.jsx b/src/test/TorrentList.search.test.jsx
new file mode 100644
index 0000000..6c588fd
--- /dev/null
+++ b/src/test/TorrentList.search.test.jsx
@@ -0,0 +1,65 @@
+// TorrentList.test.jsx
+import React from 'react';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import TorrentList from '../components/TorrentList';
+import axios from 'axios';
+import { MemoryRouter } from 'react-router-dom'; // ✅ 引入 MemoryRouter
+
+
+import { vi } from 'vitest';
+beforeAll(() => {
+ Object.defineProperty(window, 'matchMedia', {
+ writable: true,
+ value: vi.fn().mockImplementation((query) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+ });
+});
+
+vi.mock('axios');
+
+
+
+describe('TorrentList - 搜索功能', () => {
+ test('搜索关键词后应正确调用接口并显示结果', async () => {
+ const mockTorrents = [
+ { id: 1, title: '测试种子1', uploader_id: 123 },
+ { id: 2, title: '测试种子2', uploader_id: 456 },
+ ];
+
+ axios.get.mockResolvedValueOnce({ data: mockTorrents });
+
+ render(
+ <MemoryRouter>
+ <TorrentList />
+ </MemoryRouter>
+ );
+
+ // 输入关键词
+ const input = screen.getByPlaceholderText(/搜索种子/i);
+ fireEvent.change(input, { target: { value: '测试' } });
+
+ // 模拟点击搜索按钮
+ const button = screen.getByRole('button', { name: /搜\s*索/i });
+ fireEvent.click(button);
+
+
+ // 等待并断言结果被渲染
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith(
+ 'http://localhost:8080/torrent/search',
+ { params: { keyword: '测试' } }
+ );
+ });
+
+ // expect(await screen.findByText('测试种子1')).toBeInTheDocument();
+ // expect(await screen.findByText('测试种子2')).toBeInTheDocument();
+ });
+});
diff --git a/src/test/TorrentUpload.test.jsx b/src/test/TorrentUpload.test.jsx
new file mode 100644
index 0000000..482184e
--- /dev/null
+++ b/src/test/TorrentUpload.test.jsx
@@ -0,0 +1,489 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import UploadTorrent from '../components/upload';
+import { Form, Select } from 'antd';
+
+// 模拟 axios
+vi.mock('axios');
+
+// 模拟 antd 的 message
+vi.mock('antd', async () => {
+ const actual = await vi.importActual('antd');
+ return {
+ ...actual,
+ message: {
+ success: vi.fn(),
+ error: vi.fn(),
+ },
+ };
+});
+
+describe('UploadTorrent 组件', () => {
+ const mockCategories = [
+ { categoryid: '1', category_name: '电影' },
+ { categoryid: '2', category_name: '剧集' },
+ { categoryid: '3', category_name: '音乐' },
+ { categoryid: '4', category_name: '动漫' },
+ { categoryid: '5', category_name: '游戏' },
+ { categoryid: '6', category_name: '综艺' },
+ { categoryid: '7', category_name: '体育' },
+ { categoryid: '8', category_name: '软件' },
+ { categoryid: '9', category_name: '学习资料' },
+ { categoryid: '10', category_name: '纪录片' },
+ { categoryid: '11', category_name: '音乐' },
+ ];
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ // 模拟 axios.get 返回分类数据
+ vi.mocked(axios.get).mockResolvedValueOnce({ data: mockCategories });
+ });
+
+ it('应该正确渲染而不崩溃', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(screen.getByText('上传种子')).toBeInTheDocument();
+ });
+
+ // 验证一些基本元素是否存在
+ expect(screen.getByText('封面图片')).toBeInTheDocument();
+ expect(screen.getByText('种子文件')).toBeInTheDocument();
+ expect(screen.getByText('标题')).toBeInTheDocument();
+ expect(screen.getByText('描述')).toBeInTheDocument();
+ expect(screen.getByText('分类')).toBeInTheDocument();
+ expect(screen.getByText('上传种子')).toBeInTheDocument();
+ });
+
+ it('应该加载分类数据', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 验证分类下拉框是否渲染了正确的选项
+ const categorySelect = screen.getByLabelText('分类');
+ expect(categorySelect).toBeInTheDocument();
+
+ // 打开下拉菜单并检查选项
+ fireEvent.mouseDown(categorySelect);
+ mockCategories.forEach(category => {
+ expect(screen.getByText(category.category_name)).toBeInTheDocument();
+ });
+ });
+
+ it('应该在选择分类后显示相应的动态字段', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 选择"电影"分类
+ const categorySelect = screen.getByLabelText('分类');
+ fireEvent.mouseDown(categorySelect);
+ fireEvent.click(screen.getByText('电影'));
+
+ // 验证电影相关的字段是否显示
+ expect(screen.getByLabelText('字幕/说明')).toBeInTheDocument();
+ expect(screen.getByLabelText('地区')).toBeInTheDocument();
+ expect(screen.getByLabelText('年份')).toBeInTheDocument();
+ expect(screen.getByLabelText('类型')).toBeInTheDocument();
+ expect(screen.getByLabelText('编码格式')).toBeInTheDocument();
+ expect(screen.getByLabelText('分辨率')).toBeInTheDocument();
+ });
+
+ it('应该在选择"剧集"分类后显示相应的动态字段', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 选择"剧集"分类
+ const categorySelect = screen.getByLabelText('分类');
+ fireEvent.mouseDown(categorySelect);
+ fireEvent.click(screen.getByText('剧集'));
+
+ // 验证剧集相关的字段是否显示
+ expect(screen.getByLabelText('地区')).toBeInTheDocument();
+ expect(screen.getByLabelText('格式')).toBeInTheDocument();
+ expect(screen.getByLabelText('类型')).toBeInTheDocument();
+ });
+
+ it('应该在选择"游戏"分类后显示相应的动态字段', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 选择"游戏"分类
+ const categorySelect = screen.getByLabelText('分类');
+ fireEvent.mouseDown(categorySelect);
+ fireEvent.click(screen.getByText('游戏'));
+
+ // 验证游戏相关的字段是否显示
+ expect(screen.getByLabelText('平台')).toBeInTheDocument();
+ expect(screen.getByLabelText('类型')).toBeInTheDocument();
+ expect(screen.getByLabelText('语言')).toBeInTheDocument();
+ expect(screen.getByLabelText('数据类型')).toBeInTheDocument();
+ });
+
+ it('应该在选择"综艺"分类后显示相应的动态字段', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 选择"综艺"分类
+ const categorySelect = screen.getByLabelText('分类');
+ fireEvent.mouseDown(categorySelect);
+ fireEvent.click(screen.getByText('综艺'));
+
+ // 验证综艺相关的字段是否显示
+ expect(screen.getByLabelText('是否大陆综艺')).toBeInTheDocument();
+ expect(screen.getByLabelText('类型')).toBeInTheDocument();
+ expect(screen.getByLabelText('格式')).toBeInTheDocument();
+ });
+
+ it('应该在选择"动漫"分类后显示相应的动态字段', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 选择"动漫"分类
+ const categorySelect = screen.getByLabelText('分类');
+ fireEvent.mouseDown(categorySelect);
+ fireEvent.click(screen.getByText('动漫'));
+
+ // 验证动漫相关的字段是否显示
+ expect(screen.getByLabelText('类型')).toBeInTheDocument();
+ expect(screen.getByLabelText('格式')).toBeInTheDocument();
+ expect(screen.getByLabelText('分辨率')).toBeInTheDocument();
+ });
+
+ it('应该在选择"学习资料"分类后显示相应的动态字段', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 选择"学习资料"分类
+ const categorySelect = screen.getByLabelText('分类');
+ fireEvent.mouseDown(categorySelect);
+ fireEvent.click(screen.getByText('学习资料'));
+
+ // 验证学习资料相关的字段是否显示
+ expect(screen.getByLabelText('类型')).toBeInTheDocument();
+ expect(screen.getByLabelText('格式')).toBeInTheDocument();
+ });
+
+ it('应该在选择"纪录片"分类后显示相应的动态字段', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 选择"纪录片"分类
+ const categorySelect = screen.getByLabelText('分类');
+ fireEvent.mouseDown(categorySelect);
+ fireEvent.click(screen.getByText('纪录片'));
+
+ // 验证纪录片相关的字段是否显示
+ expect(screen.getByLabelText('年份')).toBeInTheDocument();
+ expect(screen.getByLabelText('视频源')).toBeInTheDocument();
+ expect(screen.getByLabelText('格式')).toBeInTheDocument();
+ });
+
+ it('应该在选择"音乐"分类后显示相应的动态字段', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 选择"音乐"分类
+ const categorySelect = screen.getByLabelText('分类');
+ fireEvent.mouseDown(categorySelect);
+ fireEvent.click(screen.getByText('音乐'));
+
+ // 验证音乐相关的字段是否显示
+ expect(screen.getByLabelText('类型')).toBeInTheDocument();
+ expect(screen.getByLabelText('地区')).toBeInTheDocument();
+ expect(screen.getByLabelText('风格')).toBeInTheDocument();
+ expect(screen.getByLabelText('格式')).toBeInTheDocument();
+ });
+
+ it('应该在选择"软件"分类后显示相应的动态字段', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 选择"软件"分类
+ const categorySelect = screen.getByLabelText('分类');
+ fireEvent.mouseDown(categorySelect);
+ fireEvent.click(screen.getByText('软件'));
+
+ // 验证软件相关的字段是否显示
+ expect(screen.getByLabelText('平台')).toBeInTheDocument();
+ expect(screen.getByLabelText('类型')).toBeInTheDocument();
+ expect(screen.getByLabelText('格式')).toBeInTheDocument();
+ });
+
+ it('应该在选择"体育"分类后显示相应的动态字段', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 选择"体育"分类
+ const categorySelect = screen.getByLabelText('分类');
+ fireEvent.mouseDown(categorySelect);
+ fireEvent.click(screen.getByText('体育'));
+
+ // 验证体育相关的字段是否显示
+ expect(screen.getByLabelText('类型')).toBeInTheDocument();
+ expect(screen.getByLabelText('格式')).toBeInTheDocument();
+ expect(screen.getByLabelText('赛事类型')).toBeInTheDocument();
+ });
+
+ it('应该在选择分类后正确设置状态', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 选择"电影"分类
+ const categorySelect = screen.getByLabelText('分类');
+ fireEvent.mouseDown(categorySelect);
+ fireEvent.click(screen.getByText('电影'));
+
+ // 验证状态是否正确设置
+ // 注意:由于状态是组件内部的,我们需要通过 UI 变化来间接验证
+ // 这里我们验证电影相关的字段是否显示
+ expect(screen.getByLabelText('字幕/说明')).toBeInTheDocument();
+ });
+
+ it('应该在选择种子文件后更新状态', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 模拟文件选择
+ const fileInput = screen.getByLabelText('种子文件').querySelector('input');
+ const mockFile = new File(['torrent content'], 'test.torrent', { type: 'application/x-bittorrent' });
+ fireEvent.change(fileInput, { target: { files: [mockFile] } });
+
+ // 验证文件是否被设置(通过 UI 变化间接验证)
+ // 由于我们无法直接访问组件状态,我们可以通过表单验证状态来间接验证
+ // 这里我们只是验证文件输入是否正常工作
+ expect(fileInput).toBeInTheDocument();
+ });
+
+ it('应该在选择封面图片后更新状态', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 模拟图片选择
+ const imageInput = screen.getByLabelText('封面图片').querySelector('input');
+ const mockImage = new File(['image content'], 'test.jpg', { type: 'image/jpeg' });
+ fireEvent.change(imageInput, { target: { files: [mockImage] } });
+
+ // 验证图片是否被设置(通过 UI 变化间接验证)
+ expect(imageInput).toBeInTheDocument();
+ });
+
+ it('应该在表单提交时验证必填字段', async () => {
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 直接提交表单而不填写任何字段
+ const submitButton = screen.getByText('上传种子');
+ fireEvent.click(submitButton);
+
+ // 验证错误消息是否显示
+ // 注意:由于我们使用了 antd 的 Form,错误消息可能由 antd 内部处理
+ // 我们可以通过检查是否有错误提示来验证
+ // 这里我们只是验证表单提交被调用
+ // 更详细的验证可能需要更复杂的测试设置
+ });
+
+ it('应该在填写所有必填字段后成功提交表单', async () => {
+ // 模拟 axios.post
+ vi.mocked(axios.post).mockResolvedValueOnce({ data: {} });
+
+ render(
+ <MemoryRouter>
+ <UploadTorrent />
+ </MemoryRouter>
+ );
+
+ // 等待分类数据加载
+ await waitFor(() => {
+ expect(axios.get).toHaveBeenCalledWith('http://localhost:8080/categories');
+ });
+
+ // 选择分类
+ const categorySelect = screen.getByLabelText('分类');
+ fireEvent.mouseDown(categorySelect);
+ fireEvent.click(screen.getByText('电影'));
+
+ // 填写标题
+ const titleInput = screen.getByLabelText('标题');
+ fireEvent.change(titleInput, { target: { value: '测试电影' } });
+
+ // 填写描述
+ const descriptionInput = screen.getByLabelText('描述');
+ fireEvent.change(descriptionInput, { target: { value: '这是一个测试电影的描述' } });
+
+ // 选择封面图片
+ const imageInput = screen.getByLabelText('封面图片').querySelector('input');
+ const mockImage = new File(['image content'], 'test.jpg', { type: 'image/jpeg' });
+ fireEvent.change(imageInput, { target: { files: [mockImage] } });
+
+ // 选择种子文件
+ const fileInput = screen.getByLabelText('种子文件').querySelector('input');
+ const mockFile = new File(['torrent content'], 'test.torrent', { type: 'application/x-bittorrent' });
+ fireEvent.change(fileInput, { target: { files: [mockFile] } });
+
+ // 填写电影相关字段
+ const captionInput = screen.getByLabelText('字幕/说明');
+ fireEvent.change(captionInput, { target: { value: '测试字幕' } });
+
+ const regionSelect = screen.getByLabelText('地区');
+ fireEvent.mouseDown(regionSelect);
+ fireEvent.click(screen.getByText('大陆'));
+
+ const yearInput = screen.getByLabelText('年份');
+ fireEvent.change(yearInput, { target: { value: '2023' } });
+
+ const genreSelect = screen.getByLabelText('类型');
+ fireEvent.mouseDown(genreSelect);
+ fireEvent.click(screen.getByText('动作'));
+
+ const codecFormatSelect = screen.getByLabelText('编码格式');
+ fireEvent.mouseDown(codecFormatSelect);
+ fireEvent.click(screen.getByText('H.264'));
+
+ const resolutionSelect = screen.getByLabelText('分辨率');
+ fireEvent.mouseDown(resolutionSelect);
+ fireEvent.click(screen.getByText('1080p'));
+
+ // 提交表单
+ const submitButton = screen.getByText('上传种子');
+ fireEvent.click(submitButton);
+
+ // 验证 axios.post 是否被调用
+ expect(axios.post).toHaveBeenCalledWith(
+ 'http://localhost:8080/torrent/upload',
+ expect.any(FormData),
+ {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ responseType: 'blob',
+ }
+ );
+
+ // 验证成功消息是否显示
+ expect(vi.mocked(message.success)).toHaveBeenCalledWith('上传成功!');
+ });
+});
\ No newline at end of file
diff --git a/src/test/setup.js b/src/test/setup.js
new file mode 100644
index 0000000..43c149b
--- /dev/null
+++ b/src/test/setup.js
@@ -0,0 +1,2 @@
+// src/test/setup.js
+import '@testing-library/jest-dom';
diff --git a/src/torrentdetailmanage.css b/src/torrentdetailmanage.css
new file mode 100644
index 0000000..b38cf6d
--- /dev/null
+++ b/src/torrentdetailmanage.css
@@ -0,0 +1,53 @@
+/* TorrentDetail.css */
+
+body, html, #root {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ background-color: #f4f6f9;
+ font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', sans-serif;
+}
+
+.torrent-detail-page {
+ min-height: 100vh;
+ padding: 32px 48px;
+ background-color: #f4f6f9;
+ box-sizing: border-box;
+}
+
+.torrent1-card {
+ width: 100%;
+ max-width: 1080px;
+ min-width: 1000px; /* 确保不会太窄 */
+ margin: 0 auto;
+ border-radius: 16px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
+ border: none;
+ background: #fff;
+ padding: 32px;
+}
+
+.torrent-title {
+ font-size: 28px;
+ font-weight: 600;
+ color: #d46b08;
+ margin-bottom: 20px;
+}
+
+.torrent-cover-image {
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
+ border: 1px solid #eee;
+}
+
+.torrent-buttons {
+ margin-top: 32px;
+ display: flex;
+ justify-content: flex-end;
+ gap: 20px;
+}
+
+.torrent-buttons .ant-btn {
+ font-size: 16px;
+ padding: 10px 28px;
+}
diff --git a/src/torrentlist.css b/src/torrentlist.css
new file mode 100644
index 0000000..b6fa9d1
--- /dev/null
+++ b/src/torrentlist.css
@@ -0,0 +1,168 @@
+/* 加载状态 */
+.loading-container {
+ height: 16rem; /* 64 x 4 */
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.spinner {
+ border: 4px solid #f3f3f3;
+ border-top: 4px solid #f59e0b; /* 橙黄色 */
+ border-radius: 50%;
+ width: 48px;
+ height: 48px;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* 容器和网格 */
+.torrents-container {
+ padding: 16px;
+ background-color: #f9fafb;
+ min-height: 100vh;
+}
+
+.torrents-grid {
+ display: grid;
+ /* grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); */
+ grid-template-columns: repeat(4, 1fr); /* 固定每行4列 */
+ gap: 24px;
+}
+
+.torrents1-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr); /* 固定每行4列 */
+ gap: 24px;
+}
+
+
+/* 卡片样式 */
+.torrent-card {
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgb(0 0 0 / 0.1);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ transition: box-shadow 0.3s ease;
+}
+
+.torrent-card:hover {
+ box-shadow: 0 6px 16px rgb(0 0 0 / 0.15);
+}
+
+/* 封面区域 */
+.cover {
+ height: 160px;
+ overflow: hidden;
+ background-color: #e5e7eb;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.cover-image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.no-cover {
+ color: #9ca3af;
+ font-size: 14px;
+}
+
+/* 信息区域 */
+.info {
+ padding: 16px;
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #d97706; /* 橙色 */
+ margin: 0 0 6px 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.description {
+ font-size: 14px;
+ color: #6b7280;
+ margin: 0 0 12px 0;
+ height: 36px; /* 限制两行高度 */
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.details {
+ font-size: 12px;
+ color: #6b7280;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 16px;
+}
+
+.details span {
+ white-space: nowrap;
+}
+
+/* 操作按钮 */
+.actions {
+ margin-top: auto;
+ display: flex;
+ gap: 12px;
+}
+
+.btn {
+ flex: 1;
+ padding: 8px 12px;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ text-align: center;
+ text-decoration: none;
+ user-select: none;
+ transition: background-color 0.3s ease;
+ border: none;
+}
+
+.btn-download {
+ background-color: #f97316; /* 明亮橙色 */
+ color: white;
+}
+
+.btn-download:hover {
+ background-color: #ea580c; /* 深橙 */
+}
+
+.btn-detail {
+ background-color: #e5e7eb;
+ color: #374151;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.btn-detail:hover {
+ background-color: #d1d5db;
+}
+
+/* 没有数据提示 */
+.no-data {
+ text-align: center;
+ color: #6b7280;
+ font-size: 18px;
+ margin-top: 80px;
+}
+
diff --git a/src/upload.css b/src/upload.css
new file mode 100644
index 0000000..3e35e89
--- /dev/null
+++ b/src/upload.css
@@ -0,0 +1,151 @@
+
+
+ /* 表单组样式 */
+ .form-group {
+ margin-bottom: 1rem;
+ }
+
+ .form-label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-weight: 500;
+ }
+
+ .form-control {
+ width: 100%;
+ padding: 0.5rem;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ transition: border-color 0.3s;
+ }
+
+ .form-control:focus {
+ outline: none;
+ border-color: #ff8c00;
+ box-shadow: 0 0 0 3px rgba(255, 140, 0, 0.2);
+ }
+
+ /* 提交按钮样式 */
+ .submit-btn {
+ background-color: #ff8c00;
+ color: white;
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: 500;
+ transition: background-color 0.3s;
+ }
+
+ .submit-btn:hover {
+ background-color: #ff7f00;
+ }
+
+ /* 成功消息样式 */
+ .success-message {
+ margin-top: 1rem;
+ padding: 0.75rem;
+ background-color: #fff3cd;
+ color: #856404;
+ border: 1px solid #ffeeba;
+ border-radius: 4px;
+ }
+
+ /* 表单组样式 */
+ .form-group {
+ margin-bottom: 1.5rem;
+ }
+
+ .form-label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-weight: 500;
+ }
+
+ .form-control {
+ width: 100%;
+ padding: 0.75rem;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ transition: border-color 0.3s;
+ font-size: 1rem;
+ }
+
+ .form-control:focus {
+ outline: none;
+ border-color: #ff8c00;
+ box-shadow: 0 0 0 3px rgba(255, 140, 0, 0.2);
+ }
+
+ /* 上传区域样式 */
+ .upload-area {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s;
+ }
+
+ .upload-area:hover {
+ background-color: rgba(255, 140, 0, 0.05);
+ }
+
+ /* 自定义下拉框样式 */
+ .custom-select {
+ width: 100%;
+ padding: 0.75rem 2.5rem 0.75rem 1rem;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ appearance: none;
+ background-color: white;
+ font-size: 1rem;
+ cursor: pointer;
+ transition: border-color 0.3s;
+ }
+
+ .custom-select:focus {
+ outline: none;
+ border-color: #ff8c00;
+ box-shadow: 0 0 0 3px rgba(255, 140, 0, 0.2);
+ }
+
+ .select-arrow {
+ position: absolute;
+ right: 1rem;
+ top: 50%;
+ transform: translateY(-50%);
+ pointer-events: none;
+ }
+
+ /* 提交按钮样式 */
+ .submit-btn {
+ background-color: #ff8c00;
+ color: white;
+ padding: 0.75rem 2rem;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+ font-weight: 600;
+ font-size: 1rem;
+ transition: background-color 0.3s;
+ display: inline-block;
+ }
+
+ .submit-btn:hover {
+ background-color: #ff7f00;
+ }
+
+ /* 成功消息样式 */
+ .success-message {
+ margin-top: 1.5rem;
+ padding: 0.75rem 1.5rem;
+ background-color: #fff3cd;
+ color: #856404;
+ border: 1px solid #ffeeba;
+ border-radius: 8px;
+ }
+
+ /* 居中布局 */
+ .text-center {
+ text-align: center;
+ }
diff --git a/src/uploadtorrent.css b/src/uploadtorrent.css
new file mode 100644
index 0000000..213336c
--- /dev/null
+++ b/src/uploadtorrent.css
@@ -0,0 +1,95 @@
+/* UploadTorrent.css */
+.container {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 10px;
+}
+
+.card {
+ width: 100%;
+ max-width: 1000px;;
+ min-width: 800px; /* 最小宽度 800px(确保不会太窄) */
+ margin: 0 auto;
+ padding: 20px;
+ background-color: #fff;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ border-radius: 8px;
+}
+
+.title {
+ text-align: center;
+ font-size: 1.5rem;
+ font-weight: bold;
+ color: #ff8c00;
+ margin-bottom: 20px;
+ padding-bottom: 10px;
+ border-bottom: 2px solid #ffe0b2;
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.uploadArea {
+ text-align: center;
+ padding: 20px;
+ border: 2px dashed #ffe0b2;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: border-color 0.3s;
+}
+
+.uploadArea:hover {
+ border-color: #ff8c00;
+}
+
+.uploadIcon {
+ color: #ffe0b2;
+ font-size: 2rem;
+}
+
+.uploadText {
+ margin-top: 10px;
+ font-size: 0.8rem;
+ color: #666;
+}
+
+.hiddenInput {
+ display: none;
+}
+
+.input {
+ width: 100%;
+ text-align: center;
+}
+
+.textArea {
+ width: 100%;
+ text-align: center;
+}
+
+.select {
+ width: 100%;
+}
+
+.submitButton {
+ width: 100%;
+ background-color: #ff8c00;
+ border-color: #ff8c00;
+}
+
+.submitButton:hover {
+ background-color: #ff7f00;
+ border-color: #ff7f00;
+}
+
+.successMessage {
+ text-align: center;
+ color: green;
+ font-weight: bold;
+ margin-top: 10px;
+}