20260812:1、天地图地理编码;逆地理编码;2、地图选点优化
This commit is contained in:
parent
f1f9965089
commit
1f197f80a1
@ -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);
|
||||
}
|
||||
}
|
||||
@ -231,3 +231,4 @@ map:
|
||||
baidu_ak: 0SD4KrYeMXVu8Ox2pxQGDOw3NmDYiKIw
|
||||
gaode_ak: 3af85be39e2f28334097781349336157
|
||||
gaode_secret: d0561ad20142e74f8c59edc5deafdbe4
|
||||
tianditu_ak: b920e6076a06aa279c974f5f76cf746e
|
||||
|
||||
@ -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(预留)
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}*/
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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({
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user