|
@@ -1,13 +1,286 @@
|
|
|
<script setup lang="ts">
|
|
|
+import { CircleCheck, Location, Plus, Search } from '@element-plus/icons-vue'
|
|
|
+import { ElMessage, type AutocompleteInstance } from 'element-plus'
|
|
|
+
|
|
|
+const ICON_CLASS = 'map-main-marker'
|
|
|
+
|
|
|
+const emits = defineEmits(['done'])
|
|
|
+
|
|
|
+const props = defineProps([
|
|
|
+ 'modelValue',
|
|
|
+ 'height',
|
|
|
+ 'center',
|
|
|
+ 'mapKey',
|
|
|
+ 'markerSrc',
|
|
|
+ 'zoom',
|
|
|
+ 'selectedZoom',
|
|
|
+ 'mapStyle',
|
|
|
+ 'poiType',
|
|
|
+ 'poiRadius',
|
|
|
+ 'poiKeywords',
|
|
|
+ 'okText',
|
|
|
+ 'required',
|
|
|
+ 'message',
|
|
|
+ 'suggestionCity'
|
|
|
+])
|
|
|
+
|
|
|
+let mapIns: any = null
|
|
|
+// 地图容器
|
|
|
+const mapRef = ref<any>()
|
|
|
+const autocompleteRef = ref<AutocompleteInstance>()
|
|
|
+
|
|
|
+// 是否是poi列表点击移动
|
|
|
+const isItemClickMove = ref<boolean>(false)
|
|
|
+// 图标样式
|
|
|
+const centerIconClass = ref<Array<string>>([ICON_CLASS])
|
|
|
+
|
|
|
+// 当前选中
|
|
|
+const current = ref<any>(null)
|
|
|
+
|
|
|
+// 是否加载中
|
|
|
+const loading = ref<boolean>(false)
|
|
|
+
|
|
|
+const poiData = ref<any[]>([])
|
|
|
+
|
|
|
+// 关键字
|
|
|
+const keywords = ref<string>('')
|
|
|
+// 最后一次搜索关键字
|
|
|
+const lastKeywords = ref<any>(null)
|
|
|
+
|
|
|
+// 关键字搜索数据
|
|
|
+const suggestionData = ref<any[]>([])
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
initMap()
|
|
|
})
|
|
|
/* 初始化地图 */
|
|
|
-const initMap = () => {}
|
|
|
+const initMap = () => {
|
|
|
+ mapIns = new BMapGL.Map(mapRef.value) // 创建Map实例
|
|
|
+ mapIns.centerAndZoom(new BMapGL.Point(props.center[0], props.center[1]), props.zoom) // 初始化地图,设置中心点坐标和地图级别
|
|
|
+ mapIns.enableScrollWheelZoom(true) //开启鼠标滚轮缩放
|
|
|
+ if (props.mapStyle) {
|
|
|
+ mapIns.setMapStyleV2({ styleId: props.mapStyle })
|
|
|
+ }
|
|
|
+
|
|
|
+ mapIns.on('moveend', () => {
|
|
|
+ if (isItemClickMove.value) {
|
|
|
+ isItemClickMove.value = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+ bounceIcon()
|
|
|
+ const { lng, lat } = mapIns.getCenter()
|
|
|
+ searchPOI(lng, lat)
|
|
|
+ })
|
|
|
+
|
|
|
+ // 第一次加载
|
|
|
+ searchPOI(props.center[0], props.center[1])
|
|
|
+}
|
|
|
+
|
|
|
+/* poi 搜索 */
|
|
|
+const searchPOI = (lng: number, lat: number) => {
|
|
|
+ loading.value = true
|
|
|
+ current.value = null
|
|
|
+ searchNearBy(lng, lat)
|
|
|
+ .then(res => {
|
|
|
+ loading.value = false
|
|
|
+ poiData.value = res
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ loading.value = false
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/* 设置地图中心点 */
|
|
|
+const setMapCenter = (lng: number, lat: number, zoom: number) => {
|
|
|
+ if (lng && lat) {
|
|
|
+ if (zoom == null) {
|
|
|
+ mapIns.setCenter([lng, lat])
|
|
|
+ } else {
|
|
|
+ mapIns.flyTo(new BMapGL.Point(lng, lat), zoom)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* poi 点击 */
|
|
|
+const handleItemClick = (item: any) => {
|
|
|
+ const { lng, lat } = item
|
|
|
+ setMapCenter(lng, lat, props.selectedZoom)
|
|
|
+ isItemClickMove.value = true
|
|
|
+ current.value = item
|
|
|
+}
|
|
|
+
|
|
|
+/* 让地图图标跳动 */
|
|
|
+const bounceIcon = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ centerIconClass.value = [ICON_CLASS]
|
|
|
+ setTimeout(() => {
|
|
|
+ centerIconClass.value = [ICON_CLASS, 'map-main-marker-bounce']
|
|
|
+ }, 0)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/* 确认选择 */
|
|
|
+const handleConfirm = () => {
|
|
|
+ // 是否强制选择
|
|
|
+ if (props.required && !current.value) {
|
|
|
+ ElMessage.error(props.message)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 如果右侧不选择,则返回当前中心点
|
|
|
+ const { lng, lat } = mapIns.getCenter()
|
|
|
+ emits('done', current.value || { lng, lat })
|
|
|
+}
|
|
|
+
|
|
|
+/* 关键字搜索 */
|
|
|
+const handleSelect = (keyword: string, callback?: any) => {
|
|
|
+ if (!keyword || lastKeywords.value === keyword) {
|
|
|
+ callback && callback(suggestionData.value)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ lastKeywords.value = keyword
|
|
|
+ searchKeywords(keyword)
|
|
|
+ .then(result => {
|
|
|
+ suggestionData.value = result
|
|
|
+ callback && callback(suggestionData.value)
|
|
|
+ })
|
|
|
+ .catch((e: Error) => {
|
|
|
+ console.error(e)
|
|
|
+ callback && callback(suggestionData.value)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/* 关键字搜索选中 */
|
|
|
+const handleSelectSelect = (item: any) => {
|
|
|
+ autocompleteRef.value && autocompleteRef.value.blur()
|
|
|
+ const { lng, lat } = item
|
|
|
+ setMapCenter(lng, lat, props.selectedZoom)
|
|
|
+ bounceIcon()
|
|
|
+}
|
|
|
+
|
|
|
+/* 检索附近兴趣点 */
|
|
|
+const searchNearBy = (lng: number, lat: number): Promise<any[]> => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ jsonp(
|
|
|
+ `https://api.map.baidu.com/place/v2/search?query=${props.poiKeywords}&location=${lat},${lng}&radius=${props.poiRadius}&page_size=20&output=json&ak=${props.mapKey}`,
|
|
|
+ (result: any) => {
|
|
|
+ if (result.status === 0) {
|
|
|
+ resolve(result.results.map((d: any) => formatPoi(d)))
|
|
|
+ } else {
|
|
|
+ reject(new Error(result.message))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ )
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 关键字检索
|
|
|
+const searchKeywords = (keyword: string): Promise<any[]> => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ jsonp(
|
|
|
+ `https://api.map.baidu.com/place/v2/suggestion?query=${keyword}®ion=${props.suggestionCity}&city_limit=true&output=json&ak=${props.mapKey}`,
|
|
|
+ (res: any) => {
|
|
|
+ if (res.status === 0) {
|
|
|
+ const tips = (res.result as any[]).filter(d => !!d.location)
|
|
|
+ resolve(tips.map(d => formatPoi(d)))
|
|
|
+ } else {
|
|
|
+ reject(new Error(res.message))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ )
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const jsonp = (url: string, callback: Function) => {
|
|
|
+ // 创建一个随机的回调函数名,避免命名冲突
|
|
|
+ const callbackFuncName = 'jsonp_callback_' + Math.round(100000 * Math.random())
|
|
|
+ // 将回调函数名添加到 URL 的查询参数中
|
|
|
+ url += (url.indexOf('?') === -1 ? '?' : '&') + 'callback=' + callbackFuncName
|
|
|
+ // 创建一个全局的回调函数
|
|
|
+ ;(window as any)[callbackFuncName] = function (data: any) {
|
|
|
+ // 回调函数执行后,删除全局函数并释放资源
|
|
|
+ delete (window as any)[callbackFuncName]
|
|
|
+ document.body.removeChild(script)
|
|
|
+
|
|
|
+ // 调用传入的回调函数处理数据
|
|
|
+ callback(data)
|
|
|
+ }
|
|
|
+ // 创建一个 script 标签
|
|
|
+ const script = document.createElement('script')
|
|
|
+ script.src = url
|
|
|
+
|
|
|
+ // 将 script 标签插入到页面中,发起 JSONP 请求
|
|
|
+ document.body.appendChild(script)
|
|
|
+}
|
|
|
+
|
|
|
+/* 格式化返回的点位 */
|
|
|
+const formatPoi = (item: any) => {
|
|
|
+ return {
|
|
|
+ ...item,
|
|
|
+ lng: item.location.lng,
|
|
|
+ lat: item.location.lat
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ /* 销毁地图 */
|
|
|
+ mapIns && mapIns.destroy()
|
|
|
+ mapIns = null
|
|
|
+ current.value = null
|
|
|
+ loading.value = false
|
|
|
+ poiData.value = []
|
|
|
+})
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
- <div></div>
|
|
|
+ <div class="map-container">
|
|
|
+ <div class="map-main">
|
|
|
+ <div ref="mapRef" :style="{ height }"></div>
|
|
|
+ <el-icon class="map-icon-plus" color="var(--el-color-primary)">
|
|
|
+ <Plus />
|
|
|
+ </el-icon>
|
|
|
+ <img :src="markerSrc" :class="centerIconClass" />
|
|
|
+ <el-autocomplete
|
|
|
+ v-model="keywords"
|
|
|
+ valueKey="name"
|
|
|
+ :clearable="true"
|
|
|
+ :fetch-suggestions="handleSelect"
|
|
|
+ placeholder="请输入关键字进行搜索"
|
|
|
+ @select="handleSelectSelect"
|
|
|
+ class="map-keywrod-input"
|
|
|
+ ref="autocompleteRef"
|
|
|
+ >
|
|
|
+ <template #prefix>
|
|
|
+ <el-icon class="el-input__icon">
|
|
|
+ <Search />
|
|
|
+ </el-icon>
|
|
|
+ </template>
|
|
|
+ </el-autocomplete>
|
|
|
+ </div>
|
|
|
+ <div class="map-poi-side">
|
|
|
+ <div class="map-poi-list" v-loading="loading">
|
|
|
+ <div class="map-poi-item" v-for="item in poiData" :key="item.id" @click="handleItemClick(item)">
|
|
|
+ <div class="map-poi-icon">
|
|
|
+ <el-icon size="18">
|
|
|
+ <Location />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="map-poi-desc">
|
|
|
+ <div class="map-poi-title">{{ item.name }}</div>
|
|
|
+ <div class="map-poi-address">{{ item.address }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="map-poi-icon map-poi-icon-checked">
|
|
|
+ <el-icon size="18" color="var(--el-color-primary)" v-if="current === item">
|
|
|
+ <CircleCheck />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="map-poi-button">
|
|
|
+ <el-button type="primary" @click="handleConfirm" style="width: 100%">{{ okText }}</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<style scoped lang="scss"></style>
|