550 lines
12 KiB
Vue
550 lines
12 KiB
Vue
<template>
|
||
<view class="vehicle-page">
|
||
<view class="header">
|
||
<text class="title">车辆定位</text>
|
||
<text class="subtitle">获取当前车辆位置信息</text>
|
||
</view>
|
||
|
||
<view class="content">
|
||
<!-- 获取位置按钮 -->
|
||
<button class="get-location-btn" @click="handleGetLocation" :disabled="isLoading">
|
||
{{ isLoading ? '获取中...' : '获取当前位置' }}
|
||
</button>
|
||
|
||
<!-- 位置信息显示 -->
|
||
<view v-if="locationInfo" class="location-info">
|
||
<view class="info-card">
|
||
<view class="card-title">
|
||
<Location2 size="24" color="#667eea" />
|
||
<text class="title-text">位置信息</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="label">纬度:</text>
|
||
<text class="value">{{ locationInfo.latitude }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="label">经度:</text>
|
||
<text class="value">{{ locationInfo.longitude }}</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="label">高度:</text>
|
||
<text class="value">{{ locationInfo.altitude || '未知' }}m</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="label">精确度:</text>
|
||
<text class="value">{{ locationInfo.accuracy }}m</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="label">速度:</text>
|
||
<text class="value">{{ locationInfo.speed || 0 }}m/s</text>
|
||
</view>
|
||
<view class="info-item">
|
||
<text class="label">方向角:</text>
|
||
<text class="value">{{ locationInfo.heading || 0 }}°</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 地址信息显示 -->
|
||
<view v-if="addressInfo" class="address-info">
|
||
<view class="info-card">
|
||
<view class="card-title">
|
||
<Location size="24" color="#667eea" />
|
||
<text class="title-text">地址信息</text>
|
||
</view>
|
||
<view class="address-item">
|
||
<text class="address-text">{{ addressInfo.address }}</text>
|
||
</view>
|
||
<view v-if="addressInfo.formatted_addresses" class="address-item">
|
||
<text class="address-text">{{ addressInfo.formatted_addresses.recommend }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 操作按钮 -->
|
||
<view v-if="locationInfo" class="action-buttons">
|
||
<button class="action-btn primary" @click="openMap">在地图中查看</button>
|
||
<button class="action-btn secondary" @click="copyLocation">复制坐标</button>
|
||
<button class="action-btn secondary" @click="shareLocation">分享位置</button>
|
||
</view>
|
||
|
||
<!-- 历史记录 -->
|
||
<view v-if="locationHistory.length > 0" class="history-section">
|
||
<view class="section-title">
|
||
<Clock size="20" color="#666" />
|
||
<text class="title-text">历史记录</text>
|
||
</view>
|
||
<view class="history-list">
|
||
<view v-for="(item, index) in locationHistory" :key="index" class="history-item">
|
||
<view class="history-time">{{ item.time }}</view>
|
||
<view class="history-coords">{{ item.latitude }}, {{ item.longitude }}</view>
|
||
<view class="history-actions">
|
||
<button class="mini-btn" @click="viewHistoryLocation(item)">查看</button>
|
||
<button class="mini-btn" @click="deleteHistoryItem(index)">删除</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- Toast 提示 -->
|
||
<nut-toast v-model:visible="showToast" :msg="toastMsg" />
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted } from 'vue'
|
||
import { Toast } from '@nutui/nutui-taro'
|
||
import { Location2, Location, Clock } from '@nutui/icons-vue'
|
||
import Taro from '@tarojs/taro'
|
||
import { checkLoginAndRedirect } from '../../utils/auth.js'
|
||
|
||
// 状态管理
|
||
const isLoading = ref(false)
|
||
const locationInfo = ref(null)
|
||
const addressInfo = ref(null)
|
||
const showToast = ref(false)
|
||
const toastMsg = ref('')
|
||
const locationHistory = ref([])
|
||
|
||
// 显示提示信息
|
||
const showMessage = (msg) => {
|
||
toastMsg.value = msg
|
||
showToast.value = true
|
||
}
|
||
|
||
// 检查位置权限
|
||
const checkLocationPermission = () => {
|
||
return new Promise((resolve) => {
|
||
Taro.getSetting({
|
||
success: (res) => {
|
||
if (res.authSetting['scope.userLocation']) {
|
||
resolve(true)
|
||
} else {
|
||
resolve(false)
|
||
}
|
||
},
|
||
fail: () => {
|
||
resolve(false)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
// 请求位置权限
|
||
const requestLocationPermission = () => {
|
||
return new Promise((resolve) => {
|
||
Taro.authorize({
|
||
scope: 'scope.userLocation',
|
||
success: () => {
|
||
resolve(true)
|
||
},
|
||
fail: () => {
|
||
resolve(false)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
// 打开位置设置页面
|
||
const openLocationSetting = () => {
|
||
Taro.openSetting({
|
||
success: (res) => {
|
||
if (res.authSetting['scope.userLocation']) {
|
||
showMessage('位置权限已开启')
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 获取当前位置
|
||
const getCurrentLocation = (options = {}) => {
|
||
return new Promise((resolve, reject) => {
|
||
const defaultOptions = {
|
||
type: 'wgs84',
|
||
altitude: true,
|
||
isHighAccuracy: true,
|
||
highAccuracyExpireTime: 4000,
|
||
success: (res) => {
|
||
console.log('获取位置成功:', res)
|
||
resolve(res)
|
||
},
|
||
fail: (error) => {
|
||
console.error('获取位置失败:', error)
|
||
reject(error)
|
||
}
|
||
}
|
||
|
||
Taro.getLocation({ ...defaultOptions, ...options })
|
||
})
|
||
}
|
||
|
||
// 获取位置信息(包含权限检查)
|
||
const getLocationWithPermission = async (options = {}) => {
|
||
try {
|
||
// 检查权限
|
||
const hasPermission = await checkLocationPermission()
|
||
|
||
if (!hasPermission) {
|
||
// 请求权限
|
||
const granted = await requestLocationPermission()
|
||
|
||
if (!granted) {
|
||
// 权限被拒绝,引导用户手动开启
|
||
Taro.showModal({
|
||
title: '位置权限',
|
||
content: '获取位置信息需要开启位置权限,请在设置中开启位置权限',
|
||
showCancel: true,
|
||
cancelText: '取消',
|
||
confirmText: '去设置',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
openLocationSetting()
|
||
}
|
||
}
|
||
})
|
||
throw new Error('位置权限被拒绝')
|
||
}
|
||
}
|
||
|
||
// 获取位置信息
|
||
const location = await getCurrentLocation(options)
|
||
return location
|
||
|
||
} catch (error) {
|
||
console.error('获取位置信息失败:', error)
|
||
throw error
|
||
}
|
||
}
|
||
|
||
// 处理获取位置
|
||
const handleGetLocation = async () => {
|
||
isLoading.value = true
|
||
|
||
try {
|
||
// 获取位置信息
|
||
const location = await getLocationWithPermission({
|
||
type: 'wgs84',
|
||
altitude: true,
|
||
isHighAccuracy: true
|
||
})
|
||
|
||
locationInfo.value = location
|
||
showMessage('获取位置成功')
|
||
|
||
// 保存到历史记录
|
||
const now = new Date()
|
||
const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
|
||
|
||
const historyItem = {
|
||
...location,
|
||
time: timeStr,
|
||
timestamp: now.getTime()
|
||
}
|
||
|
||
locationHistory.value.unshift(historyItem)
|
||
|
||
// 限制历史记录数量
|
||
if (locationHistory.value.length > 10) {
|
||
locationHistory.value = locationHistory.value.slice(0, 10)
|
||
}
|
||
|
||
// 保存到本地存储
|
||
Taro.setStorageSync('locationHistory', locationHistory.value)
|
||
|
||
} catch (error) {
|
||
console.error('获取位置失败:', error)
|
||
showMessage('获取位置失败:' + error.message)
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 在地图中查看位置
|
||
const openMap = () => {
|
||
if (!locationInfo.value) return
|
||
|
||
Taro.openLocation({
|
||
latitude: locationInfo.value.latitude,
|
||
longitude: locationInfo.value.longitude,
|
||
name: '当前位置',
|
||
address: addressInfo.value?.address || '未知地址',
|
||
scale: 18
|
||
})
|
||
}
|
||
|
||
// 复制坐标
|
||
const copyLocation = () => {
|
||
if (!locationInfo.value) return
|
||
|
||
const coordinate = `${locationInfo.value.latitude}, ${locationInfo.value.longitude}`
|
||
|
||
Taro.setClipboardData({
|
||
data: coordinate,
|
||
success: () => {
|
||
showMessage('坐标已复制到剪贴板')
|
||
},
|
||
fail: () => {
|
||
showMessage('复制失败')
|
||
}
|
||
})
|
||
}
|
||
|
||
// 分享位置
|
||
const shareLocation = () => {
|
||
if (!locationInfo.value) return
|
||
|
||
const shareText = `我的位置:${locationInfo.value.latitude}, ${locationInfo.value.longitude}`
|
||
|
||
Taro.showShareMenu({
|
||
withShareTicket: true,
|
||
success: () => {
|
||
showMessage('位置已分享')
|
||
},
|
||
fail: () => {
|
||
showMessage('分享失败')
|
||
}
|
||
})
|
||
}
|
||
|
||
// 查看历史位置
|
||
const viewHistoryLocation = (item) => {
|
||
locationInfo.value = item
|
||
showMessage('已加载历史位置')
|
||
}
|
||
|
||
// 删除历史记录项
|
||
const deleteHistoryItem = (index) => {
|
||
Taro.showModal({
|
||
title: '确认删除',
|
||
content: '确定要删除这条历史记录吗?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
locationHistory.value.splice(index, 1)
|
||
Taro.setStorageSync('locationHistory', locationHistory.value)
|
||
showMessage('删除成功')
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 页面加载时读取历史记录
|
||
onMounted(() => {
|
||
// 检查登录状态
|
||
if (!checkLoginAndRedirect()) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const history = Taro.getStorageSync('locationHistory')
|
||
if (history && Array.isArray(history)) {
|
||
locationHistory.value = history
|
||
}
|
||
} catch (error) {
|
||
console.error('读取历史记录失败:', error)
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style>
|
||
.vehicle-page {
|
||
min-height: 100vh;
|
||
background: #f5f5f5;
|
||
padding: 40rpx;
|
||
}
|
||
|
||
.header {
|
||
text-align: center;
|
||
margin-bottom: 60rpx;
|
||
}
|
||
|
||
.title {
|
||
font-size: 48rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
display: block;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.content {
|
||
background: white;
|
||
border-radius: 24rpx;
|
||
padding: 40rpx;
|
||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.get-location-btn {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
background: #667eea;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 16rpx;
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.get-location-btn:disabled {
|
||
background: #ccc;
|
||
color: #999;
|
||
}
|
||
|
||
.location-info,
|
||
.address-info {
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.info-card {
|
||
background: #f8f9fa;
|
||
border-radius: 16rpx;
|
||
padding: 30rpx;
|
||
}
|
||
|
||
.card-title {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.title-text {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-left: 12rpx;
|
||
}
|
||
|
||
.info-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20rpx 0;
|
||
border-bottom: 1rpx solid #e0e0e0;
|
||
}
|
||
|
||
.info-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.label {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.value {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.address-item {
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.address-text {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
margin-bottom: 40rpx;
|
||
}
|
||
|
||
.action-btn {
|
||
flex: 1;
|
||
height: 72rpx;
|
||
border: none;
|
||
border-radius: 12rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.action-btn.primary {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
|
||
.action-btn.secondary {
|
||
background: #f0f0f0;
|
||
color: #333;
|
||
}
|
||
|
||
.action-btn:active {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.history-section {
|
||
margin-top: 40rpx;
|
||
}
|
||
|
||
.section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.section-title .title-text {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
margin-left: 8rpx;
|
||
}
|
||
|
||
.history-list {
|
||
background: #f8f9fa;
|
||
border-radius: 16rpx;
|
||
padding: 20rpx;
|
||
}
|
||
|
||
.history-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20rpx 0;
|
||
border-bottom: 1rpx solid #e0e0e0;
|
||
}
|
||
|
||
.history-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.history-time {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
min-width: 100rpx;
|
||
}
|
||
|
||
.history-coords {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
flex: 1;
|
||
margin: 0 20rpx;
|
||
}
|
||
|
||
.history-actions {
|
||
display: flex;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.mini-btn {
|
||
padding: 8rpx 16rpx;
|
||
background: #667eea;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8rpx;
|
||
font-size: 22rpx;
|
||
}
|
||
|
||
.mini-btn:active {
|
||
opacity: 0.8;
|
||
}
|
||
</style>
|