1. 项目概述
在Vue3项目中获取经纬度信息是许多地理位置相关应用的基础功能。无论是外卖配送、共享单车、地图导航还是社交应用,准确获取用户位置坐标都是核心需求。不同于简单的调用浏览器API,实际开发中需要考虑兼容性、权限管理、错误处理等多方面因素。
最近在开发一个社区服务应用时,我发现很多开发者对获取经纬度的理解还停留在navigator.geolocation.getCurrentPosition()这个基础API层面。实际上,现代Web应用需要考虑更多细节:如何优雅处理用户拒绝授权?如何在移动端获取更高精度的定位?如何将获取的坐标转换为第三方地图服务需要的格式?
2. 核心需求解析
2.1 基础定位功能实现
在Vue3中获取经纬度的最基础实现确实只需要几行代码:
const getLocation = () => { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (position) => { console.log(position.coords.latitude, position.coords.longitude) }, (error) => { console.error(error.message) } ) } else { console.log('Geolocation is not supported by this browser.') } }但这段代码存在几个明显问题:
- 没有处理用户拒绝授权的情况
- 移动端定位精度不足
- 缺乏超时处理机制
- 没有考虑隐私合规要求
2.2 增强型定位方案
针对上述问题,我们需要构建更健壮的定位方案:
const getEnhancedLocation = (options = {}) => { return new Promise((resolve, reject) => { if (!navigator.geolocation) { reject(new Error('浏览器不支持地理位置API')) return } const defaultOptions = { enableHighAccuracy: true, // 启用高精度模式 timeout: 10000, // 10秒超时 maximumAge: 0 // 不使用缓存位置 } navigator.geolocation.getCurrentPosition( (position) => { const { latitude, longitude, accuracy } = position.coords if (accuracy > 100) { // 精度大于100米视为不准确 reject(new Error('获取的位置精度不足')) } else { resolve({ latitude, longitude }) } }, (error) => { let message = '获取位置失败' switch(error.code) { case error.PERMISSION_DENIED: message = '用户拒绝了位置请求' break case error.POSITION_UNAVAILABLE: message = '位置信息不可用' break case error.TIMEOUT: message = '请求位置超时' break } reject(new Error(message)) }, { ...defaultOptions, ...options } ) }) }3. Vue3中的最佳实践
3.1 使用Composition API封装
在Vue3中,我们可以利用Composition API创建可复用的定位逻辑:
// useGeolocation.js import { ref } from 'vue' export function useGeolocation(options = {}) { const coords = ref(null) const error = ref(null) const isLoading = ref(false) const getCurrentPosition = () => { isLoading.value = true error.value = null return new Promise((resolve, reject) => { if (!navigator.geolocation) { error.value = '浏览器不支持地理位置API' isLoading.value = false reject(error.value) return } navigator.geolocation.getCurrentPosition( (position) => { coords.value = { lat: position.coords.latitude, lng: position.coords.longitude, accuracy: position.coords.accuracy } isLoading.value = false resolve(coords.value) }, (err) => { error.value = getErrorMessage(err) isLoading.value = false reject(error.value) }, options ) }) } const getErrorMessage = (error) => { switch(error.code) { case error.PERMISSION_DENIED: return '用户拒绝了位置请求' case error.POSITION_UNAVAILABLE: return '位置信息不可用' case error.TIMEOUT: return '请求位置超时' default: return '未知错误' } } return { coords, error, isLoading, getCurrentPosition } }3.2 在组件中使用
<template> <div> <button @click="getLocation" :disabled="isLoading"> {{ isLoading ? '获取中...' : '获取当前位置' }} </button> <div v-if="coords"> <p>纬度: {{ coords.lat }}</p> <p>经度: {{ coords.lng }}</p> <p>精度: {{ coords.accuracy }}米</p> </div> <div v-if="error" class="error"> {{ error }} </div> </div> </template> <script setup> import { useGeolocation } from './useGeolocation' const { coords, error, isLoading, getCurrentPosition } = useGeolocation({ enableHighAccuracy: true, timeout: 10000 }) const getLocation = async () => { try { await getCurrentPosition() } catch (err) { console.error(err) } } </script>4. 高级应用场景
4.1 持续位置监听
对于需要持续更新位置的场景(如导航应用),可以使用watchPosition:
const watchLocation = (callback, options = {}) => { const defaultOptions = { enableHighAccuracy: true, maximumAge: 30000, timeout: 10000 } if (!navigator.geolocation) { throw new Error('浏览器不支持地理位置API') } return navigator.geolocation.watchPosition( (position) => { callback({ lat: position.coords.latitude, lng: position.coords.longitude, accuracy: position.coords.accuracy, heading: position.coords.heading, speed: position.coords.speed }) }, (error) => { console.error(getErrorMessage(error)) }, { ...defaultOptions, ...options } ) } // 停止监听 const clearWatch = (watchId) => { navigator.geolocation.clearWatch(watchId) }4.2 坐标转换与地图集成
获取到经纬度后,通常需要与地图服务(如百度地图、高德地图、Google Maps)集成:
// 百度地图BD09坐标转WGS84 const bd09ToWgs84 = (bdLat, bdLng) => { const x_pi = 3.14159265358979324 * 3000.0 / 180.0 const x = bdLng - 0.0065 const y = bdLat - 0.006 const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_pi) const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_pi) const wgsLng = z * Math.cos(theta) const wgsLat = z * Math.sin(theta) return { lat: wgsLat, lng: wgsLng } } // 高德地图GCJ02坐标转WGS84 const gcj02ToWgs84 = (gcjLat, gcjLng) => { const ee = 0.00669342162296594323 const a = 6378245.0 const transformLat = (x, y) => { let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x)) ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0 ret += (20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin(y / 3.0 * Math.PI)) * 2.0 / 3.0 ret += (160.0 * Math.sin(y / 12.0 * Math.PI) + 320 * Math.sin(y * Math.PI / 30.0)) * 2.0 / 3.0 return ret } const transformLng = (x, y) => { let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x)) ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0 ret += (20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin(x / 3.0 * Math.PI)) * 2.0 / 3.0 ret += (150.0 * Math.sin(x / 12.0 * Math.PI) + 300.0 * Math.sin(x / 30.0 * Math.PI)) * 2.0 / 3.0 return ret } let lat = +gcjLat let lng = +gcjLng let dLat = transformLat(lng - 105.0, lat - 35.0) let dLng = transformLng(lng - 105.0, lat - 35.0) const radLat = lat / 180.0 * Math.PI let magic = Math.sin(radLat) magic = 1 - ee * magic * magic const sqrtMagic = Math.sqrt(magic) dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * Math.PI) dLng = (dLng * 180.0) / (a / sqrtMagic * Math.cos(radLat) * Math.PI) return { lat: lat - dLat, lng: lng - dLng } }5. 常见问题与解决方案
5.1 移动端定位精度问题
问题现象:在移动设备上获取的定位精度经常超过100米,无法满足业务需求。
解决方案:
- 确保启用enableHighAccuracy选项
- 在Android上需要检查是否开启了高精度模式
- 考虑使用HTML5 Geolocation API结合IP定位提高精度
- 对于关键业务场景,可以提示用户开启GPS
const getHighAccuracyLocation = () => { return new Promise((resolve, reject) => { if (!navigator.geolocation) { reject(new Error('浏览器不支持地理位置API')) return } // 先尝试快速获取粗略位置 navigator.geolocation.getCurrentPosition( (roughPosition) => { // 如果精度已经足够好,直接返回 if (roughPosition.coords.accuracy <= 50) { resolve(roughPosition.coords) return } // 否则启动高精度定位 const watchId = navigator.geolocation.watchPosition( (position) => { if (position.coords.accuracy <= 30) { navigator.geolocation.clearWatch(watchId) resolve(position.coords) } }, (error) => { navigator.geolocation.clearWatch(watchId) reject(error) }, { enableHighAccuracy: true, maximumAge: 0, timeout: 20000 } ) // 设置超时 setTimeout(() => { navigator.geolocation.clearWatch(watchId) reject(new Error('高精度定位超时')) }, 30000) }, reject, { enableHighAccuracy: false, maximumAge: 60000, timeout: 5000 } ) }) }5.2 用户拒绝授权处理
问题现象:用户拒绝位置权限后,应用无法正常获取位置信息。
解决方案:
- 提供友好的错误提示
- 引导用户手动开启权限
- 提供备用方案(如手动输入地址)
<template> <div v-if="permissionDenied" class="permission-denied"> <h3>需要位置权限才能提供服务</h3> <p>您已拒绝位置权限请求,请手动开启:</p> <div v-if="isIOS"> <p>前往 设置 > 隐私 > 定位服务,找到本应用并设置为"使用App期间"</p> <button @click="openSettings">打开设置</button> </div> <div v-else-if="isAndroid"> <p>前往 设置 > 应用管理 > 找到本应用 > 权限管理,开启位置权限</p> <button @click="openAppSettings">打开应用设置</button> </div> <div v-else> <p>请在浏览器设置中启用位置权限</p> </div> <p>或者手动输入您的位置:</p> <input v-model="manualLocation" placeholder="输入地址或地名"> <button @click="useManualLocation">使用此位置</button> </div> </template> <script setup> import { ref, computed } from 'vue' const permissionDenied = ref(false) const manualLocation = ref('') const isIOS = computed(() => /iPad|iPhone|iPod/.test(navigator.userAgent)) const isAndroid = computed(() => /Android/.test(navigator.userAgent)) const openSettings = () => { if (isIOS.value) { // iOS无法直接跳转设置,只能提示用户手动操作 alert('请在设置中开启位置权限后重新打开应用') } } const openAppSettings = () => { if (window.cordova && cordova.plugins.settings) { cordova.plugins.settings.open('application_details', () => {}, () => {}) } else { alert('请在应用设置中开启位置权限') } } const useManualLocation = () => { // 调用地理编码API将地址转换为经纬度 console.log('使用手动输入的位置:', manualLocation.value) } </script>5.3 隐私合规要求
随着数据隐私法规的完善,位置信息获取需要特别注意合规要求:
- 明确告知用户:在获取位置前,必须清晰说明用途
- 最小必要原则:只获取业务必需的位置精度
- 数据安全:传输和存储位置信息需要加密
- 用户控制:提供随时关闭位置共享的选项
<template> <div v-if="showConsentDialog" class="consent-dialog"> <div class="dialog-content"> <h3>位置服务请求</h3> <p>我们需要获取您的位置信息以提供附近服务,我们承诺:</p> <ul> <li>仅在使用期间获取位置</li> <li>不会将位置信息用于其他用途</li> <li>您可以随时在设置中关闭位置权限</li> </ul> <div class="actions"> <button @click="acceptConsent">同意并继续</button> <button @click="denyConsent">拒绝</button> </div> </div> </div> </template> <script setup> import { ref, onMounted } from 'vue' const showConsentDialog = ref(false) // 检查是否已经同意过 const checkConsent = () => { const consented = localStorage.getItem('locationConsent') if (consented !== 'true') { showConsentDialog.value = true } } const acceptConsent = () => { localStorage.setItem('locationConsent', 'true') showConsentDialog.value = false // 开始获取位置 } const denyConsent = () => { localStorage.setItem('locationConsent', 'false') showConsentDialog.value = false // 使用备用方案 } onMounted(() => { checkConsent() }) </script>6. 性能优化与调试技巧
6.1 减少定位频率
频繁获取位置会消耗大量电量,特别是在移动设备上。应该根据业务需求合理设置定位频率:
// 节流定位请求 const throttledGetLocation = (() => { let lastPosition = null let lastTimestamp = 0 const cacheTime = 30000 // 30秒缓存 return async (forceUpdate = false) => { const now = Date.now() if (!forceUpdate && lastPosition && (now - lastTimestamp) < cacheTime) { return lastPosition } try { const position = await getEnhancedLocation() lastPosition = position lastTimestamp = now return position } catch (error) { if (lastPosition) { console.warn('获取新位置失败,返回缓存位置') return lastPosition } throw error } } })()6.2 调试位置信息
开发过程中可以使用浏览器工具模拟不同位置:
Chrome DevTools:
- 打开DevTools (F12)
- 转到"传感器"标签
- 模拟地理位置坐标
- 可以设置不同的精度值测试边界情况
Firefox:
- about:config 中设置 geo.provider.testing = true
- about:config 中设置 geo.provider.network.url = "data:application/json,{...}"
真实设备测试:
// 在开发环境中使用固定位置 if (process.env.NODE_ENV === 'development') { const mockPosition = { coords: { latitude: 39.9042, longitude: 116.4074, accuracy: 30 } } navigator.geolocation.getCurrentPosition = (success) => success(mockPosition) }
6.3 跨平台兼容性处理
不同平台和浏览器对Geolocation API的实现有差异:
const getUniversalLocation = async () => { // 处理微信内置浏览器 if (window.__wxjs_is_wkwebview === true) { try { const res = await new Promise((resolve, reject) => { wx.getLocation({ type: 'wgs84', success: resolve, fail: reject }) }) return { lat: res.latitude, lng: res.longitude, accuracy: res.accuracy || 50 } } catch (err) { console.error('微信获取位置失败:', err) throw new Error('微信获取位置失败') } } // 处理其他浏览器 try { const position = await getEnhancedLocation() return { lat: position.latitude, lng: position.longitude, accuracy: position.accuracy || 100 } } catch (err) { console.error('标准API获取位置失败:', err) // 备用方案:使用IP定位 try { const ipLocation = await fetch('https://ipapi.co/json/') .then(res => res.json()) return { lat: ipLocation.latitude, lng: ipLocation.longitude, accuracy: 5000, // IP定位精度很低 source: 'ip' } } catch (ipErr) { console.error('IP定位失败:', ipErr) throw new Error('所有定位方式均失败') } } }7. 与地图服务集成实战
7.1 百度地图集成示例
<template> <div class="map-container"> <div id="map-container" style="width:100%; height:400px"></div> <button @click="locateOnMap">在地图上显示我的位置</button> </div> </template> <script setup> import { onMounted, ref } from 'vue' const map = ref(null) onMounted(() => { // 百度地图API加载 const script = document.createElement('script') script.src = `https://api.map.baidu.com/api?v=3.0&ak=您的AK&callback=initMap` script.async = true window.initMap = () => { map.value = new BMap.Map('map-container') map.value.centerAndZoom(new BMap.Point(116.404, 39.915), 15) map.value.enableScrollWheelZoom() } document.head.appendChild(script) }) const locateOnMap = async () => { try { const { lat, lng } = await getEnhancedLocation() // 将WGS84坐标转换为百度坐标 const convertor = new BMap.Convertor() const pointArr = [new BMap.Point(lng, lat)] convertor.translate(pointArr, 1, 5, (data) => { if (data.status === 0) { const bdPoint = data.points[0] map.value.clearOverlays() map.value.panTo(bdPoint) const marker = new BMap.Marker(bdPoint) map.value.addOverlay(marker) const circle = new BMap.Circle(bdPoint, 100, { strokeColor: 'blue', strokeWeight: 2, strokeOpacity: 0.5, fillColor: '#BDE8FF', fillOpacity: 0.4 }) map.value.addOverlay(circle) } }) } catch (error) { console.error('定位失败:', error) alert(`定位失败: ${error.message}`) } } </script>7.2 高德地图集成示例
<template> <div class="map-container"> <div id="amap-container" style="width:100%; height:400px"></div> <button @click="locateOnAMap">在地图上显示我的位置</button> </div> </template> <script setup> import { onMounted, ref } from 'vue' const amap = ref(null) onMounted(() => { // 高德地图API加载 const script = document.createElement('script') script.src = `https://webapi.amap.com/maps?v=2.0&key=您的KEY&callback=initAMap` script.async = true window.initAMap = () => { amap.value = new AMap.Map('amap-container', { zoom: 15, center: [116.397428, 39.90923] }) } document.head.appendChild(script) }) const locateOnAMap = async () => { try { const { lat, lng } = await getEnhancedLocation() // 高德地图使用GCJ02坐标系,如果获取的是WGS84需要转换 const gcjPoint = gcj02ToWgs84(lat, lng) amap.value.clearMap() amap.value.setCenter([gcjPoint.lng, gcjPoint.lat]) new AMap.Marker({ position: [gcjPoint.lng, gcjPoint.lat], map: amap.value }) new AMap.Circle({ center: [gcjPoint.lng, gcjPoint.lat], radius: 100, //半径 strokeColor: "#0091ea", strokeOpacity: 0.8, strokeWeight: 2, fillColor: "#00b0ff", fillOpacity: 0.4, map: amap.value }) } catch (error) { console.error('定位失败:', error) alert(`定位失败: ${error.message}`) } } </script>7.3 使用第三方定位SDK
对于要求更高的应用,可以考虑使用专业定位SDK:
// 使用腾讯定位SDK const initTencentLocation = () => { const script = document.createElement('script') script.src = 'https://map.qq.com/api/gljs?v=1.exp&key=您的KEY&libraries=location' script.async = true script.onload = () => { const locationService = new qq.maps.LocationService({ complete: (result) => { console.log('腾讯定位结果:', result) } }) locationService.searchLocalCity() } document.head.appendChild(script) } // 使用高德定位SDK const initAMapLocation = () => { const script = document.createElement('script') script.src = 'https://webapi.amap.com/maps?v=2.0&key=您的KEY&plugin=AMap.Geolocation' script.async = true script.onload = () => { const geolocation = new AMap.Geolocation({ enableHighAccuracy: true, timeout: 10000, showMarker: false }) geolocation.getCurrentPosition((status, result) => { if (status === 'complete') { console.log('高德定位结果:', result) } else { console.error('高德定位失败:', result) } }) } document.head.appendChild(script) }8. 单元测试与质量保障
8.1 测试定位功能
为定位功能编写单元测试可以确保核心逻辑的可靠性:
// geolocation.test.js import { useGeolocation } from './useGeolocation' describe('useGeolocation', () => { beforeAll(() => { // 模拟navigator.geolocation global.navigator.geolocation = { getCurrentPosition: jest.fn() } }) it('should handle successful location fetch', async () => { const mockPosition = { coords: { latitude: 39.9042, longitude: 116.4074, accuracy: 30 } } navigator.geolocation.getCurrentPosition.mockImplementationOnce( (success) => success(mockPosition) ) const { coords, getCurrentPosition } = useGeolocation() await getCurrentPosition() expect(coords.value).toEqual({ lat: 39.9042, lng: 116.4074, accuracy: 30 }) }) it('should handle permission denied error', async () => { const mockError = { code: 1, message: 'User denied Geolocation' } navigator.geolocation.getCurrentPosition.mockImplementationOnce( (_, error) => error(mockError) ) const { error, getCurrentPosition } = useGeolocation() await expect(getCurrentPosition()).rejects.toThrow('用户拒绝了位置请求') expect(error.value).toBe('用户拒绝了位置请求') }) it('should fallback to IP location when geolocation fails', async () => { // 测试备用定位方案 }) })8.2 E2E测试定位流程
使用Cypress进行端到端测试:
// e2e/geolocation.spec.js describe('Geolocation Feature', () => { it('should display user location when permission granted', () => { cy.visit('/', { onBeforeLoad(win) { cy.stub(win.navigator.geolocation, 'getCurrentPosition').callsFake((cb) => { cb({ coords: { latitude: 37.7749, longitude: -122.4194, accuracy: 20 } }) }) } }) cy.get('button').contains('获取当前位置').click() cy.contains('纬度: 37.7749').should('exist') cy.contains('经度: -122.4194').should('exist') }) it('should show permission denied message', () => { cy.visit('/', { onBeforeLoad(win) { cy.stub(win.navigator.geolocation, 'getCurrentPosition').callsFake((_, err) => { err({ code: 1, message: 'User denied Geolocation' }) }) } }) cy.get('button').contains('获取当前位置').click() cy.contains('用户拒绝了位置请求').should('exist') }) })8.3 性能监控
监控定位功能的性能表现:
// 监控定位耗时 const trackGeolocationPerformance = async () => { const startTime = performance.now() try { await getEnhancedLocation() const duration = performance.now() - startTime // 上报性能数据 analytics.track('geolocation_performance', { duration, accuracy: 'high', success: true }) } catch (error) { const duration = performance.now() - startTime analytics.track('geolocation_performance', { duration, accuracy: 'unknown', success: false, error: error.message }) } } // 监控定位成功率 const monitorGeolocationSuccessRate = () => { let totalAttempts = 0 let successCount = 0 const originalGetLocation = navigator.geolocation.getCurrentPosition navigator.geolocation.getCurrentPosition = function(success, error, options) { totalAttempts++ const wrappedSuccess = (position) => { successCount++ success(position) } const wrappedError = (err) => { error(err) } originalGetLocation.call(navigator.geolocation, wrappedSuccess, wrappedError, options) } // 定期上报成功率 setInterval(() => { if (totalAttempts > 0) { const successRate = (successCount / totalAttempts) * 100 analytics.track('geolocation_success_rate', { successRate, totalAttempts }) // 重置计数器 totalAttempts = 0 successCount = 0 } }, 60000) // 每分钟上报一次 }9. 安全与隐私最佳实践
9.1 敏感数据处理
位置信息属于敏感个人数据,需要特别处理:
// 位置数据加密存储 const encryptLocationData = (location) => { const key = '您的加密密钥' const iv = crypto.getRandomValues(new Uint8Array(16)) const encoder = new TextEncoder() const data = encoder.encode(JSON.stringify(location)) return crypto.subtle.importKey( 'raw', encoder.encode(key), { name: 'AES-GCM' }, false, ['encrypt'] ).then((cryptoKey) => { return crypto.subtle.encrypt( { name: 'AES-GCM', iv: iv }, cryptoKey, data ) }).then((encrypted) => { return { iv: Array.from(iv).map(b => b.toString(16).padStart(2, '0')).join(''), data: Array.from(new Uint8Array(encrypted)).map(b => b.toString(16).padStart(2, '0')).join('') } }) } // 位置数据模糊处理 const obfuscateLocation = (lat, lng, radius = 500) => { // 在指定半径内随机偏移 const randomAngle = Math.random() * Math.PI * 2 const randomRadius = Math.sqrt(Math.random()) * radius const earthRadius = 6371000 // 地球半径(米) const dy = randomRadius * Math.sin(randomAngle) const dx = randomRadius * Math.cos(randomAngle) const newLat = lat + (dy / earthRadius) * (180 / Math.PI) const newLng = lng + (dx / earthRadius) * (180 / Math.PI) / Math.cos(lat * Math.PI/180) return { lat: parseFloat(newLat.toFixed(6)), lng: parseFloat(newLng.toFixed(6)), originalLat: lat, originalLng: lng, obfuscated: true } }9.2 权限生命周期管理
// 检查位置权限状态 const checkPermissionState = async () => { if (navigator.permissions) { try { const result = await navigator.permissions.query({ name: 'geolocation' }) return result.state } catch (err) { console.warn('Permission API not supported:', err) return 'unknown' } } return 'unknown' } // 监听权限变化 const watchPermissionChanges = (callback) => { if (navigator.permissions) { navigator.permissions.query({ name: 'geolocation' }).then((permissionStatus) => { permissionStatus.onchange = () => { callback(permissionStatus.state) } }) } } // 示例使用 watchPermissionChanges((state) => { console.log('位置权限状态变化:', state) if (state === 'granted') { // 权限被授予,可以获取位置 } else if (state === 'denied') { // 权限被拒绝,显示备用界面 } })9.3 GDPR合规处理
对于需要遵守GDPR的应用:
<template> <div v-if="showGDPRConsent" class="gdpr-consent"> <h3>数据使用同意</h3> <p>我们使用您的位置数据来提供个性化服务。根据GDPR要求,我们需要您的明确同意:</p> <div class="consent-options"> <label> <input type="checkbox" v-model="consent.location"> 同意使用我的精确位置(误差小于100米) </label> <label> <input type="checkbox" v-model="consent.analytics"> 同意匿名分析我的位置使用模式 </label> <label> <input type="checkbox" v-model="consent.storage"> 同意在本地存储我的位置历史(最多30天) </label> </div> <div class="actions"> <button @click="saveConsent">保存选择</button> <button @click="rejectAll">拒绝所有</button> </div> <p class="privacy-link"> <a @click="showPrivacyPolicy">查看隐私政策</a> </p> </div> </template> <script setup> import { ref, onMounted } from 'vue' const showGDPRConsent = ref(false) const consent = ref({ location: false, analytics: false, storage: false }) const checkGDPRConsent = () => { const savedConsent = localStorage.getItem('gdprLocationConsent') if (!savedConsent) { showGDPRConsent.value = true } else { consent.value = JSON.parse(savedConsent) } } const saveConsent = () => { localStorage.setItem