返回 AiToEarn
videoInfo.tsx
1 /*
2 * @Author: nevin
3 * @Date: 2025-03-01 19:27:35
4 * @LastEditTime: 2025-03-24 14:28:36
5 * @LastEditors: nevin
6 * @Description: video
7 */
8 import { Button, Modal, Tag, Divider, Row, Col, message } from 'antd';
9 import { Task, TaskStatusName, TaskTypeName, TaskVideo } from '@@/types/task';
10 import { PubType } from '@@/publish/PublishEnum';
11 import {
12 icpCreatePubRecord,
13 icpCreateVideoPubRecord,
14 icpPubVideo,
15 } from '@/icp/publish';
16 import { forwardRef, useImperativeHandle, useState } from 'react';
17 import { taskApi } from '@/api/task';
18 import styles from './videoInfo.module.scss';
19 import {
20 ClockCircleOutlined,
21 TeamOutlined,
22 TagOutlined,
23 DollarOutlined,
24 PlayCircleOutlined,
25 CopyOutlined,
26 } from '@ant-design/icons';
27 import ChooseAccountModule from '@/views/publish/components/ChooseAccountModule/ChooseAccountModule';
28 import { ipcDownFile } from '@/icp/tools';
29 import { PlatType } from '@@/AccountEnum';
30 import VideoPlayer from '@/components/VideoPlayer';
31
32 const FILE_BASE_URL = import.meta.env.VITE_APP_FILE_HOST;
33
34 export interface TaskInfoRef {
35 init: (pubRecord: Task<TaskVideo>) => Promise<void>;
36 }
37
38 interface TaskInfoProps {
39 onTaskApplied?: () => void; // 新增
40 }
41
42 const Com = forwardRef<TaskInfoRef, TaskInfoProps>((props: any, ref) => {
43 const [isModalOpen, setIsModalOpen] = useState(false);
44 const [taskInfo, setTaskInfo] = useState<Task<TaskVideo> | null>();
45 const [downloading, setDownloading] = useState(false);
46 const [videoPlayback, setVideoPlayback] = useState<{
47 visible: boolean;
48 url: string;
49 title: string;
50 }>({
51 visible: false,
52 url: '',
53 title: '',
54 });
55
56 // 在组件内添加一个新的状态来存储任务记录
57 const [taskRecord, setTaskRecord] = useState<{
58 _id: string;
59 createTime: string;
60 isFirstTimeSubmission: boolean;
61 status: string;
62 taskId: string;
63 } | null>(null);
64
65 // 从props中获取刷新函数
66 const { onTaskApplied } = props;
67
68 async function init(inTaskInfo: Task<TaskVideo>) {
69 setTaskInfo(inTaskInfo);
70 setIsModalOpen(true);
71 }
72
73 useImperativeHandle(ref, () => ({
74 init: init,
75 }));
76
77 /**
78 * 接受任务
79 */
80 async function taskApply() {
81 if (!taskInfo) return;
82 }
83
84 /**
85 * 完成任务
86 */
87 async function taskDone() {
88 console.log('完成任务', taskInfo, taskRecord);
89 if (!taskInfo || !taskRecord) {
90 message.error('任务信息不完整,无法完成任务');
91 return;
92 }
93
94 try {
95 // 使用任务记录的 ID 而不是任务 ID
96 const res = await taskApi.taskDone(taskRecord._id, {
97 submissionUrl: taskInfo.title,
98 screenshotUrls: [taskInfo.imageUrl],
99 qrCodeScanResult: taskInfo.title,
100 });
101 message.success('任务发布成功!');
102 } catch (error) {
103 message.error('完成任务失败,请稍后再试');
104 }
105 }
106
107 const handleCancel = () => {
108 setIsModalOpen(false);
109 };
110
111 // 复制任务ID
112 const copyTaskId = (id: string) => {
113 navigator.clipboard
114 .writeText(id)
115 .then(() => {
116 message.success('任务ID已复制到剪贴板');
117 })
118 .catch(() => {
119 message.error('复制失败,请手动复制');
120 });
121 };
122
123 // 账户选择弹框显示隐藏状态
124 const [chooseAccountOpen, setChooseAccountOpen] = useState(false);
125 const [accountListChoose, setAccountListChoose] = useState<any[]>([]);
126
127 // 下载文件到本地并发布
128 const downloadAndPublish = async (aList: any) => {
129 console.log('下载文件到本地并发布', aList, taskInfo);
130 if (!taskInfo || !taskInfo.dataInfo) {
131 message.error('任务信息不完整,无法发布');
132 return;
133 }
134
135 setDownloading(true);
136 try {
137 console.log('开始下载文件...');
138 message.loading('正在下载视频和封面图片,请稍候...', 0);
139
140 // 构建完整的文件URL
141 const videoUrl = FILE_BASE_URL + taskInfo.dataInfo.videoUrl;
142 const coverUrl = FILE_BASE_URL + taskInfo.imageUrl;
143
144 console.log('下载视频:', videoUrl);
145 console.log('下载封面:', coverUrl);
146
147 // 使用IPC接口下载视频文件到本地临时目录
148 const localVideoPath = await ipcDownFile(videoUrl);
149
150 // 使用IPC接口下载封面图片到本地临时目录
151 const localCoverPath = await ipcDownFile(coverUrl);
152
153 console.log('视频已下载到:', localVideoPath);
154 console.log('封面已下载到:', localCoverPath);
155
156 message.destroy();
157 message.success('文件下载完成,开始发布...');
158
159 // 创建一级记录
160 const recordRes = await icpCreatePubRecord({
161 title: taskInfo.dataInfo.title || '/',
162 desc: taskInfo.dataInfo.desc || '/',
163 type: PubType.VIDEO,
164 videoPath: localVideoPath,
165 coverPath: localCoverPath,
166 });
167
168 // 创建二级记录
169 for (const vData of aList) {
170 // console.log('vData', vData)
171 const account = vData;
172 // console.log('account', account)
173
174 await icpCreateVideoPubRecord({
175 type: account.type,
176 accountId: account.id,
177 pubRecordId: recordRes.id,
178 publishTime: new Date(),
179 title: taskInfo.dataInfo.title,
180 // topics: vData.pubParams.topics,
181 desc: taskInfo.dataInfo.desc,
182 videoPath: localVideoPath,
183 coverPath: localCoverPath,
184 });
185 }
186
187 // 发布视频
188 const okRes = await icpPubVideo(recordRes.id);
189
190 // 成功数据
191 const successList = okRes.filter((v) => v.code === 1);
192
193 console.log('发布成功', successList);
194 console.log('发布结果', okRes);
195
196 if (successList.length > 0) {
197 message.success(`成功发布到${successList.length}个平台`);
198 taskDone();
199 } else {
200 message.warning('发布完成,但没有成功发布的平台');
201 }
202 } catch (error) {
203 console.error('下载或发布过程中出错:', error);
204 message.error('下载或发布过程中出错,请重试');
205 } finally {
206 setDownloading(false);
207 setChooseAccountOpen(false);
208 }
209 };
210
211 // 打开视频播放器
212 const openVideoPlayer = (e: React.MouseEvent) => {
213 e.stopPropagation();
214 if (taskInfo?.dataInfo?.videoUrl) {
215 setVideoPlayback({
216 visible: true,
217 url: `${FILE_BASE_URL}${taskInfo.dataInfo.videoUrl}`,
218 title: taskInfo.title || '视频播放',
219 });
220 } else {
221 message.info('该任务暂无视频');
222 }
223 };
224
225 // 关闭视频播放器
226 const closeVideoPlayer = () => {
227 setVideoPlayback((prev) => ({ ...prev, visible: false }));
228 };
229
230 return (
231 <>
232 <VideoPlayer
233 videoUrl={videoPlayback.url}
234 visible={videoPlayback.visible}
235 onClose={closeVideoPlayer}
236 title={videoPlayback.title}
237 />
238
239 <ChooseAccountModule
240 open={chooseAccountOpen}
241 onClose={() => !downloading && setChooseAccountOpen(false)}
242 platChooseProps={{
243 choosedAccounts: accountListChoose,
244 pubType: PubType.VIDEO,
245 allowPlatSet: new Set([PlatType.Douyin]),
246 }}
247 onPlatConfirm={async (aList) => {
248 console.log('账号:', aList);
249 setAccountListChoose(aList);
250 console.log('选择的账号:', aList);
251 // 下载文件并发布
252 await downloadAndPublish(aList);
253 }}
254 />
255
256 <Modal
257 title={null}
258 open={isModalOpen}
259 onCancel={handleCancel}
260 footer={null}
261 width={800}
262 className={styles.taskInfoModal}
263 destroyOnClose
264 >
265 {taskInfo ? (
266 <div className={styles.taskInfoContainer}>
267 <div className={styles.taskInfoHeader}>
268 <h2 className={styles.taskTitle}>
269 {taskInfo.title}
270 <span className={styles.taskId}>
271 ID: {taskInfo._id}
272 <CopyOutlined
273 className={styles.copyIcon}
274 onClick={() => copyTaskId(taskInfo._id)}
275 />
276 </span>
277 </h2>
278 <Tag color="#a66ae4" className={styles.taskTag}>
279 {(taskInfo.dataInfo as any)?.type || '视频任务'}
280 </Tag>
281 </div>
282
283 <div className={styles.taskInfoContent}>
284 <Row gutter={24}>
285 <Col span={12}>
286 <div className={styles.videoContainer}>
287 <div
288 className={styles.taskImageContainer}
289 onClick={openVideoPlayer}
290 >
291 <img
292 src={`${FILE_BASE_URL}${taskInfo.imageUrl}`}
293 alt={taskInfo.title}
294 className={styles.taskImage}
295 />
296 <div className={styles.videoOverlay}>
297 <PlayCircleOutlined className={styles.playIcon} />
298 </div>
299 </div>
300 <div className={styles.videoCaption}>视频封面预览</div>
301 </div>
302 </Col>
303
304 <Col span={12}>
305 <div className={styles.taskDetails}>
306 <div className={styles.taskDetail}>
307 <TagOutlined className={styles.detailIcon} />
308 <span className={styles.detailLabel}>任务类型:</span>
309 <span className={styles.detailValue}>
310 {TaskTypeName.get(taskInfo.type) || '视频任务'}
311 </span>
312 </div>
313
314 <div className={styles.taskDetail}>
315 <ClockCircleOutlined className={styles.detailIcon} />
316 <span className={styles.detailLabel}>截止时间:</span>
317 <span className={styles.detailValue}>
318 {taskInfo.deadline || '2025/03/17'}
319 </span>
320 </div>
321
322 <div className={styles.taskDetail}>
323 <TeamOutlined className={styles.detailIcon} />
324 <span className={styles.detailLabel}>招募人数:</span>
325 <span className={styles.detailValue}>
326 {taskInfo.maxRecruits || 100}
327 </span>
328 </div>
329
330 <div className={styles.taskDetail}>
331 <DollarOutlined className={styles.detailIcon} />
332 <span className={styles.detailLabel}>任务奖励:</span>
333 <span className={styles.detailValue}>
334 ¥{taskInfo.reward || 5}
335 </span>
336 </div>
337
338 <div className={styles.taskDetail}>
339 <span className={styles.detailLabel}>任务要求:</span>
340 <span className={styles.detailValue}>
341 {taskInfo.requirement || '不许删文'}
342 </span>
343 </div>
344 </div>
345 </Col>
346 </Row>
347
348 <Divider />
349
350 <div className={styles.taskDescription}>
351 <h3 className={styles.sectionTitle}>任务描述</h3>
352 <div
353 className={styles.descriptionContent}
354 dangerouslySetInnerHTML={{
355 __html: taskInfo.description || '暂无描述',
356 }}
357 />
358 </div>
359
360 {taskInfo.dataInfo && (
361 <>
362 <Divider />
363 <div className={styles.videoDetails}>
364 <h3 className={styles.sectionTitle}>视频详情</h3>
365 <div className={styles.videoInfo}>
366 <div className={styles.videoInfoItem}>
367 <span className={styles.videoInfoLabel}>视频标题:</span>
368 <span className={styles.videoInfoValue}>
369 {taskInfo.dataInfo.title || '未知'}
370 </span>
371 </div>
372
373 <div className={styles.videoInfoItem}>
374 <span className={styles.videoInfoLabel}>视频描述:</span>
375 <span className={styles.videoInfoValue}>
376 {taskInfo.dataInfo.desc || '暂无描述'}
377 </span>
378 </div>
379 </div>
380 </div>
381 </>
382 )}
383 </div>
384
385 <div className={styles.taskInfoFooter}>
386 <div className={styles.taskStatus}>
387 <span className={styles.statusLabel}>任务状态:</span>
388 <Tag color="#a66ae4">
389 {TaskStatusName.get(taskInfo.status) || '进行中'}
390 </Tag>
391 </div>
392
393 <div className={styles.taskActions}>
394 <Button
395 key="back"
396 onClick={handleCancel}
397 className={styles.cancelButton}
398 >
399 取消
400 </Button>
401 <Button
402 key="submit"
403 type="primary"
404 disabled={taskInfo.isAccepted}
405 onClick={taskApply}
406 className={styles.applyButton}
407 style={{ backgroundColor: '#a66ae4', borderColor: '#a66ae4' }}
408 >
409 {taskInfo.isAccepted ? '已接受任务' : '接受任务'}
410 </Button>
411 </div>
412 </div>
413 </div>
414 ) : (
415 <div className={styles.noDataContainer}>暂无任务信息</div>
416 )}
417 </Modal>
418 </>
419 );
420 });
421
422 export default Com;
423
423 lines Plain Text