@@ -11,7 +11,7 @@
< / el-input >
< el-dialog
title = "选择出差城市"
title = "选择出差城市 / 地点 "
:visible.sync = "dialogVisible"
width = "900px"
:append-to-body = "true"
@@ -22,44 +22,47 @@
< div class = "search-box" >
< el-input
v-model = "searchKeyword"
placeholder = "输入城市名 搜索"
placeholder = "输入城市或具体地点 搜索"
size = "small"
prefix -icon = " el -icon -search "
@keyup.enter ="searchCity "
@keyup.enter.native ="searchLocation "
clearable
@clear ="searchKeyword = ''"
>
< el-button slot = "append" @click ="searchCity " > 搜索 < / el -button >
< el-button slot = "append" @click ="searchLocation " > 搜索 < / el -button >
< / el-input >
< / div >
< div class = "hot-cities" v-if = "!searchKeyword" >
<!-- 热门城市模块 -- >
< div class = "hot-cities" v-if = "!searchKeyword || searchResults.length === 0" >
< div class = "section-title" > 热门城市 < / div >
< div class = "city-list" >
< span
v-for = "city in hotCities"
:key = "city"
class = "city-item"
@click ="selectCityFromList (city)"
@click ="selectHot City(city)"
>
{ { city } }
< / span >
< / div >
< / div >
< div class = "search-results" v-else >
<!-- 搜索结果模块 -- >
< div class = "search-results" v-if = "searchResults.length > 0" >
< div class = "section-title" > 搜索结果 < / div >
< div class = "result-list" >
< div
v-for = "result in searchResults"
:key = "result.id "
v-for = "( result, index) in searchResults"
:key = "index "
class = "result-item"
@click ="selectCityFromList (result.name )"
@click ="selectLocation (result)"
>
< i class = "el-icon-location-outline" > < / i >
< span > { { result . name } } < / span >
< / div >
< div v-if = "searchResults.length === 0" class="no-result" >
未找到相关城市
< div class = "result-info" >
< div class = "poi-name" > { { result . name } } < / div >
< div class = "poi-address" > { { result . address } } < / div >
< / div >
< / div >
< / div >
< / div >
@@ -68,15 +71,19 @@
< div class = "map-container" >
< div id = "amapContainer" class = "amap-wrapper" > < / div >
< div class = "map-tip" >
< i class = "el-icon-info" > < / i > 点击地图上的位置 , 自动识别城市
< i class = "el-icon-info" > < / i > 点击地图上的位置或搜索地点 , 自动识别归属 城市
< / div >
< / div >
< / div >
< div slot = "footer" class = "dialog-footer" >
< div class = "selected-info" v-if = "selectedCity" >
已选择 : < span class = "selected-city" > { { selectedCity } } < / span >
识别城市 : < span class = "selected-city" > { { selectedCity } } < / span >
< span v-if = "selectedPoi" style="margin-left: 10px; color: #909399; font-size: 12px;" >
( {{ selectedPoi }} )
< / span >
< / div >
< div v-else > < / div >
< div >
< el-button @click ="closeDialog" > 取消 < / el -button >
< el-button type = "primary" @click ="confirmCity" :disabled = "!selectedCity" > 确定 < / el-button >
@@ -88,13 +95,13 @@
< script >
import AMapLoader from '@amap/amap-jsapi-loader'
import { getAmapKey } from '@/api/hrm/travel'
import { getAmapKey , getAmapSecurityKey } from '@/api/hrm/travel'
export default {
name : 'AmapCitySelect' ,
props : {
value : { type : String , default : '' } ,
placeholder : { type : String , default : '请选择出差城市 ' }
placeholder : { type : String , default : '请选择出差地点 ' }
} ,
data ( ) {
return {
@@ -102,16 +109,17 @@ export default {
dialogVisible : false ,
searchKeyword : '' ,
searchResults : [ ] ,
selectedCity : '' ,
selectedCity : '' , // 最终表单需要的城市名
selectedPoi : '' , // 具体的地点名(仅用于展示)
map : null ,
geocoder : null ,
placeSearch : null , // 新增地点搜索对象
marker : null ,
AMap : null ,
hotCities : [
'北京市' , '上海市' , '广州市' , '深圳市' ,
'杭州市' , '南京市' , '成都市' , '武汉市' ,
'重庆市' , '天津市' , '苏州市' , '西安市' ,
'石家庄市' , '唐山市' , '保定市' , '邯郸市'
'重庆市' , '天津市' , '苏州市' , '西安市'
]
}
} ,
@@ -122,45 +130,43 @@ export default {
}
} ,
methods : {
// 弹窗打开时调用
async onDialogOpened ( ) {
await this . loadAMap ( )
const AMap = await this . loadAMap ( )
if ( ! AMap ) return
this . $nextTick ( ( ) => {
this . initMap ( )
} )
} ,
// 加载高德地图
async loadAMap ( ) {
if ( this . AMap ) return this . AMap
if ( this . AMap && window . _AMapSecurityConfig ) return this . AMap
try {
const res = await getAmapKey ( )
console . log ( "接口完整返回:" , JSON . stringify ( res ) )
const amapkey = res . msg
console . log ( '获取到的Key:' , amapkey )
const [ keyRes , securityRes ] = await Promise . all ( [ getAmapKey ( ) , getAmapSecurityKey ( ) ] )
const amapkey = keyRes . data || keyRes . msg
const securityKey = securityRes . data || securityRes . msg
if ( ! amapkey ) {
throw new Error ( '获取高德地图Key失败' )
}
if ( ! amapkey ) throw new Error ( '未获取到高德地图 Key' )
window . _AMapSecurityConfig = { securityJsCode : securityKey }
this . AMap = await AMapLoader . load ( {
key : amapkey ,
version : '2.0' ,
// 加入 PlaceSearch 插件
plugins : [ 'AMap.Geocoder' , 'AMap.PlaceSearch' ]
} )
return this . AMap
} catch ( error ) {
console . error ( '高德 地图加载失败:' , error )
this . $message . error ( '地图加载失败,请刷新 重试' )
console . error ( '地图加载失败:' , error )
this . $message . error ( '地图加载失败,请重试' )
return null
}
} ,
initMap ( ) {
const container = document . getElementById ( 'amapContainer' )
if ( ! container ) {
console . error ( '地图容器不存在' )
setTimeout ( ( ) => this . initMap ( ) , 100 )
return
}
@@ -171,92 +177,127 @@ export default {
this . map = new this . AMap . Map ( 'amapContainer' , {
zoom : 12 ,
center : [ 116.397428 , 39.90923 ] ,
center : [ 116.397428 , 39.90923 ] , // 默认北京
resizeEnable : true
} )
this . geocoder = new this . AMap . Geocoder ( )
// 初始化地点搜索插件
this . placeSearch = new this . AMap . PlaceSearch ( {
pageSize : 15 , // 单页显示结果条数
pageIndex : 1 , // 页码
autoFitView : false // 禁用自动调整视图,我们自己控制
} )
// 监听地图点击
this . map . on ( 'click' , ( e ) => {
const lng = e . lnglat . getLng ( )
const lat = e . lnglat . getLat ( )
this . getCityByLngLat ( lng , lat )
this . selectedPoi = '地图选点'
this . getCityByLngLat ( lng , lat , true )
} )
} ,
getCityByLngLat ( lng , lat ) {
// 通过经纬度逆解析城市
getCityByLngLat ( lng , lat , setCenter = false ) {
this . geocoder . getAddress ( [ lng , lat ] , ( status , result ) => {
if ( status === 'complete' && result . regeocode ) {
const addrComp = result . regeocode . addressComponent
let city = addrComp . city
if ( ! city || city === '[]' ) {
// 直辖市的 city 可能是空的,取 province
if ( ! city || city === '[]' || city . length === 0 ) {
city = addrComp . province
}
this . selectedCity = city
this . addMarker ( lng , lat )
this . $message . success ( ` 识别到城市: ${ city } ` )
} else {
this . $message . warning ( '无法识别该位置的城市' )
if ( setCenter ) {
this . map . setCenter ( [ lng , lat ] )
}
}
} )
} ,
// 在地图上打点
addMarker ( lng , lat ) {
if ( this . marker ) {
this . marker . setMap ( null )
}
this . marker = new this . AMap . Marker ( {
position : [ lng , lat ] ,
map : this . map
map : this . map ,
animation : 'AMAP_ANIMATION_DROP' // 加上掉落动画
} )
} ,
openMapDialog ( ) {
this . dialogVisible = true
this . selectedCity = ''
this . selectedCity = this . value || ''
this . selectedPoi = ''
this . searchKeyword = ''
this . searchResults = [ ]
} ,
async searchCity ( ) {
// 搜索地点核心方法
async searchLocation ( ) {
if ( ! this . searchKeyword . trim ( ) ) {
this . $message . warning ( '请输入城市名称' )
this . searchResults = [ ]
return
}
await this . loadAMap ( )
this . geocoder . getLocation ( this . searchKeyword , ( status , result ) => {
if ( status === 'complete' && result . geocodes && result . geocodes . length ) {
const cities = [ ]
result . geocodes . forEach ( item => {
const cityName = item . city || item . province
if ( cityName && ! citie s . find ( c => c . name === cityName ) ) {
cities . push ( { id : item . adcode , name : cityName } )
}
} )
this . searchResults = cities
if ( this . searchResults . length === 0 ) {
this . $message . info ( '未找到相关城市' )
}
// 使用 PlaceSearch 搜索具体地点
this . placeSearch . search ( this . searchKeyword , ( status , result ) => {
if ( status === 'complete' && result . info === 'OK' ) {
// 提取返回的 POI 列表
const pois = result . poiList . pois
this . searchResults = poi s. map ( poi => ( {
id : poi . id ,
name : poi . name ,
address : poi . address && typeof poi . address === 'string' ? poi . address : poi . adname ,
location : poi . location , // 包含 lng/lat 的对象
city : poi . cityname || poi . pname // 城市名
} ) )
} else {
this . searchResults = [ ]
this . $message . info ( '未找到相关城市 ' )
this . $message . info ( '未找到相关地点,请尝试更换关键词 ' )
}
} )
} ,
async selectCityFromList ( city ) {
this . selectedCity = city
await this . loadAMap ( )
// 选中左侧搜索结果列表的具体地点
selectLocation ( poi ) {
if ( ! poi . location ) {
this . $message . warning ( '该地点缺少坐标信息' )
return
}
const lng = poi . location . lng
const lat = poi . location . lat
// 设置地图中心点并放大(层级15看街道)
this . map . setZoomAndCenter ( 15 , [ lng , lat ] )
this . addMarker ( lng , lat )
// 更新选择数据
this . selectedCity = poi . city
this . selectedPoi = poi . name
this . $message . success ( ` 已定位到: ${ poi . name } ` )
} ,
// 选中热门城市
async selectHotCity ( city ) {
await this . loadAMap ( )
this . selectedCity = city
this . selectedPoi = city
// 获取城市中心点坐标
this . geocoder . getLocation ( city , ( status , result ) => {
if ( status === 'complete' && result . geocodes . length ) {
const loc = result . geocodes [ 0 ] . location
this . map . setCenter ( [ loc . lng , loc . lat ] )
this . map . setZoom ( 12 )
this . map . setZoomAnd Center ( 11 , [ loc . lng , loc . lat ] )
this . addMarker ( loc . lng , loc . lat )
this . $message . success ( ` 已定位到: ${ city } ` )
}
} )
} ,
@@ -264,21 +305,16 @@ export default {
confirmCity ( ) {
if ( this . selectedCity ) {
this . cityName = this . selectedCity
this . $emit ( 'input' , this . selectedCity )
// 组件 v-model 抛出城市名
this . $emit ( 'input' , this . selectedCity )
// 如果你需要具体的地点名,可以额外抛出一个事件
this . $emit ( 'poi-change' , { city : this . selectedCity , poi : this . selectedPoi } )
this . closeDialog ( )
this . $message . success ( ` 已选择: ${ this . selectedCity } ` )
}
} ,
closeDialog ( ) {
this . dialogVisible = false
this . selectedCity = ''
this . searchKeyword = ''
this . searchResults = [ ]
if ( this . marker ) {
this . marker . setMap ( null )
this . marker = null
}
}
}
}
@@ -287,28 +323,49 @@ export default {
< style lang = "scss" scoped >
. amap - city - select { width : 100 % ; }
. map - selector { display : flex ; gap : 20 px ; min - height : 450 px ;
. city - sidebar { width : 28 0px ; flex - shrink : 0 ;
. city - sidebar { width : 30 0px ; flex - shrink : 0 ; display : flex ; flex - direction : column ;
. search - box { margin - bottom : 16 px ; }
. section - title { font - size : 14 px ; font - weight : 500 ; color : # 303133 ; margin - bottom : 12 px ; padding - bottom : 8 px ; border - bottom : 1 px solid # e4e7ed ; }
. section - title { font - size : 14 px ; font - weight : bold ; color : # 303133 ; margin - bottom : 12 px ; padding - bottom : 8 px ; border - bottom : 1 px solid # ebeef5 ; }
. hot - cities . city - list { display : flex ; flex - wrap : wrap ; gap : 10 px ;
. city - item { padding : 4 px 12 px ; background : # f5f7fa ; border - radius : 4 px ; font - size : 13 px ; cursor : pointer ;
. city - item { padding : 6 px 14 px ; background : # f4f4f5 ; color : # 606266 ; border - radius : 4 px ; font - size : 13 px ; cursor : pointer ; transition : all 0.2 s ;
& : hover { background : # 409 eff ; color : white ; }
}
}
. search - results . result - list { max - height : 380 px ; overflow - y : auto ;
. result - item { padding : 10 px 12 px ; cursor : pointer ; display : flex ; align - items : center ; gap : 8 px ; border - bottom : 1 px solid # f0f0f0 ;
& : hover { background : # ecf5ff ; }
i { color : # 409 eff ; }
/* 搜索结果列表样式升级 */
. search - results {
flex : 1 ;
display : flex ;
flex - direction : column ;
overflow : hidden ;
}
. result - list {
flex : 1 ; overflow - y : auto ; padding - right : 5 px ;
/* 自定义滚动条 */
& : : - webkit - scrollbar { width : 4 px ; }
& : : - webkit - scrollbar - thumb { background : # dcdfe6 ; border - radius : 4 px ; }
. result - item {
padding : 12 px 10 px ; cursor : pointer ; display : flex ; align - items : flex - start ; gap : 10 px ; border - bottom : 1 px solid # ebeef5 ; transition : background 0.2 s ;
& : hover { background : # f0f7ff ; }
i { color : # 409 eff ; margin - top : 3 px ; font - size : 16 px ; }
. result - info {
flex : 1 ; overflow : hidden ;
. poi - name { font - size : 14 px ; color : # 303133 ; font - weight : 500 ; margin - bottom : 4 px ; white - space : nowrap ; overflow : hidden ; text - overflow : ellipsis ; }
. poi - address { font - size : 12 px ; color : # 909399 ; white - space : nowrap ; overflow : hidden ; text - overflow : ellipsis ; }
}
}
. no - result { padding : 40 px 0 ; text - align : center ; color : # 909399 ; }
}
}
. map - container { flex : 1 ;
. a map- wrapp er { width : 100 % ; height : 400 px ; border : 1 px solid # e4e7ed ; border - radius : 8 px ; overflow : hidde n; }
. map - tip { margin - top : 8 px ; font - size : 12 px ; c olor : # 909399 ; text - align : center ; i { margin - right : 4 px ; } }
. map - contain er { flex : 1 ; display : flex ; flex - direction : colum n;
. a map- wrapper { flex : 1 ; width : 100 % ; border : 1 px s olid # dcdfe6 ; border - radius : 6 px ; overflow : hidden ; }
. map - tip { margin - top : 10 px ; font - size : 12 px ; color : # 909399 ; text - align : center ; i { margin - right : 4 px ; color : # e6a23c ; } }
}
}
. dialog - footer { display : flex ; justify - content : space - between ; align - items : center ;
. selected - info { font - size : 13 px ; color : # 606266 ; . selected - city { color : # 409 eff ; font - weight : 500 ; } }
. selected - info { font - size : 14 px ; color : # 606266 ; . selected - city { color : # 409 eff ; font - weight : bold ; font - size : 15 px ; } }
}
< / style >