Просмотр исходного кода

feat: 小程序订单详情页展示退款申请记录时间线

- getUserOrderDetail 返回 refunds 列表,包含每次申请的
  状态/金额/原因/备注/退款商品明细
- 订单详情页新增退款记录时间线组件(圆点+竖线)
- 颜色语义:绿色=已退款,红色=已拒绝,橙色=审核中
- 拒绝记录显示拒绝原因,方便用户了解后重新提交

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 2 недель назад
Родитель
Сommit
05ab22f62e

+ 22 - 0
haha-mp/src/api/order.ts

@@ -53,6 +53,28 @@ export interface OrderInfo {
   videoUrl?: string;
   confidence?: number;
   products: OrderProduct[];
+  refunds?: RefundRecord[]; // 退款记录列表
+}
+
+/** 退款记录 */
+export interface RefundRecord {
+  id: string;
+  refundNo: string;
+  refundAmount: number;
+  status: string; // PENDING / REFUNDED / REJECTED
+  reason: string;
+  remark?: string; // 审核备注(拒绝原因)
+  createTime: string;
+  reviewTime?: string;
+  items?: RefundRecordItem[];
+}
+
+/** 退款商品明细 */
+export interface RefundRecordItem {
+  productName: string;
+  quantity: number;
+  price: number;
+  refundAmount: number;
 }
 
 /**

+ 162 - 12
haha-mp/src/pages/orderDetail/orderDetail.vue

@@ -53,18 +53,35 @@
           </view>
         </view>
 
-        <!-- 退款状态提示 -->
-        <view v-if="order.refundStatus === 'PENDING'" class="refund-notice pending">
-          <view class="notice-dot"></view>
-          <text class="notice-text">退款申请已提交,正在审核中</text>
-        </view>
-        <view v-else-if="order.refundAmount > 0 && order.refundAmount < (order.paidAmount || order.totalAmount || 0)" class="refund-notice partial">
-          <view class="notice-dot"></view>
-          <text class="notice-text">已退款 <text class="notice-amount">¥{{ order.refundAmount.toFixed(2) }}</text>,可继续申请退款</text>
-        </view>
-        <view v-else-if="order.refundAmount > 0 && order.refundAmount >= (order.paidAmount || order.totalAmount || 0)" class="refund-notice full">
-          <view class="notice-dot"></view>
-          <text class="notice-text">已全额退款</text>
+        <!-- 退款历史记录 -->
+        <view v-if="order.refunds && order.refunds.length > 0" class="refund-history">
+          <view class="history-title">退款记录</view>
+          <view v-for="(record, index) in order.refunds" :key="index" class="history-item">
+            <view class="history-timeline">
+              <view :class="['timeline-dot', record.status === 'REFUNDED' ? 'success' : record.status === 'REJECTED' ? 'rejected' : 'pending']"></view>
+              <view v-if="index < order.refunds.length - 1" class="timeline-line"></view>
+            </view>
+            <view class="history-content">
+              <view class="history-header">
+                <text :class="['history-status', record.status === 'REFUNDED' ? 'success' : record.status === 'REJECTED' ? 'rejected' : 'pending']">
+                  {{ record.status === 'REFUNDED' ? '已退款' : record.status === 'REJECTED' ? '已拒绝' : '审核中' }}
+                </text>
+                <text class="history-amount">¥{{ (record.refundAmount || 0).toFixed(2) }}</text>
+              </view>
+              <view class="history-meta">
+                <text class="history-time">{{ record.createTime }}</text>
+              </view>
+              <text v-if="record.reason" class="history-reason">原因:{{ record.reason }}</text>
+              <text v-if="record.remark && record.status === 'REJECTED'" class="history-reject-reason">拒绝原因:{{ record.remark }}</text>
+              <!-- 退款商品 -->
+              <view v-if="record.items && record.items.length > 0" class="history-items">
+                <view v-for="(item, i) in record.items" :key="i" class="history-item-row">
+                  <text class="item-name">{{ item.productName }}</text>
+                  <text class="item-meta">×{{ item.quantity }} ¥{{ (item.refundAmount || 0).toFixed(2) }}</text>
+                </view>
+              </view>
+            </view>
+          </view>
         </view>
 
         <view class="card-footer-actions">
@@ -531,6 +548,139 @@ const viewVideo = (url: string) => {
   }
 }
 
+/* 退款历史记录 */
+.refund-history {
+  padding: 0 $spacing-lg;
+  margin-bottom: $spacing-md;
+}
+
+.history-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: $color-text-primary;
+  margin-bottom: $spacing-md;
+}
+
+.history-item {
+  display: flex;
+  gap: $spacing-md;
+
+  &:last-child .timeline-line {
+    display: none;
+  }
+}
+
+.history-timeline {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 40rpx;
+  flex-shrink: 0;
+}
+
+.timeline-dot {
+  width: 16rpx;
+  height: 16rpx;
+  border-radius: 50%;
+  margin-top: 6rpx;
+  flex-shrink: 0;
+
+  &.success { background-color: $color-success; }
+  &.rejected { background-color: $color-error; }
+  &.pending { background-color: $color-warning; }
+}
+
+.timeline-line {
+  width: 2rpx;
+  flex: 1;
+  min-height: 30rpx;
+  background-color: $color-border;
+  margin: 8rpx 0;
+}
+
+.history-content {
+  flex: 1;
+  padding-bottom: $spacing-md;
+  min-width: 0;
+}
+
+.history-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 6rpx;
+}
+
+.history-status {
+  font-size: 26rpx;
+  font-weight: 500;
+
+  &.success { color: $color-success; }
+  &.rejected { color: $color-error; }
+  &.pending { color: $color-warning; }
+}
+
+.history-amount {
+  font-size: 28rpx;
+  font-weight: 700;
+  color: $color-text-primary;
+}
+
+.history-meta {
+  margin-bottom: 4rpx;
+}
+
+.history-time {
+  font-size: 22rpx;
+  color: $color-text-tertiary;
+}
+
+.history-reason {
+  display: block;
+  font-size: 24rpx;
+  color: $color-text-secondary;
+  margin-top: 6rpx;
+  line-height: 1.5;
+}
+
+.history-reject-reason {
+  display: block;
+  font-size: 24rpx;
+  color: $color-error;
+  margin-top: 6rpx;
+  line-height: 1.5;
+}
+
+.history-items {
+  margin-top: 12rpx;
+  padding: 16rpx;
+  background-color: $color-bg-secondary;
+  border-radius: $radius-sm;
+}
+
+.history-item-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 6rpx 0;
+
+  .item-name {
+    font-size: 24rpx;
+    color: $color-text-primary;
+    flex: 1;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+
+  .item-meta {
+    font-size: 24rpx;
+    color: $color-text-secondary;
+    flex-shrink: 0;
+    margin-left: 16rpx;
+  }
+}
+
 /* 订单信息卡片 */
 .info-card {
   padding: 0;

+ 40 - 0
haha-service/src/main/java/com/haha/service/impl/OrderServiceImpl.java

@@ -820,6 +820,46 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
 
         Map<String, Object> detail = buildOrderMap(order);
         detail.put("statusText", OrderConstants.getStatusDesc(order.getStatus()));
+
+        // 加载退款申请记录
+        try {
+            if (refundService != null) {
+                List<com.haha.entity.Refund> refundList = refundService.getByOrderId(order.getId());
+                if (refundList != null && !refundList.isEmpty()) {
+                    List<Map<String, Object>> refundRecords = new ArrayList<>();
+                    for (com.haha.entity.Refund refund : refundList) {
+                        Map<String, Object> record = new HashMap<>();
+                        record.put("id", refund.getId());
+                        record.put("refundNo", refund.getRefundNo());
+                        record.put("refundAmount", refund.getRefundAmount());
+                        record.put("status", refund.getStatus());
+                        record.put("reason", refund.getReason());
+                        record.put("remark", refund.getRemark());
+                        record.put("createTime", refund.getCreateTime());
+                        record.put("reviewTime", refund.getReviewTime());
+                        // 加载退款商品明细
+                        List<com.haha.entity.RefundItem> items = refundService.getRefundItems(refund.getId());
+                        if (items != null && !items.isEmpty()) {
+                            List<Map<String, Object>> itemMaps = new ArrayList<>();
+                            for (com.haha.entity.RefundItem item : items) {
+                                Map<String, Object> im = new HashMap<>();
+                                im.put("productName", item.getProductName());
+                                im.put("quantity", item.getQuantity());
+                                im.put("price", item.getPrice());
+                                im.put("refundAmount", item.getRefundAmount());
+                                itemMaps.add(im);
+                            }
+                            record.put("items", itemMaps);
+                        }
+                        refundRecords.add(record);
+                    }
+                    detail.put("refunds", refundRecords);
+                }
+            }
+        } catch (Exception e) {
+            log.warn("加载订单 {} 退款记录失败: {}", order.getId(), e.getMessage());
+        }
+
         return detail;
     }