持久化 affiliate_dash roleName/gameChannel;免扫码授权时明确提示,有绑定链接时提交前仍可显示二维码。 Co-authored-by: Cursor <cursoragent@cursor.com>
269 lines
8.2 KiB
TypeScript
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>
|
|
)
|
|
}
|