美化前端
Change-Id: I46437caf832dd8f18358354f024724f7e31524cb
diff --git a/config/routes.ts b/config/routes.ts
index 14cfb53..47b2774 100644
--- a/config/routes.ts
+++ b/config/routes.ts
@@ -159,5 +159,16 @@
component: './Torrent/torrentUpload.tsx',
hideInMenu: true,
},
-
+ {
+ name: '审核种子界面',
+ path: '/torrent-audit-list',
+ component: './Torrent/torrentAuditList.tsx',
+ hideInMenu: true,
+ },
+{
+ name: '审核种子详情界面',
+ path: '/torrent-audit',
+ component: './Torrent/torrentAudit.tsx',
+ hideInMenu: true,
+ },
];
diff --git a/nginx.conf b/nginx.conf
index 91da21a..f41b74a 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -1,21 +1,50 @@
server {
- listen 80;
+ listen 80;
+ server_name _;
+
+ # 开启Gzip压缩(可选但推荐)
+ gzip on;
+ gzip_types text/plain application/javascript application/x-javascript text/css;
+
+ # 静态资源缓存优化
+ location ~* \.(js|css|jpg|jpeg|png|gif|ico|svg|woff2)$ {
+ root /usr/share/nginx/html;
+ expires 1y;
+ add_header Cache-Control "public, no-transform";
+ try_files $uri =404;
+ }
- server_name _; # 允许所有域名访问
-
- # 静态资源处理
+ # 前端SPA路由处理
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
- # 反向代理API请求到后端
+ # API代理
location /api/ {
- proxy_pass http://thunderhub-backend:5004; # 代理路径与后端API前缀匹配
+ # Docker容器服务名解析
+ resolver 127.0.0.11 valid=30s; # Docker内置DNS
+
+ proxy_pass http://thunderhub-backend:5004$request_uri;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
+
+ # 超时设置(按需调整)
+ proxy_connect_timeout 30s;
+ proxy_read_timeout 90s;
+
+ # WebSocket支持(如果后端需要)
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ # 禁止访问隐藏文件
+ location ~ /\. {
+ deny all;
+ return 404;
}
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 34ee658..ad3ecc5 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
"deploy": "npm run build && npm run gh-pages",
"preview": "npm run build && max preview --port 5004",
"serve": "umi-serve",
- "start": "npm run start",
+ "start": "npm run dev -- --port 5004",
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev",
"start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev",
"start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
@@ -63,6 +63,7 @@
"@ant-design/use-emotion-css": "1.0.4",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
+ "@mui/icons-material": "^7.1.1",
"@mui/material": "^7.1.1",
"@testing-library/dom": "^10.4.0",
"@umijs/route-utils": "^4.0.1",
@@ -72,7 +73,6 @@
"axios": "^1.9.0",
"classnames": "^2.5.1",
"dayjs": "^1.11.13",
- "dva": "^3.0.0-alpha.1",
"fabric": "^6.4.0",
"highlight.js": "^11.10.0",
"lodash": "^4.17.21",
diff --git a/src/pages/Torrent/torrentAudit.tsx b/src/pages/Torrent/torrentAudit.tsx
new file mode 100644
index 0000000..278a3cd
--- /dev/null
+++ b/src/pages/Torrent/torrentAudit.tsx
@@ -0,0 +1,160 @@
+import React, { useEffect, useState } from 'react';
+import { useSearchParams, useNavigate } from 'react-router-dom';
+import { Card, Button, Spin, Tag, message, Input } from 'antd';
+import { ArrowLeftOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons';
+import { getTorrentInfo,auditTorrent } from '../../services/bt/index';
+
+// 状态映射
+const statusMap: Record<number, string> = {
+ 0: '审核中',
+ 1: '已发布',
+ 2: '审核不通过',
+ 3: '已上架修改重审中',
+ 10: '已下架',
+};
+
+const TorrentAudit: React.FC = () => {
+ const [searchParams] = useSearchParams();
+ const id = searchParams.get('id');
+ const navigate = useNavigate();
+
+ const [loading, setLoading] = useState(true);
+ const [torrent, setTorrent] = useState<any>(null);
+ const [auditLoading, setAuditLoading] = useState(false);
+ const [rejectReason, setRejectReason] = useState('');
+
+ useEffect(() => {
+ if (!id) {
+ message.error('未指定种子ID');
+ navigate(-1);
+ return;
+ }
+ setLoading(true);
+ getTorrentInfo({ id })
+ .then(res => setTorrent(res.data))
+ .finally(() => setLoading(false));
+ }, [id, navigate]);
+
+ const handleAudit = async (status: 1 | 2) => {
+ if (status === 2 && !rejectReason.trim()) {
+ message.warning('请填写拒绝理由');
+ return;
+ }
+ setAuditLoading(true);
+ try {
+ await auditTorrent({
+ id: Number(id),
+ status,
+ remark: status === 2 ? rejectReason.trim() : undefined,
+ });
+ message.success(status === 1 ? '审核通过' : '审核拒绝');
+ navigate(-1);
+ } catch {
+ message.error('操作失败');
+ } finally {
+ setAuditLoading(false);
+ }
+ };
+
+ return (
+ <div style={{ minHeight: '100vh', background: '#f4f8ff', padding: 32 }}>
+ <div style={{ maxWidth: 800, margin: '0 auto' }}>
+ <Button
+ icon={<ArrowLeftOutlined />}
+ type="link"
+ onClick={() => navigate(-1)}
+ style={{ marginBottom: 16 }}
+ >
+ 返回
+ </Button>
+ <Card
+ loading={loading}
+ title={
+ <span>
+ 审核种子
+ {torrent && (
+ <Tag color={torrent.status === 0 ? 'orange' : torrent.status === 1 ? 'green' : 'red'} style={{ marginLeft: 16 }}>
+ {statusMap[torrent.status]}
+ </Tag>
+ )}
+ </span>
+ }
+ >
+ {loading ? (
+ <Spin />
+ ) : !torrent ? (
+ <div>未找到种子信息</div>
+ ) : (
+ <>
+ <h2>{torrent.title}</h2>
+ <div style={{ marginBottom: 8 }}>
+ <Tag color="blue">{torrent.categoryName || '未知分类'}</Tag>
+ {torrent.tags?.map((tag: string) => (
+ <Tag key={tag}>{tag}</Tag>
+ ))}
+ </div>
+ <div style={{ color: '#888', marginBottom: 8 }}>
+ 上传者:{torrent.owner} | 上传时间:{torrent.createdAt}
+ </div>
+ <div style={{ marginBottom: 16 }}>
+ <span>文件大小:{torrent.size}</span>
+ <span style={{ marginLeft: 24 }}>做种人数:{torrent.seeders}</span>
+ <span style={{ marginLeft: 24 }}>下载人数:{torrent.leechers}</span>
+ <span style={{ marginLeft: 24 }}>完成次数:{torrent.completed}</span>
+ </div>
+ <div
+ style={{
+ background: '#f6f8fa',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 24,
+ minHeight: 80,
+ }}
+ dangerouslySetInnerHTML={{ __html: torrent.description || '' }}
+ />
+ {torrent.status === 0 ? (
+ <>
+ <div style={{ display: 'flex', gap: 16, alignItems: 'center', marginBottom: 16 }}>
+ <Button
+ type="primary"
+ icon={<CheckOutlined />}
+ loading={auditLoading}
+ onClick={() => handleAudit(1)}
+ >
+ 审核通过
+ </Button>
+ <Button
+ danger
+ icon={<CloseOutlined />}
+ loading={auditLoading}
+ onClick={() => handleAudit(2)}
+ >
+ 审核拒绝
+ </Button>
+ <Input.TextArea
+ value={rejectReason}
+ onChange={e => setRejectReason(e.target.value)}
+ placeholder="拒绝理由(仅拒绝时填写)"
+ autoSize={{ minRows: 1, maxRows: 3 }}
+ style={{ width: 260 }}
+ disabled={auditLoading}
+ />
+ </div>
+ </>
+ ) : (
+ <div style={{ color: '#888', marginTop: 16 }}>
+ 当前状态:{statusMap[torrent.status]}
+ {torrent.status === 2 && torrent.reason && (
+ <div style={{ color: '#d4380d', marginTop: 8 }}>拒绝理由:{torrent.reason}</div>
+ )}
+ </div>
+ )}
+ </>
+ )}
+ </Card>
+ </div>
+ </div>
+ );
+};
+
+export default TorrentAudit;
\ No newline at end of file
diff --git a/src/pages/Torrent/torrentAuditList.tsx b/src/pages/Torrent/torrentAuditList.tsx
new file mode 100644
index 0000000..7c30eb8
--- /dev/null
+++ b/src/pages/Torrent/torrentAuditList.tsx
@@ -0,0 +1,333 @@
+import React, { useEffect, useState } from "react";
+import { styled } from "@mui/material/styles";
+
+import { getCategories, getTorrentList } from "../../services/bt/index";
+import {
+Box,
+Button,
+Card,
+CardContent,
+Chip,
+CircularProgress,
+Container,
+MenuItem,
+Pagination,
+Select,
+Typography,
+} from "@mui/material";
+
+// 星空背景
+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: "createTime", 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 TorrentAuditListPage: 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);
+
+useEffect(() => {
+ getCategories().then((res) => {
+ setCategories(res.data || []);
+ });
+}, []);
+
+useEffect(() => {
+ setLoading(true);
+ getTorrentList({
+ category: selectedCat !== null ? String(selectedCat) : undefined,
+ sortField: sort.sortField,
+ sortDirection: sort.sortDirection,
+ pageNum: page,
+ pageSize: PAGE_SIZE,
+ })
+ .then((res) => {
+ setTorrents(res.data || []);
+ setTotal(res.page?.total || 0);
+ })
+ .finally(() => setLoading(false));
+}, [selectedCat, sort, page]);
+
+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",
+ }}
+ >
+ ThunderHub 种子审核
+ </Typography>
+ <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 ? (
+ <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-audit/${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 TorrentAuditListPage;
\ No newline at end of file
diff --git a/src/pages/Torrent/torrentDetail.tsx b/src/pages/Torrent/torrentDetail.tsx
index d841b04..7a25162 100644
--- a/src/pages/Torrent/torrentDetail.tsx
+++ b/src/pages/Torrent/torrentDetail.tsx
@@ -77,7 +77,7 @@
};
const statusMap: Record<number, string> = {
- 0: '候选中',
+ 0: '审核中',
1: '已发布',
2: '审核不通过',
3: '已上架修改重审中',
diff --git a/src/pages/Torrent/torrentList.tsx b/src/pages/Torrent/torrentList.tsx
index b881aea..c97e13e 100644
--- a/src/pages/Torrent/torrentList.tsx
+++ b/src/pages/Torrent/torrentList.tsx
@@ -2,98 +2,105 @@
import { styled } from "@mui/material/styles";
import axios from "axios";
import {
- Box,
- Button,
- Card,
- CardContent,
- Chip,
- CircularProgress,
- Container,
- MenuItem,
- Pagination,
- Select,
- Typography,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ Chip,
+ CircularProgress,
+ Container,
+ MenuItem,
+ Pagination,
+ Select,
+ Typography,
+ TextField,
+ InputAdornment,
+ IconButton,
} from "@mui/material";
-import { getCategories, getTorrentList } from "../../services/bt/index";
+import { getCategories, getTorrentList,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,
-
-
-
- // 使用CSS动画实现星空
- "&:before, &:after": {
- content: '""',
- position: "absolute",
+ minHeight: "100vh",
+ width: "auto",
+ background: "radial-gradient(ellipse at bottom, #1b2735 0%, #090a0f 100%)",
+ overflow: "auto",
+ position: "relative",
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 },
- }
+ "&: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",
+ 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",
-},
+ 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: "createTime", sortDirection: "desc" } },
-{ label: "下载量", value: { sortField: "completions", sortDirection: "desc" } },
-{ label: "推荐", value: { sortField: "seeders", sortDirection: "desc" } },
+ { 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 = () => {
@@ -107,16 +114,21 @@
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) => {
- // 直接使用返回的data数组
setCategories(res.data || []);
});
}, []);
// 获取种子列表
useEffect(() => {
+ if (isSearchMode) return;
setLoading(true);
getTorrentList({
category: selectedCat !== null ? String(selectedCat) : undefined,
@@ -126,200 +138,374 @@
pageSize: PAGE_SIZE,
})
.then((res) => {
- // 直接使用返回的data数组和page对象
setTorrents(res.data || []);
setTotal(res.page?.total || 0);
})
.finally(() => setLoading(false));
- }, [selectedCat, sort, page]);
+ }, [selectedCat, sort, page, isSearchMode]);
-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",
- }}
- >
- ThunderHub 星空PT种子广场
- </Typography>
- <CategoryBar>
- <Chip
- label="全部"
- color={selectedCat === null ? "primary" : "default"}
- onClick={() => {
- setSelectedCat(null);
- setPage(1);
- }}
+ // 搜索
+ 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={{
- fontWeight: 600,
- fontSize: 16,
- color: selectedCat === null ? undefined : "#fff",
+ color: "#fff",
+ fontWeight: 700,
+ mb: 3,
+ letterSpacing: 2,
+ textShadow: "0 2px 16px #0ea5e9",
}}
- />
- {categories.map((cat) => (
+ >
+ ThunderHub 星空PT种子广场
+ </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
- key={cat.id}
- label={cat.name}
- color={selectedCat === cat.id ? "primary" : "default"}
+ label="全部"
+ color={selectedCat === null ? "primary" : "default"}
onClick={() => {
- setSelectedCat(cat.id);
+ setSelectedCat(null);
setPage(1);
}}
sx={{
fontWeight: 600,
fontSize: 16,
- color: selectedCat === cat.id ? undefined : "#fff",
+ color: selectedCat === null ? 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>
+ {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",
+ }}
+ />
))}
- </Select>
- </CategoryBar>
- {loading ? (
- <Box sx={{ display: "flex", justifyContent: "center", mt: 8 }}>
- <CircularProgress color="info" />
- </Box>
- ) : (
- <>
- <Box
+ <Box sx={{ flex: 1 }} />
+ <Select
+ size="small"
+ value={JSON.stringify(sort)}
+ onChange={(e) => {
+ setSort(JSON.parse(e.target.value));
+ setPage(1);
+ }}
sx={{
- display: "flex",
- flexWrap: "wrap",
- gap: 3,
- justifyContent: { xs: "center", md: "flex-start" },
+ color: "#fff",
+ background: "#1e293b",
+ borderRadius: 2,
+ ".MuiOutlinedInput-notchedOutline": { border: 0 },
+ minWidth: 120,
}}
>
- {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 ||
- "未知"
- }
+ {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={{
- background: "#0ea5e9",
- color: "#fff",
- fontWeight: 600,
+ color: "#38bdf8",
+ fontWeight: 700,
+ mb: 1,
+ textOverflow: "ellipsis",
+ overflow: "hidden",
+ whiteSpace: "nowrap",
}}
- />
- {torrent.free === "1" && (
+ title={torrent.title}
+ >
+ {torrent.title}
+ </Typography>
+ <Box
+ sx={{
+ display: "flex",
+ gap: 1,
+ alignItems: "center",
+ mb: 1,
+ flexWrap: "wrap",
+ }}
+ >
<Chip
size="small"
- label="促销"
+ label={
+ categories.find((c) => c.id === torrent.category)?.name ||
+ "未知"
+ }
sx={{
- background: "#fbbf24",
- color: "#1e293b",
+ background: "#0ea5e9",
+ 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}
+ {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>
- <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>
-);
+ <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;
\ No newline at end of file
diff --git a/src/pages/Torrent/torrentUpload.tsx b/src/pages/Torrent/torrentUpload.tsx
index b5d45bc..434d65b 100644
--- a/src/pages/Torrent/torrentUpload.tsx
+++ b/src/pages/Torrent/torrentUpload.tsx
@@ -15,7 +15,9 @@
import{
getCategories,
addTorrent,
- uploadTorrentFile
+ uploadTorrentFile,
+ deleteTorrent,
+ getTracker,
} from '../../services/bt/index';
const { Option } = Select;
@@ -89,16 +91,41 @@
subheading: values.subheading || '',
remark: values.remark || '',
});
- if (!addRes?.id) {
+ if (!addRes?.data) {
throw new Error('种子信息添加失败');
}
// 2. 上传.torrent文件
- await uploadTorrentFile(fileList[0], addRes.id);
-
+ let upRes = await uploadTorrentFile(fileList[0], addRes.data);
+ console.log('上传结果:', upRes);
+ if (upRes.code == 500) {
+ throw new Error(upRes.msg);
+ await deleteTorrent( addRes.data);
+ }else{
message.success('种子上传成功');
form.resetFields();
setFileList([]);
setCustomTags([]);
+ // 获取并显示 trackerUrl
+ const trackerRes = await getTracker();
+ if (trackerRes.data) {
+ const trackerUrl = trackerRes.data;
+ Modal.info({
+ title: 'Tracker 服务器',
+ content: (
+ <div style={{ marginTop: 12, color: '#232946', wordBreak: 'break-all' }}>
+ {trackerUrl}
+ <br />
+ <span style={{ color: '#667eea', fontWeight: 500 }}>
+ torrentId={addRes.data}
+ </span>
+ </div>
+ ),
+ width: 480,
+ okText: '关闭',
+ });
+ }
+ }
+
} catch (err: any) {
message.error(err.message || '上传失败');
} finally {
@@ -270,7 +297,11 @@
}}
>
{categories.map((cat) => (
- <Option key={cat.id} value={cat.id}>
+ <Option
+ key={cat.id}
+ value={cat.id}
+ style={{ color: '#fff', background: '#232946' }}
+ >
{cat.name}
</Option>
))}
@@ -424,6 +455,41 @@
>
上传
</Button>
+ <Button
+ type="default"
+ block
+ style={{
+ marginTop: 16,
+ background: 'rgba(102,126,234,0.10)',
+ border: '1px solid #667eea',
+ color: '#bfcfff',
+ fontWeight: 500,
+ letterSpacing: 1,
+ }}
+ onClick={async () => {
+ try {
+ const res = await getTracker();
+ if (res.data) {
+ Modal.info({
+ title: 'Tracker 服务器',
+ content: (
+ <div style={{ marginTop: 12, color: '#232946' }}>
+ {res.data}
+ </div>
+ ),
+ width: 480,
+ okText: '关闭',
+ });
+ } else {
+ message.info('暂无可用的 Tracker 服务器');
+ }
+ } catch (err) {
+ message.error('获取 Tracker 服务器失败');
+ }
+ }}
+ >
+ 查看 Tracker 服务器
+ </Button>
</Form.Item>
</Form>
</div>
diff --git a/src/services/bt/index.tsx b/src/services/bt/index.tsx
index 4a97406..5d9a25c 100644
--- a/src/services/bt/index.tsx
+++ b/src/services/bt/index.tsx
@@ -145,8 +145,6 @@
return request('/api/torrent/upload', {
method: 'POST',
data: formData,
- requestType: 'form',
- responseType: 'blob',
});
}
@@ -171,4 +169,96 @@
return request('/api/torrent/tracker', {
method: 'POST',
});
+}
+
+export interface TorrentSearchRequest {
+ infoHash?: string;
+ category?: number;
+ status?: number;
+ fileStatus?: number;
+ owner?: number;
+ type?: number;
+ nameKeyword?: string;
+ titleKeyword?: string;
+ descriptionKeyword?: string;
+ minSize?: number;
+ maxSize?: number;
+ createTimeStart?: string; // ISO string
+ createTimeEnd?: string; // ISO string
+ sortField?: string;
+ sortDirection?: string;
+ pageNum?: number;
+ pageSize?: number;
+}
+
+export interface UserEntity {
+ user_id: number;
+ user_name: string;
+ nick_name: string;
+ user_type: string;
+ email: string;
+ phonenumber: string;
+ gender: string;
+ avatar: string;
+ password: string;
+ status: number;
+ login_ip: string;
+ login_date: string;
+ create_by: string;
+ create_time: string;
+ update_by: string;
+ update_time: string;
+ remark: string;
+ full_name: string;
+ state: number;
+ added: string;
+ last_login: string;
+ last_access: string;
+ last_home: string;
+ last_offer: string;
+ forum_access: string;
+ last_staffmsg: string;
+ last_pm: string;
+ last_comment: string;
+ last_post: string;
+ last_active: string;
+ privacy: number;
+ reg_ip: string;
+ level: number;
+ seedtime: number;
+ leechtime: number;
+ real_uploaded: number;
+ real_downloaded: number;
+ modcomment: string;
+ warning_by: number;
+ warning_times: number;
+ warning: boolean;
+ warning_until: string;
+ download: number;
+ upload: number;
+ invited_by: number;
+ bonus: number;
+ exp: number;
+ check_code: string;
+ reg_type: number;
+}
+
+export interface TorrentSearchResult {
+ torrent: TorrentEntity;
+ ownerInfo: UserEntity;
+}
+
+export interface TorrentSearchResponse {
+ records: TorrentSearchResult[];
+ total: number;
+ size: number;
+ current: number;
+ pages: number;
+}
+
+export async function searchTorrents(params: TorrentSearchRequest) {
+ return request<TorrentSearchResponse>('/api/torrent/search', {
+ method: 'POST',
+ data: params,
+ });
}
\ No newline at end of file