blob: 3519f17cef9ed763d48be5d74c02e8c9b068ebd7 [file] [log] [blame]
22301009afbcf4b2025-04-10 16:08:39 +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="main-page">
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({});
262 const [tagMode, setTagMode] = useState('all'); // 支持 tag_mode 参数
263 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,
288 include_fields: 'seed_id,title,category,tags,size,upload_time,downloads',
22301009ecc1c1c2025-04-09 21:56:23 +0800289 };
290
22301009afbcf4b2025-04-10 16:08:39 +0800291 if (searchTerm.trim()) {
292 params.search = searchTerm.trim();
293 }
294
295 const tags = Object.entries(selectedFilters)
296 .filter(([_, value]) => value !== '不限')
297 .map(([_, value]) => value);
298
299 if (tags.length > 0) {
300 params.tags = tags.join(',');
301 params.tag_mode = tagMode;
302 }
303
304 return params;
305 };
306
307 const fetchSeeds = async () => {
308 setLoading(true);
309 setErrorMsg('');
310 try {
311 const params = buildQueryParams();
312 const queryString = new URLSearchParams(params).toString();
313 const response = await fetch(`${API_BASE}/echo/seeds?${queryString}`);
314 const data = await response.json();
315
316 if (data.status !== 'success') throw new Error(data.message || '获取失败');
317 const seeds = data.seeds || [];
318 setSeeds(seeds);
319 setFilteredSeeds(seeds);
320 } catch (error) {
321 console.error('获取种子列表失败:', error);
322 setErrorMsg(error.message || '获取失败,请稍后再试。');
323 setSeeds([]);
324 setFilteredSeeds([]);
325 } finally {
326 setLoading(false);
327 }
328 };
329
330 const fetchFilterOptions = async () => {
331 if (activeTab === '猜你喜欢') return;
332 const category = CATEGORY_MAP[activeTab];
333 try {
334 const res = await axios.get(`${API_BASE}/echo/seed-filters?category=${category}`);
335 setFilters(res.data || {});
336 const defaultSelections = {};
337 for (const key in res.data) {
338 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
387 return (
388 <div className="main-page">
389 <header className="header">
390 <div className="logo-and-name">
391 <img src={logo} alt="网站logo" className="logo" />
392 <span className="site-name">Echo</span>
393 </div>
394 <div className="user-and-message">
395 <img src="user-avatar.png" alt="用户头像" className="user-avatar" />
396 <span className="message-center">消息</span>
397 </div>
398 </header>
399
400 <nav className="nav">
401 <Link to="/friend-moments" className="nav-item">好友动态</Link>
402 <Link to="/forum" className="nav-item">论坛</Link>
403 <Link to="/interest-groups" className="nav-item">兴趣小组</Link>
404 <Link to="/seed-list" className="nav-item active">种子列表</Link>
405 <Link to="/publish-seed" className="nav-item">发布种子</Link>
406 </nav>
407
22301009ecc1c1c2025-04-09 21:56:23 +0800408 <div className="controls">
409 <input
410 type="text"
411 placeholder="搜索种子..."
412 value={searchTerm}
413 onChange={(e) => setSearchTerm(e.target.value)}
414 className="search-input"
415 />
416 <select value={sortOption} onChange={(e) => setSortOption(e.target.value)} className="sort-select">
417 <option value="最新">最新</option>
418 <option value="最热">最热</option>
22301009afbcf4b2025-04-10 16:08:39 +0800419 </select>
420 <select value={tagMode} onChange={(e) => setTagMode(e.target.value)} className="tag-mode-select">
421 <option value="any">包含任意标签</option>
422 <option value="all">包含所有标签</option>
22301009ecc1c1c2025-04-09 21:56:23 +0800423 </select>
424 </div>
425
426 <div className="tag-filters">
427 {TAGS.map((tag) => (
428 <button
429 key={tag}
22301009afbcf4b2025-04-10 16:08:39 +0800430 className={`tag-button ${activeTab === tag ? 'active-tag' : ''}`}
22301009ecc1c1c2025-04-09 21:56:23 +0800431 onClick={() => {
22301009afbcf4b2025-04-10 16:08:39 +0800432 setActiveTab(tag);
433 setFilters({});
434 setSelectedFilters({});
22301009ecc1c1c2025-04-09 21:56:23 +0800435 }}
436 >
437 {tag}
438 </button>
439 ))}
440 </div>
441
22301009afbcf4b2025-04-10 16:08:39 +0800442 {activeTab !== '猜你喜欢' && Object.keys(filters).length > 0 && (
443 <div className="filter-bar">
444 {Object.entries(filters).map(([key, options]) => (
445 <div className="filter-group" key={key}>
446 <label>{key}:</label>
447 <select
448 value={selectedFilters[key]}
449 onChange={(e) =>
450 setSelectedFilters({ ...selectedFilters, [key]: e.target.value })
451 }
452 >
453 {options.map((opt) => (
454 <option key={opt} value={opt}>{opt}</option>
455 ))}
456 </select>
457 </div>
458 ))}
459 </div>
460 )}
461
22301009ecc1c1c2025-04-09 21:56:23 +0800462 <div className="seed-list-content">
463 {activeTab === '猜你喜欢' ? (
22301009afbcf4b2025-04-10 16:08:39 +0800464 <Recommend />
22301009ecc1c1c2025-04-09 21:56:23 +0800465 ) : loading ? (
466 <p>加载中...</p>
22301009afbcf4b2025-04-10 16:08:39 +0800467 ) : errorMsg ? (
468 <p className="error-text">{errorMsg}</p>
22301009ecc1c1c2025-04-09 21:56:23 +0800469 ) : filteredSeeds.length === 0 ? (
470 <p>未找到符合条件的种子。</p>
471 ) : (
472 <div className="seed-cards">
473 {filteredSeeds.map((seed, index) => (
474 <div key={index} className="seed-card">
475 <div className="seed-card-header">
22301009afbcf4b2025-04-10 16:08:39 +0800476 <h3>{seed.title}</h3>
22301009ecc1c1c2025-04-09 21:56:23 +0800477 </div>
478 <div className="seed-card-body">
22301009afbcf4b2025-04-10 16:08:39 +0800479 <p><strong>大小:</strong> {seed.size || '未知'} GB</p>
480 <p><strong>时间:</strong> {seed.upload_time?.split('T')[0] || '未知'}</p>
481 <p><strong>下载数:</strong> {seed.downloads ?? 0}</p>
22301009ecc1c1c2025-04-09 21:56:23 +0800482 </div>
483 <div className="seed-card-actions">
22301009afbcf4b2025-04-10 16:08:39 +0800484 <button className="btn-primary" onClick={() => handleDownload(seed.seed_id)}>下载</button>
485 <Link href={`/seed/${seed.seed_id}`} className="btn-secondary">详情</Link>
22301009ecc1c1c2025-04-09 21:56:23 +0800486 <button className="btn-outline">收藏</button>
487 </div>
488 </div>
489 ))}
490 </div>
491 )}
492 </div>
493 </div>
494 );
495};
496
497export default SeedList;