feat(ui): refine order management pages

This commit is contained in:
zetaloop
2026-04-25 21:32:04 +08:00
parent 8b71e7e70e
commit e9a1bb4dac
2 changed files with 105 additions and 44 deletions
+56 -28
View File
@@ -1,10 +1,11 @@
"use client"
import { Badge } from "@/components/ui/badge"
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 } from "@/lib/api"
import {
@@ -37,6 +38,13 @@ const disputeStatusLabels: Record<string, string> = {
appealed: "申诉中",
}
const disputeStatusVariants: Record<string, StatusBadgeProps["status"]> = {
open: "warning",
reviewing: "info",
resolved: "success",
appealed: "info",
}
function deriveMinimalTimeline<TCreatedAt>(dispute: {
id: string
status: string
@@ -189,14 +197,16 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
if (loading) {
return (
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">...</div>
<div className="container mx-auto max-w-lg px-4 py-8">
<EmptyState title="争议加载中" icon={Clock} />
</div>
)
}
if (!order) {
return (
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">
<div className="container mx-auto max-w-lg px-4 py-8">
<EmptyState title="订单不存在" description="无法找到对应订单。" icon={FileText} />
</div>
)
}
@@ -206,8 +216,12 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
)
if (!isParticipant) {
return (
<div className="container mx-auto py-8 px-4 max-w-lg text-center text-muted-foreground">
访
<div className="container mx-auto max-w-lg px-4 py-8">
<EmptyState
title="无法访问争议页面"
description="仅该订单参与方可访问争议页面。"
icon={AlertTriangle}
/>
</div>
)
}
@@ -232,11 +246,13 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
</Link>
<Card className="hover:shadow-card-hover">
<Card className="border-border/80 shadow-sm">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle></CardTitle>
<Badge variant="outline">{disputeStatusLabels[existingDispute.status]}</Badge>
<StatusBadge status={disputeStatusVariants[existingDispute.status] ?? "neutral"}>
{disputeStatusLabels[existingDispute.status]}
</StatusBadge>
</div>
</CardHeader>
<CardContent className="space-y-5">
@@ -263,7 +279,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
{existingDispute.evidence.map((url) => (
<div
key={url}
className="relative h-20 w-20 rounded border overflow-hidden bg-muted"
className="relative h-20 w-20 overflow-hidden rounded-md border border-border/60 bg-muted/30"
>
<Image src={url} alt="发起方证据" fill unoptimized className="object-cover" />
</div>
@@ -284,7 +300,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
{existingDispute.respondentEvidence.map((url) => (
<div
key={url}
className="relative h-20 w-20 rounded border overflow-hidden bg-muted"
className="relative h-20 w-20 overflow-hidden rounded-md border border-border/60 bg-muted/30"
>
<Image
src={url}
@@ -323,7 +339,11 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
className="hidden"
onChange={(event) => handleFileSelect(event, setResponseFiles)}
/>
<Button variant="outline" onClick={() => responseFileInputRef.current?.click()}>
<Button
variant="outline"
className="border-border/60"
onClick={() => responseFileInputRef.current?.click()}
>
<Upload className="mr-1 h-4 w-4" />
</Button>
@@ -332,7 +352,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
{responseFiles.map((url, index) => (
<div
key={url}
className="relative h-16 w-16 rounded border overflow-hidden bg-muted"
className="relative h-16 w-16 overflow-hidden rounded-md border border-border/60 bg-muted/30"
>
<Image
src={url}
@@ -343,6 +363,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
/>
<button
type="button"
aria-label={`移除回应证据 ${index + 1}`}
className="absolute right-0 top-0 rounded-bl bg-background/90 p-0.5"
onClick={() => removeFile(index, responseFiles, setResponseFiles)}
>
@@ -405,6 +426,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
/>
<Button
variant="outline"
className="border-border/60"
onClick={() => {
if (!userId) return
void Promise.resolve(
@@ -430,9 +452,12 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
<div className="space-y-2">
<Label className="text-muted-foreground">线</Label>
<div className="space-y-2">
<div className="rounded-lg border border-border/60">
{timeline.map((item) => (
<div key={item.id} className="text-sm flex items-start justify-between gap-3">
<div
key={item.id}
className="flex items-start justify-between gap-3 border-b border-border/60 px-3 py-2 text-sm last:border-b-0"
>
<span>{item.content}</span>
<span className="text-xs text-muted-foreground shrink-0">
{new Date(item.createdAt).toLocaleString("zh-CN")}
@@ -449,15 +474,17 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
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">
{Math.floor(DISPUTE_TO_RESOLVED_MS / 1000)}
</p>
<Link href={`/order/${id}`} className="text-sm text-primary hover:underline">
</Link>
<div className="container mx-auto max-w-lg px-4 py-8">
<EmptyState
title="争议已提交,请等待平台处理"
description={`平台将在约 ${Math.floor(DISPUTE_TO_RESOLVED_MS / 1000)} 秒内给出模拟处理结果。`}
icon={AlertTriangle}
action={
<Button variant="outline" className="border-border/60" asChild>
<Link href={`/order/${id}`}></Link>
</Button>
}
/>
</div>
)
}
@@ -472,10 +499,10 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
</Link>
<Card className="hover:shadow-card-hover">
<Card className="border-border/80 shadow-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-yellow-500" />
<AlertTriangle className="h-5 w-5 text-warning" />
</CardTitle>
<p className="text-sm text-muted-foreground">{order.service.title}</p>
@@ -504,7 +531,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
/>
<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"
className="w-full rounded-md border-2 border-dashed border-border/60 bg-muted/20 p-6 text-center text-sm text-muted-foreground transition-colors hover:border-primary/60 hover:bg-muted/30 hover:text-foreground disabled:opacity-50"
onClick={() => fileInputRef.current?.click()}
disabled={files.length >= 5}
>
@@ -518,7 +545,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
{files.map((fileUrl, index) => (
<div
key={fileUrl}
className="relative h-16 w-16 rounded border overflow-hidden bg-muted"
className="relative h-16 w-16 overflow-hidden rounded-md border border-border/60 bg-muted/30"
>
<Image
src={fileUrl}
@@ -529,6 +556,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
/>
<button
type="button"
aria-label={`移除证据截图 ${index + 1}`}
className="absolute right-0 top-0 rounded-bl bg-background/90 p-0.5"
onClick={() => removeFile(index, files, setFiles)}
>
@@ -541,7 +569,7 @@ export default function DisputePage({ params }: { params: Promise<{ id: string }
<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">
<div className="space-y-1 rounded-lg border border-border/60 bg-muted/20 p-3 text-xs text-muted-foreground">
<p>· </p>
<p>· </p>
<p>· {Math.floor(DISPUTE_TO_RESOLVED_MS / 1000)} </p>