blob: ce5908b7a398ee66114c87689b38434012207d8f [file] [log] [blame]
Krishyae71688a2025-04-10 21:25:17 +08001// import React, { useState, useEffect } from 'react';
2// import { Link } from 'wouter';
3// import axios from 'axios';
4// import logo from '../../assets/logo.png';
5// import Recommend from './Recommend/Recommend';
6// import './SeedList.css';
7
8// const API_BASE = process.env.REACT_APP_API_BASE;
9
10// const SeedList = () => {
11// const [seeds, setSeeds] = useState([]);
12// const [filteredSeeds, setFilteredSeeds] = useState([]);
13// const [loading, setLoading] = useState(true);
14// const [searchTerm, setSearchTerm] = useState('');
15// const [sortOption, setSortOption] = useState('最新');
16// const [activeTab, setActiveTab] = useState('种子列表');
17
18// const [filters, setFilters] = useState({});
19// const [selectedFilters, setSelectedFilters] = useState({});
20
21// const TAGS = ['猜你喜欢', '电影', '电视剧', '动漫', '音乐', '游戏', '综艺', '软件', '体育', '学习', '纪录片', '其他'];
22
23// const CATEGORY_MAP = {
24// '电影': 'movie',
25// '电视剧': 'tv',
26// '动漫': 'anime',
27// '音乐': 'music',
28// '游戏': 'game',
29// '综艺': 'variety',
30// '软件': 'software',
31// '体育': 'sports',
32// '学习': 'study',
33// '纪录片': 'documentary',
34// '其他': 'other'
35// };
36
37// const buildQueryParams = () => {
38// const category = CATEGORY_MAP[activeTab];
39
40// const params = {
41// category,
42// sort_by: sortOption === '最新' ? 'newest'
43// : sortOption === '最热' ? 'downloads'
44// : undefined,
45// page: 1,
46// limit: 20,
47// };
48
49// const tags = Object.entries(selectedFilters)
50// .filter(([_, value]) => value !== '不限')
51// .map(([_, value]) => value);
52
53// if (tags.length > 0) params.tags = tags.join(',');
54
55// return params;
56// };
57
58// const fetchSeeds = async () => {
59// setLoading(true);
60// try {
61// const params = buildQueryParams();
62// const queryString = new URLSearchParams(params).toString();
63// const response = await fetch(`${API_BASE}/echo/seeds?${queryString}`);
64// const data = await response.json();
65// const seeds = data?.seeds || [];
66// setSeeds(seeds);
67// setFilteredSeeds(seeds);
68// } catch (error) {
69// console.error('获取种子列表失败:', error);
70// } finally {
71// setLoading(false);
72// }
73// };
74
75// const fetchFilterOptions = async () => {
76// if (activeTab === '猜你喜欢') return;
77// const category = CATEGORY_MAP[activeTab];
78// try {
79// const res = await axios.get(`${API_BASE}/echo/seed-filters?category=${category}`);
80// setFilters(res.data || {});
81// const defaultSelections = {};
82// for (const key in res.data) {
83// defaultSelections[key] = '不限';
84// }
85// setSelectedFilters(defaultSelections);
86// } catch (err) {
87// console.error('获取筛选项失败:', err);
88// setFilters({});
89// setSelectedFilters({});
90// }
91// };
92
93// useEffect(() => {
94// if (activeTab !== '猜你喜欢') {
95// fetchFilterOptions();
96// }
97// }, [activeTab]);
98
99// useEffect(() => {
100// if (activeTab !== '猜你喜欢') {
101// fetchSeeds();
102// }
103// }, [activeTab, sortOption, selectedFilters]);
104
105// const handleDownload = async (seedId) => {
106// const peer_id = 'echo-' + Math.random().toString(36).substring(2, 10);
107// const ip = '127.0.0.1';
108// const port = 6881;
109// const uploaded = 0;
110// const downloaded = 0;
111// const left = 0;
112
113// try {
114// const response = await axios.get(`${API_BASE}/echo/seeds/${seedId}/download`, {
115// params: {
116// peer_id,
117// ip,
118// port,
119// uploaded,
120// downloaded,
121// left,
122// },
123// responseType: 'blob'
124// });
125
126// const blob = new Blob([response.data], { type: 'application/x-bittorrent' });
127// const downloadUrl = URL.createObjectURL(blob);
128// const a = document.createElement('a');
129// a.href = downloadUrl;
130// a.download = `${seedId}.torrent`;
131// a.click();
132// URL.revokeObjectURL(downloadUrl);
133// } catch (error) {
134// console.error('下载失败:', error);
135// alert('下载失败,请稍后再试。');
136// }
137// };
138
139// return (
140// <div className="friend-moments">
141// <header className="header">
142// <div className="logo-and-name">
143// <img src={logo} alt="网站logo" className="logo" />
144// <span className="site-name">Echo</span>
145// </div>
146// <div className="user-and-message">
147// <img src="user-avatar.png" alt="用户头像" className="user-avatar" />
148// <span className="message-center">消息</span>
149// </div>
150// </header>
151
152// <nav className="nav">
153// <Link to="/friend-moments" className="nav-item">好友动态</Link>
154// <Link to="/forum" className="nav-item">论坛</Link>
155// <Link to="/interest-groups" className="nav-item">兴趣小组</Link>
156// <Link to="/seed-list" className="nav-item active">种子列表</Link>
157// <Link to="/publish-seed" className="nav-item">发布种子</Link>
158// </nav>
159
160// <div className="controls">
161// <input
162// type="text"
163// placeholder="搜索种子..."
164// value={searchTerm}
165// onChange={(e) => setSearchTerm(e.target.value)}
166// className="search-input"
167// />
168// <select value={sortOption} onChange={(e) => setSortOption(e.target.value)} className="sort-select">
169// <option value="最新">最新</option>
170// <option value="最热">最热</option>
171// </select>
172// </div>
173
174// <div className="tag-filters">
175// {TAGS.map((tag) => (
176// <button
177// key={tag}
178// className={`tag-button ${activeTab === tag ? 'active-tag' : ''}`}
179// onClick={() => {
180// setActiveTab(tag);
181// setFilters({});
182// setSelectedFilters({});
183// }}
184// >
185// {tag}
186// </button>
187// ))}
188// </div>
189
190// {activeTab !== '猜你喜欢' && Object.keys(filters).length > 0 && (
191// <div className="filter-bar">
192// {Object.entries(filters).map(([key, options]) => (
193// <div className="filter-group" key={key}>
194// <label>{key}:</label>
195// <select
196// value={selectedFilters[key]}
197// onChange={(e) =>
198// setSelectedFilters({ ...selectedFilters, [key]: e.target.value })
199// }
200// >
201// {options.map((opt) => (
202// <option key={opt} value={opt}>{opt}</option>
203// ))}
204// </select>
205// </div>
206// ))}
207// </div>
208// )}
209
210// <div className="seed-list-content">
211// {activeTab === '猜你喜欢' ? (
212// <Recommend />
213// ) : loading ? (
214// <p>加载中...</p>
215// ) : filteredSeeds.length === 0 ? (
216// <p>未找到符合条件的种子。</p>
217// ) : (
218// <div className="seed-cards">
219// {filteredSeeds.map((seed, index) => (
220// <div key={index} className="seed-card">
221// <div className="seed-card-header">
222// <h3>{seed.title}</h3>
223// </div>
224// <div className="seed-card-body">
225// <p><strong>大小:</strong> {seed.size || '未知'} GB</p>
226// <p><strong>时间:</strong> {seed.upload_time?.split('T')[0] || '未知'}</p>
227// <p><strong>下载数:</strong> {seed.downloads ?? 0}</p>
228// </div>
229// <div className="seed-card-actions">
230// <button className="btn-primary" onClick={() => handleDownload(seed.seed_id)}>下载</button>
231// <Link href={`/seed/${seed.seed_id}`} className="btn-secondary">详情</Link>
232// <button className="btn-outline">收藏</button>
233// </div>
234// </div>
235// ))}
236// </div>
237// )}
238// </div>
239// </div>
240// );
241// };
242
243// export default SeedList;
22301009ecc1c1c2025-04-09 21:56:23 +0800244import React, { useState, useEffect } from 'react';
22301009ecc1c1c2025-04-09 21:56:23 +0800245import { Link } from 'wouter';
22301009afbcf4b2025-04-10 16:08:39 +0800246import axios from 'axios';
22301009ecc1c1c2025-04-09 21:56:23 +0800247import logo from '../../assets/logo.png';
22301009afbcf4b2025-04-10 16:08:39 +0800248import Recommend from './Recommend/Recommend';
22301009f9641c52025-04-15 21:14:56 +0800249import Header from '../../components/Header'; // 引入 Header 组件
22301009ecc1c1c2025-04-09 21:56:23 +0800250import './SeedList.css';
251
22301009afbcf4b2025-04-10 16:08:39 +0800252const API_BASE = process.env.REACT_APP_API_BASE;
253
22301009ecc1c1c2025-04-09 21:56:23 +0800254const SeedList = () => {
255 const [seeds, setSeeds] = useState([]);
256 const [filteredSeeds, setFilteredSeeds] = useState([]);
257 const [loading, setLoading] = useState(true);
258 const [searchTerm, setSearchTerm] = useState('');
22301009ecc1c1c2025-04-09 21:56:23 +0800259 const [sortOption, setSortOption] = useState('最新');
22301009afbcf4b2025-04-10 16:08:39 +0800260 const [activeTab, setActiveTab] = useState('种子列表');
261 const [filters, setFilters] = useState({});
262 const [selectedFilters, setSelectedFilters] = useState({});
223010095b28c672025-04-10 20:12:45 +0800263 const [tagMode, setTagMode] = useState('all');
22301009afbcf4b2025-04-10 16:08:39 +0800264 const [errorMsg, setErrorMsg] = useState('');
22301009ecc1c1c2025-04-09 21:56:23 +0800265
22301009afbcf4b2025-04-10 16:08:39 +0800266 const TAGS = ['猜你喜欢', '电影', '电视剧', '动漫', '音乐', '游戏', '综艺', '软件', '体育', '学习', '纪录片', '其他'];
22301009ecc1c1c2025-04-09 21:56:23 +0800267
22301009afbcf4b2025-04-10 16:08:39 +0800268 const CATEGORY_MAP = {
269 '电影': 'movie',
270 '电视剧': 'tv',
271 '动漫': 'anime',
272 '音乐': 'music',
273 '游戏': 'game',
274 '综艺': 'variety',
275 '软件': 'software',
276 '体育': 'sports',
277 '学习': 'education',
278 '纪录片': 'documentary',
279 '其他': 'other'
280 };
281
282 const buildQueryParams = () => {
283 const category = CATEGORY_MAP[activeTab];
284 const params = {
285 category,
286 sort_by: sortOption === '最新' ? 'newest' : sortOption === '最热' ? 'downloads' : undefined,
287 page: 1,
288 limit: 20,
223010095b28c672025-04-10 20:12:45 +0800289 include_fields: 'seed_id,title,category,tags,size,upload_time,downloads,image_url',
22301009ecc1c1c2025-04-09 21:56:23 +0800290 };
291
223010095b28c672025-04-10 20:12:45 +0800292 if (searchTerm.trim()) params.search = searchTerm.trim();
22301009afbcf4b2025-04-10 16:08:39 +0800293
294 const tags = Object.entries(selectedFilters)
295 .filter(([_, value]) => value !== '不限')
296 .map(([_, value]) => value);
297
298 if (tags.length > 0) {
299 params.tags = tags.join(',');
300 params.tag_mode = tagMode;
301 }
302
303 return params;
304 };
305
306 const fetchSeeds = async () => {
307 setLoading(true);
308 setErrorMsg('');
309 try {
310 const params = buildQueryParams();
311 const queryString = new URLSearchParams(params).toString();
312 const response = await fetch(`${API_BASE}/echo/seeds?${queryString}`);
313 const data = await response.json();
314
315 if (data.status !== 'success') throw new Error(data.message || '获取失败');
316 const seeds = data.seeds || [];
317 setSeeds(seeds);
318 setFilteredSeeds(seeds);
319 } catch (error) {
320 console.error('获取种子列表失败:', error);
321 setErrorMsg(error.message || '获取失败,请稍后再试。');
322 setSeeds([]);
323 setFilteredSeeds([]);
324 } finally {
325 setLoading(false);
326 }
327 };
328
329 const fetchFilterOptions = async () => {
330 if (activeTab === '猜你喜欢') return;
331 const category = CATEGORY_MAP[activeTab];
332 try {
333 const res = await axios.get(`${API_BASE}/echo/seed-filters?category=${category}`);
223010095b28c672025-04-10 20:12:45 +0800334 const filterData = res.data || {};
335 setFilters(filterData);
336
22301009afbcf4b2025-04-10 16:08:39 +0800337 const defaultSelections = {};
223010095b28c672025-04-10 20:12:45 +0800338 for (const key in filterData) {
22301009afbcf4b2025-04-10 16:08:39 +0800339 defaultSelections[key] = '不限';
340 }
341 setSelectedFilters(defaultSelections);
342 } catch (err) {
343 console.error('获取筛选项失败:', err);
344 setFilters({});
345 setSelectedFilters({});
346 }
347 };
22301009ecc1c1c2025-04-09 21:56:23 +0800348
349 useEffect(() => {
22301009afbcf4b2025-04-10 16:08:39 +0800350 if (activeTab !== '猜你喜欢') {
351 fetchFilterOptions();
22301009ecc1c1c2025-04-09 21:56:23 +0800352 }
22301009afbcf4b2025-04-10 16:08:39 +0800353 }, [activeTab]);
22301009ecc1c1c2025-04-09 21:56:23 +0800354
22301009afbcf4b2025-04-10 16:08:39 +0800355 useEffect(() => {
356 if (activeTab !== '猜你喜欢') {
357 fetchSeeds();
22301009ecc1c1c2025-04-09 21:56:23 +0800358 }
22301009afbcf4b2025-04-10 16:08:39 +0800359 }, [activeTab, sortOption, selectedFilters, tagMode, searchTerm]);
22301009ecc1c1c2025-04-09 21:56:23 +0800360
22301009afbcf4b2025-04-10 16:08:39 +0800361 const handleDownload = async (seedId) => {
362 const peer_id = 'echo-' + Math.random().toString(36).substring(2, 10);
363 const ip = '127.0.0.1';
364 const port = 6881;
365 const uploaded = 0;
366 const downloaded = 0;
367 const left = 0;
368
369 try {
370 const response = await axios.get(`${API_BASE}/echo/seeds/${seedId}/download`, {
371 params: { peer_id, ip, port, uploaded, downloaded, left },
372 responseType: 'blob'
373 });
374
375 const blob = new Blob([response.data], { type: 'application/x-bittorrent' });
376 const downloadUrl = URL.createObjectURL(blob);
377 const a = document.createElement('a');
378 a.href = downloadUrl;
379 a.download = `${seedId}.torrent`;
380 a.click();
381 URL.revokeObjectURL(downloadUrl);
382 } catch (error) {
383 console.error('下载失败:', error);
384 alert('下载失败,请稍后再试。');
22301009ecc1c1c2025-04-09 21:56:23 +0800385 }
22301009afbcf4b2025-04-10 16:08:39 +0800386 };
22301009ecc1c1c2025-04-09 21:56:23 +0800387
223010095b28c672025-04-10 20:12:45 +0800388 const handleFilterChange = (key, value) => {
389 setSelectedFilters((prev) => ({
390 ...prev,
391 [key]: value
392 }));
393 };
394
395 const clearFilter = (key) => {
396 setSelectedFilters((prev) => ({
397 ...prev,
398 [key]: '不限'
399 }));
400 };
401
22301009ecc1c1c2025-04-09 21:56:23 +0800402 return (
22301009f9641c52025-04-15 21:14:56 +0800403 <div className="main-page">
404 <Header /> {/* 引用 Header 组件 */}
22301009ecc1c1c2025-04-09 21:56:23 +0800405
22301009ecc1c1c2025-04-09 21:56:23 +0800406 <div className="controls">
407 <input
408 type="text"
409 placeholder="搜索种子..."
410 value={searchTerm}
411 onChange={(e) => setSearchTerm(e.target.value)}
412 className="search-input"
413 />
414 <select value={sortOption} onChange={(e) => setSortOption(e.target.value)} className="sort-select">
415 <option value="最新">最新</option>
416 <option value="最热">最热</option>
22301009afbcf4b2025-04-10 16:08:39 +0800417 </select>
418 <select value={tagMode} onChange={(e) => setTagMode(e.target.value)} className="tag-mode-select">
419 <option value="any">包含任意标签</option>
420 <option value="all">包含所有标签</option>
22301009ecc1c1c2025-04-09 21:56:23 +0800421 </select>
422 </div>
423
424 <div className="tag-filters">
425 {TAGS.map((tag) => (
426 <button
427 key={tag}
22301009afbcf4b2025-04-10 16:08:39 +0800428 className={`tag-button ${activeTab === tag ? 'active-tag' : ''}`}
22301009ecc1c1c2025-04-09 21:56:23 +0800429 onClick={() => {
22301009afbcf4b2025-04-10 16:08:39 +0800430 setActiveTab(tag);
431 setFilters({});
432 setSelectedFilters({});
22301009ecc1c1c2025-04-09 21:56:23 +0800433 }}
434 >
435 {tag}
436 </button>
437 ))}
438 </div>
439
22301009afbcf4b2025-04-10 16:08:39 +0800440 {activeTab !== '猜你喜欢' && Object.keys(filters).length > 0 && (
441 <div className="filter-bar">
442 {Object.entries(filters).map(([key, options]) => (
443 <div className="filter-group" key={key}>
444 <label>{key}:</label>
445 <select
446 value={selectedFilters[key]}
223010095b28c672025-04-10 20:12:45 +0800447 onChange={(e) => handleFilterChange(key, e.target.value)}
22301009afbcf4b2025-04-10 16:08:39 +0800448 >
449 {options.map((opt) => (
450 <option key={opt} value={opt}>{opt}</option>
451 ))}
452 </select>
223010095b28c672025-04-10 20:12:45 +0800453 {selectedFilters[key] !== '不限' && (
454 <button className="clear-filter-btn" onClick={() => clearFilter(key)}>✕</button>
455 )}
22301009afbcf4b2025-04-10 16:08:39 +0800456 </div>
457 ))}
458 </div>
459 )}
460
22301009ecc1c1c2025-04-09 21:56:23 +0800461 <div className="seed-list-content">
462 {activeTab === '猜你喜欢' ? (
22301009afbcf4b2025-04-10 16:08:39 +0800463 <Recommend />
22301009ecc1c1c2025-04-09 21:56:23 +0800464 ) : loading ? (
465 <p>加载中...</p>
22301009afbcf4b2025-04-10 16:08:39 +0800466 ) : errorMsg ? (
467 <p className="error-text">{errorMsg}</p>
22301009ecc1c1c2025-04-09 21:56:23 +0800468 ) : filteredSeeds.length === 0 ? (
469 <p>未找到符合条件的种子。</p>
470 ) : (
471 <div className="seed-cards">
472 {filteredSeeds.map((seed, index) => (
473 <div key={index} className="seed-card">
223010095b28c672025-04-10 20:12:45 +0800474 {seed.image_url && (
475 <img src={seed.image_url} alt={seed.title} className="seed-cover" />
476 )}
22301009ecc1c1c2025-04-09 21:56:23 +0800477 <div className="seed-card-header">
22301009afbcf4b2025-04-10 16:08:39 +0800478 <h3>{seed.title}</h3>
22301009ecc1c1c2025-04-09 21:56:23 +0800479 </div>
223010095b28c672025-04-10 20:12:45 +0800480
22301009ecc1c1c2025-04-09 21:56:23 +0800481 <div className="seed-card-body">
223010095b28c672025-04-10 20:12:45 +0800482 <div className="seed-info">
483 <span>{seed.size || '未知'} GB</span>
484 <span>{seed.upload_time?.split('T')[0] || '未知'}</span>
485 <span>{seed.downloads ?? 0} 次下载</span>
486 </div>
487 {seed.tags && seed.tags.length > 0 && (
488 <div className="seed-card-tags">
489 {seed.tags.map((tag, i) => (
490 <span key={i} className="tag-label">{tag}</span>
491 ))}
492 </div>
493 )}
22301009ecc1c1c2025-04-09 21:56:23 +0800494 </div>
223010095b28c672025-04-10 20:12:45 +0800495
22301009ecc1c1c2025-04-09 21:56:23 +0800496 <div className="seed-card-actions">
22301009afbcf4b2025-04-10 16:08:39 +0800497 <button className="btn-primary" onClick={() => handleDownload(seed.seed_id)}>下载</button>
498 <Link href={`/seed/${seed.seed_id}`} className="btn-secondary">详情</Link>
22301009ecc1c1c2025-04-09 21:56:23 +0800499 <button className="btn-outline">收藏</button>
500 </div>
501 </div>
502 ))}
503 </div>
504 )}
505 </div>
506 </div>
507 );
508};
509
510export default SeedList;