blob: f32224ed5d2637128c175a87a90fc356a4aa53db [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';
22301009ecc1c1c2025-04-09 21:56:23 +08006import './SeedList.css';
7
22301009afbcf4b2025-04-10 16:08:39 +08008const API_BASE = process.env.REACT_APP_API_BASE;
9
22301009ecc1c1c2025-04-09 21:56:23 +080010const SeedList = () => {
11 const [seeds, setSeeds] = useState([]);
12 const [filteredSeeds, setFilteredSeeds] = useState([]);
13 const [loading, setLoading] = useState(true);
14 const [searchTerm, setSearchTerm] = useState('');
22301009ecc1c1c2025-04-09 21:56:23 +080015 const [sortOption, setSortOption] = useState('最新');
22301009afbcf4b2025-04-10 16:08:39 +080016 const [activeTab, setActiveTab] = useState('种子列表');
17 const [filters, setFilters] = useState({});
18 const [selectedFilters, setSelectedFilters] = useState({});
223010095b28c672025-04-10 20:12:45 +080019 const [tagMode, setTagMode] = useState('all');
22301009afbcf4b2025-04-10 16:08:39 +080020 const [errorMsg, setErrorMsg] = useState('');
22301009ecc1c1c2025-04-09 21:56:23 +080021
22301009afbcf4b2025-04-10 16:08:39 +080022 const TAGS = ['猜你喜欢', '电影', '电视剧', '动漫', '音乐', '游戏', '综艺', '软件', '体育', '学习', '纪录片', '其他'];
22301009ecc1c1c2025-04-09 21:56:23 +080023
22301009afbcf4b2025-04-10 16:08:39 +080024 const CATEGORY_MAP = {
25 '电影': 'movie',
26 '电视剧': 'tv',
27 '动漫': 'anime',
28 '音乐': 'music',
29 '游戏': 'game',
30 '综艺': 'variety',
31 '软件': 'software',
32 '体育': 'sports',
33 '学习': 'education',
34 '纪录片': 'documentary',
35 '其他': 'other'
36 };
37
38 const buildQueryParams = () => {
39 const category = CATEGORY_MAP[activeTab];
40 const params = {
41 category,
42 sort_by: sortOption === '最新' ? 'newest' : sortOption === '最热' ? 'downloads' : undefined,
43 page: 1,
44 limit: 20,
223010095b28c672025-04-10 20:12:45 +080045 include_fields: 'seed_id,title,category,tags,size,upload_time,downloads,image_url',
22301009ecc1c1c2025-04-09 21:56:23 +080046 };
47
223010095b28c672025-04-10 20:12:45 +080048 if (searchTerm.trim()) params.search = searchTerm.trim();
22301009afbcf4b2025-04-10 16:08:39 +080049
50 const tags = Object.entries(selectedFilters)
51 .filter(([_, value]) => value !== '不限')
52 .map(([_, value]) => value);
53
54 if (tags.length > 0) {
55 params.tags = tags.join(',');
56 params.tag_mode = tagMode;
57 }
58
59 return params;
60 };
61
62 const fetchSeeds = async () => {
63 setLoading(true);
64 setErrorMsg('');
65 try {
66 const params = buildQueryParams();
67 const queryString = new URLSearchParams(params).toString();
68 const response = await fetch(`${API_BASE}/echo/seeds?${queryString}`);
69 const data = await response.json();
70
71 if (data.status !== 'success') throw new Error(data.message || '获取失败');
72 const seeds = data.seeds || [];
73 setSeeds(seeds);
74 setFilteredSeeds(seeds);
75 } catch (error) {
76 console.error('获取种子列表失败:', error);
77 setErrorMsg(error.message || '获取失败,请稍后再试。');
78 setSeeds([]);
79 setFilteredSeeds([]);
80 } finally {
81 setLoading(false);
82 }
83 };
84
85 const fetchFilterOptions = async () => {
86 if (activeTab === '猜你喜欢') return;
87 const category = CATEGORY_MAP[activeTab];
88 try {
89 const res = await axios.get(`${API_BASE}/echo/seed-filters?category=${category}`);
223010095b28c672025-04-10 20:12:45 +080090 const filterData = res.data || {};
91 setFilters(filterData);
92
22301009afbcf4b2025-04-10 16:08:39 +080093 const defaultSelections = {};
223010095b28c672025-04-10 20:12:45 +080094 for (const key in filterData) {
22301009afbcf4b2025-04-10 16:08:39 +080095 defaultSelections[key] = '不限';
96 }
97 setSelectedFilters(defaultSelections);
98 } catch (err) {
99 console.error('获取筛选项失败:', err);
100 setFilters({});
101 setSelectedFilters({});
102 }
103 };
22301009ecc1c1c2025-04-09 21:56:23 +0800104
105 useEffect(() => {
22301009afbcf4b2025-04-10 16:08:39 +0800106 if (activeTab !== '猜你喜欢') {
107 fetchFilterOptions();
22301009ecc1c1c2025-04-09 21:56:23 +0800108 }
22301009afbcf4b2025-04-10 16:08:39 +0800109 }, [activeTab]);
22301009ecc1c1c2025-04-09 21:56:23 +0800110
22301009afbcf4b2025-04-10 16:08:39 +0800111 useEffect(() => {
112 if (activeTab !== '猜你喜欢') {
113 fetchSeeds();
22301009ecc1c1c2025-04-09 21:56:23 +0800114 }
22301009afbcf4b2025-04-10 16:08:39 +0800115 }, [activeTab, sortOption, selectedFilters, tagMode, searchTerm]);
22301009ecc1c1c2025-04-09 21:56:23 +0800116
22301009afbcf4b2025-04-10 16:08:39 +0800117 const handleDownload = async (seedId) => {
118 const peer_id = 'echo-' + Math.random().toString(36).substring(2, 10);
119 const ip = '127.0.0.1';
120 const port = 6881;
121 const uploaded = 0;
122 const downloaded = 0;
123 const left = 0;
124
125 try {
126 const response = await axios.get(`${API_BASE}/echo/seeds/${seedId}/download`, {
127 params: { peer_id, ip, port, uploaded, downloaded, left },
128 responseType: 'blob'
129 });
130
131 const blob = new Blob([response.data], { type: 'application/x-bittorrent' });
132 const downloadUrl = URL.createObjectURL(blob);
133 const a = document.createElement('a');
134 a.href = downloadUrl;
135 a.download = `${seedId}.torrent`;
136 a.click();
137 URL.revokeObjectURL(downloadUrl);
138 } catch (error) {
139 console.error('下载失败:', error);
140 alert('下载失败,请稍后再试。');
22301009ecc1c1c2025-04-09 21:56:23 +0800141 }
22301009afbcf4b2025-04-10 16:08:39 +0800142 };
22301009ecc1c1c2025-04-09 21:56:23 +0800143
223010095b28c672025-04-10 20:12:45 +0800144 const handleFilterChange = (key, value) => {
145 setSelectedFilters((prev) => ({
146 ...prev,
147 [key]: value
148 }));
149 };
150
151 const clearFilter = (key) => {
152 setSelectedFilters((prev) => ({
153 ...prev,
154 [key]: '不限'
155 }));
156 };
157
22301009ecc1c1c2025-04-09 21:56:23 +0800158 return (
159 <div className="main-page">
160 <header className="header">
161 <div className="logo-and-name">
162 <img src={logo} alt="网站logo" className="logo" />
163 <span className="site-name">Echo</span>
164 </div>
165 <div className="user-and-message">
166 <img src="user-avatar.png" alt="用户头像" className="user-avatar" />
167 <span className="message-center">消息</span>
168 </div>
169 </header>
170
171 <nav className="nav">
172 <Link to="/friend-moments" className="nav-item">好友动态</Link>
173 <Link to="/forum" className="nav-item">论坛</Link>
174 <Link to="/interest-groups" className="nav-item">兴趣小组</Link>
175 <Link to="/seed-list" className="nav-item active">种子列表</Link>
176 <Link to="/publish-seed" className="nav-item">发布种子</Link>
177 </nav>
178
22301009ecc1c1c2025-04-09 21:56:23 +0800179 <div className="controls">
180 <input
181 type="text"
182 placeholder="搜索种子..."
183 value={searchTerm}
184 onChange={(e) => setSearchTerm(e.target.value)}
185 className="search-input"
186 />
187 <select value={sortOption} onChange={(e) => setSortOption(e.target.value)} className="sort-select">
188 <option value="最新">最新</option>
189 <option value="最热">最热</option>
22301009afbcf4b2025-04-10 16:08:39 +0800190 </select>
191 <select value={tagMode} onChange={(e) => setTagMode(e.target.value)} className="tag-mode-select">
192 <option value="any">包含任意标签</option>
193 <option value="all">包含所有标签</option>
22301009ecc1c1c2025-04-09 21:56:23 +0800194 </select>
195 </div>
196
197 <div className="tag-filters">
198 {TAGS.map((tag) => (
199 <button
200 key={tag}
22301009afbcf4b2025-04-10 16:08:39 +0800201 className={`tag-button ${activeTab === tag ? 'active-tag' : ''}`}
22301009ecc1c1c2025-04-09 21:56:23 +0800202 onClick={() => {
22301009afbcf4b2025-04-10 16:08:39 +0800203 setActiveTab(tag);
204 setFilters({});
205 setSelectedFilters({});
22301009ecc1c1c2025-04-09 21:56:23 +0800206 }}
207 >
208 {tag}
209 </button>
210 ))}
211 </div>
212
22301009afbcf4b2025-04-10 16:08:39 +0800213 {activeTab !== '猜你喜欢' && Object.keys(filters).length > 0 && (
214 <div className="filter-bar">
215 {Object.entries(filters).map(([key, options]) => (
216 <div className="filter-group" key={key}>
217 <label>{key}:</label>
218 <select
219 value={selectedFilters[key]}
223010095b28c672025-04-10 20:12:45 +0800220 onChange={(e) => handleFilterChange(key, e.target.value)}
22301009afbcf4b2025-04-10 16:08:39 +0800221 >
222 {options.map((opt) => (
223 <option key={opt} value={opt}>{opt}</option>
224 ))}
225 </select>
223010095b28c672025-04-10 20:12:45 +0800226 {selectedFilters[key] !== '不限' && (
227 <button className="clear-filter-btn" onClick={() => clearFilter(key)}>✕</button>
228 )}
22301009afbcf4b2025-04-10 16:08:39 +0800229 </div>
230 ))}
231 </div>
232 )}
233
22301009ecc1c1c2025-04-09 21:56:23 +0800234 <div className="seed-list-content">
235 {activeTab === '猜你喜欢' ? (
22301009afbcf4b2025-04-10 16:08:39 +0800236 <Recommend />
22301009ecc1c1c2025-04-09 21:56:23 +0800237 ) : loading ? (
238 <p>加载中...</p>
22301009afbcf4b2025-04-10 16:08:39 +0800239 ) : errorMsg ? (
240 <p className="error-text">{errorMsg}</p>
22301009ecc1c1c2025-04-09 21:56:23 +0800241 ) : filteredSeeds.length === 0 ? (
242 <p>未找到符合条件的种子。</p>
243 ) : (
244 <div className="seed-cards">
245 {filteredSeeds.map((seed, index) => (
246 <div key={index} className="seed-card">
223010095b28c672025-04-10 20:12:45 +0800247 {seed.image_url && (
248 <img src={seed.image_url} alt={seed.title} className="seed-cover" />
249 )}
22301009ecc1c1c2025-04-09 21:56:23 +0800250 <div className="seed-card-header">
22301009afbcf4b2025-04-10 16:08:39 +0800251 <h3>{seed.title}</h3>
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-body">
223010095b28c672025-04-10 20:12:45 +0800255 <div className="seed-info">
256 <span>{seed.size || '未知'} GB</span>
257 <span>{seed.upload_time?.split('T')[0] || '未知'}</span>
258 <span>{seed.downloads ?? 0} 次下载</span>
259 </div>
260 {seed.tags && seed.tags.length > 0 && (
261 <div className="seed-card-tags">
262 {seed.tags.map((tag, i) => (
263 <span key={i} className="tag-label">{tag}</span>
264 ))}
265 </div>
266 )}
22301009ecc1c1c2025-04-09 21:56:23 +0800267 </div>
223010095b28c672025-04-10 20:12:45 +0800268
22301009ecc1c1c2025-04-09 21:56:23 +0800269 <div className="seed-card-actions">
22301009afbcf4b2025-04-10 16:08:39 +0800270 <button className="btn-primary" onClick={() => handleDownload(seed.seed_id)}>下载</button>
271 <Link href={`/seed/${seed.seed_id}`} className="btn-secondary">详情</Link>
22301009ecc1c1c2025-04-09 21:56:23 +0800272 <button className="btn-outline">收藏</button>
273 </div>
274 </div>
275 ))}
276 </div>
277 )}
278 </div>
279 </div>
280 );
281};
282
283export default SeedList;