blob: a74d0085c727fe69846b13a12e039709a6cc4f73 [file] [log] [blame]
2230100901d3ff92025-06-07 16:16:26 +08001// export default SeedList;
22301009ecc1c1c2025-04-09 21:56:23 +08002import React, { useState, useEffect } from 'react';
22301009ecc1c1c2025-04-09 21:56:23 +08003import { Link } from 'wouter';
22301009afbcf4b2025-04-10 16:08:39 +08004import axios from 'axios';
22301009afbcf4b2025-04-10 16:08:39 +08005import Recommend from './Recommend/Recommend';
22301009f9641c52025-04-15 21:14:56 +08006import Header from '../../components/Header'; // 引入 Header 组件
22301009ecc1c1c2025-04-09 21:56:23 +08007import './SeedList.css';
2230100980aaf0d2025-06-05 23:20:05 +08008import { useUser } from '../../context/UserContext';
22301009afbcf4b2025-04-10 16:08:39 +08009
22301009ecc1c1c2025-04-09 21:56:23 +080010const SeedList = () => {
11 const [seeds, setSeeds] = useState([]);
22301009ecc1c1c2025-04-09 21:56:23 +080012 const [loading, setLoading] = useState(true);
13 const [searchTerm, setSearchTerm] = useState('');
22301009ecc1c1c2025-04-09 21:56:23 +080014 const [sortOption, setSortOption] = useState('最新');
22301009afbcf4b2025-04-10 16:08:39 +080015 const [activeTab, setActiveTab] = useState('种子列表');
16 const [filters, setFilters] = useState({});
17 const [selectedFilters, setSelectedFilters] = useState({});
2230100980aaf0d2025-06-05 23:20:05 +080018 const [tagMode, setTagMode] = useState('any'); // 与接口对应,any / all
22301009afbcf4b2025-04-10 16:08:39 +080019 const [errorMsg, setErrorMsg] = useState('');
2230100980aaf0d2025-06-05 23:20:05 +080020 const { user } = useUser();
21
22301009afbcf4b2025-04-10 16:08:39 +080022 const TAGS = ['猜你喜欢', '电影', '电视剧', '动漫', '音乐', '游戏', '综艺', '软件', '体育', '学习', '纪录片', '其他'];
22301009ecc1c1c2025-04-09 21:56:23 +080023
2230100901d3ff92025-06-07 16:16:26 +080024const CATEGORY_MAP = {
25 '电影': 'movie',
26 '电视剧': 'tv',
27 '动漫': 'anime',
28 '音乐': 'music',
29 '游戏': 'game',
30 '综艺': 'variety',
31 '软件': 'software',
32 '体育': 'sports',
33 '学习': 'study',
34 '纪录片': 'documentary',
35 '其他': 'other',
2230100980aaf0d2025-06-05 23:20:05 +080036 '猜你喜欢': '',
37 '种子列表': '',
2230100901d3ff92025-06-07 16:16:26 +080038};
39
22301009afbcf4b2025-04-10 16:08:39 +080040
41 const buildQueryParams = () => {
2230100980aaf0d2025-06-05 23:20:05 +080042 const category = CATEGORY_MAP[activeTab] || '';
2230100901d3ff92025-06-07 16:16:26 +080043 const orderKey = sortOption === '最新' ? 'upload_time' : (sortOption === '最热' ? 'downloads' : 'upload_time');
22301009afbcf4b2025-04-10 16:08:39 +080044 const params = {
22301009afbcf4b2025-04-10 16:08:39 +080045 page: 1,
2230100980aaf0d2025-06-05 23:20:05 +080046 size: 20,
47 orderKey,
48 orderDesc: true,
22301009ecc1c1c2025-04-09 21:56:23 +080049 };
50
2230100980aaf0d2025-06-05 23:20:05 +080051 if (searchTerm.trim()) {
52 params.title = searchTerm.trim();
53 }
54 if (category) {
55 params.category = category;
56 }
22301009afbcf4b2025-04-10 16:08:39 +080057
58 const tags = Object.entries(selectedFilters)
2230100980aaf0d2025-06-05 23:20:05 +080059 .filter(([_, value]) => value !== '不限')
60 .map(([_, value]) => value);
22301009afbcf4b2025-04-10 16:08:39 +080061
62 if (tags.length > 0) {
2230100980aaf0d2025-06-05 23:20:05 +080063 params.tags = tags;
64 params.tagMode = tagMode; // any 或 all
22301009afbcf4b2025-04-10 16:08:39 +080065 }
66
67 return params;
68 };
69
70 const fetchSeeds = async () => {
2230100980aaf0d2025-06-05 23:20:05 +080071 if (activeTab === '猜你喜欢') return;
22301009afbcf4b2025-04-10 16:08:39 +080072 setLoading(true);
73 setErrorMsg('');
74 try {
75 const params = buildQueryParams();
2230100901d3ff92025-06-07 16:16:26 +080076 const response = await axios.get('/seeds/list', { params });
Krishya6bf199c2025-06-06 21:14:23 +080077
2230100980aaf0d2025-06-05 23:20:05 +080078 const data = response.data;
22301009afbcf4b2025-04-10 16:08:39 +080079
2230100980aaf0d2025-06-05 23:20:05 +080080 if (data.code !== 0) {
81 throw new Error(data.msg || '获取失败');
82 }
83
84 setSeeds(data.data || []);
22301009afbcf4b2025-04-10 16:08:39 +080085 } catch (error) {
86 console.error('获取种子列表失败:', error);
87 setErrorMsg(error.message || '获取失败,请稍后再试。');
88 setSeeds([]);
22301009afbcf4b2025-04-10 16:08:39 +080089 } finally {
90 setLoading(false);
91 }
92 };
93
94 const fetchFilterOptions = async () => {
2230100980aaf0d2025-06-05 23:20:05 +080095 if (activeTab === '猜你喜欢' || !CATEGORY_MAP[activeTab]) return;
22301009afbcf4b2025-04-10 16:08:39 +080096 const category = CATEGORY_MAP[activeTab];
97 try {
2230100980aaf0d2025-06-05 23:20:05 +080098 const res = await axios.get(`/seed-filters?category=${category}`);
223010095b28c672025-04-10 20:12:45 +080099 const filterData = res.data || {};
100 setFilters(filterData);
101
22301009afbcf4b2025-04-10 16:08:39 +0800102 const defaultSelections = {};
223010095b28c672025-04-10 20:12:45 +0800103 for (const key in filterData) {
22301009afbcf4b2025-04-10 16:08:39 +0800104 defaultSelections[key] = '不限';
105 }
106 setSelectedFilters(defaultSelections);
107 } catch (err) {
108 console.error('获取筛选项失败:', err);
109 setFilters({});
110 setSelectedFilters({});
111 }
112 };
22301009ecc1c1c2025-04-09 21:56:23 +0800113
114 useEffect(() => {
2230100980aaf0d2025-06-05 23:20:05 +0800115 fetchFilterOptions();
22301009afbcf4b2025-04-10 16:08:39 +0800116 }, [activeTab]);
22301009ecc1c1c2025-04-09 21:56:23 +0800117
22301009afbcf4b2025-04-10 16:08:39 +0800118 useEffect(() => {
2230100980aaf0d2025-06-05 23:20:05 +0800119 fetchSeeds();
22301009afbcf4b2025-04-10 16:08:39 +0800120 }, [activeTab, sortOption, selectedFilters, tagMode, searchTerm]);
22301009ecc1c1c2025-04-09 21:56:23 +0800121
22301009afbcf4b2025-04-10 16:08:39 +0800122 const handleDownload = async (seedId) => {
2230100980aaf0d2025-06-05 23:20:05 +0800123 if (!user || !user.userId) {
124 alert('请先登录再下载种子文件');
125 return;
126 }
22301009afbcf4b2025-04-10 16:08:39 +0800127
128 try {
2230100980aaf0d2025-06-05 23:20:05 +0800129 const response = await axios.get(`/seeds/${seedId}/download`, {
130 params: {
131 passkey: user.userId,
132 },
22301009afbcf4b2025-04-10 16:08:39 +0800133 responseType: 'blob'
134 });
135
136 const blob = new Blob([response.data], { type: 'application/x-bittorrent' });
137 const downloadUrl = URL.createObjectURL(blob);
138 const a = document.createElement('a');
139 a.href = downloadUrl;
140 a.download = `${seedId}.torrent`;
141 a.click();
142 URL.revokeObjectURL(downloadUrl);
143 } catch (error) {
144 console.error('下载失败:', error);
145 alert('下载失败,请稍后再试。');
22301009ecc1c1c2025-04-09 21:56:23 +0800146 }
22301009afbcf4b2025-04-10 16:08:39 +0800147 };
22301009ecc1c1c2025-04-09 21:56:23 +0800148
223010095b28c672025-04-10 20:12:45 +0800149 const handleFilterChange = (key, value) => {
2230100980aaf0d2025-06-05 23:20:05 +0800150 setSelectedFilters(prev => ({
151 ...prev,
223010095b28c672025-04-10 20:12:45 +0800152 [key]: value
153 }));
154 };
155
156 const clearFilter = (key) => {
2230100980aaf0d2025-06-05 23:20:05 +0800157 setSelectedFilters(prev => ({
158 ...prev,
223010095b28c672025-04-10 20:12:45 +0800159 [key]: '不限'
160 }));
161 };
162
22301009ecc1c1c2025-04-09 21:56:23 +0800163 return (
Krishyaf1d0ea82025-05-03 17:01:58 +0800164 <div className="seed-list-container">
2230100980aaf0d2025-06-05 23:20:05 +0800165 <Header />
22301009ecc1c1c2025-04-09 21:56:23 +0800166
22301009ecc1c1c2025-04-09 21:56:23 +0800167 <div className="controls">
168 <input
169 type="text"
170 placeholder="搜索种子..."
171 value={searchTerm}
172 onChange={(e) => setSearchTerm(e.target.value)}
173 className="search-input"
174 />
2230100980aaf0d2025-06-05 23:20:05 +0800175 <select
176 value={sortOption}
177 onChange={(e) => setSortOption(e.target.value)}
178 className="sort-select"
179 >
22301009ecc1c1c2025-04-09 21:56:23 +0800180 <option value="最新">最新</option>
181 <option value="最热">最热</option>
22301009afbcf4b2025-04-10 16:08:39 +0800182 </select>
2230100980aaf0d2025-06-05 23:20:05 +0800183 <select
184 value={tagMode}
185 onChange={(e) => setTagMode(e.target.value)}
186 className="tag-mode-select"
187 >
22301009afbcf4b2025-04-10 16:08:39 +0800188 <option value="any">包含任意标签</option>
189 <option value="all">包含所有标签</option>
22301009ecc1c1c2025-04-09 21:56:23 +0800190 </select>
191 </div>
192
193 <div className="tag-filters">
2230100980aaf0d2025-06-05 23:20:05 +0800194 {TAGS.map(tag => (
22301009ecc1c1c2025-04-09 21:56:23 +0800195 <button
196 key={tag}
2230100980aaf0d2025-06-05 23:20:05 +0800197 className={`tag-button ${activeTab === tag ? 'active-tag' : ''}`}
22301009ecc1c1c2025-04-09 21:56:23 +0800198 onClick={() => {
22301009afbcf4b2025-04-10 16:08:39 +0800199 setActiveTab(tag);
200 setFilters({});
201 setSelectedFilters({});
22301009ecc1c1c2025-04-09 21:56:23 +0800202 }}
203 >
204 {tag}
205 </button>
206 ))}
207 </div>
208
2230100980aaf0d2025-06-05 23:20:05 +0800209 {activeTab !== '猜你喜欢' && Object.keys(filters).length > 0 && (
22301009afbcf4b2025-04-10 16:08:39 +0800210 <div className="filter-bar">
211 {Object.entries(filters).map(([key, options]) => (
212 <div className="filter-group" key={key}>
213 <label>{key}:</label>
214 <select
215 value={selectedFilters[key]}
223010095b28c672025-04-10 20:12:45 +0800216 onChange={(e) => handleFilterChange(key, e.target.value)}
22301009afbcf4b2025-04-10 16:08:39 +0800217 >
2230100980aaf0d2025-06-05 23:20:05 +0800218 {options.map(opt => (
22301009afbcf4b2025-04-10 16:08:39 +0800219 <option key={opt} value={opt}>{opt}</option>
220 ))}
221 </select>
2230100980aaf0d2025-06-05 23:20:05 +0800222 {selectedFilters[key] !== '不限' && (
223 <button
224 className="clear-filter-btn"
225 onClick={() => clearFilter(key)}
226 >
227
228 </button>
223010095b28c672025-04-10 20:12:45 +0800229 )}
22301009afbcf4b2025-04-10 16:08:39 +0800230 </div>
231 ))}
232 </div>
233 )}
234
22301009ecc1c1c2025-04-09 21:56:23 +0800235 <div className="seed-list-content">
2230100980aaf0d2025-06-05 23:20:05 +0800236 {activeTab === '猜你喜欢' ? (
22301009afbcf4b2025-04-10 16:08:39 +0800237 <Recommend />
2230100980aaf0d2025-06-05 23:20:05 +0800238 ) : loading ? (
22301009ecc1c1c2025-04-09 21:56:23 +0800239 <p>加载中...</p>
2230100980aaf0d2025-06-05 23:20:05 +0800240 ) : errorMsg ? (
22301009afbcf4b2025-04-10 16:08:39 +0800241 <p className="error-text">{errorMsg}</p>
2230100980aaf0d2025-06-05 23:20:05 +0800242 ) : seeds.length === 0 ? (
22301009ecc1c1c2025-04-09 21:56:23 +0800243 <p>未找到符合条件的种子。</p>
244 ) : (
Krishya25590de2025-04-21 19:03:49 +0800245 <div className="seed-list-card">
246 <div className="seed-list-header">
247 <div className="seed-header-cover"></div>
248 <div className="seed-header-title">种子名称</div>
249 <div className="seed-header-size">大小</div>
250 <div className="seed-header-upload-time">上传时间</div>
251 <div className="seed-header-downloads">下载次数</div>
252 <div className="seed-header-actions">操作</div>
253 </div>
254 <div className="seed-list-body">
2230100980aaf0d2025-06-05 23:20:05 +0800255 {seeds.map((seed, index) => {
2230100980aaf0d2025-06-05 23:20:05 +0800256 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)) {
22301111a289e262025-06-07 22:38:46 +0800264 tagsArray = seed.tags.split(',').map(t => t.trim()).filter(t => t);
2230100980aaf0d2025-06-05 23:20:05 +0800265 }
266 } catch {
22301111a289e262025-06-07 22:38:46 +0800267 tagsArray = seed.tags.split(',').map(t => t.trim()).filter(t => t);
2230100980aaf0d2025-06-05 23:20:05 +0800268 }
269 }
270 }
271
272 return (
2230100980aaf0d2025-06-05 23:20:05 +0800273 <Link to={`/seed/${seed.id}`} key={index} className="seed-item-link">
274 <div className="seed-item">
223010094158f3a2025-06-06 19:59:10 +0800275 {seed.imageUrl && (
2230100980aaf0d2025-06-05 23:20:05 +0800276 <img
223010094158f3a2025-06-06 19:59:10 +0800277 src={seed.imageUrl}
2230100980aaf0d2025-06-05 23:20:05 +0800278 alt={seed.title}
279 className="seed-item-cover"
280 />
281 )}
223010094158f3a2025-06-06 19:59:10 +0800282
2230100980aaf0d2025-06-05 23:20:05 +0800283 <div className="seed-item-title">
284 <div className="seed-title-row">
285 <h3 className="seed-title">{seed.title}</h3>
286 <div className="seed-tags">
287 {tagsArray.map((tag, i) => (
288 <span key={i} className="tag-label">{tag}</span>
289 ))}
290 </div>
Krishya25590de2025-04-21 19:03:49 +0800291 </div>
292 </div>
2230100980aaf0d2025-06-05 23:20:05 +0800293 <div className="seed-item-size">{seed.size || '未知'}</div>
294 <div className="seed-item-upload-time">{seed.upload_time?.split('T')[0] || '未知'}</div>
295 <div className="seed-item-downloads">{seed.downloads ?? 0} 次下载</div>
296 <div
297 className="seed-item-actions"
2230100901d3ff92025-06-07 16:16:26 +0800298 onClick={e => e.stopPropagation()}
Krishya25590de2025-04-21 19:03:49 +0800299 >
2230100980aaf0d2025-06-05 23:20:05 +0800300 <button
301 className="btn-primary"
302 onClick={e => {
303 e.preventDefault();
304 e.stopPropagation();
305 handleDownload(seed.id);
306 }}
307 >
308 下载
309 </button>
310 <button
2230100901d3ff92025-06-07 16:16:26 +0800311 className="btn-outline"
312 onClick={async (e) => {
313 e.preventDefault();
314 e.stopPropagation();
2230100980aaf0d2025-06-05 23:20:05 +0800315
2230100901d3ff92025-06-07 16:16:26 +0800316 if (!user || !user.userId) {
317 alert('请先登录再收藏');
318 return;
319 }
2230100980aaf0d2025-06-05 23:20:05 +0800320
2230100901d3ff92025-06-07 16:16:26 +0800321 try {
322 const res = await axios.post(`/seeds/${seed.id}/favorite-toggle`, null, {
323 params: { user_id: user.userId },
324 });
2230100980aaf0d2025-06-05 23:20:05 +0800325
2230100901d3ff92025-06-07 16:16:26 +0800326 if (res.data.code === 0) {
327 alert('操作成功');
328 } else {
329 alert(res.data.msg || '操作失败');
330 }
331 } catch (err) {
332 console.error('收藏失败:', err);
333 alert('收藏失败,请稍后再试。');
334 }
335 }}
336 >
337 收藏
338 </button>
2230100980aaf0d2025-06-05 23:20:05 +0800339 </div>
Krishya25590de2025-04-21 19:03:49 +0800340 </div>
2230100980aaf0d2025-06-05 23:20:05 +0800341 </Link>
342 );
343 })}
Krishya25590de2025-04-21 19:03:49 +0800344 </div>
22301009ecc1c1c2025-04-09 21:56:23 +0800345 </div>
346 )}
347 </div>
348 </div>
349 );
350};
351
352export default SeedList;