优化订单履约后台界面
This commit is contained in:
@@ -37,6 +37,7 @@ import StatusTag from '@/components/admin/StatusTag'
|
|||||||
import {
|
import {
|
||||||
closeAdminTask,
|
closeAdminTask,
|
||||||
completeAdminTaskManualDispatch,
|
completeAdminTaskManualDispatch,
|
||||||
|
consumeAdminTaskKuaishouIndustryVoucher,
|
||||||
dispatchAdminTaskKuaishouCloudFulfillment,
|
dispatchAdminTaskKuaishouCloudFulfillment,
|
||||||
fetchAdminTaskDetail,
|
fetchAdminTaskDetail,
|
||||||
fetchAdminTaskScreenshot,
|
fetchAdminTaskScreenshot,
|
||||||
@@ -44,6 +45,7 @@ import {
|
|||||||
prepareAdminTaskKuaishouCloudFulfillment,
|
prepareAdminTaskKuaishouCloudFulfillment,
|
||||||
rebindAdminTaskKuaishouCloudRole,
|
rebindAdminTaskKuaishouCloudRole,
|
||||||
refreshAdminTaskKuaishouCloudRoleInfo,
|
refreshAdminTaskKuaishouCloudRoleInfo,
|
||||||
|
resendAdminTaskKuaishouIndustryVoucherCode,
|
||||||
retryAdminTask,
|
retryAdminTask,
|
||||||
returnNumberAdminTaskKuaishouCloudFulfillment,
|
returnNumberAdminTaskKuaishouCloudFulfillment,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
@@ -59,6 +61,7 @@ import {
|
|||||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
|
|
||||||
type TaskEvent = AdminTaskDetail['events'][number]
|
type TaskEvent = AdminTaskDetail['events'][number]
|
||||||
|
type KuaishouIndustryVoucher = NonNullable<AdminTaskDetail['kuaishouIndustryVoucher']>
|
||||||
type ManualDispatchForm = {
|
type ManualDispatchForm = {
|
||||||
deliveryReference: string
|
deliveryReference: string
|
||||||
deliveredCredential: string
|
deliveredCredential: string
|
||||||
@@ -252,6 +255,12 @@ export default function AdminTaskDetailPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FulfillmentOverviewPanel
|
||||||
|
detail={resolvedDetail}
|
||||||
|
claimUrl={claimUrl}
|
||||||
|
claimLinkInvalid={claimLinkInvalid}
|
||||||
|
/>
|
||||||
|
|
||||||
<TaskActionPanel
|
<TaskActionPanel
|
||||||
detail={resolvedDetail}
|
detail={resolvedDetail}
|
||||||
canManageTaskLifecycle={canManageTaskLifecycle}
|
canManageTaskLifecycle={canManageTaskLifecycle}
|
||||||
@@ -297,6 +306,22 @@ export default function AdminTaskDetailPage() {
|
|||||||
`确认退还任务 ${resolvedDetail.task.taskNo} 当前使用的虚拟号吗?退号后该流程会正式收口。`,
|
`确认退还任务 ${resolvedDetail.task.taskNo} 当前使用的虚拟号吗?退号后该流程会正式收口。`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
onResendIndustryVoucher={() =>
|
||||||
|
runTaskAction(
|
||||||
|
'resend-industry-voucher',
|
||||||
|
() => resendAdminTaskKuaishouIndustryVoucherCode(resolvedDetail.task.taskId),
|
||||||
|
'电子凭证发码回调已重发',
|
||||||
|
`确认重新发送任务 ${resolvedDetail.task.taskNo} 的电子凭证发码回调吗?`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onConsumeIndustryVoucher={() =>
|
||||||
|
runTaskAction(
|
||||||
|
'consume-industry-voucher',
|
||||||
|
() => consumeAdminTaskKuaishouIndustryVoucher(resolvedDetail.task.taskId),
|
||||||
|
'电子凭证已执行核销',
|
||||||
|
`确认立即核销任务 ${resolvedDetail.task.taskNo} 绑定的电子凭证吗?`,
|
||||||
|
)
|
||||||
|
}
|
||||||
onMarkManualReview={() =>
|
onMarkManualReview={() =>
|
||||||
runTaskAction(
|
runTaskAction(
|
||||||
'manual-review',
|
'manual-review',
|
||||||
@@ -405,6 +430,15 @@ export default function AdminTaskDetailPage() {
|
|||||||
<Empty description="无快手 Cloud 履约上下文" />
|
<Empty description="无快手 Cloud 履约上下文" />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'industry-voucher',
|
||||||
|
label: '电子凭证',
|
||||||
|
children: resolvedDetail.kuaishouIndustryVoucher ? (
|
||||||
|
<KuaishouIndustryVoucherPanel voucher={resolvedDetail.kuaishouIndustryVoucher} />
|
||||||
|
) : (
|
||||||
|
<Empty description="无电子凭证绑定" />
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'resolution',
|
key: 'resolution',
|
||||||
label: '兑换重试',
|
label: '兑换重试',
|
||||||
@@ -457,6 +491,8 @@ function TaskActionPanel({
|
|||||||
onPrepareKuaishouCloud,
|
onPrepareKuaishouCloud,
|
||||||
onDispatchKuaishouCloud,
|
onDispatchKuaishouCloud,
|
||||||
onReturnKuaishouCloud,
|
onReturnKuaishouCloud,
|
||||||
|
onResendIndustryVoucher,
|
||||||
|
onConsumeIndustryVoucher,
|
||||||
onMarkManualReview,
|
onMarkManualReview,
|
||||||
onCloseTask,
|
onCloseTask,
|
||||||
}: {
|
}: {
|
||||||
@@ -469,94 +505,147 @@ function TaskActionPanel({
|
|||||||
onPrepareKuaishouCloud: () => void
|
onPrepareKuaishouCloud: () => void
|
||||||
onDispatchKuaishouCloud: () => void
|
onDispatchKuaishouCloud: () => void
|
||||||
onReturnKuaishouCloud: () => void
|
onReturnKuaishouCloud: () => void
|
||||||
|
onResendIndustryVoucher: () => void
|
||||||
|
onConsumeIndustryVoucher: () => void
|
||||||
onMarkManualReview: () => void
|
onMarkManualReview: () => void
|
||||||
onCloseTask: () => void
|
onCloseTask: () => void
|
||||||
}) {
|
}) {
|
||||||
const operations = detail.operations
|
const operations = detail.operations
|
||||||
const loading = Boolean(actionLoadingKey)
|
const loading = Boolean(actionLoadingKey)
|
||||||
|
const hasCloudActions =
|
||||||
|
operations.canPrepareKuaishouCloudFulfillment ||
|
||||||
|
operations.canRebindKuaishouCloudRole ||
|
||||||
|
operations.canDispatchKuaishouCloudFulfillment ||
|
||||||
|
operations.canReturnKuaishouCloudFulfillment
|
||||||
|
const hasIndustryVoucherActions =
|
||||||
|
operations.canResendKuaishouIndustryVoucherCode ||
|
||||||
|
operations.canConsumeKuaishouIndustryVoucher ||
|
||||||
|
Boolean(detail.kuaishouIndustryVoucher)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="任务操作">
|
<Card title="任务操作">
|
||||||
<Space wrap>
|
<div className="task-action-groups">
|
||||||
{canManageTaskLifecycle ? (
|
{canManageTaskLifecycle ? (
|
||||||
<Button
|
<section className="task-action-group">
|
||||||
type="primary"
|
<Typography.Text type="secondary">通用</Typography.Text>
|
||||||
icon={<ReloadOutlined />}
|
<Space wrap>
|
||||||
disabled={loading || !operations.canRetry}
|
<Button
|
||||||
loading={actionLoadingKey === 'retry'}
|
type="primary"
|
||||||
onClick={onRetry}
|
icon={<ReloadOutlined />}
|
||||||
>
|
disabled={loading || !operations.canRetry}
|
||||||
重试任务
|
loading={actionLoadingKey === 'retry'}
|
||||||
</Button>
|
onClick={onRetry}
|
||||||
|
>
|
||||||
|
重试任务
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<UserSwitchOutlined />}
|
||||||
|
disabled={loading || !operations.canMarkManualReview}
|
||||||
|
loading={actionLoadingKey === 'manual-review'}
|
||||||
|
onClick={onMarkManualReview}
|
||||||
|
>
|
||||||
|
转人工
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
{operations.canRebindKuaishouCloudRole ? (
|
|
||||||
<Button
|
{hasCloudActions ? (
|
||||||
icon={<SwapOutlined />}
|
<section className="task-action-group">
|
||||||
disabled={loading}
|
<Typography.Text type="secondary">快手 Cloud</Typography.Text>
|
||||||
loading={actionLoadingKey === 'rebind-role'}
|
<Space wrap>
|
||||||
onClick={onRebindRole}
|
{canManageTaskLifecycle && operations.canPrepareKuaishouCloudFulfillment ? (
|
||||||
>
|
<Button
|
||||||
换绑角色
|
type="primary"
|
||||||
</Button>
|
ghost
|
||||||
|
icon={<ToolOutlined />}
|
||||||
|
disabled={loading}
|
||||||
|
loading={actionLoadingKey === 'prepare-kuaishou-cloud'}
|
||||||
|
onClick={onPrepareKuaishouCloud}
|
||||||
|
>
|
||||||
|
准备资源
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{operations.canRebindKuaishouCloudRole ? (
|
||||||
|
<Button
|
||||||
|
icon={<SwapOutlined />}
|
||||||
|
disabled={loading}
|
||||||
|
loading={actionLoadingKey === 'rebind-role'}
|
||||||
|
onClick={onRebindRole}
|
||||||
|
>
|
||||||
|
换绑角色
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{canManageTaskLifecycle && operations.canDispatchKuaishouCloudFulfillment ? (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<SendOutlined />}
|
||||||
|
disabled={loading}
|
||||||
|
loading={actionLoadingKey === 'dispatch-kuaishou-cloud'}
|
||||||
|
onClick={onDispatchKuaishouCloud}
|
||||||
|
>
|
||||||
|
发货
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{canManageTaskLifecycle && operations.canReturnKuaishouCloudFulfillment ? (
|
||||||
|
<Button
|
||||||
|
icon={<RollbackOutlined />}
|
||||||
|
disabled={loading}
|
||||||
|
loading={actionLoadingKey === 'return-kuaishou-cloud'}
|
||||||
|
onClick={onReturnKuaishouCloud}
|
||||||
|
>
|
||||||
|
退号
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
{canManageTaskLifecycle && operations.canPrepareKuaishouCloudFulfillment ? (
|
|
||||||
<Button
|
{canManageTaskLifecycle && hasIndustryVoucherActions ? (
|
||||||
type="primary"
|
<section className="task-action-group">
|
||||||
ghost
|
<Typography.Text type="secondary">电子凭证</Typography.Text>
|
||||||
icon={<ToolOutlined />}
|
<Space wrap>
|
||||||
disabled={loading}
|
<Button
|
||||||
loading={actionLoadingKey === 'prepare-kuaishou-cloud'}
|
icon={<ReloadOutlined />}
|
||||||
onClick={onPrepareKuaishouCloud}
|
disabled={loading || !operations.canResendKuaishouIndustryVoucherCode}
|
||||||
>
|
loading={actionLoadingKey === 'resend-industry-voucher'}
|
||||||
准备绑定资源
|
onClick={onResendIndustryVoucher}
|
||||||
</Button>
|
>
|
||||||
) : null}
|
重发发码
|
||||||
{canManageTaskLifecycle && operations.canDispatchKuaishouCloudFulfillment ? (
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<SendOutlined />}
|
ghost
|
||||||
disabled={loading}
|
icon={<CheckCircleOutlined />}
|
||||||
loading={actionLoadingKey === 'dispatch-kuaishou-cloud'}
|
disabled={loading || !operations.canConsumeKuaishouIndustryVoucher}
|
||||||
onClick={onDispatchKuaishouCloud}
|
loading={actionLoadingKey === 'consume-industry-voucher'}
|
||||||
>
|
onClick={onConsumeIndustryVoucher}
|
||||||
确认绑定完成并发货
|
>
|
||||||
</Button>
|
核销凭证
|
||||||
) : null}
|
</Button>
|
||||||
{canManageTaskLifecycle && operations.canReturnKuaishouCloudFulfillment ? (
|
</Space>
|
||||||
<Button
|
</section>
|
||||||
icon={<RollbackOutlined />}
|
|
||||||
disabled={loading}
|
|
||||||
loading={actionLoadingKey === 'return-kuaishou-cloud'}
|
|
||||||
onClick={onReturnKuaishouCloud}
|
|
||||||
>
|
|
||||||
退还号码
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
{canManageTaskLifecycle ? (
|
|
||||||
<Button
|
|
||||||
icon={<UserSwitchOutlined />}
|
|
||||||
disabled={loading || !operations.canMarkManualReview}
|
|
||||||
loading={actionLoadingKey === 'manual-review'}
|
|
||||||
onClick={onMarkManualReview}
|
|
||||||
>
|
|
||||||
转人工
|
|
||||||
</Button>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{canCloseTasks ? (
|
{canCloseTasks ? (
|
||||||
<Button
|
<section className="task-action-group">
|
||||||
danger
|
<Typography.Text type="secondary">收口</Typography.Text>
|
||||||
icon={<CloseCircleOutlined />}
|
<Button
|
||||||
disabled={loading || !operations.canClose}
|
danger
|
||||||
loading={actionLoadingKey === 'close'}
|
icon={<CloseCircleOutlined />}
|
||||||
onClick={onCloseTask}
|
disabled={loading || !operations.canClose}
|
||||||
>
|
loading={actionLoadingKey === 'close'}
|
||||||
关闭任务
|
onClick={onCloseTask}
|
||||||
</Button>
|
>
|
||||||
|
关闭任务
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
{!canManageTaskLifecycle && !canCloseTasks && !operations.canRebindKuaishouCloudRole ? (
|
|
||||||
|
{!canManageTaskLifecycle && !canCloseTasks && !hasCloudActions ? (
|
||||||
<Typography.Text type="secondary">当前账号没有可执行的任务操作</Typography.Text>
|
<Typography.Text type="secondary">当前账号没有可执行的任务操作</Typography.Text>
|
||||||
) : null}
|
) : null}
|
||||||
</Space>
|
</div>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Typography.Text className="task-action-hint" type="secondary">
|
<Typography.Text className="task-action-hint" type="secondary">
|
||||||
操作执行中,请稍候。
|
操作执行中,请稍候。
|
||||||
@@ -566,6 +655,236 @@ function TaskActionPanel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FulfillmentOverviewPanel({
|
||||||
|
detail,
|
||||||
|
claimUrl,
|
||||||
|
claimLinkInvalid,
|
||||||
|
}: {
|
||||||
|
detail: AdminTaskDetail
|
||||||
|
claimUrl: string
|
||||||
|
claimLinkInvalid: boolean
|
||||||
|
}) {
|
||||||
|
const executor = resolveTaskExecutorDisplay(detail.task.executorKey)
|
||||||
|
const steps = buildFulfillmentOverviewSteps(detail, claimUrl, claimLinkInvalid)
|
||||||
|
const currentBlocker = resolveTaskBlocker(detail.task, claimUrl, claimLinkInvalid)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title="履约链路总览"
|
||||||
|
extra={<Tag color={executor.color}>{executor.label}</Tag>}
|
||||||
|
>
|
||||||
|
<div className="fulfillment-overview-grid">
|
||||||
|
{steps.map((step) => (
|
||||||
|
<div key={step.key} className="fulfillment-step-card">
|
||||||
|
<Space size={[6, 6]} wrap>
|
||||||
|
<StatusTag value={step.status} />
|
||||||
|
<Typography.Text strong>{step.label}</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
<Typography.Text type="secondary">{step.detail}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Alert
|
||||||
|
className="platform-section-gap"
|
||||||
|
type={currentBlocker.level}
|
||||||
|
showIcon
|
||||||
|
message={currentBlocker.title}
|
||||||
|
description={currentBlocker.detail}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFulfillmentOverviewSteps(
|
||||||
|
detail: AdminTaskDetail,
|
||||||
|
claimUrl: string,
|
||||||
|
claimLinkInvalid: boolean,
|
||||||
|
) {
|
||||||
|
const task = detail.task
|
||||||
|
const executor = resolveTaskExecutorDisplay(task.executorKey)
|
||||||
|
const voucher = detail.kuaishouIndustryVoucher
|
||||||
|
const manualDispatch = detail.manualDispatch
|
||||||
|
const claimStatus = claimUrl
|
||||||
|
? 'success'
|
||||||
|
: claimLinkInvalid
|
||||||
|
? 'failed'
|
||||||
|
: detail.claimToken?.status || 'pending'
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'source',
|
||||||
|
label: '订单来源',
|
||||||
|
status: detail.order?.payStatus || task.status,
|
||||||
|
detail: `${detail.order?.provider || '-'} / ${detail.order?.platform || '-'} / ${task.platformOrderId || '-'}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'product',
|
||||||
|
label: '商品解析',
|
||||||
|
status: detail.orderItem || task.skuName ? 'success' : 'pending_config',
|
||||||
|
detail: detail.orderItem?.skuName || task.skuName || task.skuCode || '未命中履约商品',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'planner',
|
||||||
|
label: '履约计划',
|
||||||
|
status: task.executorKey ? 'success' : 'pending_config',
|
||||||
|
detail: executor.label,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'prepare',
|
||||||
|
label: '任务准备',
|
||||||
|
status: task.resourceStatus || task.status,
|
||||||
|
detail: resolveTaskPrepareSummary(detail),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delivery',
|
||||||
|
label: '平台执行',
|
||||||
|
status: resolveDeliveryStepStatus(detail),
|
||||||
|
detail: resolveDeliveryStepSummary(detail),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'voucher',
|
||||||
|
label: '电子凭证',
|
||||||
|
status: voucher ? voucher.sendCallbackStatus || voucher.status : 'skipped',
|
||||||
|
detail: voucher
|
||||||
|
? `发码 ${voucher.sendCallbackStatus || '-'} / 凭证 ${voucher.status || '-'}`
|
||||||
|
: '当前任务未绑定电子凭证',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'claim',
|
||||||
|
label: '交付链接',
|
||||||
|
status: task.executorKey === 'manual_dispatch' ? 'skipped' : claimStatus,
|
||||||
|
detail:
|
||||||
|
task.executorKey === 'manual_dispatch'
|
||||||
|
? '人工履约不需要客户领取链接'
|
||||||
|
: claimUrl
|
||||||
|
? '已有可用链接'
|
||||||
|
: claimLinkInvalid
|
||||||
|
? '链接已失效'
|
||||||
|
: '等待生成或同步链接',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'finish',
|
||||||
|
label: '履约收口',
|
||||||
|
status: manualDispatch?.outcome || task.deliveryStatus || task.status,
|
||||||
|
detail: task.resultMessage || task.lastError || '等待履约完成',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTaskBlocker(
|
||||||
|
task: AdminTaskDetail['task'],
|
||||||
|
claimUrl: string,
|
||||||
|
claimLinkInvalid: boolean,
|
||||||
|
): { level: 'success' | 'info' | 'warning' | 'error'; title: string; detail: string } {
|
||||||
|
if (['completed', 'redeemed', 'delivered'].includes(task.status) || task.deliveryStatus === 'delivered') {
|
||||||
|
return {
|
||||||
|
level: 'success',
|
||||||
|
title: '履约已完成',
|
||||||
|
detail: task.resultMessage || '任务已经收口。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['manual_review', 'retry_pending', 'failed'].includes(task.status)) {
|
||||||
|
return {
|
||||||
|
level: 'error',
|
||||||
|
title: '当前卡在异常处理',
|
||||||
|
detail: task.lastError || task.resultMessage || '需要人工检查后重试或收口。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claimLinkInvalid) {
|
||||||
|
return {
|
||||||
|
level: 'warning',
|
||||||
|
title: '领取链接不可用',
|
||||||
|
detail: '链接状态不是 active,可重新生成或检查平台交付链接。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!claimUrl && task.executorKey !== 'manual_dispatch') {
|
||||||
|
return {
|
||||||
|
level: 'warning',
|
||||||
|
title: '等待交付链接',
|
||||||
|
detail: '任务尚未拿到可交付链接,先检查任务准备和平台执行状态。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.customerStatus === 'waiting_customer') {
|
||||||
|
return {
|
||||||
|
level: 'info',
|
||||||
|
title: '等待客户操作',
|
||||||
|
detail: '资源已准备,客户还没有完成绑定或领取动作。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
level: 'info',
|
||||||
|
title: '履约推进中',
|
||||||
|
detail: task.lastError || '当前任务没有明确异常,可按链路状态继续观察。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTaskPrepareSummary(detail: AdminTaskDetail) {
|
||||||
|
const flow = detail.kuaishouCloudFulfillment
|
||||||
|
if (flow) {
|
||||||
|
return flow.binding.bindPreparedAt
|
||||||
|
? `绑定资源 ${flow.binding.vnPhone || flow.binding.vnId || '-'}`
|
||||||
|
: '等待准备 Cloud 资源'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detail.task.executorKey === 'manual_dispatch') {
|
||||||
|
return '人工履约任务无需自动准备资源'
|
||||||
|
}
|
||||||
|
|
||||||
|
return detail.task.resultMessage || detail.task.lastError || '等待执行器准备'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDeliveryStepStatus(detail: AdminTaskDetail) {
|
||||||
|
const flow = detail.kuaishouCloudFulfillment
|
||||||
|
if (flow) {
|
||||||
|
return flow.dispatch.status || detail.task.deliveryStatus || detail.task.status
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detail.manualDispatch) {
|
||||||
|
return detail.manualDispatch.outcome || detail.task.deliveryStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
return detail.task.deliveryStatus || detail.task.status
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDeliveryStepSummary(detail: AdminTaskDetail) {
|
||||||
|
const flow = detail.kuaishouCloudFulfillment
|
||||||
|
if (flow) {
|
||||||
|
if (flow.dispatch.dispatchAt) {
|
||||||
|
return `已于 ${formatAdminDateTime(flow.dispatch.dispatchAt)} 发货`
|
||||||
|
}
|
||||||
|
return flow.dispatch.note || '等待平台发货执行'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detail.manualDispatch) {
|
||||||
|
return detail.manualDispatch.resultMessage || detail.manualDispatch.deliveryReference || '人工履约已回写'
|
||||||
|
}
|
||||||
|
|
||||||
|
return detail.task.resultMessage || detail.task.lastError || '等待执行器推进'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTaskExecutorDisplay(value: unknown) {
|
||||||
|
const executorKey = String(value || '').trim()
|
||||||
|
if (executorKey === 'kuaishou_ct_assisted') {
|
||||||
|
return { label: '快手 Cloud', color: 'blue' }
|
||||||
|
}
|
||||||
|
if (executorKey === 'kuaishou-industry') {
|
||||||
|
return { label: '行业电子凭证', color: 'cyan' }
|
||||||
|
}
|
||||||
|
if (executorKey === 'kuaishou_feifei') {
|
||||||
|
return { label: 'kuaishou-feifei', color: 'purple' }
|
||||||
|
}
|
||||||
|
if (executorKey === 'manual_dispatch') {
|
||||||
|
return { label: '人工履约', color: 'orange' }
|
||||||
|
}
|
||||||
|
return { label: executorKey || '未分配执行器', color: 'default' }
|
||||||
|
}
|
||||||
|
|
||||||
function ManualDispatchPanel({
|
function ManualDispatchPanel({
|
||||||
detail,
|
detail,
|
||||||
form,
|
form,
|
||||||
@@ -680,6 +999,65 @@ function ManualDispatchPanel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function KuaishouIndustryVoucherPanel({ voucher }: { voucher: KuaishouIndustryVoucher }) {
|
||||||
|
const consumeDetails = Array.isArray(voucher.consumeDetails) ? voucher.consumeDetails : []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="电子凭证">
|
||||||
|
<Descriptions column={{ xs: 1, md: 2, xl: 3 }} bordered size="small">
|
||||||
|
<Descriptions.Item label="订单号">{voucher.oid || '-'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="凭证码">{voucher.voucherCode || '-'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="序号">{voucher.unitIndex || '-'}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="凭证状态">
|
||||||
|
<StatusTag value={voucher.status} />
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="发码回调">
|
||||||
|
<StatusTag value={voucher.sendCallbackStatus} />
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="发码次数">{voucher.sendCallbackAttemptCount}</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="发码时间">
|
||||||
|
{formatAdminDateTime(voucher.sendCallbackSentAt)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="核销流水">
|
||||||
|
{voucher.consumeSerialNum || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="核销时间">
|
||||||
|
{formatAdminDateTime(voucher.consumedAt)}
|
||||||
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="最近异常" span={3}>
|
||||||
|
{voucher.sendCallbackLastError || '-'}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
|
||||||
|
{voucher.sendCallbackLastError ? (
|
||||||
|
<Alert
|
||||||
|
className="platform-section-gap"
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message="电子凭证发码回调存在异常"
|
||||||
|
description={voucher.sendCallbackLastError}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{consumeDetails.length > 0 ? (
|
||||||
|
<Table<Record<string, unknown>>
|
||||||
|
className="platform-section-gap"
|
||||||
|
rowKey={(_, index) => String(index ?? 0)}
|
||||||
|
pagination={false}
|
||||||
|
dataSource={consumeDetails}
|
||||||
|
scroll={{ x: 760 }}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: '核销明细',
|
||||||
|
render: (_, row) => <JsonPreview value={row} />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function KuaishouCloudPanel({
|
function KuaishouCloudPanel({
|
||||||
flow,
|
flow,
|
||||||
canRefresh,
|
canRefresh,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
SwapOutlined,
|
SwapOutlined,
|
||||||
UserSwitchOutlined,
|
UserSwitchOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { Alert, App, Button, Card, DatePicker, Form, Input, Select, Space, Table, Typography } from 'antd'
|
import { Alert, App, Button, Card, DatePicker, Form, Input, Select, Space, Table, Tag, Typography } from 'antd'
|
||||||
import type { TableColumnsType } from 'antd'
|
import type { TableColumnsType } from 'antd'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
@@ -36,6 +36,12 @@ type TaskFilterForm = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TaskListAction = 'retry' | 'rebind_role' | 'manual_review' | 'close'
|
type TaskListAction = 'retry' | 'rebind_role' | 'manual_review' | 'close'
|
||||||
|
type TaskListBlocker = {
|
||||||
|
label: string
|
||||||
|
color: string
|
||||||
|
level: 'normal' | 'error'
|
||||||
|
detail: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function AdminTasksPage() {
|
export default function AdminTasksPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -100,12 +106,38 @@ export default function AdminTasksPage() {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '履约 / 卡点',
|
||||||
|
minWidth: 260,
|
||||||
|
render: (_, row) => {
|
||||||
|
const executor = resolveTaskExecutorDisplay(row.executorKey)
|
||||||
|
const blocker = resolveTaskListBlocker(row)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Space size={[4, 4]} wrap>
|
||||||
|
<Tag color={executor.color}>{executor.label}</Tag>
|
||||||
|
<Tag color={blocker.color}>{blocker.label}</Tag>
|
||||||
|
</Space>
|
||||||
|
<Typography.Text
|
||||||
|
className="task-list-blocker-detail"
|
||||||
|
type={blocker.level === 'error' ? 'danger' : 'secondary'}
|
||||||
|
title={blocker.detail}
|
||||||
|
ellipsis
|
||||||
|
>
|
||||||
|
{blocker.detail}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
minWidth: 250,
|
minWidth: 280,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space size={[0, 6]} wrap>
|
<Space size={[0, 6]} wrap>
|
||||||
<StatusTag value={row.status} kind="task" />
|
<StatusTag value={row.status} kind="task" />
|
||||||
|
<StatusTag value={row.deliveryStatus} kind="delivery" />
|
||||||
<StatusTag value={row.resourceStatus} kind="resource" />
|
<StatusTag value={row.resourceStatus} kind="resource" />
|
||||||
<StatusTag value={row.customerStatus} kind="customer" />
|
<StatusTag value={row.customerStatus} kind="customer" />
|
||||||
</Space>
|
</Space>
|
||||||
@@ -364,7 +396,7 @@ export default function AdminTasksPage() {
|
|||||||
loading={query.isLoading || query.isFetching}
|
loading={query.isLoading || query.isFetching}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data?.items || []}
|
dataSource={data?.items || []}
|
||||||
scroll={{ x: 1240 }}
|
scroll={{ x: 1500 }}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: data?.pagination.page || page,
|
current: data?.pagination.page || page,
|
||||||
pageSize: data?.pagination.pageSize || pageSize,
|
pageSize: data?.pagination.pageSize || pageSize,
|
||||||
@@ -389,6 +421,133 @@ function canRebindListTask(item: AdminTaskListItem, canManageTaskLifecycle: bool
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveTaskExecutorDisplay(value: unknown) {
|
||||||
|
const executorKey = String(value || '').trim()
|
||||||
|
if (executorKey === 'kuaishou_ct_assisted') {
|
||||||
|
return { label: '快手 Cloud', color: 'blue' }
|
||||||
|
}
|
||||||
|
if (executorKey === 'kuaishou-industry') {
|
||||||
|
return { label: '行业电子凭证', color: 'cyan' }
|
||||||
|
}
|
||||||
|
if (executorKey === 'kuaishou_feifei') {
|
||||||
|
return { label: 'kuaishou-feifei', color: 'purple' }
|
||||||
|
}
|
||||||
|
if (executorKey === 'manual_dispatch') {
|
||||||
|
return { label: '人工履约', color: 'orange' }
|
||||||
|
}
|
||||||
|
return { label: executorKey || '未分配执行器', color: 'default' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTaskListBlocker(item: AdminTaskListItem): TaskListBlocker {
|
||||||
|
const taskStatus = normalizeTaskListStatus(item.status)
|
||||||
|
const deliveryStatus = normalizeTaskListStatus(item.deliveryStatus)
|
||||||
|
const resourceStatus = normalizeTaskListStatus(item.resourceStatus)
|
||||||
|
const customerStatus = normalizeTaskListStatus(item.customerStatus)
|
||||||
|
const errorText = item.lastError || item.resultMessage
|
||||||
|
|
||||||
|
if (
|
||||||
|
['completed', 'redeemed'].includes(taskStatus) ||
|
||||||
|
['delivered', 'completed', 'success', 'consumed'].includes(deliveryStatus)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
label: '已收口',
|
||||||
|
color: 'green',
|
||||||
|
level: 'normal',
|
||||||
|
detail: item.resultMessage || '履约链路已完成。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskStatus === 'closed') {
|
||||||
|
return {
|
||||||
|
label: '已关闭',
|
||||||
|
color: 'default',
|
||||||
|
level: 'normal',
|
||||||
|
detail: item.resultMessage || item.lastError || '任务已关闭,不会继续自动推进。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
item.lastError ||
|
||||||
|
['manual_review', 'retry_pending', 'failed'].includes(taskStatus) ||
|
||||||
|
['resource_exception', 'binding_exception', 'failed'].includes(resourceStatus) ||
|
||||||
|
['customer_exception', 'failed'].includes(customerStatus) ||
|
||||||
|
['failed'].includes(deliveryStatus)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
label: '异常处理',
|
||||||
|
color: 'red',
|
||||||
|
level: 'error',
|
||||||
|
detail: errorText || '需要进入详情页按履约层级检查并重试。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.executorKey === 'manual_dispatch') {
|
||||||
|
return {
|
||||||
|
label: '人工履约',
|
||||||
|
color: 'orange',
|
||||||
|
level: 'normal',
|
||||||
|
detail: item.resultMessage || '等待人工回写履约结果。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['pending_config'].includes(resourceStatus) || !item.executorKey) {
|
||||||
|
return {
|
||||||
|
label: '待配置',
|
||||||
|
color: 'gold',
|
||||||
|
level: 'normal',
|
||||||
|
detail: item.resultMessage || '商品还没有命中明确的履约配置。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
['not_started', 'pending', 'pending_prepare', 'pending_binding', 'pending_binding_prepare'].includes(
|
||||||
|
resourceStatus,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
label: '资源层',
|
||||||
|
color: 'gold',
|
||||||
|
level: 'normal',
|
||||||
|
detail: item.resultMessage || '等待准备履约资源,可在详情页触发对应执行器动作。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
['waiting_customer', 'waiting_binding', 'waiting_user_claim', 'user_binding'].includes(
|
||||||
|
customerStatus,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
label: '客户层',
|
||||||
|
color: 'blue',
|
||||||
|
level: 'normal',
|
||||||
|
detail: item.roleConfirmedAt
|
||||||
|
? '客户已提交角色信息,等待平台履约收口。'
|
||||||
|
: '等待客户打开链接、绑定或提交信息。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deliveryStatus && !['pending', 'not_started'].includes(deliveryStatus)) {
|
||||||
|
return {
|
||||||
|
label: '平台层',
|
||||||
|
color: 'cyan',
|
||||||
|
level: 'normal',
|
||||||
|
detail: item.resultMessage || '平台履约执行中,后续可在详情页按层查看。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: '推进中',
|
||||||
|
color: 'blue',
|
||||||
|
level: 'normal',
|
||||||
|
detail: item.resultMessage || '当前没有明确异常,等待任务自动推进。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTaskListStatus(value: unknown) {
|
||||||
|
return String(value || '').trim().toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
function cleanParams(params: Record<string, unknown>) {
|
function cleanParams(params: Record<string, unknown>) {
|
||||||
const next = new URLSearchParams()
|
const next = new URLSearchParams()
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
|||||||
@@ -91,3 +91,26 @@ export function returnNumberAdminTaskKuaishouCloudFulfillment(taskId: number | s
|
|||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resendAdminTaskKuaishouIndustryVoucherCode(taskId: number | string) {
|
||||||
|
return apiPost<AdminTaskActionResponse>(
|
||||||
|
`/api/v1/admin/tasks/${taskId}/kuaishou-industry/resend-code`,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function consumeAdminTaskKuaishouIndustryVoucher(
|
||||||
|
taskId: number | string,
|
||||||
|
payload: {
|
||||||
|
consumeType?: string
|
||||||
|
storeName?: string
|
||||||
|
storeAddress?: string
|
||||||
|
expressCode?: string
|
||||||
|
expressNo?: string
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
return apiPost<AdminTaskActionResponse>(
|
||||||
|
`/api/v1/admin/tasks/${taskId}/kuaishou-industry/consume`,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ select {
|
|||||||
.cell-stack {
|
.cell-stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.muted {
|
.muted {
|
||||||
@@ -310,6 +311,55 @@ select {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-action-groups {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-action-group {
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 86px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #edf0f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f8fafc;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-action-group > .ant-typography {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-list-blocker-detail {
|
||||||
|
max-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fulfillment-overview-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fulfillment-step-card {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #edf0f5;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #ffffff;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fulfillment-step-card .ant-typography {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.manual-dispatch-grid {
|
.manual-dispatch-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
@@ -1003,6 +1053,7 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.manual-dispatch-grid,
|
.manual-dispatch-grid,
|
||||||
|
.fulfillment-overview-grid,
|
||||||
.task-flow-grid {
|
.task-flow-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export interface AdminTaskOperations {
|
|||||||
canRebindKuaishouCloudRole: boolean
|
canRebindKuaishouCloudRole: boolean
|
||||||
canDispatchKuaishouCloudFulfillment: boolean
|
canDispatchKuaishouCloudFulfillment: boolean
|
||||||
canReturnKuaishouCloudFulfillment: boolean
|
canReturnKuaishouCloudFulfillment: boolean
|
||||||
|
canResendKuaishouIndustryVoucherCode?: boolean
|
||||||
|
canConsumeKuaishouIndustryVoucher?: boolean
|
||||||
canViewSensitiveTaskData: boolean
|
canViewSensitiveTaskData: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +203,20 @@ export interface AdminTaskDetail {
|
|||||||
}
|
}
|
||||||
notes: string
|
notes: string
|
||||||
}
|
}
|
||||||
|
kuaishouIndustryVoucher?: null | {
|
||||||
|
oid: string
|
||||||
|
voucherCode: string
|
||||||
|
unitIndex: number
|
||||||
|
status: string
|
||||||
|
sendCallbackStatus: string
|
||||||
|
sendCallbackLastError: string
|
||||||
|
sendCallbackAttemptCount: number
|
||||||
|
sendCallbackSentAt: string | null
|
||||||
|
consumeSerialNum: string
|
||||||
|
consumeDetails: Array<Record<string, unknown>>
|
||||||
|
consumedAt: string | null
|
||||||
|
destroyedAt: string | null
|
||||||
|
}
|
||||||
manualDispatch: null | {
|
manualDispatch: null | {
|
||||||
outcome: string
|
outcome: string
|
||||||
deliveryReference: string
|
deliveryReference: string
|
||||||
|
|||||||
Reference in New Issue
Block a user