Files
order_site/apps/frontend/src/pages/claim/ClaimPage.tsx
T
yml2213andCursor 8e0cb7e295 fix(claim): 领取页展示游戏角色名,并说明乐玩免扫码场景
持久化 affiliate_dash roleName/gameChannel;免扫码授权时明确提示,有绑定链接时提交前仍可显示二维码。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 12:39:47 +08:00

269 lines
8.2 KiB
TypeScript

import { ExclamationCircleOutlined, LoadingOutlined } from '@ant-design/icons'
import { Card, Empty, Spin } from 'antd'
import QRCode from 'qrcode'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useParams } from 'react-router'
import { showError, showSuccess } from '@/lib/feedback'
import { fetchClaimDetail, submitAffiliateDashClaim, submitClaimUid } from '@/services/claim'
import type { ClaimDetailData } from '@/types/claim'
import { AffiliateDashClaimPanel, AffiliateDashResultStep } from './ClaimAffiliateDashSteps'
import { ClaimHeaderCard } from './ClaimHeaderCard'
import { ClaimUidStep } from './ClaimUidStep'
import { resolveNextPollDelayMs, resolveRolePollDelayMs } from './claim-poll'
import { createClaimSnapshot, type ClaimSnapshot } from './claim-snapshot'
export default function ClaimPage() {
const { token: routeToken } = useParams<{ token: string }>()
const token = String(routeToken || '').trim()
const [loading, setLoading] = useState(true)
const [submittingUid, setSubmittingUid] = useState(false)
const [uidInput, setUidInput] = useState('')
const [submittingSubmit, setSubmittingSubmit] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const [detail, setDetail] = useState<ClaimDetailData | null>(null)
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('')
const pollTimerRef = useRef(0)
const rolePollBaselineRef = useRef({ roleKey: '', at: 0 })
const snapshot = useMemo(() => createClaimSnapshot(detail), [detail])
const stopPolling = useCallback(() => {
if (pollTimerRef.current) {
window.clearTimeout(pollTimerRef.current)
pollTimerRef.current = 0
}
}, [])
const generateQRCode = useCallback(async (url: string) => {
if (!url) {
setQrCodeDataUrl('')
return
}
try {
const dataUrl = await QRCode.toDataURL(url, {
width: 280,
margin: 2,
color: {
dark: '#0f172a',
light: '#ffffff',
},
})
setQrCodeDataUrl(dataUrl)
} catch (error) {
setQrCodeDataUrl('')
console.error('生成二维码失败:', error)
}
}, [])
const applyDetail = useCallback(
async (nextDetail: ClaimDetailData) => {
setDetail(nextDetail)
const expectedUid = String(nextDetail.claimIdentity?.expectedUid || '').trim()
if (expectedUid) {
setUidInput(expectedUid)
}
const bindUrl = String(nextDetail.affiliateDash?.bindUrl || '').trim()
const qrUrl = String(nextDetail.affiliateDash?.qrUrl || '').trim()
if (bindUrl) {
await generateQRCode(bindUrl)
} else if (qrUrl) {
setQrCodeDataUrl(qrUrl)
} else {
setQrCodeDataUrl('')
}
},
[generateQRCode],
)
const loadDetail = useCallback(
async (options: { silent?: boolean } = {}) => {
if (!token) {
setLoading(false)
setErrorMessage('领取链接无效,请检查链接是否完整')
stopPolling()
return null
}
if (!options.silent) {
setLoading(true)
}
setErrorMessage('')
try {
const response = await fetchClaimDetail(token)
await applyDetail(response.data)
return response.data
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : '读取领取信息失败')
stopPolling()
return null
} finally {
if (!options.silent) {
setLoading(false)
}
}
},
[applyDetail, stopPolling, token],
)
const resolveNextPollDelay = useCallback((nextSnapshot: ClaimSnapshot) => {
const rolePoll = resolveRolePollDelayMs(nextSnapshot, rolePollBaselineRef.current)
rolePollBaselineRef.current = rolePoll.baseline
return resolveNextPollDelayMs(nextSnapshot, rolePoll.delay)
}, [])
useEffect(() => {
rolePollBaselineRef.current = { roleKey: '', at: 0 }
setDetail(null)
setQrCodeDataUrl('')
setUidInput('')
void loadDetail()
return () => {
stopPolling()
}
}, [loadDetail, stopPolling])
useEffect(() => {
stopPolling()
if (snapshot.channelOffline) {
return undefined
}
const delay = resolveNextPollDelay(snapshot)
if (!delay) {
return undefined
}
pollTimerRef.current = window.setTimeout(() => {
pollTimerRef.current = 0
void loadDetail({ silent: true })
}, delay)
return () => {
stopPolling()
}
}, [loadDetail, resolveNextPollDelay, snapshot, stopPolling])
async function handleSubmitUid() {
if (submittingUid) {
return
}
const uid = uidInput.trim()
if (!uid) {
showError('请输入游戏 UID')
return
}
setSubmittingUid(true)
try {
const response = await submitClaimUid(token, uid)
await applyDetail(response.data)
showSuccess('UID 已提交')
} catch (error) {
showError(error instanceof Error ? error.message : '提交 UID 失败')
} finally {
setSubmittingUid(false)
}
}
async function handleSubmitAffiliateDash() {
const affiliateDash = snapshot.affiliateDash
const uid =
snapshot.expectedUid || affiliateDash?.expectedGameAccount || affiliateDash?.gameAccount || ''
const bindUuid = affiliateDash?.bindUuid || ''
if (!snapshot.isAffiliateDashReadyToSubmit) {
showError(
snapshot.isAffiliateDashBindMismatch
? '绑定账号与填写 UID 不一致,请用正确账号重新扫码绑定'
: '绑定尚未完成,请先绑定账号',
)
return
}
if (!uid || !bindUuid) {
showError('绑定尚未完成,请先绑定账号')
return
}
setSubmittingSubmit(true)
try {
const response = await submitAffiliateDashClaim(token, {
gameAccount: uid,
bindUuid,
})
await applyDetail(response.data)
showSuccess('发货已提交')
} catch (error) {
showError(error instanceof Error ? error.message : '提交发货失败')
} finally {
setSubmittingSubmit(false)
}
}
return (
<main className="claim-page">
<ClaimHeaderCard
order={snapshot.order}
product={snapshot.product}
currentStep={snapshot.currentStep}
progressText={snapshot.progressText}
expectedUid={snapshot.expectedUid}
/>
{loading ? (
<Card className="claim-content-card claim-state-card">
<Spin indicator={<LoadingOutlined spin />} size="large" />
<p>正在读取当前领取进度...</p>
</Card>
) : errorMessage ? (
<Card className="claim-content-card claim-state-card">
<ExclamationCircleOutlined className="claim-large-icon claim-icon-error" />
<p>{errorMessage}</p>
</Card>
) : !detail ? (
<Card className="claim-content-card claim-state-card">
<Empty description="流程数据不完整,请联系客服处理" />
</Card>
) : snapshot.channelOffline ? (
<Card className="claim-content-card claim-state-card">
<ExclamationCircleOutlined className="claim-large-icon claim-icon-error" />
<p>{snapshot.channelOfflineMessage || '该履约渠道已下线,无法继续领取'}</p>
</Card>
) : snapshot.currentStep === 1 ? (
<ClaimUidStep
uidInput={uidInput}
submitting={submittingUid}
onChange={setUidInput}
onSubmit={() => void handleSubmitUid()}
/>
) : snapshot.hasRedeemResult && snapshot.isAffiliateDashFlow ? (
<AffiliateDashResultStep snapshot={snapshot} />
) : snapshot.isAffiliateDashFlow && snapshot.affiliateDash ? (
<AffiliateDashClaimPanel
affiliateDash={snapshot.affiliateDash}
expectedUid={snapshot.expectedUid}
taskLastError={snapshot.task?.lastError}
qrCodeDataUrl={qrCodeDataUrl}
submittingUid={submittingUid}
submittingSubmit={submittingSubmit}
uidInput={uidInput}
onUidChange={setUidInput}
onResubmitUid={() => void handleSubmitUid()}
onSubmit={() => void handleSubmitAffiliateDash()}
/>
) : (
<Card className="claim-content-card claim-state-card">
<Empty description="当前领取流程不可用,请联系客服处理" />
</Card>
)}
</main>
)
}