Files
juwan-frontend/app/(order)/order/[id]/page.tsx
T
zetaloop cf0fea9926 refactor(orders): replace local state machine with minimal cache
Strip store/orders.ts to a thin local cache with setOrders and
updateOrder only. Remove all client-side state transition logic,
actor validation, chat sync, notification generation, and wallet
integration — these are now handled by the backend API.

Fix components/order-actions.tsx and stores that depended on
the removed order store methods (markDisputed, autoTimeout*).
2026-05-01 17:32:06 +08:00

321 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, getPlayerById, getUserById, listReviewsByOrder } from "@/lib/api"
import { statusLabels } from "@/lib/constants"
import type { OrderStatus, Player, User } from "@/lib/types"
import { useAuthStore } from "@/store/auth"
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 currentUserId = useAuthStore((state) => state.user?.id)
const [reviews, setReviews] = useState<Awaited<ReturnType<typeof listReviewsByOrder>>>([])
const [chatTarget, setChatTarget] = useState<User | null>(null)
const [player, setPlayer] = useState<Player | null>(null)
const [order, setOrder] = useState<Awaited<ReturnType<typeof getOrderById>> | undefined>(
undefined,
)
const [loading, setLoading] = useState(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 () => {
if (!order || !currentUserId) {
setPlayer(null)
setChatTarget(null)
return
}
try {
setPlayer(null)
setChatTarget(null)
const player = await getPlayerById(String(order.playerId))
if (!cancelled) setPlayer(player ?? null)
if (String(order.consumerId) === currentUserId) {
if (!cancelled) setChatTarget(player?.user ?? null)
return
}
if (player?.user.id === currentUserId) {
const consumer = await getUserById(String(order.consumerId))
if (!cancelled) setChatTarget(consumer ?? null)
}
} catch {
if (!cancelled) setPlayer(null)
if (!cancelled) setChatTarget(null)
}
})()
return () => {
cancelled = true
}
}, [currentUserId, order])
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])
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 statusSteps =
order.status === "disputed"
? disputedStatusSteps
: order.status === "cancelled"
? cancelledStatusSteps
: normalStatusSteps
const currentStepIndex = statusSteps.indexOf(order.status)
const isPlayerParticipant = player?.user.id === currentUserId
return (
<div className="container mx-auto py-8 px-4 max-w-2xl">
<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>
<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}
chatTargetId={chatTarget?.id}
isPlayerParticipant={isPlayerParticipant}
serviceId={order.service.id}
/>
</div>
)
}