React+Google Maps API:动态追踪多途经点地址及显示方案
动态追踪多个途经点并关联Google Maps API显示解决方案
核心问题修正
现有代码存在几个关键问题导致无法正确追踪多个途经点:
- 单个
stopRef无法同时绑定多个输入框,必须为每个途经点分配独立的引用 waypointInputList的状态字段不统一(初始用waypointInput,添加时用waypoint),导致状态更新混乱- 未利用Autocomplete的
onPlaceChanged事件获取有效地址,直接取输入框值可能得到未验证的内容 - 计算路线时仅处理单个途经点,未批量遍历所有输入
关键实现步骤
- 用ref数组管理多个Autocomplete实例:创建一个ref数组,每个元素对应一个途经点的Autocomplete组件,方便获取用户选中的地点详情
- 同步维护途经点状态数组:用
useState存储每个途经点的输入值,确保和界面输入同步 - 绑定onPlaceChanged事件:每个Autocomplete触发选中事件时,更新对应索引的状态值,保证地址是Google Maps验证过的有效地点
- 批量生成途经点参数:计算路线时,过滤空值的途经点,转换成Google Maps API要求的格式
修改后的完整代码
import { React, useRef, useState, Fragment } from "react"; import styles from "./RouteForm.module.css"; import KeyboardDoubleArrowDownRoundedIcon from "@mui/icons-material/KeyboardDoubleArrowDownRounded"; import CloseRoundedIcon from "@mui/icons-material/CloseRounded"; import PlaceIcon from "@mui/icons-material/Place"; import AddCircleIcon from "@mui/icons-material/AddCircle"; import RemoveCircleIcon from "@mui/icons-material/RemoveCircle"; import { GoogleMap, useJsApiLoader, Autocomplete, DirectionsRenderer, } from "@react-google-maps/api"; const libraries = ["places"]; const RouteForm = (props) => { const originRef = useRef(null); const destinationRef = useRef(null); // 创建ref数组存储每个途经点的Autocomplete实例 const waypointAutocompleteRefs = useRef([]); const [stopContainerVisible, setStopContainerVisible] = useState(false); const [directionsRes, setDirectionsRes] = useState(null); const [routeFormVisible, setRouteFormVisible] = useState(false); // 统一状态字段,每个项存储输入值 const [waypointInputList, setWaypointInputList] = useState([ { value: "" }, ]); const handleRouteSubmit = (e) => { e.preventDefault(); calculateRoute(); }; const toggleFormVisibility = () => { setRouteFormVisible(!routeFormVisible); }; const { isLoaded } = useJsApiLoader({ id: "google-map-script", googleMapsApiKey: "AIzaSyBjizW_mGoQQI6jma6lTEWpaSsoYhKa6y8", libraries: libraries, }); const toggleStopVisibility = () => { setStopContainerVisible((prevValue) => !prevValue); }; const center = { lat: 42.4414, lng: 19.2624 }; if (!isLoaded) { return ( <div> <p>Loading...</p> </div> ); } const handleWaypointAdd = () => { // 添加时保持字段统一 setWaypointInputList([...waypointInputList, { value: "" }]); }; const handleWaypointRemove = (index) => { const list = [...waypointInputList]; list.splice(index, 1); setWaypointInputList(list); // 同步移除ref数组中的对应项 waypointAutocompleteRefs.current.splice(index, 1); }; // 处理Autocomplete选中事件 const handlePlaceChanged = (index) => { const autocomplete = waypointAutocompleteRefs.current[index]; if (!autocomplete) return; const place = autocomplete.getPlace(); if (place.formatted_address) { const updatedList = [...waypointInputList]; updatedList[index].value = place.formatted_address; setWaypointInputList(updatedList); } }; async function calculateRoute() { const originValue = originRef.current?.value; const destinationValue = destinationRef.current?.value; if (!originValue || !destinationValue) { return; } const directionService = new google.maps.DirectionsService(); // 生成途经点数组,过滤空值 const waypoints = waypointInputList .filter(item => item.value.trim() !== "") .map(item => ({ location: item.value, stopover: true, })); try { const results = await directionService.route({ origin: originValue, destination: destinationValue, travelMode: google.maps.TravelMode.DRIVING, waypoints: waypoints, }); setDirectionsRes(results); } catch (error) { console.error("路线计算失败:", error); } } return ( <Fragment> <div className={styles["form-and-map-container"]}> <GoogleMap center={center} zoom={15} mapContainerStyle={{ width: "100%", height: "100%", }} options={{ zoomControl: false, streetViewControl: false, mapTypeControl: false, fullscreenControl: false, }} > {directionsRes && <DirectionsRenderer directions={directionsRes} />} </GoogleMap> <div className={styles.container}> <div className={styles.expand} onClick={toggleFormVisibility}> {routeFormVisible ? ( <CloseRoundedIcon fontSize="inherit" className={styles["expand-icon"]} /> ) : ( <KeyboardDoubleArrowDownRoundedIcon fontSize="inherit" className={styles["expand-icon"]} /> )} <span>Create Route</span> </div> <div className={styles["form-container"]}> {routeFormVisible && ( <form onSubmit={handleRouteSubmit}> <div className={styles.inputs}> <div className={styles["input1-container"]}> <label> <PlaceIcon /> </label> <Autocomplete className={styles.input}> <input type="text" placeholder="Origin" ref={originRef} required /> </Autocomplete> </div> <div className={styles["input2-container"]}> <label> <PlaceIcon sx={{ color: "#beff0a" }} /> </label> <Autocomplete className={styles.input}> <input type="text" placeholder="Destination" ref={destinationRef} required /> </Autocomplete> </div> </div> <div className={styles.btns}> <button type="submit"> Create Route </button> <span className={styles["add-stop"]} onClick={toggleStopVisibility} > Add Stop </span> </div> {stopContainerVisible && ( <ul className={styles["stop-container"]}> {waypointInputList.map((singleWaypoint, index) => ( <li key={index}> {index < waypointInputList.length - 1 && ( <button className={styles["remove-stop-btn"]} onClick={() => handleWaypointRemove(index)} type="button" > <RemoveCircleIcon /> </button> )} <Autocomplete className={styles.input} ref={(el) => waypointAutocompleteRefs.current[index] = el} onPlaceChanged={() => handlePlaceChanged(index)} > <input type="text" placeholder="Stopping place" value={singleWaypoint.value} onChange={(e) => { // 手动输入时同步更新状态 const updatedList = [...waypointInputList]; updatedList[index].value = e.target.value; setWaypointInputList(updatedList); }} /> </Autocomplete> {waypointInputList.length - 1 === index && waypointInputList.length <= 4 && ( <button type="button" className={styles["add-stop-btn"]} onClick={handleWaypointAdd} > <AddCircleIcon /> </button> )} </li> ))} </ul> )} </form> )} </div> </div> </div> </Fragment> ); }; export default RouteForm;
代码说明
- 用
waypointAutocompleteRefs数组存储每个途经点的Autocomplete实例,通过onPlaceChanged事件获取用户选中的标准地址 - 同时支持手动输入和Autocomplete选择,手动输入时通过
onChange同步更新状态 - 计算路线时过滤空的途经点,避免无效参数传递给API
- 修正了添加/删除按钮的
type属性,防止触发表单默认提交行为
内容的提问来源于stack exchange,提问作者Orwellian
相关产品推荐
相关产品推荐

