diff --git a/apps/backend/src/services/admin/admin-read-service.ts b/apps/backend/src/services/admin/admin-read-service.ts index dea8c153..52a9f969 100644 --- a/apps/backend/src/services/admin/admin-read-service.ts +++ b/apps/backend/src/services/admin/admin-read-service.ts @@ -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, diff --git a/apps/frontend-vue/src/types/admin/tasks.ts b/apps/frontend-vue/src/types/admin/tasks.ts index 84ff4f61..baa0b711 100644 --- a/apps/frontend-vue/src/types/admin/tasks.ts +++ b/apps/frontend-vue/src/types/admin/tasks.ts @@ -212,6 +212,8 @@ export interface AdminTaskDetail { sendCallbackLastError: string sendCallbackAttemptCount: number sendCallbackSentAt: string | null + consumeSerialNum: string + consumeDetails: Array> consumedAt: string | null destroyedAt: string | null } diff --git a/apps/frontend-vue/src/views/admin/tasks/AdminTaskDetail.css b/apps/frontend-vue/src/views/admin/tasks/AdminTaskDetail.css index b1c38a58..f65f5fbb 100644 --- a/apps/frontend-vue/src/views/admin/tasks/AdminTaskDetail.css +++ b/apps/frontend-vue/src/views/admin/tasks/AdminTaskDetail.css @@ -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); diff --git a/apps/frontend-vue/src/views/admin/tasks/AdminTaskDetailView.vue b/apps/frontend-vue/src/views/admin/tasks/AdminTaskDetailView.vue index 358763f1..475ce392 100644 --- a/apps/frontend-vue/src/views/admin/tasks/AdminTaskDetailView.vue +++ b/apps/frontend-vue/src/views/admin/tasks/AdminTaskDetailView.vue @@ -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" /> + + +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() + +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) +} + + + + +