refactor: 提取 useGeolocation hook 和 joinRoom 工具函数
- useGeolocation: 将 PanicPage 中 ~50 行 GPS 定位逻辑(requestGps + reverseGeocode + 状态管理)提取为独立 hook - joinRoom: 统一 3 处重复的 POST /api/room/:id/join 调用(room、invite、panic 页面)
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
|||||||
Coffee,
|
Coffee,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { getUserId } from "@/lib/userId";
|
import { getUserId } from "@/lib/userId";
|
||||||
|
import { joinRoom } from "@/lib/room";
|
||||||
import { Skeleton, SkeletonCircle } from "@/components/Skeleton";
|
import { Skeleton, SkeletonCircle } from "@/components/Skeleton";
|
||||||
import Button from "@/components/Button";
|
import Button from "@/components/Button";
|
||||||
import { getSceneConfig } from "@/lib/sceneConfig";
|
import { getSceneConfig } from "@/lib/sceneConfig";
|
||||||
@@ -48,12 +49,7 @@ export default function InvitePage() {
|
|||||||
const handleJoin = async () => {
|
const handleJoin = async () => {
|
||||||
setJoining(true);
|
setJoining(true);
|
||||||
try {
|
try {
|
||||||
const userId = getUserId();
|
await joinRoom(roomId, getUserId());
|
||||||
await fetch(`/api/room/${roomId}/join`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ userId }),
|
|
||||||
});
|
|
||||||
router.push(`/room/${roomId}`);
|
router.push(`/room/${roomId}`);
|
||||||
} catch {
|
} catch {
|
||||||
setJoining(false);
|
setJoining(false);
|
||||||
|
|||||||
+15
-84
@@ -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 { Plus, LogIn, Loader2, MapPin, Navigation, X, Users, Heart, Sparkles, ChevronRight, Flame, ArrowLeft } from "lucide-react";
|
||||||
import { getUserId, getCachedPreferences } from "@/lib/userId";
|
import { getUserId, getCachedPreferences } from "@/lib/userId";
|
||||||
import { SCENES, getSceneConfig } from "@/lib/sceneConfig";
|
import { SCENES, getSceneConfig } from "@/lib/sceneConfig";
|
||||||
|
import { useGeolocation } from "@/hooks/useGeolocation";
|
||||||
|
import { joinRoom } from "@/lib/room";
|
||||||
import type { SceneType } from "@/types";
|
import type { SceneType } from "@/types";
|
||||||
|
|
||||||
interface LocationSuggestion {
|
interface LocationSuggestion {
|
||||||
@@ -17,52 +19,12 @@ interface LocationSuggestion {
|
|||||||
lng: number;
|
lng: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type GpsStatus = "idle" | "locating" | "success" | "failed" | "denied";
|
|
||||||
|
|
||||||
const DISTANCE_OPTIONS = [
|
const DISTANCE_OPTIONS = [
|
||||||
{ label: "1km", value: 1000 },
|
{ label: "1km", value: 1000 },
|
||||||
{ label: "3km", value: 3000 },
|
{ label: "3km", value: 3000 },
|
||||||
{ label: "5km", value: 5000 },
|
{ label: "5km", value: 5000 },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
type GpsResult =
|
|
||||||
| { ok: true; lat: number; lng: number }
|
|
||||||
| { ok: false; reason: "unsupported" | "denied" | "timeout" | "unknown" };
|
|
||||||
|
|
||||||
function requestGps(): Promise<GpsResult> {
|
|
||||||
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<string | null> {
|
|
||||||
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() {
|
export default function PanicPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [roomCode, setRoomCode] = useState("");
|
const [roomCode, setRoomCode] = useState("");
|
||||||
@@ -81,9 +43,7 @@ export default function PanicPage() {
|
|||||||
const suggestRef = useRef<HTMLDivElement>(null);
|
const suggestRef = useRef<HTMLDivElement>(null);
|
||||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>(null);
|
const debounceRef = useRef<ReturnType<typeof setTimeout>>(null);
|
||||||
|
|
||||||
const [gpsStatus, setGpsStatus] = useState<GpsStatus>("idle");
|
const geo = useGeolocation();
|
||||||
const [gpsCoords, setGpsCoords] = useState<{ lat: number; lng: number } | null>(null);
|
|
||||||
const [gpsLocationName, setGpsLocationName] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const [scene, setScene] = useState<SceneType>("eat");
|
const [scene, setScene] = useState<SceneType>("eat");
|
||||||
const sceneConfig = getSceneConfig(scene);
|
const sceneConfig = getSceneConfig(scene);
|
||||||
@@ -101,25 +61,6 @@ export default function PanicPage() {
|
|||||||
setPriceRange("any");
|
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) => {
|
const fetchSuggestions = useCallback(async (query: string) => {
|
||||||
if (query.length < 1) {
|
if (query.length < 1) {
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
@@ -170,16 +111,6 @@ export default function PanicPage() {
|
|||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
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 () => {
|
const handleCreate = async () => {
|
||||||
setError("");
|
setError("");
|
||||||
@@ -188,9 +119,9 @@ export default function PanicPage() {
|
|||||||
|
|
||||||
if (selectedLocation) {
|
if (selectedLocation) {
|
||||||
coords = { lat: selectedLocation.lat, lng: selectedLocation.lng };
|
coords = { lat: selectedLocation.lat, lng: selectedLocation.lng };
|
||||||
} else if (gpsCoords) {
|
} else if (geo.coords) {
|
||||||
coords = gpsCoords;
|
coords = geo.coords;
|
||||||
} else if (gpsStatus === "locating") {
|
} else if (geo.status === "locating") {
|
||||||
setError("正在定位中,请稍候...");
|
setError("正在定位中,请稍候...");
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@@ -220,7 +151,7 @@ export default function PanicPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoadingText("正在进入房间...");
|
setLoadingText("正在进入房间...");
|
||||||
await joinRoom(data.roomId);
|
await joinRoom(data.roomId, getUserId());
|
||||||
router.push(`/room/${data.roomId}`);
|
router.push(`/room/${data.roomId}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : "创建失败,请重试");
|
setError(e instanceof Error ? e.message : "创建失败,请重试");
|
||||||
@@ -238,7 +169,7 @@ export default function PanicPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
await joinRoom(roomCode);
|
await joinRoom(roomCode, getUserId());
|
||||||
router.push(`/room/${roomCode}`);
|
router.push(`/room/${roomCode}`);
|
||||||
} catch {
|
} catch {
|
||||||
setError("房间不存在,请检查房间号");
|
setError("房间不存在,请检查房间号");
|
||||||
@@ -384,32 +315,32 @@ export default function PanicPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!selectedLocation && !locationQuery && gpsStatus === "locating" && (
|
{!selectedLocation && !locationQuery && geo.status === "locating" && (
|
||||||
<div className="mt-1.5 flex items-center gap-1.5 px-1">
|
<div className="mt-1.5 flex items-center gap-1.5 px-1">
|
||||||
<Loader2 size={12} className="shrink-0 animate-spin text-orange-400" />
|
<Loader2 size={12} className="shrink-0 animate-spin text-orange-400" />
|
||||||
<span className="text-xs text-muted">正在获取当前位置...</span>
|
<span className="text-xs text-muted">正在获取当前位置...</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!selectedLocation && !locationQuery && gpsStatus === "success" && (
|
{!selectedLocation && !locationQuery && geo.status === "success" && (
|
||||||
<div className="mt-1.5 flex items-center gap-1.5 px-1">
|
<div className="mt-1.5 flex items-center gap-1.5 px-1">
|
||||||
<Navigation size={12} className="shrink-0 text-orange-400" />
|
<Navigation size={12} className="shrink-0 text-orange-400" />
|
||||||
<span className="truncate text-xs text-orange-300/80">
|
<span className="truncate text-xs text-orange-300/80">
|
||||||
当前位置:{gpsLocationName || "已定位"}
|
当前位置:{geo.locationName || "已定位"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!selectedLocation && !locationQuery && (gpsStatus === "failed" || gpsStatus === "denied") && (
|
{!selectedLocation && !locationQuery && (geo.status === "failed" || geo.status === "denied") && (
|
||||||
<div className="mt-1.5 flex items-center justify-between px-1">
|
<div className="mt-1.5 flex items-center justify-between px-1">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<MapPin size={12} className="shrink-0 text-amber-500" />
|
<MapPin size={12} className="shrink-0 text-amber-500" />
|
||||||
<span className="text-xs text-amber-400/80">
|
<span className="text-xs text-amber-400/80">
|
||||||
{gpsStatus === "denied" ? "定位权限被拒绝" : "定位失败"},请搜索选择位置
|
{geo.status === "denied" ? "定位权限被拒绝" : "定位失败"},请搜索选择位置
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={doGpsLocate}
|
onClick={geo.retry}
|
||||||
className="shrink-0 text-xs font-medium text-orange-400 active:text-orange-300"
|
className="shrink-0 text-xs font-medium text-orange-400 active:text-orange-300"
|
||||||
>
|
>
|
||||||
重试
|
重试
|
||||||
@@ -417,7 +348,7 @@ export default function PanicPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!selectedLocation && !locationQuery && gpsStatus === "idle" && (
|
{!selectedLocation && !locationQuery && geo.status === "idle" && (
|
||||||
<div className="mt-1.5 flex items-center gap-1.5 px-1">
|
<div className="mt-1.5 flex items-center gap-1.5 px-1">
|
||||||
<Navigation size={12} className="shrink-0 text-dim" />
|
<Navigation size={12} className="shrink-0 text-dim" />
|
||||||
<span className="text-xs text-dim">将使用当前定位</span>
|
<span className="text-xs text-dim">将使用当前定位</span>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import LeaveConfirmModal from "@/components/LeaveConfirmModal";
|
|||||||
import Button from "@/components/Button";
|
import Button from "@/components/Button";
|
||||||
import { useRoomPolling } from "@/hooks/useRoomPolling";
|
import { useRoomPolling } from "@/hooks/useRoomPolling";
|
||||||
import { getUserId } from "@/lib/userId";
|
import { getUserId } from "@/lib/userId";
|
||||||
|
import { joinRoom } from "@/lib/room";
|
||||||
import { getSceneConfig } from "@/lib/sceneConfig";
|
import { getSceneConfig } from "@/lib/sceneConfig";
|
||||||
|
|
||||||
export default function RoomPage() {
|
export default function RoomPage() {
|
||||||
@@ -31,14 +32,9 @@ export default function RoomPage() {
|
|||||||
const id = getUserId();
|
const id = getUserId();
|
||||||
setUserId(id);
|
setUserId(id);
|
||||||
|
|
||||||
fetch(`/api/room/${roomId}/join`, {
|
joinRoom(roomId, id)
|
||||||
method: "POST",
|
.then(() => setJoined(true))
|
||||||
headers: { "Content-Type": "application/json" },
|
.catch(() => setJoinFailed(true));
|
||||||
body: JSON.stringify({ userId: id }),
|
|
||||||
}).then((res) => {
|
|
||||||
if (res.ok) setJoined(true);
|
|
||||||
else setJoinFailed(true);
|
|
||||||
}).catch(() => setJoinFailed(true));
|
|
||||||
}, [roomId]);
|
}, [roomId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -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<GpsResult> {
|
||||||
|
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<string | null> {
|
||||||
|
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<GpsStatus>("idle");
|
||||||
|
const [coords, setCoords] = useState<{ lat: number; lng: number } | null>(null);
|
||||||
|
const [locationName, setLocationName] = useState<string | null>(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 };
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export async function joinRoom(roomId: string, userId: string): Promise<void> {
|
||||||
|
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 || "加入房间失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user