Files
ks-app-employment-service/pages/complete-info/components/map-location-picker.vue
2025-10-21 22:58:47 +08:00

821 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<AppLayout title="选择地址" :showBack="true">
<view class="map-container">
<!-- 搜索框 -->
<view class="search-box">
<view class="search-input-wrapper">
<uni-icons type="search" size="20" color="#999"></uni-icons>
<input
class="search-input"
v-model="searchKeyword"
placeholder="输入关键词搜索地址(支持模糊搜索)"
@input="onSearchInput"
@confirm="searchLocation"
/>
<uni-icons
v-if="searchKeyword"
type="clear"
size="18"
color="#999"
@click="clearSearch"
></uni-icons>
</view>
</view>
<!-- 搜索结果列表 -->
<view class="search-results" v-if="showSearchResults">
<scroll-view scroll-y class="results-scroll" v-if="searchResults.length > 0">
<view
class="result-item"
v-for="(item, index) in searchResults"
:key="index"
@click="selectSearchResult(item)"
>
<view class="result-name">{{ item.name }}</view>
<view class="result-address">{{ item.address }}</view>
</view>
</scroll-view>
<view class="empty-results" v-else-if="isSearching">
<view class="loading-icon">
<uni-icons type="loop" size="40" color="#999"></uni-icons>
</view>
<text>搜索中...</text>
</view>
<view class="empty-results" v-else>
<uni-icons type="info" size="40" color="#999"></uni-icons>
<text>未找到相关地址请尝试其他关键词</text>
<view class="search-tips">
<text class="tip-title">搜索建议</text>
<text class="tip-item"> 输入具体地址名称</text>
<text class="tip-item"> 输入地标建筑名称</text>
<text class="tip-item"> 输入街道或区域名称</text>
</view>
</view>
</view>
<!-- 地图 -->
<view class="map-wrapper" v-show="!showSearchResults">
<!-- #ifdef H5 -->
<view id="amap-container" class="amap-container"></view>
<!-- #endif -->
<!-- #ifndef H5 -->
<map
id="map"
class="map"
:latitude="latitude"
:longitude="longitude"
:markers="markers"
:show-location="true"
@markertap="onMarkerTap"
@regionchange="onRegionChange"
@tap="onMapTap"
>
<cover-view class="map-center-marker">
<cover-image src="/static/icon/Location.png" class="marker-icon"></cover-image>
</cover-view>
</map>
<!-- #endif -->
</view>
<!-- 当前位置信息 -->
<view class="location-info" v-if="currentAddress && !showSearchResults">
<view class="info-title">当前选择位置</view>
<view class="info-name">{{ currentAddress.name }}</view>
<view class="info-address">{{ currentAddress.address }}</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-actions">
<button class="locate-btn" @click="getCurrentLocation" :disabled="isLocating">
<uni-icons type="location-filled" size="20" color="#256BFA"></uni-icons>
<text>{{ isLocating ? '定位中...' : '定位' }}</text>
</button>
<button class="confirm-btn" @click="confirmLocation">确认选择</button>
</view>
</view>
</AppLayout>
</template>
<script setup>
import { ref, inject, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
const { $api } = inject('globalFunction')
// 搜索相关
const searchKeyword = ref('')
const searchResults = ref([])
const showSearchResults = ref(false)
const isSearching = ref(false)
const isLocating = ref(false)
let searchTimer = null
// 地图相关
const latitude = ref(36.066938)
const longitude = ref(120.382665)
const markers = ref([])
const currentAddress = ref(null)
// H5地图实例
let map = null
let AMap = null
let geocoder = null
let placeSearch = null
onLoad((options) => {
// 可以接收初始位置参数
if (options.latitude && options.longitude) {
latitude.value = parseFloat(options.latitude)
longitude.value = parseFloat(options.longitude)
}
})
onMounted(() => {
// #ifdef H5
initAmapH5()
// #endif
// #ifndef H5
// 先设置默认位置,避免地图显示空白
markers.value = [{
id: 1,
latitude: latitude.value,
longitude: longitude.value,
iconPath: '/static/icon/Location.png',
width: 30,
height: 30
}]
// 延迟执行定位,避免页面加载时立即定位失败
setTimeout(() => {
getCurrentLocation()
}, 1000)
// #endif
})
// H5端初始化高德地图
const initAmapH5 = () => {
// #ifdef H5
if (window.AMap) {
AMap = window.AMap
initMap()
} else {
const script = document.createElement('script')
script.src = 'https://webapi.amap.com/maps?v=2.0&key=9cfc9370bd8a941951da1cea0308e9e3&plugin=AMap.Geocoder,AMap.PlaceSearch'
script.onload = () => {
AMap = window.AMap
initMap()
}
document.head.appendChild(script)
}
// #endif
}
// 初始化地图
const initMap = () => {
// #ifdef H5
map = new AMap.Map('amap-container', {
zoom: 15,
center: [longitude.value, latitude.value],
resizeEnable: true
})
// 创建标记
const marker = new AMap.Marker({
position: [longitude.value, latitude.value],
draggable: true
})
marker.on('dragend', (e) => {
const position = e.target.getPosition()
longitude.value = position.lng
latitude.value = position.lat
reverseGeocode(position.lng, position.lat)
})
map.add(marker)
// 初始化地理编码
geocoder = new AMap.Geocoder({
city: '全国'
})
// 初始化地点搜索
placeSearch = new AMap.PlaceSearch({
city: '全国',
pageSize: 10
})
// 地图点击事件
map.on('click', (e) => {
const { lng, lat } = e.lnglat
longitude.value = lng
latitude.value = lat
marker.setPosition([lng, lat])
reverseGeocode(lng, lat)
})
// 获取当前位置信息
reverseGeocode(longitude.value, latitude.value)
// #endif
}
// 搜索输入
const onSearchInput = () => {
if (searchTimer) {
clearTimeout(searchTimer)
}
if (!searchKeyword.value.trim()) {
showSearchResults.value = false
searchResults.value = []
isSearching.value = false
return
}
showSearchResults.value = true
isSearching.value = true
searchTimer = setTimeout(() => {
if (searchKeyword.value.trim()) {
searchLocation()
}
}, 300) // 优化防抖时间从500ms改为300ms
}
// 搜索地点
const searchLocation = () => {
if (!searchKeyword.value.trim()) {
return
}
showSearchResults.value = true
isSearching.value = true
// #ifdef H5
if (placeSearch) {
placeSearch.search(searchKeyword.value, (status, result) => {
isSearching.value = false
if (status === 'complete' && result.poiList) {
searchResults.value = result.poiList.pois.map(poi => ({
name: poi.name,
address: poi.address || poi.pname + poi.cityname + poi.adname,
location: poi.location,
lng: poi.location.lng,
lat: poi.location.lat
}))
} else {
searchResults.value = []
}
})
}
// #endif
// #ifndef H5
// 小程序端使用uni.request调用高德API
uni.request({
url: 'https://restapi.amap.com/v3/place/text',
data: {
key: '9cfc9370bd8a941951da1cea0308e9e3',
keywords: searchKeyword.value,
city: '全国',
offset: 20,
citylimit: false, // 不限制城市,支持全国搜索
extensions: 'all' // 返回详细信息
},
success: (res) => {
isSearching.value = false
console.log('搜索响应:', res.data) // 调试日志
if (res.data.status === '1' && res.data.pois && res.data.pois.length > 0) {
searchResults.value = res.data.pois.map(poi => {
const [lng, lat] = poi.location.split(',')
return {
name: poi.name,
address: poi.address || `${poi.pname || ''}${poi.cityname || ''}${poi.adname || ''}`,
lng: parseFloat(lng),
lat: parseFloat(lat)
}
})
console.log('搜索结果:', searchResults.value) // 调试日志
} else {
// 如果第一次搜索没有结果,尝试更宽泛的搜索
if (searchKeyword.value.length > 2) {
tryAlternativeSearch()
} else {
searchResults.value = []
console.log('搜索无结果:', res.data) // 调试日志
}
}
},
fail: (err) => {
isSearching.value = false
searchResults.value = []
console.error('搜索请求失败:', err) // 调试日志
$api.msg('搜索失败,请检查网络连接')
}
})
// #endif
}
// 备用搜索策略
const tryAlternativeSearch = () => {
// 尝试使用地理编码API搜索
uni.request({
url: 'https://restapi.amap.com/v3/geocode/geo',
data: {
key: '9cfc9370bd8a941951da1cea0308e9e3',
address: searchKeyword.value,
city: '全国'
},
success: (res) => {
isSearching.value = false
console.log('备用搜索响应:', res.data) // 调试日志
if (res.data.status === '1' && res.data.geocodes && res.data.geocodes.length > 0) {
searchResults.value = res.data.geocodes.map(geo => {
const [lng, lat] = geo.location.split(',')
return {
name: geo.formatted_address,
address: geo.formatted_address,
lng: parseFloat(lng),
lat: parseFloat(lat)
}
})
console.log('备用搜索结果:', searchResults.value) // 调试日志
} else {
searchResults.value = []
console.log('备用搜索也无结果:', res.data) // 调试日志
}
},
fail: (err) => {
isSearching.value = false
searchResults.value = []
console.error('备用搜索失败:', err) // 调试日志
}
})
}
// 选择搜索结果
const selectSearchResult = (item) => {
longitude.value = item.lng
latitude.value = item.lat
currentAddress.value = {
name: item.name,
address: item.address,
longitude: item.lng,
latitude: item.lat
}
// #ifdef H5
if (map) {
map.setCenter([item.lng, item.lat])
const marker = map.getAllOverlays('marker')[0]
if (marker) {
marker.setPosition([item.lng, item.lat])
}
}
// #endif
// #ifndef H5
markers.value = [{
id: 1,
latitude: item.lat,
longitude: item.lng,
iconPath: '/static/icon/Location.png',
width: 30,
height: 30
}]
// #endif
showSearchResults.value = false
searchKeyword.value = ''
}
// 清除搜索
const clearSearch = () => {
searchKeyword.value = ''
searchResults.value = []
showSearchResults.value = false
isSearching.value = false
if (searchTimer) {
clearTimeout(searchTimer)
}
}
// 逆地理编码(根据坐标获取地址)
const reverseGeocode = (lng, lat) => {
// #ifdef H5
if (geocoder) {
geocoder.getAddress([lng, lat], (status, result) => {
if (status === 'complete' && result.regeocode) {
const addressComponent = result.regeocode.addressComponent
const formattedAddress = result.regeocode.formattedAddress
currentAddress.value = {
name: addressComponent.building || addressComponent.township,
address: formattedAddress,
longitude: lng,
latitude: lat
}
}
})
}
// #endif
// #ifndef H5
uni.request({
url: 'https://restapi.amap.com/v3/geocode/regeo',
data: {
key: '9cfc9370bd8a941951da1cea0308e9e3',
location: `${lng},${lat}`
},
success: (res) => {
if (res.data.status === '1' && res.data.regeocode) {
const addressComponent = res.data.regeocode.addressComponent
const formattedAddress = res.data.regeocode.formatted_address
currentAddress.value = {
name: addressComponent.building || addressComponent.township || '选择的位置',
address: formattedAddress,
longitude: lng,
latitude: lat
}
}
}
})
// #endif
}
// 获取当前定位
const getCurrentLocation = () => {
if (isLocating.value) return // 防止重复定位
isLocating.value = true
uni.showLoading({ title: '定位中...' })
// 先检查定位权限
uni.getSetting({
success: (settingRes) => {
if (settingRes.authSetting['scope.userLocation'] === false) {
// 用户拒绝了定位权限,引导用户开启
isLocating.value = false
uni.hideLoading()
uni.showModal({
title: '定位权限',
content: '需要获取您的位置信息来提供更好的服务,请在设置中开启定位权限',
confirmText: '去设置',
success: (modalRes) => {
if (modalRes.confirm) {
uni.openSetting()
}
}
})
return
}
// 执行定位
uni.getLocation({
type: 'gcj02',
altitude: false,
success: (res) => {
console.log('定位成功:', res) // 调试日志
longitude.value = res.longitude
latitude.value = res.latitude
// #ifdef H5
if (map) {
map.setCenter([res.longitude, res.latitude])
const marker = map.getAllOverlays('marker')[0]
if (marker) {
marker.setPosition([res.longitude, res.latitude])
}
}
// #endif
// #ifndef H5
// 更新小程序端标记
markers.value = [{
id: 1,
latitude: res.latitude,
longitude: res.longitude,
iconPath: '/static/icon/Location.png',
width: 30,
height: 30
}]
// #endif
reverseGeocode(res.longitude, res.latitude)
uni.hideLoading()
isLocating.value = false
},
fail: (err) => {
console.error('定位失败:', err) // 调试日志
uni.hideLoading()
isLocating.value = false
// 根据错误类型给出不同提示
let errorMsg = '定位失败'
if (err.errMsg.includes('auth deny')) {
errorMsg = '定位权限被拒绝,请在设置中开启'
} else if (err.errMsg.includes('timeout')) {
errorMsg = '定位超时,请重试'
} else if (err.errMsg.includes('network')) {
errorMsg = '网络异常,请检查网络连接'
}
uni.showModal({
title: '定位失败',
content: errorMsg + ',是否使用默认位置?',
confirmText: '使用默认位置',
cancelText: '重试',
success: (modalRes) => {
if (modalRes.confirm) {
// 使用默认位置(北京)
longitude.value = 116.397428
latitude.value = 39.90923
reverseGeocode(longitude.value, latitude.value)
} else {
// 重试定位
setTimeout(() => {
getCurrentLocation()
}, 2000)
}
}
})
}
})
},
fail: () => {
uni.hideLoading()
isLocating.value = false
$api.msg('无法获取定位权限设置')
}
})
}
// 地图区域变化(小程序端)
const onRegionChange = (e) => {
// #ifndef H5
// 只有在用户手动拖动地图结束时才更新位置
if (e.type === 'end' && e.causedBy === 'drag') {
const mapContext = uni.createMapContext('map')
mapContext.getCenterLocation({
success: (res) => {
longitude.value = res.longitude
latitude.value = res.latitude
reverseGeocode(res.longitude, res.latitude)
}
})
}
// #endif
}
// 地图点击事件(小程序端)
const onMapTap = (e) => {
// #ifndef H5
const { latitude: lat, longitude: lng } = e.detail
longitude.value = lng
latitude.value = lat
// 更新标记
markers.value = [{
id: 1,
latitude: lat,
longitude: lng,
iconPath: '/static/icon/Location.png',
width: 30,
height: 30
}]
reverseGeocode(lng, lat)
// #endif
}
// 确认选择
const confirmLocation = () => {
if (!currentAddress.value) {
$api.msg('请选择地址')
return
}
// 返回上一页并传递数据
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
if (prevPage) {
prevPage.$vm.handleLocationSelected({
address: currentAddress.value.address,
name: currentAddress.value.name,
longitude: currentAddress.value.longitude,
latitude: currentAddress.value.latitude
})
}
uni.navigateBack()
}
const onMarkerTap = (e) => {
console.log('marker点击', e)
}
</script>
<style lang="stylus" scoped>
.map-container
width: 100%
height: 100vh
position: relative
display: flex
flex-direction: column
.search-box
position: absolute
top: 20rpx
left: 32rpx
right: 32rpx
z-index: 10
.search-input-wrapper
background: #fff
border-radius: 40rpx
padding: 20rpx 30rpx
display: flex
align-items: center
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1)
.search-input
flex: 1
margin: 0 20rpx
font-size: 28rpx
uni-icons
flex-shrink: 0
.search-results
position: absolute
top: 100rpx
left: 32rpx
right: 32rpx
bottom: 180rpx
background: #fff
border-radius: 20rpx
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1)
z-index: 9
overflow: hidden
.results-scroll
height: 100%
.result-item
padding: 30rpx
border-bottom: 1rpx solid #f0f0f0
&:active
background: #f5f5f5
.result-name
font-size: 32rpx
color: #333
font-weight: 500
margin-bottom: 10rpx
.result-address
font-size: 26rpx
color: #999
.empty-results
height: 100%
display: flex
flex-direction: column
align-items: center
justify-content: center
color: #999
font-size: 28rpx
.loading-icon
animation: rotate 1s linear infinite
margin-bottom: 20rpx
uni-icons
margin-bottom: 20rpx
text
padding: 0 60rpx
text-align: center
line-height: 1.5
.search-tips
margin-top: 40rpx
padding: 0 40rpx
.tip-title
font-size: 26rpx
color: #666
font-weight: 500
margin-bottom: 20rpx
display: block
.tip-item
font-size: 24rpx
color: #999
line-height: 1.8
display: block
margin-bottom: 8rpx
@keyframes rotate
from
transform: rotate(0deg)
to
transform: rotate(360deg)
.map-wrapper
flex: 1
position: relative
.map, .amap-container
width: 100%
height: 100%
.map-center-marker
position: absolute
left: 50%
top: 50%
transform: translate(-50%, -100%)
z-index: 5
.marker-icon
width: 60rpx
height: 80rpx
.location-info
position: absolute
bottom: 180rpx
left: 32rpx
right: 32rpx
background: #fff
border-radius: 20rpx
padding: 30rpx
box-shadow: 0 -4rpx 12rpx rgba(0,0,0,0.1)
z-index: 10
.info-title
font-size: 24rpx
color: #999
margin-bottom: 10rpx
.info-name
font-size: 32rpx
color: #333
font-weight: 500
margin-bottom: 10rpx
.info-address
font-size: 28rpx
color: #666
.bottom-actions
position: absolute
bottom: 0
left: 0
right: 0
background: #fff
padding: 20rpx 32rpx
display: flex
gap: 20rpx
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.1)
z-index: 11
.locate-btn
width: 120rpx
height: 80rpx
background: #fff
border: 2rpx solid #256BFA
border-radius: 40rpx
display: flex
flex-direction: column
align-items: center
justify-content: center
font-size: 24rpx
color: #256BFA
&:disabled
opacity: 0.5
color: #999
border-color: #999
text
margin-top: 4rpx
.confirm-btn
flex: 1
height: 80rpx
background: #256BFA
color: #fff
border-radius: 40rpx
font-size: 32rpx
border: none
button::after
border: none
</style>