blob: be8d13fe9de91dfc5ef7c43166a911eb4930d122 [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';
22301009afbcf4b2025-04-10 16:08:39 +08004import Recommend from './Recommend/Recommend';
22301009f9641c52025-04-15 21:14:56 +08005import Header from '../../components/Header'; // 引入 Header 组件
22301009ecc1c1c2025-04-09 21:56:23 +08006import './SeedList.css';
2230100980aaf0d2025-06-05 23:20:05 +08007import { useUser } from '../../context/UserContext';
22301009afbcf4b2025-04-10 16:08:39 +08008
22301009ecc1c1c2025-04-09 21:56:23 +08009const SeedList = () => {
10 const [seeds, setSeeds] = useState([]);
22301009ecc1c1c2025-04-09 21:56:23 +080011 const [loading, setLoading] = useState(true);
12 const [searchTerm, setSearchTerm] = useState('');
22301009ecc1c1c2025-04-09 21:56:23 +080013 const [sortOption, setSortOption] = useState('最新');
22301009afbcf4b2025-04-10 16:08:39 +080014 const [activeTab, setActiveTab] = useState('种子列表');
15 const [filters, setFilters] = useState({});
16 const [selectedFilters, setSelectedFilters] = useState({});
2230100980aaf0d2025-06-05 23:20:05 +080017 const [tagMode, setTagMode] = useState('any'); // 与接口对应,any / all
22301009afbcf4b2025-04-10 16:08:39 +080018 const [errorMsg, setErrorMsg] = useState('');
2230100980aaf0d2025-06-05 23:20:05 +080019 const { user } = useUser();
20
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 = {
2230100980aaf0d2025-06-05 23:20:05 +080025 '电影': '电影',
26 '电视剧': '电视剧',
27 '动漫': '动漫',
28 '音乐': '音乐',
29 '游戏': '游戏',
30 '综艺': '综艺',
31 '软件': '软件',
32 '体育': '体育',
33 '学习': '学习',
34 '纪录片': '纪录片',
35 '其他': '其他',
36 '猜你喜欢': '',
37 '种子列表': '',
22301009afbcf4b2025-04-10 16:08:39 +080038 };
39
40 const buildQueryParams = () => {
2230100980aaf0d2025-06-05 23:20:05 +080041 const category = CATEGORY_MAP[activeTab] || '';
42 const orderKey = sortOption === '最新' ? ['upload_time'] : (sortOption === '最热' ? ['downloads'] : ['upload_time']);
22301009afbcf4b2025-04-10 16:08:39 +080043 const params = {
22301009afbcf4b2025-04-10 16:08:39 +080044 page: 1,
2230100980aaf0d2025-06-05 23:20:05 +080045 size: 20,
46 orderKey,
47 orderDesc: true,
22301009ecc1c1c2025-04-09 21:56:23 +080048 };
49
2230100980aaf0d2025-06-05 23:20:05 +080050 if (searchTerm.trim()) {
51 params.title = searchTerm.trim();
52 }
53 if (category) {
54 params.category = category;
55 }
22301009afbcf4b2025-04-10 16:08:39 +080056
57 const tags = Object.entries(selectedFilters)
2230100980aaf0d2025-06-05 23:20:05 +080058 .filter(([_, value]) => value !== '不限')
59 .map(([_, value]) => value);
22301009afbcf4b2025-04-10 16:08:39 +080060
61 if (tags.length > 0) {
2230100980aaf0d2025-06-05 23:20:05 +080062 params.tags = tags;
63 params.tagMode = tagMode; // any 或 all
22301009afbcf4b2025-04-10 16:08:39 +080064 }
65
66 return params;
67 };
68
69 const fetchSeeds = async () => {
2230100980aaf0d2025-06-05 23:20:05 +080070 if (activeTab === '猜你喜欢') return;
22301009afbcf4b2025-04-10 16:08:39 +080071 setLoading(true);
72 setErrorMsg('');
73 try {
74 const params = buildQueryParams();
2230100980aaf0d2025-06-05 23:20:05 +080075 const response = await axios.post('/seeds/list', params);
76 const data = response.data;
22301009afbcf4b2025-04-10 16:08:39 +080077
2230100980aaf0d2025-06-05 23:20:05 +080078 if (data.code !== 0) {
79 throw new Error(data.msg || '获取失败');
80 }
81
82 setSeeds(data.data || []);
22301009afbcf4b2025-04-10 16:08:39 +080083 } catch (error) {
84 console.error('获取种子列表失败:', error);
85 setErrorMsg(error.message || '获取失败,请稍后再试。');
86 setSeeds([]);
22301009afbcf4b2025-04-10 16:08:39 +080087 } finally {
88 setLoading(false);
89 }
90 };
91
92 const fetchFilterOptions = async () => {
2230100980aaf0d2025-06-05 23:20:05 +080093 if (activeTab === '猜你喜欢' || !CATEGORY_MAP[activeTab]) return;
22301009afbcf4b2025-04-10 16:08:39 +080094 const category = CATEGORY_MAP[activeTab];
95 try {
2230100980aaf0d2025-06-05 23:20:05 +080096 const res = await axios.get(`/seed-filters?category=${category}`);
223010095b28c672025-04-10 20:12:45 +080097 const filterData = res.data || {};
98 setFilters(filterData);
99
22301009afbcf4b2025-04-10 16:08:39 +0800100 const defaultSelections = {};
223010095b28c672025-04-10 20:12:45 +0800101 for (const key in filterData) {
22301009afbcf4b2025-04-10 16:08:39 +0800102 defaultSelections[key] = '不限';
103 }
104 setSelectedFilters(defaultSelections);
105 } catch (err) {
106 console.error('获取筛选项失败:', err);
107 setFilters({});
108 setSelectedFilters({});
109 }
110 };
22301009ecc1c1c2025-04-09 21:56:23 +0800111
112 useEffect(() => {
2230100980aaf0d2025-06-05 23:20:05 +0800113 fetchFilterOptions();
22301009afbcf4b2025-04-10 16:08:39 +0800114 }, [activeTab]);
22301009ecc1c1c2025-04-09 21:56:23 +0800115
22301009afbcf4b2025-04-10 16:08:39 +0800116 useEffect(() => {
2230100980aaf0d2025-06-05 23:20:05 +0800117 fetchSeeds();
22301009afbcf4b2025-04-10 16:08:39 +0800118 }, [activeTab, sortOption, selectedFilters, tagMode, searchTerm]);
22301009ecc1c1c2025-04-09 21:56:23 +0800119
2230100980aaf0d2025-06-05 23:20:05 +0800120 // ✅ 修改后的下载函数
22301009afbcf4b2025-04-10 16:08:39 +0800121 const handleDownload = async (seedId) => {
2230100980aaf0d2025-06-05 23:20:05 +0800122 if (!user || !user.userId) {
123 alert('请先登录再下载种子文件');
124 return;
125 }
22301009afbcf4b2025-04-10 16:08:39 +0800126
127 try {
2230100980aaf0d2025-06-05 23:20:05 +0800128 const response = await axios.get(`/seeds/${seedId}/download`, {
129 params: {
130 passkey: user.userId,
131 },
22301009afbcf4b2025-04-10 16:08:39 +0800132 responseType: 'blob'
133 });
134
135 const blob = new Blob([response.data], { type: 'application/x-bittorrent' });
136 const downloadUrl = URL.createObjectURL(blob);
137 const a = document.createElement('a');
138 a.href = downloadUrl;
139 a.download = `${seedId}.torrent`;
140 a.click();
141 URL.revokeObjectURL(downloadUrl);
142 } catch (error) {
143 console.error('下载失败:', error);
144 alert('下载失败,请稍后再试。');
22301009ecc1c1c2025-04-09 21:56:23 +0800145 }
22301009afbcf4b2025-04-10 16:08:39 +0800146 };
22301009ecc1c1c2025-04-09 21:56:23 +0800147
223010095b28c672025-04-10 20:12:45 +0800148 const handleFilterChange = (key, value) => {
2230100980aaf0d2025-06-05 23:20:05 +0800149 setSelectedFilters(prev => ({
150 ...prev,
223010095b28c672025-04-10 20:12:45 +0800151 [key]: value
152 }));
153 };
154
155 const clearFilter = (key) => {
2230100980aaf0d2025-06-05 23:20:05 +0800156 setSelectedFilters(prev => ({
157 ...prev,
223010095b28c672025-04-10 20:12:45 +0800158 [key]: '不限'
159 }));
160 };
161
22301009ecc1c1c2025-04-09 21:56:23 +0800162 return (
Krishyaf1d0ea82025-05-03 17:01:58 +0800163 <div className="seed-list-container">
2230100980aaf0d2025-06-05 23:20:05 +0800164 <Header />
22301009ecc1c1c2025-04-09 21:56:23 +0800165
22301009ecc1c1c2025-04-09 21:56:23 +0800166 <div className="controls">
167 <input
168 type="text"
169 placeholder="搜索种子..."
170 value={searchTerm}
171 onChange={(e) => setSearchTerm(e.target.value)}
172 className="search-input"
173 />
2230100980aaf0d2025-06-05 23:20:05 +0800174 <select
175 value={sortOption}
176 onChange={(e) => setSortOption(e.target.value)}
177 className="sort-select"
178 >
22301009ecc1c1c2025-04-09 21:56:23 +0800179 <option value="最新">最新</option>
180 <option value="最热">最热</option>
22301009afbcf4b2025-04-10 16:08:39 +0800181 </select>
2230100980aaf0d2025-06-05 23:20:05 +0800182 <select
183 value={tagMode}
184 onChange={(e) => setTagMode(e.target.value)}
185 className="tag-mode-select"
186 >
22301009afbcf4b2025-04-10 16:08:39 +0800187 <option value="any">包含任意标签</option>
188 <option value="all">包含所有标签</option>
22301009ecc1c1c2025-04-09 21:56:23 +0800189 </select>
190 </div>
191
192 <div className="tag-filters">
2230100980aaf0d2025-06-05 23:20:05 +0800193 {TAGS.map(tag => (
22301009ecc1c1c2025-04-09 21:56:23 +0800194 <button
195 key={tag}
2230100980aaf0d2025-06-05 23:20:05 +0800196 className={`tag-button ${activeTab === tag ? 'active-tag' : ''}`}
22301009ecc1c1c2025-04-09 21:56:23 +0800197 onClick={() => {
22301009afbcf4b2025-04-10 16:08:39 +0800198 setActiveTab(tag);
199 setFilters({});
200 setSelectedFilters({});
22301009ecc1c1c2025-04-09 21:56:23 +0800201 }}
202 >
203 {tag}
204 </button>
205 ))}
206 </div>
207
2230100980aaf0d2025-06-05 23:20:05 +0800208 {activeTab !== '猜你喜欢' && Object.keys(filters).length > 0 && (
22301009afbcf4b2025-04-10 16:08:39 +0800209 <div className="filter-bar">
210 {Object.entries(filters).map(([key, options]) => (
211 <div className="filter-group" key={key}>
212 <label>{key}:</label>
213 <select
214 value={selectedFilters[key]}
223010095b28c672025-04-10 20:12:45 +0800215 onChange={(e) => handleFilterChange(key, e.target.value)}
22301009afbcf4b2025-04-10 16:08:39 +0800216 >
2230100980aaf0d2025-06-05 23:20:05 +0800217 {options.map(opt => (
22301009afbcf4b2025-04-10 16:08:39 +0800218 <option key={opt} value={opt}>{opt}</option>
219 ))}
220 </select>
2230100980aaf0d2025-06-05 23:20:05 +0800221 {selectedFilters[key] !== '不限' && (
222 <button
223 className="clear-filter-btn"
224 onClick={() => clearFilter(key)}
225 >
226
227 </button>
223010095b28c672025-04-10 20:12:45 +0800228 )}
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">
2230100980aaf0d2025-06-05 23:20:05 +0800235 {activeTab === '猜你喜欢' ? (
22301009afbcf4b2025-04-10 16:08:39 +0800236 <Recommend />
2230100980aaf0d2025-06-05 23:20:05 +0800237 ) : loading ? (
22301009ecc1c1c2025-04-09 21:56:23 +0800238 <p>加载中...</p>
2230100980aaf0d2025-06-05 23:20:05 +0800239 ) : errorMsg ? (
22301009afbcf4b2025-04-10 16:08:39 +0800240 <p className="error-text">{errorMsg}</p>
2230100980aaf0d2025-06-05 23:20:05 +0800241 ) : seeds.length === 0 ? (
22301009ecc1c1c2025-04-09 21:56:23 +0800242 <p>未找到符合条件的种子。</p>
243 ) : (
Krishya25590de2025-04-21 19:03:49 +0800244 <div className="seed-list-card">
245 <div className="seed-list-header">
246 <div className="seed-header-cover"></div>
247 <div className="seed-header-title">种子名称</div>
248 <div className="seed-header-size">大小</div>
249 <div className="seed-header-upload-time">上传时间</div>
250 <div className="seed-header-downloads">下载次数</div>
251 <div className="seed-header-actions">操作</div>
252 </div>
253 <div className="seed-list-body">
2230100980aaf0d2025-06-05 23:20:05 +0800254 {seeds.map((seed, index) => {
255 // 处理 tags 字段,兼容字符串和数组
256 let tagsArray = [];
257 if (seed.tags) {
258 if (Array.isArray(seed.tags)) {
259 tagsArray = seed.tags;
260 } else if (typeof seed.tags === 'string') {
261 try {
262 tagsArray = JSON.parse(seed.tags);
263 if (!Array.isArray(tagsArray)) {
264 // 解析后不是数组,按逗号分割
265 tagsArray = seed.tags.split(',').map(t => t.trim()).filter(t => t);
266 }
267 } catch {
268 // 解析失败,按逗号分割
269 tagsArray = seed.tags.split(',').map(t => t.trim()).filter(t => t);
270 }
271 }
272 }
273
274 return (
275
276 <Link to={`/seed/${seed.id}`} key={index} className="seed-item-link">
277 <div className="seed-item">
278 {seed.image_url && (
279 <img
280 src={seed.image_url}
281 alt={seed.title}
282 className="seed-item-cover"
283 />
284 )}
285 <div className="seed-item-title">
286 <div className="seed-title-row">
287 <h3 className="seed-title">{seed.title}</h3>
288 <div className="seed-tags">
289 {tagsArray.map((tag, i) => (
290 <span key={i} className="tag-label">{tag}</span>
291 ))}
292 </div>
Krishya25590de2025-04-21 19:03:49 +0800293 </div>
294 </div>
2230100980aaf0d2025-06-05 23:20:05 +0800295 <div className="seed-item-size">{seed.size || '未知'}</div>
296 <div className="seed-item-upload-time">{seed.upload_time?.split('T')[0] || '未知'}</div>
297 <div className="seed-item-downloads">{seed.downloads ?? 0} 次下载</div>
298 <div
299 className="seed-item-actions"
300 onClick={e => e.stopPropagation()} // 阻止事件冒泡,避免跳转
Krishya25590de2025-04-21 19:03:49 +0800301 >
2230100980aaf0d2025-06-05 23:20:05 +0800302 <button
303 className="btn-primary"
304 onClick={e => {
305 e.preventDefault();
306 e.stopPropagation();
307 handleDownload(seed.id);
308 }}
309 >
310 下载
311 </button>
312 <button
313 className="btn-outline"
314 onClick={async (e) => {
315 e.preventDefault();
316 e.stopPropagation();
317
318 if (!user || !user.userId) {
319 alert('请先登录再收藏');
320 return;
321 }
322
323 try {
324 const res = await axios.post(`/seeds/${seed.id}/favorite-toggle`, null, {
325 params: { user_id: user.userId },
326 });
327
328 if (res.data.code === 0) {
329 alert('操作成功'); // 你可以改成 toast 或 icon 状态提示
330 } else {
331 alert(res.data.msg || '操作失败');
332 }
333 } catch (err) {
334 console.error('收藏失败:', err);
335 alert('收藏失败,请稍后再试。');
336 }
337 }}
338>
339 收藏
340</button>
341
342 </div>
Krishya25590de2025-04-21 19:03:49 +0800343 </div>
2230100980aaf0d2025-06-05 23:20:05 +0800344 </Link>
345 );
346 })}
Krishya25590de2025-04-21 19:03:49 +0800347 </div>
22301009ecc1c1c2025-04-09 21:56:23 +0800348 </div>
349 )}
350 </div>
351 </div>
352 );
353};
354
355export default SeedList;