拆分领取页 ClaimPage 为多模块

将 snapshot、轮询、共享 UI、UID/lewan/feifei 步骤组件物理拆分,主文件只保留页面编排与状态。
This commit is contained in:
yml2213
2026-07-10 13:55:43 +08:00
parent 8de56b0354
commit c70c3ff6a5
8 changed files with 968 additions and 940 deletions
@@ -0,0 +1,110 @@
import {
CheckCircleOutlined,
ExclamationCircleOutlined,
LinkOutlined,
} from '@ant-design/icons'
import { Alert, Button, Card, Typography } from 'antd'
import type {
ClaimKuaishouFeifeiFlowInfo,
ClaimOrderInfo,
ClaimProductInfo,
} from '@/types/claim'
import type { ClaimSnapshot } from './claim-snapshot'
import { InfoTile, UidEditRow } from './claim-shared'
export function FeifeiClaimPanel({
feifei,
expectedUid,
order,
product,
taskLastError,
onOpen,
uidInput,
submittingUid,
onUidChange,
onResubmitUid,
}: {
feifei: ClaimKuaishouFeifeiFlowInfo
expectedUid: string
order: ClaimOrderInfo | null
product: ClaimProductInfo | null
taskLastError?: string
onOpen: () => void
uidInput: string
submittingUid: boolean
onUidChange: (value: string) => void
onResubmitUid: () => void
}) {
const openReady = Boolean(feifei.h5UrlWithUid || feifei.h5.rechargeUrl || feifei.h5.entryUrl)
return (
<Card className="claim-content-card">
<div className="claim-feifei-main">
<span className="claim-feifei-label">kuaishou-feifei</span>
<Typography.Title level={2}> 2 </Typography.Title>
<p>{feifei.rechargeStatusLabel || '待领取'}</p>
</div>
<UidEditRow
uidInput={uidInput}
submitting={submittingUid}
onChange={onUidChange}
onSubmit={onResubmitUid}
/>
<div className="claim-info-grid">
<InfoTile label="填写 UID" value={expectedUid || '-'} />
<InfoTile label="订单号" value={order?.platformOrderId || '-'} />
<InfoTile label="平台单号" value={feifei.orderNo || feifei.platformOrderNo || '-'} />
<InfoTile label="领取商品" value={product?.title || '-'} />
</div>
<Alert
type="info"
showIcon
message="将携带你填写的 UID 打开领取页,请确认游戏内角色一致。"
/>
<Button
type="primary"
size="large"
icon={<LinkOutlined />}
disabled={!openReady || !expectedUid}
onClick={onOpen}
block
style={{ marginTop: 16 }}
>
</Button>
{taskLastError ? (
<Alert type="warning" showIcon message={taskLastError} style={{ marginTop: 12 }} />
) : null}
</Card>
)
}
export function FeifeiResultStep({ snapshot }: { snapshot: ClaimSnapshot }) {
return (
<Card className="claim-content-card">
<div className="claim-result-header">
{snapshot.resultVariant === 'warning' ? (
<ExclamationCircleOutlined className="claim-large-icon claim-icon-warning" />
) : (
<CheckCircleOutlined className={`claim-large-icon claim-icon-${snapshot.resultVariant}`} />
)}
<div>
<Typography.Title level={2}>{snapshot.resultTitle}</Typography.Title>
<p className="claim-muted">{snapshot.resultDescription}</p>
</div>
</div>
<div className="claim-info-grid">
<InfoTile label="填写 UID" value={snapshot.expectedUid || '-'} />
<InfoTile label="状态" value={snapshot.feifei?.rechargeStatusLabel || '-'} />
<InfoTile label="订单号" value={snapshot.order?.platformOrderId || '-'} />
<InfoTile label="领取商品" value={snapshot.product?.title || '-'} />
</div>
</Card>
)
}
@@ -0,0 +1,40 @@
import { Card } from 'antd'
import type { ClaimOrderInfo, ClaimProductInfo } from '@/types/claim'
import { ClaimProductItems } from './claim-shared'
export function ClaimHeaderCard({
order,
product,
currentStep,
progressText,
expectedUid,
}: {
order: ClaimOrderInfo | null
product: ClaimProductInfo | null
currentStep: number
progressText: string
expectedUid: string
}) {
return (
<Card className="claim-header-card">
<div className="claim-header-copy">
<span className="eyebrow"></span>
<h1>{product?.title || '商品领取'}</h1>
<p>{order?.platformOrderId || '-'}</p>
{expectedUid ? <p> UID{expectedUid}</p> : null}
<ClaimProductItems product={product} compact />
</div>
<div className="claim-step-indicator">
<div className="claim-step-dots" aria-label={`当前第 ${currentStep}`}>
{[1, 2, 3, 4].map((step) => (
<span key={step} className={currentStep >= step ? 'claim-step-dot active' : 'claim-step-dot'}>
{step}
</span>
))}
</div>
<span className="claim-step-progress-text">{progressText}</span>
</div>
</Card>
)
}
@@ -0,0 +1,328 @@
import {
CheckCircleOutlined,
ExclamationCircleOutlined,
LinkOutlined,
LoadingOutlined,
ReloadOutlined,
SwapOutlined,
} from '@ant-design/icons'
import { Alert, Button, Card, Space, Tooltip, Typography } from 'antd'
import type { ClaimProductInfo } from '@/types/claim'
import { formatAdminDateTime } from '@/utils/admin-time'
import type { ClaimSnapshot } from './claim-snapshot'
import { ClaimProductItems, InfoRow, InfoTile, UidEditRow } from './claim-shared'
export function KuaishouCloudClaimSteps({
snapshot,
qrCodeDataUrl,
refreshingRole,
confirmingRole,
rebindingRole,
redeeming,
onOpenBindUrl,
onRefreshRole,
onRebindRole,
onConfirmRole,
onConfirmRedeem,
uidInput,
submittingUid,
onUidChange,
onResubmitUid,
}: {
snapshot: ClaimSnapshot
qrCodeDataUrl: string
refreshingRole: boolean
confirmingRole: boolean
rebindingRole: boolean
redeeming: boolean
onOpenBindUrl: (useCurrentPage?: boolean) => void
onRefreshRole: () => void
onRebindRole: () => void
onConfirmRole: () => void
onConfirmRedeem: () => void
uidInput: string
submittingUid: boolean
onUidChange: (value: string) => void
onResubmitUid: () => void
}) {
if (!snapshot.flow) {
return null
}
if (snapshot.currentStep === 3) {
return (
<ClaimConfirmStep
roleName={snapshot.roleName}
roleId={snapshot.roleId}
expectedUid={snapshot.expectedUid}
product={snapshot.product}
redeeming={redeeming}
rebindingRole={rebindingRole}
onConfirmRedeem={onConfirmRedeem}
onRebindRole={onRebindRole}
/>
)
}
return (
<ClaimBindingStep
snapshot={snapshot}
qrCodeDataUrl={qrCodeDataUrl}
refreshingRole={refreshingRole}
confirmingRole={confirmingRole}
rebindingRole={rebindingRole}
redeeming={redeeming}
onOpenBindUrl={onOpenBindUrl}
onRefreshRole={onRefreshRole}
onRebindRole={onRebindRole}
onConfirmRole={onConfirmRole}
onConfirmRedeem={onConfirmRedeem}
uidInput={uidInput}
submittingUid={submittingUid}
onUidChange={onUidChange}
onResubmitUid={onResubmitUid}
/>
)
}
function ClaimBindingStep({
snapshot,
qrCodeDataUrl,
refreshingRole,
confirmingRole,
rebindingRole,
redeeming,
onOpenBindUrl,
onRefreshRole,
onRebindRole,
onConfirmRole,
onConfirmRedeem,
uidInput,
submittingUid,
onUidChange,
onResubmitUid,
}: {
snapshot: ClaimSnapshot
qrCodeDataUrl: string
refreshingRole: boolean
confirmingRole: boolean
rebindingRole: boolean
redeeming: boolean
onOpenBindUrl: (useCurrentPage?: boolean) => void
onRefreshRole: () => void
onRebindRole: () => void
onConfirmRole: () => void
onConfirmRedeem: () => void
uidInput: string
submittingUid: boolean
onUidChange: (value: string) => void
onResubmitUid: () => void
}) {
const flow = snapshot.flow
if (!flow) {
return null
}
return (
<Card className="claim-content-card claim-binding-card">
<Typography.Title level={2}> 2 </Typography.Title>
<p className="claim-muted">
使 UID <strong>{snapshot.expectedUid || '-'}</strong>
</p>
<UidEditRow
uidInput={uidInput}
submitting={submittingUid}
onChange={onUidChange}
onSubmit={onResubmitUid}
/>
{snapshot.isBindingPrepared && qrCodeDataUrl ? (
<div className="claim-qr-block">
<div className="claim-qr-guide-panel">
<div className="claim-scan-label claim-scan-label-left">Q区用Q扫</div>
<img src={qrCodeDataUrl} alt="绑定二维码" className="claim-qr-code" />
<div className="claim-scan-label claim-scan-label-right">V区用V扫</div>
<p className="claim-save-hint"></p>
</div>
<Button icon={<LinkOutlined />} onClick={() => onOpenBindUrl(false)}>
</Button>
</div>
) : (
<Alert
type="info"
showIcon
icon={<LoadingOutlined spin />}
message="系统正在准备绑定资源,请稍后自动刷新。"
/>
)}
<div className={snapshot.isUidMatched ? 'claim-role-panel' : 'claim-role-panel pending'}>
<InfoRow label="填写 UID" value={snapshot.expectedUid || '-'} />
<InfoRow label="当前角色" value={snapshot.roleName || '待识别'} />
<InfoRow label="角色 ID" value={snapshot.roleId || '-'} />
<InfoRow label="匹配状态" value={snapshot.isUidMatched ? '已匹配' : '未匹配'} />
</div>
{!snapshot.isUidMatched && snapshot.roleId ? (
<Alert
type="warning"
showIcon
message={`当前绑定角色 ID${snapshot.roleId})与填写 UID${snapshot.expectedUid})不一致,请换绑正确角色。`}
/>
) : null}
<Space size={12} wrap className="claim-action-row">
<Button size="large" icon={<ReloadOutlined />} loading={refreshingRole} onClick={onRefreshRole}>
</Button>
<Button
size="large"
icon={<SwapOutlined />}
loading={rebindingRole}
disabled={confirmingRole || redeeming}
onClick={onRebindRole}
>
</Button>
{snapshot.isUidMatched ? (
<Button
type="primary"
size="large"
danger
icon={<CheckCircleOutlined />}
loading={redeeming}
disabled={rebindingRole}
onClick={onConfirmRedeem}
>
UID
</Button>
) : (
<Tooltip title={snapshot.confirmRoleDisabledReason || undefined}>
<span className="claim-action-tooltip-wrap">
<Button
type="primary"
size="large"
icon={<CheckCircleOutlined />}
loading={confirmingRole}
disabled={Boolean(snapshot.confirmRoleDisabledReason)}
onClick={onConfirmRole}
>
</Button>
</span>
</Tooltip>
)}
</Space>
<div className="claim-meta-footer">
<span>{formatAdminDateTime(flow.role.refreshedAt)}</span>
<span>{formatAdminDateTime(flow.binding.bindExpiresAt)}</span>
</div>
</Card>
)
}
function ClaimConfirmStep({
roleName,
roleId,
expectedUid,
product,
redeeming,
rebindingRole,
onConfirmRedeem,
onRebindRole,
}: {
roleName: string
roleId: string
expectedUid: string
product: ClaimProductInfo | null
redeeming: boolean
rebindingRole: boolean
onConfirmRedeem: () => void
onRebindRole: () => void
}) {
return (
<Card className="claim-content-card">
<Typography.Title level={2}> 3 </Typography.Title>
<p className="claim-muted"> UID </p>
<div className="claim-info-grid">
<InfoTile label="填写 UID" value={expectedUid || '-'} />
<InfoTile label="角色名称" value={roleName || '-'} />
<InfoTile label="角色 ID" value={roleId || '-'} />
<InfoTile label="领取商品" value={product?.title || '-'} />
<InfoTile label="商品数量" value={String(product?.quantity || 0)} />
</div>
<ClaimProductItems product={product} />
<Alert type="warning" showIcon message="兑换后不可取消,也不可退货。" />
<Space size={12} wrap className="claim-action-row">
<Button
size="large"
icon={<SwapOutlined />}
loading={rebindingRole}
disabled={redeeming}
onClick={onRebindRole}
>
</Button>
<Button
type="primary"
size="large"
icon={<CheckCircleOutlined />}
loading={redeeming}
disabled={rebindingRole}
onClick={onConfirmRedeem}
>
</Button>
</Space>
</Card>
)
}
export function ClaimResultStep({ snapshot }: { snapshot: ClaimSnapshot }) {
const flow = snapshot.flow
return (
<Card className="claim-content-card">
<div className="claim-result-header">
{snapshot.resultVariant === 'warning' ? (
<ExclamationCircleOutlined className="claim-large-icon claim-icon-warning" />
) : (
<CheckCircleOutlined className={`claim-large-icon claim-icon-${snapshot.resultVariant}`} />
)}
<div>
<Typography.Title level={2}>{snapshot.resultTitle}</Typography.Title>
<p className="claim-muted">{snapshot.resultDescription}</p>
</div>
</div>
<div className="claim-info-grid">
<InfoTile label="填写 UID" value={snapshot.expectedUid || '-'} />
<InfoTile label="结果时间" value={formatAdminDateTime(flow?.dispatch.dispatchAt || null)} />
<InfoTile label="领取商品" value={snapshot.product?.title || '-'} />
<InfoTile label="角色名称" value={snapshot.roleName || '-'} />
<InfoTile label="角色 ID" value={snapshot.roleId || '-'} />
<InfoTile label="订单号" value={snapshot.order?.platformOrderId || '-'} />
</div>
<ClaimProductItems product={snapshot.product} />
<Alert
type={snapshot.resultVariant}
showIcon
message={
snapshot.resultVariant === 'warning'
? '当前兑换未完成,客服会根据后台任务记录继续处理。'
: '客户侧操作已经完成,后续履约结果请以后台任务界面为准。'
}
/>
</Card>
)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,54 @@
import { Alert, Button, Card, Input, Space, Typography } from 'antd'
import type { ClaimProductInfo } from '@/types/claim'
import { ClaimProductItems, InfoTile } from './claim-shared'
export function ClaimUidStep({
uidInput,
submitting,
onChange,
onSubmit,
product,
}: {
uidInput: string
submitting: boolean
onChange: (value: string) => void
onSubmit: () => void
product: ClaimProductInfo | null
}) {
return (
<Card className="claim-content-card">
<Typography.Title level={2}> 1 UID</Typography.Title>
<p className="claim-muted">
UID
</p>
<div className="claim-info-grid">
<InfoTile label="领取商品" value={product?.title || '-'} />
<InfoTile label="商品数量" value={String(product?.quantity || 0)} />
</div>
<ClaimProductItems product={product} />
<Space direction="vertical" size={12} style={{ width: '100%' }}>
<Input
size="large"
placeholder="请输入游戏 UID"
value={uidInput}
maxLength={64}
onChange={(event) => onChange(event.target.value)}
onPressEnter={onSubmit}
/>
<Button type="primary" size="large" loading={submitting} onClick={onSubmit} block>
UID
</Button>
</Space>
<Alert
type="info"
showIcon
style={{ marginTop: 16 }}
message="UID 需与游戏内角色 ID 一致,填错将导致无法发货。"
/>
</Card>
)
}
@@ -0,0 +1,68 @@
import { isClaimInactiveTaskStatus } from '@/domain/task-status'
import type { ClaimSnapshot } from './claim-snapshot'
export const BINDING_PREPARE_POLL_MS = 5_000
export const ROLE_FAST_POLL_MS = 10_000
export const ROLE_SLOW_POLL_MS = 30_000
export const ROLE_IDLE_POLL_MS = 60_000
export const ROLE_FAST_WINDOW_MS = 3 * 60_000
export const ROLE_IDLE_WINDOW_MS = 10 * 60_000
export const FEIFEI_POLL_MS = 15_000
export function resolveRolePollDelayMs(
nextSnapshot: ClaimSnapshot,
baseline: { roleKey: string; at: number },
now = Date.now(),
) {
const nextRoleKey = [nextSnapshot.roleName, nextSnapshot.roleId].join('::')
let baselineAt = baseline.at
let baselineKey = baseline.roleKey
if (nextRoleKey !== baselineKey) {
baselineKey = nextRoleKey
baselineAt = now
}
if (!baselineAt) {
baselineAt = now
}
const elapsedMs = now - baselineAt
let delay = ROLE_IDLE_POLL_MS
if (elapsedMs < ROLE_FAST_WINDOW_MS) {
delay = ROLE_FAST_POLL_MS
} else if (elapsedMs < ROLE_IDLE_WINDOW_MS) {
delay = ROLE_SLOW_POLL_MS
}
return {
delay,
baseline: { roleKey: baselineKey, at: baselineAt },
}
}
export function resolveNextPollDelayMs(
nextSnapshot: ClaimSnapshot,
rolePollDelayMs: number,
): number {
if (nextSnapshot.hasRedeemResult || isClaimInactiveTaskStatus(nextSnapshot.task?.status)) {
return 0
}
if (!nextSnapshot.hasExpectedUid) {
return 0
}
if (nextSnapshot.isFeifeiFlow) {
return FEIFEI_POLL_MS
}
if (nextSnapshot.currentStep === 2) {
if (!nextSnapshot.isBindingPrepared) {
return BINDING_PREPARE_POLL_MS
}
return rolePollDelayMs
}
return 0
}
@@ -0,0 +1,92 @@
import { Button, Input, Space } from 'antd'
import type { ClaimProductInfo } from '@/types/claim'
export function ClaimProductItems({
product,
compact = false,
}: {
product: ClaimProductInfo | null
compact?: boolean
}) {
const items = getProductItems(product)
const hasMultipleItems = Boolean(product?.isBundle || items.length > 1)
if (!items.length) {
return null
}
return (
<div className={compact ? 'claim-product-items compact' : 'claim-product-items'}>
<div className="claim-product-items-header">
<span>{hasMultipleItems ? '包含商品' : '商品明细'}</span>
{hasMultipleItems ? <strong>{items.length} </strong> : null}
</div>
<div className="claim-product-items-list">
{items.map((item) => (
<div key={item.key} className="claim-product-item-row">
<span>{item.name}</span>
<strong>x{item.quantity}</strong>
</div>
))}
</div>
</div>
)
}
export function UidEditRow({
uidInput,
submitting,
onChange,
onSubmit,
}: {
uidInput: string
submitting: boolean
onChange: (value: string) => void
onSubmit: () => void
}) {
return (
<Space.Compact style={{ width: '100%', marginBottom: 16 }}>
<Input
size="large"
value={uidInput}
maxLength={64}
placeholder="游戏 UID"
onChange={(event) => onChange(event.target.value)}
onPressEnter={onSubmit}
/>
<Button size="large" loading={submitting} onClick={onSubmit}>
UID
</Button>
</Space.Compact>
)
}
export function InfoTile({ label, value }: { label: string; value: string }) {
return (
<div className="claim-info-item">
<span>{label}</span>
<strong>{value}</strong>
</div>
)
}
export function InfoRow({ label, value }: { label: string; value: string }) {
return (
<div className="claim-role-panel-item">
<span>{label}</span>
<strong>{value}</strong>
</div>
)
}
function getProductItems(product: ClaimProductInfo | null) {
const items = Array.isArray(product?.items) ? product.items : []
return items
.map((item) => ({
key: item.cloudSkuId || item.name,
name: String(item.name || '').trim(),
quantity: Math.max(1, Number(item.quantity || 1) || 1),
}))
.filter((item) => item.name)
}
@@ -0,0 +1,260 @@
import {
TASK_STATUS,
hasKuaishouCloudRedeemResultStatus,
isKuaishouCloudCompletedStatus,
isKuaishouCloudRoleConfirmedStatus,
normalizeTaskStatus,
} from '@/domain/task-status'
import type {
ClaimDetailData,
ClaimKuaishouCloudFlowInfo,
ClaimKuaishouFeifeiFlowInfo,
} from '@/types/claim'
export type ResultVariant = 'success' | 'warning' | 'info'
export type ClaimSnapshot = ReturnType<typeof createClaimSnapshot>
export function createClaimSnapshot(
detail: ClaimDetailData | null,
options: { rebindingRole?: boolean } = {},
) {
const flow = detail?.kuaishouCloudFulfillment || null
const feifei = detail?.kuaishouFeifei || null
const order = detail?.order || null
const orderItem = detail?.orderItem || null
const product = detail?.product || null
const task = detail?.task || null
const claimIdentity = detail?.claimIdentity || null
const expectedUid = String(claimIdentity?.expectedUid || '').trim()
const hasExpectedUid = Boolean(expectedUid || claimIdentity?.ready)
const roleName = flow?.role.name || flow?.binding.roleName || ''
const roleId = flow?.role.rid || flow?.binding.roleId || ''
const isFeifeiFlow = detail?.flowType === 'kuaishou_feifei'
const isBindUrlExpired = isDateExpired(flow?.binding.bindExpiresAt || '')
const isBindingPrepared =
flow?.binding.prepareStatus === 'ready' &&
Boolean(String(flow?.binding.bindUrl || '').trim()) &&
!isBindUrlExpired
const isBindingPreparing = flow?.binding.prepareStatus === 'pending'
const isRoleReady = Boolean(roleName || roleId)
const isUidMatched =
Boolean(expectedUid) && Boolean(roleId) && normalizeUid(expectedUid) === normalizeUid(roleId)
const confirmRoleDisabledReason = resolveConfirmRoleDisabledReason({
isBindingPrepared,
isRoleReady,
isUidMatched,
expectedUid,
roleId,
rebindingRole: Boolean(options.rebindingRole),
})
const isRoleConfirmed = isKuaishouCloudRoleConfirmedStatus(task?.status)
const isDispatched = String(flow?.dispatch.status || '').trim() === 'success'
const normalizedStatus = normalizeTaskStatus(task?.status)
const isRedeemFailed =
normalizedStatus === TASK_STATUS.MANUAL_REVIEW ||
normalizedStatus === TASK_STATUS.FAILED ||
String(flow?.dispatch.status || '').trim() === 'failed' ||
(isFeifeiFlow && [40, 50].includes(Number(feifei?.rechargeStatus || 0)))
const isCompleted =
isKuaishouCloudCompletedStatus(task?.status) ||
(isFeifeiFlow && Number(feifei?.rechargeStatus || 0) === 30)
const hasRedeemResult =
isDispatched ||
hasKuaishouCloudRedeemResultStatus(task?.status) ||
(isFeifeiFlow && [30, 40, 50, 60].includes(Number(feifei?.rechargeStatus || 0)))
const currentStep = resolveCurrentStep({
hasExpectedUid,
hasRedeemResult,
isRoleConfirmed,
isFeifeiFlow,
isUidMatched,
})
const progressText = resolveProgressText({
feifei,
isFeifeiFlow,
hasExpectedUid,
expectedUid,
hasRedeemResult,
isRoleConfirmed,
isBindingPrepared,
isUidMatched,
})
const resultTitle = resolveResultTitle({ isRedeemFailed, isCompleted, isDispatched })
const resultDescription = resolveResultDescription({
detail,
flow,
feifei,
task,
isRedeemFailed,
isCompleted,
})
const resultVariant: ResultVariant = isRedeemFailed
? 'warning'
: isCompleted || isDispatched
? 'success'
: 'info'
return {
flow,
feifei,
order,
orderItem,
product,
task,
claimIdentity,
expectedUid,
hasExpectedUid,
roleName,
roleId,
isFeifeiFlow,
isBindUrlExpired,
isBindingPrepared,
isBindingPreparing,
isRoleReady,
isUidMatched,
confirmRoleDisabledReason,
isRoleConfirmed,
isDispatched,
isRedeemFailed,
isCompleted,
hasRedeemResult,
currentStep,
progressText,
resultTitle,
resultDescription,
resultVariant,
}
}
export function normalizeUid(value: unknown) {
return String(value || '')
.trim()
.replace(/\s+/g, '')
}
function resolveConfirmRoleDisabledReason(options: {
isBindingPrepared: boolean
isRoleReady: boolean
isUidMatched: boolean
expectedUid: string
roleId: string
rebindingRole: boolean
}) {
if (!options.isBindingPrepared) {
return '绑定二维码还在准备中,请稍后自动刷新'
}
if (!options.isRoleReady) {
return '请先扫码绑定自己的角色,并刷新角色信息'
}
if (!options.isUidMatched) {
return `当前绑定角色 ID${options.roleId || '-'})与填写 UID${options.expectedUid || '-'})不一致`
}
if (options.rebindingRole) {
return '正在换绑角色,请稍候'
}
return ''
}
function resolveCurrentStep(options: {
hasExpectedUid: boolean
hasRedeemResult: boolean
isRoleConfirmed: boolean
isFeifeiFlow: boolean
isUidMatched?: boolean
}) {
if (!options.hasExpectedUid) {
return 1
}
if (options.hasRedeemResult) {
return 4
}
if (!options.isFeifeiFlow && options.isRoleConfirmed) {
return 3
}
return 2
}
function resolveProgressText(options: {
feifei: ClaimKuaishouFeifeiFlowInfo | null
isFeifeiFlow: boolean
hasExpectedUid: boolean
expectedUid: string
hasRedeemResult: boolean
isRoleConfirmed: boolean
isBindingPrepared: boolean
isUidMatched: boolean
}) {
if (!options.hasExpectedUid) {
return '请填写游戏 UID'
}
if (options.isFeifeiFlow) {
return options.feifei?.rechargeStatusLabel || `UID ${options.expectedUid},请打开领取链接`
}
if (options.hasRedeemResult) {
return '兑换结果已生成'
}
if (options.isRoleConfirmed) {
return '角色已确认,等待兑换'
}
if (!options.isBindingPrepared) {
return '绑定链接准备中,请稍候'
}
if (options.isUidMatched) {
return 'UID 已匹配,请确认兑换'
}
return '请完成扫码绑定并匹配 UID'
}
function resolveResultTitle(options: {
isRedeemFailed: boolean
isCompleted: boolean
isDispatched: boolean
}) {
if (options.isRedeemFailed) {
return '兑换遇到问题'
}
if (options.isCompleted) {
return '兑换成功'
}
if (options.isDispatched) {
return '兑换请求已提交'
}
return '结果已记录'
}
function resolveResultDescription(options: {
detail: ClaimDetailData | null
flow: ClaimKuaishouCloudFlowInfo | null
feifei: ClaimKuaishouFeifeiFlowInfo | null
task: ClaimDetailData['task'] | null
isRedeemFailed: boolean
isCompleted: boolean
}) {
if (options.isRedeemFailed) {
const message = String(
options.task?.lastError ||
options.flow?.dispatch.errorMessage ||
options.feifei?.rechargeResultMessage ||
options.detail?.result?.resultMessage ||
'',
).trim()
return message || '当前兑换流程需要客服处理,后续结果请以后台任务界面为准。'
}
if (options.isCompleted) {
return '当前兑换流程已经完成。'
}
return '你的兑换请求已经提交。后续结果会由系统保存在后台任务界面。'
}
function isDateExpired(value: string | null) {
const normalized = String(value || '').trim()
if (!normalized) {
return false
}
const expiresTime = Date.parse(normalized)
return Number.isFinite(expiresTime) && expiresTime <= Date.now()
}