feat(ui): refine public discovery pages

This commit is contained in:
zetaloop
2026-04-25 20:12:23 +08:00
parent 0999f1905e
commit 93b880f932
3 changed files with 79 additions and 98 deletions
+51 -73
View File
@@ -5,6 +5,7 @@ import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import { EmptyState } from "@/components/ui/empty-state"
import { IconInput } from "@/components/ui/icon-input"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
@@ -26,6 +27,7 @@ import {
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
import { StatusBadge } from "@/components/ui/status-badge"
import { Switch } from "@/components/ui/switch"
import { listGames } from "@/lib/api"
import { searchCatalog } from "@/lib/api/search"
@@ -33,18 +35,7 @@ import { GameIcon } from "@/lib/game-icons"
import type { SearchResponse, SearchResultItem, SearchSort } from "@/lib/search/types"
import type { Game, Player } from "@/lib/types"
import { cn } from "@/lib/utils"
import {
CheckCircle2,
Clock,
Filter,
Gamepad2,
Search,
SlidersHorizontal,
Star,
Store,
User,
XCircle,
} from "lucide-react"
import { Filter, Gamepad2, Search, SlidersHorizontal, Star, Store, User } from "lucide-react"
import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation"
import { Suspense, useCallback, useEffect, useState } from "react"
@@ -70,34 +61,6 @@ function resetPagination(params: URLSearchParams) {
params.set("offset", "0")
}
function StatusBadge({ status }: { status: Player["status"] }) {
switch (status) {
case "available":
return (
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200 gap-1">
<CheckCircle2 className="w-3 h-3" />
</Badge>
)
case "busy":
return (
<Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-200 gap-1">
<Clock className="w-3 h-3" />
</Badge>
)
case "offline":
return (
<Badge variant="outline" className="bg-gray-50 text-gray-500 border-gray-200 gap-1">
<XCircle className="w-3 h-3" />
线
</Badge>
)
default:
return null
}
}
function PlayerCard({ player }: { player: Player }) {
const minPrice =
!player.services || player.services.length === 0
@@ -111,7 +74,7 @@ function PlayerCard({ player }: { player: Player }) {
return (
<Link href={`/player/${player.id}`} className="block h-full">
<Card className="h-full overflow-hidden flex flex-col p-0 gap-0">
<Card className="h-full overflow-hidden flex flex-col p-0 gap-0 shadow-sm border-border/80">
<CardHeader className="p-4 pb-2 flex flex-row items-start justify-between space-y-0">
<div className="flex items-center gap-3">
<Avatar className="h-12 w-12 border-2 border-background shadow-sm">
@@ -122,26 +85,43 @@ function PlayerCard({ player }: { player: Player }) {
<h3 className="font-semibold text-base leading-none mb-1">{player.user.nickname}</h3>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
{player.shopName ? (
<span className="flex items-center gap-0.5 text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded-full">
<Badge variant="info" className="gap-0.5 px-1.5 py-0.5 rounded-full font-normal">
<Store className="w-3 h-3" />
{player.shopName}
</span>
</Badge>
) : (
<span className="flex items-center gap-0.5 text-gray-600 bg-gray-100 px-1.5 py-0.5 rounded-full">
<Badge
variant="neutral"
className="gap-0.5 px-1.5 py-0.5 rounded-full font-normal"
>
<User className="w-3 h-3" />
</span>
</Badge>
)}
</div>
</div>
</div>
<StatusBadge status={player.status} />
<StatusBadge
status={
player.status === "available"
? "success"
: player.status === "busy"
? "warning"
: "neutral"
}
>
{player.status === "available"
? "可接单"
: player.status === "busy"
? "忙碌中"
: "离线"}
</StatusBadge>
</CardHeader>
<CardContent className="p-4 pt-2 flex-grow">
<div className="flex items-center gap-4 mb-3 text-sm">
<div className="flex items-center gap-1 text-yellow-500 font-medium">
<Star className="w-4 h-4 fill-current" />
<div className="flex items-center gap-1 text-warning font-medium">
<Star className="w-4 h-4 fill-warning text-warning" />
{player.rating.toFixed(1)}
</div>
<div className="text-muted-foreground"> {player.totalOrders}</div>
@@ -194,7 +174,7 @@ type ShopSearchItem = Extract<SearchResultItem, { type: "shop" }>
function ShopCard({ item }: { item: ShopSearchItem }) {
return (
<Link href={`/shop/${item.shop.id}`} className="block h-full">
<Card className="h-full overflow-hidden flex flex-col p-0 gap-0">
<Card className="h-full overflow-hidden flex flex-col p-0 gap-0 shadow-sm border-border/80">
<CardHeader className="p-4 pb-2 flex flex-row items-start justify-between space-y-0">
<div className="flex items-center gap-3">
<Avatar className="h-12 w-12 border-2 border-background shadow-sm">
@@ -204,20 +184,22 @@ function ShopCard({ item }: { item: ShopSearchItem }) {
<div>
<h3 className="font-semibold text-base leading-none mb-1">{item.shop.name}</h3>
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<span className="flex items-center gap-0.5 text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded-full">
<Badge variant="info" className="gap-0.5 px-1.5 py-0.5 rounded-full font-normal">
<Store className="w-3 h-3" />
</span>
</Badge>
</div>
</div>
</div>
<StatusBadge status={item.hasAvailable ? "available" : "busy"} />
<StatusBadge status={item.hasAvailable ? "success" : "warning"}>
{item.hasAvailable ? "可接单" : "忙碌中"}
</StatusBadge>
</CardHeader>
<CardContent className="p-4 pt-2 flex-grow">
<div className="flex items-center gap-4 mb-3 text-sm">
<div className="flex items-center gap-1 text-yellow-500 font-medium">
<Star className="w-4 h-4 fill-current" />
<div className="flex items-center gap-1 text-warning font-medium">
<Star className="w-4 h-4 fill-warning text-warning" />
{item.shop.rating}
</div>
<div className="text-muted-foreground"> {item.shop.totalOrders}</div>
@@ -585,7 +567,7 @@ function SearchPageContent() {
icon={<Search />}
type="search"
placeholder="搜索陪玩、游戏、标签..."
className="border-border bg-card shadow-card transition-shadow focus:shadow-card-hover"
className="border-border bg-card shadow-sm transition-shadow focus-visible:shadow-md"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
@@ -730,24 +712,20 @@ function SearchPageContent() {
))}
</div>
) : (
<div className="text-center py-20 bg-muted/30 rounded-lg border border-dashed">
<div className="mx-auto w-12 h-12 rounded-full bg-muted flex items-center justify-center mb-4">
<Search className="w-6 h-6 text-muted-foreground" />
</div>
<h3 className="text-lg font-medium"></h3>
<p className="text-muted-foreground mt-1 max-w-sm mx-auto">
{error ? "请求失败,请稍后重试" : "尝试调整筛选条件或更换搜索关键词"}
</p>
<Button
variant="outline"
className="mt-4 rounded-full"
onClick={() => {
clearAllFilters()
}}
>
</Button>
</div>
<EmptyState
icon={Search}
title="未找到相关陪玩"
description={error ? "请求失败,请稍后重试" : "尝试调整筛选条件或更换搜索关键词"}
action={
<Button
variant="outline"
className="rounded-full"
onClick={() => clearAllFilters()}
>
</Button>
}
/>
)}
{canLoadMore && (