From fb49e21eb270a6183dbfb0642282de406a64746d Mon Sep 17 00:00:00 2001 From: kurihada Date: Tue, 24 Feb 2026 17:36:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=BB=91=E5=8A=A8?= =?UTF-8?q?=E5=8F=82=E4=B8=8E=E6=84=9F=20-=20=E8=BF=9B=E5=BA=A6=E6=9D=A1?= =?UTF-8?q?=E3=80=81=E5=AE=9E=E6=97=B6=E6=B0=94=E6=B3=A1=E3=80=81=E7=83=AD?= =?UTF-8?q?=E5=BA=A6=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 卡片上方显示滑动进度条和计数 (3/15) - 轮询检测到当前卡片新增 like 时弹出"有人也想去这家!"气泡 - 卡片图片角落显示"🔥N 人想去"热度标签 - 后端 GET /api/room/[id] 新增 likeCounts 字段 --- src/app/api/room/[id]/route.ts | 8 ++++ src/app/room/[id]/page.tsx | 6 ++- src/components/RestaurantCard.tsx | 23 ++++++++---- src/components/SwipeDeck.tsx | 62 ++++++++++++++++++++++++++++++- src/components/SwipeableCard.tsx | 4 +- src/hooks/useRoomPolling.ts | 1 + src/types/index.ts | 1 + 7 files changed, 94 insertions(+), 11 deletions(-) diff --git a/src/app/api/room/[id]/route.ts b/src/app/api/room/[id]/route.ts index 8ae8343..954ea11 100644 --- a/src/app/api/room/[id]/route.ts +++ b/src/app/api/room/[id]/route.ts @@ -37,12 +37,20 @@ export async function GET( matchLikes = best.likes; } + const likeCounts: Record = {}; + for (const [rid, users] of Object.entries(data.likes)) { + if (users.length > 0) { + likeCounts[rid] = users.length; + } + } + return NextResponse.json({ roomId: id, userCount: data.users.length, match, matchType, matchLikes, + likeCounts, restaurants: data.restaurants, }); } catch (e) { diff --git a/src/app/room/[id]/page.tsx b/src/app/room/[id]/page.tsx index 5386392..51ff22e 100644 --- a/src/app/room/[id]/page.tsx +++ b/src/app/room/[id]/page.tsx @@ -14,8 +14,9 @@ export default function RoomPage() { const [userId, setUserId] = useState(""); const [joined, setJoined] = useState(false); - const { userCount, match, matchType, matchLikes, restaurants, mutate } = - useRoomPolling(roomId); + const { + userCount, match, matchType, matchLikes, likeCounts, restaurants, mutate, + } = useRoomPolling(roomId); useEffect(() => { const id = getUserId(); @@ -54,6 +55,7 @@ export default function RoomPage() { matchedRestaurantId={match} matchType={matchType} matchLikes={matchLikes} + likeCounts={likeCounts} userCount={userCount} onReset={handleReset} /> diff --git a/src/components/RestaurantCard.tsx b/src/components/RestaurantCard.tsx index 66147b3..cab45bf 100644 --- a/src/components/RestaurantCard.tsx +++ b/src/components/RestaurantCard.tsx @@ -1,18 +1,19 @@ "use client"; import { useCallback } from "react"; -import { Star, MapPin, Clock, ExternalLink } from "lucide-react"; +import { Star, MapPin, Clock, ExternalLink, Flame } from "lucide-react"; import { Restaurant } from "@/types"; interface RestaurantCardProps { restaurant: Restaurant; + likeCount?: number; } function stopAll(e: React.SyntheticEvent) { e.stopPropagation(); } -export default function RestaurantCard({ restaurant }: RestaurantCardProps) { +export default function RestaurantCard({ restaurant, likeCount = 0 }: RestaurantCardProps) { const openLink = useCallback( (url: string) => (e: React.MouseEvent | React.TouchEvent) => { e.stopPropagation(); @@ -37,11 +38,19 @@ export default function RestaurantCard({ restaurant }: RestaurantCardProps) { />
- {restaurant.category && ( - - {restaurant.category} - - )} +
+ {restaurant.category && ( + + {restaurant.category} + + )} + {likeCount > 0 && ( + + + {likeCount} 人想去 + + )} +
diff --git a/src/components/SwipeDeck.tsx b/src/components/SwipeDeck.tsx index e687620..1de4ee2 100644 --- a/src/components/SwipeDeck.tsx +++ b/src/components/SwipeDeck.tsx @@ -1,11 +1,12 @@ "use client"; import { useState, useCallback, useRef, useEffect } from "react"; -import { AnimatePresence } from "framer-motion"; +import { AnimatePresence, motion } from "framer-motion"; import SwipeableCard from "./SwipeableCard"; import ActionButtons from "./ActionButtons"; import MatchResult from "./MatchResult"; import { Restaurant, SwipeDirection, MatchType } from "@/types"; +import { Heart } from "lucide-react"; interface SwipeDeckProps { restaurants: Restaurant[]; @@ -14,6 +15,7 @@ interface SwipeDeckProps { matchedRestaurantId: string | null; matchType: MatchType; matchLikes: number; + likeCounts: Record; userCount: number; onReset: () => Promise; } @@ -25,6 +27,7 @@ export default function SwipeDeck({ matchedRestaurantId, matchType, matchLikes, + likeCounts, userCount, onReset, }: SwipeDeckProps) { @@ -32,8 +35,10 @@ export default function SwipeDeck({ const [showMatch, setShowMatch] = useState(false); const [localMatchId, setLocalMatchId] = useState(null); const [resetting, setResetting] = useState(false); + const [bubble, setBubble] = useState(""); const swipeFnRef = useRef<((direction: SwipeDirection) => void) | null>(null); const swipingRef = useRef(false); + const prevLikeCounts = useRef>({}); const resolvedMatchId = matchedRestaurantId ?? localMatchId; @@ -43,6 +48,26 @@ export default function SwipeDeck({ } }, [resolvedMatchId, showMatch]); + useEffect(() => { + const currentCard = restaurants[currentIndex]; + if (!currentCard || resolvedMatchId) return; + + const prev = prevLikeCounts.current; + const rid = currentCard.id; + const oldCount = prev[rid] ?? 0; + const newCount = likeCounts[rid] ?? 0; + + if (newCount > oldCount && oldCount > 0) { + setBubble("有人也想去这家!"); + const timer = setTimeout(() => setBubble(""), 2000); + return () => clearTimeout(timer); + } + }, [likeCounts, currentIndex, restaurants, resolvedMatchId]); + + useEffect(() => { + prevLikeCounts.current = { ...likeCounts }; + }, [likeCounts]); + const registerSwipe = useCallback( (fn: (direction: SwipeDirection) => void) => { swipeFnRef.current = fn; @@ -101,6 +126,7 @@ export default function SwipeDeck({ setCurrentIndex(0); setShowMatch(false); setLocalMatchId(null); + prevLikeCounts.current = {}; } finally { setResetting(false); } @@ -117,6 +143,24 @@ export default function SwipeDeck({ return ( <> + {!allSwiped && !resolvedMatchId && ( +
+
+ +
+ + {currentIndex}/{restaurants.length} + +
+ )} +
{!resolvedMatchId && ( @@ -132,6 +176,7 @@ export default function SwipeDeck({ isTop={isTop} onSwipe={handleSwipe} registerSwipe={isTop ? registerSwipe : undefined} + likeCount={likeCounts[restaurant.id] ?? 0} /> ); })} @@ -144,6 +189,21 @@ export default function SwipeDeck({

等待其他人完成选择...

)} + + + {bubble && ( + + + {bubble} + + )} +
diff --git a/src/components/SwipeableCard.tsx b/src/components/SwipeableCard.tsx index c2a4a54..0b777f3 100644 --- a/src/components/SwipeableCard.tsx +++ b/src/components/SwipeableCard.tsx @@ -21,6 +21,7 @@ interface SwipeableCardProps { isTop: boolean; onSwipe: (direction: SwipeDirection) => void; registerSwipe?: (fn: (direction: SwipeDirection) => void) => void; + likeCount: number; } function SwipeOverlay({ x }: { x: MotionValue }) { @@ -54,6 +55,7 @@ export default function SwipeableCard({ isTop, onSwipe, registerSwipe, + likeCount, }: SwipeableCardProps) { const x = useMotionValue(0); const rotate = useTransform(x, [-300, 300], [-ROTATION_RANGE, ROTATION_RANGE]); @@ -108,7 +110,7 @@ export default function SwipeableCard({ transition={{ type: "spring", stiffness: 300, damping: 25 }} > - + ); } diff --git a/src/hooks/useRoomPolling.ts b/src/hooks/useRoomPolling.ts index ec13249..03d9012 100644 --- a/src/hooks/useRoomPolling.ts +++ b/src/hooks/useRoomPolling.ts @@ -29,6 +29,7 @@ export function useRoomPolling(roomId: string) { match: data?.match ?? null, matchType: data?.matchType ?? null, matchLikes: data?.matchLikes ?? 0, + likeCounts: data?.likeCounts ?? {}, restaurants: data?.restaurants ?? [], isLoading, error, diff --git a/src/types/index.ts b/src/types/index.ts index d1026ba..cae0294 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -23,5 +23,6 @@ export interface RoomStatus { match: string | null; matchType: MatchType; matchLikes: number; + likeCounts: Record; restaurants: Restaurant[]; }