增加电子凭证核销详情

This commit is contained in:
yml2213
2026-07-08 17:52:10 +08:00
parent 9a6d083096
commit 85209d8082
5 changed files with 201 additions and 0 deletions
@@ -168,6 +168,8 @@ function mapAdminKuaishouIndustryVoucher(voucher: {
send_callback_attempt_count?: unknown
send_callback_last_error?: unknown
send_callback_sent_at?: unknown
consume_serial_num?: unknown
consume_details_json?: unknown
consumed_at?: unknown
destroyed_at?: unknown
}) {
@@ -180,6 +182,8 @@ function mapAdminKuaishouIndustryVoucher(voucher: {
sendCallbackAttemptCount: Number(voucher.send_callback_attempt_count || 0) || 0,
sendCallbackLastError: String(voucher.send_callback_last_error || '').trim(),
sendCallbackSentAt: voucher.send_callback_sent_at || null,
consumeSerialNum: String(voucher.consume_serial_num || '').trim(),
consumeDetails: normalizeAdminJsonArray(voucher.consume_details_json),
consumedAt: voucher.consumed_at || null,
destroyedAt: voucher.destroyed_at || null,
}
@@ -382,11 +386,32 @@ function mapKuaishouIndustryVoucherContext(value: unknown): JsonRecord | null {
sendCallbackLastError: String(record.sendCallbackLastError || '').trim(),
sendCallbackAttemptCount: Number(record.sendCallbackAttemptCount || 0) || 0,
sendCallbackSentAt: record.sendCallbackSentAt || null,
consumeSerialNum: String(record.consumeSerialNum || '').trim(),
consumeDetails: normalizeAdminJsonArray(record.consumeDetails),
consumedAt: record.consumedAt || null,
destroyedAt: record.destroyedAt || null,
}
}
function normalizeAdminJsonArray(value: unknown): JsonRecord[] {
if (Array.isArray(value)) {
return value.filter((item): item is JsonRecord =>
Boolean(item && typeof item === 'object' && !Array.isArray(item)),
)
}
try {
const parsed = JSON.parse(String(value || '[]'))
return Array.isArray(parsed)
? parsed.filter((item): item is JsonRecord =>
Boolean(item && typeof item === 'object' && !Array.isArray(item)),
)
: []
} catch {
return []
}
}
function resolveKuaishouIndustryVoucherForTask(
task: TaskRow,
taskContext: JsonRecord,
@@ -212,6 +212,8 @@ export interface AdminTaskDetail {
sendCallbackLastError: string
sendCallbackAttemptCount: number
sendCallbackSentAt: string | null
consumeSerialNum: string
consumeDetails: Array<Record<string, unknown>>
consumedAt: string | null
destroyedAt: string | null
}
@@ -121,6 +121,34 @@
gap: var(--space-3);
}
.section-headline {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-3);
margin-bottom: var(--space-3);
}
.section-headline h3 {
margin: 0;
}
.section-actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
justify-content: flex-end;
}
.voucher-detail-block {
margin-top: var(--space-4);
}
.voucher-detail-block h4 {
margin: 0 0 var(--space-3);
color: var(--text-primary);
}
.section-copy {
margin: 0 0 var(--space-3);
color: var(--text-secondary);
@@ -13,6 +13,7 @@ import AdminTaskActionBar from './components/AdminTaskActionBar.vue'
import AdminTaskEventsSection from './components/AdminTaskEventsSection.vue'
import AdminTaskHeroSection from './components/AdminTaskHeroSection.vue'
import AdminTaskInfoSection from './components/AdminTaskInfoSection.vue'
import AdminTaskKuaishouIndustryVoucherSection from './components/AdminTaskKuaishouIndustryVoucherSection.vue'
import AdminTaskManualDispatchSection from './components/AdminTaskManualDispatchSection.vue'
import AdminTaskRedeemResolutionSection from './components/AdminTaskRedeemResolutionSection.vue'
import AdminTaskScreenshotSection from './components/AdminTaskScreenshotSection.vue'
@@ -122,6 +123,14 @@ onMounted(loadDetail)
:claim-link-invalid="claimLinkInvalid"
/>
<AdminTaskKuaishouIndustryVoucherSection
v-if="detail.kuaishouIndustryVoucher"
:detail="detail"
:action-loading="actionLoading"
@resend-industry-code="submitKuaishouIndustryResendCode"
@consume-industry-voucher="submitKuaishouIndustryConsume"
/>
<AdminTaskKuaishouCloudPanel
v-if="isKuaishouCloudTask && kuaishouCloudFlow"
:detail="detail"
@@ -0,0 +1,137 @@
<script setup lang="ts">
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
import type { AdminTaskDetail } from '@/types/admin'
import { formatAdminDateTime } from '@/utils/admin-time'
interface Props {
detail: AdminTaskDetail
actionLoading: boolean
}
defineProps<Props>()
const emit = defineEmits<{
resendIndustryCode: []
consumeIndustryVoucher: []
}>()
function formatConsumeDetailTime(value: unknown) {
const parsed = Number(value || 0)
if (Number.isFinite(parsed) && parsed > 0) {
return formatAdminDateTime(new Date(parsed).toISOString())
}
return formatAdminDateTime(value as string | null)
}
function formatValue(value: unknown) {
if (value === null || value === undefined || value === '') {
return '-'
}
if (typeof value === 'object') {
return JSON.stringify(value)
}
return String(value)
}
</script>
<template>
<el-card v-if="detail.kuaishouIndustryVoucher" shadow="never" class="section-card">
<div class="section-headline">
<h3>电子凭证与核销</h3>
<div class="section-actions">
<el-button
v-if="detail.operations.canResendKuaishouIndustryVoucherCode"
:loading="actionLoading"
round
@click="emit('resendIndustryCode')"
>
重发发码回调
</el-button>
<el-button
v-if="detail.operations.canConsumeKuaishouIndustryVoucher"
:loading="actionLoading"
round
type="warning"
plain
@click="emit('consumeIndustryVoucher')"
>
手动核销
</el-button>
</div>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="平台订单号">
{{ detail.kuaishouIndustryVoucher.oid || '-' }}
</el-descriptions-item>
<el-descriptions-item label="券序号">
{{ detail.kuaishouIndustryVoucher.unitIndex || '-' }}
</el-descriptions-item>
<el-descriptions-item label="核销码">
{{ detail.kuaishouIndustryVoucher.voucherCode || '-' }}
</el-descriptions-item>
<el-descriptions-item label="券状态">
<AdminStatusTag :status="detail.kuaishouIndustryVoucher.status || ''" />
</el-descriptions-item>
<el-descriptions-item label="发码回调">
<AdminStatusTag :status="detail.kuaishouIndustryVoucher.sendCallbackStatus || ''" />
</el-descriptions-item>
<el-descriptions-item label="回调次数">
{{ detail.kuaishouIndustryVoucher.sendCallbackAttemptCount || 0 }}
</el-descriptions-item>
<el-descriptions-item label="回调时间">
{{ formatAdminDateTime(detail.kuaishouIndustryVoucher.sendCallbackSentAt) }}
</el-descriptions-item>
<el-descriptions-item label="回调异常">
{{ detail.kuaishouIndustryVoucher.sendCallbackLastError || '-' }}
</el-descriptions-item>
<el-descriptions-item label="核销流水">
{{ detail.kuaishouIndustryVoucher.consumeSerialNum || '-' }}
</el-descriptions-item>
<el-descriptions-item label="核销时间">
{{ formatAdminDateTime(detail.kuaishouIndustryVoucher.consumedAt) }}
</el-descriptions-item>
<el-descriptions-item label="销毁时间">
{{ formatAdminDateTime(detail.kuaishouIndustryVoucher.destroyedAt) }}
</el-descriptions-item>
</el-descriptions>
<div class="voucher-detail-block">
<h4>核销明细</h4>
<el-table
v-if="detail.kuaishouIndustryVoucher.consumeDetails.length > 0"
:data="detail.kuaishouIndustryVoucher.consumeDetails"
size="small"
>
<el-table-column label="流水号" min-width="160">
<template #default="{ row }">{{ formatValue(row.serialNum) }}</template>
</el-table-column>
<el-table-column label="核销类型" width="120">
<template #default="{ row }">{{ formatValue(row.consumeType) }}</template>
</el-table-column>
<el-table-column label="时间" width="180">
<template #default="{ row }">{{ formatConsumeDetailTime(row.consumeTime) }}</template>
</el-table-column>
<el-table-column label="来源" width="150">
<template #default="{ row }">{{ formatValue(row.source) }}</template>
</el-table-column>
<el-table-column label="门店/物流">
<template #default="{ row }">
{{
[row.storeName, row.storeAddress, row.expressCode, row.expressNo]
.map(formatValue)
.filter((item) => item !== '-')
.join(' / ') || '-'
}}
</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无核销明细" :image-size="64" />
</div>
</el-card>
</template>
<style scoped src="../AdminTaskDetail.css"></style>