智慧教务系统
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

<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>