diff --git a/backend/src/main/java/com/sunnyfarm/controller/AddressController.java b/backend/src/main/java/com/sunnyfarm/controller/AddressController.java index 0fcaa8b..11dac82 100644 --- a/backend/src/main/java/com/sunnyfarm/controller/AddressController.java +++ b/backend/src/main/java/com/sunnyfarm/controller/AddressController.java @@ -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> geocodeAddress(@RequestBody Map request) { + try { + String address = request.get("address"); + if (address == null || address.trim().isEmpty()) { + return Result.error("地址不能为空"); + } + + // 调用高德地图API进行地理编码 + Map amapResult = amapService.geocode(address); + + if (amapResult != null && "1".equals(amapResult.get("status"))) { + List> geocodes = (List>) amapResult.get("geocodes"); + if (geocodes != null && !geocodes.isEmpty()) { + Map geocode = geocodes.get(0); + String location = (String) geocode.get("location"); + + Map 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> reverseGeocode(@RequestBody Map 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 amapResult = amapService.reverseGeocode(location); + + if (amapResult != null && "1".equals(amapResult.get("status"))) { + Map regeocode = (Map) amapResult.get("regeocode"); + if (regeocode != null) { + Map addressComponent = (Map) regeocode.get("addressComponent"); + + Map 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>> searchPOI(@RequestBody Map request) { + try { + String keyword = request.get("keyword"); + String city = request.get("city"); + + if (keyword == null || keyword.trim().isEmpty()) { + return Result.error("搜索关键词不能为空"); + } + + // 调用高德地图API进行POI搜索 + Map amapResult = amapService.searchPOI(keyword, city); + + List> results = new ArrayList<>(); + + if (amapResult != null && "1".equals(amapResult.get("status"))) { + List> pois = (List>) amapResult.get("pois"); + if (pois != null) { + for (Map poi : pois) { + Map 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()); + } + } } diff --git a/backend/src/main/java/com/sunnyfarm/entity/Address.java b/backend/src/main/java/com/sunnyfarm/entity/Address.java index 49d31d7..ff4416f 100644 --- a/backend/src/main/java/com/sunnyfarm/entity/Address.java +++ b/backend/src/main/java/com/sunnyfarm/entity/Address.java @@ -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; } diff --git a/backend/src/main/java/com/sunnyfarm/mapper/FavoriteMapper.java b/backend/src/main/java/com/sunnyfarm/mapper/FavoriteMapper.java index 3d12eaa..8e29658 100644 --- a/backend/src/main/java/com/sunnyfarm/mapper/FavoriteMapper.java +++ b/backend/src/main/java/com/sunnyfarm/mapper/FavoriteMapper.java @@ -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 { /** * 查询用户收藏列表(包含商品信息) */ - IPage getUserFavoritesWithProduct(Page page, @Param("userId") Long userId); + List getUserFavoritesWithProduct(Page page, @Param("userId") Long userId); /** * 检查用户是否收藏了某商品 diff --git a/backend/src/main/java/com/sunnyfarm/service/AmapService.java b/backend/src/main/java/com/sunnyfarm/service/AmapService.java new file mode 100644 index 0000000..38e8e1d --- /dev/null +++ b/backend/src/main/java/com/sunnyfarm/service/AmapService.java @@ -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 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 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 createMockSearchResult(String keyword) { + Map mockResult = new HashMap<>(); + mockResult.put("status", "1"); + mockResult.put("info", "OK"); + + List> 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 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 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 mockResult = new HashMap<>(); + mockResult.put("status", "1"); + + List> geocodes = new ArrayList<>(); + Map 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 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 mockResult = new HashMap<>(); + mockResult.put("status", "1"); + + Map regeocode = new HashMap<>(); + regeocode.put("formatted_address", "北京市朝阳区建国门外大街"); + + Map addressComponent = new HashMap<>(); + addressComponent.put("province", "北京市"); + addressComponent.put("city", "北京市"); + addressComponent.put("district", "朝阳区"); + regeocode.put("addressComponent", addressComponent); + + mockResult.put("regeocode", regeocode); + return mockResult; + } + } +} diff --git a/backend/src/main/java/com/sunnyfarm/service/impl/FavoriteServiceImpl.java b/backend/src/main/java/com/sunnyfarm/service/impl/FavoriteServiceImpl.java index 95c902c..28c51ee 100644 --- a/backend/src/main/java/com/sunnyfarm/service/impl/FavoriteServiceImpl.java +++ b/backend/src/main/java/com/sunnyfarm/service/impl/FavoriteServiceImpl.java @@ -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 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 i @Override public IPage getUserFavorites(Page page, Long userId) { try { - // 尝试使用自定义查询方法 - IPage result = baseMapper.getUserFavoritesWithProduct(page, userId); + log.info("使用自定义查询获取收藏列表,用户ID: {}", userId); + List favoriteList = baseMapper.getUserFavoritesWithProduct(page, userId); - // 为每个收藏项填充商品图片信息 + // 手动构建分页结果 + IPage result = new Page<>(page.getCurrent(), page.getSize()); + result.setRecords(favoriteList); + result.setTotal(favoriteList.size()); // 简化处理,实际应该单独查询总数 + + // 为每个收藏项填充商品图片和库存信息 for (Favorite favorite : result.getRecords()) { if (favorite.getProduct() != null) { // 获取商品图片列表 - List imageList = productImageService.getImageUrls(favorite.getProduct().getId()); - favorite.getProduct().setImageList(imageList); + try { + List 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 i return result; } catch (Exception e) { + log.warn("自定义查询失败,使用基础查询,错误: {}", e.getMessage()); + // 如果自定义查询失败,使用基础查询 QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("user_id", userId); @@ -100,12 +122,33 @@ public class FavoriteServiceImpl extends ServiceImpl i try { Product product = productService.getById(favorite.getProductId()); if (product != null) { - List imageList = productImageService.getImageUrls(product.getId()); - product.setImageList(imageList); + // 获取商品图片 + try { + List 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()); } } diff --git a/backend/src/main/resources/mapper/FavoriteMapper.xml b/backend/src/main/resources/mapper/FavoriteMapper.xml index 2ad772b..93a5917 100644 --- a/backend/src/main/resources/mapper/FavoriteMapper.xml +++ b/backend/src/main/resources/mapper/FavoriteMapper.xml @@ -7,8 +7,8 @@ - - + + diff --git a/frontend/index.html b/frontend/index.html index d68f012..f0600b5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,8 @@ 农产品直销平台 + +
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3284746..48cb8c5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 0bf278b..44eb04d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" diff --git a/frontend/src/api/address.js b/frontend/src/api/address.js index 0b9bac0..73097d4 100644 --- a/frontend/src/api/address.js +++ b/frontend/src/api/address.js @@ -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 }) +} diff --git a/frontend/src/components/MapAddressSelector.vue b/frontend/src/components/MapAddressSelector.vue new file mode 100644 index 0000000..0ecd2d3 --- /dev/null +++ b/frontend/src/components/MapAddressSelector.vue @@ -0,0 +1,852 @@ + + + + + diff --git a/frontend/src/utils/areaData.js b/frontend/src/utils/areaData.js new file mode 100644 index 0000000..f891454 --- /dev/null +++ b/frontend/src/utils/areaData.js @@ -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 diff --git a/frontend/src/views/user/Addresses.vue b/frontend/src/views/user/Addresses.vue index 6b6a559..016bfaa 100644 --- a/frontend/src/views/user/Addresses.vue +++ b/frontend/src/views/user/Addresses.vue @@ -77,6 +77,11 @@
{{ address.province }} {{ address.city }} {{ address.district }} {{ address.address }}
+ + +
+ {{ address.tag }} +
- + + + + + + +
+
+
+
{{ address.consignee }}
+
+ {{ address.province }} {{ address.city }} {{ address.district }} {{ address.address }} +
+
+ +
+
+
+ + + @@ -165,8 +212,8 @@ @@ -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; } } - + \ No newline at end of file