master
parent
2222957812
commit
eb7db42e92
@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<div class="tdt-picker">
|
||||
<el-input
|
||||
class="tdt-picker-trigger"
|
||||
:value="displayValue"
|
||||
readonly
|
||||
placeholder="请选择签到地点"
|
||||
@click.native="openDialog"
|
||||
>
|
||||
<el-button slot="append" icon="el-icon-location" @click="openDialog">选择位置</el-button>
|
||||
</el-input>
|
||||
|
||||
<el-dialog
|
||||
title="在地图上选择签到地点"
|
||||
:visible.sync="dialogVisible"
|
||||
width="90%"
|
||||
top="4vh"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
@close="clearSearch"
|
||||
@opened="onDialogOpened"
|
||||
@closed="onDialogClosed"
|
||||
>
|
||||
<div class="tdt-picker-toolbar">
|
||||
<div class="tdt-picker-search-wrap">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
class="tdt-picker-search"
|
||||
clearable
|
||||
placeholder="搜索地址或地点"
|
||||
@input="onKeywordInput"
|
||||
@keyup.enter.native="search"
|
||||
>
|
||||
<el-button slot="append" icon="el-icon-search" :loading="searching" @click="search">搜索</el-button>
|
||||
</el-input>
|
||||
<div v-if="searchResults.length" class="tdt-picker-results">
|
||||
<div
|
||||
v-for="(item, index) in searchResults"
|
||||
:key="`${item.lon || item.lonlat}-${index}`"
|
||||
class="tdt-picker-result"
|
||||
@click="chooseSearchResult(item)"
|
||||
>
|
||||
<div class="tdt-picker-result-name">{{ item.name || item.address }}</div>
|
||||
<div class="tdt-picker-result-address">{{ item.address || item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="success" icon="el-icon-location-information" :loading="locating" @click="locate">当前位置</el-button>
|
||||
<span class="tdt-picker-current">{{ draft.address || '点击地图选择位置' }}</span>
|
||||
</div>
|
||||
|
||||
<div ref="map" class="tdt-picker-map"></div>
|
||||
|
||||
<span slot="footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!hasCoordinates || !draft.address" @click="confirm">确认选点</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
loadTianDiTu,
|
||||
gcj02ToWgs84,
|
||||
wgs84ToGcj02,
|
||||
formatCoordinate,
|
||||
reverseGeocode,
|
||||
searchTianDiTu
|
||||
} from '@/utils/tianditu'
|
||||
|
||||
const DEFAULT_CENTER = [120.585315, 31.298886]
|
||||
|
||||
export default {
|
||||
name: 'TianDiTuPicker',
|
||||
props: {
|
||||
value: { type: Array, default: () => [] },
|
||||
zoom: { type: Number, default: 11 }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
map: null,
|
||||
marker: null,
|
||||
dialogVisible: false,
|
||||
keyword: '',
|
||||
searching: false,
|
||||
locating: false,
|
||||
searchTimer: null,
|
||||
searchVersion: 0,
|
||||
searchResults: [],
|
||||
draft: { lng: '', lat: '', address: '' }
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clearSearch()
|
||||
},
|
||||
computed: {
|
||||
hasCoordinates() {
|
||||
return Number.isFinite(Number(this.draft.lng)) && Number.isFinite(Number(this.draft.lat)) && !(Number(this.draft.lng) === 0 && Number(this.draft.lat) === 0)
|
||||
},
|
||||
displayValue() {
|
||||
if (this.value && this.value[2]) return this.value[2]
|
||||
return ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
deep: true,
|
||||
handler(value) {
|
||||
if (this.map && value && value.length >= 2) {
|
||||
const wgs = gcj02ToWgs84(value[0], value[1])
|
||||
this.setDraft(wgs[0], wgs[1], value[2])
|
||||
this.setMarker(wgs[0], wgs[1], false)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clearSearch() {
|
||||
clearTimeout(this.searchTimer)
|
||||
this.searchVersion += 1
|
||||
this.searching = false
|
||||
this.searchResults = []
|
||||
},
|
||||
onKeywordInput() {
|
||||
this.clearSearch()
|
||||
if (!this.keyword.trim()) return
|
||||
this.searchTimer = setTimeout(() => this.search(false), 350)
|
||||
},
|
||||
openDialog() {
|
||||
this.dialogVisible = true
|
||||
this.searchResults = []
|
||||
this.keyword = ''
|
||||
if (this.value && this.value.length >= 2) {
|
||||
const wgs = gcj02ToWgs84(this.value[0], this.value[1])
|
||||
this.setDraft(wgs[0], wgs[1], this.value[2])
|
||||
} else {
|
||||
this.setDraft('', '', '')
|
||||
}
|
||||
},
|
||||
async onDialogOpened() {
|
||||
try {
|
||||
const T = await loadTianDiTu()
|
||||
const center = this.draft.lng && this.draft.lat ? [this.draft.lng, this.draft.lat] : DEFAULT_CENTER
|
||||
this.map = new T.Map(this.$refs.map)
|
||||
this.map.centerAndZoom(new T.LngLat(center[0], center[1]), this.zoom)
|
||||
this.map.addEventListener('click', this.onMapClick)
|
||||
if (this.draft.lng && this.draft.lat) this.setMarker(this.draft.lng, this.draft.lat, false)
|
||||
} catch (error) {
|
||||
this.$message.error(error.message || '天地图加载失败')
|
||||
}
|
||||
},
|
||||
onDialogClosed() {
|
||||
if (this.map) {
|
||||
this.map.clearOverLays()
|
||||
this.map = null
|
||||
this.marker = null
|
||||
}
|
||||
},
|
||||
async search(showMessage = true) {
|
||||
clearTimeout(this.searchTimer)
|
||||
if (!this.keyword.trim()) return showMessage && this.$message.warning('请输入搜索地址或地点')
|
||||
const version = ++this.searchVersion
|
||||
this.searching = true
|
||||
try {
|
||||
const results = await searchTianDiTu(this.keyword.trim())
|
||||
if (version !== this.searchVersion || !this.dialogVisible) return
|
||||
this.searchResults = results
|
||||
if (showMessage && !results.length) this.$message.info('没有找到相关地点')
|
||||
} catch (error) {
|
||||
if (version === this.searchVersion && this.dialogVisible) this.$message.error(error.message || '搜索失败,请稍后重试')
|
||||
} finally {
|
||||
if (version === this.searchVersion) this.searching = false
|
||||
}
|
||||
},
|
||||
chooseSearchResult(item) {
|
||||
const coords = String(item.lonlat || item.lon || '').split(',')
|
||||
if (coords.length < 2) return
|
||||
const lng = Number(coords[0])
|
||||
const lat = Number(coords[1])
|
||||
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
|
||||
this.setDraft(lng, lat, item.address || item.name || '')
|
||||
this.clearSearch()
|
||||
this.keyword = item.name || item.address || ''
|
||||
if (this.map) {
|
||||
this.map.centerAndZoom(new window.T.LngLat(lng, lat), 16)
|
||||
this.setMarker(lng, lat, false)
|
||||
}
|
||||
},
|
||||
locate() {
|
||||
if (!navigator.geolocation) return this.$message.warning('当前浏览器不支持定位')
|
||||
this.locating = true
|
||||
navigator.geolocation.getCurrentPosition(async position => {
|
||||
const lng = position.coords.longitude
|
||||
const lat = position.coords.latitude
|
||||
this.setDraft(lng, lat, '')
|
||||
if (this.map) {
|
||||
this.map.centerAndZoom(new window.T.LngLat(lng, lat), 16)
|
||||
this.setMarker(lng, lat, true)
|
||||
}
|
||||
try {
|
||||
const address = await reverseGeocode(lng, lat)
|
||||
if (address) this.setDraft(lng, lat, address)
|
||||
} catch (error) {
|
||||
this.setDraft(lng, lat, '')
|
||||
this.$message.warning('当前位置地址解析失败,请稍后重试')
|
||||
}
|
||||
this.locating = false
|
||||
}, error => {
|
||||
this.locating = false
|
||||
this.$message.error(error.code === 1 ? '请允许浏览器获取当前位置' : '当前位置获取失败')
|
||||
}, { enableHighAccuracy: true, timeout: 10000 })
|
||||
},
|
||||
async onMapClick(event) {
|
||||
const lng = event.lnglat.getLng()
|
||||
const lat = event.lnglat.getLat()
|
||||
this.setDraft(lng, lat, '已选择地图位置')
|
||||
this.setMarker(lng, lat, true)
|
||||
try {
|
||||
const address = await reverseGeocode(lng, lat)
|
||||
if (address) this.setDraft(lng, lat, address)
|
||||
} catch (error) {
|
||||
this.setDraft(lng, lat, '')
|
||||
this.$message.warning('点位地址解析失败,请重新选择')
|
||||
}
|
||||
},
|
||||
setDraft(lng, lat, address) {
|
||||
this.draft = { lng, lat, address: address || '' }
|
||||
},
|
||||
setMarker(lng, lat, pan) {
|
||||
if (!this.map || !window.T) return
|
||||
if (this.marker) this.map.removeOverLay(this.marker)
|
||||
this.marker = new window.T.Marker(new window.T.LngLat(lng, lat))
|
||||
this.map.addOverLay(this.marker)
|
||||
if (pan) this.map.panTo(new window.T.LngLat(lng, lat))
|
||||
},
|
||||
confirm() {
|
||||
const gcj = wgs84ToGcj02(this.draft.lng, this.draft.lat)
|
||||
this.$emit('input', [gcj[0], gcj[1], this.draft.address])
|
||||
this.$emit('change', { longitude: gcj[0], latitude: gcj[1], address: this.draft.address })
|
||||
this.dialogVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tdt-picker-trigger { width: 100%; }
|
||||
.tdt-picker-toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
|
||||
.tdt-picker-search-wrap { position: relative; flex: 1; min-width: 0; }
|
||||
.tdt-picker-search { width: 100%; }
|
||||
.tdt-picker-current { max-width: 32%; overflow: hidden; color: #606266; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.tdt-picker-results { position: absolute; top: 42px; right: 0; left: 0; z-index: 3000; max-height: 260px; overflow-y: auto; background: #fff; border: 1px solid #dcdfe6; border-radius: 0 0 4px 4px; box-shadow: 0 2px 12px rgba(0, 0, 0, .12); }
|
||||
.tdt-picker-result { padding: 8px 12px; cursor: pointer; border-bottom: 1px solid #f2f6fc; }
|
||||
.tdt-picker-result:hover { background: #f5f7fa; }
|
||||
.tdt-picker-result-name { color: #303133; font-size: 14px; }
|
||||
.tdt-picker-result-address { margin-top: 3px; color: #909399; font-size: 12px; }
|
||||
.tdt-picker-map { width: 100%; height: 520px; }
|
||||
@media (max-width: 900px) {
|
||||
.tdt-picker-toolbar { flex-wrap: wrap; }
|
||||
.tdt-picker-search-wrap { flex-basis: 100%; }
|
||||
.tdt-picker-current { max-width: 100%; }
|
||||
.tdt-picker-map { height: 420px; }
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,105 @@
|
||||
export const TIAN_DI_TU_KEY = process.env.VUE_APP_TIANDITU_KEY || 'c0e64d52ae7934dfd448c59953559c40'
|
||||
|
||||
let loadPromise
|
||||
|
||||
export function loadTianDiTu() {
|
||||
if (window.T) return Promise.resolve(window.T)
|
||||
if (loadPromise) return loadPromise
|
||||
|
||||
loadPromise = new Promise((resolve, reject) => {
|
||||
const existing = document.querySelector('script[data-tianditu-sdk]')
|
||||
if (existing) {
|
||||
existing.addEventListener('load', () => resolve(window.T))
|
||||
existing.addEventListener('error', reject)
|
||||
return
|
||||
}
|
||||
const script = document.createElement('script')
|
||||
script.dataset.tiandituSdk = 'true'
|
||||
script.src = `https://api.tianditu.gov.cn/api?v=4.0&tk=${encodeURIComponent(TIAN_DI_TU_KEY)}`
|
||||
script.onload = () => window.T ? resolve(window.T) : reject(new Error('天地图 SDK 加载失败'))
|
||||
script.onerror = () => reject(new Error('天地图 SDK 加载失败'))
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
return loadPromise
|
||||
}
|
||||
|
||||
const PI = Math.PI
|
||||
const AXIS = 6378245.0
|
||||
const EE = 0.00669342162296594323
|
||||
|
||||
function outOfChina(lng, lat) {
|
||||
return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271
|
||||
}
|
||||
|
||||
function transformLat(x, y) {
|
||||
let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x))
|
||||
ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0
|
||||
ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0
|
||||
ret += (160.0 * Math.sin(y / 12.0 * PI) + 320 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0
|
||||
return ret
|
||||
}
|
||||
|
||||
function transformLng(x, y) {
|
||||
let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x))
|
||||
ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0
|
||||
ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0
|
||||
ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0
|
||||
return ret
|
||||
}
|
||||
|
||||
export function wgs84ToGcj02(lng, lat) {
|
||||
lng = Number(lng); lat = Number(lat)
|
||||
if (outOfChina(lng, lat)) return [lng, lat]
|
||||
const dLat = transformLat(lng - 105.0, lat - 35.0)
|
||||
const dLng = transformLng(lng - 105.0, lat - 35.0)
|
||||
const radLat = lat / 180.0 * PI
|
||||
let magic = Math.sin(radLat)
|
||||
magic = 1 - EE * magic * magic
|
||||
const sqrtMagic = Math.sqrt(magic)
|
||||
return [lng + (dLng * 180.0) / (AXIS / sqrtMagic * Math.cos(radLat) * PI), lat + (dLat * 180.0) / ((AXIS * (1 - EE)) / (magic * sqrtMagic) * PI)]
|
||||
}
|
||||
|
||||
export function gcj02ToWgs84(lng, lat) {
|
||||
lng = Number(lng); lat = Number(lat)
|
||||
if (outOfChina(lng, lat)) return [lng, lat]
|
||||
const gcj = wgs84ToGcj02(lng, lat)
|
||||
return [lng * 2 - gcj[0], lat * 2 - gcj[1]]
|
||||
}
|
||||
|
||||
export function formatCoordinate(value) {
|
||||
return Number(value).toFixed(6)
|
||||
}
|
||||
|
||||
export async function reverseGeocode(lng, lat) {
|
||||
const postStr = encodeURIComponent(JSON.stringify({ lon: Number(lng), lat: Number(lat), ver: 1 }))
|
||||
const response = await fetch(`https://api.tianditu.gov.cn/geocoder?postStr=${postStr}&type=geocode&tk=${encodeURIComponent(TIAN_DI_TU_KEY)}`)
|
||||
if (!response.ok) throw new Error('天地图地址解析请求失败')
|
||||
const data = await response.json()
|
||||
const result = data && data.result ? data.result : {}
|
||||
const component = result.addressComponent || result.address_component || {}
|
||||
return result.address || result.formatted_address || component.address || [
|
||||
component.province,
|
||||
component.city,
|
||||
component.county,
|
||||
component.road,
|
||||
component.roadname,
|
||||
component.poi
|
||||
].filter(Boolean).join('')
|
||||
}
|
||||
|
||||
export async function searchTianDiTu(keyword) {
|
||||
const postStr = encodeURIComponent(JSON.stringify({
|
||||
keyWord: keyword,
|
||||
level: 12,
|
||||
mapBound: '72,0,137,56',
|
||||
queryType: 1,
|
||||
count: 10,
|
||||
start: 0,
|
||||
show: 2
|
||||
}))
|
||||
const response = await fetch(`https://api.tianditu.gov.cn/v2/search?postStr=${postStr}&type=query&tk=${encodeURIComponent(TIAN_DI_TU_KEY)}`)
|
||||
if (!response.ok) throw new Error('天地图搜索请求失败')
|
||||
const data = await response.json()
|
||||
if (data && data.code && data.code !== 1000) throw new Error(data.msg || '天地图搜索失败')
|
||||
return data && Array.isArray(data.pois) ? data.pois : []
|
||||
}
|
||||
Loading…
Reference in new issue