"use client" import * as React from "react" import { useState } from "react" import { Button } from "@/components/ui/button" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { Spinner } from "@/components/ui/spinner" import { AlertTriangle, X, Layers, Banknote, TicketSlash } from "lucide-react" import { formatDateTime } from "@/lib/utils" import type { Order, DisputeWithOrder, Dispute } from "@/lib/services" import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Label } from "@/components/ui/label" import { DisputeService } from "@/lib/services" import { toast } from "sonner" import { useIsMobile } from "@/hooks/use-mobile" import { usePublicConfig } from "@/hooks/use-public-config" import { useTransaction } from "@/contexts/transaction-context" import { useUser } from "@/contexts/user-context" import { Textarea } from "@/components/ui/textarea" import { typeConfig } from "@/components/common/general/table-filter" import { VisuallyHidden } from "@radix-ui/react-visually-hidden" type DisputeViewer = 'payer' | 'payee' /** * 活动详情弹窗 */ export function OrderDetailDialog({ order }: { order: Order }) { const [open, setOpen] = useState(false) return ( <> setOpen(true)} > 查看详情 积分流转详情 查看积分活动的详细信息 {order.app_name || 'RECEIPT'} {formatDateTime(order.created_at)} Total Credits {parseFloat(order.amount).toFixed(2)} 编号 {order.order_no} 类型 {typeConfig[order.type]?.label || order.type} 消费方 {order.payer_username} 服务方 {order.payee_username} 备注 {order.remark} {(order.status === 'success' || order.status === 'refund') && ( 时间 {formatDateTime(order.trade_time)} )} {[...Array(30)].map((_, i) => ( ))} LINUX DO Credit > ) } /** * 发起争议弹窗 */ export function CreateDisputeDialog({ order, onSuccess }: { order: Order; onSuccess?: () => void }) { const isMobile = useIsMobile() const { config: publicConfig, loading: configLoading, error: configError } = usePublicConfig() const { updateOrderStatus } = useTransaction() const [open, setOpen] = useState(false) const [loading, setLoading] = useState(false) const [reason, setReason] = useState("") const resetForm = () => { setReason("") } const handleButtonClick = async () => { if (configLoading) { try { await new Promise(resolve => setTimeout(resolve, 100)) if (configLoading) { toast.error('配置加载中', { description: '请稍候再试' }) return } } catch { toast.error('配置加载失败', { description: '请刷新页面重试' }) return } } if (configError || !publicConfig) { toast.error('无法获取配置', { description: configError?.message || '请刷新页面重试' }) return } const tradeTime = new Date(order.trade_time) const expiryTime = new Date(tradeTime.getTime() + publicConfig.dispute_time_window_hours * 60 * 60 * 1000) const now = new Date() if (now > expiryTime) { toast.error('无法发起争议', { description: '已超过最晚发起争议时间' }) return } setOpen(true) } const handleOpenChange = (newOpen: boolean) => { if (newOpen && !loading) { resetForm() } setOpen(newOpen) } const handleCreateDispute = async () => { if (!reason.trim()) { toast.error('表单验证失败', { description: '请填写争议原因' }) return } if (reason.length > 100) { toast.error('表单验证失败', { description: '争议原因不能超过 100 个字符' }) return } try { setLoading(true) const dispute = await DisputeService.createDispute({ order_id: order.id, reason: reason.trim() }) toast.success('争议已发起', { description: '请等待服务方处理' }) updateOrderStatus(order.id, { status: 'disputing', dispute_id: dispute.id }) setOpen(false) onSuccess?.() } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : '发起争议失败' toast.error('发起争议失败', { description: errorMessage }) } finally { setLoading(false) } } return ( <> 发起争议 发起争议 如果您对该积分活动有疑问,可以发起争议,服务方将在规定时间内处理。 争议原因 setReason(e.target.value)} disabled={loading} className={isMobile ? "text-sm" : ""} /> {reason.length}/100 取消 { e.preventDefault(); handleCreateDispute() }} disabled={loading} className="bg-primary h-8 text-xs" > {loading ? <> 提交中> : '确认发起'} > ) } /** * 取消争议弹窗 */ export function CancelDisputeDialog({ order, onSuccess }: { order: Order; onSuccess?: () => void }) { const { updateOrderStatus } = useTransaction() const [open, setOpen] = useState(false) const [loading, setLoading] = useState(false) const handleCancelDispute = async () => { try { setLoading(true) if (!order.dispute_id) { toast.error('取消争议失败', { description: '未找到争议记录' }) return } await DisputeService.closeDispute({ dispute_id: order.dispute_id }) toast.success('争议已取消', { description: '争议已成功取消' }) updateOrderStatus(order.id, { status: 'success' }) setOpen(false) onSuccess?.() } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : '取消争议失败' toast.error('取消争议失败', { description: errorMessage }) } finally { setLoading(false) } } return ( <> setOpen(true)} > 争议正在进行中,点击取消 争议取消 您确定取消当前的争议吗?取消后争议将恢复正常。 取消 { e.preventDefault(); handleCancelDispute() }} disabled={loading} className="bg-primary h-8 text-xs" > {loading ? <> 取消中> : '确认取消'} > ) } /** * 查看争议历史弹窗 */ export function ViewDisputeHistoryDialog({ order, viewer = 'payer' }: { order: Order; viewer?: DisputeViewer }) { const [open, setOpen] = useState(false) const [disputeHistory, setDisputeHistory] = useState(null) const [fetchingHistory, setFetchingHistory] = useState(false) const disputeConfig = React.useMemo(() => { switch (order.status) { case 'refused': return { tooltip: '争议已拒绝,点击查看', timelineText: '服务方驳回争议', isRed: true, showTimestamp: true, showContent: false, content: null } case 'refund': return { tooltip: '争议已退款', timelineText: '服务方已退款', isRed: false, showTimestamp: true, showContent: true, content: '退款已完成' } case 'disputing': return { tooltip: '争议处理中,点击查看', timelineText: '争议进行中,等待服务方处理', isRed: false, showTimestamp: false, showContent: false, content: null } } }, [order.status]) if (!disputeConfig) return null const resetForm = () => { setDisputeHistory(null) } const fetchDisputeHistory = async () => { try { setFetchingHistory(true) if (!order.dispute_id) { toast.error('获取争议记录失败', { description: '未找到争议记录' }) return } const params = { page: 1, page_size: 1, dispute_id: order.dispute_id } const res = viewer === 'payee' ? await DisputeService.listMerchantDisputes(params) : await DisputeService.listDisputes(params) if (res.disputes && res.disputes.length > 0) { setDisputeHistory(res.disputes[0]) } else { toast.error('未找到争议记录') } } catch (error) { const errorMessage = error instanceof Error ? error.message : '获取争议记录失败' toast.error('获取争议记录失败', { description: errorMessage }) } finally { setFetchingHistory(false) } } const handleOpenChange = (newOpen: boolean) => { if (newOpen) { setOpen(true) resetForm() fetchDisputeHistory() } else { setOpen(false) } } const parseDisputeReason = (fullReason: string) => { const match = fullReason.match(/^(.*?)\s*\[(?:服务方|受托方)拒绝理由:\s*(.*?)\]$/) if (match) { return { userReason: match[1].trim(), merchantReason: match[2].trim() } } return { userReason: fullReason, merchantReason: null } } return ( <> handleOpenChange(true)} > {disputeConfig.tooltip} 争议详情 查看争议处理记录 {fetchingHistory ? ( ) : disputeHistory ? ( 消费方发起争议 {formatDateTime(disputeHistory.created_at)} {parseDisputeReason(disputeHistory.reason).userReason} {disputeConfig.timelineText} {disputeConfig.showTimestamp && ( {formatDateTime(disputeHistory.updated_at)} )} {order.status === 'refused' && ( {parseDisputeReason(disputeHistory.reason).merchantReason || "未提供拒绝理由"} )} {order.status === 'refund' && disputeConfig.showContent && disputeConfig.content && ( {disputeConfig.content} )} ) : ( 无法加载争议记录 )} 关闭 > ) } /** * 退款审核活动弹窗 */ export function RefundReviewDialog({ order, onSuccess }: { order: Order; onSuccess?: () => void }) { const { updateOrderStatus } = useTransaction() const { refetch: refetchUser } = useUser() const [open, setOpen] = useState(false) const [loading, setLoading] = useState(false) const [action, setAction] = useState<'refund' | 'closed' | null>(null) const [reason, setReason] = useState("") const [disputeInfo, setDisputeInfo] = useState(null) const [fetchingDispute, setFetchingDispute] = useState(false) const resetForm = () => { setAction(null) setReason("") setDisputeInfo(null) } const fetchDisputeInfo = async () => { try { setFetchingDispute(true) if (!order.dispute_id) { toast.error('获取争议详情失败', { description: '未找到争议记录' }) return } const res = await DisputeService.listMerchantDisputes({ page: 1, page_size: 1, dispute_id: order.dispute_id }) if (res.disputes && res.disputes.length > 0) { setDisputeInfo(res.disputes[0]) } else { toast.error('获取争议详情失败', { description: '未找到争议记录' }) } } catch (error) { const errorMessage = error instanceof Error ? error.message : '获取争议详情失败' toast.error('获取争议详情失败', { description: errorMessage }) } finally { setFetchingDispute(false) } } const handleOpenChange = (newOpen: boolean) => { if (newOpen) { setOpen(true) resetForm() fetchDisputeInfo() } else if (!loading) { setOpen(false) } } const handleSubmit = async () => { if (!action) { toast.error('请选择处理方式') return } if (action === 'closed' && !reason.trim()) { toast.error('请填写拒绝原因') return } if (reason.length > 100) { toast.error('拒绝理由不能超过 100 个字符') return } try { setLoading(true) if (!order.dispute_id) { toast.error('处理失败', { description: '未找到争议记录' }) return } await DisputeService.refundReview({ dispute_id: order.dispute_id, status: action, reason: action === 'closed' ? reason.trim() : undefined, }) toast.success('处理成功', { description: action === 'refund' ? '已同意积分退回,争议结束' : '已拒绝积分退回,此积分活动正常进行' }) updateOrderStatus(order.id, { status: action === 'refund' ? 'refund' : 'refused' }) setOpen(false) refetchUser() onSuccess?.() } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : '处理争议失败' toast.error('处理争议失败', { description: errorMessage }) } finally { setLoading(false) } } return ( <> { setOpen(true) fetchDisputeInfo() }} > 处理争议 处理争议 请审核此争议。您可以选择同意或拒绝退回积分。 {fetchingDispute ? ( ) : ( {disputeInfo && ( 发起争议理由 • {formatDateTime(disputeInfo.created_at)} {disputeInfo.reason} )} setAction('refund')} > 同意积分退回 积分全额退回,争议结束 setAction('closed')} > 拒绝积分退回 拒绝积分退回,此积分活动正常进行 {action === 'closed' && ( 拒绝原因 * setReason(e.target.value)} disabled={loading} className="h-9 text-sm" /> {reason.length}/100 )} )} 取消 { e.preventDefault(); handleSubmit() }} disabled={loading || !action} className="bg-primary h-8 text-xs" > {loading ? <> 提交中> : '确认处理'} > ) }
查看详情
{formatDateTime(order.created_at)}
Total Credits
LINUX DO Credit
发起争议
{reason.length}/100
争议正在进行中,点击取消
{disputeConfig.tooltip}
处理争议
{disputeInfo.reason}