| 1 | /* |
| 2 | * @Author: nevin |
| 3 | * @Date: 2025-03-01 19:27:35 |
| 4 | * @LastEditTime: 2025-03-24 14:10:00 |
| 5 | * @LastEditors: nevin |
| 6 | * @Description: 用户任务信息 |
| 7 | */ |
| 8 | import { |
| 9 | Button, |
| 10 | Modal, |
| 11 | Form, |
| 12 | Input, |
| 13 | Upload, |
| 14 | message, |
| 15 | Tag, |
| 16 | Divider, |
| 17 | Space, |
| 18 | Tooltip, |
| 19 | Alert, |
| 20 | } from 'antd'; |
| 21 | import { |
| 22 | Task, |
| 23 | TaskProduct, |
| 24 | TaskPromotion, |
| 25 | TaskTypeName, |
| 26 | TaskVideo, |
| 27 | } from '@@/types/task'; |
| 28 | import { forwardRef, useImperativeHandle, useState } from 'react'; |
| 29 | import { taskApi } from '@/api/task'; |
| 30 | import { UserTask, UserTaskStatus } from '@/api/types/task'; |
| 31 | import styles from './mineInfo.module.scss'; |
| 32 | import { |
| 33 | LinkOutlined, |
| 34 | QrcodeOutlined, |
| 35 | ClockCircleOutlined, |
| 36 | FileTextOutlined, |
| 37 | DollarOutlined, |
| 38 | InfoCircleOutlined, |
| 39 | PictureOutlined, |
| 40 | } from '@ant-design/icons'; |
| 41 | import dayjs from 'dayjs'; |
| 42 | |
| 43 | const FILE_BASE_URL = import.meta.env.VITE_APP_FILE_HOST; |
| 44 | const VITE_APP_URL = import.meta.env.VITE_APP_URL; |
| 45 | |
| 46 | export interface MineTaskInfoRef { |
| 47 | init: ( |
| 48 | inData: UserTask<Task<TaskProduct | TaskPromotion | TaskVideo>>, |
| 49 | ) => Promise<void>; |
| 50 | } |
| 51 | |
| 52 | // 可以提交的任务状态 |
| 53 | const SUBMITTABLE_STATUSES = [UserTaskStatus.DODING, UserTaskStatus.REJECTED]; |
| 54 | |
| 55 | const Com = forwardRef<MineTaskInfoRef>((props: any, ref) => { |
| 56 | const [isModalOpen, setIsModalOpen] = useState(false); |
| 57 | const [mineTaskInfo, setMineTaskInfo] = useState<UserTask< |
| 58 | Task<TaskProduct | TaskPromotion | TaskVideo> |
| 59 | > | null>(); |
| 60 | const [form] = Form.useForm(); |
| 61 | const [submitting, setSubmitting] = useState(false); |
| 62 | const [fileList, setFileList] = useState<any[]>([]); |
| 63 | |
| 64 | // 从props中获取刷新函数 |
| 65 | const { onTaskSubmitted } = props; |
| 66 | |
| 67 | async function init( |
| 68 | inData: UserTask<Task<TaskProduct | TaskPromotion | TaskVideo>>, |
| 69 | ) { |
| 70 | setMineTaskInfo(inData); |
| 71 | setIsModalOpen(true); |
| 72 | |
| 73 | // 重置表单 |
| 74 | form.resetFields(); |
| 75 | setFileList([]); |
| 76 | |
| 77 | // 如果有已提交的数据,填充表单 |
| 78 | if ( |
| 79 | inData.submissionUrl || |
| 80 | inData.qrCodeScanResult || |
| 81 | inData.screenshotUrls?.length |
| 82 | ) { |
| 83 | form.setFieldsValue({ |
| 84 | submissionUrl: inData.submissionUrl || '', |
| 85 | qrCodeScanResult: inData.qrCodeScanResult || '', |
| 86 | }); |
| 87 | |
| 88 | if (inData.screenshotUrls?.length) { |
| 89 | const initialFileList = inData.screenshotUrls.map((url, index) => ({ |
| 90 | uid: `-${index}`, |
| 91 | name: `截图${index + 1}.jpg`, |
| 92 | status: 'done', |
| 93 | url: url.startsWith('http') ? url : `${FILE_BASE_URL}/${url}`, |
| 94 | })); |
| 95 | setFileList(initialFileList); |
| 96 | } |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | useImperativeHandle(ref, () => ({ |
| 101 | init: init, |
| 102 | })); |
| 103 | |
| 104 | async function taskDone() { |
| 105 | if (!mineTaskInfo) return; |
| 106 | |
| 107 | try { |
| 108 | await form.validateFields(); |
| 109 | const values = form.getFieldsValue(); |
| 110 | |
| 111 | setSubmitting(true); |
| 112 | |
| 113 | // 处理截图文件 |
| 114 | const screenshotUrls = fileList |
| 115 | .filter((file) => file.status === 'done') |
| 116 | .map((file) => { |
| 117 | console.log('file', file); |
| 118 | return file.response?.data.name; |
| 119 | }); |
| 120 | |
| 121 | const doneInfo = { |
| 122 | submissionUrl: values.submissionUrl, |
| 123 | qrCodeScanResult: values.qrCodeScanResult, |
| 124 | screenshotUrls, |
| 125 | }; |
| 126 | |
| 127 | const res = await taskApi.taskDone(mineTaskInfo.id, doneInfo); |
| 128 | |
| 129 | setMineTaskInfo({ |
| 130 | ...mineTaskInfo, |
| 131 | status: res.status, // 更新为待审核 |
| 132 | ...doneInfo, // 更新提交的信息 |
| 133 | }); |
| 134 | |
| 135 | message.success('任务提交成功!'); |
| 136 | setIsModalOpen(false); |
| 137 | |
| 138 | // 调用父组件传递的刷新函数 |
| 139 | if (onTaskSubmitted && typeof onTaskSubmitted === 'function') { |
| 140 | onTaskSubmitted(); |
| 141 | } |
| 142 | } catch (error) { |
| 143 | console.error('提交任务失败', error); |
| 144 | message.error('提交任务失败,请检查表单并重试'); |
| 145 | } finally { |
| 146 | setSubmitting(false); |
| 147 | } |
| 148 | } |
| 149 | |
| 150 | const handleCancel = () => { |
| 151 | setIsModalOpen(false); |
| 152 | }; |
| 153 | |
| 154 | // 格式化日期 |
| 155 | const formatDate = (dateString?: string) => { |
| 156 | if (!dateString) return '暂无日期'; |
| 157 | return dayjs(dateString).format('YYYY/MM/DD HH:mm'); |
| 158 | }; |
| 159 | |
| 160 | // 判断任务是否可提交 |
| 161 | const canSubmitTask = () => { |
| 162 | if (!mineTaskInfo) return false; |
| 163 | |
| 164 | // 只有在进行中或已拒绝状态才允许提交 |
| 165 | return SUBMITTABLE_STATUSES.includes(mineTaskInfo.status as UserTaskStatus); |
| 166 | }; |
| 167 | |
| 168 | // 处理上传图片变化 |
| 169 | const handleUploadChange = ({ fileList }: any) => { |
| 170 | setFileList(fileList); |
| 171 | }; |
| 172 | |
| 173 | // 上传前检查 |
| 174 | const beforeUpload = (file: any) => { |
| 175 | const isImage = file.type.startsWith('image/'); |
| 176 | if (!isImage) { |
| 177 | message.error('只能上传图片文件!'); |
| 178 | } |
| 179 | |
| 180 | const isLt5M = file.size / 1024 / 1024 < 5; |
| 181 | if (!isLt5M) { |
| 182 | message.error('图片大小不能超过5MB!'); |
| 183 | } |
| 184 | |
| 185 | return isImage && isLt5M; |
| 186 | }; |
| 187 | |
| 188 | return ( |
| 189 | <Modal |
| 190 | title={null} |
| 191 | open={isModalOpen} |
| 192 | onCancel={handleCancel} |
| 193 | footer={null} |
| 194 | width={800} |
| 195 | destroyOnClose |
| 196 | className={styles.taskInfoModal} |
| 197 | > |
| 198 | {mineTaskInfo ? ( |
| 199 | <div className={styles.taskInfoContainer}> |
| 200 | <div className={styles.taskInfoHeader}> |
| 201 | <h2 className={styles.taskTitle}> |
| 202 | {mineTaskInfo.taskId?.title || '未知任务'} |
| 203 | <span className={styles.taskId}> |
| 204 | 订单号: {(mineTaskInfo as any)._id} |
| 205 | </span> |
| 206 | </h2> |
| 207 | <Tag |
| 208 | color={ |
| 209 | mineTaskInfo.status === UserTaskStatus.DODING |
| 210 | ? 'processing' |
| 211 | : mineTaskInfo.status === UserTaskStatus.PENDING |
| 212 | ? 'warning' |
| 213 | : mineTaskInfo.status === UserTaskStatus.APPROVED |
| 214 | ? 'success' |
| 215 | : mineTaskInfo.status === UserTaskStatus.REJECTED |
| 216 | ? 'error' |
| 217 | : 'default' |
| 218 | } |
| 219 | className={styles.statusTag} |
| 220 | > |
| 221 | {mineTaskInfo.status === UserTaskStatus.DODING |
| 222 | ? '进行中' |
| 223 | : mineTaskInfo.status === UserTaskStatus.PENDING |
| 224 | ? '待审核' |
| 225 | : mineTaskInfo.status === UserTaskStatus.APPROVED |
| 226 | ? '已通过' |
| 227 | : mineTaskInfo.status === UserTaskStatus.REJECTED |
| 228 | ? '已拒绝' |
| 229 | : '未知状态'} |
| 230 | </Tag> |
| 231 | </div> |
| 232 | |
| 233 | <div className={styles.taskInfoContent}> |
| 234 | <div className={styles.taskDetails}> |
| 235 | <div className={styles.taskDetail}> |
| 236 | <ClockCircleOutlined className={styles.detailIcon} /> |
| 237 | <span className={styles.detailLabel}>接单时间:</span> |
| 238 | <span className={styles.detailValue}> |
| 239 | {formatDate(mineTaskInfo.createTime)} |
| 240 | </span> |
| 241 | </div> |
| 242 | |
| 243 | <div className={styles.taskDetail}> |
| 244 | <FileTextOutlined className={styles.detailIcon} /> |
| 245 | <span className={styles.detailLabel}>任务类型:</span> |
| 246 | <span className={styles.detailValue}> |
| 247 | {TaskTypeName.get(mineTaskInfo.taskId?.type) || '未知类型'} |
| 248 | </span> |
| 249 | </div> |
| 250 | |
| 251 | <div className={styles.taskDetail}> |
| 252 | <DollarOutlined className={styles.detailIcon} /> |
| 253 | <span className={styles.detailLabel}>任务奖励:</span> |
| 254 | <span className={styles.detailValue}> |
| 255 | ¥{mineTaskInfo.taskId?.reward || 5} |
| 256 | </span> |
| 257 | </div> |
| 258 | </div> |
| 259 | |
| 260 | {mineTaskInfo.status === UserTaskStatus.REJECTED && mineTaskInfo.verificationNote && ( |
| 261 | <div className={styles.rejectionNote}> |
| 262 | <Alert |
| 263 | type="error" |
| 264 | showIcon |
| 265 | message={ |
| 266 | <div> |
| 267 | <div className={styles.rejectionTitle}>拒绝原因:</div> |
| 268 | <div className={styles.rejectionContent}>{mineTaskInfo.verificationNote}</div> |
| 269 | </div> |
| 270 | } |
| 271 | /> |
| 272 | </div> |
| 273 | )} |
| 274 | |
| 275 | <Divider /> |
| 276 | |
| 277 | <div className={styles.taskDescription}> |
| 278 | <h3 className={styles.sectionTitle}>任务描述</h3> |
| 279 | <div |
| 280 | className={styles.descriptionContent} |
| 281 | dangerouslySetInnerHTML={{ |
| 282 | __html: mineTaskInfo.taskId?.description || '暂无描述', |
| 283 | }} |
| 284 | /> |
| 285 | </div> |
| 286 | |
| 287 | <Divider /> |
| 288 | |
| 289 | <div className={styles.submissionSection}> |
| 290 | <h3 className={styles.sectionTitle}> |
| 291 | 提交任务 |
| 292 | {!canSubmitTask() && ( |
| 293 | <Tooltip title="当前任务状态不允许提交"> |
| 294 | <InfoCircleOutlined className={styles.infoIcon} /> |
| 295 | </Tooltip> |
| 296 | )} |
| 297 | </h3> |
| 298 | |
| 299 | <Form |
| 300 | form={form} |
| 301 | layout="vertical" |
| 302 | disabled={!canSubmitTask() || submitting} |
| 303 | > |
| 304 | <Form.Item |
| 305 | name="submissionUrl" |
| 306 | label="提交链接" |
| 307 | rules={[{ required: true, message: '请输入提交链接' }]} |
| 308 | > |
| 309 | <Input |
| 310 | placeholder="请输入任务完成后的链接" |
| 311 | prefix={<LinkOutlined />} |
| 312 | /> |
| 313 | </Form.Item> |
| 314 | |
| 315 | <Form.Item |
| 316 | name="qrCodeScanResult" |
| 317 | label="二维码扫描结果" |
| 318 | rules={[{ required: false, message: '请输入二维码扫描结果' }]} |
| 319 | > |
| 320 | <Input |
| 321 | placeholder="请输入二维码扫描结果(如有)" |
| 322 | prefix={<QrcodeOutlined />} |
| 323 | /> |
| 324 | </Form.Item> |
| 325 | |
| 326 | <Form.Item |
| 327 | name="screenshotUrls" |
| 328 | label="任务截图" |
| 329 | rules={[{ required: true, message: '请上传任务截图' }]} |
| 330 | > |
| 331 | <Upload |
| 332 | listType="picture-card" |
| 333 | fileList={fileList} |
| 334 | onChange={handleUploadChange} |
| 335 | beforeUpload={beforeUpload} |
| 336 | action={VITE_APP_URL + '/oss/upload/permanent'} |
| 337 | multiple |
| 338 | maxCount={5} |
| 339 | > |
| 340 | {fileList.length >= 5 ? null : ( |
| 341 | <div> |
| 342 | <PictureOutlined /> |
| 343 | <div style={{ marginTop: 8 }}>上传截图</div> |
| 344 | </div> |
| 345 | )} |
| 346 | </Upload> |
| 347 | </Form.Item> |
| 348 | |
| 349 | <div className={styles.uploadTips}> |
| 350 | <p>提示:</p> |
| 351 | <ul> |
| 352 | <li>请上传清晰的任务完成截图,最多5张</li> |
| 353 | <li>图片格式支持JPG、PNG,单张大小不超过5MB</li> |
| 354 | <li>截图应包含任务完成的关键信息,如发布内容、评论等</li> |
| 355 | </ul> |
| 356 | </div> |
| 357 | </Form> |
| 358 | </div> |
| 359 | </div> |
| 360 | |
| 361 | <div className={styles.taskInfoFooter}> |
| 362 | <Space> |
| 363 | <Button onClick={handleCancel}>关闭</Button> |
| 364 | {canSubmitTask() && ( |
| 365 | <Button type="primary" onClick={taskDone} loading={submitting}> |
| 366 | 提交任务 |
| 367 | </Button> |
| 368 | )} |
| 369 | </Space> |
| 370 | </div> |
| 371 | </div> |
| 372 | ) : ( |
| 373 | <div className={styles.noDataContainer}>暂无任务信息</div> |
| 374 | )} |
| 375 | </Modal> |
| 376 | ); |
| 377 | }); |
| 378 | |
| 379 | export default Com; |
| 380 |