feat(iot): 迁移 ele 的 map 组件
parent
250d3eb39f
commit
f1f8f4e64a
|
|
@ -0,0 +1,3 @@
|
|||
export { default as MapDialog } from './src/map-dialog.vue';
|
||||
|
||||
export { loadBaiduMapSdk } from './src/utils';
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
<!-- 地图选择弹窗组件:基于百度地图 GL 实现 -->
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, reactive, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { ElButton, ElForm, ElFormItem, ElInput, ElOption, ElSelect } from 'element-plus';
|
||||
|
||||
import { loadBaiduMapSdk } from './utils';
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [
|
||||
data: {
|
||||
address: string;
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
},
|
||||
];
|
||||
}>();
|
||||
|
||||
const mapContainerRef = ref<HTMLElement>();
|
||||
const state = reactive({
|
||||
lonLat: '', // 经纬度字符串,格式为 "经度,纬度"
|
||||
address: '', // 地址信息
|
||||
loading: false, // 地址搜索加载状态
|
||||
latitude: '', // 纬度
|
||||
longitude: '', // 经度
|
||||
map: null as any, // 百度地图实例
|
||||
mapAddressOptions: [] as any[], // 地址搜索选项
|
||||
mapMarker: null as any, // 地图标记点
|
||||
geocoder: null as any, // 地理编码器实例
|
||||
mapContainerReady: false, // 地图容器是否准备好
|
||||
});
|
||||
|
||||
// 初始经纬度(打开弹窗时传入)
|
||||
const initLongitude = ref<number | undefined>();
|
||||
const initLatitude = ref<number | undefined>();
|
||||
|
||||
/** 弹窗打开动画完成后初始化地图 */
|
||||
async function handleDialogOpened() {
|
||||
// 先显示地图容器
|
||||
state.mapContainerReady = true;
|
||||
|
||||
// 等待下一个 DOM 更新周期,确保地图容器已渲染
|
||||
await nextTick();
|
||||
// 加载百度地图 SDK
|
||||
await loadBaiduMapSdk();
|
||||
initMapInstance();
|
||||
}
|
||||
|
||||
/** 弹窗关闭后清理地图 */
|
||||
function handleDialogClosed() {
|
||||
// 销毁地图实例
|
||||
if (state.map) {
|
||||
state.map.destroy?.();
|
||||
state.map = null;
|
||||
}
|
||||
state.mapMarker = null;
|
||||
state.geocoder = null;
|
||||
state.mapContainerReady = false;
|
||||
}
|
||||
|
||||
/** 初始化地图实例 */
|
||||
function initMapInstance() {
|
||||
if (!mapContainerRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化地图和地理编码器
|
||||
initMap();
|
||||
initGeocoder();
|
||||
|
||||
// 监听地图点击事件
|
||||
state.map.addEventListener('click', (e: any) => {
|
||||
const point = e.latlng;
|
||||
state.lonLat = `${point.lng},${point.lat}`;
|
||||
regeoCode(state.lonLat);
|
||||
});
|
||||
|
||||
// 如果有初始经纬度,加载标记点
|
||||
if (initLongitude.value && initLatitude.value) {
|
||||
const lonLat = `${initLongitude.value},${initLatitude.value}`;
|
||||
regeoCode(lonLat);
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化地图 */
|
||||
function initMap() {
|
||||
state.map = new window.BMapGL.Map(mapContainerRef.value);
|
||||
state.map.centerAndZoom(new window.BMapGL.Point(116.404, 39.915), 11);
|
||||
state.map.enableScrollWheelZoom();
|
||||
state.map.disableDoubleClickZoom();
|
||||
|
||||
state.map.addControl(new window.BMapGL.NavigationControl());
|
||||
state.map.addControl(new window.BMapGL.ScaleControl());
|
||||
state.map.addControl(new window.BMapGL.ZoomControl());
|
||||
}
|
||||
|
||||
/** 初始化地理编码器 */
|
||||
function initGeocoder() {
|
||||
state.geocoder = new window.BMapGL.Geocoder();
|
||||
}
|
||||
|
||||
/** 搜索地址 */
|
||||
function autoSearch(queryValue: string) {
|
||||
if (!queryValue) {
|
||||
state.mapAddressOptions = [];
|
||||
return;
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
const localSearch = new window.BMapGL.LocalSearch(state.map, {
|
||||
onSearchComplete: (results: any) => {
|
||||
state.loading = false;
|
||||
const temp: any[] = [];
|
||||
|
||||
if (results && results._pois) {
|
||||
results._pois.forEach((p: any) => {
|
||||
const point = p.point;
|
||||
if (point && point.lng && point.lat) {
|
||||
temp.push({
|
||||
name: p.title,
|
||||
value: `${point.lng},${point.lat}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
state.mapAddressOptions = temp;
|
||||
},
|
||||
});
|
||||
|
||||
localSearch.search(queryValue);
|
||||
}
|
||||
|
||||
/** 处理地址选择 */
|
||||
function handleAddressSelect(value: string) {
|
||||
if (value) {
|
||||
regeoCode(value);
|
||||
}
|
||||
}
|
||||
|
||||
/** 添加标记点 */
|
||||
function setMarker(lnglat: string[]) {
|
||||
if (!lnglat) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.mapMarker !== null) {
|
||||
state.map.removeOverlay(state.mapMarker);
|
||||
}
|
||||
|
||||
const point = new window.BMapGL.Point(lnglat[0], lnglat[1]);
|
||||
state.mapMarker = new window.BMapGL.Marker(point);
|
||||
|
||||
state.map.addOverlay(state.mapMarker);
|
||||
state.map.centerAndZoom(point, 16);
|
||||
}
|
||||
|
||||
/** 经纬度转地址、添加标记点 */
|
||||
function regeoCode(lonLat: string) {
|
||||
if (!lonLat) {
|
||||
return;
|
||||
}
|
||||
const lnglat = lonLat.split(',');
|
||||
if (lnglat.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.longitude = lnglat[0]!;
|
||||
state.latitude = lnglat[1]!;
|
||||
const point = new window.BMapGL.Point(lnglat[0], lnglat[1]);
|
||||
state.map.centerAndZoom(point, 16);
|
||||
|
||||
setMarker(lnglat);
|
||||
getAddress(lnglat);
|
||||
}
|
||||
|
||||
/** 根据经纬度获取地址信息 */
|
||||
function getAddress(lnglat: string[]) {
|
||||
const point = new window.BMapGL.Point(lnglat[0], lnglat[1]);
|
||||
state.geocoder.getLocation(point, (result: any) => {
|
||||
if (result && result.address) {
|
||||
state.address = result.address;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 确认选择 */
|
||||
function handleConfirm() {
|
||||
if (state.longitude && state.latitude) {
|
||||
emit('confirm', {
|
||||
longitude: state.longitude,
|
||||
latitude: state.latitude,
|
||||
address: state.address,
|
||||
});
|
||||
}
|
||||
modalApi.close();
|
||||
}
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (isOpen) {
|
||||
handleDialogOpened();
|
||||
} else {
|
||||
handleDialogClosed();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** 打开弹窗 */
|
||||
function open(longitude?: number, latitude?: number) {
|
||||
initLongitude.value = longitude;
|
||||
initLatitude.value = latitude;
|
||||
state.longitude = longitude ? String(longitude) : '';
|
||||
state.latitude = latitude ? String(latitude) : '';
|
||||
state.address = '';
|
||||
state.mapAddressOptions = [];
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal :footer="false" class="w-[700px]" title="百度地图">
|
||||
<div class="w-full">
|
||||
<!-- 第一行:位置搜索 -->
|
||||
<ElForm label-width="80px">
|
||||
<ElFormItem label="定位位置">
|
||||
<ElSelect
|
||||
v-model="state.address"
|
||||
:filter-method="autoSearch"
|
||||
:loading="state.loading"
|
||||
class="w-full"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="可输入地址查询经纬度"
|
||||
remote
|
||||
@change="handleAddressSelect"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in state.mapAddressOptions"
|
||||
:key="item.value"
|
||||
:label="item.name"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<!-- 第二行:坐标显示 -->
|
||||
<ElFormItem label="当前坐标">
|
||||
<div class="flex gap-2">
|
||||
<ElInput
|
||||
:model-value="state.longitude"
|
||||
disabled
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prepend>经度</template>
|
||||
</ElInput>
|
||||
<ElInput
|
||||
:model-value="state.latitude"
|
||||
disabled
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prepend>纬度</template>
|
||||
</ElInput>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<!-- 第三行:地图 -->
|
||||
<div
|
||||
v-if="state.mapContainerReady"
|
||||
ref="mapContainerRef"
|
||||
class="mt-[10px] h-[400px] w-full"
|
||||
></div>
|
||||
<div
|
||||
v-else
|
||||
class="mt-[10px] flex h-[400px] w-full items-center justify-center"
|
||||
>
|
||||
<span class="text-gray-400">地图加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<ElButton type="primary" @click="handleConfirm">确 定</ElButton>
|
||||
<ElButton @click="modalApi.close()">取 消</ElButton>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/* eslint-disable @typescript-eslint/no-dynamic-delete */
|
||||
/**
|
||||
* 百度地图 SDK 加载工具
|
||||
*/
|
||||
|
||||
// 扩展 Window 接口以包含百度地图 GL API
|
||||
declare global {
|
||||
interface Window {
|
||||
BMapGL: any;
|
||||
}
|
||||
}
|
||||
|
||||
// 全局回调名称
|
||||
const CALLBACK_NAME = '__BAIDU_MAP_LOAD_CALLBACK__';
|
||||
|
||||
// SDK 加载状态
|
||||
let loadPromise: null | Promise<void> = null;
|
||||
|
||||
/**
|
||||
* 加载百度地图 GL SDK
|
||||
* @param timeout 超时时间(毫秒),默认 10000
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export const loadBaiduMapSdk = (timeout = 10_000): Promise<void> => {
|
||||
// 已加载完成
|
||||
if (window.BMapGL) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// 正在加载中,返回同一个 Promise
|
||||
if (loadPromise) {
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
loadPromise = new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
loadPromise = null;
|
||||
reject(new Error('百度地图 SDK 加载超时'));
|
||||
}, timeout);
|
||||
|
||||
// 全局回调
|
||||
(window as any)[CALLBACK_NAME] = () => {
|
||||
clearTimeout(timeoutId);
|
||||
delete (window as any)[CALLBACK_NAME];
|
||||
resolve();
|
||||
};
|
||||
|
||||
// 创建 script 标签
|
||||
const script = document.createElement('script');
|
||||
script.src = `https://api.map.baidu.com/api?v=1.0&type=webgl&ak=${
|
||||
import.meta.env.VITE_BAIDU_MAP_KEY
|
||||
}&callback=${CALLBACK_NAME}`;
|
||||
script.addEventListener('onerror', () => {
|
||||
clearTimeout(timeoutId);
|
||||
loadPromise = null;
|
||||
delete (window as any)[CALLBACK_NAME];
|
||||
reject(new Error('百度地图 SDK 加载失败'));
|
||||
});
|
||||
document.body.append(script);
|
||||
});
|
||||
|
||||
return loadPromise;
|
||||
};
|
||||
Loading…
Reference in New Issue