From 1b06f4fc0e79205937cf342c3b02dfcc8b6d60ff Mon Sep 17 00:00:00 2001 From: kurihada Date: Tue, 24 Feb 2026 17:43:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E7=94=A8=E6=88=B7=E6=BB=91?= =?UTF-8?q?=E5=8A=A8=E5=BC=95=E5=AF=BC=EF=BC=8C=E9=A6=96=E6=AC=A1=E8=BF=9B?= =?UTF-8?q?=E5=85=A5=E5=B1=95=E7=A4=BA=E6=89=8B=E5=8A=BF=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 首张卡片叠加半透明引导层,左滑不想去/右滑想去动画提示 - 支持点击消失、拖拽消失、3秒自动消失 - localStorage 记录标记,每个用户只展示一次 - 修复 React Strict Mode 下 effect 双执行导致引导不显示的问题 --- src/components/SwipeDeck.tsx | 6 +++ src/components/SwipeGuide.tsx | 97 +++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/components/SwipeGuide.tsx diff --git a/src/components/SwipeDeck.tsx b/src/components/SwipeDeck.tsx index 1de4ee2..7970ec5 100644 --- a/src/components/SwipeDeck.tsx +++ b/src/components/SwipeDeck.tsx @@ -5,6 +5,7 @@ import { AnimatePresence, motion } from "framer-motion"; import SwipeableCard from "./SwipeableCard"; import ActionButtons from "./ActionButtons"; import MatchResult from "./MatchResult"; +import SwipeGuide from "./SwipeGuide"; import { Restaurant, SwipeDirection, MatchType } from "@/types"; import { Heart } from "lucide-react"; @@ -36,6 +37,7 @@ export default function SwipeDeck({ const [localMatchId, setLocalMatchId] = useState(null); const [resetting, setResetting] = useState(false); const [bubble, setBubble] = useState(""); + const [guideVisible, setGuideVisible] = useState(true); const swipeFnRef = useRef<((direction: SwipeDirection) => void) | null>(null); const swipingRef = useRef(false); const prevLikeCounts = useRef>({}); @@ -97,6 +99,7 @@ export default function SwipeDeck({ if (!current) return; swipingRef.current = false; + if (guideVisible) setGuideVisible(false); const action = direction === "right" ? "like" : "nope"; sendSwipe(current.id, action); @@ -163,6 +166,9 @@ export default function SwipeDeck({
+ {currentIndex === 0 && !resolvedMatchId && guideVisible && ( + setGuideVisible(false)} /> + )} {!resolvedMatchId && ( {restaurants.map((restaurant, index) => { diff --git a/src/components/SwipeGuide.tsx b/src/components/SwipeGuide.tsx new file mode 100644 index 0000000..951f060 --- /dev/null +++ b/src/components/SwipeGuide.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useEffect, useState, useCallback } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { ThumbsDown, ThumbsUp } from "lucide-react"; + +const STORAGE_KEY = "nw_guide_v2"; + +interface SwipeGuideProps { + onDismiss: () => void; +} + +export default function SwipeGuide({ onDismiss }: SwipeGuideProps) { + const [visible, setVisible] = useState(() => { + if (typeof window === "undefined") return false; + return !localStorage.getItem(STORAGE_KEY); + }); + + const dismiss = useCallback(() => { + setVisible(false); + localStorage.setItem(STORAGE_KEY, "1"); + onDismiss(); + }, [onDismiss]); + + useEffect(() => { + if (!visible) { + onDismiss(); + return; + } + + const timer = setTimeout(dismiss, 3000); + return () => clearTimeout(timer); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + + {visible && ( + +
+ +
+ +
+ 不想去 + ← 左滑 +
+ + + + 滑动卡片 + + + + +
+ +
+ 想去 + 右滑 → +
+
+
+ )} +
+ ); +}