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

676 lines
16 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 && currentBannerImage">
<cover-image :src="currentBannerImage" class="index-swiper-img" mode="aspectFill" @tap="handleBannerTap"></cover-image>
<!-- 轮播指示器 -->
<cover-view class="banner-indicators" v-if="props.bannerImages.length > 1">
<cover-view
v-for="(img, idx) in props.bannerImages"
:key="idx"
class="indicator-dot"
:class="{ active: idx === currentBannerIndex }">
</cover-view>
</cover-view>
</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>
<cover-view class="side-btn service" @tap="handleService">
<cover-image class="side-icon" src="/static/customer-service.png"></cover-image>
</cover-view>
<cover-view class="side-btn search" @tap="handleSearch">
<cover-image class="side-icon" src="/static/other_device.png"></cover-image>
</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 // 是否隐藏地图上的覆盖层元素(如中心定位图标、轮播图等)
},
bannerImages: {
type: Array,
default: () => [] // 广告图片列表
}
})
// Emits
const emit = defineEmits([
'relocate',
'scan',
'showList',
'markerTap',
'mapCenterChange',
'bannerClick'
])
// 响应式数据
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 currentBannerIndex = ref(0) // 当前显示的广告索引
let bannerTimer = null // 广告轮播定时器
// 计算当前显示的广告图片
const currentBannerImage = computed(() => {
if (props.bannerImages && props.bannerImages.length > 0) {
return props.bannerImages[currentBannerIndex.value]
}
// 降级:如果没有广告,显示默认图片
return '/static/index_swiper.png'
})
// 验证坐标有效性
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) {
return
}
const newCenter = {
longitude: Number(location.longitude),
latitude: Number(location.latitude)
}
// 直接更新地图中心,触发地图组件的视图更新
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) {
mapCenter.value = {
longitude: newLocation.longitude,
latitude: newLocation.latitude
}
updateMapMarkers()
}
}
}, {
immediate: true,
deep: true
})
// 监听位置列表变化
watch(() => props.filteredPositions, (newPositions) => {
updateMapMarkers()
}, {
deep: true
})
// 监听广告图片变化,启动或停止轮播
watch(() => props.bannerImages, (newImages, oldImages) => {
// 先停止旧的轮播
stopBannerRotation()
currentBannerIndex.value = 0
// 如果有多张图片,启动新的轮播
if (newImages && newImages.length > 1) {
// 使用 nextTick 确保 DOM 已更新
nextTick(() => {
startBannerRotation()
})
}
}, {
immediate: true,
deep: true
})
// 地图加载完成事件
const onMapUpdated = () => {
isLoading.value = false
}
// 地图区域变化事件(带防抖优化)
const onMapRegionChange = (e) => {
// 只处理结束事件
if (!e || e.type !== 'end') {
return
}
const causedBy = e.causedBy || e.detail?.causedBy
if (causedBy === 'gesture' || causedBy === 'scale' || causedBy === 'drag'||causedBy==='update') {
// 清除之前的定时器
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;
// 触发父组件查询新位置的场地
emit('mapCenterChange', newCenter)
}, 500)
} else {
// 兜底方案:如果事件中没有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
emit('mapCenterChange', newCenter)
}
},
fail: (err) => {
console.error('获取地图中心失败:', err)
}
})
}
}, 500)
}
}
}
// 标记点点击事件
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 handleBannerTap = () => {
// 触发父组件处理点击事件
emit('bannerClick', currentBannerIndex.value)
}
// 启动广告轮播
const startBannerRotation = () => {
// 如果只有一张或没有图片,不需要轮播
if (!props.bannerImages || props.bannerImages.length <= 1) {
return
}
// 清除旧的定时器
stopBannerRotation()
// 每3秒切换一次
bannerTimer = setInterval(() => {
const nextIndex = (currentBannerIndex.value + 1) % props.bannerImages.length
currentBannerIndex.value = nextIndex
}, 3000)
}
// 停止广告轮播
const stopBannerRotation = () => {
if (bannerTimer) {
clearInterval(bannerTimer)
bannerTimer = null
}
}
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()
}
// 初始化广告轮播
if (props.bannerImages && props.bannerImages.length > 1) {
startBannerRotation()
}
})
})
onUnmounted(() => {
// 清理工作
if (regionChangeTimer) {
clearTimeout(regionChangeTimer)
regionChangeTimer = null
}
// 停止广告轮播
stopBannerRotation()
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: 60rpx;
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;
left: 50%;
transform: translateX(-50%);
right: 0;
overflow: hidden;
&-img {
width: 100%;
height: 100%;
border-radius: 20rpx;
}
.banner-indicators {
position: absolute;
bottom: 10rpx;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8rpx;
z-index: 2;
.indicator-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.5);
transition: all 0.3s ease;
&.active {
width: 24rpx;
border-radius: 6rpx;
background-color: rgba(255, 255, 255, 0.9);
}
}
}
}
</style>