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.***/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 {
***pany: string;
department: string;
assetType: string;
}
地图组件实现
1. 基础地图组件
import React, { useEffect, useState } from 'react';
import { Map, Marker, InfoWindow } from 'react-amap';
const ReactAmap***ponent: 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' }}>
<Map
amapkey="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 ReactAmap***ponent;
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 (
<Marker
key={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]
};
};
<Marker
position={[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 (
<InfoWindow
position={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.su***ess && 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.***pany) {
conditions.push(`***_ID = '${filters.***pany}'`);
}
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 (
<MarkerClusterer
gridSize={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 层级问题
<InfoWindow
isCustom={true}
closeWhenClickMap={false}
showShadow={false}
>
{/* 自定义内容 */}
</InfoWindow>
4. React 18 兼容性
问题: React 18 中出现 ReactDOM.render 警告
解决方案:
- 避免使用
ReactDOMServer.renderToString - 直接在 InfoWindow 中渲染 React 组件
- 使用最新版本的 react-amap
5. 性能优化建议
- 使用标记点聚合减少渲染数量
- 实现视口内标记点的懒加载
- 避免在 render 方法中创建新对象
- 使用 React.memo 优化组件重渲染
const Marker***ponent = React.memo(({ marker, onClick }) => {
const [lng, lat] = marker.AS_LONGITUDE_LATITUDE.split(',').map(Number);
return (
<Marker
position={[lng, lat]}
events={{ click: () => onClick(marker) }}
/>
);
});
总结
本文档涵盖了在 React 项目中使用高德地图的主要场景和最佳实践。通过合理的组件设计、事件处理和性能优化,可以构建出功能丰富、用户体验良好的地图应用。
在实际开发中,建议根据具体需求选择合适的功能模块,并注意处理边界情况和错误状态,确保应用的稳定性和可用性。