| 1 | /* |
| 2 | * @Author: nevin |
| 3 | * @Date: 2025-02-10 22:20:15 |
| 4 | * @LastEditTime: 2025-03-02 00:17:11 |
| 5 | * @LastEditors: nevin |
| 6 | * @Description: 视频任务 |
| 7 | */ |
| 8 | import { Button, Card, Tag, Spin, message, Tooltip } from 'antd'; |
| 9 | import { useState, useEffect, useRef } from 'react'; |
| 10 | import { Task, TaskType, TaskVideo } from '@@/types/task'; |
| 11 | import { taskApi } from '@/api/task'; |
| 12 | import { TaskInfoRef } from './components/popInfo'; |
| 13 | import TaskInfo from './components/videoInfo'; |
| 14 | import styles from './videoTask.module.scss'; |
| 15 | import { |
| 16 | ClockCircleOutlined, |
| 17 | TeamOutlined, |
| 18 | RightOutlined, |
| 19 | PlayCircleOutlined, |
| 20 | CopyOutlined, |
| 21 | } from '@ant-design/icons'; |
| 22 | import dayjs from 'dayjs'; |
| 23 | import VideoPlayer from '@/components/VideoPlayer'; |
| 24 | |
| 25 | // 导入平台图标 |
| 26 | import KwaiIcon from '../../assets/svgs/account/ks.svg'; |
| 27 | import WxSphIcon from '../../assets/svgs/account/wx-sph.svg'; |
| 28 | import XhsIcon from '../../assets/svgs/account/xhs.svg'; |
| 29 | import DouyinIcon from '../../assets/svgs/account/douyin.svg'; |
| 30 | |
| 31 | const FILE_BASE_URL = import.meta.env.VITE_APP_FILE_HOST; |
| 32 | |
| 33 | // 平台类型映射 |
| 34 | const PLATFORM_MAP = { |
| 35 | KWAI: { name: '快手', color: '#FF5000', icon: KwaiIcon }, |
| 36 | wxSph: { name: '视频号', color: '#FA9A32', icon: WxSphIcon }, |
| 37 | xhs: { name: '小红书', color: '#fe2c55', icon: XhsIcon }, |
| 38 | douyin: { name: '抖音', color: '#000000', icon: DouyinIcon }, |
| 39 | }; |
| 40 | |
| 41 | export default function Page() { |
| 42 | const [taskList, setTaskList] = useState<Task<TaskVideo>[]>([]); |
| 43 | const [pageInfo, setPageInfo] = useState({ |
| 44 | pageSize: 10, |
| 45 | pageNo: 1, |
| 46 | totalCount: 0, |
| 47 | }); |
| 48 | const [loading, setLoading] = useState(true); |
| 49 | const [hasMore, setHasMore] = useState(true); |
| 50 | |
| 51 | const Ref_TaskInfo = useRef<TaskInfoRef>(null); |
| 52 | |
| 53 | // 添加视频播放状态 |
| 54 | const [videoPlayback, setVideoPlayback] = useState<{ |
| 55 | visible: boolean; |
| 56 | url: string; |
| 57 | title: string; |
| 58 | }>({ |
| 59 | visible: false, |
| 60 | url: '', |
| 61 | title: '', |
| 62 | }); |
| 63 | |
| 64 | async function getTaskList(isLoadMore = false) { |
| 65 | setLoading(true); |
| 66 | try { |
| 67 | const res = await taskApi.getTaskList<TaskVideo>({ |
| 68 | ...pageInfo, |
| 69 | type: TaskType.VIDEO, |
| 70 | }); |
| 71 | |
| 72 | if (isLoadMore) { |
| 73 | setTaskList((prev) => [...prev, ...res.items]); |
| 74 | } else { |
| 75 | setTaskList(res.items); |
| 76 | } |
| 77 | |
| 78 | setPageInfo((prev) => ({ |
| 79 | ...prev, |
| 80 | totalCount: (res as any).totalCount, |
| 81 | })); |
| 82 | |
| 83 | // 检查是否还有更多数据 |
| 84 | setHasMore(pageInfo.pageNo * pageInfo.pageSize < (res as any).totalCount); |
| 85 | } catch (error) { |
| 86 | console.error('获取任务列表失败', error); |
| 87 | } finally { |
| 88 | setLoading(false); |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | useEffect(() => { |
| 93 | getTaskList(); |
| 94 | }, []); |
| 95 | |
| 96 | // 加载更多数据 |
| 97 | const loadMore = () => { |
| 98 | setPageInfo((prev) => ({ |
| 99 | ...prev, |
| 100 | pageNo: prev.pageNo + 1, |
| 101 | })); |
| 102 | getTaskList(true); |
| 103 | }; |
| 104 | |
| 105 | // 格式化日期 |
| 106 | const formatDate = (dateString?: string) => { |
| 107 | if (!dateString) return '2025/03/17'; |
| 108 | return dayjs(dateString).format('YYYY/MM/DD'); |
| 109 | }; |
| 110 | |
| 111 | // 复制任务ID |
| 112 | const copyTaskId = (id: string, e: React.MouseEvent) => { |
| 113 | e.stopPropagation(); |
| 114 | navigator.clipboard |
| 115 | .writeText(id) |
| 116 | .then(() => { |
| 117 | message.success('任务ID已复制到剪贴板'); |
| 118 | }) |
| 119 | .catch(() => { |
| 120 | message.error('复制失败,请手动复制'); |
| 121 | }); |
| 122 | }; |
| 123 | |
| 124 | // 渲染平台图标 |
| 125 | const renderPlatformTags = (accountTypes?: string[]) => { |
| 126 | if (!accountTypes || accountTypes.length === 0) { |
| 127 | return <Tag color="#f50">全平台</Tag>; |
| 128 | } |
| 129 | |
| 130 | return ( |
| 131 | <div className={styles.platformIcons}> |
| 132 | {accountTypes.map((type) => { |
| 133 | const platform = (PLATFORM_MAP as any)[type]; |
| 134 | if (!platform) return null; |
| 135 | |
| 136 | return ( |
| 137 | <Tooltip key={type} title={platform.name}> |
| 138 | <div |
| 139 | className={styles.platformIconWrapper} |
| 140 | style={{ backgroundColor: platform.color }} |
| 141 | > |
| 142 | <img |
| 143 | src={platform.icon} |
| 144 | className={styles.platformIcon} |
| 145 | alt={platform.name} |
| 146 | /> |
| 147 | </div> |
| 148 | </Tooltip> |
| 149 | ); |
| 150 | })} |
| 151 | </div> |
| 152 | ); |
| 153 | }; |
| 154 | |
| 155 | // 打开视频播放器 |
| 156 | const openVideoPlayer = (task: Task<TaskVideo>, e: React.MouseEvent) => { |
| 157 | e.stopPropagation(); |
| 158 | if (task.dataInfo?.videoUrl) { |
| 159 | setVideoPlayback({ |
| 160 | visible: true, |
| 161 | url: `${FILE_BASE_URL}${task.dataInfo.videoUrl}`, |
| 162 | title: task.title || '视频播放', |
| 163 | }); |
| 164 | } else { |
| 165 | message.info('该任务暂无视频'); |
| 166 | } |
| 167 | }; |
| 168 | |
| 169 | // 关闭视频播放器 |
| 170 | const closeVideoPlayer = () => { |
| 171 | setVideoPlayback((prev) => ({ ...prev, visible: false })); |
| 172 | }; |
| 173 | |
| 174 | // 刷新任务列表的函数 |
| 175 | const refreshTaskList = () => { |
| 176 | setPageInfo({ |
| 177 | pageSize: 10, |
| 178 | pageNo: 1, |
| 179 | totalCount: 0, |
| 180 | }); |
| 181 | getTaskList(); |
| 182 | }; |
| 183 | |
| 184 | return ( |
| 185 | <div className={styles.videoTaskContainer}> |
| 186 | <TaskInfo ref={Ref_TaskInfo} onTaskApplied={refreshTaskList} /> |
| 187 | |
| 188 | {/* 添加视频播放组件 */} |
| 189 | <VideoPlayer |
| 190 | videoUrl={videoPlayback.url} |
| 191 | visible={videoPlayback.visible} |
| 192 | onClose={closeVideoPlayer} |
| 193 | title={videoPlayback.title} |
| 194 | /> |
| 195 | |
| 196 | <div className={styles.taskList}> |
| 197 | {taskList.map((task) => ( |
| 198 | <Card |
| 199 | key={task._id} |
| 200 | className={styles.taskCard} |
| 201 | styles={{ body: { padding: 0 } }} |
| 202 | > |
| 203 | <div className={styles.taskCardContent}> |
| 204 | <div |
| 205 | className={styles.taskImageContainer} |
| 206 | onClick={(e) => openVideoPlayer(task, e)} |
| 207 | > |
| 208 | <img |
| 209 | src={`${FILE_BASE_URL}${task.imageUrl}`} |
| 210 | alt={task.title} |
| 211 | className={styles.taskImage} |
| 212 | /> |
| 213 | <div className={styles.videoOverlay}> |
| 214 | <PlayCircleOutlined className={styles.playIcon} /> |
| 215 | </div> |
| 216 | </div> |
| 217 | |
| 218 | <div className={styles.taskInfo}> |
| 219 | <div className={styles.taskHeader}> |
| 220 | <h3 className={styles.taskTitle}> |
| 221 | {task.title} |
| 222 | <span className={styles.taskId}> |
| 223 | ID: {task.id} |
| 224 | <Tooltip title="复制任务ID"> |
| 225 | <CopyOutlined |
| 226 | className={styles.copyIcon} |
| 227 | onClick={(e) => copyTaskId(task._id, e)} |
| 228 | /> |
| 229 | </Tooltip> |
| 230 | </span> |
| 231 | </h3> |
| 232 | <Tag color="#a66ae4" className={styles.taskTag}> |
| 233 | {(task.dataInfo as any)?.type || '视频任务'} |
| 234 | </Tag> |
| 235 | </div> |
| 236 | |
| 237 | <div |
| 238 | className={styles.taskDescription} |
| 239 | dangerouslySetInnerHTML={{ |
| 240 | __html: task.description || '不许删文', |
| 241 | }} |
| 242 | /> |
| 243 | |
| 244 | <div className={styles.taskDetails}> |
| 245 | <div className={styles.taskDetail}> |
| 246 | <ClockCircleOutlined className={styles.detailIcon} /> |
| 247 | <span className={styles.detailLabel}>截止时间:</span> |
| 248 | <span className={styles.detailValue}> |
| 249 | {formatDate(task.deadline)} |
| 250 | </span> |
| 251 | </div> |
| 252 | |
| 253 | <div className={styles.taskDetail}> |
| 254 | <TeamOutlined className={styles.detailIcon} /> |
| 255 | <span className={styles.detailLabel}>招募人数:</span> |
| 256 | <span className={styles.detailValue}> |
| 257 | {task.maxRecruits || 100} |
| 258 | </span> |
| 259 | </div> |
| 260 | </div> |
| 261 | |
| 262 | <div className={styles.taskRequirement}> |
| 263 | <span className={styles.requirementLabel}>任务要求:</span> |
| 264 | <span className={styles.requirementValue}> |
| 265 | {task.requirement || '不许删文'} |
| 266 | </span> |
| 267 | </div> |
| 268 | |
| 269 | <div className={styles.platformContainer}> |
| 270 | <span className={styles.platformLabel}>可用平台:</span> |
| 271 | {renderPlatformTags(task.accountTypes)} |
| 272 | </div> |
| 273 | </div> |
| 274 | |
| 275 | <div className={styles.taskAction}> |
| 276 | <div className={styles.taskStatus}> |
| 277 | <Tag color="#a66ae4">进行中</Tag> |
| 278 | </div> |
| 279 | |
| 280 | <div className={styles.taskReward}> |
| 281 | <span className={styles.rewardLabel}>每篇可赚</span> |
| 282 | <span className={styles.rewardValue}> |
| 283 | ¥{task.reward || 5} |
| 284 | </span> |
| 285 | </div> |
| 286 | |
| 287 | <Button |
| 288 | type="primary" |
| 289 | className={styles.viewButton} |
| 290 | onClick={() => Ref_TaskInfo.current?.init(task)} |
| 291 | style={{ backgroundColor: '#a66ae4', borderColor: '#a66ae4' }} |
| 292 | > |
| 293 | 去查看 <RightOutlined /> |
| 294 | </Button> |
| 295 | </div> |
| 296 | </div> |
| 297 | </Card> |
| 298 | ))} |
| 299 | |
| 300 | {loading && ( |
| 301 | <div className={styles.loadingContainer}> |
| 302 | <Spin size="large" /> |
| 303 | </div> |
| 304 | )} |
| 305 | |
| 306 | {!loading && hasMore && ( |
| 307 | <div className={styles.loadMoreContainer}> |
| 308 | <Button onClick={loadMore}>查看更多任务</Button> |
| 309 | </div> |
| 310 | )} |
| 311 | </div> |
| 312 | </div> |
| 313 | ); |
| 314 | } |
| 315 |