修改前端页面
Change-Id: I344849fc3dfca5725e0ef2a107e2c89a14fce8da
diff --git a/front/src/AnimePage.js b/front/src/AnimePage.js
index 118d8fb..03ad8b2 100644
--- a/front/src/AnimePage.js
+++ b/front/src/AnimePage.js
@@ -1,4 +1,5 @@
-import React from "react";
+import React, { useState, useEffect } from "react";
+import HomeIcon from "@mui/icons-material/Home";
import MovieIcon from "@mui/icons-material/Movie";
import EmailIcon from "@mui/icons-material/Email";
import MusicNoteIcon from "@mui/icons-material/MusicNote";
@@ -7,11 +8,14 @@
import SportsMartialArtsIcon from "@mui/icons-material/SportsMartialArts";
import PersonIcon from "@mui/icons-material/Person";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
+import ForumIcon from "@mui/icons-material/Forum";
+import HelpIcon from "@mui/icons-material/Help";
import "./App.css";
import { useNavigate } from "react-router-dom";
import { API_BASE_URL } from "./config";
const navItems = [
+ { label: "首页", icon: <HomeIcon />, path: "/home" },
{ label: "电影", icon: <MovieIcon />, path: "/movie" },
{ label: "剧集", icon: <EmailIcon />, path: "/tv" },
{ label: "音乐", icon: <MusicNoteIcon />, path: "/music" },
@@ -19,15 +23,9 @@
{ label: "游戏", icon: <SportsEsportsIcon />, path: "/game" },
{ label: "体育", icon: <SportsMartialArtsIcon />, path: "/sport" },
{ label: "资料", icon: <PersonIcon />, path: "/info" },
- { label: "发布", icon: <AccountCircleIcon />, path: "/publish" }, // Added Publish option
-];
-
-const animeTypes = [
- "华语动漫(大陆)",
- "欧美动漫",
- "日韩动漫",
- "港台动漫",
- "其他"
+ { label: "论坛", icon: <ForumIcon />, path: "/forum" },
+ { label: "发布", icon: <AccountCircleIcon />, path: "/publish" },
+ { label: "求种", icon: <HelpIcon />, path: "/begseed" },
];
const areaTabs = [
@@ -35,26 +33,45 @@
{ label: "日漫", icon: <EmailIcon fontSize="small" /> },
{ label: "欧美动漫", icon: <PersonIcon fontSize="small" /> },
{ label: "韩漫", icon: <EmojiPeopleIcon fontSize="small" /> },
- // { label: "其他", icon: <PersonIcon fontSize="small" /> },
];
export default function AnimePage() {
const navigate = useNavigate();
- const [activeTab, setActiveTab] = React.useState(0);
- const [animeList, setAnimeList] = React.useState([]);
+ const [searchText, setSearchText] = useState('');
+ const [userInfo, setUserInfo] = useState({ avatar_url: '', username: '' });
+ const [userPT, setUserPT] = useState({ magic: 0, ratio: 0, upload: 0, download: 0 });
+ const [activeTab, setActiveTab] = useState(0);
+ const [animeList, setAnimeList] = useState([]);
+
+ useEffect(() => {
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userId = match ? match[2] : null;
+ if (!userId) return;
+ fetch(`${API_BASE_URL}/api/get-userpt?userid=${encodeURIComponent(userId)}`)
+ .then(res => res.json())
+ .then(data => {
+ setUserInfo({ avatar_url: data.user.avatar_url, username: data.user.username });
+ setUserPT({
+ magic: data.magic_value || data.magic || 0,
+ ratio: data.share_ratio || data.share || 0,
+ upload: data.upload_amount || data.upload || 0,
+ download: data.download_amount || data.download || 0,
+ });
+ })
+ .catch(err => console.error('Fetching user profile failed', err));
+ }, []);
// 每个tab对应的动漫类型
const animeTypesList = [
- ["华语动漫(大陆)", "欧美动漫", "日韩动漫", "港台动漫", "其他"], // 大陆
- ["港台热血", "港台搞笑", "港台其他"], // 港台
- ["欧美冒险", "欧美科幻", "欧美其他"], // 欧美
- ["日韩热血", "日韩恋爱", "日韩其他"], // 日韩
+ ["华语动漫(大陆)", "欧美动漫", "日韩动漫", "港台动漫", "其他"], // 国创
+ ["日漫热血", "日漫搞笑", "日漫其他"], // 日漫
+ ["欧美冒险", "欧美科幻", "欧美其他"], // 欧美动漫
+ ["韩漫爱情", "韩漫奇幻", "韩漫其他"], // 韩漫
["其他类型1", "其他类型2"] // 其他
];
const animeTypes = animeTypesList[activeTab] || [];
- React.useEffect(() => {
- // 假设后端接口为 /api/animes?area=大陆
+ useEffect(() => {
const area = areaTabs[activeTab].label;
fetch(`${API_BASE_URL}/api/get-seed-list-by-tag?tag=${encodeURIComponent(area)}`)
.then(res => res.json())
@@ -62,25 +79,40 @@
.catch(() => setAnimeList([]));
}, [activeTab]);
+ // 搜索按钮处理
+ const handleSearch = () => {
+ const area = areaTabs[activeTab].label;
+ fetch(`${API_BASE_URL}/api/search-seeds?tag=${encodeURIComponent(area)}&keyword=${encodeURIComponent(searchText)}`)
+ .then(res => res.json())
+ .then(data => {
+ setAnimeList(data);
+ })
+ .catch(() => setAnimeList([]));
+ };
+
return (
<div className="container">
- {/* 顶部空白与音乐界面一致,用户栏绝对定位在页面右上角 */}
+ {/* 顶部空白与动漫界面一致,用户栏绝对定位在页面右上角 */}
<div style={{ height: 80 }} />
<div className="user-bar" style={{ position: 'fixed', top: 18, right: 42, zIndex: 100, display: 'flex', alignItems: 'center', background: '#e0f3ff', borderRadius: 12, padding: '6px 18px', boxShadow: '0 2px 8px #b2d8ea', minWidth: 320, minHeight: 48, width: 420 }}>
<div style={{ cursor: 'pointer', marginRight: 16 }} onClick={() => navigate('/user')}>
- <AccountCircleIcon style={{ fontSize: 38, color: '#1a237e', background: '#e0f3ff', borderRadius: '50%' }} />
+ {userInfo.avatar_url ? (
+ <img src={userInfo.avatar_url} alt="用户头像" style={{ width: 38, height: 38, borderRadius: '50%', objectFit: 'cover' }} />
+ ) : (
+ <AccountCircleIcon style={{ fontSize: 38, color: '#1a237e', background: '#e0f3ff', borderRadius: '50%' }} />
+ )}
</div>
- <div style={{ color: '#222', fontWeight: 500, marginRight: 24 }}>用户栏</div>
+ <div style={{ color: '#222', fontWeight: 500, marginRight: 24 }}>{userInfo.username || '用户栏'}</div>
<div style={{ display: 'flex', gap: 28, flex: 1, justifyContent: 'flex-end', alignItems: 'center' }}>
- <span style={{ color: '#1976d2', fontWeight: 500 }}>魔力值: <b>12345</b></span>
- <span style={{ color: '#1976d2', fontWeight: 500 }}>分享率: <b>2.56</b></span>
- <span style={{ color: '#1976d2', fontWeight: 500 }}>上传量: <b>100GB</b></span>
- <span style={{ color: '#1976d2', fontWeight: 500 }}>下载量: <b>50GB</b></span>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>魔力值: <b>{userPT.magic}</b></span>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>分享率: <b>{userPT.ratio}</b></span>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>上传量: <b>{userPT.upload}</b></span>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>下载量: <b>{userPT.download}</b></span>
</div>
</div>
- {/* 下方内容整体下移,留出与音乐界面一致的间距 */}
+ {/* 下方内容整体下移,留出与动漫界面一致的间距 */}
<div style={{ height: 32 }} />
- <nav className="nav-bar">
+ <nav className="nav-bar card">
{navItems.map((item) => (
<div
key={item.label}
@@ -93,8 +125,13 @@
))}
</nav>
<div className="search-section card">
- <input className="search-input" placeholder="输入搜索关键词" />
- <button className="search-btn">
+ <input
+ className="search-input"
+ placeholder="输入搜索关键词"
+ value={searchText}
+ onChange={e => setSearchText(e.target.value)}
+ />
+ <button className="search-btn" onClick={handleSearch}>
<span role="img" aria-label="search">🔍</span>
</button>
</div>
@@ -116,6 +153,9 @@
<th>动漫类型</th>
<th>标题</th>
<th>发布者</th>
+ <th>大小</th>
+ <th>热度</th>
+ <th>折扣倍率</th>
</tr>
</thead>
<tbody>
@@ -132,7 +172,10 @@
{item.title}
</a>
</td>
- <td>{item.user.username}</td>
+ <td>{item.username}</td>
+ <td>{item.seedsize}</td>
+ <td>{item.downloadtimes}</td>
+ <td>{item.discount == null ? 1 : item.discount}</td>
</tr>
))
) : (
@@ -149,6 +192,9 @@
</a>
</td>
<td>发布者{index + 1}</td>
+ <td>--</td>
+ <td>--</td>
+ <td>1</td>
</tr>
))
)}
@@ -172,4 +218,4 @@
<span className="page-info">第 <b>{page}</b> 页</span>
</div>
);
-}
+}
\ No newline at end of file
diff --git a/front/src/GamePage.js b/front/src/GamePage.js
index 1de4d30..cae28cd 100644
--- a/front/src/GamePage.js
+++ b/front/src/GamePage.js
@@ -1,4 +1,5 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
+import HomeIcon from "@mui/icons-material/Home";
import MovieIcon from "@mui/icons-material/Movie";
import EmailIcon from "@mui/icons-material/Email";
import MusicNoteIcon from "@mui/icons-material/MusicNote";
@@ -7,11 +8,14 @@
import SportsMartialArtsIcon from "@mui/icons-material/SportsMartialArts";
import PersonIcon from "@mui/icons-material/Person";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
+import ForumIcon from "@mui/icons-material/Forum";
+import HelpIcon from "@mui/icons-material/Help";
import "./App.css";
import { useNavigate } from "react-router-dom";
import { API_BASE_URL } from "./config";
const navItems = [
+ { label: "首页", icon: <HomeIcon />, path: "/home" },
{ label: "电影", icon: <MovieIcon />, path: "/movie" },
{ label: "剧集", icon: <EmailIcon />, path: "/tv" },
{ label: "音乐", icon: <MusicNoteIcon />, path: "/music" },
@@ -19,15 +23,9 @@
{ label: "游戏", icon: <SportsEsportsIcon />, path: "/game" },
{ label: "体育", icon: <SportsMartialArtsIcon />, path: "/sport" },
{ label: "资料", icon: <PersonIcon />, path: "/info" },
- { label: "发布", icon: <AccountCircleIcon />, path: "/publish" }, // Added Publish option
-];
-
-const gameTypesList = [
- ["PC"],
- ["主机"],
- ["移动"],
- ["掌机"],
- ["视频"]
+ { label: "论坛", icon: <ForumIcon />, path: "/forum" },
+ { label: "发布", icon: <AccountCircleIcon />, path: "/publish" },
+ { label: "求种", icon: <HelpIcon />, path: "/begseed" },
];
const areaTabs = [
@@ -38,14 +36,45 @@
{ label: "视频", icon: <PersonIcon fontSize="small" /> },
];
+// 每个tab对应的游戏类型
+const gameTypesList = [
+ ["PC单机", "PC网络", "PC独立", "PC其他"], // PC
+ ["主机动作", "主机RPG", "主机其他"], // 主机
+ ["移动休闲", "移动竞技", "移动其他"], // 移动
+ ["掌机冒险", "掌机其他"], // 掌机
+ ["游戏视频", "赛事视频", "其他视频"], // 视频
+ ["其他类型1", "其他类型2"] // 其他
+];
+
export default function GamePage() {
const navigate = useNavigate();
+ const [searchText, setSearchText] = useState('');
+ const [userInfo, setUserInfo] = useState({ avatar_url: '', username: '' });
+ const [userPT, setUserPT] = useState({ magic: 0, ratio: 0, upload: 0, download: 0 });
const [activeTab, setActiveTab] = useState(0);
const [gameList, setGameList] = useState([]);
+ useEffect(() => {
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userId = match ? match[2] : null;
+ if (!userId) return;
+ fetch(`${API_BASE_URL}/api/get-userpt?userid=${encodeURIComponent(userId)}`)
+ .then(res => res.json())
+ .then(data => {
+ setUserInfo({ avatar_url: data.user.avatar_url, username: data.user.username });
+ setUserPT({
+ magic: data.magic_value || data.magic || 0,
+ ratio: data.share_ratio || data.share || 0,
+ upload: data.upload_amount || data.upload || 0,
+ download: data.download_amount || data.download || 0,
+ });
+ })
+ .catch(err => console.error('Fetching user profile failed', err));
+ }, []);
+
const gameTypes = gameTypesList[activeTab] || [];
- React.useEffect(() => {
+ useEffect(() => {
const area = areaTabs[activeTab].label;
fetch(`${API_BASE_URL}/api/get-seed-list-by-tag?tag=${encodeURIComponent(area)}`)
.then(res => res.json())
@@ -53,68 +82,38 @@
.catch(() => setGameList([]));
}, [activeTab]);
+ // 搜索按钮处理
+ const handleSearch = () => {
+ const area = areaTabs[activeTab].label;
+ fetch(`${API_BASE_URL}/api/search-seeds?tag=${encodeURIComponent(area)}&keyword=${encodeURIComponent(searchText)}`)
+ .then(res => res.json())
+ .then(data => {
+ setGameList(data);
+ })
+ .catch(() => setGameList([]));
+ };
+
return (
<div className="container">
- {/* 预留顶部空间用于放图片 */}
+ {/* 顶部空白与游戏界面一致,用户栏绝对定位在页面右上角 */}
<div style={{ height: 80 }} />
- <div
- className="user-bar"
- style={{
- position: "fixed",
- top: 18,
- right: 42,
- zIndex: 100,
- display: "flex",
- alignItems: "center",
- background: "#e0f3ff",
- borderRadius: 12,
- padding: "6px 18px",
- boxShadow: "0 2px 8px #b2d8ea",
- minWidth: 320,
- minHeight: 48,
- width: 420,
- }}
- >
- <div
- style={{ cursor: "pointer", marginRight: 16 }}
- onClick={() => navigate("/user")}
- >
- <AccountCircleIcon
- style={{
- fontSize: 38,
- color: "#1a237e",
- background: "#e0f3ff",
- borderRadius: "50%",
- }}
- />
+ <div className="user-bar" style={{ position: 'fixed', top: 18, right: 42, zIndex: 100, display: 'flex', alignItems: 'center', background: '#e0f3ff', borderRadius: 12, padding: '6px 18px', boxShadow: '0 2px 8px #b2d8ea', minWidth: 320, minHeight: 48, width: 420 }}>
+ <div style={{ cursor: 'pointer', marginRight: 16 }} onClick={() => navigate('/user')}>
+ {userInfo.avatar_url ? (
+ <img src={userInfo.avatar_url} alt="用户头像" style={{ width: 38, height: 38, borderRadius: '50%', objectFit: 'cover' }} />
+ ) : (
+ <AccountCircleIcon style={{ fontSize: 38, color: '#1a237e', background: '#e0f3ff', borderRadius: '50%' }} />
+ )}
</div>
- <div style={{ color: "#222", fontWeight: 500, marginRight: 24 }}>
- 用户栏
- </div>
- <div
- style={{
- display: "flex",
- gap: 28,
- flex: 1,
- justifyContent: "flex-end",
- alignItems: "center",
- }}
- >
- <span style={{ color: "#1976d2", fontWeight: 500 }}>
- 魔力值: <b>12345</b>
- </span>
- <span style={{ color: "#1976d2", fontWeight: 500 }}>
- 分享率: <b>2.56</b>
- </span>
- <span style={{ color: "#1976d2", fontWeight: 500 }}>
- 上传量: <b>100GB</b>
- </span>
- <span style={{ color: "#1976d2", fontWeight: 500 }}>
- 下载量: <b>50GB</b>
- </span>
+ <div style={{ color: '#222', fontWeight: 500, marginRight: 24 }}>{userInfo.username || '用户栏'}</div>
+ <div style={{ display: 'flex', gap: 28, flex: 1, justifyContent: 'flex-end', alignItems: 'center' }}>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>魔力值: <b>{userPT.magic}</b></span>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>分享率: <b>{userPT.ratio}</b></span>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>上传量: <b>{userPT.upload}</b></span>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>下载量: <b>{userPT.download}</b></span>
</div>
</div>
- {/* 下方内容整体下移,留出与音乐界面一致的间距 */}
+ {/* 下方内容整体下移,留出与游戏界面一致的间距 */}
<div style={{ height: 32 }} />
<nav className="nav-bar card">
{navItems.map((item) => (
@@ -129,22 +128,17 @@
))}
</nav>
<div className="search-section card">
- <input className="search-input" placeholder="输入搜索关键词" />
- <button className="search-btn">
- <span role="img" aria-label="search">
- 🔍
- </span>
+ <input
+ className="search-input"
+ placeholder="输入搜索关键词"
+ value={searchText}
+ onChange={e => setSearchText(e.target.value)}
+ />
+ <button className="search-btn" onClick={handleSearch}>
+ <span role="img" aria-label="search">🔍</span>
</button>
</div>
- <div
- className="area-tabs"
- style={{
- display: "flex",
- justifyContent: "center",
- gap: 24,
- margin: "18px 0",
- }}
- >
+ <div className="area-tabs" style={{ display: 'flex', justifyContent: 'center', gap: 24, margin: '18px 0' }}>
{areaTabs.map((tab, idx) => (
<div
key={tab.label}
@@ -162,39 +156,48 @@
<th>游戏类型</th>
<th>标题</th>
<th>发布者</th>
+ <th>大小</th>
+ <th>热度</th>
+ <th>折扣倍率</th>
</tr>
</thead>
<tbody>
{gameList.length > 0 ? (
- gameList.map((item, idx) => (
- <tr key={item.id || idx}>
+ gameList.map((item, index) => (
+ <tr key={item.id || index}>
<td>
- <a href={`/torrent/${item.seedid}`} style={{ color: "#1a237e", textDecoration: "none" }}>
+ <a href={`/torrent/${item.seedid}`} style={{ color: '#1a237e', textDecoration: 'none' }}>
{item.seedtag}
</a>
</td>
<td>
- <a href={`/torrent/${item.seedid}`} style={{ color: "#1a237e", textDecoration: "none" }}>
+ <a href={`/torrent/${item.seedid}`} style={{ color: '#1a237e', textDecoration: 'none' }}>
{item.title}
</a>
</td>
- <td>{item.user.username}</td>
+ <td>{item.username}</td>
+ <td>{item.seedsize}</td>
+ <td>{item.downloadtimes}</td>
+ <td>{item.discount == null ? 1 : item.discount}</td>
</tr>
))
) : (
- gameTypes.map((type, idx) => (
+ gameTypes.map((type, index) => (
<tr key={type}>
<td>
- <a href={`/torrent/${type}`} style={{ color: "#1a237e", textDecoration: "none" }}>
+ <a href={`/torrent/${type}`} style={{ color: '#1a237e', textDecoration: 'none' }}>
{type}
</a>
</td>
<td>
- <a href={`/torrent/${type}`} style={{ color: "#1a237e", textDecoration: "none" }}>
- 种子{idx + 1}
+ <a href={`/torrent/${type}`} style={{ color: '#1a237e', textDecoration: 'none' }}>
+ 种子{index + 1}
</a>
</td>
- <td></td>
+ <td>发布者{index + 1}</td>
+ <td>--</td>
+ <td>--</td>
+ <td>1</td>
</tr>
))
)}
@@ -212,24 +215,10 @@
const total = 5;
return (
<div className="pagination">
- <button
- onClick={() => setPage((p) => Math.max(1, p - 1))}
- disabled={page === 1}
- >
- 上一页
- </button>
- <span className="page-num">
- {page}/{total}
- </span>
- <button
- onClick={() => setPage((p) => Math.min(total, p + 1))}
- disabled={page === total}
- >
- 下一页
- </button>
- <span className="page-info">
- 第 <b>{page}</b> 页
- </span>
+ <button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1}>上一页</button>
+ <span className="page-num">{page}/{total}</span>
+ <button onClick={() => setPage(p => Math.min(total, p + 1))} disabled={page === total}>下一页</button>
+ <span className="page-info">第 <b>{page}</b> 页</span>
</div>
);
-}
+}
\ No newline at end of file
diff --git a/front/src/InfoPage.js b/front/src/InfoPage.js
index 0ed6ade..cec0b4d 100644
--- a/front/src/InfoPage.js
+++ b/front/src/InfoPage.js
@@ -1,4 +1,5 @@
-import React from "react";
+import React, { useState, useEffect } from "react";
+import HomeIcon from "@mui/icons-material/Home";
import MovieIcon from "@mui/icons-material/Movie";
import EmailIcon from "@mui/icons-material/Email";
import MusicNoteIcon from "@mui/icons-material/MusicNote";
@@ -7,11 +8,14 @@
import SportsMartialArtsIcon from "@mui/icons-material/SportsMartialArts";
import PersonIcon from "@mui/icons-material/Person";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
+import ForumIcon from "@mui/icons-material/Forum";
+import HelpIcon from "@mui/icons-material/Help";
import "./App.css";
import { useNavigate } from "react-router-dom";
import { API_BASE_URL } from "./config";
const navItems = [
+ { label: "首页", icon: <HomeIcon />, path: "/home" },
{ label: "电影", icon: <MovieIcon />, path: "/movie" },
{ label: "剧集", icon: <EmailIcon />, path: "/tv" },
{ label: "音乐", icon: <MusicNoteIcon />, path: "/music" },
@@ -19,29 +23,44 @@
{ label: "游戏", icon: <SportsEsportsIcon />, path: "/game" },
{ label: "体育", icon: <SportsMartialArtsIcon />, path: "/sport" },
{ label: "资料", icon: <PersonIcon />, path: "/info" },
- { label: "发布", icon: <AccountCircleIcon />, path: "/publish" }, // Added Publish option
+ { label: "论坛", icon: <ForumIcon />, path: "/forum" },
+ { label: "发布", icon: <AccountCircleIcon />, path: "/publish" },
+ { label: "求种", icon: <HelpIcon />, path: "/begseed" },
];
-const infoTypes = [""];
-
const areaTabs = [
{ label: "出版物", icon: <MovieIcon fontSize="small" /> },
{ label: "学习教程", icon: <EmailIcon fontSize="small" /> },
{ label: "素材模板", icon: <PersonIcon fontSize="small" /> },
{ label: "演讲交流", icon: <EmojiPeopleIcon fontSize="small" /> },
- { label: "日常娱乐", icon: <PersonIcon fontSize="small" />, active: true },
-];
-
-const exampleTorrents = [
- { type: "Documentary", title: "实例1", id: 1 },
- { type: "Biography", title: "实例2", id: 2 },
- { type: "History", title: "实例3", id: 3 },
+ { label: "日常娱乐", icon: <PersonIcon fontSize="small" /> },
];
export default function InfoPage() {
const navigate = useNavigate();
- const [activeTab, setActiveTab] = React.useState(0);
- const [infoList, setInfoList] = React.useState(0);
+ const [searchText, setSearchText] = useState('');
+ const [userInfo, setUserInfo] = useState({ avatar_url: '', username: '' });
+ const [userPT, setUserPT] = useState({ magic: 0, ratio: 0, upload: 0, download: 0 });
+ const [activeTab, setActiveTab] = useState(0);
+ const [infoList, setInfoList] = useState([]);
+
+ useEffect(() => {
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userId = match ? match[2] : null;
+ if (!userId) return;
+ fetch(`${API_BASE_URL}/api/get-userpt?userid=${encodeURIComponent(userId)}`)
+ .then(res => res.json())
+ .then(data => {
+ setUserInfo({ avatar_url: data.user.avatar_url, username: data.user.username });
+ setUserPT({
+ magic: data.magic_value || data.magic || 0,
+ ratio: data.share_ratio || data.share || 0,
+ upload: data.upload_amount || data.upload || 0,
+ download: data.download_amount || data.download || 0,
+ });
+ })
+ .catch(err => console.error('Fetching user profile failed', err));
+ }, []);
// 每个tab对应的资料类型
const infoTypesList = [
@@ -50,83 +69,52 @@
["模板A", "模板B"], // 素材模板
["演讲A", "演讲B"], // 演讲交流
["娱乐A", "娱乐B"], // 日常娱乐
+ ["其他类型1", "其他类型2"] // 其他
];
const infoTypes = infoTypesList[activeTab] || [];
- React.useEffect(() => {
- // 这里假设后端接口为 /api/get-seed-list-by-tag?tag=大陆
+ useEffect(() => {
const area = areaTabs[activeTab].label;
fetch(`${API_BASE_URL}/api/get-seed-list-by-tag?tag=${encodeURIComponent(area)}`)
.then(res => res.json())
.then(data => {
- console.log('资料区返回数据:', data);
setInfoList(data);
})
.catch(() => setInfoList([]));
}, [activeTab]);
+ // 搜索按钮处理
+ const handleSearch = () => {
+ const area = areaTabs[activeTab].label;
+ fetch(`${API_BASE_URL}/api/search-seeds?tag=${encodeURIComponent(area)}&keyword=${encodeURIComponent(searchText)}`)
+ .then(res => res.json())
+ .then(data => {
+ setInfoList(data);
+ })
+ .catch(() => setInfoList([]));
+ };
+
return (
<div className="container">
- {/* 顶部空白与音乐界面一致,用户栏绝对定位在页面右上角 */}
+ {/* 顶部空白与资料界面一致,用户栏绝对定位在页面右上角 */}
<div style={{ height: 80 }} />
- <div
- className="user-bar"
- style={{
- position: "fixed",
- top: 18,
- right: 42,
- zIndex: 100,
- display: "flex",
- alignItems: "center",
- background: "#e0f3ff",
- borderRadius: 12,
- padding: "6px 18px",
- boxShadow: "0 2px 8px #b2d8ea",
- minWidth: 320,
- minHeight: 48,
- width: 420,
- }}
- >
- <div
- style={{ cursor: "pointer", marginRight: 16 }}
- onClick={() => navigate("/user")}
- >
- <AccountCircleIcon
- style={{
- fontSize: 38,
- color: "#1a237e",
- background: "#e0f3ff",
- borderRadius: "50%",
- }}
- />
+ <div className="user-bar" style={{ position: 'fixed', top: 18, right: 42, zIndex: 100, display: 'flex', alignItems: 'center', background: '#e0f3ff', borderRadius: 12, padding: '6px 18px', boxShadow: '0 2px 8px #b2d8ea', minWidth: 320, minHeight: 48, width: 420 }}>
+ <div style={{ cursor: 'pointer', marginRight: 16 }} onClick={() => navigate('/user')}>
+ {userInfo.avatar_url ? (
+ <img src={userInfo.avatar_url} alt="用户头像" style={{ width: 38, height: 38, borderRadius: '50%', objectFit: 'cover' }} />
+ ) : (
+ <AccountCircleIcon style={{ fontSize: 38, color: '#1a237e', background: '#e0f3ff', borderRadius: '50%' }} />
+ )}
</div>
- <div style={{ color: "#222", fontWeight: 500, marginRight: 24 }}>
- 用户栏
- </div>
- <div
- style={{
- display: "flex",
- gap: 28,
- flex: 1,
- justifyContent: "flex-end",
- alignItems: "center",
- }}
- >
- <span style={{ color: "#1976d2", fontWeight: 500 }}>
- 魔力值: <b>12345</b>
- </span>
- <span style={{ color: "#1976d2", fontWeight: 500 }}>
- 分享率: <b>2.56</b>
- </span>
- <span style={{ color: "#1976d2", fontWeight: 500 }}>
- 上传量: <b>100GB</b>
- </span>
- <span style={{ color: "#1976d2", fontWeight: 500 }}>
- 下载量: <b>50GB</b>
- </span>
+ <div style={{ color: '#222', fontWeight: 500, marginRight: 24 }}>{userInfo.username || '用户栏'}</div>
+ <div style={{ display: 'flex', gap: 28, flex: 1, justifyContent: 'flex-end', alignItems: 'center' }}>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>魔力值: <b>{userPT.magic}</b></span>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>分享率: <b>{userPT.ratio}</b></span>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>上传量: <b>{userPT.upload}</b></span>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>下载量: <b>{userPT.download}</b></span>
</div>
</div>
- {/* 下方内容整体下移,留出与音乐界面一致的间距 */}
+ {/* 下方内容整体下移,留出与资料界面一致的间距 */}
<div style={{ height: 32 }} />
<nav className="nav-bar card">
{navItems.map((item) => (
@@ -141,22 +129,17 @@
))}
</nav>
<div className="search-section card">
- <input className="search-input" placeholder="输入搜索关键词" />
- <button className="search-btn">
- <span role="img" aria-label="search">
- 🔍
- </span>
+ <input
+ className="search-input"
+ placeholder="输入搜索关键词"
+ value={searchText}
+ onChange={e => setSearchText(e.target.value)}
+ />
+ <button className="search-btn" onClick={handleSearch}>
+ <span role="img" aria-label="search">🔍</span>
</button>
</div>
- <div
- className="area-tabs"
- style={{
- display: "flex",
- justifyContent: "center",
- gap: 24,
- margin: "18px 0",
- }}
- >
+ <div className="area-tabs" style={{ display: 'flex', justifyContent: 'center', gap: 24, margin: '18px 0' }}>
{areaTabs.map((tab, idx) => (
<div
key={tab.label}
@@ -174,6 +157,9 @@
<th>资料类型</th>
<th>标题</th>
<th>发布者</th>
+ <th>大小</th>
+ <th>热度</th>
+ <th>折扣倍率</th>
</tr>
</thead>
<tbody>
@@ -190,11 +176,14 @@
{item.title}
</a>
</td>
- <td>{item.user.username}</td>
+ <td>{item.username}</td>
+ <td>{item.seedsize}</td>
+ <td>{item.downloadtimes}</td>
+ <td>{item.discount == null ? 1 : item.discount}</td>
</tr>
))
) : (
- infoTypesList.map((type, index) => (
+ infoTypes.map((type, index) => (
<tr key={type}>
<td>
<a href={`/torrent/${type}`} style={{ color: '#1a237e', textDecoration: 'none' }}>
@@ -207,6 +196,9 @@
</a>
</td>
<td>发布者{index + 1}</td>
+ <td>--</td>
+ <td>--</td>
+ <td>1</td>
</tr>
))
)}
@@ -224,24 +216,10 @@
const total = 5;
return (
<div className="pagination">
- <button
- onClick={() => setPage((p) => Math.max(1, p - 1))}
- disabled={page === 1}
- >
- 上一页
- </button>
- <span className="page-num">
- {page}/{total}
- </span>
- <button
- onClick={() => setPage((p) => Math.min(total, p + 1))}
- disabled={page === total}
- >
- 下一页
- </button>
- <span className="page-info">
- 第 <b>{page}</b> 页
- </span>
+ <button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1}>上一页</button>
+ <span className="page-num">{page}/{total}</span>
+ <button onClick={() => setPage(p => Math.min(total, p + 1))} disabled={page === total}>下一页</button>
+ <span className="page-info">第 <b>{page}</b> 页</span>
</div>
);
-}
+}
\ No newline at end of file
diff --git a/front/src/MigrationPage.js b/front/src/MigrationPage.js
index 1a9f79b..b8235e4 100644
--- a/front/src/MigrationPage.js
+++ b/front/src/MigrationPage.js
@@ -24,6 +24,7 @@
const [selectedId, setSelectedId] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
+ const [grantedUploadInput, setGrantedUploadInput] = useState(0);
// Helper to load migrations list
const fetchMigrations = async () => {
@@ -32,7 +33,6 @@
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,
@@ -57,18 +57,34 @@
fetchMigrations();
}, []);
- if (loading) return <div>加载中...</div>;
- if (error) return <div>加载失败:{error}</div>;
-
+ // 获取当前选中的迁移申请
const selectedMigration = migrations.find(m => m.migration_id === selectedId) || {};
- // Approve selected migration and refresh
+ // 每次切换迁移申请时,重置输入框为0或当前已迁移量
+ useEffect(() => {
+ // 审核通过后,输入框应为已迁移量,否则为0
+ if (selectedMigration.approved === 1) {
+ setGrantedUploadInput(selectedMigration.granted_uploaded || 0);
+ } else {
+ setGrantedUploadInput(0);
+ }
+ }, [selectedId, migrations, selectedMigration.approved, selectedMigration.granted_uploaded]);
+
+ // 审核通过
const handleApprove = async () => {
+ const max = selectedMigration.pending_uploaded || 0;
+ let value = Number(grantedUploadInput);
+ if (isNaN(value) || value < 0) value = 0;
+ if (value > max) value = max;
+
try {
const res = await fetch(`${API_BASE_URL}/api/migrations-approve`, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ migration_id: selectedMigration.migration_id })
+ body: JSON.stringify({
+ migration_id: selectedMigration.migration_id,
+ granted_uploaded: value
+ })
});
if (!res.ok) throw new Error(`请求失败,状态码 ${res.status}`);
alert("已通过迁移");
@@ -77,13 +93,17 @@
alert(`操作失败:${err.message}`);
}
};
- // Reject selected migration and refresh
+
+ // 审核不通过
const handleReject = async () => {
try {
const res = await fetch(`${API_BASE_URL}/api/migrations-reject`, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ migration_id: selectedMigration.migration_id })
+ body: JSON.stringify({
+ migration_id: selectedMigration.migration_id,
+ granted_uploaded: 0
+ })
});
if (!res.ok) throw new Error(`请求失败,状态码 ${res.status}`);
alert("已拒绝迁移");
@@ -93,6 +113,9 @@
}
};
+ if (loading) return <div>加载中...</div>;
+ if (error) return <div>加载失败:{error}</div>;
+
return (
<div style={{ display: "flex", minHeight: "100vh", background: "#f7faff" }}>
{/* 侧栏 */}
@@ -143,12 +166,32 @@
<FileViewer url={selectedMigration.application_url} />
</div>
<div style={{ marginBottom: 18 }}>
- <b>待迁移魔法值:</b>{selectedMigration.pending_magic},
- <b>已迁移魔法值:</b>{selectedMigration.granted_magic}
- </div>
- <div style={{ marginBottom: 18 }}>
<b>待迁移上传量:</b>{selectedMigration.pending_uploaded},
- <b>已迁移上传量:</b>{selectedMigration.granted_uploaded}
+ <b>已迁移上传量:</b>
+ {selectedMigration.approved === 1 ? (
+ <span>{selectedMigration.granted_uploaded}</span>
+ ) : (
+ <input
+ type="number"
+ min={0}
+ max={selectedMigration.pending_uploaded}
+ value={grantedUploadInput}
+ onChange={e => {
+ let val = Number(e.target.value);
+ if (isNaN(val) || val < 0) val = 0;
+ if (val > selectedMigration.pending_uploaded) val = selectedMigration.pending_uploaded;
+ setGrantedUploadInput(val);
+ }}
+ style={{
+ width: 100,
+ marginLeft: 8,
+ padding: "4px 8px",
+ borderRadius: 4,
+ border: "1px solid #bdbdbd"
+ }}
+ disabled={selectedMigration.approved === 1}
+ />
+ )}
</div>
</div>
{/* 审核按钮 */}
diff --git a/front/src/MoviePage.js b/front/src/MoviePage.js
index cdc8112..79f2c8b 100644
--- a/front/src/MoviePage.js
+++ b/front/src/MoviePage.js
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react";
+import HomeIcon from "@mui/icons-material/Home";
import MovieIcon from "@mui/icons-material/Movie";
import EmailIcon from "@mui/icons-material/Email";
import MusicNoteIcon from "@mui/icons-material/MusicNote";
@@ -9,11 +10,12 @@
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
import ForumIcon from "@mui/icons-material/Forum";
import HelpIcon from "@mui/icons-material/Help";
-import { useNavigate } from "react-router-dom";
import "./App.css";
+import { useNavigate } from "react-router-dom";
import { API_BASE_URL } from "./config";
const navItems = [
+ { label: "首页", icon: <HomeIcon />, path: "/home" },
{ label: "电影", icon: <MovieIcon />, path: "/movie" },
{ label: "剧集", icon: <EmailIcon />, path: "/tv" },
{ label: "音乐", icon: <MusicNoteIcon />, path: "/music" },
@@ -31,13 +33,6 @@
{ label: "港台", icon: <EmailIcon fontSize="small" /> },
{ label: "欧美", icon: <PersonIcon fontSize="small" /> },
{ label: "日韩", icon: <EmojiPeopleIcon fontSize="small" /> },
- // { label: "其他", icon: <PersonIcon fontSize="small" /> },
-];
-
-const exampleTorrents = [
- { type: "Action", title: "实例1", id: 1 },
- { type: "Drama", title: "实例2", id: 2 },
- { type: "Comedy", title: "实例3", id: 3 },
];
export default function MoviePage() {
@@ -47,17 +42,14 @@
const [userPT, setUserPT] = useState({ magic: 0, ratio: 0, upload: 0, download: 0 });
const [activeTab, setActiveTab] = React.useState(0);
const [movieList, setMovieList] = React.useState([]);
-
useEffect(() => {
const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
const userId = match ? match[2] : null;
- console.log("User ID from cookie:", userId);
if (!userId) return;
fetch(`${API_BASE_URL}/api/get-userpt?userid=${encodeURIComponent(userId)}`)
.then(res => res.json())
.then(data => {
- console.log('用户PT信息:', data);
setUserInfo({ avatar_url: data.user.avatar_url, username: data.user.username });
setUserPT({
magic: data.magic_value || data.magic || 0,
@@ -80,12 +72,10 @@
const movieTypes = movieTypesList[activeTab] || [];
React.useEffect(() => {
- // 这里假设后端接口为 /api/get-seed-list-by-tag?tag=大陆
const area = areaTabs[activeTab].label;
fetch(`${API_BASE_URL}/api/get-seed-list-by-tag?tag=${encodeURIComponent(area)}`)
.then(res => res.json())
.then(data => {
- console.log('电影区返回数据:', data);
setMovieList(data);
})
.catch(() => setMovieList([]));
@@ -97,7 +87,6 @@
fetch(`${API_BASE_URL}/api/search-seeds?tag=${encodeURIComponent(area)}&keyword=${encodeURIComponent(searchText)}`)
.then(res => res.json())
.then(data => {
- console.log('搜索返回数据:', data);
setMovieList(data);
})
.catch(() => setMovieList([]));
@@ -166,6 +155,9 @@
<th>电影类型</th>
<th>标题</th>
<th>发布者</th>
+ <th>大小</th>
+ <th>热度</th>
+ <th>折扣倍率</th>
</tr>
</thead>
<tbody>
@@ -182,7 +174,10 @@
{item.title}
</a>
</td>
- <td>{item.user.username}</td>
+ <td>{item.username}</td>
+ <td>{item.seedsize}</td>
+ <td>{item.downloadtimes}</td>
+ <td>{item.discount == null ? 1 : item.discount}</td>
</tr>
))
) : (
@@ -199,6 +194,9 @@
</a>
</td>
<td>发布者{index + 1}</td>
+ <td>--</td>
+ <td>--</td>
+ <td>1</td>
</tr>
))
)}
diff --git a/front/src/MusicPage.js b/front/src/MusicPage.js
index fb03fe1..097cf3c 100644
--- a/front/src/MusicPage.js
+++ b/front/src/MusicPage.js
@@ -1,4 +1,5 @@
-import React from "react";
+import React, { useState, useEffect } from "react";
+import HomeIcon from "@mui/icons-material/Home";
import MovieIcon from "@mui/icons-material/Movie";
import EmailIcon from "@mui/icons-material/Email";
import MusicNoteIcon from "@mui/icons-material/MusicNote";
@@ -7,11 +8,14 @@
import SportsMartialArtsIcon from "@mui/icons-material/SportsMartialArts";
import PersonIcon from "@mui/icons-material/Person";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
+import ForumIcon from "@mui/icons-material/Forum";
+import HelpIcon from "@mui/icons-material/Help";
import "./App.css";
import { useNavigate } from "react-router-dom";
import { API_BASE_URL } from "./config";
const navItems = [
+ { label: "首页", icon: <HomeIcon />, path: "/home" },
{ label: "电影", icon: <MovieIcon />, path: "/movie" },
{ label: "剧集", icon: <EmailIcon />, path: "/tv" },
{ label: "音乐", icon: <MusicNoteIcon />, path: "/music" },
@@ -19,7 +23,9 @@
{ label: "游戏", icon: <SportsEsportsIcon />, path: "/game" },
{ label: "体育", icon: <SportsMartialArtsIcon />, path: "/sport" },
{ label: "资料", icon: <PersonIcon />, path: "/info" },
- { label: "发布", icon: <AccountCircleIcon />, path: "/publish" }, // Added Publish option
+ { label: "论坛", icon: <ForumIcon />, path: "/forum" },
+ { label: "发布", icon: <AccountCircleIcon />, path: "/publish" },
+ { label: "求种", icon: <HelpIcon />, path: "/begseed" },
];
const areaTabs = [
@@ -30,29 +36,43 @@
{ label: "说唱", icon: <PersonIcon fontSize="small" /> },
];
-const exampleTorrents = [
- { type: "Pop", title: "实例1", id: 1 },
- { type: "Rock", title: "实例2", id: 2 },
- { type: "Jazz", title: "实例3", id: 3 },
-];
-
export default function MusicPage() {
const navigate = useNavigate();
- const [activeTab, setActiveTab] = React.useState(0);
- const [musicList, setMusicList] = React.useState([]);
+ const [searchText, setSearchText] = useState('');
+ const [userInfo, setUserInfo] = useState({ avatar_url: '', username: '' });
+ const [userPT, setUserPT] = useState({ magic: 0, ratio: 0, upload: 0, download: 0 });
+ const [activeTab, setActiveTab] = useState(0);
+ const [musicList, setMusicList] = useState([]);
+
+ useEffect(() => {
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userId = match ? match[2] : null;
+ if (!userId) return;
+ fetch(`${API_BASE_URL}/api/get-userpt?userid=${encodeURIComponent(userId)}`)
+ .then(res => res.json())
+ .then(data => {
+ setUserInfo({ avatar_url: data.user.avatar_url, username: data.user.username });
+ setUserPT({
+ magic: data.magic_value || data.magic || 0,
+ ratio: data.share_ratio || data.share || 0,
+ upload: data.upload_amount || data.upload || 0,
+ download: data.download_amount || data.download || 0,
+ });
+ })
+ .catch(err => console.error('Fetching user profile failed', err));
+ }, []);
// 每个tab对应的音乐类型
const musicTypesList = [
- ["华语音乐(大陆)", "欧美音乐", "日韩音乐", "港台音乐", "其他"], // 大陆
- ["港台流行", "港台摇滚", "港台其他"], // 港台
- ["欧美流行", "欧美摇滚", "欧美其他"], // 欧美
- ["日韩流行", "日韩其他"], // 日韩
- ["其他类型1", "其他类型2"] // 其他
+ ["华语音乐(大陆)", "欧美音乐", "日韩音乐", "港台音乐", "其他"], // 古典音乐
+ ["港台流行", "港台摇滚", "港台其他"], // 流行音乐
+ ["欧美流行", "欧美摇滚", "欧美其他"], // 摇滚
+ ["日韩流行", "日韩其他"], // 电子音乐
+ ["其他类型1", "其他类型2"] // 说唱
];
const musicTypes = musicTypesList[activeTab] || [];
- React.useEffect(() => {
- // 假设后端接口为 /api/musics?area=大陆
+ useEffect(() => {
const area = areaTabs[activeTab].label;
fetch(`${API_BASE_URL}/api/get-seed-list-by-tag?tag=${encodeURIComponent(area)}`)
.then(res => res.json())
@@ -60,23 +80,38 @@
.catch(() => setMusicList([]));
}, [activeTab]);
+ // 搜索按钮处理
+ const handleSearch = () => {
+ const area = areaTabs[activeTab].label;
+ fetch(`${API_BASE_URL}/api/search-seeds?tag=${encodeURIComponent(area)}&keyword=${encodeURIComponent(searchText)}`)
+ .then(res => res.json())
+ .then(data => {
+ setMusicList(data);
+ })
+ .catch(() => setMusicList([]));
+ };
+
return (
- <div className="container music-bg">
+ <div className="container">
{/* 顶部空白与音乐界面一致,用户栏绝对定位在页面右上角 */}
<div style={{ height: 80 }} />
<div className="user-bar" style={{ position: 'fixed', top: 18, right: 42, zIndex: 100, display: 'flex', alignItems: 'center', background: '#e0f3ff', borderRadius: 12, padding: '6px 18px', boxShadow: '0 2px 8px #b2d8ea', minWidth: 320, minHeight: 48, width: 420 }}>
<div style={{ cursor: 'pointer', marginRight: 16 }} onClick={() => navigate('/user')}>
- <AccountCircleIcon style={{ fontSize: 38, color: '#1a237e', background: '#e0f3ff', borderRadius: '50%' }} />
+ {userInfo.avatar_url ? (
+ <img src={userInfo.avatar_url} alt="用户头像" style={{ width: 38, height: 38, borderRadius: '50%', objectFit: 'cover' }} />
+ ) : (
+ <AccountCircleIcon style={{ fontSize: 38, color: '#1a237e', background: '#e0f3ff', borderRadius: '50%' }} />
+ )}
</div>
- <div style={{ color: '#222', fontWeight: 500, marginRight: 24 }}>用户栏</div>
+ <div style={{ color: '#222', fontWeight: 500, marginRight: 24 }}>{userInfo.username || '用户栏'}</div>
<div style={{ display: 'flex', gap: 28, flex: 1, justifyContent: 'flex-end', alignItems: 'center' }}>
- <span style={{ color: '#1976d2', fontWeight: 500 }}>魔力值: <b>12345</b></span>
- <span style={{ color: '#1976d2', fontWeight: 500 }}>分享率: <b>2.56</b></span>
- <span style={{ color: '#1976d2', fontWeight: 500 }}>上传量: <b>100GB</b></span>
- <span style={{ color: '#1976d2', fontWeight: 500 }}>下载量: <b>50GB</b></span>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>魔力值: <b>{userPT.magic}</b></span>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>分享率: <b>{userPT.ratio}</b></span>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>上传量: <b>{userPT.upload}</b></span>
+ <span style={{ color: '#1976d2', fontWeight: 500 }}>下载量: <b>{userPT.download}</b></span>
</div>
</div>
- {/* 下方内容整体下移,留出与其他页面一致的间距 */}
+ {/* 下方内容整体下移,留出与音乐界面一致的间距 */}
<div style={{ height: 32 }} />
<nav className="nav-bar card">
{navItems.map((item) => (
@@ -91,12 +126,17 @@
))}
</nav>
<div className="search-section card">
- <input className="search-input" placeholder="输入搜索关键词" />
- <button className="search-btn">
+ <input
+ className="search-input"
+ placeholder="输入搜索关键词"
+ value={searchText}
+ onChange={e => setSearchText(e.target.value)}
+ />
+ <button className="search-btn" onClick={handleSearch}>
<span role="img" aria-label="search">🔍</span>
</button>
</div>
- <div className="area-tabs card" style={{ display: 'flex', justifyContent: 'center', gap: 24, margin: '18px 0' }}>
+ <div className="area-tabs" style={{ display: 'flex', justifyContent: 'center', gap: 24, margin: '18px 0' }}>
{areaTabs.map((tab, idx) => (
<div
key={tab.label}
@@ -114,6 +154,9 @@
<th>音乐类型</th>
<th>标题</th>
<th>发布者</th>
+ <th>大小</th>
+ <th>热度</th>
+ <th>折扣倍率</th>
</tr>
</thead>
<tbody>
@@ -130,7 +173,10 @@
{item.title}
</a>
</td>
- <td>{item.user.username}</td>
+ <td>{item.username}</td>
+ <td>{item.seedsize}</td>
+ <td>{item.downloadtimes}</td>
+ <td>{item.discount == null ? 1 : item.discount}</td>
</tr>
))
) : (
@@ -147,6 +193,9 @@
</a>
</td>
<td>发布者{index + 1}</td>
+ <td>--</td>
+ <td>--</td>
+ <td>1</td>
</tr>
))
)}
@@ -170,4 +219,4 @@
<span className="page-info">第 <b>{page}</b> 页</span>
</div>
);
-}
+}
\ No newline at end of file
diff --git a/front/src/PublishPage.js b/front/src/PublishPage.js
index 52eb956..df03cae 100644
--- a/front/src/PublishPage.js
+++ b/front/src/PublishPage.js
@@ -31,7 +31,7 @@
const file = e.target.files[0];
if (file && file.name.split('.').pop() !== 'torrent') {
alert('仅能上传.torrent类型文件');
- e.target.value = null; // Clear the input
+ e.target.value = null;
} else {
setFormData({ ...formData, torrentFile: file });
}
@@ -39,25 +39,21 @@
const handleSubmit = async (e) => {
e.preventDefault();
- // 假设userid和tag可以从表单或用户信息中获取,这里用示例数据
- const userid = '550e8400-e29b-41d4-a716-446655440000';
- const tag = formData.type ? formData.type : '高清';
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userId = match ? match[2] : null;
+
if (!formData.torrentFile) {
alert('请上传.torrent文件');
return;
}
const data = new FormData();
- data.append('userid', userid);
+ data.append('userid', userId);
data.append('title', formData.title);
data.append('tag', subType);
data.append('file', formData.torrentFile);
data.append('subtitle', formData.subtitle);
- // data.append('subtype', subType);
- // console.log(data.get('tag'));
try {
- // console.log(data.get('userid'));
- // console.log(data.get('title'));
const response = await fetch(`${API_BASE_URL}/api/save-torrent`, {
method: 'POST',
body: data,
diff --git a/front/src/SeedPromotionPage.js b/front/src/SeedPromotionPage.js
index 249533c..6e09f62 100644
--- a/front/src/SeedPromotionPage.js
+++ b/front/src/SeedPromotionPage.js
@@ -1,45 +1,73 @@
import React, { useEffect, useState } from "react";
import { API_BASE_URL } from "./config";
-// 示例数据,实际应从后端获取
-const mockSeeds = [
- {
- seed_id: "seed001",
- title: "三体 1080P 蓝光",
- tags: "科幻,电影",
- popularity: 123,
- promotion: {
- start_time: "",
- end_time: "",
- discount: 1,
- },
- },
- {
- seed_id: "seed002",
- title: "灌篮高手 国语配音",
- tags: "动画,体育",
- popularity: 88,
- promotion: {
- start_time: "",
- end_time: "",
- discount: 1,
- },
- },
-];
-
export default function SeedPromotionPage() {
const [seeds, setSeeds] = useState([]);
const [currentTime, setCurrentTime] = useState("");
+ const [loading, setLoading] = useState(true);
+ // 时间戳转datetime-local字符串
+ const tsToDatetimeLocal = (ts) => {
+ if (!ts) return "";
+ const d = new Date(ts);
+ const pad = (n) => n.toString().padStart(2, "0");
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
+ };
+
+ // 加载所有种子和促销信息
useEffect(() => {
- // 获取当前时间,格式为 yyyy-MM-ddTHH:mm
const now = new Date();
const pad = (n) => n.toString().padStart(2, "0");
const localISOTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;
setCurrentTime(localISOTime);
- // 实际应从后端获取种子及促销信息
- setSeeds(mockSeeds);
+ async function fetchData() {
+ setLoading(true);
+ try {
+ // 获取所有种子
+ const seedsRes = await fetch(`${API_BASE_URL}/api/all-seeds`);
+ if (!seedsRes.ok) throw new Error("获取种子列表失败");
+ const seedsData = await seedsRes.json();
+
+ // 获取所有促销信息
+ const promoRes = await fetch(`${API_BASE_URL}/api/all-seed-promotions`);
+ if (!promoRes.ok) throw new Error("获取促销信息失败");
+ const promoData = await promoRes.json();
+
+ // 构建促销信息映射
+ const promoMap = {};
+ promoData.forEach(p => {
+ const seedid = p.seed?.seedid;
+ promoMap[seedid] = {
+ start_time: p.startTime ? tsToDatetimeLocal(p.startTime) : "",
+ end_time: p.endTime ? tsToDatetimeLocal(p.endTime) : "",
+ discount: typeof p.discount === "number" ? p.discount : 1,
+ };
+ });
+
+ // 合并数据
+ const mergedSeeds = seedsData.map(seed => ({
+ seed_id: seed.seedid,
+ title: seed.title,
+ tags: seed.seedtag,
+ popularity: seed.downloadtimes,
+ promotion: promoMap[seed.seedid] || {
+ start_time: "",
+ end_time: "",
+ discount: 1,
+ },
+ }));
+
+ setSeeds(mergedSeeds);
+ } catch (err) {
+ setSeeds([]);
+ alert(err.message || "加载失败");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ fetchData();
}, []);
// 输入框变更处理
@@ -64,101 +92,126 @@
return start && end && end < start;
};
+ // 提交促销设置
+ const handleStartPromotion = async (seed) => {
+ const { start_time, end_time, discount } = seed.promotion;
+ try {
+ const res = await fetch(`${API_BASE_URL}/api/set-seed-promotion`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ seed_id: seed.seed_id,
+ start_time,
+ end_time,
+ discount,
+ }),
+ });
+ if (!res.ok) {
+ const errData = await res.json();
+ throw new Error(errData.message || "促销设置失败");
+ }
+ alert(`已为「${seed.title}」开启促销!`);
+ } catch (err) {
+ alert(err.message || "促销设置失败");
+ }
+ };
+
return (
<div style={{ padding: 40, maxWidth: 900, margin: "0 auto" }}>
<h1 style={{ textAlign: "center", marginBottom: 32 }}>种子促销管理</h1>
- <table style={{ width: "100%", background: "#fff", borderRadius: 10, boxShadow: "0 2px 8px #e0e7ff" }}>
- <thead>
- <tr style={{ background: "#f5f5f5" }}>
- <th>标题</th>
- <th>标签</th>
- <th>热度</th>
- <th>促销开始时间</th>
- <th>促销结束时间</th>
- <th>促销倍率</th>
- <th>操作</th>
- </tr>
- </thead>
- <tbody>
- {seeds.map((seed) => {
- const { start_time, end_time, discount } = seed.promotion;
- const endTimeInvalid = isEndTimeInvalid(start_time, end_time);
- const canStartPromotion = start_time && end_time && !endTimeInvalid && discount >= 1;
- return (
- <tr key={seed.seed_id}>
- <td>{seed.title}</td>
- <td>{seed.tags}</td>
- <td>{seed.popularity}</td>
- <td>
- <input
- type="datetime-local"
- value={start_time}
- min={currentTime}
- onChange={(e) =>
- handlePromotionChange(seed.seed_id, "start_time", e.target.value)
- }
- />
- </td>
- <td>
- <input
- type="datetime-local"
- value={end_time}
- min={start_time || currentTime}
- onChange={(e) =>
- handlePromotionChange(seed.seed_id, "end_time", e.target.value)
- }
- style={endTimeInvalid ? { border: "1.5px solid #e53935" } : {}}
- />
- {endTimeInvalid && (
- <div style={{ color: "#e53935", fontSize: 12 }}>
- 结束时间不能早于开始时间
+ {loading ? (
+ <div style={{ textAlign: "center", color: "#666", margin: 40 }}>正在加载...</div>
+ ) : (
+ <table style={{ width: "100%", background: "#fff", borderRadius: 10, boxShadow: "0 2px 8px #e0e7ff" }}>
+ <thead>
+ <tr style={{ background: "#f5f5f5" }}>
+ <th>标题</th>
+ <th>标签</th>
+ <th>热度</th>
+ <th>促销开始时间</th>
+ <th>促销结束时间</th>
+ <th>促销倍率</th>
+ <th>操作</th>
+ </tr>
+ </thead>
+ <tbody>
+ {seeds.map((seed) => {
+ const { start_time, end_time, discount } = seed.promotion;
+ const endTimeInvalid = isEndTimeInvalid(start_time, end_time);
+ const canStartPromotion = start_time && end_time && !endTimeInvalid && discount >= 1;
+ return (
+ <tr key={seed.seed_id}>
+ <td>{seed.title}</td>
+ <td>{seed.tags}</td>
+ <td>{seed.popularity}</td>
+ <td>
+ <input
+ type="datetime-local"
+ value={start_time}
+ min={currentTime}
+ onChange={(e) =>
+ handlePromotionChange(seed.seed_id, "start_time", e.target.value)
+ }
+ />
+ </td>
+ <td>
+ <input
+ type="datetime-local"
+ value={end_time}
+ min={start_time || currentTime}
+ onChange={(e) =>
+ handlePromotionChange(seed.seed_id, "end_time", e.target.value)
+ }
+ style={endTimeInvalid ? { border: "1.5px solid #e53935" } : {}}
+ />
+ {endTimeInvalid && (
+ <div style={{ color: "#e53935", fontSize: 12 }}>
+ 结束时间不能早于开始时间
+ </div>
+ )}
+ </td>
+ <td>
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
+ <button
+ style={{ width: 28, height: 28, fontSize: 18, borderRadius: 4, border: "1px solid #ccc", background: "#f5f5f5", cursor: discount > 1 ? "pointer" : "not-allowed" }}
+ onClick={() =>
+ discount > 1 &&
+ handlePromotionChange(seed.seed_id, "discount", discount - 1)
+ }
+ disabled={discount <= 1}
+ >-</button>
+ <span style={{ minWidth: 24, textAlign: "center" }}>{discount}</span>
+ <button
+ style={{ width: 28, height: 28, fontSize: 18, borderRadius: 4, border: "1px solid #ccc", background: "#f5f5f5", cursor: "pointer" }}
+ onClick={() =>
+ handlePromotionChange(seed.seed_id, "discount", discount + 1)
+ }
+ >+</button>
</div>
- )}
- </td>
- <td>
- <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
+ </td>
+ <td>
<button
- style={{ width: 28, height: 28, fontSize: 18, borderRadius: 4, border: "1px solid #ccc", background: "#f5f5f5", cursor: discount > 1 ? "pointer" : "not-allowed" }}
- onClick={() =>
- discount > 1 &&
- handlePromotionChange(seed.seed_id, "discount", discount - 1)
- }
- disabled={discount <= 1}
- >-</button>
- <span style={{ minWidth: 24, textAlign: "center" }}>{discount}</span>
- <button
- style={{ width: 28, height: 28, fontSize: 18, borderRadius: 4, border: "1px solid #ccc", background: "#f5f5f5", cursor: "pointer" }}
- onClick={() =>
- handlePromotionChange(seed.seed_id, "discount", discount + 1)
- }
- >+</button>
- </div>
- </td>
- <td>
- <button
- style={{
- background: canStartPromotion ? "#1976d2" : "#ccc",
- color: "#fff",
- border: "none",
- borderRadius: 6,
- padding: "4px 16px",
- cursor: canStartPromotion ? "pointer" : "not-allowed",
- fontWeight: 600,
- }}
- disabled={!canStartPromotion}
- onClick={() => {
- // 这里可调用后端API开启促销
- alert(`已为「${seed.title}」开启促销!`);
- }}
- >
- 开启促销
- </button>
- </td>
- </tr>
- );
- })}
- </tbody>
- </table>
+ style={{
+ background: canStartPromotion ? "#1976d2" : "#ccc",
+ color: "#fff",
+ border: "none",
+ borderRadius: 6,
+ padding: "4px 16px",
+ cursor: canStartPromotion ? "pointer" : "not-allowed",
+ fontWeight: 600,
+ }}
+ disabled={!canStartPromotion}
+ onClick={() => handleStartPromotion(seed)}
+ >
+ 开启促销
+ </button>
+ </td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ )}
</div>
);
}
\ No newline at end of file
diff --git a/front/src/SportPage.js b/front/src/SportPage.js
index 7dc452d..2874495 100644
--- a/front/src/SportPage.js
+++ b/front/src/SportPage.js
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react";
+import HomeIcon from "@mui/icons-material/Home";
import MovieIcon from "@mui/icons-material/Movie";
import EmailIcon from "@mui/icons-material/Email";
import MusicNoteIcon from "@mui/icons-material/MusicNote";
@@ -7,12 +8,14 @@
import SportsMartialArtsIcon from "@mui/icons-material/SportsMartialArts";
import PersonIcon from "@mui/icons-material/Person";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
+import ForumIcon from "@mui/icons-material/Forum";
+import HelpIcon from "@mui/icons-material/Help";
import "./App.css";
import { useNavigate } from "react-router-dom";
import { API_BASE_URL } from "./config";
-import ForumIcon from "@mui/icons-material/Forum";
const navItems = [
+ { label: "首页", icon: <HomeIcon />, path: "/home" },
{ label: "电影", icon: <MovieIcon />, path: "/movie" },
{ label: "剧集", icon: <EmailIcon />, path: "/tv" },
{ label: "音乐", icon: <MusicNoteIcon />, path: "/music" },
@@ -21,65 +24,56 @@
{ label: "体育", icon: <SportsMartialArtsIcon />, path: "/sport" },
{ label: "资料", icon: <PersonIcon />, path: "/info" },
{ label: "论坛", icon: <ForumIcon />, path: "/forum" },
- { label: "发布", icon: <AccountCircleIcon />, path: "/publish" }, // Added Publish option
+ { label: "发布", icon: <AccountCircleIcon />, path: "/publish" },
+ { label: "求种", icon: <HelpIcon />, path: "/begseed" },
];
-const sportTypes = [""];
-
const areaTabs = [
{ label: "篮球", icon: <MovieIcon fontSize="small" /> },
{ label: "足球", icon: <EmailIcon fontSize="small" /> },
{ label: "羽毛球", icon: <EmojiPeopleIcon fontSize="small" /> },
{ label: "排球", icon: <PersonIcon fontSize="small" /> },
- { label: "电竞", icon: <PersonIcon fontSize="small" />, active: true },
-];
-
-const exampleTorrents = [
- { type: "Soccer", title: "实例1", id: 1 },
- { type: "Basketball", title: "实例2", id: 2 },
- { type: "Tennis", title: "实例3", id: 3 },
+ { label: "电竞", icon: <PersonIcon fontSize="small" /> },
];
export default function SportPage() {
const navigate = useNavigate();
- const [searchText, setSearchText] = React.useState('');
- const [userInfo, setUserInfo] = useState({ avatar_url: '', username: '' });
- const [userPT, setUserPT] = useState({ magic: 0, ratio: 0, upload: 0, download: 0 });
- const [activeTab, setActiveTab] = React.useState(0);
- const [sportList, setSportList] = React.useState([]);
+ const [searchText, setSearchText] = useState('');
+ const [userInfo, setUserInfo] = useState({ avatar_url: '', username: '' });
+ const [userPT, setUserPT] = useState({ magic: 0, ratio: 0, upload: 0, download: 0 });
+ const [activeTab, setActiveTab] = useState(0);
+ const [sportList, setSportList] = useState([]);
- useEffect(() => {
- const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
- const userId = match ? match[2] : null;
- console.log("User ID from cookie:", userId);
- if (!userId) return;
- fetch(`${API_BASE_URL}/api/get-userpt?userid=${encodeURIComponent(userId)}`)
- .then(res => res.json())
- .then(data => {
- console.log('用户PT信息:', data);
- setUserInfo({ avatar_url: data.user.avatar_url, username: data.user.username });
- setUserPT({
- magic: data.magic_value || data.magic || 0,
- ratio: data.share_ratio || data.share || 0,
- upload: data.upload_amount || data.upload || 0,
- download: data.download_amount || data.download || 0,
- });
- })
- .catch(err => console.error('Fetching user profile failed', err));
- }, []);
+ useEffect(() => {
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userId = match ? match[2] : null;
+ if (!userId) return;
+ fetch(`${API_BASE_URL}/api/get-userpt?userid=${encodeURIComponent(userId)}`)
+ .then(res => res.json())
+ .then(data => {
+ setUserInfo({ avatar_url: data.user.avatar_url, username: data.user.username });
+ setUserPT({
+ magic: data.magic_value || data.magic || 0,
+ ratio: data.share_ratio || data.share || 0,
+ upload: data.upload_amount || data.upload || 0,
+ download: data.download_amount || data.download || 0,
+ });
+ })
+ .catch(err => console.error('Fetching user profile failed', err));
+ }, []);
- // 每个tab对应的运动类型
+ // 每个tab对应的体育类型
const sportTypesList = [
["篮球赛事", "篮球技巧", "篮球明星"], // 篮球
["足球赛事", "足球技巧", "足球明星"], // 足球
["羽毛球赛事", "羽毛球技巧"], // 羽毛球
["排球赛事", "排球技巧"], // 排球
["电竞赛事", "电竞技巧"], // 电竞
+ ["其他类型1", "其他类型2"] // 其他
];
const sportTypes = sportTypesList[activeTab] || [];
- React.useEffect(() => {
- // 假设后端接口为 /api/sports?area=篮球
+ useEffect(() => {
const area = areaTabs[activeTab].label;
fetch(`${API_BASE_URL}/api/get-seed-list-by-tag?tag=${encodeURIComponent(area)}`)
.then(res => res.json())
@@ -87,21 +81,20 @@
.catch(() => setSportList([]));
}, [activeTab]);
- // 搜索按钮处理
- const handleSearch = () => {
- const area = areaTabs[activeTab].label;
- fetch(`${API_BASE_URL}/api/search-seeds?tag=${encodeURIComponent(area)}&keyword=${encodeURIComponent(searchText)}`)
- .then(res => res.json())
- .then(data => {
- console.log('搜索返回数据:', data);
- setSportList(data);
- })
- .catch(() => setSportList([]));
- };
+ // 搜索按钮处理
+ const handleSearch = () => {
+ const area = areaTabs[activeTab].label;
+ fetch(`${API_BASE_URL}/api/search-seeds?tag=${encodeURIComponent(area)}&keyword=${encodeURIComponent(searchText)}`)
+ .then(res => res.json())
+ .then(data => {
+ setSportList(data);
+ })
+ .catch(() => setSportList([]));
+ };
return (
<div className="container">
- {/* 顶部空白与音乐界面一致,用户栏绝对定位在页面右上角 */}
+ {/* 顶部空白与体育界面一致,用户栏绝对定位在页面右上角 */}
<div style={{ height: 80 }} />
<div className="user-bar" style={{ position: 'fixed', top: 18, right: 42, zIndex: 100, display: 'flex', alignItems: 'center', background: '#e0f3ff', borderRadius: 12, padding: '6px 18px', boxShadow: '0 2px 8px #b2d8ea', minWidth: 320, minHeight: 48, width: 420 }}>
<div style={{ cursor: 'pointer', marginRight: 16 }} onClick={() => navigate('/user')}>
@@ -119,7 +112,7 @@
<span style={{ color: '#1976d2', fontWeight: 500 }}>下载量: <b>{userPT.download}</b></span>
</div>
</div>
- {/* 下方内容整体下移,留出与音乐界面一致的间距 */}
+ {/* 下方内容整体下移,留出与体育界面一致的间距 */}
<div style={{ height: 32 }} />
<nav className="nav-bar card">
{navItems.map((item) => (
@@ -144,15 +137,7 @@
<span role="img" aria-label="search">🔍</span>
</button>
</div>
- <div
- className="area-tabs"
- style={{
- display: "flex",
- justifyContent: "center",
- gap: 24,
- margin: "18px 0",
- }}
- >
+ <div className="area-tabs" style={{ display: 'flex', justifyContent: 'center', gap: 24, margin: '18px 0' }}>
{areaTabs.map((tab, idx) => (
<div
key={tab.label}
@@ -170,6 +155,9 @@
<th>体育类型</th>
<th>标题</th>
<th>发布者</th>
+ <th>大小</th>
+ <th>热度</th>
+ <th>折扣倍率</th>
</tr>
</thead>
<tbody>
@@ -186,7 +174,10 @@
{item.title}
</a>
</td>
- <td>{item.user.username}</td>
+ <td>{item.username}</td>
+ <td>{item.seedsize}</td>
+ <td>{item.downloadtimes}</td>
+ <td>{item.discount == null ? 1 : item.discount}</td>
</tr>
))
) : (
@@ -203,6 +194,9 @@
</a>
</td>
<td>发布者{index + 1}</td>
+ <td>--</td>
+ <td>--</td>
+ <td>1</td>
</tr>
))
)}
@@ -220,24 +214,10 @@
const total = 5;
return (
<div className="pagination">
- <button
- onClick={() => setPage((p) => Math.max(1, p - 1))}
- disabled={page === 1}
- >
- 上一页
- </button>
- <span className="page-num">
- {page}/{total}
- </span>
- <button
- onClick={() => setPage((p) => Math.min(total, p + 1))}
- disabled={page === total}
- >
- 下一页
- </button>
- <span className="page-info">
- 第 <b>{page}</b> 页
- </span>
+ <button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1}>上一页</button>
+ <span className="page-num">{page}/{total}</span>
+ <button onClick={() => setPage(p => Math.min(total, p + 1))} disabled={page === total}>下一页</button>
+ <span className="page-info">第 <b>{page}</b> 页</span>
</div>
);
-}
+}
\ No newline at end of file
diff --git a/front/src/TVPage.js b/front/src/TVPage.js
index 7e33a87..d75a172 100644
--- a/front/src/TVPage.js
+++ b/front/src/TVPage.js
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react";
+import HomeIcon from "@mui/icons-material/Home";
import MovieIcon from "@mui/icons-material/Movie";
import EmailIcon from "@mui/icons-material/Email";
import MusicNoteIcon from "@mui/icons-material/MusicNote";
@@ -7,11 +8,14 @@
import SportsMartialArtsIcon from "@mui/icons-material/SportsMartialArts";
import PersonIcon from "@mui/icons-material/Person";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
-import { useNavigate } from "react-router-dom";
+import ForumIcon from "@mui/icons-material/Forum";
+import HelpIcon from "@mui/icons-material/Help";
import "./App.css";
+import { useNavigate } from "react-router-dom";
import { API_BASE_URL } from "./config";
const navItems = [
+ { label: "首页", icon: <HomeIcon />, path: "/home" },
{ label: "电影", icon: <MovieIcon />, path: "/movie" },
{ label: "剧集", icon: <EmailIcon />, path: "/tv" },
{ label: "音乐", icon: <MusicNoteIcon />, path: "/music" },
@@ -19,15 +23,9 @@
{ label: "游戏", icon: <SportsEsportsIcon />, path: "/game" },
{ label: "体育", icon: <SportsMartialArtsIcon />, path: "/sport" },
{ label: "资料", icon: <PersonIcon />, path: "/info" },
- { label: "发布", icon: <AccountCircleIcon />, path: "/publish" }, // Added Publish option
-];
-
-const tvTypesList = [
- ["华语剧集(大陆)", "欧美剧集", "日韩剧集", "港台剧集", "其他"], // 大陆
- ["港台都市", "港台爱情", "港台悬疑", "港台其他"], // 港台
- ["欧美悬疑", "欧美历史", "欧美其他"], // 欧美
- ["日韩青春", "日韩家庭", "日韩其他"], // 日韩
- ["其他类型1", "其他类型2"] // 其他
+ { label: "论坛", icon: <ForumIcon />, path: "/forum" },
+ { label: "发布", icon: <AccountCircleIcon />, path: "/publish" },
+ { label: "求种", icon: <HelpIcon />, path: "/begseed" },
];
const areaTabs = [
@@ -35,36 +33,33 @@
{ label: "港剧", icon: <EmailIcon fontSize="small" /> },
{ label: "欧美剧", icon: <PersonIcon fontSize="small" /> },
{ label: "日韩剧", icon: <EmojiPeopleIcon fontSize="small" /> },
- // { label: "其他", icon: <PersonIcon fontSize="small" /> },
];
export default function TVPage() {
const navigate = useNavigate();
const [searchText, setSearchText] = React.useState('');
- const [userInfo, setUserInfo] = useState({ avatar_url: '', username: '' });
- const [userPT, setUserPT] = useState({ magic: 0, ratio: 0, upload: 0, download: 0 });
+ const [userInfo, setUserInfo] = useState({ avatar_url: '', username: '' });
+ const [userPT, setUserPT] = useState({ magic: 0, ratio: 0, upload: 0, download: 0 });
const [activeTab, setActiveTab] = React.useState(0);
const [tvList, setTvList] = React.useState([]);
- useEffect(() => {
- const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
- const userId = match ? match[2] : null;
- console.log("User ID from cookie:", userId);
- if (!userId) return;
- fetch(`${API_BASE_URL}/api/get-userpt?userid=${encodeURIComponent(userId)}`)
- .then(res => res.json())
- .then(data => {
- console.log('用户PT信息:', data);
- setUserInfo({ avatar_url: data.user.avatar_url, username: data.user.username });
- setUserPT({
- magic: data.magic_value || data.magic || 0,
- ratio: data.share_ratio || data.share || 0,
- upload: data.upload_amount || data.upload || 0,
- download: data.download_amount || data.download || 0,
- });
- })
- .catch(err => console.error('Fetching user profile failed', err));
- }, []);
+ useEffect(() => {
+ const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
+ const userId = match ? match[2] : null;
+ if (!userId) return;
+ fetch(`${API_BASE_URL}/api/get-userpt?userid=${encodeURIComponent(userId)}`)
+ .then(res => res.json())
+ .then(data => {
+ setUserInfo({ avatar_url: data.user.avatar_url, username: data.user.username });
+ setUserPT({
+ magic: data.magic_value || data.magic || 0,
+ ratio: data.share_ratio || data.share || 0,
+ upload: data.upload_amount || data.upload || 0,
+ download: data.download_amount || data.download || 0,
+ });
+ })
+ .catch(err => console.error('Fetching user profile failed', err));
+ }, []);
React.useEffect(() => {
// 假设后端接口为 /api/tvs?area=大陆
@@ -75,19 +70,25 @@
.catch(() => setTvList([]));
}, [activeTab]);
- // 搜索按钮处理
- const handleSearch = () => {
- const area = areaTabs[activeTab].label;
- fetch(`${API_BASE_URL}/api/search-seeds?tag=${encodeURIComponent(area)}&keyword=${encodeURIComponent(searchText)}`)
- .then(res => res.json())
- .then(data => {
- console.log('搜索返回数据:', data);
- setTvList(data);
- })
- .catch(() => setTvList([]));
- };
+ // 搜索按钮处理
+ const handleSearch = () => {
+ const area = areaTabs[activeTab].label;
+ fetch(`${API_BASE_URL}/api/search-seeds?tag=${encodeURIComponent(area)}&keyword=${encodeURIComponent(searchText)}`)
+ .then(res => res.json())
+ .then(data => {
+ console.log('搜索返回数据:', data);
+ setTvList(data);
+ })
+ .catch(() => setTvList([]));
+ };
- // 每个tab对应的剧集类型
+ const tvTypesList = [
+ ["华语剧集(大陆)", "欧美剧集", "日韩剧集", "港台剧集", "其他"], // 大陆
+ ["港台都市", "港台爱情", "港台悬疑", "港台其他"], // 港台
+ ["欧美悬疑", "欧美历史", "欧美其他"], // 欧美
+ ["日韩青春", "日韩家庭", "日韩其他"], // 日韩
+ ["其他类型1", "其他类型2"] // 其他
+ ];
const tvTypes = tvTypesList[activeTab] || [];
return (
@@ -153,6 +154,9 @@
<th>剧集类型</th>
<th>标题</th>
<th>发布者</th>
+ <th>大小</th>
+ <th>热度</th>
+ <th>折扣倍率</th>
</tr>
</thead>
<tbody>
@@ -169,7 +173,10 @@
{item.title}
</a>
</td>
- <td>{item.user.username}</td>
+ <td>{item.username}</td>
+ <td>{item.seedsize}</td>
+ <td>{item.downloadtimes}</td>
+ <td>{item.discount == null ? 1 : item.discount}</td>
</tr>
))
) : (
@@ -186,6 +193,9 @@
</a>
</td>
<td>发布者{index + 1}</td>
+ <td>--</td>
+ <td>--</td>
+ <td>1</td>
</tr>
))
)}
diff --git a/front/src/UserProfile.js b/front/src/UserProfile.js
index eed84da..fb44b43 100644
--- a/front/src/UserProfile.js
+++ b/front/src/UserProfile.js
@@ -51,9 +51,11 @@
// 账号迁移相关
const [migrationOpen, setMigrationOpen] = useState(false);
+ const [migrationUpload, setMigrationUpload] = useState('');
const [migrationEmail, setMigrationEmail] = useState('');
const [migrationPassword, setMigrationPassword] = useState('');
const [migrationStatus, setMigrationStatus] = useState('');
+
// 兑换结果计算
React.useEffect(() => {
if (!exchangeMagic || isNaN(exchangeMagic)) {
@@ -66,17 +68,13 @@
// 获取用户信息
useEffect(() => {
const fetchUserInfo = async () => {
- // 假设userid存储在localStorage或其他地方
- // 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;
+ const userid = match ? match[2] : null;
if (!userid) return;
try {
const res = await fetch(`${API_BASE_URL}/api/user-profile?userid=${userid}`);
if (res.ok) {
const data = await res.json();
- // console.log("获取用户信息:", data);
setUserInfo(data);
setTempUserInfo(data);
}
@@ -87,13 +85,10 @@
fetchUserInfo();
}, []);
- // 获取上传种子
useEffect(() => {
const fetchUserSeeds = async () => {
- // 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;
+ const userid = match ? match[2] : null;
if (!userid) return;
try {
const res = await fetch(`${API_BASE_URL}/api/user-seeds?userid=${userid}`);
@@ -107,7 +102,7 @@
};
fetchUserSeeds();
}, []);
- // 获取收藏种子
+
useEffect(() => {
const fetchUserFavorites = async () => {
const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
@@ -126,7 +121,7 @@
};
fetchUserFavorites();
}, []);
- // 获取活跃度
+
useEffect(() => {
const fetchUserStats = async () => {
const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
@@ -150,15 +145,13 @@
};
const handleSave = async () => {
- if (tempUserInfo.gender === "男"){
+ if (tempUserInfo.gender === "男") {
tempUserInfo.gender = "m";
- }else if (tempUserInfo.gender === "女"){
+ } else if (tempUserInfo.gender === "女") {
tempUserInfo.gender = "f";
}
setUserInfo({ ...tempUserInfo });
- // 获取userid
- // 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;
try {
@@ -205,7 +198,7 @@
return;
}
try {
- const res = await fetch(`${API_BASE_URL}/api/invite`, {
+ const res = await fetch(`${API_BASE_URL}/api/invite`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userid, invite_email: inviteEmail }),
@@ -226,7 +219,9 @@
console.error("邀请失败", err);
setInviteStatus("邀请失败,请检查网络");
}
- }; // 兑换
+ };
+
+ // 兑换魔力值
const handleExchange = async () => {
const magic = Number(exchangeMagic);
if (!magic || isNaN(magic) || magic <= 0) return;
@@ -234,14 +229,14 @@
alert("魔力值不足!");
return;
}
-
+
// 检查兑换结果是否为整数
const calculatedExchangeResult = magic / exchangeRate[exchangeType];
if (!Number.isInteger(calculatedExchangeResult)) {
alert("兑换结果必须为整数,请调整魔力值!");
return;
}
-
+
// 获取userid
const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
const userid = match ? match[2] : null;
@@ -256,9 +251,9 @@
const res = await fetch(`${API_BASE_URL}/api/exchange`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- userid,
- magic,
+ body: JSON.stringify({
+ userid,
+ magic,
exchangeType,
exchangeResult: calculatedExchangeResult
}),
@@ -300,7 +295,7 @@
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userid, seedid }),
- }); if (res.ok) {
+ }); if (res.ok) {
setUserFavorites(userFavorites.filter((s) => (s.seedid || s.seed_id) !== seedid));
alert('已取消收藏');
} else {
@@ -315,14 +310,13 @@
// 申诉提交逻辑
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);
@@ -346,37 +340,42 @@
alert('申诉失败,请检查网络');
}
};
- // 账号迁移提交逻辑
+
+ // 账号迁移
const handleMigrationSubmit = async () => {
if (!appealFile) {
setMigrationStatus('请选择PDF文件');
return;
}
-
- // 获取当前用户ID
+ if (!migrationUpload || isNaN(migrationUpload) || Number(migrationUpload) <= 0) {
+ setMigrationStatus('请输入有效的待发放上传量');
+ return;
+ }
+
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);
-
+ formData.append('uploadtogive', migrationUpload);
+
const res = await fetch(`${API_BASE_URL}/api/migrate-account`, {
method: 'POST',
body: formData,
});
-
+
if (res.ok) {
setMigrationStatus('账号迁移申请已提交,请等待管理员审核');
setTimeout(() => {
setMigrationOpen(false);
setAppealFile(null);
+ setMigrationUpload('');
setMigrationStatus('');
}, 2000);
} else {
@@ -682,7 +681,7 @@
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 match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
const userid = match ? match[2] : null;
@@ -734,39 +733,40 @@
}}>
{userFavorites.length === 0 ? (
<div style={{ color: '#b2b2b2', fontSize: 18, textAlign: 'center' }}>(暂无收藏种子)</div>
- ) : ( <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
- {userFavorites.map((seed, idx) => (
- <li
- key={seed.seedid || idx}
- style={{
- display: 'flex',
- alignItems: 'center',
- padding: '12px 0',
- borderBottom: idx === userFavorites.length - 1 ? 'none' : '1px solid #e0e7ff',
- cursor: 'pointer',
- transition: 'background 0.15s'
- }} onClick={e => { if (e.target.classList.contains('remove-favorite-btn')) return;
- navigate(`/torrent/${seed.seedid || seed.seed_id}`);
+ ) : (<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
+ {userFavorites.map((seed, idx) => (
+ <li
+ key={seed.seedid || idx}
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ padding: '12px 0',
+ borderBottom: idx === userFavorites.length - 1 ? 'none' : '1px solid #e0e7ff',
+ cursor: 'pointer',
+ transition: 'background 0.15s'
+ }} 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', 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);
}}
- onMouseOver={e => e.currentTarget.style.background = '#f3f6ff'}
- onMouseOut={e => e.currentTarget.style.background = ''}
- >
- <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>
+ >取消收藏</Button>
+ </li>
+ ))}
+ </ul>
)}
</div>
</div>
@@ -807,12 +807,24 @@
<Button onClick={handleAppealSubmit} variant="contained" color="primary" disabled={!appealTitle || !appealFile}>提交</Button>
<Button onClick={() => setAppealOpen(false)} variant="outlined">取消</Button>
</DialogActions>
- </Dialog> {/* 账号迁移弹窗 */}
+ </Dialog>
+ {/* 账号迁移弹窗 */}
<Dialog open={migrationOpen} onClose={() => setMigrationOpen(false)}>
<DialogTitle>账号迁移</DialogTitle>
<DialogContent>
<div style={{ marginBottom: 16 }}>
- </div> <div>
+ <TextField
+ label="待发放上传量"
+ type="number"
+ fullWidth
+ value={migrationUpload}
+ onChange={e => setMigrationUpload(e.target.value)}
+ size="small"
+ inputProps={{ min: 1 }}
+ style={{ marginBottom: 18 }}
+ />
+ </div>
+ <div>
<input
type="file"
accept=".pdf"