blob: 2831434793ee4a3bce3083c27bb81222758427a8 [file] [log] [blame]
22301009ecc1c1c2025-04-09 21:56:23 +08001import React, { useState, useEffect } from 'react';
22301009ecc1c1c2025-04-09 21:56:23 +08002import { Link } from 'wouter';
22301009afbcf4b2025-04-10 16:08:39 +08003import axios from 'axios';
22301009ecc1c1c2025-04-09 21:56:23 +08004import logo from '../../assets/logo.png';
22301009afbcf4b2025-04-10 16:08:39 +08005import Recommend from './Recommend/Recommend';
22301009f9641c52025-04-15 21:14:56 +08006import Header from '../../components/Header'; // 引入 Header 组件
22301009ecc1c1c2025-04-09 21:56:23 +08007import './SeedList.css';
8
22301009afbcf4b2025-04-10 16:08:39 +08009const API_BASE = process.env.REACT_APP_API_BASE;
10
22301009ecc1c1c2025-04-09 21:56:23 +080011const SeedList = () => {
12 const [seeds, setSeeds] = useState([]);
13 const [filteredSeeds, setFilteredSeeds] = useState([]);
14 const [loading, setLoading] = useState(true);
15 const [searchTerm, setSearchTerm] = useState('');
22301009ecc1c1c2025-04-09 21:56:23 +080016 const [sortOption, setSortOption] = useState('最新');
22301009afbcf4b2025-04-10 16:08:39 +080017 const [activeTab, setActiveTab] = useState('种子列表');
18 const [filters, setFilters] = useState({});
19 const [selectedFilters, setSelectedFilters] = useState({});
223010095b28c672025-04-10 20:12:45 +080020 const [tagMode, setTagMode] = useState('all');
22301009afbcf4b2025-04-10 16:08:39 +080021 const [errorMsg, setErrorMsg] = useState('');
22301009ecc1c1c2025-04-09 21:56:23 +080022
22301009afbcf4b2025-04-10 16:08:39 +080023 const TAGS = ['猜你喜欢', '电影', '电视剧', '动漫', '音乐', '游戏', '综艺', '软件', '体育', '学习', '纪录片', '其他'];
22301009ecc1c1c2025-04-09 21:56:23 +080024
22301009afbcf4b2025-04-10 16:08:39 +080025 const CATEGORY_MAP = {
26 '电影': 'movie',
27 '电视剧': 'tv',
28 '动漫': 'anime',
29 '音乐': 'music',
30 '游戏': 'game',
31 '综艺': 'variety',
32 '软件': 'software',
33 '体育': 'sports',
34 '学习': 'education',
35 '纪录片': 'documentary',
36 '其他': 'other'
37 };
38
39 const buildQueryParams = () => {
40 const category = CATEGORY_MAP[activeTab];
41 const params = {
42 category,
43 sort_by: sortOption === '最新' ? 'newest' : sortOption === '最热' ? 'downloads' : undefined,
44 page: 1,
45 limit: 20,
223010095b28c672025-04-10 20:12:45 +080046 include_fields: 'seed_id,title,category,tags,size,upload_time,downloads,image_url',
22301009ecc1c1c2025-04-09 21:56:23 +080047 };
48
223010095b28c672025-04-10 20:12:45 +080049 if (searchTerm.trim()) params.search = searchTerm.trim();
22301009afbcf4b2025-04-10 16:08:39 +080050
51 const tags = Object.entries(selectedFilters)
Krishya25590de2025-04-21 19:03:49 +080052 .filter(([_, value]) => value!== '不限')
53 .map(([_, value]) => value);
22301009afbcf4b2025-04-10 16:08:39 +080054
55 if (tags.length > 0) {
56 params.tags = tags.join(',');
57 params.tag_mode = tagMode;
58 }
59
60 return params;
61 };
62
63 const fetchSeeds = async () => {
64 setLoading(true);
65 setErrorMsg('');
66 try {
67 const params = buildQueryParams();
68 const queryString = new URLSearchParams(params).toString();
69 const response = await fetch(`${API_BASE}/echo/seeds?${queryString}`);
70 const data = await response.json();
71
Krishya25590de2025-04-21 19:03:49 +080072 if (data.status!== 'success') throw new Error(data.message || '获取失败');
22301009afbcf4b2025-04-10 16:08:39 +080073 const seeds = data.seeds || [];
74 setSeeds(seeds);
75 setFilteredSeeds(seeds);
76 } catch (error) {
77 console.error('获取种子列表失败:', error);
78 setErrorMsg(error.message || '获取失败,请稍后再试。');
79 setSeeds([]);
80 setFilteredSeeds([]);
81 } finally {
82 setLoading(false);
83 }
84 };
85
86 const fetchFilterOptions = async () => {
87 if (activeTab === '猜你喜欢') return;
88 const category = CATEGORY_MAP[activeTab];
89 try {
90 const res = await axios.get(`${API_BASE}/echo/seed-filters?category=${category}`);
223010095b28c672025-04-10 20:12:45 +080091 const filterData = res.data || {};
92 setFilters(filterData);
93
22301009afbcf4b2025-04-10 16:08:39 +080094 const defaultSelections = {};
223010095b28c672025-04-10 20:12:45 +080095 for (const key in filterData) {
22301009afbcf4b2025-04-10 16:08:39 +080096 defaultSelections[key] = '不限';
97 }
98 setSelectedFilters(defaultSelections);
99 } catch (err) {
100 console.error('获取筛选项失败:', err);
101 setFilters({});
102 setSelectedFilters({});
103 }
104 };
22301009ecc1c1c2025-04-09 21:56:23 +0800105
106 useEffect(() => {
Krishya25590de2025-04-21 19:03:49 +0800107 if (activeTab!== '猜你喜欢') {
22301009afbcf4b2025-04-10 16:08:39 +0800108 fetchFilterOptions();
22301009ecc1c1c2025-04-09 21:56:23 +0800109 }
22301009afbcf4b2025-04-10 16:08:39 +0800110 }, [activeTab]);
22301009ecc1c1c2025-04-09 21:56:23 +0800111
22301009afbcf4b2025-04-10 16:08:39 +0800112 useEffect(() => {
Krishya25590de2025-04-21 19:03:49 +0800113 if (activeTab!== '猜你喜欢') {
22301009afbcf4b2025-04-10 16:08:39 +0800114 fetchSeeds();
22301009ecc1c1c2025-04-09 21:56:23 +0800115 }
22301009afbcf4b2025-04-10 16:08:39 +0800116 }, [activeTab, sortOption, selectedFilters, tagMode, searchTerm]);
22301009ecc1c1c2025-04-09 21:56:23 +0800117
22301009afbcf4b2025-04-10 16:08:39 +0800118 const handleDownload = async (seedId) => {
119 const peer_id = 'echo-' + Math.random().toString(36).substring(2, 10);
120 const ip = '127.0.0.1';
121 const port = 6881;
122 const uploaded = 0;
123 const downloaded = 0;
124 const left = 0;
125
126 try {
127 const response = await axios.get(`${API_BASE}/echo/seeds/${seedId}/download`, {
128 params: { peer_id, ip, port, uploaded, downloaded, left },
129 responseType: 'blob'
130 });
131
132 const blob = new Blob([response.data], { type: 'application/x-bittorrent' });
133 const downloadUrl = URL.createObjectURL(blob);
134 const a = document.createElement('a');
135 a.href = downloadUrl;
136 a.download = `${seedId}.torrent`;
137 a.click();
138 URL.revokeObjectURL(downloadUrl);
139 } catch (error) {
140 console.error('下载失败:', error);
141 alert('下载失败,请稍后再试。');
22301009ecc1c1c2025-04-09 21:56:23 +0800142 }
22301009afbcf4b2025-04-10 16:08:39 +0800143 };
22301009ecc1c1c2025-04-09 21:56:23 +0800144
223010095b28c672025-04-10 20:12:45 +0800145 const handleFilterChange = (key, value) => {
146 setSelectedFilters((prev) => ({
Krishya25590de2025-04-21 19:03:49 +0800147 ...prev,
223010095b28c672025-04-10 20:12:45 +0800148 [key]: value
149 }));
150 };
151
152 const clearFilter = (key) => {
153 setSelectedFilters((prev) => ({
Krishya25590de2025-04-21 19:03:49 +0800154 ...prev,
223010095b28c672025-04-10 20:12:45 +0800155 [key]: '不限'
156 }));
157 };
158
22301009ecc1c1c2025-04-09 21:56:23 +0800159 return (
22301009f9641c52025-04-15 21:14:56 +0800160 <div className="main-page">
161 <Header /> {/* 引用 Header 组件 */}
22301009ecc1c1c2025-04-09 21:56:23 +0800162
22301009ecc1c1c2025-04-09 21:56:23 +0800163 <div className="controls">
164 <input
165 type="text"
166 placeholder="搜索种子..."
167 value={searchTerm}
168 onChange={(e) => setSearchTerm(e.target.value)}
169 className="search-input"
170 />
171 <select value={sortOption} onChange={(e) => setSortOption(e.target.value)} className="sort-select">
172 <option value="最新">最新</option>
173 <option value="最热">最热</option>
22301009afbcf4b2025-04-10 16:08:39 +0800174 </select>
175 <select value={tagMode} onChange={(e) => setTagMode(e.target.value)} className="tag-mode-select">
176 <option value="any">包含任意标签</option>
177 <option value="all">包含所有标签</option>
22301009ecc1c1c2025-04-09 21:56:23 +0800178 </select>
179 </div>
180
181 <div className="tag-filters">
182 {TAGS.map((tag) => (
183 <button
184 key={tag}
Krishya25590de2025-04-21 19:03:49 +0800185 className={`tag-button ${activeTab === tag? 'active-tag' : ''}`}
22301009ecc1c1c2025-04-09 21:56:23 +0800186 onClick={() => {
22301009afbcf4b2025-04-10 16:08:39 +0800187 setActiveTab(tag);
188 setFilters({});
189 setSelectedFilters({});
22301009ecc1c1c2025-04-09 21:56:23 +0800190 }}
191 >
192 {tag}
193 </button>
194 ))}
195 </div>
196
Krishya25590de2025-04-21 19:03:49 +0800197 {activeTab!== '猜你喜欢' && Object.keys(filters).length > 0 && (
22301009afbcf4b2025-04-10 16:08:39 +0800198 <div className="filter-bar">
199 {Object.entries(filters).map(([key, options]) => (
200 <div className="filter-group" key={key}>
201 <label>{key}:</label>
202 <select
203 value={selectedFilters[key]}
223010095b28c672025-04-10 20:12:45 +0800204 onChange={(e) => handleFilterChange(key, e.target.value)}
22301009afbcf4b2025-04-10 16:08:39 +0800205 >
206 {options.map((opt) => (
207 <option key={opt} value={opt}>{opt}</option>
208 ))}
209 </select>
Krishya25590de2025-04-21 19:03:49 +0800210 {selectedFilters[key]!== '不限' && (
223010095b28c672025-04-10 20:12:45 +0800211 <button className="clear-filter-btn" onClick={() => clearFilter(key)}>✕</button>
212 )}
22301009afbcf4b2025-04-10 16:08:39 +0800213 </div>
214 ))}
215 </div>
216 )}
217
22301009ecc1c1c2025-04-09 21:56:23 +0800218 <div className="seed-list-content">
Krishya25590de2025-04-21 19:03:49 +0800219 {activeTab === '猜你喜欢'? (
22301009afbcf4b2025-04-10 16:08:39 +0800220 <Recommend />
Krishya25590de2025-04-21 19:03:49 +0800221 ) : loading? (
22301009ecc1c1c2025-04-09 21:56:23 +0800222 <p>加载中...</p>
Krishya25590de2025-04-21 19:03:49 +0800223 ) : errorMsg? (
22301009afbcf4b2025-04-10 16:08:39 +0800224 <p className="error-text">{errorMsg}</p>
Krishya25590de2025-04-21 19:03:49 +0800225 ) : filteredSeeds.length === 0? (
22301009ecc1c1c2025-04-09 21:56:23 +0800226 <p>未找到符合条件的种子。</p>
227 ) : (
Krishya25590de2025-04-21 19:03:49 +0800228 <div className="seed-list-card">
229 <div className="seed-list-header">
230 <div className="seed-header-cover"></div>
231 <div className="seed-header-title">种子名称</div>
232 <div className="seed-header-size">大小</div>
233 <div className="seed-header-upload-time">上传时间</div>
234 <div className="seed-header-downloads">下载次数</div>
235 <div className="seed-header-actions">操作</div>
236 </div>
237 <div className="seed-list-body">
238 {filteredSeeds.map((seed, index) => (
239 <Link href={`/seed/${seed.seed_id}`} key={index} className="seed-item-link">
240 <div className="seed-item">
241 {seed.image_url && (
242 <img src={seed.image_url} alt={seed.title} className="seed-item-cover" />
243 )}
244 <div className="seed-item-title">
245 <div className="seed-title-row">
246 <h3 className="seed-title">{seed.title}</h3>
247 <div className="seed-tags">
248 {seed.tags && seed.tags.map((tag, i) => (
249 <span key={i} className="tag-label">{tag}</span>
250 ))}
251 </div>
252 </div>
223010095b28c672025-04-10 20:12:45 +0800253 </div>
Krishya25590de2025-04-21 19:03:49 +0800254 <div className="seed-item-size">{seed.size || '未知'}</div>
255 <div className="seed-item-upload-time">{seed.upload_time?.split('T')[0] || '未知'}</div>
256 <div className="seed-item-downloads">{seed.downloads?? 0} 次下载</div>
257 <div
258 className="seed-item-actions"
259 onClick={(e) => e.stopPropagation()} // 阻止事件冒泡,避免跳转
260 >
261 <button
262 className="btn-primary"
263 onClick={(e) => {
264 e.preventDefault(); // 阻止跳转
265 e.stopPropagation();
266 handleDownload(seed.seed_id);
267 }}
268 >
269 下载
270 </button>
271 <button
272 className="btn-outline"
273 onClick={(e) => {
274 e.preventDefault();
275 e.stopPropagation();
276 // TODO: 收藏操作
277 }}
278 >
279 收藏
280 </button>
281 </div>
282 </div>
283 </Link>
284 ))}
285 </div>
22301009ecc1c1c2025-04-09 21:56:23 +0800286 </div>
287 )}
288 </div>
289 </div>
290 );
291};
292
293export default SeedList;
Krishya25590de2025-04-21 19:03:49 +0800294