feat(ui): refine public detail pages
This commit is contained in:
@@ -10,10 +10,12 @@ import {
|
||||
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { getPlayerById, listReviewsByTargetUser, listServicesByPlayer } from "@/lib/api"
|
||||
import { CheckCircle, Clock, MapPin, MessageSquare, ShoppingBag, Star } from "lucide-react"
|
||||
import { Clock, Gamepad2, MapPin, MessageSquare, ShoppingBag, Star } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
@@ -34,7 +36,7 @@ export default async function PlayerDetailPage({ params }: { params: Promise<{ i
|
||||
<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-card">
|
||||
<Avatar className="w-32 h-32 border border-border shadow-sm">
|
||||
<AvatarImage src={player.user.avatar} alt={player.user.nickname} />
|
||||
<AvatarFallback>{player.user.nickname[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
@@ -45,16 +47,26 @@ export default async function PlayerDetailPage({ params }: { params: Promise<{ i
|
||||
<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"
|
||||
<StatusBadge
|
||||
status={
|
||||
player.status === "available"
|
||||
? "success"
|
||||
: player.status === "busy"
|
||||
? "warning"
|
||||
: "neutral"
|
||||
}
|
||||
className="text-sm font-normal"
|
||||
>
|
||||
{player.status === "available" ? "可接单" : "忙碌中"}
|
||||
</Badge>
|
||||
{player.status === "available"
|
||||
? "可接单"
|
||||
: player.status === "busy"
|
||||
? "忙碌中"
|
||||
: "离线"}
|
||||
</StatusBadge>
|
||||
</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" />
|
||||
<div className="flex items-center text-warning">
|
||||
<Star className="w-4 h-4 fill-warning" />
|
||||
<span className="ml-1 font-medium text-foreground">{player.rating}</span>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
@@ -75,11 +87,13 @@ export default async function PlayerDetailPage({ params }: { params: Promise<{ i
|
||||
<FavoriteButton initialFavorited={false} targetType="player" targetId={player.id} />
|
||||
</div>
|
||||
|
||||
<div className="bg-muted/50 p-4 rounded-xl">
|
||||
<p className="text-sm leading-relaxed">
|
||||
{player.user.bio || "这个打手很懒,什么都没写~"}
|
||||
</p>
|
||||
</div>
|
||||
{player.user.bio ? (
|
||||
<div className="bg-muted/50 p-4 rounded-xl border border-border/50">
|
||||
<p className="text-sm leading-relaxed">{player.user.bio}</p>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState className="min-h-[120px] p-6" title="暂无个人简介" />
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{player.tags.map((tag) => (
|
||||
@@ -103,7 +117,9 @@ export default async function PlayerDetailPage({ params }: { params: Promise<{ i
|
||||
<Card key={service.id} className="flex flex-col h-full">
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-start">
|
||||
<Badge variant="outline">{service.gameName}</Badge>
|
||||
<Badge variant="info" className="font-normal">
|
||||
{service.gameName}
|
||||
</Badge>
|
||||
<div className="text-lg font-bold text-primary">
|
||||
¥{service.price}{" "}
|
||||
<span className="text-sm text-muted-foreground font-normal">
|
||||
@@ -140,8 +156,12 @@ export default async function PlayerDetailPage({ params }: { params: Promise<{ i
|
||||
</Card>
|
||||
))}
|
||||
{playerServices.length === 0 && (
|
||||
<div className="col-span-full text-center py-12 text-muted-foreground">
|
||||
<p>暂无服务</p>
|
||||
<div className="col-span-full">
|
||||
<EmptyState
|
||||
title="暂无服务"
|
||||
description="该陪玩尚未添加任何服务项"
|
||||
icon={Gamepad2}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -166,21 +186,20 @@ export default async function PlayerDetailPage({ params }: { params: Promise<{ i
|
||||
{new Date(review.createdAt).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center text-yellow-500">
|
||||
<div className="flex items-center text-warning">
|
||||
{[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"}`}
|
||||
className={`w-4 h-4 ${star <= review.rating ? "fill-warning" : "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-full">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
<span>平台认证评价</span>
|
||||
</div>
|
||||
<StatusBadge status="success" className="text-xs font-normal">
|
||||
平台认证评价
|
||||
</StatusBadge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,10 +207,7 @@ export default async function PlayerDetailPage({ params }: { params: Promise<{ i
|
||||
</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>
|
||||
<EmptyState title="暂无评价" description="还没有收到任何评价" icon={MessageSquare} />
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -27,7 +27,7 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id:
|
||||
返回社区
|
||||
</Link>
|
||||
|
||||
<Card className="hover:shadow-card-hover">
|
||||
<Card className="shadow-sm border-border/80">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar className="h-10 w-10">
|
||||
@@ -76,7 +76,7 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id:
|
||||
<Link href={`/order/${post.linkedOrderId}`}>
|
||||
<div className="rounded-lg border bg-muted/30 p-3 text-sm hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Star className="h-3.5 w-3.5 text-yellow-500" />
|
||||
<Star className="h-3.5 w-3.5 text-warning fill-warning" />
|
||||
<span className="font-medium">关联订单</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">点击查看订单详情</p>
|
||||
|
||||
@@ -3,6 +3,7 @@ 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 { EmptyState } from "@/components/ui/empty-state"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { getShopById, listPlayersByShop, listReviews, listServices } from "@/lib/api"
|
||||
import { getShopSections } from "@/lib/domain/shop-template"
|
||||
@@ -61,8 +62,8 @@ export default async function ShopPage({ params }: PageProps) {
|
||||
<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" />
|
||||
<div className="flex items-center gap-1 text-warning">
|
||||
<Star className="w-4 h-4 fill-warning" />
|
||||
<span className="font-medium">{shop.rating}</span>
|
||||
<span className="text-muted-foreground">评分</span>
|
||||
</div>
|
||||
@@ -103,7 +104,16 @@ export default async function ShopPage({ params }: PageProps) {
|
||||
)
|
||||
|
||||
case "announcements":
|
||||
if (shop.announcements.length === 0) return null
|
||||
if (shop.announcements.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
key="announcements"
|
||||
title="暂无公告"
|
||||
description="店长还没有发布任何公告内容"
|
||||
icon={Megaphone}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Card key="announcements">
|
||||
<CardHeader>
|
||||
@@ -126,7 +136,16 @@ export default async function ShopPage({ params }: PageProps) {
|
||||
)
|
||||
|
||||
case "services":
|
||||
if (shopServices.length === 0) return null
|
||||
if (shopServices.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
key="services"
|
||||
title="暂无服务"
|
||||
description="这家店铺还没上架任何服务"
|
||||
icon={Gamepad2}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div key="services" className="space-y-4">
|
||||
<h3 className="text-xl font-bold flex items-center gap-2">
|
||||
@@ -138,7 +157,9 @@ export default async function ShopPage({ params }: PageProps) {
|
||||
<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>
|
||||
<Badge variant="info" className="font-normal">
|
||||
{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>
|
||||
@@ -168,7 +189,16 @@ export default async function ShopPage({ params }: PageProps) {
|
||||
)
|
||||
|
||||
case "players":
|
||||
if (shopPlayers.length === 0) return null
|
||||
if (shopPlayers.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
key="players"
|
||||
title="暂无打手"
|
||||
description="这家店目前还没有招募打手"
|
||||
icon={Users}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div key="players" className="space-y-4">
|
||||
<h3 className="text-xl font-bold flex items-center gap-2">
|
||||
@@ -188,18 +218,18 @@ export default async function ShopPage({ params }: PageProps) {
|
||||
<span
|
||||
className={`absolute bottom-0 right-0 w-4 h-4 border-2 border-background rounded-full ${
|
||||
player.status === "available"
|
||||
? "bg-green-500"
|
||||
? "bg-success"
|
||||
: player.status === "busy"
|
||||
? "bg-yellow-500"
|
||||
: "bg-gray-500"
|
||||
? "bg-warning"
|
||||
: "bg-neutral"
|
||||
}`}
|
||||
/>
|
||||
</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>
|
||||
<Star className="w-3 h-3 fill-warning text-warning" />
|
||||
<span className="text-foreground">{player.rating}</span>
|
||||
<span>•</span>
|
||||
<span>{player.totalOrders}单</span>
|
||||
</div>
|
||||
@@ -220,7 +250,16 @@ export default async function ShopPage({ params }: PageProps) {
|
||||
)
|
||||
|
||||
case "reviews":
|
||||
if (shopReviews.length === 0) return null
|
||||
if (shopReviews.length === 0) {
|
||||
return (
|
||||
<EmptyState
|
||||
key="reviews"
|
||||
title="暂无评价"
|
||||
description="这家店还没收到任何评价"
|
||||
icon={Star}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div key="reviews" className="space-y-4">
|
||||
<h3 className="text-xl font-bold flex items-center gap-2">
|
||||
@@ -246,7 +285,7 @@ export default async function ShopPage({ params }: PageProps) {
|
||||
key={`star-${star}`}
|
||||
className={`w-3 h-3 ${
|
||||
star <= review.rating
|
||||
? "fill-yellow-500 text-yellow-500"
|
||||
? "fill-warning text-warning"
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user