|
|
|
@ -12,9 +12,7 @@ |
|
|
|
<div |
|
|
|
v-if="!props.modelValue.lat || !props.modelValue.lng" |
|
|
|
class="map-placeholder" |
|
|
|
> |
|
|
|
{{ props.placeholder }} |
|
|
|
</div> |
|
|
|
></div> |
|
|
|
</div> |
|
|
|
<div class="address-search"> |
|
|
|
<el-select |
|
|
|
@ -55,10 +53,7 @@ |
|
|
|
:value="item.id" |
|
|
|
/> |
|
|
|
</el-select> |
|
|
|
<el-input |
|
|
|
v-model="detailAddress" |
|
|
|
:placeholder="t('detailAddressPlaceholder')" |
|
|
|
/> |
|
|
|
<el-input v-model="detailAddress" placeholder="输入地区" /> |
|
|
|
<el-button |
|
|
|
type="primary" |
|
|
|
@click="handleAddressSearch" |
|
|
|
@ -78,11 +73,13 @@ |
|
|
|
</template> |
|
|
|
|
|
|
|
<script lang="ts" setup> |
|
|
|
import { ref, onMounted, watch, computed } from 'vue' |
|
|
|
import { getAreaListByPid, getAreaByCode } from '@/app/api/sys' |
|
|
|
import { createMarker, latLngToAddress, addressToLatLng } from '@/utils/qqmap' |
|
|
|
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, |
|
|
|
@ -104,6 +101,7 @@ const props = defineProps({ |
|
|
|
|
|
|
|
const emit = defineEmits(['update:visible', 'update:modelValue', 'confirm']) |
|
|
|
|
|
|
|
// ====================== 状态管理 ====================== |
|
|
|
const dialogVisible = computed({ |
|
|
|
get() { |
|
|
|
return props.visible |
|
|
|
@ -113,10 +111,74 @@ const dialogVisible = computed({ |
|
|
|
}, |
|
|
|
}) |
|
|
|
|
|
|
|
watch(dialogVisible, (newVal) => { |
|
|
|
emit('update:modelValue', newVal) |
|
|
|
// 地图相关 |
|
|
|
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() |
|
|
|
} |
|
|
|
@ -126,80 +188,164 @@ const handleConfirm = () => { |
|
|
|
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: detailAddress.value, |
|
|
|
address: fullAddress, |
|
|
|
}) |
|
|
|
|
|
|
|
dialogVisible.value = false |
|
|
|
} |
|
|
|
|
|
|
|
// 地图相关 |
|
|
|
let map: any |
|
|
|
let marker: any |
|
|
|
const mapKey = ref('') |
|
|
|
// 地址解析逻辑 |
|
|
|
const parseAndSetAddress = async (address: string) => { |
|
|
|
// 解析地址 |
|
|
|
const { provinceName, cityName, districtName, detail } = parseAddress(address) |
|
|
|
detailAddress.value = detail |
|
|
|
|
|
|
|
// 区域选择 |
|
|
|
const province = ref('') |
|
|
|
const city = ref('') |
|
|
|
const district = ref('') |
|
|
|
const detailAddress = ref('') |
|
|
|
const provinceList = ref<any[]>([]) |
|
|
|
const cityList = ref<any[]>([]) |
|
|
|
const districtList = ref<any[]>([]) |
|
|
|
// 1. 处理省份选择 |
|
|
|
if (provinceName) { |
|
|
|
const provinceItem = provinceList.value.find((p) => |
|
|
|
p.name.includes(provinceName) |
|
|
|
) |
|
|
|
if (provinceItem) { |
|
|
|
province.value = provinceItem.id |
|
|
|
|
|
|
|
// 初始化地图 |
|
|
|
const initMapScript = () => { |
|
|
|
const container = document.getElementById('container') |
|
|
|
if (!container) return |
|
|
|
// 2. 处理城市选择 |
|
|
|
const cityRes = await getAreaListByPid(province.value) |
|
|
|
cityList.value = cityRes.data |
|
|
|
|
|
|
|
mapScript = document.createElement('script') |
|
|
|
mapKey.value = 'IZQBZ-3UHEU-WTCVD-2464U-I5N4V-ZFFU3' |
|
|
|
mapScript.type = 'text/javascript' |
|
|
|
mapScript.src = |
|
|
|
'https://map.qq.com/api/gljs?libraries=tools,service&v=1.exp&key=IZQBZ-3UHEU-WTCVD-2464U-I5N4V-ZFFU3' |
|
|
|
document.body.appendChild(mapScript) |
|
|
|
mapScript.onload = () => { |
|
|
|
setTimeout(() => { |
|
|
|
initMap() |
|
|
|
}, 500) |
|
|
|
} |
|
|
|
if (cityName) { |
|
|
|
const cityItem = cityList.value.find((c) => c.name.includes(cityName)) |
|
|
|
if (cityItem) { |
|
|
|
city.value = cityItem.id |
|
|
|
|
|
|
|
mapScript.onerror = (error) => { |
|
|
|
console.error('地图脚本加载失败:', error) |
|
|
|
// 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() |
|
|
|
} |
|
|
|
|
|
|
|
let mapScript: HTMLScriptElement | null = null |
|
|
|
// 地址拆分函数 |
|
|
|
const parseAddress = (address: string) => { |
|
|
|
let provinceName = '' |
|
|
|
let cityName = '' |
|
|
|
let districtName = '' |
|
|
|
let detail = address |
|
|
|
|
|
|
|
watch( |
|
|
|
dialogVisible, |
|
|
|
(newVal) => { |
|
|
|
if (newVal) { |
|
|
|
if (mapScript) { |
|
|
|
document.body.removeChild(mapScript) |
|
|
|
mapScript = null |
|
|
|
// 提取省/直辖市/自治区 |
|
|
|
const provinceMatch = address.match(/^([^省市自治区]+[省市区]|[^市]+市)/) |
|
|
|
if (provinceMatch) { |
|
|
|
provinceName = provinceMatch[0] |
|
|
|
detail = address.slice(provinceName.length) |
|
|
|
} |
|
|
|
initMapScript() |
|
|
|
getAreaListByPid(0).then((res) => { |
|
|
|
provinceList.value = res.data |
|
|
|
}) |
|
|
|
} else { |
|
|
|
|
|
|
|
// 提取市 |
|
|
|
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 |
|
|
|
} |
|
|
|
}, |
|
|
|
{ immediate: true } |
|
|
|
) |
|
|
|
|
|
|
|
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 = () => { |
|
|
|
console.log('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, |
|
|
|
@ -221,20 +367,37 @@ const initMap = () => { |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
// 区域选择处理 |
|
|
|
const handleProvinceChange = (val: string) => { |
|
|
|
getAreaListByPid(val).then((res) => { |
|
|
|
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 = (val: string) => { |
|
|
|
getAreaListByPid(val).then((res) => { |
|
|
|
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) => { |
|
|
|
@ -242,7 +405,8 @@ const handleDistrictChange = (val: string) => { |
|
|
|
} |
|
|
|
|
|
|
|
// 地址搜索 |
|
|
|
const handleAddressSearch = () => { |
|
|
|
const handleAddressSearch = async () => { |
|
|
|
try { |
|
|
|
const address = [ |
|
|
|
province.value |
|
|
|
? provinceList.value.find((p) => p.id === province.value)?.name |
|
|
|
@ -254,8 +418,11 @@ const handleAddressSearch = () => { |
|
|
|
detailAddress.value, |
|
|
|
].join('') |
|
|
|
|
|
|
|
addressToLatLng({ mapKey: mapKey.value, address }).then( |
|
|
|
({ message, result }) => { |
|
|
|
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, |
|
|
|
@ -272,17 +439,25 @@ const handleAddressSearch = () => { |
|
|
|
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) { |
|
|
|
if (newVal.lat && newVal.lng && map) { |
|
|
|
const latLng = new (window as any).TMap.LatLng(newVal.lat, newVal.lng) |
|
|
|
map?.setCenter(latLng) |
|
|
|
map.setCenter(latLng) |
|
|
|
marker?.updateGeometries({ |
|
|
|
id: 'center', |
|
|
|
position: latLng, |
|
|
|
@ -291,6 +466,10 @@ watch( |
|
|
|
}, |
|
|
|
{ immediate: true } |
|
|
|
) |
|
|
|
|
|
|
|
onBeforeUnmount(() => { |
|
|
|
cleanupMap() |
|
|
|
}) |
|
|
|
</script> |
|
|
|
|
|
|
|
<style scoped> |
|
|
|
|