feat(frontend): add P2 quality gates

This commit is contained in:
yml2213
2026-05-15 17:35:50 +08:00
parent a65c0713fb
commit 787b01749c
62 changed files with 4907 additions and 1355 deletions
@@ -33,7 +33,7 @@
gap: var(--space-4);
background:
radial-gradient(circle at top right, rgba(14, 165, 233, 0.12), transparent 26%),
linear-gradient(135deg, rgba(255,255,255,.98), rgba(239,246,255,.96));
linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(239, 246, 255, 0.96));
}
.hero-copy {
@@ -58,7 +58,7 @@
color: var(--color-primary);
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: .08em;
letter-spacing: 0.08em;
text-transform: uppercase;
}
@@ -72,7 +72,7 @@
padding: 14px var(--space-4);
border-radius: 18px;
border: 1px solid var(--border-default);
background: rgba(255,255,255,.82);
background: rgba(255, 255, 255, 0.82);
}
.hero-metric span {
@@ -89,10 +89,18 @@
word-break: break-word;
}
.hero-metric--blue { box-shadow: inset 0 0 0 1px rgba(59,130,246,.08); }
.hero-metric--green { box-shadow: inset 0 0 0 1px rgba(16,185,129,.08); }
.hero-metric--amber { box-shadow: inset 0 0 0 1px rgba(245,158,11,.08); }
.hero-metric--slate { box-shadow: inset 0 0 0 1px rgba(100,116,139,.08); }
.hero-metric--blue {
box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.08);
}
.hero-metric--green {
box-shadow: inset 0 0 0 1px rgba(16, 185, 129, 0.08);
}
.hero-metric--amber {
box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.08);
}
.hero-metric--slate {
box-shadow: inset 0 0 0 1px rgba(100, 116, 139, 0.08);
}
.hero-inline,
.hero-link-row {
@@ -126,7 +134,7 @@
margin-top: 14px;
padding: 14px var(--space-4);
border-radius: var(--radius-md);
background: rgba(248,250,252,.92);
background: rgba(248, 250, 252, 0.92);
border: 1px solid var(--border-default);
}
@@ -174,7 +182,7 @@
padding: 14px var(--space-4);
border-radius: var(--radius-md);
border: 1px solid var(--border-default);
background: rgba(248,250,252,.92);
background: rgba(248, 250, 252, 0.92);
}
.redeem-summary-card span,
@@ -260,7 +268,7 @@
margin-top: var(--space-4);
padding: 14px;
border-radius: var(--radius-md);
background: rgba(248,250,252,.9);
background: rgba(248, 250, 252, 0.9);
}
.manual-summary p {
@@ -277,7 +285,7 @@
gap: var(--space-4);
background:
radial-gradient(circle at top right, rgba(37, 99, 235, 0.08), transparent 28%),
linear-gradient(180deg, rgba(255,255,255,.96), rgba(248,250,252,.96));
linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(248, 250, 252, 0.96));
}
.flow-card-head {
@@ -304,13 +312,13 @@
padding: var(--space-4);
border-radius: 18px;
border: 1px solid var(--border-default);
background: rgba(248,250,252,.9);
background: rgba(248, 250, 252, 0.9);
}
.flow-block--spotlight {
background:
linear-gradient(160deg, rgba(255,255,255,.98), rgba(239,246,255,.95)),
rgba(248,250,252,.9);
linear-gradient(160deg, rgba(255, 255, 255, 0.98), rgba(239, 246, 255, 0.95)),
rgba(248, 250, 252, 0.9);
}
.flow-block h4 {
@@ -386,7 +394,7 @@
.checklist-item {
padding: 14px var(--space-4);
border-radius: var(--radius-md);
background: rgba(255,255,255,.84);
background: rgba(255, 255, 255, 0.84);
border: 1px solid var(--border-default);
}
@@ -418,7 +426,7 @@
.role-hero div {
padding: 14px var(--space-4);
border-radius: var(--radius-md);
background: rgba(255,255,255,.88);
background: rgba(255, 255, 255, 0.88);
border: 1px solid var(--border-default);
}
@@ -471,7 +479,7 @@
width: min(100%, 720px);
border-radius: 18px;
border: 1px solid var(--border-default);
box-shadow: 0 20px 48px rgba(15,23,42,.08);
box-shadow: 0 20px 48px rgba(15, 23, 42, 0.08);
}
@media (max-width: 900px) {
@@ -22,7 +22,15 @@ import {
} from '@/services/admin'
import type { AdminTaskActionResponse, AdminTaskDetail } from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth'
import { formatAutoDeliveryReason, formatAutoDeliveryTrigger, formatKuaishouRoleInfoLabel, formatRedeemOutcomeLabel, formatRedeemResolutionStatus, formatTaskEventPayload, formatTaskEventType } from '@/utils/admin-display'
import {
formatAutoDeliveryReason,
formatAutoDeliveryTrigger,
formatKuaishouRoleInfoLabel,
formatRedeemOutcomeLabel,
formatRedeemResolutionStatus,
formatTaskEventPayload,
formatTaskEventType,
} from '@/utils/admin-display'
import { formatAdminDateTime } from '@/utils/admin-time'
const route = useRoute()
@@ -39,11 +47,15 @@ const manualDispatchForm = reactive({
})
const canManageTaskLifecycle = computed(() => hasAdminRole('operator'))
const canCloseTasks = computed(() => hasAdminRole('support'))
const canViewSensitiveTaskData = computed(() => Boolean(detail.value?.operations.canViewSensitiveTaskData))
const canViewSensitiveTaskData = computed(() =>
Boolean(detail.value?.operations.canViewSensitiveTaskData),
)
const kuaishouCloudFlow = computed(() => detail.value?.kuaishouCloudFulfillment || null)
const kuaishouCloudRole = computed(() => kuaishouCloudFlow.value?.role || null)
const kuaishouCloudCollapsePanels = ref(['claim', 'ticket', 'cloud'])
const isKuaishouCloudTask = computed(() => detail.value?.task.executorKey === 'kuaishou_ct_assisted')
const isKuaishouCloudTask = computed(
() => detail.value?.task.executorKey === 'kuaishou_ct_assisted',
)
const summaryCards = computed(() => {
if (!detail.value) {
return []
@@ -114,9 +126,10 @@ const kuaishouCloudChecklist = computed(() => {
key: 'role',
label: '角色识别',
status: role?.status || 'pending',
detail: role?.name || role?.rid
? `${role?.name || '-'} / ${role?.rid || '-'}`
: '客户绑定后刷新角色信息',
detail:
role?.name || role?.rid
? `${role?.name || '-'} / ${role?.rid || '-'}`
: '客户绑定后刷新角色信息',
},
{
key: 'dispatch',
@@ -140,7 +153,7 @@ const kuaishouCloudChecklist = computed(() => {
status: flow.consume.status || 'pending',
detail: flow.consume.consumedAt
? `已于 ${formatAdminDateTime(flow.consume.consumedAt)} 完成核销`
: (flow.consume.errorMessage || '等待退号后核销收口'),
: flow.consume.errorMessage || '等待退号后核销收口',
},
]
})
@@ -238,7 +251,8 @@ function clearScreenshotPreview() {
function syncManualDispatchForm(taskDetail: AdminTaskDetail) {
manualDispatchForm.deliveryReference = taskDetail.manualDispatch?.deliveryReference || ''
manualDispatchForm.deliveredCredential = taskDetail.manualDispatch?.deliveredCredential || ''
manualDispatchForm.resultMessage = taskDetail.manualDispatch?.resultMessage || taskDetail.task.resultMessage || ''
manualDispatchForm.resultMessage =
taskDetail.manualDispatch?.resultMessage || taskDetail.task.resultMessage || ''
}
async function submitManualDispatch(outcome: 'delivered' | 'failed') {
@@ -247,17 +261,19 @@ async function submitManualDispatch(outcome: 'delivered' | 'failed') {
}
const actionLabel = outcome === 'failed' ? '标记履约失败' : '标记已完成履约'
const confirmText = outcome === 'failed'
? `确认把任务 ${detail.value.task.taskNo} 回写为人工履约失败吗?这会把任务直接收口并保留失败原因。`
: `确认把任务 ${detail.value.task.taskNo} 回写为人工履约完成吗?这会把任务直接标记为已发放`
const confirmText =
outcome === 'failed'
? `确认把任务 ${detail.value.task.taskNo} 回写为人工履约失败吗?这会把任务直接收口并保留失败原因`
: `确认把任务 ${detail.value.task.taskNo} 回写为人工履约完成吗?这会把任务直接标记为已发放。`
await runAction(
() => completeAdminTaskManualDispatch(detail.value!.task.taskId, {
outcome,
resultMessage: manualDispatchForm.resultMessage,
deliveryReference: manualDispatchForm.deliveryReference,
deliveredCredential: manualDispatchForm.deliveredCredential,
}),
() =>
completeAdminTaskManualDispatch(detail.value!.task.taskId, {
outcome,
resultMessage: manualDispatchForm.resultMessage,
deliveryReference: manualDispatchForm.deliveryReference,
deliveredCredential: manualDispatchForm.deliveredCredential,
}),
actionLabel,
confirmText,
)
@@ -365,10 +381,7 @@ onBeforeUnmount(clearScreenshotPreview)
<template>
<section class="admin-panel">
<AdminPageHeader
title="任务详情"
description="查看任务全量信息并执行人工操作。"
/>
<AdminPageHeader title="任务详情" description="查看任务全量信息并执行人工操作。" />
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
@@ -380,9 +393,9 @@ onBeforeUnmount(clearScreenshotPreview)
<span class="hero-eyebrow">{{ detail.task.executorKey || 'task' }}</span>
<h2>{{ detail.task.taskNo }}</h2>
<p>
订单 {{ detail.order?.platformOrderId || '-' }} ·
商品 {{ detail.orderItem?.skuName || detail.orderItem?.skuCode || '-' }} ·
店铺 {{ detail.order?.shopName || detail.order?.shopId || '-' }}
订单 {{ detail.order?.platformOrderId || '-' }} · 商品
{{ detail.orderItem?.skuName || detail.orderItem?.skuCode || '-' }} · 店铺
{{ detail.order?.shopName || detail.order?.shopId || '-' }}
</p>
</div>
<div class="hero-meta-grid">
@@ -412,9 +425,20 @@ onBeforeUnmount(clearScreenshotPreview)
<section v-else class="info-card">
<h2>{{ detail.task.taskNo }}</h2>
<p>状态<AdminStatusTag :status="detail.task.status" /> · 订单{{ detail.order?.platformOrderId || '-' }}</p>
<p>执行器{{ detail.task.executorKey || '-' }} · 履约<AdminStatusTag :status="detail.task.deliveryStatus" /></p>
<p>系统绑定<AdminStatusTag :status="detail.task.systemBindingStatus" /> · 完整绑定<AdminStatusTag :status="detail.task.userBindingStatus" /></p>
<p>
状态<AdminStatusTag :status="detail.task.status" /> · 订单{{
detail.order?.platformOrderId || '-'
}}
</p>
<p>
执行器{{ detail.task.executorKey || '-' }} · 履约<AdminStatusTag
:status="detail.task.deliveryStatus"
/>
</p>
<p>
系统绑定<AdminStatusTag :status="detail.task.systemBindingStatus" /> ·
完整绑定<AdminStatusTag :status="detail.task.userBindingStatus" />
</p>
<p>角色{{ detail.task.roleName || '-' }} / {{ detail.task.roleId || '-' }}</p>
<p>最后错误{{ detail.task.lastError || '-' }}</p>
<p v-if="claimUrl">
@@ -431,7 +455,13 @@ onBeforeUnmount(clearScreenshotPreview)
:loading="actionLoading"
round
type="primary"
@click="runAction(() => retryAdminTask(detail!.task.taskId), '任务已重试', `确认重试任务 ${detail!.task.taskNo} 吗?`)"
@click="
runAction(
() => retryAdminTask(detail!.task.taskId),
'任务已重试',
`确认重试任务 ${detail!.task.taskNo} 吗?`,
)
"
>
重试任务
</el-button>
@@ -440,7 +470,13 @@ onBeforeUnmount(clearScreenshotPreview)
:disabled="!detail.operations.canRegenerateClaimLink"
:loading="actionLoading"
round
@click="runAction(() => regenerateAdminTaskClaimLink(detail!.task.taskId), '领取链接已重新生成', `确认重新生成任务 ${detail!.task.taskNo} 的领取链接吗?旧链接会失效。`)"
@click="
runAction(
() => regenerateAdminTaskClaimLink(detail!.task.taskId),
'领取链接已重新生成',
`确认重新生成任务 ${detail!.task.taskNo} 的领取链接吗?旧链接会失效。`,
)
"
>
重发链接
</el-button>
@@ -449,11 +485,13 @@ onBeforeUnmount(clearScreenshotPreview)
:loading="actionLoading"
round
type="warning"
@click="runAction(
() => confirmAdminTaskAssistedRole(detail!.task.taskId),
'角色已确认',
`确认以客服身份锁定任务 ${detail!.task.taskNo} 当前识别到的角色信息吗?`
)"
@click="
runAction(
() => confirmAdminTaskAssistedRole(detail!.task.taskId),
'角色已确认',
`确认以客服身份锁定任务 ${detail!.task.taskNo} 当前识别到的角色信息吗?`,
)
"
>
客服确认角色
</el-button>
@@ -462,11 +500,13 @@ onBeforeUnmount(clearScreenshotPreview)
:loading="actionLoading"
round
type="success"
@click="runAction(
() => redeemAdminTaskAssisted(detail!.task.taskId),
'兑换任务已启动',
`确认开始执行任务 ${detail!.task.taskNo} 的自动兑换吗?`
)"
@click="
runAction(
() => redeemAdminTaskAssisted(detail!.task.taskId),
'兑换任务已启动',
`确认开始执行任务 ${detail!.task.taskNo} 的自动兑换吗?`,
)
"
>
客服开始兑换
</el-button>
@@ -502,7 +542,13 @@ onBeforeUnmount(clearScreenshotPreview)
:disabled="!detail.operations.canMarkManualReview"
:loading="actionLoading"
round
@click="runAction(() => markAdminTaskManualReview(detail!.task.taskId), '任务已转人工处理', `确认将任务 ${detail!.task.taskNo} 转为人工处理吗?`)"
@click="
runAction(
() => markAdminTaskManualReview(detail!.task.taskId),
'任务已转人工处理',
`确认将任务 ${detail!.task.taskNo} 转为人工处理吗?`,
)
"
>
转人工
</el-button>
@@ -531,7 +577,13 @@ onBeforeUnmount(clearScreenshotPreview)
:loading="actionLoading"
round
type="danger"
@click="runAction(() => closeAdminTask(detail!.task.taskId), '任务已关闭', `确认关闭任务 ${detail!.task.taskNo} 吗?关闭后不会自动继续推进。`)"
@click="
runAction(
() => closeAdminTask(detail!.task.taskId),
'任务已关闭',
`确认关闭任务 ${detail!.task.taskNo} 吗?关闭后不会自动继续推进。`,
)
"
>
关闭任务
</el-button>
@@ -541,32 +593,93 @@ onBeforeUnmount(clearScreenshotPreview)
<h3>任务信息</h3>
<table class="data-table">
<tbody>
<tr><th>商品</th><td>{{ detail.orderItem?.skuName || '-' }}</td></tr>
<tr><th>SKU</th><td>{{ detail.orderItem?.skuCode || '-' }}</td></tr>
<tr><th>任务执行器</th><td>{{ detail.task.executorKey || '-' }}</td></tr>
<tr><th>履约状态</th><td><AdminStatusTag :status="detail.task.deliveryStatus || ''" /></td></tr>
<tr><th>结果代码</th><td>{{ detail.task.resultCode || '-' }}</td></tr>
<tr><th>结果说明</th><td>{{ detail.task.resultMessage || '-' }}</td></tr>
<tr v-if="canViewSensitiveTaskData"><th>库存凭据</th><td>{{ detail.inventory?.displayValue || '-' }}</td></tr>
<tr v-if="canViewSensitiveTaskData"><th>凭据类型</th><td>{{ detail.inventory?.credentialType || '-' }}</td></tr>
<tr v-if="canViewSensitiveTaskData"><th>库存状态</th><td><AdminStatusTag :status="detail.inventory?.status || ''" /></td></tr>
<tr v-if="canViewSensitiveTaskData"><th>Claim Token</th><td>{{ detail.claimToken?.token || '-' }}</td></tr>
<tr v-else><th>库存 / Token</th><td>客服账号不可查看库存凭据与原始领取 Token</td></tr>
<tr><th>Token 状态</th><td><AdminStatusTag :status="detail.claimToken?.status || ''" /></td></tr>
<tr><th>Token 过期</th><td>{{ formatAdminDateTime(detail.claimToken?.expiredAt) }}</td></tr>
<tr>
<th>商品</th>
<td>{{ detail.orderItem?.skuName || '-' }}</td>
</tr>
<tr>
<th>SKU</th>
<td>{{ detail.orderItem?.skuCode || '-' }}</td>
</tr>
<tr>
<th>任务执行器</th>
<td>{{ detail.task.executorKey || '-' }}</td>
</tr>
<tr>
<th>履约状态</th>
<td><AdminStatusTag :status="detail.task.deliveryStatus || ''" /></td>
</tr>
<tr>
<th>结果代码</th>
<td>{{ detail.task.resultCode || '-' }}</td>
</tr>
<tr>
<th>结果说明</th>
<td>{{ detail.task.resultMessage || '-' }}</td>
</tr>
<tr v-if="canViewSensitiveTaskData">
<th>库存凭据</th>
<td>{{ detail.inventory?.displayValue || '-' }}</td>
</tr>
<tr v-if="canViewSensitiveTaskData">
<th>凭据类型</th>
<td>{{ detail.inventory?.credentialType || '-' }}</td>
</tr>
<tr v-if="canViewSensitiveTaskData">
<th>库存状态</th>
<td><AdminStatusTag :status="detail.inventory?.status || ''" /></td>
</tr>
<tr v-if="canViewSensitiveTaskData">
<th>Claim Token</th>
<td>{{ detail.claimToken?.token || '-' }}</td>
</tr>
<tr v-else>
<th>库存 / Token</th>
<td>客服账号不可查看库存凭据与原始领取 Token</td>
</tr>
<tr>
<th>Token 状态</th>
<td><AdminStatusTag :status="detail.claimToken?.status || ''" /></td>
</tr>
<tr>
<th>Token 过期</th>
<td>{{ formatAdminDateTime(detail.claimToken?.expiredAt) }}</td>
</tr>
<tr>
<th>领取链接</th>
<td>
<a v-if="claimUrl" class="claim-link" :href="claimUrl" target="_blank" rel="noreferrer">{{ claimUrl }}</a>
<a
v-if="claimUrl"
class="claim-link"
:href="claimUrl"
target="_blank"
rel="noreferrer"
>{{ claimUrl }}</a
>
<span v-else-if="claimLinkInvalid">已失效</span>
<span v-else>-</span>
</td>
</tr>
<tr><th>浏览器会话</th><td>{{ detail.task.browserSessionId || '-' }}</td></tr>
<tr><th>用户打开链接</th><td>{{ formatAdminDateTime(detail.task.claimedAt) }}</td></tr>
<tr><th>用户提交绑定</th><td>{{ formatAdminDateTime(detail.task.roleConfirmedAt) }}</td></tr>
<tr><th>完整绑定成功</th><td>{{ formatAdminDateTime(detail.task.redeemedAt) }}</td></tr>
<tr><th>截图路径</th><td>{{ detail.task.screenshotPath || '-' }}</td></tr>
<tr>
<th>浏览器会话</th>
<td>{{ detail.task.browserSessionId || '-' }}</td>
</tr>
<tr>
<th>用户打开链接</th>
<td>{{ formatAdminDateTime(detail.task.claimedAt) }}</td>
</tr>
<tr>
<th>用户提交绑定</th>
<td>{{ formatAdminDateTime(detail.task.roleConfirmedAt) }}</td>
</tr>
<tr>
<th>完整绑定成功</th>
<td>{{ formatAdminDateTime(detail.task.redeemedAt) }}</td>
</tr>
<tr>
<th>截图路径</th>
<td>{{ detail.task.screenshotPath || '-' }}</td>
</tr>
</tbody>
</table>
</section>
@@ -575,7 +688,10 @@ onBeforeUnmount(clearScreenshotPreview)
<div class="flow-card-head">
<div>
<h3>快手 Cloud 履约面板</h3>
<p class="section-copy">这是一套独立的快手 Cloud 任务面板优先展示角色信息与当前履约推进点次级明细可按需展开</p>
<p class="section-copy">
这是一套独立的快手 Cloud
任务面板优先展示角色信息与当前履约推进点次级明细可按需展开
</p>
</div>
<div class="flow-head-actions">
<el-button
@@ -637,7 +753,9 @@ onBeforeUnmount(clearScreenshotPreview)
<div class="spotlight-head">
<div>
<h4>角色信息</h4>
<p class="flow-copy">角色信息来自 cloudtentacles `vn/bind_info`客服可在客户绑定后手动刷新</p>
<p class="flow-copy">
角色信息来自 cloudtentacles `vn/bind_info`客服可在客户绑定后手动刷新
</p>
</div>
<div class="spotlight-actions">
<AdminStatusTag :status="kuaishouCloudRole?.status || 'pending'" />
@@ -666,8 +784,13 @@ onBeforeUnmount(clearScreenshotPreview)
<strong>{{ formatAdminDateTime(kuaishouCloudRole?.refreshedAt) }}</strong>
</div>
</div>
<p v-if="kuaishouCloudRole?.errorMessage" class="role-warning">{{ kuaishouCloudRole.errorMessage }}</p>
<div v-if="kuaishouRoleInfoEntries.length > 0" class="flow-meta-list flow-meta-list--dense">
<p v-if="kuaishouCloudRole?.errorMessage" class="role-warning">
{{ kuaishouCloudRole.errorMessage }}
</p>
<div
v-if="kuaishouRoleInfoEntries.length > 0"
class="flow-meta-list flow-meta-list--dense"
>
<div v-for="entry in kuaishouRoleInfoEntries" :key="entry.key">
<span>{{ entry.label }}</span>
<strong>{{ entry.value }}</strong>
@@ -677,7 +800,9 @@ onBeforeUnmount(clearScreenshotPreview)
<article class="flow-block flow-block--checklist">
<h4>流程检查</h4>
<p class="flow-copy">客服可以先看这里快速判断卡在哪一步再决定是刷新角色准备资源发货还是退号</p>
<p class="flow-copy">
客服可以先看这里快速判断卡在哪一步再决定是刷新角色准备资源发货还是退号
</p>
<div class="checklist-grid">
<div v-for="item in kuaishouCloudChecklist" :key="item.key" class="checklist-item">
<div class="checklist-item-head">
@@ -693,57 +818,158 @@ onBeforeUnmount(clearScreenshotPreview)
<el-collapse v-model="kuaishouCloudCollapsePanels" class="flow-collapse">
<el-collapse-item name="claim" title="客户领取与绑定">
<div class="flow-meta-list flow-meta-list--two-column">
<div><span>客户链接</span><strong>{{ claimUrl || '-' }}</strong></div>
<div><span>绑定链接</span><strong>{{ kuaishouCloudFlow.binding.bindUrl || '-' }}</strong></div>
<div><span>Token 状态</span><strong>{{ detail.claimToken?.status || '-' }}</strong></div>
<div><span>准备时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.binding.bindPreparedAt) }}</strong></div>
<div><span>首次提交</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.ticket.capturedAt) }}</strong></div>
<div><span>校验时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.ticket.verifiedAt) }}</strong></div>
<div><span>虚拟号 ID</span><strong>{{ kuaishouCloudFlow.binding.vnId || '-' }}</strong></div>
<div><span>虚拟号码</span><strong>{{ kuaishouCloudFlow.binding.vnPhone || '-' }}</strong></div>
<div>
<span>客户链接</span><strong>{{ claimUrl || '-' }}</strong>
</div>
<div>
<span>绑定链接</span><strong>{{ kuaishouCloudFlow.binding.bindUrl || '-' }}</strong>
</div>
<div>
<span>Token 状态</span><strong>{{ detail.claimToken?.status || '-' }}</strong>
</div>
<div>
<span>准备时间</span
><strong>{{
formatAdminDateTime(kuaishouCloudFlow.binding.bindPreparedAt)
}}</strong>
</div>
<div>
<span>首次提交</span
><strong>{{ formatAdminDateTime(kuaishouCloudFlow.ticket.capturedAt) }}</strong>
</div>
<div>
<span>校验时间</span
><strong>{{ formatAdminDateTime(kuaishouCloudFlow.ticket.verifiedAt) }}</strong>
</div>
<div>
<span>虚拟号 ID</span><strong>{{ kuaishouCloudFlow.binding.vnId || '-' }}</strong>
</div>
<div>
<span>虚拟号码</span><strong>{{ kuaishouCloudFlow.binding.vnPhone || '-' }}</strong>
</div>
</div>
<p class="manual-hint">客户会先在领取页查看图文指引并提交核销码只有校验通过后才会自动跳转绑定链接</p>
<p class="manual-hint">
客户会先在领取页查看图文指引并提交核销码只有校验通过后才会自动跳转绑定链接
</p>
</el-collapse-item>
<el-collapse-item name="ticket" title="核销与商品信息">
<div class="flow-meta-list flow-meta-list--two-column">
<div><span>核销码</span><strong>{{ kuaishouCloudFlow.ticket.code || '-' }}</strong></div>
<div><span>校验状态</span><strong>{{ kuaishouCloudFlow.ticket.status || '-' }}</strong></div>
<div><span>商品标题</span><strong>{{ kuaishouCloudFlow.ticket.goodsTitle || '-' }}</strong></div>
<div><span>剩余次数</span><strong>{{ kuaishouCloudFlow.ticket.leftCount || 0 }}</strong></div>
<div>
<span>核销码</span><strong>{{ kuaishouCloudFlow.ticket.code || '-' }}</strong>
</div>
<div>
<span>校验状态</span><strong>{{ kuaishouCloudFlow.ticket.status || '-' }}</strong>
</div>
<div>
<span>商品标题</span
><strong>{{ kuaishouCloudFlow.ticket.goodsTitle || '-' }}</strong>
</div>
<div>
<span>剩余次数</span><strong>{{ kuaishouCloudFlow.ticket.leftCount || 0 }}</strong>
</div>
</div>
<p class="manual-hint">"确认绑定完成并发货"已经合并为一个按钮只有核销码通过校验后才能继续</p>
<p class="manual-hint">
"确认绑定完成并发货"已经合并为一个按钮只有核销码通过校验后才能继续
</p>
</el-collapse-item>
<el-collapse-item name="cloud" title="Cloud 资源与发货">
<div class="flow-meta-list flow-meta-list--two-column">
<div><span>cloudSourceKey</span><strong>{{ kuaishouCloudFlow.binding.cloudSourceKey || '-' }}</strong></div>
<div><span>cloud SKU</span><strong>{{ kuaishouCloudFlow.binding.skuId ? `${kuaishouCloudFlow.binding.skuId} / ${kuaishouCloudFlow.binding.skuName || '-'}` : '-' }}</strong></div>
<div><span>VN Key</span><strong>{{ kuaishouCloudFlow.binding.vnKey || '-' }}</strong></div>
<div><span>自动购买</span><strong>{{ kuaishouCloudFlow.purchase.autoBuyEnabled ? '开启' : '关闭' }}</strong></div>
<div><span>背包命中</span><strong>{{ kuaishouCloudFlow.purchase.usedKnapsack ? '是' : '否' }}</strong></div>
<div><span>触发购买</span><strong>{{ kuaishouCloudFlow.purchase.purchaseTriggered ? '是' : '否' }}</strong></div>
<div><span>购买时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.purchase.purchaseAt) }}</strong></div>
<div><span>发货时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.dispatch.dispatchAt) }}</strong></div>
<div><span>发送类型</span><strong>{{ kuaishouCloudFlow.dispatch.sendType || '-' }}</strong></div>
<div><span>结果说明</span><strong>{{ kuaishouCloudFlow.dispatch.note || detail.task.resultMessage || '-' }}</strong></div>
<div>
<span>cloudSourceKey</span
><strong>{{ kuaishouCloudFlow.binding.cloudSourceKey || '-' }}</strong>
</div>
<div>
<span>cloud SKU</span
><strong>{{
kuaishouCloudFlow.binding.skuId
? `${kuaishouCloudFlow.binding.skuId} / ${kuaishouCloudFlow.binding.skuName || '-'}`
: '-'
}}</strong>
</div>
<div>
<span>VN Key</span><strong>{{ kuaishouCloudFlow.binding.vnKey || '-' }}</strong>
</div>
<div>
<span>自动购买</span
><strong>{{ kuaishouCloudFlow.purchase.autoBuyEnabled ? '开启' : '关闭' }}</strong>
</div>
<div>
<span>背包命中</span
><strong>{{ kuaishouCloudFlow.purchase.usedKnapsack ? '是' : '否' }}</strong>
</div>
<div>
<span>触发购买</span
><strong>{{ kuaishouCloudFlow.purchase.purchaseTriggered ? '是' : '否' }}</strong>
</div>
<div>
<span>购买时间</span
><strong>{{ formatAdminDateTime(kuaishouCloudFlow.purchase.purchaseAt) }}</strong>
</div>
<div>
<span>发货时间</span
><strong>{{ formatAdminDateTime(kuaishouCloudFlow.dispatch.dispatchAt) }}</strong>
</div>
<div>
<span>发送类型</span
><strong>{{ kuaishouCloudFlow.dispatch.sendType || '-' }}</strong>
</div>
<div>
<span>结果说明</span
><strong>{{
kuaishouCloudFlow.dispatch.note || detail.task.resultMessage || '-'
}}</strong>
</div>
</div>
</el-collapse-item>
<el-collapse-item name="closure" title="流程收口">
<div class="flow-meta-list flow-meta-list--two-column">
<div><span>退号时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.returnNumber.returnedAt) }}</strong></div>
<div><span>自动退号</span><strong>{{ kuaishouCloudFlow.returnNumber.autoReturnEnabled ? '开启' : '关闭' }}</strong></div>
<div><span>自动核销</span><strong>{{ kuaishouCloudFlow.consume.autoConsumeEnabled ? '开启' : '关闭' }}</strong></div>
<div><span>核销店铺</span><strong>{{ kuaishouCloudFlow.consume.shopId || '-' }}</strong></div>
<div><span>核销店铺名</span><strong>{{ kuaishouCloudFlow.consume.shopName || '-' }}</strong></div>
<div><span>核销时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.consume.consumedAt) }}</strong></div>
<div><span>核销异常</span><strong>{{ kuaishouCloudFlow.consume.errorMessage || '-' }}</strong></div>
<div><span>备注</span><strong>{{ kuaishouCloudFlow.notes || '-' }}</strong></div>
<div>
<span>退号时间</span
><strong>{{
formatAdminDateTime(kuaishouCloudFlow.returnNumber.returnedAt)
}}</strong>
</div>
<div>
<span>自动退号</span
><strong>{{
kuaishouCloudFlow.returnNumber.autoReturnEnabled ? '开启' : '关闭'
}}</strong>
</div>
<div>
<span>自动核销</span
><strong>{{
kuaishouCloudFlow.consume.autoConsumeEnabled ? '开启' : '关闭'
}}</strong>
</div>
<div>
<span>核销店铺</span><strong>{{ kuaishouCloudFlow.consume.shopId || '-' }}</strong>
</div>
<div>
<span>核销店铺名</span
><strong>{{ kuaishouCloudFlow.consume.shopName || '-' }}</strong>
</div>
<div>
<span>核销时间</span
><strong>{{ formatAdminDateTime(kuaishouCloudFlow.consume.consumedAt) }}</strong>
</div>
<div>
<span>核销异常</span
><strong>{{ kuaishouCloudFlow.consume.errorMessage || '-' }}</strong>
</div>
<div>
<span>备注</span><strong>{{ kuaishouCloudFlow.notes || '-' }}</strong>
</div>
</div>
</el-collapse-item>
<el-collapse-item v-if="kuaishouRoleInfoEntries.length > 0" name="raw" title="角色原始字段">
<el-collapse-item
v-if="kuaishouRoleInfoEntries.length > 0"
name="raw"
title="角色原始字段"
>
<div class="flow-meta-list flow-meta-list--two-column">
<div v-for="entry in kuaishouRoleInfoEntries" :key="`raw-${entry.key}`">
<span>{{ entry.label }}</span>
@@ -759,7 +985,9 @@ onBeforeUnmount(clearScreenshotPreview)
class="info-card"
>
<h3>咸鱼自动发货</h3>
<p class="section-copy">这里展示这条任务最近一次触发咸鱼自动发货的状态原因和接口回执摘要</p>
<p class="section-copy">
这里展示这条任务最近一次触发咸鱼自动发货的状态原因和接口回执摘要
</p>
<div class="redeem-summary-grid">
<article class="redeem-summary-card">
@@ -770,7 +998,9 @@ onBeforeUnmount(clearScreenshotPreview)
</article>
<article class="redeem-summary-card">
<span>触发来源</span>
<strong>{{ formatAutoDeliveryTrigger(detail.task.agisoAutoDelivery?.trigger || '') }}</strong>
<strong>{{
formatAutoDeliveryTrigger(detail.task.agisoAutoDelivery?.trigger || '')
}}</strong>
</article>
<article class="redeem-summary-card">
<span>最近执行</span>
@@ -779,22 +1009,35 @@ onBeforeUnmount(clearScreenshotPreview)
<article class="redeem-summary-card">
<span>接口回执</span>
<strong>
{{ detail.task.agisoAutoDelivery?.requestId || (detail.task.agisoAutoDelivery?.responseStatus ? `HTTP ${detail.task.agisoAutoDelivery.responseStatus}` : '-') }}
{{
detail.task.agisoAutoDelivery?.requestId ||
(detail.task.agisoAutoDelivery?.responseStatus
? `HTTP ${detail.task.agisoAutoDelivery.responseStatus}`
: '-')
}}
</strong>
</article>
</div>
<div class="auto-delivery-note">
<p>{{ resolveTaskAutoDeliverySummary(detail) }}</p>
<p v-if="detail.task.agisoAutoDelivery?.reason">原因{{ formatAutoDeliveryReason(detail.task.agisoAutoDelivery.reason) }}</p>
<p v-if="detail.task.agisoAutoDelivery?.responseStatus">HTTP{{ detail.task.agisoAutoDelivery.responseStatus }}</p>
<p v-if="detail.task.agisoAutoDelivery?.requestId">请求 ID{{ detail.task.agisoAutoDelivery.requestId }}</p>
<p v-if="detail.task.agisoAutoDelivery?.reason">
原因{{ formatAutoDeliveryReason(detail.task.agisoAutoDelivery.reason) }}
</p>
<p v-if="detail.task.agisoAutoDelivery?.responseStatus">
HTTP{{ detail.task.agisoAutoDelivery.responseStatus }}
</p>
<p v-if="detail.task.agisoAutoDelivery?.requestId">
请求 ID{{ detail.task.agisoAutoDelivery.requestId }}
</p>
</div>
</section>
<section v-if="detail.redeemResolution" class="info-card">
<h3>兑换重试链路</h3>
<p class="section-copy">这里记录自动兑换时每次使用过的 CDK判定结果以及是否切换到了新的同类型凭据</p>
<p class="section-copy">
这里记录自动兑换时每次使用过的 CDK判定结果以及是否切换到了新的同类型凭据
</p>
<div class="redeem-summary-grid">
<article class="redeem-summary-card">
@@ -850,7 +1093,9 @@ onBeforeUnmount(clearScreenshotPreview)
<section v-if="!isKuaishouCloudTask" class="info-card">
<h3>库存绑定</h3>
<p class="section-copy">这里展示任务当前和历史绑定过的库存项便于核对多库存任务的真实履约轨迹</p>
<p class="section-copy">
这里展示任务当前和历史绑定过的库存项便于核对多库存任务的真实履约轨迹
</p>
<table class="data-table">
<thead>
<tr>
@@ -879,7 +1124,9 @@ onBeforeUnmount(clearScreenshotPreview)
<td>
<div>ID {{ binding.inventoryItemId }}</div>
<div>{{ binding.skuCode || '-' }}</div>
<div class="binding-meta">{{ binding.credentialType || '-' }} · {{ binding.batchNo || '无批次' }}</div>
<div class="binding-meta">
{{ binding.credentialType || '-' }} · {{ binding.batchNo || '无批次' }}
</div>
</td>
<td>
<div class="status-stack">
@@ -887,10 +1134,16 @@ onBeforeUnmount(clearScreenshotPreview)
<AdminStatusTag :status="binding.inventoryStatus" />
</div>
<div v-if="binding.invalidReason" class="binding-meta">
{{ binding.inventoryStatus === 'invalid' ? '作废原因' : binding.inventoryStatus === 'consumed' ? '消耗原因' : '处理原因' }}{{ binding.invalidReason }}
{{
binding.inventoryStatus === 'invalid'
? '作废原因'
: binding.inventoryStatus === 'consumed'
? '消耗原因'
: '处理原因'
}}{{ binding.invalidReason }}
</div>
</td>
<td>{{ canViewSensitiveTaskData ? (binding.displayValue || '-') : '客服不可见' }}</td>
<td>{{ canViewSensitiveTaskData ? binding.displayValue || '-' : '客服不可见' }}</td>
<td>
<div>绑定 {{ formatAdminDateTime(binding.createdAt) }}</div>
<div class="binding-meta">消费 {{ formatAdminDateTime(binding.consumedAt) }}</div>
@@ -901,11 +1154,14 @@ onBeforeUnmount(clearScreenshotPreview)
v-if="hasAdminRole('admin') && binding.canRelease"
:loading="actionLoading"
link
@click="runAction(
() => releaseAdminTaskInventoryBinding(detail!.task.taskId, binding.bindingId),
'库存绑定已释放',
`确认释放绑定 #${binding.bindingId} 对应的库存项 ${binding.displayValue || binding.inventoryItemId} 吗?`
)"
@click="
runAction(
() =>
releaseAdminTaskInventoryBinding(detail!.task.taskId, binding.bindingId),
'库存绑定已释放',
`确认释放绑定 #${binding.bindingId} 对应的库存项 ${binding.displayValue || binding.inventoryItemId} 吗?`,
)
"
>
释放绑定
</el-button>
@@ -953,8 +1209,16 @@ onBeforeUnmount(clearScreenshotPreview)
</div>
<p class="manual-hint">回写后会直接更新任务终态不再走旧式领取链接流程</p>
<div v-if="detail.manualDispatch" class="manual-summary">
<p>最近回写<AdminStatusTag :status="detail.manualDispatch.outcome || detail.task.deliveryStatus" /> · {{ formatAdminDateTime(detail.manualDispatch.completedAt) }}</p>
<p>处理人{{ detail.manualDispatch.completedBy?.username || '-' }} / {{ detail.manualDispatch.completedBy?.role || '-' }}</p>
<p>
最近回写<AdminStatusTag
:status="detail.manualDispatch.outcome || detail.task.deliveryStatus"
/>
· {{ formatAdminDateTime(detail.manualDispatch.completedAt) }}
</p>
<p>
处理人{{ detail.manualDispatch.completedBy?.username || '-' }} /
{{ detail.manualDispatch.completedBy?.role || '-' }}
</p>
<p>流水号{{ detail.manualDispatch.deliveryReference || '-' }}</p>
<p>交付内容{{ detail.manualDispatch.deliveredCredential || '-' }}</p>
<p>备注{{ detail.manualDispatch.resultMessage || '-' }}</p>
@@ -979,8 +1243,20 @@ onBeforeUnmount(clearScreenshotPreview)
<section v-if="detail.screenshotUrl" class="info-card">
<h3>{{ screenshotSectionTitle }}</h3>
<p v-if="!screenshotPreviewUrl" class="screenshot-empty">截图加载中或当前不可读取</p>
<a v-else class="screenshot-link" :href="screenshotPreviewUrl" target="_blank" rel="noreferrer">打开原图</a>
<img v-if="screenshotPreviewUrl" class="screenshot-preview" :src="screenshotPreviewUrl" :alt="screenshotSectionTitle" />
<a
v-else
class="screenshot-link"
:href="screenshotPreviewUrl"
target="_blank"
rel="noreferrer"
>打开原图</a
>
<img
v-if="screenshotPreviewUrl"
class="screenshot-preview"
:src="screenshotPreviewUrl"
:alt="screenshotSectionTitle"
/>
</section>
</template>
</section>
@@ -74,7 +74,9 @@ function formatBindingCountSummary(item: AdminTaskListItem) {
return `${summary.totalBindingCount} 条 · 预占 ${summary.reservedBindingCount} · 已消费 ${summary.consumedBindingCount} · 已释放 ${summary.releasedBindingCount}`
}
onMounted(() => { void loadTasks() })
onMounted(() => {
void loadTasks()
})
async function runAction(
taskId: number,
@@ -83,8 +85,14 @@ async function runAction(
confirmText: string,
) {
try {
await showConfirm(confirmText, '确认操作', { type: 'warning', confirmButtonText: '继续执行', cancelButtonText: '取消' })
} catch { return }
await showConfirm(confirmText, '确认操作', {
type: 'warning',
confirmButtonText: '继续执行',
cancelButtonText: '取消',
})
} catch {
return
}
actionLoadingId.value = taskId
try {
@@ -94,23 +102,45 @@ async function runAction(
await loadTasks()
} catch (error) {
showError(error instanceof Error ? error.message : '操作失败')
} finally { actionLoadingId.value = null }
} finally {
actionLoadingId.value = null
}
}
function handleActionCommand(actionKey: AdminTaskActionKey, item: AdminTaskListItem) {
if (actionKey === 'retry') {
void runAction(item.taskId, () => retryAdminTask(item.taskId), '任务已重试', `确认重试任务 ${item.taskNo} 吗?`)
void runAction(
item.taskId,
() => retryAdminTask(item.taskId),
'任务已重试',
`确认重试任务 ${item.taskNo} 吗?`,
)
return
}
if (actionKey === 'regenerate_claim_link') {
void runAction(item.taskId, () => regenerateAdminTaskClaimLink(item.taskId), '领取链接已重新生成', `确认重新生成任务 ${item.taskNo} 的领取链接吗?旧链接会失效。`)
void runAction(
item.taskId,
() => regenerateAdminTaskClaimLink(item.taskId),
'领取链接已重新生成',
`确认重新生成任务 ${item.taskNo} 的领取链接吗?旧链接会失效。`,
)
return
}
if (actionKey === 'manual_review') {
void runAction(item.taskId, () => markAdminTaskManualReview(item.taskId), '任务已转人工处理', `确认将任务 ${item.taskNo} 转为人工处理吗?`)
void runAction(
item.taskId,
() => markAdminTaskManualReview(item.taskId),
'任务已转人工处理',
`确认将任务 ${item.taskNo} 转为人工处理吗?`,
)
return
}
void runAction(item.taskId, () => closeAdminTask(item.taskId), '任务已关闭', `确认关闭任务 ${item.taskNo} 吗?关闭后不会自动继续推进。`)
void runAction(
item.taskId,
() => closeAdminTask(item.taskId),
'任务已关闭',
`确认关闭任务 ${item.taskNo} 吗?关闭后不会自动继续推进。`,
)
}
function resetFilters() {
@@ -127,7 +157,10 @@ function resetFilters() {
<template>
<div class="tasks-page list-page">
<AdminPageHeader title="交付任务" description="更紧凑地查看任务状态、角色绑定、库存凭据和异常信息。">
<AdminPageHeader
title="交付任务"
description="更紧凑地查看任务状态、角色绑定、库存凭据和异常信息。"
>
<template #extra>
<span class="total-badge"> {{ pagination.total }} 条任务</span>
</template>
@@ -142,14 +175,44 @@ function resetFilters() {
</template>
<div class="filter-grid">
<el-select v-model="status" placeholder="状态" clearable>
<el-option v-for="opt in adminTaskStatusOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
<el-option
v-for="opt in adminTaskStatusOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-input v-model="taskNo" placeholder="任务号(模糊匹配)" clearable @keyup.enter="loadTasks(1)" />
<el-input v-model="platformOrderId" placeholder="平台订单号" clearable @keyup.enter="loadTasks(1)" />
<el-input v-model="roleId" placeholder="角色唯一标识" clearable @keyup.enter="loadTasks(1)" />
<el-input
v-model="taskNo"
placeholder="任务号(模糊匹配)"
clearable
@keyup.enter="loadTasks(1)"
/>
<el-input
v-model="platformOrderId"
placeholder="平台订单号"
clearable
@keyup.enter="loadTasks(1)"
/>
<el-input
v-model="roleId"
placeholder="角色唯一标识"
clearable
@keyup.enter="loadTasks(1)"
/>
<el-input v-model="skuCode" placeholder="商品 SKU" clearable @keyup.enter="loadTasks(1)" />
<el-date-picker v-model="dateFrom" type="date" placeholder="开始日期" value-format="YYYY-MM-DD" />
<el-date-picker v-model="dateTo" type="date" placeholder="结束日期" value-format="YYYY-MM-DD" />
<el-date-picker
v-model="dateFrom"
type="date"
placeholder="开始日期"
value-format="YYYY-MM-DD"
/>
<el-date-picker
v-model="dateTo"
type="date"
placeholder="结束日期"
value-format="YYYY-MM-DD"
/>
<div class="filter-buttons">
<el-button round @click="resetFilters">重置</el-button>
<el-button round type="primary" @click="loadTasks(1)">查询</el-button>
@@ -157,10 +220,29 @@ function resetFilters() {
</div>
</el-card>
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon :closable="false" style="margin-top: 16px" />
<el-alert v-if="lastClaimUrl" :title="`最近生成的领取链接:${lastClaimUrl}`" type="info" show-icon :closable="false" style="margin-top: 16px" />
<el-alert
v-if="errorMessage"
:title="errorMessage"
type="error"
show-icon
:closable="false"
style="margin-top: 16px"
/>
<el-alert
v-if="lastClaimUrl"
:title="`最近生成的领取链接:${lastClaimUrl}`"
type="info"
show-icon
:closable="false"
style="margin-top: 16px"
/>
<el-card shadow="never" class="section-card" v-loading="loading" element-loading-text="任务列表加载中">
<el-card
shadow="never"
class="section-card"
v-loading="loading"
element-loading-text="任务列表加载中"
>
<template v-if="items.length === 0 && !loading">
<el-empty description="当前筛选条件下暂无任务" :image-size="60" />
</template>
@@ -175,14 +257,22 @@ function resetFilters() {
<el-table-column label="任务 / 商品" min-width="220">
<template #default="{ row }">
<div class="cell-stack">
<RouterLink :to="`/admin/tasks/${row.taskId}`" class="task-no id-link" :title="row.taskNo">
<RouterLink
:to="`/admin/tasks/${row.taskId}`"
class="task-no id-link"
:title="row.taskNo"
>
{{ row.taskNo }}
</RouterLink>
<div class="cell-meta-row">
<span class="compact-chip" :title="row.platformOrderId">订单 {{ row.platformOrderId }}</span>
<span class="compact-chip" :title="row.platformOrderId"
>订单 {{ row.platformOrderId }}</span
>
<span class="compact-chip compact-chip--muted">重试 {{ row.retryCount }} </span>
</div>
<span class="cell-title" :title="row.skuName || row.skuCode">{{ row.skuName || row.skuCode }}</span>
<span class="cell-title" :title="row.skuName || row.skuCode">{{
row.skuName || row.skuCode
}}</span>
<span class="cell-subline">SKU {{ row.skuCode || '-' }}</span>
</div>
</template>
@@ -208,12 +298,24 @@ function resetFilters() {
<el-table-column label="角色 / 凭据" min-width="180">
<template #default="{ row }">
<div class="cell-stack">
<span class="cell-title" :title="row.roleName || row.roleId || '-'">{{ row.roleName || '-' }}</span>
<span class="cell-subline" :title="row.roleId || '-'">角色ID {{ row.roleId || '-' }}</span>
<span class="cell-title" :title="row.roleName || row.roleId || '-'">{{
row.roleName || '-'
}}</span>
<span class="cell-subline" :title="row.roleId || '-'"
>角色ID {{ row.roleId || '-' }}</span
>
<span class="cell-subline">登录 {{ row.loginType || '-' }}</span>
<span class="cell-subline" :title="row.inventoryDisplayMasked || '-'">凭据 {{ row.inventoryDisplayMasked || '-' }}</span>
<span class="cell-subline" :title="formatBindingRoles(row)">绑定 {{ formatBindingRoles(row) }}</span>
<span class="cell-subline cell-subline--strong" :title="formatBindingCountSummary(row)">{{ formatBindingCountSummary(row) }}</span>
<span class="cell-subline" :title="row.inventoryDisplayMasked || '-'"
>凭据 {{ row.inventoryDisplayMasked || '-' }}</span
>
<span class="cell-subline" :title="formatBindingRoles(row)"
>绑定 {{ formatBindingRoles(row) }}</span
>
<span
class="cell-subline cell-subline--strong"
:title="formatBindingCountSummary(row)"
>{{ formatBindingCountSummary(row) }}</span
>
</div>
</template>
</el-table-column>
@@ -222,25 +324,43 @@ function resetFilters() {
<div class="cell-stack">
<span class="cell-subline">创建 {{ formatAdminDateTime(row.createdAt) }}</span>
<span class="cell-subline">更新 {{ formatAdminDateTime(row.updatedAt) }}</span>
<span v-if="row.claimedAt" class="cell-subline">领取 {{ formatAdminDateTime(row.claimedAt) }}</span>
<span v-if="row.roleConfirmedAt" class="cell-subline">确认 {{ formatAdminDateTime(row.roleConfirmedAt) }}</span>
<span v-if="row.redeemedAt" class="cell-subline">兑换 {{ formatAdminDateTime(row.redeemedAt) }}</span>
<span v-if="row.claimedAt" class="cell-subline"
>领取 {{ formatAdminDateTime(row.claimedAt) }}</span
>
<span v-if="row.roleConfirmedAt" class="cell-subline"
>确认 {{ formatAdminDateTime(row.roleConfirmedAt) }}</span
>
<span v-if="row.redeemedAt" class="cell-subline"
>兑换 {{ formatAdminDateTime(row.redeemedAt) }}</span
>
</div>
</template>
</el-table-column>
<el-table-column label="异常 / 操作" min-width="220">
<template #default="{ row }">
<div class="cell-stack">
<span class="cell-error" :title="row.lastError || '-'">{{ row.lastError || '当前无异常' }}</span>
<span class="cell-error" :title="row.lastError || '-'">{{
row.lastError || '当前无异常'
}}</span>
<div class="action-stack">
<el-button
v-if="canManageTaskLifecycle"
:disabled="row.executorKey === 'manual_dispatch' || !['retry_pending', 'manual_review', 'waiting_inventory'].includes(row.status)"
:disabled="
row.executorKey === 'manual_dispatch' ||
!['retry_pending', 'manual_review', 'waiting_inventory'].includes(row.status)
"
:loading="actionLoadingId === row.taskId"
size="small"
type="danger"
plain
@click="runAction(row.taskId, () => retryAdminTask(row.taskId), '任务已重试', `确认重试任务 ${row.taskNo} 吗?`)"
@click="
runAction(
row.taskId,
() => retryAdminTask(row.taskId),
'任务已重试',
`确认重试任务 ${row.taskNo} 吗?`,
)
"
>
重试
</el-button>
@@ -290,13 +410,31 @@ function resetFilters() {
@import '../../../styles/admin-list-pages.css';
.filter-grid {
grid-template-columns: minmax(150px, 0.75fr) minmax(220px, 1fr) minmax(220px, 1fr) minmax(210px, 0.95fr);
grid-template-columns: minmax(150px, 0.75fr) minmax(220px, 1fr) minmax(220px, 1fr) minmax(
210px,
0.95fr
);
}
.filter-grid :deep(.el-date-editor) {
width: 100%;
}
.filter-grid :deep(.el-date-editor) { width: 100%; }
.progress-row { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.progress-row :deep(.el-tag) { min-width: 0; max-width: calc(100% - 42px); }
.progress-row :deep(.el-tag__content) { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.progress-row {
display: flex;
align-items: center;
gap: var(--space-2);
min-width: 0;
}
.progress-row :deep(.el-tag) {
min-width: 0;
max-width: calc(100% - 42px);
}
.progress-row :deep(.el-tag__content) {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.progress-label {
display: inline-flex;
@@ -314,10 +452,28 @@ function resetFilters() {
flex: 0 0 auto;
}
.cell-error { display: block; min-width: 0; line-height: 1.55; color: #475467; word-break: break-word; font-size: var(--text-xs); }
.cell-error {
display: block;
min-width: 0;
line-height: 1.55;
color: #475467;
word-break: break-word;
font-size: var(--text-xs);
}
.cell-subline--strong { color: var(--text-primary); font-weight: 600; }
.cell-subline--strong {
color: var(--text-primary);
font-weight: 600;
}
.action-stack { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.action-stack :deep(.el-button) { flex: 0 0 auto; margin-left: 0; }
.action-stack {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
align-items: center;
}
.action-stack :deep(.el-button) {
flex: 0 0 auto;
margin-left: 0;
}
</style>