blob: c97e13e3d8f69880f54fa43b4558ae25e704b07f [file] [log] [blame]
Jiarenxiang36728482025-06-07 21:51:26 +08001import React, { useEffect, useState } from "react";
2import { styled } from "@mui/material/styles";
3import axios from "axios";
4import {
Jiarenxiang24d681b2025-06-08 19:27:05 +08005 Box,
6 Button,
7 Card,
8 CardContent,
9 Chip,
10 CircularProgress,
11 Container,
12 MenuItem,
13 Pagination,
14 Select,
15 Typography,
16 TextField,
17 InputAdornment,
18 IconButton,
Jiarenxiang36728482025-06-07 21:51:26 +080019} from "@mui/material";
Jiarenxiang24d681b2025-06-08 19:27:05 +080020import { getCategories, getTorrentList,searchTorrents } from "../../services/bt/index";
21
Jiarenxiang36728482025-06-07 21:51:26 +080022// 优化后的星空背景
23const StarBg = styled("div")({
Jiarenxiang24d681b2025-06-08 19:27:05 +080024 minHeight: "100vh",
25 width: "auto",
26 background: "radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%)",
27 overflow: "auto",
28 position: "relative",
Jiarenxiang36728482025-06-07 21:51:26 +080029 top: 0,
30 left: 0,
Jiarenxiang24d681b2025-06-08 19:27:05 +080031 "&:before, &:after": {
32 content: '""',
33 position: "absolute",
34 top: 0,
35 left: 0,
36 width: "100%",
37 height: "100%",
38 pointerEvents: "none",
39 },
40 "&:before": {
41 background: `
42 radial-gradient(1px 1px at 20% 30%, rgba(255,255,255,0.8), rgba(255,255,255,0)),
43 radial-gradient(1px 1px at 40% 70%, rgba(255,255,255,0.8), rgba(255,255,255,0)),
44 radial-gradient(1.5px 1.5px at 60% 20%, rgba(255,255,255,0.9), rgba(255,255,255,0)),
45 radial-gradient(1.5px 1.5px at 80% 90%, rgba(255,255,255,0.9), rgba(255,255,255,0))
46 `,
47 backgroundRepeat: "repeat",
48 backgroundSize: "200px 200px",
49 animation: "twinkle 10s infinite ease-in-out",
50 },
51 "&:after": {
52 background: `
53 radial-gradient(1px 1px at 70% 40%, rgba(255,255,255,0.7), rgba(255,255,255,0)),
54 radial-gradient(1.2px 1.2px at 10% 80%, rgba(255,255,255,0.7), rgba(255,255,255,0)),
55 radial-gradient(1.5px 1.5px at 30% 60%, rgba(255,255,255,0.8), rgba(255,255,255,0))
56 `,
57 backgroundRepeat: "repeat",
58 backgroundSize: "300px 300px",
59 animation: "twinkle 15s infinite 5s ease-in-out",
60 },
61 "@keyframes twinkle": {
62 "0%, 100%": { opacity: 0.3 },
63 "50%": { opacity: 0.8 },
64 }
Jiarenxiang36728482025-06-07 21:51:26 +080065});
66
67// 分类标签栏
68const CategoryBar = styled(Box)({
Jiarenxiang24d681b2025-06-08 19:27:05 +080069 display: "flex",
70 gap: 12,
71 alignItems: "center",
72 marginBottom: 24,
73 flexWrap: "wrap",
Jiarenxiang36728482025-06-07 21:51:26 +080074});
75
76// 种子卡片
77const TorrentCard = styled(Card)({
Jiarenxiang24d681b2025-06-08 19:27:05 +080078 background: "rgba(30, 41, 59, 0.85)",
79 color: "#fff",
80 borderRadius: 16,
81 boxShadow: "0 4px 24px 0 rgba(0,0,0,0.4)",
82 border: "1px solid #334155",
83 transition: "transform 0.2s",
84 "&:hover": {
85 transform: "scale(1.025)",
86 boxShadow: "0 8px 32px 0 #0ea5e9",
87 },
Jiarenxiang36728482025-06-07 21:51:26 +080088});
89
90const sortOptions = [
Jiarenxiang24d681b2025-06-08 19:27:05 +080091 { label: "最新", value: { sortField: "create_time", sortDirection: "desc" } },
92 { label: "下载量", value: { sortField: "completions", sortDirection: "desc" } },
93 { label: "推荐", value: { sortField: "seeders", sortDirection: "desc" } },
Jiarenxiang36728482025-06-07 21:51:26 +080094];
95
Jiarenxiang24d681b2025-06-08 19:27:05 +080096const statusMap: Record<number, string> = {
97 0: '审核中',
98 1: '已发布',
99 2: '审核不通过',
100 3: '已上架修改重审中',
101 10: '已下架',
102};
103
Jiarenxiang36728482025-06-07 21:51:26 +0800104const PAGE_SIZE = 20;
105
106const TorrentListPage: React.FC = () => {
107 const [categories, setCategories] = useState<
108 { id: number; name: string; remark?: string | null; type?: number | null }[]
109 >([]);
110 const [selectedCat, setSelectedCat] = useState<number | null>(null);
111 const [sort, setSort] = useState(sortOptions[0].value);
112 const [torrents, setTorrents] = useState<any[]>([]);
113 const [loading, setLoading] = useState(false);
114 const [page, setPage] = useState(1);
115 const [total, setTotal] = useState(0);
116
Jiarenxiang24d681b2025-06-08 19:27:05 +0800117 // 搜索相关
118 const [searchKeyword, setSearchKeyword] = useState("");
119 const [searching, setSearching] = useState(false);
120 const [isSearchMode, setIsSearchMode] = useState(false);
121
Jiarenxiang36728482025-06-07 21:51:26 +0800122 // 获取分类
123 useEffect(() => {
124 getCategories().then((res) => {
Jiarenxiang36728482025-06-07 21:51:26 +0800125 setCategories(res.data || []);
126 });
127 }, []);
128
129 // 获取种子列表
130 useEffect(() => {
Jiarenxiang24d681b2025-06-08 19:27:05 +0800131 if (isSearchMode) return;
Jiarenxiang36728482025-06-07 21:51:26 +0800132 setLoading(true);
133 getTorrentList({
134 category: selectedCat !== null ? String(selectedCat) : undefined,
135 sortField: sort.sortField,
136 sortDirection: sort.sortDirection,
137 pageNum: page,
138 pageSize: PAGE_SIZE,
139 })
140 .then((res) => {
Jiarenxiang36728482025-06-07 21:51:26 +0800141 setTorrents(res.data || []);
142 setTotal(res.page?.total || 0);
143 })
144 .finally(() => setLoading(false));
Jiarenxiang24d681b2025-06-08 19:27:05 +0800145 }, [selectedCat, sort, page, isSearchMode]);
Jiarenxiang36728482025-06-07 21:51:26 +0800146
Jiarenxiang24d681b2025-06-08 19:27:05 +0800147 // 搜索
148 const handleSearch = async () => {
149 if (!searchKeyword.trim()) {
150 setIsSearchMode(false);
151 setPage(1);
152 return;
153 }
154 setSearching(true);
155 setIsSearchMode(true);
156 try {
157 const res = await searchTorrents({
158 titleKeyword: searchKeyword.trim(),
159 category: selectedCat !== null ? selectedCat : undefined,
160 sortField: sort.sortField,
161 sortDirection: sort.sortDirection,
162 pageNum: page,
163 pageSize: PAGE_SIZE,
164 });
165 setTorrents((res.records || []).map((r: any) => ({
166 ...r.torrent,
167 ownerInfo: r.ownerInfo,
168 })));
169 setTotal(res.total || 0);
170 } finally {
171 setSearching(false);
172 }
173 };
174
175 // 搜索模式下翻页
176 useEffect(() => {
177 if (!isSearchMode) return;
178 handleSearch();
179 // eslint-disable-next-line
180 }, [page, sort, selectedCat]);
181
182 // 切换分类/排序时退出搜索模式
183 useEffect(() => {
184 setIsSearchMode(false);
185 setSearchKeyword("");
186 setPage(1);
187 }, [selectedCat, sort]);
188
189 return (
190 <StarBg>
191 <Container maxWidth="lg" sx={{ pt: 6, pb: 6, position: "relative" }}>
192 <Typography
193 variant="h3"
Jiarenxiang36728482025-06-07 21:51:26 +0800194 sx={{
Jiarenxiang24d681b2025-06-08 19:27:05 +0800195 color: "#fff",
196 fontWeight: 700,
197 mb: 3,
198 letterSpacing: 2,
199 textShadow: "0 2px 16px #0ea5e9",
Jiarenxiang36728482025-06-07 21:51:26 +0800200 }}
Jiarenxiang24d681b2025-06-08 19:27:05 +0800201 >
202 ThunderHub 星空PT种子广场
203 </Typography>
204 {/* 搜索框 */}
205 <Box
206 sx={{
207 mb: 4,
208 display: "flex",
209 alignItems: "center",
210 gap: 2,
211 background: "rgba(30,41,59,0.7)",
212 borderRadius: 3,
213 px: 2,
214 py: 1.5,
215 boxShadow: "0 2px 12px 0 #0ea5e950",
216 border: "1px solid #334155",
217 maxWidth: 700,
218 mx: { xs: "auto", md: 0 },
219 }}
220 >
221 <TextField
222 variant="outlined"
223 size="small"
224 placeholder="🔍 搜索种子标题"
225 value={searchKeyword}
226 onChange={(e) => setSearchKeyword(e.target.value)}
227 onKeyDown={(e) => {
228 if (e.key === "Enter") {
229 setPage(1);
230 handleSearch();
231 }
232 }}
233 sx={{
234 background: "rgba(17,24,39,0.95)",
235 borderRadius: 2,
236 input: {
237 color: "#fff",
238 fontSize: 17,
239 fontWeight: 500,
240 letterSpacing: 1,
241 px: 1,
242 },
243 width: 320,
244 ".MuiOutlinedInput-notchedOutline": { border: 0 },
245 boxShadow: "0 1px 4px 0 #0ea5e930",
246 }}
247 InputProps={{
248 endAdornment: (
249 <InputAdornment position="end">
250 <IconButton
251 onClick={() => {
252 setPage(1);
253 handleSearch();
254 }}
255 edge="end"
256 sx={{
257 color: "#0ea5e9",
258 bgcolor: "#1e293b",
259 borderRadius: 2,
260 "&:hover": { bgcolor: "#0ea5e9", color: "#fff" },
261 transition: "all 0.2s",
262 }}
263 >
264 <svg width="22" height="22" fill="none" viewBox="0 0 24 24">
265 <circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="2"/>
266 <path stroke="currentColor" strokeWidth="2" strokeLinecap="round" d="M20 20l-3.5-3.5"/>
267 </svg>
268 </IconButton>
269 </InputAdornment>
270 ),
271 }}
272 />
273 {isSearchMode && (
274 <Button
275 variant="outlined"
276 color="secondary"
277 onClick={() => {
278 setIsSearchMode(false);
279 setSearchKeyword("");
280 setPage(1);
281 }}
282 sx={{
283 ml: 1,
284 color: "#fff",
285 borderColor: "#64748b",
286 borderRadius: 2,
287 fontWeight: 600,
288 letterSpacing: 1,
289 px: 2,
290 py: 0.5,
291 background: "rgba(51,65,85,0.7)",
292 "&:hover": {
293 borderColor: "#0ea5e9",
294 background: "#0ea5e9",
295 color: "#fff",
296 },
297 transition: "all 0.2s",
298 }}
299 >
300 清除搜索
301 </Button>
302 )}
303 <Box sx={{ flex: 1 }} />
304 <Button
305 variant="contained"
306 color="primary"
307 onClick={() => window.open("/torrent-upload", "_self")}
308 sx={{
309 borderRadius: 2,
310 fontWeight: 700,
311 letterSpacing: 1,
312 px: 3,
313 py: 1,
314 boxShadow: "0 2px 8px 0 #0ea5e950",
315 background: "#0ea5e9",
316 color: "#fff",
317 "&:hover": {
318 background: "#38bdf8",
319 },
320 transition: "all 0.2s",
321 }}
322 >
323 分享资源
324 </Button>
325 </Box>
326 <CategoryBar>
Jiarenxiang36728482025-06-07 21:51:26 +0800327 <Chip
Jiarenxiang24d681b2025-06-08 19:27:05 +0800328 label="全部"
329 color={selectedCat === null ? "primary" : "default"}
Jiarenxiang36728482025-06-07 21:51:26 +0800330 onClick={() => {
Jiarenxiang24d681b2025-06-08 19:27:05 +0800331 setSelectedCat(null);
Jiarenxiang36728482025-06-07 21:51:26 +0800332 setPage(1);
333 }}
334 sx={{
335 fontWeight: 600,
336 fontSize: 16,
Jiarenxiang24d681b2025-06-08 19:27:05 +0800337 color: selectedCat === null ? undefined : "#fff",
Jiarenxiang36728482025-06-07 21:51:26 +0800338 }}
339 />
Jiarenxiang24d681b2025-06-08 19:27:05 +0800340 {categories.map((cat) => (
341 <Chip
342 key={cat.id}
343 label={cat.name}
344 color={selectedCat === cat.id ? "primary" : "default"}
345 onClick={() => {
346 setSelectedCat(cat.id);
347 setPage(1);
348 }}
349 sx={{
350 fontWeight: 600,
351 fontSize: 16,
352 color: selectedCat === cat.id ? undefined : "#fff",
353 }}
354 />
Jiarenxiang36728482025-06-07 21:51:26 +0800355 ))}
Jiarenxiang24d681b2025-06-08 19:27:05 +0800356 <Box sx={{ flex: 1 }} />
357 <Select
358 size="small"
359 value={JSON.stringify(sort)}
360 onChange={(e) => {
361 setSort(JSON.parse(e.target.value));
362 setPage(1);
363 }}
Jiarenxiang36728482025-06-07 21:51:26 +0800364 sx={{
Jiarenxiang24d681b2025-06-08 19:27:05 +0800365 color: "#fff",
366 background: "#1e293b",
367 borderRadius: 2,
368 ".MuiOutlinedInput-notchedOutline": { border: 0 },
369 minWidth: 120,
Jiarenxiang36728482025-06-07 21:51:26 +0800370 }}
371 >
Jiarenxiang24d681b2025-06-08 19:27:05 +0800372 {sortOptions.map((opt) => (
373 <MenuItem key={opt.label} value={JSON.stringify(opt.value)}>
374 {opt.label}
375 </MenuItem>
376 ))}
377 </Select>
378 </CategoryBar>
379 {(loading || searching) ? (
380 <Box sx={{ display: "flex", justifyContent: "center", mt: 8 }}>
381 <CircularProgress color="info" />
382 </Box>
383 ) : (
384 <>
385 <Box
386 sx={{
387 display: "flex",
388 flexWrap: "wrap",
389 gap: 3,
390 justifyContent: { xs: "center", md: "flex-start" },
391 }}
392 >
393 {torrents.map((torrent) => (
394 <Box
395 key={torrent.id}
396 sx={{
397 flex: "1 1 320px",
398 maxWidth: { xs: "100%", sm: "48%", md: "32%" },
399 minWidth: 300,
400 mb: 3,
401 display: "flex",
402 cursor: "pointer",
403 }}
404 onClick={() => window.open(`/torrent-detail/${torrent.id}`, "_self")}
405 >
406 <TorrentCard sx={{ width: "100%" }}>
407 <CardContent>
408 <Typography
409 variant="h6"
Jiarenxiang36728482025-06-07 21:51:26 +0800410 sx={{
Jiarenxiang24d681b2025-06-08 19:27:05 +0800411 color: "#38bdf8",
412 fontWeight: 700,
413 mb: 1,
414 textOverflow: "ellipsis",
415 overflow: "hidden",
416 whiteSpace: "nowrap",
Jiarenxiang36728482025-06-07 21:51:26 +0800417 }}
Jiarenxiang24d681b2025-06-08 19:27:05 +0800418 title={torrent.title}
419 >
420 {torrent.title}
421 </Typography>
422 <Box
423 sx={{
424 display: "flex",
425 gap: 1,
426 alignItems: "center",
427 mb: 1,
428 flexWrap: "wrap",
429 }}
430 >
Jiarenxiang36728482025-06-07 21:51:26 +0800431 <Chip
432 size="small"
Jiarenxiang24d681b2025-06-08 19:27:05 +0800433 label={
434 categories.find((c) => c.id === torrent.category)?.name ||
435 "未知"
436 }
Jiarenxiang36728482025-06-07 21:51:26 +0800437 sx={{
Jiarenxiang24d681b2025-06-08 19:27:05 +0800438 background: "#0ea5e9",
439 color: "#fff",
Jiarenxiang36728482025-06-07 21:51:26 +0800440 fontWeight: 600,
441 }}
442 />
Jiarenxiang24d681b2025-06-08 19:27:05 +0800443 {torrent.free === "1" && (
444 <Chip
445 size="small"
446 label="促销"
447 sx={{
448 background: "#fbbf24",
449 color: "#1e293b",
450 fontWeight: 600,
451 }}
452 />
453 )}
454 {/* 状态标签 */}
455 {typeof torrent.status !== "undefined" && (
456 <Chip
457 size="small"
458 label={statusMap[torrent.status] || "未知状态"}
459 sx={{
460 background: "#64748b",
461 color: "#fff",
462 fontWeight: 600,
463 }}
464 />
465 )}
466 </Box>
467 <Typography variant="body2" sx={{ color: "#cbd5e1", mb: 1 }}>
468 上传时间:{torrent.createTime}
Jiarenxiang36728482025-06-07 21:51:26 +0800469 </Typography>
Jiarenxiang24d681b2025-06-08 19:27:05 +0800470 <Box sx={{ display: "flex", gap: 2, mt: 1 }}>
471 <Typography variant="body2" sx={{ color: "#38bdf8" }}>
472 做种:{torrent.seeders}
473 </Typography>
474 <Typography variant="body2" sx={{ color: "#f472b6" }}>
475 下载量:{torrent.completions}
476 </Typography>
477 <Typography variant="body2" sx={{ color: "#fbbf24" }}>
478 下载中:{torrent.leechers}
479 </Typography>
480 </Box>
481 </CardContent>
482 </TorrentCard>
483 </Box>
484 ))}
485 </Box>
486 <Box sx={{ display: "flex", justifyContent: "center", mt: 4 }}>
487 <Pagination
488 count={Math.ceil(total / PAGE_SIZE)}
489 page={page}
490 onChange={(_, v) => setPage(v)}
491 color="primary"
492 sx={{
493 ".MuiPaginationItem-root": {
494 color: "#fff",
495 background: "#1e293b",
496 border: "1px solid #334155",
497 },
498 ".Mui-selected": {
499 background: "#0ea5e9 !important",
500 },
501 }}
502 />
503 </Box>
504 </>
505 )}
506 </Container>
507 </StarBg>
508 );
Jiarenxiang36728482025-06-07 21:51:26 +0800509};
510
511export default TorrentListPage;