map_update

This commit is contained in:
superlishunqin 2025-09-28 06:58:05 +08:00
parent a3e39115db
commit 4b58d6ed54
13 changed files with 1566 additions and 96 deletions

View File

@ -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());
}
}
}

View File

@ -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;
}

View File

@ -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);
/**
* 检查用户是否收藏了某商品

View 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;
}
}
}

View File

@ -8,24 +8,31 @@ 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) {
QueryWrapper<Favorite> wrapper = new QueryWrapper<>();
@ -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) {
// 获取商品图片列表
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) {
// 获取商品图片
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());
}
}

View File

@ -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">

View File

@ -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>

View File

@ -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",

View File

@ -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"

View File

@ -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 })
}

View 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>

View 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

View File

@ -78,6 +78,11 @@
{{ 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">
<el-tag v-if="address.isDefault" type="success" size="small">
@ -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;