refactor(custom): 地图服务商更换
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
527
src/components/TiandituPicker.vue
Normal file
527
src/components/TiandituPicker.vue
Normal file
@@ -0,0 +1,527 @@
|
||||
<template>
|
||||
<div class="tianditu-picker">
|
||||
<!-- 顶部搜索框 -->
|
||||
<div class="search-header">
|
||||
<n-input-group>
|
||||
<n-auto-complete
|
||||
v-model:value="keyword"
|
||||
placeholder="搜索地点"
|
||||
clearable
|
||||
@select="selectFromSearch"
|
||||
@input:value="handleInput"
|
||||
/>
|
||||
<n-button type="primary" @click="search">搜索位置</n-button>
|
||||
</n-input-group>
|
||||
</div>
|
||||
|
||||
<!-- 地图区域 -->
|
||||
<div id="mapDiv" class="map-container"></div>
|
||||
|
||||
<!-- 底部位置信息和搜索结果 -->
|
||||
<div class="location-footer">
|
||||
<!-- 统一的结果列表 -->
|
||||
<div class="results-section">
|
||||
<div class="results-header">位置选择</div>
|
||||
<div class="results-list">
|
||||
<!-- 我的位置项 -->
|
||||
<div
|
||||
class="result-item current-location-item"
|
||||
:class="{ selected: !showSearchResults }"
|
||||
@click="relocateToCurrentPosition"
|
||||
>
|
||||
<div class="location-icon">📍</div>
|
||||
<div class="result-content">
|
||||
<div class="result-name">我的位置</div>
|
||||
<div class="result-address">
|
||||
{{ currentLocation?.address || '请点击获取当前位置' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索结果项 -->
|
||||
<div
|
||||
v-for="(result, index) in searchResults"
|
||||
:key="`search-${index}`"
|
||||
class="result-item search-result-item"
|
||||
@click="selectSearchResult(result)"
|
||||
>
|
||||
<div class="location-icon">📍</div>
|
||||
<div class="result-content">
|
||||
<div class="result-name">{{ result.name }}</div>
|
||||
<div class="result-address">{{ result.address }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons">
|
||||
<n-space justify="space-between">
|
||||
<n-button type="tertiary" @click="getCurrentLocation">定位当前位置</n-button>
|
||||
<n-space>
|
||||
<n-button @click="$emit('cancel')">取消</n-button>
|
||||
<n-button type="primary" @click="confirmLocation">确认位置</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
const emit = defineEmits(['confirm', 'cancel'])
|
||||
|
||||
const keyword = ref('')
|
||||
const selectedLocation = ref(null)
|
||||
const currentLocation = ref(null)
|
||||
const searchResults = ref([])
|
||||
const showSearchResults = ref(false)
|
||||
let map = null
|
||||
let marker = null
|
||||
|
||||
const TDT_KEY = '42db4f3dfd1a18d31e73ee90aa2ce054'
|
||||
|
||||
// 初始化地图
|
||||
const initMap = () => {
|
||||
if (typeof T === 'undefined') {
|
||||
console.error('天地图API未加载完成')
|
||||
return
|
||||
}
|
||||
|
||||
map = new window.T.Map('mapDiv')
|
||||
map.centerAndZoom(new window.T.LngLat(116.40969, 39.89945), 12)
|
||||
|
||||
// 地图点击事件
|
||||
map.addEventListener('click', function (e) {
|
||||
const lnglat = e.lnglat
|
||||
addMarker(lnglat)
|
||||
getAddress(lnglat)
|
||||
// 点击地图时隐藏搜索结果
|
||||
showSearchResults.value = false
|
||||
})
|
||||
|
||||
// 获取当前位置
|
||||
getCurrentLocation()
|
||||
}
|
||||
|
||||
// 添加标记
|
||||
const addMarker = (lnglat) => {
|
||||
if (marker) {
|
||||
map.removeOverLay(marker)
|
||||
}
|
||||
|
||||
// 创建自定义图标
|
||||
const icon = new window.T.Icon({
|
||||
iconUrl:
|
||||
'data:image/svg+xml;base64,' +
|
||||
btoa(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="40" viewBox="0 0 32 40">
|
||||
<path fill="#ff4757" d="M16 0C7.2 0 0 7.2 0 16c0 8.8 16 24 16 24s16-15.2 16-24C32 7.2 24.8 0 16 0zm0 22c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6z"/>
|
||||
<circle fill="white" cx="16" cy="16" r="4"/>
|
||||
</svg>
|
||||
`),
|
||||
iconSize: new window.T.Point(32, 40),
|
||||
iconAnchor: new window.T.Point(16, 40),
|
||||
})
|
||||
|
||||
marker = new window.T.Marker(lnglat, { icon })
|
||||
map.addOverLay(marker)
|
||||
map.panTo(lnglat)
|
||||
|
||||
selectedLocation.value = {
|
||||
lat: lnglat.lat,
|
||||
lng: lnglat.lng,
|
||||
address: '正在获取地址...',
|
||||
}
|
||||
}
|
||||
|
||||
// 地理编码
|
||||
const getAddress = (lnglat) => {
|
||||
const geocoder = new window.T.Geocoder()
|
||||
geocoder.getLocation(lnglat, function (result) {
|
||||
const address = result.getAddress()
|
||||
if (selectedLocation.value) {
|
||||
selectedLocation.value.address = address
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取当前位置
|
||||
const getCurrentLocation = () => {
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const lat = position.coords.latitude
|
||||
const lng = position.coords.longitude
|
||||
const lnglat = new window.T.LngLat(lng, lat)
|
||||
|
||||
map.centerAndZoom(lnglat, 15)
|
||||
addMarker(lnglat)
|
||||
|
||||
// 获取地址信息,专门为当前位置设置
|
||||
const geocoder = new window.T.Geocoder()
|
||||
geocoder.getLocation(lnglat, function (result) {
|
||||
const address = result.getAddress()
|
||||
// 同时更新当前位置和选中位置
|
||||
currentLocation.value = {
|
||||
lat: lat,
|
||||
lng: lng,
|
||||
address: address,
|
||||
}
|
||||
if (selectedLocation.value) {
|
||||
selectedLocation.value.address = address
|
||||
}
|
||||
})
|
||||
},
|
||||
(error) => {
|
||||
console.error('获取位置失败:', error)
|
||||
$message.warning('无法获取当前位置,请手动选择')
|
||||
}
|
||||
)
|
||||
} else {
|
||||
$message.warning('浏览器不支持定位功能')
|
||||
}
|
||||
}
|
||||
|
||||
// 使用axios调用天地图HTTP API搜索
|
||||
const searchWithAPI = async (query) => {
|
||||
try {
|
||||
console.log('搜索关键词:', query)
|
||||
|
||||
// 构建搜索参数
|
||||
const postStr = {
|
||||
keyWord: query,
|
||||
level: 12,
|
||||
mapBound: '116.02524,39.83833,116.65592,39.99185',
|
||||
queryType: 1,
|
||||
start: 0,
|
||||
count: 10,
|
||||
}
|
||||
|
||||
const response = await axios.get('https://api.tianditu.gov.cn/v2/search', {
|
||||
params: {
|
||||
postStr: JSON.stringify(postStr),
|
||||
type: 'query',
|
||||
tk: TDT_KEY,
|
||||
},
|
||||
timeout: 10000, // 10秒超时
|
||||
})
|
||||
|
||||
console.log('搜索响应:', response.data)
|
||||
|
||||
if (
|
||||
response.data.status.infocode === 1000 &&
|
||||
response.data.pois &&
|
||||
response.data.pois.length > 0
|
||||
) {
|
||||
return response.data.pois.map((poi) => ({
|
||||
name: poi.name,
|
||||
address: poi.address,
|
||||
lonlat: poi.lonlat,
|
||||
adminName: poi.adminName,
|
||||
}))
|
||||
}
|
||||
return []
|
||||
} catch (error) {
|
||||
console.error('搜索失败:', error)
|
||||
|
||||
// 更详细的错误处理
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
$message.error('请求超时,请重试')
|
||||
} else if (error.response) {
|
||||
$message.error(`搜索失败: ${error.response.status}`)
|
||||
} else if (error.request) {
|
||||
$message.error('网络请求失败,请检查网络连接')
|
||||
} else {
|
||||
$message.error('搜索出错,请重试')
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索功能
|
||||
const search = async () => {
|
||||
if (!keyword.value || !map) return
|
||||
|
||||
try {
|
||||
const results = await searchWithAPI(keyword.value)
|
||||
|
||||
if (results.length > 0) {
|
||||
searchResults.value = results
|
||||
showSearchResults.value = true
|
||||
console.log('搜索结果:', results)
|
||||
} else {
|
||||
$message.warning('未找到相关位置')
|
||||
searchResults.value = []
|
||||
showSearchResults.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索出错:', error)
|
||||
$message.error('搜索失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 选择搜索结果
|
||||
const selectSearchResult = (result) => {
|
||||
if (!result.lonlat) return
|
||||
|
||||
const [lng, lat] = result.lonlat.split(',').map(Number)
|
||||
const lnglat = new window.T.LngLat(lng, lat)
|
||||
|
||||
addMarker(lnglat)
|
||||
// 只更新选中位置,不影响当前位置
|
||||
if (selectedLocation.value) {
|
||||
selectedLocation.value.address = `${result.name} - ${result.address}`
|
||||
}
|
||||
showSearchResults.value = false
|
||||
}
|
||||
|
||||
// 处理搜索输入
|
||||
const handleInput = (value) => {
|
||||
if (value && value.length > 1) {
|
||||
// 延迟搜索,避免频繁请求
|
||||
clearTimeout(handleInput.timer)
|
||||
handleInput.timer = setTimeout(() => {
|
||||
search()
|
||||
}, 500)
|
||||
} else {
|
||||
searchResults.value = []
|
||||
showSearchResults.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 从搜索建议中选择
|
||||
const selectFromSearch = (value, option) => {
|
||||
if (option && option.data) {
|
||||
selectSearchResult(option.data)
|
||||
}
|
||||
}
|
||||
|
||||
// 确认选择的位置
|
||||
const confirmLocation = () => {
|
||||
if (selectedLocation.value) {
|
||||
emit('confirm', {
|
||||
latlng: {
|
||||
lat: selectedLocation.value.lat,
|
||||
lng: selectedLocation.value.lng,
|
||||
},
|
||||
address: selectedLocation.value.address || '',
|
||||
})
|
||||
} else {
|
||||
$message.warning('请先选择位置')
|
||||
}
|
||||
}
|
||||
|
||||
// 重新定位到当前位置
|
||||
const relocateToCurrentPosition = () => {
|
||||
if (currentLocation.value) {
|
||||
// 如果已有当前位置信息,直接使用
|
||||
const lnglat = new window.T.LngLat(currentLocation.value.lng, currentLocation.value.lat)
|
||||
addMarker(lnglat)
|
||||
if (selectedLocation.value) {
|
||||
selectedLocation.value.address = currentLocation.value.address
|
||||
}
|
||||
} else {
|
||||
// 重新获取当前位置
|
||||
getCurrentLocation()
|
||||
}
|
||||
showSearchResults.value = false
|
||||
}
|
||||
|
||||
// 加载天地图API
|
||||
const loadTiandituScript = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof T !== 'undefined') {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.type = 'text/javascript'
|
||||
script.src = `https://api.tianditu.gov.cn/api?v=4.0&tk=${TDT_KEY}`
|
||||
script.onload = resolve
|
||||
script.onerror = reject
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await loadTiandituScript()
|
||||
await nextTick()
|
||||
initMap()
|
||||
} catch (error) {
|
||||
console.error('加载天地图API失败:', error)
|
||||
$message.error('地图加载失败,请检查网络连接')
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (map) {
|
||||
map = null
|
||||
marker = null
|
||||
}
|
||||
if (handleInput.timer) {
|
||||
clearTimeout(handleInput.timer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tianditu-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
.search-header {
|
||||
padding: 12px 0;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #eee;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.location-footer {
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
|
||||
.results-section {
|
||||
.results-header {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
background-color: #f5f5f5;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.results-list {
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.location-icon {
|
||||
font-size: 18px;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
flex: 1;
|
||||
|
||||
.result-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.result-address {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.current-location-item {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 深色模式适配
|
||||
.dark {
|
||||
.tianditu-picker {
|
||||
background-color: #1f1f1f;
|
||||
|
||||
.search-header {
|
||||
background-color: #2a2a2a;
|
||||
border-bottom-color: #3a3a3a;
|
||||
}
|
||||
|
||||
.location-footer {
|
||||
background-color: #2a2a2a;
|
||||
border-top-color: #3a3a3a;
|
||||
|
||||
.results-section {
|
||||
.results-header {
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
border-bottom-color: #3a3a3a;
|
||||
}
|
||||
|
||||
.results-list {
|
||||
.result-item {
|
||||
border-bottom-color: #3a3a3a;
|
||||
|
||||
&:hover {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
.result-name {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.result-address {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
.current-location-item {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
background-color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
background-color: #2a2a2a;
|
||||
border-top-color: #3a3a3a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import api from '../api'
|
||||
import Upload from '@/components/Upload.vue'
|
||||
import TiandituPicker from '@/components/TiandituPicker.vue'
|
||||
|
||||
onMounted(() => {
|
||||
getInfo()
|
||||
@@ -114,6 +115,13 @@ window.addEventListener('message', (res) => {
|
||||
showModal.value = false
|
||||
})
|
||||
|
||||
const confirm = (data) => {
|
||||
model.value.lt = `${data.latlng.lat},${data.latlng.lng}`
|
||||
model.value.lat = data.latlng.lat
|
||||
model.value.lon = data.latlng.lng
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (!errors) {
|
||||
@@ -214,19 +222,14 @@ const submit = () => {
|
||||
<!-- h5地图 -->
|
||||
<n-modal v-if="showModal" v-model:show="showModal">
|
||||
<n-card
|
||||
style="width: 600px; height: 600px"
|
||||
style="width: 600px; height: auto"
|
||||
title="查找地图位置"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<iframe
|
||||
src="https://apis.map.qq.com/tools/locpicker?type=1&key=4EJBZ-TZXCV-IHUPX-UMI2L-MK3N3-37FSQ&referer=myapp"
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
<TiandituPicker @confirm="confirm" @cancel="showModal = false" />
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</CommonPage>
|
||||
|
||||
Reference in New Issue
Block a user