You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
503 lines
12 KiB
503 lines
12 KiB
<template>
|
|
<el-dialog
|
|
v-model="dialogVisible"
|
|
:title="t('mapPickerTitle')"
|
|
width="800px"
|
|
:before-close="handleClose"
|
|
:close-on-click-modal="false"
|
|
:close-on-press-escape="false"
|
|
:show-close="false"
|
|
>
|
|
<div class="map-container" id="container">
|
|
<div
|
|
v-if="!props.modelValue.lat || !props.modelValue.lng"
|
|
class="map-placeholder"
|
|
></div>
|
|
</div>
|
|
<div class="address-search">
|
|
<el-select
|
|
v-model="province"
|
|
:placeholder="t('provincePlaceholder')"
|
|
@change="handleProvinceChange"
|
|
>
|
|
<el-option
|
|
v-for="item in provinceList"
|
|
:key="item.id"
|
|
:label="item.name"
|
|
:value="item.id"
|
|
/>
|
|
</el-select>
|
|
<el-select
|
|
v-model="city"
|
|
:placeholder="t('cityPlaceholder')"
|
|
@change="handleCityChange"
|
|
:disabled="!province"
|
|
>
|
|
<el-option
|
|
v-for="item in cityList"
|
|
:key="item.id"
|
|
:label="item.name"
|
|
:value="item.id"
|
|
/>
|
|
</el-select>
|
|
<el-select
|
|
v-model="district"
|
|
:placeholder="t('districtPlaceholder')"
|
|
@change="handleDistrictChange"
|
|
:disabled="!province || !city"
|
|
>
|
|
<el-option
|
|
v-for="item in districtList"
|
|
:key="item.id"
|
|
:label="item.name"
|
|
:value="item.id"
|
|
/>
|
|
</el-select>
|
|
<el-input v-model="detailAddress" placeholder="输入地区" />
|
|
<el-button
|
|
type="primary"
|
|
@click="handleAddressSearch"
|
|
:disabled="!province || !city || !district"
|
|
>{{ t('search') }}</el-button
|
|
>
|
|
</div>
|
|
<template #footer>
|
|
<span class="dialog-footer">
|
|
<el-button @click="dialogVisible = false">{{ t('cancel') }}</el-button>
|
|
<el-button type="primary" @click="handleConfirm">
|
|
{{ t('confirm') }}
|
|
</el-button>
|
|
</span>
|
|
</template>
|
|
</el-dialog>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { ref, onMounted, watch, computed, nextTick, onBeforeUnmount } from 'vue'
|
|
import { ElMessage } from 'element-plus'
|
|
import { getAreaListByPid } from '@/app/api/sys'
|
|
import { createMarker, addressToLatLng } from '@/utils/qqmap'
|
|
import { t } from '@/lang'
|
|
|
|
// ====================== Props定义 ======================
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: Object,
|
|
default: () => ({
|
|
lat: null,
|
|
lng: null,
|
|
address: '',
|
|
}),
|
|
},
|
|
placeholder: {
|
|
type: String,
|
|
default: t('mapPickerPlaceholder'),
|
|
},
|
|
visible: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
})
|
|
|
|
const emit = defineEmits(['update:visible', 'update:modelValue', 'confirm'])
|
|
|
|
// ====================== 状态管理 ======================
|
|
const dialogVisible = computed({
|
|
get() {
|
|
return props.visible
|
|
},
|
|
set(value) {
|
|
emit('update:visible', value)
|
|
},
|
|
})
|
|
|
|
// 地图相关
|
|
let map: any = null
|
|
let marker: any = null
|
|
const mapKey = ref('YOUR_API_KEY') // 替换为你的API Key
|
|
let mapScript: HTMLScriptElement | null = null
|
|
let resizeTimer: number | null = null
|
|
|
|
// 区域选择
|
|
const province = ref('')
|
|
const city = ref('')
|
|
const district = ref('')
|
|
const detailAddress = ref('')
|
|
const provinceList = ref<any[]>([])
|
|
const cityList = ref<any[]>([])
|
|
const districtList = ref<any[]>([])
|
|
|
|
// ====================== 生命周期 ======================
|
|
onMounted(() => {
|
|
// 预加载地图SDK(可选优化)
|
|
const preloadScript = document.createElement('script')
|
|
preloadScript.src = `https://map.qq.com/api/gljs?key= ${mapKey.value}`
|
|
document.head.appendChild(preloadScript)
|
|
})
|
|
|
|
// ====================== 监听器 ======================
|
|
watch(
|
|
dialogVisible,
|
|
async (newVal) => {
|
|
if (newVal) {
|
|
try {
|
|
// 清理旧实例
|
|
cleanupMap()
|
|
|
|
// 等待DOM更新
|
|
await nextTick()
|
|
|
|
// 确保容器存在
|
|
const container = document.getElementById('container')
|
|
if (!container) throw new Error('地图容器未找到')
|
|
|
|
// 加载地图SDK
|
|
await loadMapSDK()
|
|
|
|
// 初始化地图
|
|
initMap()
|
|
|
|
// 获取省份数据
|
|
const res = await getAreaListByPid(0)
|
|
provinceList.value = res.data
|
|
|
|
// 如果存在初始地址,进行解析
|
|
if (props.modelValue.address) {
|
|
await parseAndSetAddress(props.modelValue.address)
|
|
}
|
|
|
|
// 绑定resize监听器
|
|
bindResizeListener()
|
|
} catch (error) {
|
|
handleError(error)
|
|
}
|
|
} else {
|
|
cleanupMap()
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
// ====================== 方法实现 ======================
|
|
const handleClose = (done: () => void) => {
|
|
done()
|
|
}
|
|
|
|
const handleConfirm = () => {
|
|
if (!props.modelValue.lat || !props.modelValue.lng) {
|
|
ElMessage.warning(t('mapPickerWarning'))
|
|
return
|
|
}
|
|
|
|
// 拼接完整地址
|
|
const provinceName = province.value
|
|
? provinceList.value.find((p) => p.id === province.value)?.name || ''
|
|
: ''
|
|
const cityName = city.value
|
|
? cityList.value.find((c) => c.id === city.value)?.name || ''
|
|
: ''
|
|
const districtName = district.value
|
|
? districtList.value.find((d) => d.id === district.value)?.name || ''
|
|
: ''
|
|
|
|
const fullAddress = `${provinceName}${cityName}${districtName}${detailAddress.value}`
|
|
|
|
emit('confirm', {
|
|
lat: props.modelValue.lat,
|
|
lng: props.modelValue.lng,
|
|
address: fullAddress,
|
|
})
|
|
|
|
dialogVisible.value = false
|
|
}
|
|
|
|
// 地址解析逻辑
|
|
const parseAndSetAddress = async (address: string) => {
|
|
// 解析地址
|
|
const { provinceName, cityName, districtName, detail } = parseAddress(address)
|
|
detailAddress.value = detail
|
|
|
|
// 1. 处理省份选择
|
|
if (provinceName) {
|
|
const provinceItem = provinceList.value.find((p) =>
|
|
p.name.includes(provinceName)
|
|
)
|
|
if (provinceItem) {
|
|
province.value = provinceItem.id
|
|
|
|
// 2. 处理城市选择
|
|
const cityRes = await getAreaListByPid(province.value)
|
|
cityList.value = cityRes.data
|
|
|
|
if (cityName) {
|
|
const cityItem = cityList.value.find((c) => c.name.includes(cityName))
|
|
if (cityItem) {
|
|
city.value = cityItem.id
|
|
|
|
// 3. 处理区县选择
|
|
const districtRes = await getAreaListByPid(city.value)
|
|
districtList.value = districtRes.data
|
|
|
|
if (districtName) {
|
|
const districtItem = districtList.value.find((d) =>
|
|
d.name.includes(districtName)
|
|
)
|
|
if (districtItem) {
|
|
district.value = districtItem.id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 触发地址搜索
|
|
handleAddressSearch()
|
|
}
|
|
|
|
// 地址拆分函数
|
|
const parseAddress = (address: string) => {
|
|
let provinceName = ''
|
|
let cityName = ''
|
|
let districtName = ''
|
|
let detail = address
|
|
|
|
// 提取省/直辖市/自治区
|
|
const provinceMatch = address.match(/^([^省市自治区]+[省市区]|[^市]+市)/)
|
|
if (provinceMatch) {
|
|
provinceName = provinceMatch[0]
|
|
detail = address.slice(provinceName.length)
|
|
}
|
|
|
|
// 提取市
|
|
const cityMatch = detail.match(/^([^市区]+市)/)
|
|
if (cityMatch) {
|
|
cityName = cityMatch[0]
|
|
detail = detail.slice(cityName.length)
|
|
}
|
|
|
|
// 提取区/县
|
|
const districtMatch = detail.match(/^([^区县]+[区县])/)
|
|
if (districtMatch) {
|
|
districtName = districtMatch[0]
|
|
detail = detail.slice(districtName.length)
|
|
}
|
|
|
|
return {
|
|
provinceName,
|
|
cityName,
|
|
districtName,
|
|
detail: detail.trim() || '',
|
|
}
|
|
}
|
|
|
|
// ====================== 地图相关方法 ======================
|
|
const cleanupMap = () => {
|
|
if (mapScript) {
|
|
document.body.removeChild(mapScript)
|
|
mapScript = null
|
|
}
|
|
|
|
if (map) {
|
|
map.destroy()
|
|
map = null
|
|
}
|
|
|
|
marker = null
|
|
|
|
if (resizeTimer) {
|
|
clearTimeout(resizeTimer)
|
|
resizeTimer = null
|
|
}
|
|
|
|
window.removeEventListener('resize', handleResize)
|
|
}
|
|
|
|
const loadMapSDK = (): Promise<void> => {
|
|
return new Promise((resolve, reject) => {
|
|
mapKey.value = 'AKTBZ-OGICT-E5NXQ-LGEGK-H5AJ5-M2BOX'
|
|
|
|
mapScript = document.createElement('script')
|
|
mapScript.type = 'text/javascript'
|
|
mapScript.src = `https://map.qq.com/api/gljs?libraries=tools ,service&v=1.exp&key=${mapKey.value}`
|
|
|
|
mapScript.onload = () => {
|
|
if ((window as any).TMap) {
|
|
resolve()
|
|
} else {
|
|
reject(new Error('TMap未定义'))
|
|
}
|
|
}
|
|
|
|
mapScript.onerror = (error) => {
|
|
reject(error)
|
|
}
|
|
|
|
document.body.appendChild(mapScript)
|
|
})
|
|
}
|
|
|
|
const initMap = () => {
|
|
const container = document.getElementById('container')
|
|
if (!(window as any).TMap || !container) {
|
|
throw new Error('地图SDK未加载或容器不存在')
|
|
}
|
|
|
|
const TMap = (window as any).TMap
|
|
const center = new TMap.LatLng(39.90403, 116.407526)
|
|
|
|
map = new TMap.Map('container', {
|
|
center,
|
|
zoom: 12,
|
|
})
|
|
|
|
marker = createMarker(map)
|
|
|
|
map.on('click', (evt: any) => {
|
|
map.setCenter(evt.latLng)
|
|
marker.updateGeometries({
|
|
id: 'center',
|
|
position: evt.latLng,
|
|
})
|
|
emit('update:modelValue', {
|
|
lat: evt.latLng.lat,
|
|
lng: evt.latLng.lng,
|
|
address: detailAddress.value,
|
|
})
|
|
})
|
|
}
|
|
|
|
const bindResizeListener = () => {
|
|
window.addEventListener('resize', handleResize)
|
|
}
|
|
|
|
const handleResize = () => {
|
|
if (resizeTimer) clearTimeout(resizeTimer)
|
|
resizeTimer = setTimeout(() => {
|
|
map?.setSize()
|
|
}, 300)
|
|
}
|
|
|
|
// ====================== 区域选择处理 ======================
|
|
const handleProvinceChange = async (val: string) => {
|
|
try {
|
|
const res = await getAreaListByPid(val)
|
|
cityList.value = res.data
|
|
city.value = ''
|
|
district.value = ''
|
|
} catch (error) {
|
|
handleError(error)
|
|
}
|
|
}
|
|
|
|
const handleCityChange = async (val: string) => {
|
|
try {
|
|
const res = await getAreaListByPid(val)
|
|
districtList.value = res.data
|
|
district.value = ''
|
|
} catch (error) {
|
|
handleError(error)
|
|
}
|
|
}
|
|
|
|
const handleDistrictChange = (val: string) => {
|
|
// TODO: 处理区县选择
|
|
}
|
|
|
|
// 地址搜索
|
|
const handleAddressSearch = async () => {
|
|
try {
|
|
const address = [
|
|
province.value
|
|
? provinceList.value.find((p) => p.id === province.value)?.name
|
|
: '',
|
|
city.value ? cityList.value.find((c) => c.id === city.value)?.name : '',
|
|
district.value
|
|
? districtList.value.find((d) => d.id === district.value)?.name
|
|
: '',
|
|
detailAddress.value,
|
|
].join('')
|
|
|
|
const { message, result } = await addressToLatLng({
|
|
mapKey: mapKey.value,
|
|
address,
|
|
})
|
|
|
|
if (message == 'Success' || message == 'query ok') {
|
|
const latLng = new (window as any).TMap.LatLng(
|
|
result.location.lat,
|
|
result.location.lng
|
|
)
|
|
map.setCenter(latLng)
|
|
marker.updateGeometries({
|
|
id: 'center',
|
|
position: latLng,
|
|
})
|
|
emit('update:modelValue', {
|
|
lat: result.location.lat,
|
|
lng: result.location.lng,
|
|
address: detailAddress.value,
|
|
})
|
|
}
|
|
} catch (error) {
|
|
handleError(error)
|
|
}
|
|
}
|
|
|
|
// ====================== 错误处理 ======================
|
|
const handleError = (error: any) => {
|
|
console.error('地图组件错误:', error)
|
|
ElMessage.error(t('mapLoadFailed'))
|
|
dialogVisible.value = false
|
|
}
|
|
|
|
// 回显处理
|
|
watch(
|
|
() => props.modelValue,
|
|
(newVal) => {
|
|
if (newVal.lat && newVal.lng && map) {
|
|
const latLng = new (window as any).TMap.LatLng(newVal.lat, newVal.lng)
|
|
map.setCenter(latLng)
|
|
marker?.updateGeometries({
|
|
id: 'center',
|
|
position: latLng,
|
|
})
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
)
|
|
|
|
onBeforeUnmount(() => {
|
|
cleanupMap()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.map-picker {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.map-container {
|
|
width: 100%;
|
|
height: 400px;
|
|
border: 1px solid #dcdfe6;
|
|
border-radius: 4px;
|
|
position: relative;
|
|
}
|
|
|
|
.map-placeholder {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: #909399;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.address-search {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
</style>
|
|
|