From b98920858cc44725689fe8eea0afcf4f18520025 Mon Sep 17 00:00:00 2001 From: kurihada Date: Thu, 26 Feb 2026 17:50:28 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8F=90=E5=8F=96=20useToast=20hoo?= =?UTF-8?q?k=20+=20Toast=20=E7=BB=84=E4=BB=B6=EF=BC=8C=E6=B6=88=E9=99=A4?= =?UTF-8?q?=204=20=E5=A4=84=E9=87=8D=E5=A4=8D=E7=9A=84=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 state + setTimeout 自动消失逻辑封装为 useToast hook, Toast UI 统一为组件支持 top/bottom 两种位置,净减约 12 行。 --- src/app/blindbox/[code]/page.tsx | 35 ++++++++------------------------ src/app/profile/page.tsx | 35 +++++++++----------------------- src/components/MatchResult.tsx | 33 ++++++++---------------------- src/components/Toast.tsx | 34 +++++++++++++++++++++++++++++++ src/components/TopNav.tsx | 30 +++++++-------------------- src/hooks/useToast.ts | 17 ++++++++++++++++ 6 files changed, 86 insertions(+), 98 deletions(-) create mode 100644 src/components/Toast.tsx create mode 100644 src/hooks/useToast.ts diff --git a/src/app/blindbox/[code]/page.tsx b/src/app/blindbox/[code]/page.tsx index 33819b8..4f02594 100644 --- a/src/app/blindbox/[code]/page.tsx +++ b/src/app/blindbox/[code]/page.tsx @@ -23,6 +23,8 @@ import { import confetti from "canvas-confetti"; import { getCachedProfile, isRegistered } from "@/lib/userId"; import ShareCardModal from "@/components/ShareCardModal"; +import Toast from "@/components/Toast"; +import { useToast } from "@/hooks/useToast"; import { BlindboxRoomSkeleton } from "@/components/Skeleton"; import type { UserProfile } from "@/types"; @@ -182,7 +184,7 @@ export default function BlindboxRoomPage() { const [error, setError] = useState(""); const [showInvite, setShowInvite] = useState(false); const [showShareCard, setShowShareCard] = useState(false); - const [toast, setToast] = useState(""); + const toast = useToast(); const [confirmLeave, setConfirmLeave] = useState(false); const [leaving, setLeaving] = useState(false); @@ -314,8 +316,7 @@ export default function BlindboxRoomPage() { } setMyIdeas((prev) => prev.map((i) => (i.id === ideaId ? { ...i, content: trimmed } : i))); } catch (e) { - setToast(e instanceof Error ? e.message : "编辑失败"); - setTimeout(() => setToast(""), 2200); + toast.show(e instanceof Error ? e.message : "编辑失败"); } }, [profile]); @@ -334,8 +335,7 @@ export default function BlindboxRoomPage() { setMyIdeas((prev) => prev.filter((i) => i.id !== ideaId)); setPoolCount((c) => Math.max(0, c - 1)); } catch (e) { - setToast(e instanceof Error ? e.message : "删除失败"); - setTimeout(() => setToast(""), 2200); + toast.show(e instanceof Error ? e.message : "删除失败"); } }, [profile]); @@ -395,8 +395,7 @@ export default function BlindboxRoomPage() { if (!room) return; try { await navigator.clipboard.writeText(room.code); - setToast("房间号已复制"); - setTimeout(() => setToast(""), 2000); + toast.show("房间号已复制"); } catch { /* ignore */ } }; @@ -441,8 +440,7 @@ export default function BlindboxRoomPage() { } router.replace("/blindbox"); } catch (e) { - setToast(e instanceof Error ? e.message : "操作失败"); - setTimeout(() => setToast(""), 2200); + toast.show(e instanceof Error ? e.message : "操作失败"); setConfirmLeave(false); } finally { setLeaving(false); @@ -817,10 +815,7 @@ export default function BlindboxRoomPage() { drawer: revealedIdea.drawnBy ?? undefined, roomName: room.name, }} - onToast={(msg) => { - setToast(msg); - setTimeout(() => setToast(""), 2200); - }} + onToast={toast.show} /> )} @@ -855,19 +850,7 @@ export default function BlindboxRoomPage() { )} - {/* Toast */} - - {toast && ( - - {toast} - - )} - +
diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 28ca239..9d17bbd 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -24,6 +24,8 @@ import { Heart, } from "lucide-react"; import EmptyState from "@/components/EmptyState"; +import Toast from "@/components/Toast"; +import { useToast } from "@/hooks/useToast"; import RestaurantImage from "@/components/RestaurantImage"; import { ProfileCardSkeleton, RecordItemSkeleton } from "@/components/Skeleton"; import { getUserId, getCachedProfile, setCachedProfile, setCachedPreferences, logout } from "@/lib/userId"; @@ -68,12 +70,7 @@ export default function ProfilePage() { const [showHistory, setShowHistory] = useState(true); const [showFavorites, setShowFavorites] = useState(true); - const [toast, setToast] = useState(""); - - const showToast = useCallback((msg: string) => { - setToast(msg); - setTimeout(() => setToast(""), 2200); - }, []); + const toast = useToast(); useEffect(() => { const cached = getCachedProfile(); @@ -141,7 +138,7 @@ export default function ProfilePage() { setProfile((prev) => prev ? { ...prev, username: trimmed } : prev); setCachedProfile({ id: userId, username: trimmed, avatar: profile!.avatar }); setEditingUsername(false); - showToast("用户名已更新"); + toast.show("用户名已更新"); } else { setUsernameMsg(data.error ?? "更新失败"); } @@ -180,7 +177,7 @@ export default function ProfilePage() { setCurrentPassword(""); setNewPassword(""); setConfirmPassword(""); - showToast("密码已更新"); + toast.show("密码已更新"); } else { setPasswordMsg(data.error ?? "更新失败"); } @@ -202,10 +199,10 @@ export default function ProfilePage() { setProfile((prev) => prev ? { ...prev, avatar: emoji } : prev); setCachedProfile({ id: userId, username: profile!.username, avatar: emoji }); setEditingAvatar(false); - showToast("头像已更新"); + toast.show("头像已更新"); } } catch { - showToast("更新失败"); + toast.show("更新失败"); } }; @@ -244,9 +241,9 @@ export default function ProfilePage() { body: JSON.stringify({ userId, favoriteId: favId }), }); setFavorites((f) => f.filter((x) => x.id !== favId)); - showToast("已取消收藏"); + toast.show("已取消收藏"); } catch { - showToast("操作失败"); + toast.show("操作失败"); } }; @@ -714,19 +711,7 @@ export default function ProfilePage() { - - {toast && ( - - {toast} - - )} - + ); } diff --git a/src/components/MatchResult.tsx b/src/components/MatchResult.tsx index a736f6c..9c7c99d 100644 --- a/src/components/MatchResult.tsx +++ b/src/components/MatchResult.tsx @@ -34,6 +34,8 @@ import { isRegistered } from "@/lib/userId"; import ShareCardModal from "@/components/ShareCardModal"; import RestaurantImage from "@/components/RestaurantImage"; import AuthModal from "@/components/AuthModal"; +import Toast from "@/components/Toast"; +import { useToast } from "@/hooks/useToast"; interface MatchResultProps { restaurant: Restaurant; @@ -194,7 +196,7 @@ export default function MatchResult({ const router = useRouter(); const [showRunnerUps, setShowRunnerUps] = useState(false); const [showShareCard, setShowShareCard] = useState(false); - const [toast, setToast] = useState(""); + const toast = useToast(); const celebratedRef = useRef(false); const historySavedRef = useRef(false); const isSolo = userCount <= 1; @@ -204,11 +206,6 @@ export default function MatchResult({ const [registered, setRegistered] = useState(() => isRegistered()); const [showAuth, setShowAuth] = useState(false); - const showToast = useCallback((msg: string) => { - setToast(msg); - setTimeout(() => setToast(""), 2200); - }, []); - useEffect(() => { if (isUnanimous && !celebratedRef.current) { const timer = setTimeout(() => { @@ -247,9 +244,9 @@ export default function MatchResult({ (profile: UserProfile) => { setRegistered(true); setShowAuth(false); - showToast(`欢迎,${profile.username}!记录已保存`); + toast.show(`欢迎,${profile.username}!记录已保存`); }, - [showToast], + [toast], ); const handleFavorite = useCallback(async () => { @@ -263,13 +260,13 @@ export default function MatchResult({ }); if (res.ok) { setFavorited(true); - showToast("已收藏"); + toast.show("已收藏"); } } catch { /* ignore */ } setFavLoading(false); - }, [registered, userId, restaurant, favorited, favLoading, showToast]); + }, [registered, userId, restaurant, favorited, favLoading, toast]); if (matchType === "no_match") { return ; @@ -635,22 +632,10 @@ export default function MatchResult({ userCount, scene, }} - onToast={showToast} + onToast={toast.show} /> - - {toast && ( - - {toast} - - )} - + ); } diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx new file mode 100644 index 0000000..1bd6b42 --- /dev/null +++ b/src/components/Toast.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { motion, AnimatePresence } from "framer-motion"; + +interface ToastProps { + message: string; + position?: "top" | "bottom"; +} + +const positionClass = { + top: "top-10", + bottom: "bottom-8", +}; + +export default function Toast({ message, position = "top" }: ToastProps) { + const isTop = position === "top"; + const y = isTop ? -12 : 12; + + return ( + + {message && ( + + {message} + + )} + + ); +} diff --git a/src/components/TopNav.tsx b/src/components/TopNav.tsx index e078c08..edccb5f 100644 --- a/src/components/TopNav.tsx +++ b/src/components/TopNav.tsx @@ -1,12 +1,13 @@ "use client"; -import { useState, useCallback } from "react"; +import { useState } from "react"; import { QrCode, LogOut, Crown, Lock } from "lucide-react"; -import { motion, AnimatePresence } from "framer-motion"; import QrInviteModal from "./QrInviteModal"; import RoomManageModal from "./RoomManageModal"; +import Toast from "@/components/Toast"; import type { UserProfile, SceneType } from "@/types"; import { getSceneConfig } from "@/lib/sceneConfig"; +import { useToast } from "@/hooks/useToast"; interface TopNavProps { roomId: string; @@ -36,15 +37,10 @@ export default function TopNav({ scene = "eat", }: TopNavProps) { const sceneConfig = getSceneConfig(scene); - const [toast, setToast] = useState(""); + const toast = useToast(); const [showQr, setShowQr] = useState(false); const [showManage, setShowManage] = useState(false); - const showToast = useCallback((msg: string) => { - setToast(msg); - setTimeout(() => setToast(""), 2200); - }, []); - return ( <> - - {toast && ( - - {toast} - - )} - + setShowQr(false)} roomId={roomId} - onToast={showToast} + onToast={toast.show} scene={scene} /> @@ -116,7 +100,7 @@ export default function TopNav({ swipeCounts={swipeCounts} totalCards={totalCards} userProfiles={userProfiles} - onToast={showToast} + onToast={toast.show} /> )} diff --git a/src/hooks/useToast.ts b/src/hooks/useToast.ts new file mode 100644 index 0000000..16abab4 --- /dev/null +++ b/src/hooks/useToast.ts @@ -0,0 +1,17 @@ +import { useState, useCallback, useRef } from "react"; + +export function useToast(duration = 2200) { + const [message, setMessage] = useState(""); + const timerRef = useRef>(undefined); + + const show = useCallback( + (msg: string) => { + clearTimeout(timerRef.current); + setMessage(msg); + timerRef.current = setTimeout(() => setMessage(""), duration); + }, + [duration], + ); + + return { message, show } as const; +}