diff --git a/app/(main)/player/[id]/page.tsx b/app/(main)/player/[id]/page.tsx index 0160e88..041ed00 100644 --- a/app/(main)/player/[id]/page.tsx +++ b/app/(main)/player/[id]/page.tsx @@ -1,8 +1,211 @@ -export default function PlayerDetailPage({ params: _params }: { params: Promise<{ id: string }> }) { +import { CheckCircle, Clock, MapPin, MessageSquare, ShoppingBag, Star } from "lucide-react" +import Link from "next/link" +import { notFound } from "next/navigation" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Separator } from "@/components/ui/separator" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { mockPlayers, mockReviews, mockServices } from "@/lib/mock-data" + +export default async function PlayerDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params + const player = mockPlayers.find((p) => p.id === id) + + if (!player) { + notFound() + } + + const playerReviews = mockReviews.filter((r) => r.toUserId === player.id) + const playerServices = + player.services && player.services.length > 0 + ? player.services + : mockServices.filter((s) => s.playerId === player.id) + return ( -
-

打手详情

-

评分、服务列表、评价、所属店铺

+
+
+
+ + + {player.user.nickname[0]} + +
+ +
+
+
+

+ {player.user.nickname} + + {player.status === "available" ? "可接单" : "忙碌中"} + +

+
+
+ + {player.rating} +
+ + 接单 {player.totalOrders} + + 完成率 {(player.completionRate * 100).toFixed(0)}% +
+
+ + {player.shopId && ( + + + + )} +
+ +
+

+ {player.user.bio || "这个打手很懒,什么都没写~"} +

+
+ +
+ {player.tags.map((tag) => ( + + {tag} + + ))} +
+
+
+ + + + + 服务列表 ({playerServices.length}) + + + 评价 ({playerReviews.length}) + + + + +
+ {playerServices.map((service) => ( + + +
+ {service.gameName} +
+ ¥{service.price}{" "} + + / {service.unit} + +
+
+ {service.title} + + {service.description} + +
+ + {service.rankRange && ( +
+ + 段位: {service.rankRange} +
+ )} +
+ +
+ {service.availability.map((time) => ( + {time} + ))} +
+
+
+ + + +
+ ))} + {playerServices.length === 0 && ( +
+

暂无服务

+
+ )} +
+
+ + +
+ {playerReviews.length > 0 ? ( + playerReviews.map((review) => ( + + +
+ + + {review.fromUserName[0]} + +
+
+
+
{review.fromUserName}
+
+ {new Date(review.createdAt).toLocaleDateString()} +
+
+
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+
+

{review.content}

+ {review.sealed && ( +
+ + 平台认证评价 +
+ )} +
+
+
+
+ )) + ) : ( +
+ +

暂无评价

+
+ )} +
+
+
) } diff --git a/app/(main)/search/page.tsx b/app/(main)/search/page.tsx index 622582c..c321440 100644 --- a/app/(main)/search/page.tsx +++ b/app/(main)/search/page.tsx @@ -1,8 +1,575 @@ -export default function SearchPage() { +"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 } 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 { mockGames, mockPlayers } from "@/lib/mock-data" +import type { Player } 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 = useMemo(() => { + if (!player.services || player.services.length === 0) return 0 + return Math.min(...player.services.map((s) => s.price)) + }, [player.services]) + + const unit = useMemo(() => { + if (!player.services || player.services.length === 0) return "局" + const cheapestService = player.services.reduce((prev, curr) => + prev.price < curr.price ? prev : curr, + ) + return cheapestService.unit + }, [player.services]) + 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 FilterProps { + 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({ + selectedGames, + onGameChange, + priceRange, + onPriceChange, + onlyOnline, + onOnlineChange, + minRating, + onRatingChange, + className, +}: FilterProps) { + return ( +
+
+

+ + 游戏类型 +

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

价格区间 (元)

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

+ + 最低评分 +

+ +
) } + +function SearchPageContent() { + const searchParams = useSearchParams() + const router = useRouter() + + const [searchQuery, setSearchQuery] = useState(searchParams.get("q") || "") + const [selectedGames, setSelectedGames] = useState([]) + const [priceRange, setPriceRange] = useState<{ min: string; max: string }>({ min: "", max: "" }) + const [onlyOnline, setOnlyOnline] = useState(false) + const [minRating, setMinRating] = useState("0") + const [sortBy, setSortBy] = useState("composite") + const [visibleCount, setVisibleCount] = useState(12) + + useEffect(() => { + const q = searchParams.get("q") + if (q !== null && q !== searchQuery) { + setSearchQuery(q) + } + }, [searchParams, searchQuery]) + + 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 filteredPlayers = useMemo(() => { + return mockPlayers.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 = priceRange.min ? Number(priceRange.min) : 0 + const maxP = priceRange.max ? Number(priceRange.max) : Infinity + const playerMinPrice = Math.min(...(player.services?.map((s) => s.price) || [0])) + + if (playerMinPrice < minP) return false + if (priceRange.max && playerMinPrice > maxP) return false + + if (onlyOnline && player.status !== "available") return false + + if (player.rating < Number(minRating)) return false + + return true + }) + }, [searchQuery, selectedGames, priceRange, onlyOnline, minRating]) + + const sortedPlayers = useMemo(() => { + return [...filteredPlayers].sort((a, b) => { + switch (sortBy) { + case "rating": + return b.rating - a.rating + case "price_asc": { + const priceA = Math.min(...(a.services?.map((s) => s.price) || [0])) + const priceB = Math.min(...(b.services?.map((s) => s.price) || [0])) + return priceA - priceB + } + case "price_desc": { + const priceA = Math.min(...(a.services?.map((s) => s.price) || [0])) + const priceB = Math.min(...(b.services?.map((s) => s.price) || [0])) + return priceB - priceA + } + case "orders": + return b.totalOrders - a.totalOrders + case "composite": { + const scoreA = a.rating * Math.log10(a.totalOrders + 1) + const scoreB = b.rating * Math.log10(b.totalOrders + 1) + return scoreB - scoreA + } + default: + return 0 + } + }) + }, [filteredPlayers, sortBy]) + + const displayedPlayers = sortedPlayers.slice(0, visibleCount) + + return ( +
+
+
+

寻找陪玩

+

找到最适合你的游戏伙伴,一起畅玩游戏世界

+
+ +
+
+ + setSearchQuery(e.target.value)} + /> +
+ + + + + + + + 筛选条件 + 调整筛选条件以找到更匹配的陪玩 + +
+ +
+ + + + + +
+
+
+
+ +
+ + +
+
+
+ + + + +
+
+ 共找到 {filteredPlayers.length} 位陪玩 +
+
+ + {displayedPlayers.length > 0 ? ( +
+ {displayedPlayers.map((player) => ( + + ))} +
+ ) : ( +
+
+ +
+

未找到相关陪玩

+

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

+ +
+ )} + + {filteredPlayers.length > visibleCount && ( +
+ +
+ )} +
+
+
+ ) +} + +export default function SearchPage() { + return ( + +
+
+ } + > + + + ) +} diff --git a/app/(main)/shop/[id]/page.tsx b/app/(main)/shop/[id]/page.tsx index eea668a..1d3db30 100644 --- a/app/(main)/shop/[id]/page.tsx +++ b/app/(main)/shop/[id]/page.tsx @@ -1,8 +1,273 @@ -export default function ShopDetailPage({ params: _params }: { params: Promise<{ id: string }> }) { +import { Gamepad2, Megaphone, ShoppingBag, Star, Users } from "lucide-react" +import Image from "next/image" +import Link from "next/link" +import { notFound } from "next/navigation" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Badge } from "@/components/ui/badge" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Separator } from "@/components/ui/separator" +import { mockPlayers, mockReviews, mockServices, mockShops } from "@/lib/mock-data" + +interface PageProps { + params: Promise<{ id: string }> +} + +export default async function ShopPage({ params }: PageProps) { + const { id } = await params + const shop = mockShops.find((s) => s.id === id) + + if (!shop) { + notFound() + } + + const shopPlayers = mockPlayers.filter((p) => p.shopId === shop.id) + const playerIds = shopPlayers.map((p) => p.id) + const shopServices = mockServices.filter((s) => playerIds.includes(s.playerId)) + const shopReviews = mockReviews.filter((r) => playerIds.includes(r.toUserId)) + + const sortedSections = [...shop.templateConfig.sections] + .filter((s) => s.enabled) + .sort((a, b) => a.order - b.order) + return ( -
-

店铺详情

-

店铺主页、服务列表、打手列表

+
+ {sortedSections.map((section) => { + switch (section.type) { + case "banner": + return ( +
+ {shop.banner ? ( + {shop.name} + ) : ( +
+ )} +
+

+ {shop.name} +

+
+
+ ) + + case "intro": + return ( + + +
+
+
+

{shop.name}

+ + {shop.commissionType === "percentage" + ? `抽成 ${shop.commissionValue}%` + : `固定抽成 ${shop.commissionValue}`} + +
+

{shop.description}

+
+
+ + {shop.rating} + 评分 +
+ +
+ + {shop.totalOrders} + 总订单 +
+ +
+ + {shop.playerCount} + 陪玩师 +
+
+
+
+ + + {shop.owner.nickname[0]} + +
+

店长:{shop.owner.nickname}

+

+ {shop.owner.bio || "暂无介绍"} +

+
+
+
+
+
+ ) + + case "announcements": + if (shop.announcements.length === 0) return null + return ( + + + + + 店铺公告 + + + +
    + {shop.announcements.map((announcement) => ( +
  • + + {announcement} +
  • + ))} +
+
+
+ ) + + case "services": + if (shopServices.length === 0) return null + return ( +
+

+ + 热门服务 +

+
+ {shopServices.map((service) => ( + + +
+ {service.gameName} +
+ ¥{service.price} + /{service.unit} +
+
+ {service.title} +
+ +

+ {service.description} +

+ {service.rankRange && ( +
+ 段位:{service.rankRange} +
+ )} +
+
+ ))} +
+
+ ) + + case "players": + if (shopPlayers.length === 0) return null + return ( +
+

+ + 明星陪玩 +

+
+ {shopPlayers.map((player) => ( + + + +
+ + + {player.user.nickname[0]} + + +
+
+

{player.user.nickname}

+
+ + {player.rating} + + {player.totalOrders}单 +
+
+
+ {player.games.slice(0, 3).map((game) => ( + + {game} + + ))} +
+
+
+ + ))} +
+
+ ) + + case "reviews": + if (shopReviews.length === 0) return null + return ( +
+

+ + 最新评价 +

+
+ {shopReviews.map((review) => ( + + +
+ + + {review.fromUserName[0]} + +
+
+
+

{review.fromUserName}

+
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+
+ + {new Date(review.createdAt).toLocaleDateString()} + +
+

{review.content}

+
+
+
+
+ ))} +
+
+ ) + + default: + return null + } + })}
) } diff --git a/app/(order)/chat/[id]/page.tsx b/app/(order)/chat/[id]/page.tsx index 94af656..1793815 100644 --- a/app/(order)/chat/[id]/page.tsx +++ b/app/(order)/chat/[id]/page.tsx @@ -1,8 +1,126 @@ -export default function ChatDetailPage({ params: _params }: { params: Promise<{ id: string }> }) { +"use client" + +import { ArrowLeft, Lock, Send } from "lucide-react" +import Link from "next/link" +import { use, useState } from "react" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { ScrollArea } from "@/components/ui/scroll-area" +import { mockChatMessages, mockChatSessions } from "@/lib/mock-data" +import { cn } from "@/lib/utils" + +export default function ChatDetailPage({ params }: { params: Promise<{ id: string }> }) { + const { id } = use(params) + const session = mockChatSessions.find((s) => s.id === id) + const messages = mockChatMessages.filter((m) => m.sessionId === id) + const [input, setInput] = useState("") + + if (!session) { + return ( +
+ 会话不存在 +
+ ) + } + + const other = session.participants[1] + const currentUserId = session.participants[0].id + return ( -
-

聊天

-

与打手沟通

+
+
+ + + + + + {other.name[0]} + +
+ {other.name} +
+ + {session.type === "order" ? "订单会话" : "咨询会话"} + + {session.readonly && ( + + + 只读 + + )} +
+
+
+ + +
+ {messages.map((msg) => { + if (msg.type === "system") { + return ( +
+ + {msg.content} + +
+ ) + } + const isMine = msg.senderId === currentUserId + return ( +
+ + + {msg.senderName[0]} + +
+

+ {msg.content} +

+

+ {new Date(msg.createdAt).toLocaleTimeString("zh-CN", { + hour: "2-digit", + minute: "2-digit", + })} +

+
+
+ ) + })} +
+
+ + {!session.readonly ? ( +
+
{ + e.preventDefault() + setInput("") + }} + > + setInput(e.target.value)} + placeholder="输入消息..." + className="flex-1" + /> + +
+
+ ) : ( +
+ + 订单已关闭,会话为只读状态 +
+ )}
) } diff --git a/app/(order)/chat/page.tsx b/app/(order)/chat/page.tsx index 38d9a73..134c654 100644 --- a/app/(order)/chat/page.tsx +++ b/app/(order)/chat/page.tsx @@ -1,8 +1,61 @@ +import { Lock, MessageSquare } from "lucide-react" +import Link from "next/link" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Badge } from "@/components/ui/badge" +import { Card, CardContent } from "@/components/ui/card" +import { mockChatSessions } from "@/lib/mock-data" + export default function ChatListPage() { return ( -
-

消息

-

咨询会话和订单会话

+
+

消息

+ +
+ {mockChatSessions.map((session) => { + const other = session.participants[1] + return ( + + + + + + {other.name[0]} + +
+
+ {other.name} + + {session.type === "order" ? "订单" : "咨询"} + + {session.readonly && } +
+

{session.lastMessage}

+
+
+ {session.lastMessageAt && ( + + {new Date(session.lastMessageAt).toLocaleDateString("zh-CN")} + + )} + {session.unreadCount > 0 && ( + + {session.unreadCount} + + )} +
+
+
+ + ) + })} + + {mockChatSessions.length === 0 && ( +
+ + 暂无消息 +
+ )} +
) } diff --git a/app/(order)/dispute/[id]/page.tsx b/app/(order)/dispute/[id]/page.tsx index 1d1f1ea..79d05ce 100644 --- a/app/(order)/dispute/[id]/page.tsx +++ b/app/(order)/dispute/[id]/page.tsx @@ -1,8 +1,176 @@ -export default function DisputePage({ params: _params }: { params: Promise<{ id: string }> }) { +"use client" + +import { AlertTriangle, ArrowLeft, Clock, FileText } from "lucide-react" +import Link from "next/link" +import { use, useState } from "react" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Label } from "@/components/ui/label" +import { Separator } from "@/components/ui/separator" +import { Textarea } from "@/components/ui/textarea" +import { mockDisputes, mockOrders } from "@/lib/mock-data" + +const disputeStatusLabels: Record = { + open: "已提交", + reviewing: "审核中", + resolved: "已解决", + appealed: "申诉中", +} + +export default function DisputePage({ params }: { params: Promise<{ id: string }> }) { + const { id } = use(params) + const order = mockOrders.find((o) => o.id === id) + const existingDispute = mockDisputes.find((d) => d.orderId === id) + const [reason, setReason] = useState("") + const [submitted, setSubmitted] = useState(false) + + if (!order) { + return ( +
+ 订单不存在 +
+ ) + } + + if (existingDispute) { + return ( +
+ + + 返回订单 + + + + +
+ 争议详情 + {disputeStatusLabels[existingDispute.status]} +
+
+ +
+
+ + 发起人: + {existingDispute.initiatorName} +
+
+ + 提交时间: + {new Date(existingDispute.createdAt).toLocaleString("zh-CN")} +
+
+ +
+ +

{existingDispute.reason}

+
+ {existingDispute.evidence.length > 0 && ( +
+ +
+ {existingDispute.evidence.map((url) => ( +
+ 截图 +
+ ))} +
+
+ )} + {existingDispute.result && ( + <> + +
+ +

+ {existingDispute.result === "full_refund" + ? "全额退款" + : existingDispute.result === "full_payment" + ? "全额支付给打手" + : "部分退款"} +

+
+ + )} +
+
+
+ ) + } + + if (submitted) { + return ( +
+ +

争议已提交

+

+ 平台将在 3 个工作日内审核你的争议申请,期间聊天会话仍可使用。 +

+ +
+ ) + } + return ( -
-

争议仲裁

-

提交争议说明和证据

+
+ + + 返回订单 + + + + + + + 发起争议 + +

+ {order.service.title} · {order.playerName} +

+
+ +
+ +