diff --git a/src/app/api/location/regeo/route.ts b/src/app/api/location/regeo/route.ts new file mode 100644 index 0000000..8225c5a --- /dev/null +++ b/src/app/api/location/regeo/route.ts @@ -0,0 +1,54 @@ +import { NextResponse } from "next/server"; + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const lat = searchParams.get("lat"); + const lng = searchParams.get("lng"); + + if (!lat || !lng) { + return NextResponse.json( + { error: "lat and lng are required" }, + { status: 400 }, + ); + } + + const apiKey = process.env.AMAP_API_KEY; + if (!apiKey) { + return NextResponse.json( + { error: "AMAP_API_KEY not configured" }, + { status: 500 }, + ); + } + + try { + const url = new URL("https://restapi.amap.com/v3/geocode/regeo"); + url.searchParams.set("key", apiKey); + url.searchParams.set("location", `${lng},${lat}`); + url.searchParams.set("extensions", "base"); + + const res = await fetch(url.toString()); + const data = await res.json(); + + if (data.status !== "1" || !data.regeocode) { + return NextResponse.json({ name: null }); + } + + const comp = data.regeocode.addressComponent; + const district = comp?.district || comp?.city || ""; + const township = comp?.township || ""; + const neighborhood = comp?.neighborhood?.name || ""; + + const name = [district, township, neighborhood] + .filter(Boolean) + .join(" ") + .trim(); + + return NextResponse.json({ + name: name || data.regeocode.formatted_address || null, + formatted: data.regeocode.formatted_address || null, + }); + } catch (e) { + console.error("Regeo error:", e); + return NextResponse.json({ name: null }); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index a050c5b..8e55d1a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -18,7 +18,7 @@ interface LocationSuggestion { lng: number; } -const SHANGHAI_COORDS = { lat: 31.2222, lng: 121.4764 }; +type GpsStatus = "idle" | "locating" | "success" | "failed" | "denied"; const DISTANCE_OPTIONS = [ { label: "1km", value: 1000 }, @@ -35,26 +35,44 @@ const PRICE_OPTIONS = [ const HOT_CUISINES = ["火锅", "日料", "烧烤", "西餐", "川菜", "咖啡甜品", "小吃快餐"] as const; -function getLocation(): Promise<{ lat: number; lng: number }> { - if (process.env.NODE_ENV === "development") { - return Promise.resolve(SHANGHAI_COORDS); - } +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(SHANGHAI_COORDS); + resolve({ ok: false, reason: "unsupported" }); return; } navigator.geolocation.getCurrentPosition( (pos) => - resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude }), - () => resolve(SHANGHAI_COORDS), - { timeout: 5000, enableHighAccuracy: false }, + 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 LandingPage() { const router = useRouter(); const [roomCode, setRoomCode] = useState(""); @@ -73,6 +91,10 @@ export default function LandingPage() { 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 [profile, setProfile] = useState(null); const [authModalOpen, setAuthModalOpen] = useState(false); @@ -86,6 +108,25 @@ export default function LandingPage() { if (prefs.radius) setRadius(prefs.radius); }, []); + 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([]); @@ -148,20 +189,26 @@ export default function LandingPage() { }; const handleCreate = async () => { - setLoading(true); setError(""); - try { - let coords: { lat: number; lng: number }; + let coords: { lat: number; lng: number }; - if (selectedLocation) { - coords = { lat: selectedLocation.lat, lng: selectedLocation.lng }; - setLoadingText("正在搜索周边美食..."); - } else { - setLoadingText("正在获取位置..."); - coords = await getLocation(); - setLoadingText("正在搜索周边美食..."); - } + if (selectedLocation) { + coords = { lat: selectedLocation.lat, lng: selectedLocation.lng }; + } else if (gpsCoords) { + coords = gpsCoords; + } else if (gpsStatus === "locating") { + setError("正在定位中,请稍候..."); + return; + } else { + setError("无法获取位置,请在上方搜索并选择一个地点"); + return; + } + + setLoading(true); + + try { + setLoadingText("正在搜索周边美食..."); const res = await fetch("/api/room/create", { method: "POST", @@ -321,7 +368,40 @@ export default function LandingPage() { )} - {!selectedLocation && !locationQuery && ( + {!selectedLocation && !locationQuery && gpsStatus === "locating" && ( +
+ + 正在获取当前位置... +
+ )} + + {!selectedLocation && !locationQuery && gpsStatus === "success" && ( +
+ + + 当前位置:{gpsLocationName || "已定位"} + +
+ )} + + {!selectedLocation && !locationQuery && (gpsStatus === "failed" || gpsStatus === "denied") && ( +
+
+ + + {gpsStatus === "denied" ? "定位权限被拒绝" : "定位失败"},请搜索选择位置 + +
+ +
+ )} + + {!selectedLocation && !locationQuery && gpsStatus === "idle" && (
将使用当前定位