blob: 37730f38176c64fce51db46b115087e811fa7c23 [file] [log] [blame]
Krishyaf1d0ea82025-05-03 17:01:58 +08001import React, { useEffect, useState, useRef } from 'react';
2import './Promotion.css';
Krishya6bf199c2025-06-06 21:14:23 +08003import { useUser } from '../../../context/UserContext';
Krishyadbfadaa2025-06-09 20:33:15 +08004import axios from 'axios';
5import PromotionCarousel from './PromotionCarousel';
6import PromotionDetailDialog from './PromotionDetailDialog';
7import TorrentDetailDialog from './TorrentDetailDialog';
8import CreatePromotionDialog from './CreatePromotionDialog';
9import CategoryPromotionDialog from './CategoryPromotionDialog';
10import ColdTorrentsDialog from './ColdTorrentsDialog';
Krishyaf1d0ea82025-05-03 17:01:58 +080011
Krishyaf1d0ea82025-05-03 17:01:58 +080012const Promotion = () => {
Krishya6bf199c2025-06-06 21:14:23 +080013 const { user } = useUser();
Krishyaf1d0ea82025-05-03 17:01:58 +080014 const [promotions, setPromotions] = useState([]);
Krishya6bf199c2025-06-06 21:14:23 +080015 const [torrents, setTorrents] = useState([]);
Krishyaf1d0ea82025-05-03 17:01:58 +080016 const [loading, setLoading] = useState(true);
Krishyaf1d0ea82025-05-03 17:01:58 +080017 const [promoIndex, setPromoIndex] = useState(0);
Krishyaf1d0ea82025-05-03 17:01:58 +080018 const promoTimerRef = useRef(null);
Krishya6bf199c2025-06-06 21:14:23 +080019 const [showCreateDialog, setShowCreateDialog] = useState(false);
Krishyadbfadaa2025-06-09 20:33:15 +080020 const [showCategoryDialog, setShowCategoryDialog] = useState(false);
Krishya6bf199c2025-06-06 21:14:23 +080021 const [formData, setFormData] = useState({
22 name: '',
23 startTime: '',
24 endTime: '',
25 discountPercentage: '',
Krishyadbfadaa2025-06-09 20:33:15 +080026 applicableTorrentIds: []
Krishya6bf199c2025-06-06 21:14:23 +080027 });
Krishyadbfadaa2025-06-09 20:33:15 +080028 const [categoryFormData, setCategoryFormData] = useState({
29 name: '',
30 startTime: '',
31 endTime: '',
32 discountPercentage: '',
33 category: 'movie'
34 });
35
36 // 冷门资源列表状态
37 const [coldTorrents, setColdTorrents] = useState([]);
38 const [showColdDialog, setShowColdDialog] = useState(false);
39 // 新增状态:控制促销对话框中冷门资源表格的显示
40 const [showPromoColdTable, setShowPromoColdTable] = useState(false);
41
42 // 新增状态:促销详情数据和详情对话框显示控制
43 const [promotionDetail, setPromotionDetail] = useState(null);
44 const [showDetailDialog, setShowDetailDialog] = useState(false);
45
46 // 新增状态:种子详情数据和种子详情对话框显示控制
47 const [torrentDetail, setTorrentDetail] = useState(null);
48 const [showTorrentDialog, setShowTorrentDialog] = useState(false);
Krishya6bf199c2025-06-06 21:14:23 +080049
Krishyaf1d0ea82025-05-03 17:01:58 +080050 useEffect(() => {
51 fetchData();
Krishya6bf199c2025-06-06 21:14:23 +080052 fetchTorrentList();
Krishyaf1d0ea82025-05-03 17:01:58 +080053 }, []);
54
Krishyaf1d0ea82025-05-03 17:01:58 +080055 useEffect(() => {
56 if (promotions.length === 0) return;
Krishyaf1d0ea82025-05-03 17:01:58 +080057 clearInterval(promoTimerRef.current);
58 promoTimerRef.current = setInterval(() => {
59 setPromoIndex(prev => (prev + 1) % promotions.length);
60 }, 5000);
61 return () => clearInterval(promoTimerRef.current);
62 }, [promotions]);
63
Krishyaf1d0ea82025-05-03 17:01:58 +080064 const fetchData = async () => {
65 try {
Krishya6bf199c2025-06-06 21:14:23 +080066 const response = await fetch('/seeds/promotions');
67 const json = await response.json();
Krishyadbfadaa2025-06-09 20:33:15 +080068 if (json.code === 0 || json.code === 200) {
69 setPromotions(json.data || []);
70 } else {
71 console.error('获取促销活动失败:', json.msg);
72 }
Krishyaf1d0ea82025-05-03 17:01:58 +080073 } catch (error) {
Krishya8f2fec82025-06-04 21:54:46 +080074 console.error('获取促销活动失败:', error);
Krishyaf1d0ea82025-05-03 17:01:58 +080075 } finally {
76 setLoading(false);
77 }
78 };
79
Krishyadbfadaa2025-06-09 20:33:15 +080080 const formatImageUrl = (url) => {
81 if (!url) return '';
82 const filename = url.split('/').pop();
83 return `http://localhost:5011/uploads/torrents/${filename}`;
84 };
85
86 // 修正后的获取种子详情函数
87 const fetchTorrentDetail = async (torrentId) => {
88 try {
89 // 修正参数名称为torrentId
90 const res = await axios.post(`/seeds/info/${torrentId}`);
91 if (res.data.code === 0) {
92 const seedData = res.data.data;
93
94 // 处理封面图片
95 let cover = seedData.imageUrl;
96 if (!cover && seedData.imgUrl) {
97 const imgs = seedData.imgUrl
98 .split(',')
99 .map((i) => i.trim())
100 .filter(Boolean);
101 cover = imgs.length > 0 ? formatImageUrl(imgs[0]) : null;
102 }
103
104 setTorrentDetail({...seedData, coverImage: cover});
105 setShowTorrentDialog(true);
106 } else {
107 alert(`获取种子详情失败: ${res.data.msg || '未知错误'}`);
108 }
109 } catch (err) {
110 console.error('获取种子详情失败:', err);
111 alert('获取种子详情失败');
112 }
113};
114
115
116const fetchPromotionDetail = async (promotionId) => {
117 try {
118 const response = await fetch(`/seeds/promotions/${promotionId}`);
119 const json = await response.json();
120 if (json.code === 0 || json.code === 200) {
121 // 正确解析applicableTorrentIds
122 const data = {
123 ...json.data,
124 applicableTorrentIds: parseTorrentIds(json.data.applicableTorrentIds)
125 };
126 setPromotionDetail(data);
127 setShowDetailDialog(true);
128 } else {
129 alert(`获取促销详情失败: ${json.msg || '未知错误'}`);
130 }
131 } catch (error) {
132 console.error('获取促销详情失败:', error);
133 alert('获取促销详情失败');
134 }
135};
136
137// 解析种子ID字符串为数组
138const parseTorrentIds = (idString) => {
139 try {
140 // 处理类似 "[69, 49, 6]" 的字符串
141 return JSON.parse(idString);
142 } catch (e) {
143 console.error('解析种子ID失败:', e);
144 return [];
145 }
146};
147
148 // 关闭详情对话框
149 const closeDetailDialog = () => {
150 setShowDetailDialog(false);
151 setPromotionDetail(null);
152 };
153
154 // 关闭种子详情对话框
155 const closeTorrentDialog = () => {
156 setShowTorrentDialog(false);
157 setTorrentDetail(null);
158 };
159
Krishya6bf199c2025-06-06 21:14:23 +0800160 const fetchTorrentList = async () => {
161 try {
162 const response = await fetch('/seeds/list');
163 const json = await response.json();
Krishyadbfadaa2025-06-09 20:33:15 +0800164 if (json.code === 0 || json.code === 200) {
165 // 为每个种子添加selected状态
166 const torrentsWithSelection = (json.data || []).map(torrent => ({
167 ...torrent,
168 selected: false
169 }));
170 setTorrents(torrentsWithSelection);
171 } else {
172 console.error('获取种子列表失败:', json.msg);
173 }
Krishya6bf199c2025-06-06 21:14:23 +0800174 } catch (error) {
175 console.error('获取种子列表失败:', error);
176 }
177 };
178
Krishyadbfadaa2025-06-09 20:33:15 +0800179 const fetchColdTorrents = async () => {
180 try {
181 const response = await fetch('/seeds/cold');
182 const json = await response.json();
183 if (json.code === 0 || json.code === 200) {
184 setColdTorrents(json.data || []); // 存储冷门资源数据
185 setShowColdDialog(true); // 打开模态框
186 } else {
187 alert(`获取冷门资源失败: ${json.msg || '未知错误'}`);
188 }
189 } catch (error) {
190 console.error('获取冷门资源失败:', error);
191 alert('获取冷门资源失败');
192 }
193 };
194
195 // 从服务器获取冷门资源并显示在促销对话框中
196 const fetchPromoColdTorrents = async () => {
197 try {
198 const response = await fetch('/seeds/cold');
199 const json = await response.json();
200 if (json.code === 0 || json.code === 200) {
201 setColdTorrents(json.data || []);
202 setShowPromoColdTable(true);
203 } else {
204 alert(`获取冷门资源失败: ${json.msg || '未知错误'}`);
205 }
206 } catch (error) {
207 console.error('获取冷门资源失败:', error);
208 alert('获取冷门资源失败');
209 }
210 };
211
212 // 处理促销对话框中种子的选择
213 const handlePromoTorrentSelection = (torrentId, isChecked) => {
214 setFormData(prev => {
215 const ids = [...prev.applicableTorrentIds];
216 if (isChecked) {
217 ids.push(torrentId);
218 } else {
219 const index = ids.indexOf(torrentId);
220 if (index !== -1) ids.splice(index, 1);
221 }
222 return {
223 ...prev,
224 applicableTorrentIds: ids
225 };
226 });
227 };
228
229 // 关闭冷门资源模态框
230 const closeColdDialog = () => {
231 setShowColdDialog(false);
232 setColdTorrents([]);
233 };
234
Krishya6bf199c2025-06-06 21:14:23 +0800235 const openCreateDialog = () => {
Krishya6bf199c2025-06-06 21:14:23 +0800236 setFormData({
237 name: '',
238 startTime: '',
239 endTime: '',
240 discountPercentage: '',
Krishyadbfadaa2025-06-09 20:33:15 +0800241 applicableTorrentIds: []
Krishya6bf199c2025-06-06 21:14:23 +0800242 });
243 setShowCreateDialog(true);
244 };
245
Krishya6bf199c2025-06-06 21:14:23 +0800246 const closeCreateDialog = () => {
247 setShowCreateDialog(false);
248 };
249
Krishyadbfadaa2025-06-09 20:33:15 +0800250 const openCategoryDialog = () => {
251 setCategoryFormData({
252 name: '',
253 startTime: '',
254 endTime: '',
255 discountPercentage: '',
256 category: 'movie'
257 });
258 setShowCategoryDialog(true);
259 };
260
261 const closeCategoryDialog = () => {
262 setShowCategoryDialog(false);
263 };
264
Krishya6bf199c2025-06-06 21:14:23 +0800265 const handleInputChange = (e) => {
266 const { name, value } = e.target;
267 setFormData(prev => ({
268 ...prev,
269 [name]: value
270 }));
271 };
272
Krishyadbfadaa2025-06-09 20:33:15 +0800273 const handleCategoryInputChange = (e) => {
274 const { name, value } = e.target;
275 setCategoryFormData(prev => ({
276 ...prev,
277 [name]: value
278 }));
279 };
280
281 const formatSize = (bytes) => {
282 if (bytes === 0) return '0 B';
283
284 const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
285 const i = Math.floor(Math.log(bytes) / Math.log(1024));
286
287 return parseFloat((bytes / Math.pow(1024, i)).toFixed(2)) + ' ' + sizes[i];
288 };
289
290 const handleTorrentSelection = (torrentId, isChecked) => {
291 setTorrents(prev => prev.map(torrent =>
292 torrent.id === torrentId
293 ? {...torrent, selected: isChecked}
294 : torrent
295 ));
296
297 setFormData(prev => {
298 const ids = [...prev.applicableTorrentIds];
299 if (isChecked) {
300 ids.push(torrentId);
301 } else {
302 const index = ids.indexOf(torrentId);
303 if (index !== -1) ids.splice(index, 1);
304 }
305 return {
306 ...prev,
307 applicableTorrentIds: ids
308 };
309 });
310 };
311
Krishya6bf199c2025-06-06 21:14:23 +0800312 const handleCreatePromotion = async () => {
Krishya6bf199c2025-06-06 21:14:23 +0800313 if (!formData.name.trim()) {
314 alert('促销名称不能为空');
315 return;
316 }
317 if (!formData.startTime || !formData.endTime) {
318 alert('促销开始时间和结束时间不能为空');
319 return;
320 }
321 if (new Date(formData.startTime) >= new Date(formData.endTime)) {
322 alert('促销结束时间必须晚于开始时间');
323 return;
324 }
325 if (!formData.discountPercentage || isNaN(formData.discountPercentage)) {
326 alert('折扣百分比必须是数字');
327 return;
328 }
Krishyadbfadaa2025-06-09 20:33:15 +0800329 if (formData.applicableTorrentIds.length === 0) {
330 alert('请至少选择一个适用的种子');
331 return;
332 }
Krishya6bf199c2025-06-06 21:14:23 +0800333
334 const newPromo = {
335 name: formData.name,
336 startTime: new Date(formData.startTime).toISOString(),
337 endTime: new Date(formData.endTime).toISOString(),
338 discountPercentage: Number(formData.discountPercentage),
Krishyadbfadaa2025-06-09 20:33:15 +0800339 applicableTorrentIds: formData.applicableTorrentIds // 使用formData中的种子ID
Krishya6bf199c2025-06-06 21:14:23 +0800340 };
341
Krishya6bf199c2025-06-06 21:14:23 +0800342 try {
343 const res = await fetch('/seeds/promotions', {
344 method: 'POST',
345 headers: { 'Content-Type': 'application/json' },
346 body: JSON.stringify(newPromo)
347 });
348 const json = await res.json();
Krishyadbfadaa2025-06-09 20:33:15 +0800349 if (json.code === 0 || json.code === 200) {
Krishya6bf199c2025-06-06 21:14:23 +0800350 alert('促销活动创建成功');
351 fetchData();
352 setShowCreateDialog(false);
353 } else {
Krishyadbfadaa2025-06-09 20:33:15 +0800354 alert(`创建失败: ${json.msg || '未知错误'}`);
Krishya6bf199c2025-06-06 21:14:23 +0800355 }
356 } catch (err) {
357 console.error('创建促销失败:', err);
358 alert('创建促销失败');
359 }
360 };
361
Krishyadbfadaa2025-06-09 20:33:15 +0800362 const handleCreateCategoryPromotion = async () => {
363 if (!categoryFormData.name.trim()) {
364 alert('促销名称不能为空');
365 return;
366 }
367 if (!categoryFormData.startTime || !categoryFormData.endTime) {
368 alert('促销开始时间和结束时间不能为空');
369 return;
370 }
371 if (new Date(categoryFormData.startTime) >= new Date(categoryFormData.endTime)) {
372 alert('促销结束时间必须晚于开始时间');
373 return;
374 }
375 if (!categoryFormData.discountPercentage || isNaN(categoryFormData.discountPercentage)) {
376 alert('折扣百分比必须是数字');
377 return;
378 }
379
380 const newPromo = {
381 name: categoryFormData.name,
382 startTime: new Date(categoryFormData.startTime).toISOString(),
383 endTime: new Date(categoryFormData.endTime).toISOString(),
384 discountPercentage: Number(categoryFormData.discountPercentage)
385 };
386
387 try {
388 const res = await fetch(`/seeds/promotions/category?category=${categoryFormData.category}`, {
389 method: 'POST',
390 headers: { 'Content-Type': 'application/json' },
391 body: JSON.stringify(newPromo)
392 });
393 const json = await res.json();
394 if (json.code === 0 || json.code === 200) {
395 alert('分类促销活动创建成功');
396 fetchData();
397 setShowCategoryDialog(false);
398 } else {
399 alert(`创建失败: ${json.msg || '未知错误'}`);
400 }
401 } catch (err) {
402 console.error('创建分类促销失败:', err);
403 alert('创建分类促销失败');
404 }
405 };
406
Krishya6bf199c2025-06-06 21:14:23 +0800407 const handleDeletePromotion = async (promotionId) => {
408 if (!window.confirm('确认删除该促销活动吗?')) return;
409
410 try {
411 const res = await fetch(`/seeds/promotions/${promotionId}`, { method: 'DELETE' });
412 const json = await res.json();
Krishya73cd8822025-06-07 15:48:41 +0800413 if (json.code === 0 || json.code === 200) {
Krishya6bf199c2025-06-06 21:14:23 +0800414 alert('删除成功');
415 fetchData();
416 } else {
Krishyadbfadaa2025-06-09 20:33:15 +0800417 alert(`删除失败: ${json.msg || '未知错误'}`);
Krishya6bf199c2025-06-06 21:14:23 +0800418 }
419 } catch (err) {
420 console.error('删除失败:', err);
421 }
422 };
423
424 const isAdmin = user?.role === 'admin';
425 const prevPromo = () => setPromoIndex((promoIndex - 1 + promotions.length) % promotions.length);
Krishyadbfadaa2025-06-09 20:33:15 +0800426 const nextPromo = () => setPromoIndex((promoIndex + 1) % promotions.length);
Krishya6bf199c2025-06-06 21:14:23 +0800427 const currentPromo = promotions[promoIndex];
428
Krishyadbfadaa2025-06-09 20:33:15 +0800429 const formatDateTime = (dateTime) => {
430 if (!dateTime) return '未知';
431 return new Date(dateTime).toLocaleString();
432 };
433
434 const getUploadBonusDisplay = (promo) => {
435 if (!promo || !promo.discountPercentage) return '无';
436 const bonus = promo.discountPercentage;
437 return bonus > 0 ? `+${bonus}%` : `-${Math.abs(bonus)}%`;
438 };
439
440 const getDownloadDiscountDisplay = (promo) => {
441 if (!promo || !promo.downloadDiscount) return '无';
442 const discount = (1 - promo.downloadDiscount) * 100;
443 return discount > 0 ? `-${discount.toFixed(1)}%` : '无';
444 };
445
Krishyaf1d0ea82025-05-03 17:01:58 +0800446 if (loading) {
447 return <div className="promotion-container">加载中...</div>;
Krishyadbfadaa2025-06-09 20:33:15 +0800448 };
Krishyaf1d0ea82025-05-03 17:01:58 +0800449
Krishyaf1d0ea82025-05-03 17:01:58 +0800450 return (
451 <div className="promotion-container carousel-container">
Krishyadbfadaa2025-06-09 20:33:15 +0800452 <PromotionCarousel
453 promotions={promotions}
454 currentPromo={currentPromo}
455 prevPromo={prevPromo}
456 nextPromo={nextPromo}
457 isAdmin={isAdmin}
458 openCreateDialog={openCreateDialog}
459 openCategoryDialog={openCategoryDialog}
460 fetchColdTorrents={fetchColdTorrents}
461 handleDeletePromotion={handleDeletePromotion}
462 fetchPromotionDetail={fetchPromotionDetail}
463 />
Krishya6bf199c2025-06-06 21:14:23 +0800464
Krishyadbfadaa2025-06-09 20:33:15 +0800465 {/* 新增:冷门资源模态框 */}
466 <ColdTorrentsDialog
467 showColdDialog={showColdDialog}
468 coldTorrents={coldTorrents}
469 closeColdDialog={closeColdDialog}
470 fetchTorrentDetail={fetchTorrentDetail}
471 />
Krishya6bf199c2025-06-06 21:14:23 +0800472
Krishyadbfadaa2025-06-09 20:33:15 +0800473 {/* 促销详情对话框 */}
474 <PromotionDetailDialog
475 showDetailDialog={showDetailDialog}
476 promotionDetail={promotionDetail}
477 closeDetailDialog={closeDetailDialog}
478 torrents={torrents}
479 fetchTorrentDetail={fetchTorrentDetail}
480 />
Krishya6bf199c2025-06-06 21:14:23 +0800481
Krishyadbfadaa2025-06-09 20:33:15 +0800482 {/* 种子详情对话框 */}
483 <TorrentDetailDialog
484 showTorrentDialog={showTorrentDialog}
485 torrentDetail={torrentDetail}
486 closeTorrentDialog={closeTorrentDialog}
487 />
488
489 {/* 创建冷门资源促销弹窗 */}
490 <CreatePromotionDialog
491 showCreateDialog={showCreateDialog}
492 formData={formData}
493 handleInputChange={handleInputChange}
494 closeCreateDialog={closeCreateDialog}
495 handleCreatePromotion={handleCreatePromotion}
496 fetchPromoColdTorrents={fetchPromoColdTorrents}
497 showPromoColdTable={showPromoColdTable}
498 coldTorrents={coldTorrents}
499 handlePromoTorrentSelection={handlePromoTorrentSelection}
500 />
501
502 {/* 创建特定分类促销弹窗 */}
503 <CategoryPromotionDialog
504 showCategoryDialog={showCategoryDialog}
505 categoryFormData={categoryFormData}
506 handleCategoryInputChange={handleCategoryInputChange}
507 closeCategoryDialog={closeCategoryDialog}
508 handleCreateCategoryPromotion={handleCreateCategoryPromotion}
509 />
Krishyaf1d0ea82025-05-03 17:01:58 +0800510 </div>
511 );
512};
513
Krishyadbfadaa2025-06-09 20:33:15 +0800514export default Promotion;
515