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
|
baidu_ak: 0SD4KrYeMXVu8Ox2pxQGDOw3NmDYiKIw
|
||||||
gaode_ak: 3af85be39e2f28334097781349336157
|
gaode_ak: 3af85be39e2f28334097781349336157
|
||||||
gaode_secret: d0561ad20142e74f8c59edc5deafdbe4
|
gaode_secret: d0561ad20142e74f8c59edc5deafdbe4
|
||||||
|
tianditu_ak: b920e6076a06aa279c974f5f76cf746e
|
||||||
|
|||||||
@ -389,6 +389,9 @@ public interface FastBeeConstant {
|
|||||||
// 高德(https://lbs.amap.com/api/webservice/guide/api/georegeo)
|
// 高德(https://lbs.amap.com/api/webservice/guide/api/georegeo)
|
||||||
String GAODE_BASE = "https://restapi.amap.com";
|
String GAODE_BASE = "https://restapi.amap.com";
|
||||||
|
|
||||||
|
// 天地图(http://lbs.tianditu.gov.cn/server/geocodinginterface.html)
|
||||||
|
String TIANDITU_BASE = "https://api.tianditu.gov.cn";
|
||||||
|
|
||||||
// 腾讯 TENCENT(预留)
|
// 腾讯 TENCENT(预留)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,7 +33,9 @@ public interface ILocationService {
|
|||||||
* @param coordinate 不同(BD09,GCJ02,WGS84)实现类坐标
|
* @param coordinate 不同(BD09,GCJ02,WGS84)实现类坐标
|
||||||
* @return 世界坐标系
|
* @return 世界坐标系
|
||||||
*/
|
*/
|
||||||
Coordinate convertToWgs84(Coordinate coordinate);
|
default Coordinate convertToWgs84(Coordinate coordinate) {
|
||||||
|
return coordinate;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,5 +43,7 @@ public interface ILocationService {
|
|||||||
* @param wgs84Coordinate WGS84 坐标
|
* @param wgs84Coordinate WGS84 坐标
|
||||||
* @return 转换后的目标坐标系坐标
|
* @return 转换后的目标坐标系坐标
|
||||||
*/
|
*/
|
||||||
Coordinate convertPlatformCoordinateFromWgs84(Coordinate wgs84Coordinate);
|
default Coordinate convertPlatformCoordinateFromWgs84(Coordinate wgs84Coordinate) {
|
||||||
|
return wgs84Coordinate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,9 @@ public class LocationServiceFactory {
|
|||||||
@Resource
|
@Resource
|
||||||
private ILocationService gaodeLocationService;
|
private ILocationService gaodeLocationService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ILocationService tiandituLocationService;
|
||||||
|
|
||||||
// 从配置文件读取当前使用的服务商
|
// 从配置文件读取当前使用的服务商
|
||||||
@Value("${map.provider:baidu}")
|
@Value("${map.provider:baidu}")
|
||||||
private String provider;
|
private String provider;
|
||||||
@ -40,6 +43,8 @@ public class LocationServiceFactory {
|
|||||||
}
|
}
|
||||||
if ("gaode".equalsIgnoreCase(provider)) {
|
if ("gaode".equalsIgnoreCase(provider)) {
|
||||||
return gaodeLocationService;
|
return gaodeLocationService;
|
||||||
|
} else if ("tianditu".equalsIgnoreCase(provider)) {
|
||||||
|
return tiandituLocationService;
|
||||||
} /*else if ("tencent".equalsIgnoreCase(provider)) {
|
} /*else if ("tencent".equalsIgnoreCase(provider)) {
|
||||||
return tencentLocationService;
|
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查询项目列表
|
// 根据负责人ID查询项目列表
|
||||||
export function listProjectByUser(userId) {
|
export function listProjectByUser(userId) {
|
||||||
return request({
|
return request({
|
||||||
|
|||||||
@ -49,7 +49,7 @@
|
|||||||
<el-tag :type="getStatusTagType(scope.row.projectStatus)">{{ getStatusText(scope.row.projectStatus) }}</el-tag>
|
<el-tag :type="getStatusTagType(scope.row.projectStatus)">{{ getStatusText(scope.row.projectStatus) }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="坐标" align="center" width="150">
|
<el-table-column label="坐标" align="center" width="180">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<span v-if="scope.row.longitude && scope.row.latitude">
|
<span v-if="scope.row.longitude && scope.row.latitude">
|
||||||
{{ scope.row.longitude }}°, {{ scope.row.latitude }}°
|
{{ scope.row.longitude }}°, {{ scope.row.latitude }}°
|
||||||
@ -57,6 +57,14 @@
|
|||||||
<span v-else style="color: #999">未设置</span>
|
<span v-else style="color: #999">未设置</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</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">
|
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
|
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
|
||||||
@ -77,7 +85,7 @@
|
|||||||
</el-card>
|
</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-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@ -138,7 +146,7 @@
|
|||||||
<el-input v-model="form.address" placeholder="请输入项目地址" :disabled="isView" />
|
<el-input v-model="form.address" placeholder="请输入项目地址" :disabled="isView" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="项目描述" prop="projectDesc">
|
<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-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
@ -153,8 +161,18 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 地图坐标选择对话框 -->
|
<!-- 地图坐标选择对话框 -->
|
||||||
<el-dialog title="地图选点" :visible.sync="mapPickerOpen" width="800px" append-to-body @closed="disposeMap">
|
<el-dialog title="地图选点" :visible.sync="mapPickerOpen" width="900px" append-to-body @closed="disposeMap">
|
||||||
<div style="height: 400px; border: 1px solid #ddd; position: relative">
|
<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 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">
|
<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> 地址解析中...
|
<i class="el-icon-loading"></i> 地址解析中...
|
||||||
@ -175,7 +193,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<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 { listUserByScope } from '@/api/system/user';
|
||||||
import { DEFAULT_TIANDITU_AK, DEFAULT_TIANDITU_TILE_KEY, loadTiandituMap } from '@/utils/tdtMap';
|
import { DEFAULT_TIANDITU_AK, DEFAULT_TIANDITU_TILE_KEY, loadTiandituMap } from '@/utils/tdtMap';
|
||||||
import ProjectDeviceSelect from './device-select.vue';
|
import ProjectDeviceSelect from './device-select.vue';
|
||||||
@ -202,6 +220,8 @@ export default {
|
|||||||
pickerLongitude: null,
|
pickerLongitude: null,
|
||||||
pickerLatitude: null,
|
pickerLatitude: null,
|
||||||
pickerAddress: '',
|
pickerAddress: '',
|
||||||
|
pickerSearchAddress: '',
|
||||||
|
pickerCoordinate: '',
|
||||||
geocoding: false,
|
geocoding: false,
|
||||||
queryParams: {
|
queryParams: {
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
@ -220,6 +240,10 @@ export default {
|
|||||||
tiandituMap: null,
|
tiandituMap: null,
|
||||||
tiandituMarker: null,
|
tiandituMarker: null,
|
||||||
tiandituGeocoder: null,
|
tiandituGeocoder: null,
|
||||||
|
// 地图 click 监听(具名引用,便于销毁时移除,避免重复注册导致一次点击多次逆地理编码)
|
||||||
|
tiandituClickHandler: null,
|
||||||
|
// 当前进行中的逆地理编码坐标,用于去重
|
||||||
|
geocodeRequestKey: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@ -398,29 +422,134 @@ export default {
|
|||||||
// ==================== 地图选点 ====================
|
// ==================== 地图选点 ====================
|
||||||
/** 打开地图选点 */
|
/** 打开地图选点 */
|
||||||
openMapPicker() {
|
openMapPicker() {
|
||||||
this.pickerLongitude = this.form.longitude || null;
|
this.pickerLongitude = this.form.longitude != null ? this.form.longitude : null;
|
||||||
this.pickerLatitude = this.form.latitude || null;
|
this.pickerLatitude = this.form.latitude != null ? this.form.latitude : null;
|
||||||
this.pickerAddress = this.form.address || '';
|
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.mapPickerOpen = true;
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.initTiandituMap();
|
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() {
|
initTiandituMap() {
|
||||||
loadTiandituMap().then(T => {
|
loadTiandituMap().then(T => {
|
||||||
const container = this.$refs.mapContainer;
|
const container = this.$refs.mapContainer;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
const centerLng = this.pickerLongitude || 104.0;
|
const hasCoord = this.pickerLongitude != null && this.pickerLatitude != null;
|
||||||
const centerLat = this.pickerLatitude || 35.0;
|
const centerLng = hasCoord ? this.pickerLongitude : 104.0;
|
||||||
const zoom = (this.pickerLongitude && this.pickerLatitude) ? 12 : 4;
|
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(),先清理旧实例再重建
|
// 天地图 v4 没有 destroy(),先清理旧实例再重建
|
||||||
this.resetMap();
|
this.resetMap();
|
||||||
|
|
||||||
const map = new T.Map(container);
|
const map = new T.Map(container);
|
||||||
map.centerAndZoom(new T.LngLat(centerLng, centerLat), zoom);
|
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());
|
map.addControl(new T.Control.Zoom());
|
||||||
this.tiandituGeocoder = T.Geocoder ? new T.Geocoder() : null;
|
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' }));
|
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);
|
const point = new T.LngLat(this.pickerLongitude, this.pickerLatitude);
|
||||||
this.tiandituMarker = new T.Marker(point);
|
this.tiandituMarker = new T.Marker(point);
|
||||||
map.addOverLay(this.tiandituMarker);
|
map.addOverLay(this.tiandituMarker);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击选点
|
// 点击选点
|
||||||
map.addEventListener('click', (e) => {
|
this.tiandituClickHandler = (e) => {
|
||||||
const lnglat = e.lnglat;
|
const lnglat = e.lnglat;
|
||||||
this.pickerLongitude = Number(lnglat.getLng().toFixed(6));
|
this.pickerLongitude = Number(lnglat.getLng().toFixed(6));
|
||||||
this.pickerLatitude = Number(lnglat.getLat().toFixed(6));
|
this.pickerLatitude = Number(lnglat.getLat().toFixed(6));
|
||||||
// 同步更新表单坐标字段
|
// 同步更新表单坐标字段
|
||||||
this.form.longitude = this.pickerLongitude;
|
this.form.longitude = this.pickerLongitude;
|
||||||
this.form.latitude = this.pickerLatitude;
|
this.form.latitude = this.pickerLatitude;
|
||||||
|
this.placeMarker(this.pickerLongitude, this.pickerLatitude, false);
|
||||||
if (this.tiandituMarker) {
|
|
||||||
map.removeOverLay(this.tiandituMarker);
|
|
||||||
}
|
|
||||||
this.tiandituMarker = new T.Marker(new T.LngLat(this.pickerLongitude, this.pickerLatitude));
|
|
||||||
map.addOverLay(this.tiandituMarker);
|
|
||||||
|
|
||||||
this.reverseGeocode(this.pickerLongitude, this.pickerLatitude);
|
this.reverseGeocode(this.pickerLongitude, this.pickerLatitude);
|
||||||
});
|
};
|
||||||
|
map.addEventListener('click', this.tiandituClickHandler);
|
||||||
|
|
||||||
this.tiandituMap = map;
|
this.tiandituMap = map;
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
@ -462,8 +587,32 @@ export default {
|
|||||||
this.$message.error('天地图加载失败,请检查网络后重试');
|
this.$message.error('天地图加载失败,请检查网络后重试');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
/** 逆地理编码:经纬度 → 地址 */
|
/** 逆地理编码:经纬度 → 中文地址(优先走后端天地图服务端接口) */
|
||||||
reverseGeocode(lng, lat) {
|
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;
|
this.geocoding = true;
|
||||||
if (this.tiandituGeocoder && window.T) {
|
if (this.tiandituGeocoder && window.T) {
|
||||||
this.tiandituGeocoder.getLocation(new window.T.LngLat(lng, lat), result => {
|
this.tiandituGeocoder.getLocation(new window.T.LngLat(lng, lat), result => {
|
||||||
@ -493,6 +642,13 @@ export default {
|
|||||||
};
|
};
|
||||||
document.head.appendChild(script);
|
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) {
|
applyGeocodeAddress(response) {
|
||||||
try {
|
try {
|
||||||
@ -522,6 +678,11 @@ export default {
|
|||||||
resetMap() {
|
resetMap() {
|
||||||
if (this.tiandituMap) {
|
if (this.tiandituMap) {
|
||||||
try {
|
try {
|
||||||
|
// 先移除 click 监听,否则天地图 v4 会把监听绑在同一个容器元素上,
|
||||||
|
// 多次打开弹窗后一次点击会触发所有旧监听,产生重复的逆地理编码请求
|
||||||
|
if (this.tiandituClickHandler && typeof this.tiandituMap.removeEventListener === 'function') {
|
||||||
|
this.tiandituMap.removeEventListener('click', this.tiandituClickHandler);
|
||||||
|
}
|
||||||
if (typeof this.tiandituMap.clearOverLays === 'function') {
|
if (typeof this.tiandituMap.clearOverLays === 'function') {
|
||||||
this.tiandituMap.clearOverLays();
|
this.tiandituMap.clearOverLays();
|
||||||
}
|
}
|
||||||
@ -529,6 +690,7 @@ export default {
|
|||||||
console.warn('清理天地图覆盖物失败:', e);
|
console.warn('清理天地图覆盖物失败:', e);
|
||||||
}
|
}
|
||||||
this.tiandituMap = null;
|
this.tiandituMap = null;
|
||||||
|
this.tiandituClickHandler = null;
|
||||||
}
|
}
|
||||||
const container = this.$refs.mapContainer;
|
const container = this.$refs.mapContainer;
|
||||||
if (container) {
|
if (container) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user