uniapp 封装地图运动轨迹

注意:封装的地图运动轨迹只测试了微信小程序,其他平台未测试

不过实现功能的核心API就是这几个:uniapp.dcloud.net.cn/api/locatio...

展示的核心组件是:uniapp.dcloud.net.cn/component/m...

不同平台根据文档中的差异说明修改代码即可

功能效果:

开始前先在配置文件manifest.json中找到 mp-weixin 的配置项,然后加上下面的配置

json 复制代码
"permission": {
			"scope.userLocation": {
				"desc": "你的位置信息将用于小程序位置接口的效果展示"
			}
		},
		"requiredPrivateInfos": [
			"getLocation",
			"startLocationUpdate",
			"startLocationUpdateBackground",
			"onLocationChange"
		],
		"requiredBackgroundModes": ["location"],

代码

xml 复制代码
<template>
	<view class="container">
		<!-- 地图容器 -->
		<map
			id="map"
			class="map"
			:latitude="mapCenter.latitude"
			:longitude="mapCenter.longitude"
			:scale="16"
			:markers="markers"
			:polyline="polyline"
			:enable-3D="true"
			:show-compass="false"
			:enable-zoom="true"
			:enable-scroll="true"
			:enable-rotate="true"
			:enable-overlooking="true"
			:enable-satellite="false"
			:enable-traffic="false"
			:show-location="true"
		></map>

		<div class="operate-box">
			<!-- 定位模式切换 -->
			<view class="mode-panel" v-if="!isTracking">
				<view class="mode-item">
					<text class="mode-label">定位模式 :</text>
					<view class="mode-switch">
						<text class="mode-text" :class="{ active: !isBackgroundMode }">前台定位</text>
						<switch
							:checked="isBackgroundMode"
							:disabled="!isLocationBackgroundAuth"
							@change="toggleBackgroundMode"
							color="#007AFF"
						/>
						<text class="mode-text" :class="{ active: isBackgroundMode }">后台定位</text>
					</view>
				</view>
			</view>
			<!-- 控制按钮 -->
			<view class="control-panel">
				<view class="control-btn" :class="{ active: isTracking }" @click="toggleTracking">
					{{ isTracking ? '停止记录' : '开始记录' }}
				</view>
				<view
					class="control-btn clear-btn"
					@click="clearTrack"
					v-if="!isTracking && trackPoints.length > 0"
				>
					清除轨迹
				</view>
				<view class="control-btn" v-if="!isTracking && trackPoints.length > 0" @click="saveTrack">
					保存轨迹记录
				</view>
			</view>
		</div>

		<!-- 运动信息面板 -->
		<view class="info-panel" v-if="trackPoints.length > 0">
			<view class="info-item">
				<text class="info-label">总距离:</text>
				<text class="info-value">{{ totalDistance }}km</text>
			</view>
			<view class="info-item">
				<text class="info-label">运动时长:</text>
				<text class="info-value">{{ formatDuration }}</text>
			</view>
			<view class="info-item">
				<text class="info-label">平均速度:</text>
				<text class="info-value">{{ averageSpeed }}km/h</text>
			</view>
		</view>
	</view>
</template>

<script setup>
	import { ref, onMounted, computed, watchEffect, getCurrentInstance, onBeforeUnmount } from 'vue';
	import { onLoad } from '@dcloudio/uni-app';

	const { proxy } = getCurrentInstance();
	const eventChannel = proxy.getOpenerEventChannel();

	import { getSetting } from '@/utils/index';

	onShareAppMessage((res) => {
		return {
			title: '运动轨迹',
			path: `/pages/home/index`,
		};
	});

	onLoad((res) => {
		if (res.isViewMode) {
			isViewMode.value = true;
			let trackData = uni.getStorageSync('trackData');
			if (trackData) {
				trackData = JSON.parse(trackData);
				mapCenter.value = trackData.mapCenter;
				markers.value = trackData.markers;
				polyline.value = trackData.polyline;
			}
		}
		if (isViewMode.value) return;

		// 检查后台定位权限
		getSetting('scope.userLocationBackground', '请在设置中开启后台定位权限', () => {
			isLocationBackgroundAuth.value = true;
			isBackgroundMode.value = true;
		});
		// 检查前台定位权限
		getSetting('scope.userLocation', '请在设置中开启地理位置权限', () => {
			isLocationAuth.value = true;
			getCurrentLocation();
		});
	});

	onBeforeUnmount(() => {
		// 停止位置监听
		stopLocationTracking();
	});

	const isViewMode = ref(false); // 是否是查看模式

	const isLocationAuth = ref(false); // 是否授权地理位置权限
	const isLocationBackgroundAuth = ref(false); // 是否授权后台定位权限

	const isTracking = ref(false); // 是否正在记录
	const isBackgroundMode = ref(false); // 是否使用后台定位模式

	const trackPoints = ref([]); // 轨迹点
	const markers = ref([]); // 地图标记点
	const polyline = ref([]); // 轨迹线

	// 地图中心点
	const mapCenter = ref({
		latitude: 39.909,
		longitude: 116.397,
	});

	let timer = null; // 定时器
	const startTime = ref(null); // 开始时间
	const currentTime = ref(Date.now()); // 当前时间,用于实时更新

	// 计算两点间距离(米)
	function calculateDistance(lat1, lng1, lat2, lng2) {
		const R = 6371000; // 地球半径(米)
		const dLat = ((lat2 - lat1) * Math.PI) / 180;
		const dLng = ((lng2 - lng1) * Math.PI) / 180;
		const a =
			Math.sin(dLat / 2) * Math.sin(dLat / 2) +
			Math.cos((lat1 * Math.PI) / 180) *
				Math.cos((lat2 * Math.PI) / 180) *
				Math.sin(dLng / 2) *
				Math.sin(dLng / 2);
		const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
		return R * c;
	}

	// 总距离
	const totalDistance = computed(() => {
		if (trackPoints.value.length < 2) return 0;

		let distance = 0;
		for (let i = 1; i < trackPoints.value.length; i++) {
			distance += calculateDistance(
				trackPoints.value[i - 1].latitude,
				trackPoints.value[i - 1].longitude,
				trackPoints.value[i].latitude,
				trackPoints.value[i].longitude
			);
		}
		return (distance / 1000).toFixed(2);
	});

	// 运动时长
	const formatDuration = computed(() => {
		if (!startTime.value) return '00:00:00';

		const duration = currentTime.value - startTime.value;
		const hours = Math.floor(duration / 3600000);
		const minutes = Math.floor((duration % 3600000) / 60000);
		const seconds = Math.floor((duration % 60000) / 1000);

		return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds
			.toString()
			.padStart(2, '0')}`;
	});

	// 平均速度
	const averageSpeed = computed(() => {
		if (totalDistance.value <= 0 || !startTime.value) return 0;

		const duration = (currentTime.value - startTime.value) / 3600000; // 小时
		return (totalDistance.value / duration).toFixed(2);
	});

	// 获取当前位置
	function getCurrentLocation() {
		return new Promise((resolve, reject) => {
			uni.getLocation({
				type: 'gcj02',
				success: (res) => {
					mapCenter.value = {
						latitude: res.latitude,
						longitude: res.longitude,
					};
					resolve();
				},
				fail: (err) => {
					console.error('获取位置失败:', err);
					reject(err);
				},
			});
		});
	}

	// 添加地图标记点
	function addMarker(data) {
		markers.value.push({
			id: data.id,
			latitude: data.latitude,
			longitude: data.longitude,
			// title: title,
			width: 20,
			height: 20,
			callout: {
				content: data.content,
				color: '#ffffff',
				fontSize: 12,
				borderRadius: 4,
				bgColor: '#007AFF',
				padding: 4,
				display: 'ALWAYS',
			},
		});
	}

	// 启动定时器
	function startTimer() {
		if (timer) return;
		timer = setInterval(() => {
			currentTime.value = Date.now();
		}, 1000); // 每秒更新一次
	}
	// 停止定时器
	function stopTimer() {
		if (timer) {
			clearInterval(timer);
			timer = null;
		}
	}

	// 开始/停止记录
	function toggleTracking() {
		if (isTracking.value) {
			stopLocationTracking();
		} else {
			startLocationTracking();
		}
	}

	// 切换后台定位模式
	function toggleBackgroundMode(e) {
		isBackgroundMode.value = e.detail.value;
		console.log('切换定位模式:', isBackgroundMode.value ? '后台定位' : '前台定位');
	}

	// 开始位置监听
	function startLocationTracking() {
		if (isBackgroundMode.value) {
			// 后台定位模式
			if (isLocationAuth.value && isLocationBackgroundAuth.value) {
				initLocationTracking();
			} else {
				if (!isLocationAuth.value) {
					uni.showModal({
						title: '需要地理位置权限',
						content: '请在设置中开启地理位置权限',
						showCancel: false,
					});
				} else if (!isLocationBackgroundAuth.value) {
					uni.showModal({
						title: '需要后台定位权限',
						content: '请在设置中开启后台定位权限',
						showCancel: false,
					});
				}
			}
		} else {
			// 前台定位模式
			if (isLocationAuth.value) {
				initLocationTracking();
			} else {
				uni.showModal({
					title: '需要地理位置权限',
					content: '请在设置中开启地理位置权限',
					showCancel: false,
				});
			}
		}
	}

	// 初始化位置监听
	async function initLocationTracking() {
		// 根据模式选择不同的位置监听方式
		const locationConfig = {
			success: () => {
				console.log('开始监听位置变化');
				isTracking.value = true;
				startTime.value = Date.now();
				startTimer(); // 开始定时器

				// 添加位置变化监听
				uni.onLocationChange((res) => {
					handleLocationChange(res);
				});
			},
			fail: (err) => {
				console.error('开始监听位置变化失败:', err);
				uni.showToast({
					title: '开始监听失败',
					icon: 'none',
				});
			},
		};

		if (isBackgroundMode.value) {
			// 使用后台定位
			console.log('使用后台定位模式');
			uni.startLocationUpdateBackground(locationConfig);
		} else {
			// 使用前台定位
			console.log('使用前台定位模式');
			uni.startLocationUpdate(locationConfig);
		}
		await getCurrentLocation();
		addMarker({
			latitude: mapCenter.value.latitude,
			longitude: mapCenter.value.longitude,
			id: new Date().getTime(),
			content: '起始点',
		});
	}

	// 处理位置变化
	function handleLocationChange(location) {
		console.log('位置变化', location);

		// 添加轨迹点
		const point = {
			latitude: location.latitude,
			longitude: location.longitude,
			timestamp: Date.now(),
		};

		if (checkLocation(point)) {
			trackPoints.value.push(point);

			// 更新轨迹线
			updatePolyline();
		}

		// 更新地图中心点(跟随用户位置)
		mapCenter.value = {
			latitude: location.latitude,
			longitude: location.longitude,
		};
	}

	// 校验返回的经纬度是否合法:和上一个经纬度相差不能超过100米
	function checkLocation(location) {
		if (trackPoints.value.length === 0) return true;
		const lastPoint = trackPoints.value[trackPoints.value.length - 1];
		const distance = calculateDistance(
			lastPoint.latitude,
			lastPoint.longitude,
			location.latitude,
			location.longitude
		);
		console.log('距离上次位置', distance + '米');
		return distance < 100;
	}

	// 更新轨迹线
	function updatePolyline() {
		if (trackPoints.value.length < 2) return;

		polyline.value = [
			{
				points: trackPoints.value.map((point) => ({
					latitude: point.latitude,
					longitude: point.longitude,
				})),
				color: '#007AFF',
				width: 4,
				arrowLine: true,
				colorList: [
					'#FF0000',
					'#FF7F00',
					'#FFFF00',
					'#00FF00',
					'#00FFFF',
					'#0000FF',
					'#8A2BE2',
					'#8B00FF',
				],
			},
		];
	}

	// 清除轨迹
	function clearTrack() {
		uni.showModal({
			title: '确认清除',
			content: '确定要清除所有轨迹记录吗?',
			success: (res) => {
				if (res.confirm) {
					trackPoints.value = [];
					polyline.value = [];
					startTime.value = null;
					currentTime.value = Date.now(); // 清除时也重置当前时间

					uni.showToast({
						title: '轨迹已清除',
						icon: 'success',
					});
				}
			},
		});
	}

	// 停止位置监听
	function stopLocationTracking() {
		uni.stopLocationUpdate({
			success: async () => {
				console.log('停止监听位置变化');
				isTracking.value = false;
				stopTimer(); // 停止定时器

				// 移除位置变化监听
				uni.offLocationChange();

				await getCurrentLocation();
				addMarker({
					latitude: mapCenter.value.latitude,
					longitude: mapCenter.value.longitude,
					id: new Date().getTime(),
					content: '结束点',
				});
			},
			fail: (err) => {
				console.error('停止监听位置变化失败:', err);
			},
		});
	}

	// 保存轨迹记录
	function saveTrack() {
		console.log('保存轨迹记录');
		let saveData = {
			mapCenter: mapCenter.value,
			markers: markers.value,
			polyline: polyline.value,
		};
		uni.setStorageSync('trackData', JSON.stringify(saveData));
		uni.showToast({
			title: '轨迹记录已保存',
			icon: 'success',
		});
		setTimeout(() => {
			uni.reLaunch({
				url: `/pages/home/index?isViewMode=1`,
			});
		}, 1000);
	}
</script>

<style lang="scss" scoped>
	.container {
		position: relative;
		width: 100%;
		height: 100vh;
	}

	.map {
		width: 100%;
		height: 100%;
	}

	.operate-box {
		position: absolute;
		top: 0;
		left: 0;
		z-index: 100;
		width: 100%;
		padding: 20rpx;
		display: flex;
		justify-content: center;
		align-items: center;
		flex-direction: column;
	}

	.mode-panel {
		background: rgba(255, 255, 255, 0.95);
		border-radius: 12rpx;
		padding: 15rpx;
		box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
		margin-bottom: 20rpx;
	}
	.mode-item {
		display: flex;
		align-items: center;
		margin-bottom: 10rpx;

		&:last-child {
			margin-bottom: 0;
		}
	}
	.mode-label {
		font-size: 26rpx;
		color: #666;
		margin-right: 10rpx;
	}
	.mode-switch {
		display: flex;
		align-items: center;
		background: #f0f0f0;
		border-radius: 15rpx;
		padding: 6rpx 10rpx;
		switch {
			margin: 0 10rpx;
		}
	}
	.mode-text {
		font-size: 24rpx;
		color: #333;
		padding: 4rpx 10rpx;
		border-radius: 10rpx;

		&.active {
			background: #007aff;
			color: white;
		}
	}

	.control-panel {
		display: flex;
		justify-content: center;
		flex-wrap: wrap;
		gap: 10rpx;
		width: 100%;
	}
	.control-btn {
		padding: 10rpx 20rpx;
		background: rgba(255, 255, 255, 0.9);
		border: 1px solid #ddd;
		border-radius: 22rpx;
		font-size: 32rpx;
		color: #333;
		display: flex;
		justify-content: center;
		align-items: center;

		&.active {
			background: #007aff;
			color: white;
			border-color: #007aff;
		}

		&:disabled {
			opacity: 0.5;
		}
	}
	.clear-btn {
		background: rgba(255, 59, 48, 0.9);
		color: white;
		border-color: #ff3b30;
	}

	.info-panel {
		position: absolute;
		bottom: 20rpx;
		left: 20rpx;
		right: 20rpx;
		background: rgba(255, 255, 255, 0.95);
		border-radius: 12rpx;
		padding: 15rpx;
		box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
		z-index: 100;
	}
	.info-item {
		display: flex;
		justify-content: space-between;
		align-items: center;
		margin-bottom: 8rpx;

		&:last-child {
			margin-bottom: 0;
		}
	}
	.info-label {
		font-size: 28rpx;
		color: #666;
	}
	.info-value {
		font-size: 32rpx;
		color: #333;
		font-weight: 500;
	}
</style>
相关推荐
再学一点就睡3 小时前
手写 Promise 静态方法:从原理到实现
前端·javascript·面试
再学一点就睡3 小时前
前端必会:Promise 全解析,从原理到实战
前端·javascript·面试
前端工作日常4 小时前
我理解的eslint配置
前端·eslint
前端工作日常4 小时前
项目价值判断的核心标准
前端·程序员
90后的晨仔5 小时前
理解 Vue 的列表渲染:从传统 DOM 到响应式世界的演进
前端·vue.js
OEC小胖胖5 小时前
性能优化(一):时间分片(Time Slicing):让你的应用在高负载下“永不卡顿”的秘密
前端·javascript·性能优化·web
烛阴5 小时前
ABS - Rhomb
前端·webgl
植物系青年5 小时前
10+核心功能点!低代码平台实现不完全指南 🧭(下)
前端·低代码
植物系青年5 小时前
10+核心功能点!低代码平台实现不完全指南 🧭(上)
前端·低代码
桑晒.6 小时前
CSRF漏洞原理及利用
前端·web安全·网络安全·csrf