268 lines
9.3 KiB
TypeScript
268 lines
9.3 KiB
TypeScript
"use client"
|
||
|
||
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, use, useEffect, useRef, useState } from "react"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Separator } from "@/components/ui/separator"
|
||
import { Textarea } from "@/components/ui/textarea"
|
||
import { mockDisputes, mockOrders } from "@/lib/mock"
|
||
|
||
const disputeStatusLabels: Record<string, string> = {
|
||
open: "已提交",
|
||
reviewing: "审核中",
|
||
resolved: "已解决",
|
||
appealed: "申诉中",
|
||
}
|
||
|
||
export default function DisputePage({ params }: { params: Promise<{ id: string }> }) {
|
||
const { id } = use(params)
|
||
const router = useRouter()
|
||
const searchParams = useSearchParams()
|
||
const order = mockOrders.find((o) => o.id === id)
|
||
const existingDispute = mockDisputes.find((d) => d.orderId === id)
|
||
const [reason, setReason] = useState("")
|
||
const [submitted, setSubmitted] = useState(false)
|
||
const [files, setFiles] = useState<string[]>([])
|
||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||
const filesRef = useRef<string[]>([])
|
||
|
||
useEffect(() => {
|
||
filesRef.current = files
|
||
}, [files])
|
||
|
||
useEffect(
|
||
() => () => {
|
||
filesRef.current.forEach((url) => {
|
||
URL.revokeObjectURL(url)
|
||
})
|
||
},
|
||
[],
|
||
)
|
||
|
||
const handleFileSelect = (event: ChangeEvent<HTMLInputElement>) => {
|
||
const selectedFiles = event.target.files
|
||
if (!selectedFiles?.length) return
|
||
|
||
setFiles((prev) => {
|
||
const remaining = 5 - prev.length
|
||
if (remaining <= 0) return prev
|
||
const nextUrls = Array.from(selectedFiles)
|
||
.slice(0, remaining)
|
||
.map((file) => URL.createObjectURL(file))
|
||
return [...prev, ...nextUrls]
|
||
})
|
||
|
||
event.target.value = ""
|
||
}
|
||
|
||
const handleRemoveFile = (index: number) => {
|
||
setFiles((prev) => {
|
||
const removed = prev[index]
|
||
if (removed) URL.revokeObjectURL(removed)
|
||
return prev.filter((_, currentIndex) => currentIndex !== index)
|
||
})
|
||
}
|
||
|
||
const handleSubmit = () => {
|
||
setSubmitted(true)
|
||
router.replace(`/dispute/${id}?submitted=1`)
|
||
}
|
||
|
||
const showSubmitted = submitted || searchParams.get("submitted") === "1"
|
||
|
||
if (!order) {
|
||
return (
|
||
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">
|
||
订单不存在
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (existingDispute) {
|
||
return (
|
||
<div className="container mx-auto py-8 px-4 max-w-lg">
|
||
<Link
|
||
href={`/order/${id}`}
|
||
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground mb-4"
|
||
>
|
||
<ArrowLeft className="h-4 w-4" />
|
||
返回订单
|
||
</Link>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle>争议详情</CardTitle>
|
||
<Badge variant="outline">{disputeStatusLabels[existingDispute.status]}</Badge>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2 text-sm">
|
||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||
<span className="text-muted-foreground">发起人:</span>
|
||
{existingDispute.initiatorName}
|
||
</div>
|
||
<div className="flex items-center gap-2 text-sm">
|
||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||
<span className="text-muted-foreground">提交时间:</span>
|
||
{new Date(existingDispute.createdAt).toLocaleString("zh-CN")}
|
||
</div>
|
||
</div>
|
||
<Separator />
|
||
<div>
|
||
<Label className="text-muted-foreground">争议原因</Label>
|
||
<p className="mt-1 text-sm">{existingDispute.reason}</p>
|
||
</div>
|
||
{existingDispute.evidence.length > 0 && (
|
||
<div>
|
||
<Label className="text-muted-foreground">证据截图</Label>
|
||
<div className="mt-1 flex gap-2">
|
||
{existingDispute.evidence.map((url) => (
|
||
<div
|
||
key={url}
|
||
className="relative h-20 w-20 rounded border overflow-hidden bg-muted"
|
||
>
|
||
<Image src={url} alt="证据截图" fill className="object-cover" />
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
{existingDispute.result && (
|
||
<>
|
||
<Separator />
|
||
<div>
|
||
<Label className="text-muted-foreground">仲裁结果</Label>
|
||
<p className="mt-1 text-sm font-medium">
|
||
{existingDispute.result === "full_refund"
|
||
? "全额退款"
|
||
: existingDispute.result === "full_payment"
|
||
? "全额支付给打手"
|
||
: "部分退款"}
|
||
</p>
|
||
</div>
|
||
</>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (showSubmitted) {
|
||
return (
|
||
<div className="container mx-auto py-8 px-4 max-w-lg text-center space-y-4">
|
||
<AlertTriangle className="h-12 w-12 mx-auto text-yellow-500" />
|
||
<h2 className="text-xl font-bold">争议已提交,请等待平台处理</h2>
|
||
<p className="text-sm text-muted-foreground">平台将在 3 个工作日内审核你的争议申请。</p>
|
||
<Link href={`/order/${id}`} className="text-sm text-primary hover:underline">
|
||
返回订单详情
|
||
</Link>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="container mx-auto py-8 px-4 max-w-lg">
|
||
<Link
|
||
href={`/order/${id}`}
|
||
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground mb-4"
|
||
>
|
||
<ArrowLeft className="h-4 w-4" />
|
||
返回订单
|
||
</Link>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<AlertTriangle className="h-5 w-5 text-yellow-500" />
|
||
发起争议
|
||
</CardTitle>
|
||
<p className="text-sm text-muted-foreground">
|
||
{order.service.title} · {order.playerName}
|
||
</p>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="dispute-reason">争议原因</Label>
|
||
<Textarea
|
||
id="dispute-reason"
|
||
placeholder="请详细描述你遇到的问题..."
|
||
value={reason}
|
||
onChange={(e) => setReason(e.target.value)}
|
||
rows={4}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>上传证据截图</Label>
|
||
<input
|
||
ref={fileInputRef}
|
||
type="file"
|
||
accept="image/*"
|
||
multiple
|
||
className="hidden"
|
||
onChange={handleFileSelect}
|
||
/>
|
||
<button
|
||
type="button"
|
||
className="w-full border-2 border-dashed rounded-md p-6 text-center text-sm text-muted-foreground hover:border-primary/50 hover:text-foreground transition-colors disabled:opacity-50"
|
||
onClick={() => fileInputRef.current?.click()}
|
||
disabled={files.length >= 5}
|
||
>
|
||
<div className="flex flex-col items-center gap-2">
|
||
<Upload className="h-5 w-5" />
|
||
<span>点击上传截图(最多5张)</span>
|
||
</div>
|
||
</button>
|
||
{files.length > 0 && (
|
||
<div className="grid grid-cols-5 gap-2">
|
||
{files.map((fileUrl, index) => (
|
||
<div
|
||
key={fileUrl}
|
||
className="relative h-16 w-16 rounded border overflow-hidden bg-muted"
|
||
>
|
||
<Image
|
||
src={fileUrl}
|
||
alt={`证据截图 ${index + 1}`}
|
||
fill
|
||
unoptimized
|
||
className="object-cover"
|
||
/>
|
||
<button
|
||
type="button"
|
||
className="absolute right-0 top-0 rounded-bl bg-background/90 p-0.5"
|
||
onClick={() => handleRemoveFile(index)}
|
||
>
|
||
<X className="h-3 w-3" />
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
<div className="text-xs text-muted-foreground">最多5张</div>
|
||
</div>
|
||
|
||
<div className="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground space-y-1">
|
||
<p>· 提交争议后,订单资金将继续托管</p>
|
||
<p>· 聊天记录将作为证据保留</p>
|
||
<p>· 平台将在 3 个工作日内审核</p>
|
||
<p>· 对仲裁结果不满可申诉一次</p>
|
||
</div>
|
||
|
||
<Button className="w-full" disabled={!reason.trim()} onClick={handleSubmit}>
|
||
提交争议
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
)
|
||
}
|