import { useCallback, useEffect, useMemo, useState, type CSSProperties, type ReactNode } from 'react' import { useParams, useSearchParams } from 'react-router-dom' import { Alert, Button, Descriptions, Form, Image, Input, Modal, Result, Space, Spin, Steps, Tag, Typography, message, } from 'antd' import { CheckCircleOutlined, ClockCircleOutlined, CopyOutlined, DownloadOutlined, EyeOutlined, LoadingOutlined, QrcodeOutlined, ReloadOutlined, SafetyCertificateOutlined, SendOutlined, SyncOutlined, } from '@ant-design/icons' import { deliveryApi } from '../api' import type { DeliveryBindResult, DeliveryOrderInfo, DeliverySubmitResult } from '../types' import { formatDateTime } from '../utils/time' import { generateDeliveryVoucher } from '../utils/voucher' import { resolveProductImage } from '../utils/productImage' const statusMap: Record = { paid: { color: 'orange', text: '待发货' }, delivering: { color: 'blue', text: '发货中' }, delivered: { color: 'green', text: '已交付' }, ship_failed: { color: 'red', text: '发货失败' }, cancelled: { color: 'default', text: '已取消' }, } function dataURLtoBlob(dataurl: string): Blob { const arr = dataurl.split(',') const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/png' const bstr = atob(arr[1]) let n = bstr.length const u8arr = new Uint8Array(n) while (n--) { u8arr[n] = bstr.charCodeAt(n) } return new Blob([u8arr], { type: mime }) } export default function Delivery() { const { orderNo = '' } = useParams() const [searchParams] = useSearchParams() const [form] = Form.useForm<{ game_account: string }>() const [order, setOrder] = useState(null) const [bindResult, setBindResult] = useState(null) const [boundInfo, setBoundInfo] = useState<{ bound: boolean role_name?: string game_account?: string game_channel?: string } | null>(null) const [checkingBind, setCheckingBind] = useState(false) const [submitResult, setSubmitResult] = useState(null) const [loading, setLoading] = useState(true) const [refreshing, setRefreshing] = useState(false) const [binding, setBinding] = useState(false) const [submitting, setSubmitting] = useState(false) const [error, setError] = useState('') const [voucherImageURL, setVoucherImageURL] = useState('') const [generatingImage, setGeneratingImage] = useState(false) const [copyingImage, setCopyingImage] = useState(false) const [previewModalOpen, setPreviewModalOpen] = useState(false) const [manualCheckCooldown, setManualCheckCooldown] = useState(0) useEffect(() => { if (manualCheckCooldown <= 0) return const timer = setTimeout(() => { setManualCheckCooldown((prev) => Math.max(0, prev - 1)) }, 1000) return () => clearTimeout(timer) }, [manualCheckCooldown]) 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) const getOrGenerateVoucher = async () => { if (!order) return '' if (voucherImageURL) return voucherImageURL setGeneratingImage(true) try { const url = await generateDeliveryVoucher(order) setVoucherImageURL(url) return url } catch (e) { message.error('生成发货凭证图失败:' + (e instanceof Error ? e.message : '请稍后重试')) return '' } finally { setGeneratingImage(false) } } const handleDownloadVoucher = async () => { const url = await getOrGenerateVoucher() if (!url) return const link = document.createElement('a') link.href = url link.download = `发货凭据_${order?.order_no || 'order'}.png` document.body.appendChild(link) link.click() document.body.removeChild(link) message.success('凭证图片已生成并开始下载') } const handlePreviewVoucher = async () => { setPreviewModalOpen(true) await getOrGenerateVoucher() } const handleCopyVoucherImage = async () => { setCopyingImage(true) try { const url = await getOrGenerateVoucher() if (!url) { return } if ( typeof window !== 'undefined' && navigator.clipboard && typeof ClipboardItem !== 'undefined' && navigator.clipboard.write ) { const blob = dataURLtoBlob(url) const item = new ClipboardItem({ 'image/png': blob }) await navigator.clipboard.write([item]) message.success('结果图片已复制到剪贴板!可直接粘贴到聊天窗口(Ctrl+V / ⌘+V)') } else { setPreviewModalOpen(true) message.info('当前浏览器环境不支持直接写入剪贴板图片,已为您打开大图,可长按或右键复制图片') } } catch { setPreviewModalOpen(true) message.warning('写入剪贴板失败,已为您打开凭据预览,可长按或右键复制图片') } finally { setCopyingImage(false) } } const fetchOrder = useCallback( async (silent = false) => { if (!decodedOrderNo) { setError('发货链接缺少订单号') setLoading(false) return } if (!silent) { setLoading(true) } try { const data = await deliveryApi.getOrder(decodedOrderNo, deliveryAuth) setOrder(data) setError('') } catch (e) { if (!silent) { setError(e instanceof Error ? e.message : '订单加载失败') } } finally { if (!silent) { setLoading(false) } } }, [decodedOrderNo, deliveryAuth] ) useEffect(() => { fetchOrder(false) }, [fetchOrder]) // 当订单处于发货中 (delivering) 时,每 3 秒自动轮询发货进度 useEffect(() => { if (order?.status !== 'delivering') { return } const timer = setInterval(() => { fetchOrder(true) }, 3000) return () => clearInterval(timer) }, [order?.status, fetchOrder]) const handleManualRefresh = async () => { setRefreshing(true) try { await fetchOrder(true) message.success('状态已刷新') } catch { message.error('刷新状态失败') } finally { setRefreshing(false) } } const checkBindStatus = useCallback( async (silent = false) => { if (!bindResult?.bind_uuid) return if (!silent) setCheckingBind(true) try { const res = await deliveryApi.getBindResult(decodedOrderNo, bindResult.bind_uuid, deliveryAuth) if (res.bound) { setBoundInfo({ bound: true, role_name: res.role_name, game_account: res.game_account, game_channel: res.game_channel, }) if (!silent) { message.success(`扫码授权成功!已绑定角色:${res.role_name || res.game_account}`) } } else if (!silent) { message.info(res.message || '尚未检测到扫码授权,请使用微信/QQ扫码后重试') } } catch (e) { if (!silent) { const errStr = e instanceof Error ? e.message : String(e || '') if (errStr.includes('频繁') || errStr.includes('110001')) { message.info('正在同步扫码状态,请稍候...') } else { message.error(errStr || '检查绑定状态失败') } } } finally { if (!silent) setCheckingBind(false) } }, [bindResult?.bind_uuid, decodedOrderNo, deliveryAuth] ) const handleManualCheckBind = () => { if (manualCheckCooldown > 0 || checkingBind) return setManualCheckCooldown(3) checkBindStatus(false) } useEffect(() => { if (!bindResult?.bind_uuid || boundInfo?.bound || submitting || submitResult) { return } const timer = setInterval(() => { checkBindStatus(true) }, 2500) return () => clearInterval(timer) }, [bindResult?.bind_uuid, boundInfo?.bound, submitting, submitResult, checkBindStatus]) const bindAccount = async () => { const values = await form.validateFields() setBinding(true) setSubmitResult(null) setBoundInfo(null) try { const result = await deliveryApi.bind(decodedOrderNo, values.game_account, deliveryAuth) setBindResult(result) message.success('绑定二维码已生成,请使用手机微信/QQ扫码授权') } 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 } if (!boundInfo?.bound) { message.warning('请先完成手机微信/QQ扫码授权后再提交发货') 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, game_uid: values.game_account, role_name: (result.game_account as Record)?.role_name || boundInfo?.role_name || prev.role_name, } : prev ) message.success(result.message || '发货已提交') } catch (e) { message.error(e instanceof Error ? e.message : '提交发货失败') } finally { setSubmitting(false) } } if (loading) { return ( ) } if (error || !order) { return ( ) } const status = statusMap[order.status] || { color: 'default', text: order.status } const step = submitResult ? 2 : boundInfo?.bound ? 2 : bindResult ? 1 : 0 return (
游戏道具发货 订单编号:{decodedOrderNo}
{order.status === 'delivering' && ( )}
{order.product?.name || '待发货商品'} {order.product?.sku || '-'}
{status.text}
{order.buyer_name || '-'} {order.amount} 积分 {formatDateTime(order.created_at)} {order.product?.game || '自动发货'} {order.ship_fail_reason && order.status !== 'ship_failed' && ( )} {!order.can_ship && order.status !== 'delivering' && order.status !== 'delivered' && order.status !== 'ship_failed' && ( )}
{/* 状态 1:已发货完成 */} {order.status === 'delivered' && (
} loading={copyingImage || generatingImage} onClick={handleCopyVoucherImage} style={{ background: '#16a34a', borderColor: '#16a34a' }} > 复制结果图片 , , , , ]} >
{/* 商品卡片信息 */}
{(() => { const imgSrc = resolveProductImage({ sku: order.product?.sku, url: order.product?.image }) if (!imgSrc) { return (
道具
) } return ( {order.product?.name ) })()}
{order.product?.name || '游戏道具商品'}
{order.product?.sku || '-'} {order.product?.game && {order.product.game}} 已发放 1 件
{order.game_uid || gameAccount || '-'} {order.role_name || '-'} {order.game_channel || '-'} {formatDateTime(order.created_at)} {order.shipped_at ? formatDateTime(order.shipped_at) : '已完成'}
官方履约已完成:本次道具发货已成功直充到账。点击「复制结果图片」可直接复制凭据图并在微信、客服聊天窗直接粘贴(Ctrl+V / ⌘+V)。
)} {/* 状态 2:发货处理中 */} {order.status === 'delivering' && (
} />
道具正在极速发货中... 已成功提交上游发放节点,通常在 10 ~ 30 秒内到账,页面会自动更新。
{order.game_uid || gameAccount || '-'} {order.role_name || '正在识别...'} {order.game_channel || '自动识别'} }>队列发放中
系统正在自动刷新进度(每 3 秒)
)} {/* 状态 3:发货失败且不可自动重试 */} {order.status === 'ship_failed' && !order.can_ship && (
重新查询订单状态 , ]} />
)} {/* 状态 4:待绑定扫码(含未提交上游的 ship_failed 可重试) */} {(order.status === 'paid' || (order.status === 'ship_failed' && order.can_ship)) && (
{order.status === 'ship_failed' && ( )} }, { title: '微信/QQ 扫码绑定', icon: }, { title: '提交发货', icon: }, ]} />
{bindResult && !boundInfo?.bound && ( )} {bindResult && !boundInfo?.bound && ( } message="正在等待扫码授权" description="请使用手机微信或手机QQ扫描下方二维码完成角色授权。系统检测到授权成功后将自动解锁发货按钮。" /> )} {boundInfo?.bound && (
角色名称:{boundInfo.role_name || '-'}
游戏账号 UID:{boundInfo.game_account || gameAccount || '-'}
{boundInfo.game_channel &&
游戏区服:{boundInfo.game_channel}
}
已确认绑定无误,请点击上方「扫码已确认,立即发货」按钮完成充值发货。
} /> )} {bindResult && (
绑定二维码 绑定凭证 {bindResult.bind_uuid} 打开绑定链接
)} {submitResult && ( )} )} 发货电子凭证预览 } open={previewModalOpen} onCancel={() => setPreviewModalOpen(false)} footer={[ , , , ]} width={640} centered > {generatingImage ? (
正在为您绘制高清发货凭证图片...
) : voucherImageURL ? (
发货电子凭证
提示:支持长按或右键复制/保存图片到本地相册,作为官方维权及售后保障证据。
) : (
)}
) } function CenteredShell({ children }: { children: ReactNode }) { return (
{children}
) } const panelStyle: CSSProperties = { background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8, padding: 24, }