Files
opencloud/src/components/cloud/MapPickerModal.vue
T

213 lines
5.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
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.
<script setup lang="ts">
import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { NButton } from 'naive-ui'
import { loadAMap } from '@/lib/amap'
const props = withDefaults(defineProps<{
open: boolean
latitude: number | null
longitude: number | null
title?: string
description?: string
footerText?: string
}>(), {
title: '地图选点',
description: '点击地图即可回填经纬度,也可以继续手动修改。',
footerText: '建议点选大致拍摄位置,保存时会继续做模糊化处理。',
})
const emit = defineEmits<{
close: []
confirm: [payload: { latitude: number; longitude: number }]
}>()
const loading = ref(false)
const errorMsg = ref('')
const pickedLat = ref<number | null>(null)
const pickedLng = ref<number | null>(null)
const mapEl = ref<HTMLDivElement | null>(null)
let mapPickerAMap: typeof AMap | null = null
let mapPickerInstance: AMap.Map | null = null
let mapPickerMarker: AMap.Marker | null = null
function formatCoordinate(value: number | null) {
return value === null ? '未选择' : value.toFixed(6)
}
function destroyMapPicker() {
mapPickerMarker?.setMap(null)
mapPickerMarker = null
mapPickerInstance?.destroy()
mapPickerInstance = null
}
function placeMarker(longitude: number, latitude: number) {
if (!mapPickerAMap || !mapPickerInstance) return
if (!mapPickerMarker) {
mapPickerMarker = new mapPickerAMap.Marker({
position: [longitude, latitude],
title: '拍摄位置',
} as AMap.MarkerOptions)
mapPickerMarker.setMap(mapPickerInstance)
} else {
mapPickerMarker.setPosition([longitude, latitude])
}
}
function extractLngLat(event: unknown) {
const payload = event as { lnglat?: { lng?: number; lat?: number } } | undefined
const lng = payload?.lnglat?.lng
const lat = payload?.lnglat?.lat
if (typeof lng !== 'number' || typeof lat !== 'number') return null
return { lng, lat }
}
function handleMapClick(event: unknown) {
const picked = extractLngLat(event)
if (!picked) return
pickedLng.value = picked.lng
pickedLat.value = picked.lat
placeMarker(picked.lng, picked.lat)
}
async function initializeMap() {
loading.value = true
errorMsg.value = ''
pickedLat.value = props.latitude
pickedLng.value = props.longitude
await nextTick()
destroyMapPicker()
try {
mapPickerAMap = await loadAMap()
const hasCoordinates =
typeof props.latitude === 'number' &&
typeof props.longitude === 'number'
const center: [number, number] = hasCoordinates
? [props.longitude as number, props.latitude as number]
: [104.07, 30.67]
mapPickerInstance = new mapPickerAMap.Map(mapEl.value!, {
viewMode: '2D',
pitch: 0,
rotation: 0,
zoom: hasCoordinates ? 11 : 4,
center,
mapStyle: 'amap://styles/normal',
resizeEnable: true,
} as AMap.MapOptions)
mapPickerInstance.on('click', handleMapClick)
if (hasCoordinates) {
placeMarker(props.longitude as number, props.latitude as number)
}
} catch (error) {
errorMsg.value = error instanceof Error ? error.message : '地图加载失败'
} finally {
loading.value = false
}
}
function close() {
emit('close')
}
function confirm() {
if (pickedLat.value === null || pickedLng.value === null) return
emit('confirm', {
latitude: pickedLat.value,
longitude: pickedLng.value,
})
}
watch(() => props.open, open => {
if (open) {
initializeMap()
} else {
loading.value = false
errorMsg.value = ''
destroyMapPicker()
}
})
onBeforeUnmount(() => {
destroyMapPicker()
})
</script>
<template>
<Teleport to="body">
<div
v-if="open"
class="fixed inset-0 z-[140] flex items-center justify-center bg-slate-950/55 px-4 py-6"
@click="close"
>
<div class="w-full max-w-3xl overflow-hidden bg-white shadow-2xl" @click.stop>
<div class="flex items-start justify-between border-b border-slate-200 px-6 py-5">
<div>
<h3 class="text-xl font-semibold text-slate-900">{{ title }}</h3>
<p class="mt-1 text-sm text-slate-500">{{ description }}</p>
</div>
<button
type="button"
class="p-2 text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700"
@click="close"
>
✕
</button>
</div>
<div class="px-6 py-5">
<div class="mb-4 flex flex-wrap items-center gap-3 border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
<span>当前选择:</span>
<span class="font-medium text-slate-900">纬度 {{ formatCoordinate(pickedLat) }}</span>
<span class="font-medium text-slate-900">经度 {{ formatCoordinate(pickedLng) }}</span>
</div>
<div class="relative overflow-hidden border border-slate-200 bg-slate-100">
<div ref="mapEl" class="h-[420px] w-full"></div>
<div
v-if="loading"
class="absolute inset-0 flex items-center justify-center bg-white/80 text-sm font-medium text-slate-600"
>
正在加载地图...
</div>
<div
v-if="errorMsg"
class="absolute inset-0 flex items-center justify-center bg-white/90 px-6 text-center text-sm text-red-600"
>
{{ errorMsg }}
</div>
</div>
</div>
<div class="flex items-center justify-between border-t border-slate-200 px-6 py-4">
<p class="text-sm text-slate-500">{{ footerText }}</p>
<div class="flex gap-3">
<NButton secondary strong @click="close">取消</NButton>
<NButton
type="primary"
secondary
strong
:disabled="pickedLat === null || pickedLng === null"
@click="confirm"
>
使用这个位置
</NButton>
</div>
</div>
</div>
</div>
</Teleport>
</template>