278 lines
13 KiB
TypeScript
278 lines
13 KiB
TypeScript
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, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { Separator } from "@/components/ui/separator"
|
||
import { getShopById, listPlayersByShop, listReviews, listServices } from "@/lib/api"
|
||
import { Gamepad2, Megaphone, ShoppingBag, Star, Users } from "lucide-react"
|
||
import Image from "next/image"
|
||
import Link from "next/link"
|
||
import { notFound } from "next/navigation"
|
||
|
||
interface PageProps {
|
||
params: Promise<{ id: string }>
|
||
}
|
||
|
||
export default async function ShopPage({ params }: PageProps) {
|
||
const { id } = await params
|
||
const shop = await getShopById(id)
|
||
|
||
if (!shop) {
|
||
notFound()
|
||
}
|
||
|
||
const [shopPlayers, allServices] = await Promise.all([listPlayersByShop(shop.id), listServices()])
|
||
const playerIds = shopPlayers.map((p) => p.id)
|
||
const shopServices = allServices.filter((s) => playerIds.includes(s.playerId))
|
||
const shopReviews = listReviews().filter((r) => playerIds.includes(r.toUserId))
|
||
const sortedSections = [...shop.templateConfig.sections]
|
||
.filter((s) => s.enabled)
|
||
.sort((a, b) => a.order - b.order)
|
||
|
||
return (
|
||
<div className="container mx-auto py-6 space-y-8">
|
||
{sortedSections.map((section) => {
|
||
switch (section.type) {
|
||
case "banner":
|
||
return (
|
||
<div
|
||
key="banner"
|
||
className="relative h-48 md:h-64 rounded-xl overflow-hidden bg-muted"
|
||
>
|
||
{shop.banner ? (
|
||
<Image src={shop.banner} alt={shop.name} fill className="object-cover" />
|
||
) : (
|
||
<div className="w-full h-full bg-gradient-to-r from-primary/20 to-primary/10" />
|
||
)}
|
||
<div className="absolute inset-0 bg-black/40 flex items-center justify-center">
|
||
<h1 className="text-3xl md:text-5xl font-bold text-primary-foreground text-center px-4">
|
||
{shop.name}
|
||
</h1>
|
||
</div>
|
||
</div>
|
||
)
|
||
|
||
case "intro":
|
||
return (
|
||
<Card key="intro">
|
||
<CardContent className="pt-6">
|
||
<div className="flex flex-col md:flex-row gap-6 items-start">
|
||
<div className="flex-1 space-y-4">
|
||
<h2 className="text-2xl font-bold">{shop.name}</h2>
|
||
<p className="text-muted-foreground">{shop.description}</p>
|
||
<div className="flex flex-wrap gap-4 text-sm">
|
||
<div className="flex items-center gap-1">
|
||
<Star className="w-4 h-4 text-yellow-500 fill-yellow-500" />
|
||
<span className="font-medium">{shop.rating}</span>
|
||
<span className="text-muted-foreground">评分</span>
|
||
</div>
|
||
<Separator orientation="vertical" className="h-4" />
|
||
<div className="flex items-center gap-1">
|
||
<ShoppingBag className="w-4 h-4" />
|
||
<span className="font-medium">{shop.totalOrders}</span>
|
||
<span className="text-muted-foreground">总订单</span>
|
||
</div>
|
||
<Separator orientation="vertical" className="h-4" />
|
||
<div className="flex items-center gap-1">
|
||
<Users className="w-4 h-4" />
|
||
<span className="font-medium">{shop.playerCount}</span>
|
||
<span className="text-muted-foreground">打手</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-4">
|
||
<Avatar className="w-16 h-16">
|
||
<AvatarImage src={shop.owner.avatar} />
|
||
<AvatarFallback>{shop.owner.nickname[0]}</AvatarFallback>
|
||
</Avatar>
|
||
<div>
|
||
<p className="font-medium">店长:{shop.owner.nickname}</p>
|
||
<p className="text-sm text-muted-foreground">
|
||
{shop.owner.bio || "暂无介绍"}
|
||
</p>
|
||
</div>
|
||
<FavoriteButton
|
||
initialFavorited={false}
|
||
targetType="shop"
|
||
targetId={shop.id}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
|
||
case "announcements":
|
||
if (shop.announcements.length === 0) return null
|
||
return (
|
||
<Card key="announcements">
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Megaphone className="w-5 h-5 text-primary" />
|
||
店铺公告
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<ul className="space-y-2">
|
||
{shop.announcements.map((announcement) => (
|
||
<li key={announcement} className="flex items-start gap-2 text-sm">
|
||
<span className="mt-1.5 w-1.5 h-1.5 rounded-full bg-primary shrink-0" />
|
||
<span>{announcement}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
|
||
case "services":
|
||
if (shopServices.length === 0) return null
|
||
return (
|
||
<div key="services" className="space-y-4">
|
||
<h3 className="text-xl font-bold flex items-center gap-2">
|
||
<Gamepad2 className="w-5 h-5" />
|
||
热门服务
|
||
</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{shopServices.map((service) => (
|
||
<Card key={service.id} className="flex flex-col h-full">
|
||
<CardHeader className="pb-2">
|
||
<div className="flex justify-between items-start">
|
||
<Badge variant="outline">{service.gameName}</Badge>
|
||
<div className="text-right">
|
||
<span className="text-lg font-bold text-primary">¥{service.price}</span>
|
||
<span className="text-xs text-muted-foreground">/{service.unit}</span>
|
||
</div>
|
||
</div>
|
||
<CardTitle className="text-base mt-2">{service.title}</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="flex-grow flex flex-col">
|
||
<p className="text-sm text-muted-foreground line-clamp-2 mb-2">
|
||
{service.description}
|
||
</p>
|
||
{service.rankRange && (
|
||
<div className="text-xs bg-muted px-2 py-1 rounded-md inline-block w-fit">
|
||
段位:{service.rankRange}
|
||
</div>
|
||
)}
|
||
<div className="mt-auto pt-3">
|
||
<Button className="w-full" size="sm" asChild>
|
||
<Link href={`/order/new?serviceId=${service.id}`}>立即下单</Link>
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
|
||
case "players":
|
||
if (shopPlayers.length === 0) return null
|
||
return (
|
||
<div key="players" className="space-y-4">
|
||
<h3 className="text-xl font-bold flex items-center gap-2">
|
||
<Users className="w-5 h-5" />
|
||
明星陪玩
|
||
</h3>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||
{shopPlayers.map((player) => (
|
||
<Link key={player.id} href={`/player/${player.id}`}>
|
||
<Card className="h-full">
|
||
<CardContent className="pt-6 text-center space-y-3">
|
||
<div className="relative inline-block">
|
||
<Avatar className="w-20 h-20 mx-auto">
|
||
<AvatarImage src={player.user.avatar} />
|
||
<AvatarFallback>{player.user.nickname[0]}</AvatarFallback>
|
||
</Avatar>
|
||
<span
|
||
className={`absolute bottom-0 right-0 w-4 h-4 border-2 border-background rounded-full ${
|
||
player.status === "available"
|
||
? "bg-green-500"
|
||
: player.status === "busy"
|
||
? "bg-yellow-500"
|
||
: "bg-gray-500"
|
||
}`}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<h4 className="font-bold">{player.user.nickname}</h4>
|
||
<div className="flex items-center justify-center gap-1 text-sm text-muted-foreground mt-1">
|
||
<Star className="w-3 h-3 fill-yellow-500 text-yellow-500" />
|
||
<span>{player.rating}</span>
|
||
<span>•</span>
|
||
<span>{player.totalOrders}单</span>
|
||
</div>
|
||
</div>
|
||
<div className="flex flex-wrap justify-center gap-1">
|
||
{player.games.slice(0, 3).map((game) => (
|
||
<Badge key={game} variant="secondary" className="text-xs">
|
||
{game}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
|
||
case "reviews":
|
||
if (shopReviews.length === 0) return null
|
||
return (
|
||
<div key="reviews" className="space-y-4">
|
||
<h3 className="text-xl font-bold flex items-center gap-2">
|
||
<Star className="w-5 h-5" />
|
||
最新评价
|
||
</h3>
|
||
<div className="space-y-4">
|
||
{shopReviews.map((review) => (
|
||
<Card key={review.id}>
|
||
<CardContent className="pt-6">
|
||
<div className="flex gap-4">
|
||
<Avatar>
|
||
<AvatarImage src={review.fromUserAvatar} />
|
||
<AvatarFallback>{review.fromUserName[0]}</AvatarFallback>
|
||
</Avatar>
|
||
<div className="flex-1 space-y-1">
|
||
<div className="flex justify-between items-start">
|
||
<div>
|
||
<p className="font-medium text-sm">{review.fromUserName}</p>
|
||
<div className="flex items-center gap-0.5">
|
||
{[1, 2, 3, 4, 5].map((star) => (
|
||
<Star
|
||
key={`star-${star}`}
|
||
className={`w-3 h-3 ${
|
||
star <= review.rating
|
||
? "fill-yellow-500 text-yellow-500"
|
||
: "text-muted-foreground"
|
||
}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<span className="text-xs text-muted-foreground">
|
||
{new Date(review.createdAt).toLocaleDateString()}
|
||
</span>
|
||
</div>
|
||
<p className="text-sm text-muted-foreground">{review.content}</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)
|
||
|
||
default:
|
||
return null
|
||
}
|
||
})}
|
||
</div>
|
||
)
|
||
}
|