| | |
| | | activeIndex: { type: Number, default: null }, |
| | | cityName: { type: String, default: '' }, |
| | | showSearch: { type: Boolean, default: true }, |
| | | directPlacement: { type: Boolean, default: false }, |
| | | address: { type: String, default: '' }, |
| | | searchLabel: { type: String, default: '' }, |
| | | compact: { type: Boolean, default: false }, |
| | | }) |
| | | const emit = defineEmits(['place', 'address-resolved']) |
| | |
| | | let hasFittedPoints = false |
| | | let suggestTimer |
| | | let suggestionRequestId = 0 |
| | | let placementRequestId = 0 |
| | | let skipNextSuggestion = false |
| | | let preferResolvedAddress = false |
| | | let lastSyncedAddress = '' |
| | | |
| | | const isMarking = computed(() => Number.isInteger(props.activeIndex)) |
| | | const placementIndex = computed(() => { |
| | | if (Number.isInteger(props.activeIndex)) return props.activeIndex |
| | | return props.directPlacement && props.points.length ? 0 : null |
| | | }) |
| | | const isMarking = computed(() => Number.isInteger(placementIndex.value)) |
| | | const hasPosition = (point) => ( |
| | | point?.longitude != null |
| | | && point?.latitude != null |
| | |
| | | if (!hasPosition(point)) return [] |
| | | const content = document.createElement('button') |
| | | content.type = 'button' |
| | | content.className = `amap-meeting-marker${props.activeIndex === index ? ' is-active' : ''}` |
| | | content.className = `amap-meeting-marker${placementIndex.value === index ? ' is-active' : ''}` |
| | | const markerLabel = document.createElement('span') |
| | | markerLabel.textContent = String(index + 1) |
| | | content.appendChild(markerLabel) |
| | |
| | | anchor: 'bottom-center', |
| | | content, |
| | | title: content.title, |
| | | zIndex: props.activeIndex === index ? 120 : 100, |
| | | zIndex: placementIndex.value === index ? 120 : 100, |
| | | }) |
| | | marker.on('click', () => map.setZoomAndCenter(17, marker.getPosition())) |
| | | return [marker] |
| | |
| | | } |
| | | |
| | | const focusCurrentPoint = () => { |
| | | const point = props.points[props.activeIndex] |
| | | const point = props.points[placementIndex.value] |
| | | if (map && hasPosition(point)) { |
| | | map.setZoomAndCenter(Math.max(map.getZoom(), 17), [Number(point.longitude), Number(point.latitude)]) |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | const handleMapClick = async (event) => { |
| | | const placeAtCoordinates = async (coordinates, fallbackAddress = '') => { |
| | | if (!isMarking.value) { |
| | | ElMessage.info('请先选择需要标注的汇合点') |
| | | return |
| | | } |
| | | const index = placementIndex.value |
| | | const requestId = ++placementRequestId |
| | | const address = await resolveAddress(coordinates) |
| | | if (requestId !== placementRequestId) return |
| | | preferResolvedAddress = true |
| | | emit('place', { |
| | | index, |
| | | ...coordinates, |
| | | address: address || fallbackAddress, |
| | | }) |
| | | } |
| | | |
| | | const handleMapClick = (event) => { |
| | | const coordinates = { |
| | | longitude: Number(event.lnglat.getLng().toFixed(6)), |
| | | latitude: Number(event.lnglat.getLat().toFixed(6)), |
| | | } |
| | | emit('place', { |
| | | index: props.activeIndex, |
| | | ...coordinates, |
| | | address: await resolveAddress(coordinates), |
| | | }) |
| | | placeAtCoordinates(coordinates) |
| | | } |
| | | |
| | | const getLocationCoordinates = (location) => { |
| | |
| | | return |
| | | } |
| | | map.setZoomAndCenter(17, [coordinates.longitude, coordinates.latitude]) |
| | | if (!isMarking.value) { |
| | | ElMessage.info('请先点击“标注汇合点”选择需要设置的汇合点') |
| | | return |
| | | } |
| | | emit('place', { |
| | | index: props.activeIndex, |
| | | ...coordinates, |
| | | address: await resolveAddress(coordinates), |
| | | }) |
| | | await placeAtCoordinates( |
| | | coordinates, |
| | | [poi.district, poi.address, poi.name].filter(Boolean).join(' · '), |
| | | ) |
| | | } |
| | | |
| | | const clearSearch = () => { |
| | | suggestionRequestId += 1 |
| | | window.clearTimeout(suggestTimer) |
| | | searchResults.value = [] |
| | | } |
| | | |
| | | const syncKeywordAddress = (address) => { |
| | | const value = typeof address === 'string' ? address.trim() : '' |
| | | if (!value) { |
| | | lastSyncedAddress = '' |
| | | return |
| | | } |
| | | if (preferResolvedAddress || !keyword.value.trim() || keyword.value === lastSyncedAddress) { |
| | | skipNextSuggestion = true |
| | | keyword.value = value |
| | | preferResolvedAddress = false |
| | | } |
| | | lastSyncedAddress = value |
| | | } |
| | | |
| | | onMounted(async () => { |
| | |
| | | updateSearchCity() |
| | | moveToCity() |
| | | }) |
| | | watch(() => props.address, syncKeywordAddress, { immediate: true }) |
| | | watch(keyword, loadSuggestions) |
| | | |
| | | onBeforeUnmount(() => { |
| | | window.clearTimeout(suggestTimer) |
| | | suggestionRequestId += 1 |
| | | placementRequestId += 1 |
| | | clearMarkers() |
| | | map?.destroy() |
| | | map = null |
| | |
| | | |
| | | <template> |
| | | <div class="amap-picker" :class="{ compact }"> |
| | | <div v-if="showSearch" class="map-search"> |
| | | <div v-if="showSearch" class="map-search" :class="{ 'has-label': searchLabel }"> |
| | | <span v-if="searchLabel" class="map-search-label">{{ searchLabel }}</span> |
| | | <el-input |
| | | v-model="keyword" |
| | | clearable |
| | | placeholder="搜索景区、地址或地标" |
| | | placeholder="搜索景区、地址或地标,选择后自动定位" |
| | | @keyup.enter="searchLocation" |
| | | @clear="clearSearch" |
| | | /> |
| | |
| | | <div v-loading="loading" class="map-shell" :class="{ marking: isMarking }"> |
| | | <div ref="mapContainer" class="map-container" /> |
| | | <div v-if="loadError" class="map-error">{{ loadError }}</div> |
| | | <span v-else-if="isMarking" class="marking-tip">点击地图设置“{{ points[activeIndex]?.pointName }}”</span> |
| | | <span v-else-if="isMarking" class="marking-tip">点击地图设置“{{ points[placementIndex]?.pointName }}”</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | <style scoped> |
| | | .amap-picker { width: min(100%, 900px); } |
| | | .amap-picker.compact { width: 100%; } |
| | | .map-search { position: relative; z-index: 10; display: grid; grid-template-columns: minmax(280px, 440px) auto; gap: 10px; width: fit-content; margin-bottom: 12px; } |
| | | .map-search { position: relative; z-index: 10; display: grid; grid-template-columns: minmax(280px, 440px) auto; gap: 10px; align-items: center; width: fit-content; margin-bottom: 12px; } |
| | | .compact .map-search { width: 100%; grid-template-columns: minmax(0, 1fr) 72px; } |
| | | .map-search.has-label { width: 100%; grid-template-columns: 92px minmax(0, 1fr) 72px; } |
| | | .map-search-label { color: #475467; font-size: 14px; font-weight: 500; white-space: nowrap; } |
| | | .search-results { position: absolute; top: 40px; left: 0; width: 440px; max-height: 280px; overflow-y: auto; background: white; border: 1px solid #d0d5dd; box-shadow: 0 8px 24px rgba(16, 24, 40, .14); } |
| | | .compact .search-results { width: calc(100% - 82px); } |
| | | .map-search.has-label .search-results { left: 102px; width: calc(100% - 184px); } |
| | | .search-results button { display: flex; flex-direction: column; gap: 3px; width: 100%; padding: 10px 12px; color: #344054; text-align: left; background: white; border: 0; border-bottom: 1px solid #eaecf0; cursor: pointer; } |
| | | .search-results button:hover { background: #f2f4f7; } |
| | | .search-results strong { font-size: 13px; } |