diff --git a/apps/backend/src/services/admin/admin-read-service.ts b/apps/backend/src/services/admin/admin-read-service.ts index 9f9fb237..da3a92f5 100644 --- a/apps/backend/src/services/admin/admin-read-service.ts +++ b/apps/backend/src/services/admin/admin-read-service.ts @@ -368,6 +368,9 @@ export async function getAdminTaskDetail( platformOrderId: order.platform_order_id, payStatus: order.pay_status, orderStatus: order.order_status, + claimStatus: (order.claim_status as 'active' | 'paused' | 'closed') || 'active', + claimStatusReason: order.claim_status_reason || null, + claimStatusUpdatedAt: order.claim_status_updated_at || null, } : null, orderItem: orderItem diff --git a/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx b/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx index 04d5c4aa..978472dc 100644 --- a/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx +++ b/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx @@ -3,7 +3,11 @@ import { CheckCircleOutlined, CloseCircleOutlined, CopyOutlined, + LinkOutlined, + PauseCircleOutlined, + PlayCircleOutlined, ReloadOutlined, + StopOutlined, UserSwitchOutlined, } from '@ant-design/icons' import { @@ -12,9 +16,11 @@ import { Button, Card, Descriptions, + Divider, Empty, Image, Input, + Modal, Skeleton, Space, Table, @@ -23,7 +29,7 @@ import { Typography, } from 'antd' import type { TableColumnsType } from 'antd' -import { useQuery } from '@tanstack/react-query' +import { useMutation, useQuery } from '@tanstack/react-query' import { useEffect, useMemo, useState } from 'react' import { useNavigate, useParams } from 'react-router' @@ -39,6 +45,7 @@ import { markAdminTaskManualReview, resendAdminTaskKuaishouIndustryVoucherCode, retryAdminTask, + updateAdminOrderClaimStatus, } from '@/services/admin' import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin' import { hasAdminRole } from '@/utils/admin-auth' @@ -65,6 +72,9 @@ export default function AdminTaskDetailPage() { const [actionLoadingKey, setActionLoadingKey] = useState('') const [lastClaimUrl, setLastClaimUrl] = useState('') const [screenshotPreviewUrl, setScreenshotPreviewUrl] = useState('') + const [statusModalVisible, setStatusModalVisible] = useState(false) + const [targetStatus, setTargetStatus] = useState<'active' | 'paused' | 'closed'>('active') + const [statusReason, setStatusReason] = useState('') const [manualDispatchForm, setManualDispatchForm] = useState({ deliveryReference: '', deliveredCredential: '', @@ -231,6 +241,61 @@ export default function AdminTaskDetailPage() { ) } + const orderId = resolvedDetail.order?.orderId ? String(resolvedDetail.order.orderId) : '' + const platformOrderId = + resolvedDetail.order?.platformOrderId || resolvedDetail.task.platformOrderId || '' + const h5ClaimUrl = platformOrderId + ? `${window.location.origin}/h5/${encodeURIComponent(platformOrderId)}` + : '' + const claimStatus = resolvedDetail.order?.claimStatus || 'active' + const claimStatusReason = resolvedDetail.order?.claimStatusReason || '' + + const statusMutation = useMutation({ + mutationFn: (data: { status: 'active' | 'paused' | 'closed'; reason?: string }) => { + if (!orderId) throw new Error('当前任务无关联订单信息') + return updateAdminOrderClaimStatus(orderId, data) + }, + onSuccess: () => { + message.success('订单兑换状态已更新') + setStatusModalVisible(false) + setStatusReason('') + void query.refetch() + }, + onError: (err: unknown) => { + message.error(err instanceof Error ? err.message : '更新状态失败') + }, + }) + + const handleCopyH5Link = () => { + if (!h5ClaimUrl) return + navigator.clipboard + .writeText(h5ClaimUrl) + .then(() => message.success('已复制 H5 领取链接: ' + h5ClaimUrl)) + .catch(() => message.error('复制失败,请手动复制')) + } + + const openStatusModal = (status: 'active' | 'paused' | 'closed') => { + if (!orderId) { + message.warning('当前任务无关联订单 ID,无法更改兑换状态') + return + } + if (status === 'active') { + modal.confirm({ + title: '恢复启用订单兑换', + content: '恢复后,买家可正常进入 H5 页面提交 UID 领取,确认恢复吗?', + onOk: () => + statusMutation.mutateAsync({ + status: 'active', + reason: '客服在任务详情中恢复启用', + }), + }) + return + } + setTargetStatus(status) + setStatusReason('') + setStatusModalVisible(true) + } + return (
- {resolvedDetail.claimToken ? ( - - - {claimUrl ? ( - {claimUrl} - ) : claimLinkInvalid ? ( - 领取链接已失效 - ) : ( - 暂无可用领取链接 - )} - - - {isExternalClaimLink ? ( - 外部交付链接 - ) : ( - - {resolvedDetail.claimToken.expiredAt - ? `到期:${formatAdminDateTime(resolvedDetail.claimToken.expiredAt)}` - : '长期有效'} - - )} - + {h5ClaimUrl || resolvedDetail.claimToken ? ( + + 领取链接与兑换控制 + {resolvedDetail.order ? ( + claimStatus === 'active' ? ( + H5兑换有效 + ) : claimStatus === 'paused' ? ( + H5兑换已暂停 + ) : ( + H5兑换已关闭 + ) + ) : null} + } + > + + {h5ClaimUrl ? ( +
+ +
+ + 固定 H5 链接 + + {h5ClaimUrl} + + + + + {orderId ? ( + <> + {claimStatus !== 'paused' && ( + + )} + {claimStatus !== 'closed' && ( + + )} + {claimStatus !== 'active' && ( + + )} + + ) : null} + +
+ {claimStatusReason ? ( + + 状态备注:{claimStatusReason} + + ) : null} +
+
+ ) : null} + + {resolvedDetail.claimToken ? ( +
+ + 历史/备用 Token 链接: + + + {claimUrl ? ( + {claimUrl} + ) : claimLinkInvalid ? ( + 链接已失效 + ) : ( + 暂无可用链接 + )} + + {isExternalClaimLink ? ( + 外部交付链接 + ) : ( + + {resolvedDetail.claimToken.expiredAt + ? `到期:${formatAdminDateTime(resolvedDetail.claimToken.expiredAt)}` + : '长期有效'} + + )} + + +
+ ) : null}
) : null} @@ -432,6 +604,48 @@ export default function AdminTaskDetailPage() { }, ]} /> + + + statusMutation.mutateAsync({ + status: targetStatus, + reason: statusReason.trim() || undefined, + }) + } + onCancel={() => setStatusModalVisible(false)} + okText="确认变更" + cancelText="取消" + centered + > +
+ +
+ 原因 / 备注(可选): + setStatusReason(e.target.value)} + style={{ marginTop: 6 }} + /> +
+
+
) } diff --git a/apps/frontend/src/pages/admin/AdminTasksPage.tsx b/apps/frontend/src/pages/admin/AdminTasksPage.tsx index 9da30cb6..23d41e89 100644 --- a/apps/frontend/src/pages/admin/AdminTasksPage.tsx +++ b/apps/frontend/src/pages/admin/AdminTasksPage.tsx @@ -1,5 +1,6 @@ import { CloseCircleOutlined, + CopyOutlined, ReloadOutlined, SearchOutlined, UserSwitchOutlined, @@ -124,7 +125,26 @@ export default function AdminTasksPage() { navigate(`/admin/tasks/${row.taskId}`)}> {row.taskNo} - {row.platformOrderId} + + {row.platformOrderId} + {row.platformOrderId ? ( +