feat: 新增「喝什么」场景,支持奶茶/咖啡/酒吧等饮品店搜索

引入场景系统(SceneType),首页增加「吃什么」「喝什么」切换 Tab,
不同场景使用不同的高德 POI 类型、热门标签、价格区间和全链路文案。
场景信息存储在房间数据中,邀请/分享/匹配结果等页面自动适配。
This commit is contained in:
2026-02-25 01:12:44 +08:00
parent 6866b70278
commit c86a6c0909
13 changed files with 197 additions and 47 deletions
+9 -6
View File
@@ -20,7 +20,7 @@ import {
Share2,
Zap,
} from "lucide-react";
import { Restaurant, MatchType, RunnerUp } from "@/types";
import { Restaurant, MatchType, RunnerUp, SceneType } from "@/types";
import { fireCelebration, playChime } from "@/lib/celebrate";
import { isRegistered } from "@/lib/userId";
@@ -36,6 +36,7 @@ interface MatchResultProps {
onReset: () => Promise<void>;
onNarrow: (restaurantIds: string[]) => Promise<void>;
resetting: boolean;
scene?: SceneType;
}
function buildNavUrl(restaurant: Restaurant): string {
@@ -85,7 +86,7 @@ function NoMatchResult({
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.45 }}
>
</motion.p>
<motion.div
@@ -176,6 +177,7 @@ export default function MatchResult({
onReset,
onNarrow,
resetting,
scene = "eat",
}: MatchResultProps) {
const router = useRouter();
const [showRunnerUps, setShowRunnerUps] = useState(false);
@@ -220,10 +222,11 @@ export default function MatchResult({
}, [userId, roomId, restaurant, matchType, userCount]);
const handleShare = useCallback(async () => {
const verb = scene === "drink" ? "喝" : "吃";
const lines = [
isUnanimous
? `🎉 默契度 100%${userCount} 人全员一致选了同一家!`
: `🎉 我们用 NoWhatever 选好了!`,
: `🎉 我们用 NoWhatever 选好了去哪${verb}`,
``,
`📍 ${restaurant.name}`,
restaurant.rating ? `${restaurant.rating}` : "",
@@ -257,7 +260,7 @@ export default function MatchResult({
} catch {
showToast("复制失败,请手动复制");
}
}, [restaurant, showToast, isUnanimous, userCount]);
}, [restaurant, showToast, isUnanimous, userCount, scene]);
if (matchType === "no_match") {
return <NoMatchResult onReset={onReset} resetting={resetting} />;
@@ -515,7 +518,7 @@ export default function MatchResult({
className="flex items-center gap-1.5 text-sm font-medium text-amber-200 underline underline-offset-2 hover:text-white"
>
<RefreshCw size={13} />
</motion.button>
</>
) : (
@@ -536,7 +539,7 @@ export default function MatchResult({
}`}
>
<RefreshCw size={13} />
</motion.button>
</>
)}
+9 -4
View File
@@ -4,12 +4,15 @@ import { useCallback, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { QRCodeSVG } from "qrcode.react";
import { X, Copy, Share2, QrCode } from "lucide-react";
import type { SceneType } from "@/types";
import { getSceneConfig } from "@/lib/sceneConfig";
interface QrInviteModalProps {
open: boolean;
onClose: () => void;
roomId: string;
onToast: (msg: string) => void;
scene?: SceneType;
}
export default function QrInviteModal({
@@ -17,7 +20,9 @@ export default function QrInviteModal({
onClose,
roomId,
onToast,
scene = "eat",
}: QrInviteModalProps) {
const sceneConfig = getSceneConfig(scene);
const inviteUrl =
typeof window !== "undefined"
? `${window.location.origin}/invite/${roomId}`
@@ -39,8 +44,8 @@ export default function QrInviteModal({
const handleShare = useCallback(async () => {
const shareData = {
title: "别说随便啦,来滑卡片决定吃什么!",
text: "我建好房间了,快点开链接一起选餐厅,滑中同一家就去吃!",
title: sceneConfig.shareTitle,
text: sceneConfig.shareText,
url: inviteUrl,
};
@@ -54,7 +59,7 @@ export default function QrInviteModal({
}
handleCopy();
}, [inviteUrl, handleCopy]);
}, [inviteUrl, handleCopy, sceneConfig]);
return (
<AnimatePresence>
@@ -88,7 +93,7 @@ export default function QrInviteModal({
<h2 className="text-lg font-bold"></h2>
</div>
<p className="mt-1 text-xs text-zinc-400">
{sceneConfig.qrSubtitle}
</p>
<div className="mt-5 rounded-2xl border-2 border-dashed border-emerald-200 bg-emerald-50/30 p-4">
+4 -1
View File
@@ -6,7 +6,7 @@ import SwipeableCard from "./SwipeableCard";
import ActionButtons from "./ActionButtons";
import MatchResult from "./MatchResult";
import SwipeGuide from "./SwipeGuide";
import { Restaurant, SwipeDirection, MatchType, RunnerUp, UserProfile } from "@/types";
import { Restaurant, SwipeDirection, MatchType, RunnerUp, UserProfile, SceneType } from "@/types";
import { Heart, Undo2, Check } from "lucide-react";
import { getAvatar, getAvatarBg } from "@/lib/avatars";
@@ -149,6 +149,7 @@ interface SwipeDeckProps {
userProfiles: Record<string, UserProfile>;
onReset: () => Promise<void>;
onNarrow: (restaurantIds: string[]) => Promise<void>;
scene?: SceneType;
}
export default function SwipeDeck({
@@ -166,6 +167,7 @@ export default function SwipeDeck({
userProfiles,
onReset,
onNarrow,
scene = "eat",
}: SwipeDeckProps) {
const [currentIndex, setCurrentIndex] = useState(initialIndex);
const [showMatch, setShowMatch] = useState(false);
@@ -426,6 +428,7 @@ export default function SwipeDeck({
onReset={handleReset}
onNarrow={handleNarrow}
resetting={resetting}
scene={scene}
/>
)}
</>
+6 -1
View File
@@ -5,7 +5,8 @@ import { Users, QrCode, LogOut, Crown, Lock } from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import QrInviteModal from "./QrInviteModal";
import RoomManageModal from "./RoomManageModal";
import type { UserProfile } from "@/types";
import type { UserProfile, SceneType } from "@/types";
import { getSceneConfig } from "@/lib/sceneConfig";
interface TopNavProps {
roomId: string;
@@ -18,6 +19,7 @@ interface TopNavProps {
swipeCounts?: Record<string, number>;
totalCards?: number;
userProfiles?: Record<string, UserProfile>;
scene?: SceneType;
}
export default function TopNav({
@@ -31,7 +33,9 @@ export default function TopNav({
swipeCounts = {},
totalCards = 0,
userProfiles = {},
scene = "eat",
}: TopNavProps) {
const sceneConfig = getSceneConfig(scene);
const [toast, setToast] = useState("");
const [showQr, setShowQr] = useState(false);
const [showManage, setShowManage] = useState(false);
@@ -110,6 +114,7 @@ export default function TopNav({
onClose={() => setShowQr(false)}
roomId={roomId}
onToast={showToast}
scene={scene}
/>
{isCreator && (