feat(task): 任务详情与列表页增加H5订单号领取链接及兑换有效性控制
- 后端 AdminTaskDetail 视图补充关联订单的 claimStatus、claimStatusReason 等状态字段 - 任务详情页升级“领取链接”卡片:支持固定 H5 链接展示、状态标签(有效/暂停/关闭),并支持一键复制及【暂停/关闭/恢复启用】弹窗操作 - 任务详情页保留历史 Token 链接作为辅助对比展示 - 任务列表页在平台订单号旁增加一键复制 H5 领取链接的快捷按钮 - 验证方式:前后端类型检查、全量单元测试(后端 674、前端 132)及前后端全量构建均通过
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<ManualDispatchForm>({
|
||||
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 (
|
||||
<section className="page-stack">
|
||||
<PageHeader
|
||||
@@ -332,31 +397,138 @@ export default function AdminTaskDetailPage() {
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
{resolvedDetail.claimToken ? (
|
||||
<Card title="领取链接">
|
||||
<Space direction="vertical" size={8} className="full-width">
|
||||
{claimUrl ? (
|
||||
<Typography.Text copyable={{ text: claimUrl }}>{claimUrl}</Typography.Text>
|
||||
) : claimLinkInvalid ? (
|
||||
<Typography.Text type="secondary">领取链接已失效</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text type="secondary">暂无可用领取链接</Typography.Text>
|
||||
)}
|
||||
<Space wrap>
|
||||
<StatusTag value={resolvedDetail.claimToken.status} />
|
||||
{isExternalClaimLink ? (
|
||||
<Typography.Text type="secondary">外部交付链接</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text type="secondary">
|
||||
{resolvedDetail.claimToken.expiredAt
|
||||
? `到期:${formatAdminDateTime(resolvedDetail.claimToken.expiredAt)}`
|
||||
: '长期有效'}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Button icon={<CopyOutlined />} disabled={!claimUrl} onClick={copyClaimUrl}>
|
||||
复制链接
|
||||
</Button>
|
||||
{h5ClaimUrl || resolvedDetail.claimToken ? (
|
||||
<Card
|
||||
title={
|
||||
<Space size={12}>
|
||||
<span>领取链接与兑换控制</span>
|
||||
{resolvedDetail.order ? (
|
||||
claimStatus === 'active' ? (
|
||||
<Tag color="success">H5兑换有效</Tag>
|
||||
) : claimStatus === 'paused' ? (
|
||||
<Tag color="warning">H5兑换已暂停</Tag>
|
||||
) : (
|
||||
<Tag color="error">H5兑换已关闭</Tag>
|
||||
)
|
||||
) : null}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" size={16} className="full-width">
|
||||
{h5ClaimUrl ? (
|
||||
<div
|
||||
style={{
|
||||
background: '#fafafa',
|
||||
padding: 12,
|
||||
borderRadius: 6,
|
||||
border: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
<Space direction="vertical" size={8} className="full-width">
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<Tag color="blue">固定 H5 链接</Tag>
|
||||
<Typography.Text strong copyable={{ text: h5ClaimUrl }}>
|
||||
{h5ClaimUrl}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
<Space wrap>
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<CopyOutlined />}
|
||||
onClick={handleCopyH5Link}
|
||||
>
|
||||
复制 H5 链接
|
||||
</Button>
|
||||
{orderId ? (
|
||||
<>
|
||||
{claimStatus !== 'paused' && (
|
||||
<Button
|
||||
danger
|
||||
style={{ color: '#fa8c16', borderColor: '#ffa39e' }}
|
||||
icon={<PauseCircleOutlined />}
|
||||
onClick={() => openStatusModal('paused')}
|
||||
>
|
||||
暂停兑换
|
||||
</Button>
|
||||
)}
|
||||
{claimStatus !== 'closed' && (
|
||||
<Button
|
||||
danger
|
||||
icon={<StopOutlined />}
|
||||
onClick={() => openStatusModal('closed')}
|
||||
>
|
||||
关闭兑换
|
||||
</Button>
|
||||
)}
|
||||
{claimStatus !== 'active' && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => openStatusModal('active')}
|
||||
>
|
||||
恢复启用
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
{claimStatusReason ? (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
|
||||
状态备注:{claimStatusReason}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{resolvedDetail.claimToken ? (
|
||||
<div>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
style={{ fontSize: 12, display: 'block', marginBottom: 6 }}
|
||||
>
|
||||
历史/备用 Token 链接:
|
||||
</Typography.Text>
|
||||
<Space wrap align="center">
|
||||
{claimUrl ? (
|
||||
<Typography.Text copyable={{ text: claimUrl }}>{claimUrl}</Typography.Text>
|
||||
) : claimLinkInvalid ? (
|
||||
<Typography.Text type="secondary">链接已失效</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text type="secondary">暂无可用链接</Typography.Text>
|
||||
)}
|
||||
<StatusTag value={resolvedDetail.claimToken.status} />
|
||||
{isExternalClaimLink ? (
|
||||
<Typography.Text type="secondary">外部交付链接</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text type="secondary">
|
||||
{resolvedDetail.claimToken.expiredAt
|
||||
? `到期:${formatAdminDateTime(resolvedDetail.claimToken.expiredAt)}`
|
||||
: '长期有效'}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
size="small"
|
||||
disabled={!claimUrl}
|
||||
onClick={copyClaimUrl}
|
||||
>
|
||||
复制 Token 链接
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
) : null}
|
||||
</Space>
|
||||
</Card>
|
||||
) : null}
|
||||
@@ -432,6 +604,48 @@ export default function AdminTaskDetailPage() {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={targetStatus === 'paused' ? '暂停订单兑换' : '关闭订单兑换'}
|
||||
open={statusModalVisible}
|
||||
confirmLoading={statusMutation.isPending}
|
||||
onOk={() =>
|
||||
statusMutation.mutateAsync({
|
||||
status: targetStatus,
|
||||
reason: statusReason.trim() || undefined,
|
||||
})
|
||||
}
|
||||
onCancel={() => setStatusModalVisible(false)}
|
||||
okText="确认变更"
|
||||
cancelText="取消"
|
||||
centered
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 16 }}>
|
||||
<Alert
|
||||
type={targetStatus === 'paused' ? 'warning' : 'error'}
|
||||
showIcon
|
||||
message={
|
||||
targetStatus === 'paused'
|
||||
? '暂停后,买家访问 H5 链接将提示“订单兑换已暂停”,无法提交 UID。可在售后处理完成后恢复启用。'
|
||||
: '关闭后,买家访问 H5 链接将提示“订单兑换已关闭”,完全阻断兑换。通常用于退款完成场景。'
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<Typography.Text type="secondary">原因 / 备注(可选):</Typography.Text>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder={
|
||||
targetStatus === 'paused'
|
||||
? '例如:买家申请退款中 / 售后核实'
|
||||
: '例如:买家已全额退款'
|
||||
}
|
||||
value={statusReason}
|
||||
onChange={(e) => setStatusReason(e.target.value)}
|
||||
style={{ marginTop: 6 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
CloseCircleOutlined,
|
||||
CopyOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
UserSwitchOutlined,
|
||||
@@ -124,7 +125,26 @@ export default function AdminTasksPage() {
|
||||
<Typography.Link onClick={() => navigate(`/admin/tasks/${row.taskId}`)}>
|
||||
{row.taskNo}
|
||||
</Typography.Link>
|
||||
<span className="muted">{row.platformOrderId}</span>
|
||||
<Space size={4}>
|
||||
<span className="muted">{row.platformOrderId}</span>
|
||||
{row.platformOrderId ? (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
title="复制 H5 领取链接"
|
||||
style={{ padding: 0, height: 'auto', color: '#64748b' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
const h5Url = `${window.location.origin}/h5/${encodeURIComponent(row.platformOrderId)}`
|
||||
navigator.clipboard
|
||||
.writeText(h5Url)
|
||||
.then(() => message.success('已复制 H5 领取链接: ' + h5Url))
|
||||
.catch(() => message.error('复制失败,请手动复制'))
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -87,6 +87,9 @@ export interface AdminTaskDetail {
|
||||
platformOrderId: string
|
||||
payStatus: string
|
||||
orderStatus: string
|
||||
claimStatus?: 'active' | 'paused' | 'closed'
|
||||
claimStatusReason?: string | null
|
||||
claimStatusUpdatedAt?: string | null
|
||||
}
|
||||
orderItem: null | {
|
||||
orderItemId: number
|
||||
|
||||
Reference in New Issue
Block a user