import { ArrowLeftOutlined, CopyOutlined, FileTextOutlined, PauseCircleOutlined, PlayCircleOutlined, RollbackOutlined, StopOutlined, } from '@ant-design/icons' import { Alert, App, Button, Card, DatePicker, Descriptions, Form, Input, InputNumber, Modal, Radio, Select, Skeleton, Space, Tag, Typography, } from 'antd' import PercentTable from '@/components/common/PercentTable' import type { TableColumnsType } from 'antd' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import type { Dayjs } from 'dayjs' import { useEffect, useState } from 'react' import { useNavigate, useParams, useSearchParams } from 'react-router' import dayjs from '@/lib/dayjs' import JsonPreview from '@/components/admin/JsonPreview' import ManualOrderCostModal from '@/components/admin/ManualOrderCostModal' import PageHeader from '@/components/admin/PageHeader' import StatusTag from '@/components/admin/StatusTag' import { createAdminOrderRefund, fetchAdminOrderDetail, updateAdminOrderClaimStatus, voidAdminOrderRefund, } from '@/services/admin' import type { AdminOrderDetail, AdminOrderRefund, AdminTaskListItem } from '@/types/admin' import { hasAdminPermission } from '@/utils/admin-auth' import { formatAdminDateTime } from '@/utils/admin-time' import { resolveOrderFulfillmentMode, resolveVoucherStateLabel } from '@/utils/admin-display' import { CostPreviewModal } from './cost-center/CostPreviewModal' import { formatMoney } from './panels/shared' export default function AdminOrderDetailPage() { const navigate = useNavigate() const queryClient = useQueryClient() const { message, modal } = App.useApp() const { orderId = '' } = useParams() const [searchParams, setSearchParams] = useSearchParams() const [statusModalVisible, setStatusModalVisible] = useState(false) const [manualCostOpen, setManualCostOpen] = useState(false) const [costPreviewItemId, setCostPreviewItemId] = useState(null) const [refundModalOpen, setRefundModalOpen] = useState(false) const [refundScope, setRefundScope] = useState<'order' | 'item'>('order') const [refundItemId, setRefundItemId] = useState(null) const [refundAmountYuan, setRefundAmountYuan] = useState(null) const [refundReason, setRefundReason] = useState('买家退款') const [refundNote, setRefundNote] = useState('') const [refundedAt, setRefundedAt] = useState(dayjs()) const [refunding, setRefunding] = useState(false) const [voidTarget, setVoidTarget] = useState(null) const [voidReason, setVoidReason] = useState('') const [voiding, setVoiding] = useState(false) const [targetStatus, setTargetStatus] = useState<'active' | 'paused' | 'closed'>('active') const [statusReason, setStatusReason] = useState('') useEffect(() => { if (searchParams.get('manualCost') === '1') { setManualCostOpen(true) } }, [searchParams]) function closeManualCost() { setManualCostOpen(false) if (searchParams.get('manualCost')) { const next = new URLSearchParams(searchParams) next.delete('manualCost') setSearchParams(next, { replace: true }) } } const query = useQuery({ queryKey: ['admin-order-detail', orderId], queryFn: () => fetchAdminOrderDetail(orderId), enabled: Boolean(orderId), }) const detail = query.data?.data const statusMutation = useMutation({ mutationFn: async (data: { status: 'active' | 'paused' | 'closed'; reason?: string }) => { const targetOrderId = orderId || detail?.order?.orderId if (!targetOrderId) throw new Error('订单 ID 缺失') return updateAdminOrderClaimStatus(targetOrderId, data) }, onSuccess: () => { message.success('订单兑换状态已更新') setStatusModalVisible(false) setStatusReason('') void queryClient.invalidateQueries({ queryKey: ['admin-order-detail', orderId] }) }, onError: (err: unknown) => { message.error(err instanceof Error ? err.message : '更新状态失败') }, }) const handleCopyH5Link = () => { if (!detail?.order.platformOrderId) return const h5Url = `${window.location.origin}/h5/${encodeURIComponent(detail.order.platformOrderId)}` navigator.clipboard .writeText(h5Url) .then(() => message.success('已复制 H5 领取链接: ' + h5Url)) .catch(() => message.error('复制失败,请手动复制')) } const openStatusModal = (status: 'active' | 'paused' | 'closed') => { if (status === 'active') { modal.confirm({ title: '恢复启用订单兑换', content: '恢复后,买家可正常进入 H5 页面提交 UID 领取,确认恢复吗?', onOk: async () => { await statusMutation.mutateAsync({ status: 'active', reason: '管理员恢复启用' }) }, }) return } setTargetStatus(status) setStatusReason('') setStatusModalVisible(true) } function openRefundModal(itemId?: number) { const items = detail?.items || [] const target = itemId ? items.find((item) => item.orderItemId === itemId) : null const fallback = items.find((item) => item.refundableFen > 0) || null const focus = target || fallback setRefundScope(target ? 'item' : 'order') setRefundItemId(focus?.orderItemId ?? null) setRefundAmountYuan(focus ? focus.refundableFen / 100 : 0) setRefundReason('买家退款') setRefundNote('') setRefundedAt(dayjs()) setRefundModalOpen(true) } async function submitRefund() { if (!detail) return if (refundScope === 'item' && !refundItemId) { message.warning('请选择要退款的商品') return } const amountFen = Math.round(Number(refundAmountYuan || 0) * 100) if (refundScope === 'item' && amountFen <= 0) { message.warning('请输入退款金额') return } setRefunding(true) try { const response = await createAdminOrderRefund(detail.order.orderId, { orderItemId: refundScope === 'item' ? refundItemId || undefined : undefined, amountFen: refundScope === 'item' ? amountFen : undefined, reason: refundReason.trim() || '手动退款', note: refundNote.trim(), refundedAt: refundedAt.format('YYYY-MM-DD HH:mm:ss'), }) const result = response.data message.success( `已登记 ${result.refunds.length} 条退款${result.fullRefunded ? '(订单已全额退款)' : ''}${ result.closedExceptionCount > 0 ? `,自动关闭 ${result.closedExceptionCount} 条成本异常` : '' }`, ) setRefundModalOpen(false) await queryClient.invalidateQueries({ queryKey: ['admin-order-detail', orderId] }) await queryClient.invalidateQueries({ queryKey: ['admin-finance'] }) } catch (error) { message.error(error instanceof Error ? error.message : '登记退款失败') } finally { setRefunding(false) } } async function submitVoidRefund() { if (!detail || !voidTarget) return setVoiding(true) try { await voidAdminOrderRefund(detail.order.orderId, voidTarget.id, { reason: voidReason.trim() || '撤销退款', }) message.success('退款已撤销') setVoidTarget(null) setVoidReason('') await queryClient.invalidateQueries({ queryKey: ['admin-order-detail', orderId] }) await queryClient.invalidateQueries({ queryKey: ['admin-finance'] }) } catch (error) { message.error(error instanceof Error ? error.message : '撤销退款失败') } finally { setVoiding(false) } } if (query.isLoading) { return (
} />
) } if (query.error || !detail) { return (
} />
) } const taskColumns: TableColumnsType = [ { title: '任务号', dataIndex: 'taskNo', minWidth: 180, render: (_, row) => ( navigate(`/admin/tasks/${row.taskId}`)}> {row.taskNo} ), }, { title: '状态', minWidth: 220, render: (_, row) => ( ), }, { title: '角色', minWidth: 160, render: (_, row) => (
{row.roleName || '-'} {row.roleId || '-'}
), }, { title: '错误', dataIndex: 'lastError', minWidth: 220, render: (value) => {value || '-'}, }, ] const allTasksCompleted = detail.tasks.length > 0 && detail.tasks.every( (t) => ['completed', 'redeemed', 'delivered', 'success'].includes(t.status) || t.deliveryStatus === 'delivered', ) const isOrderFulfilled = detail.order.orderStatus === 'completed' || allTasksCompleted const isOrderRefundedOrCancelled = detail.order.payStatus === 'refunded' || detail.order.orderStatus === 'cancelled' const canPauseOrClose = !isOrderFulfilled && !isOrderRefundedOrCancelled const isAutoClaimOrder = Boolean( detail.order.fulfillmentMode === 'affiliate_dash' || (detail.order.fulfillmentMode === 'mixed' && detail.order.canClaimH5), ) const fulfillmentMode = resolveOrderFulfillmentMode(detail.order) const voucherLabel = resolveVoucherStateLabel(detail.order.voucherState) return (
} /> {hasAdminPermission('finance.config') ? ( ) : null} {isAutoClaimOrder ? ( <> {detail.order.claimStatus === 'paused' || detail.order.claimStatus === 'closed' ? ( ) : canPauseOrClose ? ( ) : null} {canPauseOrClose && detail.order.claimStatus !== 'closed' ? ( ) : null} ) : null} } > {detail.order.orderId} {detail.order.shopName || detail.order.shopId || '-'} {detail.order.platformOrderId} {fulfillmentMode.label} {detail.order.hasVoucher && voucherLabel ? ( 🎫 {voucherLabel} ) : ( 无 )} {isAutoClaimOrder ? ( {detail.order.claimStatus === 'paused' ? ( }> 已暂停兑换 ) : detail.order.claimStatus === 'closed' ? ( }> 已关闭兑换 ) : ( }> 正常有效 )} {detail.order.claimStatusReason ? ( ({detail.order.claimStatusReason}) ) : null} ) : null} {detail.order.totalAmount} {detail.order.currency} {formatAdminDateTime(detail.order.createdAt)} {formatAdminDateTime(detail.order.updatedAt)}
{detail.order.itemTitle || detail.order.itemSummary || '-'} {detail.order.skuNick ? ( {detail.order.skuNick} ) : null}
rowKey="orderItemId" pagination={false} dataSource={detail.items} scroll={{ x: 760 }} columns={[ { title: '商品', dataIndex: 'itemTitle', minWidth: 240, render: (value) => {value || '-'}, }, { title: 'SKU / 规格', minWidth: 180, render: (_, item) => (
{item.skuNick ? ( {item.skuNick} ) : null} {/* 商品列已是内部 SKU 时不再重复展示同一串 */} {item.skuCode && item.skuCode !== item.itemTitle ? ( {item.skuCode} ) : null}
), }, { title: '数量', dataIndex: 'quantity', width: 90 }, { title: '收入 / 已退', width: 190, render: (_, item) => (
{formatMoney(item.revenueFen)} 已退 {formatMoney(item.refundedFen)} · 可退 {formatMoney(item.refundableFen)}
), }, { title: '履约模式', dataIndex: 'deliveryMode', minWidth: 140 }, { title: '成本', width: 110, render: (_, item) => ( ), }, ]} />
{detail.refunds.length > 0 ? ( 退款记录 {formatMoney( detail.refunds .filter((item) => item.status === 'confirmed') .reduce((sum, item) => sum + item.amountFen, 0), )} } > rowKey="id" pagination={false} dataSource={detail.refunds} scroll={{ x: 980 }} columns={[ { title: '退款单号', dataIndex: 'refundNo', width: 190, render: (value: string, row) => (
{value} {row.batchNo}
), }, { title: '商品', dataIndex: 'skuName', width: 240, render: (value: string) => value || '-', }, { title: '金额', dataIndex: 'amountFen', width: 120, render: (value: number) => formatMoney(value), }, { title: '原因', dataIndex: 'reason', width: 220, render: (value: string, row) => (
{value || '-'} {row.note ? {row.note} : null}
), }, { title: '退款时间', dataIndex: 'refundedAt', width: 180, render: (value: string) => formatAdminDateTime(value), }, { title: '操作人', dataIndex: 'operatorName', width: 120, render: (value: string) => value || '-', }, { title: '状态', dataIndex: 'status', width: 100, render: (value: string, row) => value === 'confirmed' ? ( 已确认 ) : ( 已撤销{row.voidReason ? `:${row.voidReason}` : ''} ), }, { title: '操作', width: 90, render: (_, row) => hasAdminPermission('finance.config') && row.status === 'confirmed' ? ( ) : null, }, ]} />
) : null} rowKey="taskId" pagination={false} dataSource={detail.tasks} columns={taskColumns} scroll={{ x: 900 }} /> setStatusModalVisible(false)} confirmLoading={statusMutation.isPending} onOk={() => { if (!statusReason.trim()) { message.warning('请填写原因或备注(如:买家申请退款)') return } statusMutation.mutate({ status: targetStatus, reason: statusReason.trim() }) }} >

{targetStatus === 'paused' ? '暂停后,买家访问 H5 链接将被拦截并提示“兑换已暂停,请联系客服”,后续可随时恢复启用。' : '关闭后,买家将无法继续进入 H5 页面兑换,适用于已退款或订单作废。'}

setStatusReason(e.target.value)} />
({ orderItemId: item.orderItemId, matchSku: item.matchSku, skuName: item.skuName, skuCode: item.skuCode, quantity: item.quantity, }))} onClose={closeManualCost} /> setRefundModalOpen(false)} onOk={() => void submitRefund()} confirmLoading={refunding} okText="确认退款" cancelText="取消" width={560} > sum + item.refundableFen, 0), )}`} description="整单退款会按每个商品剩余可退金额拆成多条记录;登记后当天销售事实立即重算。" /> { const next = event.target.value as 'order' | 'item' setRefundScope(next) if (next === 'item') { const focus = detail.items.find((item) => item.refundableFen > 0) || null setRefundItemId(focus?.orderItemId ?? null) setRefundAmountYuan(focus ? focus.refundableFen / 100 : 0) } }} > 整单退款 指定商品 {refundScope === 'item' ? ( <> setRefundReason(event.target.value)} placeholder="退款原因,如:买家退款 / 售后补偿 / 发错货" /> { if (value) setRefundedAt(value) }} style={{ width: '100%' }} /> setRefundNote(event.target.value)} placeholder="备注(可选,如平台退款单号、沟通记录)" /> { setVoidTarget(null) setVoidReason('') }} onOk={() => void submitVoidRefund()} confirmLoading={voiding} okText="确认撤销" okButtonProps={{ danger: true }} cancelText="取消" width={520} > setVoidReason(event.target.value)} placeholder="撤销原因(可选)" /> setCostPreviewItemId(null)} />
) } function BackButton() { const navigate = useNavigate() return ( ) }