From bc15fcc349e8b470e2a644f2c2b1daf773700e6c Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 17 May 2026 09:45:55 +0800 Subject: [PATCH] [Snow Team] claim-refactor: auto-commit work --- apps/frontend/src/views/claim/ClaimView.vue | 5 +- .../views/claim/KuaishouCloudClaimView.vue | 839 ------------------ .../kuaishou-cloud/KuaishouCloudClaimView.vue | 168 ++++ .../components/ClaimBindingStep.vue | 180 ++++ .../components/ClaimConfirmStep.vue | 128 +++ .../components/ClaimHeaderCard.vue | 96 ++ .../components/ClaimResultStep.vue | 151 ++++ .../components/ClaimSummaryCard.vue | 67 ++ .../components/ClaimTicketStep.vue | 149 ++++ .../composables/useKuaishouCloudClaim.ts | 368 ++++++++ 10 files changed, 1309 insertions(+), 842 deletions(-) delete mode 100644 apps/frontend/src/views/claim/KuaishouCloudClaimView.vue create mode 100644 apps/frontend/src/views/claim/kuaishou-cloud/KuaishouCloudClaimView.vue create mode 100644 apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimBindingStep.vue create mode 100644 apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimConfirmStep.vue create mode 100644 apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimHeaderCard.vue create mode 100644 apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimResultStep.vue create mode 100644 apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimSummaryCard.vue create mode 100644 apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimTicketStep.vue create mode 100644 apps/frontend/src/views/claim/kuaishou-cloud/composables/useKuaishouCloudClaim.ts diff --git a/apps/frontend/src/views/claim/ClaimView.vue b/apps/frontend/src/views/claim/ClaimView.vue index a5c41bba..72756140 100644 --- a/apps/frontend/src/views/claim/ClaimView.vue +++ b/apps/frontend/src/views/claim/ClaimView.vue @@ -5,7 +5,7 @@ import { useRoute } from 'vue-router' import { fetchClaimDetail } from '@/services/claim' import ClaimTencentView from './ClaimTencentView.vue' -import KuaishouCloudClaimView from './KuaishouCloudClaimView.vue' +import KuaishouCloudClaimView from './kuaishou-cloud/KuaishouCloudClaimView.vue' const route = useRoute() const token = computed(() => String(route.params.token || '').trim()) @@ -63,8 +63,7 @@ onMounted(() => { display: grid; place-items: center; padding: 24px; - background: - radial-gradient(circle at top left, rgba(74, 222, 128, 0.12), transparent 28%), + background: radial-gradient(circle at top left, rgba(74, 222, 128, 0.12), transparent 28%), linear-gradient(180deg, #f8fafc 0%, #eef4fb 100%); } diff --git a/apps/frontend/src/views/claim/KuaishouCloudClaimView.vue b/apps/frontend/src/views/claim/KuaishouCloudClaimView.vue deleted file mode 100644 index 620f713f..00000000 --- a/apps/frontend/src/views/claim/KuaishouCloudClaimView.vue +++ /dev/null @@ -1,839 +0,0 @@ - - - - - diff --git a/apps/frontend/src/views/claim/kuaishou-cloud/KuaishouCloudClaimView.vue b/apps/frontend/src/views/claim/kuaishou-cloud/KuaishouCloudClaimView.vue new file mode 100644 index 00000000..778c6192 --- /dev/null +++ b/apps/frontend/src/views/claim/kuaishou-cloud/KuaishouCloudClaimView.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimBindingStep.vue b/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimBindingStep.vue new file mode 100644 index 00000000..ebda5e58 --- /dev/null +++ b/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimBindingStep.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimConfirmStep.vue b/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimConfirmStep.vue new file mode 100644 index 00000000..2b4d066d --- /dev/null +++ b/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimConfirmStep.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimHeaderCard.vue b/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimHeaderCard.vue new file mode 100644 index 00000000..0c2dbfcc --- /dev/null +++ b/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimHeaderCard.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimResultStep.vue b/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimResultStep.vue new file mode 100644 index 00000000..7fd89f1e --- /dev/null +++ b/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimResultStep.vue @@ -0,0 +1,151 @@ + + + + + diff --git a/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimSummaryCard.vue b/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimSummaryCard.vue new file mode 100644 index 00000000..a77f6f16 --- /dev/null +++ b/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimSummaryCard.vue @@ -0,0 +1,67 @@ + + + + + diff --git a/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimTicketStep.vue b/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimTicketStep.vue new file mode 100644 index 00000000..cd61ecf8 --- /dev/null +++ b/apps/frontend/src/views/claim/kuaishou-cloud/components/ClaimTicketStep.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/apps/frontend/src/views/claim/kuaishou-cloud/composables/useKuaishouCloudClaim.ts b/apps/frontend/src/views/claim/kuaishou-cloud/composables/useKuaishouCloudClaim.ts new file mode 100644 index 00000000..869abf69 --- /dev/null +++ b/apps/frontend/src/views/claim/kuaishou-cloud/composables/useKuaishouCloudClaim.ts @@ -0,0 +1,368 @@ +import { computed, onBeforeUnmount, onMounted, ref } from 'vue' +import QRCode from 'qrcode' +import { ElMessageBox } from 'element-plus' + +import { showError, showSuccess } from '@/lib/feedback' +import { + confirmKuaishouCloudClaimRole, + fetchClaimDetail, + redeemKuaishouCloudClaim, + verifyKuaishouCloudClaimTicket, +} from '@/services/claim' +import type { ClaimDetailData } from '@/types/claim' + +export function useKuaishouCloudClaim(token: () => string) { + const loading = ref(true) + const submitting = ref(false) + const refreshingRole = ref(false) + const confirmingRole = ref(false) + const redeeming = ref(false) + const errorMessage = ref('') + const detail = ref(null) + const ticketCode = ref('') + const qrCodeDataUrl = ref('') + let pollTimer = 0 + + // ── derived data ──────────────────────────────────────── + + const flow = computed(() => detail.value?.kuaishouCloudFulfillment || null) + const order = computed(() => detail.value?.order || null) + const orderItem = computed(() => detail.value?.orderItem || null) + const task = computed(() => detail.value?.task || null) + + // ── computed status flags ─────────────────────────────── + + const roleName = computed(() => flow.value?.role.name || flow.value?.binding.roleName || '') + const roleId = computed(() => flow.value?.role.rid || flow.value?.binding.roleId || '') + const isTicketVerified = computed(() => flow.value?.ticket.status === 'verified') + + const isBindUrlExpired = computed(() => { + const expiresAt = String(flow.value?.binding.bindExpiresAt || '').trim() + if (!expiresAt) { + return false + } + const expiresTime = Date.parse(expiresAt) + return Number.isFinite(expiresTime) && expiresTime <= Date.now() + }) + + const isBindingPrepared = computed( + () => + flow.value?.binding.prepareStatus === 'ready' && + Boolean(String(flow.value?.binding.bindUrl || '').trim()) && + !isBindUrlExpired.value, + ) + + const isBindingPreparing = computed(() => flow.value?.binding.prepareStatus === 'pending') + const canEnterBindingStep = computed(() => isTicketVerified.value) + const isRoleReady = computed(() => Boolean(roleName.value || roleId.value)) + const isRoleConfirmed = computed(() => String(task.value?.status || '').trim() === 'role_confirmed') + const isDispatched = computed(() => String(flow.value?.dispatch.status || '').trim() === 'success') + + const isCompleted = computed( + () => + String(task.value?.status || '').trim() === 'completed' || + String(flow.value?.consume.status || '').trim() === 'success', + ) + + const hasRedeemResult = computed(() => { + const status = String(task.value?.status || '').trim() + return ( + isDispatched.value || + ['dispatched_pending_return', 'completed', 'manual_review', 'failed'].includes(status) + ) + }) + + const canSubmitTicket = computed( + () => + !['completed', 'manual_review', 'closed', 'expired'].includes( + String(task.value?.status || '').trim(), + ) && !submitting.value, + ) + + const currentStep = computed(() => { + if (hasRedeemResult.value) { + return 4 + } + if (isRoleConfirmed.value) { + return 3 + } + if (canEnterBindingStep.value) { + return 2 + } + return 1 + }) + + const progressText = computed(() => { + if (hasRedeemResult.value) { + return '兑换结果已生成' + } + if (isRoleConfirmed.value) { + return '角色已确认,等待兑换' + } + if (isTicketVerified.value) { + return isBindingPrepared.value ? '请完成扫码绑定' : '绑定链接刷新中,请稍候' + } + return '等待提交核销码' + }) + + const resultTitle = computed(() => { + if (isCompleted.value) { + return '兑换成功' + } + if (isDispatched.value) { + return '兑换请求已提交' + } + return '结果已记录' + }) + + const resultDescription = computed(() => { + if (isCompleted.value) { + return '当前兑换流程已经完成。发货、退号和核销结果会保存在后台任务界面。' + } + return '你的兑换请求已经提交。发货、退号和核销结果会由系统保存在后台任务界面,无需在此页面等待。' + }) + + // ── actions ───────────────────────────────────────────── + + async function generateQRCode(url: string) { + if (!url) { + qrCodeDataUrl.value = '' + return + } + try { + qrCodeDataUrl.value = await QRCode.toDataURL(url, { + width: 280, + margin: 2, + color: { + dark: '#0f172a', + light: '#ffffff', + }, + }) + } catch (error) { + qrCodeDataUrl.value = '' + console.error('生成二维码失败:', error) + } + } + + async function applyDetail(nextDetail: ClaimDetailData) { + detail.value = nextDetail + if (!ticketCode.value && nextDetail.kuaishouCloudFulfillment?.ticket.code) { + ticketCode.value = nextDetail.kuaishouCloudFulfillment.ticket.code + } + await generateQRCode(String(nextDetail.kuaishouCloudFulfillment?.binding.bindUrl || '').trim()) + } + + async function loadDetail(options: { silent?: boolean } = {}) { + if (!options.silent) { + loading.value = true + } + errorMessage.value = '' + try { + const response = await fetchClaimDetail(token()) + await applyDetail(response.data) + syncPolling() + } catch (error) { + errorMessage.value = error instanceof Error ? error.message : '读取领取信息失败' + stopPolling() + } finally { + if (!options.silent) { + loading.value = false + } + } + } + + async function submitTicket() { + const normalizedTicketCode = ticketCode.value.trim() + if (!normalizedTicketCode) { + showError('请输入核销码') + return + } + + submitting.value = true + try { + const response = await verifyKuaishouCloudClaimTicket(token(), { + ticketCode: normalizedTicketCode, + }) + await applyDetail(response.data) + showSuccess('核销码验证通过,系统已开始准备绑定资源') + syncPolling() + } catch (error) { + showError(error instanceof Error ? error.message : '核销码验证失败') + } finally { + submitting.value = false + } + } + + async function refreshRole() { + refreshingRole.value = true + try { + await loadDetail({ silent: true }) + if (isRoleReady.value) { + showSuccess('角色信息已刷新') + } else { + showError( + flow.value?.role.errorMessage || '暂时还没有识别到角色信息,请完成绑定后稍等片刻再试', + ) + } + } finally { + refreshingRole.value = false + } + } + + async function confirmRole() { + confirmingRole.value = true + try { + const response = await confirmKuaishouCloudClaimRole(token()) + await applyDetail(response.data) + showSuccess('角色已确认,进入下一步') + syncPolling() + } catch (error) { + showError(error instanceof Error ? error.message : '确认角色失败') + } finally { + confirmingRole.value = false + } + } + + async function confirmRedeem() { + try { + await ElMessageBox.confirm( + '兑换后不可取消,也不可退货。请确认角色和商品信息完全正确。', + '确认兑换', + { + confirmButtonText: '确认兑换', + cancelButtonText: '取消', + type: 'warning', + center: true, + }, + ) + } catch { + return + } + + redeeming.value = true + try { + const response = await redeemKuaishouCloudClaim(token()) + await applyDetail(response.data) + showSuccess('兑换请求已提交') + syncPolling() + } catch (error) { + showError(error instanceof Error ? error.message : '兑换失败') + } finally { + redeeming.value = false + } + } + + function openBindUrl(useCurrentPage = false) { + const bindUrl = String(flow.value?.binding.bindUrl || '').trim() + if (!bindUrl) { + showError('绑定链接还没准备好,请稍后刷新') + return + } + if (useCurrentPage) { + window.location.assign(bindUrl) + return + } + window.open(bindUrl, '_blank', 'noopener,noreferrer') + } + + // ── polling ───────────────────────────────────────────── + + function stopPolling() { + if (pollTimer) { + window.clearInterval(pollTimer) + pollTimer = 0 + } + } + + function syncPolling() { + const taskStatus = String(task.value?.status || '').trim() + const shouldPoll = + Boolean(flow.value) && !hasRedeemResult.value && !['closed', 'expired'].includes(taskStatus) + + if (!shouldPoll) { + stopPolling() + return + } + if (pollTimer) { + return + } + pollTimer = window.setInterval(() => { + void loadDetail({ silent: true }) + }, 4000) + } + + // ── utility ───────────────────────────────────────────── + + function formatDateTime(value: string | null) { + if (!value) { + return '-' + } + try { + return new Date(value).toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) + } catch { + return value + } + } + + // ── lifecycle ─────────────────────────────────────────── + + onMounted(() => { + void loadDetail() + }) + + onBeforeUnmount(() => { + stopPolling() + }) + + return { + // state + loading, + submitting, + refreshingRole, + confirmingRole, + redeeming, + errorMessage, + detail, + ticketCode, + qrCodeDataUrl, + // derived data + flow, + order, + orderItem, + task, + // computed status + roleName, + roleId, + isTicketVerified, + isBindUrlExpired, + isBindingPrepared, + isBindingPreparing, + canEnterBindingStep, + isRoleReady, + isRoleConfirmed, + isDispatched, + isCompleted, + hasRedeemResult, + canSubmitTicket, + currentStep, + progressText, + resultTitle, + resultDescription, + // actions + loadDetail, + submitTicket, + refreshRole, + confirmRole, + confirmRedeem, + openBindUrl, + // utility + formatDateTime, + } +}