feat: 两级匹配机制 - 全票通过即时匹配 + 滑完自动推荐得票最高
- 后端 GET /api/room/[id] 新增 findBestMatch,滑完后选出得票最高餐厅 - 平票时取高德评分更高的一家,永远不会出现"无结果"死局 - 返回 matchType (unanimous/best) 和 matchLikes 区分匹配类型 - 全票通过:绿色庆祝 + "大家一拍即合!" - 得票最高:橙色推荐 + "N/M 人想去这家" - 移除 noMatch 死局页面,简化 SwipeDeck 状态管理
This commit is contained in:
@@ -1,19 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
import SwipeableCard from "./SwipeableCard";
|
||||
import ActionButtons from "./ActionButtons";
|
||||
import MatchResult from "./MatchResult";
|
||||
import { Restaurant, SwipeDirection } from "@/types";
|
||||
import { Frown, RotateCcw } from "lucide-react";
|
||||
import { Restaurant, SwipeDirection, MatchType } from "@/types";
|
||||
|
||||
interface SwipeDeckProps {
|
||||
restaurants: Restaurant[];
|
||||
roomId: string;
|
||||
userId: string;
|
||||
matchedRestaurantId: string | null;
|
||||
noMatch: boolean;
|
||||
matchType: MatchType;
|
||||
matchLikes: number;
|
||||
userCount: number;
|
||||
onReset: () => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -22,7 +23,9 @@ export default function SwipeDeck({
|
||||
roomId,
|
||||
userId,
|
||||
matchedRestaurantId,
|
||||
noMatch,
|
||||
matchType,
|
||||
matchLikes,
|
||||
userCount,
|
||||
onReset,
|
||||
}: SwipeDeckProps) {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
@@ -110,13 +113,13 @@ export default function SwipeDeck({
|
||||
? restaurants.find((r) => r.id === resolvedMatchId) ?? null
|
||||
: null;
|
||||
|
||||
const showWaiting = allSwiped && !resolvedMatchId && !noMatch;
|
||||
const showWaiting = allSwiped && !resolvedMatchId;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex flex-1 items-center justify-center px-4">
|
||||
<div className="relative h-[70vh] w-full max-w-sm">
|
||||
{!resolvedMatchId && !noMatch && (
|
||||
{!resolvedMatchId && (
|
||||
<AnimatePresence>
|
||||
{restaurants.map((restaurant, index) => {
|
||||
if (index < currentIndex || index > currentIndex + 1)
|
||||
@@ -141,39 +144,20 @@ export default function SwipeDeck({
|
||||
<p className="text-sm text-zinc-400">等待其他人完成选择...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{noMatch && !showMatch && (
|
||||
<motion.div
|
||||
className="flex h-full flex-col items-center justify-center gap-4"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-zinc-100">
|
||||
<Frown size={32} className="text-zinc-400" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-lg font-bold text-zinc-700">没有达成共识</p>
|
||||
<p className="mt-1 text-sm text-zinc-400">
|
||||
大家口味不太一样,换一批试试?
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
disabled={resetting}
|
||||
className="mt-2 flex items-center gap-2 rounded-xl bg-emerald-500 px-6 py-2.5 text-sm font-bold text-white shadow-md shadow-emerald-200 transition-colors hover:bg-emerald-600 disabled:opacity-50"
|
||||
>
|
||||
<RotateCcw size={16} className={resetting ? "animate-spin" : ""} />
|
||||
{resetting ? "重置中..." : "再来一轮"}
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ActionButtons onAction={handleButtonAction} disabled={isDone || noMatch} />
|
||||
<ActionButtons onAction={handleButtonAction} disabled={isDone} />
|
||||
|
||||
{showMatch && matchRestaurant && (
|
||||
<MatchResult restaurant={matchRestaurant} onReset={handleReset} />
|
||||
<MatchResult
|
||||
restaurant={matchRestaurant}
|
||||
matchType={matchType ?? "unanimous"}
|
||||
matchLikes={matchLikes}
|
||||
userCount={userCount}
|
||||
onReset={handleReset}
|
||||
resetting={resetting}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user