Merge "新增求种和个人中心接口"
diff --git a/front/src/BegInfo.js b/front/src/BegInfo.js
index 1b3e2af..8ea3f6b 100644
--- a/front/src/BegInfo.js
+++ b/front/src/BegInfo.js
@@ -1,7 +1,8 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
+import { API_BASE_URL } from "./config";
-// 求种任务示例数据
+// 求种任务示例数据(作为后备数据)
const begSeedList = [
{
beg_id: "beg001",
@@ -45,61 +46,311 @@
export default function BegInfo() {
const { begid } = useParams();
- const beg = begSeedList.find((b) => b.beg_id === begid);
- const [seeds, setSeeds] = useState(
- submitSeedList.filter((s) => s.beg_id === begid)
- );
+ const [beg, setBeg] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [seeds, setSeeds] = useState([]);
+ const [seedInfoMap, setSeedInfoMap] = useState({});
const [showForm, setShowForm] = useState(false);
+ const [userSeeds, setUserSeeds] = useState([]);
+ const [loadingUserSeeds, setLoadingUserSeeds] = useState(false);
const [formData, setFormData] = useState({
- title: "",
- subtitle: "",
- torrentFile: null,
+ selectedSeedId: "",
});
- if (!beg) return <div style={{ padding: 40 }}>未找到该求种信息</div>;
+ // 从后端获取求种详情
+ const fetchBegSeedDetail = async () => {
+ setLoading(true);
+ try {
+ const response = await fetch(`${API_BASE_URL}/api/begseed-detail?begid=${begid}`);
+ if (!response.ok) {
+ throw new Error(`请求失败,状态码: ${response.status}`);
+ }
+ const data = await response.json();
+
+
+ // 格式化数据以匹配前端期望的格式
+ const formattedBeg = {
+ beg_id: data.beg_id || data.begid || data.id,
+ info: data.info || data.description || data.content,
+ beg_count: data.beg_count || data.begCount || 1,
+ reward_magic: data.reward_magic || data.rewardMagic || data.magic,
+ deadline: data.deadline || data.endtime,
+ has_match: data.has_match || data.hasMatch || data.completed || 0,
+ };
+
+ setBeg(formattedBeg);
+ setError(null);
+ } catch (err) {
+ console.error('获取求种详情失败:', err);
+ setError(err.message);
+ // 如果API调用失败,使用默认数据
+ const fallbackBeg = begSeedList.find((b) => b.beg_id === begid);
+ setBeg(fallbackBeg || null);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 从后端获取已提交的种子列表
+ const fetchSubmittedSeeds = async () => {
+ try {
+ const response = await fetch(`${API_BASE_URL}/api/begseed-submissions?begid=${begid}`);
+ if (!response.ok) {
+ throw new Error(`请求失败,状态码: ${response.status}`);
+ }
+ const data = await response.json();
+ console.log('获取到的种子提交数据:', data);
+
+ // 新的数据结构:数组,每个元素包含seed对象和votes字段
+ const submissions = Array.isArray(data) ? data : [];
+
+ // 格式化种子数据
+ const formattedSeeds = submissions.map(item => ({
+ seed_id: item.seed?.seedid || item.seedid,
+ beg_id: begid,
+ votes: item.votes || 0, // 每个种子单独的投票数
+ title: item.seed?.title || item.title || "未知标题",
+ subtitle: item.seed?.subtitle || item.subtitle || "无简介",
+ seedsize: item.seed?.seedsize || item.seedsize,
+ downloadtimes: item.seed?.downloadtimes || item.downloadtimes || 0,
+ url: item.seed?.url || item.url,
+ user: item.seed?.user || item.user
+ }));
+
+ // 构建种子信息映射
+ const newSeedInfoMap = {};
+ submissions.forEach(item => {
+ const seedId = item.seed?.seedid || item.seedid;
+ if (seedId) {
+ newSeedInfoMap[seedId] = {
+ title: item.seed?.title || item.title || "未知标题",
+ subtitle: item.seed?.subtitle || item.subtitle || "无简介",
+ };
+ }
+ });
+
+ setSeeds(formattedSeeds);
+ setSeedInfoMap(newSeedInfoMap);
+ } catch (err) {
+ console.error('获取种子提交列表失败:', err);
+ // 如果API调用失败,使用默认数据
+ const fallbackSeeds = submitSeedList.filter((s) => s.beg_id === begid);
+ setSeeds(fallbackSeeds);
+ setSeedInfoMap(seedInfoMap);
+ }
+ };
+
+ // 组件挂载时获取数据
+ useEffect(() => {
+ fetchBegSeedDetail();
+ fetchSubmittedSeeds();
+ }, [begid]);
+
+ // 加载状态
+ if (loading) {
+ return (
+ <div className="container">
+ <div style={{ textAlign: "center", margin: "40px 0", color: "#666" }}>
+ 正在加载求种详情...
+ </div>
+ </div>
+ );
+ }
+
+ // 未找到求种信息
+ if (!beg) {
+ return (
+ <div className="container">
+ <div style={{ padding: 40, textAlign: "center", color: "#666" }}>
+ 未找到该求种信息
+ </div>
+ </div>
+ );
+ }
const isExpired = new Date(beg.deadline) < new Date();
const isFinished = beg.has_match === 1;
const isActive = !isExpired && !isFinished;
- // 投票功能(前端+1)
- const handleVote = (seed_id) => {
- setSeeds((prev) =>
- prev.map((s) =>
- s.seed_id === seed_id ? { ...s, votes: s.votes + 1 } : s
- )
- );
+ // 获取用户的所有种子
+ const fetchUserSeeds = async () => {
+ setLoadingUserSeeds(true);
+ try {
+ // 获取用户ID
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userId = match ? match[2] : null;
+
+ if (!userId) {
+ alert("请先登录后再获取种子列表");
+ setLoadingUserSeeds(false);
+ return;
+ }
+
+ const response = await fetch(`${API_BASE_URL}/api/user-seeds?userid=${userId}`);
+ if (!response.ok) {
+ throw new Error(`请求失败,状态码: ${response.status}`);
+ }
+ const data = await response.json();
+
+ // 格式化种子数据
+ const formattedSeeds = Array.isArray(data) ? data.map(seed => ({
+ seedid: seed.seedid || seed.id,
+ title: seed.title || "未知标题",
+ subtitle: seed.subtitle || "无简介",
+ seedsize: seed.seedsize,
+ downloadtimes: seed.downloadtimes || 0,
+ url: seed.url
+ })) : [];
+
+ setUserSeeds(formattedSeeds);
+ } catch (err) {
+ console.error('获取用户种子失败:', err);
+ alert(`获取种子列表失败: ${err.message}`);
+ } finally {
+ setLoadingUserSeeds(false);
+ }
+ };
+
+ // 投票功能(发送到后端)
+ const handleVote = async (seed_id) => {
+ try {
+ // 获取用户ID
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userId = match ? match[2] : null;
+
+ if (!userId) {
+ alert("请先登录后再投票");
+ return;
+ }
+
+ const response = await fetch(`${API_BASE_URL}/api/vote-seed`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ userid: userId,
+ seedid: seed_id,
+ begid: begid,
+ }),
+ });
+
+
+ if (response.ok) {
+ // 投票成功,重新获取数据以更新投票计数
+ await fetchSubmittedSeeds();
+ alert("投票成功!");
+ } else if (response.status === 409) {
+ alert("您已投过票,不能重复投票");
+ }
+ else {
+ const errorData = await response.json();
+ alert(`投票失败: ${errorData.message || '未知错误'}`);
+ }
+ } catch (err) {
+ console.error('投票失败:', err);
+ // 如果后端调用失败,更新本地状态作为后备
+ setSeeds((prev) =>
+ prev.map((s) =>
+ s.seed_id === seed_id ? { ...s, votes: s.votes + 1 } : s
+ )
+ );
+ alert("投票成功(前端演示)");
+ }
};
// 上传表单处理
const handleFormChange = (e) => {
- const { name, value, files } = e.target;
- if (name === "torrentFile") {
- setFormData((f) => ({ ...f, torrentFile: files[0] }));
- } else {
- setFormData((f) => ({ ...f, [name]: value }));
- }
+ const { name, value } = e.target;
+ setFormData((f) => ({ ...f, [name]: value }));
};
- const handleSubmitSeed = (e) => {
+ const handleSubmitSeed = async (e) => {
e.preventDefault();
- // 这里只做前端演示,实际应上传到后端
- const newSeedId = "seed" + Math.floor(Math.random() * 10000);
- setSeeds((prev) => [
- ...prev,
- {
- beg_id: begid,
- seed_id: newSeedId,
- votes: 0,
- },
- ]);
- seedInfoMap[newSeedId] = {
- title: formData.title,
- subtitle: formData.subtitle,
- };
- setShowForm(false);
- setFormData({ title: "", subtitle: "", torrentFile: null });
- alert("提交成功(前端演示)");
+
+ if (!formData.selectedSeedId) {
+ alert("请选择一个种子");
+ return;
+ }
+
+ try {
+ // 获取用户ID
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userId = match ? match[2] : null;
+
+ if (!userId) {
+ alert("请先登录后再提交种子");
+ return;
+ }
+ // console.log('提交种子数据:', {
+ // userid: userId,
+ // begid: begid,
+ // seedid: formData.selectedSeedId,
+ // });
+
+ const response = await fetch(`${API_BASE_URL}/api/submit-seed`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ userid: userId,
+ begid: begid,
+ seedid: formData.selectedSeedId,
+ }),
+ });
+
+ if (response.ok) {
+ // 提交成功,重新获取所有数据以刷新页面
+ await Promise.all([
+ fetchBegSeedDetail(),
+ fetchSubmittedSeeds()
+ ]);
+ setShowForm(false);
+ setFormData({ selectedSeedId: "" });
+ setUserSeeds([]);
+ alert("提交成功!");
+ } else {
+ const errorData = await response.json();
+ alert(`提交失败: ${errorData.message || '未知错误'}`);
+ }
+ } catch (err) {
+ console.error('提交种子失败:', err);
+ // 如果后端调用失败,使用前端演示逻辑
+ const newSeedId = "seed" + Math.floor(Math.random() * 10000);
+
+ // 从用户种子列表中找到选中种子的信息
+ const selectedSeed = userSeeds.find(seed => seed.seedid === formData.selectedSeedId);
+
+ setSeeds((prev) => [
+ ...prev,
+ {
+ beg_id: begid,
+ seed_id: newSeedId,
+ votes: 0,
+ title: selectedSeed?.title || "未知标题",
+ subtitle: selectedSeed?.subtitle || "无简介",
+ seedsize: selectedSeed?.seedsize,
+ downloadtimes: selectedSeed?.downloadtimes || 0,
+ url: selectedSeed?.url,
+ user: { username: "当前用户" }
+ },
+ ]);
+
+ setSeedInfoMap(prev => ({
+ ...prev,
+ [newSeedId]: {
+ title: selectedSeed?.title || "未知标题",
+ subtitle: selectedSeed?.subtitle || "无简介",
+ }
+ }));
+
+ setShowForm(false);
+ setFormData({ selectedSeedId: "" });
+ setUserSeeds([]);
+ alert("提交成功(前端演示)");
+ }
};
return (
@@ -107,6 +358,21 @@
<h1 style={{ margin: "24px 0 32px 0", color: "#1976d2" }}>
求种详情
</h1>
+
+ {/* 错误状态 */}
+ {error && (
+ <div style={{
+ textAlign: "center",
+ margin: "20px 0",
+ padding: "10px",
+ background: "#ffebee",
+ color: "#c62828",
+ borderRadius: "4px"
+ }}>
+ 加载失败: {error} (已显示默认数据)
+ </div>
+ )}
+
<div
style={{
background: "#e3f7e7",
@@ -135,26 +401,36 @@
</div>
<h2 style={{ margin: "24px 0 12px 0" }}>已提交种子</h2>
- <table className="movie-table" style={{ maxWidth: 700, margin: "0 auto" }}>
+ <table className="movie-table" style={{ maxWidth: 1000, margin: "0 auto" }}>
<thead>
<tr>
<th>标题</th>
<th>简介</th>
+ <th>文件大小</th>
+ <th>下载次数</th>
<th>投票数</th>
+ <th>上传者</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{seeds.length === 0 ? (
<tr>
- <td colSpan={4} style={{ textAlign: "center" }}>暂无提交的种子</td>
+ <td colSpan={7} style={{ textAlign: "center" }}>暂无提交的种子</td>
</tr>
) : (
seeds.map((s) => (
<tr key={s.seed_id}>
- <td>{seedInfoMap[s.seed_id]?.title || "未知标题"}</td>
- <td>{seedInfoMap[s.seed_id]?.subtitle || "无简介"}</td>
- <td>{s.votes}</td>
+ <td>
+ <a href={`/torrent/${s.seed_id}`} style={{ color: '#1a237e', textDecoration: 'none' }}>
+ {s.title}
+ </a>
+ </td>
+ <td>{s.subtitle || "无简介"}</td>
+ <td>{s.seedsize ? `${s.seedsize} MB` : "未知"}</td>
+ <td>{s.downloadtimes || 0}</td>
+ <td style={{ fontWeight: 'bold', color: '#1976d2' }}>{s.votes || 0}</td>
+ <td>{s.user?.username || "未知用户"}</td>
<td>
{isActive ? (
<button
@@ -182,10 +458,20 @@
</tbody>
</table>
+ {/* 显示总投票数 */}
+ {seeds.length > 0 && (
+ <div style={{ textAlign: "center", margin: "16px 0", color: "#666" }}>
+ 总投票数: {seeds.reduce((total, seed) => total + (seed.votes || 0), 0)}
+ </div>
+ )}
+
{isActive && (
<div style={{ margin: "32px 0", textAlign: "center" }}>
<button
- onClick={() => setShowForm(true)}
+ onClick={() => {
+ setShowForm(true);
+ fetchUserSeeds();
+ }}
style={{
fontSize: 18,
padding: "12px 36px",
@@ -199,7 +485,7 @@
transition: "background 0.2s",
}}
>
- 提交悬赏种子
+ 提交种子
</button>
</div>
)}
@@ -216,60 +502,76 @@
boxShadow: "0 2px 8px #e0e7ff",
}}
>
- <h3 style={{ color: "#1976d2", marginBottom: 18 }}>上传种子</h3>
+ <h3 style={{ color: "#1976d2", marginBottom: 18 }}>选择种子</h3>
+
+ {/* 加载用户种子状态 */}
+ {loadingUserSeeds && (
+ <div style={{ textAlign: "center", margin: "16px 0", color: "#666" }}>
+ 正在加载您的种子列表...
+ </div>
+ )}
+
+ {/* 选择已有种子 */}
+ {userSeeds.length > 0 ? (
+ <div style={{ marginBottom: 24 }}>
+ <div style={{ marginBottom: 16 }}>
+ <label style={{ display: "inline-block", width: 80, fontWeight: 500 }}>选择种子:</label>
+ <select
+ name="selectedSeedId"
+ value={formData.selectedSeedId}
+ onChange={handleFormChange}
+ style={{
+ padding: "8px 12px",
+ borderRadius: 6,
+ border: "1px solid #b2d8ea",
+ width: 300,
+ background: "#fff",
+ fontSize: 14,
+ }}
+ >
+ <option value="">请选择一个种子</option>
+ {userSeeds.map((seed) => (
+ <option key={seed.seedid} value={seed.seedid}>
+ {seed.title} - {seed.subtitle || "无简介"} ({seed.seedsize ? `${seed.seedsize} MB` : "未知大小"})
+ </option>
+ ))}
+ </select>
+ </div>
+ {formData.selectedSeedId && (
+ <div style={{
+ padding: 12,
+ background: "#e8f5e8",
+ borderRadius: 6,
+ border: "1px solid #4caf50",
+ color: "#2e7d32"
+ }}>
+ ✓ 已选择种子,点击提交即可使用此种子
+ </div>
+ )}
+ </div>
+ ) : (
+ !loadingUserSeeds && (
+ <div style={{
+ textAlign: "center",
+ margin: "20px 0",
+ padding: "16px",
+ background: "#fff3cd",
+ color: "#856404",
+ border: "1px solid #ffeaa7",
+ borderRadius: 6
+ }}>
+ 您还没有上传过种子,无法参与悬赏
+ </div>
+ )
+ )}
+
<form onSubmit={handleSubmitSeed}>
- <div style={{ marginBottom: 16 }}>
- <label style={{ display: "inline-block", width: 60 }}>标题:</label>
- <input
- type="text"
- name="title"
- value={formData.title}
- onChange={handleFormChange}
- required
- style={{
- padding: "6px 12px",
- borderRadius: 6,
- border: "1px solid #b2d8ea",
- width: 280,
- }}
- />
- </div>
- <div style={{ marginBottom: 16 }}>
- <label style={{ display: "inline-block", width: 60 }}>简介:</label>
- <input
- type="text"
- name="subtitle"
- value={formData.subtitle}
- onChange={handleFormChange}
- style={{
- padding: "6px 12px",
- borderRadius: 6,
- border: "1px solid #b2d8ea",
- width: 280,
- }}
- />
- </div>
- <div style={{ marginBottom: 16 }}>
- <label style={{ display: "inline-block", width: 60 }}>种子文件:</label>
- <input
- type="file"
- name="torrentFile"
- accept=".torrent"
- onChange={handleFormChange}
- required
- style={{
- padding: "6px 0",
- borderRadius: 6,
- border: "1px solid #b2d8ea",
- width: 280,
- }}
- />
- </div>
<div style={{ marginTop: 18 }}>
<button
type="submit"
+ disabled={!formData.selectedSeedId}
style={{
- background: "#1976d2",
+ background: formData.selectedSeedId ? "#1976d2" : "#b0b0b0",
color: "#fff",
border: "none",
borderRadius: 6,
@@ -277,14 +579,18 @@
fontWeight: 500,
fontSize: 16,
marginRight: 18,
- cursor: "pointer",
+ cursor: formData.selectedSeedId ? "pointer" : "not-allowed",
}}
>
- 提交
+ 提交种子
</button>
<button
type="button"
- onClick={() => setShowForm(false)}
+ onClick={() => {
+ setShowForm(false);
+ setFormData({ selectedSeedId: "" });
+ setUserSeeds([]);
+ }}
style={{
background: "#b0b0b0",
color: "#fff",
diff --git a/front/src/BegSeedPage.js b/front/src/BegSeedPage.js
index a456147..0854535 100644
--- a/front/src/BegSeedPage.js
+++ b/front/src/BegSeedPage.js
@@ -1,46 +1,85 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
import HelpIcon from "@mui/icons-material/Help";
import { useNavigate } from "react-router-dom";
-
-// 初始硬编码数据
-const initialBegSeedList = [
- {
- beg_id: "beg001",
- info: "求《三体》高清资源",
- beg_count: 5,
- reward_magic: 100,
- deadline: "2025-06-10T23:59:59",
- has_match: 0,
- },
- {
- beg_id: "beg002",
- info: "求《灌篮高手》国语配音版",
- beg_count: 3,
- reward_magic: 50,
- deadline: "2024-05-01T23:59:59",
- has_match: 1,
- },
- {
- beg_id: "beg003",
- info: "求《黑暗之魂3》PC版种子",
- beg_count: 2,
- reward_magic: 80,
- deadline: "2024-04-01T23:59:59",
- has_match: 0,
- },
-];
+import { API_BASE_URL } from "./config";
export default function BegSeedPage() {
const navigate = useNavigate();
const now = new Date();
- const [begSeedList, setBegSeedList] = useState(initialBegSeedList);
+ const [begSeedList, setBegSeedList] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
const [showForm, setShowForm] = useState(false);
+ const [refreshKey, setRefreshKey] = useState(0); // 用于强制重新渲染
const [formData, setFormData] = useState({
info: "",
reward_magic: "",
deadline: "",
});
+ // 从后端获取求种列表
+ const fetchBegSeedList = async () => {
+ setLoading(true);
+ try {
+ const response = await fetch(`${API_BASE_URL}/api/begseed-list`);
+ if (!response.ok) {
+ throw new Error(`请求失败,状态码: ${response.status}`);
+ }
+ const data = await response.json();
+ console.log("获取到的求种列表数据:", data);
+
+ // 格式化数据以匹配前端期望的格式
+ const formattedData = data.map(item => ({
+ beg_id: item.beg_id || item.begid || item.id,
+ info: item.info || item.description || item.content,
+ beg_count: item.beg_count || item.begCount || 1,
+ reward_magic: item.reward_magic || item.rewardMagic || item.magic,
+ deadline: item.deadline || item.endtime,
+ has_match: item.has_match || item.hasMatch || item.completed || 0,
+ }));
+
+ setBegSeedList(formattedData);
+ setError(null);
+ } catch (err) {
+ console.error('获取求种列表失败:', err);
+ setError(err.message);
+ // 如果API调用失败,使用默认数据
+ setBegSeedList([
+ {
+ beg_id: "beg001",
+ info: "求《三体》高清资源",
+ beg_count: 5,
+ reward_magic: 100,
+ deadline: "2025-06-10T23:59:59",
+ has_match: 0,
+ },
+ {
+ beg_id: "beg002",
+ info: "求《灌篮高手》国语配音版",
+ beg_count: 3,
+ reward_magic: 50,
+ deadline: "2024-05-01T23:59:59",
+ has_match: 1,
+ },
+ {
+ beg_id: "beg003",
+ info: "求《黑暗之魂3》PC版种子",
+ beg_count: 2,
+ reward_magic: 80,
+ deadline: "2024-04-01T23:59:59",
+ has_match: 0,
+ },
+ ]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 组件挂载时获取数据
+ useEffect(() => {
+ fetchBegSeedList();
+ }, []);
+
// 表单输入处理
const handleFormChange = (e) => {
const { name, value } = e.target;
@@ -51,23 +90,66 @@
};
// 提交新求种任务
- const handleSubmit = (e) => {
+ const handleSubmit = async (e) => {
e.preventDefault();
- const newBegId = "beg" + Math.floor(Math.random() * 10000);
- setBegSeedList([
- {
- beg_id: newBegId,
- info: formData.info,
- beg_count: 1,
- reward_magic: Number(formData.reward_magic),
- deadline: formData.deadline,
- has_match: 0,
- },
- ...begSeedList,
- ]);
- setShowForm(false);
- setFormData({ info: "", reward_magic: "", deadline: "" });
- alert("发布成功(前端演示)");
+
+ // 获取用户ID
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userId = match ? match[2] : null;
+
+ if (!userId) {
+ alert("请先登录后再发布求种任务");
+ return;
+ }
+
+ try {
+ const response = await fetch(`${API_BASE_URL}/api/create-begseed`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ userid: userId,
+ info: formData.info,
+ reward_magic: Number(formData.reward_magic),
+ deadline: formData.deadline,
+ }),
+ });
+
+ if (response.ok) {
+ // 成功创建,重新获取列表并强制重新渲染
+ setLoading(true); // 显示加载状态
+ await fetchBegSeedList();
+ setShowForm(false);
+ setFormData({ info: "", reward_magic: "", deadline: "" });
+ setRefreshKey(prev => prev + 1); // 强制重新渲染
+ alert("发布成功!");
+ } else {
+ const errorData = await response.json();
+ alert(`发布失败: ${errorData.message || '未知错误'}`);
+ }
+ } catch (err) {
+ console.error('发布求种任务失败:', err);
+ // 如果后端调用失败,则使用前端演示逻辑
+ // setLoading(true); // 显示加载状态
+ // const newBegId = "beg" + Math.floor(Math.random() * 10000);
+ // setBegSeedList([
+ // {
+ // beg_id: newBegId,
+ // info: formData.info,
+ // beg_count: 1,
+ // reward_magic: Number(formData.reward_magic),
+ // deadline: formData.deadline,
+ // has_match: 0,
+ // },
+ // ...begSeedList,
+ // ]);
+ // setLoading(false); // 隐藏加载状态
+ // setShowForm(false);
+ // setFormData({ info: "", reward_magic: "", deadline: "" });
+ // setRefreshKey(prev => prev + 1); // 强制重新渲染
+ // alert("发布成功(前端演示)");
+ }
};
return (
@@ -76,19 +158,42 @@
<HelpIcon style={{ verticalAlign: "middle", marginRight: 8 }} />
求种列表
</h1>
+
+ {/* 加载状态 */}
+ {loading && (
+ <div style={{ textAlign: "center", margin: "40px 0", color: "#666" }}>
+ 正在加载求种列表...
+ </div>
+ )}
+
+ {/* 错误状态 */}
+ {error && (
+ <div style={{
+ textAlign: "center",
+ margin: "20px 0",
+ padding: "10px",
+ background: "#ffebee",
+ color: "#c62828",
+ borderRadius: "4px"
+ }}>
+ 加载失败: {error} (已显示默认数据)
+ </div>
+ )}
+
<div style={{ margin: "0 0 32px 0", textAlign: "center" }}>
<button
onClick={() => setShowForm(true)}
+ disabled={loading}
style={{
fontSize: 18,
padding: "12px 36px",
- background: "linear-gradient(90deg, #42a5f5 0%, #1976d2 100%)",
+ background: loading ? "#ccc" : "linear-gradient(90deg, #42a5f5 0%, #1976d2 100%)",
color: "#fff",
border: "none",
borderRadius: 8,
fontWeight: 600,
boxShadow: "0 2px 8px #b2d8ea",
- cursor: "pointer",
+ cursor: loading ? "not-allowed" : "pointer",
transition: "background 0.2s",
}}
>
@@ -195,7 +300,7 @@
</form>
</div>
)}
- <div style={{ display: "flex", flexWrap: "wrap", gap: 24 }}>
+ <div key={refreshKey} style={{ display: "flex", flexWrap: "wrap", gap: 24 }}>
{begSeedList.map((beg) => {
const isExpired =
new Date(beg.deadline) < now || beg.has_match === 1;
diff --git a/front/src/MigrationPage.js b/front/src/MigrationPage.js
index f29a534..1a9f79b 100644
--- a/front/src/MigrationPage.js
+++ b/front/src/MigrationPage.js
@@ -32,6 +32,7 @@
const res = await fetch(`${API_BASE_URL}/api/migrations`);
if (!res.ok) throw new Error(`请求失败,状态码 ${res.status}`);
const data = await res.json();
+ console.log("Fetched migrations:", data);
const formatted = data.map(item => ({
migration_id: item.profileurl,
user_id: item.user.userid,
diff --git a/front/src/UserProfile.js b/front/src/UserProfile.js
index f6ef9f4..eed84da 100644
--- a/front/src/UserProfile.js
+++ b/front/src/UserProfile.js
@@ -28,7 +28,7 @@
const [userStats, setUserStats] = useState({
magic: 0,
upload: 0,
- download: 0,
+ viptime: 0,
ratio: 0,
});
@@ -42,13 +42,18 @@
const [exchangeResult, setExchangeResult] = useState(0);
// 兑换比例
- const exchangeRate = { uploaded: 10, downloaded: 10, vip_downloads: 100 };
+ const exchangeRate = { uploaded: 0.1, downloaded: 0.1, vip_downloads: 100 };
// 用户申诉相关
const [appealOpen, setAppealOpen] = useState(false);
const [appealTitle, setAppealTitle] = useState('');
const [appealFile, setAppealFile] = useState(null);
+ // 账号迁移相关
+ const [migrationOpen, setMigrationOpen] = useState(false);
+ const [migrationEmail, setMigrationEmail] = useState('');
+ const [migrationPassword, setMigrationPassword] = useState('');
+ const [migrationStatus, setMigrationStatus] = useState('');
// 兑换结果计算
React.useEffect(() => {
if (!exchangeMagic || isNaN(exchangeMagic)) {
@@ -71,7 +76,7 @@
const res = await fetch(`${API_BASE_URL}/api/user-profile?userid=${userid}`);
if (res.ok) {
const data = await res.json();
- console.log("获取用户信息:", data);
+ // console.log("获取用户信息:", data);
setUserInfo(data);
setTempUserInfo(data);
}
@@ -102,18 +107,30 @@
};
fetchUserSeeds();
}, []);
-
- // 收藏种子(示例数据)
+ // 获取收藏种子
useEffect(() => {
- setUserFavorites([
- { seedid: 'fav1', title: '收藏种子1', tags: '标签A', downloadtimes: 10 },
- ]);
+ const fetchUserFavorites = async () => {
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userid = match ? match[2] : null;
+ if (!userid) return;
+ try {
+ const res = await fetch(`${API_BASE_URL}/api/user-favorites?userid=${userid}`);
+ if (res.ok) {
+ const data = await res.json();
+ // console.log("获取收藏种子列表:", data);
+ setUserFavorites(data);
+ }
+ } catch (err) {
+ console.error("获取收藏种子列表失败", err);
+ }
+ };
+ fetchUserFavorites();
}, []);
-
// 获取活跃度
useEffect(() => {
const fetchUserStats = async () => {
- const userid = "550e8400-e29b-41d4-a716-446655440000";
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userid = match ? match[2] : null;
if (!userid) return;
try {
const res = await fetch(`${API_BASE_URL}/api/user-stats?userid=${userid}`);
@@ -171,57 +188,205 @@
};
// 邀请
- const handleInvite = () => {
- if (!inviteEmail) return;
+ const handleInvite = async () => {
+ if (!inviteEmail) {
+ setInviteStatus("请输入邀请邮箱");
+ return;
+ }
+ // 获取userid
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userid = match ? match[2] : null;
+ if (!userid) {
+ setInviteStatus("未获取到用户ID");
+ return;
+ }
if (userInfo.invite_left <= 0) {
setInviteStatus("邀请次数已用完");
return;
}
- setInviteStatus("邀请成功!(示例,无后端)");
- setUserInfo((prev) => ({
- ...prev,
- invite_left: prev.invite_left - 1,
- }));
- setTempUserInfo((prev) => ({
- ...prev,
- invite_left: prev.invite_left - 1,
- }));
- setInviteEmail('');
- };
-
- // 兑换
- const handleExchange = () => {
+ try {
+ const res = await fetch(`${API_BASE_URL}/api/invite`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userid, invite_email: inviteEmail }),
+ });
+ if (res.ok) {
+ const data = await res.json();
+ setInviteStatus("邀请成功");
+ // 更新剩余次数
+ const left = data.invite_left !== undefined ? data.invite_left : userInfo.invite_left - 1;
+ setUserInfo(prev => ({ ...prev, invite_left: left }));
+ setTempUserInfo(prev => ({ ...prev, invite_left: left }));
+ setInviteEmail('');
+ } else {
+ const errorText = await res.text();
+ setInviteStatus("邀请失败:" + errorText);
+ }
+ } catch (err) {
+ console.error("邀请失败", err);
+ setInviteStatus("邀请失败,请检查网络");
+ }
+ }; // 兑换
+ const handleExchange = async () => {
const magic = Number(exchangeMagic);
if (!magic || isNaN(magic) || magic <= 0) return;
if (magic > userStats.magic) {
alert("魔力值不足!");
return;
}
- let newStats = { ...userStats };
- if (exchangeType === "uploaded") {
- newStats.upload += magic / exchangeRate.uploaded;
- } else if (exchangeType === "downloaded") {
- newStats.download = Math.max(0, newStats.download - magic / exchangeRate.downloaded);
- } else if (exchangeType === "vip_downloads") {
- newStats.vip_downloads += magic / exchangeRate.vip_downloads;
+
+ // 检查兑换结果是否为整数
+ const calculatedExchangeResult = magic / exchangeRate[exchangeType];
+ if (!Number.isInteger(calculatedExchangeResult)) {
+ alert("兑换结果必须为整数,请调整魔力值!");
+ return;
}
- newStats.magic -= magic;
- setUserStats(newStats);
- setExchangeMagic('');
- alert("兑换成功!(示例,无后端)");
- };
+
+ // 获取userid
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userid = match ? match[2] : null;
+ if (!userid) {
+ alert("未获取到用户ID");
+ return;
+ }
+ console.log("兑换请求参数:", { userid, magic, exchangeType, exchangeResult: calculatedExchangeResult });
+ try {
+ // 发送兑换请求到后端
+ const res = await fetch(`${API_BASE_URL}/api/exchange`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ userid,
+ magic,
+ exchangeType,
+ exchangeResult: calculatedExchangeResult
+ }),
+ });
+ // console.log("兑换请求结果:", res);
+ if (res.ok) {
+ // 兑换成功后重新获取用户数据
+ const statsRes = await fetch(`${API_BASE_URL}/api/user-stats?userid=${userid}`);
+ if (statsRes.ok) {
+ const updatedStats = await statsRes.json();
+ setUserStats(updatedStats);
+ }
+ setExchangeMagic('');
+ alert("兑换成功!");
+ } else {
+ const errorText = await res.text();
+ alert("兑换失败:" + errorText);
+ }
+ } catch (err) {
+ console.error("兑换失败", err);
+ alert("兑换失败,请检查网络");
+ }
+ };
// 删除种子
const handleDeleteSeed = (seedid) => {
setUserSeeds(userSeeds.filter((s) => s.seedid !== seedid));
};
+ // 取消收藏
+ const handleRemoveFavorite = async (seedid) => {
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userid = match ? match[2] : null;
+ if (!userid) {
+ alert('未获取到用户ID');
+ return;
+ }
+ try {
+ const res = await fetch(`${API_BASE_URL}/api/remove-favorite`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ userid, seedid }),
+ }); if (res.ok) {
+ setUserFavorites(userFavorites.filter((s) => (s.seedid || s.seed_id) !== seedid));
+ alert('已取消收藏');
+ } else {
+ alert('取消收藏失败,请重试');
+ }
+ } catch (err) {
+ console.error('取消收藏失败', err);
+ alert('取消收藏失败,请检查网络');
+ }
+ };
+
// 申诉提交逻辑
- const handleAppealSubmit = () => {
- alert('申诉已提交!(示例,无后端)');
- setAppealOpen(false);
- setAppealTitle('');
- setAppealFile(null);
+ const handleAppealSubmit = async () => {
+ if (!appealTitle || !appealFile) return;
+ // 获取userid
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userid = match ? match[2] : null;
+ if (!userid) {
+ alert('未获取到用户ID');
+ return;
+ }
+ // 构建表单数据
+ const formData = new FormData();
+ formData.append('userid', userid);
+ formData.append('content', appealTitle);
+ formData.append('file', appealFile);
+ try {
+ const res = await fetch(`${API_BASE_URL}/api/submit-appeal`, {
+ method: 'POST',
+ body: formData,
+ });
+ if (res.ok) {
+ alert('申诉已提交');
+ setAppealOpen(false);
+ setAppealTitle('');
+ setAppealFile(null);
+ } else {
+ const errorText = await res.text();
+ alert('申诉失败:' + errorText);
+ }
+ } catch (err) {
+ console.error('申诉失败', err);
+ alert('申诉失败,请检查网络');
+ }
+ };
+ // 账号迁移提交逻辑
+ const handleMigrationSubmit = async () => {
+ if (!appealFile) {
+ setMigrationStatus('请选择PDF文件');
+ return;
+ }
+
+ // 获取当前用户ID
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const currentUserId = match ? match[2] : null;
+ if (!currentUserId) {
+ setMigrationStatus('未获取到当前用户ID');
+ return;
+ }
+
+ try {
+ // 构建表单数据
+ const formData = new FormData();
+ formData.append('userid', currentUserId);
+ formData.append('file', appealFile);
+
+ const res = await fetch(`${API_BASE_URL}/api/migrate-account`, {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (res.ok) {
+ setMigrationStatus('账号迁移申请已提交,请等待管理员审核');
+ setTimeout(() => {
+ setMigrationOpen(false);
+ setAppealFile(null);
+ setMigrationStatus('');
+ }, 2000);
+ } else {
+ const errorText = await res.text();
+ setMigrationStatus('迁移失败:' + errorText);
+ }
+ } catch (err) {
+ console.error('账号迁移失败', err);
+ setMigrationStatus('迁移失败,请检查网络');
+ }
};
return (
@@ -369,20 +534,25 @@
<MenuItem value="m">男性</MenuItem>
<MenuItem value="f">女性</MenuItem>
</TextField>
- </div>
- <div style={{ display: 'flex', gap: 16, marginTop: 24, justifyContent: 'flex-end' }}>
+ </div> <div style={{ display: 'flex', gap: 16, marginTop: 24, justifyContent: 'flex-end' }}>
<Button
variant="contained"
color="primary"
onClick={handleSave}
- sx={{ fontSize: 16, borderRadius: 2, padding: '10px 24px' }}
+ sx={{ fontSize: 16, borderRadius: 2, padding: '6px 12px' }}
>保存</Button>
<Button
variant="contained"
color="error"
onClick={() => setAppealOpen(true)}
- sx={{ fontSize: 16, borderRadius: 2, padding: '10px 24px' }}
+ sx={{ fontSize: 16, borderRadius: 2, padding: '6px 12px' }}
>用户申诉</Button>
+ <Button
+ variant="contained"
+ color="warning"
+ onClick={() => setMigrationOpen(true)}
+ sx={{ fontSize: 16, borderRadius: 2, padding: '6px 12px' }}
+ >账号迁移</Button>
</div>
</div>
</div>
@@ -432,11 +602,14 @@
<MenuItem value="uploaded">上传量(增加)</MenuItem>
<MenuItem value="downloaded">下载量(减少)</MenuItem>
<MenuItem value="vip_downloads">VIP下载次数(增加)</MenuItem>
- </TextField>
- <span style={{ marginLeft: 8, color: '#43a047' }}>
- 可兑换:<b>{exchangeResult}</b> {exchangeType === 'vip_downloads' ? '次' : 'GB'}
- </span>
- <Button
+ </TextField> <span style={{ marginLeft: 8, color: '#43a047' }}>
+ 可兑换:<b>{exchangeResult}</b> {exchangeType === 'vip_downloads' ? '次' : 'MB'}
+ {!Number.isInteger(exchangeResult) && exchangeResult > 0 && (
+ <span style={{ color: '#e53935', fontSize: '12px', marginLeft: 8 }}>
+ (结果必须为整数)
+ </span>
+ )}
+ </span><Button
variant="contained"
color="primary"
onClick={handleExchange}
@@ -444,19 +617,19 @@
!exchangeMagic ||
isNaN(exchangeMagic) ||
Number(exchangeMagic) <= 0 ||
- Number(exchangeMagic) > userStats.magic
+ Number(exchangeMagic) > userStats.magic ||
+ !Number.isInteger(exchangeResult)
}
sx={{
marginLeft: 2,
minWidth: 80,
- background: (!exchangeMagic || isNaN(exchangeMagic) || Number(exchangeMagic) <= 0 || Number(exchangeMagic) > userStats.magic) ? '#ccc' : undefined
+ background: (!exchangeMagic || isNaN(exchangeMagic) || Number(exchangeMagic) <= 0 || Number(exchangeMagic) > userStats.magic || !Number.isInteger(exchangeResult)) ? '#ccc' : undefined
}}
>兑换</Button>
- </div>
- <div>上传量:<b style={{ color: '#43a047' }}>{userStats.upload?.toFixed(2)} GB</b></div>
- <div>下载量:<b style={{ color: '#e53935' }}>{userStats.download?.toFixed(2)} GB</b></div>
+ </div> <div>上传量:<b style={{ color: '#43a047' }}>{(userStats.upload / 1000000)?.toFixed(2)} MB</b></div>
+ <div>下载量:<b style={{ color: '#e53935' }}>{(userStats.download / 1000000)?.toFixed(2)} MB</b></div>
<div>上传/下载值:<b style={{ color: '#ff9800' }}>{userStats.download === 0 ? "∞" : (userStats.upload / userStats.download).toFixed(2)}</b></div>
- <div>VIP下载次数:<b style={{ color: '#1976d2' }}>{userStats.vip_downloads}</b></div>
+ <div>VIP下载次数:<b style={{ color: '#1976d2' }}>{userStats.viptime}</b></div>
</div>
</div>
{/* 右上:个人上传种子列表 */}
@@ -509,11 +682,14 @@
variant="contained"
color="error"
size="small"
- sx={{ marginLeft: 2, borderRadius: 1, minWidth: 60 }}
- onClick={async e => {
+ sx={{ marginLeft: 2, borderRadius: 1, minWidth: 60 }} onClick={async e => {
e.stopPropagation();
- // const userid = localStorage.getItem("userid");
- const userid = "550e8400-e29b-41d4-a716-446655440000"; // 示例userid
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userid = match ? match[2] : null;
+ if (!userid) {
+ alert('未获取到用户ID');
+ return;
+ }
try {
const res = await fetch(`${API_BASE_URL}/api/delete-seed`, {
method: 'POST',
@@ -558,8 +734,7 @@
}}>
{userFavorites.length === 0 ? (
<div style={{ color: '#b2b2b2', fontSize: 18, textAlign: 'center' }}>(暂无收藏种子)</div>
- ) : (
- <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
+ ) : ( <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
{userFavorites.map((seed, idx) => (
<li
key={seed.seedid || idx}
@@ -570,16 +745,25 @@
borderBottom: idx === userFavorites.length - 1 ? 'none' : '1px solid #e0e7ff',
cursor: 'pointer',
transition: 'background 0.15s'
- }}
- onClick={e => {
- navigate(`/torrent/${seed.seedid}`);
+ }} onClick={e => { if (e.target.classList.contains('remove-favorite-btn')) return;
+ navigate(`/torrent/${seed.seedid || seed.seed_id}`);
}}
onMouseOver={e => e.currentTarget.style.background = '#f3f6ff'}
onMouseOut={e => e.currentTarget.style.background = ''}
>
- <span style={{ flex: 2, fontWeight: 500, color: '#1a237e', textDecoration: 'underline' }}>{seed.title}</span>
- <span style={{ flex: 1, color: '#5c6bc0' }}>{seed.tags}</span>
- <span style={{ flex: 1, color: '#ff9800', textAlign: 'right' }}>人气: {seed.downloadtimes}</span>
+ <span style={{ flex: 2, fontWeight: 500, color: '#1a237e', textDecoration: 'underline', cursor: 'pointer' }}>{seed.seed.title}</span>
+ <span style={{ flex: 1, color: '#5c6bc0' }}>{seed.seed.tags}</span>
+ <span style={{ flex: 1, color: '#ff9800', textAlign: 'right' }}>人气: {seed.seed.downloadtimes}</span>
+ <Button
+ className="remove-favorite-btn"
+ variant="contained"
+ color="warning"
+ size="small"
+ sx={{ marginLeft: 2, borderRadius: 1, minWidth: 80 }} onClick={e => {
+ e.stopPropagation();
+ handleRemoveFavorite(seed.seedid || seed.seed_id);
+ }}
+ >取消收藏</Button>
</li>
))}
</ul>
@@ -598,19 +782,66 @@
onChange={e => setAppealTitle(e.target.value)}
size="small"
/>
- </div>
- <div>
+ </div> <div>
<input
type="file"
- onChange={e => setAppealFile(e.target.files[0])}
+ accept=".pdf"
+ onChange={e => {
+ const file = e.target.files[0];
+ if (file && file.type !== 'application/pdf') {
+ alert('请选择PDF文件');
+ e.target.value = '';
+ setAppealFile(null);
+ } else {
+ setAppealFile(file);
+ }
+ }}
style={{ marginTop: 8 }}
/>
+ <div style={{ fontSize: 12, color: '#666', marginTop: 4 }}>
+ 请选择PDF文件(最大100MB)
+ </div>
</div>
</DialogContent>
<DialogActions>
<Button onClick={handleAppealSubmit} variant="contained" color="primary" disabled={!appealTitle || !appealFile}>提交</Button>
<Button onClick={() => setAppealOpen(false)} variant="outlined">取消</Button>
</DialogActions>
+ </Dialog> {/* 账号迁移弹窗 */}
+ <Dialog open={migrationOpen} onClose={() => setMigrationOpen(false)}>
+ <DialogTitle>账号迁移</DialogTitle>
+ <DialogContent>
+ <div style={{ marginBottom: 16 }}>
+ </div> <div>
+ <input
+ type="file"
+ accept=".pdf"
+ onChange={e => {
+ const file = e.target.files[0];
+ if (file && file.type !== 'application/pdf') {
+ alert('请选择PDF文件');
+ e.target.value = '';
+ setAppealFile(null);
+ } else {
+ setAppealFile(file);
+ }
+ }}
+ style={{ marginTop: 8 }}
+ />
+ <div style={{ fontSize: 12, color: '#666', marginTop: 4 }}>
+ 请选择PDF文件(最大10MB)
+ </div>
+ </div>
+ {migrationStatus && (
+ <div style={{ color: migrationStatus.includes('成功') ? '#43a047' : '#e53935', fontSize: 14, marginTop: 8 }}>
+ {migrationStatus}
+ </div>
+ )}
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={handleMigrationSubmit} variant="contained" color="primary">提交迁移</Button>
+ <Button onClick={() => setMigrationOpen(false)} variant="outlined">取消</Button>
+ </DialogActions>
</Dialog>
</div>
);
diff --git a/front/src/config.js b/front/src/config.js
index e28dd5f..0239e94 100644
--- a/front/src/config.js
+++ b/front/src/config.js
@@ -1 +1 @@
-export const API_BASE_URL = "http://10.126.59.25:8081";
\ No newline at end of file
+export const API_BASE_URL = "http://10.126.59.25:8082";
\ No newline at end of file