React (react-amap)高德地图使用(加标记、缩放、缩略图)
React 高德地图使用指南
本文档基于实际项目经验,详细介绍如何在 React 项目中集成和使用高德地图。
目录
- 环境准备
- 基础配置
- 地图组件实现
- 标记点功能
- 信息窗口
- 事件处理
- 样式定制
- 常见问题
环境准备
1. 安装依赖
npm install react-amap
# 或
yarn add react-amap
2. 获取高德地图 API Key
- 访问 高德开放平台
- 注册账号并创建应用
- 获取 Web 端 API Key
基础配置
1. 在 HTML 中引入高德地图 API
在 public/index.html
中添加:
<script src="https://webapi.amap.com/maps?v=1.4.15&key=YOUR_API_KEY"></script>
2. TypeScript 类型定义
interface MarkerData {ID: string;AS_NAME: string;AS_ADDRESS: string;AS_LONGITUDE_LATITUDE: string;AREA: string | number;ROOM_COUNT: number;IS_OPERATING?: number;[key: string]: any;
}interface FilterParams {company: string;department: string;assetType: string;
}
地图组件实现
1. 基础地图组件
import React, { useEffect, useState } from 'react';
import { Map, Marker, InfoWindow } from 'react-amap';const ReactAmapComponent: React.FC = () => {const [mapCenter, setMapCenter] = useState([116.397428, 39.90923]);const [zoom, setZoom] = useState(10);const [markers, setMarkers] = useState<MarkerData[]>([]);return (<div style={{ width: '100%', height: '600px' }}><Mapamapkey="YOUR_API_KEY"center={mapCenter}zoom={zoom}loading={<div>地图加载中...</div>}events={{created: (mapInstance) => {console.log('地图创建成功', mapInstance);},click: (e) => {console.log('地图点击', e.lnglat);}}}>{/* 标记点将在这里渲染 */}</Map></div>);
};export default ReactAmapComponent;
2. 地图配置选项
const mapOptions = {amapkey: "YOUR_API_KEY",center: [116.397428, 39.90923], // 地图中心点zoom: 10, // 缩放级别mapStyle: 'amap://styles/normal', // 地图样式features: ['bg', 'point', 'road', 'building'], // 地图要素viewMode: '3D', // 地图模式pitch: 0, // 地图俯仰角度rotation: 0, // 地图旋转角度animateEnable: true, // 地图平移过程中是否使用动画keyboardEnable: true, // 地图是否可通过键盘控制dragEnable: true, // 地图是否可通过鼠标拖拽平移zoomEnable: true, // 地图是否可缩放doubleClickZoom: true, // 地图是否可通过双击鼠标放大地图scrollWheel: true, // 地图是否可通过鼠标滚轮缩放浏览
};
标记点功能
1. 渲染标记点
const renderMarkers = () => {return markers.map((marker) => {const [lng, lat] = marker.AS_LONGITUDE_LATITUDE.split(',').map(Number);return (<Markerkey={marker.ID}position={[lng, lat]}title={marker.AS_NAME}events={{click: () => handleMarkerClick(marker)}}/>);});
};const handleMarkerClick = (marker: MarkerData) => {console.log('标记点击', marker);setActiveInfoWindow(marker.ID);
};
2. 自定义标记点图标
const getMarkerIcon = (isOperating: number) => {return {image: isOperating === 1 ? '/icons/marker-operating.png' : '/icons/marker-non-operating.png',size: [32, 32],imageSize: [32, 32]};
};<Markerposition={[lng, lat]}icon={getMarkerIcon(marker.IS_OPERATING)}events={{click: () => handleMarkerClick(marker)}}
/>
信息窗口
1. 基础信息窗口
const [activeInfoWindow, setActiveInfoWindow] = useState<string | null>(null);const renderInfoWindow = () => {if (!activeInfoWindow) return null;const activeMarker = markers.find(m => m.ID === activeInfoWindow);if (!activeMarker) return null;const [lng, lat] = activeMarker.AS_LONGITUDE_LATITUDE.split(',').map(Number);const position = [lng, lat];return (<InfoWindowposition={position}visible={true}closeWhenClickMap={false}showShadow={false}isCustom={true}events={{close: () => setActiveInfoWindow(null)}}><div style={{width: '380px',maxHeight: '480px',overflow: 'auto',padding: '0',margin: '0'}}><CardInfo assetData={activeMarker} onClose={() => setActiveInfoWindow(null)} /></div></InfoWindow>);
};
2. 自定义信息窗口组件
interface CardInfoProps {assetData: MarkerData;onClose?: () => void;
}const CardInfo: React.FC<CardInfoProps> = ({ assetData, onClose }) => {return (<div className="map-info-window"><div className="info-window-header"><div className="info-window-title">{assetData.AS_NAME || '资产详情'}</div><button className="info-window-close" type="button" aria-label="关闭"onClick={onClose}>×</button></div><div className="info-window-content"><div className="info-section"><div className="section-title">基础信息</div><div className="info-grid"><div className="info-row"><div className="info-label">资产编号:</div><div className="info-value">{assetData.AS_CODE || '-'}</div></div><div className="info-row"><div className="info-label">建筑面积:</div><div className="info-value">{assetData.AREA || '0'} m²</div></div></div></div></div><style jsx>{`.map-info-window {background: white;border-radius: 8px;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);overflow: hidden;}.info-window-header {background: #f8f9fa;padding: 15px;border-bottom: 1px solid #e9ecef;position: relative;}.info-window-title {font-size: 16px;font-weight: bold;color: #333;margin: 0;}.info-window-close {position: absolute;top: 18px;right: 15px;background: none;border: none;font-size: 18px;cursor: pointer;color: #999;}`}</style></div>);
};
事件处理
1. 地图事件
const mapEvents = {created: (mapInstance: any) => {console.log('地图实例创建', mapInstance);},click: (e: any) => {console.log('地图点击', e.lnglat);setActiveInfoWindow(null); // 点击地图关闭信息窗口},zoomchange: (e: any) => {console.log('缩放级别改变', e.zoom);},moveend: (e: any) => {console.log('地图移动结束', e.target.getCenter());}
};
2. 标记点事件
const markerEvents = {click: (marker: MarkerData) => {handleMarkerClick(marker);},mouseover: (e: any) => {console.log('鼠标悬停在标记上');},mouseout: (e: any) => {console.log('鼠标离开标记');}
};
样式定制
1. 信息窗口样式
.map-info-window {background: white;border-radius: 8px;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);overflow: hidden;font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}.info-window-header {background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);padding: 15px;color: white;position: relative;
}.info-window-title {font-size: 16px;font-weight: bold;margin: 0;
}.info-window-close {position: absolute;top: 18px;right: 15px;background: none;border: none;font-size: 18px;cursor: pointer;color: rgba(255, 255, 255, 0.8);transition: color 0.3s;
}.info-window-close:hover {color: white;
}.info-window-content {padding: 18px 15px;max-height: 450px;overflow-y: auto;
}.info-section {margin-bottom: 20px;
}.section-title {font-size: 15px;font-weight: bold;color: #1890ff;margin-bottom: 15px;
}.info-grid {display: grid;grid-template-columns: 1fr 1fr;gap: 10px;
}.info-row {display: flex;margin-bottom: 10px;font-size: 14px;
}.info-label {color: #666;width: 85px;flex-shrink: 0;
}.info-value {color: #333;flex-grow: 1;
}
2. 地图容器样式
.map-container {width: 100%;height: 600px;border-radius: 8px;overflow: hidden;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}.map-loading {display: flex;align-items: center;justify-content: center;height: 100%;background: #f5f5f5;color: #666;
}
数据处理
1. 获取地图数据
const fetchMapData = async (filters: FilterParams) => {try {setLoading(true);const params = {tableName: 'AS_ASSET_INFO',condition: buildCondition(filters),pageSize: 1000};const response = await getDataByCondition(params);if (response.success && response.data) {const validMarkers = response.data.filter((item: any) => item.AS_LONGITUDE_LATITUDE && item.AS_LONGITUDE_LATITUDE.includes(','));setMarkers(validMarkers);updateMapCenter(validMarkers);}} catch (error) {console.error('获取地图数据失败:', error);} finally {setLoading(false);}
};
2. 构建查询条件
const buildCondition = (filters: FilterParams) => {const conditions = [];if (filters.company) {conditions.push(`COM_ID = '${filters.company}'`);}if (filters.department) {conditions.push(`AS_CORE_ORG_ID = '${filters.department}'`);}if (filters.assetType) {conditions.push(`ASSET_TYPE = '${filters.assetType}'`);}return conditions.length > 0 ? conditions.join(' AND ') : '';
};
3. 更新地图中心点
const updateMapCenter = (markers: MarkerData[]) => {if (markers.length === 0) return;const validCoords = markers.filter(marker => marker.AS_LONGITUDE_LATITUDE).map(marker => {const [lng, lat] = marker.AS_LONGITUDE_LATITUDE.split(',').map(Number);return [lng, lat];});if (validCoords.length > 0) {const avgLng = validCoords.reduce((sum, coord) => sum + coord[0], 0) / validCoords.length;const avgLat = validCoords.reduce((sum, coord) => sum + coord[1], 0) / validCoords.length;setMapCenter([avgLng, avgLat]);}
};
性能优化
1. 标记点聚合
import { MarkerClusterer } from 'react-amap';const renderClusteredMarkers = () => {return (<MarkerClusterergridSize={60}maxZoom={18}averageCenter={true}styles={[{url: '/icons/cluster-small.png',size: [40, 40],textColor: '#fff',textSize: 12},{url: '/icons/cluster-medium.png',size: [50, 50],textColor: '#fff',textSize: 14},{url: '/icons/cluster-large.png',size: [60, 60],textColor: '#fff',textSize: 16}]}>{renderMarkers()}</MarkerClusterer>);
};
2. 懒加载和虚拟化
const [visibleMarkers, setVisibleMarkers] = useState<MarkerData[]>([]);const updateVisibleMarkers = useCallback((bounds: any) => {const visible = markers.filter(marker => {const [lng, lat] = marker.AS_LONGITUDE_LATITUDE.split(',').map(Number);return bounds.contains([lng, lat]);});setVisibleMarkers(visible);
}, [markers]);const mapEvents = {moveend: (e: any) => {const bounds = e.target.getBounds();updateVisibleMarkers(bounds);},zoomend: (e: any) => {const bounds = e.target.getBounds();updateVisibleMarkers(bounds);}
};
常见问题
1. 地图不显示
问题: 地图容器为空或显示空白
解决方案:
- 检查 API Key 是否正确
- 确保地图容器有明确的宽高
- 检查网络连接和控制台错误
// 确保容器有明确尺寸
<div style={{ width: '100%', height: '600px' }}><Map amapkey="YOUR_API_KEY" />
</div>
2. 标记点不显示
问题: 标记点无法在地图上显示
解决方案:
- 检查坐标格式是否正确
- 确保坐标在有效范围内
- 检查标记点数据是否正确传递
// 坐标验证
const isValidCoordinate = (lngLat: string) => {const [lng, lat] = lngLat.split(',').map(Number);return !isNaN(lng) && !isNaN(lat) && lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90;
};
3. 信息窗口样式问题
问题: 信息窗口样式不生效或显示异常
解决方案:
- 使用
isCustom={true}
启用自定义样式 - 确保 CSS 样式正确加载
- 检查 z-index 层级问题
<InfoWindowisCustom={true}closeWhenClickMap={false}showShadow={false}
>{/* 自定义内容 */}
</InfoWindow>
4. React 18 兼容性
问题: React 18 中出现 ReactDOM.render
警告
解决方案:
- 避免使用
ReactDOMServer.renderToString
- 直接在 InfoWindow 中渲染 React 组件
- 使用最新版本的 react-amap
5. 性能优化建议
- 使用标记点聚合减少渲染数量
- 实现视口内标记点的懒加载
- 避免在 render 方法中创建新对象
- 使用 React.memo 优化组件重渲染
const MarkerComponent = React.memo(({ marker, onClick }) => {const [lng, lat] = marker.AS_LONGITUDE_LATITUDE.split(',').map(Number);return (<Markerposition={[lng, lat]}events={{ click: () => onClick(marker) }}/>);
});
总结
本文档涵盖了在 React 项目中使用高德地图的主要场景和最佳实践。通过合理的组件设计、事件处理和性能优化,可以构建出功能丰富、用户体验良好的地图应用。
在实际开发中,建议根据具体需求选择合适的功能模块,并注意处理边界情况和错误状态,确保应用的稳定性和可用性。