uniapp中renderjs + Leaflet.js实现离线地图
示例代码template view classcontainer view classmapWrapper !-- 地图容器 -- view classmap idmapContainer :propmapProps :change:propmapRender.updateMap map:readyonMapReady/view /view view classinfoPanel view classinfoItem text classlabel当前位置/text text classvalue{{ currentLocationText }}/text /view view classinfoItem text classlabel地图状态/text text classvalue{{ mapStatusText }}/text /view /view /view /template script modulemapRender langrenderjs export default { data() { return { leafletMap: null, // 地图实例 markers: [] // 标记数组 } }, mounted() { // 【步骤1】组件挂载后开始加载Leaflet库 console.log(开始加载Leaflet库) this.loadLeaflet() }, methods: { // 加载Leaflet库 loadLeaflet() { // 【步骤2】检查Leaflet库是否已加载 if (typeof window.L undefined) { // 【步骤3】Leaflet未加载使用在线CDN加载 console.log(使用在线CDN加载Leaflet) // 加载Leaflet脚本 const script document.createElement(script) script.src https://unpkg.com/leaflet1.9.4/dist/leaflet.js // CDN地址 script.async false // 同步加载确保依赖顺序 // 【步骤4】脚本加载成功回调 script.onload () { console.log(Leaflet CDN脚本加载成功) // 【步骤5】加载Leaflet样式文件 const link document.createElement(link) link.rel stylesheet link.href https://unpkg.com/leaflet1.9.4/dist/leaflet.css // 样式CDN地址 // 【步骤6】样式加载成功回调 link.onload () { console.log(Leaflet CDN样式加载成功) // 【步骤7】样式加载完成后初始化地图 this.initMap() } // 【步骤8】样式加载失败回调 link.onerror () { console.error(Leaflet样式加载失败) // 即使样式加载失败也尝试初始化地图 this.initMap() } document.head.appendChild(link) } // 【步骤9】脚本加载失败回调 script.onerror () { console.error(Leaflet CDN加载失败) // 显示地图占位符 this.showMapPlaceholder() } document.head.appendChild(script) } else { // 【步骤10】Leaflet已加载直接初始化地图 console.log(Leaflet已加载) this.initMap() } }, // 显示地图占位符 showMapPlaceholder() { // 【步骤11】地图加载失败时显示占位符 console.log(显示地图占位符) const mapContainer document.getElementById(mapContainer) if (mapContainer) { mapContainer.innerHTML div styledisplay: flex; justify-content: center; align-items: center; height: 100%; color: #999; 地图加载失败 /div } // 【步骤12】触发地图就绪事件以便应用继续运行 console.log(触发map:ready事件) this.$emit(map:ready) }, // 初始化地图 initMap() { // 【步骤13】再次检查Leaflet是否加载 if (typeof window.L undefined) { console.error(Leaflet未加载) this.showMapPlaceholder() return } try { // 【步骤14】获取地图容器 const mapContainer document.getElementById(mapContainer) if (!mapContainer) { console.error(未找到地图容器) return } // 【步骤15】确保地图容器有尺寸 console.log(Map container height before:, mapContainer.offsetHeight) if (mapContainer.offsetHeight 0) { mapContainer.style.height 400px console.log(Set map container height to 400px) } console.log(Map container height after:, mapContainer.offsetHeight) // 【步骤16】创建地图实例 console.log(开始创建地图实例) this.leafletMap window.L.map(mapContainer, { center: [30.238886, 120.145369], // 默认中心点杭州 zoom: 13 // 默认缩放级别 }) console.log(地图实例创建成功) // 【步骤17】添加OpenStreetMap图层 console.log(开始添加地图图层) window.L.tileLayer(https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png, { attribution: copy; OpenStreetMap contributors }).addTo(this.leafletMap) console.log(地图图层添加成功) // 【步骤18】触发地图就绪事件通知主模块 console.log(触发map:ready事件) this.$emit(map:ready) // 【步骤19】调用主模块的onMapReady方法 console.log(调用主模块onMapReady方法) this.$ownerInstance.callMethod(onMapReady) // 【步骤20】检查是否有标记点数据 console.log(Map initialized, checking for markers data) if (this.prop this.prop.markers) { console.log(Found markers data in prop, updating markers:, this.prop.markers) this.updateMarkers(this.prop.markers) } } catch (error) { // 【步骤21】创建地图失败捕获错误并显示占位符 console.error(创建地图失败:, error) this.showMapPlaceholder() } }, // 更新地图 updateMap(newValue, oldValue, ownerInstance, instance) { // 【步骤22】如果地图实例未初始化先初始化 if (!this.leafletMap) { console.log(地图实例未初始化先初始化地图) this.initMap() return } const { latitude, longitude, markers } newValue // 【步骤23】更新地图中心点 if (latitude longitude) { console.log(更新地图中心点:, [latitude, longitude]) this.leafletMap.setView([latitude, longitude], 13) } // 【步骤24】更新标记 console.log(更新地图标记) this.updateMarkers(markers) }, // 更新标记 updateMarkers(markers) { console.log(updateMarkers called with markers:, markers) // 【步骤25】检查地图实例和Leaflet是否可用 if (!this.leafletMap || typeof window.L undefined) { console.log(Leaflet map not initialized, skipping marker update) return } // 【步骤26】清除现有标记 console.log(清除现有标记) this.leafletMap.eachLayer(layer { if (layer instanceof window.L.Marker) { this.leafletMap.removeLayer(layer) } }) // 【步骤27】添加新标记 if (markers markers.length 0) { console.log(Adding, markers.length, markers to map) markers.forEach(marker { console.log(Adding marker:, marker) // 创建自定义图标 const icon window.L.divIcon({ className: custom-marker, html: img src${marker.iconPath} stylewidth: 28px; /, iconSize: [28, 39] }) // 添加标记到地图 window.L.marker([marker.latitude, marker.longitude], { title: marker.title, icon: icon }).addTo(this.leafletMap) .bindPopup(marker.title) }) } else { console.log(No markers to add) } } } } /script script export default { data() { return { latitude: 30.238886, longitude: 120.145369, markers: [], currentLocationText: 获取中..., mapStatusText: 初始化中..., currentPlatform: } }, computed: { // 传递给renderjs的地图属性 mapProps() { return { latitude: this.latitude, longitude: this.longitude, markers: this.markers } } }, onLoad() { this.checkPlatform() this.getCurrentLocation() }, methods: { // 检查平台 checkPlatform() { const platform uni.getSystemInfoSync().platform if (platform ios || platform android) { this.currentPlatform H5 } else if (typeof window ! undefined typeof navigator ! undefined) { this.currentPlatform H5 } else { this.currentPlatform 其他 } console.log(当前平台:, this.currentPlatform) }, // 地图就绪事件 onMapReady() { this.mapStatusText 地图初始化完成 // 添加测试点位 this.addTestMarker() }, // 获取当前位置 getCurrentLocation() { let that this that.$getLocation().then(res { that.latitude res.latitude that.longitude res.longitude that.currentLocationText ${res.latitude.toFixed(4)}, ${res.longitude.toFixed(4)} // 添加当前位置标记 that.addMyLocationMarker(res.latitude, res.longitude) }).catch(err { console.error(获取位置失败:, err) that.currentLocationText 获取位置失败 }) }, // 添加我的位置标记 addMyLocationMarker(latitude, longitude) { const marker { id: Date.now(), latitude, longitude, title: 我的位置, iconPath: /static/mypos.png } // 创建新数组以触发响应式更新 this.markers [...this.markers, marker] }, // 添加测试点位 addTestMarker() { // 在当前位置附近创建测试点位 const testLat this.latitude 0.005 const testLng this.longitude 0.005 const marker { id: Date.now() 1, latitude: testLat, longitude: testLng, title: 测试点位, iconPath: /static/charge.png } // 创建新数组以触发响应式更新 this.markers [...this.markers, marker] } } } /script style langscss scoped .container { background: #f5f5f5; display: flex; flex-direction: column; min-height: 100vh; .header { background: #fff; padding: 20rpx 32rpx; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); .title { font-size: 32rpx; font-weight: 600; color: #333; } } .mapWrapper { margin: 16rpx; border-radius: 16rpx; background: #fff; overflow: hidden; height: 600rpx; .map { width: 100%; height: 600rpx; } } .infoPanel { background: #fff; padding: 20rpx 32rpx; box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.05); .infoItem { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f0f0f0; :last-child { border-bottom: none; } .label { font-size: 24rpx; color: #666; } .value { font-size: 24rpx; color: #333; font-weight: 500; } } } } /style支持POI搜索template view classcontainer view classmapWrapper !-- 地图容器 -- view classmap idmapContainer :propmapProps :change:propmapRender.updateMap map:readyonMapReady/view /view view classsearchPanel view classsearchInputWrapper input typetext v-modelsearchKeyword placeholder搜索位置 classsearchInput / button clicksearchLocation classsearchButton搜索/button /view view v-ifsearchResults.length 0 classsearchResults view v-for(result, index) in searchResults :keyindex clickselectSearchResult(result) classsearchResultItem text{{ result.name }}/text text classsearchResultAddress{{ result.address }}/text /view /view /view view classinfoPanel view classinfoItem text classlabel当前位置/text text classvalue{{ currentLocationText }}/text /view view classinfoItem text classlabel地图状态/text text classvalue{{ mapStatusText }}/text /view /view /view /template script modulemapRender langrenderjs export default { data() { return { leafletMap: null, // 地图实例 markers: [] // 标记数组 } }, mounted() { // 【步骤1】组件挂载后开始加载Leaflet库 console.log(开始加载Leaflet库) this.loadLeaflet() }, methods: { // 加载Leaflet库 loadLeaflet() { // 【步骤2】检查Leaflet库是否已加载 if (typeof window.L undefined) { // 【步骤3】Leaflet未加载使用在线CDN加载 console.log(使用在线CDN加载Leaflet) // 加载Leaflet脚本 const script document.createElement(script) script.src https://web.fuja.me/libs/leaflet/leaflet.js // CDN地址 script.async false // 同步加载确保依赖顺序 // 【步骤4】脚本加载成功回调 script.onload () { console.log(Leaflet CDN脚本加载成功) // 【步骤5】加载Leaflet样式文件 const link document.createElement(link) link.rel stylesheet link.href https://web.fuja.me/libs/leaflet/leaflet.css // 样式CDN地址 // 【步骤6】样式加载成功回调 link.onload () { console.log(Leaflet CDN样式加载成功) // 【步骤7】样式加载完成后初始化地图 this.initMap() } // 【步骤8】样式加载失败回调 link.onerror () { console.error(Leaflet样式加载失败) // 即使样式加载失败也尝试初始化地图 this.initMap() } document.head.appendChild(link) } // 【步骤9】脚本加载失败回调 script.onerror () { console.error(Leaflet CDN加载失败) // 显示地图占位符 this.showMapPlaceholder() } document.head.appendChild(script) } else { // 【步骤10】Leaflet已加载直接初始化地图 console.log(Leaflet已加载) this.initMap() } }, // 显示地图占位符 showMapPlaceholder() { // 【步骤11】地图加载失败时显示占位符 console.log(显示地图占位符) const mapContainer document.getElementById(mapContainer) if (mapContainer) { mapContainer.innerHTML div styledisplay: flex; justify-content: center; align-items: center; height: 100%; color: #999; 地图加载失败 /div } // 【步骤12】触发地图就绪事件以便应用继续运行 console.log(触发map:ready事件) this.$emit(map:ready) }, // 初始化地图 initMap() { // 【步骤13】再次检查Leaflet是否加载 if (typeof window.L undefined) { console.error(Leaflet未加载) this.showMapPlaceholder() return } try { // 【步骤14】获取地图容器 const mapContainer document.getElementById(mapContainer) if (!mapContainer) { console.error(未找到地图容器) return } // 【步骤15】确保地图容器有尺寸 console.log(Map container height before:, mapContainer.offsetHeight) if (mapContainer.offsetHeight 0) { mapContainer.style.height 400px console.log(Set map container height to 400px) } console.log(Map container height after:, mapContainer.offsetHeight) // 【步骤16】创建地图实例 console.log(开始创建地图实例) this.leafletMap window.L.map(mapContainer, { center: [30.238886, 120.145369], // 默认中心点杭州 zoom: 13 // 默认缩放级别 }) console.log(地图实例创建成功) // 【步骤17】添加OpenStreetMap图层 console.log(开始添加地图图层) window.L.tileLayer(https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png, { attribution: copy; OpenStreetMap contributors }).addTo(this.leafletMap) console.log(地图图层添加成功) // 【步骤18】触发地图就绪事件通知主模块 console.log(触发map:ready事件) this.$emit(map:ready) // 【步骤19】调用主模块的onMapReady方法 console.log(调用主模块onMapReady方法) this.$ownerInstance.callMethod(onMapReady) // 【步骤20】检查是否有标记点数据 console.log(Map initialized, checking for markers data) if (this.prop this.prop.markers) { console.log(Found markers data in prop, updating markers:, this.prop.markers) this.updateMarkers(this.prop.markers) } } catch (error) { // 【步骤21】创建地图失败捕获错误并显示占位符 console.error(创建地图失败:, error) this.showMapPlaceholder() } }, // 更新地图 updateMap(newValue, oldValue, ownerInstance, instance) { // 【步骤22】如果地图实例未初始化先初始化 if (!this.leafletMap) { console.log(地图实例未初始化先初始化地图) this.initMap() return } const { latitude, longitude, markers } newValue // 【步骤23】更新地图中心点 if (latitude longitude) { console.log(更新地图中心点:, [latitude, longitude]) this.leafletMap.setView([latitude, longitude], 13) } // 【步骤24】更新标记 console.log(更新地图标记) this.updateMarkers(markers) }, // 更新标记 updateMarkers(markers) { console.log(updateMarkers called with markers:, markers) // 【步骤25】检查地图实例和Leaflet是否可用 if (!this.leafletMap || typeof window.L undefined) { console.log(Leaflet map not initialized, skipping marker update) return } // 【步骤26】清除现有标记 console.log(清除现有标记) this.leafletMap.eachLayer(layer { if (layer instanceof window.L.Marker) { this.leafletMap.removeLayer(layer) } }) // 【步骤27】添加新标记 if (markers markers.length 0) { console.log(Adding, markers.length, markers to map) markers.forEach(marker { console.log(Adding marker:, marker) // 创建自定义图标 const icon window.L.divIcon({ className: custom-marker, html: img src${marker.iconPath} stylewidth: 28px; /, iconSize: [28, 39] }) // 添加标记到地图 window.L.marker([marker.latitude, marker.longitude], { title: marker.title, icon: icon }).addTo(this.leafletMap) .bindPopup(marker.title) }) } else { console.log(No markers to add) } } } } /script script export default { data() { return { latitude: 30.238886, longitude: 120.145369, markers: [], currentLocationText: 获取中..., mapStatusText: 初始化中..., currentPlatform: , searchKeyword: , searchResults: [] } }, computed: { // 传递给renderjs的地图属性 mapProps() { return { latitude: this.latitude, longitude: this.longitude, markers: this.markers } } }, methods: { // 检查平台 checkPlatform() { const platform uni.getSystemInfoSync().platform if (platform ios || platform android) { this.currentPlatform H5 } else if (typeof window ! undefined typeof navigator ! undefined) { this.currentPlatform H5 } else { this.currentPlatform 其他 } console.log(当前平台:, this.currentPlatform) }, // 地图就绪事件 onMapReady() { this.mapStatusText 地图初始化完成 // 添加测试点位 this.addTestMarker() }, // 获取当前位置 getCurrentLocation() { let that this that.$getLocation().then(res { that.latitude res.latitude that.longitude res.longitude that.currentLocationText ${res.latitude.toFixed(4)}, ${res.longitude.toFixed(4)} // 添加当前位置标记 that.addMyLocationMarker(res.latitude, res.longitude) }).catch(err { console.error(获取位置失败:, err) that.currentLocationText 获取位置失败 }) }, // 添加我的位置标记 addMyLocationMarker(latitude, longitude) { const marker { id: Date.now(), latitude, longitude, title: 我的位置, iconPath: https://api.vyvox.io/static/mypos.png } // 创建新数组以触发响应式更新 this.markers [...this.markers, marker] }, // 添加测试点位 addTestMarker() { // 在当前位置附近创建测试点位 const testLat this.latitude 0.005 const testLng this.longitude 0.005 const marker { id: Date.now() 1, latitude: testLat, longitude: testLng, title: 测试点位, iconPath: https://api.vyvox.io/static/charge.svg } // 创建新数组以触发响应式更新 this.markers [...this.markers, marker] }, // 搜索位置使用 OpenStreetMap Nominatim API searchLocation() { if (!this.searchKeyword) { uni.showToast({ title: 请输入搜索关键词, icon: none }) return } // 使用 OpenStreetMap Nominatim API 进行地理编码 const url https://nominatim.openstreetmap.org/search?formatjsonq${encodeURIComponent(this.searchKeyword)}limit5 uni.request({ url: url, method: GET, header: { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 }, success: (res) { if (res.data res.data.length 0) { this.searchResults res.data.map(item { return { name: item.display_name, address: item.display_name, latitude: parseFloat(item.lat), longitude: parseFloat(item.lon) } }) } else { uni.showToast({ title: 未找到相关位置, icon: none }) this.searchResults [] } }, fail: (err) { console.error(搜索位置失败:, err) uni.showToast({ title: 搜索失败请重试, icon: none }) } }) }, // 选择搜索结果 selectSearchResult(result) { // 更新地图位置 this.latitude result.latitude this.longitude result.longitude this.currentLocationText ${result.latitude.toFixed(4)}, ${result.longitude.toFixed(4)} // 清除搜索结果 this.searchResults [] // 添加选中位置的标记 this.markers [] const marker { id: Date.now(), latitude: result.latitude, longitude: result.longitude, title: result.name, iconPath: https://api.vyvox.io/static/charge.svg } this.markers [marker] } } } /script style langscss scoped .container { background: #f5f5f5; display: flex; flex-direction: column; min-height: 100vh; .header { background: #fff; padding: 20rpx 32rpx; } .mapWrapper { margin: 16rpx; border-radius: 16rpx; background: #fff; overflow: hidden; height: 600rpx; .map { width: 100%; height: 600rpx; } } .searchPanel { margin: 0 16rpx 16rpx; border-radius: 16rpx; background: #fff; overflow: hidden; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); .searchInputWrapper { display: flex; padding: 16rpx; .searchInput { flex: 1; padding: 16rpx; border: 1rpx solid #e0e0e0; border-radius: 8rpx; font-size: 28rpx; margin-right: 12rpx; } .searchButton { padding: 0 32rpx; background: #007aff; color: #fff; border: none; border-radius: 8rpx; font-size: 28rpx; display: flex; align-items: center; justify-content: center; } } .searchResults { max-height: 400rpx; overflow-y: auto; .searchResultItem { padding: 16rpx; border-top: 1rpx solid #f0f0f0; cursor: pointer; :hover { background: #f5f5f5; } .searchResultAddress { display: block; font-size: 24rpx; color: #999; margin-top: 8rpx; } } } } .infoPanel { background: #fff; padding: 20rpx 32rpx; box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.05); .infoItem { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f0f0f0; :last-child { border-bottom: none; } .label { font-size: 24rpx; color: #666; } .value { font-size: 24rpx; color: #333; font-weight: 500; } } } } /styleH5 效果展示APP 效果展示地图使用的是 OpenStreet