blob: 8ffecb3df4f6af83132159bb528eb447f2a71775 [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';
22301009ecc1c1c2025-04-09 21:56:23 +0800249import './SeedList.css';
250
22301009afbcf4b2025-04-10 16:08:39 +0800251const API_BASE = process.env.REACT_APP_API_BASE;
252
22301009ecc1c1c2025-04-09 21:56:23 +0800253const SeedList = () => {
254 const [seeds, setSeeds] = useState([]);
255 const [filteredSeeds, setFilteredSeeds] = useState([]);
256 const [loading, setLoading] = useState(true);
257 const [searchTerm, setSearchTerm] = useState('');
22301009ecc1c1c2025-04-09 21:56:23 +0800258 const [sortOption, setSortOption] = useState('最新');
22301009afbcf4b2025-04-10 16:08:39 +0800259 const [activeTab, setActiveTab] = useState('种子列表');
260 const [filters, setFilters] = useState({});
261 const [selectedFilters, setSelectedFilters] = useState({});
223010095b28c672025-04-10 20:12:45 +0800262 const [tagMode, setTagMode] = useState('all');
22301009afbcf4b2025-04-10 16:08:39 +0800263 const [errorMsg, setErrorMsg] = useState('');
22301009ecc1c1c2025-04-09 21:56:23 +0800264
22301009afbcf4b2025-04-10 16:08:39 +0800265 const TAGS = ['猜你喜欢', '电影', '电视剧', '动漫', '音乐', '游戏', '综艺', '软件', '体育', '学习', '纪录片', '其他'];
22301009ecc1c1c2025-04-09 21:56:23 +0800266
22301009afbcf4b2025-04-10 16:08:39 +0800267 const CATEGORY_MAP = {
268 '电影': 'movie',
269 '电视剧': 'tv',
270 '动漫': 'anime',
271 '音乐': 'music',
272 '游戏': 'game',
273 '综艺': 'variety',
274 '软件': 'software',
275 '体育': 'sports',
276 '学习': 'education',
277 '纪录片': 'documentary',
278 '其他': 'other'
279 };
280
281 const buildQueryParams = () => {
282 const category = CATEGORY_MAP[activeTab];
283 const params = {
284 category,
285 sort_by: sortOption === '最新' ? 'newest' : sortOption === '最热' ? 'downloads' : undefined,
286 page: 1,
287 limit: 20,
223010095b28c672025-04-10 20:12:45 +0800288 include_fields: 'seed_id,title,category,tags,size,upload_time,downloads,image_url',
22301009ecc1c1c2025-04-09 21:56:23 +0800289 };
290
223010095b28c672025-04-10 20:12:45 +0800291 if (searchTerm.trim()) params.search = searchTerm.trim();
22301009afbcf4b2025-04-10 16:08:39 +0800292
293 const tags = Object.entries(selectedFilters)
294 .filter(([_, value]) => value !== '不限')
295 .map(([_, value]) => value);
296
297 if (tags.length > 0) {
298 params.tags = tags.join(',');
299 params.tag_mode = tagMode;
300 }
301
302 return params;
303 };
304
305 const fetchSeeds = async () => {
306 setLoading(true);
307 setErrorMsg('');
308 try {
309 const params = buildQueryParams();
310 const queryString = new URLSearchParams(params).toString();
311 const response = await fetch(`${API_BASE}/echo/seeds?${queryString}`);
312 const data = await response.json();
313
314 if (data.status !== 'success') throw new Error(data.message || '获取失败');
315 const seeds = data.seeds || [];
316 setSeeds(seeds);
317 setFilteredSeeds(seeds);
318 } catch (error) {
319 console.error('获取种子列表失败:', error);
320 setErrorMsg(error.message || '获取失败,请稍后再试。');
321 setSeeds([]);
322 setFilteredSeeds([]);
323 } finally {
324 setLoading(false);
325 }
326 };
327
328 const fetchFilterOptions = async () => {
329 if (activeTab === '猜你喜欢') return;
330 const category = CATEGORY_MAP[activeTab];
331 try {
332 const res = await axios.get(`${API_BASE}/echo/seed-filters?category=${category}`);
223010095b28c672025-04-10 20:12:45 +0800333 const filterData = res.data || {};
334 setFilters(filterData);
335
22301009afbcf4b2025-04-10 16:08:39 +0800336 const defaultSelections = {};
223010095b28c672025-04-10 20:12:45 +0800337 for (const key in filterData) {
22301009afbcf4b2025-04-10 16:08:39 +0800338 defaultSelections[key] = '不限';
339 }
340 setSelectedFilters(defaultSelections);
341 } catch (err) {
342 console.error('获取筛选项失败:', err);
343 setFilters({});
344 setSelectedFilters({});
345 }
346 };
22301009ecc1c1c2025-04-09 21:56:23 +0800347
348 useEffect(() => {
22301009afbcf4b2025-04-10 16:08:39 +0800349 if (activeTab !== '猜你喜欢') {
350 fetchFilterOptions();
22301009ecc1c1c2025-04-09 21:56:23 +0800351 }
22301009afbcf4b2025-04-10 16:08:39 +0800352 }, [activeTab]);
22301009ecc1c1c2025-04-09 21:56:23 +0800353
22301009afbcf4b2025-04-10 16:08:39 +0800354 useEffect(() => {
355 if (activeTab !== '猜你喜欢') {
356 fetchSeeds();
22301009ecc1c1c2025-04-09 21:56:23 +0800357 }
22301009afbcf4b2025-04-10 16:08:39 +0800358 }, [activeTab, sortOption, selectedFilters, tagMode, searchTerm]);
22301009ecc1c1c2025-04-09 21:56:23 +0800359
22301009afbcf4b2025-04-10 16:08:39 +0800360 const handleDownload = async (seedId) => {
361 const peer_id = 'echo-' + Math.random().toString(36).substring(2, 10);
362 const ip = '127.0.0.1';
363 const port = 6881;
364 const uploaded = 0;
365 const downloaded = 0;
366 const left = 0;
367
368 try {
369 const response = await axios.get(`${API_BASE}/echo/seeds/${seedId}/download`, {
370 params: { peer_id, ip, port, uploaded, downloaded, left },
371 responseType: 'blob'
372 });
373
374 const blob = new Blob([response.data], { type: 'application/x-bittorrent' });
375 const downloadUrl = URL.createObjectURL(blob);
376 const a = document.createElement('a');
377 a.href = downloadUrl;
378 a.download = `${seedId}.torrent`;
379 a.click();
380 URL.revokeObjectURL(downloadUrl);
381 } catch (error) {
382 console.error('下载失败:', error);
383 alert('下载失败,请稍后再试。');
22301009ecc1c1c2025-04-09 21:56:23 +0800384 }
22301009afbcf4b2025-04-10 16:08:39 +0800385 };
22301009ecc1c1c2025-04-09 21:56:23 +0800386
223010095b28c672025-04-10 20:12:45 +0800387 const handleFilterChange = (key, value) => {
388 setSelectedFilters((prev) => ({
389 ...prev,
390 [key]: value
391 }));
392 };
393
394 const clearFilter = (key) => {
395 setSelectedFilters((prev) => ({
396 ...prev,
397 [key]: '不限'
398 }));
399 };
400
22301009ecc1c1c2025-04-09 21:56:23 +0800401 return (
Krishyae71688a2025-04-10 21:25:17 +0800402 <div className="friend-moments">
22301009ecc1c1c2025-04-09 21:56:23 +0800403 <header className="header">
404 <div className="logo-and-name">
405 <img src={logo} alt="网站logo" className="logo" />
406 <span className="site-name">Echo</span>
407 </div>
408 <div className="user-and-message">
409 <img src="user-avatar.png" alt="用户头像" className="user-avatar" />
410 <span className="message-center">消息</span>
411 </div>
412 </header>
413
414 <nav className="nav">
415 <Link to="/friend-moments" className="nav-item">好友动态</Link>
416 <Link to="/forum" className="nav-item">论坛</Link>
417 <Link to="/interest-groups" className="nav-item">兴趣小组</Link>
418 <Link to="/seed-list" className="nav-item active">种子列表</Link>
419 <Link to="/publish-seed" className="nav-item">发布种子</Link>
420 </nav>
421
22301009ecc1c1c2025-04-09 21:56:23 +0800422 <div className="controls">
423 <input
424 type="text"
425 placeholder="搜索种子..."
426 value={searchTerm}
427 onChange={(e) => setSearchTerm(e.target.value)}
428 className="search-input"
429 />
430 <select value={sortOption} onChange={(e) => setSortOption(e.target.value)} className="sort-select">
431 <option value="最新">最新</option>
432 <option value="最热">最热</option>
22301009afbcf4b2025-04-10 16:08:39 +0800433 </select>
434 <select value={tagMode} onChange={(e) => setTagMode(e.target.value)} className="tag-mode-select">
435 <option value="any">包含任意标签</option>
436 <option value="all">包含所有标签</option>
22301009ecc1c1c2025-04-09 21:56:23 +0800437 </select>
438 </div>
439
440 <div className="tag-filters">
441 {TAGS.map((tag) => (
442 <button
443 key={tag}
22301009afbcf4b2025-04-10 16:08:39 +0800444 className={`tag-button ${activeTab === tag ? 'active-tag' : ''}`}
22301009ecc1c1c2025-04-09 21:56:23 +0800445 onClick={() => {
22301009afbcf4b2025-04-10 16:08:39 +0800446 setActiveTab(tag);
447 setFilters({});
448 setSelectedFilters({});
22301009ecc1c1c2025-04-09 21:56:23 +0800449 }}
450 >
451 {tag}
452 </button>
453 ))}
454 </div>
455
22301009afbcf4b2025-04-10 16:08:39 +0800456 {activeTab !== '猜你喜欢' && Object.keys(filters).length > 0 && (
457 <div className="filter-bar">
458 {Object.entries(filters).map(([key, options]) => (
459 <div className="filter-group" key={key}>
460 <label>{key}:</label>
461 <select
462 value={selectedFilters[key]}
223010095b28c672025-04-10 20:12:45 +0800463 onChange={(e) => handleFilterChange(key, e.target.value)}
22301009afbcf4b2025-04-10 16:08:39 +0800464 >
465 {options.map((opt) => (
466 <option key={opt} value={opt}>{opt}</option>
467 ))}
468 </select>
223010095b28c672025-04-10 20:12:45 +0800469 {selectedFilters[key] !== '不限' && (
470 <button className="clear-filter-btn" onClick={() => clearFilter(key)}>✕</button>
471 )}
22301009afbcf4b2025-04-10 16:08:39 +0800472 </div>
473 ))}
474 </div>
475 )}
476
22301009ecc1c1c2025-04-09 21:56:23 +0800477 <div className="seed-list-content">
478 {activeTab === '猜你喜欢' ? (
22301009afbcf4b2025-04-10 16:08:39 +0800479 <Recommend />
22301009ecc1c1c2025-04-09 21:56:23 +0800480 ) : loading ? (
481 <p>加载中...</p>
22301009afbcf4b2025-04-10 16:08:39 +0800482 ) : errorMsg ? (
483 <p className="error-text">{errorMsg}</p>
22301009ecc1c1c2025-04-09 21:56:23 +0800484 ) : filteredSeeds.length === 0 ? (
485 <p>未找到符合条件的种子。</p>
486 ) : (
487 <div className="seed-cards">
488 {filteredSeeds.map((seed, index) => (
489 <div key={index} className="seed-card">
223010095b28c672025-04-10 20:12:45 +0800490 {seed.image_url && (
491 <img src={seed.image_url} alt={seed.title} className="seed-cover" />
492 )}
22301009ecc1c1c2025-04-09 21:56:23 +0800493 <div className="seed-card-header">
22301009afbcf4b2025-04-10 16:08:39 +0800494 <h3>{seed.title}</h3>
22301009ecc1c1c2025-04-09 21:56:23 +0800495 </div>
223010095b28c672025-04-10 20:12:45 +0800496
22301009ecc1c1c2025-04-09 21:56:23 +0800497 <div className="seed-card-body">
223010095b28c672025-04-10 20:12:45 +0800498 <div className="seed-info">
499 <span>{seed.size || '未知'} GB</span>
500 <span>{seed.upload_time?.split('T')[0] || '未知'}</span>
501 <span>{seed.downloads ?? 0} 次下载</span>
502 </div>
503 {seed.tags && seed.tags.length > 0 && (
504 <div className="seed-card-tags">
505 {seed.tags.map((tag, i) => (
506 <span key={i} className="tag-label">{tag}</span>
507 ))}
508 </div>
509 )}
22301009ecc1c1c2025-04-09 21:56:23 +0800510 </div>
223010095b28c672025-04-10 20:12:45 +0800511
22301009ecc1c1c2025-04-09 21:56:23 +0800512 <div className="seed-card-actions">
22301009afbcf4b2025-04-10 16:08:39 +0800513 <button className="btn-primary" onClick={() => handleDownload(seed.seed_id)}>下载</button>
514 <Link href={`/seed/${seed.seed_id}`} className="btn-secondary">详情</Link>
22301009ecc1c1c2025-04-09 21:56:23 +0800515 <button className="btn-outline">收藏</button>
516 </div>
517 </div>
518 ))}
519 </div>
520 )}
521 </div>
522 </div>
523 );
524};
525
526export default SeedList;