523 lines
13 KiB
Vue
523 lines
13 KiB
Vue
|
<template>
|
|||
|
<view class="map-container">
|
|||
|
<u-notice-bar mode="horizontal" :list="list"></u-notice-bar>
|
|||
|
<map id="map" class="map" :latitude="latitude" :longitude="longitude" :polygons="polygons" :markers="markers"
|
|||
|
:show-location="true" @tap="handleMapTap" @markertap="handleMarkerTap" @markerdragend="handleMarkerDrag"
|
|||
|
scale="14">
|
|||
|
</map>
|
|||
|
|
|||
|
<!-- 定位按钮 -->
|
|||
|
<cover-image class="location-btn" src="https://lxnapi.ccttiot.com/bike/img/static/uoxanRjBrBrtcYwRGXKa"
|
|||
|
@tap="moveToLocation"></cover-image>
|
|||
|
|
|||
|
<!-- 提示文本 -->
|
|||
|
<cover-view class="tip-text" v-if="isDrawing">
|
|||
|
{{ editMode ? '点击锚点进行删除' : '点击地图添加锚点,拖动锚点可调整位置' }}
|
|||
|
</cover-view>
|
|||
|
|
|||
|
<!-- 控制按钮组 -->
|
|||
|
<cover-view class="controls">
|
|||
|
<!-- 未开始绘制时显示开始按钮 -->
|
|||
|
<cover-view class="control-btn" @tap="startDrawing" v-if="!isDrawing">开始绘制</cover-view>
|
|||
|
|
|||
|
<!-- 绘制过程中显示的按钮组 -->
|
|||
|
<block v-else>
|
|||
|
<cover-view class="control-btn-group">
|
|||
|
<!-- 绘制锚点按钮 -->
|
|||
|
<cover-view class="control-btn control-btn-small" :class="{ 'control-btn-active': !editMode }"
|
|||
|
@tap="toggleEditMode('draw')">绘制锚点</cover-view>
|
|||
|
|
|||
|
<!-- 删除锚点按钮 -->
|
|||
|
<cover-view class="control-btn control-btn-small" :class="{ 'control-btn-active': editMode }"
|
|||
|
@tap="toggleEditMode('delete')">删除锚点</cover-view>
|
|||
|
</cover-view>
|
|||
|
|
|||
|
<cover-view class="control-btn-group">
|
|||
|
<!-- 完成按钮 -->
|
|||
|
<cover-view class="control-btn" @tap="finishDrawing" :class="{ 'btn-disabled': points.length < 3 }">完成绘制
|
|||
|
</cover-view>
|
|||
|
|
|||
|
<!-- 清除按钮 -->
|
|||
|
<cover-view class="control-btn control-btn-danger" @tap="clearPolygon">清除</cover-view>
|
|||
|
</cover-view>
|
|||
|
</block>
|
|||
|
</cover-view>
|
|||
|
</view>
|
|||
|
</template>
|
|||
|
|
|||
|
<script>
|
|||
|
export default {
|
|||
|
name: 'CustomMap',
|
|||
|
props: {
|
|||
|
areaInfo: {
|
|||
|
type: Object,
|
|||
|
default: () => ({})
|
|||
|
}
|
|||
|
},
|
|||
|
data() {
|
|||
|
return {
|
|||
|
latitude: 39.909,
|
|||
|
longitude: 116.397,
|
|||
|
points: [],
|
|||
|
polygons: [],
|
|||
|
markers: [],
|
|||
|
isDrawing: false,
|
|||
|
editMode: false,
|
|||
|
mapContext: null,
|
|||
|
initialCenter: {
|
|||
|
latitude: 39.909,
|
|||
|
longitude: 116.397
|
|||
|
},
|
|||
|
list: [
|
|||
|
'提示:点击"开始绘制"按钮开始绘制区域',
|
|||
|
'点击地图添加锚点',
|
|||
|
'点击锚点可删除',
|
|||
|
'至少需要3个锚点才能形成有效区域'
|
|||
|
],
|
|||
|
|
|||
|
// noticeText: '提示:点击"开始绘制"按钮开始绘制区域,点击地图添加锚点,拖动锚点可调整位置,点击锚点可删除。至少需要3个锚点才能形成有效区域。',
|
|||
|
}
|
|||
|
},
|
|||
|
mounted() {
|
|||
|
this.mapContext = uni.createMapContext('map', this)
|
|||
|
},
|
|||
|
watch: {
|
|||
|
areaInfo: {
|
|||
|
handler(newVal) {
|
|||
|
if (newVal.boundary && newVal.latitude && newVal.longitude) {
|
|||
|
this.initBoundary(newVal)
|
|||
|
}
|
|||
|
},
|
|||
|
immediate: true
|
|||
|
}
|
|||
|
},
|
|||
|
methods: {
|
|||
|
// 初始化边界
|
|||
|
initBoundary(info) {
|
|||
|
this.initialCenter = {
|
|||
|
latitude: info.latitude,
|
|||
|
longitude: info.longitude
|
|||
|
}
|
|||
|
this.latitude = info.latitude
|
|||
|
this.longitude = info.longitude
|
|||
|
|
|||
|
// 解析边界字符串
|
|||
|
const points = info.boundaryStr.split('],[').map(pointStr => {
|
|||
|
// 清理首尾的方括号
|
|||
|
const cleanStr = pointStr.replace(/^\[|\]$/g, '')
|
|||
|
// 分割经纬度
|
|||
|
const [longitude, latitude] = cleanStr.split(',')
|
|||
|
|
|||
|
return {
|
|||
|
longitude: Number(longitude),
|
|||
|
latitude: Number(latitude)
|
|||
|
}
|
|||
|
})
|
|||
|
|
|||
|
console.log('解析后的点:', points)
|
|||
|
|
|||
|
// 设置点和标记
|
|||
|
this.points = points.filter(point =>
|
|||
|
!isNaN(point.latitude) && !isNaN(point.longitude)
|
|||
|
)
|
|||
|
|
|||
|
this.updateMarkers()
|
|||
|
this.updatePolygonWithNearestNeighbor()
|
|||
|
},
|
|||
|
|
|||
|
// 更新标记
|
|||
|
updateMarkers() {
|
|||
|
this.markers = this.points.map((point, index) => ({
|
|||
|
id: index,
|
|||
|
latitude: point.latitude,
|
|||
|
longitude: point.longitude,
|
|||
|
width: 20,
|
|||
|
height: 20,
|
|||
|
// callout: {
|
|||
|
// // content: `${index + 1}`,
|
|||
|
// color: '#ffffff',
|
|||
|
// fontSize: 14,
|
|||
|
// borderRadius: 4,
|
|||
|
// bgColor: '#007AFF',
|
|||
|
// padding: 4,
|
|||
|
// display: 'ALWAYS'
|
|||
|
// },
|
|||
|
draggable: true
|
|||
|
}))
|
|||
|
},
|
|||
|
|
|||
|
// 移动到当前位置
|
|||
|
async moveToLocation() {
|
|||
|
try {
|
|||
|
const [err, res] = await uni.getLocation({
|
|||
|
type: 'gcj02'
|
|||
|
});
|
|||
|
|
|||
|
if (err) {
|
|||
|
throw new Error('获取位置失败');
|
|||
|
}
|
|||
|
|
|||
|
this.latitude = res.latitude;
|
|||
|
this.longitude = res.longitude;
|
|||
|
|
|||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|||
|
|
|||
|
this.mapContext.moveToLocation({
|
|||
|
latitude: res.latitude,
|
|||
|
longitude: res.longitude,
|
|||
|
success: () => {
|
|||
|
setTimeout(() => {
|
|||
|
this.mapContext.getScale({
|
|||
|
success: () => { }
|
|||
|
});
|
|||
|
}, 500);
|
|||
|
}
|
|||
|
});
|
|||
|
} catch (error) {
|
|||
|
uni.showToast({
|
|||
|
title: '定位失败,请检查定位权限',
|
|||
|
icon: 'none'
|
|||
|
});
|
|||
|
}
|
|||
|
},
|
|||
|
|
|||
|
// 切换编辑模式
|
|||
|
toggleEditMode(mode) {
|
|||
|
this.editMode = mode === 'delete'
|
|||
|
uni.showToast({
|
|||
|
title: this.editMode ? '请点击锚点进行删除' : '请点击地图添加锚点',
|
|||
|
icon: 'none'
|
|||
|
})
|
|||
|
},
|
|||
|
|
|||
|
// 开始绘制
|
|||
|
startDrawing() {
|
|||
|
// 保存当前中心点
|
|||
|
const currentCenter = {
|
|||
|
latitude: this.latitude,
|
|||
|
longitude: this.longitude
|
|||
|
}
|
|||
|
|
|||
|
// this.points = []
|
|||
|
// this.markers = []
|
|||
|
// this.polygons = []
|
|||
|
this.isDrawing = true
|
|||
|
this.editMode = false
|
|||
|
|
|||
|
// 恢复地图中心点
|
|||
|
this.$nextTick(() => {
|
|||
|
this.latitude = currentCenter.latitude
|
|||
|
this.longitude = currentCenter.longitude
|
|||
|
})
|
|||
|
|
|||
|
uni.showToast({
|
|||
|
title: '开始绘制区域',
|
|||
|
icon: 'none'
|
|||
|
})
|
|||
|
},
|
|||
|
|
|||
|
// 检查点是否过近
|
|||
|
isPointTooClose(point1, point2, threshold = 0.0001) {
|
|||
|
const distance = Math.sqrt(
|
|||
|
Math.pow(point1.latitude - point2.latitude, 2) +
|
|||
|
Math.pow(point1.longitude - point2.longitude, 2)
|
|||
|
)
|
|||
|
return distance < threshold
|
|||
|
},
|
|||
|
|
|||
|
// 清理过近的点
|
|||
|
cleanNearbyPoints() {
|
|||
|
if (this.points.length < 2) return
|
|||
|
|
|||
|
let cleanedPoints = []
|
|||
|
let pointsToKeep = new Set()
|
|||
|
|
|||
|
// 第一遍:标记要保留的点
|
|||
|
for (let i = 0; i < this.points.length; i++) {
|
|||
|
let shouldKeep = true
|
|||
|
for (let j = 0; j < i; j++) {
|
|||
|
if (pointsToKeep.has(j) && this.isPointTooClose(this.points[i], this.points[j])) {
|
|||
|
shouldKeep = false
|
|||
|
break
|
|||
|
}
|
|||
|
}
|
|||
|
if (shouldKeep) {
|
|||
|
pointsToKeep.add(i)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 第二遍:只保留标记的点
|
|||
|
pointsToKeep.forEach(index => {
|
|||
|
cleanedPoints.push(this.points[index])
|
|||
|
})
|
|||
|
|
|||
|
// 更新点
|
|||
|
this.points = cleanedPoints
|
|||
|
this.updateMarkers()
|
|||
|
},
|
|||
|
|
|||
|
// 处理地图点击
|
|||
|
handleMapTap(e) {
|
|||
|
if (!this.isDrawing || this.editMode) return
|
|||
|
|
|||
|
const { latitude, longitude } = e.detail
|
|||
|
|
|||
|
// 检查新点是否与现有点过近
|
|||
|
const newPoint = { latitude, longitude }
|
|||
|
for (let point of this.points) {
|
|||
|
if (this.isPointTooClose(newPoint, point)) {
|
|||
|
uni.showToast({
|
|||
|
title: '锚点位置过近',
|
|||
|
icon: 'none'
|
|||
|
})
|
|||
|
return
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// 添加新的点
|
|||
|
this.points.push(newPoint)
|
|||
|
this.updateMarkers()
|
|||
|
|
|||
|
// 更新多边形
|
|||
|
this.updatePolygonWithNearestNeighbor()
|
|||
|
},
|
|||
|
|
|||
|
// 处理标记点击
|
|||
|
handleMarkerTap(e) {
|
|||
|
if (!this.editMode) return
|
|||
|
|
|||
|
const markerId = e.detail.markerId
|
|||
|
|
|||
|
// 删除对应的点
|
|||
|
this.points = this.points.filter((_, index) => index !== markerId)
|
|||
|
this.updateMarkers()
|
|||
|
|
|||
|
// 更新多边形
|
|||
|
this.updatePolygonWithNearestNeighbor()
|
|||
|
},
|
|||
|
|
|||
|
// 处理标记拖动
|
|||
|
handleMarkerDrag(e) {
|
|||
|
const { markerId, latitude, longitude } = e.detail
|
|||
|
|
|||
|
// 更新点的位置
|
|||
|
this.points[markerId] = { latitude, longitude }
|
|||
|
this.updateMarkers()
|
|||
|
|
|||
|
// 更新多边形
|
|||
|
this.updatePolygonWithNearestNeighbor()
|
|||
|
},
|
|||
|
|
|||
|
// 更新多边形
|
|||
|
updatePolygonWithNearestNeighbor() {
|
|||
|
if (this.points.length < 2) {
|
|||
|
this.polygons = []
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
// 清理过近的点
|
|||
|
this.cleanNearbyPoints()
|
|||
|
|
|||
|
// 如果点数太少,直接连接
|
|||
|
if (this.points.length < 3) {
|
|||
|
this.polygons = [{
|
|||
|
points: [...this.points],
|
|||
|
strokeWidth: 2,
|
|||
|
strokeColor: '#FF0000',
|
|||
|
fillColor: '#FF000050',
|
|||
|
zIndex: 1
|
|||
|
}]
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
// 计算中心点
|
|||
|
let centerLat = this.points.reduce((sum, p) => sum + p.latitude, 0) / this.points.length
|
|||
|
let centerLng = this.points.reduce((sum, p) => sum + p.longitude, 0) / this.points.length
|
|||
|
|
|||
|
// 按照与中心点的角度排序
|
|||
|
let sortedPoints = [...this.points].sort((a, b) => {
|
|||
|
let angleA = Math.atan2(a.latitude - centerLat, a.longitude - centerLng)
|
|||
|
let angleB = Math.atan2(b.latitude - centerLat, b.longitude - centerLng)
|
|||
|
return angleA - angleB
|
|||
|
})
|
|||
|
|
|||
|
// 闭合多边形
|
|||
|
sortedPoints.push(sortedPoints[0])
|
|||
|
|
|||
|
// 更新多边形
|
|||
|
this.polygons = [{
|
|||
|
points: sortedPoints,
|
|||
|
strokeWidth: 2,
|
|||
|
strokeColor: '#FF0000',
|
|||
|
fillColor: '#FF000050',
|
|||
|
zIndex: 1
|
|||
|
}]
|
|||
|
},
|
|||
|
|
|||
|
// 完成绘制
|
|||
|
finishDrawing() {
|
|||
|
if (this.points.length < 3) {
|
|||
|
uni.showToast({
|
|||
|
title: '至少需要3个锚点才能形成区域',
|
|||
|
icon: 'none'
|
|||
|
})
|
|||
|
return
|
|||
|
}
|
|||
|
|
|||
|
// 最后清理一次过近的点
|
|||
|
this.cleanNearbyPoints()
|
|||
|
|
|||
|
// 计算中心点
|
|||
|
const centerLat = this.points.reduce((sum, p) => sum + Number(p.latitude), 0) / this.points.length
|
|||
|
const centerLng = this.points.reduce((sum, p) => sum + Number(p.longitude), 0) / this.points.length
|
|||
|
|
|||
|
// 按角度排序
|
|||
|
let sortedPoints = [...this.points].sort((a, b) => {
|
|||
|
let angleA = Math.atan2(a.latitude - centerLat, a.longitude - centerLng)
|
|||
|
let angleB = Math.atan2(b.latitude - centerLat, b.longitude - centerLng)
|
|||
|
return angleA - angleB
|
|||
|
})
|
|||
|
|
|||
|
// 格式化坐标点,保留5位小数
|
|||
|
const formattedPoints = sortedPoints.map(point => ({
|
|||
|
latitude: Number(Number(point.latitude).toFixed(5)),
|
|||
|
longitude: Number(Number(point.longitude).toFixed(5))
|
|||
|
}))
|
|||
|
|
|||
|
// 只关闭编辑模式,保留绘制结果
|
|||
|
this.isDrawing = false
|
|||
|
this.editMode = false
|
|||
|
|
|||
|
// 更新多边形显示
|
|||
|
this.polygons = [{
|
|||
|
points: formattedPoints,
|
|||
|
strokeWidth: 2,
|
|||
|
strokeColor: '#FF0000',
|
|||
|
fillColor: '#FF000050',
|
|||
|
zIndex: 1
|
|||
|
}]
|
|||
|
|
|||
|
// 发送给父组件
|
|||
|
this.$emit('on-polygon-complete', {
|
|||
|
points: formattedPoints,
|
|||
|
polygon: this.polygons[0]
|
|||
|
})
|
|||
|
},
|
|||
|
|
|||
|
// 清除绘制
|
|||
|
clearPolygon() {
|
|||
|
const currentCenter = {
|
|||
|
latitude: this.latitude,
|
|||
|
longitude: this.longitude
|
|||
|
}
|
|||
|
|
|||
|
this.points = []
|
|||
|
this.markers = []
|
|||
|
this.polygons = []
|
|||
|
this.isDrawing = false
|
|||
|
this.editMode = false
|
|||
|
|
|||
|
this.$nextTick(() => {
|
|||
|
this.latitude = currentCenter.latitude
|
|||
|
this.longitude = currentCenter.longitude
|
|||
|
})
|
|||
|
|
|||
|
uni.showToast({
|
|||
|
title: '已清除绘制',
|
|||
|
icon: 'none'
|
|||
|
})
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
</script>
|
|||
|
|
|||
|
<style>
|
|||
|
.map-container {
|
|||
|
width: 100%;
|
|||
|
height: 100vh;
|
|||
|
position: relative;
|
|||
|
}
|
|||
|
|
|||
|
.map {
|
|||
|
width: 100%;
|
|||
|
height: 100%;
|
|||
|
}
|
|||
|
|
|||
|
.location-btn {
|
|||
|
position: fixed;
|
|||
|
right: 30rpx;
|
|||
|
bottom: 300rpx;
|
|||
|
width: 80rpx;
|
|||
|
height: 80rpx;
|
|||
|
z-index: 100;
|
|||
|
}
|
|||
|
|
|||
|
.tip-text {
|
|||
|
position: fixed;
|
|||
|
top: 40rpx;
|
|||
|
left: 50%;
|
|||
|
transform: translateX(-50%);
|
|||
|
background-color: rgba(0, 0, 0, 0.7);
|
|||
|
color: #ffffff;
|
|||
|
padding: 16rpx 30rpx;
|
|||
|
border-radius: 30rpx;
|
|||
|
font-size: 28rpx;
|
|||
|
z-index: 100;
|
|||
|
}
|
|||
|
|
|||
|
.control-btn-group {
|
|||
|
display: flex;
|
|||
|
gap: 20rpx;
|
|||
|
margin-bottom: 20rpx;
|
|||
|
}
|
|||
|
|
|||
|
.control-btn-small {
|
|||
|
min-width: 120rpx;
|
|||
|
height: 60rpx;
|
|||
|
line-height: 60rpx;
|
|||
|
font-size: 24rpx;
|
|||
|
padding: 0 20rpx;
|
|||
|
}
|
|||
|
|
|||
|
.controls {
|
|||
|
position: fixed;
|
|||
|
bottom: 60rpx;
|
|||
|
left: 0;
|
|||
|
right: 0;
|
|||
|
display: flex;
|
|||
|
flex-direction: column;
|
|||
|
align-items: center;
|
|||
|
padding: 0 30rpx;
|
|||
|
z-index: 100;
|
|||
|
}
|
|||
|
|
|||
|
.control-btn {
|
|||
|
pointer-events: auto;
|
|||
|
min-width: 200rpx;
|
|||
|
height: 80rpx;
|
|||
|
line-height: 80rpx;
|
|||
|
background-color: #ffffff;
|
|||
|
border-radius: 10rpx;
|
|||
|
font-size: 28rpx;
|
|||
|
text-align: center;
|
|||
|
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
|
|||
|
margin: 0 10rpx;
|
|||
|
padding: 0 40rpx;
|
|||
|
}
|
|||
|
|
|||
|
.control-btn-danger {
|
|||
|
background-color: #ff4444;
|
|||
|
color: #ffffff;
|
|||
|
}
|
|||
|
|
|||
|
.control-btn-active {
|
|||
|
background-color: #007AFF;
|
|||
|
color: #ffffff;
|
|||
|
}
|
|||
|
|
|||
|
.btn-disabled {
|
|||
|
opacity: 0.5;
|
|||
|
background-color: #cccccc;
|
|||
|
pointer-events: none;
|
|||
|
}
|
|||
|
</style>
|