feat: best 匹配结果页展示 Top 3 候选排行,支持折叠查看备选餐厅
This commit is contained in:
+250
-143
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
MapPin,
|
||||
@@ -13,13 +14,16 @@ import {
|
||||
RotateCcw,
|
||||
SearchX,
|
||||
Home,
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import { Restaurant, MatchType } from "@/types";
|
||||
import { Restaurant, MatchType, RunnerUp } from "@/types";
|
||||
|
||||
interface MatchResultProps {
|
||||
restaurant: Restaurant;
|
||||
matchType: MatchType;
|
||||
matchLikes: number;
|
||||
runnerUps: RunnerUp[];
|
||||
allRestaurants: Restaurant[];
|
||||
userCount: number;
|
||||
onReset: () => Promise<void>;
|
||||
resetting: boolean;
|
||||
@@ -104,23 +108,81 @@ function NoMatchResult({
|
||||
);
|
||||
}
|
||||
|
||||
function RunnerUpCard({
|
||||
restaurant,
|
||||
likes,
|
||||
userCount,
|
||||
}: {
|
||||
restaurant: Restaurant;
|
||||
likes: number;
|
||||
userCount: number;
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href={buildNavUrl(restaurant)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex gap-3 rounded-xl bg-white/10 p-2.5 backdrop-blur-sm transition-colors hover:bg-white/20"
|
||||
>
|
||||
<img
|
||||
src={restaurant.image}
|
||||
alt={restaurant.name}
|
||||
className="h-16 w-16 shrink-0 rounded-lg object-cover"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<div className="flex min-w-0 flex-1 flex-col justify-center">
|
||||
<p className="truncate text-sm font-bold text-white">
|
||||
{restaurant.name}
|
||||
</p>
|
||||
<div className="mt-1 flex items-center gap-2 text-xs text-white/70">
|
||||
<span className="flex items-center gap-0.5">
|
||||
<Star size={11} className="fill-yellow-300 text-yellow-300" />
|
||||
{restaurant.rating}
|
||||
</span>
|
||||
<span>{restaurant.price}</span>
|
||||
{restaurant.distance && (
|
||||
<span className="flex items-center gap-0.5">
|
||||
<MapPin size={11} />
|
||||
{restaurant.distance}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-0.5 text-[11px] font-medium text-amber-200">
|
||||
{likes}/{userCount} 人想去
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MatchResult({
|
||||
restaurant,
|
||||
matchType,
|
||||
matchLikes,
|
||||
runnerUps,
|
||||
allRestaurants,
|
||||
userCount,
|
||||
onReset,
|
||||
resetting,
|
||||
}: MatchResultProps) {
|
||||
const [showRunnerUps, setShowRunnerUps] = useState(false);
|
||||
|
||||
if (matchType === "no_match") {
|
||||
return <NoMatchResult onReset={onReset} resetting={resetting} />;
|
||||
}
|
||||
|
||||
const isUnanimous = matchType === "unanimous";
|
||||
|
||||
const runnerUpRestaurants = runnerUps
|
||||
.map((ru) => {
|
||||
const r = allRestaurants.find((rest) => rest.id === ru.id);
|
||||
return r ? { restaurant: r, likes: ru.likes } : null;
|
||||
})
|
||||
.filter((x): x is { restaurant: Restaurant; likes: number } => x !== null);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={`fixed inset-0 z-50 flex flex-col items-center justify-center overflow-y-auto px-6 py-10 ${
|
||||
className={`fixed inset-0 z-50 flex flex-col items-center overflow-y-auto px-6 py-10 ${
|
||||
isUnanimous
|
||||
? "bg-linear-to-b from-emerald-500 to-teal-600"
|
||||
: "bg-linear-to-b from-amber-500 to-orange-500"
|
||||
@@ -129,153 +191,198 @@ export default function MatchResult({
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.4 }}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0, rotate: -20 }}
|
||||
animate={{ scale: 1, rotate: 0 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 12, delay: 0.2 }}
|
||||
>
|
||||
{isUnanimous ? (
|
||||
<PartyPopper size={56} className="text-yellow-300" />
|
||||
) : (
|
||||
<Trophy size={56} className="text-yellow-200" />
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
<motion.h1
|
||||
className="mt-3 text-4xl font-black text-white"
|
||||
initial={{ y: 30, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.35 }}
|
||||
>
|
||||
就去这了!
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
className={`mt-1 text-sm font-medium ${isUnanimous ? "text-emerald-100" : "text-amber-100"}`}
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.45 }}
|
||||
>
|
||||
{isUnanimous
|
||||
? "大家一拍即合!"
|
||||
: `${matchLikes}/${userCount} 人想去这家`}
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
className="mt-6 w-full max-w-sm overflow-hidden rounded-2xl bg-white shadow-2xl"
|
||||
initial={{ y: 60, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ type: "spring", stiffness: 180, damping: 18, delay: 0.5 }}
|
||||
>
|
||||
<img
|
||||
src={restaurant.image}
|
||||
alt={restaurant.name}
|
||||
className="h-44 w-full object-cover"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h2 className="text-lg font-bold leading-tight text-zinc-900">
|
||||
{restaurant.name}
|
||||
</h2>
|
||||
{restaurant.category && (
|
||||
<span className="shrink-0 rounded-full bg-emerald-50 px-2 py-0.5 text-[10px] font-semibold text-emerald-600">
|
||||
{restaurant.category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center gap-3 text-sm text-zinc-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Star size={13} className="fill-amber-400 text-amber-400" />
|
||||
{restaurant.rating}
|
||||
</span>
|
||||
<span className="font-semibold text-emerald-600">
|
||||
{restaurant.price}
|
||||
</span>
|
||||
{restaurant.distance && (
|
||||
<span className="flex items-center gap-1">
|
||||
<MapPin size={13} />
|
||||
{restaurant.distance}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{restaurant.address && (
|
||||
<p className="mt-2 text-xs leading-relaxed text-zinc-400">
|
||||
{restaurant.address}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{restaurant.openTime && (
|
||||
<div className="mt-1.5 flex items-center gap-1 text-xs text-zinc-400">
|
||||
<Clock size={12} />
|
||||
<span>{restaurant.openTime}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{restaurant.tag && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{restaurant.tag
|
||||
.split(",")
|
||||
.slice(0, 4)
|
||||
.map((t) => (
|
||||
<span
|
||||
key={t}
|
||||
className="rounded bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700"
|
||||
>
|
||||
{t.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="mt-5 flex w-full max-w-sm flex-col gap-2.5"
|
||||
initial={{ y: 30, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.65 }}
|
||||
>
|
||||
<motion.a
|
||||
href={buildNavUrl(restaurant)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`flex items-center justify-center gap-2 rounded-full bg-white px-8 py-3 text-sm font-bold shadow-lg transition-colors hover:bg-emerald-50 ${
|
||||
isUnanimous ? "text-emerald-600" : "text-orange-600"
|
||||
}`}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
<div className="flex w-full max-w-sm flex-1 flex-col items-center justify-center">
|
||||
<motion.div
|
||||
initial={{ scale: 0, rotate: -20 }}
|
||||
animate={{ scale: 1, rotate: 0 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 12, delay: 0.2 }}
|
||||
>
|
||||
<Navigation size={16} />
|
||||
导航过去
|
||||
</motion.a>
|
||||
{isUnanimous ? (
|
||||
<PartyPopper size={56} className="text-yellow-300" />
|
||||
) : (
|
||||
<Trophy size={56} className="text-yellow-200" />
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{restaurant.tel && (
|
||||
<motion.h1
|
||||
className="mt-3 text-4xl font-black text-white"
|
||||
initial={{ y: 30, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.35 }}
|
||||
>
|
||||
就去这了!
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
className={`mt-1 text-sm font-medium ${isUnanimous ? "text-emerald-100" : "text-amber-100"}`}
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.45 }}
|
||||
>
|
||||
{isUnanimous
|
||||
? "大家一拍即合!"
|
||||
: `${matchLikes}/${userCount} 人想去这家`}
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
className="mt-6 w-full overflow-hidden rounded-2xl bg-white shadow-2xl"
|
||||
initial={{ y: 60, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ type: "spring", stiffness: 180, damping: 18, delay: 0.5 }}
|
||||
>
|
||||
<img
|
||||
src={restaurant.image}
|
||||
alt={restaurant.name}
|
||||
className="h-44 w-full object-cover"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h2 className="text-lg font-bold leading-tight text-zinc-900">
|
||||
{restaurant.name}
|
||||
</h2>
|
||||
{restaurant.category && (
|
||||
<span className="shrink-0 rounded-full bg-emerald-50 px-2 py-0.5 text-[10px] font-semibold text-emerald-600">
|
||||
{restaurant.category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex items-center gap-3 text-sm text-zinc-500">
|
||||
<span className="flex items-center gap-1">
|
||||
<Star size={13} className="fill-amber-400 text-amber-400" />
|
||||
{restaurant.rating}
|
||||
</span>
|
||||
<span className="font-semibold text-emerald-600">
|
||||
{restaurant.price}
|
||||
</span>
|
||||
{restaurant.distance && (
|
||||
<span className="flex items-center gap-1">
|
||||
<MapPin size={13} />
|
||||
{restaurant.distance}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{restaurant.address && (
|
||||
<p className="mt-2 text-xs leading-relaxed text-zinc-400">
|
||||
{restaurant.address}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{restaurant.openTime && (
|
||||
<div className="mt-1.5 flex items-center gap-1 text-xs text-zinc-400">
|
||||
<Clock size={12} />
|
||||
<span>{restaurant.openTime}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{restaurant.tag && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{restaurant.tag
|
||||
.split(",")
|
||||
.slice(0, 4)
|
||||
.map((t) => (
|
||||
<span
|
||||
key={t}
|
||||
className="rounded bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700"
|
||||
>
|
||||
{t.trim()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="mt-5 flex w-full flex-col gap-2.5"
|
||||
initial={{ y: 30, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.65 }}
|
||||
>
|
||||
<motion.a
|
||||
href={`tel:${restaurant.tel}`}
|
||||
className="flex items-center justify-center gap-2 rounded-full bg-white/20 px-8 py-3 text-sm font-bold text-white backdrop-blur-sm transition-colors hover:bg-white/30"
|
||||
href={buildNavUrl(restaurant)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`flex items-center justify-center gap-2 rounded-full bg-white px-8 py-3 text-sm font-bold shadow-lg transition-colors hover:bg-emerald-50 ${
|
||||
isUnanimous ? "text-emerald-600" : "text-orange-600"
|
||||
}`}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Phone size={15} />
|
||||
打电话订位
|
||||
<Navigation size={16} />
|
||||
导航过去
|
||||
</motion.a>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
<motion.button
|
||||
className={`mt-4 flex items-center gap-1.5 text-sm font-medium underline underline-offset-2 hover:text-white ${
|
||||
isUnanimous ? "text-emerald-200" : "text-amber-200"
|
||||
}`}
|
||||
onClick={onReset}
|
||||
disabled={resetting}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.8 }}
|
||||
>
|
||||
<RotateCcw size={13} className={resetting ? "animate-spin" : ""} />
|
||||
{resetting ? "重置中..." : "再来一轮"}
|
||||
</motion.button>
|
||||
{restaurant.tel && (
|
||||
<motion.a
|
||||
href={`tel:${restaurant.tel}`}
|
||||
className="flex items-center justify-center gap-2 rounded-full bg-white/20 px-8 py-3 text-sm font-bold text-white backdrop-blur-sm transition-colors hover:bg-white/30"
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Phone size={15} />
|
||||
打电话订位
|
||||
</motion.a>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{!isUnanimous && runnerUpRestaurants.length > 0 && (
|
||||
<motion.div
|
||||
className="mt-5 w-full"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.75 }}
|
||||
>
|
||||
<button
|
||||
onClick={() => setShowRunnerUps((v) => !v)}
|
||||
className="flex w-full items-center justify-center gap-1.5 py-2 text-xs font-semibold text-white/80 transition-colors hover:text-white"
|
||||
>
|
||||
其他候选({runnerUpRestaurants.length})
|
||||
<motion.span
|
||||
animate={{ rotate: showRunnerUps ? 180 : 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="inline-flex"
|
||||
>
|
||||
<ChevronDown size={14} />
|
||||
</motion.span>
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{showRunnerUps && (
|
||||
<motion.div
|
||||
className="flex flex-col gap-2"
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
>
|
||||
{runnerUpRestaurants.map(({ restaurant: r, likes }) => (
|
||||
<RunnerUpCard
|
||||
key={r.id}
|
||||
restaurant={r}
|
||||
likes={likes}
|
||||
userCount={userCount}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<motion.button
|
||||
className={`mt-4 flex items-center gap-1.5 text-sm font-medium underline underline-offset-2 hover:text-white ${
|
||||
isUnanimous ? "text-emerald-200" : "text-amber-200"
|
||||
}`}
|
||||
onClick={onReset}
|
||||
disabled={resetting}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.8 }}
|
||||
>
|
||||
<RotateCcw size={13} className={resetting ? "animate-spin" : ""} />
|
||||
{resetting ? "重置中..." : "再来一轮"}
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user