"use client" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" 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 { IconInput } from "@/components/ui/icon-input" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Separator } from "@/components/ui/separator" import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, } from "@/components/ui/sheet" import { Switch } from "@/components/ui/switch" import { listGames } from "@/lib/api" import { searchCatalog } from "@/lib/api/search" 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 Link from "next/link" import { useRouter, useSearchParams } from "next/navigation" import { Suspense, useCallback, useEffect, useState } from "react" const SEARCH_SORTS: ReadonlySet = new Set([ "composite", "rating", "orders", "price_asc", "price_desc", ]) const DEFAULT_LIMIT = 12 function numberParam(value: string | null, fallback: number) { if (value === null) return fallback const parsed = Number(value) return Number.isFinite(parsed) ? parsed : fallback } function resetPagination(params: URLSearchParams) { params.set("limit", String(DEFAULT_LIMIT)) params.set("offset", "0") } function StatusBadge({ status }: { status: Player["status"] }) { switch (status) { case "available": return ( 可接单 ) case "busy": return ( 忙碌中 ) case "offline": return ( 离线 ) default: return null } } function PlayerCard({ player }: { player: Player }) { const minPrice = !player.services || player.services.length === 0 ? 0 : Math.min(...player.services.map((s) => s.price)) const unit = !player.services || player.services.length === 0 ? "局" : player.services.reduce((prev, curr) => (prev.price < curr.price ? prev : curr)).unit return (
{player.user.nickname.slice(0, 2)}

{player.user.nickname}

{player.shopName ? ( {player.shopName} ) : ( 个人 )}
{player.rating.toFixed(1)}
接单 {player.totalOrders}
{player.games.slice(0, 3).map((game) => ( {game} ))} {player.games.length > 3 && ( +{player.games.length - 3} )}
{player.tags.slice(0, 2).map((tag) => ( {tag} ))}
¥{minPrice} /{unit}
) } type ShopSearchItem = Extract function ShopCard({ item }: { item: ShopSearchItem }) { return (
{item.shop.name.slice(0, 2)}

{item.shop.name}

店铺
{item.shop.rating}
接单 {item.shop.totalOrders}
{item.games.slice(0, 3).map((game) => ( {game} ))} {item.games.length > 3 && ( +{item.games.length - 3} )}
¥{item.minPrice} /{item.unit}
) } interface FilterProps { games: Game[] selectedGames: string[] onGameChange: (game: string, checked: boolean) => void priceRange: { min: string; max: string } onPriceChange: (type: "min" | "max", value: string) => void onlyOnline: boolean onOnlineChange: (checked: boolean) => void minRating: string onRatingChange: (value: string) => void className?: string } function FilterSection({ games, selectedGames, onGameChange, priceRange, onPriceChange, onlyOnline, onOnlineChange, minRating, onRatingChange, className, }: FilterProps) { return (

游戏类型

{games.map((game) => (
onGameChange(game.name, checked as boolean)} />
))}

价格区间 (元) {(priceRange.min || priceRange.max) && ( ¥{priceRange.min || "-"} - ¥{priceRange.max || "-"} )}

onPriceChange("min", e.target.value)} min={0} /> - onPriceChange("max", e.target.value)} min={0} />

最低评分

) } function SearchPageContent() { const searchParams = useSearchParams() const router = useRouter() const [games, setGames] = useState([]) const [searchQuery, setSearchQuery] = useState(searchParams.get("q") || "") const [selectedGames, setSelectedGames] = useState(() => { const selected = searchParams.getAll("game") return [...new Set(selected)] }) const [priceRange, setPriceRange] = useState<{ min: string; max: string }>({ min: searchParams.get("min") || "", max: searchParams.get("max") || "", }) const [onlyOnline, setOnlyOnline] = useState((searchParams.get("online") ?? "0") === "1") const [minRating, setMinRating] = useState(searchParams.get("minRating") ?? "0") const [sortBy, setSortBy] = useState(() => { const sortParam = (searchParams.get("sort") ?? "composite") as SearchSort return SEARCH_SORTS.has(sortParam) ? sortParam : "composite" }) const [response, setResponse] = useState(null) const [hasLoaded, setHasLoaded] = useState(false) const [error, setError] = useState(null) useEffect(() => { let cancelled = false listGames() .then((items) => { if (cancelled) return setGames(items) }) .catch(() => { if (cancelled) return setGames([]) }) return () => { cancelled = true } }, []) const replaceUrl = useCallback( (updater: (params: URLSearchParams) => void) => { const params = new URLSearchParams(searchParams.toString()) updater(params) router.replace(`/search?${params.toString()}`, { scroll: false }) }, [router, searchParams], ) useEffect(() => { const timer = setTimeout(() => { const urlQ = searchParams.get("q") ?? "" if (searchQuery === urlQ) return replaceUrl((params) => { if (searchQuery) params.set("q", searchQuery) else params.delete("q") resetPagination(params) }) }, 500) return () => clearTimeout(timer) }, [replaceUrl, searchParams, searchQuery]) useEffect(() => { const controller = new AbortController() const q = searchParams.get("q") || undefined const selected = searchParams.getAll("game") const min = searchParams.get("min") || undefined const max = searchParams.get("max") || undefined const onlyOnlineParam = (searchParams.get("online") ?? "0") === "1" const minRatingParam = searchParams.get("minRating") ?? "0" const sortParam = (searchParams.get("sort") ?? "composite") as SearchSort const sort: SearchSort = SEARCH_SORTS.has(sortParam) ? sortParam : "composite" const limit = numberParam(searchParams.get("limit"), 12) const offset = numberParam(searchParams.get("offset"), 0) searchCatalog({ q, selectedGames: selected, min, max, onlyOnline: onlyOnlineParam, minRating: minRatingParam, sort, limit, offset, signal: controller.signal, }) .then((res) => { if (controller.signal.aborted) return setResponse(res) setHasLoaded(true) setError(null) }) .catch((err: unknown) => { if (controller.signal.aborted) return setError(err instanceof Error ? err.message : "Search request failed") setHasLoaded(true) }) return () => controller.abort() }, [searchParams]) const handleGameChange = (game: string, checked: boolean) => { const next = checked ? [...new Set([...selectedGames, game])] : selectedGames.filter((item) => item !== game) setSelectedGames(next) replaceUrl((params) => { params.delete("game") for (const value of next) params.append("game", value) resetPagination(params) }) } const handlePriceChange = (type: "min" | "max", value: string) => { const next = { ...priceRange, [type]: value } setPriceRange(next) replaceUrl((params) => { if (next.min) params.set("min", next.min) else params.delete("min") if (next.max) params.set("max", next.max) else params.delete("max") resetPagination(params) }) } const handleOnlineChange = (checked: boolean) => { setOnlyOnline(checked) replaceUrl((params) => { if (checked) params.set("online", "1") else params.delete("online") resetPagination(params) }) } const handleRatingChange = (value: string) => { setMinRating(value) replaceUrl((params) => { if (value && value !== "0") params.set("minRating", value) else params.delete("minRating") resetPagination(params) }) } const setSortAndSync = (next: SearchSort) => { setSortBy(next) replaceUrl((params) => { if (next === "composite") params.delete("sort") else params.set("sort", next) resetPagination(params) }) } const resetFilters = () => { setSelectedGames([]) setPriceRange({ min: "", max: "" }) setOnlyOnline(false) setMinRating("0") replaceUrl((params) => { params.delete("game") params.delete("min") params.delete("max") params.delete("online") params.delete("minRating") resetPagination(params) }) } const clearAllFilters = () => { setSearchQuery("") setSelectedGames([]) setPriceRange({ min: "", max: "" }) setOnlyOnline(false) setMinRating("0") replaceUrl((params) => { params.delete("q") params.delete("game") params.delete("min") params.delete("max") params.delete("online") params.delete("minRating") resetPagination(params) }) } const items = response?.items ?? [] const total = response?.meta.total ?? 0 const canLoadMore = response ? response.items.length < response.meta.total : false return (

寻找陪玩

找陪玩

} type="search" placeholder="搜索陪玩、游戏、标签..." className="border-border bg-card shadow-card transition-shadow focus:shadow-card-hover" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} />
筛选条件 试试其他筛选条件
共找到 {total} 位陪玩
{!hasLoaded ? (
) : items.length > 0 ? (
{items.map((result) => (
{result.type === "player" ? ( ) : ( )}
))}
) : (

未找到相关陪玩

{error ? "请求失败,请稍后重试" : "尝试调整筛选条件或更换搜索关键词"}

)} {canLoadMore && (
)}
) } export default function SearchPage() { return (
} >
) }