blob: 2f336ac9bb6872d8f9b5773bfa461e0998bd8f5c [file] [log] [blame]
import React, { useEffect, useState } from "react";
import { styled } from "@mui/material/styles";
import axios from "axios";
import {
Box,
Button,
Card,
CardContent,
Chip,
CircularProgress,
Container,
MenuItem,
Pagination,
Select,
Typography,
TextField,
InputAdornment,
IconButton,
} from "@mui/material";
import { getCategories, getMyTorrentList,searchTorrents } from "../../services/bt/index";
// 优化后的星空背景
const StarBg = styled("div")({
minHeight: "100vh",
width: "auto",
background: "radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%)",
overflow: "auto",
position: "relative",
top: 0,
left: 0,
"&:before, &:after": {
content: '""',
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
pointerEvents: "none",
},
"&:before": {
background: `
radial-gradient(1px 1px at 20% 30%, rgba(255,255,255,0.8), rgba(255,255,255,0)),
radial-gradient(1px 1px at 40% 70%, rgba(255,255,255,0.8), rgba(255,255,255,0)),
radial-gradient(1.5px 1.5px at 60% 20%, rgba(255,255,255,0.9), rgba(255,255,255,0)),
radial-gradient(1.5px 1.5px at 80% 90%, rgba(255,255,255,0.9), rgba(255,255,255,0))
`,
backgroundRepeat: "repeat",
backgroundSize: "200px 200px",
animation: "twinkle 10s infinite ease-in-out",
},
"&:after": {
background: `
radial-gradient(1px 1px at 70% 40%, rgba(255,255,255,0.7), rgba(255,255,255,0)),
radial-gradient(1.2px 1.2px at 10% 80%, rgba(255,255,255,0.7), rgba(255,255,255,0)),
radial-gradient(1.5px 1.5px at 30% 60%, rgba(255,255,255,0.8), rgba(255,255,255,0))
`,
backgroundRepeat: "repeat",
backgroundSize: "300px 300px",
animation: "twinkle 15s infinite 5s ease-in-out",
},
"@keyframes twinkle": {
"0%, 100%": { opacity: 0.3 },
"50%": { opacity: 0.8 },
}
});
// 分类标签栏
const CategoryBar = styled(Box)({
display: "flex",
gap: 12,
alignItems: "center",
marginBottom: 24,
flexWrap: "wrap",
});
// 种子卡片
const TorrentCard = styled(Card)({
background: "rgba(30, 41, 59, 0.85)",
color: "#fff",
borderRadius: 16,
boxShadow: "0 4px 24px 0 rgba(0,0,0,0.4)",
border: "1px solid #334155",
transition: "transform 0.2s",
"&:hover": {
transform: "scale(1.025)",
boxShadow: "0 8px 32px 0 #0ea5e9",
},
});
const sortOptions = [
{ label: "最新", value: { sortField: "create_time", sortDirection: "desc" } },
{ label: "下载量", value: { sortField: "completions", sortDirection: "desc" } },
{ label: "推荐", value: { sortField: "seeders", sortDirection: "desc" } },
];
const statusMap: Record<number, string> = {
0: '审核中',
1: '已发布',
2: '审核不通过',
3: '已上架修改重审中',
10: '已下架',
};
const PAGE_SIZE = 20;
const TorrentListPage: React.FC = () => {
const [categories, setCategories] = useState<
{ id: number; name: string; remark?: string | null; type?: number | null }[]
>([]);
const [selectedCat, setSelectedCat] = useState<number | null>(null);
const [sort, setSort] = useState(sortOptions[0].value);
const [torrents, setTorrents] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
// 搜索相关
const [searchKeyword, setSearchKeyword] = useState("");
const [searching, setSearching] = useState(false);
const [isSearchMode, setIsSearchMode] = useState(false);
// 获取分类
useEffect(() => {
getCategories().then((res) => {
setCategories(res.data || []);
});
}, []);
// 获取种子列表
useEffect(() => {
if (isSearchMode) return;
setLoading(true);
getMyTorrentList()
.then((res) => {
setTorrents(res.datarow || []);
setTotal(res.data.page?.size || 0);
})
.finally(() => setLoading(false));
}, [selectedCat, sort, page, isSearchMode]);
// 搜索
const handleSearch = async () => {
if (!searchKeyword.trim()) {
setIsSearchMode(false);
setPage(1);
return;
}
setSearching(true);
setIsSearchMode(true);
try {
const res = await searchTorrents({
titleKeyword: searchKeyword.trim(),
category: selectedCat !== null ? selectedCat : undefined,
sortField: sort.sortField,
sortDirection: sort.sortDirection,
pageNum: page,
pageSize: PAGE_SIZE,
});
setTorrents((res.records || []).map((r: any) => ({
...r.torrent,
ownerInfo: r.ownerInfo,
})));
setTotal(res.total || 0);
} finally {
setSearching(false);
}
};
// 搜索模式下翻页
useEffect(() => {
if (!isSearchMode) return;
handleSearch();
// eslint-disable-next-line
}, [page, sort, selectedCat]);
// 切换分类/排序时退出搜索模式
useEffect(() => {
setIsSearchMode(false);
setSearchKeyword("");
setPage(1);
}, [selectedCat, sort]);
return (
<StarBg>
<Container maxWidth="lg" sx={{ pt: 6, pb: 6, position: "relative" }}>
<Typography
variant="h3"
sx={{
color: "#fff",
fontWeight: 700,
mb: 3,
letterSpacing: 2,
textShadow: "0 2px 16px #0ea5e9",
}}
>
我的种子
</Typography>
搜索框
<Box
sx={{
mb: 4,
display: "flex",
alignItems: "center",
gap: 2,
background: "rgba(30,41,59,0.7)",
borderRadius: 3,
px: 2,
py: 1.5,
boxShadow: "0 2px 12px 0 #0ea5e950",
border: "1px solid #334155",
maxWidth: 700,
mx: { xs: "auto", md: 0 },
}}
>
<TextField
variant="outlined"
size="small"
placeholder="🔍 搜索种子标题"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
setPage(1);
handleSearch();
}
}}
sx={{
background: "rgba(17,24,39,0.95)",
borderRadius: 2,
input: {
color: "#fff",
fontSize: 17,
fontWeight: 500,
letterSpacing: 1,
px: 1,
},
width: 320,
".MuiOutlinedInput-notchedOutline": { border: 0 },
boxShadow: "0 1px 4px 0 #0ea5e930",
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => {
setPage(1);
handleSearch();
}}
edge="end"
sx={{
color: "#0ea5e9",
bgcolor: "#1e293b",
borderRadius: 2,
"&:hover": { bgcolor: "#0ea5e9", color: "#fff" },
transition: "all 0.2s",
}}
>
<svg width="22" height="22" fill="none" viewBox="0 0 24 24">
<circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="2"/>
<path stroke="currentColor" strokeWidth="2" strokeLinecap="round" d="M20 20l-3.5-3.5"/>
</svg>
</IconButton>
</InputAdornment>
),
}}
/>
{isSearchMode && (
<Button
variant="outlined"
color="secondary"
onClick={() => {
setIsSearchMode(false);
setSearchKeyword("");
setPage(1);
}}
sx={{
ml: 1,
color: "#fff",
borderColor: "#64748b",
borderRadius: 2,
fontWeight: 600,
letterSpacing: 1,
px: 2,
py: 0.5,
background: "rgba(51,65,85,0.7)",
"&:hover": {
borderColor: "#0ea5e9",
background: "#0ea5e9",
color: "#fff",
},
transition: "all 0.2s",
}}
>
清除搜索
</Button>
)}
<Box sx={{ flex: 1 }} />
<Button
variant="contained"
color="primary"
onClick={() => window.open("/torrent-upload", "_self")}
sx={{
borderRadius: 2,
fontWeight: 700,
letterSpacing: 1,
px: 3,
py: 1,
boxShadow: "0 2px 8px 0 #0ea5e950",
background: "#0ea5e9",
color: "#fff",
"&:hover": {
background: "#38bdf8",
},
transition: "all 0.2s",
}}
>
分享资源
</Button>
</Box>
<CategoryBar>
<Chip
label="全部"
color={selectedCat === null ? "primary" : "default"}
onClick={() => {
setSelectedCat(null);
setPage(1);
}}
sx={{
fontWeight: 600,
fontSize: 16,
color: selectedCat === null ? undefined : "#fff",
}}
/>
{categories.map((cat) => (
<Chip
key={cat.id}
label={cat.name}
color={selectedCat === cat.id ? "primary" : "default"}
onClick={() => {
setSelectedCat(cat.id);
setPage(1);
}}
sx={{
fontWeight: 600,
fontSize: 16,
color: selectedCat === cat.id ? undefined : "#fff",
}}
/>
))}
<Box sx={{ flex: 1 }} />
<Select
size="small"
value={JSON.stringify(sort)}
onChange={(e) => {
setSort(JSON.parse(e.target.value));
setPage(1);
}}
sx={{
color: "#fff",
background: "#1e293b",
borderRadius: 2,
".MuiOutlinedInput-notchedOutline": { border: 0 },
minWidth: 120,
}}
>
{sortOptions.map((opt) => (
<MenuItem key={opt.label} value={JSON.stringify(opt.value)}>
{opt.label}
</MenuItem>
))}
</Select>
</CategoryBar>
{(loading || searching) ? (
<Box sx={{ display: "flex", justifyContent: "center", mt: 8 }}>
<CircularProgress color="info" />
</Box>
) : (
<>
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: 3,
justifyContent: { xs: "center", md: "flex-start" },
}}
>
{torrents.map((torrent) => (
<Box
key={torrent.id}
sx={{
flex: "1 1 320px",
maxWidth: { xs: "100%", sm: "48%", md: "32%" },
minWidth: 300,
mb: 3,
display: "flex",
cursor: "pointer",
}}
onClick={() => window.open(`/torrent-detail/${torrent.id}`, "_self")}
>
<TorrentCard sx={{ width: "100%" }}>
<CardContent>
<Typography
variant="h6"
sx={{
color: "#38bdf8",
fontWeight: 700,
mb: 1,
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
}}
title={torrent.title}
>
{torrent.title}
</Typography>
<Box
sx={{
display: "flex",
gap: 1,
alignItems: "center",
mb: 1,
flexWrap: "wrap",
}}
>
<Chip
size="small"
label={
categories.find((c) => c.id === torrent.category)?.name ||
"未知"
}
sx={{
background: "#0ea5e9",
color: "#fff",
fontWeight: 600,
}}
/>
{torrent.free === "1" && (
<Chip
size="small"
label="促销"
sx={{
background: "#fbbf24",
color: "#1e293b",
fontWeight: 600,
}}
/>
)}
{/* 状态标签 */}
{typeof torrent.status !== "undefined" && (
<Chip
size="small"
label={statusMap[torrent.status] || "未知状态"}
sx={{
background: "#64748b",
color: "#fff",
fontWeight: 600,
}}
/>
)}
</Box>
<Typography variant="body2" sx={{ color: "#cbd5e1", mb: 1 }}>
上传时间:{torrent.createTime}
</Typography>
<Box sx={{ display: "flex", gap: 2, mt: 1 }}>
<Typography variant="body2" sx={{ color: "#38bdf8" }}>
做种:{torrent.seeders}
</Typography>
<Typography variant="body2" sx={{ color: "#f472b6" }}>
下载量:{torrent.completions}
</Typography>
<Typography variant="body2" sx={{ color: "#fbbf24" }}>
下载中:{torrent.leechers}
</Typography>
</Box>
</CardContent>
</TorrentCard>
</Box>
))}
</Box>
<Box sx={{ display: "flex", justifyContent: "center", mt: 4 }}>
<Pagination
count={Math.ceil(total / PAGE_SIZE)}
page={page}
onChange={(_, v) => setPage(v)}
color="primary"
sx={{
".MuiPaginationItem-root": {
color: "#fff",
background: "#1e293b",
border: "1px solid #334155",
},
".Mui-selected": {
background: "#0ea5e9 !important",
},
}}
/>
</Box>
</>
)}
</Container>
</StarBg>
);
};
export default TorrentListPage;