| 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 |