返回 AiToEarn
mineTask.tsx
根目录 / project / aitoearn-electron / src / views / task / mineTask.tsx
1 /*
2 * @Author: nevin
3 * @Date: 2025-02-27 19:37:08
4 * @LastEditTime: 2025-03-24 14:11:03
5 * @LastEditors: nevin
6 * @Description: 我的任务列表
7 */
8 import {
9 Button,
10 Card,
11 Spin,
12 Modal,
13 Select,
14 Space,
15 Tag,
16 Empty,
17 message,
18 } from 'antd';
19 import { useState, useEffect, useRef } from 'react';
20 import { useInView } from 'react-intersection-observer';
21 import { taskApi } from '@/api/task';
22 import { UserTask, UserTaskStatus } from '@/api/types/task';
23 import { Task, TaskDataInfo } from '@@/types/task';
24 import MineTaskInfo, { MineTaskInfoRef } from './components/mineInfo';
25 import { WithdrawRef } from './components/withdraw';
26 import Withdraw from './components/withdraw';
27 import styles from './mineTask.module.scss';
28 import {
29 ReloadOutlined,
30 FilterOutlined,
31 ClockCircleOutlined,
32 DollarOutlined,
33 FileTextOutlined,
34 RightOutlined,
35 QuestionCircleOutlined,
36 } from '@ant-design/icons';
37 import dayjs from 'dayjs';
38 const FILE_BASE_URL = import.meta.env.VITE_APP_FILE_HOST;
39
40 import bindTaskImg from '@/assets/task/binds.png';
41 import checkTaskImg from '@/assets/task/waits.png';
42 import publishTaskImg from '@/assets/task/fabu.png';
43 import rewardTaskImg from '@/assets/task/jiesuan.png';
44 import qr2 from '@/assets/task/qr0.jpg';
45 import logo from '@/assets/logo.png';
46
47 const UserTaskStatusNameMap = new Map<UserTaskStatus, string>([
48 [UserTaskStatus.DODING, '进行中'],
49 [UserTaskStatus.PENDING, '待审核'],
50 [UserTaskStatus.APPROVED, '已通过'],
51 [UserTaskStatus.REJECTED, '已拒绝'],
52 // [UserTaskStatus.COMPLETED, '已完成'],
53 [UserTaskStatus.CANCELLED, '已取消'],
54 // [UserTaskStatus.PENDING_REWARD, '待发放奖励'],
55 [UserTaskStatus.REWARDED, '已发放奖励'],
56 ]);
57
58 // 状态对应的颜色
59 const UserTaskStatusColorMap = new Map<UserTaskStatus, string>([
60 [UserTaskStatus.DODING, 'processing'],
61 [UserTaskStatus.PENDING, 'warning'],
62 [UserTaskStatus.APPROVED, 'success'],
63 [UserTaskStatus.REJECTED, 'error'],
64 [UserTaskStatus.CANCELLED, 'default'],
65 [UserTaskStatus.REWARDED, 'success'],
66 ]);
67
68 // 在文件顶部添加任务类型映射
69 const TASK_TYPE_MAP = {
70 video: { name: '视频任务', color: '#a66ae4' },
71 promotion: { name: '推广任务', color: '#1890ff' },
72 product: { name: '挂车市场任务', color: '#52c41a' },
73 article: { name: '文章任务', color: '#0958d9' },
74 };
75
76 // 渲染空状态
77 const renderEmptyState = () => {
78 return (
79 <div className={styles.emptyContainer}>
80 <div className={styles.emptyText}>
81 <h3>接单前请务必先浏览《接单指南与常见问题解答》</h3>
82 <p>所有关于接单的秘诀都在这里,请不要错过</p>
83 </div>
84
85 <div className={styles.guideContainer}>
86 <div className={styles.guideStep}>
87 <h4>接单前完成以下步骤,接单快人一步</h4>
88
89 <div className={styles.qrCodeContainer}>
90
91
92 <div className={styles.qrCodeItem}>
93 <div className={styles.qrCodeWrapper}>
94 <img
95 src={qr2}
96 alt="扫码开启更多权益"
97 className={styles.qrCode}
98 />
99 </div>
100 <p className={styles.qrCodeText}>扫码开启更多权益</p>
101 <p className={styles.qrCodeSubtext}>手机扫码哎哟赚公众号</p>
102 </div>
103 </div>
104 </div>
105
106 <div className={styles.processContainer}>
107 <h4 className={styles.processTitle}>接单流程</h4>
108
109 <div className={styles.processSteps}>
110 <div className={styles.processStep}>
111 <div className={styles.processIcon}>
112 <img
113 src={bindTaskImg}
114 alt="前往管理中心绑定账号"
115 className={styles.stepIcon}
116 />
117 </div>
118 <p className={styles.stepText}>前往管理中心绑定账号</p>
119 </div>
120
121 <div className={styles.processDivider}></div>
122
123 <div className={styles.processStep}>
124 <div className={styles.processIcon}>
125 <img
126 src={checkTaskImg}
127 alt="等待平台验证通过"
128 className={styles.stepIcon}
129 />
130 </div>
131 <p className={styles.stepText}>等待平台验证通过</p>
132 </div>
133
134 <div className={styles.processDivider}></div>
135
136 <div className={styles.processStep}>
137 <div className={styles.processIcon}>
138 <img
139 src={publishTaskImg}
140 alt="一键发文"
141 className={styles.stepIcon}
142 />
143 </div>
144 <p className={styles.stepText}>一键发文</p>
145 </div>
146
147 <div className={styles.processDivider}></div>
148
149 <div className={styles.processStep}>
150 <div className={styles.processIcon}>
151 <img
152 src={rewardTaskImg}
153 alt="成功发文等待结算"
154 className={styles.stepIcon}
155 />
156 </div>
157 <p className={styles.stepText}>成功发文等待结算</p>
158 </div>
159 </div>
160 </div>
161 </div>
162 </div>
163 );
164 };
165
166 export default function Page() {
167 const [taskList, setTaskList] = useState<any>([]);
168 const [pageInfo, setPageInfo] = useState({
169 pageSize: 20,
170 page: 1,
171 totalCount: 0,
172 });
173 const [loading, setLoading] = useState(true);
174 const [isGuideModalVisible, setIsGuideModalVisible] = useState(false);
175 const [statusFilter, setStatusFilter] = useState<UserTaskStatus | null>(null);
176 const [hasMore, setHasMore] = useState(true);
177
178 // 使用 react-intersection-observer 创建一个观察器
179 const { ref: loadMoreRef, inView } = useInView({
180 threshold: 0.1,
181 triggerOnce: false,
182 rootMargin: '100px 0px',
183 });
184
185 // 当底部元素进入视图时加载更多数据
186 useEffect(() => {
187 if (inView && hasMore && !loading) {
188 loadMore();
189 }
190 }, [inView, hasMore, loading]);
191
192 const fetchTaskDetails = async (isLoadMore = false, pageInfoParam: any) => {
193 setLoading(true);
194 try {
195 const params = {
196 ...(pageInfoParam || pageInfo),
197 status: statusFilter,
198 };
199
200 // 如果没有选择状态筛选,则不传status参数
201 if (statusFilter === null) {
202 delete (params as any).status;
203 }
204
205 const tasks = await taskApi.getMineTaskList(params as any);
206
207 if (isLoadMore) {
208 setTaskList((prev:any) => [...prev, ...tasks.items]);
209 } else {
210 setTaskList(tasks.items);
211 }
212
213 setPageInfo((prev) => ({
214 ...prev,
215 totalCount: (tasks as any).meta.totalItems,
216 }));
217
218 // 检查是否还有更多数据
219 setHasMore(
220 pageInfo.page * pageInfo.pageSize < (tasks as any).meta.totalItems,
221 );
222 } catch (error) {
223 console.error('获取任务列表失败', error);
224 message.error('获取任务列表失败,请稍后再试');
225 } finally {
226 setLoading(false);
227 }
228 };
229
230 useEffect(() => {
231 loadMore()
232 }, [statusFilter]);
233
234 // 加载更多数据
235 const loadMore = async () => {
236 // const nextPage = pageInfo.page + 1;
237 setPageInfo((prev) => ({
238 ...prev,
239 page: prev.page + 1,
240 }));
241 console.log('pageInfo', pageInfo)
242 await fetchTaskDetails(true, false);
243 };
244
245 // 刷新数据
246 const refreshData = async () => {
247 let pageInfo = {
248 pageSize: 10,
249 page: 1,
250 totalCount: 0,
251 }
252 setPageInfo(pageInfo);
253 await fetchTaskDetails(false, pageInfo);
254 message.success('数据已刷新');
255 };
256
257 const Ref_MineTaskInfo = useRef<MineTaskInfoRef>(null);
258 const Ref_Withdraw = useRef<WithdrawRef>(null);
259
260 async function withdraw(task: UserTask<Task<TaskDataInfo>>) {
261 Ref_Withdraw.current?.init(task);
262 }
263
264 // 打开指南弹窗
265 const openGuideModal = () => {
266 setIsGuideModalVisible(true);
267 };
268
269 // 关闭指南弹窗
270 const closeGuideModal = () => {
271 setIsGuideModalVisible(false);
272 };
273
274 // 格式化日期
275 const formatDate = (dateString?: string) => {
276 if (!dateString) return '暂无日期';
277 return dayjs(dateString).format('YYYY/MM/DD HH:mm');
278 };
279
280 // 查看任务详情
281 const viewTaskDetail = (task: UserTask<Task<TaskDataInfo>>) => {
282 Ref_MineTaskInfo.current?.init(task);
283 };
284
285
286 return (
287 <div className={styles.mineTaskContainer}>
288 {/* <MineTaskInfo ref={Ref_MineTaskInfo} onTaskSubmitted={refreshTaskList} /> */}
289 <MineTaskInfo ref={Ref_MineTaskInfo} />
290 <Withdraw ref={Ref_Withdraw} />
291
292 <div className={styles.pageHeader}>
293 <h2 className={styles.pageTitle}>我的任务</h2>
294 <div className={styles.pageActions}>
295 <Space size={12}>
296 <Select
297 className={styles.statusFilter}
298 placeholder="筛选任务状态"
299 allowClear
300 style={{ width: 150 }}
301 onChange={(value) => setStatusFilter(value)}
302 value={statusFilter}
303 options={Array.from(UserTaskStatusNameMap).map(
304 ([value, label]) => ({
305 value,
306 label,
307 }),
308 )}
309 suffixIcon={<FilterOutlined />}
310 />
311 <Button
312 className={styles.refreshButton}
313 icon={<ReloadOutlined />}
314 onClick={refreshData}
315 >
316 刷新
317 </Button>
318 <Button
319 type="primary"
320 onClick={openGuideModal}
321 icon={<QuestionCircleOutlined />}
322 >
323 接单指南
324 </Button>
325 </Space>
326 </div>
327 </div>
328
329 {loading && taskList.length === 0 ? (
330 <div className={styles.loadingContainer}>
331 <Spin size="large" />
332 </div>
333 ) : taskList.length == 0 ? (
334 <div className={styles.emptyContainer}>
335 <Empty description={false} />
336 <div className={styles.emptyText}>
337 <p>您还没有接受任何任务,可以前往任务市场查看更多任务</p>
338 </div>
339 {renderEmptyState()}
340 </div>
341 ) : (
342 <div className={styles.taskList}>
343 {taskList.map((task:any) => {
344 // 获取任务详情,处理可能的undefined情况
345 const taskDetail = task.taskId || {};
346
347 return (
348 <Card key={task._id} className={styles.taskCard} bordered={false}>
349 <div className={styles.taskCardContent}>
350 {/* 添加任务图片显示 */}
351 <div className={styles.taskImageContainer}>
352 <img
353 src={` ${ task.taskId?.imageUrl ? FILE_BASE_URL+ task.taskId?.imageUrl: logo }`}
354 alt={taskDetail.title}
355 className={styles.taskImage}
356 />
357 </div>
358
359 <div className={styles.taskInfo}>
360 <div className={styles.taskHeader}>
361 <h3 className={styles.taskTitle}>
362 {taskDetail.title || '未知任务'}
363 <span className={styles.taskId}>
364 订单号: {task._id}
365 </span>
366 </h3>
367 <div className={styles.tagContainer}>
368 {/* 添加任务类型标签 */}
369 <Tag
370 color={
371 (TASK_TYPE_MAP as any)[task.taskId?.type]?.color ||
372 '#a66ae4'
373 }
374 className={styles.typeTag}
375 >
376 {(TASK_TYPE_MAP as any)[task.taskId?.type]?.name ||
377 '视频任务'}
378 </Tag>
379
380 {/* 原有的状态标签 */}
381 <Tag
382 color={
383 UserTaskStatusColorMap.get(
384 task.status as UserTaskStatus,
385 ) || 'default'
386 }
387 className={styles.statusTag}
388 >
389 {UserTaskStatusNameMap.get(
390 task.status as UserTaskStatus,
391 ) || '未知状态'}
392 </Tag>
393 </div>
394 </div>
395
396 <div className={styles.taskDetails}>
397 <div className={styles.taskDetail}>
398 <ClockCircleOutlined className={styles.detailIcon} />
399 <span className={styles.detailLabel}>接单时间:</span>
400 <span className={styles.detailValue}>
401 {formatDate(task.createTime)}
402 </span>
403 </div>
404
405 <div className={styles.taskDetail}>
406 <FileTextOutlined className={styles.detailIcon} />
407 <span className={styles.detailLabel}>任务要求:</span>
408 <span className={styles.detailValue}>
409 {taskDetail.requirement || '不许删文'}
410 </span>
411 </div>
412
413 <div className={styles.taskDetail}>
414 <DollarOutlined className={styles.detailIcon} />
415 <span className={styles.detailLabel}>任务奖励:</span>
416 <span className={styles.detailValue}>
417 ¥{taskDetail.reward || 5}
418 </span>
419 </div>
420 </div>
421 </div>
422
423 <div className={styles.taskAction}>
424 {/* {(task.status === UserTaskStatus.APPROVED ||
425 task.status === UserTaskStatus.COMPLETED ||
426 task.status === UserTaskStatus.REWARDED) && (
427 <Button
428 type="primary"
429 danger
430 className={styles.withdrawButton}
431 style={{
432 backgroundColor: '#a66ae4',
433 borderColor: '#a66ae4',
434 color: 'white',
435 marginBottom: '10px',
436 }}
437 onClick={() => withdraw(task)}
438 >
439 申请提现 <RightOutlined />
440 </Button>
441 )} */}
442
443 <Button
444 className={styles.viewButton}
445 onClick={() => viewTaskDetail(task)}
446 style={{
447 backgroundColor: '#a66ae4',
448 borderColor: '#a66ae4',
449 color: 'white',
450 }}
451 >
452 查看详情 <RightOutlined />
453 </Button>
454 </div>
455 </div>
456 </Card>
457 );
458 })}
459
460 {loading && (
461 <div className={styles.loadingContainer}>
462 <Spin size="large" />
463 </div>
464 )}
465
466 {/* 添加加载更多触发器 */}
467 <div ref={loadMoreRef} className={styles.loadMoreTrigger}>
468 {!loading && hasMore && (
469 <div className={styles.loadMoreContainer}>
470 <Button type="link" loading={loading}>
471 {loading ? '加载中...' : '加载更多'}
472 </Button>
473 </div>
474 )}
475 {!loading && !hasMore && taskList.length > 0 && (
476 <div className={styles.loadMoreContainer}>
477 <span style={{ color: '#999' }}>没有更多任务了</span>
478 </div>
479 )}
480 </div>
481 </div>
482 )}
483
484 <Modal
485 title="接单指南"
486 open={isGuideModalVisible}
487 onCancel={closeGuideModal}
488 footer={null}
489 width={800}
490 className={styles.guideModal}
491 >
492 <div className={styles.guideContainer}>
493 <div className={styles.guideHeader}>
494 <h2>如何接单赚钱</h2>
495 <div className={styles.guideDate}>更新时间: 2025年4月16日</div>
496 </div>
497
498 <div className={styles.guideIntro}>
499 <p>
500 尊敬的AiToEarn流量主用户:
501 </p>
502 <p>
503 您好,为了更好地保障广告主和流量主的权益,建立一个健康、公平的广告交易系统,请仔细阅读并确认以下"《AiToEarn任务市场接单合作须知》"后再操作接单:
504 </p>
505 <p>
506 AiToEarn任务市场服务:指AiToEarn为流量主和广告主提供的撮合交易以及技术支持服务。AiToEarn在此过程中提供平台的运营与维护,监督管理平台内经营主体及交易行为,提升广告主与流量主交易体验。
507 </p>
508 </div>
509
510 <div className={styles.guideSection}>
511 <h3>操作步骤</h3>
512 <ol className={styles.guideSteps}>
513 <li>
514 <p>关注公众号"任务市场",获取最新任务通知和平台动态。</p>
515 <div className={styles.qrCodeContainer}>
516 <div className={styles.qrCodeItem}>
517 <div className={styles.qrCodeWrapper}>
518 <img
519 src={qr2}
520 alt="任务市场公众号"
521 className={styles.qrCode}
522 />
523 </div>
524 <div className={styles.qrCodeText}>任务市场公众号</div>
525 <div className={styles.qrCodeSubtext}>获取最新任务通知</div>
526 </div>
527 </div>
528 </li>
529 <li>
530 <p>点击下方链接下载哎哟赚接单软件(已下载可忽略)</p>
531 <p className={styles.guideLink}>https://att.yikart.cn/aiToEarn</p>
532 </li>
533 <li>
534 <p>
535 【账号】添加小红书、抖音、视频号、B站等平台账号,等待3天左右,可以前往【任务市场】查看任务和接单(如下图)
536 </p>
537 <div className={styles.guideImageContainer}>
538 <img
539 src={bindTaskImg}
540 alt="管理中心-添加账号"
541 className={styles.guideImage}
542 />
543 <p className={styles.guideImageCaption}>管理中心-添加账号</p>
544 </div>
545 <p>列表会展示添加成功的账号</p>
546 <div className={styles.guideImageContainer}>
547 <img
548 src={checkTaskImg}
549 alt="绑定账号后等待3天左右会显示项目"
550 className={styles.guideImage}
551 />
552 <p className={styles.guideImageCaption}>
553 绑定账号后等待3天左右会显示项目
554 </p>
555 </div>
556 </li>
557 <li>
558 <p>
559 耐心等待任务下发,任务下发后,请仔细判断是否符合自己账号调性,确认合适再进行发布,一旦发布不可删除,否则必须补发且影响您后续得到派单。
560 </p>
561 </li>
562 <li>
563 <p>
564 建议日常无单时积极运营账号,保持真诚、分享、有用的人设,有利于后续接到更多高质量的订单。连续接广告无原创内容、不经常更新、批量做矩阵账号都是封号高危原因,对后续发展不利。
565 </p>
566 </li>
567 </ol>
568 </div>
569
570 <div className={styles.guideSection}>
571 <h3>常见问题</h3>
572 <div className={styles.guideFaq}>
573 <div className={styles.faqItem}>
574 <h4>一.关于流量主账号</h4>
575 <div className={styles.faqAnswer}>
576 <p>
577 1.1流量主须自行负责在AiToEarn平台的用户账号和密码,且须对在用户账号密码下发生的所有活动(包括但不限于发布需求信息、网上点击同意各类协议、规则、参与需求投标等)承担责任。流量主有权根据需要更改登录和账户提现密码。如因流量主的过错导致的任何损失由流量主自行承担,该过错包括但不限于:不按照交易提示操作,未及时进行交易操作,遗忘或泄漏密码等。
578 </p>
579 <p >
580 1.2流量主应当注册账户时提供真实准确的注册信息,包括但不限于真实姓名、身份证号、联系电话、地址、邮政编码等,保证AiToEarn工作人员可以通过上述联系方式与流量主进行联系。与此同时,流量主也应当在相关资料实际变更时及时更新有关注册资料。
581 </p>
582 </div>
583 </div>
584
585 <div className={styles.faqItem}>
586 <h4>二.推广内容审核说明</h4>
587 <div className={styles.faqAnswer}>
588 <p>
589 2.1针对推广内容,AiToEarn任务市场平台有权对广告主投放内容进行初步审核,并对其中明显涉嫌违反法律法规的内容进行强制性处理,但平台的审核仅为形式审核,重在参照广告法条款进行审核,但因每个社交媒体平台自身规则差异造成的内容违规或下架,AiToEarn不承担相关责任。
590 </p>
591 <p>2.2流量主在接受推广任务前,请自行判断可能带来的不确定性风险,我方不承担任何因推广素材造成的包括但不限于系统删文、禁言及封号、隐藏限流等责任。</p>
592 </div>
593 </div>
594
595 <div className={styles.faqItem}>
596 <h4>三.发布与结算</h4>
597 <div className={styles.faqAnswer}>
598 <p>3.1流量主如约完成推送后,推广信息因相关政策或其它非流量主主动因素导致被删除(包括但不限于:用户举报、平台通知配合删除等),如账号数据正常、账号质量过关且在结算期内,AiToEarn任务市场平台应当按照统计金额向乙方支付费用。</p>
599 <p>3.2流量主发布订单内容后,因自身原因人为删除、隐藏、账号封禁等情况导致广告主推广内容不能正常展示推广的,须按照对应订单要求补发或协商处理,否则会受到相应接单限制或处罚。</p>
600 <p>3.3流量主不得擅自更改接单推广信息,包括但不限于标题、图片、视频、音频、文字等(与AiToEarn任务市场工作人员沟通确认后的更改除外),如擅自更改将不予结算并影响接单权限。</p>
601 <p>3.4AiToEarn任务市场订单实行发布后结算方式,结算金额实时到达流量主AiToEarn账户,流量主自行提现操作,平台不收取提现服务费。</p>
602 </div>
603 </div>
604
605 <div className={styles.faqItem}>
606 <h4>违规提示或账号封禁怎么处理?</h4>
607 <div className={styles.faqAnswer}>
608 <p>将订单号和违规截图私聊发给工作人员。</p>
609 <p>如未回复消息请耐心等待(工作时间:工作日9:00-18:00)</p>
610 <p>前往哎哟赚查看更多新媒体相关信息</p>
611 </div>
612 </div>
613 </div>
614 </div>
615 </div>
616 </Modal>
617 </div>
618 );
619 }
620
620 lines Plain Text