589 lines
14 KiB
Vue
589 lines
14 KiB
Vue
<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> |