Files
uni-fans-score/components/MapComponent.vue
T

524 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="map-container">
<!-- 地图容器 -->
<view class="map-wrapper">
<!-- 使用小程序原生地图组件 -->
<map id="map" class="native-map" :longitude="mapCenter.longitude" :latitude="mapCenter.latitude"
:markers="mapMarkers" :scale="mapZoom" :show-location="true" @regionchange="onMapRegionChange"
@markertap="onMapMarkerTap" @callouttap="onCalloutTap" @updated="onMapUpdated" @error="onMapError">
<!-- 覆盖在地图上的可点击控件使用 cover-view 以兼容小程序原生组件层级 -->
<cover-view class="index-swiper">
<image src="/static/index_swiper.png" class="index-swiper" mode="aspectFit"></image>
</cover-view>
<cover-view class="map-side-controls">
<cover-view class="side-btn service" @tap="handleService">
<cover-image class="side-icon" src="/static/customer-service.png"></cover-image>
<!-- <cover-view class="side-text">客服</cover-view> -->
</cover-view>
<cover-view class="side-btn search" @tap="handleSearch">
<cover-image class="side-icon" src="/static/search-icon.png"></cover-image>
<!-- <cover-view class="side-text">搜索</cover-view> -->
</cover-view>
<cover-view class="side-btn locate" @tap="handleRelocate">
<cover-image class="side-icon" src="/static/location.png"></cover-image>
<!-- <cover-view class="side-text">定位</cover-view> -->
</cover-view>
</cover-view>
<!-- 使用原生 marker 方案渲染中心指示 regionchange 同步到地图中心 -->
</map>
<!-- 地图加载状态 -->
<view class="map-loading" v-if="isLoading">
<view class="loading-content">
<view class="loading-spinner"></view>
<text>地图加载中...</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
nextTick,
getCurrentInstance
} from 'vue'
// 导入地图工具函数
import {
calculateDistanceSync
} from '../utils/mapUtils.js'
// 引用折叠面板组件的ref
const collapseRef = ref(null)
// 使用指南步骤
const guideSteps = ref([{
title: '扫码使用',
desc: '找到附近设备,扫描设备上的二维码'
},
{
title: '免押金支付',
desc: '无需支付押金,使用支付分免押即可完成租借'
},
{
title: '开始使用',
desc: '设备自动解锁,风扇弹出后取出即可开始使用'
},
{
title: '归还设备',
desc: '使用完毕后,按照设备规格要求将风扇还入即可结束订单'
}
])
// Props
const props = defineProps({
userLocation: {
type: Object,
default: null
},
positionList: {
type: Array,
default: () => []
},
filteredPositions: {
type: Array,
default: () => []
},
searchKeyword: {
type: String,
default: ''
},
noticeText: {
type: String,
default: ''
},
enableMarkers: {
type: Boolean,
default: false
}
})
// Emits
const emit = defineEmits([
'relocate',
'scan',
'showList',
'markerTap',
'mapCenterChange'
])
// 响应式数据
const isLoading = ref(true)
const mapCenter = ref({
longitude: 116.397128,
latitude: 39.916527
})
const mapZoom = ref(17)
const mapMarkers = ref([]) // 用于地图组件的markers
const mapContext = ref(null) // 地图上下文
// 方法
const updateMapMarkers = () => {
const markers = []
// 中心 marker(始终存在,使用传入中心坐标或 userLocation
const centerLng = Number(mapCenter.value.longitude || (props.userLocation && props.userLocation.longitude))
const centerLat = Number(mapCenter.value.latitude || (props.userLocation && props.userLocation.latitude))
if (!isNaN(centerLng) && !isNaN(centerLat)) {
markers.push({
id: 999999, // 固定 id 作为中心点
latitude: centerLat,
longitude: centerLng,
iconPath: '/static/location-icon.png',
width: 30,
height: 40,
anchor: {
x: 0.5,
y: 1
} // 图钉尖端对准坐标
})
}
// 可选:周边位置点
if (props.enableMarkers && props.filteredPositions && props.filteredPositions.length > 0) {
props.filteredPositions.forEach((pos, index) => {
if (pos.longitude && pos.latitude) {
const lat = parseFloat(pos.latitude)
const lng = parseFloat(pos.longitude)
if (lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
markers.push({
id: index + 1,
latitude: lat,
longitude: lng,
width: 24,
height: 24,
title: pos.name
})
}
}
})
}
mapMarkers.value = markers
isLoading.value = false
}
// 移动地图到指定位置(使用 includePoints 以避免 mapid 错误,并兼容不同基础库)
const moveToLocation = (location) => {
if (!location || !location.longitude || !location.latitude) return
if (!mapContext.value) return
try {
mapContext.value.includePoints({
points: [{
longitude: Number(location.longitude),
latitude: Number(location.latitude)
}],
padding: [60, 60, 60, 60],
success: () => {
console.log('地图已移动到指定位置(includePoints)')
},
fail: (err) => {
console.warn('includePoints 失败,尝试 moveToLocation():', err)
// 回退尝试(不传参,部分基础库仅支持移动到用户当前位置)
try {
mapContext.value.moveToLocation()
} catch (e) {
console.error('moveToLocation 回退失败:', e)
}
}
})
} catch (e) {
console.error('移动地图异常:', e)
}
}
// 监听用户位置变化
watch(() => props.userLocation, (newLocation) => {
if (newLocation && newLocation.longitude && newLocation.latitude) {
mapCenter.value = {
longitude: newLocation.longitude,
latitude: newLocation.latitude
}
updateMapMarkers()
moveToLocation(newLocation)
}
}, {
immediate: true,
deep: true
})
// 监听位置列表变化
watch(() => props.filteredPositions, (newPositions) => {
updateMapMarkers()
}, {
deep: true
})
// 地图加载完成事件
const onMapUpdated = () => {
isLoading.value = false
}
// 地图区域变化事件
const onMapRegionChange = (e) => {
// 在手势或缩放结束时更新中心坐标
if (e && e.type === 'end') {
if (mapContext.value) {
mapContext.value.getCenterLocation({
success: (res) => {
if (res && res.longitude && res.latitude) {
mapCenter.value = {
longitude: res.longitude,
latitude: res.latitude
}
// 更新中心 marker 位置
updateMapMarkers()
emit('mapCenterChange', mapCenter.value)
}
}
})
}
}
}
// 标记点点击事件
const onMapMarkerTap = (e) => {
const markerId = e.markerId
const marker = mapMarkers.value.find(item => item.id === markerId)
if (marker) {
if (markerId === 0) { // 用户位置标记
uni.showToast({
title: '这是您的位置',
icon: 'none'
})
return
}
if (marker.position) {
emit('markerTap', marker.position)
}
}
}
// 标记点气泡点击事件
const onCalloutTap = (e) => {
const markerId = e.markerId
const marker = mapMarkers.value.find(item => item.id === markerId)
if (marker && marker.position) {
emit('markerTap', marker.position)
}
}
// 地图错误事件
const onMapError = (error) => {
console.error('地图加载失败:', error)
isLoading.value = false
}
const handleRelocate = () => {
// 直接委托父级处理定位并移动地图,避免内部重复弹 loading
try {
emit('relocate')
} catch (e) {}
}
const handleSearch = () => {
try {
uni.navigateTo({ url: '/pages/search/index' })
} catch (e) {}
}
const handleService = () => {
uni.navigateTo({
url: '/pages/help/index'
})
}
const handleScan = () => {
emit('scan')
}
const handleShowList = () => {
emit('showList')
}
// 生命周期钩子
onMounted(() => {
// 初始化地图上下文
nextTick(() => {
// 需要使用nextTick确保地图组件已经渲染
const inst = getCurrentInstance()
const vm = (inst && (inst.proxy || inst)) || undefined
try {
mapContext.value = uni.createMapContext('map', vm)
} catch (e) {
// 兼容:如果第二参不被支持,退回单参
mapContext.value = uni.createMapContext('map')
}
updateMapMarkers()
// 初始化折叠面板
if (collapseRef.value) {
collapseRef.value.init()
}
})
})
onUnmounted(() => {
// 清理工作
mapContext.value = null
})
// 折叠面板事件处理
const onCollapseChange = (names) => {}
const onCollapseOpen = (names) => {}
const onCollapseClose = (names) => {}
// 暴露给父组件的方法
defineExpose({
mapCenter: computed(() => mapCenter.value),
moveToLocation,
updateMapMarkers,
initCollapse: () => {
if (collapseRef.value) {
collapseRef.value.init()
}
}
})
</script>
<style lang="scss" scoped>
/* 地图容器 */
.map-container {
flex: 1;
// position: fixed;
// top: 0;
left: 0;
right: 0;
bottom: 0;
width: 94vw;
height: 78vh;
margin: 20rpx;
border-radius: 20rpx;
overflow: hidden;
.map-wrapper {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
border-radius: 0;
.native-map {
width: 100%;
height: 100%;
display: block;
border-radius: 0;
}
}
.map-loading {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 8rpx solid #f3f3f3;
border-top: 8rpx solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20rpx;
}
text {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
}
}
/* 地图中心图钉(cover-view,避免使用 transform 以兼容小程序) */
.center-pin {
position: absolute;
left: 50%;
top: 50%;
z-index: 16;
width: 0;
height: 0;
}
.center-pin .pin-img {
position: absolute;
width: 48rpx;
height: 64rpx;
/* 使图钉尖端对准中心点:X 向左偏半宽,Y 向上偏高度-阴影间距 */
margin-left: -24rpx;
margin-top: -68rpx;
}
.center-pin .pin-shadow {
position: absolute;
width: 28rpx;
height: 8rpx;
border-radius: 10rpx;
background: rgba(0, 0, 0, 0.18);
margin-left: -14rpx;
margin-top: -8rpx;
}
.map-side-controls {
position: absolute;
right: 20rpx;
bottom: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
height: 300rpx;
margin: auto;
gap: 12rpx;
.side-btn {
// min-width: 160rpx;
margin: auto;
// height: 72rpx;
background: rgba(255, 255, 255, 0.96);
border-radius: 36rpx;
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.12);
padding: 20rpx;
border: 2rpx solid #e0e0e0;
&:active {
transform: scale(0.95);
}
text {
font-size: 26rpx;
color: #333;
white-space: nowrap;
font-weight: 500;
}
// &.search {
// border-color: #07c160;
// }
}
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.side-icon {
width: 44rpx;
height: 44rpx;
}
.index-swiper {
width: 92vw;
height: 180rpx;
border-radius: 20rpx;
z-index: 1000;
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
right: 0;
}
</style>