feat: 优化定位体验——反向地理编码显示地名,定位失败明确提示
- 新增 /api/location/regeo 接口,通过高德逆地理编码将 GPS 坐标转为地名 - 页面加载时自动定位,成功后显示"当前位置:浦东新区 xxx" - 定位失败/权限被拒时显示明确提示+重试按钮,不再静默默认上海 - 无可用位置时阻止创建房间,引导用户手动搜索选择地点
This commit is contained in:
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
+101
-21
@@ -18,7 +18,7 @@ interface LocationSuggestion {
|
|||||||
lng: number;
|
lng: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SHANGHAI_COORDS = { lat: 31.2222, lng: 121.4764 };
|
type GpsStatus = "idle" | "locating" | "success" | "failed" | "denied";
|
||||||
|
|
||||||
const DISTANCE_OPTIONS = [
|
const DISTANCE_OPTIONS = [
|
||||||
{ label: "1km", value: 1000 },
|
{ label: "1km", value: 1000 },
|
||||||
@@ -35,26 +35,44 @@ const PRICE_OPTIONS = [
|
|||||||
|
|
||||||
const HOT_CUISINES = ["火锅", "日料", "烧烤", "西餐", "川菜", "咖啡甜品", "小吃快餐"] as const;
|
const HOT_CUISINES = ["火锅", "日料", "烧烤", "西餐", "川菜", "咖啡甜品", "小吃快餐"] as const;
|
||||||
|
|
||||||
function getLocation(): Promise<{ lat: number; lng: number }> {
|
type GpsResult =
|
||||||
if (process.env.NODE_ENV === "development") {
|
| { ok: true; lat: number; lng: number }
|
||||||
return Promise.resolve(SHANGHAI_COORDS);
|
| { ok: false; reason: "unsupported" | "denied" | "timeout" | "unknown" };
|
||||||
}
|
|
||||||
|
|
||||||
|
function requestGps(): Promise<GpsResult> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!navigator.geolocation) {
|
if (!navigator.geolocation) {
|
||||||
resolve(SHANGHAI_COORDS);
|
resolve({ ok: false, reason: "unsupported" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
(pos) =>
|
(pos) =>
|
||||||
resolve({ lat: pos.coords.latitude, lng: pos.coords.longitude }),
|
resolve({ ok: true, lat: pos.coords.latitude, lng: pos.coords.longitude }),
|
||||||
() => resolve(SHANGHAI_COORDS),
|
(err) => {
|
||||||
{ timeout: 5000, enableHighAccuracy: false },
|
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 LandingPage() {
|
export default function LandingPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [roomCode, setRoomCode] = useState("");
|
const [roomCode, setRoomCode] = useState("");
|
||||||
@@ -73,6 +91,10 @@ export default function LandingPage() {
|
|||||||
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 [gpsCoords, setGpsCoords] = useState<{ lat: number; lng: number } | null>(null);
|
||||||
|
const [gpsLocationName, setGpsLocationName] = useState<string | null>(null);
|
||||||
|
|
||||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||||
const [authModalOpen, setAuthModalOpen] = useState(false);
|
const [authModalOpen, setAuthModalOpen] = useState(false);
|
||||||
|
|
||||||
@@ -86,6 +108,25 @@ export default function LandingPage() {
|
|||||||
if (prefs.radius) setRadius(prefs.radius);
|
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) => {
|
const fetchSuggestions = useCallback(async (query: string) => {
|
||||||
if (query.length < 1) {
|
if (query.length < 1) {
|
||||||
setSuggestions([]);
|
setSuggestions([]);
|
||||||
@@ -148,20 +189,26 @@ export default function LandingPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
setLoading(true);
|
|
||||||
setError("");
|
setError("");
|
||||||
|
|
||||||
try {
|
let coords: { lat: number; lng: number };
|
||||||
let coords: { lat: number; lng: number };
|
|
||||||
|
|
||||||
if (selectedLocation) {
|
if (selectedLocation) {
|
||||||
coords = { lat: selectedLocation.lat, lng: selectedLocation.lng };
|
coords = { lat: selectedLocation.lat, lng: selectedLocation.lng };
|
||||||
setLoadingText("正在搜索周边美食...");
|
} else if (gpsCoords) {
|
||||||
} else {
|
coords = gpsCoords;
|
||||||
setLoadingText("正在获取位置...");
|
} else if (gpsStatus === "locating") {
|
||||||
coords = await getLocation();
|
setError("正在定位中,请稍候...");
|
||||||
setLoadingText("正在搜索周边美食...");
|
return;
|
||||||
}
|
} else {
|
||||||
|
setError("无法获取位置,请在上方搜索并选择一个地点");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoadingText("正在搜索周边美食...");
|
||||||
|
|
||||||
const res = await fetch("/api/room/create", {
|
const res = await fetch("/api/room/create", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -321,7 +368,40 @@ export default function LandingPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!selectedLocation && !locationQuery && (
|
{!selectedLocation && !locationQuery && gpsStatus === "locating" && (
|
||||||
|
<div className="mt-1.5 flex items-center gap-1.5 px-1">
|
||||||
|
<Loader2 size={12} className="shrink-0 animate-spin text-emerald-400" />
|
||||||
|
<span className="text-xs text-zinc-400">正在获取当前位置...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!selectedLocation && !locationQuery && gpsStatus === "success" && (
|
||||||
|
<div className="mt-1.5 flex items-center gap-1.5 px-1">
|
||||||
|
<Navigation size={12} className="shrink-0 text-emerald-500" />
|
||||||
|
<span className="truncate text-xs text-emerald-600">
|
||||||
|
当前位置:{gpsLocationName || "已定位"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!selectedLocation && !locationQuery && (gpsStatus === "failed" || gpsStatus === "denied") && (
|
||||||
|
<div className="mt-1.5 flex items-center justify-between px-1">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<MapPin size={12} className="shrink-0 text-amber-500" />
|
||||||
|
<span className="text-xs text-amber-600">
|
||||||
|
{gpsStatus === "denied" ? "定位权限被拒绝" : "定位失败"},请搜索选择位置
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={doGpsLocate}
|
||||||
|
className="shrink-0 text-xs font-medium text-emerald-500 active:text-emerald-700"
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!selectedLocation && !locationQuery && gpsStatus === "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-zinc-400" />
|
<Navigation size={12} className="shrink-0 text-zinc-400" />
|
||||||
<span className="text-xs text-zinc-400">将使用当前定位</span>
|
<span className="text-xs text-zinc-400">将使用当前定位</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user