Files
uni-fans-score/components/MapComponent.vue
T
2025-11-08 16:00:44 +08:00

589 lines
14 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" :class="{ 'full-width': props.fullWidth }" :style="{ '--map-height': props.customHeight || '78vh' }">
<!-- 地图容器 -->
<view class="map-wrapper">
<!-- 使用小程序原生地图组件 -->
<map id="map" class="native-map" :longitude="mapCenter.longitude" :latitude="mapCenter.latitude"
:markers="mapMarkers" :scale="mapZoom" :show-location="false" @regionchange="onMapRegionChange"
@markertap="onMapMarkerTap" @callouttap="onCalloutTap" @updated="onMapUpdated" @error="onMapError">
<!-- 覆盖在地图上的可点击控件使用 cover-view 以兼容小程序原生组件层级 -->
<cover-view class="index-swiper" v-if="!props.hideControls && !props.hideMapOverlays" @tap="handleJoinTap">
<cover-image src="/static/index_swiper.png" class="index-swiper-img" mode="aspectFit"></cover-image>
</cover-view>
<!-- 地图中心固定定位图标 -->
<cover-view class="center-location-marker" v-if="!props.hideMapOverlays">
<cover-image src="/static/location-icon.png" class="center-marker-icon"></cover-image>
</cover-view>
<cover-view class="map-side-controls" v-if="!props.hideControls && !props.hideMapOverlays">
<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 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/other_device.png"></cover-image>
<!-- <cover-view class="side-text">搜索</cover-view> -->
</cover-view>
</cover-view>
</map>
<!-- 地图加载状态 -->
<view class="map-loading" v-if="isLoading">
<view class="loading-content">
<view class="loading-spinner"></view>
<text>{{ $t('common.loadingMap') }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
nextTick,
getCurrentInstance
} from 'vue'
// 导入地图工具函数
import {
calculateDistanceSync
} from '../utils/mapUtils.js'
// 导入国际化
import { useI18n } from '../utils/i18n.js'
// 获取 i18n 实例
const { t: $t } = useI18n()
// 引用折叠面板组件的ref
const collapseRef = ref(null)
// 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
},
customHeight: {
type: String,
default: '' // 自定义高度,如 '48vh', '400rpx' 等
},
hideControls: {
type: Boolean,
default: false // 是否隐藏侧边控制按钮
},
fullWidth: {
type: Boolean,
default: false // 是否全宽显示(去掉 margin 和固定宽度)
},
hideMapOverlays: {
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 isValidCoordinate = (lat, lng) => {
const latitude = parseFloat(lat)
const longitude = parseFloat(lng)
return !isNaN(latitude) && !isNaN(longitude) &&
latitude >= -90 && latitude <= 90 &&
longitude >= -180 && longitude <= 180 &&
!(latitude === 0 && longitude === 0) // 排除 0,0 这种无效坐标
}
// 防抖定时器
let regionChangeTimer = null
// 方法
const updateMapMarkers = () => {
const markers = []
// 只添加周边场地位置点,中心定位图标改用固定的cover-view显示
if (props.enableMarkers && props.filteredPositions && props.filteredPositions.length > 0) {
props.filteredPositions.forEach((pos, index) => {
if (pos.longitude && pos.latitude && isValidCoordinate(pos.latitude, pos.longitude)) {
const lat = parseFloat(pos.latitude)
const lng = parseFloat(pos.longitude)
markers.push({
id: index + 1,
latitude: lat,
longitude: lng,
iconPath: '/static/markes_fdz.png',
width: 40,
height: 40,
callout: {
content: pos.name,
fontSize: 12,
borderRadius: 8,
bgColor: '#ffffff',
padding: 8,
display: 'BYCLICK'
}
})
}
})
}
mapMarkers.value = markers
isLoading.value = false
}
// 移动地图到指定位置(直接更新地图中心坐标)
const moveToLocation = (location) => {
if (!location || !location.longitude || !location.latitude) {
console.warn('moveToLocation: 无效的位置参数', location)
return
}
const newCenter = {
longitude: Number(location.longitude),
latitude: Number(location.latitude)
}
console.log('移动地图到:', newCenter)
// 直接更新地图中心,触发地图组件的视图更新
mapCenter.value = newCenter
// 更新标记点
updateMapMarkers()
}
// 监听用户位置变化
watch(() => props.userLocation, (newLocation, oldLocation) => {
if (newLocation && newLocation.longitude && newLocation.latitude) {
// 检查位置是否真的变化了(避免重复更新)
const isChanged = !oldLocation ||
oldLocation.longitude !== newLocation.longitude ||
oldLocation.latitude !== newLocation.latitude
if (isChanged) {
console.log('用户位置变化:', newLocation)
mapCenter.value = {
longitude: newLocation.longitude,
latitude: newLocation.latitude
}
updateMapMarkers()
}
}
}, {
immediate: true,
deep: true
})
// 监听位置列表变化
watch(() => props.filteredPositions, (newPositions) => {
updateMapMarkers()
}, {
deep: true
})
// 地图加载完成事件
const onMapUpdated = () => {
isLoading.value = false
}
// 地图区域变化事件(带防抖优化)
const onMapRegionChange = (e) => {
console.log('regionchange事件:', e)
// 只处理结束事件
if (!e || e.type !== 'end') {
return
}
// 获取事件原因
// 微信小程序:e.causedBy 可能的值:
// - 'gesture': 手势拖动
// - 'scale': 缩放
// - 'update': 调用更新接口(如 setCenterOffset 等)
const causedBy = e.causedBy || e.detail?.causedBy
console.log('地图变化原因:', causedBy, '完整事件:', e)
// 只在用户手动操作(拖动或缩放)结束时触发查询
// 排除程序调用(update)导致的变化,避免定位按钮触发多余查询
if (causedBy === 'gesture' || causedBy === 'scale' || causedBy === 'drag') {
// 清除之前的定时器
if (regionChangeTimer) {
clearTimeout(regionChangeTimer)
}
// 直接从事件对象中获取最新的中心点位置
const centerLocation = e.detail?.centerLocation || e.centerLocation
if (centerLocation && centerLocation.longitude && centerLocation.latitude) {
// 防抖:500ms后执行查询
regionChangeTimer = setTimeout(() => {
const newCenter = {
longitude: Number(centerLocation.longitude),
latitude: Number(centerLocation.latitude)
}
mapCenter.value = newCenter
console.log('地图中心变化,触发查询:', newCenter, '原因:', causedBy)
// 触发父组件查询新位置的场地
emit('mapCenterChange', newCenter)
}, 500)
} else {
console.warn('未能从事件中获取中心点位置,尝试使用API获取')
// 兜底方案:如果事件中没有centerLocation,才使用API获取
regionChangeTimer = setTimeout(() => {
if (mapContext.value) {
mapContext.value.getCenterLocation({
success: (res) => {
if (res && res.longitude && res.latitude) {
const newCenter = {
longitude: res.longitude,
latitude: res.latitude
}
mapCenter.value = newCenter
console.log('地图中心变化(API获取):', newCenter, '原因:', causedBy)
emit('mapCenterChange', newCenter)
}
},
fail: (err) => {
console.error('获取地图中心失败:', err)
}
})
}
}, 500)
}
} else {
console.log('跳过查询,原因:', causedBy)
}
}
// 标记点点击事件
const onMapMarkerTap = (e) => {
const markerId = e.detail?.markerId || e.markerId
// 查找对应的场地位置信息
if (props.filteredPositions && props.filteredPositions.length > 0) {
const position = props.filteredPositions[markerId - 1]
if (position) {
emit('markerTap', 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 handleJoinTap = () => {
uni.navigateTo({
url: '/pages/join/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(() => {
// 清理工作
if (regionChangeTimer) {
clearTimeout(regionChangeTimer)
regionChangeTimer = null
}
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: calc(100% - 20rpx); /* 减少高度,避免覆盖底部按钮 */
margin: 20rpx;
margin-bottom: 0; /* 底部不需要边距 */
border-radius: 20rpx;
overflow: hidden;
&.full-width {
width: 100%;
margin: 0;
border-radius: 0;
}
.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;
}
}
}
/* 地图中心定位图标(固定在屏幕中心) */
.center-location-marker {
position: absolute;
left: 50%;
top: 50%;
z-index: 1;
width: 60rpx;
height: 80rpx;
margin-left: -30rpx;
margin-top: -80rpx;
pointer-events: none;
}
.center-marker-icon {
width: 60rpx;
height: 80rpx;
display: block;
}
.map-side-controls {
position: absolute;
right: 20rpx;
bottom: 160rpx; /* 向上移动,避免被底部按钮遮挡 */
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 {
margin-top: 20rpx;
width: 90vw;
height: 120rpx;
border-radius: 20rpx;
z-index: 1;
position: absolute;
// top: 10rpx;
left: 50%;
transform: translateX(-50%);
right: 0;
&-img {
width: 100%;
height: 100%;
border-radius: 20rpx;
}
}
</style>