diff --git a/src/app/invite/[id]/page.tsx b/src/app/invite/[id]/page.tsx index 163f275..817475a 100644 --- a/src/app/invite/[id]/page.tsx +++ b/src/app/invite/[id]/page.tsx @@ -12,6 +12,7 @@ import { Coffee, } from "lucide-react"; import { getUserId } from "@/lib/userId"; +import { joinRoom } from "@/lib/room"; import { Skeleton, SkeletonCircle } from "@/components/Skeleton"; import Button from "@/components/Button"; import { getSceneConfig } from "@/lib/sceneConfig"; @@ -48,12 +49,7 @@ export default function InvitePage() { const handleJoin = async () => { setJoining(true); try { - const userId = getUserId(); - await fetch(`/api/room/${roomId}/join`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ userId }), - }); + await joinRoom(roomId, getUserId()); router.push(`/room/${roomId}`); } catch { setJoining(false); diff --git a/src/app/panic/page.tsx b/src/app/panic/page.tsx index b6b6e8c..67b056f 100644 --- a/src/app/panic/page.tsx +++ b/src/app/panic/page.tsx @@ -6,6 +6,8 @@ import { motion, AnimatePresence } from "framer-motion"; import { Plus, LogIn, Loader2, MapPin, Navigation, X, Users, Heart, Sparkles, ChevronRight, Flame, ArrowLeft } from "lucide-react"; import { getUserId, getCachedPreferences } from "@/lib/userId"; import { SCENES, getSceneConfig } from "@/lib/sceneConfig"; +import { useGeolocation } from "@/hooks/useGeolocation"; +import { joinRoom } from "@/lib/room"; import type { SceneType } from "@/types"; interface LocationSuggestion { @@ -17,52 +19,12 @@ interface LocationSuggestion { lng: number; } -type GpsStatus = "idle" | "locating" | "success" | "failed" | "denied"; - const DISTANCE_OPTIONS = [ { label: "1km", value: 1000 }, { label: "3km", value: 3000 }, { label: "5km", value: 5000 }, ] as const; -type GpsResult = - | { ok: true; lat: number; lng: number } - | { ok: false; reason: "unsupported" | "denied" | "timeout" | "unknown" }; - -function requestGps(): Promise { - return new Promise((resolve) => { - if (!navigator.geolocation) { - resolve({ ok: false, reason: "unsupported" }); - return; - } - - navigator.geolocation.getCurrentPosition( - (pos) => - resolve({ ok: true, lat: pos.coords.latitude, lng: pos.coords.longitude }), - (err) => { - const reason = - err.code === err.PERMISSION_DENIED - ? "denied" - : err.code === err.TIMEOUT - ? "timeout" - : "unknown"; - resolve({ ok: false, reason }); - }, - { timeout: 8000, enableHighAccuracy: false }, - ); - }); -} - -async function reverseGeocode(lat: number, lng: number): Promise { - try { - const res = await fetch(`/api/location/regeo?lat=${lat}&lng=${lng}`); - const data = await res.json(); - return data.name || data.formatted || null; - } catch { - return null; - } -} - export default function PanicPage() { const router = useRouter(); const [roomCode, setRoomCode] = useState(""); @@ -81,9 +43,7 @@ export default function PanicPage() { const suggestRef = useRef(null); const debounceRef = useRef>(null); - const [gpsStatus, setGpsStatus] = useState("idle"); - const [gpsCoords, setGpsCoords] = useState<{ lat: number; lng: number } | null>(null); - const [gpsLocationName, setGpsLocationName] = useState(null); + const geo = useGeolocation(); const [scene, setScene] = useState("eat"); const sceneConfig = getSceneConfig(scene); @@ -101,25 +61,6 @@ export default function PanicPage() { setPriceRange("any"); }, []); - const doGpsLocate = useCallback(async () => { - setGpsStatus("locating"); - const result = await requestGps(); - if (result.ok) { - setGpsCoords({ lat: result.lat, lng: result.lng }); - setGpsStatus("success"); - const name = await reverseGeocode(result.lat, result.lng); - if (name) setGpsLocationName(name); - } else { - setGpsCoords(null); - setGpsLocationName(null); - setGpsStatus(result.reason === "denied" ? "denied" : "failed"); - } - }, []); - - useEffect(() => { - doGpsLocate(); - }, [doGpsLocate]); - const fetchSuggestions = useCallback(async (query: string) => { if (query.length < 1) { setSuggestions([]); @@ -170,16 +111,6 @@ export default function PanicPage() { return () => document.removeEventListener("mousedown", handleClickOutside); }, []); - const joinRoom = async (roomId: string) => { - const userId = getUserId(); - const res = await fetch(`/api/room/${roomId}/join`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ userId }), - }); - if (!res.ok) throw new Error("房间不存在"); - return roomId; - }; const handleCreate = async () => { setError(""); @@ -188,9 +119,9 @@ export default function PanicPage() { if (selectedLocation) { coords = { lat: selectedLocation.lat, lng: selectedLocation.lng }; - } else if (gpsCoords) { - coords = gpsCoords; - } else if (gpsStatus === "locating") { + } else if (geo.coords) { + coords = geo.coords; + } else if (geo.status === "locating") { setError("正在定位中,请稍候..."); return; } else { @@ -220,7 +151,7 @@ export default function PanicPage() { } setLoadingText("正在进入房间..."); - await joinRoom(data.roomId); + await joinRoom(data.roomId, getUserId()); router.push(`/room/${data.roomId}`); } catch (e) { setError(e instanceof Error ? e.message : "创建失败,请重试"); @@ -238,7 +169,7 @@ export default function PanicPage() { setLoading(true); setError(""); try { - await joinRoom(roomCode); + await joinRoom(roomCode, getUserId()); router.push(`/room/${roomCode}`); } catch { setError("房间不存在,请检查房间号"); @@ -384,32 +315,32 @@ export default function PanicPage() { )} - {!selectedLocation && !locationQuery && gpsStatus === "locating" && ( + {!selectedLocation && !locationQuery && geo.status === "locating" && (
正在获取当前位置...
)} - {!selectedLocation && !locationQuery && gpsStatus === "success" && ( + {!selectedLocation && !locationQuery && geo.status === "success" && (
- 当前位置:{gpsLocationName || "已定位"} + 当前位置:{geo.locationName || "已定位"}
)} - {!selectedLocation && !locationQuery && (gpsStatus === "failed" || gpsStatus === "denied") && ( + {!selectedLocation && !locationQuery && (geo.status === "failed" || geo.status === "denied") && (
- {gpsStatus === "denied" ? "定位权限被拒绝" : "定位失败"},请搜索选择位置 + {geo.status === "denied" ? "定位权限被拒绝" : "定位失败"},请搜索选择位置
)} - {!selectedLocation && !locationQuery && gpsStatus === "idle" && ( + {!selectedLocation && !locationQuery && geo.status === "idle" && (
将使用当前定位 diff --git a/src/app/room/[id]/page.tsx b/src/app/room/[id]/page.tsx index 5b095c1..f2e9f69 100644 --- a/src/app/room/[id]/page.tsx +++ b/src/app/room/[id]/page.tsx @@ -9,6 +9,7 @@ import LeaveConfirmModal from "@/components/LeaveConfirmModal"; import Button from "@/components/Button"; import { useRoomPolling } from "@/hooks/useRoomPolling"; import { getUserId } from "@/lib/userId"; +import { joinRoom } from "@/lib/room"; import { getSceneConfig } from "@/lib/sceneConfig"; export default function RoomPage() { @@ -31,14 +32,9 @@ export default function RoomPage() { const id = getUserId(); setUserId(id); - fetch(`/api/room/${roomId}/join`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ userId: id }), - }).then((res) => { - if (res.ok) setJoined(true); - else setJoinFailed(true); - }).catch(() => setJoinFailed(true)); + joinRoom(roomId, id) + .then(() => setJoined(true)) + .catch(() => setJoinFailed(true)); }, [roomId]); useEffect(() => { diff --git a/src/hooks/useGeolocation.ts b/src/hooks/useGeolocation.ts new file mode 100644 index 0000000..2f154ca --- /dev/null +++ b/src/hooks/useGeolocation.ts @@ -0,0 +1,70 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; + +export type GpsStatus = "idle" | "locating" | "success" | "failed" | "denied"; + +type GpsResult = + | { ok: true; lat: number; lng: number } + | { ok: false; reason: "unsupported" | "denied" | "timeout" | "unknown" }; + +function requestGps(): Promise { + return new Promise((resolve) => { + if (!navigator.geolocation) { + resolve({ ok: false, reason: "unsupported" }); + return; + } + + navigator.geolocation.getCurrentPosition( + (pos) => + resolve({ ok: true, lat: pos.coords.latitude, lng: pos.coords.longitude }), + (err) => { + const reason = + err.code === err.PERMISSION_DENIED + ? "denied" + : err.code === err.TIMEOUT + ? "timeout" + : "unknown"; + resolve({ ok: false, reason }); + }, + { timeout: 8000, enableHighAccuracy: false }, + ); + }); +} + +async function reverseGeocode(lat: number, lng: number): Promise { + try { + const res = await fetch(`/api/location/regeo?lat=${lat}&lng=${lng}`); + const data = await res.json(); + return data.name || data.formatted || null; + } catch { + return null; + } +} + +export function useGeolocation() { + const [status, setStatus] = useState("idle"); + const [coords, setCoords] = useState<{ lat: number; lng: number } | null>(null); + const [locationName, setLocationName] = useState(null); + + const locate = useCallback(async () => { + setStatus("locating"); + const result = await requestGps(); + if (result.ok) { + setCoords({ lat: result.lat, lng: result.lng }); + setStatus("success"); + const name = await reverseGeocode(result.lat, result.lng); + if (name) setLocationName(name); + } else { + setCoords(null); + setLocationName(null); + setStatus(result.reason === "denied" ? "denied" : "failed"); + } + }, []); + + useEffect(() => { + locate(); + }, [locate]); + + return { status, coords, locationName, retry: locate }; +} diff --git a/src/lib/room.ts b/src/lib/room.ts new file mode 100644 index 0000000..cf617a2 --- /dev/null +++ b/src/lib/room.ts @@ -0,0 +1,11 @@ +export async function joinRoom(roomId: string, userId: string): Promise { + const res = await fetch(`/api/room/${roomId}/join`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userId }), + }); + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || "加入房间失败"); + } +}