vue开发的项目要做海外版本,但是国内百度、高德等地图咔咔收费,只能找国外版本地图,对比参考了Mapbox、Leaflet、echarts等多种实现,最终选择了Leaflet开发,简单记录一下。效果如下图:
划重点:Leaflet必须翻墙(代理)使用外网才能正常渲染使用
一、Leaflet简介
Leaflet是一个轻量级的Web地图库,它支持多种平台和浏览器,并具有广泛的功能、良好的性能以及易用性。Leaflet的主要特点包括:
- 轻量级:Leaflet的文件大小较小,加载速度快,特别适合在移动设备和低带宽环境下使用。
- 易用性:Leaflet提供了简单直观的API,使开发者能够快速上手并创建交互式地图。
- 可定制性:Leaflet支持自定义图层、标记样式和交互行为,开发者可以根据自己的需求进行定制。
- 跨平台兼容性:Leaflet可以在各种现代浏览器和移动设备上运行,并且与多种前端框架(如React、Vue等)兼容。
二、Leaflet简单教程
Leaflet 是一个开源的 JavaScript 库,用于在网页上创建交互式地图。以下是一个基本的 Leaflet 教程,帮助你开始使用 Leaflet 创建一个简单的地图:
1. 引入 Leaflet 库
首先,在 HTML 文件中引入 Leaflet 库的 CSS 和 JavaScript 文件。从 Leaflet 的官方网站下载最新的库文件,或者使用 CDN 链接。Leaflet官网地址
<!DOCTYPE html>
<html>
<head>
<title>Leaflet 入门教程</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"></script>
</head>
<body>
<!-- 地图容器 -->
<div id="map" style="width: 100%; height: 400px;"></div>
<script>
// 在这里编写 JavaScript 代码来初始化地图
</script>
</body>
</html>
2. 初始化地图
在 JavaScript 代码中,使用 Leaflet 的 API 来初始化地图,并设置地图的中心位置、缩放级别和其他选项。
<script>
// 初始化地图
var map = L.map('map').setView([51.505, -0.09], 13); // 设置地图的中心位置和缩放级别
// 添加瓦片图层
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
}).addTo(map);
</script>
L.map('map')
创建一个新的地图实例,并将其绑定到 ID 为 "map" 的 HTML 元素上。setView
方法设置地图的中心位置和缩放级别。使用 L.tileLayer
创建一个瓦片图层,并将其添加到地图上。此处使用了 OpenStreetMap 的瓦片图层作为示例。
3. 添加标记和弹窗
使用 Leaflet 添加标记(Marker)和弹窗(Popup)来显示地图上的特定位置的信息。
<script>
// ...之前的代码...
// 创建一个标记并添加到地图上
var marker = L.marker([51.5, -0.09]).addTo(map)
.bindPopup("<b>Hello world!</b><br />I am a popup.").openPopup(); // 绑定一个弹窗并打开它
</script>
三、实际使用(以单页面使用为例)
准备工作(安装依赖):
npm install leaflet //地图主服务
npm install leaflet-geosearch //地图搜索服务
npm install leaflet.markercluster//点位聚合
1、页面引入:
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import "leaflet.markercluster";
import "leaflet.markercluster/dist/MarkerCluster.css";
import "leaflet.markercluster/dist/MarkerCluster.Default.css";
2、页面定义元素(本身为空的块状元素,如果高度为0则无法查看)
<div id="map" style="height:500px"></div>
3、实例,直接上代码:
initMap() {
// 地图的实例必须在页面加载完成后 在进行否则会报错
this.$nextTick(() => {
this.map=null
if(this.data){
// 判断是否有数据传入用于默认选择, setView视图设置 map实例化
if(this.data.GisLat&&this.data.GisLat!=''&&this.data.GisLon&&this.data.GisLon!=''){
this.map = L.map("map").setView([this.data.GisLat , this.data.GisLon], 13);
}else{
this.map = L.map("map").setView([36.18, 120.41], 13);
}
}else{
this.map = L.map("map").setView([36.18, 120.41], 13);
}
//获取地图碎片,并添加到map实例上
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
// 最大缩放
maxZoom: 19,
}).addTo(this.map);
//自定义标识图标
var myIcon = L.divIcon({ className: "my-div-icon" });
//聚合使用因为暂时没用所以只是
var markers = L.markerClusterGroup();
let num = 120.413;
for (var i = 0; i < 10000; i++) {
let a = [36.18, num];
var marker = L.marker(new L.LatLng(a[0], a[1]), {
icon: myIcon,
});
marker.bindPopup("标记的位置是: 提示内容");
markers.addLayer(marker);
num += 0.001;
}
this.map.addLayer(markers);
//地图标点
var currentMarker;
if(this.data&&this.map){
if(this.data.GisLat&&this.data.GisLat!=''&&this.data.GisLon&&this.data.GisLon!=''){
Marker 标识 bindPopup 弹层 openPopup 打开弹层
currentMarker= L.marker([this.data.GisLat, this.data.GisLon],{ icon:myIcon })
.addTo(this.map).bindPopup(this.data.Address).openPopup();
this.result.lng = this.data.GisLon
this.result.lat = this.data.GisLat
this.result.address = this.data.Address
}
}
var that = this;
// 地图点击事件,时间内部this的指向会发生变化 所以不能用this
this.map.on("click", function (e) {
if (currentMarker) {
//删除标识
that.map.removeLayer(currentMarker);
}
currentMarker=L.marker([e.latlng.lat, e.latlng.lng], { icon: myIcon }).addTo(that.map);
// 获取点击的经纬度
var latlng = e.latlng;
// 显示经纬度
// console.log("经度: " + latlng.lng + ", 纬度: " + latlng.lat);
that.result.lng = e.latlng.lng
that.result.lat = e.latlng.lat
// 使用Nominatim逆地理编码服务获取地址名称
var url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng.lat}&lon=${latlng.lng}`;
// 发送请求
fetch(url)
.then((response) => response.json())
.then((data) => {
// 输出完整的地址信息
that.result.address =data.display_name
if (currentMarker) {
· //删除标识
that.map.removeLayer(currentMarker);
}
currentMarker=L.marker([e.latlng.lat, e.latlng.lng], { icon: myIcon })
.addTo(that.map).bindPopup(data.display_name).openPopup();
})
.catch((err) => {
console.error(err);
// alert("无法获取地址信息");
});
});
});
},
4、搜索服务引入:
import { OpenStreetMapProvider } from "leaflet-geosearch";
5、页面定义搜索元素:
<div class="search-container">
<input type="text" id="search-input" placeholder="Search for a location" v-model="searchTerm" @input="search" />
<ul v-if="searchResults.length" class="search-results">
<li v-for="(result,index) in searchResults" :key="index" @click="flyTo(result)">{{ result.label }}</li>
</ul>
</div>
6、将搜索内容返回英文
// 切换为英文显示搜索内容
provider: new OpenStreetMapProvider({
params: {
"accept-language": "en", // 设置返回结果的语言为英文
},
}),
7、事件
// 页面搜索事件
async search() {
if (this.searchTerm.length > 2) {
const results = await this.provider.search({
query: this.searchTerm,
});
this.searchResults = results;
} else {
this.searchResults = [];
}
},
// 点击赋值
flyTo(result) {
this.map.flyTo([result.y, result.x], 8);
this.searchResults = [];
this.searchTerm = "";
},
8、封装成组件完整代码:
<template>
<el-dialog :title="title"class="dialogYK" :close-on-click-modal="false" :visible.sync="visible"
:before-close="cancelDialog" :append-to-body="addToBody">
<div class="map-container">
<div id="map"></div>
<div class="search-container">
<input type="text" id="search-input" placeholder="Search for a location" v-model="searchTerm" @input="search" />
<ul v-if="searchResults.length" class="search-results">
<li v-for="(result,index) in searchResults" :key="index" @click="flyTo(result)">{{ result.label }}</li>
</ul>
</div>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="cancelDialog">Cancel</el-button>
<el-button type="primary" @click="confirmDialog">Confirm</el-button>
</div>
</el-dialog>
</template>
<script>
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import { OpenStreetMapProvider } from "leaflet-geosearch";
import "leaflet.markercluster";
import "leaflet.markercluster/dist/MarkerCluster.css";
import "leaflet.markercluster/dist/MarkerCluster.Default.css";
export default {
props: {
// 对话框标题
title: {
type: String,
default: "Dialog Box",
},
// 初始化位置信息
data: {
type: Object,
default: () => {
return {};
},
},
addToBody: {
type: Boolean,
default: true,
},
visible: {
type: Boolean,
default: false,
},
},
data() {
return {
result:{
lng:"",
lat:"",
address:""
},
map: null,
searchTerm: "",
searchResults: [],
// 切换为英文显示搜索内容
provider: new OpenStreetMapProvider({
params: {
"accept-language": "en", // 设置返回结果的语言为英文
},
}),
};
},
mounted() {
this.initMap();
},
methods: {
initMap() {
this.$nextTick(() => {
this.map=null
if(this.data){
// 判断是否有数据传入用于默认选择
if(this.data.GisLat&&this.data.GisLat!=''&&this.data.GisLon&&this.data.GisLon!=''){
this.map = L.map("map").setView([this.data.GisLat , this.data.GisLon], 13);
}else{
this.map = L.map("map").setView([36.18, 120.41], 13);
}
}else{
this.map = L.map("map").setView([36.18, 120.41], 13);
}
//获取地图碎片
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
}).addTo(this.map);
var myIcon = L.divIcon({ className: "my-div-icon" });
//聚合使用
var markers = L.markerClusterGroup();
let num = 120.413;
for (var i = 0; i < 10000; i++) {
let a = [36.18, num];
var marker = L.marker(new L.LatLng(a[0], a[1]), {
icon: myIcon,
});
marker.bindPopup("标记的位置是: 提示内容");
markers.addLayer(marker);
num += 0.001;
}
this.map.addLayer(markers);
//地图标点
var currentMarker;
if(this.data&&this.map){
if(this.data.GisLat&&this.data.GisLat!=''&&this.data.GisLon&&this.data.GisLon!=''){
currentMarker= L.marker([this.data.GisLat, this.data.GisLon], { icon:myIcon })
.addTo(this.map).bindPopup(this.data.Address).openPopup();
this.result.lng = this.data.GisLon
this.result.lat = this.data.GisLat
this.result.address = this.data.Address
}
}
var that = this;
// 地图点击事件
this.map.on("click", function (e) {
if (currentMarker) {
that.map.removeLayer(currentMarker);
}
currentMarker=L.marker([e.latlng.lat, e.latlng.lng], { icon: myIcon }).addTo(that.map);
// 获取点击的经纬度
var latlng = e.latlng;
// 显示经纬度
// console.log("经度: " + latlng.lng + ", 纬度: " + latlng.lat);
that.result.lng = e.latlng.lng
that.result.lat = e.latlng.lat
// 使用Nominatim逆地理编码服务获取地址名称
var url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng.lat}&lon=${latlng.lng}`;
// 发送请求
fetch(url)
.then((response) => response.json())
.then((data) => {
console.log(data,123321);
// 输出完整的地址信息
// console.log("地址: " + data.display_name);
that.result.address =data.display_name
if (currentMarker) {
that.map.removeLayer(currentMarker);
}
currentMarker=L.marker([e.latlng.lat, e.latlng.lng], { icon: myIcon })
.addTo(that.map).bindPopup(data.display_name).openPopup();
})
.catch((err) => {
console.error(err);
// alert("无法获取地址信息");
});
});
});
},
confirmDialog() {
if (this.result.address === '') {
this.$message.warning('Please Click the Map to Select Address')
return
}
this.$emit('success', this.result)
this.cancelDialog()
},
// 关闭
cancelDialog() {
this.$emit("close");
},
// 页面搜索事件
async search() {
if (this.searchTerm.length > 2) {
const results = await this.provider.search({
query: this.searchTerm,
});
this.searchResults = results;
} else {
this.searchResults = [];
}
},
// 点击赋值
flyTo(result) {
this.map.flyTo([result.y, result.x], 8);
this.searchResults = [];
this.searchTerm = "";
},
},
};
</script>
<style lang="scss" scoped>
.map-container {
position: relative;
}
#map {
height: 500px;
}
.search-container {
position: absolute;
top: 10px;
left: 60px;
right: 10px;
z-index: 1000;
}
#search-input {
width: 100%;
padding: 10px;
box-sizing: border-box;
font-size: 16px;
border: 2px solid #ddd;
border-radius: 5px;
}
.search-results {
list-style: none;
margin: 0;
padding: 0;
background: white;
border: 1px solid #ddd;
border-top: none;
border-radius: 0 0 5px 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
max-height: 300px;
overflow-y: auto;
}
.search-results li {
padding: 10px;
border-top: 1px solid #ddd;
cursor: pointer;
}
.search-results li:hover {
background-color: #f0f0f0;
}
::v-deep .my-div-icon {
background-image: url("./marker-icon.png");
width: 20px !important;
height: 20px !important;
}
</style>