admin后台 第一阶段目录优化
This commit is contained in:
@@ -0,0 +1,487 @@
|
||||
.admin-panel {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.claim-link {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.info-card,
|
||||
.hero-card,
|
||||
.error-copy {
|
||||
padding: 18px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.info-card p {
|
||||
margin: 10px 0 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
margin: 0 0 var(--space-3);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
display: grid;
|
||||
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));
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.hero-copy h2 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
font-size: 30px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.hero-copy p,
|
||||
.hero-inline span,
|
||||
.hero-link-row span {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.hero-eyebrow {
|
||||
color: var(--color-primary);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hero-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.hero-metric {
|
||||
padding: 14px var(--space-4);
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--border-default);
|
||||
background: rgba(255,255,255,.82);
|
||||
}
|
||||
|
||||
.hero-metric span {
|
||||
display: block;
|
||||
margin-bottom: var(--space-2);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.hero-metric strong {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-lg);
|
||||
line-height: 1.3;
|
||||
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-inline,
|
||||
.hero-link-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.error-copy {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.section-copy {
|
||||
margin: 0 0 var(--space-3);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.auto-delivery-tag-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.auto-delivery-note {
|
||||
margin-top: 14px;
|
||||
padding: 14px var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(248,250,252,.92);
|
||||
border: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.auto-delivery-note p {
|
||||
margin: 0 0 var(--space-2);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.auto-delivery-note p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: var(--space-3) 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
width: 160px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.status-stack {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.redeem-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: var(--space-3);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.redeem-summary-card,
|
||||
.redeem-attempt-card {
|
||||
padding: 14px var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-default);
|
||||
background: rgba(248,250,252,.92);
|
||||
}
|
||||
|
||||
.redeem-summary-card span,
|
||||
.redeem-attempt-grid span {
|
||||
display: block;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.redeem-summary-card strong,
|
||||
.redeem-attempt-grid strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.redeem-attempts {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.redeem-attempt-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.redeem-outcome {
|
||||
color: var(--color-primary);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.redeem-attempt-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.redeem-attempt-copy {
|
||||
margin: var(--space-3) 0 0;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.table-empty {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.binding-primary {
|
||||
margin-top: 4px;
|
||||
color: var(--color-success);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.binding-meta {
|
||||
margin-top: 4px;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.manual-grid {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.field-block {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.manual-hint {
|
||||
margin: 14px 0 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.manual-summary {
|
||||
margin-top: var(--space-4);
|
||||
padding: 14px;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(248,250,252,.9);
|
||||
}
|
||||
|
||||
.manual-summary p {
|
||||
margin: 0 0 var(--space-2);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.manual-summary p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.flow-card {
|
||||
display: grid;
|
||||
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));
|
||||
}
|
||||
|
||||
.flow-card-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.flow-head-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.kuaishou-focus-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.flow-block {
|
||||
padding: var(--space-4);
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--border-default);
|
||||
background: rgba(248,250,252,.9);
|
||||
}
|
||||
|
||||
.flow-block--spotlight {
|
||||
background:
|
||||
linear-gradient(160deg, rgba(255,255,255,.98), rgba(239,246,255,.95)),
|
||||
rgba(248,250,252,.9);
|
||||
}
|
||||
|
||||
.flow-block h4 {
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.flow-block p {
|
||||
margin: 10px 0 0;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.flow-meta-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.flow-meta-list div {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.flow-meta-list span {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.flow-meta-list strong {
|
||||
color: var(--text-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.flow-meta-list--dense {
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
}
|
||||
|
||||
.flow-meta-list--two-column {
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
}
|
||||
|
||||
.spotlight-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.spotlight-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.flow-copy {
|
||||
margin: var(--space-2) 0 0;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.flow-collapse {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.checklist-grid {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
padding: 14px var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255,255,255,.84);
|
||||
border: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.checklist-item-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.checklist-item-head span {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.checklist-item strong {
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.role-hero {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-4);
|
||||
}
|
||||
|
||||
.role-hero div {
|
||||
padding: 14px var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255,255,255,.88);
|
||||
border: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.role-hero span {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.role-hero strong {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-lg);
|
||||
line-height: 1.35;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.role-warning {
|
||||
margin: 14px 0 0;
|
||||
padding: var(--space-3) 14px;
|
||||
border-radius: 14px;
|
||||
background: var(--color-warning-light);
|
||||
color: #b54708;
|
||||
}
|
||||
|
||||
.event-copy {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.event-time {
|
||||
margin-top: 6px;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.screenshot-link {
|
||||
display: inline-block;
|
||||
margin-bottom: var(--space-3);
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.screenshot-empty {
|
||||
margin: 0 0 var(--space-3);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.screenshot-preview {
|
||||
display: block;
|
||||
width: min(100%, 720px);
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--border-default);
|
||||
box-shadow: 0 20px 48px rgba(15,23,42,.08);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero-inline,
|
||||
.hero-link-row,
|
||||
.flow-card-head,
|
||||
.flow-head-actions,
|
||||
.spotlight-head,
|
||||
.spotlight-actions,
|
||||
.checklist-item-head {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,989 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
|
||||
import { showConfirm, showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
closeAdminTask,
|
||||
confirmAdminTaskAssistedRole,
|
||||
completeAdminTaskManualDispatch,
|
||||
dispatchAdminTaskKuaishouCloudFulfillment,
|
||||
redeemAdminTaskAssisted,
|
||||
fetchAdminTaskScreenshot,
|
||||
fetchAdminTaskDetail,
|
||||
markAdminTaskManualReview,
|
||||
prepareAdminTaskKuaishouCloudFulfillment,
|
||||
refreshAdminTaskKuaishouCloudRoleInfo,
|
||||
regenerateAdminTaskClaimLink,
|
||||
releaseAdminTaskInventoryBinding,
|
||||
returnNumberAdminTaskKuaishouCloudFulfillment,
|
||||
retryAdminTask,
|
||||
} 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 { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
const route = useRoute()
|
||||
const loading = ref(true)
|
||||
const actionLoading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const detail = ref<AdminTaskDetail | null>(null)
|
||||
const lastClaimUrl = ref('')
|
||||
const screenshotPreviewUrl = ref('')
|
||||
const manualDispatchForm = reactive({
|
||||
deliveryReference: '',
|
||||
deliveredCredential: '',
|
||||
resultMessage: '',
|
||||
})
|
||||
const canManageTaskLifecycle = computed(() => hasAdminRole('operator'))
|
||||
const canCloseTasks = computed(() => hasAdminRole('support'))
|
||||
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 summaryCards = computed(() => {
|
||||
if (!detail.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label: '任务状态',
|
||||
value: detail.value.task.status || '-',
|
||||
tone: 'blue',
|
||||
},
|
||||
{
|
||||
label: '履约状态',
|
||||
value: detail.value.task.deliveryStatus || '-',
|
||||
tone: 'green',
|
||||
},
|
||||
{
|
||||
label: '当前角色',
|
||||
value: kuaishouCloudRole.value?.name || detail.value.task.roleName || '-',
|
||||
tone: 'amber',
|
||||
},
|
||||
{
|
||||
label: '角色 ID',
|
||||
value: kuaishouCloudRole.value?.rid || detail.value.task.roleId || '-',
|
||||
tone: 'slate',
|
||||
},
|
||||
]
|
||||
})
|
||||
const kuaishouRoleInfoEntries = computed(() => {
|
||||
const rawInfo = kuaishouCloudRole.value?.rawInfo
|
||||
if (!rawInfo) {
|
||||
return []
|
||||
}
|
||||
|
||||
return Object.entries(rawInfo)
|
||||
.filter(([, value]) => value !== null && value !== undefined && String(value).trim() !== '')
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
label: formatKuaishouRoleInfoLabel(key),
|
||||
value: typeof value === 'object' ? JSON.stringify(value) : String(value),
|
||||
}))
|
||||
})
|
||||
const kuaishouCloudChecklist = computed(() => {
|
||||
const flow = kuaishouCloudFlow.value
|
||||
const role = kuaishouCloudRole.value
|
||||
if (!flow) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'ticket',
|
||||
label: '核销码校验',
|
||||
status: flow.ticket.status || 'pending',
|
||||
detail: flow.ticket.verifiedAt
|
||||
? `已于 ${formatAdminDateTime(flow.ticket.verifiedAt)} 校验`
|
||||
: '等待客户提交并校验核销码',
|
||||
},
|
||||
{
|
||||
key: 'bind',
|
||||
label: '绑定资源',
|
||||
status: flow.binding.prepareStatus || 'pending',
|
||||
detail: flow.binding.bindPreparedAt
|
||||
? `绑定资源已准备,VN ${flow.binding.vnId || '-'}`
|
||||
: '等待准备 Cloud 绑定资源',
|
||||
},
|
||||
{
|
||||
key: 'role',
|
||||
label: '角色识别',
|
||||
status: role?.status || 'pending',
|
||||
detail: role?.name || role?.rid
|
||||
? `${role?.name || '-'} / ${role?.rid || '-'}`
|
||||
: '客户绑定后刷新角色信息',
|
||||
},
|
||||
{
|
||||
key: 'dispatch',
|
||||
label: '发货执行',
|
||||
status: flow.dispatch.status || 'pending',
|
||||
detail: flow.dispatch.dispatchAt
|
||||
? `已于 ${formatAdminDateTime(flow.dispatch.dispatchAt)} 发货`
|
||||
: '等待客服确认绑定并发货',
|
||||
},
|
||||
{
|
||||
key: 'return',
|
||||
label: '退还号码',
|
||||
status: flow.returnNumber.status || 'pending',
|
||||
detail: flow.returnNumber.returnedAt
|
||||
? `已于 ${formatAdminDateTime(flow.returnNumber.returnedAt)} 退号`
|
||||
: '发货后执行退还号码',
|
||||
},
|
||||
{
|
||||
key: 'consume',
|
||||
label: '快手核销',
|
||||
status: flow.consume.status || 'pending',
|
||||
detail: flow.consume.consumedAt
|
||||
? `已于 ${formatAdminDateTime(flow.consume.consumedAt)} 完成核销`
|
||||
: (flow.consume.errorMessage || '等待退号后核销收口'),
|
||||
},
|
||||
]
|
||||
})
|
||||
const claimUrl = computed(() => {
|
||||
const tokenStatus = String(detail.value?.claimToken?.status || '').trim()
|
||||
const taskStatus = String(detail.value?.task.status || '').trim()
|
||||
|
||||
if (tokenStatus !== 'active' || ['closed', 'expired'].includes(taskStatus)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return lastClaimUrl.value || detail.value?.claimToken?.claimUrl || ''
|
||||
})
|
||||
const claimLinkInvalid = computed(() => {
|
||||
const tokenStatus = String(detail.value?.claimToken?.status || '').trim()
|
||||
return Boolean(detail.value?.claimToken?.claimUrl) && tokenStatus && tokenStatus !== 'active'
|
||||
})
|
||||
const screenshotSectionTitle = computed(() => {
|
||||
if (detail.value?.task.status === 'redeemed') {
|
||||
return '结果截图'
|
||||
}
|
||||
|
||||
return detail.value?.review.required ? '客服复核截图' : '结果截图'
|
||||
})
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchAdminTaskDetail(String(route.params.taskId || ''))
|
||||
detail.value = response.data
|
||||
syncManualDispatchForm(response.data)
|
||||
await loadScreenshotPreview(response.data)
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取任务详情失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runAction(
|
||||
action: () => Promise<{ data: AdminTaskActionResponse }>,
|
||||
successMessage: string,
|
||||
confirmText: string,
|
||||
) {
|
||||
try {
|
||||
await showConfirm(confirmText, '确认操作', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '继续执行',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
actionLoading.value = true
|
||||
|
||||
try {
|
||||
const response = await action()
|
||||
if (response.data.claimUrl) {
|
||||
lastClaimUrl.value = response.data.claimUrl
|
||||
}
|
||||
showSuccess(successMessage)
|
||||
await loadDetail()
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '操作失败')
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadScreenshotPreview(taskDetail: AdminTaskDetail) {
|
||||
clearScreenshotPreview()
|
||||
|
||||
if (!taskDetail.screenshotUrl) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = await fetchAdminTaskScreenshot(taskDetail.task.taskId)
|
||||
screenshotPreviewUrl.value = URL.createObjectURL(blob)
|
||||
} catch {
|
||||
screenshotPreviewUrl.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function clearScreenshotPreview() {
|
||||
if (screenshotPreviewUrl.value) {
|
||||
URL.revokeObjectURL(screenshotPreviewUrl.value)
|
||||
screenshotPreviewUrl.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function syncManualDispatchForm(taskDetail: AdminTaskDetail) {
|
||||
manualDispatchForm.deliveryReference = taskDetail.manualDispatch?.deliveryReference || ''
|
||||
manualDispatchForm.deliveredCredential = taskDetail.manualDispatch?.deliveredCredential || ''
|
||||
manualDispatchForm.resultMessage = taskDetail.manualDispatch?.resultMessage || taskDetail.task.resultMessage || ''
|
||||
}
|
||||
|
||||
async function submitManualDispatch(outcome: 'delivered' | 'failed') {
|
||||
if (!detail.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const actionLabel = outcome === 'failed' ? '标记履约失败' : '标记已完成履约'
|
||||
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,
|
||||
}),
|
||||
actionLabel,
|
||||
confirmText,
|
||||
)
|
||||
}
|
||||
|
||||
async function submitKuaishouCloudPrepare() {
|
||||
if (!detail.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await runAction(
|
||||
() => prepareAdminTaskKuaishouCloudFulfillment(detail.value!.task.taskId),
|
||||
'绑定资源已准备完成',
|
||||
`确认开始为任务 ${detail.value.task.taskNo} 准备绑定资源吗?系统会自动检查背包、余额并申请虚拟号。`,
|
||||
)
|
||||
}
|
||||
|
||||
async function submitKuaishouCloudDispatch() {
|
||||
if (!detail.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await runAction(
|
||||
() => dispatchAdminTaskKuaishouCloudFulfillment(detail.value!.task.taskId),
|
||||
'已完成绑定确认并发货',
|
||||
`确认客户已经完成绑定,并立即为任务 ${detail.value.task.taskNo} 执行发货吗?这个动作会把"确认绑定完成"和"发货"合并为一步。`,
|
||||
)
|
||||
}
|
||||
|
||||
async function submitKuaishouCloudRefreshRoleInfo() {
|
||||
if (!detail.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await runAction(
|
||||
() => refreshAdminTaskKuaishouCloudRoleInfo(detail.value!.task.taskId),
|
||||
'角色信息已刷新',
|
||||
`确认刷新任务 ${detail.value.task.taskNo} 当前云绑定角色信息吗?系统会重新查询 cloudtentacles 的绑定结果。`,
|
||||
)
|
||||
}
|
||||
|
||||
async function submitKuaishouCloudReturnNumber() {
|
||||
if (!detail.value) {
|
||||
return
|
||||
}
|
||||
|
||||
await runAction(
|
||||
() => returnNumberAdminTaskKuaishouCloudFulfillment(detail.value!.task.taskId),
|
||||
'号码已退还',
|
||||
`确认退还任务 ${detail.value.task.taskNo} 当前使用的虚拟号吗?退号后该流程会正式收口。`,
|
||||
)
|
||||
}
|
||||
|
||||
async function copyText(value: string, successMessage: string) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
showError('当前没有可复制的内容')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
showSuccess(successMessage)
|
||||
} catch {
|
||||
showError('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
function openExternalLink(value: string) {
|
||||
const text = String(value || '').trim()
|
||||
if (!text) {
|
||||
return
|
||||
}
|
||||
|
||||
window.open(text, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
function resolveTaskAutoDeliveryStatus(taskDetail: AdminTaskDetail) {
|
||||
if (taskDetail.task.agisoAutoDelivery?.status) {
|
||||
return taskDetail.task.agisoAutoDelivery.status
|
||||
}
|
||||
|
||||
return taskDetail.task.deliveryStatus === 'delivered' ? 'pending' : 'waiting'
|
||||
}
|
||||
|
||||
function resolveTaskAutoDeliverySummary(taskDetail: AdminTaskDetail) {
|
||||
const autoDelivery = taskDetail.task.agisoAutoDelivery
|
||||
|
||||
if (autoDelivery?.errorMessage) {
|
||||
return autoDelivery.errorMessage
|
||||
}
|
||||
|
||||
if (autoDelivery?.reason) {
|
||||
return formatAutoDeliveryReason(autoDelivery.reason)
|
||||
}
|
||||
|
||||
return taskDetail.task.deliveryStatus === 'delivered'
|
||||
? '当前任务已交付,等待订单下其他任务完成后统一自动发货'
|
||||
: '当前任务尚未完成交付,暂不会触发自动发货'
|
||||
}
|
||||
|
||||
onMounted(loadDetail)
|
||||
onBeforeUnmount(clearScreenshotPreview)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="admin-panel">
|
||||
<AdminPageHeader
|
||||
title="任务详情"
|
||||
description="查看任务全量信息并执行人工操作。"
|
||||
/>
|
||||
|
||||
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
|
||||
|
||||
<el-skeleton v-if="loading" :rows="6" animated />
|
||||
|
||||
<template v-else-if="detail">
|
||||
<section v-if="isKuaishouCloudTask && kuaishouCloudFlow" class="hero-card">
|
||||
<div class="hero-copy">
|
||||
<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 || '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero-meta-grid">
|
||||
<article
|
||||
v-for="card in summaryCards"
|
||||
:key="card.label"
|
||||
:class="['hero-metric', `hero-metric--${card.tone}`]"
|
||||
>
|
||||
<span>{{ card.label }}</span>
|
||||
<strong>{{ card.value }}</strong>
|
||||
</article>
|
||||
</div>
|
||||
<div class="hero-inline">
|
||||
<span>系统绑定 <AdminStatusTag :status="detail.task.systemBindingStatus" /></span>
|
||||
<span>完整绑定 <AdminStatusTag :status="detail.task.userBindingStatus" /></span>
|
||||
<span v-if="detail.task.lastError">最近异常 {{ detail.task.lastError }}</span>
|
||||
</div>
|
||||
<div v-if="claimUrl" class="hero-link-row">
|
||||
<span>客户领取页</span>
|
||||
<a class="claim-link" :href="claimUrl" target="_blank" rel="noreferrer">{{ claimUrl }}</a>
|
||||
</div>
|
||||
<div v-else-if="claimLinkInvalid" class="hero-link-row">
|
||||
<span>客户领取页</span>
|
||||
<strong>已失效</strong>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>角色:{{ detail.task.roleName || '-' }} / {{ detail.task.roleId || '-' }}</p>
|
||||
<p>最后错误:{{ detail.task.lastError || '-' }}</p>
|
||||
<p v-if="claimUrl">
|
||||
领取链接:
|
||||
<a class="claim-link" :href="claimUrl" target="_blank" rel="noreferrer">{{ claimUrl }}</a>
|
||||
</p>
|
||||
<p v-else-if="claimLinkInvalid">领取链接:已失效</p>
|
||||
</section>
|
||||
|
||||
<section class="action-row">
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle"
|
||||
:disabled="!detail.operations.canRetry"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="primary"
|
||||
@click="runAction(() => retryAdminTask(detail!.task.taskId), '任务已重试', `确认重试任务 ${detail!.task.taskNo} 吗?`)"
|
||||
>
|
||||
重试任务
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="detail.operations.canRegenerateClaimLink"
|
||||
:disabled="!detail.operations.canRegenerateClaimLink"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
@click="runAction(() => regenerateAdminTaskClaimLink(detail!.task.taskId), '领取链接已重新生成', `确认重新生成任务 ${detail!.task.taskNo} 的领取链接吗?旧链接会失效。`)"
|
||||
>
|
||||
重发链接
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="detail.operations.canSupportConfirmRole"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="warning"
|
||||
@click="runAction(
|
||||
() => confirmAdminTaskAssistedRole(detail!.task.taskId),
|
||||
'角色已确认',
|
||||
`确认以客服身份锁定任务 ${detail!.task.taskNo} 当前识别到的角色信息吗?`
|
||||
)"
|
||||
>
|
||||
客服确认角色
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="detail.operations.canSupportRedeem"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="success"
|
||||
@click="runAction(
|
||||
() => redeemAdminTaskAssisted(detail!.task.taskId),
|
||||
'兑换任务已启动',
|
||||
`确认开始执行任务 ${detail!.task.taskNo} 的自动兑换吗?`
|
||||
)"
|
||||
>
|
||||
客服开始兑换
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle && detail.operations.canPrepareKuaishouCloudFulfillment"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="primary"
|
||||
@click="submitKuaishouCloudPrepare"
|
||||
>
|
||||
准备绑定资源
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle && detail.operations.canDispatchKuaishouCloudFulfillment"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="success"
|
||||
@click="submitKuaishouCloudDispatch"
|
||||
>
|
||||
确认绑定完成并发货
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle && detail.operations.canReturnKuaishouCloudFulfillment"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="warning"
|
||||
@click="submitKuaishouCloudReturnNumber"
|
||||
>
|
||||
退还号码
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle"
|
||||
:disabled="!detail.operations.canMarkManualReview"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
@click="runAction(() => markAdminTaskManualReview(detail!.task.taskId), '任务已转人工处理', `确认将任务 ${detail!.task.taskNo} 转为人工处理吗?`)"
|
||||
>
|
||||
转人工
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle && detail.operations.canCompleteManualDispatch"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="success"
|
||||
@click="submitManualDispatch('delivered')"
|
||||
>
|
||||
人工完成
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle && detail.operations.canCompleteManualDispatch"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="danger"
|
||||
plain
|
||||
@click="submitManualDispatch('failed')"
|
||||
>
|
||||
履约失败
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canCloseTasks"
|
||||
:disabled="!detail.operations.canClose"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
type="danger"
|
||||
@click="runAction(() => closeAdminTask(detail!.task.taskId), '任务已关闭', `确认关闭任务 ${detail!.task.taskNo} 吗?关闭后不会自动继续推进。`)"
|
||||
>
|
||||
关闭任务
|
||||
</el-button>
|
||||
</section>
|
||||
|
||||
<section v-if="!isKuaishouCloudTask" class="info-card">
|
||||
<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>
|
||||
<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>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section v-if="isKuaishouCloudTask && kuaishouCloudFlow" class="info-card flow-card">
|
||||
<div class="flow-card-head">
|
||||
<div>
|
||||
<h3>快手 Cloud 履约面板</h3>
|
||||
<p class="section-copy">这是一套独立的快手 Cloud 任务面板,优先展示角色信息与当前履约推进点,次级明细可按需展开。</p>
|
||||
</div>
|
||||
<div class="flow-head-actions">
|
||||
<el-button
|
||||
v-if="detail.operations.canRefreshKuaishouCloudRoleInfo"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
@click="submitKuaishouCloudRefreshRoleInfo"
|
||||
>
|
||||
刷新角色信息
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="kuaishouCloudFlow.binding.bindUrl"
|
||||
round
|
||||
@click="copyText(kuaishouCloudFlow.binding.bindUrl, '绑定链接已复制')"
|
||||
>
|
||||
复制绑定链接
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="kuaishouCloudFlow.binding.bindUrl"
|
||||
round
|
||||
type="primary"
|
||||
plain
|
||||
@click="openExternalLink(kuaishouCloudFlow.binding.bindUrl)"
|
||||
>
|
||||
打开绑定页
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="redeem-summary-grid">
|
||||
<article class="redeem-summary-card">
|
||||
<span>准备状态</span>
|
||||
<div class="auto-delivery-tag-line">
|
||||
<AdminStatusTag :status="kuaishouCloudFlow.binding.prepareStatus" />
|
||||
</div>
|
||||
</article>
|
||||
<article class="redeem-summary-card">
|
||||
<span>发货状态</span>
|
||||
<div class="auto-delivery-tag-line">
|
||||
<AdminStatusTag :status="kuaishouCloudFlow.dispatch.status" />
|
||||
</div>
|
||||
</article>
|
||||
<article class="redeem-summary-card">
|
||||
<span>退号状态</span>
|
||||
<div class="auto-delivery-tag-line">
|
||||
<AdminStatusTag :status="kuaishouCloudFlow.returnNumber.status" />
|
||||
</div>
|
||||
</article>
|
||||
<article class="redeem-summary-card">
|
||||
<span>当前任务状态</span>
|
||||
<div class="auto-delivery-tag-line">
|
||||
<AdminStatusTag :status="detail.task.status" />
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="kuaishou-focus-grid">
|
||||
<article class="flow-block flow-block--spotlight">
|
||||
<div class="spotlight-head">
|
||||
<div>
|
||||
<h4>角色信息</h4>
|
||||
<p class="flow-copy">角色信息来自 cloudtentacles 的 `vn/bind_info`,客服可在客户绑定后手动刷新。</p>
|
||||
</div>
|
||||
<div class="spotlight-actions">
|
||||
<AdminStatusTag :status="kuaishouCloudRole?.status || 'pending'" />
|
||||
<el-button
|
||||
v-if="detail.operations.canRefreshKuaishouCloudRoleInfo"
|
||||
:loading="actionLoading"
|
||||
round
|
||||
size="small"
|
||||
@click="submitKuaishouCloudRefreshRoleInfo"
|
||||
>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="role-hero">
|
||||
<div>
|
||||
<span>角色名</span>
|
||||
<strong>{{ kuaishouCloudRole?.name || detail.task.roleName || '等待识别' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>角色 ID</span>
|
||||
<strong>{{ kuaishouCloudRole?.rid || detail.task.roleId || '等待识别' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>最近刷新</span>
|
||||
<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">
|
||||
<div v-for="entry in kuaishouRoleInfoEntries" :key="entry.key">
|
||||
<span>{{ entry.label }}</span>
|
||||
<strong>{{ entry.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="flow-block flow-block--checklist">
|
||||
<h4>流程检查</h4>
|
||||
<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">
|
||||
<span>{{ item.label }}</span>
|
||||
<AdminStatusTag :status="item.status" />
|
||||
</div>
|
||||
<strong>{{ item.detail }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
</el-collapse-item>
|
||||
|
||||
<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>
|
||||
<strong>{{ entry.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</section>
|
||||
|
||||
<section
|
||||
v-if="detail.order?.provider === 'agiso' && detail.order?.platform === 'xianyu'"
|
||||
class="info-card"
|
||||
>
|
||||
<h3>咸鱼自动发货</h3>
|
||||
<p class="section-copy">这里展示这条任务最近一次触发咸鱼自动发货的状态、原因和接口回执摘要。</p>
|
||||
|
||||
<div class="redeem-summary-grid">
|
||||
<article class="redeem-summary-card">
|
||||
<span>当前状态</span>
|
||||
<div class="auto-delivery-tag-line">
|
||||
<AdminStatusTag :status="resolveTaskAutoDeliveryStatus(detail)" />
|
||||
</div>
|
||||
</article>
|
||||
<article class="redeem-summary-card">
|
||||
<span>触发来源</span>
|
||||
<strong>{{ formatAutoDeliveryTrigger(detail.task.agisoAutoDelivery?.trigger || '') }}</strong>
|
||||
</article>
|
||||
<article class="redeem-summary-card">
|
||||
<span>最近执行</span>
|
||||
<strong>{{ formatAdminDateTime(detail.task.agisoAutoDelivery?.updatedAt) }}</strong>
|
||||
</article>
|
||||
<article class="redeem-summary-card">
|
||||
<span>接口回执</span>
|
||||
<strong>
|
||||
{{ 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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="detail.redeemResolution" class="info-card">
|
||||
<h3>兑换重试链路</h3>
|
||||
<p class="section-copy">这里记录自动兑换时每次使用过的 CDK、判定结果,以及是否切换到了新的同类型凭据。</p>
|
||||
|
||||
<div class="redeem-summary-grid">
|
||||
<article class="redeem-summary-card">
|
||||
<span>处理结果</span>
|
||||
<strong>{{ formatRedeemResolutionStatus(detail.redeemResolution.status) }}</strong>
|
||||
</article>
|
||||
<article class="redeem-summary-card">
|
||||
<span>任务收口状态</span>
|
||||
<strong>{{ detail.redeemResolution.taskStatus || detail.task.status || '-' }}</strong>
|
||||
</article>
|
||||
<article class="redeem-summary-card">
|
||||
<span>更换新码次数</span>
|
||||
<strong>{{ detail.redeemResolution.replacementCount }}</strong>
|
||||
</article>
|
||||
<article class="redeem-summary-card">
|
||||
<span>完成时间</span>
|
||||
<strong>{{ formatAdminDateTime(detail.redeemResolution.finishedAt) }}</strong>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-if="detail.redeemResolution.attempts.length > 0" class="redeem-attempts">
|
||||
<article
|
||||
v-for="attempt in detail.redeemResolution.attempts"
|
||||
:key="`${attempt.attempt}-${attempt.inventoryItemId || 'na'}`"
|
||||
class="redeem-attempt-card"
|
||||
>
|
||||
<div class="redeem-attempt-head">
|
||||
<strong>第 {{ attempt.attempt }} 次尝试</strong>
|
||||
<span class="redeem-outcome">{{ formatRedeemOutcomeLabel(attempt.outcome) }}</span>
|
||||
</div>
|
||||
<div class="redeem-attempt-grid">
|
||||
<div>
|
||||
<span>库存项</span>
|
||||
<strong>{{ attempt.inventoryItemId ? `#${attempt.inventoryItemId}` : '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>凭据</span>
|
||||
<strong>{{ attempt.codeMasked || '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>类型</span>
|
||||
<strong>{{ attempt.credentialType || '-' }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>结果码</span>
|
||||
<strong>{{ attempt.resultCode || '-' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<p class="redeem-attempt-copy">{{ attempt.resultMessage || '-' }}</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="!isKuaishouCloudTask" class="info-card">
|
||||
<h3>库存绑定</h3>
|
||||
<p class="section-copy">这里展示任务当前和历史绑定过的库存项,便于核对多库存任务的真实履约轨迹。</p>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>绑定</th>
|
||||
<th>角色</th>
|
||||
<th>库存项</th>
|
||||
<th>状态</th>
|
||||
<th>凭据内容</th>
|
||||
<th>时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="detail.inventoryBindings.length === 0">
|
||||
<td colspan="7" class="table-empty">当前任务还没有库存绑定记录</td>
|
||||
</tr>
|
||||
<tr v-for="binding in detail.inventoryBindings" :key="binding.bindingId">
|
||||
<td>
|
||||
<div>#{{ binding.bindingId }}</div>
|
||||
<div v-if="binding.isPrimary" class="binding-primary">主库存项</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ binding.roleKey || '-' }}</div>
|
||||
<div class="binding-meta">数量 {{ binding.quantity }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>ID {{ binding.inventoryItemId }}</div>
|
||||
<div>{{ binding.skuCode || '-' }}</div>
|
||||
<div class="binding-meta">{{ binding.credentialType || '-' }} · {{ binding.batchNo || '无批次' }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="status-stack">
|
||||
<AdminStatusTag :status="binding.bindingStatus" />
|
||||
<AdminStatusTag :status="binding.inventoryStatus" />
|
||||
</div>
|
||||
<div v-if="binding.invalidReason" class="binding-meta">
|
||||
{{ binding.inventoryStatus === 'invalid' ? '作废原因' : binding.inventoryStatus === 'consumed' ? '消耗原因' : '处理原因' }}:{{ binding.invalidReason }}
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ canViewSensitiveTaskData ? (binding.displayValue || '-') : '客服不可见' }}</td>
|
||||
<td>
|
||||
<div>绑定 {{ formatAdminDateTime(binding.createdAt) }}</div>
|
||||
<div class="binding-meta">消费 {{ formatAdminDateTime(binding.consumedAt) }}</div>
|
||||
<div class="binding-meta">释放 {{ formatAdminDateTime(binding.releasedAt) }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<el-button
|
||||
v-if="hasAdminRole('admin') && binding.canRelease"
|
||||
:loading="actionLoading"
|
||||
link
|
||||
@click="runAction(
|
||||
() => releaseAdminTaskInventoryBinding(detail!.task.taskId, binding.bindingId),
|
||||
'库存绑定已释放',
|
||||
`确认释放绑定 #${binding.bindingId} 对应的库存项 ${binding.displayValue || binding.inventoryItemId} 吗?`
|
||||
)"
|
||||
>
|
||||
释放绑定
|
||||
</el-button>
|
||||
<span v-else class="binding-meta">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section v-if="detail.task.executorKey === 'manual_dispatch'" class="info-card">
|
||||
<h3>人工履约回写</h3>
|
||||
<div class="manual-grid">
|
||||
<label class="field-block">
|
||||
<span>外部流水 / 单号</span>
|
||||
<el-input
|
||||
v-model="manualDispatchForm.deliveryReference"
|
||||
:disabled="!canManageTaskLifecycle"
|
||||
maxlength="120"
|
||||
placeholder="例如快递单号、平台消息回执号"
|
||||
/>
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>已交付内容</span>
|
||||
<el-input
|
||||
v-model="manualDispatchForm.deliveredCredential"
|
||||
:disabled="!canManageTaskLifecycle"
|
||||
maxlength="500"
|
||||
placeholder="可填写人工发送的卡密、链接或关键信息"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
/>
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>处理备注</span>
|
||||
<el-input
|
||||
v-model="manualDispatchForm.resultMessage"
|
||||
:disabled="!canManageTaskLifecycle"
|
||||
maxlength="500"
|
||||
placeholder="说明实际履约结果,失败时建议写清原因"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
/>
|
||||
</label>
|
||||
</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>流水号:{{ detail.manualDispatch.deliveryReference || '-' }}</p>
|
||||
<p>交付内容:{{ detail.manualDispatch.deliveredCredential || '-' }}</p>
|
||||
<p>备注:{{ detail.manualDispatch.resultMessage || '-' }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="detail.events.length > 0" class="info-card">
|
||||
<h3>任务事件</h3>
|
||||
<table class="data-table">
|
||||
<tbody>
|
||||
<tr v-for="event in detail.events" :key="event.eventId">
|
||||
<th>{{ formatTaskEventType(event.eventType) }}</th>
|
||||
<td>
|
||||
<div class="event-copy">{{ formatTaskEventPayload(event.payload) }}</div>
|
||||
<div class="event-time">{{ formatAdminDateTime(event.createdAt) }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<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" />
|
||||
</section>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped src="./AdminTaskDetail.css"></style>
|
||||
@@ -0,0 +1,323 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue'
|
||||
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
|
||||
import { showConfirm, showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
closeAdminTask,
|
||||
fetchAdminTasks,
|
||||
markAdminTaskManualReview,
|
||||
regenerateAdminTaskClaimLink,
|
||||
retryAdminTask,
|
||||
} from '@/services/admin'
|
||||
import type { AdminPagination, AdminTaskActionResponse, AdminTaskListItem } from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
import { adminTaskStatusOptions } from '@/utils/admin-options'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
const loading = ref(true)
|
||||
const actionLoadingId = ref<number | null>(null)
|
||||
const errorMessage = ref('')
|
||||
const status = ref('')
|
||||
const taskNo = ref('')
|
||||
const platformOrderId = ref('')
|
||||
const roleId = ref('')
|
||||
const skuCode = ref('')
|
||||
const dateFrom = ref('')
|
||||
const dateTo = ref('')
|
||||
const items = ref<AdminTaskListItem[]>([])
|
||||
const lastClaimUrl = ref('')
|
||||
const pagination = ref<AdminPagination>({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
const canManageTaskLifecycle = hasAdminRole('operator')
|
||||
const canCloseTasks = hasAdminRole('support')
|
||||
|
||||
type AdminTaskActionKey = 'retry' | 'regenerate_claim_link' | 'manual_review' | 'close'
|
||||
|
||||
async function loadTasks(page = pagination.value.page) {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchAdminTasks({
|
||||
page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
status: status.value.trim(),
|
||||
taskNo: taskNo.value.trim(),
|
||||
platformOrderId: platformOrderId.value.trim(),
|
||||
roleId: roleId.value.trim(),
|
||||
skuCode: skuCode.value.trim(),
|
||||
dateFrom: dateFrom.value.trim(),
|
||||
dateTo: dateTo.value.trim(),
|
||||
})
|
||||
items.value = response.data.items
|
||||
pagination.value = response.data.pagination
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取任务列表失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function formatBindingRoles(item: AdminTaskListItem) {
|
||||
const roles = item.bindingSummary.roleKeys
|
||||
if (!Array.isArray(roles) || roles.length === 0) return '无绑定角色'
|
||||
return roles.join(' / ')
|
||||
}
|
||||
|
||||
function formatBindingCountSummary(item: AdminTaskListItem) {
|
||||
const summary = item.bindingSummary
|
||||
return `共 ${summary.totalBindingCount} 条 · 预占 ${summary.reservedBindingCount} · 已消费 ${summary.consumedBindingCount} · 已释放 ${summary.releasedBindingCount}`
|
||||
}
|
||||
|
||||
onMounted(() => { void loadTasks() })
|
||||
|
||||
async function runAction(
|
||||
taskId: number,
|
||||
action: () => Promise<{ data: AdminTaskActionResponse }>,
|
||||
successMessage: string,
|
||||
confirmText: string,
|
||||
) {
|
||||
try {
|
||||
await showConfirm(confirmText, '确认操作', { type: 'warning', confirmButtonText: '继续执行', cancelButtonText: '取消' })
|
||||
} catch { return }
|
||||
|
||||
actionLoadingId.value = taskId
|
||||
try {
|
||||
const response = await action()
|
||||
if (response.data.claimUrl) lastClaimUrl.value = response.data.claimUrl
|
||||
showSuccess(successMessage)
|
||||
await loadTasks()
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '操作失败')
|
||||
} finally { actionLoadingId.value = null }
|
||||
}
|
||||
|
||||
function handleActionCommand(actionKey: AdminTaskActionKey, item: AdminTaskListItem) {
|
||||
if (actionKey === 'retry') {
|
||||
void runAction(item.taskId, () => retryAdminTask(item.taskId), '任务已重试', `确认重试任务 ${item.taskNo} 吗?`)
|
||||
return
|
||||
}
|
||||
if (actionKey === 'regenerate_claim_link') {
|
||||
void runAction(item.taskId, () => regenerateAdminTaskClaimLink(item.taskId), '领取链接已重新生成', `确认重新生成任务 ${item.taskNo} 的领取链接吗?旧链接会失效。`)
|
||||
return
|
||||
}
|
||||
if (actionKey === 'manual_review') {
|
||||
void runAction(item.taskId, () => markAdminTaskManualReview(item.taskId), '任务已转人工处理', `确认将任务 ${item.taskNo} 转为人工处理吗?`)
|
||||
return
|
||||
}
|
||||
void runAction(item.taskId, () => closeAdminTask(item.taskId), '任务已关闭', `确认关闭任务 ${item.taskNo} 吗?关闭后不会自动继续推进。`)
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
status.value = ''
|
||||
taskNo.value = ''
|
||||
platformOrderId.value = ''
|
||||
roleId.value = ''
|
||||
skuCode.value = ''
|
||||
dateFrom.value = ''
|
||||
dateTo.value = ''
|
||||
void loadTasks(1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tasks-page list-page">
|
||||
<AdminPageHeader title="交付任务" description="更紧凑地查看任务状态、角色绑定、库存凭据和异常信息。">
|
||||
<template #extra>
|
||||
<span class="total-badge">共 {{ pagination.total }} 条任务</span>
|
||||
</template>
|
||||
</AdminPageHeader>
|
||||
|
||||
<el-card shadow="never" class="section-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">查询任务</span>
|
||||
<span class="card-desc">按状态、任务号、订单号、角色标识、SKU 和日期筛选。</span>
|
||||
</div>
|
||||
</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-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="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" />
|
||||
<div class="filter-buttons">
|
||||
<el-button round @click="resetFilters">重置</el-button>
|
||||
<el-button round type="primary" @click="loadTasks(1)">查询</el-button>
|
||||
</div>
|
||||
</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-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>
|
||||
|
||||
<template v-else>
|
||||
<div class="tasks-toolbar table-toolbar">
|
||||
<strong>任务列表</strong>
|
||||
<span>第 {{ pagination.page }} 页,当前展示 {{ items.length }} 条</span>
|
||||
</div>
|
||||
|
||||
<el-table :data="items" stripe size="small" class="tasks-table data-table">
|
||||
<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">
|
||||
{{ row.taskNo }}
|
||||
</RouterLink>
|
||||
<div class="cell-meta-row">
|
||||
<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-subline">SKU {{ row.skuCode || '-' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="进度" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<div class="progress-row">
|
||||
<span class="progress-label">任务</span>
|
||||
<AdminStatusTag :status="row.status" />
|
||||
</div>
|
||||
<div class="progress-row">
|
||||
<span class="progress-label">系统</span>
|
||||
<AdminStatusTag :status="row.systemBindingStatus" />
|
||||
</div>
|
||||
<div class="progress-row">
|
||||
<span class="progress-label">完整</span>
|
||||
<AdminStatusTag :status="row.userBindingStatus" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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-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>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时间" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<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>
|
||||
</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>
|
||||
<div class="action-stack">
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle"
|
||||
: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} 吗?`)"
|
||||
>
|
||||
重试
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle && row.executorKey !== 'manual_dispatch'"
|
||||
size="small"
|
||||
@click="handleActionCommand('regenerate_claim_link', row)"
|
||||
>
|
||||
重发链接
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageTaskLifecycle"
|
||||
size="small"
|
||||
@click="handleActionCommand('manual_review', row)"
|
||||
>
|
||||
转人工
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canCloseTasks && !['redeemed', 'closed'].includes(row.status)"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleActionCommand('close', row)"
|
||||
>
|
||||
关闭任务
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-if="pagination.total > 0"
|
||||
v-model:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
layout="total, prev, pager, next"
|
||||
style="margin-top: 16px; justify-content: center"
|
||||
@current-change="loadTasks"
|
||||
/>
|
||||
</template>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@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);
|
||||
}
|
||||
.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-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 34px;
|
||||
min-height: 22px;
|
||||
padding: 0 7px;
|
||||
border-radius: var(--radius-full);
|
||||
background: #f7f9fc;
|
||||
border: 1px solid var(--border-default);
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
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-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; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user