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