blob: 2f336ac9bb6872d8f9b5773bfa461e0998bd8f5c [file] [log] [blame]
Jiarenxiang56cbf662025-06-09 21:46:47 +08001import React, { useEffect, useState } from "react";
2import { styled } from "@mui/material/styles";
3import axios from "axios";
4import {
5 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,
19} from "@mui/material";
20import { getCategories, getMyTorrentList,searchTorrents } from "../../services/bt/index";
21
22// 优化后的星空背景
23const StarBg = styled("div")({
24 minHeight: "100vh",
25 width: "auto",
26 background: "radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%)",
27 overflow: "auto",
28 position: "relative",
29 top: 0,
30 left: 0,
31 "&: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 }
65});
66
67// 分类标签栏
68const CategoryBar = styled(Box)({
69 display: "flex",
70 gap: 12,
71 alignItems: "center",
72 marginBottom: 24,
73 flexWrap: "wrap",
74});
75
76// 种子卡片
77const TorrentCard = styled(Card)({
78 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 },
88});
89
90const sortOptions = [
91 { label: "最新", value: { sortField: "create_time", sortDirection: "desc" } },
92 { label: "下载量", value: { sortField: "completions", sortDirection: "desc" } },
93 { label: "推荐", value: { sortField: "seeders", sortDirection: "desc" } },
94];
95
96const statusMap: Record<number, string> = {
97 0: '审核中',
98 1: '已发布',
99 2: '审核不通过',
100 3: '已上架修改重审中',
101 10: '已下架',
102};
103
104const 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
117 // 搜索相关
118 const [searchKeyword, setSearchKeyword] = useState("");
119 const [searching, setSearching] = useState(false);
120 const [isSearchMode, setIsSearchMode] = useState(false);
121
122 // 获取分类
123 useEffect(() => {
124 getCategories().then((res) => {
125 setCategories(res.data || []);
126 });
127 }, []);
128
129 // 获取种子列表
130 useEffect(() => {
131 if (isSearchMode) return;
132 setLoading(true);
133 getMyTorrentList()
134 .then((res) => {
135 setTorrents(res.datarow || []);
136 setTotal(res.data.page?.size || 0);
137 })
138 .finally(() => setLoading(false));
139 }, [selectedCat, sort, page, isSearchMode]);
140
141 // 搜索
142 const handleSearch = async () => {
143 if (!searchKeyword.trim()) {
144 setIsSearchMode(false);
145 setPage(1);
146 return;
147 }
148 setSearching(true);
149 setIsSearchMode(true);
150 try {
151 const res = await searchTorrents({
152 titleKeyword: searchKeyword.trim(),
153 category: selectedCat !== null ? selectedCat : undefined,
154 sortField: sort.sortField,
155 sortDirection: sort.sortDirection,
156 pageNum: page,
157 pageSize: PAGE_SIZE,
158 });
159 setTorrents((res.records || []).map((r: any) => ({
160 ...r.torrent,
161 ownerInfo: r.ownerInfo,
162 })));
163 setTotal(res.total || 0);
164 } finally {
165 setSearching(false);
166 }
167 };
168
169 // 搜索模式下翻页
170 useEffect(() => {
171 if (!isSearchMode) return;
172 handleSearch();
173 // eslint-disable-next-line
174 }, [page, sort, selectedCat]);
175
176 // 切换分类/排序时退出搜索模式
177 useEffect(() => {
178 setIsSearchMode(false);
179 setSearchKeyword("");
180 setPage(1);
181 }, [selectedCat, sort]);
182
183 return (
184 <StarBg>
185 <Container maxWidth="lg" sx={{ pt: 6, pb: 6, position: "relative" }}>
186 <Typography
187 variant="h3"
188 sx={{
189 color: "#fff",
190 fontWeight: 700,
191 mb: 3,
192 letterSpacing: 2,
193 textShadow: "0 2px 16px #0ea5e9",
194 }}
195 >
196 我的种子
197 </Typography>
198 搜索框
199 <Box
200 sx={{
201 mb: 4,
202 display: "flex",
203 alignItems: "center",
204 gap: 2,
205 background: "rgba(30,41,59,0.7)",
206 borderRadius: 3,
207 px: 2,
208 py: 1.5,
209 boxShadow: "0 2px 12px 0 #0ea5e950",
210 border: "1px solid #334155",
211 maxWidth: 700,
212 mx: { xs: "auto", md: 0 },
213 }}
214 >
215 <TextField
216 variant="outlined"
217 size="small"
218 placeholder="🔍 搜索种子标题"
219 value={searchKeyword}
220 onChange={(e) => setSearchKeyword(e.target.value)}
221 onKeyDown={(e) => {
222 if (e.key === "Enter") {
223 setPage(1);
224 handleSearch();
225 }
226 }}
227 sx={{
228 background: "rgba(17,24,39,0.95)",
229 borderRadius: 2,
230 input: {
231 color: "#fff",
232 fontSize: 17,
233 fontWeight: 500,
234 letterSpacing: 1,
235 px: 1,
236 },
237 width: 320,
238 ".MuiOutlinedInput-notchedOutline": { border: 0 },
239 boxShadow: "0 1px 4px 0 #0ea5e930",
240 }}
241 InputProps={{
242 endAdornment: (
243 <InputAdornment position="end">
244 <IconButton
245 onClick={() => {
246 setPage(1);
247 handleSearch();
248 }}
249 edge="end"
250 sx={{
251 color: "#0ea5e9",
252 bgcolor: "#1e293b",
253 borderRadius: 2,
254 "&:hover": { bgcolor: "#0ea5e9", color: "#fff" },
255 transition: "all 0.2s",
256 }}
257 >
258 <svg width="22" height="22" fill="none" viewBox="0 0 24 24">
259 <circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="2"/>
260 <path stroke="currentColor" strokeWidth="2" strokeLinecap="round" d="M20 20l-3.5-3.5"/>
261 </svg>
262 </IconButton>
263 </InputAdornment>
264 ),
265 }}
266 />
267 {isSearchMode && (
268 <Button
269 variant="outlined"
270 color="secondary"
271 onClick={() => {
272 setIsSearchMode(false);
273 setSearchKeyword("");
274 setPage(1);
275 }}
276 sx={{
277 ml: 1,
278 color: "#fff",
279 borderColor: "#64748b",
280 borderRadius: 2,
281 fontWeight: 600,
282 letterSpacing: 1,
283 px: 2,
284 py: 0.5,
285 background: "rgba(51,65,85,0.7)",
286 "&:hover": {
287 borderColor: "#0ea5e9",
288 background: "#0ea5e9",
289 color: "#fff",
290 },
291 transition: "all 0.2s",
292 }}
293 >
294 清除搜索
295 </Button>
296 )}
297 <Box sx={{ flex: 1 }} />
298 <Button
299 variant="contained"
300 color="primary"
301 onClick={() => window.open("/torrent-upload", "_self")}
302 sx={{
303 borderRadius: 2,
304 fontWeight: 700,
305 letterSpacing: 1,
306 px: 3,
307 py: 1,
308 boxShadow: "0 2px 8px 0 #0ea5e950",
309 background: "#0ea5e9",
310 color: "#fff",
311 "&:hover": {
312 background: "#38bdf8",
313 },
314 transition: "all 0.2s",
315 }}
316 >
317 分享资源
318 </Button>
319 </Box>
320 <CategoryBar>
321 <Chip
322 label="全部"
323 color={selectedCat === null ? "primary" : "default"}
324 onClick={() => {
325 setSelectedCat(null);
326 setPage(1);
327 }}
328 sx={{
329 fontWeight: 600,
330 fontSize: 16,
331 color: selectedCat === null ? undefined : "#fff",
332 }}
333 />
334 {categories.map((cat) => (
335 <Chip
336 key={cat.id}
337 label={cat.name}
338 color={selectedCat === cat.id ? "primary" : "default"}
339 onClick={() => {
340 setSelectedCat(cat.id);
341 setPage(1);
342 }}
343 sx={{
344 fontWeight: 600,
345 fontSize: 16,
346 color: selectedCat === cat.id ? undefined : "#fff",
347 }}
348 />
349 ))}
350 <Box sx={{ flex: 1 }} />
351 <Select
352 size="small"
353 value={JSON.stringify(sort)}
354 onChange={(e) => {
355 setSort(JSON.parse(e.target.value));
356 setPage(1);
357 }}
358 sx={{
359 color: "#fff",
360 background: "#1e293b",
361 borderRadius: 2,
362 ".MuiOutlinedInput-notchedOutline": { border: 0 },
363 minWidth: 120,
364 }}
365 >
366 {sortOptions.map((opt) => (
367 <MenuItem key={opt.label} value={JSON.stringify(opt.value)}>
368 {opt.label}
369 </MenuItem>
370 ))}
371 </Select>
372 </CategoryBar>
373 {(loading || searching) ? (
374 <Box sx={{ display: "flex", justifyContent: "center", mt: 8 }}>
375 <CircularProgress color="info" />
376 </Box>
377 ) : (
378 <>
379 <Box
380 sx={{
381 display: "flex",
382 flexWrap: "wrap",
383 gap: 3,
384 justifyContent: { xs: "center", md: "flex-start" },
385 }}
386 >
387 {torrents.map((torrent) => (
388 <Box
389 key={torrent.id}
390 sx={{
391 flex: "1 1 320px",
392 maxWidth: { xs: "100%", sm: "48%", md: "32%" },
393 minWidth: 300,
394 mb: 3,
395 display: "flex",
396 cursor: "pointer",
397 }}
398 onClick={() => window.open(`/torrent-detail/${torrent.id}`, "_self")}
399 >
400 <TorrentCard sx={{ width: "100%" }}>
401 <CardContent>
402 <Typography
403 variant="h6"
404 sx={{
405 color: "#38bdf8",
406 fontWeight: 700,
407 mb: 1,
408 textOverflow: "ellipsis",
409 overflow: "hidden",
410 whiteSpace: "nowrap",
411 }}
412 title={torrent.title}
413 >
414 {torrent.title}
415 </Typography>
416 <Box
417 sx={{
418 display: "flex",
419 gap: 1,
420 alignItems: "center",
421 mb: 1,
422 flexWrap: "wrap",
423 }}
424 >
425 <Chip
426 size="small"
427 label={
428 categories.find((c) => c.id === torrent.category)?.name ||
429 "未知"
430 }
431 sx={{
432 background: "#0ea5e9",
433 color: "#fff",
434 fontWeight: 600,
435 }}
436 />
437 {torrent.free === "1" && (
438 <Chip
439 size="small"
440 label="促销"
441 sx={{
442 background: "#fbbf24",
443 color: "#1e293b",
444 fontWeight: 600,
445 }}
446 />
447 )}
448 {/* 状态标签 */}
449 {typeof torrent.status !== "undefined" && (
450 <Chip
451 size="small"
452 label={statusMap[torrent.status] || "未知状态"}
453 sx={{
454 background: "#64748b",
455 color: "#fff",
456 fontWeight: 600,
457 }}
458 />
459 )}
460 </Box>
461 <Typography variant="body2" sx={{ color: "#cbd5e1", mb: 1 }}>
462 上传时间:{torrent.createTime}
463 </Typography>
464 <Box sx={{ display: "flex", gap: 2, mt: 1 }}>
465 <Typography variant="body2" sx={{ color: "#38bdf8" }}>
466 做种:{torrent.seeders}
467 </Typography>
468 <Typography variant="body2" sx={{ color: "#f472b6" }}>
469 下载量:{torrent.completions}
470 </Typography>
471 <Typography variant="body2" sx={{ color: "#fbbf24" }}>
472 下载中:{torrent.leechers}
473 </Typography>
474 </Box>
475 </CardContent>
476 </TorrentCard>
477 </Box>
478 ))}
479 </Box>
480 <Box sx={{ display: "flex", justifyContent: "center", mt: 4 }}>
481 <Pagination
482 count={Math.ceil(total / PAGE_SIZE)}
483 page={page}
484 onChange={(_, v) => setPage(v)}
485 color="primary"
486 sx={{
487 ".MuiPaginationItem-root": {
488 color: "#fff",
489 background: "#1e293b",
490 border: "1px solid #334155",
491 },
492 ".Mui-selected": {
493 background: "#0ea5e9 !important",
494 },
495 }}
496 />
497 </Box>
498 </>
499 )}
500 </Container>
501 </StarBg>
502 );
503};
504
505export default TorrentListPage;