From 4e2d11f0a5820e75eec85b4a70f75fc5706dd6eb Mon Sep 17 00:00:00 2001 From: kurihada Date: Tue, 24 Feb 2026 18:06:48 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E6=88=BF=E9=97=B4=E6=97=B6=E6=8C=87=E5=AE=9A=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E9=A4=90=E5=8E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增位置搜索框,通过高德输入提示 API 提供地点联想, 用户可选择指定位置或使用默认当前定位来查询周边餐厅。 --- src/app/api/location/suggest/route.ts | 52 +++++++++ src/app/page.tsx | 154 +++++++++++++++++++++++++- 2 files changed, 200 insertions(+), 6 deletions(-) create mode 100644 src/app/api/location/suggest/route.ts diff --git a/src/app/api/location/suggest/route.ts b/src/app/api/location/suggest/route.ts new file mode 100644 index 0000000..2771b72 --- /dev/null +++ b/src/app/api/location/suggest/route.ts @@ -0,0 +1,52 @@ +import { NextResponse } from "next/server"; + +export async function GET(req: Request) { + const { searchParams } = new URL(req.url); + const keywords = searchParams.get("keywords")?.trim(); + + if (!keywords) { + return NextResponse.json([]); + } + + 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/assistant/inputtips"); + url.searchParams.set("key", apiKey); + url.searchParams.set("keywords", keywords); + url.searchParams.set("datatype", "poi"); + + const res = await fetch(url.toString()); + const data = await res.json(); + + if (data.status !== "1" || !data.tips) { + return NextResponse.json([]); + } + + const suggestions = data.tips + .filter((t: { location?: string }) => t.location && t.location !== "") + .slice(0, 8) + .map((t: { id: string; name: string; district?: string; address?: string; location: string }) => { + const [lng, lat] = t.location.split(",").map(Number); + return { + id: t.id, + name: t.name, + district: t.district || "", + address: t.address || "", + lat, + lng, + }; + }); + + return NextResponse.json(suggestions); + } catch (e) { + console.error("Location suggest error:", e); + return NextResponse.json([]); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index dde34f4..39eb400 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,11 +1,20 @@ "use client"; -import { useState } from "react"; +import { useState, useRef, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; -import { motion } from "framer-motion"; -import { Plus, LogIn, Utensils, Loader2 } from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Plus, LogIn, Utensils, Loader2, MapPin, Navigation, X } from "lucide-react"; import { getUserId } from "@/lib/userId"; +interface LocationSuggestion { + id: string; + name: string; + district: string; + address: string; + lat: number; + lng: number; +} + const SHANGHAI_COORDS = { lat: 31.2222, lng: 121.4764 }; function getLocation(): Promise<{ lat: number; lng: number }> { @@ -35,6 +44,64 @@ export default function LandingPage() { const [loadingText, setLoadingText] = useState(""); const [error, setError] = useState(""); + const [locationQuery, setLocationQuery] = useState(""); + const [suggestions, setSuggestions] = useState([]); + const [showSuggestions, setShowSuggestions] = useState(false); + const [selectedLocation, setSelectedLocation] = useState(null); + const [fetchingSuggestions, setFetchingSuggestions] = useState(false); + const suggestRef = useRef(null); + const debounceRef = useRef>(null); + + const fetchSuggestions = useCallback(async (query: string) => { + if (query.length < 1) { + setSuggestions([]); + setShowSuggestions(false); + return; + } + setFetchingSuggestions(true); + try { + const res = await fetch(`/api/location/suggest?keywords=${encodeURIComponent(query)}`); + const data: LocationSuggestion[] = await res.json(); + setSuggestions(data); + setShowSuggestions(data.length > 0); + } catch { + setSuggestions([]); + } finally { + setFetchingSuggestions(false); + } + }, []); + + const handleLocationInput = (val: string) => { + setLocationQuery(val); + setSelectedLocation(null); + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => fetchSuggestions(val), 300); + }; + + const handleSelectLocation = (loc: LocationSuggestion) => { + setSelectedLocation(loc); + setLocationQuery(loc.name); + setShowSuggestions(false); + setSuggestions([]); + }; + + const clearLocation = () => { + setSelectedLocation(null); + setLocationQuery(""); + setSuggestions([]); + setShowSuggestions(false); + }; + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (suggestRef.current && !suggestRef.current.contains(e.target as Node)) { + setShowSuggestions(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + const joinRoom = async (roomId: string) => { const userId = getUserId(); const res = await fetch(`/api/room/${roomId}/join`, { @@ -49,12 +116,18 @@ export default function LandingPage() { const handleCreate = async () => { setLoading(true); setError(""); - setLoadingText("正在获取位置..."); try { - const coords = await getLocation(); + let coords: { lat: number; lng: number }; - setLoadingText("正在搜索周边美食..."); + if (selectedLocation) { + coords = { lat: selectedLocation.lat, lng: selectedLocation.lng }; + setLoadingText("正在搜索周边美食..."); + } else { + setLoadingText("正在获取位置..."); + coords = await getLocation(); + setLoadingText("正在搜索周边美食..."); + } const res = await fetch("/api/room/create", { method: "POST", @@ -127,6 +200,75 @@ export default function LandingPage() { animate={{ y: 0, opacity: 1 }} transition={{ duration: 0.5, delay: 0.15 }} > +
+
+ + handleLocationInput(e.target.value)} + onFocus={() => suggestions.length > 0 && setShowSuggestions(true)} + disabled={loading} + className="h-10 w-full rounded-xl border border-zinc-200 bg-white pl-9 pr-9 text-sm text-zinc-700 outline-none transition-colors placeholder:text-zinc-300 focus:border-emerald-400 focus:ring-2 focus:ring-emerald-100 disabled:opacity-50" + /> + {(selectedLocation || locationQuery) && !loading && ( + + )} + {fetchingSuggestions && ( + + )} +
+ + {selectedLocation && ( +
+ + + {selectedLocation.district} {selectedLocation.address || selectedLocation.name} + +
+ )} + + {!selectedLocation && !locationQuery && ( +
+ + 将使用当前定位 +
+ )} + + + {showSuggestions && ( + + {suggestions.map((s) => ( +
  • + +
  • + ))} +
    + )} +
    +
    +