"use client" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { EmptyState } from "@/components/ui/empty-state" import { Label } from "@/components/ui/label" import { Separator } from "@/components/ui/separator" import { StatusBadge, type StatusBadgeProps } from "@/components/ui/status-badge" import { Textarea } from "@/components/ui/textarea" import { getOrderById, getPlayerById, uploadFile } from "@/lib/api" import { getDisputeByOrderId, submitDispute, submitDisputeAppeal, submitDisputeResponse, } from "@/lib/api/disputes" import { notifyInfo } from "@/lib/toast" import { useAuthStore } from "@/store/auth" import { AlertTriangle, ArrowLeft, Clock, FileText, Upload, X } from "lucide-react" import Image from "next/image" import Link from "next/link" import { useRouter, useSearchParams } from "next/navigation" import { type ChangeEvent, type Dispatch, type SetStateAction, use, useEffect, useRef, useState, } from "react" const disputeStatusLabels: Record = { open: "已提交", reviewing: "审核中", resolved: "已解决", appealed: "申诉中", } const disputeStatusVariants: Record = { open: "warning", reviewing: "info", resolved: "success", appealed: "info", } function deriveMinimalTimeline(dispute: { id: string status: string createdAt: TCreatedAt timeline?: { id: string; content: string; createdAt: TCreatedAt }[] }) { const existing = dispute.timeline if (existing?.length) return existing const steps = [ { status: "open", content: "争议已提交" }, { status: "reviewing", content: "平台审核中" }, { status: "resolved", content: "争议已解决" }, { status: "appealed", content: "已发起申诉" }, ] const currentIndex = steps.findIndex((step) => step.status === dispute.status) const lastIndex = currentIndex >= 0 ? currentIndex : 0 return steps.slice(0, lastIndex + 1).map((step) => ({ id: `${dispute.id}-${step.status}`, content: step.content, createdAt: dispute.createdAt, })) } export default function DisputePage({ params }: { params: Promise<{ id: string }> }) { const { id } = use(params) const router = useRouter() const searchParams = useSearchParams() const userId = useAuthStore((state) => state.user?.id) const [loading, setLoading] = useState(true) const [order, setOrder] = useState> | null>(null) const [existingDispute, setExistingDispute] = useState > | null>(null) const [playerUserId, setPlayerUserId] = useState(null) const [reason, setReason] = useState("") const [files, setFiles] = useState([]) const [responseReason, setResponseReason] = useState("") const [responseFiles, setResponseFiles] = useState([]) const [appealReason, setAppealReason] = useState("") const [uploading, setUploading] = useState(false) const fileInputRef = useRef(null) const responseFileInputRef = useRef(null) const filesRef = useRef([]) const responseFilesRef = useRef([]) useEffect(() => { let cancelled = false const reset = () => { setLoading(true) setOrder(null) setExistingDispute(null) setPlayerUserId(null) } const load = async () => { reset() const [nextOrder, nextDispute] = await Promise.all([ Promise.resolve(getOrderById(id)), Promise.resolve(getDisputeByOrderId(id)), ]) if (cancelled) return setOrder(nextOrder ?? null) setExistingDispute(nextDispute ?? null) if (nextOrder) { const player = await getPlayerById(String(nextOrder.playerId)) if (cancelled) return setPlayerUserId(player?.user.id ?? null) } setLoading(false) } void load() return () => { cancelled = true } }, [id]) useEffect(() => { filesRef.current = files }, [files]) useEffect(() => { responseFilesRef.current = responseFiles }, [responseFiles]) useEffect( () => () => { filesRef.current = [] responseFilesRef.current = [] }, [], ) const handleFileSelect = async ( event: ChangeEvent, setter: Dispatch>, currentFiles: string[], ) => { const selectedFiles = event.target.files if (!selectedFiles?.length) return setUploading(true) try { const remaining = 5 - currentFiles.length if (remaining <= 0) return const nextUrls = await Promise.all( Array.from(selectedFiles) .slice(0, remaining) .map((file) => uploadFile(file, "dispute")), ) setter((prev) => [...prev, ...nextUrls]) } catch { notifyInfo("证据上传失败") } finally { setUploading(false) event.target.value = "" } } const removeFile = ( index: number, filesState: string[], setter: Dispatch>, ) => { setter((prev) => prev.filter((_, currentIndex) => currentIndex !== index)) } const reloadDispute = async () => { const nextDispute = await getDisputeByOrderId(id) setExistingDispute(nextDispute ?? null) } const handleSubmit = () => { if (!userId || !reason.trim()) return void Promise.resolve( submitDispute({ orderId: id, reason, evidence: files, }), ).then((result) => { if (!result.decision.ok) { notifyInfo(result.decision.error.msg) return } router.replace(`/dispute/${id}?submitted=1`) }) } const showSubmitted = !loading && searchParams.get("submitted") === "1" && !existingDispute if (loading) { return (
) } if (!order) { return (
) } const isParticipant = Boolean( userId && (String(order.consumerId) === userId || playerUserId === userId), ) if (!isParticipant) { return (
) } if (existingDispute) { const canRespond = isParticipant && !existingDispute.respondentReason && (existingDispute.status === "open" || existingDispute.status === "reviewing") const canAppeal = isParticipant && existingDispute.status === "resolved" && !existingDispute.appealedAt const timeline = deriveMinimalTimeline(existingDispute) return (
返回订单
争议详情 {disputeStatusLabels[existingDispute.status]}
发起人: 发起方
提交时间: {new Date(existingDispute.createdAt).toLocaleString("zh-CN")}

{existingDispute.reason}

{existingDispute.evidence.length > 0 && (
{existingDispute.evidence.map((url) => (
发起方证据
))}
)}
{existingDispute.respondentReason ? ( <>

{existingDispute.respondentReason}

{existingDispute.respondentEvidence.length > 0 && (
{existingDispute.respondentEvidence.map((url) => (
对方证据
))}
)} ) : (

对方暂未提交回应材料。

)}
{canRespond && ( <>