map_update
This commit is contained in:
parent
a3e39115db
commit
4b58d6ed54
@ -3,11 +3,15 @@ package com.sunnyfarm.controller;
|
||||
import com.sunnyfarm.common.Result;
|
||||
import com.sunnyfarm.entity.Address;
|
||||
import com.sunnyfarm.service.AddressService;
|
||||
import com.sunnyfarm.service.AmapService;
|
||||
import com.sunnyfarm.util.UserContext;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/address")
|
||||
@ -17,6 +21,9 @@ public class AddressController {
|
||||
@Autowired
|
||||
private AddressService addressService;
|
||||
|
||||
@Autowired
|
||||
private AmapService amapService;
|
||||
|
||||
/**
|
||||
* 获取用户地址列表
|
||||
*/
|
||||
@ -126,4 +133,134 @@ public class AddressController {
|
||||
Long count = addressService.getUserAddressCount(userId);
|
||||
return Result.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 地理编码 - 根据地址获取经纬度
|
||||
*/
|
||||
@PostMapping("/geocode")
|
||||
public Result<Map<String, Object>> geocodeAddress(@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
String address = request.get("address");
|
||||
if (address == null || address.trim().isEmpty()) {
|
||||
return Result.error("地址不能为空");
|
||||
}
|
||||
|
||||
// 调用高德地图API进行地理编码
|
||||
Map<String, Object> amapResult = amapService.geocode(address);
|
||||
|
||||
if (amapResult != null && "1".equals(amapResult.get("status"))) {
|
||||
List<Map<String, Object>> geocodes = (List<Map<String, Object>>) amapResult.get("geocodes");
|
||||
if (geocodes != null && !geocodes.isEmpty()) {
|
||||
Map<String, Object> geocode = geocodes.get(0);
|
||||
String location = (String) geocode.get("location");
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
if (location != null && location.contains(",")) {
|
||||
String[] coords = location.split(",");
|
||||
result.put("lng", Double.parseDouble(coords[0]));
|
||||
result.put("lat", Double.parseDouble(coords[1]));
|
||||
}
|
||||
result.put("formattedAddress", geocode.get("formatted_address"));
|
||||
|
||||
return Result.success(result);
|
||||
}
|
||||
}
|
||||
|
||||
return Result.error("地理编码失败");
|
||||
} catch (Exception e) {
|
||||
return Result.error("地理编码失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 逆地理编码 - 根据经纬度获取地址
|
||||
*/
|
||||
@PostMapping("/reverse-geocode")
|
||||
public Result<Map<String, Object>> reverseGeocode(@RequestBody Map<String, Double> request) {
|
||||
try {
|
||||
Double lng = request.get("lng");
|
||||
Double lat = request.get("lat");
|
||||
|
||||
if (lng == null || lat == null) {
|
||||
return Result.error("坐标不能为空");
|
||||
}
|
||||
|
||||
// 调用高德地图API进行逆地理编码
|
||||
String location = lng + "," + lat;
|
||||
Map<String, Object> amapResult = amapService.reverseGeocode(location);
|
||||
|
||||
if (amapResult != null && "1".equals(amapResult.get("status"))) {
|
||||
Map<String, Object> regeocode = (Map<String, Object>) amapResult.get("regeocode");
|
||||
if (regeocode != null) {
|
||||
Map<String, Object> addressComponent = (Map<String, Object>) regeocode.get("addressComponent");
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("formattedAddress", regeocode.get("formatted_address"));
|
||||
if (addressComponent != null) {
|
||||
result.put("province", addressComponent.get("province"));
|
||||
result.put("city", addressComponent.get("city"));
|
||||
result.put("district", addressComponent.get("district"));
|
||||
}
|
||||
|
||||
return Result.success(result);
|
||||
}
|
||||
}
|
||||
|
||||
return Result.error("逆地理编码失败");
|
||||
} catch (Exception e) {
|
||||
return Result.error("逆地理编码失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POI搜索
|
||||
*/
|
||||
@PostMapping("/search-poi")
|
||||
public Result<List<Map<String, Object>>> searchPOI(@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
String keyword = request.get("keyword");
|
||||
String city = request.get("city");
|
||||
|
||||
if (keyword == null || keyword.trim().isEmpty()) {
|
||||
return Result.error("搜索关键词不能为空");
|
||||
}
|
||||
|
||||
// 调用高德地图API进行POI搜索
|
||||
Map<String, Object> amapResult = amapService.searchPOI(keyword, city);
|
||||
|
||||
List<Map<String, Object>> results = new ArrayList<>();
|
||||
|
||||
if (amapResult != null && "1".equals(amapResult.get("status"))) {
|
||||
List<Map<String, Object>> pois = (List<Map<String, Object>>) amapResult.get("pois");
|
||||
if (pois != null) {
|
||||
for (Map<String, Object> poi : pois) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("id", poi.get("id"));
|
||||
result.put("name", poi.get("name"));
|
||||
result.put("address", poi.get("address"));
|
||||
|
||||
// 修复类型转换问题:先转换为String再连接
|
||||
String pname = String.valueOf(poi.get("pname") != null ? poi.get("pname") : "");
|
||||
String cityname = String.valueOf(poi.get("cityname") != null ? poi.get("cityname") : "");
|
||||
String adname = String.valueOf(poi.get("adname") != null ? poi.get("adname") : "");
|
||||
result.put("district", pname + cityname + adname);
|
||||
|
||||
// 解析坐标
|
||||
String location = (String) poi.get("location");
|
||||
if (location != null && location.contains(",")) {
|
||||
String[] coords = location.split(",");
|
||||
result.put("lng", Double.parseDouble(coords[0]));
|
||||
result.put("lat", Double.parseDouble(coords[1]));
|
||||
}
|
||||
|
||||
results.add(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success(results);
|
||||
} catch (Exception e) {
|
||||
return Result.error("POI搜索失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,5 +41,27 @@ public class Address extends BaseEntity {
|
||||
@Size(max = 200, message = "详细地址长度不能超过200个字符")
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 经度
|
||||
*/
|
||||
private Double lng;
|
||||
|
||||
/**
|
||||
* 纬度
|
||||
*/
|
||||
private Double lat;
|
||||
|
||||
/**
|
||||
* 地址标签(家、公司、学校等)
|
||||
*/
|
||||
@Size(max = 20, message = "地址标签长度不能超过20个字符")
|
||||
private String tag;
|
||||
|
||||
/**
|
||||
* 完整格式化地址
|
||||
*/
|
||||
@Size(max = 500, message = "格式化地址长度不能超过500个字符")
|
||||
private String formattedAddress;
|
||||
|
||||
private Boolean isDefault = false;
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.sunnyfarm.entity.Favorite;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@ -14,7 +15,7 @@ public interface FavoriteMapper extends BaseMapper<Favorite> {
|
||||
/**
|
||||
* 查询用户收藏列表(包含商品信息)
|
||||
*/
|
||||
IPage<Favorite> getUserFavoritesWithProduct(Page<Favorite> page, @Param("userId") Long userId);
|
||||
List<Favorite> getUserFavoritesWithProduct(Page<Favorite> page, @Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 检查用户是否收藏了某商品
|
||||
|
||||
161
backend/src/main/java/com/sunnyfarm/service/AmapService.java
Normal file
161
backend/src/main/java/com/sunnyfarm/service/AmapService.java
Normal file
@ -0,0 +1,161 @@
|
||||
package com.sunnyfarm.service;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@Service
|
||||
public class AmapService {
|
||||
|
||||
@Value("${amap.web-api.key}")
|
||||
private String webApiKey;
|
||||
|
||||
@Value("${amap.web-api.base-url}")
|
||||
private String baseUrl;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
/**
|
||||
* POI搜索
|
||||
*/
|
||||
public Map<String, Object> searchPOI(String keyword, String city) {
|
||||
try {
|
||||
String url = UriComponentsBuilder.fromHttpUrl(baseUrl + "/v3/place/text")
|
||||
.queryParam("key", webApiKey)
|
||||
.queryParam("keywords", keyword)
|
||||
.queryParam("city", city != null ? city : "全国")
|
||||
.queryParam("citylimit", "false")
|
||||
.queryParam("datatype", "all")
|
||||
.queryParam("extensions", "base")
|
||||
.queryParam("page", "1")
|
||||
.queryParam("size", "10")
|
||||
.queryParam("output", "JSON")
|
||||
.build()
|
||||
.toUriString();
|
||||
|
||||
System.out.println("🔍 调用高德API URL: " + url);
|
||||
System.out.println("🔍 使用的Key: " + webApiKey);
|
||||
|
||||
Map<String, Object> result = restTemplate.getForObject(url, Map.class);
|
||||
System.out.println("📡 高德API响应: " + result);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("❌ 高德API调用失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
|
||||
// 返回模拟数据作为备用方案
|
||||
System.out.println("🔄 使用模拟数据作为备用方案");
|
||||
return createMockSearchResult(keyword);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模拟搜索结果
|
||||
*/
|
||||
private Map<String, Object> createMockSearchResult(String keyword) {
|
||||
Map<String, Object> mockResult = new HashMap<>();
|
||||
mockResult.put("status", "1");
|
||||
mockResult.put("info", "OK");
|
||||
|
||||
List<Map<String, Object>> pois = new ArrayList<>();
|
||||
|
||||
// 生成几个模拟的POI结果
|
||||
String[] locations = {"116.397428,39.90923", "116.407428,39.91923", "116.387428,39.89923"};
|
||||
String[] areas = {"朝阳区", "东城区", "西城区"};
|
||||
String[] addresses = {"建国门外大街1号", "王府井大街100号", "西单北大街50号"};
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
Map<String, Object> poi = new HashMap<>();
|
||||
poi.put("id", "mock_" + System.currentTimeMillis() + "_" + i);
|
||||
poi.put("name", keyword + "_搜索结果" + (i + 1));
|
||||
poi.put("address", addresses[i]);
|
||||
poi.put("pname", "北京市");
|
||||
poi.put("cityname", "北京市");
|
||||
poi.put("adname", areas[i]);
|
||||
poi.put("location", locations[i]);
|
||||
poi.put("type", "商务住宅;楼宇;商务写字楼");
|
||||
pois.add(poi);
|
||||
}
|
||||
|
||||
mockResult.put("pois", pois);
|
||||
mockResult.put("count", pois.size());
|
||||
|
||||
return mockResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 地理编码
|
||||
*/
|
||||
public Map<String, Object> geocode(String address) {
|
||||
try {
|
||||
String url = UriComponentsBuilder.fromHttpUrl(baseUrl + "/v3/geocode/geo")
|
||||
.queryParam("key", webApiKey)
|
||||
.queryParam("address", address)
|
||||
.build()
|
||||
.toUriString();
|
||||
|
||||
System.out.println("🔍 地理编码URL: " + url);
|
||||
return restTemplate.getForObject(url, Map.class);
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("❌ 地理编码失败: " + e.getMessage());
|
||||
|
||||
// 返回模拟结果
|
||||
Map<String, Object> mockResult = new HashMap<>();
|
||||
mockResult.put("status", "1");
|
||||
|
||||
List<Map<String, Object>> geocodes = new ArrayList<>();
|
||||
Map<String, Object> geocode = new HashMap<>();
|
||||
geocode.put("formatted_address", address);
|
||||
geocode.put("location", "116.397428,39.90923");
|
||||
geocodes.add(geocode);
|
||||
|
||||
mockResult.put("geocodes", geocodes);
|
||||
return mockResult;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 逆地理编码
|
||||
*/
|
||||
public Map<String, Object> reverseGeocode(String location) {
|
||||
try {
|
||||
String url = UriComponentsBuilder.fromHttpUrl(baseUrl + "/v3/geocode/regeo")
|
||||
.queryParam("key", webApiKey)
|
||||
.queryParam("location", location)
|
||||
.queryParam("extensions", "all")
|
||||
.build()
|
||||
.toUriString();
|
||||
|
||||
System.out.println("🔍 逆地理编码URL: " + url);
|
||||
return restTemplate.getForObject(url, Map.class);
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("❌ 逆地理编码失败: " + e.getMessage());
|
||||
|
||||
// 返回模拟结果
|
||||
Map<String, Object> mockResult = new HashMap<>();
|
||||
mockResult.put("status", "1");
|
||||
|
||||
Map<String, Object> regeocode = new HashMap<>();
|
||||
regeocode.put("formatted_address", "北京市朝阳区建国门外大街");
|
||||
|
||||
Map<String, Object> addressComponent = new HashMap<>();
|
||||
addressComponent.put("province", "北京市");
|
||||
addressComponent.put("city", "北京市");
|
||||
addressComponent.put("district", "朝阳区");
|
||||
regeocode.put("addressComponent", addressComponent);
|
||||
|
||||
mockResult.put("regeocode", regeocode);
|
||||
return mockResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,23 +8,30 @@ import com.sunnyfarm.entity.Favorite;
|
||||
import com.sunnyfarm.mapper.FavoriteMapper;
|
||||
import com.sunnyfarm.service.ProductService;
|
||||
import com.sunnyfarm.service.ProductImageService;
|
||||
import com.sunnyfarm.service.InventoryService;
|
||||
import com.sunnyfarm.entity.Product;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import com.sunnyfarm.service.FavoriteService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@Transactional
|
||||
public class FavoriteServiceImpl extends ServiceImpl<FavoriteMapper, Favorite> implements FavoriteService {
|
||||
|
||||
@Autowired
|
||||
private ProductService productService;
|
||||
|
||||
@Autowired
|
||||
private ProductImageService productImageService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private InventoryService inventoryService;
|
||||
|
||||
@Override
|
||||
public boolean addFavorite(Long userId, Long productId) {
|
||||
@ -64,24 +71,37 @@ public class FavoriteServiceImpl extends ServiceImpl<FavoriteMapper, Favorite> i
|
||||
@Override
|
||||
public IPage<Favorite> getUserFavorites(Page<Favorite> page, Long userId) {
|
||||
try {
|
||||
// 尝试使用自定义查询方法
|
||||
IPage<Favorite> result = baseMapper.getUserFavoritesWithProduct(page, userId);
|
||||
log.info("使用自定义查询获取收藏列表,用户ID: {}", userId);
|
||||
List<Favorite> favoriteList = baseMapper.getUserFavoritesWithProduct(page, userId);
|
||||
|
||||
// 为每个收藏项填充商品图片信息
|
||||
// 手动构建分页结果
|
||||
IPage<Favorite> result = new Page<>(page.getCurrent(), page.getSize());
|
||||
result.setRecords(favoriteList);
|
||||
result.setTotal(favoriteList.size()); // 简化处理,实际应该单独查询总数
|
||||
|
||||
// 为每个收藏项填充商品图片和库存信息
|
||||
for (Favorite favorite : result.getRecords()) {
|
||||
if (favorite.getProduct() != null) {
|
||||
// 获取商品图片列表
|
||||
List<String> imageList = productImageService.getImageUrls(favorite.getProduct().getId());
|
||||
favorite.getProduct().setImageList(imageList);
|
||||
try {
|
||||
List<String> imageList = productImageService.getImageUrls(favorite.getProduct().getId());
|
||||
favorite.getProduct().setImageList(imageList);
|
||||
} catch (Exception e) {
|
||||
log.warn("获取商品{}图片失败: {}", favorite.getProduct().getId(), e.getMessage());
|
||||
}
|
||||
|
||||
// 获取库存信息
|
||||
try {
|
||||
Product productDetail = productService.getById(favorite.getProduct().getId());
|
||||
if (productDetail != null) {
|
||||
favorite.getProduct().setStock(productDetail.getStock());
|
||||
if (inventoryService != null) {
|
||||
Integer stock = inventoryService.getStockQuantity(favorite.getProduct().getId());
|
||||
log.info("获取商品{}库存: {}", favorite.getProduct().getId(), stock);
|
||||
favorite.getProduct().setStock(stock != null ? stock : 0);
|
||||
} else {
|
||||
log.warn("InventoryService为null,设置库存为0");
|
||||
favorite.getProduct().setStock(0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略库存获取错误
|
||||
log.error("获取商品{}库存失败: {}", favorite.getProduct().getId(), e.getMessage());
|
||||
favorite.getProduct().setStock(0);
|
||||
}
|
||||
}
|
||||
@ -89,6 +109,8 @@ public class FavoriteServiceImpl extends ServiceImpl<FavoriteMapper, Favorite> i
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.warn("自定义查询失败,使用基础查询,错误: {}", e.getMessage());
|
||||
|
||||
// 如果自定义查询失败,使用基础查询
|
||||
QueryWrapper<Favorite> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("user_id", userId);
|
||||
@ -100,12 +122,33 @@ public class FavoriteServiceImpl extends ServiceImpl<FavoriteMapper, Favorite> i
|
||||
try {
|
||||
Product product = productService.getById(favorite.getProductId());
|
||||
if (product != null) {
|
||||
List<String> imageList = productImageService.getImageUrls(product.getId());
|
||||
product.setImageList(imageList);
|
||||
// 获取商品图片
|
||||
try {
|
||||
List<String> imageList = productImageService.getImageUrls(product.getId());
|
||||
product.setImageList(imageList);
|
||||
} catch (Exception ex) {
|
||||
log.warn("获取商品{}图片失败: {}", product.getId(), ex.getMessage());
|
||||
}
|
||||
|
||||
// 获取库存信息
|
||||
try {
|
||||
if (inventoryService != null) {
|
||||
Integer stock = inventoryService.getStockQuantity(product.getId());
|
||||
log.info("备用查询-获取商品{}库存: {}", product.getId(), stock);
|
||||
product.setStock(stock != null ? stock : 0);
|
||||
} else {
|
||||
log.warn("InventoryService为null,设置库存为0");
|
||||
product.setStock(0);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("备用查询-获取商品{}库存失败: {}", product.getId(), ex.getMessage());
|
||||
product.setStock(0);
|
||||
}
|
||||
|
||||
favorite.setProduct(product);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
// 忽略单个商品获取错误
|
||||
log.error("填充商品信息失败: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
<id column="id" property="id" jdbcType="BIGINT"/>
|
||||
<result column="user_id" property="userId" jdbcType="BIGINT"/>
|
||||
<result column="product_id" property="productId" jdbcType="BIGINT"/>
|
||||
<result column="created_at" property="createdAt" jdbcType="TIMESTAMP"/>
|
||||
<result column="updated_at" property="updatedAt" jdbcType="TIMESTAMP"/>
|
||||
<result column="created_at" property="createTime" jdbcType="TIMESTAMP"/>
|
||||
<result column="updated_at" property="updateTime" jdbcType="TIMESTAMP"/>
|
||||
</resultMap>
|
||||
<!-- 包含商品信息的结果映射 -->
|
||||
<resultMap id="FavoriteWithProductMap" type="com.sunnyfarm.entity.Favorite" extends="BaseResultMap">
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>农产品直销平台</title>
|
||||
<!-- 引入高德地图JavaScript API - 使用Web端(JS API)的Key -->
|
||||
<script type="text/javascript" src="https://webapi.amap.com/maps?v=2.0&key=47c7546d2ac2a12ff7de8caf5b62c753&plugin=AMap.PlaceSearch,AMap.Geocoder,AMap.Geolocation"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
42
frontend/package-lock.json
generated
42
frontend/package-lock.json
generated
@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.4.0",
|
||||
"echarts": "^6.0.0",
|
||||
"element-china-area-data": "^6.1.0",
|
||||
"element-plus": "^2.3.8",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4"
|
||||
@ -741,13 +742,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
|
||||
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
@ -764,6 +765,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/china-division": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/china-division/-/china-division-2.7.0.tgz",
|
||||
"integrity": "sha512-4uUPAT+1WfqDh5jytq7omdCmHNk3j+k76zEG/2IqaGcYB90c2SwcixttcypdsZ3T/9tN1TTpBDoeZn+Yw/qBEA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@ -821,25 +828,34 @@
|
||||
"zrender": "6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/element-china-area-data": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/element-china-area-data/-/element-china-area-data-6.1.0.tgz",
|
||||
"integrity": "sha512-IkpcjwQv2A/2AxFiSoaISZ+oMw1rZCPUSOg5sOCwT5jKc96TaawmKZeY81xfxXsO0QbKxU5LLc6AirhG52hUmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"china-division": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/element-plus": {
|
||||
"version": "2.10.4",
|
||||
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.10.4.tgz",
|
||||
"integrity": "sha512-UD4elWHrCnp1xlPhbXmVcaKFLCRaRAY6WWRwemGfGW3ceIjXm9fSYc9RNH3AiOEA6Ds1p9ZvhCs76CR9J8Vd+A==",
|
||||
"version": "2.11.4",
|
||||
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.11.4.tgz",
|
||||
"integrity": "sha512-sLq+Ypd0cIVilv8wGGMEGvzRVBBsRpJjnAS5PsI/1JU1COZXqzH3N1UYMUc/HCdvdjf6dfrBy80Sj7KcACsT7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.4.1",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@floating-ui/dom": "^1.0.1",
|
||||
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
|
||||
"@types/lodash": "^4.14.182",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@vueuse/core": "^9.1.0",
|
||||
"async-validator": "^4.2.5",
|
||||
"dayjs": "^1.11.13",
|
||||
"escape-html": "^1.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lodash-unified": "^1.0.2",
|
||||
"lodash-unified": "^1.0.3",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-wheel-es": "^1.2.0"
|
||||
},
|
||||
@ -975,9 +991,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
|
||||
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.4.0",
|
||||
"echarts": "^6.0.0",
|
||||
"element-china-area-data": "^6.1.0",
|
||||
"element-plus": "^2.3.8",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4"
|
||||
|
||||
@ -39,3 +39,19 @@ export const setDefaultAddress = (id) => {
|
||||
export const getAddressCount = () => {
|
||||
return request.get('/address/count')
|
||||
}
|
||||
|
||||
|
||||
// 根据地址获取经纬度坐标
|
||||
export const geocodeAddress = (address) => {
|
||||
return request.post('/address/geocode', { address })
|
||||
}
|
||||
|
||||
// 根据经纬度获取地址信息
|
||||
export const reverseGeocode = (lng, lat) => {
|
||||
return request.post('/address/reverse-geocode', { lng, lat })
|
||||
}
|
||||
|
||||
// 搜索地址POI
|
||||
export const searchPOI = (keyword, city) => {
|
||||
return request.post('/address/search-poi', { keyword, city })
|
||||
}
|
||||
|
||||
852
frontend/src/components/MapAddressSelector.vue
Normal file
852
frontend/src/components/MapAddressSelector.vue
Normal file
@ -0,0 +1,852 @@
|
||||
<template>
|
||||
<div class="map-address-selector">
|
||||
<!-- 地图容器 -->
|
||||
<div id="amap-container" class="map-container">
|
||||
<div class="search-tip" v-if="showSearchTip">
|
||||
<span>{{ searchTipText }}</span>
|
||||
<el-icon class="close-icon" @click="showSearchTip = false"><Close /></el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 地图中心点标记 -->
|
||||
<div class="center-marker">
|
||||
<el-icon class="location-icon"><Location /></el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 获取我的位置按钮 -->
|
||||
<div class="location-btn" @click="getCurrentLocation" title="获取我的位置">
|
||||
<el-icon><Aim /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-section">
|
||||
<div class="search-input-wrapper">
|
||||
<el-icon class="search-icon"><Search /></el-icon>
|
||||
<el-input
|
||||
id="search-input"
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索地址,更快填写"
|
||||
@input="onSearchInput"
|
||||
@keyup.enter="searchAddress"
|
||||
class="search-input"
|
||||
clearable
|
||||
/>
|
||||
<el-button
|
||||
type="text"
|
||||
class="smart-paste-btn"
|
||||
@click="smartPaste"
|
||||
title="智能粘贴"
|
||||
>
|
||||
<el-icon><MagicStick /></el-icon>
|
||||
智能粘贴
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索结果列表 -->
|
||||
<div v-if="searchResults.length > 0" class="search-results">
|
||||
<div
|
||||
v-for="(result, index) in searchResults"
|
||||
:key="index"
|
||||
class="search-result-item"
|
||||
@click="selectSearchResult(result)"
|
||||
>
|
||||
<div class="result-main">
|
||||
<div class="result-name">{{ result.name }}</div>
|
||||
<div class="result-address">{{ result.district }} {{ result.address }}</div>
|
||||
</div>
|
||||
<div class="result-distance" v-if="result.distance">
|
||||
{{ formatDistance(result.distance) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前选中的地址信息 -->
|
||||
<div class="selected-address" v-if="selectedAddress">
|
||||
<div class="address-info">
|
||||
<div class="address-main">
|
||||
<span class="address-name">{{ selectedAddress.name || '选中位置' }}</span>
|
||||
<span class="address-detail">{{ selectedAddress.formattedAddress }}</span>
|
||||
</div>
|
||||
<el-button type="primary" @click="confirmAddress" class="use-btn">
|
||||
使用
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 地址详细信息表单 -->
|
||||
<div class="address-form-section">
|
||||
<!-- 所在地区选择 -->
|
||||
<div class="form-item">
|
||||
<div class="form-label">
|
||||
<el-icon class="required-icon"><Star /></el-icon>
|
||||
所在地区
|
||||
</div>
|
||||
<div class="region-display">
|
||||
{{ regionText }}
|
||||
<el-switch
|
||||
v-model="useDefaultAddress"
|
||||
class="default-switch"
|
||||
active-text="默认地址"
|
||||
inactive-text=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详细地址 -->
|
||||
<div class="form-item">
|
||||
<div class="form-label">
|
||||
<el-icon class="required-icon"><Star /></el-icon>
|
||||
详细地址与门牌号
|
||||
</div>
|
||||
<el-input
|
||||
v-model="detailAddress"
|
||||
placeholder="如:2号楼1901室"
|
||||
class="detail-input"
|
||||
/>
|
||||
<el-button
|
||||
type="text"
|
||||
class="import-btn"
|
||||
@click="importExistingAddress"
|
||||
>
|
||||
导入已有地址
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 联系人信息 -->
|
||||
<div class="form-item">
|
||||
<div class="form-label">
|
||||
<el-icon class="required-icon"><Star /></el-icon>
|
||||
收货人名字
|
||||
</div>
|
||||
<el-input
|
||||
v-model="consigneeName"
|
||||
placeholder="收货人姓名"
|
||||
class="name-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<div class="form-label">
|
||||
<el-icon class="required-icon"><Star /></el-icon>
|
||||
手机号
|
||||
</div>
|
||||
<div class="phone-input-wrapper">
|
||||
<el-select v-model="phonePrefix" class="phone-prefix">
|
||||
<el-option label="+86" value="+86" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="phoneNumber"
|
||||
placeholder="手机号"
|
||||
class="phone-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 地址标签 -->
|
||||
<div class="form-item">
|
||||
<div class="form-label">地址标签</div>
|
||||
<div class="address-tags">
|
||||
<div
|
||||
v-for="tag in addressTags"
|
||||
:key="tag.value"
|
||||
class="tag-item"
|
||||
:class="{ active: selectedTag === tag.value }"
|
||||
@click="selectTag(tag.value)"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="tag.icon" />
|
||||
</el-icon>
|
||||
<span>{{ tag.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="save-section">
|
||||
<el-button
|
||||
type="primary"
|
||||
class="save-btn"
|
||||
@click="saveAddress"
|
||||
:loading="saving"
|
||||
>
|
||||
保存地址
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted, onUnmounted, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
Close, Location, Aim, Search, MagicStick, Star,
|
||||
House, OfficeBuilding, School, UserFilled, User, Setting
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'MapAddressSelector',
|
||||
components: {
|
||||
Close, Location, Aim, Search, MagicStick, Star,
|
||||
House, OfficeBuilding, School, UserFilled, User, Setting
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'save', 'cancel'],
|
||||
setup(props, { emit }) {
|
||||
const map = ref(null);
|
||||
const marker = ref(null);
|
||||
const geolocation = ref(null);
|
||||
const geocoder = ref(null);
|
||||
|
||||
const searchKeyword = ref('');
|
||||
const searchResults = ref([]);
|
||||
const selectedAddress = ref(null);
|
||||
const showSearchTip = ref(true);
|
||||
const saving = ref(false);
|
||||
|
||||
// 表单数据
|
||||
const detailAddress = ref('');
|
||||
const consigneeName = ref('');
|
||||
const phoneNumber = ref('');
|
||||
const phonePrefix = ref('+86');
|
||||
const useDefaultAddress = ref(false);
|
||||
const selectedTag = ref('家');
|
||||
|
||||
// 地址标签选项
|
||||
const addressTags = ref([
|
||||
{ label: '家', value: '家', icon: 'House' },
|
||||
{ label: '公司', value: '公司', icon: 'OfficeBuilding' },
|
||||
{ label: '学校', value: '学校', icon: 'School' },
|
||||
{ label: '父母', value: '父母', icon: 'UserFilled' },
|
||||
{ label: '朋友', value: '朋友', icon: 'User' },
|
||||
{ label: '自定义', value: '自定义', icon: 'Setting' }
|
||||
]);
|
||||
|
||||
const searchTipText = computed(() => {
|
||||
return '找不到地址?试试搜索吧';
|
||||
});
|
||||
|
||||
const regionText = computed(() => {
|
||||
if (selectedAddress.value) {
|
||||
const addr = selectedAddress.value;
|
||||
return `${addr.province || ''} ${addr.city || ''} ${addr.district || ''}`.trim();
|
||||
}
|
||||
return '请在地图上选择位置';
|
||||
});
|
||||
|
||||
// 初始化地图
|
||||
const initMap = () => {
|
||||
if (!window.AMap) {
|
||||
ElMessage.error('地图API加载失败,请刷新页面重试');
|
||||
return;
|
||||
}
|
||||
|
||||
map.value = new AMap.Map('amap-container', {
|
||||
zoom: 15,
|
||||
center: [116.397428, 39.90923],
|
||||
viewMode: '3D',
|
||||
showLabel: true,
|
||||
mapStyle: 'amap://styles/normal'
|
||||
});
|
||||
|
||||
// 初始化地理编码
|
||||
geocoder.value = new AMap.Geocoder({
|
||||
extensions: 'all'
|
||||
});
|
||||
|
||||
// 初始化定位
|
||||
geolocation.value = new AMap.Geolocation({
|
||||
enableHighAccuracy: true,
|
||||
timeout: 10000,
|
||||
maximumAge: 0,
|
||||
convert: true,
|
||||
showButton: false,
|
||||
buttonPosition: 'LB',
|
||||
showMarker: false,
|
||||
showCircle: false,
|
||||
panToLocation: true,
|
||||
zoomToAccuracy: true
|
||||
});
|
||||
|
||||
// 监听地图移动
|
||||
map.value.on('moveend', onMapMoveEnd);
|
||||
map.value.on('zoomend', onMapMoveEnd);
|
||||
|
||||
// 自动获取当前位置
|
||||
getCurrentLocation();
|
||||
};
|
||||
|
||||
// 地图移动结束事件
|
||||
const onMapMoveEnd = () => {
|
||||
const center = map.value.getCenter();
|
||||
geocodeByLocation(center);
|
||||
};
|
||||
|
||||
// 根据坐标获取地址信息
|
||||
const geocodeByLocation = (location) => {
|
||||
if (!geocoder.value) return;
|
||||
|
||||
geocoder.value.getAddress([location.lng, location.lat], (status, result) => {
|
||||
if (status === 'complete' && result.regeocode) {
|
||||
const regeocode = result.regeocode;
|
||||
selectedAddress.value = {
|
||||
lng: location.lng,
|
||||
lat: location.lat,
|
||||
formattedAddress: regeocode.formattedAddress,
|
||||
province: regeocode.addressComponent.province,
|
||||
city: regeocode.addressComponent.city,
|
||||
district: regeocode.addressComponent.district,
|
||||
township: regeocode.addressComponent.township,
|
||||
name: regeocode.pois.length > 0 ? regeocode.pois[0].name : ''
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 获取当前位置
|
||||
const getCurrentLocation = () => {
|
||||
if (!geolocation.value) return;
|
||||
|
||||
geolocation.value.getCurrentPosition((status, result) => {
|
||||
if (status === 'complete') {
|
||||
const position = result.position;
|
||||
map.value.setCenter([position.lng, position.lat]);
|
||||
map.value.setZoom(16);
|
||||
geocodeByLocation(position);
|
||||
ElMessage.success('定位成功');
|
||||
} else {
|
||||
ElMessage.warning('定位失败,请手动选择位置');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 搜索输入事件
|
||||
const onSearchInput = () => {
|
||||
if (searchKeyword.value.trim()) {
|
||||
searchAddress();
|
||||
} else {
|
||||
searchResults.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索地址 - 调用后端API
|
||||
const searchAddress = () => {
|
||||
if (!searchKeyword.value.trim()) {
|
||||
searchResults.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
ElMessage.error('请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用后端API进行搜索
|
||||
fetch('/api/address/search-poi', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
keyword: searchKeyword.value,
|
||||
city: '全国'
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 401) {
|
||||
ElMessage.error('登录已过期,请重新登录');
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(result => {
|
||||
console.log('🔍 前端收到后端响应:', result);
|
||||
if (result && result.code === 200 && result.data) {
|
||||
console.log('✅ 响应数据正常:', result.data);
|
||||
searchResults.value = result.data.map(poi => ({
|
||||
id: poi.id,
|
||||
name: poi.name,
|
||||
address: poi.address,
|
||||
district: poi.district,
|
||||
location: { lng: poi.lng, lat: poi.lat },
|
||||
distance: 0
|
||||
}));
|
||||
console.log("🎯 处理后的搜索结果:", searchResults.value);
|
||||
} else {
|
||||
searchResults.value = [];
|
||||
if (result && result.message && result.code !== 200) {
|
||||
console.error('搜索失败:', result.message);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('搜索错误:', error);
|
||||
searchResults.value = [];
|
||||
ElMessage.error('搜索失败,请稍后重试');
|
||||
});
|
||||
};
|
||||
|
||||
// 选择搜索结果
|
||||
const selectSearchResult = (result) => {
|
||||
const location = result.location;
|
||||
map.value.setCenter([location.lng, location.lat]);
|
||||
map.value.setZoom(16);
|
||||
|
||||
selectedAddress.value = {
|
||||
lng: location.lng,
|
||||
lat: location.lat,
|
||||
formattedAddress: result.district + result.address,
|
||||
name: result.name
|
||||
};
|
||||
|
||||
searchResults.value = [];
|
||||
searchKeyword.value = result.name;
|
||||
|
||||
// 重新地理编码获取详细信息
|
||||
geocodeByLocation(location);
|
||||
};
|
||||
|
||||
// 格式化距离
|
||||
const formatDistance = (distance) => {
|
||||
if (distance < 1000) {
|
||||
return Math.round(distance) + 'm';
|
||||
} else {
|
||||
return (distance / 1000).toFixed(1) + 'km';
|
||||
}
|
||||
};
|
||||
|
||||
// 确认地址
|
||||
const confirmAddress = () => {
|
||||
if (selectedAddress.value) {
|
||||
if (selectedAddress.value.name && !detailAddress.value) {
|
||||
detailAddress.value = selectedAddress.value.name;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 智能粘贴
|
||||
const smartPaste = async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text) {
|
||||
searchKeyword.value = text;
|
||||
searchAddress();
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.warning('无法读取剪贴板内容');
|
||||
}
|
||||
};
|
||||
|
||||
// 导入已有地址
|
||||
const importExistingAddress = () => {
|
||||
emit('import-existing');
|
||||
};
|
||||
|
||||
// 选择地址标签
|
||||
const selectTag = (tag) => {
|
||||
selectedTag.value = tag;
|
||||
};
|
||||
|
||||
// 保存地址
|
||||
const saveAddress = () => {
|
||||
if (!selectedAddress.value) {
|
||||
ElMessage.error('请在地图上选择地址');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!consigneeName.value.trim()) {
|
||||
ElMessage.error('请输入收货人姓名');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!phoneNumber.value.trim()) {
|
||||
ElMessage.error('请输入手机号');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^1[3-9]\d{9}$/.test(phoneNumber.value)) {
|
||||
ElMessage.error('请输入正确的手机号');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!detailAddress.value.trim()) {
|
||||
ElMessage.error('请输入详细地址');
|
||||
return;
|
||||
}
|
||||
|
||||
const addressData = {
|
||||
consignee: consigneeName.value,
|
||||
phone: phoneNumber.value,
|
||||
province: selectedAddress.value.province,
|
||||
city: selectedAddress.value.city,
|
||||
district: selectedAddress.value.district,
|
||||
address: detailAddress.value,
|
||||
isDefault: useDefaultAddress.value,
|
||||
tag: selectedTag.value,
|
||||
lng: selectedAddress.value.lng,
|
||||
lat: selectedAddress.value.lat,
|
||||
formattedAddress: selectedAddress.value.formattedAddress
|
||||
};
|
||||
|
||||
emit('save', addressData);
|
||||
};
|
||||
|
||||
// 初始化表单数据
|
||||
const initFormData = () => {
|
||||
if (props.modelValue) {
|
||||
const data = props.modelValue;
|
||||
consigneeName.value = data.consignee || '';
|
||||
phoneNumber.value = data.phone || '';
|
||||
detailAddress.value = data.address || '';
|
||||
useDefaultAddress.value = data.isDefault || false;
|
||||
selectedTag.value = data.tag || '家';
|
||||
|
||||
if (data.lng && data.lat && map.value) {
|
||||
map.value.setCenter([data.lng, data.lat]);
|
||||
selectedAddress.value = {
|
||||
lng: data.lng,
|
||||
lat: data.lat,
|
||||
formattedAddress: `${data.province} ${data.city} ${data.district} ${data.address}`,
|
||||
province: data.province,
|
||||
city: data.city,
|
||||
district: data.district
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => props.modelValue, initFormData, { immediate: true });
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(initMap, 100);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (map.value) {
|
||||
map.value.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
searchKeyword,
|
||||
searchResults,
|
||||
selectedAddress,
|
||||
showSearchTip,
|
||||
searchTipText,
|
||||
saving,
|
||||
detailAddress,
|
||||
consigneeName,
|
||||
phoneNumber,
|
||||
phonePrefix,
|
||||
useDefaultAddress,
|
||||
selectedTag,
|
||||
addressTags,
|
||||
regionText,
|
||||
getCurrentLocation,
|
||||
onSearchInput,
|
||||
searchAddress,
|
||||
selectSearchResult,
|
||||
formatDistance,
|
||||
confirmAddress,
|
||||
smartPaste,
|
||||
importExistingAddress,
|
||||
selectTag,
|
||||
saveAddress
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.map-address-selector {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
height: 200px;
|
||||
position: relative;
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.search-tip {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.center-marker {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -100%);
|
||||
z-index: 1000;
|
||||
color: #ff4757;
|
||||
font-size: 24px;
|
||||
filter: drop-shadow(0 0 3px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
.location-btn {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
color: #4CAF50;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.location-btn:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
background: white;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.search-input :deep(.el-input__wrapper) {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.smart-paste-btn {
|
||||
color: #4CAF50;
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
margin-top: 12px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-result-item:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.result-main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.result-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.result-address {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.result-distance {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.selected-address {
|
||||
background: white;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.address-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.address-main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.address-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.address-detail {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.use-btn {
|
||||
background: #ff4757;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
padding: 8px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.address-form-section {
|
||||
flex: 1;
|
||||
background: white;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
max-height: calc(85vh - 200px - 200px);
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.required-icon {
|
||||
color: #ff4757;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.region-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-input,
|
||||
.name-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.import-btn {
|
||||
color: #4CAF50;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.phone-input-wrapper {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.phone-prefix {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.phone-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.address-tags {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tag-item:hover {
|
||||
border-color: #4CAF50;
|
||||
}
|
||||
|
||||
.tag-item.active {
|
||||
border-color: #4CAF50;
|
||||
background: #f8fff8;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.save-section {
|
||||
background: white;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background: #ff6b35;
|
||||
border: none;
|
||||
border-radius: 22px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
67
frontend/src/utils/areaData.js
Normal file
67
frontend/src/utils/areaData.js
Normal file
@ -0,0 +1,67 @@
|
||||
import { provinceAndCityData, regionData } from 'element-china-area-data'
|
||||
|
||||
/**
|
||||
* 省市区数据工具类
|
||||
*/
|
||||
export class AreaDataUtil {
|
||||
/**
|
||||
* 获取所有省份
|
||||
*/
|
||||
static getProvinces() {
|
||||
return regionData.map(item => ({
|
||||
code: item.value,
|
||||
name: item.label
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据省份代码获取城市列表
|
||||
*/
|
||||
static getCitiesByProvince(provinceCode) {
|
||||
const province = regionData.find(item => item.value === provinceCode)
|
||||
return province && province.children ? province.children.map(item => ({
|
||||
code: item.value,
|
||||
name: item.label
|
||||
})) : []
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据城市代码获取区县列表
|
||||
*/
|
||||
static getDistrictsByCity(cityCode) {
|
||||
for (const province of regionData) {
|
||||
if (province.children) {
|
||||
const city = province.children.find(item => item.value === cityCode)
|
||||
if (city && city.children) {
|
||||
return city.children.map(item => ({
|
||||
code: item.value,
|
||||
name: item.label
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名称获取对应的代码
|
||||
*/
|
||||
static getCodeByName(name) {
|
||||
for (const province of regionData) {
|
||||
if (province.label === name) return province.value
|
||||
if (province.children) {
|
||||
for (const city of province.children) {
|
||||
if (city.label === name) return city.value
|
||||
if (city.children) {
|
||||
for (const district of city.children) {
|
||||
if (district.label === name) return district.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default AreaDataUtil
|
||||
@ -77,6 +77,11 @@
|
||||
<div class="address-detail">
|
||||
{{ address.province }} {{ address.city }} {{ address.district }} {{ address.address }}
|
||||
</div>
|
||||
|
||||
<!-- 地址标签 -->
|
||||
<div class="address-tag" v-if="address.tag">
|
||||
<el-tag type="info" size="small">{{ address.tag }}</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="address-footer">
|
||||
<div class="default-tag">
|
||||
@ -99,9 +104,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑地址对话框 -->
|
||||
<!-- 地图地址选择对话框 -->
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
v-model="showMapDialog"
|
||||
title="添加收货地址"
|
||||
width="90%"
|
||||
top="5vh"
|
||||
class="map-dialog"
|
||||
:show-close="true"
|
||||
@close="resetMapForm"
|
||||
>
|
||||
<MapAddressSelector
|
||||
v-model="mapAddressForm"
|
||||
@save="handleMapSave"
|
||||
@cancel="showMapDialog = false"
|
||||
@import-existing="showImportDialog"
|
||||
/>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 导入已有地址对话框 -->
|
||||
<el-dialog
|
||||
v-model="showImportDialog"
|
||||
title="导入已有地址"
|
||||
width="500px"
|
||||
>
|
||||
<div class="import-addresses">
|
||||
<div
|
||||
v-for="address in addresses"
|
||||
:key="address.id"
|
||||
class="import-address-item"
|
||||
@click="importAddress(address)"
|
||||
>
|
||||
<div class="import-address-info">
|
||||
<div class="import-name">{{ address.consignee }}</div>
|
||||
<div class="import-detail">
|
||||
{{ address.province }} {{ address.city }} {{ address.district }} {{ address.address }}
|
||||
</div>
|
||||
</div>
|
||||
<el-icon class="import-icon"><Right /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 传统表单对话框(保留作为备用) -->
|
||||
<el-dialog
|
||||
v-model="showFormDialog"
|
||||
:title="dialogTitle"
|
||||
width="500px"
|
||||
@close="resetForm"
|
||||
@ -125,25 +172,25 @@
|
||||
<el-select v-model="addressForm.province" placeholder="省份" @change="onProvinceChange">
|
||||
<el-option
|
||||
v-for="province in provinces"
|
||||
:key="province"
|
||||
:label="province"
|
||||
:value="province"
|
||||
:key="province.code"
|
||||
:label="province.name"
|
||||
:value="province.name"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select v-model="addressForm.city" placeholder="城市" @change="onCityChange">
|
||||
<el-option
|
||||
v-for="city in cities"
|
||||
:key="city"
|
||||
:label="city"
|
||||
:value="city"
|
||||
:key="city.code"
|
||||
:label="city.name"
|
||||
:value="city.name"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select v-model="addressForm.district" placeholder="区县">
|
||||
<el-option
|
||||
v-for="district in districts"
|
||||
:key="district"
|
||||
:label="district"
|
||||
:value="district"
|
||||
:key="district.code"
|
||||
:label="district.name"
|
||||
:value="district.name"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
@ -165,8 +212,8 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="showDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSave" :loading="saving">保存</el-button>
|
||||
<el-button @click="showFormDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleFormSave" :loading="saving">保存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@ -177,49 +224,36 @@
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { LocationInformation, ArrowDown, Plus } from '@element-plus/icons-vue'
|
||||
import { LocationInformation, ArrowDown, Plus, Right } from '@element-plus/icons-vue'
|
||||
import { userStore } from '../../store/user'
|
||||
import * as addressAPI from '../../api/address'
|
||||
import AreaDataUtil from '../../utils/areaData'
|
||||
import MapAddressSelector from '../../components/MapAddressSelector.vue'
|
||||
|
||||
export default {
|
||||
name: 'Addresses',
|
||||
components: {
|
||||
LocationInformation, ArrowDown, Plus
|
||||
LocationInformation, ArrowDown, Plus, Right,
|
||||
MapAddressSelector
|
||||
},
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const showDialog = ref(false)
|
||||
const showMapDialog = ref(false)
|
||||
const showFormDialog = ref(false)
|
||||
const showImportDialog = ref(false)
|
||||
const editingAddress = ref(null)
|
||||
const addressFormRef = ref()
|
||||
const addresses = ref([])
|
||||
|
||||
const defaultAvatar = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
|
||||
|
||||
// 简化的省市区数据
|
||||
const provinces = ['北京市', '上海市', '广东省', '浙江省', '江苏省', '山东省', '河南省', '湖南省', '湖北省', '四川省']
|
||||
const cityMap = {
|
||||
'北京市': ['北京市'],
|
||||
'上海市': ['上海市'],
|
||||
'广东省': ['广州市', '深圳市', '珠海市', '佛山市', '惠州市'],
|
||||
'浙江省': ['杭州市', '宁波市', '温州市', '绍兴市', '金华市'],
|
||||
'江苏省': ['南京市', '苏州市', '无锡市', '常州市', '徐州市'],
|
||||
'山东省': ['济南市', '青岛市', '烟台市', '威海市', '临沂市'],
|
||||
'河南省': ['郑州市', '洛阳市', '开封市', '新乡市', '许昌市'],
|
||||
'湖南省': ['长沙市', '株洲市', '湘潭市', '衡阳市', '邵阳市'],
|
||||
'湖北省': ['武汉市', '黄石市', '十堰市', '荆州市', '宜昌市'],
|
||||
'四川省': ['成都市', '绵阳市', '德阳市', '南充市', '宜宾市']
|
||||
}
|
||||
const districtMap = {
|
||||
'北京市': ['东城区', '西城区', '朝阳区', '丰台区', '石景山区', '海淀区'],
|
||||
'上海市': ['黄浦区', '徐汇区', '长宁区', '静安区', '普陀区', '虹口区'],
|
||||
'广州市': ['越秀区', '荔湾区', '海珠区', '天河区', '白云区', '黄埔区'],
|
||||
'深圳市': ['罗湖区', '福田区', '南山区', '宝安区', '龙岗区', '盐田区'],
|
||||
'杭州市': ['上城区', '下城区', '江干区', '拱墅区', '西湖区', '滨江区'],
|
||||
'南京市': ['玄武区', '秦淮区', '建邺区', '鼓楼区', '浦口区', '栖霞区']
|
||||
}
|
||||
// 地图地址表单数据
|
||||
const mapAddressForm = ref({})
|
||||
|
||||
// 使用area-data-china获取省市区数据
|
||||
const provinces = ref(AreaDataUtil.getProvinces())
|
||||
const cities = ref([])
|
||||
const districts = ref([])
|
||||
|
||||
@ -277,27 +311,14 @@ export default {
|
||||
|
||||
const showAddDialog = () => {
|
||||
editingAddress.value = null
|
||||
resetForm()
|
||||
showDialog.value = true
|
||||
resetMapForm()
|
||||
showMapDialog.value = true
|
||||
}
|
||||
|
||||
const editAddress = (address) => {
|
||||
editingAddress.value = address
|
||||
Object.assign(addressForm, {
|
||||
consignee: address.consignee,
|
||||
phone: address.phone,
|
||||
province: address.province,
|
||||
city: address.city,
|
||||
district: address.district,
|
||||
address: address.address,
|
||||
isDefault: address.isDefault
|
||||
})
|
||||
|
||||
// 更新城市和区县选项
|
||||
onProvinceChange(address.province)
|
||||
onCityChange(address.city)
|
||||
|
||||
showDialog.value = true
|
||||
mapAddressForm.value = { ...address }
|
||||
showMapDialog.value = true
|
||||
}
|
||||
|
||||
const deleteAddress = (address) => {
|
||||
@ -326,15 +347,56 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
const onProvinceChange = (province) => {
|
||||
cities.value = cityMap[province] || []
|
||||
// 地图地址保存处理
|
||||
const handleMapSave = async (addressData) => {
|
||||
try {
|
||||
saving.value = true
|
||||
|
||||
if (editingAddress.value) {
|
||||
// 编辑地址
|
||||
await addressAPI.updateAddress(editingAddress.value.id, addressData)
|
||||
ElMessage.success('地址更新成功')
|
||||
} else {
|
||||
// 新增地址
|
||||
await addressAPI.createAddress(addressData)
|
||||
ElMessage.success('地址添加成功')
|
||||
}
|
||||
|
||||
showMapDialog.value = false
|
||||
loadAddresses()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.response?.data?.message || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 导入地址
|
||||
const importAddress = (address) => {
|
||||
mapAddressForm.value = { ...address }
|
||||
showImportDialog.value = false
|
||||
}
|
||||
|
||||
// 传统表单相关方法(保留)
|
||||
const onProvinceChange = (provinceName) => {
|
||||
const province = provinces.value.find(p => p.name === provinceName)
|
||||
if (province) {
|
||||
cities.value = AreaDataUtil.getCitiesByProvince(province.code)
|
||||
} else {
|
||||
cities.value = []
|
||||
}
|
||||
districts.value = []
|
||||
addressForm.city = ''
|
||||
addressForm.district = ''
|
||||
}
|
||||
|
||||
const onCityChange = (city) => {
|
||||
districts.value = districtMap[city] || []
|
||||
const onCityChange = (cityName) => {
|
||||
const city = cities.value.find(c => c.name === cityName)
|
||||
if (city) {
|
||||
districts.value = AreaDataUtil.getDistrictsByCity(city.code)
|
||||
} else {
|
||||
districts.value = []
|
||||
}
|
||||
addressForm.district = ''
|
||||
}
|
||||
|
||||
@ -352,22 +414,24 @@ export default {
|
||||
districts.value = []
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
const resetMapForm = () => {
|
||||
mapAddressForm.value = {}
|
||||
}
|
||||
|
||||
const handleFormSave = async () => {
|
||||
try {
|
||||
await addressFormRef.value.validate()
|
||||
saving.value = true
|
||||
|
||||
if (editingAddress.value) {
|
||||
// 编辑地址 - 使用addressAPI.updateAddress
|
||||
await addressAPI.updateAddress(editingAddress.value.id, addressForm)
|
||||
ElMessage.success('地址更新成功')
|
||||
} else {
|
||||
// 新增地址 - 使用addressAPI.createAddress
|
||||
await addressAPI.createAddress(addressForm)
|
||||
ElMessage.success('地址添加成功')
|
||||
}
|
||||
|
||||
showDialog.value = false
|
||||
showFormDialog.value = false
|
||||
loadAddresses()
|
||||
} catch (error) {
|
||||
if (error !== false) {
|
||||
@ -426,11 +490,14 @@ export default {
|
||||
userStore,
|
||||
loading,
|
||||
saving,
|
||||
showDialog,
|
||||
showMapDialog,
|
||||
showFormDialog,
|
||||
showImportDialog,
|
||||
editingAddress,
|
||||
addressFormRef,
|
||||
addresses,
|
||||
defaultAvatar,
|
||||
mapAddressForm,
|
||||
provinces,
|
||||
cities,
|
||||
districts,
|
||||
@ -442,10 +509,13 @@ export default {
|
||||
editAddress,
|
||||
deleteAddress,
|
||||
setDefaultAddress,
|
||||
handleMapSave,
|
||||
importAddress,
|
||||
onProvinceChange,
|
||||
onCityChange,
|
||||
resetForm,
|
||||
handleSave,
|
||||
resetMapForm,
|
||||
handleFormSave,
|
||||
goHome,
|
||||
handleUserAction
|
||||
}
|
||||
@ -589,7 +659,11 @@ export default {
|
||||
.address-detail {
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.address-tag {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.address-footer {
|
||||
@ -612,6 +686,84 @@ export default {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 地图对话框样式 */
|
||||
.map-dialog :deep(.el-dialog) {
|
||||
margin: 0 auto;
|
||||
height: 85vh;
|
||||
max-height: 85vh;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.map-dialog :deep(.el-dialog__header) {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.map-dialog :deep(.el-dialog__title) {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.map-dialog :deep(.el-dialog__headerbtn) {
|
||||
top: 12px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.map-dialog :deep(.el-dialog__close) {
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.map-dialog :deep(.el-dialog__body) {
|
||||
padding: 0;
|
||||
height: calc(85vh - 60px);
|
||||
}
|
||||
|
||||
/* 导入地址样式 */
|
||||
.import-addresses {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.import-address-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.import-address-item:hover {
|
||||
border-color: #4CAF50;
|
||||
background: #f8fff8;
|
||||
}
|
||||
|
||||
.import-address-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.import-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.import-detail {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.import-icon {
|
||||
color: #4CAF50;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.addresses-container {
|
||||
padding: 10px;
|
||||
@ -642,4 +794,4 @@ export default {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
Loading…
x
Reference in New Issue
Block a user