blob: 45b9f8ca0c2d31fc2c0042f98dde3f8b01229ce1 [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';
223010091e2aea72025-06-08 16:35:54 +08005import Header from '../../components/Header';
22301009ecc1c1c2025-04-09 21:56:23 +08006import './SeedList.css';
2230100980aaf0d2025-06-05 23:20:05 +08007import { useUser } from '../../context/UserContext';
223010091e2aea72025-06-08 16:35:54 +08008import toast from 'react-hot-toast';
9import { confirmAlert } from 'react-confirm-alert';
10import 'react-confirm-alert/src/react-confirm-alert.css';
11import AuthButton from '../../components/AuthButton';
22301009afbcf4b2025-04-10 16:08:39 +080012
22301009ecc1c1c2025-04-09 21:56:23 +080013const SeedList = () => {
14 const [seeds, setSeeds] = useState([]);
22301009ecc1c1c2025-04-09 21:56:23 +080015 const [loading, setLoading] = useState(true);
16 const [searchTerm, setSearchTerm] = useState('');
22301009ecc1c1c2025-04-09 21:56:23 +080017 const [sortOption, setSortOption] = useState('最新');
22301009afbcf4b2025-04-10 16:08:39 +080018 const [activeTab, setActiveTab] = useState('种子列表');
19 const [filters, setFilters] = useState({});
20 const [selectedFilters, setSelectedFilters] = useState({});
223010091e2aea72025-06-08 16:35:54 +080021 const [tagMode, setTagMode] = useState('any');
22301009afbcf4b2025-04-10 16:08:39 +080022 const [errorMsg, setErrorMsg] = useState('');
2230100980aaf0d2025-06-05 23:20:05 +080023 const { user } = useUser();
24
22301009afbcf4b2025-04-10 16:08:39 +080025 const TAGS = ['猜你喜欢', '电影', '电视剧', '动漫', '音乐', '游戏', '综艺', '软件', '体育', '学习', '纪录片', '其他'];
22301009ecc1c1c2025-04-09 21:56:23 +080026
223010091e2aea72025-06-08 16:35:54 +080027 const CATEGORY_MAP = {
2230100901d3ff92025-06-07 16:16:26 +080028 '电影': 'movie',
29 '电视剧': 'tv',
30 '动漫': 'anime',
31 '音乐': 'music',
32 '游戏': 'game',
33 '综艺': 'variety',
34 '软件': 'software',
35 '体育': 'sports',
36 '学习': 'study',
37 '纪录片': 'documentary',
38 '其他': 'other',
223010091e2aea72025-06-08 16:35:54 +080039 '猜你喜欢': '',
40 '种子列表': '',
41 };
22301009afbcf4b2025-04-10 16:08:39 +080042
43 const buildQueryParams = () => {
2230100980aaf0d2025-06-05 23:20:05 +080044 const category = CATEGORY_MAP[activeTab] || '';
2230100901d3ff92025-06-07 16:16:26 +080045 const orderKey = sortOption === '最新' ? 'upload_time' : (sortOption === '最热' ? 'downloads' : 'upload_time');
22301009afbcf4b2025-04-10 16:08:39 +080046 const params = {
22301009afbcf4b2025-04-10 16:08:39 +080047 page: 1,
2230100980aaf0d2025-06-05 23:20:05 +080048 size: 20,
49 orderKey,
50 orderDesc: true,
22301009ecc1c1c2025-04-09 21:56:23 +080051 };
52
2230100980aaf0d2025-06-05 23:20:05 +080053 if (searchTerm.trim()) {
54 params.title = searchTerm.trim();
55 }
56 if (category) {
57 params.category = category;
58 }
22301009afbcf4b2025-04-10 16:08:39 +080059
60 const tags = Object.entries(selectedFilters)
2230100980aaf0d2025-06-05 23:20:05 +080061 .filter(([_, value]) => value !== '不限')
62 .map(([_, value]) => value);
22301009afbcf4b2025-04-10 16:08:39 +080063
64 if (tags.length > 0) {
2230100980aaf0d2025-06-05 23:20:05 +080065 params.tags = tags;
223010091e2aea72025-06-08 16:35:54 +080066 params.tagMode = tagMode;
22301009afbcf4b2025-04-10 16:08:39 +080067 }
68
69 return params;
70 };
71
72 const fetchSeeds = async () => {
223010091e2aea72025-06-08 16:35:54 +080073 if (activeTab === '猜你喜欢') return;
22301009afbcf4b2025-04-10 16:08:39 +080074 setLoading(true);
75 setErrorMsg('');
76 try {
77 const params = buildQueryParams();
2230100901d3ff92025-06-07 16:16:26 +080078 const response = await axios.get('/seeds/list', { params });
2230100980aaf0d2025-06-05 23:20:05 +080079 const data = response.data;
22301009afbcf4b2025-04-10 16:08:39 +080080
223010091e2aea72025-06-08 16:35:54 +080081 if (data.code !== 0) throw new Error(data.msg || '获取失败');
2230100980aaf0d2025-06-05 23:20:05 +080082 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
22301009afbcf4b2025-04-10 16:08:39 +0800120 const handleDownload = async (seedId) => {
22301009afbcf4b2025-04-10 16:08:39 +0800121 try {
2230100980aaf0d2025-06-05 23:20:05 +0800122 const response = await axios.get(`/seeds/${seedId}/download`, {
223010091e2aea72025-06-08 16:35:54 +0800123 params: { passkey: user.userId },
22301009afbcf4b2025-04-10 16:08:39 +0800124 responseType: 'blob'
125 });
126
127 const blob = new Blob([response.data], { type: 'application/x-bittorrent' });
128 const downloadUrl = URL.createObjectURL(blob);
129 const a = document.createElement('a');
130 a.href = downloadUrl;
131 a.download = `${seedId}.torrent`;
132 a.click();
133 URL.revokeObjectURL(downloadUrl);
134 } catch (error) {
135 console.error('下载失败:', error);
223010091e2aea72025-06-08 16:35:54 +0800136 toast.error('下载失败,请稍后再试。');
22301009ecc1c1c2025-04-09 21:56:23 +0800137 }
22301009afbcf4b2025-04-10 16:08:39 +0800138 };
22301009ecc1c1c2025-04-09 21:56:23 +0800139
223010095b28c672025-04-10 20:12:45 +0800140 const handleFilterChange = (key, value) => {
223010091e2aea72025-06-08 16:35:54 +0800141 setSelectedFilters(prev => ({ ...prev, [key]: value }));
223010095b28c672025-04-10 20:12:45 +0800142 };
143
144 const clearFilter = (key) => {
223010091e2aea72025-06-08 16:35:54 +0800145 setSelectedFilters(prev => ({ ...prev, [key]: '不限' }));
223010095b28c672025-04-10 20:12:45 +0800146 };
147
22301009ecc1c1c2025-04-09 21:56:23 +0800148 return (
Krishyaf1d0ea82025-05-03 17:01:58 +0800149 <div className="seed-list-container">
2230100980aaf0d2025-06-05 23:20:05 +0800150 <Header />
22301009ecc1c1c2025-04-09 21:56:23 +0800151
22301009ecc1c1c2025-04-09 21:56:23 +0800152 <div className="controls">
153 <input
154 type="text"
155 placeholder="搜索种子..."
156 value={searchTerm}
157 onChange={(e) => setSearchTerm(e.target.value)}
158 className="search-input"
159 />
223010091e2aea72025-06-08 16:35:54 +0800160 <select value={sortOption} onChange={(e) => setSortOption(e.target.value)} className="sort-select">
22301009ecc1c1c2025-04-09 21:56:23 +0800161 <option value="最新">最新</option>
162 <option value="最热">最热</option>
22301009afbcf4b2025-04-10 16:08:39 +0800163 </select>
223010091e2aea72025-06-08 16:35:54 +0800164 <select value={tagMode} onChange={(e) => setTagMode(e.target.value)} className="tag-mode-select">
22301009afbcf4b2025-04-10 16:08:39 +0800165 <option value="any">包含任意标签</option>
166 <option value="all">包含所有标签</option>
22301009ecc1c1c2025-04-09 21:56:23 +0800167 </select>
168 </div>
169
170 <div className="tag-filters">
2230100980aaf0d2025-06-05 23:20:05 +0800171 {TAGS.map(tag => (
Krishyadbfadaa2025-06-09 20:33:15 +0800172 <button
173 // roles={["test"]}
22301009ecc1c1c2025-04-09 21:56:23 +0800174 key={tag}
2230100980aaf0d2025-06-05 23:20:05 +0800175 className={`tag-button ${activeTab === tag ? 'active-tag' : ''}`}
22301009ecc1c1c2025-04-09 21:56:23 +0800176 onClick={() => {
22301009afbcf4b2025-04-10 16:08:39 +0800177 setActiveTab(tag);
178 setFilters({});
179 setSelectedFilters({});
22301009ecc1c1c2025-04-09 21:56:23 +0800180 }}
181 >
182 {tag}
Krishyadbfadaa2025-06-09 20:33:15 +0800183 </button>
22301009ecc1c1c2025-04-09 21:56:23 +0800184 ))}
185 </div>
186
2230100980aaf0d2025-06-05 23:20:05 +0800187 {activeTab !== '猜你喜欢' && Object.keys(filters).length > 0 && (
22301009afbcf4b2025-04-10 16:08:39 +0800188 <div className="filter-bar">
189 {Object.entries(filters).map(([key, options]) => (
190 <div className="filter-group" key={key}>
191 <label>{key}:</label>
223010091e2aea72025-06-08 16:35:54 +0800192 <select value={selectedFilters[key]} onChange={(e) => handleFilterChange(key, e.target.value)}>
2230100980aaf0d2025-06-05 23:20:05 +0800193 {options.map(opt => (
22301009afbcf4b2025-04-10 16:08:39 +0800194 <option key={opt} value={opt}>{opt}</option>
195 ))}
196 </select>
2230100980aaf0d2025-06-05 23:20:05 +0800197 {selectedFilters[key] !== '不限' && (
223010091e2aea72025-06-08 16:35:54 +0800198 <button className="clear-filter-btn" onClick={() => clearFilter(key)}>✕</button>
223010095b28c672025-04-10 20:12:45 +0800199 )}
22301009afbcf4b2025-04-10 16:08:39 +0800200 </div>
201 ))}
202 </div>
203 )}
204
22301009ecc1c1c2025-04-09 21:56:23 +0800205 <div className="seed-list-content">
2230100980aaf0d2025-06-05 23:20:05 +0800206 {activeTab === '猜你喜欢' ? (
22301009afbcf4b2025-04-10 16:08:39 +0800207 <Recommend />
2230100980aaf0d2025-06-05 23:20:05 +0800208 ) : loading ? (
22301009ecc1c1c2025-04-09 21:56:23 +0800209 <p>加载中...</p>
2230100980aaf0d2025-06-05 23:20:05 +0800210 ) : errorMsg ? (
22301009afbcf4b2025-04-10 16:08:39 +0800211 <p className="error-text">{errorMsg}</p>
2230100980aaf0d2025-06-05 23:20:05 +0800212 ) : seeds.length === 0 ? (
22301009ecc1c1c2025-04-09 21:56:23 +0800213 <p>未找到符合条件的种子。</p>
214 ) : (
Krishya25590de2025-04-21 19:03:49 +0800215 <div className="seed-list-card">
216 <div className="seed-list-header">
217 <div className="seed-header-cover"></div>
218 <div className="seed-header-title">种子名称</div>
219 <div className="seed-header-size">大小</div>
220 <div className="seed-header-upload-time">上传时间</div>
221 <div className="seed-header-downloads">下载次数</div>
222 <div className="seed-header-actions">操作</div>
223 </div>
224 <div className="seed-list-body">
2230100980aaf0d2025-06-05 23:20:05 +0800225 {seeds.map((seed, index) => {
2230100980aaf0d2025-06-05 23:20:05 +0800226 let tagsArray = [];
227 if (seed.tags) {
228 if (Array.isArray(seed.tags)) {
229 tagsArray = seed.tags;
230 } else if (typeof seed.tags === 'string') {
231 try {
232 tagsArray = JSON.parse(seed.tags);
233 if (!Array.isArray(tagsArray)) {
22301111a289e262025-06-07 22:38:46 +0800234 tagsArray = seed.tags.split(',').map(t => t.trim()).filter(t => t);
2230100980aaf0d2025-06-05 23:20:05 +0800235 }
236 } catch {
22301111a289e262025-06-07 22:38:46 +0800237 tagsArray = seed.tags.split(',').map(t => t.trim()).filter(t => t);
2230100980aaf0d2025-06-05 23:20:05 +0800238 }
239 }
240 }
241
242 return (
223010091e2aea72025-06-08 16:35:54 +0800243 <Link to={`/seed/${seed.id}`} key={index} className="seed-item-link">
2230100980aaf0d2025-06-05 23:20:05 +0800244 <div className="seed-item">
223010094158f3a2025-06-06 19:59:10 +0800245 {seed.imageUrl && (
223010091e2aea72025-06-08 16:35:54 +0800246 <img src={seed.imageUrl} alt={seed.title} className="seed-item-cover" />
2230100980aaf0d2025-06-05 23:20:05 +0800247 )}
248 <div className="seed-item-title">
249 <div className="seed-title-row">
250 <h3 className="seed-title">{seed.title}</h3>
251 <div className="seed-tags">
252 {tagsArray.map((tag, i) => (
253 <span key={i} className="tag-label">{tag}</span>
254 ))}
255 </div>
Krishya25590de2025-04-21 19:03:49 +0800256 </div>
257 </div>
2230100980aaf0d2025-06-05 23:20:05 +0800258 <div className="seed-item-size">{seed.size || '未知'}</div>
259 <div className="seed-item-upload-time">{seed.upload_time?.split('T')[0] || '未知'}</div>
Krishyadbfadaa2025-06-09 20:33:15 +0800260 <div className="seed-item-downloads">{seed.leechers ?? 0} 次下载</div>
223010091e2aea72025-06-08 16:35:54 +0800261 <div className="seed-item-actions" onClick={e => e.stopPropagation()}>
Krishyadbfadaa2025-06-09 20:33:15 +0800262 <AuthButton
263 roles={["cookie", "chocolate", "ice-cream"]}
2230100980aaf0d2025-06-05 23:20:05 +0800264 className="btn-primary"
265 onClick={e => {
266 e.preventDefault();
267 e.stopPropagation();
223010091e2aea72025-06-08 16:35:54 +0800268 if (!user || !user.userId) {
269 toast.error('请先登录再下载种子文件');
270 return;
271 }
272 confirmAlert({
273 title: '确认下载',
274 message: `是否下载种子「${seed.title}」?`,
275 buttons: [
276 {
277 label: '确认',
278 onClick: () => handleDownload(seed.id)
279 },
280 {
281 label: '取消',
282 onClick: () => { }
283 }
284 ]
285 });
2230100980aaf0d2025-06-05 23:20:05 +0800286 }}
287 >
288 下载
Krishyadbfadaa2025-06-09 20:33:15 +0800289 </AuthButton>
290 <AuthButton
291 roles={["cookie", "chocolate", "ice-cream"]}
2230100901d3ff92025-06-07 16:16:26 +0800292 className="btn-outline"
293 onClick={async (e) => {
294 e.preventDefault();
295 e.stopPropagation();
2230100980aaf0d2025-06-05 23:20:05 +0800296
2230100901d3ff92025-06-07 16:16:26 +0800297 if (!user || !user.userId) {
223010091e2aea72025-06-08 16:35:54 +0800298 toast.error('请先登录再收藏');
2230100901d3ff92025-06-07 16:16:26 +0800299 return;
300 }
2230100980aaf0d2025-06-05 23:20:05 +0800301
2230100901d3ff92025-06-07 16:16:26 +0800302 try {
303 const res = await axios.post(`/seeds/${seed.id}/favorite-toggle`, null, {
304 params: { user_id: user.userId },
305 });
2230100980aaf0d2025-06-05 23:20:05 +0800306
2230100901d3ff92025-06-07 16:16:26 +0800307 if (res.data.code === 0) {
223010091e2aea72025-06-08 16:35:54 +0800308 toast.success('操作成功');
2230100901d3ff92025-06-07 16:16:26 +0800309 } else {
223010091e2aea72025-06-08 16:35:54 +0800310 toast.error(res.data.msg || '操作失败');
2230100901d3ff92025-06-07 16:16:26 +0800311 }
312 } catch (err) {
313 console.error('收藏失败:', err);
223010091e2aea72025-06-08 16:35:54 +0800314 toast.error('收藏失败,请稍后再试。');
2230100901d3ff92025-06-07 16:16:26 +0800315 }
316 }}
317 >
318 收藏
Krishyadbfadaa2025-06-09 20:33:15 +0800319 </AuthButton>
2230100980aaf0d2025-06-05 23:20:05 +0800320 </div>
Krishya25590de2025-04-21 19:03:49 +0800321 </div>
2230100980aaf0d2025-06-05 23:20:05 +0800322 </Link>
323 );
324 })}
Krishya25590de2025-04-21 19:03:49 +0800325 </div>
22301009ecc1c1c2025-04-09 21:56:23 +0800326 </div>
327 )}
328 </div>
329 </div>
330 );
331};
332
333export default SeedList;