前端简单界面
Change-Id: I7df9774daf4df8d92b13e659effe426ab0b6180b
diff --git a/pt--frontend/src/App.css b/pt--frontend/src/App.css
new file mode 100644
index 0000000..1766c19
--- /dev/null
+++ b/pt--frontend/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/pt--frontend/src/App.jsx b/pt--frontend/src/App.jsx
new file mode 100644
index 0000000..81d3cb6
--- /dev/null
+++ b/pt--frontend/src/App.jsx
@@ -0,0 +1,17 @@
+// App.jsx
+import { Routes, Route } from 'react-router-dom';
+import Home from './pages/Home';
+import UploadPage from './pages/UploadPage';
+import TorrentDetail from './pages/Torrentdetail';
+
+function App() {
+ return (
+ <Routes>
+ <Route path="/" element={<Home />} />
+ <Route path="/upload" element={<UploadPage />} />
+ <Route path="/torrent/:id" element={<TorrentDetail />} />
+ </Routes>
+ );
+}
+
+export default App;
diff --git a/pt--frontend/src/api/activity.js b/pt--frontend/src/api/activity.js
new file mode 100644
index 0000000..0b2f802
--- /dev/null
+++ b/pt--frontend/src/api/activity.js
@@ -0,0 +1,13 @@
+import axios from 'axios';
+
+const BASE_URL = 'http://localhost:8080/activity';
+
+// 获取所有 is_show == 0 的活动预览(只含标题和图片)
+export const getActivityPreviews = () => {
+ return axios.get(`${BASE_URL}/preview`);
+};
+
+// 获取所有 is_show == 0 的完整活动信息
+export const getFullActivities = () => {
+ return axios.get(`${BASE_URL}/full`);
+};
diff --git a/pt--frontend/src/api/chat.js b/pt--frontend/src/api/chat.js
new file mode 100644
index 0000000..69f8353
--- /dev/null
+++ b/pt--frontend/src/api/chat.js
@@ -0,0 +1,37 @@
+import axios from 'axios';
+
+const BASE_URL = 'http://localhost:8080/chat'; // 根据你的后端接口地址调整
+
+// 发送消息
+export const sendMessage = async ({ senderId, receiverId, content }) => {
+ const now = new Date().toISOString(); // 发送时间
+
+ // 构造后端需要的格式,chatimformation1 或 chatimformation2 中只有一个有值
+ const payload = {
+ friend1: senderId,
+ friend2: receiverId,
+ talkTime: now,
+ chatimformation1: senderId < receiverId ? content : null, // 约定较小 ID 为 friend1 发送的消息
+ chatimformation2: senderId > receiverId ? content : null,
+ };
+
+ const response = await axios.post(`${BASE_URL}/create`, payload);
+ return response.data;
+};
+
+// 获取两个用户之间的聊天记录
+export const getMessagesByUserIds = async (senderId, receiverId) => {
+ const response = await axios.get(`${BASE_URL}/between`, {
+ params: {
+ user1: senderId,
+ user2: receiverId,
+ }
+ });
+ return response.data;
+};
+
+// 可选:获取某用户与所有人的聊天记录(如果你后续需要聊天列表用)
+export const getChatsByUser = async (userId) => {
+ const response = await axios.get(`${BASE_URL}/user/${userId}`);
+ return response.data;
+};
diff --git a/pt--frontend/src/api/comment.js b/pt--frontend/src/api/comment.js
new file mode 100644
index 0000000..28b2b4e
--- /dev/null
+++ b/pt--frontend/src/api/comment.js
@@ -0,0 +1,40 @@
+import axios from 'axios';
+
+const BASE_URL = 'http://localhost:8080/comment';
+
+// 创建评论
+export const createComment = (commentData) => {
+ return axios.post(`${BASE_URL}/create`, commentData);
+};
+
+// 删除评论
+export const deleteComment = (commentId) => {
+ return axios.delete(`${BASE_URL}/delete/${commentId}`);
+};
+
+// 更新评论
+export const updateComment = (commentData) => {
+ return axios.put(`${BASE_URL}/update`, commentData);
+};
+
+// 获取某个帖子的所有评论
+// comment.js
+export async function getCommentsByPostId(postid) {
+ try {
+ const response = await axios.get(`${BASE_URL}/post/${postid}`);
+ return Array.isArray(response.data) ? response.data : []; // 确保返回数据是数组
+ } catch (error) {
+ console.error('获取评论失败', error);
+ return [];
+ }
+}
+
+// 点赞评论
+export const likeComment = (commentId) => {
+ return axios.post(`${BASE_URL}/like/${commentId}`);
+};
+
+// 取消点赞评论
+export const unlikeComment = (commentId) => {
+ return axios.post(`${BASE_URL}/unlike/${commentId}`);
+};
diff --git a/pt--frontend/src/api/friends.js b/pt--frontend/src/api/friends.js
new file mode 100644
index 0000000..f139cc2
--- /dev/null
+++ b/pt--frontend/src/api/friends.js
@@ -0,0 +1,20 @@
+import axios from 'axios';
+
+const BASE_URL = 'http://localhost:8080/friends';
+
+// 添加好友
+export const addFriend = (friendData) => {
+ return axios.post(`${BASE_URL}/add`, friendData);
+};
+
+// ✅ 删除好友(通过 friend1 和 friend2 双向删除)
+export const deleteFriend = (friend1, friend2) => {
+ return axios.delete(`${BASE_URL}/delete`, {
+ params: { friend1, friend2 }
+ });
+};
+
+// 查询某个用户的所有好友(friend1 或 friend2)
+export const getFriendsByUserId = (userid) => {
+ return axios.get(`${BASE_URL}/list/${userid}`);
+};
diff --git a/pt--frontend/src/api/index.js b/pt--frontend/src/api/index.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/pt--frontend/src/api/index.js
diff --git a/pt--frontend/src/api/post.js b/pt--frontend/src/api/post.js
new file mode 100644
index 0000000..9aa47a1
--- /dev/null
+++ b/pt--frontend/src/api/post.js
@@ -0,0 +1,110 @@
+const BASE_URL = 'http://localhost:8080/post';
+
+/**
+ * 创建帖子(带图片)
+ * @param {FormData} formData 包含 userid、post_title、post_content、tags、rannge、is_pinned、photo
+ */
+export const createPost = (formData) => {
+ return fetch(`${BASE_URL}/create`, {
+ method: 'POST',
+ body: formData,
+ }).then(res => res.json());
+};
+
+/**
+ * 删除帖子
+ * @param {number} postid 帖子 ID
+ */
+export const deletePost = (postid) => {
+ return fetch(`${BASE_URL}/delete/${postid}`, {
+ method: 'DELETE',
+ }).then(res => res.json());
+};
+
+/**
+ * 更新帖子(JSON 格式)
+ * @param {Object} post 帖子对象
+ */
+export const updatePost = (post) => {
+ return fetch(`${BASE_URL}/update`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(post),
+ }).then(res => res.json());
+};
+
+/**
+ * 关键词搜索帖子
+ * @param {string} keyword 搜索关键词
+ */
+export const searchPosts = (keyword) => {
+ return fetch(`${BASE_URL}/search?keyword=${encodeURIComponent(keyword)}`)
+ .then(res => res.json());
+};
+
+/**
+ * 点赞帖子
+ * @param {number} postid 帖子 ID
+ */
+export const likePost = (postid) => {
+ return fetch(`${BASE_URL}/like/${postid}`, {
+ method: 'PUT',
+ }).then(res => res.json());
+};
+
+/**
+ * 取消点赞帖子
+ * @param {number} postid 帖子 ID
+ */
+export const unlikePost = (postid) => {
+ return fetch(`${BASE_URL}/unlike/${postid}`, {
+ method: 'PUT',
+ }).then(res => res.json());
+};
+
+/**
+ * 置顶帖子
+ * @param {number} postid 帖子 ID
+ */
+export const pinPost = (postid) => {
+ return fetch(`${BASE_URL}/pin/${postid}`, {
+ method: 'PUT',
+ }).then(res => res.json());
+};
+
+/**
+ * 取消置顶帖子
+ * @param {number} postid 帖子 ID
+ */
+export const unpinPost = (postid) => {
+ return fetch(`${BASE_URL}/unpin/${postid}`, {
+ method: 'PUT',
+ }).then(res => res.json());
+};
+
+/**
+ * 获取某用户所有帖子
+ * @param {number} userid 用户 ID
+ */
+export const findPostsByUserId = (userid) => {
+ return fetch(`${BASE_URL}/findByUserid?userid=${userid}`)
+ .then(res => res.json());
+};
+
+/**
+ * 获取所有置顶帖子
+ */
+export const findPinnedPosts = () => {
+ return fetch(`${BASE_URL}/findPinned`)
+ .then(res => res.json());
+};
+
+/**
+ * 获取所有帖子(排序后)
+ */
+export const getAllPostsSorted = () => {
+ return fetch(`${BASE_URL}/all`)
+ .then(res => res.json());
+};
diff --git a/pt--frontend/src/api/request.js b/pt--frontend/src/api/request.js
new file mode 100644
index 0000000..0a41208
--- /dev/null
+++ b/pt--frontend/src/api/request.js
@@ -0,0 +1,93 @@
+import axios from 'axios';
+
+const BASE_URL = 'http://localhost:8080/request';
+
+// 创建求助帖(支持上传图片)
+export const createRequest = (formData) => {
+ return axios.post(`${BASE_URL}/create`, formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+};
+
+// 修改求助帖金额
+export const updateMoney = (requestid, money) => {
+ return axios.put(`${BASE_URL}/updateMoney/${requestid}`, null, {
+ params: { money },
+ });
+};
+
+// ✅ 新增:根据名称批量更新被协助用户 ID
+export const updateLoaduserByName = (name, loaduser) => {
+ return axios.post(`${BASE_URL}/updateLoaduserByName`, null, {
+ params: { name, loaduser },
+ });
+};
+
+// 删除求助帖
+export const deleteRequest = (requestid) => {
+ return axios.delete(`${BASE_URL}/delete/${requestid}`);
+};
+
+// 根据名称查找求助帖
+export const findByName = async (name) => {
+ try {
+ const response = await axios.get(`${BASE_URL}/findByName`, {
+ params: { name },
+ });
+ return Array.isArray(response.data) ? response.data : [];
+ } catch (error) {
+ console.error('按名称查找求助帖失败', error);
+ return [];
+ }
+};
+
+// 根据发帖用户 ID 查找求助帖
+export const findByUserid = async (userid) => {
+ try {
+ const response = await axios.get(`${BASE_URL}/findByUserid`, {
+ params: { userid },
+ });
+ return Array.isArray(response.data) ? response.data : [];
+ } catch (error) {
+ console.error('按用户ID查找求助帖失败', error);
+ return [];
+ }
+};
+
+// 根据被协助用户 ID 查找求助帖
+export const findByLoaduser = async (loaduser) => {
+ try {
+ const response = await axios.get(`${BASE_URL}/findByLoaduser`, {
+ params: { loaduser },
+ });
+ return Array.isArray(response.data) ? response.data : [];
+ } catch (error) {
+ console.error('按被协助用户ID查找求助帖失败', error);
+ return [];
+ }
+};
+
+// 获取某名称的总金额
+export const getTotalMoneyByName = async (name) => {
+ try {
+ const response = await axios.get(`${BASE_URL}/totalMoneyByName`, {
+ params: { name },
+ });
+ return typeof response.data === 'number' ? response.data : 0;
+ } catch (error) {
+ console.error('获取总金额失败', error);
+ return 0;
+ }
+};
+
+export const getAllRequests = async () => {
+ try {
+ const response = await axios.get(`${BASE_URL}/all`);
+ return Array.isArray(response.data) ? response.data : [];
+ } catch (error) {
+ console.error('获取全部求助帖失败', error);
+ return [];
+ }
+};
diff --git a/pt--frontend/src/assets/react.svg b/pt--frontend/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/pt--frontend/src/assets/react.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
\ No newline at end of file
diff --git a/pt--frontend/src/components/ActivityFullList.jsx b/pt--frontend/src/components/ActivityFullList.jsx
new file mode 100644
index 0000000..02b3ebd
--- /dev/null
+++ b/pt--frontend/src/components/ActivityFullList.jsx
@@ -0,0 +1,36 @@
+// src/components/ActivityFullList.jsx
+import React, { useEffect, useState } from 'react';
+import { getFullActivities } from '../api/activity';
+
+const ActivityFullList = () => {
+ const [activities, setActivities] = useState([]);
+
+ useEffect(() => {
+ getFullActivities()
+ .then(res => setActivities(res.data))
+ .catch(err => console.error('获取完整活动失败:', err));
+ }, []);
+
+ return (
+ <div>
+ <h2>完整活动信息</h2>
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
+ {activities.map(activity => (
+ <div key={activity.activityid} style={{ borderBottom: '1px solid #ddd', paddingBottom: '16px' }}>
+ <h3>{activity.title}</h3>
+ <img
+ src={activity.photo}
+ alt={activity.title}
+ style={{ width: '300px', height: 'auto', borderRadius: '4px' }}
+ />
+ <p><strong>内容:</strong>{activity.content}</p>
+ <p><strong>时间:</strong>{activity.time}</p>
+ <p><strong>奖励:</strong>{activity.award}</p>
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+};
+
+export default ActivityFullList;
diff --git a/pt--frontend/src/components/ActivityPreview.jsx b/pt--frontend/src/components/ActivityPreview.jsx
new file mode 100644
index 0000000..05050df
--- /dev/null
+++ b/pt--frontend/src/components/ActivityPreview.jsx
@@ -0,0 +1,33 @@
+// src/components/ActivityPreview.jsx
+import React, { useEffect, useState } from 'react';
+import { getActivityPreviews } from '../api/activity';
+
+const ActivityPreview = () => {
+ const [activities, setActivities] = useState([]);
+
+ useEffect(() => {
+ getActivityPreviews()
+ .then(res => setActivities(res.data))
+ .catch(err => console.error('获取活动预览失败:', err));
+ }, []);
+
+ return (
+ <div>
+ <h2>活动预览</h2>
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '16px' }}>
+ {activities.map(activity => (
+ <div key={activity.activityid} style={{ border: '1px solid #ccc', padding: '12px', borderRadius: '8px' }}>
+ <h3>{activity.title}</h3>
+ <img
+ src={activity.photo}
+ alt={activity.title}
+ style={{ width: '200px', height: 'auto', borderRadius: '4px' }}
+ />
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+};
+
+export default ActivityPreview;
diff --git a/pt--frontend/src/components/ChatBox.jsx b/pt--frontend/src/components/ChatBox.jsx
new file mode 100644
index 0000000..aec406a
--- /dev/null
+++ b/pt--frontend/src/components/ChatBox.jsx
@@ -0,0 +1,155 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { getMessagesByUserIds, sendMessage } from '../api/chat'; // 替换为新API
+
+const ChatBox = ({ senderId, receiverId }) => { // 不再接收relationId
+ const [messages, setMessages] = useState([]);
+ const [inputContent, setInputContent] = useState('');
+ const messagesEndRef = useRef(null);
+
+ // 加载历史消息(改为通过用户ID获取)
+ const loadMessages = async () => {
+ try {
+ if (!senderId || !receiverId) return;
+ const data = await getMessagesByUserIds(senderId, receiverId);
+
+ // 格式化消息:同时处理friend1(chatimformation1)和friend2(chatimformation2)的消息
+ const formattedMessages = data.flatMap(msg => {
+ const messages = [];
+ // 处理friend1的消息(chatimformation1)
+ if (msg.chatimformation1) {
+ messages.push({
+ content: msg.chatimformation1,
+ isSelf: msg.friend1 === senderId, // 发送方是friend1,判断是否是当前用户
+ talkTime: msg.talkTime // 保留时间用于排序和唯一key
+ });
+ }
+ // 处理friend2的消息(chatimformation2)
+ if (msg.chatimformation2) {
+ messages.push({
+ content: msg.chatimformation2,
+ isSelf: msg.friend2 === senderId, // 发送方是friend2,判断是否是当前用户
+ talkTime: msg.talkTime // 保留时间用于排序和唯一key
+ });
+ }
+ return messages;
+ }).sort((a, b) => new Date(a.talkTime) - new Date(b.talkTime)); // 按时间升序排列
+
+ setMessages(formattedMessages);
+ } catch (error) {
+ console.error('加载消息失败:', error.message);
+ }
+ };
+
+ // 发送消息(不再需要relationId)
+ const handleSend = async () => {
+ if (!inputContent.trim()) return;
+ if (!senderId || !receiverId) { // 新增:校验用户ID有效性
+ console.error('未获取到有效的用户ID');
+ return;
+ }
+
+ try {
+ const newMessage = await sendMessage({
+ senderId,
+ receiverId,
+ content: inputContent
+ });
+ setMessages(prev => [...prev, { content: inputContent, isSelf: true }]);
+ setInputContent('');
+ scrollToBottom();
+ } catch (error) {
+ console.error('发送消息失败:', error.message);
+ }
+ };
+
+ // 依赖改为用户ID变化时重新加载
+ useEffect(() => {
+ if (senderId && receiverId) loadMessages();
+ }, [senderId, receiverId]);
+
+ const scrollToBottom = () => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ };
+
+ return (
+ <div className="chat-box" style={{
+ width: '400px',
+ height: '600px',
+ border: '1px solid #e5e7eb',
+ borderRadius: '8px',
+ display: 'flex',
+ flexDirection: 'column'
+ }}>
+ {/* 消息列表 */}
+ <div style={{
+ flex: 1,
+ padding: '16px',
+ overflowY: 'auto',
+ backgroundColor: '#f3f4f6'
+ }}>
+ {/* 消息列表渲染(使用数据库唯一ID作为key) */}
+ {messages.map((msg) => (
+ <div
+ key={msg.informationid}
+ style={{
+ marginBottom: '12px',
+ display: 'flex',
+ justifyContent: msg.isSelf ? 'flex-end' : 'flex-start'
+ }}
+ >
+ <div
+ style={{
+ maxWidth: '70%',
+ padding: '8px 12px',
+ borderRadius: '12px',
+ backgroundColor: msg.isSelf ? '#2563eb' : '#ffffff',
+ color: msg.isSelf ? 'white' : 'black'
+ }}
+ >
+ {msg.content}
+ </div>
+ </div>
+ ))}
+ <div ref={messagesEndRef} />
+ </div>
+
+ {/* 输入区域 */}
+ <div style={{
+ padding: '16px',
+ display: 'flex',
+ gap: '8px',
+ borderTop: '1px solid #e5e7eb'
+ }}>
+ <input
+ type="text"
+ value={inputContent}
+ onChange={(e) => setInputContent(e.target.value)}
+ onKeyPress={(e) => e.key === 'Enter' && handleSend()}
+ placeholder="输入消息..."
+ style={{
+ flex: 1,
+ padding: '8px 12px',
+ borderRadius: '6px',
+ border: '1px solid #d1d5db',
+ fontSize: '14px'
+ }}
+ />
+ <button
+ onClick={handleSend}
+ style={{
+ padding: '8px 16px',
+ backgroundColor: '#2563eb',
+ color: 'white',
+ border: 'none',
+ borderRadius: '6px',
+ cursor: 'pointer'
+ }}
+ >
+ 发送
+ </button>
+ </div>
+ </div>
+ );
+};
+
+export default ChatBox;
\ No newline at end of file
diff --git a/pt--frontend/src/components/Comment.jsx b/pt--frontend/src/components/Comment.jsx
new file mode 100644
index 0000000..9708566
--- /dev/null
+++ b/pt--frontend/src/components/Comment.jsx
@@ -0,0 +1,94 @@
+// src/components/Comment.jsx
+import React, { useState, useEffect } from 'react';
+import {
+ getCommentsByPostId,
+ createComment,
+ deleteComment,
+ likeComment,
+ unlikeComment,
+} from '../api/comment';
+
+const Comment = ({ postId, currentUser }) => {
+ const [comments, setComments] = useState([]);
+ const [newContent, setNewContent] = useState('');
+
+ useEffect(() => {
+ loadComments();
+ }, [postId]);
+
+ const loadComments = async () => {
+ const data = await getCommentsByPostId(postId);
+ setComments(data);
+ };
+
+ const handleCreate = async () => {
+ if (!newContent.trim()) return;
+
+ const commentData = {
+ postid: postId,
+ userid: currentUser.id,
+ postCommentcontent: newContent,
+ commenttime: new Date().toISOString()
+ };
+
+ await createComment(commentData);
+ setNewContent('');
+ loadComments();
+ };
+
+ const handleDelete = async (commentid) => {
+ await deleteComment(commentid);
+ loadComments();
+ };
+
+ const handleLike = async (commentid) => {
+ await likeComment(commentid);
+ loadComments();
+ };
+
+ const handleUnlike = async (commentid) => {
+ await unlikeComment(commentid);
+ loadComments();
+ };
+
+ return (
+ <div>
+ <h4 className="font-semibold text-gray-700 mb-2">评论</h4>
+ <div className="mb-2">
+ <textarea
+ value={newContent}
+ onChange={(e) => setNewContent(e.target.value)}
+ placeholder="写下你的评论..."
+ className="w-full border rounded p-2"
+ />
+ <button
+ onClick={handleCreate}
+ className="mt-2 bg-blue-500 text-white px-3 py-1 rounded hover:bg-blue-600"
+ >
+ 发布评论
+ </button>
+ </div>
+
+ <div className="space-y-2 mt-4">
+ {comments.map((comment) => (
+ <div key={comment.commentid} className="border rounded p-2">
+ <div className="text-sm text-gray-800 font-medium">用户ID:{comment.userid}</div>
+ <div className="text-gray-700">{comment.postCommentcontent}</div>
+ <div className="text-xs text-gray-500 mt-1">
+ {comment.commenttime || '暂无时间'} | 👍 {comment.likes}
+ </div>
+ <div className="flex gap-2 mt-1 text-sm">
+ <button onClick={() => handleLike(comment.commentid)} className="text-blue-500 hover:underline">点赞</button>
+ <button onClick={() => handleUnlike(comment.commentid)} className="text-yellow-500 hover:underline">取消点赞</button>
+ {comment.userid === currentUser.id && (
+ <button onClick={() => handleDelete(comment.commentid)} className="text-red-500 hover:underline">删除</button>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+};
+
+export default Comment;
diff --git a/pt--frontend/src/components/FriendManager.jsx b/pt--frontend/src/components/FriendManager.jsx
new file mode 100644
index 0000000..d28f93d
--- /dev/null
+++ b/pt--frontend/src/components/FriendManager.jsx
@@ -0,0 +1,156 @@
+import React, { useState, useEffect } from 'react';
+import { addFriend, deleteFriend, getFriendsByUserId } from '../api/friends';
+
+const FriendManager = ({ currentUser, onSelectRelation }) => {
+ const [friendId, setFriendId] = useState('');
+ const [friends, setFriends] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isRefreshing, setIsRefreshing] = useState(false);
+
+ useEffect(() => {
+ if (currentUser?.id) loadFriends(currentUser.id);
+ }, [currentUser]);
+
+ const loadFriends = async (userid) => {
+ setIsRefreshing(true);
+ try {
+ const res = await getFriendsByUserId(userid);
+ setFriends(res.data);
+ } catch (err) {
+ console.error('加载好友失败', err);
+ alert('加载好友失败,请稍后重试');
+ }
+ setIsRefreshing(false);
+ };
+
+ const handleFriendIdChange = (e) => {
+ const value = e.target.value;
+ if (/^\d*$/.test(value)) setFriendId(value);
+ };
+
+ const handleAddFriend = async () => {
+ if (!friendId) return alert('请输入好友ID');
+
+ const newFriendId = parseInt(friendId, 10);
+ if (newFriendId === currentUser.id) return alert('不能添加自己为好友');
+
+ if (friends.some(f => f.friend1 === newFriendId || f.friend2 === newFriendId)) {
+ return alert('该用户已是您的好友');
+ }
+
+ setIsLoading(true);
+ try {
+ const res = await addFriend({ friend1: currentUser.id, friend2: newFriendId });
+ if (res.data) {
+ alert('添加成功');
+ setFriendId('');
+ loadFriends(currentUser.id);
+ } else {
+ alert('添加失败');
+ }
+ } catch (err) {
+ alert('添加好友失败');
+ console.error(err);
+ }
+ setIsLoading(false);
+ };
+
+ /* ---------- 这里开始:删除好友逻辑改为 friend1 + friend2 ---------- */
+ const handleDelete = async (friend1, friend2) => {
+ if (!window.confirm('确认删除该好友吗?')) return;
+ setIsLoading(true);
+ try {
+ const res = await deleteFriend(friend1, friend2);
+ if (res.data) {
+ alert('删除成功');
+ loadFriends(currentUser.id);
+ } else {
+ alert('删除失败');
+ }
+ } catch (err) {
+ alert('删除好友失败');
+ console.error(err);
+ }
+ setIsLoading(false);
+ };
+ /* ------------------------------------------------------------------- */
+
+ return (
+ <div className="max-w-xl mx-auto p-4">
+ <h2 className="text-2xl font-bold mb-4">好友管理</h2>
+
+ {/* 添加好友区域 */}
+ <div className="mb-6 space-y-2">
+ <input
+ type="text"
+ placeholder="输入好友的用户ID"
+ value={friendId}
+ onChange={handleFriendIdChange}
+ className="border p-2 rounded w-full"
+ />
+ <button
+ onClick={handleAddFriend}
+ disabled={isLoading}
+ className={`bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 ${isLoading ? 'opacity-50 cursor-not-allowed' : ''}`}
+ >
+ {isLoading ? '添加中…' : '添加好友'}
+ </button>
+ </div>
+
+ {/* 好友列表 */}
+ <div className="flex justify-between items-center mb-3">
+ <h3 className="text-xl font-semibold">我的好友列表</h3>
+ <button
+ onClick={() => loadFriends(currentUser.id)}
+ disabled={isRefreshing}
+ className="text-sm text-blue-500 hover:underline disabled:text-gray-400"
+ >
+ {isRefreshing ? '刷新中…' : '刷新'}
+ </button>
+ </div>
+
+ {friends.length === 0 ? (
+ <p className="text-gray-500">暂无好友</p>
+ ) : (
+ <ul className="space-y-3">
+ {friends.map((f) => {
+ const friendUserId =
+ f.friend1 === currentUser.id ? f.friend2 : f.friend1;
+ return (
+ <li
+ key={f.relationid}
+ className="border p-3 rounded flex justify-between items-center hover:bg-gray-100 cursor-pointer"
+ onClick={() =>
+ onSelectRelation({
+ relationid: f.relationid,
+ friendId: friendUserId,
+ })
+ }
+ >
+ <div>
+ <p>好友用户ID:{friendUserId}</p>
+ <p className="text-sm text-gray-500">
+ 添加时间:{new Date(f.requestTime).toLocaleString()}
+ </p>
+ </div>
+ <button
+ onClick={(e) => {
+ e.stopPropagation();
+ /* ------- 传入正确的 friend1 & friend2 -------- */
+ handleDelete(f.friend1, f.friend2);
+ }}
+ className="text-red-500 hover:underline"
+ disabled={isLoading}
+ >
+ 删除
+ </button>
+ </li>
+ );
+ })}
+ </ul>
+ )}
+ </div>
+ );
+};
+
+export default FriendManager;
diff --git a/pt--frontend/src/components/Post.jsx b/pt--frontend/src/components/Post.jsx
new file mode 100644
index 0000000..10a4840
--- /dev/null
+++ b/pt--frontend/src/components/Post.jsx
@@ -0,0 +1,200 @@
+import React, { useState, useEffect } from 'react';
+import {
+ createPost,
+ findPinnedPosts,
+ likePost,
+ unlikePost,
+ searchPosts,
+ getAllPostsSorted,
+ findPostsByUserId,
+} from '../api/post';
+import Comment from './Comment';
+
+const Post = () => {
+ const [title, setTitle] = useState('');
+ const [content, setContent] = useState('');
+ const [tags, setTags] = useState('');
+ const [photo, setPhoto] = useState(null);
+ const [posts, setPosts] = useState([]);
+ const [searchKeyword, setSearchKeyword] = useState('');
+
+ const currentUser = { id: 1, username: '测试用户' };
+
+ useEffect(() => {
+ loadPinnedPosts(); // 初始加载置顶帖子
+ }, []);
+
+ const loadPinnedPosts = async () => {
+ const data = await findPinnedPosts();
+ setPosts(data);
+ };
+
+ const loadAllPosts = async () => {
+ const data = await getAllPostsSorted();
+ setPosts(data);
+ };
+
+ const loadMyPosts = async () => {
+ const data = await findPostsByUserId(currentUser.id);
+ setPosts(data);
+ };
+
+ const handleCreate = async () => {
+ const formData = new FormData();
+ formData.append('userid', currentUser.id);
+ formData.append('post_title', title);
+ formData.append('post_content', content);
+ formData.append('is_pinned', true);
+ formData.append('tags', tags);
+ formData.append('rannge', 'public');
+ if (photo) {
+ formData.append('photo', photo);
+ }
+
+ const success = await createPost(formData);
+ if (success) {
+ alert('帖子创建成功');
+ loadPinnedPosts();
+ setTitle('');
+ setContent('');
+ setTags('');
+ setPhoto(null);
+ } else {
+ alert('创建失败');
+ }
+ };
+
+ const handleLike = async (postid) => {
+ await likePost(postid);
+ loadPinnedPosts();
+ };
+
+ const handleUnlike = async (postid) => {
+ await unlikePost(postid);
+ loadPinnedPosts();
+ };
+
+ const handleSearch = async () => {
+ const result = await searchPosts(searchKeyword);
+ setPosts(result);
+ };
+
+ return (
+ <div className="p-4 max-w-3xl mx-auto">
+ <h1 className="text-2xl font-bold mb-4">创建帖子</h1>
+ <div className="space-y-3 mb-6">
+ <input
+ type="text"
+ placeholder="标题"
+ value={title}
+ onChange={(e) => setTitle(e.target.value)}
+ className="w-full border p-2 rounded"
+ />
+ <textarea
+ placeholder="内容"
+ value={content}
+ onChange={(e) => setContent(e.target.value)}
+ className="w-full border p-2 rounded"
+ />
+ <input
+ type="text"
+ placeholder="标签(用逗号分隔,如 学习,编程)"
+ value={tags}
+ onChange={(e) => setTags(e.target.value)}
+ className="w-full border p-2 rounded"
+ />
+ <input
+ type="file"
+ onChange={(e) => setPhoto(e.target.files[0])}
+ />
+ <button
+ onClick={handleCreate}
+ className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
+ >
+ 发布
+ </button>
+ </div>
+
+ <div className="mb-6 flex gap-3">
+ <input
+ type="text"
+ placeholder="搜索关键词"
+ value={searchKeyword}
+ onChange={(e) => setSearchKeyword(e.target.value)}
+ className="border p-2 rounded flex-grow"
+ />
+ <button
+ onClick={handleSearch}
+ className="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
+ >
+ 搜索
+ </button>
+ </div>
+
+ {/* 新增三个展示按钮 */}
+ <div className="mb-6 flex gap-3">
+ <button
+ onClick={loadPinnedPosts}
+ className="bg-yellow-500 text-white px-4 py-2 rounded hover:bg-yellow-600"
+ >
+ 置顶帖子
+ </button>
+ <button
+ onClick={loadAllPosts}
+ className="bg-purple-500 text-white px-4 py-2 rounded hover:bg-purple-600"
+ >
+ 所有帖子
+ </button>
+ <button
+ onClick={loadMyPosts}
+ className="bg-indigo-500 text-white px-4 py-2 rounded hover:bg-indigo-600"
+ >
+ 我的帖子
+ </button>
+ </div>
+
+ <h2 className="text-xl font-semibold mb-3">帖子列表</h2>
+ <div className="space-y-6">
+ {posts.map((post) => (
+ <div key={post.postid} className="border rounded p-4 shadow bg-white">
+ <h3 className="text-lg font-bold">{post.postTitle}</h3>
+ <p className="text-gray-700">{post.postContent}</p>
+ {post.photo && (
+ <img
+ src={`http://localhost:8080${post.photo}`}
+ alt="post"
+ className="w-64 h-auto mt-2"
+ />
+ )}
+ <div className="mt-2 text-sm text-gray-500">
+ 发布时间:{post.postCreatedTime}
+ </div>
+ <div className="mt-2 text-sm text-gray-500">
+ 标签:{post.tags || '无'}
+ </div>
+ <div className="mt-2 flex items-center gap-4">
+ <span>👍 {post.likes}</span>
+ <button
+ onClick={() => handleLike(post.postid)}
+ className="text-blue-500 hover:underline"
+ >
+ 点赞
+ </button>
+ <button
+ onClick={() => handleUnlike(post.postid)}
+ className="text-red-500 hover:underline"
+ >
+ 取消点赞
+ </button>
+ </div>
+ <div className="mt-4 border-t pt-4">
+ <Comment postId={post.postid} currentUser={currentUser} />
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+};
+
+export default Post;
diff --git a/pt--frontend/src/components/RequestBoard.jsx b/pt--frontend/src/components/RequestBoard.jsx
new file mode 100644
index 0000000..f90f2fb
--- /dev/null
+++ b/pt--frontend/src/components/RequestBoard.jsx
@@ -0,0 +1,300 @@
+import React, { useState, useEffect } from 'react';
+import {
+ createRequest,
+ deleteRequest,
+ updateMoney,
+ findByUserid,
+ findByName,
+ getTotalMoneyByName,
+ updateLoaduserByName,
+} from '../api/request';
+
+const RequestBoard = ({ currentUserId }) => {
+ const [requests, setRequests] = useState([]); // 始终存储当前用户的求助帖
+ const [searchedRequests, setSearchedRequests] = useState([]); // 存储搜索结果
+ const [formData, setFormData] = useState({
+ userid: currentUserId,
+ name: '',
+ plot: '',
+ money: '',
+ year: '',
+ country: '',
+ photo: null,
+ });
+ const [searchName, setSearchName] = useState('');
+ const [totalMoney, setTotalMoney] = useState(null);
+
+ // 获取当前用户求助帖
+ const loadUserRequests = async () => {
+ const data = await findByUserid(currentUserId);
+ setRequests(data);
+ };
+
+ useEffect(() => {
+ loadUserRequests();
+ }, []);
+
+ // 表单变更处理
+ const handleChange = (e) => {
+ const { name, value, files } = e.target;
+ setFormData((prev) => ({
+ ...prev,
+ [name]: files ? files[0] : value,
+ }));
+ };
+
+ // 提交创建
+ const handleCreate = async (e) => {
+ e.preventDefault();
+ const fd = new FormData();
+ Object.entries(formData).forEach(([key, value]) => {
+ if (value !== '' && value !== null) fd.append(key, value);
+ });
+
+ const res = await createRequest(fd);
+ if (res.data === true) {
+ alert('创建成功');
+ setFormData(prev => ({
+ ...prev,
+ name: '',
+ plot: '',
+ money: '',
+ year: '',
+ country: '',
+ photo: null,
+ }));
+ loadUserRequests(); // 创建成功后刷新当前用户帖子
+ } else {
+ alert('创建失败');
+ }
+ };
+
+ // 删除
+ const handleDelete = async (id) => {
+ await deleteRequest(id);
+ loadUserRequests(); // 删除后刷新当前用户帖子
+ };
+
+ // 更新金额
+ const handleUpdateMoney = async (id, newMoney) => {
+ if (!newMoney) return;
+ await updateMoney(id, newMoney);
+ loadUserRequests(); // 更新金额后刷新当前用户帖子
+ };
+
+ // 按名称搜索并计算总金额
+ const handleSearch = async () => {
+ const data = await findByName(searchName);
+ const total = await getTotalMoneyByName(searchName);
+ setSearchedRequests(data); // 搜索结果存储到独立状态
+ setTotalMoney(total);
+ };
+
+ // 上传更新被协助用户 ID(批量更新同名求助帖)
+ const handleUploadLoaduser = async (name) => {
+ try {
+ await updateLoaduserByName(name, currentUserId);
+ alert('更新成功');
+ loadUserRequests(); // 更新后刷新当前用户帖子
+ setSearchedRequests([]); // 同步清空搜索结果(可选)
+ } catch (error) {
+ alert('更新失败,请稍后重试');
+ console.error(error);
+ }
+ };
+
+ return (
+ <div className="p-4 max-w-4xl mx-auto">
+ <h2 className="text-2xl font-bold mb-4">发布求助帖</h2>
+ <form className="grid grid-cols-2 gap-4 mb-6" onSubmit={handleCreate}>
+ <input
+ name="name"
+ placeholder="标题/名称"
+ className="border p-2"
+ value={formData.name}
+ onChange={handleChange}
+ />
+ <textarea
+ name="plot"
+ placeholder="内容/情节"
+ className="border p-2 col-span-2"
+ value={formData.plot}
+ onChange={handleChange}
+ />
+ <input
+ name="money"
+ type="number"
+ placeholder="金额"
+ className="border p-2"
+ value={formData.money}
+ onChange={handleChange}
+ />
+ <input
+ name="year"
+ type="number"
+ placeholder="年份"
+ className="border p-2"
+ value={formData.year}
+ onChange={handleChange}
+ />
+ <input
+ name="country"
+ placeholder="国家"
+ className="border p-2"
+ value={formData.country}
+ onChange={handleChange}
+ />
+ <input
+ name="photo"
+ type="file"
+ className="border p-2 col-span-2"
+ onChange={handleChange}
+ />
+ <button
+ type="submit"
+ className="bg-blue-500 text-white p-2 col-span-2 rounded"
+ >
+ 发布
+ </button>
+ </form>
+
+ <div className="mb-6">
+ <h3 className="text-xl font-semibold mb-2">查找求助帖</h3>
+ <div className="flex gap-2 mb-2">
+ <input
+ type="text"
+ placeholder="输入标题"
+ value={searchName}
+ onChange={(e) => setSearchName(e.target.value)}
+ className="border p-2 flex-1"
+ />
+ <button
+ onClick={handleSearch}
+ className="bg-green-500 text-white p-2 rounded"
+ >
+ 查找
+ </button>
+ </div>
+ {totalMoney !== null && (
+ <p className="text-gray-700">
+ 该名称对应的总金额:<strong>{totalMoney}</strong>
+ </p>
+ )}
+ </div>
+
+ {/* 搜索结果展示(有搜索结果时显示) */}
+ {searchedRequests.length > 0 && (
+ <div className="mb-6">
+ <h3 className="text-xl font-semibold mb-2">搜索结果</h3>
+ <button
+ onClick={() => {
+ setSearchedRequests([]);
+ setSearchName('');
+ setTotalMoney(null);
+ }}
+ className="bg-gray-500 text-white p-1 px-2 rounded mb-2"
+ >
+ ← 返回我的求助帖
+ </button>
+ {searchedRequests.length === 0 ? (
+ <p className="text-gray-500">无匹配的求助帖</p>
+ ) : (
+ <div className="grid grid-cols-1 gap-4">
+ {searchedRequests.map((request) => (
+ <div key={request.requestid} className="border p-3 rounded shadow">
+ <h4 className="text-lg font-semibold">{request.name}</h4>
+ <p className="text-gray-600 mb-2">{request.plot}</p>
+ <div className="flex gap-2 mb-2">
+ <span>金额:{request.money}</span>
+ <span>年份:{request.year || '未填写'}</span>
+ <span>国家:{request.country || '未填写'}</span>
+ </div>
+ {request.photo && (
+ <img
+ src={`http://localhost:8080${request.photo}`}
+ alt="求助帖"
+ className="w-32 h-auto mb-2"
+ />
+ )}
+ <div className="flex gap-2">
+ <input
+ type="number"
+ placeholder="新金额"
+ onChange={(e) => handleUpdateMoney(request.requestid, e.target.value)}
+ className="border p-1 flex-1"
+ />
+ <button
+ onClick={() => handleDelete(request.requestid)}
+ className="bg-red-500 text-white p-1 px-2 rounded"
+ >
+ 删除
+ </button>
+ <button
+ onClick={() => handleUploadLoaduser(request.name)}
+ className="bg-blue-500 text-white p-1 px-2 rounded"
+ >
+ 上传更新loaduser
+ </button>
+ </div>
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ )}
+
+ {/* 我的求助帖展示(无搜索结果时显示) */}
+ {searchedRequests.length === 0 && (
+ <div className="mb-6">
+ <h3 className="text-xl font-semibold mb-2">我的求助帖</h3>
+ {requests.length === 0 ? (
+ <p className="text-gray-500">暂无求助帖</p>
+ ) : (
+ <div className="grid grid-cols-1 gap-4">
+ {requests.map((request) => (
+ <div key={request.requestid} className="border p-3 rounded shadow">
+ <h4 className="text-lg font-semibold">{request.name}</h4>
+ <p className="text-gray-600 mb-2">{request.plot}</p>
+ <div className="flex gap-2 mb-2">
+ <span>金额:{request.money}</span>
+ <span>年份:{request.year || '未填写'}</span>
+ <span>国家:{request.country || '未填写'}</span>
+ </div>
+ {request.photo && (
+ <img
+ src={`http://localhost:8080${request.photo}`}
+ alt="求助帖"
+ className="w-32 h-auto mb-2"
+ />
+ )}
+ <div className="flex gap-2">
+ <input
+ type="number"
+ placeholder="新金额"
+ onChange={(e) => handleUpdateMoney(request.requestid, e.target.value)}
+ className="border p-1 flex-1"
+ />
+ <button
+ onClick={() => handleDelete(request.requestid)}
+ className="bg-red-500 text-white p-1 px-2 rounded"
+ >
+ 删除
+ </button>
+ <button
+ onClick={() => handleUploadLoaduser(request.name)}
+ className="bg-blue-500 text-white p-1 px-2 rounded"
+ >
+ 上传更新loaduser
+ </button>
+ </div>
+ </div>
+ ))}
+ </div>
+ )}
+ </div>
+ )}
+ </div>
+ );
+};
+
+export default RequestBoard;
diff --git a/pt--frontend/src/components/torrentlist.jsx b/pt--frontend/src/components/torrentlist.jsx
new file mode 100644
index 0000000..be4b76b
--- /dev/null
+++ b/pt--frontend/src/components/torrentlist.jsx
@@ -0,0 +1,1319 @@
+// import { useState, useEffect } from 'react';
+// import { Link } from 'react-router-dom';
+// import axios from 'axios';
+
+// function TorrentList() {
+// const [torrents, setTorrents] = useState([]);
+// const [categories, setCategories] = useState([]);
+// const [selectedCategory, setSelectedCategory] = useState('');
+
+// // 获取所有分类
+// useEffect(() => {
+// axios.get('http://localhost:8080/categories') // 假设这个接口返回所有分类
+// .then(res => setCategories(res.data))
+// .catch(err => console.error('获取分类失败', err));
+// }, []);
+
+// // 获取种子(根据分类筛选)
+// useEffect(() => {
+// const url = selectedCategory
+// ? `http://localhost:8080/torrent/listByCategory?categoryid=${selectedCategory}`
+// : 'http://localhost:8080/torrent/list';
+
+// axios.get(url)
+// .then(res => setTorrents(res.data))
+// .catch(err => console.error('获取种子失败', err));
+// }, [selectedCategory]);
+// console.log(torrents);
+
+
+// return (
+// <div className="p-4">
+// <div className="mb-4">
+// <label className="mr-2 font-medium">选择分类:</label>
+// <select
+// value={selectedCategory}
+// onChange={e => setSelectedCategory(e.target.value)}
+// className="border rounded px-2 py-1"
+// >
+// <option value="">全部</option>
+// {categories.map(cat => (
+// <option key={cat.categoryid} value={cat.categoryid}>
+// {cat.category_name}
+// </option>
+// ))}
+// </select>
+// </div>
+// <table className="w-full border-collapse">
+// <thead>
+// <tr className="bg-gray-200">
+// <th className="p-2 border">名称</th>
+// <th className="p-2 border">上传者</th>
+// <th className="p-2 border">描述</th>
+// <th className="p-2 border">上传时间</th>
+// <th className="p-2 border">下载次数</th>
+// <th className="p-2 border">促销方式</th>
+// <th className="p-2 border">操作</th>
+// </tr>
+// </thead>
+// <tbody>
+// {torrents.map(t => (
+// <tr key={t.torrentid} className="border-t hover:bg-gray-100">
+// <td className="p-2 border">{t.filename}</td>
+// <td className="p-2 border">{t.uploader_id}</td>
+// <td className="p-2 border">{t.description}</td>
+// <td className="p-2 border">{new Date(t.uploadTime).toLocaleString()}</td>
+// <td className="p-2 border">{t.downloadCount}</td>
+// <td className="p-2 border">
+// {(() => {
+// switch(t.promotionid) {
+// case 1: return '上传加倍';
+// case 2: return '下载免费';
+// case 3: return '下载减半';
+// case 0: return '没有促销';
+// default: return '没有促销';
+// }
+// })()}
+// </td>
+// <td className="p-2 border">
+// <a
+// href={`http://localhost:8080/torrent/download/${t.torrentid}`}
+// className="text-blue-500 hover:underline"
+// target="_blank"
+// rel="noreferrer"
+// >
+// 下载
+// </a>
+// </td>
+// <Link
+// to={`/torrent/${t.torrentid}`}
+// className="text-green-600 hover:underline"
+// >
+// 查看详情
+// </Link>
+// </tr>
+// ))}
+// </tbody>
+// </table>
+// </div>
+// );
+// }
+
+// export default TorrentList;
+// import { useState, useEffect } from 'react';
+// import axios from 'axios';
+
+// function TorrentList() {
+// const [torrents, setTorrents] = useState([]);
+// const [categories, setCategories] = useState([]);
+// const [selectedCategory, setSelectedCategory] = useState('');
+// const [filters, setFilters] = useState({});
+// const [showSuccess, setShowSuccess] = useState(false);
+// 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 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: '其他' },
+// ]
+// 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 categoryFiltersConfig = {
+// 1: [ // Movie 电影
+// { id: 'resolution', label: '分辨率', type: 'select', options: ['1080p', '4K', '720p', '其他'] },
+// { id: 'codecFormat', label: '编码格式', type: 'select', options: ['H.264', 'H.265', 'AV1', 'VC1', 'X264', '其他'] },
+// { id: 'region', label: '地区', type: 'select', options: ['大陆', '港台', '欧美', '日韩', '其他'] },
+// { id: 'genre', label: '类型', type: 'select', options: ['动作', '喜剧', '爱情', '科幻', '恐怖','动作', '冒险', '历史', '悬疑', '其他'] },
+// ],
+// 2: [ // TV 剧集
+// { id: 'region', label: '地区', type: 'select', options: ['大陆', '港台', '欧美', '日韩', '其他'] },
+// { id: 'format', label: '格式', type: 'select', options: resolutions },
+// { id: 'genre', label: '类型', type: 'select', options: ['动作', '喜剧', '爱情', '科幻', '恐怖','动作', '冒险', '历史', '悬疑', '其他'] },
+// ],
+// 3: [ // Music 音乐
+// { id: 'genre', label: '类型', type: 'select', options: ['专辑', '单曲', 'EP', '现场', '其他'] },
+// { id: 'style', label: '风格', type: 'select', options: ['流行', '摇滚', '电子', '古典', '爵士', '民谣', '说唱', '其他'] },
+// ],
+// 4: [ // Anime 动漫
+// { id: 'genre', label: '类型', type: 'select', options: ['新番连载', '剧场版', 'OVA', '完结动漫', '其他'] },
+// { id: 'format', label: '格式', type: 'select', options: ['ZIP', 'RAR', '7Z', 'MKV', 'MP4', '其他'] },
+// { id: 'resolution', label: '分辨率', type: 'select', options: ['720P', '1080P', '4K', '其他'] },
+// ],
+// 5: [ // Game 游戏
+// { id: 'platform', label: '平台', type: 'select', options: ['PC', 'PS5', 'Xbox', 'Switch', '手机', '其他'] },
+// { id: 'genre', label: '类型', type: 'select', options: ['角色扮演', '射击', '冒险', '策略', '体育', '桌面游戏', '其他'] },
+// { id: 'language', label: '语言', type: 'select', options: ['中文', '英文', '日文', '其他'] },
+// { id: 'dataType', label: '数据类型', type: 'select', options: ['压缩包', '补丁', '安装包', 'nds', '其他'] },
+// ],
+// 6: [ // 综艺
+// { id: 'isMainland', label: '是否为大陆综艺', type: 'select', options: ['是','不是'] },
+// { id: 'format', label: '格式', type: 'select', options: ['1080P', '4K', 'HD', '其他'] },
+// { id: 'genre', label: '类型', type: 'select', options: ['真人秀', '选秀','访谈', '音乐', '游戏', '其他'] },
+// ],
+// 7: [ // 学习
+// { id: 'genre', label: '类型', type: 'select', options: ['计算机','软件','人文','外语','理工科','其他'] },
+// { id: 'learningformat', label: '格式', type: 'select', options: ['PDF','EPUB','视频','音频','PPT','其他'] },
+// ],
+// 8: [ // 体育
+// { id: 'eventType', label: '赛事类型', type: 'select', options: ['足球', '篮球', '网球', '乒乓球', '羽毛球', '其他'] },
+// { id: 'region', label: '地区', type: 'select', options: ['亚洲', '欧洲', '美洲', '其他'] },
+// ],
+// 9: [ // 其他
+// { id: 'otherGenre', label: '类型', type: 'select', options: othergenres },
+// ],
+// 10: [ // 纪录片
+// { id: 'source', label: '来源', type: 'select', options: sourceTypes },
+// { id: 'resolution', label: '分辨率', type: 'select', options: resolutions },
+// ],
+// 11: [ // 软件
+// { id: 'softwsreplatform', label: '平台', type: 'select', options: softwareplatforms },
+// { id: 'softwareGenre', label: '软件类型', type: 'select', options: softwaregenres },
+// { id: 'softwareFormat', label: '软件格式', type: 'select', options: softwareformats },
+// ],
+// // 其他分类...
+// };
+
+// // 获取所有分类
+// useEffect(() => {
+// axios.get('http://localhost:8080/categories')
+// .then(res => setCategories(res.data))
+// .catch(err => console.error('加载分类失败', err));
+// }, []);
+
+// // 根据选择的分类显示不同的表单字段
+// useEffect(() => {
+// setFilters({}); // 清空筛选条件
+// }, [selectedCategory]);
+
+// const handleFilterChange = (e) => {
+// const { name, value } = e.target;
+// setFilters(prev => ({ ...prev, [name]: value }));
+// };
+
+// const filteredTorrents = (torrents) => {
+// return torrents.filter(torrent => {
+// if (!selectedCategory) return true; // 如果没有选择分类,显示所有
+// if (torrent.categoryid !== parseInt(selectedCategory)) return false;
+
+// // 根据筛选条件过滤
+// for (const [key, value] of Object.entries(filters)) {
+// if (value && torrent[key] !== value) {
+// return false;
+// }
+// }
+// return true;
+// });
+// };
+
+// // 获取种子(根据分类筛选)
+// useEffect(() => {
+// let url = selectedCategory
+// ? `http://localhost:8080/torrent/listByCategory?categoryid=${selectedCategory}`
+// : 'http://localhost:8080/torrent/list';
+
+// // 添加筛选条件到 URL
+// Object.entries(filters).forEach(([key, value]) => {
+// if (value) {
+// url += `&${key}=${encodeURIComponent(value)}`;
+// }
+// });
+
+// axios.get(url)
+// .then(res => {
+// setTorrents(res.data);
+// })
+// .catch(err => console.error('获取种子失败', err));
+// }, [selectedCategory, filters]);
+
+// const handleDownload = (torrentId) => {
+// window.open(`http://localhost:8080/torrent/download/${torrentId}`, '_blank');
+// };
+
+// return (
+// <div className="p-4">
+// {/* 分类选择 */}
+// <div className="mb-4">
+// <label className="mr-2 font-medium">选择分类:</label>
+// <select
+// value={selectedCategory}
+// onChange={(e) => setSelectedCategory(e.target.value)}
+// className="border rounded px-2 py-1 mr-4"
+// >
+// <option value="">全部</option>
+// {categories.map(cat => (
+// <option key={cat.categoryid} value={cat.categoryid}>
+// {cat.category_name}
+// </option>
+// ))}
+// </select>
+
+// {/* 动态渲染筛选表单 */}
+// {selectedCategory && categoryFiltersConfig[selectedCategory] && (
+// <div className="flex flex-wrap gap-2">
+// {categoryFiltersConfig[selectedCategory].map(filter => {
+// if (filter.type === 'select') {
+// // 根据筛选字段选择对应的选项列表
+// let options;
+// switch (filter.id) {
+// case 'softwareplatform':
+// options = softwareplatforms;
+// break;
+// case 'softwareGenre':
+// options = softwaregenres;
+// break;
+// case 'softwareFormat':
+// options = softwareformats;
+// break;
+// case 'resolution':
+// options = [
+// { value: '720p', label: '720p' },
+// { value: '1080p', label: '1080p' },
+// { value: '2K', label: '2K' },
+// { value: '4K', label: '4K' },
+// { value: '8K', label: '8K' },
+// { value: '其他', label: '其他' },
+// ];
+// break;
+// case 'region':
+// if (selectedCategory === '1' || selectedCategory === '2') { // Movie or TV
+// options = [
+// { value: '大陆', label: '大陆' },
+// { value: '港台', label: '港台' },
+// { value: '欧美', label: '欧美' },
+// { value: '日韩', label: '日韩' },
+// { value: '其他', label: '其他' },
+// ];
+// }
+// else if (selectedCategory === '8') { // Sports
+// options = [
+// { value: '亚洲', label: '亚洲' },
+// { value: '欧洲', label: '欧洲' },
+// { value: '美洲', label: '美洲' },
+// { value: '其他', label: '其他' },
+// ];
+// }
+// break;
+// case 'isMainland':
+// options = [
+// { value: 'true', label: '是' },
+// { value: 'false', label: '不是' },
+// ];
+// break;
+// case 'codecFormat':
+// 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: '其他' },
+// ];
+// break;
+// case 'platform':
+// options = [
+// { value: 'PC', label: 'PC' },
+// { value: 'PS5', label: 'PS5' },
+// { value: 'Xbox', label: 'Xbox' },
+// { value: 'Switch', label: 'Switch' },
+// { value: '手机', label: '手机' },
+// { value: '其他', label: '其他' },
+// ];
+// break;
+// case 'language':
+// options = [
+// { value: '中文', label: '中文' },
+// { value: '英文', label: '英文' },
+// { value: '日文', label: '日文' },
+// { value: '其他', label: '其他' },
+// ];
+// break;
+// case'EventType':
+// options = [
+// { value: '足球', label: '足球' },
+// { value: '篮球', label: '篮球' },
+// { value: '网球', label: '网球' },
+// { value: '乒乓球', label: '乒乓球' },
+// { value: '羽毛球', label: '羽毛球' },
+// { value: '其他', label: '其他' },
+// ];
+// break;
+// case 'genre':
+// if (selectedCategory === '3') { // Music
+// options = [
+// { value: '专辑', label: '专辑' },
+// { value: '单曲', label: '单曲' },
+// { value: 'EP', label: 'EP' },
+// { value: '现场', label: '现场' },
+// { value: '其他', label: '其他' },
+// ];
+// } else if (selectedCategory === '4') { // Anime
+// options = [
+// { value: '新番连载', label: '新番连载' },
+// { value: '剧场版', label: '剧场版' },
+// { value: 'OVA', label: 'OVA' },
+// { value: '完结动漫', label: '完结动漫' },
+// { value: '其他', label: '其他' },
+// ];
+// } else if (selectedCategory === '5') { // Game
+// options = [
+// { value: '角色扮演', label: '角色扮演' },
+// { value: '射击', label: '射击' },
+// { value: '冒险', label: '冒险' },
+// { value: '策略', label: '策略' },
+// { value: '体育', label: '体育' },
+// { value: '桌面游戏', label: '桌面游戏' },
+// { value: '其他', label: '其他' },
+// ];
+// } else if (selectedCategory === '1') { // TV
+// options = [
+// { value: '动作', label: '动作' },
+// { value: '喜剧', label: '喜剧' },
+// { value: '爱情', label: '爱情' },
+// { value: '科幻', label: '科幻' },
+// { value: '恐怖', label: '恐怖' },
+// { value: '动作', label: '动作' },
+// { value: '冒险', label: '冒险' },
+// { value: '历史', label: '历史' },
+// { value: '悬疑', label: '悬疑' },
+// { value: '其他', label: '其他' },
+// ];
+// }
+// else if(selectedCategory === '2') {
+// options = [
+// { value: '动作', label: '动作' },
+// { value: '喜剧', label: '喜剧' },
+// { value: '爱情', label: '爱情' },
+// { value: '科幻', label: '科幻' },
+// { value: '恐怖', label: '恐怖' },
+// { value: '动作', label: '动作' },
+// { value: '冒险', label: '冒险' },
+// { value: '历史', label: '历史' },
+// { value: '悬疑', label: '悬疑' },
+// { value: '其他', label: '其他' },
+// ]; // Movie
+// }
+// else if(selectedCategory === '6') {
+// options = [
+// { value: '真人秀', label: '真人秀' },
+// { value: '选秀', label: '选秀' },
+// { value: '访谈', label: '访谈' },
+// { value: '音乐', label: '音乐' },
+// { value: '游戏', label: '游戏' },
+// { value: '其他', label: '其他' },
+// ];
+// }
+// break;
+// case 'style':
+// if (selectedCategory === '3') { // Music
+// options = [
+// { value: '流行', label: '流行' },
+// { value: '摇滚', label: '摇滚' },
+// { value: '电子', label: '电子' },
+// { value: '古典', label: '古典' },
+// { value: '爵士', label: '爵士' },
+// { value: '民谣', label: '民谣' },
+// { value: '说唱', label: '说唱' },
+// { value: '其他', label: '其他' },
+// ];
+// } else {
+// options = []; // 根据需要定义
+// }
+// break;
+// case 'dataType':
+// if (selectedCategory === '5') { // Game
+// options = [
+// { value: '压缩包', label: '压缩包' },
+// { value: '补丁', label: '补丁' },
+// { value: '安装包', label: '安装包' },
+// { value: 'nds', label: 'nds' },
+// { value: '其他', label: '其他' },
+// ];
+// } else {
+// options = []; // 根据需要定义
+// }
+// break;
+// case 'format': if(selectedCategory === '4') {
+// options = [
+// { value: 'ZIP', label: 'ZIP' },
+// { value: 'RAR', label: 'RAR' },
+// { value: '7Z', label: '7Z' },
+// { value: 'MKV', label: 'MKV' },
+// { value: 'MP4', label: 'MP4' },
+// { value: '其他', label: '其他' },
+// ];
+// } else if(selectedCategory === '6') {
+// options = [
+// { value: '1080P', label: '1080P' },
+// { value: '4K', label: '4K' },
+// { value: 'HD', label: 'HD' },
+// { value: '其他', label: '其他' },
+// ];
+// } else if(selectedCategory === '2') {
+// options = [ { value: '720p', label: '720p' },
+// { value: '1080p', label: '1080p' },
+// { value: '2K', label: '2K' },
+// { value: '4K', label: '4K' },
+// { value: '8K', label: '8K' },
+// { value: '其他', label: '其他' },
+// ];
+// } else {
+// options = []; // 根据需要定义
+// }
+// break;
+// case 'eventType':
+// if (selectedCategory === '7') { // Sports
+// options = [
+// { value: '足球', label: '足球' },
+// { value: '篮球', label: '篮球' },
+// { value: '网球', label: '网球' },
+// { value: '乒乓球', label: '乒乓球' },
+// { value: '羽毛球', label: '羽毛球' },
+// { value: '其他', label: '其他' },
+// ];
+// } else {
+// options = []; // 根据需要定义
+// }
+// break;
+// case 'source':
+// if (selectedCategory === '10') { // Documentary
+// options = [
+// { value: 'CCTV', label: 'CCTV' },
+// { value: '卫视', label: '卫视' },
+// { value: '国家地理', label: '国家地理' },
+// { value: 'BBC', label: 'BBC' },
+// { value: 'Discovery', label: 'Discovery' },
+// { value: '其他', label: '其他' },
+// ];
+// } else {
+// options = []; // 根据需要定义
+// }
+// break;
+// default:
+// options = []; // 默认无选项
+// }
+
+// return (
+// <select
+// key={filter.id}
+// name={filter.id}
+// value={filters[filter.id] || ''}
+// onChange={handleFilterChange}
+// className="border rounded px-2 py-1"
+// >
+// <option value="">全部</option>
+// {options.map(option => (
+// <option key={option.value} value={option.value}>
+// {option.label}
+// </option>
+// ))}
+// </select>
+// );
+// }
+// // 其他类型(如输入框)可以类似实现
+// return null;
+// })}
+// </div>
+// )}
+// </div>
+
+// {/* 筛选按钮 */}
+// <button
+// onClick={() => {
+// // 这里可以根据需要添加额外的筛选逻辑
+// // 例如,如果某些筛选需要联动,可以在这里处理
+// }}
+// className="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600 ml-4"
+// >
+// 应用筛选
+// </button>
+
+// {/* 种子列表 */}
+// <table className="w-full border-collapse mt-4">
+// <thead>
+// <tr className="bg-gray-200">
+// <th className="p-2 border">名称</th>
+// <th className="p-2 border">上传者</th>
+// <th className="p-2 border">描述</th>
+// <th className="p-2 border">上传时间</th>
+// <th className="p-2 border">下载次数</th>
+// <th className="p-2 border">促销方式</th>
+// <th className="p-2 border">操作</th>
+// </tr>
+// </thead>
+// <tbody>
+// {torrents.map(t => (
+// <tr key={t.torrentid} className="border-t hover:bg-gray-100">
+// <td className="p-2 border">{t.filename}</td>
+// <td className="p-2 border">{t.uploader_id}</td>
+// <td className="p-2 border">{t.description}</td>
+// <td className="p-2 border">{new Date(t.uploadTime).toLocaleString()}</td>
+// <td className="p-2 border">{t.downloadCount}</td>
+// <td className="p-2 border">
+// {(() => {
+// switch(t.promotionid) {
+// case 1: return '上传加倍';
+// case 2: return '下载免费';
+// case 3: return '下载减半';
+// case 0: return '没有促销';
+// default: return '没有促销';
+// }
+// })()}
+// </td>
+// <td className="p-2 border">
+// <button
+// onClick={() => handleDownload(t.torrentid)}
+// className="text-blue-500 hover:underline mr-2"
+// >
+// 下载
+// </button>
+// <a
+// href={`/torrent/${t.torrentid}`}
+// className="text-green-600 hover:underline"
+// >
+// 查看详情
+// </a>
+// </td>
+// </tr>
+// ))}
+// </tbody>
+// </table>
+
+// {/* 成功提示 */}
+// {showSuccess && (
+// <div className="mt-4 p-3 bg-green-100 text-green-800 border border-green-300 rounded">
+// 上传成功!
+// </div>
+// )}
+// </div>
+// );
+// }
+
+// export default TorrentList;
+import { useState, useEffect } from 'react';
+import axios from 'axios';
+
+// 常量配置集中管理
+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: '其他' },
+ ]
+ }
+ ]
+ }
+ // 其他分类配置...
+ }
+};
+
+// 获取分类筛选配置
+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 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 '没有促销';
+ }
+};
+
+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);
+
+ // 获取所有分类
+ useEffect(() => {
+ const fetchCategories = async () => {
+ try {
+ const res = await axios.get('http://localhost:8080/categories');
+ setCategories(res.data);
+ } catch (err) {
+ console.error('加载分类失败', err);
+ setError('加载分类失败,请稍后重试');
+ }
+ };
+ fetchCategories();
+ }, []);
+
+ // 获取种子数据
+ useEffect(() => {
+ const fetchTorrents = async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ let url = selectedCategory
+ // ? `http://localhost:8080/torrent/listByCategory?categoryid=${selectedCategory}`
+ ? `http://localhost:8080/torrent/listByCategorywithfilter?categoryid=${selectedCategory}`
+ : 'http://localhost:8080/torrent/list';
+
+ // 添加筛选参数
+ const params = new URLSearchParams();
+ Object.entries(filters).forEach(([key, value]) => {
+ if (value) params.append(key, value);
+ });
+
+ const res = await axios.get(`${url}&${params.toString()}`);
+ setTorrents(res.data);
+ console.log('torrents:', torrents);
+ } catch (err) {
+ console.error('获取种子失败', err);
+ setError('获取种子列表失败,请稍后重试');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ console.log('Fetching torrents with filters:', filters);
+ console.log('Selected category:', selectedCategory);
+ console.log(torrents);
+ const timer = setTimeout(fetchTorrents, 300); // 防抖
+ return () => clearTimeout(timer);
+ }, [selectedCategory, filters]);
+
+ // 切换分类时重置筛选条件
+ const handleCategoryChange = (categoryId) => {
+ setSelectedCategory(categoryId);
+ setFilters({});
+ };
+
+ // 处理筛选条件变化
+ 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');
+ };
+
+ // 获取当前分类的筛选配置
+ const currentFilters = getCategoryFilters(selectedCategory);
+
+ return (
+ <div className="p-4 max-w-7xl mx-auto">
+ <h1 className="text-2xl font-bold mb-6">种子列表</h1>
+
+ {/* 分类选择 */}
+ <div className="mb-6 bg-white p-4 rounded-lg shadow">
+ <div className="flex flex-wrap items-center gap-4">
+ <div className="flex items-center">
+ <label className="mr-2 font-medium whitespace-nowrap">选择分类:</label>
+ <select
+ value={selectedCategory}
+ onChange={(e) => handleCategoryChange(e.target.value)}
+ className="border rounded px-3 py-2 min-w-[150px]"
+ >
+ <option value="">全部分类</option>
+ {categories.map(cat => (
+ <option key={cat.categoryid} value={cat.categoryid}>
+ {cat.category_name}
+ </option>
+ ))}
+ </select>
+ </div>
+
+ {/* 动态筛选表单 */}
+ {currentFilters.length > 0 && (
+ <div className="flex flex-wrap gap-4">
+ {currentFilters.map(filter => (
+ <div key={filter.id} className="flex items-center">
+ <label className="mr-2 text-sm whitespace-nowrap">{filter.label}:</label>
+ <select
+ name={filter.id}
+ value={filters[filter.id] || ''}
+ onChange={handleFilterChange}
+ className="border rounded px-3 py-2 min-w-[120px]"
+ >
+ <option value="">全部</option>
+ {filter.options.map(option => (
+ <option key={option.value} value={option.value}>
+ {option.label}
+ </option>
+ ))}
+ </select>
+ </div>
+ ))}
+ </div>
+ )}
+ </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="flex justify-center items-center h-64">
+ <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
+ </div>
+ ) : (
+ /* 种子列表 */
+ <div className="bg-white rounded-lg shadow overflow-hidden">
+ <div className="overflow-x-auto">
+ <table className="w-full">
+ <thead className="bg-gray-50">
+ <tr>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">名称</th>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">大小</th>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">上传者</th>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">上传时间</th>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">下载次数</th>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">促销</th>
+ <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">操作</th>
+ </tr>
+ </thead>
+ <tbody className="bg-white divide-y divide-gray-200">
+ {torrents.length > 0 ? (
+ torrents.map(torrent => (
+ <tr key={torrent.torrentid} className="hover:bg-gray-50">
+ <td className="px-6 py-4 whitespace-nowrap">
+ <div className="text-sm font-medium text-gray-900">{torrent.filename}</div>
+ <div className="text-sm text-gray-500">{torrent.description}</div>
+ </td>
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
+ {torrent.torrentSize} B
+ </td>
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
+ {torrent.uploader_id}
+ </td>
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
+ {new Date(torrent.uploadTime).toLocaleString()}
+ </td>
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
+ {torrent.downloadCount}
+ </td>
+ <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
+ {getPromotionName(torrent.promotionid)}
+ </td>
+ <td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
+ <button
+ onClick={() => handleDownload(torrent.torrentid)}
+ className="text-blue-600 hover:text-blue-900 mr-4"
+ >
+ 下载
+ </button>
+ <a
+ href={`/torrent/${torrent.torrentid}`}
+ className="text-green-600 hover:text-green-900"
+ >
+ 详情
+ </a>
+ </td>
+ </tr>
+ ))
+ ) : (
+ <tr>
+ <td colSpan="7" className="px-6 py-4 text-center text-sm text-gray-500">
+ 没有找到符合条件的种子
+ </td>
+ </tr>
+ )}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ )}
+ </div>
+ );
+}
+
+export default TorrentList;
\ No newline at end of file
diff --git a/pt--frontend/src/components/upload.jsx b/pt--frontend/src/components/upload.jsx
new file mode 100644
index 0000000..c2cdc76
--- /dev/null
+++ b/pt--frontend/src/components/upload.jsx
@@ -0,0 +1,1461 @@
+// // import React, { useState, useEffect } from 'react';
+// // import axios from 'axios';
+
+// // function UploadTorrent() {
+// // const [title, setTitle] = useState('');
+// // const [description, setDescription] = useState('');
+// // const [categoryId, setCategoryId] = useState('');
+// // const [dpi, setDpi] = useState(''); // 可选字段
+// // const [caption, setCaption] = useState(''); // 可选字段
+// // const [file, setFile] = useState(null);
+// // const [categories, setCategories] = useState([]);
+// // const [showSuccess, setShowSuccess] = useState(false);
+
+// // useEffect(() => {
+// // axios.get('http://localhost:8080/categories')
+// // .then(res => setCategories(res.data))
+// // .catch(err => console.error('加载分类失败', err));
+// // }, []);
+
+// // const handleSubmit = async (e) => {
+// // e.preventDefault();
+// // if (!file) {
+// // alert('请选择一个 .torrent 文件');
+// // return;
+// // }
+
+// // if (!categoryId) {
+// // alert('请选择分类');
+// // return;
+// // }
+
+// // const formData = new FormData();
+// // formData.append('file', file);
+// // formData.append('title', title);
+// // formData.append('description', description);
+// // formData.append('categoryId', categoryId);
+// // if (dpi) formData.append('dpi', dpi); // 只有当 dpi 有值时才添加
+// // if (caption) formData.append('caption', caption); // 只有当 caption 有值时才添加
+
+// // try {
+// // await axios.post('http://localhost:8080/torrent/upload', formData, {
+// // headers: { 'Content-Type': 'multipart/form-data' },
+// // responseType: 'blob', // 关键:指定响应类型为 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('');
+// // setDpi('');
+// // setCaption('');
+// // setFile(null);
+// // } catch (err) {
+// // console.error('上传失败', err.response?.data || err.message);
+// // alert(err.response?.data || '上传失败,请检查后端是否启动');
+// // }
+// // };
+
+// // return (
+// // <div className="max-w-xl mx-auto mt-10 p-6 bg-white shadow rounded">
+// // <h2 className="text-2xl font-bold mb-4">上传种子</h2>
+// // <form onSubmit={handleSubmit} className="space-y-4">
+// // <input type="file" accept=".torrent" onChange={(e) => setFile(e.target.files[0])} />
+// // <input
+// // type="text"
+// // placeholder="标题"
+// // value={title}
+// // onChange={(e) => setTitle(e.target.value)}
+// // className="w-full p-2 border rounded"
+// // required
+// // />
+// // <textarea
+// // placeholder="描述"
+// // value={description}
+// // onChange={(e) => setDescription(e.target.value)}
+// // className="w-full p-2 border rounded"
+// // />
+// // <select
+// // value={categoryId}
+// // onChange={(e) => setCategoryId(e.target.value)}
+// // className="w-full p-2 border rounded"
+// // required
+// // >
+// // <option value="">请选择分类</option>
+// // {categories.map(cat => (
+// // <option key={cat.categoryid} value={cat.categoryid}>{cat.category_name}</option>
+// // ))}
+// // </select>
+// // <input
+// // type="text"
+// // placeholder="DPI(可选)"
+// // value={dpi}
+// // onChange={(e) => setDpi(e.target.value)}
+// // className="w-full p-2 border rounded"
+// // />
+// // <input
+// // type="text"
+// // placeholder="字幕(可选)"
+// // value={caption}
+// // onChange={(e) => setCaption(e.target.value)}
+// // className="w-full p-2 border rounded"
+// // />
+// // <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
+// // 上传
+// // </button>
+// // </form>
+
+// // {showSuccess && (
+// // <div className="mt-4 p-3 bg-green-100 text-green-800 border border-green-300 rounded">
+// // 上传成功!
+// // </div>
+// // )}
+// // </div>
+// // );
+// // }
+
+// // export default UploadTorrent;
+// import React, { useState, useEffect } from 'react';;
+// import axios from 'axios';
+
+// function UploadTorrent() {
+// const [title, setTitle] = useState('');
+// const [description, setDescription] = useState('');
+// const [categoryId, setCategoryId] = useState('');
+// const [dpi, setDpi] = useState('');
+// const [caption, setCaption] = useState('');
+// const [file, setFile] = useState(null);
+// const [categories, setCategories] = useState([]);
+// const [showSuccess, setShowSuccess] = useState(false);
+
+// useEffect(() => {
+// axios.get('http://localhost:8080/categories')
+// .then(res => setCategories(res.data))
+// .catch(err => console.error('加载分类失败', err));
+// }, []);
+
+// const handleSubmit = async (e) => {
+// e.preventDefault();
+// if (!file) {
+// alert('请选择一个 .torrent 文件');
+// return;
+// }
+
+// if (!categoryId) {
+// alert('请选择分类');
+// return;
+// }
+
+// const formData = new FormData();
+// formData.append('file', file);
+// formData.append('title', title);
+// formData.append('description', description);
+// formData.append('categoryId', categoryId);
+// if (dpi) formData.append('dpi', dpi);
+// if (caption) formData.append('caption', caption);
+
+// try {
+// // 发送上传请求
+// const response = await axios.post('http://localhost:8080/torrent/upload', formData, {
+// headers: { 'Content-Type': 'multipart/form-data' },
+// responseType: 'blob', // 关键:指定响应类型为 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('');
+// setDpi('');
+// setCaption('');
+// setFile(null);
+// } catch (err) {
+// console.error('上传失败', err.response?.data || err.message);
+// alert(err.response?.data || '上传失败,请检查后端是否启动');
+// }
+// };
+
+// return (
+// <div className="max-w-xl mx-auto mt-10 p-6 bg-white shadow rounded">
+// <h2 className="text-2xl font-bold mb-4">上传种子</h2>
+// <form onSubmit={handleSubmit} className="space-y-4">
+// <input type="file" accept=".torrent" onChange={(e) => setFile(e.target.files[0])} />
+// <input
+// type="text"
+// placeholder="标题"
+// value={title}
+// onChange={(e) => setTitle(e.target.value)}
+// className="w-full p-2 border rounded"
+// required
+// />
+// <textarea
+// placeholder="描述"
+// value={description}
+// onChange={(e) => setDescription(e.target.value)}
+// className="w-full p-2 border rounded"
+// />
+// <select
+// value={categoryId}
+// onChange={(e) => setCategoryId(e.target.value)}
+// className="w-full p-2 border rounded"
+// required
+// >
+// <option value="">请选择分类</option>
+// {categories.map(cat => (
+// <option key={cat.categoryid} value={cat.categoryid}>{cat.category_name}</option>
+// ))}
+// </select>
+// <input
+// type="text"
+// placeholder="DPI(可选)"
+// value={dpi}
+// onChange={(e) => setDpi(e.target.value)}
+// className="w-full p-2 border rounded"
+// />
+// <input
+// type="text"
+// placeholder="字幕/说明(可选)"
+// value={caption}
+// onChange={(e) => setCaption(e.target.value)}
+// className="w-full p-2 border rounded"
+// />
+// <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
+// 上传
+// </button>
+// </form>
+
+// {showSuccess && (
+// <div className="mt-4 p-3 bg-green-100 text-green-800 border border-green-300 rounded">
+// 上传成功!
+// </div>
+// )}
+// </div>
+// );
+// }
+
+// export default UploadTorrent;
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+
+function UploadTorrent() {
+ 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 [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 [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 (!file) {
+ alert('请选择一个 .torrent 文件');
+ return;
+ }
+
+ if (!categoryId) {
+ alert('请选择分类');
+ return;
+ }
+
+ const formData = new FormData();
+ formData.append('file', file);
+ 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-xl mx-auto mt-10 p-6 bg-white shadow rounded">
+ <h2 className="text-2xl font-bold mb-4">上传种子</h2>
+ <form onSubmit={handleSubmit} className="space-y-4">
+ <div>
+ <label className="block mb-1">种子文件 (.torrent)</label>
+ <input
+ type="file"
+ accept=".torrent"
+ onChange={(e) => setFile(e.target.files[0])}
+ className="w-full p-2 border rounded"
+ required
+ />
+ </div>
+
+ <div>
+ <label className="block mb-1">标题</label>
+ <input
+ type="text"
+ placeholder="标题"
+ value={title}
+ onChange={(e) => setTitle(e.target.value)}
+ className="w-full p-2 border rounded"
+ required
+ />
+ </div>
+
+ <div>
+ <label className="block mb-1">描述</label>
+ <textarea
+ placeholder="描述"
+ value={description}
+ onChange={(e) => setDescription(e.target.value)}
+ className="w-full p-2 border rounded"
+ />
+ </div>
+
+ <div>
+ <label className="block mb-1">分类</label>
+ <select
+ value={categoryId}
+ onChange={(e) => setCategoryId(e.target.value)}
+ className="w-full p-2 border rounded"
+ required
+ >
+ <option value="">请选择分类</option>
+ {categories.map(cat => (
+ <option key={cat.categoryid} value={cat.categoryid}>{cat.category_name}</option>
+ ))}
+ </select>
+ </div>
+
+ {/* <div>
+ <label className="block mb-1">DPI(可选)</label>
+ <input
+ type="text"
+ placeholder="DPI"
+ value={dpi}
+ onChange={(e) => setDpi(e.target.value)}
+ className="w-full p-2 border rounded"
+ />
+ </div>
+
+ <div>
+ <label className="block mb-1">字幕/说明(可选)</label>
+ <input
+ type="text"
+ placeholder="字幕/说明"
+ value={caption}
+ onChange={(e) => setCaption(e.target.value)}
+ className="w-full p-2 border rounded"
+ />
+ </div> */}
+
+ {/* 电影相关字段 */}
+ {showMovieFields && (
+ <>
+ <div>
+ <label className="block mb-1">字幕/说明</label>
+ <input
+ type="text"
+ placeholder="字幕/说明"
+ value={caption}
+ onChange={(e) => setCaption(e.target.value)}
+ className="w-full p-2 border rounded"
+ />
+ </div>
+ <div>
+ <label className="block mb-1">地区</label>
+ <select
+ value={region}
+ onChange={(e) => setRegion(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择地区</option>
+ {regions.map((regions) => (
+ <option key={regions.value} value={regions.value}>
+ {regions.label}
+ </option>
+ ))}
+ </select>
+ </div>
+ {/* <input
+ type="text"
+ placeholder="region"
+ value={region}
+ onChange={(e) => setRegion(e.target.value)}
+ className="w-full p-2 border rounded"
+ />
+ </div> */}
+
+ <div>
+ <label className="block mb-1">年份</label>
+ <input
+ type="text"
+ placeholder="年份"
+ value={year}
+ onChange={(e) => setYear(e.target.value)}
+ className="w-full p-2 border rounded"
+ />
+ </div>
+ <div>
+ <label className="block mb-1">类型</label>
+ {/* <input
+ type="text"
+ placeholder="类型"
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ <select
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择类型</option>
+ {genres.map((format) => (
+ <option key={format.value} value={format.value}>
+ {format.label}
+ </option>
+ ))}
+ </select>
+ </div>
+ {/* <div>
+ <label className="block mb-1">编码格式</label>
+ <input
+ type="text"
+ placeholder="如 H.264, H.265"
+ value={codecFormat}
+ onChange={(e) => setCodecFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ />
+ </div> */}
+ <div>
+ <label className="block mb-1">编码格式</label>
+ <select
+ value={codecFormat}
+ onChange={(e) => setCodecFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择编码格式</option>
+ {codecFormats.map((format) => (
+ <option key={format.value} value={format.value}>
+ {format.label}
+ </option>
+ ))}
+ </select>
+ </div>
+ <div>
+ <label className="block mb-1">分辨率</label>
+ <select
+ value={resolution}
+ onChange={(e) => setResolution(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择分辨率</option>
+ {resolutions.map((resolution) => (
+ <option key={resolution.value} value={resolution.value}>
+ {resolution.label}
+ </option>
+ ))}
+ </select>
+{/*
+ <input
+ type="text"
+ placeholder="如 1080p, 4K"
+ value={resolution}
+ onChange={(e) => setResolution(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ </>
+ )}
+ {/*剧集相关字段 */}
+ {showTvFields && (
+ <>
+ <div>
+ <label className="block mb-1">地区</label>
+ <select
+ value={region}
+ onChange={(e) => setRegion(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择地区</option>
+ {regions.map((regions) => (
+ <option key={regions.value} value={regions.value}>
+ {regions.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如 中国, 美国"
+ value={region}
+ onChange={(e) => setRegion(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">格式</label>
+ <select
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择格式</option>
+ {resolutions.map((format) => (
+ <option key={format.value} value={format.value}>
+ {format.label}
+ </option>
+ ))}
+ </select>
+{/* <input
+ type="text"
+ placeholder="如1080P, 4K"
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">类型</label>
+ <select
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择类型</option>
+ {genres.map((genre) => (
+ <option key={genre.value} value={genre.value}>
+ {genre.label}
+ </option>
+ ))}
+ </select>
+{/* <input
+ type="text"
+ placeholder="类型"
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ </>
+ )}
+
+ {/* 游戏相关字段 */}
+ {showGameFields && (
+ <>
+ <div>
+ <label className="block mb-1">平台</label>
+ <select
+ value={platform}
+ onChange={(e) => setPlatform(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择平台</option>
+ {platforms.map((platform) => (
+ <option key={platform.value} value={platform.value}>
+ {platform.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ <input
+ type="text"
+ placeholder="如 PC, PS5"
+ value={platform}
+ onChange={(e) => setPlatform(e.target.value)}
+ className="w-full p-2 border rounded"
+ />*/}
+ </div>
+ <div>
+ <label className="block mb-1">类型</label>
+ <select
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择类型</option>
+ {gamegenres.map((genre) => (
+ <option key={genre.value} value={genre.value}>
+ {genre.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="类型"
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">语言</label>
+ <select
+ value={language}
+ onChange={(e) => setLanguage(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择语言</option>
+ {languages.map((language) => (
+ <option key={language.value} value={language.value}>
+ {language.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如 中文, 英文"
+ value={language}
+ onChange={(e) => setLanguage(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">数据类型</label>
+ <select
+ value={dataType}
+ onChange={(e) => setdataType(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择数据类型</option>
+ {dataTypes.map((dataType) => (
+ <option key={dataType.value} value={dataType.value}>
+ {dataType.label}
+ </option>
+ ))}
+ </select>
+{/* <input
+ type="text"
+ placeholder="如压缩包,补丁"
+ value={dataType}
+ onChange={(e) => setdataType(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ </>
+ )}
+
+ {/* 综艺相关字段 */}
+ {showvarietyFields && (
+ <>
+ <div>
+ <label className="flex items-center">
+ <input
+ type="checkbox"
+ checked={isMainland}
+ onChange={(e) => setIsMainland(e.target.checked)}
+ className="mr-2"
+ />
+ 是否大陆综艺
+ </label>
+ </div>
+ <div>
+ <label className="block mb-1">类型</label>
+ <select
+ value={style}
+ onChange={(e) => setStyle(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择类型</option>
+ {varietygenres.map((style) => (
+ <option key={style.value} value={style.value}>
+ {style.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="类型"
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">格式</label>
+ <select
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择格式</option>
+ {resolutions.map((format) => (
+ <option key={format.value} value={format.value}>
+ {format.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如1080P, 4K"
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ </>
+ )}
+ {/* 动漫相关字段 */}
+ {showAnimeFields && (
+ <>
+ <div>
+ <label className="block mb-1">类型</label>
+ <select
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择类型</option>
+ {anigenres.map((genre) => (
+ <option key={genre.value} value={genre.value}>
+ {genre.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如治愈, 热血"
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">格式</label>
+ <select
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择格式</option>
+ {animeformats.map((format) => (
+ <option key={format.value} value={format.value}>
+ {format.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如1080P, 4K"
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">分辨率</label>
+ <select
+ value={resolution}
+ onChange={(e) => setResolution(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择分辨率</option>
+ {resolutions.map((resolution) => (
+ <option key={resolution.value} value={resolution.value}>
+ {resolution.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如 1080p, 4K"
+ value={resolution}
+ onChange={(e) => setResolution(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ </>
+ )}
+ {/* 学习相关字段 */}
+ {showlearningFields && (
+ <>
+ <div>
+ <label className="block mb-1">类型</label>
+ <select
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择类型</option>
+ {learninggenres.map((genre) => (
+ <option key={genre.value} value={genre.value}>
+ {genre.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如 课程, 讲座"
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">格式</label>
+ <select
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择格式</option>
+ {learningformats.map((format) => (
+ <option key={format.value} value={format.value}>
+ {format.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如1080P, 4K"
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ </>
+ )}
+ {/* 软件相关字段 */}
+ {showsoftwareFields && (
+ <>
+ <div>
+ <label className="block mb-1">平台</label>
+ <select
+ value={platform}
+ onChange={(e) => setPlatform(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择平台</option>
+ {softwareplatforms.map((platform) => (
+ <option key={platform.value} value={platform.value}>
+ {platform.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如 Windows, Mac"
+ value={platform}
+ onChange={(e) => setPlatform(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">类型</label>
+ <select
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择类型</option>
+ {softwaregenres.map((genre) => (
+ <option key={genre.value} value={genre.value}>
+ {genre.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如 学习,办公"
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">格式</label>
+ <select
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择格式</option>
+ {softwareformats.map((format) => (
+ <option key={format.value} value={format.value}>
+ {format.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如 ZIP, EXE"
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ </>
+ )}
+ {/* 体育相关字段 */}
+ {showsportsFields && (
+ <>
+ <div>
+ <label className="block mb-1">类型</label>
+ <select
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择类型</option>
+ {sportsgenres.map((genre) => (
+ <option key={genre.value} value={genre.value}>
+ {genre.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如 比赛, 训练"
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">格式</label>
+ <select
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择格式</option>
+ {resolutions.map((format) => (
+ <option key={format.value} value={format.value}>
+ {format.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如1080P, 4K"
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">赛事类型</label>
+ <select
+ value={eventType}
+ onChange={(e) => setEventType(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择赛事类型</option>
+ {eventTypes.map((eventType) => (
+ <option key={eventType.value} value={eventType.value}>
+ {eventType.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如足球、篮球"
+ value={eventType}
+ onChange={(e) => setEventType(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ </>
+ )}
+ {/* 纪录片相关字段 */}
+ {showdocFields && (
+ <>
+ <div>
+ <label className="block mb-1">年份</label>
+ <input
+ type="text"
+ placeholder="如 1999, 2020"
+ value={year}
+ onChange={(e) => setYear(e.target.value)}
+ className="w-full p-2 border rounded"
+ />
+ </div>
+ <div>
+ <label className="block mb-1">视频源</label>
+ <select
+ value={source}
+ onChange={(e) => setSource(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择视频源</option>
+ {sourceTypes.map((source) => (
+ <option key={source.value} value={source.value}>
+ {source.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如BlibliBili, YouTube"
+ value={source}
+ onChange={(e) => setSource(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">格式</label>
+ <select
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择格式</option>
+ {resolutions.map((format) => (
+ <option key={format.value} value={format.value}>
+ {format.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如1080P, 4K"
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ </>
+ )}
+ {/*音乐相关字段 */}
+ {showMusicFields && (
+ <>
+ <div>
+ <label className="block mb-1">类型</label>
+ <select
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择类型</option>
+ {musicgenres.map((genre) => (
+ <option key={genre.value} value={genre.value}>
+ {genre.label}
+ </option>
+ ))}
+ </select>
+{/*
+ <input
+ type="text"
+ placeholder="如专辑、单曲"
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">地区</label>
+ <select
+ value={region}
+ onChange={(e) => setRegion(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择地区</option>
+ {regions.map((regions) => (
+ <option key={regions.value} value={regions.value}>
+ {regions.label}
+ </option>
+ ))}
+ </select>
+{/* <input
+ type="text"
+ placeholder="如 中国, 美国"
+ value={region}
+ onChange={(e) => setRegion(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">风格</label>
+ <select
+ value={style}
+ onChange={(e) => setStyle(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择风格</option>
+ {musicstyles.map((style) => (
+ <option key={style.value} value={style.value}>
+ {style.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如流行、摇滚"
+ value={style}
+ onChange={(e) => setStyle(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ <div>
+ <label className="block mb-1">格式</label>
+ <select
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择格式</option>
+ {musicformats.map((format) => (
+ <option key={format.value} value={format.value}>
+ {format.label}
+ </option>
+ ))}
+ </select>
+{/* <input
+ type="text"
+ placeholder="如MP3, FLAC"
+ value={format}
+ onChange={(e) => setFormat(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ </>
+ )}
+ {/* 其他分类字段 */}
+ {showotherFields && (
+ <>
+ <div>
+ <label className="block mb-1">类型</label>
+ <select
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ >
+ <option value="">请选择类型</option>
+ {othergenres.map((genre) => (
+ <option key={genre.value} value={genre.value}>
+ {genre.label}
+ </option>
+ ))}
+ </select>
+ {/* <input
+ type="text"
+ placeholder="如视频、音频"
+ value={genre}
+ onChange={(e) => setGenre(e.target.value)}
+ className="w-full p-2 border rounded"
+ /> */}
+ </div>
+ </>
+ )}
+
+ {/* 其他分类字段... */}
+ {/* 提交按钮 */}
+
+ <button
+ type="submit"
+ className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
+ >
+ 上传
+ </button>
+ </form>
+
+ {showSuccess && (
+ <div className="mt-4 p-3 bg-green-100 text-green-800 border border-green-300 rounded">
+ 上传成功!
+ </div>
+ )}
+ </div>
+ );
+}
+
+export default UploadTorrent;
\ No newline at end of file
diff --git a/pt--frontend/src/index.css b/pt--frontend/src/index.css
new file mode 100644
index 0000000..08a3ac9
--- /dev/null
+++ b/pt--frontend/src/index.css
@@ -0,0 +1,68 @@
+:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
diff --git a/pt--frontend/src/main.jsx b/pt--frontend/src/main.jsx
new file mode 100644
index 0000000..055899d
--- /dev/null
+++ b/pt--frontend/src/main.jsx
@@ -0,0 +1,15 @@
+// src/main.jsx
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+import App from './App';
+
+import './index.css'; // 你的全局样式(如果有)
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+ <React.StrictMode>
+ <BrowserRouter>
+ <App />
+ </BrowserRouter>
+ </React.StrictMode>
+);
diff --git a/pt--frontend/src/pages/Home.jsx b/pt--frontend/src/pages/Home.jsx
new file mode 100644
index 0000000..2e3aa80
--- /dev/null
+++ b/pt--frontend/src/pages/Home.jsx
@@ -0,0 +1,146 @@
+// // src/pages/Home.jsx
+// import React from 'react';
+// import TorrentList from '../components/torrentlist';
+// import { Link } from 'react-router-dom';
+
+// const Home = () => {
+// return (
+// <div className="min-h-screen bg-gray-100 p-4">
+// <div className="flex justify-between items-center mb-4">
+// <h1 className="text-2xl font-bold">种子列表</h1>
+// <Link to="/upload" className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
+// 上传种子
+// </Link>
+// </div>
+// <TorrentList />
+// </div>
+// );
+// };
+
+// export default Home;
+import React, { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import TorrentList from '../components/torrentlist';
+import Post from '../components/Post';
+import FriendManager from '../components/FriendManager';
+import ChatBox from '../components/ChatBox';
+import RequestBoard from '../components/RequestBoard';
+import { getActivityPreviews, getFullActivities } from '../api/activity';
+
+const Home = () => {
+ const currentUser = {
+ id: 1,
+ username: '测试用户',
+ };
+
+ const [selectedRelation, setSelectedRelation] = useState(null);
+ 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 className="min-h-screen bg-gray-100 p-6">
+ <div className="flex justify-between items-center mb-6">
+ <h1 className="text-3xl font-bold">Pt站</h1>
+ <Link to="/upload" className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
+ 上传种子
+ </Link>
+ </div>
+
+ {/* 种子列表区域 */}
+ <div className="bg-white p-4 rounded shadow mb-8">
+ <h2 className="text-xl font-semibold mb-4">种子列表</h2>
+ <TorrentList />
+ </div>
+
+ {/* 活动区域 */}
+ <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>
+
+ {/* 主内容区 */}
+ <div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
+ {/* 帖子区域 */}
+ <div className="bg-white p-4 rounded shadow xl:col-span-1">
+ <h2 className="text-xl font-semibold mb-4">最新帖子</h2>
+ <Post />
+ </div>
+
+ {/* 好友管理区 */}
+ <div className="bg-white p-4 rounded shadow xl:col-span-1">
+ <h2 className="text-xl font-semibold mb-4">好友管理</h2>
+ <FriendManager
+ currentUser={currentUser}
+ onSelectRelation={setSelectedRelation}
+ />
+ </div>
+
+ {/* 聊天窗口 */}
+ {selectedRelation && (
+ <div className="bg-white p-4 rounded shadow xl:col-span-1">
+ <h2 className="text-xl font-semibold mb-4">聊天窗口</h2>
+ <ChatBox
+ senderId={currentUser.id}
+ receiverId={selectedRelation.friendId}
+ />
+ </div>
+ )}
+ </div>
+
+ {/* 求助帖区域 */}
+ <div className="bg-white p-4 mt-8 rounded shadow">
+ <h2 className="text-xl font-semibold mb-4">求助帖管理</h2>
+ <RequestBoard currentUserId={currentUser.id} />
+ </div>
+ </div>
+ );
+};
+
+export default Home;
\ No newline at end of file
diff --git a/pt--frontend/src/pages/Torrentdetail.jsx b/pt--frontend/src/pages/Torrentdetail.jsx
new file mode 100644
index 0000000..9aa9ba0
--- /dev/null
+++ b/pt--frontend/src/pages/Torrentdetail.jsx
@@ -0,0 +1,234 @@
+// // src/pages/TorrentDetail.jsx
+// import React, { useEffect, useState } from 'react';
+// import { useParams } from 'react-router-dom';
+// import axios from 'axios';
+
+// const TorrentDetail = () => {
+// const { id } = useParams(); // 获取 URL 中的 torrentid
+// const [torrent, setTorrent] = useState(null);
+// const [seeders, setSeeders] = useState([]);
+// const [loading, setLoading] = useState(true);
+// useEffect(() => {
+// console.log("Received Torrent:", torrent);
+// }, [torrent]);
+// useEffect(() => {
+// axios.get(`http://localhost:8080/torrent/${id}`)
+// .then(res => setTorrent(res.data))
+// setTorrent(res.data);
+// return axios.get(`http://localhost:8080/torrent/${res.data.infoHash}/seeders`);
+// }).then(res =>
+// setSeeders(res.data))
+// .catch(err => {
+// console.error("Error fetching torrent details:", err)
+// .finally(() => setLoading(false));
+// },[id]);
+
+// if (!torrent) return <div className="p-4">加载中...</div>;
+
+// // 格式化文件大小
+// 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';
+// };
+// return (
+// <div className="max-w-2xl mx-auto p-6 bg-white shadow rounded mt-6">
+// <h1 className="text-2xl font-bold mb-4">{torrent.torrentTitle}</h1>
+
+// <div className="space-y-2 text-gray-700">
+// <p><strong>简介:</strong>{torrent.description}</p>
+// {/* 修复上传人显示 */}
+// <p><strong>上传人:</strong>{torrent.uploader_id !== undefined ? torrent.uploader_id : '未知用户'}</p>
+// <p><strong>上传时间:</strong>{new Date(torrent.uploadTime).toLocaleString()}</p>
+// <p><strong>下载数:</strong>{torrent.downloadCount}</p>
+// <p><strong>文件大小:</strong>{torrent.torrentSize} B</p>
+// <p><strong>文件分辨率:</strong>{torrent.dpi}</p>
+// <p><strong>文件字幕:</strong>{torrent.caption}</p>
+// <p><strong>最后做种时间:</strong>{new Date(torrent.lastseed).toLocaleString()}</p>
+// </div>
+// {/* 做种者列表 */}
+// <div className="mt-6">
+// <h2 className="text-xl font-semibold mb-3">做种用户 ({seeders.length})</h2>
+// {seeders.length > 0 ? (
+// <div className="overflow-x-auto">
+// <table className="min-w-full bg-white border border-gray-200">
+// <thead className="bg-gray-100">
+// <tr>
+// <th className="py-2 px-4 border-b">用户名</th>
+// <th className="py-2 px-4 border-b">已上传</th>
+// <th className="py-2 px-4 border-b">上传速度</th>
+// <th className="py-2 px-4 border-b">已下载</th>
+// <th className="py-2 px-4 border-b">下载速度</th>
+// <th className="py-2 px-4 border-b">客户端</th>
+// <th className="py-2 px-4 border-b">最后活动</th>
+// </tr>
+// </thead>
+// <tbody>
+// {seeders.map((seeder, index) => (
+// <tr key={index} className={index % 2 === 0 ? 'bg-gray-50' : 'bg-white'}>
+// <td className="py-2 px-4 border-b text-center">{seeder.username}</td>
+// <td className="py-2 px-4 border-b text-center">{formatSize(seeder.uploaded)}</td>
+// <td className="py-2 px-4 border-b text-center text-green-600">
+// {formatSpeed(seeder.uploadSpeed)}
+// </td>
+// <td className="py-2 px-4 border-b text-center">{formatSize(seeder.downloaded)}</td>
+// <td className="py-2 px-4 border-b text-center">
+// {seeder.downloadSpeed > 0 ? formatSpeed(seeder.downloadSpeed) : '-'}
+// </td>
+// <td className="py-2 px-4 border-b text-center">{seeder.client}</td>
+// <td className="py-2 px-4 border-b text-center">
+// {new Date(seeder.lastActive).toLocaleTimeString()}
+// </td>
+// </tr>
+// ))}
+// </tbody>
+// </table>
+// </div>
+// ) : (
+// <div className="p-4 text-center text-gray-500 bg-gray-50 rounded">
+// 当前没有用户在做种
+// </div>
+// )}
+// </div>
+// </div>
+// );
+// };
+
+// export default TorrentDetail;
+// src/pages/TorrentDetail.jsx
+import React, { useEffect, useState } from 'react';
+import { useParams } from 'react-router-dom';
+import axios from 'axios';
+
+const TorrentDetail = () => {
+ const { id } = useParams();
+ const [torrent, setTorrent] = useState(null);
+ const [seeders, setSeeders] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ // 获取种子基本信息
+ axios.get(`http://localhost:8080/torrent/${id}`)
+ .then(res => {
+ setTorrent(res.data);
+ // 获取做种者信息
+ return axios.get(`http://localhost:8080/torrent/${res.data.infoHash}/seeders`);
+ })
+ .then(res => setSeeders(res.data))
+ .catch(err => console.error('获取数据失败', err))
+ .finally(() => setLoading(false));
+ }, [id]);
+
+ if (loading) return <div className="p-4">加载中...</div>;
+ if (!torrent) return <div className="p-4">种子不存在</div>;
+ console.log('Received Torrent:', torrent);
+ console.log('Received Seeders:', seeders);
+
+ // 格式化文件大小
+ 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';
+ };
+
+ return (
+ <div className="max-w-4xl mx-auto p-6 bg-white shadow rounded mt-6">
+ <h1 className="text-2xl font-bold mb-4">{torrent.torrentTitle}</h1>
+
+ {/* 种子基本信息 */}
+ <div className="mb-8 p-4 bg-gray-50 rounded">
+ <h2 className="text-xl font-semibold mb-3">基本信息</h2>
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-gray-700">
+ <div>
+ <p><strong>简介:</strong>{torrent.description || '暂无简介'}</p>
+ <p><strong>上传人:</strong>{torrent.uploader_id || '未知用户'}</p>
+ <p><strong>上传时间:</strong>{new Date(torrent.uploadTime).toLocaleString()}</p>
+ <p><strong>文件大小:</strong>{formatSize(torrent.torrentSize)}</p>
+ </div>
+ <div>
+ <p><strong>下载数:</strong>{torrent.downloadCount || 0}</p>
+ <p><strong>做种数:</strong>{seeders.length}</p>
+ <p><strong>文件分辨率:</strong>{torrent.dpi || '未知'}</p>
+ <p><strong>文件字幕:</strong>{torrent.caption || '无'}</p>
+ <p><strong>最后做种时间:</strong>{torrent.lastseed ? new Date(torrent.lastseed).toLocaleString() : '暂无'}</p>
+ </div>
+ </div>
+ </div>
+
+ {/* 做种者列表 */}
+ <div className="mt-6">
+ <h2 className="text-xl font-semibold mb-3">做种用户 ({seeders.length})</h2>
+ {seeders.length > 0 ? (
+ <div className="overflow-x-auto">
+ <table className="min-w-full bg-white border border-gray-200">
+ <thead className="bg-gray-100">
+ <tr>
+ <th className="py-2 px-4 border-b">用户名</th>
+ <th className="py-2 px-4 border-b">已上传</th>
+ <th className="py-2 px-4 border-b">上传时间</th>
+ <th className="py-2 px-4 border-b">完成时间</th>
+ <th className="py-2 px-4 border-b">上传速度</th>
+ <th className="py-2 px-4 border-b">已下载</th>
+ <th className="py-2 px-4 border-b">下载速度</th>
+ <th className="py-2 px-4 border-b">客户端</th>
+ <th className="py-2 px-4 border-b">端口</th>
+ <th className="py-2 px-4 border-b">最后活动</th>
+ </tr>
+ </thead>
+ <tbody>
+ {seeders.map((seeder, index) => (
+ <tr key={index} className={index % 2 === 0 ? 'bg-gray-50' : 'bg-white'}>
+ <td className="py-2 px-4 border-b text-center">{seeder.username}</td>
+ <td className="py-2 px-4 border-b text-center">{formatSize(seeder.uploaded)}</td>
+ <td className="py-2 px-4 border-b text-center text-green-600">
+ {new Date(seeder.createdAt).toLocaleString()}
+ </td>
+ <td className="py-2 px-4 border-b text-center text-green-600">
+ {new Date(seeder.completed_time).toLocaleString()}
+ </td>
+ <td className="py-2 px-4 border-b text-center text-green-600">
+ {formatSpeed(seeder.uploadSpeed)}
+ </td>
+ <td className="py-2 px-4 border-b text-center">{formatSize(seeder.downloaded)}</td>
+ <td className="py-2 px-4 border-b text-center">
+ {seeder.downloadSpeed > 0 ? formatSpeed(seeder.downloadSpeed) : '-'}
+ </td>
+ <td className="py-2 px-4 border-b text-center">{seeder.client}</td>
+ <td className="py-2 px-4 border-b text-center">{seeder.port}</td>
+ <td className="py-2 px-4 border-b text-center">
+ {seeder.lastEvent}
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ ) : (
+ <div className="p-4 text-center text-gray-500 bg-gray-50 rounded">
+ 当前没有用户在做种
+ </div>
+ )}
+ </div>
+ </div>
+ );
+};
+
+export default TorrentDetail;
\ No newline at end of file
diff --git a/pt--frontend/src/pages/UploadPage.jsx b/pt--frontend/src/pages/UploadPage.jsx
new file mode 100644
index 0000000..d0ea71b
--- /dev/null
+++ b/pt--frontend/src/pages/UploadPage.jsx
@@ -0,0 +1,14 @@
+// src/pages/UploadPage.jsx
+import React from 'react';
+import UploadTorrent from '../components/upload';
+
+const UploadPage = () => {
+ return (
+ <div className="min-h-screen bg-gray-100 p-4">
+ <h1 className="text-2xl font-bold mb-4">上传种子</h1>
+ <UploadTorrent />
+ </div>
+ );
+};
+
+export default UploadPage;