import { useEffect, useRef, useState } from 'react' import { Alert, App, Button, Card, Form, Input, InputNumber, Modal, QRCode, Radio, Select, Space, Tag, Typography, } from 'antd' import { EvidenceUpload } from '@/components/EvidenceUpload' import { acceptCheckout, cancelMyOrder, cancelOrderDispute, confirmCheckout, confirmReceive, counterCheckout as counterCheckoutApi, createOrderDispute, queryOrderPayment, startOrderPayment, submitCheckout as submitCheckoutApi, submitHandoff as submitHandoffApi, } from '@/features/orders/api' import { DISPUTE_TYPES, getOrderActionAvailability, splitEvidenceLines, toPositiveNumber, yuanToCent, } from '@/features/orders/orderActions' import type { UserOrderDetail } from '@/features/orders/types' import { useSessionStore } from '@/stores/session' import { confirmModal } from '@/shared/utils/confirm' import { formatMoney } from '@/shared/utils/format' type DialogKind = 'pay' | 'handoff' | 'checkout' | 'counter' | 'dispute' | null interface ActionFormValues { payWay?: 'WXZF' | 'ZFBZF' content?: string consumableYuan?: number coinConsumed?: number otherYuan?: number reason?: string evidenceText?: string evidenceUrls?: string[] disputeType?: string } interface OrderActionPanelProps { order: UserOrderDetail onChanged: () => void | Promise autoPay?: boolean onAutoPayHandled?: () => void } export function OrderActionPanel({ order, onChanged, autoPay = false, onAutoPayHandled, }: OrderActionPanelProps) { const { message, modal } = App.useApp() const [form] = Form.useForm() const user = useSessionStore(state => state.user) const [dialog, setDialog] = useState(null) const [busy, setBusy] = useState(null) const [payURL, setPayURL] = useState('') const autoPayTriggered = useRef(false) const payPolling = useRef(null) const actions = getOrderActionAvailability(order, user?.id) const checkout = order.checkout const closeDialog = () => { if (busy) return setDialog(null) setPayURL('') form.resetFields() } const readForm = async (): Promise => { try { return await form.validateFields() } catch { return null } } const evidencePayload = (values: ActionFormValues) => [ ...(values.evidenceUrls ?? []), ...splitEvidenceLines(values.evidenceText ?? ''), ] const openDialog = (kind: Exclude) => { form.resetFields() setPayURL('') if (kind === 'pay') { form.setFieldsValue({ payWay: 'WXZF' }) } if (kind === 'checkout') { form.setFieldsValue({ content: '', consumableYuan: 0, coinConsumed: checkout?.coin_consumed_m ?? 0, otherYuan: 0, evidenceText: '', evidenceUrls: [], }) } if (kind === 'counter') { form.setFieldsValue({ consumableYuan: (checkout?.consumable_amount_cent ?? 0) / 100, coinConsumed: checkout?.coin_consumed_m ?? 0, otherYuan: (checkout?.deposit_deduct_amount_cent ?? 0) / 100, reason: '', evidenceText: '', evidenceUrls: [], }) } if (kind === 'dispute') { form.setFieldsValue({ disputeType: actions.checkoutDisputeStage ? 'checkout_dispute' : 'cannot_login', content: '', evidenceText: '', evidenceUrls: [], }) } setDialog(kind) } useEffect(() => { if (!autoPay || autoPayTriggered.current || !actions.canPay) return autoPayTriggered.current = true openDialog('pay') onAutoPayHandled?.() // eslint-disable-next-line react-hooks/exhaustive-deps }, [autoPay, actions.canPay]) useEffect(() => { if (dialog !== 'pay' || !payURL) return let cancelled = false let attempts = 0 const poll = async () => { if (cancelled) return attempts += 1 try { const payment = await queryOrderPayment(order.id) if (cancelled) return if (payment.paid) { message.success('支付成功,订单已进入待交接') setDialog(null) setPayURL('') await onChanged() return } } catch { // 自动轮询不打断用户;保留手动检查作为明确反馈。 } if (!cancelled && attempts < 200) { payPolling.current = window.setTimeout(() => void poll(), 3000) } } payPolling.current = window.setTimeout(() => void poll(), 3000) return () => { cancelled = true if (payPolling.current !== null) { window.clearTimeout(payPolling.current) payPolling.current = null } } }, [dialog, message, onChanged, order.id, payURL]) const run = async (key: string, fn: () => Promise) => { setBusy(key) try { await fn() await onChanged() setDialog(null) setPayURL('') } catch (error) { message.error(error instanceof Error ? error.message : '操作失败,请稍后再试') } finally { setBusy(null) } } const submitPay = async () => { const values = await readForm() if (!values) return setBusy('pay') try { const payment = await startOrderPayment(order.id, values.payWay ?? 'WXZF') if (payment.paid) { message.success('支付成功') await onChanged() setDialog(null) setPayURL('') return } const url = payment.jspay_url || payment.td_code || payment.jspay_info || '' if (url) { setPayURL(url) message.info('支付单已创建,系统会自动检查支付结果') } else { message.warning('支付单已创建,但渠道没有返回支付链接,请稍后检查支付状态') } } catch (error) { message.error(error instanceof Error ? error.message : '发起支付失败') } finally { setBusy(null) } } const checkPayment = async () => { setBusy('check-pay') try { const payment = await queryOrderPayment(order.id) if (payment.paid) { message.success('支付成功,订单已进入待交接') setDialog(null) setPayURL('') await onChanged() return } message.info('支付暂未完成,请稍后再试') } catch (error) { message.error(error instanceof Error ? error.message : '查询支付状态失败') } finally { setBusy(null) } } const submitHandoff = async () => { const values = await readForm() if (!values) return await run('handoff', async () => { await submitHandoffApi(order.id, values.content ?? '') message.success('交接说明已提交') }) } const submitReceive = async () => { const confirmed = await confirmModal(modal, { title: '确认收号', content: '请确认账号可以正常登录、资料与交接说明一致。确认后订单进入使用中。', okText: '确认收号', cancelText: '再检查一下', }) if (!confirmed) return await run('receive', async () => { await confirmReceive(order.id) message.success('已确认收号') }) } const submitCheckout = async () => { const values = await readForm() if (!values) return const confirmed = await confirmModal(modal, { title: '确认提交结账', content: '结账会直接影响租金、押金赔付与双方结算金额,提交后将发送给对方确认。', okText: '提交结账', cancelText: '再检查一下', }) if (!confirmed) return await run('checkout', async () => { await submitCheckoutApi(order.id, { content: values.content ?? '', consumable_amount_cent: yuanToCent(values.consumableYuan ?? 0), coin_consumed_m: toPositiveNumber(values.coinConsumed ?? 0), other_amount_cent: yuanToCent(values.otherYuan ?? 0), evidence_urls: evidencePayload(values), }) message.success('结账已发起,等待对方确认') }) } const submitConfirmCheckout = async () => { if ((checkout?.shortfall_cent ?? 0) > 0) { message.warning('押金不足,请修改方案或发起争议') return } const confirmed = await confirmModal(modal, { title: '确认结账', content: '确认账号状态和扣款金额无误吗?确认后订单将完成。', okText: '确认结账', cancelText: '再检查一下', }) if (!confirmed) return await run('confirm-checkout', async () => { await confirmCheckout(order.id) message.success('结账已确认,订单完成') }) } const submitAcceptCheckout = async () => { if ((checkout?.shortfall_cent ?? 0) > 0) { message.warning('押金不足,请修改方案或发起争议') return } const confirmed = await confirmModal(modal, { title: '同意结账方案', content: '确定同意当前结账方案吗?同意后订单将完成结算。', okText: '同意结账', cancelText: '再检查一下', }) if (!confirmed) return await run('accept-checkout', async () => { await acceptCheckout(order.id) message.success('已同意结账,订单完成') }) } const submitCounter = async () => { const values = await readForm() if (!values) return await run('counter', async () => { await counterCheckoutApi(order.id, { content: values.reason ?? '', reason: values.reason ?? '', consumable_amount_cent: yuanToCent(values.consumableYuan ?? 0), coin_consumed_m: toPositiveNumber(values.coinConsumed ?? 0), other_amount_cent: yuanToCent(values.otherYuan ?? 0), deposit_deduct_amount_cent: yuanToCent(values.otherYuan ?? 0), evidence_urls: evidencePayload(values), }) message.success('结账方案已修改,等待对方处理') }) } const submitDispute = async () => { const values = await readForm() if (!values) return const confirmed = await confirmModal(modal, { title: actions.checkoutDisputeStage ? '发起结账争议' : '发起申诉', content: '提交后订单会进入仲裁流程,客服会根据双方证据处理。确定提交吗?', okText: '提交', cancelText: '再想想', }) if (!confirmed) return await run('dispute', async () => { await createOrderDispute(order.id, { type: actions.checkoutDisputeStage ? 'checkout_dispute' : (values.disputeType ?? 'other'), description: values.content ?? '', evidence_urls: evidencePayload(values), }) message.success('申诉已提交') }) } const cancelDispute = async () => { const confirmed = await confirmModal(modal, { title: '取消申诉', content: '取消后订单会恢复到发起申诉前的流程,确定继续吗?', okText: '取消申诉', cancelText: '保留申诉', }) if (!confirmed) return await run('cancel-dispute', async () => { await cancelOrderDispute(order.id) message.success('申诉已取消') }) } const cancelOrder = async () => { const refundReview = order.status === 'pending_handoff' const confirmed = await confirmModal(modal, { title: refundReview ? '申请取消订单' : '取消订单', content: refundReview ? '订单已支付,取消后需要客服审核退款,审核通过前账号仍会保持占用。' : '取消后订单会关闭,已冻结的押金将按原路退回。', okText: refundReview ? '申请取消' : '取消订单', cancelText: '再想想', }) if (!confirmed) return await run('cancel', async () => { await cancelMyOrder(order.id) message.success(refundReview ? '取消申请已提交' : '订单已取消') }) } const dialogTitle: Record, string> = { pay: '支付订单', handoff: '提交交接说明', checkout: '发起结账', counter: '修改结账方案', dispute: actions.checkoutDisputeStage ? '发起结账争议' : '发起申诉', } return ( {actions.canPay ? ( ) : null} {actions.canSubmitHandoff ? ( ) : null} {actions.canConfirmReceive ? ( ) : null} {actions.canSubmitCheckout ? ( ) : null} {actions.canConfirmCheckout ? ( ) : null} {actions.canAcceptCheckout ? ( ) : null} {actions.canCounterCheckout ? ( ) : null} {actions.canOpenDispute ? ( ) : null} {actions.canCancelDispute ? ( ) : null} {actions.canCancelOrder ? ( ) : null} {!actions.hasAction ? ( 当前订单没有可执行操作。 ) : null} {checkout && checkout.shortfall_cent > 0 ? ( ) : null} ) : ( ) } > {dialog === 'pay' ? ( <>
微信支付 支付宝
{payURL ? ( 请使用微信、支付宝或云闪付扫码付款,支付成功后本页会自动刷新。 } /> ) : null} ) : null} {dialog === 'handoff' ? ( <>
) : null} {dialog === 'checkout' || dialog === 'counter' ? (
{dialog === 'counter' ? ( ) : null} ) : null} {dialog === 'dispute' ? (
{!actions.checkoutDisputeStage ? (