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

527 lines
11 KiB
Vue

<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"></map>
<!-- 地图加载状态 -->
<view class="map-loading" v-if="isLoading">
<view class="loading-content">
<view class="loading-spinner"></view>
<text>地图加载中...</text>
</view>
</view>
</view>
<!-- 地图控制按钮 -->
<view class="map-controls">
<view class="control-btn location-control" @tap="handleRelocate">
<!-- <image class="control-icon" src="/static/scan-icon.png" mode="aspectFit" /> -->
<uv-icon name="map-fill" size="18"></uv-icon>
<text style="margin-left: 8rpx;">我的位置</text>
</view>
<view class="control-btn scan-control main-btn" @tap="handleScan">
<image class="control-icon" src="/static/scan-icon.png" mode="aspectFit" />
<text>扫码使用</text>
</view>
<view class="control-btn list-control" @tap="handleShowList">
<image class="control-icon" src="/static/map.png" mode="aspectFit" />
<text>附近设备</text>
</view>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
watch,
onMounted,
onUnmounted,
nextTick
} 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: ''
}
})
// 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 = () => {
mapMarkers.value = []
// 添加用户位置标记
if (props.userLocation) {
mapMarkers.value.push({
id: 0, // ID必须是数字
// iconPath: '/static/scan-icon.png',
width: 32,
height: 32,
latitude: props.userLocation.latitude,
longitude: props.userLocation.longitude,
title: '我的位置',
callout: {
content: '我的位置',
color: '#ffffff',
fontSize: 12,
borderRadius: 4,
bgColor: '#2196F3',
padding: 6,
display: 'BYCLICK' // 点击时显示
},
customCallout: {
anchorX: 0,
anchorY: 0
}
})
}
// 添加位置点标记
if (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);
// 检查纬度是否在-90到90之间,经度是否在-180到180之间
if (lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
mapMarkers.value.push({
id: index + 1, // ID必须是数字,避免和用户位置的ID冲突
// iconPath: '/static/scan-icon.png',
width: 30,
height: 30,
latitude: lat,
longitude: lng,
title: pos.name,
position: pos, // 存储原始位置数据,用于事件处理
callout: {
content: pos.name,
color: '#333333',
fontSize: 12,
borderRadius: 4,
bgColor: '#ffffff',
padding: 6,
display: 'BYCLICK' // 点击时显示
}
})
} else {
console.warn(`忽略无效坐标: ${pos.name}, 纬度=${lat}, 经度=${lng}`);
}
}
})
}
isLoading.value = false
}
// 移动地图到指定位置
const moveToLocation = (location) => {
if (!location || !location.longitude || !location.latitude) return
if (mapContext.value) {
mapContext.value.moveToLocation({
longitude: location.longitude,
latitude: location.latitude,
success: () => {
console.log('地图已移动到指定位置')
},
fail: (error) => {
console.error('移动地图失败:', error)
}
})
}
}
// 监听用户位置变化
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) => {
// 当地图区域变化结束时,更新mapCenter
if (e.type === 'end' && e.causedBy === 'drag') {
// 获取地图中心点
if (mapContext.value) {
mapContext.value.getCenterLocation({
success: (res) => {
if (res.longitude && res.latitude) {
mapCenter.value = {
longitude: res.longitude,
latitude: res.latitude
}
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 = () => {
emit('relocate')
}
const handleScan = () => {
emit('scan')
}
const handleShowList = () => {
emit('showList')
}
// 生命周期钩子
onMounted(() => {
// 初始化地图上下文
nextTick(() => {
// 需要使用nextTick确保地图组件已经渲染
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: relative;
height: 60vh;
/* 增加高度 */
width: 92%;
/* 略微增加宽度 */
margin: 10rpx auto 30rpx;
/* 调整上下间距,左右自动居中 */
border-radius: 24rpx;
/* 添加圆角 */
overflow: hidden;
/* 确保圆角生效 */
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
/* 添加阴影效果 */
.map-wrapper {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
border-radius: 24rpx;
/* 内层也添加圆角 */
.native-map {
width: 100%;
height: 100%;
display: block;
border-radius: 24rpx;
/* 地图也添加圆角 */
}
}
.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;
}
}
}
.map-controls {
position: absolute;
right: 20rpx;
bottom: 20rpx;
left: 20rpx;
display: flex;
justify-content: center;
align-items: center;
gap: 20rpx;
.control-btn {
min-width: 120rpx;
/* 减小按钮宽度 */
height: 70rpx;
/* 减小按钮高度 */
background: #ffffff;
border-radius: 35rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
padding: 0 16rpx;
&:active {
transform: scale(0.95);
}
.control-icon {
width: 32rpx;
height: 32rpx;
margin-right: 12rpx;
}
text {
font-size: 26rpx;
color: #333;
white-space: nowrap;
font-weight: 500;
}
&.main-btn {
min-width: 140rpx;
/* 减小主按钮宽度 */
height: 80rpx;
/* 减小主按钮高度 */
box-shadow: 0 6rpx 20rpx rgba(33, 150, 243, 0.4);
transform: translateY(-5rpx);
.control-icon {
width: 36rpx;
height: 36rpx;
margin-right: 16rpx;
}
text {
font-size: 28rpx;
font-weight: 600;
}
&:active {
transform: translateY(-5rpx) scale(0.95);
}
}
}
.scan-control {
background: #2196F3;
.control-icon {
filter: brightness(0) invert(1);
}
text {
color: #ffffff;
}
}
.list-control {
background: #4CAF50;
.control-icon {
filter: brightness(0) invert(1);
}
text {
color: #ffffff;
}
}
.location-control {
background: #ffffff;
border: 2rpx solid #e0e0e0;
.control-icon {
filter: none;
}
text {
color: #333;
}
}
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>