| 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; |