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
+10 -6
View File
@@ -4,6 +4,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card" import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
import { EmptyState } from "@/components/ui/empty-state"
import { listGames, listPosts } from "@/lib/api" import { listGames, listPosts } from "@/lib/api"
import { roleLabels } from "@/lib/constants" import { roleLabels } from "@/lib/constants"
import type { Game, Post } from "@/lib/types" import type { Game, Post } from "@/lib/types"
@@ -86,14 +87,15 @@ export default function CommunityPage() {
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{games.map((game) => ( {games.map((game) => (
<Badge <Button
key={game.id} key={game.id}
variant={selectedGame === game.name ? "default" : "outline"} variant={selectedGame === game.name ? "default" : "outline"}
className="cursor-pointer hover:bg-primary/80 transition-colors px-3 py-1" size="sm"
className="h-7 rounded-full px-3"
onClick={() => setSelectedGame(selectedGame === game.name ? null : game.name)} onClick={() => setSelectedGame(selectedGame === game.name ? null : game.name)}
> >
{game.name} {game.name}
</Badge> </Button>
))} ))}
</div> </div>
</div> </div>
@@ -102,11 +104,11 @@ export default function CommunityPage() {
{postsLoading ? ( {postsLoading ? (
<div className="text-center py-12 text-muted-foreground">...</div> <div className="text-center py-12 text-muted-foreground">...</div>
) : filteredPosts.length === 0 ? ( ) : filteredPosts.length === 0 ? (
<div className="text-center py-12 text-muted-foreground"></div> <EmptyState title="暂无帖子" description="此分类下暂未找到相关的讨论内容" />
) : ( ) : (
filteredPosts.map((post) => ( filteredPosts.map((post) => (
<Link key={post.id} href={`/post/${post.id}`} className="block"> <Link key={post.id} href={`/post/${post.id}`} className="block">
<Card className="hover:shadow-md transition-shadow gap-4"> <Card className="shadow-sm border-border/80 gap-4">
<CardHeader> <CardHeader>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Avatar className="h-9 w-9"> <Avatar className="h-9 w-9">
@@ -142,7 +144,9 @@ export default function CommunityPage() {
</CardContent> </CardContent>
<CardFooter className="text-sm text-muted-foreground gap-4"> <CardFooter className="text-sm text-muted-foreground gap-4">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Heart className={`h-4 w-4 ${post.liked ? "fill-red-500 text-red-500" : ""}`} /> <Heart
className={`h-4 w-4 ${post.liked ? "fill-destructive text-destructive" : ""}`}
/>
{post.likeCount} {post.likeCount}
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
+18 -19
View File
@@ -5,6 +5,7 @@ import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { IconInput } from "@/components/ui/icon-input" import { IconInput } from "@/components/ui/icon-input"
import { StatusBadge } from "@/components/ui/status-badge"
import { listGames, listPlayers, listShops } from "@/lib/api" import { listGames, listPlayers, listShops } from "@/lib/api"
import { toApiError } from "@/lib/errors" import { toApiError } from "@/lib/errors"
import { GameIcon } from "@/lib/game-icons" import { GameIcon } from "@/lib/game-icons"
@@ -55,10 +56,7 @@ export default function HomePage() {
<section className="pb-8 pt-12 lg:pb-12 lg:pt-20 text-center"> <section className="pb-8 pt-12 lg:pb-12 lg:pt-20 text-center">
<div className="space-y-8 max-w-3xl mx-auto"> <div className="space-y-8 max-w-3xl mx-auto">
<div className="space-y-4"> <div className="space-y-4">
<h1 <h1 className="mb-3 text-3xl font-bold tracking-tight text-foreground lg:text-5xl">
className="mb-3 text-3xl font-bold tracking-tight text-foreground lg:text-5xl"
style={{ fontFamily: "'Space Grotesk', sans-serif" }}
>
<span className="text-primary"></span> <span className="text-primary"></span>
</h1> </h1>
<p className="mb-8 text-base text-muted-foreground lg:text-lg"> <p className="mb-8 text-base text-muted-foreground lg:text-lg">
@@ -74,7 +72,7 @@ export default function HomePage() {
type="text" type="text"
name="q" name="q"
placeholder="搜索陪玩、店铺、游戏..." placeholder="搜索陪玩、店铺、游戏..."
className="w-full border-border bg-card shadow-card transition-shadow focus:shadow-card-hover" className="w-full border-border bg-card shadow-sm transition-shadow focus-visible:shadow-md"
/> />
</form> </form>
</div> </div>
@@ -140,10 +138,7 @@ export default function HomePage() {
{/* Players */} {/* Players */}
{!loading && !loadingError {!loading && !loadingError
? players.map((player) => ( ? players.map((player) => (
<Card <Card key={player.id} className="flex flex-col h-full border-border/80 shadow-sm">
key={player.id}
className="hover:shadow-md transition-shadow flex flex-col h-full border-muted/60"
>
<CardHeader className="flex flex-row items-center gap-3 space-y-0 pb-3"> <CardHeader className="flex flex-row items-center gap-3 space-y-0 pb-3">
<Avatar className="h-12 w-12"> <Avatar className="h-12 w-12">
<AvatarImage src={player.user.avatar} /> <AvatarImage src={player.user.avatar} />
@@ -153,22 +148,29 @@ export default function HomePage() {
<CardTitle className="text-base">{player.user.nickname}</CardTitle> <CardTitle className="text-base">{player.user.nickname}</CardTitle>
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1">
<div className="flex items-center text-sm"> <div className="flex items-center text-sm">
<Star className="h-3.5 w-3.5 fill-primary text-primary mr-0.5" /> <Star className="h-3.5 w-3.5 fill-warning text-warning mr-0.5" />
{player.rating} {player.rating}
</div> </div>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{player.totalOrders} {player.totalOrders}
</span> </span>
<Badge <StatusBadge
variant={player.status === "available" ? "default" : "secondary"} status={
player.status === "available"
? "success"
: player.status === "busy"
? "warning"
: "neutral"
}
className="text-[10px] px-1.5 py-0 font-normal" className="text-[10px] px-1.5 py-0 font-normal"
icon={false}
> >
{player.status === "available" {player.status === "available"
? "可接单" ? "可接单"
: player.status === "busy" : player.status === "busy"
? "忙碌" ? "忙碌"
: "离线"} : "离线"}
</Badge> </StatusBadge>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
@@ -208,7 +210,7 @@ export default function HomePage() {
: null} : null}
{/* Community Teaser */} {/* Community Teaser */}
<Card className="hover:shadow-md transition-shadow flex flex-col h-full border-muted/60"> <Card className="flex flex-col h-full border-border/80 shadow-sm">
<CardHeader> <CardHeader>
<CardTitle className="text-lg"></CardTitle> <CardTitle className="text-lg"></CardTitle>
<p className="text-sm text-muted-foreground"></p> <p className="text-sm text-muted-foreground"></p>
@@ -228,10 +230,7 @@ export default function HomePage() {
{/* Shops */} {/* Shops */}
{!loading && !loadingError {!loading && !loadingError
? shops.map((shop) => ( ? shops.map((shop) => (
<Card <Card key={shop.id} className="flex flex-col h-full border-border/80 shadow-sm">
key={shop.id}
className="hover:shadow-md transition-shadow flex flex-col h-full border-muted/60"
>
<CardHeader> <CardHeader>
<CardTitle className="text-lg">{shop.name}</CardTitle> <CardTitle className="text-lg">{shop.name}</CardTitle>
<p className="text-sm text-muted-foreground">{shop.description}</p> <p className="text-sm text-muted-foreground">{shop.description}</p>
@@ -239,7 +238,7 @@ export default function HomePage() {
<CardContent className="flex-grow"> <CardContent className="flex-grow">
<div className="flex items-center gap-4 text-sm"> <div className="flex items-center gap-4 text-sm">
<div className="flex items-center"> <div className="flex items-center">
<Star className="h-3.5 w-3.5 fill-primary text-primary mr-0.5" /> <Star className="h-3.5 w-3.5 fill-warning text-warning mr-0.5" />
{shop.rating} {shop.rating}
</div> </div>
<div className="flex items-center text-muted-foreground"> <div className="flex items-center text-muted-foreground">
+51 -73
View File
@@ -5,6 +5,7 @@ import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card" import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox" import { Checkbox } from "@/components/ui/checkbox"
import { EmptyState } from "@/components/ui/empty-state"
import { IconInput } from "@/components/ui/icon-input" import { IconInput } from "@/components/ui/icon-input"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
@@ -26,6 +27,7 @@ import {
SheetTitle, SheetTitle,
SheetTrigger, SheetTrigger,
} from "@/components/ui/sheet" } from "@/components/ui/sheet"
import { StatusBadge } from "@/components/ui/status-badge"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { listGames } from "@/lib/api" import { listGames } from "@/lib/api"
import { searchCatalog } from "@/lib/api/search" 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 { SearchResponse, SearchResultItem, SearchSort } from "@/lib/search/types"
import type { Game, Player } from "@/lib/types" import type { Game, Player } from "@/lib/types"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { import { Filter, Gamepad2, Search, SlidersHorizontal, Star, Store, User } from "lucide-react"
CheckCircle2,
Clock,
Filter,
Gamepad2,
Search,
SlidersHorizontal,
Star,
Store,
User,
XCircle,
} from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { useRouter, useSearchParams } from "next/navigation" import { useRouter, useSearchParams } from "next/navigation"
import { Suspense, useCallback, useEffect, useState } from "react" import { Suspense, useCallback, useEffect, useState } from "react"
@@ -70,34 +61,6 @@ function resetPagination(params: URLSearchParams) {
params.set("offset", "0") 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 }) { function PlayerCard({ player }: { player: Player }) {
const minPrice = const minPrice =
!player.services || player.services.length === 0 !player.services || player.services.length === 0
@@ -111,7 +74,7 @@ function PlayerCard({ player }: { player: Player }) {
return ( return (
<Link href={`/player/${player.id}`} className="block h-full"> <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"> <CardHeader className="p-4 pb-2 flex flex-row items-start justify-between space-y-0">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Avatar className="h-12 w-12 border-2 border-background shadow-sm"> <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> <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"> <div className="flex items-center gap-1 text-xs text-muted-foreground">
{player.shopName ? ( {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" /> <Store className="w-3 h-3" />
{player.shopName} {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" /> <User className="w-3 h-3" />
</span> </Badge>
)} )}
</div> </div>
</div> </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> </CardHeader>
<CardContent className="p-4 pt-2 flex-grow"> <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-4 mb-3 text-sm">
<div className="flex items-center gap-1 text-yellow-500 font-medium"> <div className="flex items-center gap-1 text-warning font-medium">
<Star className="w-4 h-4 fill-current" /> <Star className="w-4 h-4 fill-warning text-warning" />
{player.rating.toFixed(1)} {player.rating.toFixed(1)}
</div> </div>
<div className="text-muted-foreground"> {player.totalOrders}</div> <div className="text-muted-foreground"> {player.totalOrders}</div>
@@ -194,7 +174,7 @@ type ShopSearchItem = Extract<SearchResultItem, { type: "shop" }>
function ShopCard({ item }: { item: ShopSearchItem }) { function ShopCard({ item }: { item: ShopSearchItem }) {
return ( return (
<Link href={`/shop/${item.shop.id}`} className="block h-full"> <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"> <CardHeader className="p-4 pb-2 flex flex-row items-start justify-between space-y-0">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Avatar className="h-12 w-12 border-2 border-background shadow-sm"> <Avatar className="h-12 w-12 border-2 border-background shadow-sm">
@@ -204,20 +184,22 @@ function ShopCard({ item }: { item: ShopSearchItem }) {
<div> <div>
<h3 className="font-semibold text-base leading-none mb-1">{item.shop.name}</h3> <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"> <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" /> <Store className="w-3 h-3" />
</span> </Badge>
</div> </div>
</div> </div>
</div> </div>
<StatusBadge status={item.hasAvailable ? "available" : "busy"} /> <StatusBadge status={item.hasAvailable ? "success" : "warning"}>
{item.hasAvailable ? "可接单" : "忙碌中"}
</StatusBadge>
</CardHeader> </CardHeader>
<CardContent className="p-4 pt-2 flex-grow"> <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-4 mb-3 text-sm">
<div className="flex items-center gap-1 text-yellow-500 font-medium"> <div className="flex items-center gap-1 text-warning font-medium">
<Star className="w-4 h-4 fill-current" /> <Star className="w-4 h-4 fill-warning text-warning" />
{item.shop.rating} {item.shop.rating}
</div> </div>
<div className="text-muted-foreground"> {item.shop.totalOrders}</div> <div className="text-muted-foreground"> {item.shop.totalOrders}</div>
@@ -585,7 +567,7 @@ function SearchPageContent() {
icon={<Search />} icon={<Search />}
type="search" type="search"
placeholder="搜索陪玩、游戏、标签..." 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} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
@@ -730,24 +712,20 @@ function SearchPageContent() {
))} ))}
</div> </div>
) : ( ) : (
<div className="text-center py-20 bg-muted/30 rounded-lg border border-dashed"> <EmptyState
<div className="mx-auto w-12 h-12 rounded-full bg-muted flex items-center justify-center mb-4"> icon={Search}
<Search className="w-6 h-6 text-muted-foreground" /> title="未找到相关陪玩"
</div> description={error ? "请求失败,请稍后重试" : "尝试调整筛选条件或更换搜索关键词"}
<h3 className="text-lg font-medium"></h3> action={
<p className="text-muted-foreground mt-1 max-w-sm mx-auto"> <Button
{error ? "请求失败,请稍后重试" : "尝试调整筛选条件或更换搜索关键词"} variant="outline"
</p> className="rounded-full"
<Button onClick={() => clearAllFilters()}
variant="outline" >
className="mt-4 rounded-full"
onClick={() => { </Button>
clearAllFilters() }
}} />
>
</Button>
</div>
)} )}
{canLoadMore && ( {canLoadMore && (