02_28_version_1
This commit is contained in:
parent
86c65ece9f
commit
1810d40c01
39
backend/Dockerfile
Normal file
39
backend/Dockerfile
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# ============================================
|
||||||
|
# 农产品直销平台 - 后端 Dockerfile
|
||||||
|
# 多阶段构建: Maven编译 → JRE运行
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# ---------- 第一阶段:Maven构建 ----------
|
||||||
|
FROM maven:3.8-eclipse-temurin-17 AS builder
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# 配置Maven阿里云镜像(加速依赖下载)
|
||||||
|
RUN mkdir -p /root/.m2 && \
|
||||||
|
echo '<?xml version="1.0" encoding="UTF-8"?><settings><mirrors><mirror><id>aliyun</id><mirrorOf>*</mirrorOf><url>https://maven.aliyun.com/repository/public</url></mirror></mirrors></settings>' > /root/.m2/settings.xml
|
||||||
|
|
||||||
|
# 先复制pom.xml,利用Docker缓存层加速(依赖不变时跳过下载)
|
||||||
|
COPY pom.xml .
|
||||||
|
RUN mvn dependency:go-offline -B
|
||||||
|
|
||||||
|
# 复制源代码并打包
|
||||||
|
COPY src ./src
|
||||||
|
RUN mvn clean package -DskipTests
|
||||||
|
|
||||||
|
# ---------- 第二阶段:运行时 ----------
|
||||||
|
FROM eclipse-temurin:17-jre
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 设置时区
|
||||||
|
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
|
||||||
|
echo "Asia/Shanghai" > /etc/timezone
|
||||||
|
|
||||||
|
# 创建日志目录
|
||||||
|
RUN mkdir -p /app/logs
|
||||||
|
|
||||||
|
# 从构建阶段复制JAR包
|
||||||
|
COPY --from=builder /build/target/sunny-farm-0.0.1-SNAPSHOT.jar app.jar
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# 启动命令(支持通过JAVA_OPTS传入JVM参数)
|
||||||
|
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
|
||||||
@ -2,7 +2,6 @@ package com.sunnyfarm.dto.express;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 物流综合信息DTO(包含轨迹和地图数据)
|
* 物流综合信息DTO(包含轨迹和地图数据)
|
||||||
@ -10,103 +9,45 @@ import java.util.Map;
|
|||||||
@Data
|
@Data
|
||||||
public class ExpressInfoDTO {
|
public class ExpressInfoDTO {
|
||||||
|
|
||||||
/**
|
/** 快递单号 */
|
||||||
* 快递单号
|
|
||||||
*/
|
|
||||||
private String trackingNumber;
|
private String trackingNumber;
|
||||||
|
|
||||||
/**
|
/** 快递公司名称 */
|
||||||
* 快递公司名称
|
|
||||||
*/
|
|
||||||
private String companyName;
|
private String companyName;
|
||||||
|
|
||||||
/**
|
/** 快递公司代码 */
|
||||||
* 快递公司代码
|
|
||||||
*/
|
|
||||||
private String companyCode;
|
private String companyCode;
|
||||||
|
|
||||||
/**
|
/** 物流状态 */
|
||||||
* 物流状态
|
|
||||||
*/
|
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
/**
|
/** 当前位置 */
|
||||||
* 当前位置
|
|
||||||
*/
|
|
||||||
private String currentLocation;
|
private String currentLocation;
|
||||||
|
|
||||||
/**
|
/** 更新时间 */
|
||||||
* 更新时间
|
|
||||||
*/
|
|
||||||
private String updateTime;
|
private String updateTime;
|
||||||
|
|
||||||
/**
|
/** 运输时长 */
|
||||||
* 物流轨迹列表
|
private String takeTime;
|
||||||
*/
|
|
||||||
private List<TraceItem> traces;
|
|
||||||
|
|
||||||
/**
|
/** 快递员电话 */
|
||||||
* 地图路线数据
|
private String courierPhone;
|
||||||
*/
|
|
||||||
private MapRouteData mapRoute;
|
/** 物流轨迹地图URL(可直接iframe嵌入) */
|
||||||
|
private String mapUrl;
|
||||||
|
|
||||||
|
/** 物流轨迹列表 */
|
||||||
|
private List<TraceItem> traces;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class TraceItem {
|
public static class TraceItem {
|
||||||
/**
|
/** 时间 */
|
||||||
* 时间
|
|
||||||
*/
|
|
||||||
private String time;
|
private String time;
|
||||||
|
/** 地点 */
|
||||||
/**
|
|
||||||
* 地点
|
|
||||||
*/
|
|
||||||
private String location;
|
private String location;
|
||||||
|
/** 状态描述 */
|
||||||
/**
|
|
||||||
* 状态描述
|
|
||||||
*/
|
|
||||||
private String status;
|
private String status;
|
||||||
}
|
/** 动作类型:1揽收,2运输,202派件,211投柜,311签收 */
|
||||||
|
private String action;
|
||||||
@Data
|
|
||||||
public static class MapRouteData {
|
|
||||||
/**
|
|
||||||
* 城市列表(用于标记)
|
|
||||||
*/
|
|
||||||
private List<CityPoint> cities;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 路线坐标点(如果有路径规划)
|
|
||||||
*/
|
|
||||||
private List<List<Double>> routePoints;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 路线信息
|
|
||||||
*/
|
|
||||||
private String distance;
|
|
||||||
private String duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class CityPoint {
|
|
||||||
/**
|
|
||||||
* 城市名称
|
|
||||||
*/
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 经度
|
|
||||||
*/
|
|
||||||
private Double lng;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 纬度
|
|
||||||
*/
|
|
||||||
private Double lat;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 序号
|
|
||||||
*/
|
|
||||||
private Integer index;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
package com.sunnyfarm.service.impl;
|
package com.sunnyfarm.service.impl;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.sunnyfarm.config.properties.AliyunExpressProperties;
|
import com.sunnyfarm.config.properties.AliyunExpressProperties;
|
||||||
import com.sunnyfarm.config.properties.AmapProperties;
|
|
||||||
import com.sunnyfarm.dto.express.ExpressInfoDTO;
|
import com.sunnyfarm.dto.express.ExpressInfoDTO;
|
||||||
import com.sunnyfarm.dto.express.ExpressTraceDTO;
|
import com.sunnyfarm.dto.express.ExpressTraceDTO;
|
||||||
import com.sunnyfarm.entity.Order;
|
import com.sunnyfarm.entity.Order;
|
||||||
@ -13,16 +11,19 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.*;
|
import org.springframework.http.*;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 物流查询服务实现
|
* 物流查询服务实现
|
||||||
|
* 使用两个API:
|
||||||
|
* 1. 旧API(wuliu.market.alicloudapi.com)查询物流轨迹信息
|
||||||
|
* 2. 新API(lhkdwlgjdt.market.alicloudapi.com)查询物流轨迹地图
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@ -30,57 +31,46 @@ import java.util.stream.Collectors;
|
|||||||
public class ExpressServiceImpl implements ExpressService {
|
public class ExpressServiceImpl implements ExpressService {
|
||||||
|
|
||||||
private final AliyunExpressProperties expressProperties;
|
private final AliyunExpressProperties expressProperties;
|
||||||
private final AmapProperties amapProperties;
|
|
||||||
private final OrderMapper orderMapper;
|
private final OrderMapper orderMapper;
|
||||||
private final RestTemplate restTemplate = new RestTemplate();
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
|
|
||||||
// 预设主要城市坐标
|
// 物流轨迹地图API地址
|
||||||
private static final Map<String, double[]> CITY_COORDINATES = new HashMap<>();
|
private static final String TRACE_MAP_URL = "https://lhkdwlgjdt.market.alicloudapi.com/express/trace-map/v2";
|
||||||
|
|
||||||
static {
|
|
||||||
CITY_COORDINATES.put("广州市", new double[]{113.264385, 23.129163});
|
|
||||||
CITY_COORDINATES.put("深圳市", new double[]{114.057868, 22.543099});
|
|
||||||
CITY_COORDINATES.put("北京市", new double[]{116.407526, 39.904030});
|
|
||||||
CITY_COORDINATES.put("上海市", new double[]{121.473701, 31.230416});
|
|
||||||
CITY_COORDINATES.put("杭州市", new double[]{120.153576, 30.287459});
|
|
||||||
CITY_COORDINATES.put("南京市", new double[]{118.796877, 32.060255});
|
|
||||||
CITY_COORDINATES.put("成都市", new double[]{104.065735, 30.659462});
|
|
||||||
CITY_COORDINATES.put("武汉市", new double[]{114.305393, 30.593099});
|
|
||||||
CITY_COORDINATES.put("西安市", new double[]{108.939621, 34.341568});
|
|
||||||
CITY_COORDINATES.put("重庆市", new double[]{106.551557, 29.563009});
|
|
||||||
CITY_COORDINATES.put("天津市", new double[]{117.200983, 39.084158});
|
|
||||||
CITY_COORDINATES.put("苏州市", new double[]{120.585315, 31.298886});
|
|
||||||
CITY_COORDINATES.put("东莞市", new double[]{113.751765, 23.020673});
|
|
||||||
CITY_COORDINATES.put("佛山市", new double[]{113.121416, 23.021548});
|
|
||||||
CITY_COORDINATES.put("惠州市", new double[]{114.416196, 23.111847});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ExpressInfoDTO queryExpressInfo(String trackingNumber, String companyCode) {
|
public ExpressInfoDTO queryExpressInfo(String trackingNumber, String companyCode) {
|
||||||
log.info("🔍 查询物流信息 - 单号: {}, 公司代码: {}", trackingNumber, companyCode);
|
log.info("🔍 查询物流信息 - 单号: {}, 公司代码: {}", trackingNumber, companyCode);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 调用阿里云物流查询API
|
// 1. 用旧API查询物流轨迹
|
||||||
ExpressTraceDTO traceData = queryExpressTrace(trackingNumber, companyCode);
|
String oldApiCode = convertToOldApiCode(companyCode);
|
||||||
|
ExpressTraceDTO traceData = queryExpressTrace(trackingNumber, oldApiCode);
|
||||||
if (traceData == null || !"0".equals(traceData.getStatus())) {
|
|
||||||
throw new BusinessException("物流信息查询失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 解析物流轨迹数据
|
// 2. 解析物流轨迹数据
|
||||||
ExpressInfoDTO result = parseExpressData(traceData);
|
ExpressInfoDTO result = parseExpressData(traceData, trackingNumber);
|
||||||
|
|
||||||
// 3. 提取城市并规划路线
|
// 3. 用新API查询物流地图(不影响主流程,失败也不报错)
|
||||||
List<String> cities = extractCities(traceData);
|
try {
|
||||||
if (!cities.isEmpty()) {
|
String mapApiCode = convertToMapApiCode(companyCode);
|
||||||
ExpressInfoDTO.MapRouteData routeData = planRoute(cities);
|
// 从trackingNumber中提取手机尾号(如果有的话)
|
||||||
result.setMapRoute(routeData);
|
String phone = null;
|
||||||
|
String pureTrackingNumber = trackingNumber;
|
||||||
|
if (trackingNumber.contains(":")) {
|
||||||
|
String[] parts = trackingNumber.split(":");
|
||||||
|
pureTrackingNumber = parts[0];
|
||||||
|
phone = parts[1];
|
||||||
|
}
|
||||||
|
String mapUrl = queryTraceMap(pureTrackingNumber, mapApiCode, phone);
|
||||||
|
result.setMapUrl(mapUrl);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("⚠️ 物流地图查询失败(不影响轨迹展示): {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("✅ 物流信息查询成功 - 单号: {}", trackingNumber);
|
log.info("✅ 物流信息查询成功 - 单号: {}", trackingNumber);
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("❌ 物流信息查询失败", e);
|
log.error("❌ 物流信息查询失败", e);
|
||||||
throw new BusinessException("物流信息查询失败: " + e.getMessage());
|
throw new BusinessException("物流信息查询失败: " + e.getMessage());
|
||||||
@ -100,14 +90,56 @@ public class ExpressServiceImpl implements ExpressService {
|
|||||||
throw new BusinessException("订单暂无物流信息");
|
throw new BusinessException("订单暂无物流信息");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将订单的物流公司名称转换为代码
|
|
||||||
String companyCode = convertCompanyNameToCode(order.getShipCompany());
|
String companyCode = convertCompanyNameToCode(order.getShipCompany());
|
||||||
|
String trackingNumber = order.getShipNo();
|
||||||
|
|
||||||
return queryExpressInfo(order.getShipNo(), companyCode);
|
// 顺丰需要拼接手机尾号到trackingNumber(旧API需要)
|
||||||
|
String phone = null;
|
||||||
|
if ("SF".equals(companyCode) && order.getPhone() != null && order.getPhone().length() >= 4) {
|
||||||
|
phone = order.getPhone().substring(order.getPhone().length() - 4);
|
||||||
|
if (!trackingNumber.contains(":")) {
|
||||||
|
trackingNumber = trackingNumber + ":" + phone;
|
||||||
|
log.info("📱 顺丰快递自动拼接手机尾号: {} -> {}", order.getShipNo(), trackingNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于新地图API,中通也需要手机号
|
||||||
|
if ("ZTO".equals(companyCode) && phone == null && order.getPhone() != null && order.getPhone().length() >= 4) {
|
||||||
|
phone = order.getPhone().substring(order.getPhone().length() - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 用旧API查询物流轨迹
|
||||||
|
String oldApiCode = convertToOldApiCode(companyCode);
|
||||||
|
ExpressTraceDTO traceData = queryExpressTrace(trackingNumber, oldApiCode);
|
||||||
|
|
||||||
|
// 2. 解析物流轨迹数据
|
||||||
|
ExpressInfoDTO result = parseExpressData(traceData, order.getShipNo());
|
||||||
|
|
||||||
|
// 3. 用新API查询物流地图
|
||||||
|
try {
|
||||||
|
String mapApiCode = convertToMapApiCode(companyCode);
|
||||||
|
String mapUrl = queryTraceMap(order.getShipNo(), mapApiCode, phone);
|
||||||
|
result.setMapUrl(mapUrl);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("⚠️ 物流地图查询失败(不影响轨迹展示): {}", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("✅ 物流信息查询成功 - 单号: {}", order.getShipNo());
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("❌ 物流信息查询失败", e);
|
||||||
|
throw new BusinessException("物流信息查询失败: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 旧API:查询物流轨迹 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 调用阿里云物流查询API
|
* 调用旧API查询物流轨迹
|
||||||
*/
|
*/
|
||||||
private ExpressTraceDTO queryExpressTrace(String trackingNumber, String companyCode) {
|
private ExpressTraceDTO queryExpressTrace(String trackingNumber, String companyCode) {
|
||||||
try {
|
try {
|
||||||
@ -123,56 +155,58 @@ public class ExpressServiceImpl implements ExpressService {
|
|||||||
|
|
||||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||||
|
|
||||||
log.info("📡 调用阿里云物流API: {}", url);
|
log.info("📡 调用物流轨迹API: {}", url);
|
||||||
|
|
||||||
ResponseEntity<ExpressTraceDTO> response = restTemplate.exchange(
|
ResponseEntity<ExpressTraceDTO> response = restTemplate.exchange(
|
||||||
url,
|
url, HttpMethod.GET, entity, ExpressTraceDTO.class);
|
||||||
HttpMethod.GET,
|
|
||||||
entity,
|
|
||||||
ExpressTraceDTO.class
|
|
||||||
);
|
|
||||||
|
|
||||||
log.info("✅ 阿里云物流API响应成功");
|
ExpressTraceDTO body = response.getBody();
|
||||||
return response.getBody();
|
log.info("✅ 物流轨迹API响应 - status={}, msg={}",
|
||||||
|
body != null ? body.getStatus() : "null",
|
||||||
|
body != null ? body.getMsg() : "null");
|
||||||
|
|
||||||
|
if (body == null || (!"0".equals(body.getStatus()) && !"ok".equals(body.getMsg()))) {
|
||||||
|
String msg = body != null ? body.getMsg() : "返回为空";
|
||||||
|
throw new BusinessException("物流信息查询失败: " + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
|
||||||
|
} catch (BusinessException e) {
|
||||||
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("❌ 调用阿里云物流API失败", e);
|
log.error("❌ 调用物流轨迹API失败", e);
|
||||||
throw new BusinessException("物流查询服务异常");
|
throw new BusinessException("物流查询服务异常");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析物流数据
|
* 解析物流轨迹数据
|
||||||
*/
|
*/
|
||||||
private ExpressInfoDTO parseExpressData(ExpressTraceDTO traceData) {
|
private ExpressInfoDTO parseExpressData(ExpressTraceDTO traceData, String trackingNumber) {
|
||||||
ExpressInfoDTO result = new ExpressInfoDTO();
|
ExpressInfoDTO result = new ExpressInfoDTO();
|
||||||
ExpressTraceDTO.ExpressResult data = traceData.getResult();
|
ExpressTraceDTO.ExpressResult data = traceData.getResult();
|
||||||
|
|
||||||
result.setTrackingNumber(data.getNumber());
|
result.setTrackingNumber(trackingNumber);
|
||||||
result.setCompanyName(data.getExpName());
|
result.setCompanyName(data.getExpName());
|
||||||
result.setCompanyCode(data.getType());
|
result.setCompanyCode(data.getType());
|
||||||
result.setStatus(getStatusText(data.getDeliverystatus()));
|
result.setStatus(getStatusText(data.getDeliverystatus()));
|
||||||
result.setUpdateTime(data.getUpdateTime());
|
result.setUpdateTime(data.getUpdateTime());
|
||||||
|
result.setCourierPhone(data.getCourierPhone());
|
||||||
|
|
||||||
// 转换物流轨迹
|
|
||||||
if (data.getList() != null && !data.getList().isEmpty()) {
|
if (data.getList() != null && !data.getList().isEmpty()) {
|
||||||
List<ExpressInfoDTO.TraceItem> traces = data.getList().stream()
|
List<ExpressInfoDTO.TraceItem> traces = data.getList().stream()
|
||||||
.map(trace -> {
|
.map(trace -> {
|
||||||
ExpressInfoDTO.TraceItem item = new ExpressInfoDTO.TraceItem();
|
ExpressInfoDTO.TraceItem item = new ExpressInfoDTO.TraceItem();
|
||||||
item.setTime(trace.getTime());
|
item.setTime(trace.getTime());
|
||||||
item.setStatus(trace.getStatus());
|
item.setStatus(trace.getStatus());
|
||||||
|
item.setLocation(extractLocation(trace.getStatus()));
|
||||||
// 提取地点信息
|
|
||||||
String location = extractLocation(trace.getStatus());
|
|
||||||
item.setLocation(location);
|
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
result.setTraces(traces);
|
result.setTraces(traces);
|
||||||
|
|
||||||
// 设置当前位置(最新的轨迹)
|
|
||||||
if (!traces.isEmpty()) {
|
if (!traces.isEmpty()) {
|
||||||
result.setCurrentLocation(traces.get(0).getLocation());
|
result.setCurrentLocation(traces.get(0).getLocation());
|
||||||
}
|
}
|
||||||
@ -181,201 +215,114 @@ public class ExpressServiceImpl implements ExpressService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ==================== 新API:查询物流轨迹地图 ====================
|
||||||
* 提取城市列表
|
|
||||||
*/
|
|
||||||
private List<String> extractCities(ExpressTraceDTO traceData) {
|
|
||||||
Set<String> citySet = new LinkedHashSet<>();
|
|
||||||
|
|
||||||
if (traceData.getResult() != null && traceData.getResult().getList() != null) {
|
/**
|
||||||
for (ExpressTraceDTO.ExpressTrace trace : traceData.getResult().getList()) {
|
* 调用新API查询物流轨迹地图,返回mapUrl
|
||||||
String location = extractLocation(trace.getStatus());
|
*/
|
||||||
if (location != null && !location.isEmpty()) {
|
@SuppressWarnings("unchecked")
|
||||||
// 尝试匹配城市名称
|
private String queryTraceMap(String trackingNumber, String companyCode, String phone) {
|
||||||
for (String city : CITY_COORDINATES.keySet()) {
|
HttpHeaders headers = new HttpHeaders();
|
||||||
if (location.contains(city.replace("市", ""))) {
|
headers.set("Authorization", "APPCODE " + expressProperties.getAppCode());
|
||||||
citySet.add(city);
|
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
|
||||||
break;
|
|
||||||
}
|
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
|
||||||
}
|
body.add("shipperCode", companyCode);
|
||||||
}
|
body.add("logisticCode", trackingNumber);
|
||||||
}
|
body.add("sort", "asc");
|
||||||
|
|
||||||
|
if (phone != null && !phone.isEmpty()) {
|
||||||
|
body.add("phone", phone);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> cities = new ArrayList<>(citySet);
|
log.info("📡 调用物流地图API - 单号: {}, 公司: {}, phone: {}", trackingNumber, companyCode, phone);
|
||||||
// 反转列表,使其按照物流顺序排列
|
|
||||||
Collections.reverse(cities);
|
|
||||||
|
|
||||||
log.info("🏙️ 提取到的城市列表: {}", cities);
|
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
|
||||||
return cities;
|
|
||||||
|
Map<String, Object> response = restTemplate.postForObject(TRACE_MAP_URL, request, Map.class);
|
||||||
|
|
||||||
|
if (response == null || !Boolean.TRUE.equals(response.get("success"))) {
|
||||||
|
String msg = response != null ? (String) response.get("msg") : "返回为空";
|
||||||
|
log.warn("⚠️ 物流地图API返回失败: {}", msg);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> data = (Map<String, Object>) response.get("data");
|
||||||
|
if (data != null) {
|
||||||
|
String mapUrl = (String) data.get("mapUrl");
|
||||||
|
log.info("🗺️ 获取到物流地图URL: {}", mapUrl);
|
||||||
|
return mapUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 编码转换 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一内部编码(基于快递鸟标准)
|
||||||
|
*/
|
||||||
|
private String convertCompanyNameToCode(String companyName) {
|
||||||
|
if (companyName == null) return null;
|
||||||
|
|
||||||
|
Map<String, String> companyMap = new LinkedHashMap<>();
|
||||||
|
companyMap.put("顺丰", "SF");
|
||||||
|
companyMap.put("圆通", "YTO");
|
||||||
|
companyMap.put("中通", "ZTO");
|
||||||
|
companyMap.put("韵达", "YD");
|
||||||
|
companyMap.put("申通", "STO");
|
||||||
|
companyMap.put("邮政", "EMS");
|
||||||
|
companyMap.put("EMS", "EMS");
|
||||||
|
companyMap.put("京东", "JD");
|
||||||
|
companyMap.put("极兔", "JTSD");
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> entry : companyMap.entrySet()) {
|
||||||
|
if (companyName.contains(entry.getKey())) {
|
||||||
|
return entry.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提取地点信息
|
* 转换为旧API(wuliu)的编码
|
||||||
*/
|
*/
|
||||||
|
private String convertToOldApiCode(String code) {
|
||||||
|
if (code == null) return null;
|
||||||
|
Map<String, String> map = new HashMap<>();
|
||||||
|
map.put("SF", "SFEXPRESS");
|
||||||
|
map.put("YTO", "YTO");
|
||||||
|
map.put("ZTO", "ZTO");
|
||||||
|
map.put("YD", "YUNDA");
|
||||||
|
map.put("STO", "STO");
|
||||||
|
map.put("EMS", "EMS");
|
||||||
|
map.put("JD", "JD");
|
||||||
|
map.put("JTSD", "JTSD");
|
||||||
|
return map.getOrDefault(code, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为新API(快递鸟地图)的编码
|
||||||
|
*/
|
||||||
|
private String convertToMapApiCode(String code) {
|
||||||
|
// 新API用的就是快递鸟标准编码,和内部编码一致
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 工具方法 ====================
|
||||||
|
|
||||||
private String extractLocation(String status) {
|
private String extractLocation(String status) {
|
||||||
if (status == null) return "";
|
if (status == null) return "";
|
||||||
|
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("【(.+?)】");
|
||||||
// 匹配【城市】格式
|
java.util.regex.Matcher matcher = pattern.matcher(status);
|
||||||
Pattern pattern = Pattern.compile("【(.+?)】");
|
|
||||||
Matcher matcher = pattern.matcher(status);
|
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
return matcher.group(1);
|
return matcher.group(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 规划路线
|
|
||||||
*/
|
|
||||||
private ExpressInfoDTO.MapRouteData planRoute(List<String> cities) {
|
|
||||||
ExpressInfoDTO.MapRouteData routeData = new ExpressInfoDTO.MapRouteData();
|
|
||||||
|
|
||||||
// 添加城市标记点
|
|
||||||
List<ExpressInfoDTO.CityPoint> cityPoints = new ArrayList<>();
|
|
||||||
for (int i = 0; i < cities.size(); i++) {
|
|
||||||
String city = cities.get(i);
|
|
||||||
double[] coords = CITY_COORDINATES.get(city);
|
|
||||||
|
|
||||||
if (coords != null) {
|
|
||||||
ExpressInfoDTO.CityPoint point = new ExpressInfoDTO.CityPoint();
|
|
||||||
point.setName(city);
|
|
||||||
point.setLng(coords[0]);
|
|
||||||
point.setLat(coords[1]);
|
|
||||||
point.setIndex(i);
|
|
||||||
cityPoints.add(point);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
routeData.setCities(cityPoints);
|
|
||||||
|
|
||||||
// 如果有多个城市,规划路线
|
|
||||||
if (cityPoints.size() >= 2) {
|
|
||||||
try {
|
|
||||||
// 调用高德驾车路径规划API
|
|
||||||
ExpressInfoDTO.CityPoint origin = cityPoints.get(0);
|
|
||||||
ExpressInfoDTO.CityPoint destination = cityPoints.get(cityPoints.size() - 1);
|
|
||||||
|
|
||||||
String waypoints = null;
|
|
||||||
if (cityPoints.size() > 2) {
|
|
||||||
// 中间途经点
|
|
||||||
waypoints = cityPoints.subList(1, cityPoints.size() - 1).stream()
|
|
||||||
.map(p -> p.getLng() + "," + p.getLat())
|
|
||||||
.collect(Collectors.joining(";"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> routeResult = queryDrivingRoute(
|
|
||||||
origin.getLng() + "," + origin.getLat(),
|
|
||||||
destination.getLng() + "," + destination.getLat(),
|
|
||||||
waypoints
|
|
||||||
);
|
|
||||||
|
|
||||||
// 解析路线数据
|
|
||||||
if (routeResult != null && "1".equals(routeResult.get("status"))) {
|
|
||||||
Map<String, Object> route = (Map<String, Object>) routeResult.get("route");
|
|
||||||
if (route != null) {
|
|
||||||
List<Map<String, Object>> paths = (List<Map<String, Object>>) route.get("paths");
|
|
||||||
if (paths != null && !paths.isEmpty()) {
|
|
||||||
Map<String, Object> path = paths.get(0);
|
|
||||||
routeData.setDistance(path.get("distance").toString());
|
|
||||||
routeData.setDuration(path.get("duration").toString());
|
|
||||||
|
|
||||||
// 提取路径点
|
|
||||||
List<List<Double>> routePoints = extractRoutePoints(path);
|
|
||||||
routeData.setRoutePoints(routePoints);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("⚠️ 路径规划失败,使用直线连接: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return routeData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 调用高德驾车路径规划API
|
|
||||||
*/
|
|
||||||
private Map<String, Object> queryDrivingRoute(String origin, String destination, String waypoints) {
|
|
||||||
try {
|
|
||||||
// 构建URL,确保坐标格式正确
|
|
||||||
StringBuilder urlBuilder = new StringBuilder();
|
|
||||||
urlBuilder.append(amapProperties.getWebApi().getBaseUrl())
|
|
||||||
.append("/v3/direction/driving")
|
|
||||||
.append("?key=").append(amapProperties.getWebApi().getKey())
|
|
||||||
.append("&origin=").append(origin)
|
|
||||||
.append("&destination=").append(destination)
|
|
||||||
.append("&output=json");
|
|
||||||
|
|
||||||
if (waypoints != null && !waypoints.isEmpty()) {
|
|
||||||
urlBuilder.append("&waypoints=").append(waypoints);
|
|
||||||
}
|
|
||||||
|
|
||||||
String url = urlBuilder.toString();
|
|
||||||
|
|
||||||
log.info("🚗 调用高德路径规划API: {}", url);
|
|
||||||
|
|
||||||
// 验证坐标格式
|
|
||||||
if (!origin.matches("\\d+\\.\\d+,\\d+\\.\\d+")) {
|
|
||||||
log.error("❌ 起点坐标格式错误: {}", origin);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!destination.matches("\\d+\\.\\d+,\\d+\\.\\d+")) {
|
|
||||||
log.error("❌ 终点坐标格式错误: {}", destination);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> result = restTemplate.getForObject(url, Map.class);
|
|
||||||
|
|
||||||
log.info("✅ 高德路径规划API响应成功");
|
|
||||||
return result;
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("❌ 高德路径规划API调用失败", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提取路径点
|
|
||||||
*/
|
|
||||||
private List<List<Double>> extractRoutePoints(Map<String, Object> path) {
|
|
||||||
List<List<Double>> points = new ArrayList<>();
|
|
||||||
|
|
||||||
try {
|
|
||||||
List<Map<String, Object>> steps = (List<Map<String, Object>>) path.get("steps");
|
|
||||||
if (steps != null) {
|
|
||||||
for (Map<String, Object> step : steps) {
|
|
||||||
String polyline = (String) step.get("polyline");
|
|
||||||
if (polyline != null) {
|
|
||||||
String[] coords = polyline.split(";");
|
|
||||||
for (String coord : coords) {
|
|
||||||
String[] lngLat = coord.split(",");
|
|
||||||
if (lngLat.length == 2) {
|
|
||||||
points.add(Arrays.asList(
|
|
||||||
Double.parseDouble(lngLat[0]),
|
|
||||||
Double.parseDouble(lngLat[1])
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("⚠️ 解析路径点失败: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return points;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取状态文本
|
|
||||||
*/
|
|
||||||
private String getStatusText(String status) {
|
private String getStatusText(String status) {
|
||||||
if (status == null) return "未知";
|
if (status == null) return "未知";
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "0": return "在途中";
|
case "0": return "在途中";
|
||||||
case "1": return "已揽收";
|
case "1": return "已揽收";
|
||||||
@ -387,33 +334,4 @@ public class ExpressServiceImpl implements ExpressService {
|
|||||||
default: return "未知";
|
default: return "未知";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 转换物流公司名称为代码
|
|
||||||
*/
|
|
||||||
private String convertCompanyNameToCode(String companyName) {
|
|
||||||
if (companyName == null) return null;
|
|
||||||
|
|
||||||
Map<String, String> companyMap = new HashMap<>();
|
|
||||||
companyMap.put("韵达", "yunda");
|
|
||||||
companyMap.put("韵达快递", "yunda");
|
|
||||||
companyMap.put("顺丰", "shunfeng");
|
|
||||||
companyMap.put("顺丰快递", "shunfeng");
|
|
||||||
companyMap.put("圆通", "yuantong");
|
|
||||||
companyMap.put("圆通快递", "yuantong");
|
|
||||||
companyMap.put("中通", "zhongtong");
|
|
||||||
companyMap.put("中通快递", "zhongtong");
|
|
||||||
companyMap.put("申通", "shentong");
|
|
||||||
companyMap.put("申通快递", "shentong");
|
|
||||||
companyMap.put("EMS", "ems");
|
|
||||||
companyMap.put("邮政", "ems");
|
|
||||||
|
|
||||||
for (Map.Entry<String, String> entry : companyMap.entrySet()) {
|
|
||||||
if (companyName.contains(entry.getKey())) {
|
|
||||||
return entry.getValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -136,8 +136,8 @@
|
|||||||
<select id="getSalesTrendByMerchant" parameterType="long" resultType="com.sunnyfarm.dto.MerchantDashboardDTO$SalesTrendDTO">
|
<select id="getSalesTrendByMerchant" parameterType="long" resultType="com.sunnyfarm.dto.MerchantDashboardDTO$SalesTrendDTO">
|
||||||
SELECT
|
SELECT
|
||||||
DATE(created_at) as date,
|
DATE(created_at) as date,
|
||||||
IFNULL(SUM(actual_amount), 0) as amount,
|
IFNULL(SUM(actual_amount), 0) as sales,
|
||||||
COUNT(*) as orderCount
|
COUNT(*) as orders
|
||||||
FROM orders
|
FROM orders
|
||||||
WHERE merchant_id = #{merchantId}
|
WHERE merchant_id = #{merchantId}
|
||||||
AND status IN (2, 3, 4)
|
AND status IN (2, 3, 4)
|
||||||
|
|||||||
42
docker-compose.yml
Normal file
42
docker-compose.yml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ===== 后端服务 - Spring Boot =====
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: sunnyfarm-backend
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
- SPRING_PROFILES_ACTIVE=prod,docker
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
- JAVA_OPTS=-Xms256m -Xmx512m
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
volumes:
|
||||||
|
- ./logs/backend:/app/logs
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- sunnyfarm-net
|
||||||
|
|
||||||
|
# ===== 前端服务 - Nginx =====
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: sunnyfarm-frontend
|
||||||
|
ports:
|
||||||
|
- "53921:80"
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- sunnyfarm-net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
sunnyfarm-net:
|
||||||
|
driver: bridge
|
||||||
36
frontend/Dockerfile
Normal file
36
frontend/Dockerfile
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# ============================================
|
||||||
|
# 农产品直销平台 - 前端 Dockerfile
|
||||||
|
# 多阶段构建: Node编译 → Nginx部署
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# ---------- 第一阶段:Node构建 ----------
|
||||||
|
FROM node:18-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 配置npm镜像(加速依赖下载)
|
||||||
|
RUN npm config set registry https://registry.npmmirror.com
|
||||||
|
|
||||||
|
# 先复制package文件,利用Docker缓存层
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# 复制源代码并构建
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---------- 第二阶段:Nginx部署 ----------
|
||||||
|
FROM nginx:1.25-alpine
|
||||||
|
|
||||||
|
# 设置时区
|
||||||
|
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
|
||||||
|
echo "Asia/Shanghai" > /etc/timezone
|
||||||
|
|
||||||
|
# 复制自定义Nginx配置
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# 从构建阶段复制静态文件
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
32
frontend/nginx.conf
Normal file
32
frontend/nginx.conf
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Gzip压缩
|
||||||
|
gzip on;
|
||||||
|
gzip_min_length 1k;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
||||||
|
gzip_vary on;
|
||||||
|
|
||||||
|
# Vue Router History模式 - SPA路由支持
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 静态资源长期缓存(Vite构建带hash)
|
||||||
|
location /assets/ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 图片等资源缓存
|
||||||
|
location ~* \.(ico|png|jpg|jpeg|gif|svg|webp)$ {
|
||||||
|
expires 7d;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -36,10 +36,14 @@
|
|||||||
<div class="update-time" v-if="expressInfo.updateTime">
|
<div class="update-time" v-if="expressInfo.updateTime">
|
||||||
更新时间:{{ expressInfo.updateTime }}
|
更新时间:{{ expressInfo.updateTime }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="courier-phone" v-if="expressInfo.courierPhone">
|
||||||
|
<el-icon><Phone /></el-icon>
|
||||||
|
快递员电话:{{ expressInfo.courierPhone }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 物流地图 -->
|
<!-- 物流地图 - iframe嵌入 -->
|
||||||
<div class="express-map" v-if="showMap && expressInfo.mapRoute">
|
<div class="express-map" v-if="showMap && expressInfo.mapUrl">
|
||||||
<div class="map-header">
|
<div class="map-header">
|
||||||
<span class="map-title">
|
<span class="map-title">
|
||||||
<el-icon><MapLocation /></el-icon>
|
<el-icon><MapLocation /></el-icon>
|
||||||
@ -51,7 +55,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="mapExpanded" class="map-container">
|
<div v-show="mapExpanded" class="map-container">
|
||||||
<div id="express-map" ref="mapContainer"></div>
|
<iframe
|
||||||
|
:src="expressInfo.mapUrl"
|
||||||
|
class="map-iframe"
|
||||||
|
frameborder="0"
|
||||||
|
allowfullscreen
|
||||||
|
scrolling="no"
|
||||||
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -68,7 +78,7 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
:timestamp="trace.time"
|
:timestamp="trace.time"
|
||||||
placement="top"
|
placement="top"
|
||||||
:type="index === 0 ? 'primary' : 'info'"
|
:type="getTimelineType(trace, index)"
|
||||||
:size="index === 0 ? 'large' : 'normal'"
|
:size="index === 0 ? 'large' : 'normal'"
|
||||||
:hollow="index !== 0"
|
:hollow="index !== 0"
|
||||||
>
|
>
|
||||||
@ -87,7 +97,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import {
|
import {
|
||||||
Loading,
|
Loading,
|
||||||
@ -95,7 +105,8 @@ import {
|
|||||||
CopyDocument,
|
CopyDocument,
|
||||||
Location,
|
Location,
|
||||||
MapLocation,
|
MapLocation,
|
||||||
Clock
|
Clock,
|
||||||
|
Phone
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import request from '../utils/request'
|
import request from '../utils/request'
|
||||||
|
|
||||||
@ -114,8 +125,6 @@ const loading = ref(true)
|
|||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
const expressInfo = ref(null)
|
const expressInfo = ref(null)
|
||||||
const mapExpanded = ref(true)
|
const mapExpanded = ref(true)
|
||||||
const map = ref(null)
|
|
||||||
const mapContainer = ref(null)
|
|
||||||
|
|
||||||
// 加载物流信息
|
// 加载物流信息
|
||||||
const loadExpressInfo = async () => {
|
const loadExpressInfo = async () => {
|
||||||
@ -125,261 +134,17 @@ const loadExpressInfo = async () => {
|
|||||||
try {
|
try {
|
||||||
const response = await request.get(`/express/order/${props.orderId}`)
|
const response = await request.get(`/express/order/${props.orderId}`)
|
||||||
expressInfo.value = response
|
expressInfo.value = response
|
||||||
|
|
||||||
// 如果有地图数据且地图展开,初始化地图
|
|
||||||
if (props.showMap && expressInfo.value.mapRoute && mapExpanded.value) {
|
|
||||||
// 等待DOM完全渲染
|
|
||||||
await nextTick()
|
|
||||||
// 再等待一个周期确保容器准备好
|
|
||||||
setTimeout(() => {
|
|
||||||
if (mapContainer.value) {
|
|
||||||
initMap()
|
|
||||||
} else {
|
|
||||||
console.warn('⚠️ 地图容器仍未就绪,500ms后重试')
|
|
||||||
setTimeout(() => {
|
|
||||||
if (mapContainer.value) {
|
|
||||||
initMap()
|
|
||||||
}
|
|
||||||
}, 500)
|
|
||||||
}
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ 加载物流信息失败:', err)
|
console.error('❌ 加载物流信息失败:', err)
|
||||||
error.value = err.response?.data?.message || '加载物流信息失败'
|
error.value = err.response?.data?.message || err.message || '加载物流信息失败'
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化地图
|
|
||||||
const initMap = () => {
|
|
||||||
if (!window.AMap) {
|
|
||||||
console.warn('⚠️ 高德地图API未加载')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mapContainer.value) {
|
|
||||||
console.warn('⚠️ 地图容器未找到')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建地图实例
|
|
||||||
map.value = new AMap.Map(mapContainer.value, {
|
|
||||||
zoom: 8,
|
|
||||||
mapStyle: 'amap://styles/normal',
|
|
||||||
viewMode: '3D'
|
|
||||||
})
|
|
||||||
|
|
||||||
const routeData = expressInfo.value.mapRoute
|
|
||||||
|
|
||||||
// 添加城市标记
|
|
||||||
if (routeData.cities && routeData.cities.length > 0) {
|
|
||||||
const totalCities = routeData.cities.length
|
|
||||||
|
|
||||||
routeData.cities.forEach((city, index) => {
|
|
||||||
const isStart = index === 0
|
|
||||||
const isEnd = index === totalCities - 1
|
|
||||||
|
|
||||||
// 设置不同位置的颜色和标签
|
|
||||||
let markerColor, labelBg, labelColor, labelText, borderColor
|
|
||||||
|
|
||||||
if (isStart) {
|
|
||||||
// 起点 - 浅绿色
|
|
||||||
markerColor = '#66BB6A'
|
|
||||||
labelBg = '#E8F5E9'
|
|
||||||
labelColor = '#2E7D32'
|
|
||||||
borderColor = '#66BB6A'
|
|
||||||
labelText = '发'
|
|
||||||
} else if (isEnd) {
|
|
||||||
// 终点 - 浅蓝色
|
|
||||||
markerColor = '#42A5F5'
|
|
||||||
labelBg = '#E3F2FD'
|
|
||||||
labelColor = '#1976D2'
|
|
||||||
borderColor = '#42A5F5'
|
|
||||||
labelText = '收'
|
|
||||||
} else {
|
|
||||||
// 中间点 - 浅橙色
|
|
||||||
markerColor = '#FFA726'
|
|
||||||
labelBg = '#FFF3E0'
|
|
||||||
labelColor = '#E65100'
|
|
||||||
borderColor = '#FFA726'
|
|
||||||
labelText = index.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加圆形标记
|
|
||||||
const marker = new AMap.CircleMarker({
|
|
||||||
center: [city.lng, city.lat],
|
|
||||||
radius: 12,
|
|
||||||
strokeColor: markerColor,
|
|
||||||
strokeWeight: 3,
|
|
||||||
fillColor: markerColor,
|
|
||||||
fillOpacity: 0.8,
|
|
||||||
zIndex: 100
|
|
||||||
})
|
|
||||||
|
|
||||||
// 添加文字标签
|
|
||||||
const text = new AMap.Text({
|
|
||||||
text: isStart || isEnd ? `【${labelText}】${city.name}` : `${labelText}·${city.name}`,
|
|
||||||
position: [city.lng, city.lat],
|
|
||||||
offset: new AMap.Pixel(0, -35),
|
|
||||||
style: {
|
|
||||||
'background': labelBg,
|
|
||||||
'border': `2px solid ${borderColor}`,
|
|
||||||
'color': labelColor,
|
|
||||||
'font-size': '13px',
|
|
||||||
'font-weight': 'bold',
|
|
||||||
'padding': '6px 12px',
|
|
||||||
'border-radius': '16px',
|
|
||||||
'box-shadow': '0 2px 8px rgba(0,0,0,0.15)',
|
|
||||||
'white-space': 'nowrap'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
map.value.add([marker, text])
|
|
||||||
})
|
|
||||||
|
|
||||||
// 绘制路线
|
|
||||||
if (routeData.routePoints && routeData.routePoints.length > 0) {
|
|
||||||
// 有真实路线数据
|
|
||||||
|
|
||||||
// 先绘制白色描边(底层)
|
|
||||||
const borderLine = new AMap.Polyline({
|
|
||||||
path: routeData.routePoints,
|
|
||||||
strokeColor: '#FFFFFF',
|
|
||||||
strokeWeight: 10,
|
|
||||||
strokeOpacity: 0.8,
|
|
||||||
strokeStyle: 'solid',
|
|
||||||
lineJoin: 'round',
|
|
||||||
lineCap: 'round',
|
|
||||||
zIndex: 49
|
|
||||||
})
|
|
||||||
|
|
||||||
// 再绘制主路径(深紫色)
|
|
||||||
const polyline = new AMap.Polyline({
|
|
||||||
path: routeData.routePoints,
|
|
||||||
strokeColor: '#8B5CF6',
|
|
||||||
strokeWeight: 6,
|
|
||||||
strokeOpacity: 1,
|
|
||||||
strokeStyle: 'solid',
|
|
||||||
lineJoin: 'round',
|
|
||||||
lineCap: 'round',
|
|
||||||
zIndex: 50,
|
|
||||||
showDir: true
|
|
||||||
})
|
|
||||||
|
|
||||||
map.value.add([borderLine, polyline])
|
|
||||||
} else {
|
|
||||||
// 使用直线连接
|
|
||||||
const path = routeData.cities.map(city => [city.lng, city.lat])
|
|
||||||
|
|
||||||
// 先绘制白色描边(底层)
|
|
||||||
const borderLine = new AMap.Polyline({
|
|
||||||
path: path,
|
|
||||||
strokeColor: '#FFFFFF',
|
|
||||||
strokeWeight: 10,
|
|
||||||
strokeOpacity: 0.8,
|
|
||||||
strokeStyle: 'solid',
|
|
||||||
lineJoin: 'round',
|
|
||||||
lineCap: 'round',
|
|
||||||
zIndex: 49
|
|
||||||
})
|
|
||||||
|
|
||||||
// 再绘制主路径(深紫色)
|
|
||||||
const polyline = new AMap.Polyline({
|
|
||||||
path: path,
|
|
||||||
strokeColor: '#8B5CF6',
|
|
||||||
strokeWeight: 6,
|
|
||||||
strokeOpacity: 1,
|
|
||||||
strokeStyle: 'solid',
|
|
||||||
lineJoin: 'round',
|
|
||||||
showDir: true,
|
|
||||||
zIndex: 50
|
|
||||||
})
|
|
||||||
|
|
||||||
map.value.add([borderLine, polyline])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加当前位置的小车图标
|
|
||||||
const currentLocation = expressInfo.value.currentLocation
|
|
||||||
if (currentLocation && routeData.cities.length > 0) {
|
|
||||||
// 找到当前位置对应的城市坐标
|
|
||||||
let currentCity = null
|
|
||||||
for (const city of routeData.cities) {
|
|
||||||
if (currentLocation.includes(city.name.replace('市', ''))) {
|
|
||||||
currentCity = city
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果找到当前城市,添加小车图标
|
|
||||||
if (currentCity) {
|
|
||||||
const truckIcon = new AMap.Marker({
|
|
||||||
position: [currentCity.lng, currentCity.lat],
|
|
||||||
content: `<div style="
|
|
||||||
background: #FF6B35;
|
|
||||||
color: white;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 20px;
|
|
||||||
box-shadow: 0 4px 12px rgba(255, 107, 53, 0.5);
|
|
||||||
border: 3px solid white;
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
">🚚</div>
|
|
||||||
<style>
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { transform: scale(1); }
|
|
||||||
50% { transform: scale(1.1); }
|
|
||||||
}
|
|
||||||
</style>`,
|
|
||||||
offset: new AMap.Pixel(-20, -20),
|
|
||||||
zIndex: 200
|
|
||||||
})
|
|
||||||
|
|
||||||
// 添加当前位置提示
|
|
||||||
const currentText = new AMap.Text({
|
|
||||||
text: '当前位置',
|
|
||||||
position: [currentCity.lng, currentCity.lat],
|
|
||||||
offset: new AMap.Pixel(0, 25),
|
|
||||||
style: {
|
|
||||||
'background': '#FF6B35',
|
|
||||||
'border': 'none',
|
|
||||||
'color': 'white',
|
|
||||||
'font-size': '12px',
|
|
||||||
'font-weight': 'bold',
|
|
||||||
'padding': '4px 10px',
|
|
||||||
'border-radius': '12px',
|
|
||||||
'box-shadow': '0 2px 6px rgba(255, 107, 53, 0.4)'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
map.value.add([truckIcon, currentText])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动调整视野
|
|
||||||
map.value.setFitView()
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error('❌ 初始化地图失败:', err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换地图展开/收起
|
// 切换地图展开/收起
|
||||||
const toggleMap = async () => {
|
const toggleMap = () => {
|
||||||
mapExpanded.value = !mapExpanded.value
|
mapExpanded.value = !mapExpanded.value
|
||||||
|
|
||||||
if (mapExpanded.value && !map.value) {
|
|
||||||
await nextTick()
|
|
||||||
initMap()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制运单号
|
// 复制运单号
|
||||||
@ -398,14 +163,23 @@ const getStatusType = (status) => {
|
|||||||
'已签收': 'success',
|
'已签收': 'success',
|
||||||
'派件中': 'primary',
|
'派件中': 'primary',
|
||||||
'已揽收': 'info',
|
'已揽收': 'info',
|
||||||
'在途中': 'warning',
|
'运输中': 'warning',
|
||||||
'疑难': 'danger',
|
'问题件': 'danger',
|
||||||
'退签': 'danger',
|
'转寄': 'warning',
|
||||||
'退回': 'danger'
|
'清关中': 'info'
|
||||||
}
|
}
|
||||||
return typeMap[status] || 'info'
|
return typeMap[status] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取时间线节点类型
|
||||||
|
const getTimelineType = (trace, index) => {
|
||||||
|
if (index === 0) return 'primary'
|
||||||
|
if (trace.action === '311') return 'success'
|
||||||
|
if (trace.action === '202' || trace.action === '211') return 'primary'
|
||||||
|
if (trace.action === '1') return 'info'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
// 监听orderId变化
|
// 监听orderId变化
|
||||||
watch(() => props.orderId, () => {
|
watch(() => props.orderId, () => {
|
||||||
if (props.orderId) {
|
if (props.orderId) {
|
||||||
@ -418,12 +192,6 @@ onMounted(() => {
|
|||||||
loadExpressInfo()
|
loadExpressInfo()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (map.value) {
|
|
||||||
map.value.destroy()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -482,7 +250,8 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.current-location,
|
.current-location,
|
||||||
.update-time {
|
.update-time,
|
||||||
|
.courier-phone {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@ -514,13 +283,14 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.map-container {
|
.map-container {
|
||||||
height: 400px;
|
height: 450px;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
#express-map {
|
.map-iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.express-timeline {
|
.express-timeline {
|
||||||
@ -575,7 +345,7 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.map-container {
|
.map-container {
|
||||||
height: 300px;
|
height: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.express-header {
|
.express-header {
|
||||||
|
|||||||
@ -270,7 +270,7 @@ export default {
|
|||||||
if (item.date) {
|
if (item.date) {
|
||||||
const date = new Date(item.date)
|
const date = new Date(item.date)
|
||||||
dates.push(`${date.getMonth() + 1}/${date.getDate()}`)
|
dates.push(`${date.getMonth() + 1}/${date.getDate()}`)
|
||||||
amounts.push(parseFloat(item.amount) || 0)
|
amounts.push(parseFloat(item.sales) || 0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -92,6 +92,21 @@
|
|||||||
>
|
>
|
||||||
确认收货
|
确认收货
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="order.status === 4 && !reviewed"
|
||||||
|
type="success"
|
||||||
|
size="large"
|
||||||
|
@click="handleReviewOrder"
|
||||||
|
>
|
||||||
|
去评价
|
||||||
|
</el-button>
|
||||||
|
<el-tag
|
||||||
|
v-if="order.status === 4 && reviewed"
|
||||||
|
type="success"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
已评价
|
||||||
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
@ -226,6 +241,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 评价弹窗 -->
|
||||||
|
<ReviewModal
|
||||||
|
:visible="reviewVisible"
|
||||||
|
@update:visible="reviewVisible = $event"
|
||||||
|
:product="reviewProduct"
|
||||||
|
:orderId="reviewOrderId"
|
||||||
|
@success="onReviewSuccess"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -233,6 +257,8 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import ReviewModal from '../../components/ReviewModal.vue'
|
||||||
|
import { checkUserReview } from '../../api/review'
|
||||||
import {
|
import {
|
||||||
ArrowDown,
|
ArrowDown,
|
||||||
Document,
|
Document,
|
||||||
@ -253,6 +279,7 @@ import ExpressTracking from '../../components/ExpressTracking.vue'
|
|||||||
export default {
|
export default {
|
||||||
name: 'OrderDetail',
|
name: 'OrderDetail',
|
||||||
components: {
|
components: {
|
||||||
|
ReviewModal,
|
||||||
ArrowDown,
|
ArrowDown,
|
||||||
Document,
|
Document,
|
||||||
Location,
|
Location,
|
||||||
@ -271,6 +298,10 @@ export default {
|
|||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
|
const reviewVisible = ref(false)
|
||||||
|
const reviewed = ref(false)
|
||||||
|
const reviewProduct = ref({})
|
||||||
|
const reviewOrderId = ref(0)
|
||||||
const payLoading = ref(false)
|
const payLoading = ref(false)
|
||||||
const order = ref(null)
|
const order = ref(null)
|
||||||
|
|
||||||
@ -345,6 +376,19 @@ export default {
|
|||||||
order.value = response
|
order.value = response
|
||||||
|
|
||||||
console.log('订单详情完整数据:', JSON.stringify(response, null, 2))
|
console.log('订单详情完整数据:', JSON.stringify(response, null, 2))
|
||||||
|
|
||||||
|
// 检查评价状态
|
||||||
|
if (response.status === 4) {
|
||||||
|
try {
|
||||||
|
const items = response.orderItems || response.orderDetails || []
|
||||||
|
if (items.length > 0) {
|
||||||
|
const res = await checkUserReview(items[0].productId, response.id)
|
||||||
|
reviewed.value = res === true || res?.data === true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
reviewed.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
console.log('创建时间字段检查:')
|
console.log('创建时间字段检查:')
|
||||||
console.log('- createdAt:', response.createdAt)
|
console.log('- createdAt:', response.createdAt)
|
||||||
console.log('- created_at:', response.created_at)
|
console.log('- created_at:', response.created_at)
|
||||||
@ -530,6 +574,28 @@ export default {
|
|||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleReviewOrder = () => {
|
||||||
|
const items = order.value.orderItems || order.value.orderDetails || []
|
||||||
|
if (items.length > 0) {
|
||||||
|
const item = items[0]
|
||||||
|
reviewProduct.value = {
|
||||||
|
id: item.productId,
|
||||||
|
name: item.productName,
|
||||||
|
price: item.price,
|
||||||
|
image: item.productImage,
|
||||||
|
merchantId: order.value.merchantId
|
||||||
|
}
|
||||||
|
reviewOrderId.value = order.value.id
|
||||||
|
reviewVisible.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onReviewSuccess = () => {
|
||||||
|
reviewVisible.value = false
|
||||||
|
reviewed.value = true
|
||||||
|
ElMessage.success('评价成功!')
|
||||||
|
}
|
||||||
|
|
||||||
const goToOrders = () => {
|
const goToOrders = () => {
|
||||||
router.push('/orders')
|
router.push('/orders')
|
||||||
}
|
}
|
||||||
@ -578,6 +644,12 @@ export default {
|
|||||||
return {
|
return {
|
||||||
// 数据
|
// 数据
|
||||||
loading,
|
loading,
|
||||||
|
reviewVisible,
|
||||||
|
reviewed,
|
||||||
|
reviewProduct,
|
||||||
|
reviewOrderId,
|
||||||
|
handleReviewOrder,
|
||||||
|
onReviewSuccess,
|
||||||
payLoading,
|
payLoading,
|
||||||
order,
|
order,
|
||||||
route,
|
route,
|
||||||
|
|||||||
@ -128,6 +128,21 @@
|
|||||||
>
|
>
|
||||||
确认收货
|
确认收货
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="order.status === 4 && !order.reviewed"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
@click="handleReviewOrder(order)"
|
||||||
|
>
|
||||||
|
去评价
|
||||||
|
</el-button>
|
||||||
|
<el-tag
|
||||||
|
v-if="order.status === 4 && order.reviewed"
|
||||||
|
type="success"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
已评价
|
||||||
|
</el-tag>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="[4, 5, 6].includes(order.status)"
|
v-if="[4, 5, 6].includes(order.status)"
|
||||||
size="small"
|
size="small"
|
||||||
@ -162,12 +177,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 评价弹窗 -->
|
||||||
|
<ReviewModal
|
||||||
|
:visible="reviewVisible"
|
||||||
|
@update:visible="reviewVisible = $event"
|
||||||
|
:product="reviewProduct"
|
||||||
|
:orderId="reviewOrderId"
|
||||||
|
@success="onReviewSuccess"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import ReviewModal from '../../components/ReviewModal.vue'
|
||||||
|
import { checkUserReview } from '../../api/review'
|
||||||
import { Document, ArrowDown } from '@element-plus/icons-vue'
|
import { Document, ArrowDown } from '@element-plus/icons-vue'
|
||||||
import { userStore } from '../../store/user'
|
import { userStore } from '../../store/user'
|
||||||
import request from '../../utils/request'
|
import request from '../../utils/request'
|
||||||
@ -175,6 +201,7 @@ import request from '../../utils/request'
|
|||||||
export default {
|
export default {
|
||||||
name: 'Orders',
|
name: 'Orders',
|
||||||
components: {
|
components: {
|
||||||
|
ReviewModal,
|
||||||
Document,
|
Document,
|
||||||
ArrowDown
|
ArrowDown
|
||||||
},
|
},
|
||||||
@ -182,6 +209,9 @@ export default {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const reviewVisible = ref(false)
|
||||||
|
const reviewProduct = ref({})
|
||||||
|
const reviewOrderId = ref(0)
|
||||||
const orders = ref([])
|
const orders = ref([])
|
||||||
const activeStatus = ref('all')
|
const activeStatus = ref('all')
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
@ -262,6 +292,21 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('📋 订单列表处理完成:', orders.value.length, '条订单')
|
console.log('📋 订单列表处理完成:', orders.value.length, '条订单')
|
||||||
|
|
||||||
|
// 检查已完成订单的评价状态
|
||||||
|
for (const order of orders.value) {
|
||||||
|
if (order.status === 4) {
|
||||||
|
try {
|
||||||
|
const items = order.orderItems || order.orderDetails || []
|
||||||
|
if (items.length > 0) {
|
||||||
|
const res = await checkUserReview(items[0].productId, order.id)
|
||||||
|
order.reviewed = res === true || res?.data === true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
order.reviewed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 加载订单失败:', error)
|
console.error('❌ 加载订单失败:', error)
|
||||||
ElMessage.error('加载订单失败')
|
ElMessage.error('加载订单失败')
|
||||||
@ -347,6 +392,28 @@ export default {
|
|||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleReviewOrder = (order) => {
|
||||||
|
const items = order.orderItems || order.orderDetails || []
|
||||||
|
if (items.length > 0) {
|
||||||
|
const item = items[0]
|
||||||
|
reviewProduct.value = {
|
||||||
|
id: item.productId,
|
||||||
|
name: item.productName,
|
||||||
|
price: item.price,
|
||||||
|
image: item.productImage,
|
||||||
|
merchantId: order.merchantId
|
||||||
|
}
|
||||||
|
reviewOrderId.value = order.id
|
||||||
|
reviewVisible.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onReviewSuccess = () => {
|
||||||
|
reviewVisible.value = false
|
||||||
|
ElMessage.success('评价成功!')
|
||||||
|
loadOrders()
|
||||||
|
}
|
||||||
|
|
||||||
const handleDeleteOrder = (order) => {
|
const handleDeleteOrder = (order) => {
|
||||||
ElMessageBox.confirm('确认删除该订单?', '确认删除', {
|
ElMessageBox.confirm('确认删除该订单?', '确认删除', {
|
||||||
confirmButtonText: '确认',
|
confirmButtonText: '确认',
|
||||||
@ -453,6 +520,11 @@ export default {
|
|||||||
return {
|
return {
|
||||||
userStore,
|
userStore,
|
||||||
loading,
|
loading,
|
||||||
|
reviewVisible,
|
||||||
|
reviewProduct,
|
||||||
|
reviewOrderId,
|
||||||
|
handleReviewOrder,
|
||||||
|
onReviewSuccess,
|
||||||
orders,
|
orders,
|
||||||
activeStatus,
|
activeStatus,
|
||||||
currentPage,
|
currentPage,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user