"use client" 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, useEffect, useMemo, useState, useDeferredValue } from "react" 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 { 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, listPlayers, listServices, listShops } from "@/lib/api" import { GameIcon } from "@/lib/game-icons" import type { Game, Player, Shop } from "@/lib/types" import { cn } from "@/lib/utils" 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}
) } interface ShopResultItem { shop: Shop minPrice: number unit: string games: string[] hasAvailable: boolean } function ShopCard({ item }: { item: ShopResultItem }) { return (
{item.shop.name.slice(0, 2)}

{item.shop.name}

店铺
{item.shop.rating.toFixed(1)}
接单 {item.shop.totalOrders}
{item.games.slice(0, 3).map((game) => ( {game} ))} {item.games.length > 3 && ( +{item.games.length - 3} )}
¥{item.minPrice} /{item.unit}
) } type SearchResult = | { type: "player" id: string rating: number orders: number minPrice: number item: Player } | { type: "shop" id: string rating: number orders: number minPrice: number item: ShopResultItem } 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 = listGames() const players = listPlayers() const services = listServices() const shops = listShops() const [searchQuery, setSearchQuery] = useState(searchParams.get("q") || "") const [selectedGames, setSelectedGames] = useState(() => { const game = searchParams.get("game") return game ? [game] : [] }) const [priceRange, setPriceRange] = useState<{ min: string; max: string }>({ min: "", max: "", }) const deferredPriceRange = useDeferredValue(priceRange) const [onlyOnline, setOnlyOnline] = useState(false) const [minRating, setMinRating] = useState("0") const [sortBy, setSortBy] = useState("composite") const [visibleCount, setVisibleCount] = useState(12) useEffect(() => { const timer = setTimeout(() => { const params = new URLSearchParams(searchParams.toString()) if (searchQuery) { params.set("q", searchQuery) } else { params.delete("q") } router.replace(`/search?${params.toString()}`, { scroll: false }) }, 500) return () => clearTimeout(timer) }, [searchQuery, router, searchParams]) const handleGameChange = (game: string, checked: boolean) => { setSelectedGames((prev) => (checked ? [...prev, game] : prev.filter((g) => g !== game))) } const handlePriceChange = (type: "min" | "max", value: string) => { setPriceRange((prev) => ({ ...prev, [type]: value })) } const shopResultItems = useMemo(() => { return shops.map((shop) => { const shopPlayers = players.filter((player) => player.shopId === shop.id) const playerIds = new Set(shopPlayers.map((player) => player.id)) const shopServices = services.filter((service) => playerIds.has(service.playerId)) const minPrice = shopServices.length > 0 ? Math.min(...shopServices.map((service) => service.price)) : 0 const unit = shopServices.length > 0 ? shopServices.reduce((prev, curr) => (prev.price < curr.price ? prev : curr)).unit : "局" const shopGames = [...new Set(shopServices.map((service) => service.gameName))] const hasAvailable = shopPlayers.some((player) => player.status === "available") return { shop, minPrice, unit, games: shopGames, hasAvailable, } }) }, [players, services, shops]) const filteredPlayers = useMemo(() => { return players.filter((player) => { if (searchQuery) { const query = searchQuery.toLowerCase() const matchName = player.user.nickname.toLowerCase().includes(query) const matchTags = player.tags.some((tag) => tag.toLowerCase().includes(query)) const matchGames = player.games.some((game) => game.toLowerCase().includes(query)) if (!matchName && !matchTags && !matchGames) return false } if (selectedGames.length > 0) { const hasGame = player.games.some((game) => selectedGames.includes(game)) if (!hasGame) return false } const minP = deferredPriceRange.min ? Number(deferredPriceRange.min) : 0 const maxP = deferredPriceRange.max ? Number(deferredPriceRange.max) : Infinity const playerMinPrice = Math.min(...player.services.map((s) => s.price)) if (playerMinPrice < minP) return false if (deferredPriceRange.max && playerMinPrice > maxP) return false if (onlyOnline && player.status !== "available") return false if (player.rating < Number(minRating)) return false return true }) }, [minRating, onlyOnline, players, deferredPriceRange, searchQuery, selectedGames]) const filteredShops = useMemo(() => { return shopResultItems.filter((item) => { if (searchQuery) { const query = searchQuery.toLowerCase() const matchName = item.shop.name.toLowerCase().includes(query) const matchDescription = item.shop.description.toLowerCase().includes(query) const matchGames = item.games.some((game) => game.toLowerCase().includes(query)) if (!matchName && !matchDescription && !matchGames) return false } if (selectedGames.length > 0) { const hasGame = item.games.some((game) => selectedGames.includes(game)) if (!hasGame) return false } const minP = deferredPriceRange.min ? Number(deferredPriceRange.min) : 0 const maxP = deferredPriceRange.max ? Number(deferredPriceRange.max) : Infinity if (item.minPrice < minP) return false if (deferredPriceRange.max && item.minPrice > maxP) return false if (onlyOnline && !item.hasAvailable) return false if (item.shop.rating < Number(minRating)) return false return true }) }, [shopResultItems, searchQuery, selectedGames, deferredPriceRange, onlyOnline, minRating]) const sortedResults = useMemo(() => { const playerResults: SearchResult[] = filteredPlayers.map((player) => ({ type: "player", id: player.id, rating: player.rating, orders: player.totalOrders, minPrice: Math.min(...player.services.map((service) => service.price)), item: player, })) const shopResults: SearchResult[] = filteredShops.map((item) => ({ type: "shop", id: item.shop.id, rating: item.shop.rating, orders: item.shop.totalOrders, minPrice: item.minPrice, item, })) return [...playerResults, ...shopResults].sort((a, b) => { switch (sortBy) { case "rating": return b.rating - a.rating case "price_asc": return a.minPrice - b.minPrice case "price_desc": return b.minPrice - a.minPrice case "orders": return b.orders - a.orders case "composite": { const scoreA = a.rating * Math.log10(a.orders + 1) const scoreB = b.rating * Math.log10(b.orders + 1) return scoreB - scoreA } default: return 0 } }) }, [filteredPlayers, filteredShops, sortBy]) const displayedResults = sortedResults.slice(0, visibleCount) return (

寻找陪玩

找陪玩

setSearchQuery(e.target.value)} />
筛选条件 试试其他筛选条件
共找到 {sortedResults.length} 位陪玩
{displayedResults.length > 0 ? (
{displayedResults.map((result) => (
{result.type === "player" ? ( ) : ( )}
))}
) : (

未找到相关陪玩

尝试调整筛选条件或更换搜索关键词

)} {sortedResults.length > visibleCount && (
)}
) } export default function SearchPage() { return (
} >
) }