226 lines
9.2 KiB
TypeScript
226 lines
9.2 KiB
TypeScript
import { CheckCircle, Clock, MapPin, MessageSquare, ShoppingBag, Star } from "lucide-react"
|
|
import Link from "next/link"
|
|
import { notFound } from "next/navigation"
|
|
import { FavoriteButton } from "@/components/favorite-button"
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardFooter,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card"
|
|
import { Separator } from "@/components/ui/separator"
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
|
import {
|
|
isFavorited as checkFavorited,
|
|
listPlayers,
|
|
listReviewsByTargetUser,
|
|
listServicesByPlayer,
|
|
} from "@/lib/api"
|
|
|
|
export default async function PlayerDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
|
const { id } = await params
|
|
const player = listPlayers().find((p) => p.id === id)
|
|
|
|
if (!player) {
|
|
notFound()
|
|
}
|
|
|
|
const playerReviews = listReviewsByTargetUser(player.id)
|
|
const playerServices =
|
|
player.services && player.services.length > 0
|
|
? player.services
|
|
: listServicesByPlayer(player.id)
|
|
const isFavorited = checkFavorited("u1", "player", player.id)
|
|
|
|
return (
|
|
<div className="container mx-auto py-8 px-4 max-w-5xl">
|
|
<div className="flex flex-col md:flex-row gap-8 mb-8">
|
|
<div className="flex-shrink-0">
|
|
<Avatar className="w-32 h-32 border-4 border-background shadow-lg">
|
|
<AvatarImage src={player.user.avatar} alt={player.user.nickname} />
|
|
<AvatarFallback>{player.user.nickname[0]}</AvatarFallback>
|
|
</Avatar>
|
|
</div>
|
|
|
|
<div className="flex-grow space-y-4">
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-3xl font-bold flex items-center gap-3">
|
|
{player.user.nickname}
|
|
<Badge
|
|
variant={player.status === "available" ? "default" : "secondary"}
|
|
className="text-sm"
|
|
>
|
|
{player.status === "available" ? "可接单" : "忙碌中"}
|
|
</Badge>
|
|
</h1>
|
|
<div className="flex items-center gap-2 mt-2 text-muted-foreground">
|
|
<div className="flex items-center text-yellow-500">
|
|
<Star className="w-4 h-4 fill-current" />
|
|
<span className="ml-1 font-medium text-foreground">{player.rating}</span>
|
|
</div>
|
|
<Separator orientation="vertical" className="h-4" />
|
|
<span>接单 {player.totalOrders}</span>
|
|
<Separator orientation="vertical" className="h-4" />
|
|
<span>完成率 {(player.completionRate * 100).toFixed(0)}%</span>
|
|
</div>
|
|
</div>
|
|
|
|
{player.shopId && (
|
|
<Link href={`/shop/${player.shopId}`}>
|
|
<Button variant="outline" className="gap-2">
|
|
<ShoppingBag className="w-4 h-4" />
|
|
所属店铺: {player.shopName}
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
<FavoriteButton
|
|
initialFavorited={isFavorited}
|
|
targetType="player"
|
|
targetId={player.id}
|
|
/>
|
|
</div>
|
|
|
|
<div className="bg-muted/50 p-4 rounded-lg">
|
|
<p className="text-sm leading-relaxed">
|
|
{player.user.bio || "这个打手很懒,什么都没写~"}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
{player.tags.map((tag) => (
|
|
<Badge key={tag} variant="secondary" className="px-3 py-1">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Tabs defaultValue="services" className="space-y-6">
|
|
<TabsList className="w-full justify-start border-b rounded-none h-auto p-0 bg-transparent">
|
|
<TabsTrigger
|
|
value="services"
|
|
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-6 py-3 text-base"
|
|
>
|
|
服务列表 ({playerServices.length})
|
|
</TabsTrigger>
|
|
<TabsTrigger
|
|
value="reviews"
|
|
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-6 py-3 text-base"
|
|
>
|
|
评价 ({playerReviews.length})
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="services" className="mt-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{playerServices.map((service) => (
|
|
<Card
|
|
key={service.id}
|
|
className="flex flex-col h-full hover:shadow-md transition-shadow"
|
|
>
|
|
<CardHeader>
|
|
<div className="flex justify-between items-start">
|
|
<Badge variant="outline">{service.gameName}</Badge>
|
|
<div className="text-lg font-bold text-primary">
|
|
¥{service.price}{" "}
|
|
<span className="text-sm text-muted-foreground font-normal">
|
|
/ {service.unit}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<CardTitle className="mt-2 text-xl">{service.title}</CardTitle>
|
|
<CardDescription className="line-clamp-2 mt-1">
|
|
{service.description}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="flex-grow space-y-3 text-sm">
|
|
{service.rankRange && (
|
|
<div className="flex items-center gap-2 text-muted-foreground">
|
|
<MapPin className="w-4 h-4" />
|
|
<span>段位: {service.rankRange}</span>
|
|
</div>
|
|
)}
|
|
<div className="flex items-start gap-2 text-muted-foreground">
|
|
<Clock className="w-4 h-4 mt-0.5" />
|
|
<div className="flex flex-col">
|
|
{service.availability.map((time) => (
|
|
<span key={time}>{time}</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button className="w-full" asChild>
|
|
<Link href={`/order/new?serviceId=${service.id}`}>立即下单</Link>
|
|
</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
))}
|
|
{playerServices.length === 0 && (
|
|
<div className="col-span-full text-center py-12 text-muted-foreground">
|
|
<p>暂无服务</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="reviews" className="mt-6">
|
|
<div className="space-y-6">
|
|
{playerReviews.length > 0 ? (
|
|
playerReviews.map((review) => (
|
|
<Card key={review.id}>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-start gap-4">
|
|
<Avatar>
|
|
<AvatarImage src={review.fromUserAvatar} alt={review.fromUserName} />
|
|
<AvatarFallback>{review.fromUserName[0]}</AvatarFallback>
|
|
</Avatar>
|
|
<div className="flex-grow space-y-2">
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<div className="font-medium">{review.fromUserName}</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{new Date(review.createdAt).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center text-yellow-500">
|
|
{[1, 2, 3, 4, 5].map((star) => (
|
|
<Star
|
|
key={star}
|
|
className={`w-4 h-4 ${star <= review.rating ? "fill-current" : "text-muted stroke-muted-foreground"}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<p className="text-sm text-foreground/90">{review.content}</p>
|
|
{review.sealed && (
|
|
<div className="flex items-center gap-1 text-xs text-green-600 bg-green-50 w-fit px-2 py-1 rounded">
|
|
<CheckCircle className="w-3 h-3" />
|
|
<span>平台认证评价</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))
|
|
) : (
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
<MessageSquare className="w-12 h-12 mx-auto mb-4 opacity-20" />
|
|
<p>暂无评价</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
)
|
|
}
|