END
This commit is contained in:
parent
af926257b9
commit
364b7acbb7
@ -0,0 +1,24 @@
|
|||||||
|
package com.sunnyfarm.config.properties;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阿里云物流轨迹地图API配置属性
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "aliyun.express-map")
|
||||||
|
public class AliyunExpressMapProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AppCode
|
||||||
|
*/
|
||||||
|
private String appCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API基础URL
|
||||||
|
*/
|
||||||
|
private String baseUrl;
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.sunnyfarm.config.properties;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阿里云物流API配置属性
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "aliyun.express")
|
||||||
|
public class AliyunExpressProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AppCode
|
||||||
|
*/
|
||||||
|
private String appCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API基础URL
|
||||||
|
*/
|
||||||
|
private String baseUrl;
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package com.sunnyfarm.config.properties;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高德地图配置属性
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "amap")
|
||||||
|
public class AmapProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web服务API配置(后端调用)
|
||||||
|
*/
|
||||||
|
private WebApi webApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JS API配置(前端使用)
|
||||||
|
*/
|
||||||
|
private JsApi jsApi;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class WebApi {
|
||||||
|
private String key;
|
||||||
|
private String baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class JsApi {
|
||||||
|
private String key;
|
||||||
|
private String securityKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,6 +23,7 @@ import java.time.LocalTime;
|
|||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@ -60,6 +61,9 @@ public class AdminController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private JwtUtil jwtUtil;
|
private JwtUtil jwtUtil;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ProductImageService productImageService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理员登录
|
* 管理员登录
|
||||||
*/
|
*/
|
||||||
@ -341,8 +345,16 @@ public class AdminController {
|
|||||||
|
|
||||||
Page<Product> result = productService.page(pageParam, queryWrapper);
|
Page<Product> result = productService.page(pageParam, queryWrapper);
|
||||||
|
|
||||||
|
// 批量加载商品图片
|
||||||
|
List<Product> products = result.getRecords();
|
||||||
|
if (products != null && !products.isEmpty()) {
|
||||||
|
for (Product product : products) {
|
||||||
|
loadProductImages(product);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PageResult<Product> pageResult = new PageResult<>();
|
PageResult<Product> pageResult = new PageResult<>();
|
||||||
pageResult.setRecords(result.getRecords());
|
pageResult.setRecords(products);
|
||||||
pageResult.setTotal(result.getTotal());
|
pageResult.setTotal(result.getTotal());
|
||||||
pageResult.setCurrent(result.getCurrent());
|
pageResult.setCurrent(result.getCurrent());
|
||||||
pageResult.setSize(result.getSize());
|
pageResult.setSize(result.getSize());
|
||||||
@ -380,6 +392,43 @@ public class AdminController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员获取商品详情(不限制状态)
|
||||||
|
*/
|
||||||
|
@GetMapping("/products/{id}")
|
||||||
|
public Result<Product> getProductDetail(@PathVariable Long id) {
|
||||||
|
// 管理员端不限制商品状态,可以查看所有商品
|
||||||
|
Product product = productService.getById(id);
|
||||||
|
|
||||||
|
if (product == null) {
|
||||||
|
return Result.error("商品不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载商品图片
|
||||||
|
loadProductImages(product);
|
||||||
|
|
||||||
|
return Result.success(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载商品图片列表
|
||||||
|
*/
|
||||||
|
private void loadProductImages(Product product) {
|
||||||
|
if (product != null) {
|
||||||
|
QueryWrapper<ProductImage> imageQueryWrapper = new QueryWrapper<>();
|
||||||
|
imageQueryWrapper.eq("product_id", product.getId())
|
||||||
|
.orderByAsc("sort_order");
|
||||||
|
List<ProductImage> images = productImageService.list(imageQueryWrapper);
|
||||||
|
|
||||||
|
if (images != null && !images.isEmpty()) {
|
||||||
|
List<String> imageList = images.stream()
|
||||||
|
.map(ProductImage::getImageUrl)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
product.setImageList(imageList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取订单列表
|
* 获取订单列表
|
||||||
*/
|
*/
|
||||||
@ -418,6 +467,44 @@ public class AdminController {
|
|||||||
return Result.success(pageResult);
|
return Result.success(pageResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员获取订单详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/orders/{id}")
|
||||||
|
public Result<Order> getOrderDetail(@PathVariable Long id) {
|
||||||
|
try {
|
||||||
|
// 管理员可以查看所有订单,不需要验证用户身份
|
||||||
|
Order order = orderService.getOrderDetail(id);
|
||||||
|
|
||||||
|
if (order == null) {
|
||||||
|
return Result.error("订单不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置状态名称
|
||||||
|
order.setStatusName(getOrderStatusName(order.getStatus()));
|
||||||
|
|
||||||
|
return Result.success(order);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Result.error("获取订单详情失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单状态名称
|
||||||
|
*/
|
||||||
|
private String getOrderStatusName(Integer status) {
|
||||||
|
switch (status) {
|
||||||
|
case 1: return "待支付";
|
||||||
|
case 2: return "已支付";
|
||||||
|
case 3: return "已发货";
|
||||||
|
case 4: return "已完成";
|
||||||
|
case 5: return "已取消";
|
||||||
|
case 6: return "已退款";
|
||||||
|
default: return "未知状态";
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 获取最新动态
|
* 获取最新动态
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.sunnyfarm.controller;
|
||||||
|
|
||||||
|
import com.sunnyfarm.common.Result;
|
||||||
|
import com.sunnyfarm.dto.express.ExpressInfoDTO;
|
||||||
|
import com.sunnyfarm.service.ExpressService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物流查询Controller
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/express")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ExpressController {
|
||||||
|
|
||||||
|
private final ExpressService expressService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据快递单号查询物流信息
|
||||||
|
*/
|
||||||
|
@GetMapping("/query")
|
||||||
|
public Result<ExpressInfoDTO> queryExpress(
|
||||||
|
@RequestParam String trackingNumber,
|
||||||
|
@RequestParam(required = false) String companyCode
|
||||||
|
) {
|
||||||
|
log.info("📦 查询物流信息 - 单号: {}, 公司: {}", trackingNumber, companyCode);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ExpressInfoDTO result = expressService.queryExpressInfo(trackingNumber, companyCode);
|
||||||
|
return Result.success(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("❌ 查询物流信息失败", e);
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订单ID查询物流信息
|
||||||
|
*/
|
||||||
|
@GetMapping("/order/{orderId}")
|
||||||
|
public Result<ExpressInfoDTO> queryExpressByOrder(@PathVariable Long orderId) {
|
||||||
|
log.info("📦 根据订单查询物流 - 订单ID: {}", orderId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ExpressInfoDTO result = expressService.queryExpressByOrderId(orderId);
|
||||||
|
return Result.success(result);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("❌ 查询物流信息失败", e);
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
package com.sunnyfarm.dto.express;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高德路径规划DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AmapRouteDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态码:1=成功
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回信息
|
||||||
|
*/
|
||||||
|
private String info;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路线方案
|
||||||
|
*/
|
||||||
|
private Route route;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Route {
|
||||||
|
/**
|
||||||
|
* 起点坐标
|
||||||
|
*/
|
||||||
|
private String origin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 终点坐标
|
||||||
|
*/
|
||||||
|
private String destination;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径列表
|
||||||
|
*/
|
||||||
|
private List<Path> paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Path {
|
||||||
|
/**
|
||||||
|
* 距离(米)
|
||||||
|
*/
|
||||||
|
private String distance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间(秒)
|
||||||
|
*/
|
||||||
|
private String duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路段列表
|
||||||
|
*/
|
||||||
|
private List<Step> steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Step {
|
||||||
|
/**
|
||||||
|
* 路段坐标点串
|
||||||
|
*/
|
||||||
|
private String polyline;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 道路名称
|
||||||
|
*/
|
||||||
|
private String road;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 距离(米)
|
||||||
|
*/
|
||||||
|
private String distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
package com.sunnyfarm.dto.express;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物流综合信息DTO(包含轨迹和地图数据)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ExpressInfoDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快递单号
|
||||||
|
*/
|
||||||
|
private String trackingNumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快递公司名称
|
||||||
|
*/
|
||||||
|
private String companyName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快递公司代码
|
||||||
|
*/
|
||||||
|
private String companyCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物流状态
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前位置
|
||||||
|
*/
|
||||||
|
private String currentLocation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
private String updateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物流轨迹列表
|
||||||
|
*/
|
||||||
|
private List<TraceItem> traces;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 地图路线数据
|
||||||
|
*/
|
||||||
|
private MapRouteData mapRoute;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class TraceItem {
|
||||||
|
/**
|
||||||
|
* 时间
|
||||||
|
*/
|
||||||
|
private String time;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 地点
|
||||||
|
*/
|
||||||
|
private String location;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态描述
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package com.sunnyfarm.dto.express;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物流轨迹地图DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ExpressMapDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否成功
|
||||||
|
*/
|
||||||
|
private Boolean Success;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 原因
|
||||||
|
*/
|
||||||
|
private String Reason;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前位置
|
||||||
|
*/
|
||||||
|
private String Location;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轨迹列表
|
||||||
|
*/
|
||||||
|
private List<Trace> Traces;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Trace {
|
||||||
|
/**
|
||||||
|
* 接收时间
|
||||||
|
*/
|
||||||
|
private String AcceptTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收站点
|
||||||
|
*/
|
||||||
|
private String AcceptStation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 位置
|
||||||
|
*/
|
||||||
|
private String Location;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 操作
|
||||||
|
*/
|
||||||
|
private String Action;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
package com.sunnyfarm.dto.express;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物流轨迹DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ExpressTraceDTO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态码:0=成功
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息
|
||||||
|
*/
|
||||||
|
private String msg;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结果数据
|
||||||
|
*/
|
||||||
|
private ExpressResult result;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class ExpressResult {
|
||||||
|
/**
|
||||||
|
* 快递单号
|
||||||
|
*/
|
||||||
|
private String number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快递公司代码
|
||||||
|
*/
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快递公司名称
|
||||||
|
*/
|
||||||
|
private String expName;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 官网
|
||||||
|
*/
|
||||||
|
private String expSite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客服电话
|
||||||
|
*/
|
||||||
|
private String courierPhone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物流状态:0=在途,1=揽收,2=疑难,3=签收,4=退签,5=派件,6=退回
|
||||||
|
*/
|
||||||
|
private String deliverystatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否签收:0=未签收,1=已签收
|
||||||
|
*/
|
||||||
|
private String issign;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
private String updateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运输时长
|
||||||
|
*/
|
||||||
|
private String takeTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物流轨迹列表
|
||||||
|
*/
|
||||||
|
private List<ExpressTrace> list;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class ExpressTrace {
|
||||||
|
/**
|
||||||
|
* 时间
|
||||||
|
*/
|
||||||
|
private String time;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态描述
|
||||||
|
*/
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +1,13 @@
|
|||||||
package com.sunnyfarm.entity;
|
package com.sunnyfarm.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
import javax.validation.constraints.NotBlank;
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@ -21,4 +23,8 @@ public class ProductImage extends BaseEntity {
|
|||||||
private Boolean isMain = false; // 是否主图
|
private Boolean isMain = false; // 是否主图
|
||||||
|
|
||||||
private Integer sortOrder = 0; // 排序
|
private Integer sortOrder = 0; // 排序
|
||||||
|
|
||||||
|
// product_images表没有updated_at字段,覆盖BaseEntity中的updateTime字段并标记为不存在
|
||||||
|
@TableField(exist = false)
|
||||||
|
private LocalDateTime updateTime; // 注意:是 updateTime,不是 updatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
package com.sunnyfarm.service;
|
||||||
|
|
||||||
|
import com.sunnyfarm.dto.express.ExpressInfoDTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物流查询服务接口
|
||||||
|
*/
|
||||||
|
public interface ExpressService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询物流信息(包含轨迹和地图数据)
|
||||||
|
*
|
||||||
|
* @param trackingNumber 快递单号
|
||||||
|
* @param companyCode 快递公司代码(可选,如果不传会自动识别)
|
||||||
|
* @return 物流综合信息
|
||||||
|
*/
|
||||||
|
ExpressInfoDTO queryExpressInfo(String trackingNumber, String companyCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订单ID查询物流信息
|
||||||
|
*
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @return 物流综合信息
|
||||||
|
*/
|
||||||
|
ExpressInfoDTO queryExpressByOrderId(Long orderId);
|
||||||
|
}
|
||||||
@ -108,4 +108,9 @@ public interface ProductService extends IService<Product> {
|
|||||||
* 获取商家热销商品排行
|
* 获取商家热销商品排行
|
||||||
*/
|
*/
|
||||||
List<MerchantDashboardDTO.ProductSalesDTO> getTopProductsByMerchant(Long merchantId, Integer limit);
|
List<MerchantDashboardDTO.ProductSalesDTO> getTopProductsByMerchant(Long merchantId, Integer limit);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增加商品销量
|
||||||
|
*/
|
||||||
|
void increaseSalesCount(Long productId, Integer quantity);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,419 @@
|
|||||||
|
package com.sunnyfarm.service.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.sunnyfarm.config.properties.AliyunExpressProperties;
|
||||||
|
import com.sunnyfarm.config.properties.AmapProperties;
|
||||||
|
import com.sunnyfarm.dto.express.ExpressInfoDTO;
|
||||||
|
import com.sunnyfarm.dto.express.ExpressTraceDTO;
|
||||||
|
import com.sunnyfarm.entity.Order;
|
||||||
|
import com.sunnyfarm.exception.BusinessException;
|
||||||
|
import com.sunnyfarm.mapper.OrderMapper;
|
||||||
|
import com.sunnyfarm.service.ExpressService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.*;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 物流查询服务实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ExpressServiceImpl implements ExpressService {
|
||||||
|
|
||||||
|
private final AliyunExpressProperties expressProperties;
|
||||||
|
private final AmapProperties amapProperties;
|
||||||
|
private final OrderMapper orderMapper;
|
||||||
|
private final RestTemplate restTemplate = new RestTemplate();
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
// 预设主要城市坐标
|
||||||
|
private static final Map<String, double[]> CITY_COORDINATES = new HashMap<>();
|
||||||
|
|
||||||
|
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
|
||||||
|
public ExpressInfoDTO queryExpressInfo(String trackingNumber, String companyCode) {
|
||||||
|
log.info("🔍 查询物流信息 - 单号: {}, 公司代码: {}", trackingNumber, companyCode);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 调用阿里云物流查询API
|
||||||
|
ExpressTraceDTO traceData = queryExpressTrace(trackingNumber, companyCode);
|
||||||
|
|
||||||
|
if (traceData == null || !"0".equals(traceData.getStatus())) {
|
||||||
|
throw new BusinessException("物流信息查询失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 解析物流轨迹数据
|
||||||
|
ExpressInfoDTO result = parseExpressData(traceData);
|
||||||
|
|
||||||
|
// 3. 提取城市并规划路线
|
||||||
|
List<String> cities = extractCities(traceData);
|
||||||
|
if (!cities.isEmpty()) {
|
||||||
|
ExpressInfoDTO.MapRouteData routeData = planRoute(cities);
|
||||||
|
result.setMapRoute(routeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("✅ 物流信息查询成功 - 单号: {}", trackingNumber);
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("❌ 物流信息查询失败", e);
|
||||||
|
throw new BusinessException("物流信息查询失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExpressInfoDTO queryExpressByOrderId(Long orderId) {
|
||||||
|
log.info("🔍 根据订单ID查询物流 - 订单ID: {}", orderId);
|
||||||
|
|
||||||
|
Order order = orderMapper.selectById(orderId);
|
||||||
|
if (order == null) {
|
||||||
|
throw new BusinessException("订单不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order.getShipNo() == null || order.getShipNo().isEmpty()) {
|
||||||
|
throw new BusinessException("订单暂无物流信息");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将订单的物流公司名称转换为代码
|
||||||
|
String companyCode = convertCompanyNameToCode(order.getShipCompany());
|
||||||
|
|
||||||
|
return queryExpressInfo(order.getShipNo(), companyCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用阿里云物流查询API
|
||||||
|
*/
|
||||||
|
private ExpressTraceDTO queryExpressTrace(String trackingNumber, String companyCode) {
|
||||||
|
try {
|
||||||
|
String url = UriComponentsBuilder
|
||||||
|
.fromHttpUrl(expressProperties.getBaseUrl() + "/kdi")
|
||||||
|
.queryParam("no", trackingNumber)
|
||||||
|
.queryParam("type", companyCode != null ? companyCode : "")
|
||||||
|
.build()
|
||||||
|
.toUriString();
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.set("Authorization", "APPCODE " + expressProperties.getAppCode());
|
||||||
|
|
||||||
|
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||||
|
|
||||||
|
log.info("📡 调用阿里云物流API: {}", url);
|
||||||
|
|
||||||
|
ResponseEntity<ExpressTraceDTO> response = restTemplate.exchange(
|
||||||
|
url,
|
||||||
|
HttpMethod.GET,
|
||||||
|
entity,
|
||||||
|
ExpressTraceDTO.class
|
||||||
|
);
|
||||||
|
|
||||||
|
log.info("✅ 阿里云物流API响应成功");
|
||||||
|
return response.getBody();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("❌ 调用阿里云物流API失败", e);
|
||||||
|
throw new BusinessException("物流查询服务异常");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析物流数据
|
||||||
|
*/
|
||||||
|
private ExpressInfoDTO parseExpressData(ExpressTraceDTO traceData) {
|
||||||
|
ExpressInfoDTO result = new ExpressInfoDTO();
|
||||||
|
ExpressTraceDTO.ExpressResult data = traceData.getResult();
|
||||||
|
|
||||||
|
result.setTrackingNumber(data.getNumber());
|
||||||
|
result.setCompanyName(data.getExpName());
|
||||||
|
result.setCompanyCode(data.getType());
|
||||||
|
result.setStatus(getStatusText(data.getDeliverystatus()));
|
||||||
|
result.setUpdateTime(data.getUpdateTime());
|
||||||
|
|
||||||
|
// 转换物流轨迹
|
||||||
|
if (data.getList() != null && !data.getList().isEmpty()) {
|
||||||
|
List<ExpressInfoDTO.TraceItem> traces = data.getList().stream()
|
||||||
|
.map(trace -> {
|
||||||
|
ExpressInfoDTO.TraceItem item = new ExpressInfoDTO.TraceItem();
|
||||||
|
item.setTime(trace.getTime());
|
||||||
|
item.setStatus(trace.getStatus());
|
||||||
|
|
||||||
|
// 提取地点信息
|
||||||
|
String location = extractLocation(trace.getStatus());
|
||||||
|
item.setLocation(location);
|
||||||
|
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
result.setTraces(traces);
|
||||||
|
|
||||||
|
// 设置当前位置(最新的轨迹)
|
||||||
|
if (!traces.isEmpty()) {
|
||||||
|
result.setCurrentLocation(traces.get(0).getLocation());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取城市列表
|
||||||
|
*/
|
||||||
|
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()) {
|
||||||
|
String location = extractLocation(trace.getStatus());
|
||||||
|
if (location != null && !location.isEmpty()) {
|
||||||
|
// 尝试匹配城市名称
|
||||||
|
for (String city : CITY_COORDINATES.keySet()) {
|
||||||
|
if (location.contains(city.replace("市", ""))) {
|
||||||
|
citySet.add(city);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> cities = new ArrayList<>(citySet);
|
||||||
|
// 反转列表,使其按照物流顺序排列
|
||||||
|
Collections.reverse(cities);
|
||||||
|
|
||||||
|
log.info("🏙️ 提取到的城市列表: {}", cities);
|
||||||
|
return cities;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取地点信息
|
||||||
|
*/
|
||||||
|
private String extractLocation(String status) {
|
||||||
|
if (status == null) return "";
|
||||||
|
|
||||||
|
// 匹配【城市】格式
|
||||||
|
Pattern pattern = Pattern.compile("【(.+?)】");
|
||||||
|
Matcher matcher = pattern.matcher(status);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return matcher.group(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (status == null) return "未知";
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "0": return "在途中";
|
||||||
|
case "1": return "已揽收";
|
||||||
|
case "2": return "疑难";
|
||||||
|
case "3": return "已签收";
|
||||||
|
case "4": return "退签";
|
||||||
|
case "5": return "派件中";
|
||||||
|
case "6": 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -28,6 +28,7 @@ import java.util.*;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
@Transactional
|
||||||
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
|
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
|
||||||
@ -468,6 +469,20 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
|
|||||||
|
|
||||||
boolean success = this.updateById(order);
|
boolean success = this.updateById(order);
|
||||||
|
|
||||||
|
// 更新商品销量
|
||||||
|
if (success) {
|
||||||
|
try {
|
||||||
|
List<OrderDetail> orderDetails = orderDetailService.getByOrderId(orderId);
|
||||||
|
for (OrderDetail detail : orderDetails) {
|
||||||
|
productService.increaseSalesCount(detail.getProductId(), detail.getQuantity());
|
||||||
|
}
|
||||||
|
log.info("订单支付成功,已更新商品销量: 订单ID={}", orderId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新商品销量失败: {}", e.getMessage(), e);
|
||||||
|
// 销量更新失败不影响支付流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
try {
|
try {
|
||||||
systemLogService.logOrderPay(order.getOrderNo(), order.getUserId());
|
systemLogService.logOrderPay(order.getOrderNo(), order.getUserId());
|
||||||
|
|||||||
@ -367,4 +367,23 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
|
|||||||
public List<MerchantDashboardDTO.ProductSalesDTO> getTopProductsByMerchant(Long merchantId, Integer limit) {
|
public List<MerchantDashboardDTO.ProductSalesDTO> getTopProductsByMerchant(Long merchantId, Integer limit) {
|
||||||
return new ArrayList<>();
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void increaseSalesCount(Long productId, Integer quantity) {
|
||||||
|
if (productId == null || quantity == null || quantity <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Product product = getById(productId);
|
||||||
|
if (product != null) {
|
||||||
|
int currentSales = product.getSalesCount() != null ? product.getSalesCount() : 0;
|
||||||
|
product.setSalesCount(currentSales + quantity);
|
||||||
|
updateById(product);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 销量更新失败不影响主流程
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
129
export_copyright_code.sh
Executable file
129
export_copyright_code.sh
Executable file
@ -0,0 +1,129 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 软著代码导出脚本
|
||||||
|
# 目标路径
|
||||||
|
OUTPUT_DIR="/Users/lishunqin/Desktop/桃金娘/软著申请相关/德育大模型"
|
||||||
|
OUTPUT_FILE="$OUTPUT_DIR/sunnyfarm.txt"
|
||||||
|
|
||||||
|
# 创建输出目录
|
||||||
|
mkdir -p "$OUTPUT_DIR"
|
||||||
|
|
||||||
|
# 清空或创建输出文件
|
||||||
|
> "$OUTPUT_FILE"
|
||||||
|
|
||||||
|
echo "======================================"
|
||||||
|
echo "开始导出软著代码"
|
||||||
|
echo "======================================"
|
||||||
|
|
||||||
|
# 函数:添加文件到输出,删除空白行
|
||||||
|
add_file() {
|
||||||
|
local file=$1
|
||||||
|
local relative_path=${file#./}
|
||||||
|
|
||||||
|
echo "" >> "$OUTPUT_FILE"
|
||||||
|
echo "// ==================== $relative_path ====================" >> "$OUTPUT_FILE"
|
||||||
|
echo "" >> "$OUTPUT_FILE"
|
||||||
|
|
||||||
|
# 删除空白行并追加到输出文件
|
||||||
|
grep -v '^[[:space:]]*$' "$file" >> "$OUTPUT_FILE"
|
||||||
|
|
||||||
|
echo "✓ 已添加: $relative_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. 导出后端Java代码
|
||||||
|
echo ""
|
||||||
|
echo "【第一部分:后端Java代码】"
|
||||||
|
echo "------------------------------"
|
||||||
|
|
||||||
|
# 导出Java源码文件
|
||||||
|
find ./backend/src/main/java -type f -name "*.java" | sort | while read file; do
|
||||||
|
add_file "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 2. 导出后端配置文件
|
||||||
|
echo ""
|
||||||
|
echo "【第二部分:后端配置文件】"
|
||||||
|
echo "------------------------------"
|
||||||
|
|
||||||
|
# 导出yml配置文件(排除备份文件)
|
||||||
|
find ./backend/src/main/resources -type f \( -name "*.yml" -o -name "*.yaml" \) \
|
||||||
|
! -name "*.bak*" ! -name "*.backup*" ! -name "*.example" ! -name "*.template" | sort | while read file; do
|
||||||
|
add_file "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 导出XML映射文件
|
||||||
|
find ./backend/src/main/resources/mapper -type f -name "*.xml" 2>/dev/null | sort | while read file; do
|
||||||
|
add_file "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 导出pom.xml
|
||||||
|
if [ -f "./backend/pom.xml" ]; then
|
||||||
|
add_file "./backend/pom.xml"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. 导出前端代码
|
||||||
|
echo ""
|
||||||
|
echo "【第三部分:前端Vue/JS代码】"
|
||||||
|
echo "------------------------------"
|
||||||
|
|
||||||
|
# 导出Vue组件
|
||||||
|
find ./frontend/src -type f -name "*.vue" \
|
||||||
|
! -name "*.bak*" ! -name "*.backup*" | sort | while read file; do
|
||||||
|
add_file "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 导出JS文件
|
||||||
|
find ./frontend/src -type f -name "*.js" | sort | while read file; do
|
||||||
|
add_file "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 4. 导出前端配置文件
|
||||||
|
echo ""
|
||||||
|
echo "【第四部分:前端配置文件】"
|
||||||
|
echo "------------------------------"
|
||||||
|
|
||||||
|
# package.json
|
||||||
|
if [ -f "./frontend/package.json" ]; then
|
||||||
|
add_file "./frontend/package.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# vite.config.js
|
||||||
|
if [ -f "./frontend/vite.config.js" ]; then
|
||||||
|
add_file "./frontend/vite.config.js"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# index.html
|
||||||
|
if [ -f "./frontend/index.html" ]; then
|
||||||
|
add_file "./frontend/index.html"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 统计信息
|
||||||
|
echo ""
|
||||||
|
echo "======================================"
|
||||||
|
echo "导出完成!"
|
||||||
|
echo "======================================"
|
||||||
|
echo "输出文件: $OUTPUT_FILE"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 统计代码行数
|
||||||
|
total_lines=$(wc -l < "$OUTPUT_FILE")
|
||||||
|
echo "总行数: $total_lines"
|
||||||
|
|
||||||
|
# 统计文件大小
|
||||||
|
file_size=$(du -h "$OUTPUT_FILE" | cut -f1)
|
||||||
|
echo "文件大小: $file_size"
|
||||||
|
|
||||||
|
# 统计各部分文件数量
|
||||||
|
java_count=$(grep -c "\.java ==" "$OUTPUT_FILE" || echo "0")
|
||||||
|
vue_count=$(grep -c "\.vue ==" "$OUTPUT_FILE" || echo "0")
|
||||||
|
js_count=$(grep -c "\.js ==" "$OUTPUT_FILE" || echo "0")
|
||||||
|
xml_count=$(grep -c "\.xml ==" "$OUTPUT_FILE" || echo "0")
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "文件统计:"
|
||||||
|
echo " Java文件: $java_count"
|
||||||
|
echo " Vue文件: $vue_count"
|
||||||
|
echo " JS文件: $js_count"
|
||||||
|
echo " XML文件: $xml_count"
|
||||||
|
echo ""
|
||||||
|
echo "✅ 代码导出成功!"
|
||||||
@ -45,6 +45,11 @@ export const getProducts = (params) => {
|
|||||||
return request.get('/admin/products', { params })
|
return request.get('/admin/products', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取商品详情
|
||||||
|
export const getProductDetail = (id) => {
|
||||||
|
return request.get(`/admin/products/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
// 审核商品
|
// 审核商品
|
||||||
export const auditProduct = (id, status) => {
|
export const auditProduct = (id, status) => {
|
||||||
return request.put(`/admin/products/${id}/audit`, null, { params: { status } })
|
return request.put(`/admin/products/${id}/audit`, null, { params: { status } })
|
||||||
@ -55,6 +60,11 @@ export const getOrders = (params) => {
|
|||||||
return request.get('/admin/orders', { params })
|
return request.get('/admin/orders', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取订单详情
|
||||||
|
export const getOrderDetail = (id) => {
|
||||||
|
return request.get(`/admin/orders/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
// 获取最新动态
|
// 获取最新动态
|
||||||
export const getRecentActivities = () => {
|
export const getRecentActivities = () => {
|
||||||
return request.get('/admin/activities')
|
return request.get('/admin/activities')
|
||||||
|
|||||||
20
frontend/src/api/express.js
Normal file
20
frontend/src/api/express.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import request from '../utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据快递单号查询物流信息
|
||||||
|
*/
|
||||||
|
export function queryExpress(trackingNumber, companyCode) {
|
||||||
|
return request.get('/express/query', {
|
||||||
|
params: {
|
||||||
|
trackingNumber,
|
||||||
|
companyCode
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订单ID查询物流信息
|
||||||
|
*/
|
||||||
|
export function queryExpressByOrder(orderId) {
|
||||||
|
return request.get(`/express/order/${orderId}`)
|
||||||
|
}
|
||||||
589
frontend/src/components/ExpressTracking.vue
Normal file
589
frontend/src/components/ExpressTracking.vue
Normal file
@ -0,0 +1,589 @@
|
|||||||
|
<template>
|
||||||
|
<div class="express-tracking">
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
<span>加载物流信息中...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误状态 -->
|
||||||
|
<div v-else-if="error" class="error-container">
|
||||||
|
<el-icon><WarningFilled /></el-icon>
|
||||||
|
<span>{{ error }}</span>
|
||||||
|
<el-button type="primary" size="small" @click="loadExpressInfo">重试</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 物流信息展示 -->
|
||||||
|
<div v-else-if="expressInfo" class="express-content">
|
||||||
|
<!-- 物流基本信息 -->
|
||||||
|
<div class="express-header">
|
||||||
|
<div class="company-info">
|
||||||
|
<span class="company-name">{{ expressInfo.companyName }}</span>
|
||||||
|
<el-tag :type="getStatusType(expressInfo.status)" size="small">
|
||||||
|
{{ expressInfo.status }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="tracking-number">
|
||||||
|
运单号:{{ expressInfo.trackingNumber }}
|
||||||
|
<el-button type="text" size="small" @click="copyTrackingNumber">
|
||||||
|
<el-icon><CopyDocument /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="current-location" v-if="expressInfo.currentLocation">
|
||||||
|
<el-icon><Location /></el-icon>
|
||||||
|
当前位置:{{ expressInfo.currentLocation }}
|
||||||
|
</div>
|
||||||
|
<div class="update-time" v-if="expressInfo.updateTime">
|
||||||
|
更新时间:{{ expressInfo.updateTime }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 物流地图 -->
|
||||||
|
<div class="express-map" v-if="showMap && expressInfo.mapRoute">
|
||||||
|
<div class="map-header">
|
||||||
|
<span class="map-title">
|
||||||
|
<el-icon><MapLocation /></el-icon>
|
||||||
|
物流轨迹地图
|
||||||
|
</span>
|
||||||
|
<el-button type="text" @click="toggleMap">
|
||||||
|
{{ mapExpanded ? '收起' : '展开' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="mapExpanded" class="map-container">
|
||||||
|
<div id="express-map" ref="mapContainer"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 物流轨迹时间线 -->
|
||||||
|
<div class="express-timeline">
|
||||||
|
<div class="timeline-header">
|
||||||
|
<el-icon><Clock /></el-icon>
|
||||||
|
物流轨迹
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-timeline>
|
||||||
|
<el-timeline-item
|
||||||
|
v-for="(trace, index) in expressInfo.traces"
|
||||||
|
:key="index"
|
||||||
|
:timestamp="trace.time"
|
||||||
|
placement="top"
|
||||||
|
:type="index === 0 ? 'primary' : 'info'"
|
||||||
|
:size="index === 0 ? 'large' : 'normal'"
|
||||||
|
:hollow="index !== 0"
|
||||||
|
>
|
||||||
|
<div class="timeline-content">
|
||||||
|
<div class="trace-location" v-if="trace.location">
|
||||||
|
<el-icon><Location /></el-icon>
|
||||||
|
{{ trace.location }}
|
||||||
|
</div>
|
||||||
|
<div class="trace-status">{{ trace.status }}</div>
|
||||||
|
</div>
|
||||||
|
</el-timeline-item>
|
||||||
|
</el-timeline>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import {
|
||||||
|
Loading,
|
||||||
|
WarningFilled,
|
||||||
|
CopyDocument,
|
||||||
|
Location,
|
||||||
|
MapLocation,
|
||||||
|
Clock
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import request from '../utils/request'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
orderId: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
showMap: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const error = ref(null)
|
||||||
|
const expressInfo = ref(null)
|
||||||
|
const mapExpanded = ref(true)
|
||||||
|
const map = ref(null)
|
||||||
|
const mapContainer = ref(null)
|
||||||
|
|
||||||
|
// 加载物流信息
|
||||||
|
const loadExpressInfo = async () => {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await request.get(`/express/order/${props.orderId}`)
|
||||||
|
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) {
|
||||||
|
console.error('❌ 加载物流信息失败:', err)
|
||||||
|
error.value = err.response?.data?.message || '加载物流信息失败'
|
||||||
|
} finally {
|
||||||
|
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 () => {
|
||||||
|
mapExpanded.value = !mapExpanded.value
|
||||||
|
|
||||||
|
if (mapExpanded.value && !map.value) {
|
||||||
|
await nextTick()
|
||||||
|
initMap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制运单号
|
||||||
|
const copyTrackingNumber = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(expressInfo.value.trackingNumber)
|
||||||
|
ElMessage.success('运单号已复制')
|
||||||
|
} catch (err) {
|
||||||
|
ElMessage.error('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态类型
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const typeMap = {
|
||||||
|
'已签收': 'success',
|
||||||
|
'派件中': 'primary',
|
||||||
|
'已揽收': 'info',
|
||||||
|
'在途中': 'warning',
|
||||||
|
'疑难': 'danger',
|
||||||
|
'退签': 'danger',
|
||||||
|
'退回': 'danger'
|
||||||
|
}
|
||||||
|
return typeMap[status] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听orderId变化
|
||||||
|
watch(() => props.orderId, () => {
|
||||||
|
if (props.orderId) {
|
||||||
|
loadExpressInfo()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.orderId) {
|
||||||
|
loadExpressInfo()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (map.value) {
|
||||||
|
map.value.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.express-tracking {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container,
|
||||||
|
.error-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
gap: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.express-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.express-header {
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #F1F8E9 0%, #E8F5E8 100%);
|
||||||
|
border-bottom: 2px solid #C8E6C9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2E7D32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracking-number {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #4CAF50;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-location,
|
||||||
|
.update-time {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #81C784;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.express-map {
|
||||||
|
border-bottom: 1px solid #E8F5E8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #f8fff8;
|
||||||
|
border-bottom: 1px solid #E8F5E8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2E7D32;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-container {
|
||||||
|
height: 400px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#express-map {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.express-timeline {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2E7D32;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 2px solid #C8E6C9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-location {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: #4CAF50;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-status {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-timeline {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-timeline-item__timestamp) {
|
||||||
|
color: #81C784;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-timeline-item__node--large) {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.map-container {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.express-header {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-name {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -336,7 +336,7 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 搜索地址 - 调用后端API
|
// 搜索地址 - 调用后端API
|
||||||
const searchAddress = async () => {
|
const searchAddress = async () => {
|
||||||
if (!searchKeyword.value.trim()) {
|
if (!searchKeyword.value.trim()) {
|
||||||
searchResults.value = [];
|
searchResults.value = [];
|
||||||
@ -430,7 +430,7 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 保存地址
|
// 保存地址
|
||||||
const saveAddress = () => {
|
const saveAddress = async () => {
|
||||||
if (!selectedAddress.value) {
|
if (!selectedAddress.value) {
|
||||||
ElMessage.error('请在地图上选择地址');
|
ElMessage.error('请在地图上选择地址');
|
||||||
return;
|
return;
|
||||||
@ -456,21 +456,63 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const addressData = {
|
saving.value = true;
|
||||||
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);
|
try {
|
||||||
|
// 检查省市区信息是否完整
|
||||||
|
let province = selectedAddress.value.province;
|
||||||
|
let city = selectedAddress.value.city;
|
||||||
|
let district = selectedAddress.value.district;
|
||||||
|
|
||||||
|
// 如果省市区信息不完整,调用后端逆地理编码接口
|
||||||
|
if (!province || !city || !district) {
|
||||||
|
console.log('📍 省市区信息不完整,调用后端逆地理编码接口...');
|
||||||
|
|
||||||
|
const result = await addressAPI.reverseGeocode(
|
||||||
|
selectedAddress.value.lng,
|
||||||
|
selectedAddress.value.lat
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result && result.province && result.city && result.district) {
|
||||||
|
province = result.province;
|
||||||
|
city = result.city;
|
||||||
|
district = result.district;
|
||||||
|
|
||||||
|
// 更新 selectedAddress
|
||||||
|
selectedAddress.value.province = province;
|
||||||
|
selectedAddress.value.city = city;
|
||||||
|
selectedAddress.value.district = district;
|
||||||
|
|
||||||
|
console.log('✅ 逆地理编码成功:', { province, city, district });
|
||||||
|
} else {
|
||||||
|
saving.value = false;
|
||||||
|
ElMessage.error('无法获取地址信息,请重新选择位置');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addressData = {
|
||||||
|
consignee: consigneeName.value,
|
||||||
|
phone: phoneNumber.value,
|
||||||
|
province: province,
|
||||||
|
city: city,
|
||||||
|
district: district,
|
||||||
|
address: detailAddress.value,
|
||||||
|
isDefault: useDefaultAddress.value,
|
||||||
|
tag: selectedTag.value,
|
||||||
|
lng: selectedAddress.value.lng,
|
||||||
|
lat: selectedAddress.value.lat,
|
||||||
|
formattedAddress: selectedAddress.value.formattedAddress
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('💾 准备保存地址:', addressData);
|
||||||
|
|
||||||
|
emit('save', addressData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 保存地址失败:', error);
|
||||||
|
ElMessage.error('保存失败: ' + (error.message || '未知错误'));
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化表单数据
|
// 初始化表单数据
|
||||||
|
|||||||
@ -2,12 +2,64 @@
|
|||||||
<div class="merchant-management">
|
<div class="merchant-management">
|
||||||
<!-- 页面标题 -->
|
<!-- 页面标题 -->
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>商家管理</h2>
|
<div>
|
||||||
<p>管理平台商家入驻申请和审核</p>
|
<h2>商家管理</h2>
|
||||||
|
<p>管理平台商家入驻申请和审核</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<div class="stats-container">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card pending">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">待审核</div>
|
||||||
|
<div class="stat-value">{{ stats.pending }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><Warning /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card approved">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">已通过</div>
|
||||||
|
<div class="stat-value">{{ stats.approved }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><SuccessFilled /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card rejected">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">已拒绝</div>
|
||||||
|
<div class="stat-value">{{ stats.rejected }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><CircleCloseFilled /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card total">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">商家总数</div>
|
||||||
|
<div class="stat-value">{{ stats.total }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><Shop /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索栏 -->
|
<!-- 搜索栏 -->
|
||||||
<div class="search-bar">
|
<el-card class="search-card">
|
||||||
<el-form :inline="true" class="search-form">
|
<el-form :inline="true" class="search-form">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-input
|
<el-input
|
||||||
@ -16,10 +68,11 @@
|
|||||||
prefix-icon="Search"
|
prefix-icon="Search"
|
||||||
clearable
|
clearable
|
||||||
@clear="handleSearch"
|
@clear="handleSearch"
|
||||||
|
style="width: 260px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-select v-model="searchForm.status" placeholder="审核状态" clearable>
|
<el-select v-model="searchForm.status" placeholder="审核状态" clearable style="width: 150px">
|
||||||
<el-option label="全部" value="" />
|
<el-option label="全部" value="" />
|
||||||
<el-option label="待审核" :value="0" />
|
<el-option label="待审核" :value="0" />
|
||||||
<el-option label="已通过" :value="1" />
|
<el-option label="已通过" :value="1" />
|
||||||
@ -31,49 +84,68 @@
|
|||||||
<el-button @click="resetSearch" icon="Refresh">重置</el-button>
|
<el-button @click="resetSearch" icon="Refresh">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
<!-- 商家表格 -->
|
<!-- 商家表格 -->
|
||||||
<div class="table-container">
|
<el-card class="table-card">
|
||||||
<el-table :data="merchantList" v-loading="loading" stripe>
|
<el-table :data="merchantList" v-loading="loading" stripe>
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||||
<el-table-column prop="shopName" label="店铺名称" width="150" />
|
|
||||||
<el-table-column prop="legalPerson" label="法人" width="120" />
|
<el-table-column prop="shopName" label="店铺名称" min-width="180" show-overflow-tooltip>
|
||||||
<el-table-column prop="contactPhone" label="联系电话" width="130" />
|
|
||||||
<el-table-column prop="businessScope" label="经营范围" width="200" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="status" label="状态" width="100">
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="getStatusType(scope.row.status)">
|
<div class="shop-name">
|
||||||
|
<span class="name-text">{{ scope.row.shopName }}</span>
|
||||||
|
<el-tag v-if="scope.row.status === 0" type="warning" size="small" class="status-badge">待审核</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="legalPerson" label="法人" width="120" align="center" />
|
||||||
|
|
||||||
|
<el-table-column prop="contactPhone" label="联系电话" width="140" align="center" />
|
||||||
|
|
||||||
|
<el-table-column prop="businessScope" label="经营范围" min-width="200" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="getStatusType(scope.row.status)" effect="dark">
|
||||||
{{ getStatusText(scope.row.status) }}
|
{{ getStatusText(scope.row.status) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createdAt" label="申请时间" width="160" />
|
|
||||||
<el-table-column prop="verifyTime" label="审核时间" width="160" />
|
<el-table-column prop="createTime" label="申请时间" width="180" align="center" />
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
|
||||||
|
<el-table-column label="操作" width="300" fixed="right" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<div class="glass-action-buttons">
|
||||||
v-if="scope.row.status === 0"
|
<!-- 待审核状态显示审核按钮 -->
|
||||||
type="success"
|
<template v-if="scope.row.status === 0">
|
||||||
size="small"
|
<button
|
||||||
@click="handleAudit(scope.row, 1)"
|
class="glass-btn glass-btn-success"
|
||||||
>
|
@click="handleAudit(scope.row, 1)"
|
||||||
通过
|
>
|
||||||
</el-button>
|
<el-icon class="btn-icon"><Check /></el-icon>
|
||||||
<el-button
|
<span>通过</span>
|
||||||
v-if="scope.row.status === 0"
|
</button>
|
||||||
type="danger"
|
<button
|
||||||
size="small"
|
class="glass-btn glass-btn-danger"
|
||||||
@click="handleAudit(scope.row, 2)"
|
@click="handleAudit(scope.row, 2)"
|
||||||
>
|
>
|
||||||
拒绝
|
<el-icon class="btn-icon"><Close /></el-icon>
|
||||||
</el-button>
|
<span>拒绝</span>
|
||||||
<el-button
|
</button>
|
||||||
size="small"
|
</template>
|
||||||
@click="handleViewDetail(scope.row)"
|
|
||||||
>
|
<!-- 查看详情按钮 -->
|
||||||
查看详情
|
<button
|
||||||
</el-button>
|
class="glass-btn glass-btn-primary"
|
||||||
|
@click="handleViewDetail(scope.row)"
|
||||||
|
>
|
||||||
|
<el-icon class="btn-icon"><View /></el-icon>
|
||||||
|
<span>详情</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -90,7 +162,7 @@
|
|||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
<!-- 商家详情对话框 -->
|
<!-- 商家详情对话框 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
@ -99,90 +171,85 @@
|
|||||||
width="800px"
|
width="800px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
>
|
>
|
||||||
<div class="merchant-detail" v-if="selectedMerchant">
|
<div class="merchant-detail" v-if="selectedMerchant" v-loading="detailLoading">
|
||||||
|
<!-- 基本信息 -->
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h3>基本信息</h3>
|
<div class="section-title">
|
||||||
<div class="info-grid">
|
<el-icon><InfoFilled /></el-icon>
|
||||||
<div class="info-item">
|
<span>基本信息</span>
|
||||||
<label>商家ID:</label>
|
|
||||||
<span>{{ selectedMerchant.id }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<label>店铺名称:</label>
|
|
||||||
<span>{{ selectedMerchant.shopName }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<label>法人姓名:</label>
|
|
||||||
<span>{{ selectedMerchant.legalPerson }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<label>联系电话:</label>
|
|
||||||
<span>{{ selectedMerchant.contactPhone }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<label>经营地址:</label>
|
|
||||||
<span>{{ selectedMerchant.address }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<label>营业执照:</label>
|
|
||||||
<span>{{ selectedMerchant.businessLicense || '未上传' }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="商家ID">{{ selectedMerchant.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺名称">{{ selectedMerchant.shopName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="法人姓名">{{ selectedMerchant.legalPerson }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系电话">{{ selectedMerchant.contactPhone }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="经营地址" :span="2">
|
||||||
|
{{ selectedMerchant.address || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="营业执照" :span="2">
|
||||||
|
{{ selectedMerchant.businessLicense || '未上传' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 经营信息 -->
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h3>经营信息</h3>
|
<div class="section-title">
|
||||||
<div class="info-grid">
|
<el-icon><Shop /></el-icon>
|
||||||
<div class="info-item full-width">
|
<span>经营信息</span>
|
||||||
<label>经营范围:</label>
|
|
||||||
<span>{{ selectedMerchant.businessScope }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="经营范围">
|
||||||
|
<div class="business-scope">{{ selectedMerchant.businessScope || '-' }}</div>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 审核信息 -->
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<h3>审核信息</h3>
|
<div class="section-title">
|
||||||
<div class="info-grid">
|
<el-icon><DocumentChecked /></el-icon>
|
||||||
<div class="info-item">
|
<span>审核信息</span>
|
||||||
<label>审核状态:</label>
|
</div>
|
||||||
<el-tag :type="getStatusType(selectedMerchant.status)">
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="审核状态">
|
||||||
|
<el-tag :type="getStatusType(selectedMerchant.status)" effect="dark">
|
||||||
{{ getStatusText(selectedMerchant.status) }}
|
{{ getStatusText(selectedMerchant.status) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</div>
|
</el-descriptions-item>
|
||||||
<div class="info-item">
|
<el-descriptions-item label="申请时间">
|
||||||
<label>申请时间:</label>
|
{{ selectedMerchant.createTime }}
|
||||||
<span>{{ selectedMerchant.createdAt }}</span>
|
</el-descriptions-item>
|
||||||
</div>
|
<el-descriptions-item label="审核时间" :span="2">
|
||||||
<div class="info-item">
|
{{ selectedMerchant.verifyTime || '未审核' }}
|
||||||
<label>审核时间:</label>
|
</el-descriptions-item>
|
||||||
<span>{{ selectedMerchant.verifyTime || '未审核' }}</span>
|
</el-descriptions>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 审核操作区 -->
|
|
||||||
<div class="detail-section" v-if="selectedMerchant.status === 0">
|
|
||||||
<h3>审核操作</h3>
|
|
||||||
<div class="audit-actions">
|
|
||||||
<el-button
|
|
||||||
type="success"
|
|
||||||
@click="handleDetailAudit(selectedMerchant, 1)"
|
|
||||||
>
|
|
||||||
通过审核
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
|
||||||
type="danger"
|
|
||||||
@click="handleDetailAudit(selectedMerchant, 2)"
|
|
||||||
>
|
|
||||||
拒绝审核
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<el-button @click="detailDialogVisible = false">关闭</el-button>
|
<el-button @click="detailDialogVisible = false" size="large">关闭</el-button>
|
||||||
|
<template v-if="selectedMerchant && selectedMerchant.status === 0">
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
@click="handleAuditInDialog(1)"
|
||||||
|
size="large"
|
||||||
|
class="audit-btn"
|
||||||
|
>
|
||||||
|
<el-icon class="btn-icon"><Check /></el-icon>
|
||||||
|
<span>审核通过</span>
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
@click="handleAuditInDialog(2)"
|
||||||
|
size="large"
|
||||||
|
class="audit-btn"
|
||||||
|
>
|
||||||
|
<el-icon class="btn-icon"><Close /></el-icon>
|
||||||
|
<span>审核拒绝</span>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@ -192,12 +259,21 @@
|
|||||||
<script>
|
<script>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
Warning, SuccessFilled, CircleCloseFilled, Shop, Search, Refresh,
|
||||||
|
Check, Close, View, InfoFilled, DocumentChecked
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
import { getMerchants, auditMerchant } from '../../../api/admin'
|
import { getMerchants, auditMerchant } from '../../../api/admin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MerchantManagement',
|
name: 'MerchantManagement',
|
||||||
|
components: {
|
||||||
|
Warning, SuccessFilled, CircleCloseFilled, Shop, Search, Refresh,
|
||||||
|
Check, Close, View, InfoFilled, DocumentChecked
|
||||||
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const detailLoading = ref(false)
|
||||||
const merchantList = ref([])
|
const merchantList = ref([])
|
||||||
const detailDialogVisible = ref(false)
|
const detailDialogVisible = ref(false)
|
||||||
const selectedMerchant = ref(null)
|
const selectedMerchant = ref(null)
|
||||||
@ -213,6 +289,22 @@ export default {
|
|||||||
total: 0
|
total: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = reactive({
|
||||||
|
pending: 0,
|
||||||
|
approved: 0,
|
||||||
|
rejected: 0,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
const calculateStats = (merchants) => {
|
||||||
|
stats.pending = merchants.filter(m => m.status === 0).length
|
||||||
|
stats.approved = merchants.filter(m => m.status === 1).length
|
||||||
|
stats.rejected = merchants.filter(m => m.status === 2).length
|
||||||
|
stats.total = merchants.length
|
||||||
|
}
|
||||||
|
|
||||||
const loadMerchants = async () => {
|
const loadMerchants = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@ -226,7 +318,11 @@ export default {
|
|||||||
const result = await getMerchants(params)
|
const result = await getMerchants(params)
|
||||||
merchantList.value = result.records
|
merchantList.value = result.records
|
||||||
pagination.total = result.total
|
pagination.total = result.total
|
||||||
|
|
||||||
|
// 更新统计
|
||||||
|
calculateStats(result.records)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('加载商家列表失败:', error)
|
||||||
ElMessage.error('加载商家列表失败')
|
ElMessage.error('加载商家列表失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@ -272,27 +368,26 @@ export default {
|
|||||||
await auditMerchant(merchant.id, status)
|
await auditMerchant(merchant.id, status)
|
||||||
ElMessage.success(`商家审核${action}成功`)
|
ElMessage.success(`商家审核${action}成功`)
|
||||||
loadMerchants()
|
loadMerchants()
|
||||||
|
|
||||||
// 如果详情对话框打开着,也关闭它
|
|
||||||
if (detailDialogVisible.value) {
|
|
||||||
detailDialogVisible.value = false
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
|
console.error('审核失败:', error)
|
||||||
ElMessage.error(`商家审核${action}失败`)
|
ElMessage.error(`商家审核${action}失败`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAuditInDialog = async (status) => {
|
||||||
|
if (!selectedMerchant.value) return
|
||||||
|
|
||||||
|
await handleAudit(selectedMerchant.value, status)
|
||||||
|
detailDialogVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
const handleViewDetail = (merchant) => {
|
const handleViewDetail = (merchant) => {
|
||||||
selectedMerchant.value = merchant
|
selectedMerchant.value = merchant
|
||||||
detailDialogVisible.value = true
|
detailDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDetailAudit = async (merchant, status) => {
|
|
||||||
await handleAudit(merchant, status)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusType = (status) => {
|
const getStatusType = (status) => {
|
||||||
const types = { 0: 'warning', 1: 'success', 2: 'danger' }
|
const types = { 0: 'warning', 1: 'success', 2: 'danger' }
|
||||||
return types[status] || ''
|
return types[status] || ''
|
||||||
@ -309,9 +404,11 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
|
detailLoading,
|
||||||
merchantList,
|
merchantList,
|
||||||
searchForm,
|
searchForm,
|
||||||
pagination,
|
pagination,
|
||||||
|
stats,
|
||||||
detailDialogVisible,
|
detailDialogVisible,
|
||||||
selectedMerchant,
|
selectedMerchant,
|
||||||
handleSearch,
|
handleSearch,
|
||||||
@ -319,8 +416,8 @@ export default {
|
|||||||
handleSizeChange,
|
handleSizeChange,
|
||||||
handleCurrentChange,
|
handleCurrentChange,
|
||||||
handleAudit,
|
handleAudit,
|
||||||
|
handleAuditInDialog,
|
||||||
handleViewDetail,
|
handleViewDetail,
|
||||||
handleDetailAudit,
|
|
||||||
getStatusType,
|
getStatusType,
|
||||||
getStatusText
|
getStatusText
|
||||||
}
|
}
|
||||||
@ -331,114 +428,343 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.merchant-management {
|
.merchant-management {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
min-height: calc(100vh - 60px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header h2 {
|
.page-header h2 {
|
||||||
margin: 0 0 5px 0;
|
margin: 0 0 5px 0;
|
||||||
color: #333;
|
color: #1f2937;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header p {
|
.page-header p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #666;
|
color: #6b7280;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
/* 统计卡片样式 */
|
||||||
background: white;
|
.stats-container {
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.pending .stat-value {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.pending .stat-icon {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.approved .stat-value {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.approved .stat-icon {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.rejected .stat-value {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.rejected .stat-icon {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.total .stat-value {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.total .stat-icon {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索卡片 */
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: none;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-form {
|
.search-form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格卡片 */
|
||||||
|
.table-card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 店铺名称 */
|
||||||
|
.shop-name {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-text {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 玻璃质感按钮样式 ==================== */
|
||||||
|
.glass-action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.glass-btn {
|
||||||
background: white;
|
position: relative;
|
||||||
padding: 20px;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.glass-btn::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.3),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
transition: left 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn .btn-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn:hover .btn-icon {
|
||||||
|
transform: scale(1.2) rotate(5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通过按钮 - 绿色玻璃 */
|
||||||
|
.glass-btn-success {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(16, 185, 129, 0.8) 0%,
|
||||||
|
rgba(5, 150, 105, 0.8) 100%
|
||||||
|
);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn-success:hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(16, 185, 129, 0.9) 0%,
|
||||||
|
rgba(5, 150, 105, 0.9) 100%
|
||||||
|
);
|
||||||
|
box-shadow: 0 8px 16px -4px rgba(16, 185, 129, 0.4),
|
||||||
|
0 4px 6px -2px rgba(16, 185, 129, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拒绝按钮 - 红色玻璃 */
|
||||||
|
.glass-btn-danger {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(239, 68, 68, 0.8) 0%,
|
||||||
|
rgba(220, 38, 38, 0.8) 100%
|
||||||
|
);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn-danger:hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(239, 68, 68, 0.9) 0%,
|
||||||
|
rgba(220, 38, 38, 0.9) 100%
|
||||||
|
);
|
||||||
|
box-shadow: 0 8px 16px -4px rgba(239, 68, 68, 0.4),
|
||||||
|
0 4px 6px -2px rgba(239, 68, 68, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 详情按钮 - 蓝色玻璃 */
|
||||||
|
.glass-btn-primary {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(59, 130, 246, 0.15) 0%,
|
||||||
|
rgba(37, 99, 235, 0.15) 100%
|
||||||
|
);
|
||||||
|
color: #3b82f6;
|
||||||
|
border: 1.5px solid rgba(59, 130, 246, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn-primary:hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(59, 130, 246, 0.25) 0%,
|
||||||
|
rgba(37, 99, 235, 0.25) 100%
|
||||||
|
);
|
||||||
|
border-color: rgba(59, 130, 246, 0.8);
|
||||||
|
box-shadow: 0 8px 16px -4px rgba(59, 130, 246, 0.3),
|
||||||
|
0 4px 6px -2px rgba(59, 130, 246, 0.15),
|
||||||
|
inset 0 1px 0 0 rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页 */
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 详情对话框样式 */
|
/* 商家详情样式 */
|
||||||
.merchant-detail {
|
.merchant-detail {
|
||||||
max-height: 600px;
|
max-height: 70vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-section {
|
.detail-section {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-section h3 {
|
.section-title {
|
||||||
margin: 0 0 15px 0;
|
display: flex;
|
||||||
color: #333;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border-bottom: 2px solid #409EFF;
|
color: #1f2937;
|
||||||
padding-bottom: 5px;
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 2px solid #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-grid {
|
.section-title .el-icon {
|
||||||
display: grid;
|
color: #3b82f6;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item {
|
/* 经营范围样式 */
|
||||||
display: flex;
|
.business-scope {
|
||||||
align-items: flex-start;
|
line-height: 1.6;
|
||||||
}
|
color: #4b5563;
|
||||||
|
white-space: pre-wrap;
|
||||||
.info-item.full-width {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item label {
|
|
||||||
min-width: 100px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #666;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item span {
|
|
||||||
color: #333;
|
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audit-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
.dialog-footer {
|
||||||
display: flex;
|
text-align: right;
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 对话框底部审核按钮样式 */
|
||||||
|
.dialog-footer .audit-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer .audit-btn .btn-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.info-grid {
|
.stats-container :deep(.el-col) {
|
||||||
grid-template-columns: 1fr;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item.full-width {
|
.glass-action-buttons {
|
||||||
grid-column: 1;
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn {
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -2,12 +2,64 @@
|
|||||||
<div class="order-management">
|
<div class="order-management">
|
||||||
<!-- 页面标题 -->
|
<!-- 页面标题 -->
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>订单管理</h2>
|
<div>
|
||||||
<p>管理平台所有订单信息</p>
|
<h2>订单管理</h2>
|
||||||
|
<p>管理平台所有订单信息</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<div class="stats-container">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card pending">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">待支付</div>
|
||||||
|
<div class="stat-value">{{ stats.pending }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><Clock /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card paid">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">已支付</div>
|
||||||
|
<div class="stat-value">{{ stats.paid }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><Wallet /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card shipped">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">已发货</div>
|
||||||
|
<div class="stat-value">{{ stats.shipped }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><Van /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card total">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">订单总数</div>
|
||||||
|
<div class="stat-value">{{ stats.total }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><Document /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索栏 -->
|
<!-- 搜索栏 -->
|
||||||
<div class="search-bar">
|
<el-card class="search-card">
|
||||||
<el-form :inline="true" class="search-form">
|
<el-form :inline="true" class="search-form">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-input
|
<el-input
|
||||||
@ -16,10 +68,11 @@
|
|||||||
prefix-icon="Search"
|
prefix-icon="Search"
|
||||||
clearable
|
clearable
|
||||||
@clear="handleSearch"
|
@clear="handleSearch"
|
||||||
|
style="width: 260px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-select v-model="searchForm.status" placeholder="订单状态" clearable>
|
<el-select v-model="searchForm.status" placeholder="订单状态" clearable style="width: 150px">
|
||||||
<el-option label="全部" value="" />
|
<el-option label="全部" value="" />
|
||||||
<el-option label="待支付" :value="1" />
|
<el-option label="待支付" :value="1" />
|
||||||
<el-option label="已支付" :value="2" />
|
<el-option label="已支付" :value="2" />
|
||||||
@ -33,41 +86,60 @@
|
|||||||
<el-button @click="resetSearch" icon="Refresh">重置</el-button>
|
<el-button @click="resetSearch" icon="Refresh">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
<!-- 订单表格 -->
|
<!-- 订单表格 -->
|
||||||
<div class="table-container">
|
<el-card class="table-card">
|
||||||
<el-table :data="orderList" v-loading="loading" stripe>
|
<el-table :data="orderList" v-loading="loading" stripe>
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||||
<el-table-column prop="orderNo" label="订单号" width="160" />
|
|
||||||
<el-table-column prop="consignee" label="收件人" width="100" />
|
<el-table-column prop="orderNo" label="订单号" width="180" align="center">
|
||||||
<el-table-column prop="phone" label="电话" width="130" />
|
|
||||||
<el-table-column prop="totalAmount" label="订单金额" width="100">
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
¥{{ scope.row.totalAmount }}
|
<span class="order-no">{{ scope.row.orderNo }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="actualAmount" label="实付金额" width="100">
|
|
||||||
|
<el-table-column prop="consignee" label="收件人" width="120" align="center" />
|
||||||
|
|
||||||
|
<el-table-column prop="phone" label="电话" width="130" align="center" />
|
||||||
|
|
||||||
|
<el-table-column prop="totalAmount" label="订单金额" width="120" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
¥{{ scope.row.actualAmount }}
|
<span class="amount-text">¥{{ scope.row.totalAmount }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="status" label="状态" width="100">
|
|
||||||
|
<el-table-column prop="actualAmount" label="实付金额" width="120" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="getStatusType(scope.row.status)">
|
<span class="price">¥{{ scope.row.actualAmount }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="getStatusType(scope.row.status)" effect="dark">
|
||||||
{{ getStatusText(scope.row.status) }}
|
{{ getStatusText(scope.row.status) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createdAt" label="创建时间" width="160" />
|
|
||||||
<el-table-column label="操作" width="120" fixed="right">
|
<el-table-column prop="createTime" label="创建时间" width="180" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
{{ formatTime(scope.row.createTime) }}
|
||||||
size="small"
|
</template>
|
||||||
@click="handleViewDetail(scope.row)"
|
</el-table-column>
|
||||||
>
|
|
||||||
查看详情
|
<el-table-column label="操作" width="150" fixed="right" align="center">
|
||||||
</el-button>
|
<template #default="scope">
|
||||||
|
<div class="glass-action-buttons">
|
||||||
|
<button
|
||||||
|
class="glass-btn glass-btn-primary"
|
||||||
|
@click="handleViewDetail(scope.row)"
|
||||||
|
>
|
||||||
|
<el-icon class="btn-icon"><View /></el-icon>
|
||||||
|
<span>详情</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -84,20 +156,182 @@
|
|||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 订单详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialogVisible"
|
||||||
|
title="订单详情"
|
||||||
|
width="900px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<div v-if="currentOrder" class="order-detail" v-loading="detailLoading">
|
||||||
|
<!-- 订单基本信息 -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>订单信息</span>
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="订单号">
|
||||||
|
<span class="order-no">{{ currentOrder.orderNo }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="订单状态">
|
||||||
|
<el-tag :type="getStatusType(currentOrder.status)" effect="dark">
|
||||||
|
{{ getStatusText(currentOrder.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="下单时间">
|
||||||
|
{{ formatTime(currentOrder.createTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="支付时间">
|
||||||
|
{{ formatTime(currentOrder.payTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 收货信息 -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<el-icon><Location /></el-icon>
|
||||||
|
<span>收货信息</span>
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="收件人">{{ currentOrder.consignee }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系电话">{{ currentOrder.phone }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="收货地址" :span="2">
|
||||||
|
{{ currentOrder.address }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 订单商品 -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<el-icon><ShoppingCart /></el-icon>
|
||||||
|
<span>订单商品</span>
|
||||||
|
</div>
|
||||||
|
<el-table :data="currentOrder.orderDetails" border>
|
||||||
|
<el-table-column label="商品" min-width="200">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="product-info">
|
||||||
|
<el-image
|
||||||
|
v-if="scope.row.productImage"
|
||||||
|
:src="scope.row.productImage"
|
||||||
|
fit="cover"
|
||||||
|
style="width: 60px; height: 60px; border-radius: 6px;"
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<div class="image-slot">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
<span class="product-name">{{ scope.row.productName }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="price" label="单价" width="120" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<span class="price">¥{{ scope.row.price }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="quantity" label="数量" width="100" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag type="info">x{{ scope.row.quantity }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="totalAmount" label="小计" width="120" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<span class="price">¥{{ scope.row.totalAmount }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 金额信息 -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<el-icon><Wallet /></el-icon>
|
||||||
|
<span>金额信息</span>
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="商品总额">
|
||||||
|
<span class="amount-text">¥{{ currentOrder.totalAmount }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="优惠金额">
|
||||||
|
<span class="discount-text">-¥{{ currentOrder.discountAmount || 0 }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="实付金额" :span="2">
|
||||||
|
<span class="price-large">¥{{ currentOrder.actualAmount }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 物流信息 -->
|
||||||
|
<div class="detail-section" v-if="currentOrder.status >= 3">
|
||||||
|
<div class="section-title">
|
||||||
|
<el-icon><Van /></el-icon>
|
||||||
|
<span>物流信息</span>
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="物流公司">
|
||||||
|
{{ currentOrder.shipCompany || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="运单号">
|
||||||
|
<span class="tracking-no">{{ currentOrder.shipNo || '-' }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="发货时间">
|
||||||
|
{{ formatTime(currentOrder.shipTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="收货时间">
|
||||||
|
{{ formatTime(currentOrder.receiveTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 备注信息 -->
|
||||||
|
<div class="detail-section" v-if="currentOrder.remark">
|
||||||
|
<div class="section-title">
|
||||||
|
<el-icon><ChatDotRound /></el-icon>
|
||||||
|
<span>备注信息</span>
|
||||||
|
</div>
|
||||||
|
<div class="remark-content">
|
||||||
|
{{ currentOrder.remark }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="detailDialogVisible = false" size="large">关闭</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { getOrders } from '../../../api/admin'
|
import {
|
||||||
|
Clock, Wallet, Van, Document, Search, Refresh, View,
|
||||||
|
InfoFilled, Location, ShoppingCart, Picture, ChatDotRound
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import { getOrders, getOrderDetail } from '../../../api/admin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'OrderManagement',
|
name: 'OrderManagement',
|
||||||
|
components: {
|
||||||
|
Clock, Wallet, Van, Document, Search, Refresh, View,
|
||||||
|
InfoFilled, Location, ShoppingCart, Picture, ChatDotRound
|
||||||
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const detailLoading = ref(false)
|
||||||
const orderList = ref([])
|
const orderList = ref([])
|
||||||
|
const detailDialogVisible = ref(false)
|
||||||
|
const currentOrder = ref(null)
|
||||||
|
|
||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
@ -110,6 +344,22 @@ export default {
|
|||||||
total: 0
|
total: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = reactive({
|
||||||
|
pending: 0,
|
||||||
|
paid: 0,
|
||||||
|
shipped: 0,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
const calculateStats = (orders) => {
|
||||||
|
stats.pending = orders.filter(o => o.status === 1).length
|
||||||
|
stats.paid = orders.filter(o => o.status === 2).length
|
||||||
|
stats.shipped = orders.filter(o => o.status === 3).length
|
||||||
|
stats.total = orders.length
|
||||||
|
}
|
||||||
|
|
||||||
const loadOrders = async () => {
|
const loadOrders = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@ -123,7 +373,11 @@ export default {
|
|||||||
const result = await getOrders(params)
|
const result = await getOrders(params)
|
||||||
orderList.value = result.records
|
orderList.value = result.records
|
||||||
pagination.total = result.total
|
pagination.total = result.total
|
||||||
|
|
||||||
|
// 更新统计
|
||||||
|
calculateStats(result.records)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('加载订单列表失败:', error)
|
||||||
ElMessage.error('加载订单列表失败')
|
ElMessage.error('加载订单列表失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@ -152,9 +406,20 @@ export default {
|
|||||||
loadOrders()
|
loadOrders()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleViewDetail = (order) => {
|
const handleViewDetail = async (order) => {
|
||||||
// 查看订单详情
|
detailDialogVisible.value = true
|
||||||
console.log('查看订单详情:', order)
|
detailLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const detail = await getOrderDetail(order.id)
|
||||||
|
currentOrder.value = detail
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取订单详情失败:', error)
|
||||||
|
ElMessage.error('获取订单详情失败')
|
||||||
|
detailDialogVisible.value = false
|
||||||
|
} finally {
|
||||||
|
detailLoading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusType = (status) => {
|
const getStatusType = (status) => {
|
||||||
@ -163,7 +428,8 @@ export default {
|
|||||||
2: 'primary',
|
2: 'primary',
|
||||||
3: 'success',
|
3: 'success',
|
||||||
4: 'success',
|
4: 'success',
|
||||||
5: 'danger'
|
5: 'danger',
|
||||||
|
6: 'info'
|
||||||
}
|
}
|
||||||
return types[status] || ''
|
return types[status] || ''
|
||||||
}
|
}
|
||||||
@ -174,27 +440,38 @@ export default {
|
|||||||
2: '已支付',
|
2: '已支付',
|
||||||
3: '已发货',
|
3: '已发货',
|
||||||
4: '已完成',
|
4: '已完成',
|
||||||
5: '已取消'
|
5: '已取消',
|
||||||
|
6: '已退款'
|
||||||
}
|
}
|
||||||
return texts[status] || '未知'
|
return texts[status] || '未知'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatTime = (time) => {
|
||||||
|
if (!time) return '-'
|
||||||
|
return new Date(time).toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadOrders()
|
loadOrders()
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
|
detailLoading,
|
||||||
orderList,
|
orderList,
|
||||||
searchForm,
|
searchForm,
|
||||||
pagination,
|
pagination,
|
||||||
|
stats,
|
||||||
|
detailDialogVisible,
|
||||||
|
currentOrder,
|
||||||
handleSearch,
|
handleSearch,
|
||||||
resetSearch,
|
resetSearch,
|
||||||
handleSizeChange,
|
handleSizeChange,
|
||||||
handleCurrentChange,
|
handleCurrentChange,
|
||||||
handleViewDetail,
|
handleViewDetail,
|
||||||
getStatusType,
|
getStatusType,
|
||||||
getStatusText
|
getStatusText,
|
||||||
|
formatTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,47 +480,333 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.order-management {
|
.order-management {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
min-height: calc(100vh - 60px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header h2 {
|
.page-header h2 {
|
||||||
margin: 0 0 5px 0;
|
margin: 0 0 5px 0;
|
||||||
color: #333;
|
color: #1f2937;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header p {
|
.page-header p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #666;
|
color: #6b7280;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
/* 统计卡片样式 */
|
||||||
background: white;
|
.stats-container {
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.pending .stat-value {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.pending .stat-icon {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.paid .stat-value {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.paid .stat-icon {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.shipped .stat-value {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.shipped .stat-icon {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.total .stat-value {
|
||||||
|
color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.total .stat-icon {
|
||||||
|
color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索卡片 */
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: none;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-form {
|
.search-form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格卡片 */
|
||||||
|
.table-card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 订单号样式 */
|
||||||
|
.order-no {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #3b82f6;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 金额样式 */
|
||||||
|
.amount-text {
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-large {
|
||||||
|
color: #ef4444;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount-text {
|
||||||
|
color: #10b981;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 玻璃质感按钮样式 ==================== */
|
||||||
|
.glass-action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.glass-btn {
|
||||||
background: white;
|
position: relative;
|
||||||
padding: 20px;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.glass-btn::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.3),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
transition: left 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn .btn-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn:hover .btn-icon {
|
||||||
|
transform: scale(1.2) rotate(5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 详情按钮 - 蓝色玻璃 */
|
||||||
|
.glass-btn-primary {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(59, 130, 246, 0.15) 0%,
|
||||||
|
rgba(37, 99, 235, 0.15) 100%
|
||||||
|
);
|
||||||
|
color: #3b82f6;
|
||||||
|
border: 1.5px solid rgba(59, 130, 246, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn-primary:hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(59, 130, 246, 0.25) 0%,
|
||||||
|
rgba(37, 99, 235, 0.25) 100%
|
||||||
|
);
|
||||||
|
border-color: rgba(59, 130, 246, 0.8);
|
||||||
|
box-shadow: 0 8px 16px -4px rgba(59, 130, 246, 0.3),
|
||||||
|
0 4px 6px -2px rgba(59, 130, 246, 0.15),
|
||||||
|
inset 0 1px 0 0 rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页 */
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 订单详情样式 */
|
||||||
|
.order-detail {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 2px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title .el-icon {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 商品信息样式 */
|
||||||
|
.product-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-name {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-slot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 运单号样式 */
|
||||||
|
.tracking-no {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #10b981;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 备注内容 */
|
||||||
|
.remark-content {
|
||||||
|
padding: 16px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #4b5563;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stats-container :deep(.el-col) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-action-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -2,12 +2,64 @@
|
|||||||
<div class="product-management">
|
<div class="product-management">
|
||||||
<!-- 页面标题 -->
|
<!-- 页面标题 -->
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>商品管理</h2>
|
<div>
|
||||||
<p>管理平台所有商品和审核</p>
|
<h2>商品管理</h2>
|
||||||
|
<p>管理和审核平台所有商品</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<div class="stats-container">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card pending">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">待审核</div>
|
||||||
|
<div class="stat-value">{{ stats.pending }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><Warning /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card approved">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">已通过</div>
|
||||||
|
<div class="stat-value">{{ stats.approved }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><SuccessFilled /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card offline">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">已下架</div>
|
||||||
|
<div class="stat-value">{{ stats.offline }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><RemoveFilled /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card total">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">商品总数</div>
|
||||||
|
<div class="stat-value">{{ stats.total }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><Goods /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索栏 -->
|
<!-- 搜索栏 -->
|
||||||
<div class="search-bar">
|
<el-card class="search-card">
|
||||||
<el-form :inline="true" class="search-form">
|
<el-form :inline="true" class="search-form">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-input
|
<el-input
|
||||||
@ -16,10 +68,11 @@
|
|||||||
prefix-icon="Search"
|
prefix-icon="Search"
|
||||||
clearable
|
clearable
|
||||||
@clear="handleSearch"
|
@clear="handleSearch"
|
||||||
|
style="width: 260px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-select v-model="searchForm.status" placeholder="商品状态" clearable>
|
<el-select v-model="searchForm.status" placeholder="商品状态" clearable style="width: 150px">
|
||||||
<el-option label="全部" value="" />
|
<el-option label="全部" value="" />
|
||||||
<el-option label="审核中" :value="0" />
|
<el-option label="审核中" :value="0" />
|
||||||
<el-option label="已上架" :value="1" />
|
<el-option label="已上架" :value="1" />
|
||||||
@ -31,53 +84,104 @@
|
|||||||
<el-button @click="resetSearch" icon="Refresh">重置</el-button>
|
<el-button @click="resetSearch" icon="Refresh">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
<!-- 商品表格 -->
|
<!-- 商品表格 -->
|
||||||
<div class="table-container">
|
<el-card class="table-card">
|
||||||
<el-table :data="productList" v-loading="loading" stripe>
|
<el-table :data="productList" v-loading="loading" stripe>
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||||
<el-table-column prop="name" label="商品名称" width="200" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="price" label="价格" width="100">
|
<el-table-column label="商品图片" width="100" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
¥{{ scope.row.price }}
|
<div class="product-image">
|
||||||
|
<el-image
|
||||||
|
v-if="scope.row.imageList && scope.row.imageList.length > 0"
|
||||||
|
:src="scope.row.imageList[0]"
|
||||||
|
:alt="scope.row.name"
|
||||||
|
fit="cover"
|
||||||
|
style="width: 60px; height: 60px; border-radius: 6px; cursor: pointer;"
|
||||||
|
:preview-src-list="scope.row.imageList"
|
||||||
|
:initial-index="0"
|
||||||
|
preview-teleported
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<div class="image-slot">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
<div v-else class="no-image">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="origin" label="产地" width="120" />
|
|
||||||
<el-table-column prop="unit" label="单位" width="80" />
|
<el-table-column prop="name" label="商品名称" min-width="180" show-overflow-tooltip>
|
||||||
<el-table-column prop="salesCount" label="销量" width="80" />
|
|
||||||
<el-table-column prop="status" label="状态" width="100">
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="getStatusType(scope.row.status)">
|
<div class="product-name">
|
||||||
|
<span class="name-text">{{ scope.row.name }}</span>
|
||||||
|
<el-tag v-if="scope.row.status === 0" type="warning" size="small" class="status-badge">待审核</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="price" label="价格" width="120" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<span class="price">¥{{ scope.row.price }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="origin" label="产地" width="120" align="center" />
|
||||||
|
|
||||||
|
<el-table-column prop="unit" label="单位" width="80" align="center" />
|
||||||
|
|
||||||
|
<el-table-column prop="salesCount" label="销量" width="100" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag type="success" effect="plain">{{ scope.row.salesCount || 0 }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="getStatusType(scope.row.status)" effect="dark">
|
||||||
{{ getStatusText(scope.row.status) }}
|
{{ getStatusText(scope.row.status) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createdAt" label="创建时间" width="160" />
|
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
<el-table-column prop="createTime" label="创建时间" width="180" align="center" />
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="300" fixed="right" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<div class="glass-action-buttons">
|
||||||
v-if="scope.row.status === 0"
|
<!-- 审核中状态显示审核按钮 -->
|
||||||
type="success"
|
<template v-if="scope.row.status === 0">
|
||||||
size="small"
|
<button
|
||||||
@click="handleAudit(scope.row, 1)"
|
class="glass-btn glass-btn-success"
|
||||||
>
|
@click="handleAudit(scope.row, 1)"
|
||||||
通过
|
>
|
||||||
</el-button>
|
<el-icon class="btn-icon"><Check /></el-icon>
|
||||||
<el-button
|
<span>通过</span>
|
||||||
v-if="scope.row.status === 0"
|
</button>
|
||||||
type="danger"
|
<button
|
||||||
size="small"
|
class="glass-btn glass-btn-danger"
|
||||||
@click="handleAudit(scope.row, 2)"
|
@click="handleAudit(scope.row, 2)"
|
||||||
>
|
>
|
||||||
拒绝
|
<el-icon class="btn-icon"><Close /></el-icon>
|
||||||
</el-button>
|
<span>拒绝</span>
|
||||||
<el-button
|
</button>
|
||||||
size="small"
|
</template>
|
||||||
@click="handleViewDetail(scope.row)"
|
|
||||||
>
|
<!-- 查看详情按钮 -->
|
||||||
查看详情
|
<button
|
||||||
</el-button>
|
class="glass-btn glass-btn-primary"
|
||||||
|
@click="handleViewDetail(scope.row)"
|
||||||
|
>
|
||||||
|
<el-icon class="btn-icon"><View /></el-icon>
|
||||||
|
<span>详情</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -94,20 +198,149 @@
|
|||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 商品详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailVisible"
|
||||||
|
title="商品详情"
|
||||||
|
width="800px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<div v-if="currentProduct" class="product-detail" v-loading="detailLoading">
|
||||||
|
<!-- 商品图片 -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
<span>商品图片</span>
|
||||||
|
</div>
|
||||||
|
<div class="image-gallery">
|
||||||
|
<div
|
||||||
|
v-for="(image, index) in currentProduct.imageList"
|
||||||
|
:key="index"
|
||||||
|
class="gallery-item"
|
||||||
|
>
|
||||||
|
<el-image
|
||||||
|
:src="image"
|
||||||
|
fit="cover"
|
||||||
|
style="width: 120px; height: 120px; border-radius: 8px;"
|
||||||
|
:preview-src-list="currentProduct.imageList"
|
||||||
|
:initial-index="index"
|
||||||
|
preview-teleported
|
||||||
|
/>
|
||||||
|
<el-tag v-if="index === 0" size="small" type="warning" class="main-tag">主图</el-tag>
|
||||||
|
</div>
|
||||||
|
<div v-if="!currentProduct.imageList || currentProduct.imageList.length === 0" class="no-images">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
<span>暂无图片</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>基本信息</span>
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="商品ID">{{ currentProduct.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品名称">{{ currentProduct.name }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品价格">
|
||||||
|
<span class="price-large">¥{{ currentProduct.price }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="原价">
|
||||||
|
<span v-if="currentProduct.originPrice" class="origin-price">¥{{ currentProduct.originPrice }}</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="产地">{{ currentProduct.origin || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="单位">{{ currentProduct.unit || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="重量">{{ currentProduct.weight || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="保质期">{{ currentProduct.shelfLife || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="储存方式" :span="2">
|
||||||
|
{{ currentProduct.storageMethod || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品状态">
|
||||||
|
<el-tag :type="getStatusType(currentProduct.status)">
|
||||||
|
{{ getStatusText(currentProduct.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="销量">
|
||||||
|
<el-tag type="success">{{ currentProduct.salesCount || 0 }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="浏览量">
|
||||||
|
<el-tag type="info">{{ currentProduct.viewCount || 0 }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="分类ID">{{ currentProduct.categoryId }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商家ID">{{ currentProduct.merchantId }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间" :span="2">
|
||||||
|
{{ formatDate(currentProduct.createTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 商品描述 -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>商品描述</span>
|
||||||
|
</div>
|
||||||
|
<div class="description-content">
|
||||||
|
{{ currentProduct.description || '暂无描述' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="detailVisible = false" size="large">关闭</el-button>
|
||||||
|
<template v-if="currentProduct && currentProduct.status === 0">
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
@click="handleAuditInDialog(1)"
|
||||||
|
size="large"
|
||||||
|
class="audit-btn"
|
||||||
|
>
|
||||||
|
<el-icon class="btn-icon"><Check /></el-icon>
|
||||||
|
<span>审核通过</span>
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
@click="handleAuditInDialog(2)"
|
||||||
|
size="large"
|
||||||
|
class="audit-btn"
|
||||||
|
>
|
||||||
|
<el-icon class="btn-icon"><Close /></el-icon>
|
||||||
|
<span>审核拒绝</span>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { getProducts, auditProduct } from '../../../api/admin'
|
import {
|
||||||
|
Warning, SuccessFilled, RemoveFilled, Goods, Search, Refresh,
|
||||||
|
Picture, View, Check, Close, InfoFilled, Document
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import { getProducts, getProductDetail, auditProduct } from '../../../api/admin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ProductManagement',
|
name: 'ProductManagement',
|
||||||
|
components: {
|
||||||
|
Warning, SuccessFilled, RemoveFilled, Goods, Search, Refresh,
|
||||||
|
Picture, View, Check, Close, InfoFilled, Document
|
||||||
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const detailLoading = ref(false)
|
||||||
const productList = ref([])
|
const productList = ref([])
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const currentProduct = ref(null)
|
||||||
|
|
||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
@ -120,6 +353,23 @@ export default {
|
|||||||
total: 0
|
total: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = reactive({
|
||||||
|
pending: 0,
|
||||||
|
approved: 0,
|
||||||
|
offline: 0,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
const calculateStats = (products) => {
|
||||||
|
stats.pending = products.filter(p => p.status === 0).length
|
||||||
|
stats.approved = products.filter(p => p.status === 1).length
|
||||||
|
stats.offline = products.filter(p => p.status === 2).length
|
||||||
|
stats.total = products.length
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载商品列表
|
||||||
const loadProducts = async () => {
|
const loadProducts = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@ -133,18 +383,24 @@ export default {
|
|||||||
const result = await getProducts(params)
|
const result = await getProducts(params)
|
||||||
productList.value = result.records
|
productList.value = result.records
|
||||||
pagination.total = result.total
|
pagination.total = result.total
|
||||||
|
|
||||||
|
// 更新统计(这里只是当前页的统计,实际应该从后端获取全局统计)
|
||||||
|
calculateStats(result.records)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('加载商品列表失败:', error)
|
||||||
ElMessage.error('加载商品列表失败')
|
ElMessage.error('加载商品列表失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
pagination.page = 1
|
pagination.page = 1
|
||||||
loadProducts()
|
loadProducts()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
const resetSearch = () => {
|
const resetSearch = () => {
|
||||||
searchForm.keyword = ''
|
searchForm.keyword = ''
|
||||||
searchForm.status = ''
|
searchForm.status = ''
|
||||||
@ -152,6 +408,7 @@ export default {
|
|||||||
loadProducts()
|
loadProducts()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 分页变化
|
||||||
const handleSizeChange = (size) => {
|
const handleSizeChange = (size) => {
|
||||||
pagination.size = size
|
pagination.size = size
|
||||||
loadProducts()
|
loadProducts()
|
||||||
@ -162,6 +419,7 @@ export default {
|
|||||||
loadProducts()
|
loadProducts()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 审核商品
|
||||||
const handleAudit = async (product, status) => {
|
const handleAudit = async (product, status) => {
|
||||||
const action = status === 1 ? '通过' : '拒绝'
|
const action = status === 1 ? '通过' : '拒绝'
|
||||||
|
|
||||||
@ -181,43 +439,78 @@ export default {
|
|||||||
loadProducts()
|
loadProducts()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
|
console.error('审核失败:', error)
|
||||||
ElMessage.error(`商品审核${action}失败`)
|
ElMessage.error(`商品审核${action}失败`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleViewDetail = (product) => {
|
// 在对话框中审核
|
||||||
// 查看商品详情
|
const handleAuditInDialog = async (status) => {
|
||||||
console.log('查看商品详情:', product)
|
if (!currentProduct.value) return
|
||||||
|
|
||||||
|
await handleAudit(currentProduct.value, status)
|
||||||
|
detailVisible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查看商品详情
|
||||||
|
const handleViewDetail = async (product) => {
|
||||||
|
detailVisible.value = true
|
||||||
|
detailLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const detail = await getProductDetail(product.id)
|
||||||
|
currentProduct.value = detail
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取商品详情失败:', error)
|
||||||
|
ElMessage.error('获取商品详情失败')
|
||||||
|
detailVisible.value = false
|
||||||
|
} finally {
|
||||||
|
detailLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态类型
|
||||||
const getStatusType = (status) => {
|
const getStatusType = (status) => {
|
||||||
const types = { 0: 'warning', 1: 'success', 2: 'info' }
|
const types = { 0: 'warning', 1: 'success', 2: 'info' }
|
||||||
return types[status] || ''
|
return types[status] || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
const getStatusText = (status) => {
|
const getStatusText = (status) => {
|
||||||
const texts = { 0: '审核中', 1: '已上架', 2: '已下架' }
|
const texts = { 0: '审核中', 1: '已上架', 2: '已下架' }
|
||||||
return texts[status] || '未知'
|
return texts[status] || '未知'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化日期时间
|
||||||
|
const formatDate = (dateTime) => {
|
||||||
|
if (!dateTime) return '-'
|
||||||
|
return new Date(dateTime).toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadProducts()
|
loadProducts()
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
|
detailLoading,
|
||||||
productList,
|
productList,
|
||||||
searchForm,
|
searchForm,
|
||||||
pagination,
|
pagination,
|
||||||
|
stats,
|
||||||
|
detailVisible,
|
||||||
|
currentProduct,
|
||||||
handleSearch,
|
handleSearch,
|
||||||
resetSearch,
|
resetSearch,
|
||||||
handleSizeChange,
|
handleSizeChange,
|
||||||
handleCurrentChange,
|
handleCurrentChange,
|
||||||
handleAudit,
|
handleAudit,
|
||||||
|
handleAuditInDialog,
|
||||||
handleViewDetail,
|
handleViewDetail,
|
||||||
getStatusType,
|
getStatusType,
|
||||||
getStatusText
|
getStatusText,
|
||||||
|
formatDate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,47 +519,431 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.product-management {
|
.product-management {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
min-height: calc(100vh - 60px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header h2 {
|
.page-header h2 {
|
||||||
margin: 0 0 5px 0;
|
margin: 0 0 5px 0;
|
||||||
color: #333;
|
color: #1f2937;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header p {
|
.page-header p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #666;
|
color: #6b7280;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
/* 统计卡片样式 */
|
||||||
background: white;
|
.stats-container {
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.pending .stat-value {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.pending .stat-icon {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.approved .stat-value {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.approved .stat-icon {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.offline .stat-value {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.offline .stat-icon {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.total .stat-value {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.total .stat-icon {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索卡片 */
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: none;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-form {
|
.search-form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格卡片 */
|
||||||
|
.table-card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 商品图片 */
|
||||||
|
.product-image {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-image,
|
||||||
|
.image-slot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 商品名称 */
|
||||||
|
.product-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 价格样式 */
|
||||||
|
.price {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-large {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.origin-price {
|
||||||
|
color: #9ca3af;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 玻璃质感按钮样式 ==================== */
|
||||||
|
.glass-action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.glass-btn {
|
||||||
background: white;
|
position: relative;
|
||||||
padding: 20px;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.glass-btn::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.3),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
transition: left 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn .btn-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn:hover .btn-icon {
|
||||||
|
transform: scale(1.2) rotate(5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 通过按钮 - 绿色玻璃 */
|
||||||
|
.glass-btn-success {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(16, 185, 129, 0.8) 0%,
|
||||||
|
rgba(5, 150, 105, 0.8) 100%
|
||||||
|
);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn-success:hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(16, 185, 129, 0.9) 0%,
|
||||||
|
rgba(5, 150, 105, 0.9) 100%
|
||||||
|
);
|
||||||
|
box-shadow: 0 8px 16px -4px rgba(16, 185, 129, 0.4),
|
||||||
|
0 4px 6px -2px rgba(16, 185, 129, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拒绝按钮 - 红色玻璃 */
|
||||||
|
.glass-btn-danger {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(239, 68, 68, 0.8) 0%,
|
||||||
|
rgba(220, 38, 38, 0.8) 100%
|
||||||
|
);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn-danger:hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(239, 68, 68, 0.9) 0%,
|
||||||
|
rgba(220, 38, 38, 0.9) 100%
|
||||||
|
);
|
||||||
|
box-shadow: 0 8px 16px -4px rgba(239, 68, 68, 0.4),
|
||||||
|
0 4px 6px -2px rgba(239, 68, 68, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 详情按钮 - 蓝色玻璃 */
|
||||||
|
.glass-btn-primary {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(59, 130, 246, 0.15) 0%,
|
||||||
|
rgba(37, 99, 235, 0.15) 100%
|
||||||
|
);
|
||||||
|
color: #3b82f6;
|
||||||
|
border: 1.5px solid rgba(59, 130, 246, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn-primary:hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(59, 130, 246, 0.25) 0%,
|
||||||
|
rgba(37, 99, 235, 0.25) 100%
|
||||||
|
);
|
||||||
|
border-color: rgba(59, 130, 246, 0.8);
|
||||||
|
box-shadow: 0 8px 16px -4px rgba(59, 130, 246, 0.3),
|
||||||
|
0 4px 6px -2px rgba(59, 130, 246, 0.15),
|
||||||
|
inset 0 1px 0 0 rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮禁用状态 */
|
||||||
|
.glass-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn:disabled:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页 */
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 商品详情样式 */
|
||||||
|
.product-detail {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 2px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title .el-icon {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图片画廊 */
|
||||||
|
.image-gallery {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-images {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-images .el-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 描述内容 */
|
||||||
|
.description-content {
|
||||||
|
padding: 16px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #4b5563;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 对话框底部审核按钮样式 */
|
||||||
|
.dialog-footer .audit-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer .audit-btn .btn-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stats-container :deep(.el-col) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-action-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -2,12 +2,64 @@
|
|||||||
<div class="user-management">
|
<div class="user-management">
|
||||||
<!-- 页面标题 -->
|
<!-- 页面标题 -->
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>用户管理</h2>
|
<div>
|
||||||
<p>管理平台所有用户账号</p>
|
<h2>用户管理</h2>
|
||||||
|
<p>管理平台所有用户账号</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
|
<div class="stats-container">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card total">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">用户总数</div>
|
||||||
|
<div class="stat-value">{{ stats.total }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><User /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card verified">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">已认证</div>
|
||||||
|
<div class="stat-value">{{ stats.verified }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><CircleCheck /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card active">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">正常用户</div>
|
||||||
|
<div class="stat-value">{{ stats.active }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><SuccessFilled /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card disabled">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-info">
|
||||||
|
<div class="stat-label">禁用用户</div>
|
||||||
|
<div class="stat-value">{{ stats.disabled }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="stat-icon"><CircleClose /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索栏 -->
|
<!-- 搜索栏 -->
|
||||||
<div class="search-bar">
|
<el-card class="search-card">
|
||||||
<el-form :inline="true" class="search-form">
|
<el-form :inline="true" class="search-form">
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-input
|
<el-input
|
||||||
@ -16,55 +68,118 @@
|
|||||||
prefix-icon="Search"
|
prefix-icon="Search"
|
||||||
clearable
|
clearable
|
||||||
@clear="handleSearch"
|
@clear="handleSearch"
|
||||||
|
style="width: 260px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-select v-model="searchForm.status" placeholder="用户状态" clearable>
|
<el-select v-model="searchForm.status" placeholder="用户状态" clearable style="width: 150px">
|
||||||
<el-option label="全部" value="" />
|
<el-option label="全部" value="" />
|
||||||
<el-option label="正常" :value="1" />
|
<el-option label="正常" :value="1" />
|
||||||
<el-option label="禁用" :value="0" />
|
<el-option label="禁用" :value="0" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-select v-model="searchForm.isVerified" placeholder="认证状态" clearable style="width: 150px">
|
||||||
|
<el-option label="全部" value="" />
|
||||||
|
<el-option label="已认证" :value="1" />
|
||||||
|
<el-option label="未认证" :value="0" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="handleSearch" icon="Search">搜索</el-button>
|
<el-button type="primary" @click="handleSearch" icon="Search">搜索</el-button>
|
||||||
<el-button @click="resetSearch" icon="Refresh">重置</el-button>
|
<el-button @click="resetSearch" icon="Refresh">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
<!-- 用户表格 -->
|
<!-- 用户表格 -->
|
||||||
<div class="table-container">
|
<el-card class="table-card">
|
||||||
<el-table :data="userList" v-loading="loading" stripe>
|
<el-table :data="userList" v-loading="loading" stripe>
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||||
<el-table-column prop="username" label="用户名" width="120" />
|
|
||||||
<el-table-column prop="nickname" label="昵称" width="120" />
|
<el-table-column label="用户信息" min-width="200">
|
||||||
<el-table-column prop="phone" label="手机号" width="130" />
|
|
||||||
<el-table-column prop="email" label="邮箱" width="180" />
|
|
||||||
<el-table-column prop="isVerified" label="实名认证" width="100">
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="scope.row.isVerified ? 'success' : 'warning'">
|
<div class="user-info">
|
||||||
{{ scope.row.isVerified ? '已认证' : '未认证' }}
|
<el-avatar :size="50" :src="scope.row.avatar" class="user-avatar">
|
||||||
</el-tag>
|
<el-icon><UserFilled /></el-icon>
|
||||||
|
</el-avatar>
|
||||||
|
<div class="user-details">
|
||||||
|
<div class="user-name">
|
||||||
|
<span class="username">{{ scope.row.username }}</span>
|
||||||
|
<el-tag v-if="scope.row.isVerified" type="success" size="small" class="verified-badge">
|
||||||
|
<el-icon><CircleCheck /></el-icon> 已认证
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="user-nickname">{{ scope.row.nickname || '未设置昵称' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="status" label="状态" width="100">
|
|
||||||
|
<el-table-column prop="phone" label="手机号" width="140" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
<span class="phone-number">{{ maskPhone(scope.row.phone) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="email" label="邮箱" width="200" align="center" show-overflow-tooltip>
|
||||||
|
<template #default="scope">
|
||||||
|
<span class="email-text">{{ scope.row.email || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="gender" label="性别" width="80" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-if="scope.row.gender === 1" type="primary" effect="plain">男</el-tag>
|
||||||
|
<el-tag v-else-if="scope.row.gender === 2" type="danger" effect="plain">女</el-tag>
|
||||||
|
<el-tag v-else type="info" effect="plain">未知</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'" effect="dark">
|
||||||
{{ scope.row.status === 1 ? '正常' : '禁用' }}
|
{{ scope.row.status === 1 ? '正常' : '禁用' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createdAt" label="注册时间" width="160" />
|
|
||||||
<el-table-column prop="lastLoginTime" label="最后登录" width="160" />
|
<el-table-column prop="createTime" label="注册时间" width="180" align="center">
|
||||||
<el-table-column label="操作" width="120" fixed="right">
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
{{ formatTime(scope.row.createTime) }}
|
||||||
:type="scope.row.status === 1 ? 'danger' : 'success'"
|
</template>
|
||||||
size="small"
|
</el-table-column>
|
||||||
@click="handleStatusChange(scope.row)"
|
|
||||||
>
|
<el-table-column label="操作" width="250" fixed="right" align="center">
|
||||||
{{ scope.row.status === 1 ? '禁用' : '启用' }}
|
<template #default="scope">
|
||||||
</el-button>
|
<div class="glass-action-buttons">
|
||||||
|
<!-- 启用/禁用按钮 -->
|
||||||
|
<button
|
||||||
|
v-if="scope.row.status === 1"
|
||||||
|
class="glass-btn glass-btn-danger"
|
||||||
|
@click="handleStatusChange(scope.row)"
|
||||||
|
>
|
||||||
|
<el-icon class="btn-icon"><Lock /></el-icon>
|
||||||
|
<span>禁用</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="glass-btn glass-btn-success"
|
||||||
|
@click="handleStatusChange(scope.row)"
|
||||||
|
>
|
||||||
|
<el-icon class="btn-icon"><Unlock /></el-icon>
|
||||||
|
<span>启用</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 查看详情按钮 -->
|
||||||
|
<button
|
||||||
|
class="glass-btn glass-btn-primary"
|
||||||
|
@click="handleViewDetail(scope.row)"
|
||||||
|
>
|
||||||
|
<el-icon class="btn-icon"><View /></el-icon>
|
||||||
|
<span>详情</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -81,24 +196,183 @@
|
|||||||
@current-change="handleCurrentChange"
|
@current-change="handleCurrentChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 用户详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailVisible"
|
||||||
|
title="用户详情"
|
||||||
|
width="800px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<div v-if="currentUser" class="user-detail" v-loading="detailLoading">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>基本信息</span>
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="用户ID">{{ currentUser.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户名">{{ currentUser.username }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="昵称">
|
||||||
|
{{ currentUser.nickname || '未设置' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="性别">
|
||||||
|
<el-tag v-if="currentUser.gender === 1" type="primary" effect="plain">男</el-tag>
|
||||||
|
<el-tag v-else-if="currentUser.gender === 2" type="danger" effect="plain">女</el-tag>
|
||||||
|
<el-tag v-else type="info" effect="plain">未知</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="生日">
|
||||||
|
{{ currentUser.birthday || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="账号状态">
|
||||||
|
<el-tag :type="currentUser.status === 1 ? 'success' : 'danger'" effect="dark">
|
||||||
|
{{ currentUser.status === 1 ? '正常' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 联系方式 -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<el-icon><Phone /></el-icon>
|
||||||
|
<span>联系方式</span>
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="手机号">
|
||||||
|
<span class="phone-number">{{ currentUser.phone }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="邮箱">
|
||||||
|
<span class="email-text">{{ currentUser.email || '未绑定' }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 实名认证信息 -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<el-icon><CreditCard /></el-icon>
|
||||||
|
<span>实名认证信息</span>
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="认证状态">
|
||||||
|
<el-tag :type="currentUser.isVerified ? 'success' : 'warning'" effect="dark">
|
||||||
|
{{ currentUser.isVerified ? '已认证' : '未认证' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="真实姓名">
|
||||||
|
{{ currentUser.realName || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="身份证号" :span="2">
|
||||||
|
{{ maskIdCard(currentUser.idCard) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录信息 -->
|
||||||
|
<div class="detail-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<el-icon><Clock /></el-icon>
|
||||||
|
<span>登录信息</span>
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="注册时间">
|
||||||
|
{{ formatTime(currentUser.createTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最后登录">
|
||||||
|
{{ formatTime(currentUser.lastLoginTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间" :span="2">
|
||||||
|
{{ formatTime(currentUser.updateTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户头像 -->
|
||||||
|
<div class="detail-section" v-if="currentUser.avatar">
|
||||||
|
<div class="section-title">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
<span>用户头像</span>
|
||||||
|
</div>
|
||||||
|
<div class="avatar-container">
|
||||||
|
<el-image
|
||||||
|
:src="currentUser.avatar"
|
||||||
|
fit="cover"
|
||||||
|
style="width: 150px; height: 150px; border-radius: 50%;"
|
||||||
|
:preview-src-list="[currentUser.avatar]"
|
||||||
|
preview-teleported
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<div class="image-slot">
|
||||||
|
<el-icon><UserFilled /></el-icon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="detailVisible = false" size="large">关闭</el-button>
|
||||||
|
<template v-if="currentUser">
|
||||||
|
<el-button
|
||||||
|
v-if="currentUser.status === 1"
|
||||||
|
type="danger"
|
||||||
|
@click="handleStatusChangeInDialog(0)"
|
||||||
|
size="large"
|
||||||
|
class="action-btn"
|
||||||
|
>
|
||||||
|
<el-icon class="btn-icon"><Lock /></el-icon>
|
||||||
|
<span>禁用用户</span>
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
type="success"
|
||||||
|
@click="handleStatusChangeInDialog(1)"
|
||||||
|
size="large"
|
||||||
|
class="action-btn"
|
||||||
|
>
|
||||||
|
<el-icon class="btn-icon"><Unlock /></el-icon>
|
||||||
|
<span>启用用户</span>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
User, CircleCheck, SuccessFilled, CircleClose, Search, Refresh,
|
||||||
|
UserFilled, Lock, Unlock, View, InfoFilled, Phone, CreditCard,
|
||||||
|
Clock, Picture
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
import { getUsers, updateUserStatus } from '../../../api/admin'
|
import { getUsers, updateUserStatus } from '../../../api/admin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'UserManagement',
|
name: 'UserManagement',
|
||||||
|
components: {
|
||||||
|
User, CircleCheck, SuccessFilled, CircleClose, Search, Refresh,
|
||||||
|
UserFilled, Lock, Unlock, View, InfoFilled, Phone, CreditCard,
|
||||||
|
Clock, Picture
|
||||||
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const detailLoading = ref(false)
|
||||||
const userList = ref([])
|
const userList = ref([])
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const currentUser = ref(null)
|
||||||
|
|
||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
keyword: '',
|
keyword: '',
|
||||||
status: ''
|
status: '',
|
||||||
|
isVerified: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
@ -107,6 +381,22 @@ export default {
|
|||||||
total: 0
|
total: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const stats = reactive({
|
||||||
|
total: 0,
|
||||||
|
verified: 0,
|
||||||
|
active: 0,
|
||||||
|
disabled: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
const calculateStats = (users) => {
|
||||||
|
stats.verified = users.filter(u => u.isVerified === 1).length
|
||||||
|
stats.active = users.filter(u => u.status === 1).length
|
||||||
|
stats.disabled = users.filter(u => u.status === 0).length
|
||||||
|
stats.total = users.length
|
||||||
|
}
|
||||||
|
|
||||||
const loadUsers = async () => {
|
const loadUsers = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@ -114,13 +404,18 @@ export default {
|
|||||||
page: pagination.page,
|
page: pagination.page,
|
||||||
size: pagination.size,
|
size: pagination.size,
|
||||||
keyword: searchForm.keyword,
|
keyword: searchForm.keyword,
|
||||||
status: searchForm.status
|
status: searchForm.status,
|
||||||
|
isVerified: searchForm.isVerified
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await getUsers(params)
|
const result = await getUsers(params)
|
||||||
userList.value = result.records
|
userList.value = result.records
|
||||||
pagination.total = result.total
|
pagination.total = result.total
|
||||||
|
|
||||||
|
// 更新统计
|
||||||
|
calculateStats(result.records)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('加载用户列表失败:', error)
|
||||||
ElMessage.error('加载用户列表失败')
|
ElMessage.error('加载用户列表失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
@ -135,6 +430,7 @@ export default {
|
|||||||
const resetSearch = () => {
|
const resetSearch = () => {
|
||||||
searchForm.keyword = ''
|
searchForm.keyword = ''
|
||||||
searchForm.status = ''
|
searchForm.status = ''
|
||||||
|
searchForm.isVerified = ''
|
||||||
pagination.page = 1
|
pagination.page = 1
|
||||||
loadUsers()
|
loadUsers()
|
||||||
}
|
}
|
||||||
@ -169,25 +465,66 @@ export default {
|
|||||||
loadUsers()
|
loadUsers()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error !== 'cancel') {
|
if (error !== 'cancel') {
|
||||||
|
console.error('操作失败:', error)
|
||||||
ElMessage.error(`用户${action}失败`)
|
ElMessage.error(`用户${action}失败`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleStatusChangeInDialog = async (newStatus) => {
|
||||||
|
if (!currentUser.value) return
|
||||||
|
|
||||||
|
const user = { ...currentUser.value, status: currentUser.value.status }
|
||||||
|
currentUser.value.status = newStatus
|
||||||
|
await handleStatusChange({ ...user, status: currentUser.value.status === 1 ? 0 : 1 })
|
||||||
|
detailVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleViewDetail = (user) => {
|
||||||
|
currentUser.value = user
|
||||||
|
detailVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机号脱敏
|
||||||
|
const maskPhone = (phone) => {
|
||||||
|
if (!phone) return '-'
|
||||||
|
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 身份证号脱敏
|
||||||
|
const maskIdCard = (idCard) => {
|
||||||
|
if (!idCard) return '-'
|
||||||
|
return idCard.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2')
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (time) => {
|
||||||
|
if (!time) return '-'
|
||||||
|
return new Date(time).toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadUsers()
|
loadUsers()
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
|
detailLoading,
|
||||||
userList,
|
userList,
|
||||||
searchForm,
|
searchForm,
|
||||||
pagination,
|
pagination,
|
||||||
|
stats,
|
||||||
|
detailVisible,
|
||||||
|
currentUser,
|
||||||
handleSearch,
|
handleSearch,
|
||||||
resetSearch,
|
resetSearch,
|
||||||
handleSizeChange,
|
handleSizeChange,
|
||||||
handleCurrentChange,
|
handleCurrentChange,
|
||||||
handleStatusChange
|
handleStatusChange,
|
||||||
|
handleStatusChangeInDialog,
|
||||||
|
handleViewDetail,
|
||||||
|
maskPhone,
|
||||||
|
maskIdCard,
|
||||||
|
formatTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -196,47 +533,394 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.user-management {
|
.user-management {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
min-height: calc(100vh - 60px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header h2 {
|
.page-header h2 {
|
||||||
margin: 0 0 5px 0;
|
margin: 0 0 5px 0;
|
||||||
color: #333;
|
color: #1f2937;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header p {
|
.page-header p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #666;
|
color: #6b7280;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
/* 统计卡片样式 */
|
||||||
background: white;
|
.stats-container {
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: all 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.total .stat-value {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.total .stat-icon {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.verified .stat-value {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.verified .stat-icon {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.active .stat-value {
|
||||||
|
color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.active .stat-icon {
|
||||||
|
color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.disabled .stat-value {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card.disabled .stat-icon {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索卡片 */
|
||||||
|
.search-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border: none;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-form {
|
.search-form {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格卡片 */
|
||||||
|
.table-card {
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户信息样式 */
|
||||||
|
.user-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 2px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-details {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verified-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-nickname {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机号和邮箱样式 */
|
||||||
|
.phone-number {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-text {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==================== 玻璃质感按钮样式 ==================== */
|
||||||
|
.glass-action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.glass-btn {
|
||||||
background: white;
|
position: relative;
|
||||||
padding: 20px;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.glass-btn::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.3),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
transition: left 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn .btn-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn:hover .btn-icon {
|
||||||
|
transform: scale(1.2) rotate(5deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 启用按钮 - 绿色玻璃 */
|
||||||
|
.glass-btn-success {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(16, 185, 129, 0.8) 0%,
|
||||||
|
rgba(5, 150, 105, 0.8) 100%
|
||||||
|
);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn-success:hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(16, 185, 129, 0.9) 0%,
|
||||||
|
rgba(5, 150, 105, 0.9) 100%
|
||||||
|
);
|
||||||
|
box-shadow: 0 8px 16px -4px rgba(16, 185, 129, 0.4),
|
||||||
|
0 4px 6px -2px rgba(16, 185, 129, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 禁用按钮 - 红色玻璃 */
|
||||||
|
.glass-btn-danger {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(239, 68, 68, 0.8) 0%,
|
||||||
|
rgba(220, 38, 38, 0.8) 100%
|
||||||
|
);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn-danger:hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(239, 68, 68, 0.9) 0%,
|
||||||
|
rgba(220, 38, 38, 0.9) 100%
|
||||||
|
);
|
||||||
|
box-shadow: 0 8px 16px -4px rgba(239, 68, 68, 0.4),
|
||||||
|
0 4px 6px -2px rgba(239, 68, 68, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 详情按钮 - 蓝色玻璃 */
|
||||||
|
.glass-btn-primary {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(59, 130, 246, 0.15) 0%,
|
||||||
|
rgba(37, 99, 235, 0.15) 100%
|
||||||
|
);
|
||||||
|
color: #3b82f6;
|
||||||
|
border: 1.5px solid rgba(59, 130, 246, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn-primary:hover {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(59, 130, 246, 0.25) 0%,
|
||||||
|
rgba(37, 99, 235, 0.25) 100%
|
||||||
|
);
|
||||||
|
border-color: rgba(59, 130, 246, 0.8);
|
||||||
|
box-shadow: 0 8px 16px -4px rgba(59, 130, 246, 0.3),
|
||||||
|
0 4px 6px -2px rgba(59, 130, 246, 0.15),
|
||||||
|
inset 0 1px 0 0 rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页 */
|
||||||
.pagination {
|
.pagination {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 用户详情样式 */
|
||||||
|
.user-detail {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 2px solid #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title .el-icon {
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 头像容器 */
|
||||||
|
.avatar-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-slot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 对话框底部操作按钮样式 */
|
||||||
|
.dialog-footer .action-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer .action-btn .btn-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stats-container :deep(.el-col) {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-action-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -190,7 +190,7 @@
|
|||||||
<el-descriptions-item label="邮箱">{{ selectedCustomer.email || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="邮箱">{{ selectedCustomer.email || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="性别">{{ getGenderText(selectedCustomer.gender) }}</el-descriptions-item>
|
<el-descriptions-item label="性别">{{ getGenderText(selectedCustomer.gender) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="生日">{{ selectedCustomer.birthday || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="生日">{{ selectedCustomer.birthday || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="注册时间">{{ formatTime(selectedCustomer.createdAt) }}</el-descriptions-item>
|
<el-descriptions-item label="注册时间">{{ formatTime(selectedCustomer.createTime) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="最后登录">{{ formatTime(selectedCustomer.lastLoginTime) }}</el-descriptions-item>
|
<el-descriptions-item label="最后登录">{{ formatTime(selectedCustomer.lastLoginTime) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="订单总数">{{ selectedCustomer.orderCount }}</el-descriptions-item>
|
<el-descriptions-item label="订单总数">{{ selectedCustomer.orderCount }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="总消费金额">¥{{ selectedCustomer.totalAmount.toFixed(2) }}</el-descriptions-item>
|
<el-descriptions-item label="总消费金额">¥{{ selectedCustomer.totalAmount.toFixed(2) }}</el-descriptions-item>
|
||||||
@ -216,8 +216,8 @@
|
|||||||
<el-tag :type="getOrderStatusType(row.status)">{{ row.statusName }}</el-tag>
|
<el-tag :type="getOrderStatusType(row.status)">{{ row.statusName }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createdAt" label="下单时间" width="180">
|
<el-table-column prop="createTime" label="下单时间" width="180">
|
||||||
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
|
<template #default="{ row }">{{ formatTime(row.createTime) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="商品数量" width="100">
|
<el-table-column label="商品数量" width="100">
|
||||||
<template #default="{ row }">{{ row.orderDetails?.length || 0 }}件</template>
|
<template #default="{ row }">{{ row.orderDetails?.length || 0 }}件</template>
|
||||||
@ -285,8 +285,8 @@ export default {
|
|||||||
totalAmount: 0,
|
totalAmount: 0,
|
||||||
validOrderCount: 0, // 有效订单数(已支付)
|
validOrderCount: 0, // 有效订单数(已支付)
|
||||||
orders: [],
|
orders: [],
|
||||||
firstOrderTime: order.createdAt,
|
firstOrderTime: order.createTime,
|
||||||
lastOrderTime: order.createdAt
|
lastOrderTime: order.createTime
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,11 +301,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新首次和最近购买时间
|
// 更新首次和最近购买时间
|
||||||
if (new Date(order.createdAt) < new Date(customer.firstOrderTime)) {
|
if (new Date(order.createTime) < new Date(customer.firstOrderTime)) {
|
||||||
customer.firstOrderTime = order.createdAt
|
customer.firstOrderTime = order.createTime
|
||||||
}
|
}
|
||||||
if (new Date(order.createdAt) > new Date(customer.lastOrderTime)) {
|
if (new Date(order.createTime) > new Date(customer.lastOrderTime)) {
|
||||||
customer.lastOrderTime = order.createdAt
|
customer.lastOrderTime = order.createTime
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -80,9 +80,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="salesCount" label="销量" width="80" />
|
<el-table-column prop="salesCount" label="销量" width="80" />
|
||||||
<el-table-column prop="createdAt" label="创建时间" width="180">
|
<el-table-column prop="createTime" label="创建时间" width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ formatDate(row.createdAt) }}
|
{{ formatDate(row.createTime) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
|||||||
@ -297,39 +297,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右下角聊天悬浮球 -->
|
<!-- 右下角聊天悬浮球 - 使用Teleport挂载到body -->
|
||||||
<div
|
<Teleport to="body">
|
||||||
v-if="product && product.merchantId"
|
<div
|
||||||
class="chat-fab"
|
v-if="product && product.merchantId"
|
||||||
:style="{
|
class="chat-fab"
|
||||||
left: fabPos.x + 'px',
|
:style="{
|
||||||
top: fabPos.y + 'px'
|
left: fabPos.x + 'px',
|
||||||
}"
|
top: fabPos.y + 'px'
|
||||||
@mousedown="startDrag"
|
}"
|
||||||
@touchstart="startDragTouch"
|
@mousedown="startDrag"
|
||||||
title="联系商家"
|
@touchstart="startDragTouch"
|
||||||
@click="openChat"
|
title="联系商家"
|
||||||
>
|
@click="openChat"
|
||||||
<span class="chat-fab-icon">💬</span>
|
>
|
||||||
</div>
|
<span class="chat-fab-icon">💬</span>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
|
||||||
<!-- 聊天窗口 -->
|
<!-- 聊天窗口 - 使用Teleport挂载到body -->
|
||||||
<ChatWindow
|
<Teleport to="body">
|
||||||
v-if="product && product.merchantId && showChatWindow"
|
<ChatWindow
|
||||||
:visible="showChatWindow"
|
v-if="product && product.merchantId && showChatWindow"
|
||||||
:merchant-id="product.merchantId"
|
:visible="showChatWindow"
|
||||||
:merchant-name="product.merchantName || '商家'"
|
:merchant-id="product.merchantId"
|
||||||
user-type="user"
|
:merchant-name="product.merchantName || '商家'"
|
||||||
@close="closeChatWindow"
|
user-type="user"
|
||||||
@minimize="minimizeChatWindow"
|
@close="closeChatWindow"
|
||||||
:anchor-bottom="anchorBottom"
|
@minimize="minimizeChatWindow"
|
||||||
:anchor-right="anchorRight"
|
:anchor-bottom="anchorBottom"
|
||||||
/>
|
:anchor-right="anchorRight"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { getProductDetail, getHotProducts } from '../../api/product'
|
import { getProductDetail, getHotProducts } from '../../api/product'
|
||||||
@ -378,7 +382,10 @@ export default {
|
|||||||
const reviewPageSize = ref(10)
|
const reviewPageSize = ref(10)
|
||||||
|
|
||||||
// 拖拽相关
|
// 拖拽相关
|
||||||
const fabPos = ref({ x: window.innerWidth - 96, y: Math.max(120, window.innerHeight - 280) })
|
const fabPos = ref({
|
||||||
|
x: typeof window !== 'undefined' ? window.innerWidth - 80 : 0,
|
||||||
|
y: typeof window !== 'undefined' ? window.innerHeight - 80 : 0
|
||||||
|
})
|
||||||
const dragging = ref(false)
|
const dragging = ref(false)
|
||||||
const dragOffset = ref({ x: 0, y: 0 })
|
const dragOffset = ref({ x: 0, y: 0 })
|
||||||
|
|
||||||
@ -710,12 +717,33 @@ export default {
|
|||||||
|
|
||||||
const goToProduct = (productId) => {
|
const goToProduct = (productId) => {
|
||||||
router.push(`/product/${productId}`)
|
router.push(`/product/${productId}`)
|
||||||
loadProductDetail()
|
|
||||||
loadRecommendedProducts()
|
|
||||||
loadCartCount()
|
|
||||||
loadReviews()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听路由参数变化
|
||||||
|
watch(() => route.params.id, (newId, oldId) => {
|
||||||
|
if (newId && newId !== oldId) {
|
||||||
|
// 重置状态
|
||||||
|
loading.value = true
|
||||||
|
product.value = null
|
||||||
|
currentImage.value = ''
|
||||||
|
quantity.value = 1
|
||||||
|
isFavorite.value = false
|
||||||
|
activeTab.value = 'description'
|
||||||
|
reviewPage.value = 1
|
||||||
|
|
||||||
|
// 加载新商品数据
|
||||||
|
loadProductDetail().then(() => {
|
||||||
|
checkFavoriteStatus()
|
||||||
|
})
|
||||||
|
loadRecommendedProducts()
|
||||||
|
loadCartCount()
|
||||||
|
loadReviews()
|
||||||
|
|
||||||
|
// 滚动到顶部
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 页面挂载时加载数据
|
// 页面挂载时加载数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadProductDetail().then(() => {
|
loadProductDetail().then(() => {
|
||||||
@ -731,10 +759,36 @@ export default {
|
|||||||
if (saved) {
|
if (saved) {
|
||||||
const pos = JSON.parse(saved)
|
const pos = JSON.parse(saved)
|
||||||
if (pos && typeof pos.x === 'number' && typeof pos.y === 'number') {
|
if (pos && typeof pos.x === 'number' && typeof pos.y === 'number') {
|
||||||
fabPos.value = pos
|
fabPos.value = clampPos(pos.x, pos.y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
|
// 监听窗口大小变化
|
||||||
|
const handleResize = () => {
|
||||||
|
// 如果悬浮球在右侧,保持相对于右边的距离
|
||||||
|
const rightDistance = window.innerWidth - fabPos.value.x - 56
|
||||||
|
const bottomDistance = window.innerHeight - fabPos.value.y - 56
|
||||||
|
|
||||||
|
if (rightDistance < 100) {
|
||||||
|
// 在右侧,保持相对右边的距离
|
||||||
|
fabPos.value.x = window.innerWidth - rightDistance - 56
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bottomDistance < 100) {
|
||||||
|
// 在底部,保持相对底部的距离
|
||||||
|
fabPos.value.y = window.innerHeight - bottomDistance - 56
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后确保不超出边界
|
||||||
|
fabPos.value = clampPos(fabPos.value.x, fabPos.value.y)
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -853,6 +907,16 @@ export default {
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cart-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-badge {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.cart-container {
|
.cart-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@ -940,7 +1004,7 @@ export default {
|
|||||||
|
|
||||||
.main-image {
|
.main-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 480px;
|
height: 490px;
|
||||||
border: 1px solid #e0e0e0;
|
border: 1px solid #e0e0e0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -1460,7 +1524,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-fab {
|
.chat-fab {
|
||||||
position: fixed;
|
position: fixed !important;
|
||||||
width: 56px;
|
width: 56px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@ -1471,9 +1535,11 @@ export default {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: white;
|
color: white;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
z-index: 2100;
|
z-index: 9999;
|
||||||
transition: box-shadow 0.2s ease, transform 0.1s ease-out;
|
transition: box-shadow 0.2s ease, transform 0.1s ease-out;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
will-change: left, top;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-fab:active {
|
.chat-fab:active {
|
||||||
@ -1545,16 +1611,23 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cart-badge :deep(.el-badge__content) {
|
.cart-badge :deep(.el-badge__content) {
|
||||||
top: -6px;
|
position: absolute;
|
||||||
right: -6px;
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
transform: translate(25%, -25%);
|
||||||
background: #ff4757;
|
background: #ff4757;
|
||||||
border: 2px solid white;
|
border: 2px solid white;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
min-width: 16px;
|
min-width: 18px;
|
||||||
height: 16px;
|
height: 18px;
|
||||||
line-height: 12px;
|
line-height: 14px;
|
||||||
border-radius: 8px;
|
padding: 0 4px;
|
||||||
box-shadow: 0 1px 4px rgba(255, 71, 87, 0.3);
|
border-radius: 9px;
|
||||||
|
box-shadow: 0 2px 4px rgba(255, 71, 87, 0.4);
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -220,26 +220,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="logistics-info">
|
<!-- 物流轨迹组件 -->
|
||||||
<div class="logistics-row">
|
<ExpressTracking :order-id="order.id" :show-map="true" />
|
||||||
<span class="label">物流公司:</span>
|
|
||||||
<span class="value">{{ order.shipCompany }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="logistics-row" v-if="order.shipNo">
|
|
||||||
<span class="label">运单号:</span>
|
|
||||||
<span class="value">
|
|
||||||
{{ order.shipNo }}
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
text
|
|
||||||
size="small"
|
|
||||||
@click="copyTrackingNo"
|
|
||||||
>
|
|
||||||
复制
|
|
||||||
</el-button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -266,6 +248,7 @@ import {
|
|||||||
import { userStore } from '../../store/user'
|
import { userStore } from '../../store/user'
|
||||||
import request from '../../utils/request'
|
import request from '../../utils/request'
|
||||||
import orderApi from '../../api/order'
|
import orderApi from '../../api/order'
|
||||||
|
import ExpressTracking from '../../components/ExpressTracking.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'OrderDetail',
|
name: 'OrderDetail',
|
||||||
@ -280,7 +263,8 @@ export default {
|
|||||||
Close,
|
Close,
|
||||||
CircleCheck,
|
CircleCheck,
|
||||||
RefreshRight
|
RefreshRight
|
||||||
},
|
,
|
||||||
|
ExpressTracking},
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|||||||
477
tmp/ExpressMapTest.java
Normal file
477
tmp/ExpressMapTest.java
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阿里云物流轨迹地图API测试
|
||||||
|
* 快递单号:434815453222560(韵达快递)
|
||||||
|
*/
|
||||||
|
public class ExpressMapTest {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
String host = "https://alicloudmarket8002.kdniao.com";
|
||||||
|
String path = "/api/track/8002";
|
||||||
|
String appcode = "e7b5746ef8854cf2a6da24342ab53af6";
|
||||||
|
|
||||||
|
String body = "{\"CustomInfo\":\"0000\",\"LogisticCode\":\"434815453222560\"}";
|
||||||
|
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println("阿里云物流轨迹地图API测试");
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println("请求地址: " + host + path);
|
||||||
|
System.out.println("快递公司: 韵达快递");
|
||||||
|
System.out.println("快递单号: 434815453222560");
|
||||||
|
System.out.println("----------------------------------------");
|
||||||
|
|
||||||
|
try {
|
||||||
|
URL url = new URL(host + path);
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
|
||||||
|
connection.setRequestMethod("POST");
|
||||||
|
connection.setRequestProperty("Authorization", "APPCODE " + appcode);
|
||||||
|
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
connection.setDoOutput(true);
|
||||||
|
|
||||||
|
try (OutputStream os = connection.getOutputStream()) {
|
||||||
|
byte[] input = body.getBytes(StandardCharsets.UTF_8);
|
||||||
|
os.write(input, 0, input.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
int httpCode = connection.getResponseCode();
|
||||||
|
System.out.println("HTTP状态码: " + httpCode);
|
||||||
|
System.out.println("----------------------------------------");
|
||||||
|
|
||||||
|
BufferedReader br;
|
||||||
|
if (httpCode == 200) {
|
||||||
|
br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
|
||||||
|
} else {
|
||||||
|
br = new BufferedReader(new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
response.append(line);
|
||||||
|
}
|
||||||
|
br.close();
|
||||||
|
|
||||||
|
if (httpCode == 200) {
|
||||||
|
System.out.println("✅ API调用成功!");
|
||||||
|
System.out.println("返回结果:");
|
||||||
|
System.out.println(response.toString());
|
||||||
|
|
||||||
|
java.io.FileWriter fileWriter = new java.io.FileWriter("map_result.json");
|
||||||
|
fileWriter.write(response.toString());
|
||||||
|
fileWriter.close();
|
||||||
|
System.out.println("\n✅ 结果已保存到: map_result.json");
|
||||||
|
|
||||||
|
// 获取驾车路线数据
|
||||||
|
String routeData = getDrivingRoute();
|
||||||
|
createMapHtml(response.toString(), routeData);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
System.out.println("❌ API调用失败!");
|
||||||
|
System.out.println("错误信息: " + response.toString());
|
||||||
|
}
|
||||||
|
System.out.println("========================================");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("❌ 发生异常");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用高德Web服务API获取驾车路线
|
||||||
|
*/
|
||||||
|
private static String getDrivingRoute() {
|
||||||
|
try {
|
||||||
|
String key = "a0f9b8eecd097889d3aa21f4bd6665ff"; // Web服务API Key
|
||||||
|
String origin = "113.264385,23.129163"; // 广州
|
||||||
|
String destination = "114.057868,22.543099"; // 深圳
|
||||||
|
|
||||||
|
String urlStr = "https://restapi.amap.com/v3/direction/driving?origin=" + origin +
|
||||||
|
"&destination=" + destination +
|
||||||
|
"&key=" + key +
|
||||||
|
"&output=json";
|
||||||
|
|
||||||
|
System.out.println("\n🚗 调用高德驾车路线规划API...");
|
||||||
|
System.out.println("起点: 广州市");
|
||||||
|
System.out.println("终点: 深圳市");
|
||||||
|
|
||||||
|
URL url = new URL(urlStr);
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
|
||||||
|
int httpCode = connection.getResponseCode();
|
||||||
|
|
||||||
|
if (httpCode == 200) {
|
||||||
|
BufferedReader br = new BufferedReader(
|
||||||
|
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
response.append(line);
|
||||||
|
}
|
||||||
|
br.close();
|
||||||
|
|
||||||
|
System.out.println("✅ 驾车路线获取成功");
|
||||||
|
return response.toString();
|
||||||
|
} else {
|
||||||
|
System.out.println("❌ 驾车路线获取失败,状态码: " + httpCode);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("❌ 获取驾车路线异常: " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建HTML地图页面(使用Web服务API的路线数据)
|
||||||
|
*/
|
||||||
|
private static void createMapHtml(String jsonData, String routeData) {
|
||||||
|
try {
|
||||||
|
String routeJson = routeData != null ? routeData : "null";
|
||||||
|
|
||||||
|
String html = "<!DOCTYPE html>\n" +
|
||||||
|
"<html lang=\"zh-CN\">\n" +
|
||||||
|
"<head>\n" +
|
||||||
|
" <meta charset=\"UTF-8\">\n" +
|
||||||
|
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n" +
|
||||||
|
" <title>物流轨迹地图 - 韵达快递</title>\n" +
|
||||||
|
" <script>\n" +
|
||||||
|
" window._AMapSecurityConfig = {\n" +
|
||||||
|
" securityJsCode: '1d765513173e511dd6e1c963f1b38c5e'\n" +
|
||||||
|
" };\n" +
|
||||||
|
" </script>\n" +
|
||||||
|
" <script src=\"https://webapi.amap.com/maps?v=2.0&key=47c7546d2ac2a12ff7de8caf5b62c753\"></script>\n" +
|
||||||
|
" <style>\n" +
|
||||||
|
" * { margin: 0; padding: 0; box-sizing: border-box; }\n" +
|
||||||
|
" body { font-family: 'Arial', 'Microsoft YaHei', sans-serif; background: #f5f5f5; }\n" +
|
||||||
|
" .container { max-width: 1600px; margin: 0 auto; padding: 20px; }\n" +
|
||||||
|
" .header {\n" +
|
||||||
|
" background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n" +
|
||||||
|
" color: white;\n" +
|
||||||
|
" padding: 30px;\n" +
|
||||||
|
" border-radius: 10px;\n" +
|
||||||
|
" margin-bottom: 20px;\n" +
|
||||||
|
" box-shadow: 0 4px 6px rgba(0,0,0,0.1);\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .header h1 { font-size: 28px; margin-bottom: 10px; }\n" +
|
||||||
|
" .header p { opacity: 0.9; }\n" +
|
||||||
|
" .express-info {\n" +
|
||||||
|
" background: white;\n" +
|
||||||
|
" padding: 20px;\n" +
|
||||||
|
" border-radius: 10px;\n" +
|
||||||
|
" margin-bottom: 20px;\n" +
|
||||||
|
" box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .info-item {\n" +
|
||||||
|
" display: inline-block;\n" +
|
||||||
|
" margin-right: 30px;\n" +
|
||||||
|
" padding: 10px 0;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .info-label {\n" +
|
||||||
|
" color: #666;\n" +
|
||||||
|
" font-size: 14px;\n" +
|
||||||
|
" margin-right: 10px;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .info-value {\n" +
|
||||||
|
" color: #333;\n" +
|
||||||
|
" font-weight: bold;\n" +
|
||||||
|
" font-size: 16px;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .main-content {\n" +
|
||||||
|
" display: grid;\n" +
|
||||||
|
" grid-template-columns: 1fr 400px;\n" +
|
||||||
|
" gap: 20px;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .map-container {\n" +
|
||||||
|
" background: white;\n" +
|
||||||
|
" border-radius: 10px;\n" +
|
||||||
|
" overflow: hidden;\n" +
|
||||||
|
" box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n" +
|
||||||
|
" }\n" +
|
||||||
|
" #map {\n" +
|
||||||
|
" width: 100%;\n" +
|
||||||
|
" height: 600px;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .timeline-container {\n" +
|
||||||
|
" background: white;\n" +
|
||||||
|
" border-radius: 10px;\n" +
|
||||||
|
" padding: 20px;\n" +
|
||||||
|
" box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n" +
|
||||||
|
" max-height: 600px;\n" +
|
||||||
|
" overflow-y: auto;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .timeline-title {\n" +
|
||||||
|
" font-size: 18px;\n" +
|
||||||
|
" font-weight: bold;\n" +
|
||||||
|
" color: #333;\n" +
|
||||||
|
" margin-bottom: 20px;\n" +
|
||||||
|
" padding-bottom: 10px;\n" +
|
||||||
|
" border-bottom: 2px solid #667eea;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .timeline-item {\n" +
|
||||||
|
" position: relative;\n" +
|
||||||
|
" padding-left: 30px;\n" +
|
||||||
|
" padding-bottom: 20px;\n" +
|
||||||
|
" border-left: 2px solid #e0e0e0;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .timeline-item:last-child {\n" +
|
||||||
|
" border-left: none;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .timeline-dot {\n" +
|
||||||
|
" position: absolute;\n" +
|
||||||
|
" left: -6px;\n" +
|
||||||
|
" top: 0;\n" +
|
||||||
|
" width: 12px;\n" +
|
||||||
|
" height: 12px;\n" +
|
||||||
|
" border-radius: 50%;\n" +
|
||||||
|
" background: #667eea;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .timeline-item:first-child .timeline-dot {\n" +
|
||||||
|
" background: #28a745;\n" +
|
||||||
|
" width: 16px;\n" +
|
||||||
|
" height: 16px;\n" +
|
||||||
|
" left: -8px;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .timeline-time {\n" +
|
||||||
|
" color: #999;\n" +
|
||||||
|
" font-size: 12px;\n" +
|
||||||
|
" margin-bottom: 5px;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .timeline-location {\n" +
|
||||||
|
" color: #667eea;\n" +
|
||||||
|
" font-weight: bold;\n" +
|
||||||
|
" font-size: 14px;\n" +
|
||||||
|
" margin-bottom: 5px;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .timeline-desc {\n" +
|
||||||
|
" color: #333;\n" +
|
||||||
|
" font-size: 14px;\n" +
|
||||||
|
" line-height: 1.6;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .status-badge {\n" +
|
||||||
|
" display: inline-block;\n" +
|
||||||
|
" padding: 5px 15px;\n" +
|
||||||
|
" background: #28a745;\n" +
|
||||||
|
" color: white;\n" +
|
||||||
|
" border-radius: 20px;\n" +
|
||||||
|
" font-size: 14px;\n" +
|
||||||
|
" margin-top: 10px;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" .debug-info {\n" +
|
||||||
|
" background: #e7f3ff;\n" +
|
||||||
|
" border: 1px solid #b3d9ff;\n" +
|
||||||
|
" border-radius: 5px;\n" +
|
||||||
|
" padding: 10px;\n" +
|
||||||
|
" margin-top: 10px;\n" +
|
||||||
|
" font-size: 12px;\n" +
|
||||||
|
" color: #0066cc;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" @media (max-width: 1200px) {\n" +
|
||||||
|
" .main-content {\n" +
|
||||||
|
" grid-template-columns: 1fr;\n" +
|
||||||
|
" }\n" +
|
||||||
|
" }\n" +
|
||||||
|
" </style>\n" +
|
||||||
|
"</head>\n" +
|
||||||
|
"<body>\n" +
|
||||||
|
" <div class=\"container\">\n" +
|
||||||
|
" <div class=\"header\">\n" +
|
||||||
|
" <h1>🗺️ 物流轨迹地图</h1>\n" +
|
||||||
|
" <p>实时追踪快递位置 - 真实驾车路线展示</p>\n" +
|
||||||
|
" <span class=\"status-badge\">✓ 在途中</span>\n" +
|
||||||
|
" </div>\n" +
|
||||||
|
" <div class=\"express-info\">\n" +
|
||||||
|
" <div class=\"info-item\">\n" +
|
||||||
|
" <span class=\"info-label\">快递公司:</span>\n" +
|
||||||
|
" <span class=\"info-value\">韵达快递</span>\n" +
|
||||||
|
" </div>\n" +
|
||||||
|
" <div class=\"info-item\">\n" +
|
||||||
|
" <span class=\"info-label\">快递单号:</span>\n" +
|
||||||
|
" <span class=\"info-value\">434815453222560</span>\n" +
|
||||||
|
" </div>\n" +
|
||||||
|
" <div class=\"info-item\">\n" +
|
||||||
|
" <span class=\"info-label\">当前位置:</span>\n" +
|
||||||
|
" <span class=\"info-value\" id=\"currentLocation\">加载中...</span>\n" +
|
||||||
|
" </div>\n" +
|
||||||
|
" <div class=\"debug-info\" id=\"debugInfo\">🚗 使用Web服务API获取真实路线...</div>\n" +
|
||||||
|
" </div>\n" +
|
||||||
|
" <div class=\"main-content\">\n" +
|
||||||
|
" <div class=\"map-container\">\n" +
|
||||||
|
" <div id=\"map\"></div>\n" +
|
||||||
|
" </div>\n" +
|
||||||
|
" <div class=\"timeline-container\">\n" +
|
||||||
|
" <div class=\"timeline-title\">📦 物流轨迹</div>\n" +
|
||||||
|
" <div id=\"timeline\"></div>\n" +
|
||||||
|
" </div>\n" +
|
||||||
|
" </div>\n" +
|
||||||
|
" </div>\n" +
|
||||||
|
" <script>\n" +
|
||||||
|
" const apiData = " + jsonData + ";\n" +
|
||||||
|
" const routeData = " + routeJson + ";\n" +
|
||||||
|
" \n" +
|
||||||
|
" console.log('🔍 物流数据:', apiData);\n" +
|
||||||
|
" console.log('🚗 路线数据:', routeData);\n" +
|
||||||
|
" \n" +
|
||||||
|
" // 预设主要城市坐标\n" +
|
||||||
|
" const cityCoords = {\n" +
|
||||||
|
" '广州市': [113.264385, 23.129163],\n" +
|
||||||
|
" '深圳市': [114.057868, 22.543099]\n" +
|
||||||
|
" };\n" +
|
||||||
|
" \n" +
|
||||||
|
" // 初始化地图\n" +
|
||||||
|
" const map = new AMap.Map('map', {\n" +
|
||||||
|
" zoom: 9,\n" +
|
||||||
|
" center: [113.664385, 23.129163],\n" +
|
||||||
|
" mapStyle: 'amap://styles/normal'\n" +
|
||||||
|
" });\n" +
|
||||||
|
" \n" +
|
||||||
|
" console.log('✅ 地图初始化完成');\n" +
|
||||||
|
" \n" +
|
||||||
|
" document.getElementById('currentLocation').textContent = apiData.Location || '未知';\n" +
|
||||||
|
" \n" +
|
||||||
|
" const traces = apiData.Traces || [];\n" +
|
||||||
|
" const timelineHTML = traces.map((trace, index) => `\n" +
|
||||||
|
" <div class=\"timeline-item\">\n" +
|
||||||
|
" <div class=\"timeline-dot\"></div>\n" +
|
||||||
|
" <div class=\"timeline-time\">${trace.AcceptTime || ''}</div>\n" +
|
||||||
|
" <div class=\"timeline-location\">${trace.Location || ''}</div>\n" +
|
||||||
|
" <div class=\"timeline-desc\">${trace.AcceptStation || ''}</div>\n" +
|
||||||
|
" </div>\n" +
|
||||||
|
" `).join('');\n" +
|
||||||
|
" document.getElementById('timeline').innerHTML = timelineHTML;\n" +
|
||||||
|
" \n" +
|
||||||
|
" const cities = [...new Set(traces.map(t => t.Location).filter(l => l))];\n" +
|
||||||
|
" console.log('🏙️ 城市列表:', cities);\n" +
|
||||||
|
" \n" +
|
||||||
|
" let debugMsg = `找到 ${cities.length} 个城市: ${cities.join(' → ')}`;\n" +
|
||||||
|
" \n" +
|
||||||
|
" // 添加城市标记\n" +
|
||||||
|
" cities.forEach((city, index) => {\n" +
|
||||||
|
" const coords = cityCoords[city];\n" +
|
||||||
|
" if (coords) {\n" +
|
||||||
|
" const marker = new AMap.CircleMarker({\n" +
|
||||||
|
" center: coords,\n" +
|
||||||
|
" radius: 15,\n" +
|
||||||
|
" strokeColor: '#667eea',\n" +
|
||||||
|
" strokeWeight: 3,\n" +
|
||||||
|
" fillColor: index === 0 ? '#28a745' : '#667eea',\n" +
|
||||||
|
" fillOpacity: 0.9,\n" +
|
||||||
|
" zIndex: 100\n" +
|
||||||
|
" });\n" +
|
||||||
|
" \n" +
|
||||||
|
" const text = new AMap.Text({\n" +
|
||||||
|
" text: `${index + 1}. ${city}`,\n" +
|
||||||
|
" position: coords,\n" +
|
||||||
|
" offset: new AMap.Pixel(0, -30),\n" +
|
||||||
|
" style: {\n" +
|
||||||
|
" 'background': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',\n" +
|
||||||
|
" 'border': 'none',\n" +
|
||||||
|
" 'color': 'white',\n" +
|
||||||
|
" 'font-size': '14px',\n" +
|
||||||
|
" 'font-weight': 'bold',\n" +
|
||||||
|
" 'padding': '8px 12px',\n" +
|
||||||
|
" 'border-radius': '8px',\n" +
|
||||||
|
" 'box-shadow': '0 4px 8px rgba(102,126,234,0.4)'\n" +
|
||||||
|
" }\n" +
|
||||||
|
" });\n" +
|
||||||
|
" \n" +
|
||||||
|
" map.add([marker, text]);\n" +
|
||||||
|
" }\n" +
|
||||||
|
" });\n" +
|
||||||
|
" \n" +
|
||||||
|
" // 绘制路线\n" +
|
||||||
|
" if (routeData && routeData.status === '1' && routeData.route && routeData.route.paths) {\n" +
|
||||||
|
" console.log('✅ 使用Web服务API路线数据');\n" +
|
||||||
|
" \n" +
|
||||||
|
" const path = routeData.route.paths[0];\n" +
|
||||||
|
" const steps = path.steps;\n" +
|
||||||
|
" const polylinePath = [];\n" +
|
||||||
|
" \n" +
|
||||||
|
" // 提取所有路径点\n" +
|
||||||
|
" steps.forEach(step => {\n" +
|
||||||
|
" const points = step.polyline.split(';');\n" +
|
||||||
|
" points.forEach(point => {\n" +
|
||||||
|
" const coords = point.split(',');\n" +
|
||||||
|
" if (coords.length === 2) {\n" +
|
||||||
|
" polylinePath.push([parseFloat(coords[0]), parseFloat(coords[1])]);\n" +
|
||||||
|
" }\n" +
|
||||||
|
" });\n" +
|
||||||
|
" });\n" +
|
||||||
|
" \n" +
|
||||||
|
" // 绘制路径\n" +
|
||||||
|
" const polyline = new AMap.Polyline({\n" +
|
||||||
|
" path: polylinePath,\n" +
|
||||||
|
" strokeColor: '#667eea',\n" +
|
||||||
|
" strokeWeight: 8,\n" +
|
||||||
|
" strokeOpacity: 0.9,\n" +
|
||||||
|
" strokeStyle: 'solid',\n" +
|
||||||
|
" lineJoin: 'round',\n" +
|
||||||
|
" lineCap: 'round',\n" +
|
||||||
|
" zIndex: 50,\n" +
|
||||||
|
" showDir: true\n" +
|
||||||
|
" });\n" +
|
||||||
|
" map.add(polyline);\n" +
|
||||||
|
" \n" +
|
||||||
|
" const distance = (path.distance / 1000).toFixed(1);\n" +
|
||||||
|
" const duration = Math.round(path.duration / 60);\n" +
|
||||||
|
" \n" +
|
||||||
|
" debugMsg += `\\n✓ 已绘制真实驾车路线`;\n" +
|
||||||
|
" debugMsg += `\\n预计距离: ${distance} 公里`;\n" +
|
||||||
|
" debugMsg += `\\n预计时间: ${duration} 分钟`;\n" +
|
||||||
|
" \n" +
|
||||||
|
" map.setFitView();\n" +
|
||||||
|
" } else {\n" +
|
||||||
|
" console.warn('⚠️ 使用直线连接');\n" +
|
||||||
|
" \n" +
|
||||||
|
" const waypoints = cities.map(c => cityCoords[c]).filter(c => c);\n" +
|
||||||
|
" const polyline = new AMap.Polyline({\n" +
|
||||||
|
" path: waypoints,\n" +
|
||||||
|
" strokeColor: '#667eea',\n" +
|
||||||
|
" strokeWeight: 8,\n" +
|
||||||
|
" strokeOpacity: 0.8,\n" +
|
||||||
|
" strokeStyle: 'solid',\n" +
|
||||||
|
" lineJoin: 'round',\n" +
|
||||||
|
" showDir: true\n" +
|
||||||
|
" });\n" +
|
||||||
|
" map.add(polyline);\n" +
|
||||||
|
" \n" +
|
||||||
|
" debugMsg += '\\n⚠️ 使用直线连接(Web服务API未返回路线数据)';\n" +
|
||||||
|
" map.setFitView();\n" +
|
||||||
|
" }\n" +
|
||||||
|
" \n" +
|
||||||
|
" document.getElementById('debugInfo').textContent = debugMsg;\n" +
|
||||||
|
" console.log('🎉 地图加载完成');\n" +
|
||||||
|
" </script>\n" +
|
||||||
|
"</body>\n" +
|
||||||
|
"</html>";
|
||||||
|
|
||||||
|
java.io.FileWriter fileWriter = new java.io.FileWriter("map_view.html");
|
||||||
|
fileWriter.write(html);
|
||||||
|
fileWriter.close();
|
||||||
|
|
||||||
|
String absPath = new File("map_view.html").getAbsolutePath();
|
||||||
|
System.out.println("\n✅ HTML地图页面已创建: map_view.html");
|
||||||
|
System.out.println("\n📌 请在浏览器中打开以下文件查看物流地图:");
|
||||||
|
System.out.println(" file://" + absPath);
|
||||||
|
System.out.println("\n🔑 改进内容:");
|
||||||
|
System.out.println(" ✓ 使用Web服务API获取真实驾车路线");
|
||||||
|
System.out.println(" ✓ 在Java端调用路径规划API");
|
||||||
|
System.out.println(" ✓ 避免JS API的安全密钥问题");
|
||||||
|
System.out.println(" ✓ 显示预计距离和时间");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("❌ 创建HTML失败: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
tmp/ExpressQueryTest.java
Normal file
106
tmp/ExpressQueryTest.java
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阿里云物流API测试
|
||||||
|
* 测试快递单号:434815453222560(韵达快递)
|
||||||
|
*/
|
||||||
|
public class ExpressQueryTest {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
String host = "https://wuliu.market.alicloudapi.com";
|
||||||
|
String path = "/kdi";
|
||||||
|
String appcode = "e7b5746ef8854cf2a6da24342ab53af6";
|
||||||
|
String no = "434815453222560"; // 韵达快递单号
|
||||||
|
String type = "yunda"; // 韵达快递代码
|
||||||
|
|
||||||
|
String urlSend = host + path + "?no=" + no + "&type=" + type;
|
||||||
|
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println("阿里云物流API测试");
|
||||||
|
System.out.println("========================================");
|
||||||
|
System.out.println("请求地址: " + urlSend);
|
||||||
|
System.out.println("快递公司: 韵达快递");
|
||||||
|
System.out.println("快递单号: " + no);
|
||||||
|
System.out.println("----------------------------------------");
|
||||||
|
|
||||||
|
try {
|
||||||
|
URL url = new URL(urlSend);
|
||||||
|
HttpURLConnection httpURLCon = (HttpURLConnection) url.openConnection();
|
||||||
|
httpURLCon.setRequestProperty("Authorization", "APPCODE " + appcode);
|
||||||
|
|
||||||
|
int httpCode = httpURLCon.getResponseCode();
|
||||||
|
System.out.println("HTTP状态码: " + httpCode);
|
||||||
|
System.out.println("----------------------------------------");
|
||||||
|
|
||||||
|
if (httpCode == 200) {
|
||||||
|
String json = read(httpURLCon.getInputStream());
|
||||||
|
System.out.println("✅ API调用成功!");
|
||||||
|
System.out.println("返回结果:");
|
||||||
|
System.out.println(json);
|
||||||
|
System.out.println("========================================");
|
||||||
|
} else {
|
||||||
|
Map<String, List<String>> map = httpURLCon.getHeaderFields();
|
||||||
|
List<String> errorList = map.get("X-Ca-Error-Message");
|
||||||
|
String error = errorList != null && !errorList.isEmpty() ? errorList.get(0) : "未知错误";
|
||||||
|
|
||||||
|
System.out.println("❌ API调用失败!");
|
||||||
|
System.out.println("错误信息: " + error);
|
||||||
|
|
||||||
|
if (httpCode == 400 && error.equals("Invalid AppCode `not exists`")) {
|
||||||
|
System.out.println("原因: AppCode错误");
|
||||||
|
} else if (httpCode == 400 && error.equals("Invalid Url")) {
|
||||||
|
System.out.println("原因: 请求的 Method、Path 或者环境错误");
|
||||||
|
} else if (httpCode == 400 && error.equals("Invalid Param Location")) {
|
||||||
|
System.out.println("原因: 参数错误");
|
||||||
|
} else if (httpCode == 403 && error.equals("Unauthorized")) {
|
||||||
|
System.out.println("原因: 服务未被授权(或URL和Path不正确)");
|
||||||
|
} else if (httpCode == 403 && error.equals("Quota Exhausted")) {
|
||||||
|
System.out.println("原因: 套餐包次数用完");
|
||||||
|
} else if (httpCode == 403 && error.equals("Api Market Subscription quota exhausted")) {
|
||||||
|
System.out.println("原因: 套餐包次数用完,请续购套餐");
|
||||||
|
} else {
|
||||||
|
System.out.println("原因: 参数名错误或其他错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印所有响应头帮助调试
|
||||||
|
System.out.println("\n所有响应头:");
|
||||||
|
for (Map.Entry<String, List<String>> entry : map.entrySet()) {
|
||||||
|
System.out.println(entry.getKey() + ": " + entry.getValue());
|
||||||
|
}
|
||||||
|
System.out.println("========================================");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
System.out.println("❌ URL格式错误");
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
System.out.println("❌ URL地址错误");
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("❌ 发生异常");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取返回结果
|
||||||
|
*/
|
||||||
|
private static String read(InputStream is) throws IOException {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
BufferedReader br = new BufferedReader(new InputStreamReader(is, "utf-8"));
|
||||||
|
String line = null;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
sb.append(line);
|
||||||
|
}
|
||||||
|
br.close();
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
1
tmp/map_result.json
Normal file
1
tmp/map_result.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"Location":"深圳市","LogisticCode":"434815453222560","ShipperCode":"YD","State":"2","StateEx":"2","Success":true,"Traces":[{"Action":"1","AcceptStation":"【广州市】广东广州增城市增江街公司-罗志焰(13544414635) 已揽收","AcceptTime":"2025-10-07 16:13:31","Location":"广州市"},{"Action":"2","AcceptStation":"【广州市】已离开 广东广州增城市增江街公司;发往 广东广州分拨交付中心","AcceptTime":"2025-10-08 00:55:37","Location":"广州市"},{"Action":"2","AcceptStation":"【广州市】已到达 广东广州分拨交付中心(如遇物流问题,请联系专属客服电话:020-22145829,为您解决)","AcceptTime":"2025-10-08 01:20:43","Location":"广州市"},{"Action":"2","AcceptStation":"【广州市】已离开 广东广州分拨交付中心;发往 广东深圳分拨交付中心。(如遇物流问题,请联系专属客服电话:0755-61886188,为您解决)","AcceptTime":"2025-10-08 02:05:05","Location":"广州市"},{"Action":"2","AcceptStation":"【深圳市】已到达 广东深圳分拨交付中心(如遇物流问题,请联系专属客服电话:0755-61886188,为您解决)","AcceptTime":"2025-10-08 05:01:37","Location":"深圳市"},{"Action":"2","AcceptStation":"【深圳市】已离开 广东深圳分拨交付中心;发往 广东深圳公司南山区西丽分部。(如遇物流问题,请联系专属客服电话:0755-36368534,为您解决)","AcceptTime":"2025-10-08 06:09:59","Location":"深圳市"}]}
|
||||||
315
tmp/map_view.html
Normal file
315
tmp/map_view.html
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user