返回 AiToEarn
interactionTask.tsx
根目录 / project / aitoearn-electron / src / views / task / interactionTask.tsx
1 /*
2 * @Author: nevin
3 * @Date: 2025-03-03 10:00:00
4 * @LastEditTime: 2025-03-03 10:00:00
5 * @LastEditors: nevin
6 * @Description: 互动任务组件
7 */
8 import {
9 Card,
10 List,
11 Typography,
12 Button,
13 Space,
14 Tag,
15 Spin,
16 Modal,
17 Descriptions,
18 message,
19 Progress,
20 Image,
21 notification,
22 } from 'antd';
23 import {
24 UserOutlined,
25 ClockCircleOutlined,
26 CheckCircleOutlined,
27 } from '@ant-design/icons';
28 import styles from './task.module.scss';
29 import { useState, useEffect, useRef } from 'react';
30 import { useInView } from 'react-intersection-observer';
31 import { taskApi } from '@/api/task';
32 import { TaskVideo } from '@@/types/task';
33 import dayjs from 'dayjs';
34 import { TaskInfoRef } from './components/popInfo';
35 import ChooseAccountModule from '@/views/publish/components/ChooseAccountModule/ChooseAccountModule';
36 import { PubType } from '@@/publish/PublishEnum';
37 import { icpCreateInteractionOneKey } from '@/icp/replyother';
38 import { useNavigate } from 'react-router-dom';
39
40 // 导入平台图标
41 import KwaiIcon from '../../assets/svgs/account/ks.svg';
42 import WxSphIcon from '../../assets/svgs/account/wx-sph.svg';
43 import XhsIcon from '../../assets/svgs/account/xhs.svg';
44 import DouyinIcon from '../../assets/svgs/account/douyin.svg';
45 import logo from '@/assets/logo.png';
46 import { onInteractionProgress } from '../../icp/receiveMsg';
47
48 const { Title, Text } = Typography;
49
50 const FILE_BASE_URL = import.meta.env.VITE_APP_FILE_HOST;
51
52 // 平台配置
53 const platformConfig = {
54 KWAI: {
55 name: '快手',
56 icon: KwaiIcon,
57 color: '#FF4D4F',
58 },
59 wxSph: {
60 name: '微信视频号',
61 icon: WxSphIcon,
62 color: '#07C160',
63 },
64 xhs: {
65 name: '小红书',
66 icon: XhsIcon,
67 color: '#FF2442',
68 },
69 douyin: {
70 name: '抖音',
71 icon: DouyinIcon,
72 color: '#000000',
73 },
74 };
75
76 export default function InteractionTask() {
77 const [loading, setLoading] = useState(false);
78 const [taskList, setTaskList] = useState<any[]>([]);
79 const [pageInfo, setPageInfo] = useState({
80 pageNo: 1,
81 pageSize: 12,
82 totalCount: 0,
83 });
84 const [hasMore, setHasMore] = useState(true);
85 const selectedTaskRef = useRef<any>(null);
86 const [selectedTask, setSelectedTask] = useState<any>(null);
87 const [modalVisible, setModalVisible] = useState(false);
88 const [chooseAccountOpen, setChooseAccountOpen] = useState(false);
89 const [accountListChoose, setAccountListChoose] = useState<any[]>([]);
90 const [downloading, setDownloading] = useState(false);
91 const navigate = useNavigate();
92
93 const Ref_TaskInfo = useRef<TaskInfoRef>(null);
94
95 // 使用 react-intersection-observer 监听底部元素
96 const { ref: loadMoreRef, inView } = useInView({
97 threshold: 0.2,
98 triggerOnce: false,
99 });
100
101 // 当底部元素进入视图时加载更多数据
102 useEffect(() => {
103 if (inView && hasMore && !loading) {
104 loadMore();
105 }
106 }, [inView, hasMore, loading]);
107
108 // 加载更多数据
109 const loadMore = async () => {
110 if (loading || !hasMore) return;
111
112 setLoading(true);
113 try {
114 const nextPage = pageInfo.pageNo + 1;
115 setPageInfo((prev) => ({ ...prev, pageNo: nextPage }));
116 await getTaskList(true);
117 } catch (error) {
118 console.error('加载更多失败:', error);
119 } finally {
120 setLoading(false);
121 }
122 };
123
124 // 初始加载数据
125 useEffect(() => {
126 getTaskList();
127 }, []);
128
129 // 任务进度监听
130 useEffect(() => {
131 const unload = onInteractionProgress((args) => {
132 if (args.status === 1) {
133 taskDone();
134 notification.open({
135 message: '互动任务完成',
136 });
137 }
138 });
139 return () => {
140 unload();
141 };
142 }, []);
143
144 async function getTaskList(isLoadMore = false) {
145 setLoading(true);
146 try {
147 const res = await taskApi.getTaskList<any>({
148 ...pageInfo,
149 // pageSize: 100,
150 // type: TaskType.INTERACTION,
151 });
152
153 if (isLoadMore) {
154 setTaskList((prev) => [...prev, ...res.items]);
155 } else {
156 setTaskList(res.items);
157 }
158
159 setPageInfo((prev) => ({
160 ...prev,
161 totalCount: (res as any).totalCount,
162 }));
163
164 setHasMore(pageInfo.pageNo * pageInfo.pageSize < (res as any).totalCount);
165 } catch (error) {
166 console.error('获取任务列表失败', error);
167 } finally {
168 setLoading(false);
169 }
170 }
171
172 const formatDate = (date: string) => {
173 return dayjs(date).format('YYYY-MM-DD HH:mm');
174 };
175
176 const getPlatformTags = (accountTypes: string[]) => {
177 if (!accountTypes || accountTypes.length === 0) return null;
178
179 return accountTypes.map((type) => {
180 const platform = platformConfig[type as keyof typeof platformConfig];
181 if (!platform) return null;
182
183 return (
184 <div
185 key={type}
186 className={styles.platformIconWrapper}
187 style={{ backgroundColor: platform.color }}
188 >
189 <img
190 src={platform.icon}
191 className={styles.platformIcon}
192 alt={platform.name}
193 />
194 </div>
195 );
196 });
197 };
198
199 const handleJoinTask = (task: any) => {
200 setSelectedTask(task);
201 setModalVisible(true);
202 };
203
204 const handleCompleteTask = async () => {
205 if (!selectedTask) return;
206 setModalVisible(false);
207 setChooseAccountOpen(true);
208 };
209
210 // 在组件内添加一个新的状态来存储任务记录
211 const [taskRecord, setTaskRecord] = useState<{
212 _id: string;
213 createTime: string;
214 isFirstTimeSubmission: boolean;
215 status: string;
216 taskId: string;
217 } | null>(null);
218
219 /**
220 * 接受任务
221 */
222 async function taskApply() {}
223
224 useEffect(() => {
225 selectedTaskRef.current = selectedTask;
226 }, [selectedTask]);
227
228 /**
229 * 完成任务
230 */
231 async function taskDone() {
232 console.log('taskDone执行:', selectedTaskRef.current);
233 if (!selectedTaskRef.current) return;
234 const selectedTask = selectedTaskRef.current;
235 if (!selectedTask || !taskRecord) {
236 // message.error('任务信息不完整,无法完成任务');
237 return;
238 }
239
240 try {
241 // 使用任务记录的 ID 而不是任务 ID
242 const res = await taskApi.taskDone(taskRecord._id, {
243 submissionUrl: selectedTask.title,
244 screenshotUrls: [selectedTask.dataInfo?.imageList?.[0] || ''],
245 qrCodeScanResult: selectedTask.title,
246 });
247 message.success('任务发布成功!');
248 refreshTaskList();
249 } catch (error) {
250 message.error('完成任务失败,请稍后再试');
251 }
252 }
253
254 const handleInteraction = async (account: any) => {
255 console.log('account', account.id);
256 console.log('selectedTask', selectedTask.dataInfo);
257 console.log('selectedTask.description', selectedTask.description);
258 console.log('selectedTask.accountTypes', account.type);
259
260 const option: any = {
261 platform: account.type,
262 };
263
264 if (selectedTask.dataInfo?.commentContent) {
265 option.commentContent = selectedTask.dataInfo?.commentContent;
266 }
267
268 try {
269 setLoading(true);
270 const res: any = await icpCreateInteractionOneKey(
271 account.id,
272 [
273 {
274 dataId: selectedTask.dataInfo?.worksId,
275 readCount: 0,
276 likeCount: 0,
277 collectCount: 0,
278 forwardCount: 0,
279 commentCount: 0, // 评论数量
280 income: 0,
281 title: selectedTask.dataInfo?.title || '',
282 desc: selectedTask.dataInfo?.title || '',
283 authorId: selectedTask.dataInfo?.authorId || '',
284 author: {
285 id: selectedTask.dataInfo?.authorId || '',
286 },
287 option: {
288 xsec_token: 'ABQgeOn-14sjhmCALp9dEISLZrOOyDdGZwKtr2umjsWeo=',
289 },
290 },
291 ],
292 option,
293 );
294
295 console.log('handleInteraction', 'res', res);
296
297 // if (res.code === 1) {
298 // message.success('互动任务完成成功');
299 // // 更新任务状态
300 // setTaskList(prev => prev.map(task =>
301 // task._id === selectedTask._id ? { ...task, isAccepted: true } : task
302 // ));
303 // } else {
304 // message.error('互动任务完成失败');
305 // }
306 } catch (error) {
307 console.error('互动任务失败', error);
308 message.error('互动任务失败,请重试');
309 } finally {
310 setLoading(false);
311 setChooseAccountOpen(false);
312 }
313 };
314
315 // 刷新任务列表的函数
316 const refreshTaskList = () => {
317 setPageInfo({
318 pageSize: 10,
319 pageNo: 1,
320 totalCount: 0,
321 });
322 getTaskList();
323 };
324
325 return (
326 <div className={styles.taskList}>
327 <ChooseAccountModule
328 open={chooseAccountOpen}
329 onClose={() => !downloading && setChooseAccountOpen(false)}
330 platChooseProps={{
331 choosedAccounts: accountListChoose,
332 pubType: PubType.VIDEO,
333 allowPlatSet: new Set(selectedTask?.accountTypes || []) as any,
334 }}
335 onPlatConfirm={async (aList) => {
336 console.log('账号:', aList);
337 setAccountListChoose(aList);
338 setChooseAccountOpen(false);
339 await handleInteraction(aList[0]);
340 }}
341 />
342
343 <Spin spinning={loading}>
344 <List
345 grid={{
346 gutter: 8,
347 xs: 1,
348 sm: 2,
349 md: 3,
350 lg: 4,
351 xl: 5,
352 xxl: 6,
353 }}
354 dataSource={taskList}
355 renderItem={(item) => (
356 <List.Item>
357 <Card
358 className={styles.taskCard}
359 variant="outlined"
360 cover={
361 <div className={styles.taskImage}>
362 <Image
363 src={item.imageUrl ? FILE_BASE_URL + item.imageUrl : logo}
364 alt="logo"
365 preview={false}
366 style={{
367 width: '100%',
368 height: '200px',
369 objectFit: 'contain',
370 }}
371 />
372 </div>
373 }
374 actions={[
375 <Space key="recruits">
376 <UserOutlined />
377 <Text>
378 {item.currentRecruits}/{item.maxRecruits}
379 </Text>
380 </Space>,
381 <Space key="time">
382 <ClockCircleOutlined />
383 <Text>{item.keepTime}分钟</Text>
384 </Space>,
385 <Button
386 type="primary"
387 key="join"
388 disabled={item.isAccepted}
389 onClick={() => handleJoinTask(item)}
390 >
391 {item.isAccepted ? '已参与' : '参与任务'}
392 </Button>,
393 ]}
394 >
395 <Card.Meta
396 title={
397 <div className={styles.taskTitle}>
398 <Title level={5}>{item.title}</Title>
399 <Space>
400 <Tag color="green">¥{item.reward}</Tag>
401 <Space size={4}>
402 {getPlatformTags(item.accountTypes)}
403 </Space>
404 </Space>
405 </div>
406 }
407 description={
408 <div className={styles.taskInfo}>
409 <div className={styles.taskProgress}>
410 <Progress
411 percent={Math.round(
412 (item.currentRecruits / item.maxRecruits) * 100,
413 )}
414 size="small"
415 showInfo={false}
416 />
417 </div>
418 <Text type="secondary">{item.description}</Text>
419 <div className={styles.taskDeadline}>
420 <Text type="secondary">
421 截止时间:{formatDate(item.deadline)}
422 </Text>
423 </div>
424 </div>
425 }
426 />
427 </Card>
428 </List.Item>
429 )}
430 />
431 </Spin>
432
433 {/* 底部加载更多触发器 */}
434 <div ref={loadMoreRef} className={styles.loadMoreTrigger}>
435 {hasMore && (
436 <div className={styles.loadMoreContainer}>
437 <Button type="link" loading={loading} onClick={loadMore}>
438 {loading ? '加载中...' : '加载更多'}
439 </Button>
440 </div>
441 )}
442 </div>
443
444 <Modal
445 title="任务详情"
446 open={modalVisible}
447 onCancel={() => setModalVisible(false)}
448 footer={[
449 <Button key="cancel" onClick={() => setModalVisible(false)}>
450 取消
451 </Button>,
452 <Button
453 key="complete"
454 type="primary"
455 icon={<CheckCircleOutlined />}
456 onClick={taskApply}
457 >
458 一键完成
459 </Button>,
460 ]}
461 width={600}
462 >
463 {selectedTask && (
464 <div className={styles.taskDetail}>
465 <Descriptions column={1}>
466 <Descriptions.Item label="任务标题">
467 {selectedTask.title}
468 </Descriptions.Item>
469 <Descriptions.Item label="任务描述">
470 <div
471 dangerouslySetInnerHTML={{ __html: selectedTask.description }}
472 />
473 </Descriptions.Item>
474 <Descriptions.Item label="任务奖励">
475 ¥{selectedTask.reward}
476 </Descriptions.Item>
477 <Descriptions.Item label="评论内容">
478 {selectedTask.dataInfo?.commentContent || 'AI智能评论'}
479 </Descriptions.Item>
480 <Descriptions.Item label="作品ID">
481 {selectedTask.dataInfo?.worksId || ''}
482 </Descriptions.Item>
483 <Descriptions.Item label="任务时长">
484 {selectedTask.keepTime}分钟
485 </Descriptions.Item>
486 <Descriptions.Item label="开始时间">
487 {formatDate(selectedTask.createTime)}
488 </Descriptions.Item>
489 <Descriptions.Item label="截止时间">
490 {formatDate(selectedTask.deadline)}
491 </Descriptions.Item>
492 <Descriptions.Item label="参与人数">
493 {selectedTask.currentRecruits}/{selectedTask.maxRecruits}
494 </Descriptions.Item>
495 <Descriptions.Item label="支持平台">
496 <Space size={4}>
497 {getPlatformTags(selectedTask.accountTypes)}
498 </Space>
499 </Descriptions.Item>
500 </Descriptions>
501 </div>
502 )}
503 </Modal>
504 </div>
505 );
506 }
507
507 lines Plain Text