| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | interface LocationSelection { |
| | | address?: string; |
| | | latitude: number; |
| | | locationName?: string; |
| | | longitude: number; |
| | | } |
| | | |
| | | interface Props { |
| | | disabled?: boolean; |
| | | latitude?: number; |
| | | longitude?: number; |
| | | radiusMeters?: number; |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | disabled: false, |
| | | latitude: undefined, |
| | | longitude: undefined, |
| | | radiusMeters: 10, |
| | | }); |
| | | |
| | | const emit = defineEmits<{ |
| | | select: [location: LocationSelection]; |
| | | }>(); |
| | | |
| | | const BAIDU_MAP_SCRIPT_ID = 'xgdl-baidu-map-jsapi'; |
| | | const BAIDU_MAP_CALLBACK = '__xgdlBaiduMapReady'; |
| | | const DEFAULT_BAIDU_CENTER: [number, number] = [87.625, 43.835]; |
| | | const PI = Math.PI; |
| | | const X_PI = PI * 3000 / 180; |
| | | const EARTH_SEMI_MAJOR_AXIS = 6_378_245; |
| | | const EARTH_ECCENTRICITY_SQUARED = 0.006_693_421_622_965_943; |
| | | |
| | | const mapContainer = ref<HTMLElement>(); |
| | | const loading = ref(false); |
| | | const searchKeyword = ref(''); |
| | | const selectedAddress = ref(''); |
| | | |
| | | let BMapGL: any; |
| | | let map: any; |
| | | let marker: any; |
| | | let rangeCircle: any; |
| | | let geocoder: any; |
| | | |
| | | const env = (import.meta as any).env ?? {}; |
| | | const baiduMapKey = String(env.VITE_BAIDU_MAP_KEY ?? '').trim(); |
| | | const hasMapConfig = computed(() => Boolean(baiduMapKey)); |
| | | |
| | | function outOfChina(longitude: number, latitude: number) { |
| | | return longitude < 72.004 || longitude > 137.8347 || latitude < 0.8293 || latitude > 55.8271; |
| | | } |
| | | |
| | | function transformLatitude(longitude: number, latitude: number) { |
| | | let result = -100 + 2 * longitude + 3 * latitude + 0.2 * latitude * latitude |
| | | + 0.1 * longitude * latitude + 0.2 * Math.sqrt(Math.abs(longitude)); |
| | | result += (20 * Math.sin(6 * longitude * PI) + 20 * Math.sin(2 * longitude * PI)) * 2 / 3; |
| | | result += (20 * Math.sin(latitude * PI) + 40 * Math.sin(latitude / 3 * PI)) * 2 / 3; |
| | | result += (160 * Math.sin(latitude / 12 * PI) + 320 * Math.sin(latitude * PI / 30)) * 2 / 3; |
| | | return result; |
| | | } |
| | | |
| | | function transformLongitude(longitude: number, latitude: number) { |
| | | let result = 300 + longitude + 2 * latitude + 0.1 * longitude * longitude |
| | | + 0.1 * longitude * latitude + 0.1 * Math.sqrt(Math.abs(longitude)); |
| | | result += (20 * Math.sin(6 * longitude * PI) + 20 * Math.sin(2 * longitude * PI)) * 2 / 3; |
| | | result += (20 * Math.sin(longitude * PI) + 40 * Math.sin(longitude / 3 * PI)) * 2 / 3; |
| | | result += (150 * Math.sin(longitude / 12 * PI) + 300 * Math.sin(longitude / 30 * PI)) * 2 / 3; |
| | | return result; |
| | | } |
| | | |
| | | function wgs84ToGcj02(longitude: number, latitude: number): [number, number] { |
| | | if (outOfChina(longitude, latitude)) { |
| | | return [longitude, latitude]; |
| | | } |
| | | let deltaLatitude = transformLatitude(longitude - 105, latitude - 35); |
| | | let deltaLongitude = transformLongitude(longitude - 105, latitude - 35); |
| | | const radianLatitude = latitude / 180 * PI; |
| | | let magic = Math.sin(radianLatitude); |
| | | magic = 1 - EARTH_ECCENTRICITY_SQUARED * magic * magic; |
| | | const sqrtMagic = Math.sqrt(magic); |
| | | deltaLatitude = deltaLatitude * 180 |
| | | / ((EARTH_SEMI_MAJOR_AXIS * (1 - EARTH_ECCENTRICITY_SQUARED)) / (magic * sqrtMagic) * PI); |
| | | deltaLongitude = deltaLongitude * 180 |
| | | / (EARTH_SEMI_MAJOR_AXIS / sqrtMagic * Math.cos(radianLatitude) * PI); |
| | | return [longitude + deltaLongitude, latitude + deltaLatitude]; |
| | | } |
| | | |
| | | function gcj02ToWgs84(longitude: number, latitude: number): [number, number] { |
| | | if (outOfChina(longitude, latitude)) { |
| | | return [longitude, latitude]; |
| | | } |
| | | const [gcjLongitude, gcjLatitude] = wgs84ToGcj02(longitude, latitude); |
| | | return [longitude * 2 - gcjLongitude, latitude * 2 - gcjLatitude]; |
| | | } |
| | | |
| | | function gcj02ToBd09(longitude: number, latitude: number): [number, number] { |
| | | const z = Math.sqrt(longitude * longitude + latitude * latitude) + 0.00002 * Math.sin(latitude * X_PI); |
| | | const theta = Math.atan2(latitude, longitude) + 0.000003 * Math.cos(longitude * X_PI); |
| | | return [z * Math.cos(theta) + 0.0065, z * Math.sin(theta) + 0.006]; |
| | | } |
| | | |
| | | function bd09ToGcj02(longitude: number, latitude: number): [number, number] { |
| | | const x = longitude - 0.0065; |
| | | const y = latitude - 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); |
| | | return [z * Math.cos(theta), z * Math.sin(theta)]; |
| | | } |
| | | |
| | | function wgs84ToBd09(longitude: number, latitude: number): [number, number] { |
| | | return gcj02ToBd09(...wgs84ToGcj02(longitude, latitude)); |
| | | } |
| | | |
| | | function bd09ToWgs84(longitude: number, latitude: number): [number, number] { |
| | | return gcj02ToWgs84(...bd09ToGcj02(longitude, latitude)); |
| | | } |
| | | |
| | | function loadBaiduMap() { |
| | | const mapWindow = window as any; |
| | | if (mapWindow.BMapGL) { |
| | | return Promise.resolve(mapWindow.BMapGL); |
| | | } |
| | | if (mapWindow.__xgdlBaiduMapPromise) { |
| | | return mapWindow.__xgdlBaiduMapPromise; |
| | | } |
| | | mapWindow.__xgdlBaiduMapPromise = new Promise((resolve, reject) => { |
| | | const timeout = window.setTimeout(() => reject(new Error('ç¾åº¦å°å¾å è½½è¶
æ¶ï¼è¯·æ£æ¥ AKãååç½åååç½ç»')), 15_000); |
| | | mapWindow[BAIDU_MAP_CALLBACK] = () => { |
| | | window.clearTimeout(timeout); |
| | | resolve(mapWindow.BMapGL); |
| | | delete mapWindow[BAIDU_MAP_CALLBACK]; |
| | | }; |
| | | if (document.querySelector(`#${BAIDU_MAP_SCRIPT_ID}`)) { |
| | | return; |
| | | } |
| | | const script = document.createElement('script'); |
| | | script.id = BAIDU_MAP_SCRIPT_ID; |
| | | script.src = `https://api.map.baidu.com/api?v=1.0&type=webgl&ak=${encodeURIComponent(baiduMapKey)}&callback=${BAIDU_MAP_CALLBACK}`; |
| | | script.addEventListener('error', () => { |
| | | window.clearTimeout(timeout); |
| | | reject(new Error('ç¾åº¦å°å¾å 载失败ï¼è¯·æ£æ¥ AKãååç½åååç½ç»')); |
| | | }, { once: true }); |
| | | document.head.append(script); |
| | | }); |
| | | return mapWindow.__xgdlBaiduMapPromise; |
| | | } |
| | | |
| | | function updateOverlay(longitude: number, latitude: number, centerMap = false) { |
| | | if (!map || !BMapGL) { |
| | | return; |
| | | } |
| | | const point = new BMapGL.Point(longitude, latitude); |
| | | if (!marker) { |
| | | marker = new BMapGL.Marker(point); |
| | | map.addOverlay(marker); |
| | | } else { |
| | | marker.setPosition(point); |
| | | } |
| | | if (!rangeCircle) { |
| | | rangeCircle = new BMapGL.Circle(point, props.radiusMeters, { |
| | | fillColor: '#1677ff', |
| | | fillOpacity: 0.16, |
| | | strokeColor: '#1677ff', |
| | | strokeOpacity: 0.8, |
| | | strokeWeight: 2, |
| | | }); |
| | | map.addOverlay(rangeCircle); |
| | | } else { |
| | | rangeCircle.setCenter(point); |
| | | rangeCircle.setRadius(props.radiusMeters); |
| | | } |
| | | if (centerMap) { |
| | | map.centerAndZoom(point, 19); |
| | | } |
| | | } |
| | | |
| | | function reverseGeocode(longitude: number, latitude: number) { |
| | | return new Promise<{ address?: string; name?: string }>((resolve) => { |
| | | if (!geocoder) { |
| | | resolve({}); |
| | | return; |
| | | } |
| | | geocoder.getLocation(new BMapGL.Point(longitude, latitude), (result: any) => { |
| | | const components = result?.addressComponents ?? {}; |
| | | const nearestPoi = result?.surroundingPois?.[0]?.title; |
| | | resolve({ |
| | | address: result?.address, |
| | | name: nearestPoi || components.street || components.district || components.city, |
| | | }); |
| | | }); |
| | | }); |
| | | } |
| | | |
| | | async function selectBaiduLocation(longitude: number, latitude: number, centerMap = true) { |
| | | updateOverlay(longitude, latitude, centerMap); |
| | | const [wgsLongitude, wgsLatitude] = bd09ToWgs84(longitude, latitude); |
| | | const location = await reverseGeocode(longitude, latitude); |
| | | selectedAddress.value = location.address ?? ''; |
| | | emit('select', { |
| | | address: location.address, |
| | | latitude: Number(wgsLatitude.toFixed(7)), |
| | | locationName: location.name, |
| | | longitude: Number(wgsLongitude.toFixed(7)), |
| | | }); |
| | | } |
| | | |
| | | async function handleSearch() { |
| | | const keyword = searchKeyword.value.trim(); |
| | | if (!keyword || !geocoder) { |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | const point = await new Promise<any>((resolve, reject) => { |
| | | geocoder.getPoint(keyword, (result: any) => { |
| | | result ? resolve(result) : reject(new Error('没ææç´¢å°è¯¥å°åï¼è¯·è¾å
¥æ´å®æ´ççå¸åºåå°ç¹åç§°')); |
| | | }); |
| | | }); |
| | | await selectBaiduLocation(point.lng, point.lat); |
| | | } catch (error: any) { |
| | | message.warning(error.message || 'å°åæç´¢å¤±è´¥'); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function handleCurrentLocation() { |
| | | if (!navigator.geolocation) { |
| | | message.warning('å½åæµè§å¨ä¸æ¯æå®ä½'); |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | navigator.geolocation.getCurrentPosition( |
| | | async (position) => { |
| | | const [longitude, latitude] = wgs84ToBd09(position.coords.longitude, position.coords.latitude); |
| | | await selectBaiduLocation(longitude, latitude); |
| | | loading.value = false; |
| | | }, |
| | | () => { |
| | | message.warning('å®ä½å¤±è´¥ï¼è¯·å¼å¯æµè§å¨å®ä½æéï¼é localhost ç¯å¢éä½¿ç¨ HTTPSï¼'); |
| | | loading.value = false; |
| | | }, |
| | | { enableHighAccuracy: true, maximumAge: 0, timeout: 15_000 }, |
| | | ); |
| | | } |
| | | |
| | | async function initializeMap() { |
| | | if (!hasMapConfig.value || !mapContainer.value) { |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | BMapGL = await loadBaiduMap(); |
| | | await nextTick(); |
| | | const hasCoordinates = Number.isFinite(props.longitude) && Number.isFinite(props.latitude); |
| | | const center = hasCoordinates |
| | | ? wgs84ToBd09(Number(props.longitude), Number(props.latitude)) |
| | | : DEFAULT_BAIDU_CENTER; |
| | | map = new BMapGL.Map(mapContainer.value); |
| | | map.centerAndZoom(new BMapGL.Point(center[0], center[1]), hasCoordinates ? 19 : 12); |
| | | map.enableScrollWheelZoom(true); |
| | | map.addControl(new BMapGL.ZoomControl()); |
| | | map.addControl(new BMapGL.ScaleControl()); |
| | | geocoder = new BMapGL.Geocoder(); |
| | | map.addEventListener('click', (event: any) => { |
| | | if (!props.disabled) { |
| | | void selectBaiduLocation(event.latlng.lng, event.latlng.lat, false); |
| | | } |
| | | }); |
| | | if (hasCoordinates) { |
| | | updateOverlay(center[0], center[1]); |
| | | const location = await reverseGeocode(center[0], center[1]); |
| | | selectedAddress.value = location.address ?? ''; |
| | | } |
| | | } catch (error: any) { |
| | | message.error(error.message || 'å°å¾åå§å失败'); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | watch( |
| | | () => [props.longitude, props.latitude] as const, |
| | | ([longitude, latitude]) => { |
| | | if (!map || !Number.isFinite(longitude) || !Number.isFinite(latitude)) { |
| | | return; |
| | | } |
| | | const point = wgs84ToBd09(Number(longitude), Number(latitude)); |
| | | updateOverlay(point[0], point[1]); |
| | | }, |
| | | ); |
| | | |
| | | watch(() => props.radiusMeters, (radius) => rangeCircle?.setRadius(radius)); |
| | | |
| | | onMounted(initializeMap); |
| | | |
| | | onBeforeUnmount(() => { |
| | | map?.clearOverlays(); |
| | | map = undefined; |
| | | marker = undefined; |
| | | rangeCircle = undefined; |
| | | geocoder = undefined; |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <a-alert |
| | | v-if="!hasMapConfig" |
| | | type="warning" |
| | | show-icon |
| | | message="ç¾åº¦å°å¾å°æªé
ç½®" |
| | | description="请é
ç½® VITE_BAIDU_MAP_KEY åéæ°æå»ºå端ã" |
| | | /> |
| | | <a-spin v-else :spinning="loading"> |
| | | <div v-if="!disabled" class="mb-2 flex gap-2"> |
| | | <a-input-search |
| | | v-model:value="searchKeyword" |
| | | enter-button="æç´¢å°å" |
| | | placeholder="è¾å
¥çãå¸ãåºåå
·ä½å°ç¹ï¼ä¾å¦ï¼ä¹é²æ¨é½å¸æ°é«åååº" |
| | | @search="handleSearch" |
| | | /> |
| | | <a-button @click="handleCurrentLocation">å®ä½å°å½åä½ç½®</a-button> |
| | | </div> |
| | | <div ref="mapContainer" class="h-[360px] w-full overflow-hidden rounded border border-gray-200"></div> |
| | | <div class="mt-2 flex flex-wrap justify-between gap-2 text-xs text-gray-500"> |
| | | <span>{{ disabled ? 'æå¡ä½ç½®åå
许èå´' : 'ç¹å»å°å¾éæ©æå¡ä¸å¿ç¹ï¼èè²åå表示å
许æå¡èå´' }}</span> |
| | | <span v-if="selectedAddress">{{ selectedAddress }}</span> |
| | | </div> |
| | | </a-spin> |
| | | </template> |