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}