Files
juwan-frontend/app/(order)/order/[id]/page.tsx
T
2026-04-25 21:23:55 +08:00

334 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import OrderActions from "@/components/order-actions"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { EmptyState } from "@/components/ui/empty-state"
import { Separator } from "@/components/ui/separator"
import { StatusBadge } from "@/components/ui/status-badge"
import { getOrderById, listChatSessions, listReviewsByOrder } from "@/lib/api"
import { ORDER_ACCEPT_TIMEOUT_MS, ORDER_CLOSE_TIMEOUT_MS } from "@/lib/config/demo-timers"
import { statusLabels } from "@/lib/constants"
import type { OrderStatus } from "@/lib/types"
import { ArrowLeft, CheckCircle, Clock, Star } from "lucide-react"
import Link from "next/link"
import { use, useEffect, useState } from "react"
const normalStatusSteps: OrderStatus[] = [
"pending_payment",
"pending_accept",
"in_progress",
"pending_close",
"pending_review",
"completed",
]
const disputedStatusSteps: OrderStatus[] = [
"pending_payment",
"pending_accept",
"in_progress",
"pending_close",
"disputed",
]
const cancelledStatusSteps: OrderStatus[] = ["pending_payment", "pending_accept", "cancelled"]
type OrderStatusBadgeVariant = "success" | "warning" | "info" | "neutral" | "destructive"
const statusVariants: Record<OrderStatus, OrderStatusBadgeVariant> = {
pending_payment: "warning",
pending_accept: "info",
in_progress: "success",
pending_close: "info",
pending_review: "info",
disputed: "destructive",
completed: "success",
cancelled: "neutral",
}
export default function OrderDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params)
const [sessions, setSessions] = useState<Awaited<ReturnType<typeof listChatSessions>>>([])
const [reviews, setReviews] = useState<Awaited<ReturnType<typeof listReviewsByOrder>>>([])
const [order, setOrder] = useState<Awaited<ReturnType<typeof getOrderById>> | undefined>(
undefined,
)
const [loading, setLoading] = useState(true)
const [nowTs, setNowTs] = useState(0)
useEffect(() => {
let cancelled = false
void (async () => {
try {
const sessions = await Promise.resolve(listChatSessions())
if (cancelled) return
setSessions(sessions)
} catch {
if (cancelled) return
setSessions([])
}
})()
return () => {
cancelled = true
}
}, [])
useEffect(() => {
let cancelled = false
void (async () => {
try {
setLoading(true)
const order = await Promise.resolve(getOrderById(id))
if (cancelled) return
setOrder(order)
setLoading(false)
} catch {
if (cancelled) return
setOrder(undefined)
setLoading(false)
}
})()
return () => {
cancelled = true
}
}, [id])
useEffect(() => {
let cancelled = false
void (async () => {
try {
const reviews = await Promise.resolve(listReviewsByOrder(id))
if (cancelled) return
setReviews(reviews)
} catch {
if (cancelled) return
setReviews([])
}
})()
return () => {
cancelled = true
}
}, [id])
useEffect(() => {
if (!order) return
if (order.status !== "pending_accept" && order.status !== "pending_close") return
const timer = setInterval(() => {
setNowTs(Date.now())
}, 1000)
return () => clearInterval(timer)
}, [order])
if (loading) {
return (
<div className="container mx-auto max-w-2xl px-4 py-8">
<EmptyState title="加载中" description="正在读取订单详情..." icon={Clock} />
</div>
)
}
if (!order) {
return (
<div className="container mx-auto max-w-2xl px-4 py-8">
<EmptyState title="订单不存在" description="该订单可能已被删除或暂不可访问。" />
</div>
)
}
const chatSession = sessions.find((session) => session.type === "order" && session.orderId === id)
const statusSteps =
order.status === "disputed"
? disputedStatusSteps
: order.status === "cancelled"
? cancelledStatusSteps
: normalStatusSteps
const currentStepIndex = statusSteps.indexOf(order.status)
const timeoutHint = (() => {
if (order.status !== "pending_accept" && order.status !== "pending_close") return null
const base =
order.status === "pending_accept"
? new Date(order.createdAt).getTime()
: new Date(order.createdAt).getTime()
const timeoutMs =
order.status === "pending_accept" ? ORDER_ACCEPT_TIMEOUT_MS : ORDER_CLOSE_TIMEOUT_MS
const remainSeconds = Math.max(0, Math.ceil((timeoutMs - (nowTs - base)) / 1000))
return order.status === "pending_accept"
? `${Math.floor(ORDER_ACCEPT_TIMEOUT_MS / 1000)} 秒内无人接单,订单将自动取消(剩余 ${remainSeconds} 秒)`
: `${Math.floor(ORDER_CLOSE_TIMEOUT_MS / 1000)} 秒内未确认,订单将自动进入待评价(剩余 ${remainSeconds} 秒)`
})()
return (
<div className="container mx-auto py-8 px-4 max-w-3xl">
<Link
href="/orders"
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground mb-4"
>
<ArrowLeft className="h-4 w-4" />
</Link>
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold"></h1>
<StatusBadge status={statusVariants[order.status]}>
{statusLabels[order.status]}
</StatusBadge>
</div>
<Card className="mb-6 border-border/80 shadow-sm">
<CardContent className="pt-6">
<div className="flex items-center justify-between">
{statusSteps.map((step, i) => {
const isActive = i <= currentStepIndex
const isCurrent = i === currentStepIndex
return (
<div key={step} className="flex flex-col items-center gap-1 flex-1">
<div
className={`h-8 w-8 rounded-full flex items-center justify-center text-xs font-medium ${
isCurrent
? "bg-primary text-primary-foreground"
: isActive
? "border border-primary/30 bg-primary/10 text-primary"
: "border border-border/60 bg-muted/50 text-muted-foreground"
}`}
>
{isActive ? <CheckCircle className="h-4 w-4" /> : i + 1}
</div>
<span className="text-[10px] text-muted-foreground text-center">
{statusLabels[step]}
</span>
</div>
)
})}
</div>
</CardContent>
</Card>
{timeoutHint && (
<Card className="mb-6 border-border/80 shadow-sm">
<CardContent className="py-3 text-sm text-muted-foreground">{timeoutHint}</CardContent>
</Card>
)}
<Card className="mb-6 border-border/80 shadow-sm">
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"></span>
<span>{order.service.title}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"></span>
<span>{order.service.gameName}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"></span>
<span>
¥{order.service.price}/{order.service.unit}
</span>
</div>
<Separator />
<div className="flex justify-between text-sm">
<span className="text-muted-foreground"></span>
<Link href={`/player/${order.playerId}`} className="text-primary hover:underline">
{order.service.title}
</Link>
</div>
<Separator />
<div className="flex justify-between text-sm font-medium">
<span></span>
<span className="text-lg">¥{order.totalPrice}</span>
</div>
{order.note && (
<div className="text-sm">
<span className="text-muted-foreground">: </span>
{order.note}
</div>
)}
</CardContent>
</Card>
<Card className="mb-6 border-border/80 shadow-sm">
<CardHeader>
<CardTitle className="text-base">线</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<div className="flex items-center gap-2">
<Clock className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-muted-foreground">:</span>
{new Date(order.createdAt).toLocaleString("zh-CN")}
</div>
{order.acceptedAt && (
<div className="flex items-center gap-2">
<CheckCircle className="h-3.5 w-3.5 text-success" />
<span className="text-muted-foreground">:</span>
{new Date(order.acceptedAt).toLocaleString("zh-CN")}
</div>
)}
{order.completedAt && (
<div className="flex items-center gap-2">
<CheckCircle className="h-3.5 w-3.5 text-success" />
<span className="text-muted-foreground">:</span>
{new Date(order.completedAt).toLocaleString("zh-CN")}
</div>
)}
</CardContent>
</Card>
{reviews.length > 0 && (
<Card className="mb-6 border-border/80 shadow-sm">
<CardHeader>
<CardTitle className="text-base"></CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{reviews.map((review) => (
<div key={review.id} className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{review.fromUserName}</span>
<div className="flex">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={`star-${star}`}
className={`h-3.5 w-3.5 ${star <= review.rating ? "fill-warning text-warning" : "text-muted stroke-muted-foreground"}`}
/>
))}
</div>
</div>
{review.sealed ? (
<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">
{new Date(review.createdAt).toLocaleDateString("zh-CN")}
</p>
</div>
))}
</CardContent>
</Card>
)}
<OrderActions
orderId={order.id}
order={order}
onOrderChange={(next) => setOrder(next)}
initialStatus={order.status}
chatSessionId={chatSession?.id}
serviceId={order.service.id}
/>
</div>
)
}