feat: search, player detail, shop detail, order flow, chat, review, and dispute pages
This commit is contained in:
@@ -1,8 +1,126 @@
|
||||
export default function ChatDetailPage({ params: _params }: { params: Promise<{ id: string }> }) {
|
||||
"use client"
|
||||
|
||||
import { ArrowLeft, Lock, Send } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { use, useState } from "react"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { mockChatMessages, mockChatSessions } from "@/lib/mock-data"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export default function ChatDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = use(params)
|
||||
const session = mockChatSessions.find((s) => s.id === id)
|
||||
const messages = mockChatMessages.filter((m) => m.sessionId === id)
|
||||
const [input, setInput] = useState("")
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">
|
||||
会话不存在
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const other = session.participants[1]
|
||||
const currentUserId = session.participants[0].id
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<h1 className="text-2xl font-bold">聊天</h1>
|
||||
<p className="mt-2 text-muted-foreground">与打手沟通</p>
|
||||
<div className="flex flex-col h-[calc(100vh-3.5rem)]">
|
||||
<div className="border-b px-4 py-3 flex items-center gap-3">
|
||||
<Link href="/chat" className="text-muted-foreground hover:text-foreground">
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Link>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={other.avatar} />
|
||||
<AvatarFallback>{other.name[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<span className="text-sm font-medium">{other.name}</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
|
||||
{session.type === "order" ? "订单会话" : "咨询会话"}
|
||||
</Badge>
|
||||
{session.readonly && (
|
||||
<span className="text-[10px] text-muted-foreground flex items-center gap-0.5">
|
||||
<Lock className="h-3 w-3" />
|
||||
只读
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1 p-4">
|
||||
<div className="space-y-4 max-w-2xl mx-auto">
|
||||
{messages.map((msg) => {
|
||||
if (msg.type === "system") {
|
||||
return (
|
||||
<div key={msg.id} className="text-center">
|
||||
<span className="text-xs text-muted-foreground bg-muted px-2 py-1 rounded">
|
||||
{msg.content}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const isMine = msg.senderId === currentUserId
|
||||
return (
|
||||
<div key={msg.id} className={cn("flex gap-2", isMine && "flex-row-reverse")}>
|
||||
<Avatar className="h-8 w-8 shrink-0">
|
||||
<AvatarImage src={msg.senderAvatar} />
|
||||
<AvatarFallback>{msg.senderName[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className={cn("max-w-[70%]", isMine && "text-right")}>
|
||||
<p
|
||||
className={cn(
|
||||
"inline-block rounded-lg px-3 py-2 text-sm",
|
||||
isMine ? "bg-primary text-primary-foreground" : "bg-muted",
|
||||
)}
|
||||
>
|
||||
{msg.content}
|
||||
</p>
|
||||
<p className="text-[10px] text-muted-foreground mt-1">
|
||||
{new Date(msg.createdAt).toLocaleTimeString("zh-CN", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{!session.readonly ? (
|
||||
<div className="border-t p-4">
|
||||
<form
|
||||
className="flex gap-2 max-w-2xl mx-auto"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
setInput("")
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="输入消息..."
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button type="submit" size="icon" disabled={!input.trim()}>
|
||||
<Send className="h-4 w-4" />
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<div className="border-t p-4 text-center text-sm text-muted-foreground">
|
||||
<Lock className="h-4 w-4 inline mr-1" />
|
||||
订单已关闭,会话为只读状态
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,61 @@
|
||||
import { Lock, MessageSquare } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { mockChatSessions } from "@/lib/mock-data"
|
||||
|
||||
export default function ChatListPage() {
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<h1 className="text-2xl font-bold">消息</h1>
|
||||
<p className="mt-2 text-muted-foreground">咨询会话和订单会话</p>
|
||||
<div className="container mx-auto py-8 px-4 max-w-2xl">
|
||||
<h1 className="text-2xl font-bold mb-6">消息</h1>
|
||||
|
||||
<div className="space-y-2">
|
||||
{mockChatSessions.map((session) => {
|
||||
const other = session.participants[1]
|
||||
return (
|
||||
<Link key={session.id} href={`/chat/${session.id}`}>
|
||||
<Card className="hover:bg-accent/30 transition-colors">
|
||||
<CardContent className="flex items-center gap-3 py-3">
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarImage src={other.avatar} />
|
||||
<AvatarFallback>{other.name[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{other.name}</span>
|
||||
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
|
||||
{session.type === "order" ? "订单" : "咨询"}
|
||||
</Badge>
|
||||
{session.readonly && <Lock className="h-3 w-3 text-muted-foreground" />}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground truncate">{session.lastMessage}</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1 shrink-0">
|
||||
{session.lastMessageAt && (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{new Date(session.lastMessageAt).toLocaleDateString("zh-CN")}
|
||||
</span>
|
||||
)}
|
||||
{session.unreadCount > 0 && (
|
||||
<Badge className="h-4 min-w-4 px-1 flex items-center justify-center text-[10px]">
|
||||
{session.unreadCount}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
|
||||
{mockChatSessions.length === 0 && (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<MessageSquare className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||
暂无消息
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,176 @@
|
||||
export default function DisputePage({ params: _params }: { params: Promise<{ id: string }> }) {
|
||||
"use client"
|
||||
|
||||
import { AlertTriangle, ArrowLeft, Clock, FileText } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { use, useState } from "react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { mockDisputes, mockOrders } from "@/lib/mock-data"
|
||||
|
||||
const disputeStatusLabels: Record<string, string> = {
|
||||
open: "已提交",
|
||||
reviewing: "审核中",
|
||||
resolved: "已解决",
|
||||
appealed: "申诉中",
|
||||
}
|
||||
|
||||
export default function DisputePage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = use(params)
|
||||
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)
|
||||
|
||||
if (!order) {
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">
|
||||
订单不存在
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (existingDispute) {
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4 max-w-lg">
|
||||
<Link
|
||||
href={`/order/${id}`}
|
||||
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground mb-4"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
返回订单
|
||||
</Link>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>争议详情</CardTitle>
|
||||
<Badge variant="outline">{disputeStatusLabels[existingDispute.status]}</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">发起人:</span>
|
||||
{existingDispute.initiatorName}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">提交时间:</span>
|
||||
{new Date(existingDispute.createdAt).toLocaleString("zh-CN")}
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div>
|
||||
<Label className="text-muted-foreground">争议原因</Label>
|
||||
<p className="mt-1 text-sm">{existingDispute.reason}</p>
|
||||
</div>
|
||||
{existingDispute.evidence.length > 0 && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground">证据截图</Label>
|
||||
<div className="mt-1 flex gap-2">
|
||||
{existingDispute.evidence.map((url) => (
|
||||
<div
|
||||
key={url}
|
||||
className="h-20 w-20 rounded border bg-muted flex items-center justify-center text-xs text-muted-foreground"
|
||||
>
|
||||
截图
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{existingDispute.result && (
|
||||
<>
|
||||
<Separator />
|
||||
<div>
|
||||
<Label className="text-muted-foreground">仲裁结果</Label>
|
||||
<p className="mt-1 text-sm font-medium">
|
||||
{existingDispute.result === "full_refund"
|
||||
? "全额退款"
|
||||
: existingDispute.result === "full_payment"
|
||||
? "全额支付给打手"
|
||||
: "部分退款"}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (submitted) {
|
||||
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">
|
||||
平台将在 3 个工作日内审核你的争议申请,期间聊天会话仍可使用。
|
||||
</p>
|
||||
<Button asChild>
|
||||
<Link href={`/order/${id}`}>返回订单</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<h1 className="text-2xl font-bold">争议仲裁</h1>
|
||||
<p className="mt-2 text-muted-foreground">提交争议说明和证据</p>
|
||||
<div className="container mx-auto py-8 px-4 max-w-lg">
|
||||
<Link
|
||||
href={`/order/${id}`}
|
||||
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground mb-4"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
返回订单
|
||||
</Link>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-500" />
|
||||
发起争议
|
||||
</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{order.service.title} · {order.playerName}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dispute-reason">争议原因</Label>
|
||||
<Textarea
|
||||
id="dispute-reason"
|
||||
placeholder="请详细描述你遇到的问题..."
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>上传证据截图</Label>
|
||||
<div className="border-2 border-dashed rounded-md p-6 text-center text-sm text-muted-foreground">
|
||||
点击或拖拽上传截图(最多5张)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground space-y-1">
|
||||
<p>· 提交争议后,订单资金将继续托管</p>
|
||||
<p>· 聊天记录将作为证据保留</p>
|
||||
<p>· 平台将在 3 个工作日内审核</p>
|
||||
<p>· 对仲裁结果不满可申诉一次</p>
|
||||
</div>
|
||||
|
||||
<Button className="w-full" disabled={!reason.trim()} onClick={() => setSubmitted(true)}>
|
||||
提交争议
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,240 @@
|
||||
export default function OrderDetailPage({ params: _params }: { params: Promise<{ id: string }> }) {
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
MessageSquare,
|
||||
RefreshCw,
|
||||
Star,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { notFound } from "next/navigation"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { mockOrders, mockReviews } from "@/lib/mock-data"
|
||||
import type { OrderStatus } from "@/lib/types"
|
||||
|
||||
const statusLabels: Record<OrderStatus, string> = {
|
||||
pending_payment: "待支付",
|
||||
pending_accept: "待接单",
|
||||
in_progress: "进行中",
|
||||
pending_close: "待结单",
|
||||
pending_review: "待评价",
|
||||
disputed: "争议中",
|
||||
completed: "已完成",
|
||||
cancelled: "已取消",
|
||||
}
|
||||
|
||||
const statusSteps: OrderStatus[] = [
|
||||
"pending_payment",
|
||||
"pending_accept",
|
||||
"in_progress",
|
||||
"pending_close",
|
||||
"pending_review",
|
||||
"completed",
|
||||
]
|
||||
|
||||
export default async function OrderDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const order = mockOrders.find((o) => o.id === id)
|
||||
if (!order) notFound()
|
||||
|
||||
const reviews = mockReviews.filter((r) => r.orderId === id)
|
||||
const currentStepIndex = statusSteps.indexOf(order.status)
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<h1 className="text-2xl font-bold">订单详情</h1>
|
||||
<p className="mt-2 text-muted-foreground">订单状态、聊天、评价、争议</p>
|
||||
<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>
|
||||
<Badge variant="outline">{statusLabels[order.status]}</Badge>
|
||||
</div>
|
||||
|
||||
{order.status !== "disputed" && order.status !== "cancelled" && (
|
||||
<Card className="mb-6">
|
||||
<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
|
||||
? "bg-primary/20 text-primary"
|
||||
: "bg-muted 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">
|
||||
<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.playerName}
|
||||
</Link>
|
||||
</div>
|
||||
{order.shopName && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-muted-foreground">店铺</span>
|
||||
<Link href={`/shop/${order.shopId}`} className="text-primary hover:underline">
|
||||
{order.shopName}
|
||||
</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">
|
||||
<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-green-500" />
|
||||
<span className="text-muted-foreground">接单时间:</span>
|
||||
{new Date(order.acceptedAt).toLocaleString("zh-CN")}
|
||||
</div>
|
||||
)}
|
||||
{order.closedAt && (
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
|
||||
<span className="text-muted-foreground">结单时间:</span>
|
||||
{new Date(order.closedAt).toLocaleString("zh-CN")}
|
||||
</div>
|
||||
)}
|
||||
{order.completedAt && (
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="h-3.5 w-3.5 text-green-500" />
|
||||
<span className="text-muted-foreground">完成时间:</span>
|
||||
{new Date(order.completedAt).toLocaleString("zh-CN")}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{reviews.length > 0 && (
|
||||
<Card className="mb-6">
|
||||
<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-yellow-400 text-yellow-400" : "text-muted"}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{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>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{(order.status === "in_progress" || order.status === "pending_close") && (
|
||||
<Button asChild>
|
||||
<Link href={`/chat/chat1`}>
|
||||
<MessageSquare className="mr-1 h-4 w-4" />
|
||||
聊天
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{order.status === "pending_review" && (
|
||||
<Button asChild>
|
||||
<Link href={`/review/${order.id}`}>
|
||||
<Star className="mr-1 h-4 w-4" />
|
||||
评价
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{["in_progress", "pending_close"].includes(order.status) && (
|
||||
<Button variant="destructive" asChild>
|
||||
<Link href={`/dispute/${order.id}`}>
|
||||
<AlertTriangle className="mr-1 h-4 w-4" />
|
||||
发起争议
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{order.status === "completed" && (
|
||||
<Button variant="outline">
|
||||
<RefreshCw className="mr-1 h-4 w-4" />
|
||||
再来一单
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+125
-2
@@ -1,8 +1,131 @@
|
||||
"use client"
|
||||
|
||||
import { Clock, MessageSquare, RefreshCw } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useState } from "react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { mockOrders } from "@/lib/mock-data"
|
||||
import type { OrderStatus } from "@/lib/types"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useAuthStore } from "@/store/auth"
|
||||
|
||||
const statusLabels: Record<OrderStatus, string> = {
|
||||
pending_payment: "待支付",
|
||||
pending_accept: "待接单",
|
||||
in_progress: "进行中",
|
||||
pending_close: "待结单",
|
||||
pending_review: "待评价",
|
||||
disputed: "争议中",
|
||||
completed: "已完成",
|
||||
cancelled: "已取消",
|
||||
}
|
||||
|
||||
const statusColors: Record<OrderStatus, string> = {
|
||||
pending_payment: "bg-yellow-100 text-yellow-800",
|
||||
pending_accept: "bg-blue-100 text-blue-800",
|
||||
in_progress: "bg-green-100 text-green-800",
|
||||
pending_close: "bg-orange-100 text-orange-800",
|
||||
pending_review: "bg-purple-100 text-purple-800",
|
||||
disputed: "bg-red-100 text-red-800",
|
||||
completed: "bg-gray-100 text-gray-800",
|
||||
cancelled: "bg-gray-100 text-gray-500",
|
||||
}
|
||||
|
||||
type TabFilter = "all" | "active" | "completed" | "disputed"
|
||||
|
||||
export default function OrderListPage() {
|
||||
const [tab, setTab] = useState<TabFilter>("all")
|
||||
const { currentRole } = useAuthStore()
|
||||
|
||||
const filtered = mockOrders.filter((order) => {
|
||||
if (tab === "active")
|
||||
return ["pending_accept", "in_progress", "pending_close", "pending_review"].includes(
|
||||
order.status,
|
||||
)
|
||||
if (tab === "completed") return order.status === "completed" || order.status === "cancelled"
|
||||
if (tab === "disputed") return order.status === "disputed"
|
||||
return true
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<h1 className="text-2xl font-bold">我的订单</h1>
|
||||
<p className="mt-2 text-muted-foreground">查看和管理订单</p>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-2xl font-bold">我的订单</h1>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{currentRole === "consumer"
|
||||
? "消费者视角"
|
||||
: currentRole === "player"
|
||||
? "打手视角"
|
||||
: "店主视角"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Tabs value={tab} onValueChange={(v) => setTab(v as TabFilter)}>
|
||||
<TabsList>
|
||||
<TabsTrigger value="all">全部</TabsTrigger>
|
||||
<TabsTrigger value="active">进行中</TabsTrigger>
|
||||
<TabsTrigger value="completed">已完成</TabsTrigger>
|
||||
<TabsTrigger value="disputed">争议</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value={tab} className="mt-4 space-y-3">
|
||||
{filtered.length === 0 ? (
|
||||
<div className="text-center py-12 text-muted-foreground">暂无订单</div>
|
||||
) : (
|
||||
filtered.map((order) => (
|
||||
<Card key={order.id}>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">{order.service.title}</CardTitle>
|
||||
<Badge className={cn("text-xs", statusColors[order.status])}>
|
||||
{statusLabels[order.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{currentRole === "consumer"
|
||||
? `打手: ${order.playerName}`
|
||||
: `消费者: ${order.consumerName}`}
|
||||
{order.shopName && ` · ${order.shopName}`}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">¥{order.totalPrice}</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
{new Date(order.createdAt).toLocaleDateString("zh-CN")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{order.status === "in_progress" && (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href={`/chat/chat1`}>
|
||||
<MessageSquare className="mr-1 h-3.5 w-3.5" />
|
||||
聊天
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{order.status === "completed" && (
|
||||
<Button variant="outline" size="sm">
|
||||
<RefreshCw className="mr-1 h-3.5 w-3.5" />
|
||||
再来一单
|
||||
</Button>
|
||||
)}
|
||||
<Button size="sm" asChild>
|
||||
<Link href={`/order/${order.id}`}>查看详情</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,110 @@
|
||||
export default function ReviewPage({ params: _params }: { params: Promise<{ id: string }> }) {
|
||||
"use client"
|
||||
|
||||
import { ArrowLeft, Lock, Star } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { use, useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { mockOrders } from "@/lib/mock-data"
|
||||
|
||||
export default function ReviewPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = use(params)
|
||||
const order = mockOrders.find((o) => o.id === id)
|
||||
const [rating, setRating] = useState(0)
|
||||
const [hoverRating, setHoverRating] = useState(0)
|
||||
const [content, setContent] = useState("")
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
|
||||
if (!order) {
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4 text-center text-muted-foreground">
|
||||
订单不存在
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (submitted) {
|
||||
return (
|
||||
<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" />
|
||||
<h2 className="text-xl font-bold">评价已提交</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
你的评价已密封保存,待对方也提交评价后将同时揭晓
|
||||
</p>
|
||||
<Button asChild>
|
||||
<Link href={`/order/${id}`}>返回订单</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<h1 className="text-2xl font-bold">评价</h1>
|
||||
<p className="mt-2 text-muted-foreground">对本次服务进行评价</p>
|
||||
<div className="container mx-auto py-8 px-4 max-w-lg">
|
||||
<Link
|
||||
href={`/order/${id}`}
|
||||
className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground mb-4"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
返回订单
|
||||
</Link>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>评价服务</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{order.service.title} · {order.playerName}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label>评分</Label>
|
||||
<div className="flex gap-1">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<button
|
||||
key={star}
|
||||
type="button"
|
||||
onClick={() => setRating(star)}
|
||||
onMouseEnter={() => setHoverRating(star)}
|
||||
onMouseLeave={() => setHoverRating(0)}
|
||||
className="p-0.5"
|
||||
>
|
||||
<Star
|
||||
className={`h-8 w-8 transition-colors ${
|
||||
star <= (hoverRating || rating)
|
||||
? "fill-yellow-400 text-yellow-400"
|
||||
: "text-muted"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="review-content">评价内容(可选)</Label>
|
||||
<Textarea
|
||||
id="review-content"
|
||||
placeholder="分享你的体验..."
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md bg-muted/50 p-3 text-xs text-muted-foreground flex items-start gap-2">
|
||||
<Lock className="h-4 w-4 shrink-0 mt-0.5" />
|
||||
<span>
|
||||
评价采用密封机制:你的评价将在双方都提交后同时揭晓,确保评价的真实性和公正性。
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button className="w-full" disabled={rating === 0} onClick={() => setSubmitted(true)}>
|
||||
提交评价
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user