feat: add interactive dispute evidence upload and sealed review mechanism
This commit is contained in:
@@ -9,7 +9,7 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
|||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { roleLabels } from "@/lib/constants"
|
import { roleLabels } from "@/lib/constants"
|
||||||
import { mockComments, mockOrders, mockPosts } from "@/lib/mock-data"
|
import { mockComments, mockOrders, mockPlayers, mockPosts } from "@/lib/mock-data"
|
||||||
|
|
||||||
export default async function PostDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
export default async function PostDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
@@ -20,6 +20,9 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id:
|
|||||||
const linkedOrder = post.linkedOrderId
|
const linkedOrder = post.linkedOrderId
|
||||||
? mockOrders.find((o) => o.id === post.linkedOrderId)
|
? mockOrders.find((o) => o.id === post.linkedOrderId)
|
||||||
: null
|
: null
|
||||||
|
const linkedPlayer = linkedOrder
|
||||||
|
? mockPlayers.find((player) => player.id === linkedOrder.playerId)
|
||||||
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8 px-4 max-w-2xl">
|
<div className="container mx-auto py-8 px-4 max-w-2xl">
|
||||||
@@ -74,7 +77,8 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id:
|
|||||||
<span className="font-medium">关联订单</span>
|
<span className="font-medium">关联订单</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
{linkedOrder.service.title} · {linkedOrder.playerName} · ¥{linkedOrder.totalPrice}
|
{linkedOrder.service.gameName} · {linkedOrder.service.title} · 评分{" "}
|
||||||
|
{linkedPlayer?.rating ?? "--"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { AlertTriangle, ArrowLeft, Clock, FileText } from "lucide-react"
|
import { AlertTriangle, ArrowLeft, Clock, FileText, Upload, X } from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { use, useState } from "react"
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
|
import { type ChangeEvent, use, useEffect, useRef, useState } from "react"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
@@ -21,10 +22,59 @@ const disputeStatusLabels: Record<string, string> = {
|
|||||||
|
|
||||||
export default function DisputePage({ params }: { params: Promise<{ id: string }> }) {
|
export default function DisputePage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = use(params)
|
const { id } = use(params)
|
||||||
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
const order = mockOrders.find((o) => o.id === id)
|
const order = mockOrders.find((o) => o.id === id)
|
||||||
const existingDispute = mockDisputes.find((d) => d.orderId === id)
|
const existingDispute = mockDisputes.find((d) => d.orderId === id)
|
||||||
const [reason, setReason] = useState("")
|
const [reason, setReason] = useState("")
|
||||||
const [submitted, setSubmitted] = useState(false)
|
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) {
|
if (!order) {
|
||||||
return (
|
return (
|
||||||
@@ -106,17 +156,15 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (submitted) {
|
if (showSubmitted) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8 px-4 max-w-lg text-center space-y-4">
|
<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" />
|
<AlertTriangle className="h-12 w-12 mx-auto text-yellow-500" />
|
||||||
<h2 className="text-xl font-bold">争议已提交</h2>
|
<h2 className="text-xl font-bold">争议已提交,请等待平台处理</h2>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">平台将在 3 个工作日内审核你的争议申请。</p>
|
||||||
平台将在 3 个工作日内审核你的争议申请,期间聊天会话仍可使用。
|
<Link href={`/order/${id}`} className="text-sm text-primary hover:underline">
|
||||||
</p>
|
返回订单详情
|
||||||
<Button asChild>
|
</Link>
|
||||||
<Link href={`/order/${id}`}>返回订单</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -155,9 +203,51 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>上传证据截图</Label>
|
<Label>上传证据截图</Label>
|
||||||
<div className="border-2 border-dashed rounded-md p-6 text-center text-sm text-muted-foreground">
|
<input
|
||||||
点击或拖拽上传截图(最多5张)
|
ref={fileInputRef}
|
||||||
</div>
|
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>
|
||||||
|
|
||||||
<div className="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground space-y-1">
|
<div className="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground space-y-1">
|
||||||
@@ -167,7 +257,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
|
|||||||
<p>· 对仲裁结果不满可申诉一次</p>
|
<p>· 对仲裁结果不满可申诉一次</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button className="w-full" disabled={!reason.trim()} onClick={() => setSubmitted(true)}>
|
<Button className="w-full" disabled={!reason.trim()} onClick={handleSubmit}>
|
||||||
提交争议
|
提交争议
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -174,8 +174,12 @@ export default async function OrderDetailPage({ params }: { params: Promise<{ id
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{review.content && (
|
{review.sealed ? (
|
||||||
<p className="text-sm text-muted-foreground">{review.content}</p>
|
<p className="text-sm text-muted-foreground">评价已提交,待揭晓</p>
|
||||||
|
) : (
|
||||||
|
review.content && (
|
||||||
|
<p className="text-sm text-muted-foreground">{review.content}</p>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{new Date(review.createdAt).toLocaleDateString("zh-CN")}
|
{new Date(review.createdAt).toLocaleDateString("zh-CN")}
|
||||||
|
|||||||
@@ -30,12 +30,11 @@ export default function ReviewPage({ params }: { params: Promise<{ id: string }>
|
|||||||
<div className="container mx-auto py-8 px-4 max-w-lg text-center space-y-4">
|
<div className="container mx-auto py-8 px-4 max-w-lg text-center space-y-4">
|
||||||
<Lock className="h-12 w-12 mx-auto text-muted-foreground" />
|
<Lock className="h-12 w-12 mx-auto text-muted-foreground" />
|
||||||
<h2 className="text-xl font-bold">评价已提交</h2>
|
<h2 className="text-xl font-bold">评价已提交</h2>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">等待对方提交评价中</p>
|
||||||
你的评价已密封保存,待对方也提交评价后将同时揭晓
|
<p className="text-sm text-muted-foreground">双方都提交评价后将同时揭晓</p>
|
||||||
</p>
|
<Link href={`/order/${id}`} className="text-sm text-primary hover:underline">
|
||||||
<Button asChild>
|
返回订单详情
|
||||||
<Link href={`/order/${id}`}>返回订单</Link>
|
</Link>
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user