262 lines
9.3 KiB
TypeScript
262 lines
9.3 KiB
TypeScript
import { useEffect, useMemo, useState, type CSSProperties, type ReactNode } from 'react'
|
|
import { useParams, useSearchParams } from 'react-router-dom'
|
|
import {
|
|
Alert,
|
|
Button,
|
|
Descriptions,
|
|
Form,
|
|
Image,
|
|
Input,
|
|
Result,
|
|
Space,
|
|
Spin,
|
|
Steps,
|
|
Tag,
|
|
Typography,
|
|
message,
|
|
} from 'antd'
|
|
import {
|
|
CheckCircleOutlined,
|
|
ClockCircleOutlined,
|
|
QrcodeOutlined,
|
|
SendOutlined,
|
|
} from '@ant-design/icons'
|
|
import { deliveryApi } from '../api'
|
|
import type { DeliveryBindResult, DeliveryOrderInfo, DeliverySubmitResult } from '../types'
|
|
import { formatDateTime } from '../utils/time'
|
|
|
|
const statusMap: Record<string, { color: string; text: string }> = {
|
|
paid: { color: 'orange', text: '待发货' },
|
|
delivering: { color: 'blue', text: '履约中' },
|
|
delivered: { color: 'green', text: '已交付' },
|
|
ship_failed: { color: 'red', text: '发货失败' },
|
|
cancelled: { color: 'default', text: '已取消' },
|
|
}
|
|
|
|
export default function Delivery() {
|
|
const { channel = 'dlc', orderNo = '' } = useParams()
|
|
const [searchParams] = useSearchParams()
|
|
const [form] = Form.useForm<{ game_account: string }>()
|
|
const [order, setOrder] = useState<DeliveryOrderInfo | null>(null)
|
|
const [bindResult, setBindResult] = useState<DeliveryBindResult | null>(null)
|
|
const [submitResult, setSubmitResult] = useState<DeliverySubmitResult | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [binding, setBinding] = useState(false)
|
|
const [submitting, setSubmitting] = useState(false)
|
|
const [error, setError] = useState('')
|
|
|
|
const decodedOrderNo = useMemo(() => decodeURIComponent(orderNo), [orderNo])
|
|
const deliveryAuth = useMemo(() => {
|
|
const exp = searchParams.get('exp') || ''
|
|
const sign = searchParams.get('sign') || ''
|
|
return exp && sign ? { exp, sign } : undefined
|
|
}, [searchParams])
|
|
const gameAccount = Form.useWatch('game_account', form)
|
|
|
|
useEffect(() => {
|
|
if (!decodedOrderNo) {
|
|
setError('发货链接缺少订单号')
|
|
setLoading(false)
|
|
return
|
|
}
|
|
setLoading(true)
|
|
deliveryApi.getOrder(decodedOrderNo, deliveryAuth)
|
|
.then((data) => {
|
|
setOrder(data)
|
|
setError('')
|
|
})
|
|
.catch((e) => setError(e instanceof Error ? e.message : '订单加载失败'))
|
|
.finally(() => setLoading(false))
|
|
}, [decodedOrderNo, deliveryAuth])
|
|
|
|
const bindAccount = async () => {
|
|
const values = await form.validateFields()
|
|
setBinding(true)
|
|
setSubmitResult(null)
|
|
try {
|
|
const result = await deliveryApi.bind(decodedOrderNo, values.game_account, deliveryAuth)
|
|
setBindResult(result)
|
|
message.success('绑定二维码已生成')
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : '生成二维码失败')
|
|
} finally {
|
|
setBinding(false)
|
|
}
|
|
}
|
|
|
|
const submitDelivery = async () => {
|
|
const values = await form.validateFields()
|
|
if (!bindResult?.bind_uuid) {
|
|
message.warning('请先生成绑定二维码')
|
|
return
|
|
}
|
|
setSubmitting(true)
|
|
try {
|
|
const result = await deliveryApi.submit(decodedOrderNo, values.game_account, bindResult.bind_uuid, deliveryAuth)
|
|
setSubmitResult(result)
|
|
setOrder((prev) => prev ? { ...prev, status: result.status, can_ship: false } : prev)
|
|
message.success(result.message || '已提交')
|
|
} catch (e) {
|
|
message.error(e instanceof Error ? e.message : '提交失败')
|
|
} finally {
|
|
setSubmitting(false)
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<CenteredShell>
|
|
<Spin size="large" />
|
|
</CenteredShell>
|
|
)
|
|
}
|
|
|
|
if (error || !order) {
|
|
return (
|
|
<CenteredShell>
|
|
<Result status="warning" title="订单不可发货" subTitle={error || '订单不存在'} />
|
|
</CenteredShell>
|
|
)
|
|
}
|
|
|
|
const status = statusMap[order.status] || { color: 'default', text: order.status }
|
|
const step = submitResult ? 2 : bindResult ? 1 : 0
|
|
|
|
return (
|
|
<main style={{ minHeight: '100vh', background: '#f5f7fb', padding: '32px 16px' }}>
|
|
<div style={{ maxWidth: 880, margin: '0 auto' }}>
|
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
|
<header>
|
|
<Typography.Title level={3} style={{ marginBottom: 4 }}>
|
|
游戏道具发货
|
|
</Typography.Title>
|
|
<Typography.Text type="secondary">
|
|
{channel.toUpperCase()} · {decodedOrderNo}
|
|
</Typography.Text>
|
|
</header>
|
|
|
|
<section style={panelStyle}>
|
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
|
<Space align="start" style={{ width: '100%', justifyContent: 'space-between' }}>
|
|
<div>
|
|
<Typography.Title level={4} style={{ marginTop: 0, marginBottom: 8 }}>
|
|
{order.product?.name || '待发货商品'}
|
|
</Typography.Title>
|
|
<Typography.Text code>{order.product?.sku || '-'}</Typography.Text>
|
|
</div>
|
|
<Tag color={status.color}>{status.text}</Tag>
|
|
</Space>
|
|
|
|
<Descriptions column={{ xs: 1, sm: 2 }} size="small" bordered>
|
|
<Descriptions.Item label="买家">{order.buyer_name || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label="金额">{order.amount} 积分</Descriptions.Item>
|
|
<Descriptions.Item label="创建时间">{formatDateTime(order.created_at)}</Descriptions.Item>
|
|
<Descriptions.Item label="渠道">{order.product?.game || channel.toUpperCase()}</Descriptions.Item>
|
|
</Descriptions>
|
|
|
|
{order.ship_fail_reason && (
|
|
<Alert type="error" showIcon message={order.ship_fail_reason} />
|
|
)}
|
|
{!order.can_ship && (
|
|
<Alert type="warning" showIcon message={order.cannot_ship_reason || '当前订单暂不可发货'} />
|
|
)}
|
|
</Space>
|
|
</section>
|
|
|
|
{order.can_ship && (
|
|
<section style={panelStyle}>
|
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
|
<Steps
|
|
current={step}
|
|
items={[
|
|
{ title: '填写 UID', icon: <ClockCircleOutlined /> },
|
|
{ title: '扫码绑定', icon: <QrcodeOutlined /> },
|
|
{ title: '提交发货', icon: <CheckCircleOutlined /> },
|
|
]}
|
|
/>
|
|
|
|
<Form form={form} layout="vertical" initialValues={{ game_account: order.game_uid || '' }}>
|
|
<Form.Item
|
|
name="game_account"
|
|
label="游戏 UID"
|
|
rules={[
|
|
{ required: true, message: '请输入游戏 UID' },
|
|
{ pattern: /^\d+$/, message: '游戏 UID 仅支持数字' },
|
|
]}
|
|
>
|
|
<Input size="large" inputMode="numeric" placeholder="请输入游戏 UID" />
|
|
</Form.Item>
|
|
</Form>
|
|
|
|
<Space wrap>
|
|
<Button
|
|
type="primary"
|
|
icon={<QrcodeOutlined />}
|
|
loading={binding}
|
|
onClick={bindAccount}
|
|
>
|
|
生成绑定二维码
|
|
</Button>
|
|
<Button
|
|
icon={<SendOutlined />}
|
|
loading={submitting}
|
|
disabled={!bindResult || !gameAccount}
|
|
onClick={submitDelivery}
|
|
>
|
|
我已扫码,提交发货
|
|
</Button>
|
|
</Space>
|
|
|
|
{bindResult && (
|
|
<div style={{ display: 'flex', gap: 24, flexWrap: 'wrap', alignItems: 'center' }}>
|
|
<Image
|
|
src={bindResult.qr_url}
|
|
width={220}
|
|
height={220}
|
|
alt="绑定二维码"
|
|
style={{ background: '#fff', border: '1px solid #e5e7eb' }}
|
|
/>
|
|
<Space direction="vertical" size="small" style={{ flex: 1, minWidth: 260 }}>
|
|
<Typography.Text strong>绑定凭证</Typography.Text>
|
|
<Typography.Text code copyable={{ text: bindResult.bind_uuid }}>
|
|
{bindResult.bind_uuid}
|
|
</Typography.Text>
|
|
<Typography.Link href={bindResult.bind_url} target="_blank" rel="noreferrer">
|
|
打开绑定链接
|
|
</Typography.Link>
|
|
</Space>
|
|
</div>
|
|
)}
|
|
|
|
{submitResult && (
|
|
<Alert
|
|
type="success"
|
|
showIcon
|
|
message={submitResult.message}
|
|
description={submitResult.provider_order_no ? `上游单号:${submitResult.provider_order_no}` : undefined}
|
|
/>
|
|
)}
|
|
</Space>
|
|
</section>
|
|
)}
|
|
</Space>
|
|
</div>
|
|
</main>
|
|
)
|
|
}
|
|
|
|
function CenteredShell({ children }: { children: ReactNode }) {
|
|
return (
|
|
<main style={{ minHeight: '100vh', display: 'grid', placeItems: 'center', background: '#f5f7fb', padding: 24 }}>
|
|
{children}
|
|
</main>
|
|
)
|
|
}
|
|
|
|
const panelStyle: CSSProperties = {
|
|
background: '#fff',
|
|
border: '1px solid #e5e7eb',
|
|
borderRadius: 8,
|
|
padding: 24,
|
|
}
|