新增种子收藏和种子促销,首页推荐显示
Change-Id: Ia8632b7909845230a0becf1616dc647e0c7e292b
diff --git a/front/src/AdminPage.js b/front/src/AdminPage.js
index dac05cb..f063c09 100644
--- a/front/src/AdminPage.js
+++ b/front/src/AdminPage.js
@@ -51,11 +51,6 @@
}
};
- const handleConfigChange = (e) => {
- const { name, value } = e.target;
- setConfig({ ...config, [name]: value });
- };
-
const handleBan = (user) => {
const adminId = getUserIdFromCookie();
if (!adminId) {
@@ -97,28 +92,6 @@
.catch((err) => console.error('解封用户失败:', err));
};
- // 保存系统参数到后端
- const handleSave = () => {
- const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
- const userId = match ? match[2] : null;
- if (!userId) {
- alert('无法获取用户ID');
- return;
- }
- fetch(`${API_BASE_URL}/api/save-config`, {
- method: 'POST',
- // credentials: 'include',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ userid: userId, ...config }),
- })
- .then((res) => {
- if (!res.ok) throw new Error('Network response was not ok');
- return res.json();
- })
- .then(() => alert('系统参数已保存'))
- .catch((err) => console.error('保存系统参数失败:', err));
- };
-
// 初始化时向后端请求系统参数及用户列表
useEffect(() => {
const match = document.cookie.match('(^|;)\\s*userId=([^;]+)');
@@ -149,37 +122,10 @@
{/* 参数设置 */}
<div style={{ marginBottom: 32, padding: 18, background: "#f7faff", borderRadius: 12, display: "flex", gap: 24, alignItems: "center", justifyContent: "space-between" }}>
<b>系统参数:</b>
- <label>
- FarmNumber:
- <input type="number" name="FarmNumber" value={config.FarmNumber} onChange={handleConfigChange} disabled style={{ width: 60, margin: "0 12px" }} />
- </label>
- <label>
- FakeTime:
- <input type="number" name="FakeTime" value={config.FakeTime} onChange={handleConfigChange} disabled style={{ width: 60, margin: "0 12px" }} />
- </label>
- <label>
- BegVote:
- <input type="number" name="BegVote" value={config.BegVote} onChange={handleConfigChange} disabled style={{ width: 60, margin: "0 12px" }} />
- </label>
- <label>
- CheatTime:
- <input type="number" name="CheatTime" value={config.CheatTime} onChange={handleConfigChange} disabled style={{ width: 60, margin: "0 12px" }} />
- </label>
- {/* <button
- onClick={handleSave}
- style={{
- background: '#1976d2',
- color: '#fff',
- border: 'none',
- borderRadius: 6,
- padding: '6px 18px',
- cursor: 'pointer',
- writingMode: 'horizontal-tb',
- whiteSpace: 'nowrap'
- }}
- >
- 保存
- </button> */}
+ <span>FarmNumber: {config.FarmNumber}</span>
+ <span>FakeTime: {config.FakeTime}</span>
+ <span>BegVote: {config.BegVote}</span>
+ <span>CheatTime: {config.CheatTime}</span>
</div>
{/* 作弊用户 */}
<div style={{ marginBottom: 32 }}>
@@ -201,7 +147,7 @@
<td>{u.email}</td>
<td>{u.username}</td>
<td style={{ color: "#e53935" }}>
- {"封禁" }
+ {"封禁"}
</td>
<td>
<button
@@ -265,6 +211,12 @@
>
用户迁移
</button>
+ <button
+ style={{ background: "#ff9800", color: "#fff", border: "none", borderRadius: 8, padding: "10px 28px", fontWeight: 600, fontSize: 16, cursor: "pointer" }}
+ onClick={() => navigate("/seed-promotion")}
+ >
+ 促销管理
+ </button>
</div>
</div>
);
diff --git a/front/src/App.js b/front/src/App.js
index 58fb58c..ad42aff 100644
--- a/front/src/App.js
+++ b/front/src/App.js
@@ -1,5 +1,6 @@
import React from "react";
import { BrowserRouter as Router, Routes, Route, useNavigate, Link, Navigate } from "react-router-dom";
+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";
@@ -31,6 +32,8 @@
import MigrationPage from './MigrationPage';
import BegSeedPage from "./BegSeedPage";
import BegInfo from "./BegInfo";
+import SeedPromotionPage from "./SeedPromotionPage";
+import HomePage from "./HomePage";
const navItems = [
@@ -160,6 +163,7 @@
<Route path="/" element={<Navigate to="/login" replace />} />
{/* Protected routes */}
<Route element={<RequireAuth />}>
+ <Route path="/home" element={<HomePage />} />
<Route path="/movie" element={<MoviePage />} />
<Route path="/tv" element={<TVPage />} />
<Route path="/music" element={<MusicPage />} />
@@ -175,6 +179,7 @@
<Route path="/admin" element={<AdminPage />} />
<Route path="/appeal-review" element={<AppealPage />} />
<Route path="/migration-review" element={<MigrationPage />} />
+ <Route path="/seed-promotion" element={<SeedPromotionPage />} />
<Route path="/begseed" element={<BegSeedPage />} />
<Route path="/begseed/:begid" element={<BegInfo />} />
</Route>
diff --git a/front/src/HomePage.js b/front/src/HomePage.js
new file mode 100644
index 0000000..a657a72
--- /dev/null
+++ b/front/src/HomePage.js
@@ -0,0 +1,126 @@
+import React 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";
+import EmojiPeopleIcon from "@mui/icons-material/EmojiPeople";
+import SportsEsportsIcon from "@mui/icons-material/SportsEsports";
+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 { useNavigate } from "react-router-dom";
+import "./App.css";
+
+// 导航栏
+const navItems = [
+ { label: "首页", icon: <HomeIcon />, path: "/home" },
+ { label: "电影", icon: <MovieIcon />, path: "/movie" },
+ { label: "剧集", icon: <EmailIcon />, path: "/tv" },
+ { label: "音乐", icon: <MusicNoteIcon />, path: "/music" },
+ { label: "动漫", icon: <EmojiPeopleIcon />, path: "/anime" },
+ { label: "游戏", icon: <SportsEsportsIcon />, path: "/game" },
+ { label: "体育", icon: <SportsMartialArtsIcon />, path: "/sport" },
+ { label: "资料", icon: <PersonIcon />, path: "/info" },
+ { label: "论坛", icon: <ForumIcon />, path: "/forum" },
+ { label: "发布", icon: <AccountCircleIcon />, path: "/publish" },
+ { label: "求种", icon: <HelpIcon />, path: "/begseed" },
+];
+
+// 示例种子数据
+const exampleSeeds = [
+ {
+ id: 1,
+ tags: "电影,科幻",
+ title: "三体 1080P 蓝光",
+ popularity: 123,
+ user: { username: "Alice" },
+ },
+ {
+ id: 2,
+ tags: "动漫,热血",
+ title: "灌篮高手 国语配音",
+ popularity: 88,
+ user: { username: "Bob" },
+ },
+ {
+ id: 3,
+ tags: "音乐,流行",
+ title: "周杰伦-稻香",
+ popularity: 56,
+ user: { username: "Jay" },
+ },
+ {
+ id: 4,
+ tags: "剧集,悬疑",
+ title: "隐秘的角落",
+ popularity: 77,
+ user: { username: "小明" },
+ },
+];
+
+export default function HomePage() {
+ const navigate = useNavigate();
+
+ 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>
+ <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>
+ </div>
+ {/* 下方内容整体下移,留出与电影界面一致的间距 */}
+ <div style={{ height: 32 }} />
+ <nav className="nav-bar card">
+ {navItems.map((item) => (
+ <div
+ key={item.label}
+ className={item.label === "首页" ? "nav-item active" : "nav-item"}
+ onClick={() => navigate(item.path)}
+ >
+ {item.icon}
+ <span>{item.label}</span>
+ </div>
+ ))}
+ </nav>
+ <div className="table-section card">
+ <table className="movie-table">
+ <thead>
+ <tr>
+ <th>标签</th>
+ <th>标题</th>
+ <th>热度</th>
+ <th>发布者</th>
+ </tr>
+ </thead>
+ <tbody>
+ {exampleSeeds.map((seed) => (
+ <tr key={seed.id}>
+ <td>{seed.tags}</td>
+ <td>
+ <a href={`/torrent/${seed.id}`} style={{ color: '#1a237e', textDecoration: 'none' }}>
+ {seed.title}
+ </a>
+ </td>
+ <td>{seed.popularity}</td>
+ <td>{seed.user.username}</td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ <div style={{ height: 32 }} />
+ </div>
+ );
+}
\ No newline at end of file
diff --git a/front/src/LoginPage.js b/front/src/LoginPage.js
index 492c018..31718d1 100644
--- a/front/src/LoginPage.js
+++ b/front/src/LoginPage.js
@@ -54,7 +54,7 @@
return;
}
setErrorMessage('');
- navigate('/movie');
+ navigate('/home');
} catch (error) {
setErrorMessage('网络错误,请稍后重试');
}
diff --git a/front/src/SeedPromotionPage.js b/front/src/SeedPromotionPage.js
new file mode 100644
index 0000000..249533c
--- /dev/null
+++ b/front/src/SeedPromotionPage.js
@@ -0,0 +1,164 @@
+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("");
+
+ 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);
+ }, []);
+
+ // 输入框变更处理
+ const handlePromotionChange = (seedId, field, value) => {
+ setSeeds((prev) =>
+ prev.map((s) =>
+ s.seed_id === seedId
+ ? {
+ ...s,
+ promotion: {
+ ...s.promotion,
+ [field]: value,
+ },
+ }
+ : s
+ )
+ );
+ };
+
+ // 结束时间校验
+ const isEndTimeInvalid = (start, end) => {
+ return start && end && end < start;
+ };
+
+ 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 }}>
+ 结束时间不能早于开始时间
+ </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>
+ <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>
+ </div>
+ );
+}
\ No newline at end of file
diff --git a/front/src/TorrentDetailPage.js b/front/src/TorrentDetailPage.js
index cc2dd24..ab34d98 100644
--- a/front/src/TorrentDetailPage.js
+++ b/front/src/TorrentDetailPage.js
@@ -10,12 +10,13 @@
const [error, setError] = React.useState(null);
// 假设你从某个地方获取了 userId(例如登录状态、localStorage 等)
const [userId] = React.useState('user1550e8400-e29b-41d4-a716-44665544000023'); // 替换为实际的用户 ID
+ const [isFavorite, setIsFavorite] = React.useState(false); // 收藏状态
const handleClick = () => {
// 构造下载 URL,包含 userId 和 torrentId 参数
console.log(torrentId)
const downloadUrl = `${API_BASE_URL}/api/get-torrent?userId=${encodeURIComponent(userId)}&torrentId=${encodeURIComponent(torrentId)}`;
-
+
// 发起 GET 请求下载文件
fetch(downloadUrl)
.then(response => {
@@ -41,6 +42,11 @@
});
};
+ // 收藏按钮示例逻辑(仅前端切换)
+ const handleFavorite = () => {
+ setIsFavorite(fav => !fav);
+ };
+
React.useEffect(() => {
setLoading(true);
setError(null);
@@ -64,24 +70,65 @@
if (!detail) return <div className="container"><h1>未找到详情</h1></div>;
return (
- <div className="container">
- <h1>种子详情页</h1>
- <h2 style={{ fontSize: 'inherit', fontWeight: 'normal', textAlign: 'left' }}>标题: {detail.title || `种子${torrentId}`}</h2>
- <p style={{ fontSize: 'inherit', textAlign: 'left' }}>简介: {detail.description || `这是种子${torrentId}的详细信息。`}</p>
- <div style={{ textAlign: 'center', marginTop: '20px' }}>
- <button
- style={{
- padding: '10px 20px',
- fontSize: '16px',
- cursor: 'pointer',
- backgroundColor: '#d3f0ff',
- border: 'none',
- borderRadius: '4px'
- }}
- onClick={handleClick}
- >
- 下载
- </button>
+ <div className="container" style={{ display: "flex", justifyContent: "center", alignItems: "center", minHeight: "100vh" }}>
+ <div
+ style={{
+ background: "#fff",
+ borderRadius: 16,
+ boxShadow: "0 4px 24px #e0e7ff",
+ padding: "36px 48px",
+ maxWidth: 540,
+ width: "100%",
+ marginTop: 48,
+ }}
+ >
+ <h1 style={{ color: "#1976d2", fontWeight: 700, marginBottom: 24, fontSize: 28, letterSpacing: 1 }}>种子详情页</h1>
+ <div style={{ marginBottom: 18 }}>
+ <div style={{ fontSize: 20, fontWeight: 600, marginBottom: 8, color: "#222" }}>
+ 标题:{detail.title || `种子${torrentId}`}
+ </div>
+ <div style={{ fontSize: 16, color: "#555", marginBottom: 8 }}>
+ 简介:{detail.description || `这是种子${torrentId}的详细信息。`}
+ </div>
+ </div>
+ <div style={{ display: "flex", gap: 24, marginTop: 32, justifyContent: "center" }}>
+ <button
+ style={{
+ padding: "10px 32px",
+ fontSize: "16px",
+ cursor: "pointer",
+ background: "linear-gradient(90deg, #42a5f5 0%, #1976d2 100%)",
+ color: "#fff",
+ border: "none",
+ borderRadius: "8px",
+ fontWeight: 600,
+ boxShadow: "0 2px 8px #b2d8ea",
+ transition: "background 0.2s",
+ }}
+ onClick={handleClick}
+ >
+ 下载
+ </button>
+ <button
+ style={{
+ padding: "10px 32px",
+ fontSize: "16px",
+ cursor: "pointer",
+ background: isFavorite
+ ? "linear-gradient(90deg, #ffb74d 0%, #ff9800 100%)"
+ : "linear-gradient(90deg, #f0f0f0 0%, #bdbdbd 100%)",
+ color: isFavorite ? "#fff" : "#333",
+ border: "none",
+ borderRadius: "8px",
+ fontWeight: 600,
+ boxShadow: isFavorite ? "0 2px 8px #ffe0b2" : "0 2px 8px #e0e7ff",
+ transition: "background 0.2s",
+ }}
+ onClick={handleFavorite}
+ >
+ {isFavorite ? "已收藏" : "收藏"}
+ </button>
+ </div>
</div>
</div>
);
diff --git a/src/main/java/entity/SeedPromotion.java b/src/main/java/entity/SeedPromotion.java
new file mode 100644
index 0000000..7bea9df
--- /dev/null
+++ b/src/main/java/entity/SeedPromotion.java
@@ -0,0 +1,32 @@
+package entity;
+
+import javax.persistence.*;
+import java.util.Date;
+import com.querydsl.core.annotations.QueryEntity;
+
+@QueryEntity
+@Entity
+@Table(name = "SeedPromotion")
+public class SeedPromotion {
+
+ @Id
+ @Column(name = "promotion_id", length = 64, nullable = false)
+ public String promotionId;
+
+ @ManyToOne(optional = false)
+ @JoinColumn(name = "seed_id", referencedColumnName = "seed_id", foreignKey = @ForeignKey(name = "fk_seed_promotion"), nullable = false)
+ public Seed seed;
+
+ @Column(name = "start_time", nullable = false)
+ @Temporal(TemporalType.TIMESTAMP)
+ public Date startTime;
+
+ @Column(name = "end_time", nullable = false)
+ @Temporal(TemporalType.TIMESTAMP)
+ public Date endTime;
+
+ @Column(name = "discount", nullable = false)
+ public int discount;
+
+ public SeedPromotion() {}
+}
\ No newline at end of file
diff --git "a/\345\274\200\345\217\221\346\226\207\346\241\243/create.sql" "b/\345\274\200\345\217\221\346\226\207\346\241\243/create.sql"
index 12d9233..24fe897 100644
--- "a/\345\274\200\345\217\221\346\226\207\346\241\243/create.sql"
+++ "b/\345\274\200\345\217\221\346\226\207\346\241\243/create.sql"
@@ -188,4 +188,15 @@
FOREIGN KEY (`user_id`) REFERENCES `User` (`user_id`) ON DELETE CASCADE,
FOREIGN KEY (`beg_id`) REFERENCES `BegSeed` (`beg_id`) ON DELETE CASCADE,
FOREIGN KEY (`seed_id`) REFERENCES `Seed` (`seed_id`) ON DELETE CASCADE
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
+
+-- 种子促销表
+CREATE TABLE `SeedPromotion` (
+ `promotion_id` VARCHAR(64) NOT NULL,
+ `seed_id` VARCHAR(64) NOT NULL,
+ `start_time` DATETIME NOT NULL,
+ `end_time` DATETIME NOT NULL,
+ `discount` TINYINT NOT NULL DEFAULT 1 COMMENT '折扣率, 1表示无折扣',
+ PRIMARY KEY (`promotion_id`),
+ FOREIGN KEY (`seed_id`) REFERENCES `Seed` (`seed_id`) ON DELETE CASCADE
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
\ No newline at end of file