Files
microapp-vue3-interview/src/views/MapView.vue
2026-06-21 22:20:12 +08:00

333 lines
9.8 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="map-page">
<!-- 地图初始化加载/错误状态 -->
<div v-if="loading" class="map-status"> 地图加载中...</div>
<div v-else-if="error" class="map-status map-error"> {{ error }}</div>
<!-- 定位进行中提示 -->
<div v-if="locating" class="map-status map-locating">
{{ locatingSource === 'amap' ? '📡 基站/IP 定位中...' : '🛰 GPS 定位中...' }}
</div>
<!-- 地图容器 -->
<div
ref="containerRef"
class="map-container"
:class="{ 'map-hidden': loading || error }"
/>
<!-- 底部操作栏 -->
<div class="map-controls">
<span class="map-title">
🗺 高德地图
<span v-if="locatingSourceLabel" class="locate-badge">{{ locatingSourceLabel }}</span>
</span>
<div class="map-actions">
<button @click="resetView" :disabled="!mapInstance || locating">📍 回到默认位置</button>
<button
@click="locate"
:disabled="!mapInstance || locating"
class="btn-locate"
>
<span v-if="locating" class="spinner" />
{{ locating ? '定位中...' : '🎯 我的位置' }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'
import { useAmap, loadAMap } from '@/composables/useAmap'
const { containerRef, mapInstance, loading, error, initMap } = useAmap({
center: [116.397428, 39.90923], // 北京天安门
zoom: 12,
pitch: 30,
})
void containerRef
// ─────────────────────────────────────────
// 定位状态
// ─────────────────────────────────────────
const locating = ref(false)
const locatingSource = ref<'gps' | 'amap' | ''>('')
const locatingSourceLabel = ref('')
// ─────────────────────────────────────────
// 地图初始化
// ─────────────────────────────────────────
async function bootstrapMap() {
const map = await initMap()
if (map) {
nextTick(() => map.resize())
}
}
onMounted(() => bootstrapMap())
// ─────────────────────────────────────────
// 📍 回到默认视图
// ─────────────────────────────────────────
function resetView(): void {
locatingSourceLabel.value = ''
const map = mapInstance.value
if (!map) return
map.setCenter([116.397428, 39.90923])
map.setZoom(12)
}
// ─────────────────────────────────────────
// 🎯 我的位置 — 双通道定位
// ─────────────────────────────────────────
async function locate(): Promise<void> {
if (locating.value || !mapInstance.value) return
locating.value = true
// ── 通道 1浏览器原生 GPS高精度需 HTTPS / localhost──
const gpsResult = await tryBrowserGPS()
if (gpsResult) {
applyLocation(gpsResult.lng, gpsResult.lat, 16, '🛰️ GPS')
locating.value = false
return
}
// ── 通道 2高德 IP / 基站定位(低精度,但无需用户授权)──
const amapResult = await tryAmapGeolocation()
if (amapResult) {
applyLocation(amapResult.lng, amapResult.lat, 14, '📡 IP定位')
locating.value = false
return
}
// ── 通道 3完全失败 ──
locating.value = false
alert('无法获取您的位置,请检查浏览器定位权限或网络连接')
}
// ─────────────────────────────────────────
// 通道 1浏览器原生 Geolocation API
// ─────────────────────────────────────────
function tryBrowserGPS(): Promise<{ lng: number; lat: number } | null> {
return new Promise((resolve) => {
if (!navigator.geolocation) {
console.log('[定位] 浏览器不支持 Geolocation API降级到高德定位')
resolve(null)
return
}
locatingSource.value = 'gps'
console.log('[定位] 🛰️ 尝试浏览器 GPS 定位...')
navigator.geolocation.getCurrentPosition(
(pos) => {
const { longitude: lng, latitude: lat, accuracy } = pos.coords
console.log(`[定位] ✅ GPS 成功 — 经度:${lng} 纬度:${lat} 精度:${accuracy}`)
resolve({ lng, lat })
},
(err) => {
// GeolocationPositionError.code:
// 1 = PERMISSION_DENIED用户拒绝
// 2 = POSITION_UNAVAILABLE信号不可用
// 3 = TIMEOUT超时
const reason = ['', '用户拒绝授权', '信号不可用', '定位超时'][err.code] || err.message
console.log(`[定位] ❌ GPS 失败 (${reason}),降级到高德定位`)
resolve(null)
},
{
timeout: 8000, // 8 秒超时
enableHighAccuracy: true, // 强制启用 GPS 芯片
maximumAge: 60000, // 允许 60 秒内的缓存(避免每次冷启动 GPS
}
)
})
}
// ─────────────────────────────────────────
// 通道 2高德 IP / 基站定位
// ─────────────────────────────────────────
async function tryAmapGeolocation(): Promise<{ lng: number; lat: number } | null> {
try {
const AMap = await loadAMap()
locatingSource.value = 'amap'
console.log('[定位] 📡 尝试高德 IP 定位...')
return new Promise((resolve) => {
const geolocation = new AMap.Geolocation({
enableHighAccuracy: true, // 混合定位(优先 GPS → Wi-Fi → 基站 → IP
timeout: 6000,
panToLocation: false, // 我们自己控制地图移动
})
geolocation.getCurrentPosition((status, result) => {
if (status === 'complete' && result.position) {
const { lng, lat } = result.position
console.log(
`[定位] ✅ 高德定位成功 — 经度:${lng} 纬度:${lat}`
+ ` 地址:${result.formattedAddress} 精度:${result.accuracy}`
)
resolve({ lng, lat })
} else {
console.log(`[定位] ❌ 高德定位失败: ${result.message}`)
resolve(null)
}
})
})
} catch (err: any) {
console.log(`[定位] ❌ 高德定位插件加载失败: ${err.message}`)
return null
}
}
// ─────────────────────────────────────────
// 将定位结果应用到地图
// ─────────────────────────────────────────
function applyLocation(lng: number, lat: number, zoom: number, label: string): void {
const map = mapInstance.value
if (!map) return
map.setZoomAndCenter(zoom, [lng, lat])
locatingSourceLabel.value = label
// 3 秒后自动隐藏标签
setTimeout(() => {
if (locatingSourceLabel.value === label) {
locatingSourceLabel.value = ''
}
}, 3000)
}
</script>
<style scoped>
/* ============================================================
地图页面布局header(48px) | map(剩余) | controls(52px)
============================================================ */
.map-page {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
/* ---- 状态提示(加载/错误/定位中)---- */
.map-status {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
padding: 14px 24px;
text-align: center;
font-size: 14px;
color: #666;
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
pointer-events: none;
}
.map-error {
color: #e74c3c;
background: #fef2f2;
border: 1px solid #fecaca;
}
.map-locating {
color: #2d6cdf;
background: rgba(232, 240, 255, 0.95);
border: 1px solid #93c5fd;
}
/* ---- 地图容器 ---- */
.map-container {
flex: 1;
width: 100%;
min-height: 0;
}
.map-hidden {
visibility: hidden;
}
/* ---- 底部操作栏 ---- */
.map-controls {
display: flex;
align-items: center;
justify-content: space-between;
height: 52px;
padding: 0 20px;
background: #fff;
border-top: 1px solid #e8e8e8;
box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.04);
flex-shrink: 0;
z-index: 5;
}
.map-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 15px;
font-weight: 600;
color: #333;
}
/* 定位来源标签 */
.locate-badge {
font-size: 11px;
font-weight: 500;
padding: 2px 8px;
border-radius: 10px;
background: #e8f5e9;
color: #2e7d32;
}
.map-actions {
display: flex;
gap: 10px;
}
.map-actions button {
padding: 7px 18px;
border: 1px solid #42b883;
border-radius: 6px;
background: #fff;
color: #42b883;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 6px;
}
.map-actions button:hover:not(:disabled) {
background: #42b883;
color: #fff;
}
.map-actions button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* 定位按钮 loading 动画 */
.spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>