blob: 39a7615e265634a34007de24bb67961c73b085a3 [file] [log] [blame]
Krishyae71688a2025-04-10 21:25:17 +08001
22301009ecc1c1c2025-04-09 21:56:23 +08002import React, { useState, useEffect } from 'react';
22301009ecc1c1c2025-04-09 21:56:23 +08003import { Link } from 'wouter';
22301009afbcf4b2025-04-10 16:08:39 +08004import axios from 'axios';
22301009ecc1c1c2025-04-09 21:56:23 +08005import logo from '../../assets/logo.png';
22301009afbcf4b2025-04-10 16:08:39 +08006import Recommend from './Recommend/Recommend';
22301009f9641c52025-04-15 21:14:56 +08007import Header from '../../components/Header'; // 引入 Header 组件
22301009ecc1c1c2025-04-09 21:56:23 +08008import './SeedList.css';
9
22301009afbcf4b2025-04-10 16:08:39 +080010const API_BASE = process.env.REACT_APP_API_BASE;
11
22301009ecc1c1c2025-04-09 21:56:23 +080012const SeedList = () => {
13 const [seeds, setSeeds] = useState([]);
14 const [filteredSeeds, setFilteredSeeds] = useState([]);
15 const [loading, setLoading] = useState(true);
16 const [searchTerm, setSearchTerm] = useState('');
22301009ecc1c1c2025-04-09 21:56:23 +080017 const [sortOption, setSortOption] = useState('最新');
22301009afbcf4b2025-04-10 16:08:39 +080018 const [activeTab, setActiveTab] = useState('种子列表');
19 const [filters, setFilters] = useState({});
20 const [selectedFilters, setSelectedFilters] = useState({});
223010095b28c672025-04-10 20:12:45 +080021 const [tagMode, setTagMode] = useState('all');
22301009afbcf4b2025-04-10 16:08:39 +080022 const [errorMsg, setErrorMsg] = useState('');
22301009ecc1c1c2025-04-09 21:56:23 +080023
22301009afbcf4b2025-04-10 16:08:39 +080024 const TAGS = ['猜你喜欢', '电影', '电视剧', '动漫', '音乐', '游戏', '综艺', '软件', '体育', '学习', '纪录片', '其他'];
22301009ecc1c1c2025-04-09 21:56:23 +080025
22301009afbcf4b2025-04-10 16:08:39 +080026 const CATEGORY_MAP = {
27 '电影': 'movie',
28 '电视剧': 'tv',
29 '动漫': 'anime',
30 '音乐': 'music',
31 '游戏': 'game',
32 '综艺': 'variety',
33 '软件': 'software',
34 '体育': 'sports',
35 '学习': 'education',
36 '纪录片': 'documentary',
37 '其他': 'other'
38 };
39
40 const buildQueryParams = () => {
41 const category = CATEGORY_MAP[activeTab];
42 const params = {
43 category,
44 sort_by: sortOption === '最新' ? 'newest' : sortOption === '最热' ? 'downloads' : undefined,
45 page: 1,
46 limit: 20,
223010095b28c672025-04-10 20:12:45 +080047 include_fields: 'seed_id,title,category,tags,size,upload_time,downloads,image_url',
22301009ecc1c1c2025-04-09 21:56:23 +080048 };
49
223010095b28c672025-04-10 20:12:45 +080050 if (searchTerm.trim()) params.search = searchTerm.trim();
22301009afbcf4b2025-04-10 16:08:39 +080051
52 const tags = Object.entries(selectedFilters)
53 .filter(([_, value]) => value !== '不限')
54 .map(([_, value]) => value);
55
56 if (tags.length > 0) {
57 params.tags = tags.join(',');
58 params.tag_mode = tagMode;
59 }
60
61 return params;
62 };
63
64 const fetchSeeds = async () => {
65 setLoading(true);
66 setErrorMsg('');
67 try {
68 const params = buildQueryParams();
69 const queryString = new URLSearchParams(params).toString();
70 const response = await fetch(`${API_BASE}/echo/seeds?${queryString}`);
71 const data = await response.json();
72
73 if (data.status !== 'success') throw new Error(data.message || '获取失败');
74 const seeds = data.seeds || [];
75 setSeeds(seeds);
76 setFilteredSeeds(seeds);
77 } catch (error) {
78 console.error('获取种子列表失败:', error);
79 setErrorMsg(error.message || '获取失败,请稍后再试。');
80 setSeeds([]);
81 setFilteredSeeds([]);
82 } finally {
83 setLoading(false);
84 }
85 };
86
87 const fetchFilterOptions = async () => {
88 if (activeTab === '猜你喜欢') return;
89 const category = CATEGORY_MAP[activeTab];
90 try {
91 const res = await axios.get(`${API_BASE}/echo/seed-filters?category=${category}`);
223010095b28c672025-04-10 20:12:45 +080092 const filterData = res.data || {};
93 setFilters(filterData);
94
22301009afbcf4b2025-04-10 16:08:39 +080095 const defaultSelections = {};
223010095b28c672025-04-10 20:12:45 +080096 for (const key in filterData) {
22301009afbcf4b2025-04-10 16:08:39 +080097 defaultSelections[key] = '不限';
98 }
99 setSelectedFilters(defaultSelections);
100 } catch (err) {
101 console.error('获取筛选项失败:', err);
102 setFilters({});
103 setSelectedFilters({});
104 }
105 };
22301009ecc1c1c2025-04-09 21:56:23 +0800106
107 useEffect(() => {
22301009afbcf4b2025-04-10 16:08:39 +0800108 if (activeTab !== '猜你喜欢') {
109 fetchFilterOptions();
22301009ecc1c1c2025-04-09 21:56:23 +0800110 }
22301009afbcf4b2025-04-10 16:08:39 +0800111 }, [activeTab]);
22301009ecc1c1c2025-04-09 21:56:23 +0800112
22301009afbcf4b2025-04-10 16:08:39 +0800113 useEffect(() => {
114 if (activeTab !== '猜你喜欢') {
115 fetchSeeds();
22301009ecc1c1c2025-04-09 21:56:23 +0800116 }
22301009afbcf4b2025-04-10 16:08:39 +0800117 }, [activeTab, sortOption, selectedFilters, tagMode, searchTerm]);
22301009ecc1c1c2025-04-09 21:56:23 +0800118
22301009afbcf4b2025-04-10 16:08:39 +0800119 const handleDownload = async (seedId) => {
120 const peer_id = 'echo-' + Math.random().toString(36).substring(2, 10);
121 const ip = '127.0.0.1';
122 const port = 6881;
123 const uploaded = 0;
124 const downloaded = 0;
125 const left = 0;
126
127 try {
128 const response = await axios.get(`${API_BASE}/echo/seeds/${seedId}/download`, {
129 params: { peer_id, ip, port, uploaded, downloaded, left },
130 responseType: 'blob'
131 });
132
133 const blob = new Blob([response.data], { type: 'application/x-bittorrent' });
134 const downloadUrl = URL.createObjectURL(blob);
135 const a = document.createElement('a');
136 a.href = downloadUrl;
137 a.download = `${seedId}.torrent`;
138 a.click();
139 URL.revokeObjectURL(downloadUrl);
140 } catch (error) {
141 console.error('下载失败:', error);
142 alert('下载失败,请稍后再试。');
22301009ecc1c1c2025-04-09 21:56:23 +0800143 }
22301009afbcf4b2025-04-10 16:08:39 +0800144 };
22301009ecc1c1c2025-04-09 21:56:23 +0800145
223010095b28c672025-04-10 20:12:45 +0800146 const handleFilterChange = (key, value) => {
147 setSelectedFilters((prev) => ({
148 ...prev,
149 [key]: value
150 }));
151 };
152
153 const clearFilter = (key) => {
154 setSelectedFilters((prev) => ({
155 ...prev,
156 [key]: '不限'
157 }));
158 };
159
22301009ecc1c1c2025-04-09 21:56:23 +0800160 return (
22301009f9641c52025-04-15 21:14:56 +0800161 <div className="main-page">
162 <Header /> {/* 引用 Header 组件 */}
22301009ecc1c1c2025-04-09 21:56:23 +0800163
22301009ecc1c1c2025-04-09 21:56:23 +0800164 <div className="controls">
165 <input
166 type="text"
167 placeholder="搜索种子..."
168 value={searchTerm}
169 onChange={(e) => setSearchTerm(e.target.value)}
170 className="search-input"
171 />
172 <select value={sortOption} onChange={(e) => setSortOption(e.target.value)} className="sort-select">
173 <option value="最新">最新</option>
174 <option value="最热">最热</option>
22301009afbcf4b2025-04-10 16:08:39 +0800175 </select>
176 <select value={tagMode} onChange={(e) => setTagMode(e.target.value)} className="tag-mode-select">
177 <option value="any">包含任意标签</option>
178 <option value="all">包含所有标签</option>
22301009ecc1c1c2025-04-09 21:56:23 +0800179 </select>
180 </div>
181
182 <div className="tag-filters">
183 {TAGS.map((tag) => (
184 <button
185 key={tag}
22301009afbcf4b2025-04-10 16:08:39 +0800186 className={`tag-button ${activeTab === tag ? 'active-tag' : ''}`}
22301009ecc1c1c2025-04-09 21:56:23 +0800187 onClick={() => {
22301009afbcf4b2025-04-10 16:08:39 +0800188 setActiveTab(tag);
189 setFilters({});
190 setSelectedFilters({});
22301009ecc1c1c2025-04-09 21:56:23 +0800191 }}
192 >
193 {tag}
194 </button>
195 ))}
196 </div>
197
22301009afbcf4b2025-04-10 16:08:39 +0800198 {activeTab !== '猜你喜欢' && Object.keys(filters).length > 0 && (
199 <div className="filter-bar">
200 {Object.entries(filters).map(([key, options]) => (
201 <div className="filter-group" key={key}>
202 <label>{key}:</label>
203 <select
204 value={selectedFilters[key]}
223010095b28c672025-04-10 20:12:45 +0800205 onChange={(e) => handleFilterChange(key, e.target.value)}
22301009afbcf4b2025-04-10 16:08:39 +0800206 >
207 {options.map((opt) => (
208 <option key={opt} value={opt}>{opt}</option>
209 ))}
210 </select>
223010095b28c672025-04-10 20:12:45 +0800211 {selectedFilters[key] !== '不限' && (
212 <button className="clear-filter-btn" onClick={() => clearFilter(key)}>✕</button>
213 )}
22301009afbcf4b2025-04-10 16:08:39 +0800214 </div>
215 ))}
216 </div>
217 )}
218
22301009ecc1c1c2025-04-09 21:56:23 +0800219 <div className="seed-list-content">
220 {activeTab === '猜你喜欢' ? (
22301009afbcf4b2025-04-10 16:08:39 +0800221 <Recommend />
22301009ecc1c1c2025-04-09 21:56:23 +0800222 ) : loading ? (
223 <p>加载中...</p>
22301009afbcf4b2025-04-10 16:08:39 +0800224 ) : errorMsg ? (
225 <p className="error-text">{errorMsg}</p>
22301009ecc1c1c2025-04-09 21:56:23 +0800226 ) : filteredSeeds.length === 0 ? (
227 <p>未找到符合条件的种子。</p>
228 ) : (
229 <div className="seed-cards">
230 {filteredSeeds.map((seed, index) => (
231 <div key={index} className="seed-card">
223010095b28c672025-04-10 20:12:45 +0800232 {seed.image_url && (
233 <img src={seed.image_url} alt={seed.title} className="seed-cover" />
234 )}
22301009ecc1c1c2025-04-09 21:56:23 +0800235 <div className="seed-card-header">
22301009afbcf4b2025-04-10 16:08:39 +0800236 <h3>{seed.title}</h3>
22301009ecc1c1c2025-04-09 21:56:23 +0800237 </div>
223010095b28c672025-04-10 20:12:45 +0800238
22301009ecc1c1c2025-04-09 21:56:23 +0800239 <div className="seed-card-body">
223010095b28c672025-04-10 20:12:45 +0800240 <div className="seed-info">
241 <span>{seed.size || '未知'} GB</span>
242 <span>{seed.upload_time?.split('T')[0] || '未知'}</span>
243 <span>{seed.downloads ?? 0} 次下载</span>
244 </div>
245 {seed.tags && seed.tags.length > 0 && (
246 <div className="seed-card-tags">
247 {seed.tags.map((tag, i) => (
248 <span key={i} className="tag-label">{tag}</span>
249 ))}
250 </div>
251 )}
22301009ecc1c1c2025-04-09 21:56:23 +0800252 </div>
223010095b28c672025-04-10 20:12:45 +0800253
22301009ecc1c1c2025-04-09 21:56:23 +0800254 <div className="seed-card-actions">
22301009afbcf4b2025-04-10 16:08:39 +0800255 <button className="btn-primary" onClick={() => handleDownload(seed.seed_id)}>下载</button>
256 <Link href={`/seed/${seed.seed_id}`} className="btn-secondary">详情</Link>
22301009ecc1c1c2025-04-09 21:56:23 +0800257 <button className="btn-outline">收藏</button>
258 </div>
259 </div>
260 ))}
261 </div>
262 )}
263 </div>
264 </div>
265 );
266};
267
268export default SeedList;