|
@@ -0,0 +1,300 @@
|
|
|
|
+<script setup lang="ts">
|
|
|
|
+import AMapLoader from '@amap/amap-jsapi-loader'
|
|
|
|
+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
|
|
|
|
+let placeSearchIns: any = null
|
|
|
|
+let autoCompleteIns: 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 = () => {
|
|
|
|
+ AMapLoader.load({
|
|
|
|
+ key: props.mapKey,
|
|
|
|
+ version: '2.0',
|
|
|
|
+ plugins: ['AMap.PlaceSearch', 'AMap.AutoComplete']
|
|
|
|
+ }).then((AMap: any) => {
|
|
|
|
+ // 获取POI检索实例
|
|
|
|
+ placeSearchIns = new AMap.PlaceSearch({
|
|
|
|
+ type: props.poiType,
|
|
|
|
+ pageSize: 20,
|
|
|
|
+ pageIndex: 1
|
|
|
|
+ })
|
|
|
|
+ // 获取输入建议实例
|
|
|
|
+ autoCompleteIns = new AMap.AutoComplete({
|
|
|
|
+ city: props.suggestionCity
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // const selected = getInitSelected()
|
|
|
|
+ mapIns = new AMap.Map(mapRef.value, {
|
|
|
|
+ zoom: props.zoom, // 初缩放级别
|
|
|
|
+ center: props.center, // 初始中心点
|
|
|
|
+ resizeEnable: true, // 监控地图容器尺寸变化
|
|
|
|
+ mapStyle: 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.setZoomAndCenter(zoom, [lng, lat])
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/* 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()
|
|
|
|
+ // searchPOI(lng, lat)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/* 检索附近兴趣点 */
|
|
|
|
+const searchNearBy = (lng: number, lat: number): Promise<any[]> => {
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
+ if (!placeSearchIns) {
|
|
|
|
+ reject(new Error('搜索组件加载失败,请重试'))
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ placeSearchIns.searchNearBy(props.poiKeywords, [lng, lat], props.poiRadius, (status: string, result: any) => {
|
|
|
|
+ if (status === 'complete' && result?.poiList?.pois) {
|
|
|
|
+ const ps = result.poiList.pois.filter((d: any) => !!d.location)
|
|
|
|
+ resolve((ps as any[]).map((d: any) => formatPoi(d)))
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ if (status === 'no_data') {
|
|
|
|
+ resolve([])
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ const msg = status + ' ' + (result ? JSON.stringify(result) : '')
|
|
|
|
+ reject(new Error(msg))
|
|
|
|
+ })
|
|
|
|
+ })
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 关键字检索
|
|
|
|
+const searchKeywords = (keyword: string): Promise<any[]> => {
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
+ if (!autoCompleteIns) {
|
|
|
|
+ reject(new Error('AutoComplete instance is null'))
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ autoCompleteIns.search(keyword, (status: any, result: any) => {
|
|
|
|
+ if (status === 'error') {
|
|
|
|
+ const msg = status + ' ' + (result ? JSON.stringify(result) : '')
|
|
|
|
+ reject(new Error(msg))
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ if (!result?.tips) {
|
|
|
|
+ resolve([])
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ const tips = (result.tips as any[]).filter(d => !!d.location)
|
|
|
|
+ resolve(tips.map(d => formatPoi(d)))
|
|
|
|
+ })
|
|
|
|
+ })
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/* 格式化返回的点位 */
|
|
|
|
+const formatPoi = (item: any) => {
|
|
|
|
+ return {
|
|
|
|
+ ...item,
|
|
|
|
+ lng: item.location.lng,
|
|
|
|
+ lat: item.location.lat
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+onBeforeUnmount(() => {
|
|
|
|
+ /* 销毁地图 */
|
|
|
|
+ mapIns && mapIns.destroy()
|
|
|
|
+ mapIns = null
|
|
|
|
+ placeSearchIns = null
|
|
|
|
+ autoCompleteIns = null
|
|
|
|
+ current.value = null
|
|
|
|
+ loading.value = false
|
|
|
|
+ poiData.value = []
|
|
|
|
+})
|
|
|
|
+</script>
|
|
|
|
+
|
|
|
|
+<template>
|
|
|
|
+ <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>
|