From a5780c8393699e9c5ffbc35267af0217cf7b832b Mon Sep 17 00:00:00 2001 From: zetaloop Date: Fri, 20 Feb 2026 22:47:33 +0800 Subject: [PATCH] feat: add interactive dispute evidence upload and sealed review mechanism --- app/(main)/post/[id]/page.tsx | 8 +- app/(order)/dispute/[id]/page.tsx | 118 ++++++++++++++++++++++++++---- app/(order)/order/[id]/page.tsx | 8 +- app/(order)/review/[id]/page.tsx | 11 ++- 4 files changed, 121 insertions(+), 24 deletions(-) diff --git a/app/(main)/post/[id]/page.tsx b/app/(main)/post/[id]/page.tsx index 7075fc4..532d92d 100644 --- a/app/(main)/post/[id]/page.tsx +++ b/app/(main)/post/[id]/page.tsx @@ -9,7 +9,7 @@ import { Card, CardContent, CardHeader } from "@/components/ui/card" import { Separator } from "@/components/ui/separator" import { Textarea } from "@/components/ui/textarea" 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 }> }) { const { id } = await params @@ -20,6 +20,9 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id: const linkedOrder = post.linkedOrderId ? mockOrders.find((o) => o.id === post.linkedOrderId) : null + const linkedPlayer = linkedOrder + ? mockPlayers.find((player) => player.id === linkedOrder.playerId) + : null return (
@@ -74,7 +77,8 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id: 关联订单

- {linkedOrder.service.title} · {linkedOrder.playerName} · ¥{linkedOrder.totalPrice} + {linkedOrder.service.gameName} · {linkedOrder.service.title} · 评分{" "} + {linkedPlayer?.rating ?? "--"}

diff --git a/app/(order)/dispute/[id]/page.tsx b/app/(order)/dispute/[id]/page.tsx index eeb9c0b..30c1d96 100644 --- a/app/(order)/dispute/[id]/page.tsx +++ b/app/(order)/dispute/[id]/page.tsx @@ -1,9 +1,10 @@ "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 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 { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" @@ -21,10 +22,59 @@ const disputeStatusLabels: Record = { 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([]) + const fileInputRef = useRef(null) + const filesRef = useRef([]) + + useEffect(() => { + filesRef.current = files + }, [files]) + + useEffect( + () => () => { + filesRef.current.forEach((url) => { + URL.revokeObjectURL(url) + }) + }, + [], + ) + + const handleFileSelect = (event: ChangeEvent) => { + 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 ( @@ -106,17 +156,15 @@ export default function DisputePage({ params }: { params: Promise<{ id: string } ) } - if (submitted) { + if (showSubmitted) { return (
-

争议已提交

-

- 平台将在 3 个工作日内审核你的争议申请,期间聊天会话仍可使用。 -

- +

争议已提交,请等待平台处理

+

平台将在 3 个工作日内审核你的争议申请。

+ + 返回订单详情 +
) } @@ -155,9 +203,51 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
-
- 点击或拖拽上传截图(最多5张) -
+ + + {files.length > 0 && ( +
+ {files.map((fileUrl, index) => ( +
+ {`证据截图 + +
+ ))} +
+ )} +
最多5张
@@ -167,7 +257,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }

· 对仲裁结果不满可申诉一次

- diff --git a/app/(order)/order/[id]/page.tsx b/app/(order)/order/[id]/page.tsx index a4453f4..fdd87fd 100644 --- a/app/(order)/order/[id]/page.tsx +++ b/app/(order)/order/[id]/page.tsx @@ -174,8 +174,12 @@ export default async function OrderDetailPage({ params }: { params: Promise<{ id ))} - {review.content && ( -

{review.content}

+ {review.sealed ? ( +

评价已提交,待揭晓

+ ) : ( + review.content && ( +

{review.content}

+ ) )}

{new Date(review.createdAt).toLocaleDateString("zh-CN")} diff --git a/app/(order)/review/[id]/page.tsx b/app/(order)/review/[id]/page.tsx index 4ed9f7a..0d25305 100644 --- a/app/(order)/review/[id]/page.tsx +++ b/app/(order)/review/[id]/page.tsx @@ -30,12 +30,11 @@ export default function ReviewPage({ params }: { params: Promise<{ id: string }>

评价已提交

-

- 你的评价已密封保存,待对方也提交评价后将同时揭晓 -

- +

等待对方提交评价中

+

双方都提交评价后将同时揭晓

+ + 返回订单详情 +
) }