blob: ee9611a34b69d33b043c40586e20c57f3d9e8cd3 [file] [log] [blame]
BirdNETM178aca62025-06-08 14:43:41 +08001import { ExclamationCircleOutlined, PlusOutlined, DeleteOutlined, UploadOutlined, TrophyOutlined, CheckCircleOutlined, CloseCircleOutlined, DownloadOutlined } from '@ant-design/icons';
2import { Button, message, Modal, Switch, Upload, Form, Tag } from 'antd';
BirdNETMb0f71532025-05-26 17:37:33 +08003import React, { useRef, useState, useEffect } from 'react';
4import { FormattedMessage, useIntl } from 'umi';
5import { FooterToolbar, PageContainer } from '@ant-design/pro-layout';
6import type { ActionType, ProColumns } from '@ant-design/pro-table';
7import ProTable from '@ant-design/pro-table';
8import type { FormInstance } from 'antd';
BirdNETM02a57e52025-06-05 00:27:31 +08009import type { UploadFile } from 'antd/es/upload/interface';
BirdNETM178aca62025-06-08 14:43:41 +080010import { getRewardList, removeReward, updateReward, addReward, getTorrentID, submitReward } from './service';
BirdNETMb0f71532025-05-26 17:37:33 +080011import UpdateForm from './components/UpdateForm';
12import { getDictValueEnum } from '@/services/system/dict';
13import DictTag from '@/components/DictTag';
14import { useAccess } from 'umi';
15import { RewardItem, RewardListParams } from './data';
BirdNETM42e63bd2025-06-07 17:42:14 +080016import { BtTorrent } from '../Torrent/data';
17import { uploadTorrent } from '../Torrent/service';
BirdNETM178aca62025-06-08 14:43:41 +080018import { downloadTorrent } from '../Torrent/service';
BirdNETMb0f71532025-05-26 17:37:33 +080019/**
20 * 删除节点
21 *
22 * @param selectedRows
23 */
24const handleRemove = async (selectedRows: RewardItem[]) => {
25 const hide = message.loading('正在删除');
26 if (!selectedRows) return true;
27 try {
28 await removeReward(selectedRows.map((row) => row.rewardId).join(','));
29 hide();
30 message.success('删除成功');
31 return true;
32 } catch (error) {
33 hide();
34 message.error('删除失败,请重试');
35 return false;
36 }
37};
38
39const handleUpdate = async (fields: RewardItem) => {
40 const hide = message.loading('正在更新');
41 try {
42 const resp = await updateReward(fields);
43 hide();
44 if (resp.code === 200) {
45 message.success('更新成功');
46 } else {
47 message.error(resp.msg);
48 }
49 return true;
50 } catch (error) {
51 hide();
52 message.error('配置失败请重试!');
53 return false;
54 }
55};
56
57const handleAdd = async (fields: RewardItem) => {
58 const hide = message.loading('正在添加');
59 try {
60 const resp = await addReward(fields);
61 hide();
62 if (resp.code === 200) {
63 message.success('添加成功');
64 } else {
65 message.error(resp.msg);
66 }
67 return true;
68 } catch (error) {
69 hide();
70 message.error('配置失败请重试!');
71 return false;
72 }
73};
74
BirdNETM02a57e52025-06-05 00:27:31 +080075/**
BirdNETM178aca62025-06-08 14:43:41 +080076 * 下载已提交的种子文件
BirdNETM02a57e52025-06-05 00:27:31 +080077 * @param rewardId 悬赏ID
BirdNETM02a57e52025-06-05 00:27:31 +080078 */
BirdNETM178aca62025-06-08 14:43:41 +080079const handleDownloadSubmitted = async (rewardId: number) => {
80 const hide = message.loading('正在下载...');
BirdNETM02a57e52025-06-05 00:27:31 +080081
BirdNETM178aca62025-06-08 14:43:41 +080082 try {
83 const torrentID = await getTorrentID(rewardId);
84 // 这里需要调用下载接口,假设接口为 downloadRewardFile
85 const blob = await downloadTorrent(torrentID.torrentId);
86 const url = window.URL.createObjectURL(blob);
87 const link = document.createElement('a');
88 link.href = url;
89 link.download = `${blob.name}.torrent`;
90 document.body.appendChild(link);
91 link.click();
92 document.body.removeChild(link);
93 window.URL.revokeObjectURL(url);
94 message.success('下载成功');
95 } catch (error: any) {
96 message.error('下载失败');
97 }
98};
BirdNETM02a57e52025-06-05 00:27:31 +080099
BirdNETMb0f71532025-05-26 17:37:33 +0800100const RewardTableList: React.FC = () => {
101 const formTableRef = useRef<FormInstance>();
BirdNETM42e63bd2025-06-07 17:42:14 +0800102 const [uploadForm] = Form.useForm();
BirdNETMb0f71532025-05-26 17:37:33 +0800103 const [modalVisible, setModalVisible] = useState<boolean>(false);
104 const [readOnly, setReadOnly] = useState<boolean>(false);
105
BirdNETM02a57e52025-06-05 00:27:31 +0800106 // 接悬赏相关状态
107 const [acceptModalVisible, setAcceptModalVisible] = useState<boolean>(false);
108 const [currentAcceptReward, setCurrentAcceptReward] = useState<RewardItem>();
109 const [fileList, setFileList] = useState<UploadFile[]>([]);
110
BirdNETMb0f71532025-05-26 17:37:33 +0800111 const actionRef = useRef<ActionType>();
112 const [currentRow, setCurrentRow] = useState<RewardItem>();
113 const [selectedRows, setSelectedRows] = useState<RewardItem[]>([]);
114
115 const [statusOptions, setStatusOptions] = useState<any>([]);
116
117 const access = useAccess();
118
119 /** 国际化配置 */
120 const intl = useIntl();
121
122 useEffect(() => {
123 getDictValueEnum('reward_status').then((data) => {
124 setStatusOptions(data);
125 });
126 }, []);
127
BirdNETM42e63bd2025-06-07 17:42:14 +0800128 /**
129 * 处理接悬赏提交
130 * @param rewardId 悬赏ID
131 * @param file 上传的文件
132 */
133 const handleAcceptReward = async (rewardId: number, file: File) => {
134 const hide = message.loading('正在提交悬赏...');
135 try {
136 // 直接传递File对象给uploadTorrent函数
BirdNETM178aca62025-06-08 14:43:41 +0800137 const resp = await submitReward(file);
BirdNETM42e63bd2025-06-07 17:42:14 +0800138
139 hide();
140 if (resp.code === 200) {
141 message.success('悬赏提交成功!');
142 return true;
143 } else {
144 message.error(resp.msg || '提交失败');
145 return false;
146 }
147 } catch (error) {
148 hide();
149 message.error('提交失败,请重试!');
150 console.error('上传错误:', error);
151 return false;
BirdNETM02a57e52025-06-05 00:27:31 +0800152 }
153 };
154
BirdNETM42e63bd2025-06-07 17:42:14 +0800155 // 修复后的提交处理函数
156 const handleAcceptSubmit = async () => {
157 if (!currentAcceptReward) {
158 message.warning('无效的悬赏信息!');
159 return;
160 }
161
162 if (fileList.length === 0) {
163 message.warning('请选择一个种子文件!');
164 return;
165 }
166
167 // 获取原始File对象
168 const uploadFile = fileList[0];
169 let file: File;
170
171 if (uploadFile.originFileObj) {
172 // 如果有originFileObj,使用它(这是真正的File对象)
173 file = uploadFile.originFileObj;
174 } else {
175 message.error('文件信息异常,请重新选择文件!');
176 return;
177 }
178
179 const success = await handleAcceptReward(currentAcceptReward.rewardId, file);
180 if (success) {
181 setAcceptModalVisible(false);
182 setCurrentAcceptReward(undefined);
183 setFileList([]);
184 uploadForm.resetFields();
185 // 刷新表格数据
186 if (actionRef.current) {
187 actionRef.current.reload();
188 }
189 }
190 };
191
BirdNETM178aca62025-06-08 14:43:41 +0800192 // 文件上传前的检查
BirdNETM02a57e52025-06-05 00:27:31 +0800193 const beforeUpload = (file: File) => {
BirdNETM42e63bd2025-06-07 17:42:14 +0800194 console.log('上传前检查文件:', file.name, file.type, file.size);
195
196 // 检查文件类型
197 const isTorrent = file.name.toLowerCase().endsWith('.torrent');
198 if (!isTorrent) {
199 message.error('只能上传.torrent格式的文件!');
200 return false;
201 }
202
203 // 检查文件大小
BirdNETM02a57e52025-06-05 00:27:31 +0800204 const isValidSize = file.size / 1024 / 1024 < 50; // 限制50MB
205 if (!isValidSize) {
206 message.error('文件大小不能超过50MB!');
207 return false;
208 }
209
210 // 检查是否已经有文件了
211 if (fileList.length >= 1) {
212 message.warning('只能上传一个文件,当前文件将替换已选择的文件!');
213 }
214
215 return false; // 阻止自动上传,我们手动处理
216 };
217
BirdNETM178aca62025-06-08 14:43:41 +0800218 // 处理文件列表变化
BirdNETM42e63bd2025-06-07 17:42:14 +0800219 const handleFileChange = ({ fileList: newFileList }: { fileList: UploadFile[] }) => {
220 // 只保留最后一个文件
221 const latestFileList = newFileList.slice(-1);
222 setFileList(latestFileList);
BirdNETM02a57e52025-06-05 00:27:31 +0800223
BirdNETM42e63bd2025-06-07 17:42:14 +0800224 if (latestFileList.length > 0) {
225 console.log('已选择文件:', latestFileList[0].name);
BirdNETM02a57e52025-06-05 00:27:31 +0800226 }
227 };
228
BirdNETMb0f71532025-05-26 17:37:33 +0800229 const columns: ProColumns<RewardItem>[] = [
230 {
231 title: '悬赏ID',
232 dataIndex: 'rewardId',
233 valueType: 'text',
234 hideInSearch: true,
235 },
236 {
237 title: '悬赏标题',
238 dataIndex: 'title',
239 valueType: 'text',
240 },
241 {
BirdNETM11aacb92025-06-07 23:17:03 +0800242 title: (
243 <span>
244 <TrophyOutlined style={{ marginRight: 4, color: '#faad14' }} />
245 悬赏积分
246 </span>
247 ),
BirdNETMb0f71532025-05-26 17:37:33 +0800248 dataIndex: 'amount',
BirdNETM11aacb92025-06-07 23:17:03 +0800249 valueType: 'digit',
BirdNETMb0f71532025-05-26 17:37:33 +0800250 hideInSearch: true,
BirdNETM11aacb92025-06-07 23:17:03 +0800251 render: (_, record) => (
252 <span style={{ color: '#faad14', fontWeight: 'bold' }}>
253 <TrophyOutlined style={{ marginRight: 4 }} />
254 {record.amount}
255 </span>
256 ),
BirdNETMb0f71532025-05-26 17:37:33 +0800257 },
BirdNETM178aca62025-06-08 14:43:41 +0800258 {
259 title: '完成状态',
260 dataIndex: 'status',
261 valueType: 'select',
262 valueEnum: {
263 '0': { text: '进行中', status: 'Processing' },
264 '1': { text: '已完成', status: 'Success' },
265 '2': { text: '已取消', status: 'Error' },
266 },
267 render: (_, record) => {
268 const status = record.status;
269 let color = 'default';
270 let icon = <CloseCircleOutlined />;
271 let text = '进行中';
272
273 if (status === '1') {
274 color = 'success';
275 icon = <CheckCircleOutlined />;
276 text = '已完成';
277 } else if (status === '2') {
278 color = 'error';
279 icon = <CloseCircleOutlined />;
280 text = '已取消';
281 } else {
282 color = 'processing';
283 icon = <TrophyOutlined />;
284 text = '进行中';
285 }
286
287 return (
288 <Tag
289 color={color}
290 icon={icon}
291 >
292 {text}
293 </Tag>
294 );
295 },
296 },
BirdNETMb0f71532025-05-26 17:37:33 +0800297 {
298 title: '发布时间',
299 dataIndex: 'createTime',
300 valueType: 'dateRange',
301 render: (_, record) => {
302 return (<span>{record.createTime?.toString()}</span>);
303 },
304 search: {
305 transform: (value) => {
306 return {
307 'params[beginTime]': value[0],
308 'params[endTime]': value[1],
309 };
310 },
311 },
312 },
313 {
314 title: '备注',
315 dataIndex: 'remark',
316 valueType: 'text',
317 hideInSearch: true,
318 },
319 {
320 title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
321 dataIndex: 'option',
BirdNETM178aca62025-06-08 14:43:41 +0800322 width: '350px',
BirdNETMb0f71532025-05-26 17:37:33 +0800323 valueType: 'option',
BirdNETM178aca62025-06-08 14:43:41 +0800324 render: (_, record) => {
325 // 根据status判断:0进行中 1已完成 2已取消
326 const status = record.status;
327 const isCompleted = status === '1'; // 已完成
328 const isInProgress = status === '0'; // 进行中
329 const isCancelled = status === '2'; // 已取消
330
331 return [
332 <Button
333 type="link"
334 size="small"
335 key="view"
336 onClick={() => {
337 setModalVisible(true);
338 setCurrentRow(record);
339 setReadOnly(true);
340 }}
341 >
342 查看
343 </Button>,
344 // 只有进行中的悬赏才显示"接悬赏"按钮
345 isInProgress && (
346 <Button
347 type="link"
348 size="small"
349 key="accept"
350 style={{ color: '#52c41a' }}
351 icon={<TrophyOutlined />}
352 onClick={() => {
353 setCurrentAcceptReward(record);
354 setAcceptModalVisible(true);
355 setFileList([]);
356 }}
357 >
358 接悬赏
359 </Button>
360 ),
361 // 只有已完成的悬赏才显示"下载"按钮
362 isCompleted && (
363 <Button
364 type="link"
365 size="small"
366 key="download"
367 style={{ color: '#1890ff' }}
368 icon={<DownloadOutlined />}
369 onClick={() => {
370 handleDownloadSubmitted(record.rewardId);
371 }}
372 >
373 下载
374 </Button>
375 ),
376 <Button
377 type="link"
378 size="small"
379 key="edit"
380 hidden={!access.hasPerms('reward:reward:edit')}
381 onClick={() => {
382 setModalVisible(true);
383 setCurrentRow(record);
384 setReadOnly(false);
385 }}
386 >
387 编辑
388 </Button>,
389 <Button
390 type="link"
391 size="small"
392 danger
393 key="batchRemove"
394 hidden={!access.hasPerms('reward:reward:remove')}
395 onClick={async () => {
396 Modal.confirm({
397 title: '删除',
398 content: '确定删除该项吗?',
399 okText: '确认',
400 cancelText: '取消',
401 onOk: async () => {
402 const success = await handleRemove([record]);
403 if (success) {
404 if (actionRef.current) {
405 actionRef.current.reload();
406 }
BirdNETMb0f71532025-05-26 17:37:33 +0800407 }
BirdNETM178aca62025-06-08 14:43:41 +0800408 },
409 });
410 }}
411 >
412 删除
413 </Button>,
414 ].filter(Boolean) // 过滤掉 false 值
415 },
BirdNETMb0f71532025-05-26 17:37:33 +0800416 },
417 ];
418
419 return (
420 <PageContainer>
421 <div style={{ width: '100%', float: 'right' }}>
422 <ProTable<RewardItem>
423 headerTitle={intl.formatMessage({
424 id: 'pages.searchTable.title',
425 defaultMessage: '信息',
426 })}
427 actionRef={actionRef}
428 formRef={formTableRef}
429 rowKey="rewardId"
430 key="rewardList"
431 search={{
432 labelWidth: 120,
433 }}
434 toolBarRender={() => [
435 <Button
436 type="primary"
437 key="add"
438 hidden={!access.hasPerms('reward:reward:add')}
439 onClick={async () => {
440 setCurrentRow(undefined);
441 setModalVisible(true);
442 }}
443 >
444 <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
445 </Button>,
446 <Button
447 type="primary"
448 key="remove"
449 danger
450 hidden={selectedRows?.length === 0 || !access.hasPerms('reward:reward:remove')}
451 onClick={async () => {
452 Modal.confirm({
453 title: '是否确认删除所选数据项?',
454 icon: <ExclamationCircleOutlined />,
455 content: '请谨慎操作',
456 async onOk() {
457 const success = await handleRemove(selectedRows);
458 if (success) {
459 setSelectedRows([]);
460 actionRef.current?.reloadAndRest?.();
461 }
462 },
463 onCancel() { },
464 });
465 }}
466 >
467 <DeleteOutlined />
468 <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
469 </Button>,
470 ]}
471 request={(params) =>
472 getRewardList({ ...params } as RewardListParams).then((res) => {
473 return {
474 data: res.rows,
475 total: res.total,
476 success: true,
477 };
478 })
479 }
480 columns={columns}
481 rowSelection={{
482 onChange: (_, selectedRows) => {
483 setSelectedRows(selectedRows);
484 },
485 }}
486 />
487 </div>
BirdNETM02a57e52025-06-05 00:27:31 +0800488
BirdNETMb0f71532025-05-26 17:37:33 +0800489 {selectedRows?.length > 0 && (
490 <FooterToolbar
491 extra={
492 <div>
493 <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
494 <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
495 <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
496 </div>
497 }
498 >
499 <Button
500 key="remove"
501 danger
502 hidden={!access.hasPerms('reward:reward:remove')}
503 onClick={async () => {
504 Modal.confirm({
505 title: '删除',
506 content: '确定删除该项吗?',
507 okText: '确认',
508 cancelText: '取消',
509 onOk: async () => {
510 const success = await handleRemove(selectedRows);
511 if (success) {
512 setSelectedRows([]);
513 actionRef.current?.reloadAndRest?.();
514 }
515 },
516 });
517 }}
518 >
519 <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
520 </Button>
521 </FooterToolbar>
522 )}
BirdNETM02a57e52025-06-05 00:27:31 +0800523
524 {/* 原有的编辑/新增模态框 */}
BirdNETMb0f71532025-05-26 17:37:33 +0800525 <UpdateForm
526 readOnly={readOnly}
527 onSubmit={async (values) => {
528 let success = false;
529 if (values.rewardId) {
530 success = await handleUpdate({ ...values } as RewardItem);
531 } else {
532 success = await handleAdd({ ...values } as RewardItem);
533 }
534 if (success) {
535 setModalVisible(false);
536 setCurrentRow(undefined);
537 if (actionRef.current) {
538 actionRef.current.reload();
539 }
540 }
541 }}
542 onCancel={() => {
543 setModalVisible(false);
544 setCurrentRow(undefined);
545 setReadOnly(false);
546 }}
547 open={modalVisible}
548 values={currentRow || {}}
549 statusOptions={statusOptions}
550 />
BirdNETM02a57e52025-06-05 00:27:31 +0800551
552 {/* 接悬赏模态框 */}
553 <Modal
BirdNETM11aacb92025-06-07 23:17:03 +0800554 title={
555 <span>
556 <TrophyOutlined style={{ color: '#faad14', marginRight: 8 }} />
557 接悬赏 - {currentAcceptReward?.title || ''}
558 </span>
559 }
BirdNETM02a57e52025-06-05 00:27:31 +0800560 open={acceptModalVisible}
561 onOk={handleAcceptSubmit}
562 onCancel={() => {
563 setAcceptModalVisible(false);
564 setCurrentAcceptReward(undefined);
565 setFileList([]);
566 }}
567 width={600}
568 okText="提交悬赏"
569 cancelText="取消"
570 >
571 <div style={{ marginBottom: 16 }}>
572 <p><strong>悬赏标题:</strong>{currentAcceptReward?.title}</p>
BirdNETM11aacb92025-06-07 23:17:03 +0800573 <p>
574 <strong>悬赏积分:</strong>
575 <span style={{ color: '#faad14', fontWeight: 'bold' }}>
576 <TrophyOutlined style={{ marginRight: 4 }} />
577 {currentAcceptReward?.amount}
578 </span>
579 </p>
BirdNETM02a57e52025-06-05 00:27:31 +0800580 <p><strong>备注:</strong>{currentAcceptReward?.remark || '无'}</p>
581 </div>
582
583 <div>
584 <p style={{ marginBottom: 8, fontWeight: 'bold' }}>上传种子文件:</p>
585 <Upload
586 fileList={fileList}
587 onChange={handleFileChange}
588 beforeUpload={beforeUpload}
589 onRemove={(file) => {
590 setFileList([]);
591 }}
592 maxCount={1}
593 accept=".torrent"
594 >
595 <Button icon={<UploadOutlined />}>选择种子文件</Button>
596 </Upload>
597 <p style={{ marginTop: 8, color: '#666', fontSize: '12px' }}>
598 只能上传一个种子文件,文件大小不超过50MB
599 </p>
600 </div>
601 </Modal>
BirdNETMb0f71532025-05-26 17:37:33 +0800602 </PageContainer>
603 );
604};
605
606export default RewardTableList;