blob: 18a05877be9e257f07d8d7d8968c69c5959e3a6b [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
22301009cbd5aac2025-06-09 23:08:53 +080043 const formatBytes = (bytes) => {
44 if (bytes === 0 || bytes === null || bytes === undefined) return '0 B';
45 const k = 1024;
46 const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
47 const i = Math.floor(Math.log(bytes) / Math.log(k));
48 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
49 };
50
51
22301009afbcf4b2025-04-10 16:08:39 +080052 const buildQueryParams = () => {
2230100980aaf0d2025-06-05 23:20:05 +080053 const category = CATEGORY_MAP[activeTab] || '';
22301009cbd5aac2025-06-09 23:08:53 +080054 const orderKey = sortOption === '最新' ? 'create_time' : (sortOption === '最热' ? 'leechers' : 'create_time');
22301009afbcf4b2025-04-10 16:08:39 +080055 const params = {
22301009afbcf4b2025-04-10 16:08:39 +080056 page: 1,
2230100980aaf0d2025-06-05 23:20:05 +080057 size: 20,
58 orderKey,
59 orderDesc: true,
22301009ecc1c1c2025-04-09 21:56:23 +080060 };
61
2230100980aaf0d2025-06-05 23:20:05 +080062 if (searchTerm.trim()) {
22301009cbd5aac2025-06-09 23:08:53 +080063 params.keyword = searchTerm.trim();
2230100980aaf0d2025-06-05 23:20:05 +080064 }
65 if (category) {
66 params.category = category;
67 }
22301009afbcf4b2025-04-10 16:08:39 +080068
69 const tags = Object.entries(selectedFilters)
2230100980aaf0d2025-06-05 23:20:05 +080070 .filter(([_, value]) => value !== '不限')
71 .map(([_, value]) => value);
22301009afbcf4b2025-04-10 16:08:39 +080072
73 if (tags.length > 0) {
2230100980aaf0d2025-06-05 23:20:05 +080074 params.tags = tags;
223010091e2aea72025-06-08 16:35:54 +080075 params.tagMode = tagMode;
22301009afbcf4b2025-04-10 16:08:39 +080076 }
77
78 return params;
79 };
80
22301009cbd5aac2025-06-09 23:08:53 +080081
22301009afbcf4b2025-04-10 16:08:39 +080082 const fetchSeeds = async () => {
223010091e2aea72025-06-08 16:35:54 +080083 if (activeTab === '猜你喜欢') return;
22301009afbcf4b2025-04-10 16:08:39 +080084 setLoading(true);
85 setErrorMsg('');
86 try {
87 const params = buildQueryParams();
2230100901d3ff92025-06-07 16:16:26 +080088 const response = await axios.get('/seeds/list', { params });
2230100980aaf0d2025-06-05 23:20:05 +080089 const data = response.data;
22301009afbcf4b2025-04-10 16:08:39 +080090
223010091e2aea72025-06-08 16:35:54 +080091 if (data.code !== 0) throw new Error(data.msg || '获取失败');
2230100980aaf0d2025-06-05 23:20:05 +080092 setSeeds(data.data || []);
22301009afbcf4b2025-04-10 16:08:39 +080093 } catch (error) {
94 console.error('获取种子列表失败:', error);
95 setErrorMsg(error.message || '获取失败,请稍后再试。');
96 setSeeds([]);
22301009afbcf4b2025-04-10 16:08:39 +080097 } finally {
98 setLoading(false);
99 }
100 };
101
102 const fetchFilterOptions = async () => {
2230100980aaf0d2025-06-05 23:20:05 +0800103 if (activeTab === '猜你喜欢' || !CATEGORY_MAP[activeTab]) return;
22301009afbcf4b2025-04-10 16:08:39 +0800104 const category = CATEGORY_MAP[activeTab];
105 try {
2230100980aaf0d2025-06-05 23:20:05 +0800106 const res = await axios.get(`/seed-filters?category=${category}`);
223010095b28c672025-04-10 20:12:45 +0800107 const filterData = res.data || {};
108 setFilters(filterData);
109
22301009afbcf4b2025-04-10 16:08:39 +0800110 const defaultSelections = {};
223010095b28c672025-04-10 20:12:45 +0800111 for (const key in filterData) {
22301009afbcf4b2025-04-10 16:08:39 +0800112 defaultSelections[key] = '不限';
113 }
114 setSelectedFilters(defaultSelections);
115 } catch (err) {
116 console.error('获取筛选项失败:', err);
117 setFilters({});
118 setSelectedFilters({});
119 }
120 };
22301009ecc1c1c2025-04-09 21:56:23 +0800121
122 useEffect(() => {
2230100980aaf0d2025-06-05 23:20:05 +0800123 fetchFilterOptions();
22301009afbcf4b2025-04-10 16:08:39 +0800124 }, [activeTab]);
22301009ecc1c1c2025-04-09 21:56:23 +0800125
22301009afbcf4b2025-04-10 16:08:39 +0800126 useEffect(() => {
2230100980aaf0d2025-06-05 23:20:05 +0800127 fetchSeeds();
22301009afbcf4b2025-04-10 16:08:39 +0800128 }, [activeTab, sortOption, selectedFilters, tagMode, searchTerm]);
22301009ecc1c1c2025-04-09 21:56:23 +0800129
22301009afbcf4b2025-04-10 16:08:39 +0800130 const handleDownload = async (seedId) => {
22301009afbcf4b2025-04-10 16:08:39 +0800131 try {
2230100980aaf0d2025-06-05 23:20:05 +0800132 const response = await axios.get(`/seeds/${seedId}/download`, {
223010091e2aea72025-06-08 16:35:54 +0800133 params: { passkey: user.userId },
22301009cbd5aac2025-06-09 23:08:53 +0800134 responseType: 'blob',
135 validateStatus: () => true // 允许处理非 2xx 响应
22301009afbcf4b2025-04-10 16:08:39 +0800136 });
137
22301009cbd5aac2025-06-09 23:08:53 +0800138 if (response.data && response.data.type === 'application/json') {
139 // 服务端返回的是 JSON 而不是 torrent 文件,尝试解析内容
140 const reader = new FileReader();
141 reader.onload = () => {
142 try {
143 const json = JSON.parse(reader.result);
144 if (json.code === 403 && json.msg === '您没有权限') {
145 toast.error('您已被封禁,如有疑问请联系管理员');
146 } else {
147 toast.error(json.msg || '下载失败,请稍后再试。');
148 }
149 } catch {
150 toast.error('下载失败,请稍后再试。');
151 }
152 };
153 reader.readAsText(response.data);
154 return;
155 }
156
157 // 如果是 torrent 文件
22301009afbcf4b2025-04-10 16:08:39 +0800158 const blob = new Blob([response.data], { type: 'application/x-bittorrent' });
159 const downloadUrl = URL.createObjectURL(blob);
160 const a = document.createElement('a');
161 a.href = downloadUrl;
162 a.download = `${seedId}.torrent`;
163 a.click();
164 URL.revokeObjectURL(downloadUrl);
165 } catch (error) {
166 console.error('下载失败:', error);
223010091e2aea72025-06-08 16:35:54 +0800167 toast.error('下载失败,请稍后再试。');
22301009ecc1c1c2025-04-09 21:56:23 +0800168 }
22301009afbcf4b2025-04-10 16:08:39 +0800169 };
22301009ecc1c1c2025-04-09 21:56:23 +0800170
22301009cbd5aac2025-06-09 23:08:53 +0800171
223010095b28c672025-04-10 20:12:45 +0800172 const handleFilterChange = (key, value) => {
223010091e2aea72025-06-08 16:35:54 +0800173 setSelectedFilters(prev => ({ ...prev, [key]: value }));
223010095b28c672025-04-10 20:12:45 +0800174 };
175
176 const clearFilter = (key) => {
223010091e2aea72025-06-08 16:35:54 +0800177 setSelectedFilters(prev => ({ ...prev, [key]: '不限' }));
223010095b28c672025-04-10 20:12:45 +0800178 };
179
22301009ecc1c1c2025-04-09 21:56:23 +0800180 return (
Krishyaf1d0ea82025-05-03 17:01:58 +0800181 <div className="seed-list-container">
2230100980aaf0d2025-06-05 23:20:05 +0800182 <Header />
22301009ecc1c1c2025-04-09 21:56:23 +0800183
22301009ecc1c1c2025-04-09 21:56:23 +0800184 <div className="controls">
185 <input
186 type="text"
187 placeholder="搜索种子..."
188 value={searchTerm}
189 onChange={(e) => setSearchTerm(e.target.value)}
190 className="search-input"
191 />
223010091e2aea72025-06-08 16:35:54 +0800192 <select value={sortOption} onChange={(e) => setSortOption(e.target.value)} className="sort-select">
22301009ecc1c1c2025-04-09 21:56:23 +0800193 <option value="最新">最新</option>
194 <option value="最热">最热</option>
22301009afbcf4b2025-04-10 16:08:39 +0800195 </select>
22301009cbd5aac2025-06-09 23:08:53 +0800196 {/* <select value={tagMode} onChange={(e) => setTagMode(e.target.value)} className="tag-mode-select">
22301009afbcf4b2025-04-10 16:08:39 +0800197 <option value="any">包含任意标签</option>
198 <option value="all">包含所有标签</option>
22301009cbd5aac2025-06-09 23:08:53 +0800199 </select> */}
22301009ecc1c1c2025-04-09 21:56:23 +0800200 </div>
201
202 <div className="tag-filters">
2230100980aaf0d2025-06-05 23:20:05 +0800203 {TAGS.map(tag => (
22301009cbd5aac2025-06-09 23:08:53 +0800204 <button
22301009ecc1c1c2025-04-09 21:56:23 +0800205 key={tag}
2230100980aaf0d2025-06-05 23:20:05 +0800206 className={`tag-button ${activeTab === tag ? 'active-tag' : ''}`}
22301009ecc1c1c2025-04-09 21:56:23 +0800207 onClick={() => {
22301009afbcf4b2025-04-10 16:08:39 +0800208 setActiveTab(tag);
209 setFilters({});
210 setSelectedFilters({});
22301009ecc1c1c2025-04-09 21:56:23 +0800211 }}
212 >
213 {tag}
22301009207e2db2025-06-09 00:27:28 +0800214 </button>
22301009ecc1c1c2025-04-09 21:56:23 +0800215 ))}
216 </div>
217
2230100980aaf0d2025-06-05 23:20:05 +0800218 {activeTab !== '猜你喜欢' && Object.keys(filters).length > 0 && (
22301009afbcf4b2025-04-10 16:08:39 +0800219 <div className="filter-bar">
220 {Object.entries(filters).map(([key, options]) => (
221 <div className="filter-group" key={key}>
222 <label>{key}:</label>
223010091e2aea72025-06-08 16:35:54 +0800223 <select value={selectedFilters[key]} onChange={(e) => handleFilterChange(key, e.target.value)}>
2230100980aaf0d2025-06-05 23:20:05 +0800224 {options.map(opt => (
22301009afbcf4b2025-04-10 16:08:39 +0800225 <option key={opt} value={opt}>{opt}</option>
226 ))}
227 </select>
2230100980aaf0d2025-06-05 23:20:05 +0800228 {selectedFilters[key] !== '不限' && (
223010091e2aea72025-06-08 16:35:54 +0800229 <button className="clear-filter-btn" onClick={() => clearFilter(key)}>✕</button>
223010095b28c672025-04-10 20:12:45 +0800230 )}
22301009afbcf4b2025-04-10 16:08:39 +0800231 </div>
232 ))}
233 </div>
234 )}
235
22301009ecc1c1c2025-04-09 21:56:23 +0800236 <div className="seed-list-content">
2230100980aaf0d2025-06-05 23:20:05 +0800237 {activeTab === '猜你喜欢' ? (
22301009afbcf4b2025-04-10 16:08:39 +0800238 <Recommend />
2230100980aaf0d2025-06-05 23:20:05 +0800239 ) : loading ? (
22301009ecc1c1c2025-04-09 21:56:23 +0800240 <p>加载中...</p>
2230100980aaf0d2025-06-05 23:20:05 +0800241 ) : errorMsg ? (
22301009afbcf4b2025-04-10 16:08:39 +0800242 <p className="error-text">{errorMsg}</p>
2230100980aaf0d2025-06-05 23:20:05 +0800243 ) : seeds.length === 0 ? (
22301009ecc1c1c2025-04-09 21:56:23 +0800244 <p>未找到符合条件的种子。</p>
245 ) : (
Krishya25590de2025-04-21 19:03:49 +0800246 <div className="seed-list-card">
247 <div className="seed-list-header">
248 <div className="seed-header-cover"></div>
249 <div className="seed-header-title">种子名称</div>
250 <div className="seed-header-size">大小</div>
251 <div className="seed-header-upload-time">上传时间</div>
252 <div className="seed-header-downloads">下载次数</div>
253 <div className="seed-header-actions">操作</div>
254 </div>
255 <div className="seed-list-body">
2230100980aaf0d2025-06-05 23:20:05 +0800256 {seeds.map((seed, index) => {
2230100980aaf0d2025-06-05 23:20:05 +0800257 let tagsArray = [];
258 if (seed.tags) {
259 if (Array.isArray(seed.tags)) {
260 tagsArray = seed.tags;
261 } else if (typeof seed.tags === 'string') {
262 try {
263 tagsArray = JSON.parse(seed.tags);
264 if (!Array.isArray(tagsArray)) {
22301111a289e262025-06-07 22:38:46 +0800265 tagsArray = seed.tags.split(',').map(t => t.trim()).filter(t => t);
2230100980aaf0d2025-06-05 23:20:05 +0800266 }
267 } catch {
22301111a289e262025-06-07 22:38:46 +0800268 tagsArray = seed.tags.split(',').map(t => t.trim()).filter(t => t);
2230100980aaf0d2025-06-05 23:20:05 +0800269 }
270 }
271 }
272
273 return (
22301009cbd5aac2025-06-09 23:08:53 +0800274 // <Link to={`/seed/${seed.id}`} key={index} className="seed-item-link">
275 <Link to={`/seed/${seed.id}`} key={seed.id} className="seed-item-link">
2230100980aaf0d2025-06-05 23:20:05 +0800276 <div className="seed-item">
223010094158f3a2025-06-06 19:59:10 +0800277 {seed.imageUrl && (
223010091e2aea72025-06-08 16:35:54 +0800278 <img src={seed.imageUrl} alt={seed.title} className="seed-item-cover" />
2230100980aaf0d2025-06-05 23:20:05 +0800279 )}
280 <div className="seed-item-title">
281 <div className="seed-title-row">
282 <h3 className="seed-title">{seed.title}</h3>
283 <div className="seed-tags">
284 {tagsArray.map((tag, i) => (
285 <span key={i} className="tag-label">{tag}</span>
286 ))}
287 </div>
Krishya25590de2025-04-21 19:03:49 +0800288 </div>
289 </div>
22301009cbd5aac2025-06-09 23:08:53 +0800290 {/* <div className="seed-item-size">{seed.size || '未知'}</div> */}
291 <div className="seed-item-size">{seed.size ? formatBytes(seed.size) : '未知'}</div>
292
293 <div className="seed-item-upload-time">
294 {
295 (() => {
296 if (!seed.createdTime || !Array.isArray(seed.createdTime)) return '未知';
297 const [year, month, day, hour, minute, second] = seed.createdTime;
298 if ([year, month, day, hour, minute, second].some(v => typeof v !== 'number')) return '未知';
299
300 const date = new Date(year, month - 1, day, hour, minute, second);
301 if (isNaN(date.getTime())) return '未知';
302
303 // 格式化为 yyyy-mm-dd
304 const yyyy = date.getFullYear();
305 const mm = String(date.getMonth() + 1).padStart(2, '0'); // 月份要加1,补0
306 const dd = String(date.getDate()).padStart(2, '0');
307
308 return `${yyyy}-${mm}-${dd}`;
309 })()
310 }
311 </div>
312
313
22301009207e2db2025-06-09 00:27:28 +0800314 <div className="seed-item-downloads">{seed.leechers ?? 0} 次下载</div>
223010091e2aea72025-06-08 16:35:54 +0800315 <div className="seed-item-actions" onClick={e => e.stopPropagation()}>
22301009207e2db2025-06-09 00:27:28 +0800316 <AuthButton
22301009cbd5aac2025-06-09 23:08:53 +0800317 roles={["cookie", "chocolate", "ice-cream"]}
2230100980aaf0d2025-06-05 23:20:05 +0800318 className="btn-primary"
319 onClick={e => {
320 e.preventDefault();
321 e.stopPropagation();
223010091e2aea72025-06-08 16:35:54 +0800322 if (!user || !user.userId) {
323 toast.error('请先登录再下载种子文件');
324 return;
325 }
326 confirmAlert({
327 title: '确认下载',
328 message: `是否下载种子「${seed.title}」?`,
329 buttons: [
330 {
331 label: '确认',
332 onClick: () => handleDownload(seed.id)
333 },
334 {
335 label: '取消',
336 onClick: () => { }
337 }
338 ]
339 });
2230100980aaf0d2025-06-05 23:20:05 +0800340 }}
341 >
342 下载
22301009207e2db2025-06-09 00:27:28 +0800343 </AuthButton>
344 <AuthButton
345 roles={["cookie", "chocolate", "ice-cream"]}
2230100901d3ff92025-06-07 16:16:26 +0800346 className="btn-outline"
347 onClick={async (e) => {
348 e.preventDefault();
349 e.stopPropagation();
2230100980aaf0d2025-06-05 23:20:05 +0800350
2230100901d3ff92025-06-07 16:16:26 +0800351 if (!user || !user.userId) {
223010091e2aea72025-06-08 16:35:54 +0800352 toast.error('请先登录再收藏');
2230100901d3ff92025-06-07 16:16:26 +0800353 return;
354 }
2230100980aaf0d2025-06-05 23:20:05 +0800355
2230100901d3ff92025-06-07 16:16:26 +0800356 try {
357 const res = await axios.post(`/seeds/${seed.id}/favorite-toggle`, null, {
358 params: { user_id: user.userId },
359 });
2230100980aaf0d2025-06-05 23:20:05 +0800360
2230100901d3ff92025-06-07 16:16:26 +0800361 if (res.data.code === 0) {
223010091e2aea72025-06-08 16:35:54 +0800362 toast.success('操作成功');
2230100901d3ff92025-06-07 16:16:26 +0800363 } else {
223010091e2aea72025-06-08 16:35:54 +0800364 toast.error(res.data.msg || '操作失败');
2230100901d3ff92025-06-07 16:16:26 +0800365 }
366 } catch (err) {
367 console.error('收藏失败:', err);
223010091e2aea72025-06-08 16:35:54 +0800368 toast.error('收藏失败,请稍后再试。');
2230100901d3ff92025-06-07 16:16:26 +0800369 }
370 }}
371 >
372 收藏
22301009207e2db2025-06-09 00:27:28 +0800373 </AuthButton>
2230100980aaf0d2025-06-05 23:20:05 +0800374 </div>
Krishya25590de2025-04-21 19:03:49 +0800375 </div>
2230100980aaf0d2025-06-05 23:20:05 +0800376 </Link>
377 );
378 })}
Krishya25590de2025-04-21 19:03:49 +0800379 </div>
22301009ecc1c1c2025-04-09 21:56:23 +0800380 </div>
381 )}
382 </div>
383 </div>
384 );
385};
386
387export default SeedList;