最近在写一个项目的时候,需要使用地图来选取点位信息
使用高德地图
在技术选型上面,我们使用Antd推荐的高德地图,并支持React的组件化
npm install --save react-amap
去高德地图申请key和密钥
添加key和安全密钥
给Map添加key
<Map amapkey={MAP_KEY} plugins={plugins} events={this.handleMapEvents} zoom={17} center={[lng, lat]} doubleClickZoom={false} >
添加安全密钥
我们在开发环境下直接使用script
标签添加安全密钥
<script type="text/javascript"> window._AMapSecurityConfig = { securityJsCode:'您申请的安全密钥', } </script>
在React脚手架中,我们可以在index.html
文件中添加script标签。但是我们使用的是Umi项目,没有index.html
,我们可以更改Umi默认模板,详见:修改默认模板
- 在pages文件夹下面添加
document.ejs
文件 - 添加密钥
创建地图组件
我们最后实现的效果可能会是这个样子
我们要实现的部分主要分成三个
- 搜索框的实现,包括搜索提示
- 地图展示部分,功能有根据根据经纬度进行定位,通过点击地图来逆编码来获得地址
- 搜索结果的基本信息展示
我们在设计组件的时候,可以从功能上进行拆分,并考虑到组件复用的问题,尽量让组件原子化,只干一件事情。还可以将组件分成容器组件(只管理数据)和渲染组件(只管理视图)进行分离,并尽量保持组件的无状态,无state
划分组件
根据上面的分析,我们可以将搜索框单独设置为一个组件,并且可以复用在其他需要搜索地址的地方。将地图的展示单独设置成为一个组件,只需要接受到经纬度就好可以展示相应的信息了。搜索结果也可以单独展示出来就好了。
同时,在搜索框输入地址的时候,我们可以改变地图组件的状态;点击地图中的点位的时候,也可以改变输入框中的值。所以我们可以创建一个容器组件来管理两者互通的数据,即经纬度和地址名称
示意图:
搜索组件编码
JavaScript API支持搜索服务脱离地图使用,即使用搜索服务不再需要先实例化地图。您可通AMap.plugin
方法,加载需要的服务。同时JavaScript API将原有的通过事件监听获得服务查询结果,修改为通过方法的回调函数获得服务查询结果。
由于我们每次改变输入框中的值,就会触发一次请求,所以我们可以在输入框中加一个防抖函数
import React, { Component } from 'react'; import { Select } from 'antd'; const { Option } = Select; class SearchAddress extends Component { constructor(props) { super(props); this.state = { positionList:[], } this.handleMapEvents = { created: () => { window.AMap.plugin('AMap.PlaceSearch', () => { new window.AMap.PlaceSearch({ pageSize: 10, pageIndex: 1, }); }) }, } } onSearch =(val)=>{ const place = new window.AMap.PlaceSearch({ pageSize: 10, pageIndex: 1, city: '绵阳', }) place.search(val,(status, result)=>{ if(status === 'complete' && result.info === 'OK'){ const {poiList:{pois}}=result if(pois && Array.isArray(pois)) { this.setState({ positionList:pois }) } } }) } debounce = (fn,time) => { let timerId = null; return function (val) { if(timerId) { clearTimeout(timerId); timerId = null; } timerId = setTimeout(() => { fn.call(this,val) },time) } } // 选中Seleect框中触发回调,改变父组件的position和addressName onChange = (id) => { const { positionList } = this.state; const {changeAddressName,changePosition} = this.props; for(const item of positionList) { const { name:itemName,id:itemId } = item; if(itemId === id) { const {location : {lng,lat}} = item; const position = {lng,lat}; changePosition(position) changeAddressName(itemName) } } } render() { const {addressName} = this.props const {positionList} = this.state; return ( <div> <Select value={addressName} style={{ width: 400 }} showSearch placeholder="请输入地址" onSearch={this.debounce((val) => this.onSearch(val),300)} onChange={this.onChange} optionFilterProp="children" > { positionList.map( item=> <Option key={item.id} value={item.id}>{item.name}</Option> ) } </Select> </div> ); } } export default SearchAddress;
地图组件编码实现
当我们点击地图时,只能通过click事件得到经纬度,而得不到地址名。经纬度对于用户是没有用的, 所以我们使用逆编码来获得地址名称
我们还可以使用static propTypes和defaultProps来规定props传入值的类型和默认值
static propTypes = { position:PropTypes.object, plugins: PropTypes.array, }; static defaultProps = { position :{ lng:104.679127, lat:31.467673, }, plugins: ["ToolBar", 'Scale'] };
地图组件:
import React, { Component, Fragment } from 'react'; import {Map, Marker} from 'react-amap'; import PropTypes from 'prop-types'; import { message } from 'antd'; import { MAP_KEY } from '@/utils/Enum'; class AdvancedMap extends Component { static propTypes = { position:PropTypes.object, plugins: PropTypes.array, }; static defaultProps = { position :{ lng:104.679127, lat:31.467673, }, plugins: ["ToolBar", 'Scale'] }; constructor(props) { super(props); this.handleMapEvents = { created: () => { window.AMap.plugin('AMap.PlaceSearch', () => { new window.AMap.PlaceSearch({ pageSize: 10, pageIndex: 1, }); }) window.AMap.plugin('AMap.Geocoder', () =>{ new window.AMap.Geocoder({ city: '0816' }) }) }, click: (e) => { const { lnglat:lnglatObj,lnglat: {lng,lat} } = e; const { changePosition,changeAddressName } = this.props; const lnglatArr = [lng,lat] const geocoder = new window.AMap.Geocoder({ city: '0816' }) changePosition(lnglatObj) geocoder.getAddress(lnglatArr, (status, result) => { if (status === 'complete' && result.info === 'OK') { console.log(result,'result'); const { regeocode :{formattedAddress}} = result; changeAddressName(formattedAddress) message.success(formattedAddress) } }) } } } render() { const {position: { lng,lat }, plugins} = this.props; return ( <Fragment> <Map amapkey={MAP_KEY} plugins={plugins} events={this.handleMapEvents} zoom={17} center={[lng, lat]} doubleClickZoom={false} > <Marker position={[lng, lat]} /> </Map> </Fragment> ); } } export default AdvancedMap;
父级容器
使用父级容器主要是管理数据(position,addressName),并传递改变数据(position,addressName)的方法给子组件,子组件调用父组件传递的函数,就可以更改父组件的数据(参考父子组件参数)
import React, { Component } from 'react'; import { Col, Input, Row } from 'antd'; import SearchAddress from '@/pages/Map/SearchAddress'; import AdvancedMap from '@/pages/Map/AdvancedMap'; class Index extends Component { constructor(props) { super(props); this.state = { position: { lng:104.679127, lat:31.467673, }, addressName:'' } } changePosition = (value) => { this.setState({ position:value }) } changeAddressName = (value) => { this.setState({ addressName:value }) } render() { const {position,addressName} = this.state; return ( <div style={{ width: '100%', height: '500px' }}> <Row gutter={24}> <Col span={8}> <SearchAddress changePosition={this.changePosition} changeAddressName={this.changeAddressName} addressName={addressName} /> </Col> </Row> <br /> <AdvancedMap position={position} changePosition={this.changePosition} changeAddressName={this.changeAddressName} /> </div> ); } } export default Index;
效果
后续还要再完善一下,比如规定好每个组件的输入输出,防止使用的时候出错
第二版
实现海量点标记,并点击每一个点位的时候可以看到每个点位的具体信息 功能有:
- 如果地图上已经该标记的点位了,展示相应信息
- 如果没有该标记的点位,显示对应点位的信息并可以新增点位
海量点标记
海量点的数据肯定需要传到地图组件里面的,positions表示海量点的一个数据信息
我们拿到positions值的信息,创建Markers即可
<AdvancedMap positions={positions} />
Marker信息展示
这个页面的数据就比较复杂了,首先要展示的Form表单的结构肯定不能写死,这样复用性就很差,我们应该是通过容器组件来传入表单的结构,我们自动生成表单
表单的结构设计可以看之前的一篇:关于我对表单设计的一点思考
const formFields = [ { "label": "首页图", "defaultValue": "", "field": "picture", "type": "string", "required": true, "pattern": null, "message":'Please upload picture', "disabled":true, }, { "label": "经度", "defaultValue": "192.168.0.1", "field": "longitude", "type": "string", "required": true, "pattern":null, "message":'Please input', "disabled":true, "hidden":true }, { "label": "纬度", "defaultValue": "21", "field": "latitude", "type": "string", "required": true, "pattern": null, "message":'Please input', "disabled":true, "hidden":true, }, { "label": "活动名称", "defaultValue": "", "field": "activity", "type": "string", "required": true, "pattern":null, "message":'Please input', "disabled":false, }, { "label": "活动地址", "defaultValue": "", "field": "address", "type": "string", "required": true, "pattern":null, "message":'Please input', "disabled":false, }, { "label": "部门负责人", "defaultValue": "", "field": "responsibility", "type": "string", "required": true, "pattern":null, "message":'Please input', "disabled":false, }, { "label": "联系方式", "defaultValue": "", "field": "phoneNumber", "type": "string", "required": true, "pattern":null, "message":'Please input', "disabled":false, }, { "label": "部门介绍", "defaultValue": "", "field": "introduce", "type": "string", "required": true, "pattern":null, "message":'Please input', "disabled":false, "style":{ "width":470, "height":110, } }, ]
然后我们可以根据这些字段,动态生成一个表单
buildFormItem = (formFields) => formFields.map((fields) => { const { form: { getFieldDecorator } } = this.props; const {field, label,required, type, pattern, message,disabled,style} = fields; const uploadButton = ( <div> <Icon type="plus" /> <div className="ant-upload-text">Upload</div> </div> ); if(field === FORM_TYPE["picture"]) { return ( <Form.Item label={label}> {getFieldDecorator(field, { rules: [{ required,message ,pattern,type}], })( <Upload className={style.upload} action="/campus/campusweb/upload/pictureUpload" accept={'image/*'} // 接受图片的格式类型 listType="picture-card" // fileList={} //用来存放上传图片的数组,放在状态里面 name="multipartFile" > {uploadButton} </Upload> )} </Form.Item> ) } return ( <Form.Item label={label}> {getFieldDecorator(field, { rules: [{ required,message ,pattern,type}], })( <Input disabled={disabled} style={{ ...style }} />, )} </Form.Item> ); })
其次要思考的是,点击Marker后显示Form表单,这个事件应该是组件内部处理,还是容器组件处理呢?
我们在点击每个Marker的时候,都需要去显示点位信息,并且我们已经将每个点位信息作为一个数组传进去了,我们在点击Marker的时候会自动获取到我们点击的Marker的信息,没有必要再容器组件中控制表单的显示,也具有了更好的内聚性
this.markersEvents = { created:() => { console.log('All Markers Instance Are Below'); }, click: (MapsOption, marker) => { // 获取信息 const data = marker.getExtData(); this.openModal(data) }, }
添加点位信息
我们是将点位信息做成一个positions数组传过去了,我们想要修改点位,只能通过修改positions数组来实现点位的新增
这时候我们就要在容器组件定义一个操作修改positions数组的方法,通过子组件调用父组件传递过来的函数,修改父组件中的值
那这个函数的调用时机是什么,当我们点击地图时,会展示相关信息,如果填写之后点击保存,我们才会新增点位。即Modal
中的onOK
事件
先判断有没有点位,再进行新增
Api
MarkerMap
参数 | 说明 | 类型 | 默认值 |
positions | 当有多个点位信息需要展示时,使用positions代表所有点位的一个数组,包含每一个点位的信息 | object[] | |
marker | 当只有一个点位信息时,使用marker | object | |
formFields | 点击marker时,所要展示的表单字段信息 | object[] | |
clickMap | 点击地图时触发该函数,可以获得点击位置的经纬度坐标和点击位置的地址 | Function(lnglatObj,address) | |
formVisible | 单击地图时是否显示相关表单信息 | Boolean | false |
getFormValue | 获取表单的信息 | Function(value) |
SearchAddressInput
参数 | 说明 | 类型 | 默认值 |
addressName | 搜索框中的默认值 | String | |
chooseOneItem | 选择Select框中的一项数据时触发的回调函数 | Function(lnglatObj,address) |
源码还在整理中,就不公开了哈