拆分领取页 ClaimPage 为多模块
将 snapshot、轮询、共享 UI、UID/lewan/feifei 步骤组件物理拆分,主文件只保留页面编排与状态。
This commit is contained in:
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user