From fc0a2a018b2a345c5ba61a9ac13148c2cb1e4d09 Mon Sep 17 00:00:00 2001 From: kurihada Date: Tue, 24 Feb 2026 20:21:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8B=A6=E6=88=AA=E6=B5=8F=E8=A7=88?= =?UTF-8?q?=E5=99=A8=E8=BF=94=E5=9B=9E=E9=94=AE=EF=BC=8C=E9=80=80=E5=87=BA?= =?UTF-8?q?=E6=88=BF=E9=97=B4=E5=89=8D=E5=BC=B9=E7=AA=97=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/room/[id]/page.tsx | 48 +++++++++++++++++- src/components/LeaveConfirmModal.tsx | 75 ++++++++++++++++++++++++++++ src/components/TopNav.tsx | 7 ++- 3 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 src/components/LeaveConfirmModal.tsx diff --git a/src/app/room/[id]/page.tsx b/src/app/room/[id]/page.tsx index 86147ef..9b2cc12 100644 --- a/src/app/room/[id]/page.tsx +++ b/src/app/room/[id]/page.tsx @@ -1,9 +1,10 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useRef } from "react"; import { useParams, useRouter } from "next/navigation"; import TopNav from "@/components/TopNav"; import SwipeDeck from "@/components/SwipeDeck"; +import LeaveConfirmModal from "@/components/LeaveConfirmModal"; import { useRoomPolling } from "@/hooks/useRoomPolling"; import { getUserId } from "@/lib/userId"; @@ -15,6 +16,8 @@ export default function RoomPage() { const [userId, setUserId] = useState(""); const [joined, setJoined] = useState(false); const [joinFailed, setJoinFailed] = useState(false); + const [showLeaveConfirm, setShowLeaveConfirm] = useState(false); + const leavingRef = useRef(false); const { userCount, match, matchType, matchLikes, runnerUps, likeCounts, swipeCounts, restaurants, notFound, mutate, @@ -34,6 +37,42 @@ export default function RoomPage() { }).catch(() => setJoinFailed(true)); }, [roomId]); + useEffect(() => { + window.history.pushState({ roomGuard: true }, ""); + + const handlePopState = () => { + if (leavingRef.current) return; + window.history.pushState({ roomGuard: true }, ""); + setShowLeaveConfirm(true); + }; + + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (leavingRef.current) return; + e.preventDefault(); + }; + + window.addEventListener("popstate", handlePopState); + window.addEventListener("beforeunload", handleBeforeUnload); + return () => { + window.removeEventListener("popstate", handlePopState); + window.removeEventListener("beforeunload", handleBeforeUnload); + }; + }, []); + + const confirmLeave = useCallback(() => { + leavingRef.current = true; + setShowLeaveConfirm(false); + router.push("/"); + }, [router]); + + const cancelLeave = useCallback(() => { + setShowLeaveConfirm(false); + }, []); + + const handleExitRequest = useCallback(() => { + setShowLeaveConfirm(true); + }, []); + const handleReset = useCallback(async () => { await fetch(`/api/room/${roomId}/reset`, { method: "POST" }); await mutate(); @@ -78,7 +117,7 @@ export default function RoomPage() { return (
- + +
); } diff --git a/src/components/LeaveConfirmModal.tsx b/src/components/LeaveConfirmModal.tsx new file mode 100644 index 0000000..aaddda7 --- /dev/null +++ b/src/components/LeaveConfirmModal.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useRef } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { LogOut } from "lucide-react"; + +interface LeaveConfirmModalProps { + open: boolean; + onConfirm: () => void; + onCancel: () => void; +} + +export default function LeaveConfirmModal({ + open, + onConfirm, + onCancel, +}: LeaveConfirmModalProps) { + const backdropRef = useRef(null); + + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === backdropRef.current) onCancel(); + }; + + return ( + + {open && ( + + +
+
+ +
+ +

+ 确定要退出房间吗? +

+

+ 退出后你的滑卡进度不会丢失,可以用房间号重新加入 +

+ +
+ + +
+
+
+
+ )} +
+ ); +} diff --git a/src/components/TopNav.tsx b/src/components/TopNav.tsx index 3791a9f..ee4de36 100644 --- a/src/components/TopNav.tsx +++ b/src/components/TopNav.tsx @@ -1,7 +1,6 @@ "use client"; import { useState, useCallback } from "react"; -import { useRouter } from "next/navigation"; import { Users, QrCode, LogOut } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; import QrInviteModal from "./QrInviteModal"; @@ -9,10 +8,10 @@ import QrInviteModal from "./QrInviteModal"; interface TopNavProps { roomId: string; userCount: number; + onExit?: () => void; } -export default function TopNav({ roomId, userCount }: TopNavProps) { - const router = useRouter(); +export default function TopNav({ roomId, userCount, onExit }: TopNavProps) { const [toast, setToast] = useState(""); const [showQr, setShowQr] = useState(false); @@ -50,7 +49,7 @@ export default function TopNav({ roomId, userCount }: TopNavProps) { {userCount}