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>
|
<script setup>
|
||||||
import api from '../api'
|
import api from '../api'
|
||||||
import Upload from '@/components/Upload.vue'
|
import Upload from '@/components/Upload.vue'
|
||||||
|
import TiandituPicker from '@/components/TiandituPicker.vue'
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getInfo()
|
getInfo()
|
||||||
@@ -114,6 +115,13 @@ window.addEventListener('message', (res) => {
|
|||||||
showModal.value = false
|
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 = () => {
|
const submit = () => {
|
||||||
formRef.value?.validate(async (errors) => {
|
formRef.value?.validate(async (errors) => {
|
||||||
if (!errors) {
|
if (!errors) {
|
||||||
@@ -214,19 +222,14 @@ const submit = () => {
|
|||||||
<!-- h5地图 -->
|
<!-- h5地图 -->
|
||||||
<n-modal v-if="showModal" v-model:show="showModal">
|
<n-modal v-if="showModal" v-model:show="showModal">
|
||||||
<n-card
|
<n-card
|
||||||
style="width: 600px; height: 600px"
|
style="width: 600px; height: auto"
|
||||||
title="查找地图位置"
|
title="查找地图位置"
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
size="huge"
|
size="huge"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
>
|
>
|
||||||
<iframe
|
<TiandituPicker @confirm="confirm" @cancel="showModal = false" />
|
||||||
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>
|
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
</CommonPage>
|
</CommonPage>
|
||||||
|
|||||||
Reference in New Issue
Block a user