20260812:1、天地图地理编码;逆地理编码;2、地图选点优化

This commit is contained in:
zhugongping 2026-08-12 14:53:02 +08:00
parent f1f9965089
commit 1f197f80a1
8 changed files with 481 additions and 24 deletions

View File

@ -0,0 +1,72 @@
package com.fastbee.web.controller.map;
import com.fastbee.common.core.domain.AjaxResult;
import com.fastbee.common.utils.StringUtils;
import com.fastbee.http.map.ILocationService;
import com.fastbee.http.map.LocationServiceFactory;
import com.fastbee.http.model.AddressInfo;
import com.fastbee.http.model.Coordinate;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.math.BigDecimal;
/**
* 地图定位接口(供项目管理-地图选点使用)
*
* @author Sumuxi
* @date 2026/8/12
*/
@Api(tags = "地图定位")
@RestController
@RequestMapping("/map/location")
public class MapLocationController {
@Resource
private LocationServiceFactory locationServiceFactory;
/**
* 根据中文地址获取经纬度(正地理编码)
*/
@ApiOperation("根据中文地址获取经纬度")
@PreAuthorize("@ss.hasPermi('iot:project:edit')")
@GetMapping("/geocode")
public AjaxResult geocode(@RequestParam("address") String address,
@RequestParam(value = "provider", defaultValue = "tianditu") String provider) {
if (StringUtils.isEmpty(address)) {
return AjaxResult.error("地址不能为空");
}
ILocationService service = locationServiceFactory.getLocationService(provider);
Coordinate coordinate = service.getCoordinateByAddress(address);
if (coordinate == null || coordinate.getLongitude() == null || coordinate.getLatitude() == null) {
return AjaxResult.error("未找到该地址的坐标,请尝试更详细的地址");
}
return AjaxResult.success(coordinate);
}
/**
* 根据经纬度获取中文地址(逆地理编码)
*/
@ApiOperation("根据经纬度获取中文地址")
@PreAuthorize("@ss.hasPermi('iot:project:edit')")
@GetMapping("/reverseGeocode")
public AjaxResult reverseGeocode(@RequestParam("lng") Double lng, @RequestParam("lat") Double lat,
@RequestParam(value = "provider", defaultValue = "tianditu") String provider) {
if (lng == null || lat == null) {
return AjaxResult.error("经纬度不能为空");
}
ILocationService service = locationServiceFactory.getLocationService(provider);
AddressInfo addressInfo = service.getAddressByCoordinate(
new Coordinate(BigDecimal.valueOf(lng), BigDecimal.valueOf(lat)));
if (addressInfo == null) {
return AjaxResult.error("未获取到该坐标对应的地址");
}
return AjaxResult.success(addressInfo);
}
}

View File

@ -231,3 +231,4 @@ map:
baidu_ak: 0SD4KrYeMXVu8Ox2pxQGDOw3NmDYiKIw
gaode_ak: 3af85be39e2f28334097781349336157
gaode_secret: d0561ad20142e74f8c59edc5deafdbe4
tianditu_ak: b920e6076a06aa279c974f5f76cf746e

View File

@ -389,6 +389,9 @@ public interface FastBeeConstant {
// 高德(https://lbs.amap.com/api/webservice/guide/api/georegeo)
String GAODE_BASE = "https://restapi.amap.com";
// 天地图(http://lbs.tianditu.gov.cn/server/geocodinginterface.html)
String TIANDITU_BASE = "https://api.tianditu.gov.cn";
// 腾讯 TENCENT(预留)
}

View File

@ -33,7 +33,9 @@ public interface ILocationService {
* @param coordinate 不同(BD09,GCJ02,WGS84)实现类坐标
* @return 世界坐标系
*/
Coordinate convertToWgs84(Coordinate coordinate);
default Coordinate convertToWgs84(Coordinate coordinate) {
return coordinate;
}
/**
@ -41,5 +43,7 @@ public interface ILocationService {
* @param wgs84Coordinate WGS84 坐标
* @return 转换后的目标坐标系坐标
*/
Coordinate convertPlatformCoordinateFromWgs84(Coordinate wgs84Coordinate);
default Coordinate convertPlatformCoordinateFromWgs84(Coordinate wgs84Coordinate) {
return wgs84Coordinate;
}
}

View File

@ -20,6 +20,9 @@ public class LocationServiceFactory {
@Resource
private ILocationService gaodeLocationService;
@Resource
private ILocationService tiandituLocationService;
// 从配置文件读取当前使用的服务商
@Value("${map.provider:baidu}")
private String provider;
@ -40,6 +43,8 @@ public class LocationServiceFactory {
}
if ("gaode".equalsIgnoreCase(provider)) {
return gaodeLocationService;
} else if ("tianditu".equalsIgnoreCase(provider)) {
return tiandituLocationService;
} /*else if ("tencent".equalsIgnoreCase(provider)) {
return tencentLocationService;
}*/

View File

@ -0,0 +1,192 @@
package com.fastbee.http.map;
import com.alibaba.fastjson2.JSONObject;
import com.fastbee.common.constant.FastBeeConstant;
import com.fastbee.http.model.AddressInfo;
import com.fastbee.http.model.Coordinate;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* 天地图地图API服务
* <p>
* 文档:http://lbs.tianditu.gov.cn/server/geocodinginterface.html
* 坐标系:天地图使用 CGCS2000,与 WGS84 在实用精度下可视为一致。
*
* @author Sumuxi
* @date 2026/8/12
*/
@Slf4j
@Service("tiandituLocationService")
public class TiandituLocationService implements ILocationService {
@Value("${map.tianditu_ak:b920e6076a06aa279c974f5f76cf746e}")
private String tiandituAK;
/** 地理编码/逆地理编码共用的接口路径 */
private static final String TIANDITU_PATH_GEOCODER = "/geocoder";
/**
* 正地理编码:中文地址 → 经纬度。
* 请求:http://api.tianditu.gov.cn/geocoder?ds={"keyWord":"北京市海淀区莲花池西路28号"}&tk=您的密钥
*/
@Override
public Coordinate getCoordinateByAddress(String address) {
if (StringUtils.isBlank(address)) {
log.warn("天地图地理编码:地址为空");
return null;
}
if (StringUtils.isBlank(tiandituAK)) {
log.warn("天地图地理编码:未配置 map.tianditu_ak");
return null;
}
JSONObject ds = new JSONObject();
ds.put("keyWord", address.trim());
String encodedDs = urlEncode(ds.toJSONString());
if (encodedDs == null) {
return null;
}
String response = sendGet(FastBeeConstant.MAP.TIANDITU_BASE + TIANDITU_PATH_GEOCODER
+ "?ds=" + encodedDs + "&tk=" + tiandituAK);
if (StringUtils.isBlank(response)) {
return null;
}
JSONObject root = JSONObject.parseObject(response);
if (root == null || !"0".equals(String.valueOf(root.get("status")))) {
log.warn("天地图地理编码失败 status={} msg={}", root != null ? root.get("status") : null,
root != null ? root.getString("msg") : null);
return null;
}
JSONObject location = root.getJSONObject("location");
if (location == null) {
log.warn("天地图地理编码:响应缺少 location");
return null;
}
try {
BigDecimal lng = new BigDecimal(location.getString("lon"));
BigDecimal lat = new BigDecimal(location.getString("lat"));
log.info("天地图地理编码:地址 {} -> 经度 {},纬度 {}", address, lng, lat);
return new Coordinate(lng, lat);
} catch (NumberFormatException | NullPointerException e) {
log.warn("天地图地理编码:无法解析坐标 location={}", location.toJSONString());
return null;
}
}
/**
* 逆地理编码:经纬度 → 地址。
* 请求:http://api.tianditu.gov.cn/geocoder?postStr={"lon":116.37304,"lat":39.92594,"ver":1}&type=geocode&tk=您的密钥
*/
@Override
public AddressInfo getAddressByCoordinate(Coordinate coordinate) {
if (coordinate == null || coordinate.getLongitude() == null || coordinate.getLatitude() == null) {
log.warn("天地图逆地理编码:坐标为空");
return null;
}
if (StringUtils.isBlank(tiandituAK)) {
log.warn("天地图逆地理编码:未配置 map.tianditu_ak");
return null;
}
JSONObject postStr = new JSONObject();
postStr.put("lon", coordinate.getLongitude().doubleValue());
postStr.put("lat", coordinate.getLatitude().doubleValue());
postStr.put("ver", 1);
String encodedPostStr = urlEncode(postStr.toJSONString());
if (encodedPostStr == null) {
return null;
}
String response = sendGet(FastBeeConstant.MAP.TIANDITU_BASE + TIANDITU_PATH_GEOCODER
+ "?postStr=" + encodedPostStr + "&type=geocode&tk=" + tiandituAK);
if (StringUtils.isBlank(response)) {
return null;
}
JSONObject root = JSONObject.parseObject(response);
if (root == null || !"0".equals(String.valueOf(root.get("status")))) {
log.warn("天地图逆地理编码失败 status={} msg={}", root != null ? root.get("status") : null,
root != null ? root.getString("msg") : null);
return null;
}
JSONObject result = root.getJSONObject("result");
if (result == null) {
return null;
}
AddressInfo info = new AddressInfo();
info.setFormattedAddress(result.getString("formatted_address"));
JSONObject ac = result.getJSONObject("addressComponent");
if (ac != null) {
info.setCountry(ac.getString("nation"));
info.setProvince(ac.getString("province"));
info.setCity(ac.getString("city"));
info.setDistrict(ac.getString("county"));
info.setTown(ac.getString("town"));
info.setStreet(ac.getString("road"));
info.setBuilding(ac.getString("poi"));
}
return info;
}
private String urlEncode(String value) {
try {
return URLEncoder.encode(value, StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
log.error("天地图:参数 UTF-8 编码失败", e);
return null;
}
}
/**
* 天地图接口对 User-Agent 敏感:浏览器 UA(含 {@code HttpUtils} 固定的 IE6 UA)会被 WAF
* 识别为浏览器访问,从而拒绝服务端 Key(HTTP 403 权限类型错误)。
* 因此这里使用非浏览器 UA 直连,且请求必须携带 URL 编码后的完整查询串。
*/
private String sendGet(String url) {
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setRequestProperty("Accept", "*/*");
conn.setRequestProperty("User-Agent", "fastbee-server/3.8.5");
int code = conn.getResponseCode();
if (code != HttpURLConnection.HTTP_OK) {
log.warn("天地图请求失败 code={}", code);
return null;
}
try (BufferedReader in = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder result = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
result.append(line);
}
return result.toString();
}
} catch (IOException e) {
log.error("天地图请求异常", e);
return null;
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
}

View File

@ -61,6 +61,24 @@ export function delProject(projectId) {
})
}
// 根据中文地址查询坐标(天地图地理编码,走后端服务端密钥)
export function getCoordinateByAddress(address) {
return request({
url: '/map/location/geocode',
method: 'get',
params: { address: address, provider: 'tianditu' }
})
}
// 根据经纬度查询中文地址(天地图逆地理编码,走后端服务端密钥)
export function getAddressByCoordinate(lng, lat) {
return request({
url: '/map/location/reverseGeocode',
method: 'get',
params: { lng: lng, lat: lat, provider: 'tianditu' }
})
}
// 根据负责人ID查询项目列表
export function listProjectByUser(userId) {
return request({

View File

@ -49,7 +49,7 @@
<el-tag :type="getStatusTagType(scope.row.projectStatus)">{{ getStatusText(scope.row.projectStatus) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="坐标" align="center" width="150">
<el-table-column label="坐标" align="center" width="180">
<template slot-scope="scope">
<span v-if="scope.row.longitude && scope.row.latitude">
{{ scope.row.longitude }}°, {{ scope.row.latitude }}°
@ -57,6 +57,14 @@
<span v-else style="color: #999">未设置</span>
</template>
</el-table-column>
<el-table-column label="地址" align="center" width="430">
<template slot-scope="scope">
<span v-if="scope.row.address">
{{ scope.row.address }}
</span>
<span v-else style="color: #999">未设置</span>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
<template slot-scope="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
@ -77,7 +85,7 @@
</el-card>
<!-- 添加或修改项目对话框 -->
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body :close-on-click-modal="false">
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-row :gutter="20">
<el-col :span="12">
@ -138,7 +146,7 @@
<el-input v-model="form.address" placeholder="请输入项目地址" :disabled="isView" />
</el-form-item>
<el-form-item label="项目描述" prop="projectDesc">
<el-input v-model="form.projectDesc" type="textarea" placeholder="请输入项目描述" :rows="3" :disabled="isView" />
<el-input v-model="form.projectDesc" type="textarea" placeholder="请输入项目描述" :rows="4" :disabled="isView" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
@ -153,8 +161,18 @@
</el-dialog>
<!-- 地图坐标选择对话框 -->
<el-dialog title="地图选点" :visible.sync="mapPickerOpen" width="800px" append-to-body @closed="disposeMap">
<div style="height: 400px; border: 1px solid #ddd; position: relative">
<el-dialog title="地图选点" :visible.sync="mapPickerOpen" width="900px" append-to-body @closed="disposeMap">
<div style="margin-bottom: 8px; display: flex">
<span style="white-space: nowrap; color: #606266; width: 100px; flex-shrink: 0;">按关键字搜索:</span>
<el-input v-model="pickerSearchAddress" placeholder="输入中文地址,如:北京市海淀区莲花池西路28号" clearable :disabled="geocoding" @keyup.enter.native="searchAddress" style="flex: 1" />
<el-button type="primary" :loading="geocoding" style="margin-left: 8px; flex-shrink: 0" @click="searchAddress">定位</el-button>
</div>
<div style="margin-bottom: 8px; display: flex; align-items: center">
<span style="white-space: nowrap; color: #606266; width: 100px; flex-shrink: 0;">按坐标搜索:</span>
<el-input v-model="pickerCoordinate" placeholder="请输入坐标进行搜索,经度在前,如:116.37304,39.92594" clearable :disabled="geocoding" @keyup.enter.native="locateByCoordinate" style="flex: 1" />
<el-button type="primary" style="margin-left: 8px; flex-shrink: 0" @click="locateByCoordinate">定位</el-button>
</div>
<div style="height: 500px; border: 1px solid #ddd; position: relative">
<div ref="mapContainer" style="width: 100%; height: 100%"></div>
<div v-if="geocoding" style="position: absolute; top: 8px; right: 8px; background: rgba(255,255,255,0.9); padding: 4px 10px; border-radius: 3px; font-size: 12px; z-index: 999">
<i class="el-icon-loading"></i> 地址解析中...
@ -175,7 +193,7 @@
</template>
<script>
import { listProject, getProject, delProject, addProject, updateProject } from '@/api/iot/project';
import { listProject, getProject, delProject, addProject, updateProject, getCoordinateByAddress, getAddressByCoordinate } from '@/api/iot/project';
import { listUserByScope } from '@/api/system/user';
import { DEFAULT_TIANDITU_AK, DEFAULT_TIANDITU_TILE_KEY, loadTiandituMap } from '@/utils/tdtMap';
import ProjectDeviceSelect from './device-select.vue';
@ -202,6 +220,8 @@ export default {
pickerLongitude: null,
pickerLatitude: null,
pickerAddress: '',
pickerSearchAddress: '',
pickerCoordinate: '',
geocoding: false,
queryParams: {
pageNum: 1,
@ -220,6 +240,10 @@ export default {
tiandituMap: null,
tiandituMarker: null,
tiandituGeocoder: null,
// 地图 click 监听(具名引用,便于销毁时移除,避免重复注册导致一次点击多次逆地理编码)
tiandituClickHandler: null,
// 当前进行中的逆地理编码坐标,用于去重
geocodeRequestKey: '',
};
},
created() {
@ -398,29 +422,134 @@ export default {
// ==================== 地图选点 ====================
/** 打开地图选点 */
openMapPicker() {
this.pickerLongitude = this.form.longitude || null;
this.pickerLatitude = this.form.latitude || null;
this.pickerLongitude = this.form.longitude != null ? this.form.longitude : null;
this.pickerLatitude = this.form.latitude != null ? this.form.latitude : null;
this.pickerAddress = this.form.address || '';
this.pickerSearchAddress = this.form.address || '';
this.pickerCoordinate = (this.form.longitude != null && this.form.latitude != null)
? `${this.form.longitude},${this.form.latitude}`
: '';
this.mapPickerOpen = true;
this.$nextTick(() => {
this.initTiandituMap();
});
},
/** 根据经纬度定位 */
locateByCoordinate() {
const parts = (this.pickerCoordinate || '').trim().split(/[,,]/);
if (parts.length !== 2) {
this.$message.warning('请输入经度,纬度,经度在前,用英文逗号隔开');
return;
}
const lng = Number(parts[0].replace('°','').trim());
const lat = Number(parts[1].replace('°','').trim());
if (Number.isNaN(lng) || Number.isNaN(lat)) {
this.$message.warning('请输入有效的经度和纬度');
return;
}
if (lng < -180 || lng > 180) {
this.$message.warning('经度范围为 -180 ~ 180');
return;
}
if (lat < -90 || lat > 90) {
this.$message.warning('纬度范围为 -90 ~ 90');
return;
}
this.pickerLongitude = lng;
this.pickerLatitude = lat;
this.form.longitude = lng;
this.form.latitude = lat;
this.placeMarker(lng, lat, true);
this.reverseGeocode(lng, lat);
},
/** 正向地理编码:中文地址 → 经纬度 */
searchAddress() {
const address = (this.pickerSearchAddress || '').trim();
if (!address) {
this.$message.warning('请输入中文地址');
return;
}
this.geocoding = true;
getCoordinateByAddress(address).then(response => {
const data = response.data;
if (!data || data.longitude == null || data.latitude == null) {
this.$message.warning('未找到该地址的坐标,请尝试更详细的地址');
return;
}
this.pickerLongitude = Number(data.longitude);
this.pickerLatitude = Number(data.latitude);
this.pickerAddress = address;
this.form.longitude = this.pickerLongitude;
this.form.latitude = this.pickerLatitude;
this.form.address = address;
this.placeMarker(this.pickerLongitude, this.pickerLatitude, true);
}).catch(() => {
// 错误提示由 request 拦截器统一处理
}).finally(() => {
this.geocoding = false;
});
},
/** 在地图上放置标记(可选中并居中地图) */
placeMarker(lng, lat, center) {
const map = this.tiandituMap;
if (!map || !window.T) return;
if (this.tiandituMarker) {
map.removeOverLay(this.tiandituMarker);
}
this.tiandituMarker = new window.T.Marker(new window.T.LngLat(lng, lat));
map.addOverLay(this.tiandituMarker);
if (center) {
// 天地图 v4 Map 没有 setCenter,且 panTo 是带动画的平移:
// 若紧接着调用 setZoom,setZoom 会读取尚未移动完的旧中心并中断平移动画,
// 导致视野中心与目标点不一致。这里用 centerAndZoom 一步完成定位+缩放。
let zoom = typeof map.getZoom === 'function' ? map.getZoom() : 12;
if (zoom < 12) {
zoom = 12;
}
if (typeof map.centerAndZoom === 'function') {
map.centerAndZoom(new window.T.LngLat(lng, lat), zoom);
} else if (typeof map.panTo === 'function') {
map.panTo(new window.T.LngLat(lng, lat));
}
}
},
/** 初始化天地图 */
initTiandituMap() {
loadTiandituMap().then(T => {
const container = this.$refs.mapContainer;
if (!container) return;
const centerLng = this.pickerLongitude || 104.0;
const centerLat = this.pickerLatitude || 35.0;
const zoom = (this.pickerLongitude && this.pickerLatitude) ? 12 : 4;
const hasCoord = this.pickerLongitude != null && this.pickerLatitude != null;
const centerLng = hasCoord ? this.pickerLongitude : 104.0;
const centerLat = hasCoord ? this.pickerLatitude : 35.0;
const zoom = hasCoord ? 12 : 4;
// 地图实例已存在则复用:只更新中心与标记,避免重复注册 click 监听
if (this.tiandituMap) {
if (hasCoord) {
this.placeMarker(this.pickerLongitude, this.pickerLatitude, true);
}
return;
}
// 天地图 v4 没有 destroy(),先清理旧实例再重建
this.resetMap();
const map = new T.Map(container);
map.centerAndZoom(new T.LngLat(centerLng, centerLat), zoom);
// 天地图 v4 默认关闭部分交互,需显式启用:拖拽、滚轮缩放、双击缩放、连续缩放
if (typeof map.enableDrag === 'function') {
map.enableDrag();
}
if (typeof map.enableScrollWheelZoom === 'function') {
map.enableScrollWheelZoom();
}
if (typeof map.enableDoubleClickZoom === 'function') {
map.enableDoubleClickZoom();
}
if (typeof map.enableContinuousZoom === 'function') {
map.enableContinuousZoom();
}
map.addControl(new T.Control.Zoom());
this.tiandituGeocoder = T.Geocoder ? new T.Geocoder() : null;
@ -432,29 +561,25 @@ export default {
map.addLayer(new T.TileLayer(`${protocol}//t{s}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=${tk}`, { subdomains: '01234567' }));
// 已有坐标则添加标记
if (this.pickerLongitude && this.pickerLatitude) {
if (hasCoord) {
const point = new T.LngLat(this.pickerLongitude, this.pickerLatitude);
this.tiandituMarker = new T.Marker(point);
map.addOverLay(this.tiandituMarker);
}
// 点击选点
map.addEventListener('click', (e) => {
this.tiandituClickHandler = (e) => {
const lnglat = e.lnglat;
this.pickerLongitude = Number(lnglat.getLng().toFixed(6));
this.pickerLatitude = Number(lnglat.getLat().toFixed(6));
// 同步更新表单坐标字段
this.form.longitude = this.pickerLongitude;
this.form.latitude = this.pickerLatitude;
if (this.tiandituMarker) {
map.removeOverLay(this.tiandituMarker);
}
this.tiandituMarker = new T.Marker(new T.LngLat(this.pickerLongitude, this.pickerLatitude));
map.addOverLay(this.tiandituMarker);
this.placeMarker(this.pickerLongitude, this.pickerLatitude, false);
this.reverseGeocode(this.pickerLongitude, this.pickerLatitude);
});
};
map.addEventListener('click', this.tiandituClickHandler);
this.tiandituMap = map;
}).catch((err) => {
@ -462,8 +587,32 @@ export default {
this.$message.error('天地图加载失败,请检查网络后重试');
});
},
/** 逆地理编码:经纬度 → 地址 */
/** 逆地理编码:经纬度 → 中文地址(优先走后端天地图服务端接口) */
reverseGeocode(lng, lat) {
const key = `${Number(lng).toFixed(6)},${Number(lat).toFixed(6)}`;
// 相同坐标的请求已在进行中,直接忽略,避免一次点击触发多次请求
if (this.geocoding && this.geocodeRequestKey === key) {
return;
}
this.geocodeRequestKey = key;
this.geocoding = true;
getAddressByCoordinate(lng, lat).then(response => {
if (this.geocodeRequestKey !== key) return;
const address = this.buildAddressFromInfo(response.data);
if (address) {
this.pickerAddress = address;
this.form.address = address;
}
this.geocoding = false;
}).catch(() => {
if (this.geocodeRequestKey !== key) return;
// 后端接口失败时回退到天地图 JS API / JSONP
this.geocoding = false;
this.reverseGeocodeFallback(lng, lat);
});
},
/** 逆地理编码兜底:天地图 JS API,不可用时走 JSONP */
reverseGeocodeFallback(lng, lat) {
this.geocoding = true;
if (this.tiandituGeocoder && window.T) {
this.tiandituGeocoder.getLocation(new window.T.LngLat(lng, lat), result => {
@ -493,6 +642,13 @@ export default {
};
document.head.appendChild(script);
},
/** 后端返回的 AddressInfo → 中文地址 */
buildAddressFromInfo(info) {
if (!info) return '';
if (info.formattedAddress) return info.formattedAddress;
return [info.province, info.city, info.district, info.town, info.street, info.streetNumber, info.building]
.filter(Boolean).join('');
},
/** 设置逆地理地址 */
applyGeocodeAddress(response) {
try {
@ -522,6 +678,11 @@ export default {
resetMap() {
if (this.tiandituMap) {
try {
// 先移除 click 监听,否则天地图 v4 会把监听绑在同一个容器元素上,
// 多次打开弹窗后一次点击会触发所有旧监听,产生重复的逆地理编码请求
if (this.tiandituClickHandler && typeof this.tiandituMap.removeEventListener === 'function') {
this.tiandituMap.removeEventListener('click', this.tiandituClickHandler);
}
if (typeof this.tiandituMap.clearOverLays === 'function') {
this.tiandituMap.clearOverLays();
}
@ -529,6 +690,7 @@ export default {
console.warn('清理天地图覆盖物失败:', e);
}
this.tiandituMap = null;
this.tiandituClickHandler = null;
}
const container = this.$refs.mapContainer;
if (container) {