fix(ui): unify layout wrappers and simplify player/shop detail pages
This commit is contained in:
@@ -6,7 +6,7 @@ export default function AccountLayout({ children }: { children: React.ReactNode
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<Header />
|
||||
<div className="flex flex-1">
|
||||
<div className="container mx-auto flex flex-1 px-4">
|
||||
<div className="hidden md:block">
|
||||
<AccountSidebar />
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function VerifyPage() {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl space-y-6">
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
<h1 className="text-2xl font-bold">身份认证</h1>
|
||||
|
||||
<Card>
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function WalletPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl space-y-6">
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
<h1 className="text-2xl font-bold">钱包</h1>
|
||||
|
||||
<Card>
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<Header />
|
||||
<div className="flex flex-1">
|
||||
<div className="container mx-auto flex flex-1 px-4">
|
||||
<div className="hidden md:block">
|
||||
<DashboardSidebar />
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ export default function CommunityPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{filteredPosts.map((post) =>
|
||||
(() => {
|
||||
const linkedOrder = post.linkedOrderId
|
||||
|
||||
+7
-4
@@ -72,7 +72,10 @@ export default function HomePage() {
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{players.map((player) => (
|
||||
<Card key={player.id} className="hover:shadow-md transition-shadow">
|
||||
<Card
|
||||
key={player.id}
|
||||
className="hover:shadow-md transition-shadow flex flex-col h-full"
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center gap-3 space-y-0 pb-3">
|
||||
<Avatar className="h-12 w-12">
|
||||
<AvatarImage src={player.user.avatar} />
|
||||
@@ -99,7 +102,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pb-3">
|
||||
<CardContent className="pb-3 flex-grow">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{player.tags.map((tag) => (
|
||||
<Badge key={tag} variant="outline" className="text-xs">
|
||||
@@ -132,12 +135,12 @@ export default function HomePage() {
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{shops.map((shop) => (
|
||||
<Card key={shop.id} className="hover:shadow-md transition-shadow">
|
||||
<Card key={shop.id} className="hover:shadow-md transition-shadow flex flex-col h-full">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">{shop.name}</CardTitle>
|
||||
<p className="text-sm text-muted-foreground">{shop.description}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="flex-grow">
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
<div className="flex items-center">
|
||||
<Star className="h-3.5 w-3.5 fill-yellow-400 text-yellow-400 mr-0.5" />
|
||||
|
||||
@@ -15,12 +15,7 @@ import {
|
||||
} from "@/components/ui/card"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import {
|
||||
isFavorited as checkFavorited,
|
||||
listPlayers,
|
||||
listReviewsByTargetUser,
|
||||
listServicesByPlayer,
|
||||
} from "@/lib/api"
|
||||
import { listPlayers, listReviewsByTargetUser, listServicesByPlayer } from "@/lib/api"
|
||||
|
||||
export default async function PlayerDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
@@ -35,8 +30,6 @@ export default async function PlayerDetailPage({ params }: { params: Promise<{ i
|
||||
player.services && player.services.length > 0
|
||||
? player.services
|
||||
: listServicesByPlayer(player.id)
|
||||
const isFavorited = checkFavorited("u1", "player", player.id)
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4 max-w-5xl">
|
||||
<div className="flex flex-col md:flex-row gap-8 mb-8">
|
||||
@@ -79,11 +72,7 @@ export default async function PlayerDetailPage({ params }: { params: Promise<{ i
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
<FavoriteButton
|
||||
initialFavorited={isFavorited}
|
||||
targetType="player"
|
||||
targetId={player.id}
|
||||
/>
|
||||
<FavoriteButton initialFavorited={false} targetType="player" targetId={player.id} />
|
||||
</div>
|
||||
|
||||
<div className="bg-muted/50 p-4 rounded-lg">
|
||||
@@ -106,13 +95,13 @@ export default async function PlayerDetailPage({ params }: { params: Promise<{ i
|
||||
<TabsList className="w-full justify-start border-b rounded-none h-auto p-0 bg-transparent">
|
||||
<TabsTrigger
|
||||
value="services"
|
||||
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-6 py-3 text-base"
|
||||
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-6 py-3 text-base -mb-px"
|
||||
>
|
||||
服务列表 ({playerServices.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="reviews"
|
||||
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-6 py-3 text-base"
|
||||
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-6 py-3 text-base -mb-px"
|
||||
>
|
||||
评价 ({playerReviews.length})
|
||||
</TabsTrigger>
|
||||
|
||||
+18
-10
@@ -14,7 +14,7 @@ import {
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import { Suspense, useEffect, useMemo, useState } from "react"
|
||||
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"
|
||||
@@ -309,7 +309,14 @@ function FilterSection({
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-medium text-sm">价格区间 (元)</h3>
|
||||
<h3 className="font-medium text-sm flex items-center gap-2">
|
||||
价格区间 (元)
|
||||
{(priceRange.min || priceRange.max) && (
|
||||
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 font-normal">
|
||||
¥{priceRange.min || "-"} - ¥{priceRange.max || "-"}
|
||||
</Badge>
|
||||
)}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
@@ -381,6 +388,7 @@ function SearchPageContent() {
|
||||
min: "",
|
||||
max: "",
|
||||
})
|
||||
const deferredPriceRange = useDeferredValue(priceRange)
|
||||
const [onlyOnline, setOnlyOnline] = useState(false)
|
||||
const [minRating, setMinRating] = useState("0")
|
||||
const [sortBy, setSortBy] = useState("composite")
|
||||
@@ -445,12 +453,12 @@ function SearchPageContent() {
|
||||
if (!hasGame) return false
|
||||
}
|
||||
|
||||
const minP = priceRange.min ? Number(priceRange.min) : 0
|
||||
const maxP = priceRange.max ? Number(priceRange.max) : Infinity
|
||||
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 (priceRange.max && playerMinPrice > maxP) return false
|
||||
if (deferredPriceRange.max && playerMinPrice > maxP) return false
|
||||
|
||||
if (onlyOnline && player.status !== "available") return false
|
||||
|
||||
@@ -458,7 +466,7 @@ function SearchPageContent() {
|
||||
|
||||
return true
|
||||
})
|
||||
}, [minRating, onlyOnline, players, priceRange, searchQuery, selectedGames])
|
||||
}, [minRating, onlyOnline, players, deferredPriceRange, searchQuery, selectedGames])
|
||||
|
||||
const filteredShops = useMemo(() => {
|
||||
return shopResultItems.filter((item) => {
|
||||
@@ -475,17 +483,17 @@ function SearchPageContent() {
|
||||
if (!hasGame) return false
|
||||
}
|
||||
|
||||
const minP = priceRange.min ? Number(priceRange.min) : 0
|
||||
const maxP = priceRange.max ? Number(priceRange.max) : Infinity
|
||||
const minP = deferredPriceRange.min ? Number(deferredPriceRange.min) : 0
|
||||
const maxP = deferredPriceRange.max ? Number(deferredPriceRange.max) : Infinity
|
||||
if (item.minPrice < minP) return false
|
||||
if (priceRange.max && item.minPrice > maxP) 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, priceRange, onlyOnline, minRating])
|
||||
}, [shopResultItems, searchQuery, selectedGames, deferredPriceRange, onlyOnline, minRating])
|
||||
|
||||
const sortedResults = useMemo<SearchResult[]>(() => {
|
||||
const playerResults: SearchResult[] = filteredPlayers.map((player) => ({
|
||||
|
||||
@@ -8,13 +8,7 @@ import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
isFavorited as checkFavorited,
|
||||
getShopById,
|
||||
listPlayersByShop,
|
||||
listReviews,
|
||||
listServices,
|
||||
} from "@/lib/api"
|
||||
import { getShopById, listPlayersByShop, listReviews, listServices } from "@/lib/api"
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>
|
||||
@@ -32,8 +26,6 @@ export default async function ShopPage({ params }: PageProps) {
|
||||
const playerIds = shopPlayers.map((p) => p.id)
|
||||
const shopServices = listServices().filter((s) => playerIds.includes(s.playerId))
|
||||
const shopReviews = listReviews().filter((r) => playerIds.includes(r.toUserId))
|
||||
const isFavorited = checkFavorited("u1", "shop", shop.id)
|
||||
|
||||
const sortedSections = [...shop.templateConfig.sections]
|
||||
.filter((s) => s.enabled)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
@@ -101,7 +93,7 @@ export default async function ShopPage({ params }: PageProps) {
|
||||
</p>
|
||||
</div>
|
||||
<FavoriteButton
|
||||
initialFavorited={isFavorited}
|
||||
initialFavorited={false}
|
||||
targetType="shop"
|
||||
targetId={shop.id}
|
||||
/>
|
||||
@@ -144,7 +136,7 @@ export default async function ShopPage({ params }: PageProps) {
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{shopServices.map((service) => (
|
||||
<Card key={service.id}>
|
||||
<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>
|
||||
@@ -155,18 +147,20 @@ export default async function ShopPage({ params }: PageProps) {
|
||||
</div>
|
||||
<CardTitle className="text-base mt-2">{service.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="flex-grow flex flex-col">
|
||||
<p className="text-sm text-muted-foreground line-clamp-2 mb-2">
|
||||
{service.description}
|
||||
</p>
|
||||
{service.rankRange && (
|
||||
<div className="text-xs bg-muted px-2 py-1 rounded inline-block">
|
||||
<div className="text-xs bg-muted px-2 py-1 rounded inline-block w-fit">
|
||||
段位:{service.rankRange}
|
||||
</div>
|
||||
)}
|
||||
<Button className="w-full mt-3" size="sm" asChild>
|
||||
<Link href={`/order/new?serviceId=${service.id}`}>立即下单</Link>
|
||||
</Button>
|
||||
<div className="mt-auto pt-3">
|
||||
<Button className="w-full" size="sm" asChild>
|
||||
<Link href={`/order/new?serviceId=${service.id}`}>立即下单</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function ChatListPage() {
|
||||
<div className="container mx-auto py-8 px-4 max-w-2xl">
|
||||
<h1 className="text-2xl font-bold mb-6">消息</h1>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
{sessions.map((session) => {
|
||||
const other =
|
||||
session.participants.find((participant) => participant.id !== userId) ??
|
||||
@@ -58,10 +58,12 @@ export default function ChatListPage() {
|
||||
})}
|
||||
|
||||
{sessions.length === 0 && (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<MessageSquare className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||
暂无消息
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
||||
<MessageSquare className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||
暂无消息
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user