Browse Source

小程序商品退款页面

skyline 1 tháng trước cách đây
mục cha
commit
06902a834a
2 tập tin đã thay đổi với 590 bổ sung20 xóa
  1. 2 1
      haha-mp/src/api/order.ts
  2. 588 19
      haha-mp/src/pages/refund/refund.vue

+ 2 - 1
haha-mp/src/api/order.ts

@@ -11,7 +11,8 @@ export interface OrderProduct {
   id: string;
   name: string;
   price: number;
-  quantity: number;
+  quantity: number; // 购买数量
+  refundQuantity?: number; // 退款数量(用户选择)
   image?: string;
   subtotal?: number;
 }

+ 588 - 19
haha-mp/src/pages/refund/refund.vue

@@ -1,9 +1,69 @@
 <template>
   <view class="container">
-    <!-- 退款商品 -->
-    <view class="section-item product-selector">
-      <text class="section-label">退款商品</text>
-      <text class="arrow-right">></text>
+    <!-- 退款商品选择 -->
+    <view class="section-block product-section">
+      <text class="section-title">选择退款商品</text>
+      <view class="product-list">
+        <view 
+          v-for="(product, index) in orderInfo.products" 
+          :key="index" 
+          :class="['product-item', { 'product-item-selected': selectedProducts.includes(index) }]"
+        >
+          <!-- 商品主体信息 -->
+          <view class="product-main" @click="toggleProductSelect(index)">
+            <view class="product-checkbox">
+              <view :class="['checkbox-icon', { checked: selectedProducts.includes(index) }]">
+                <text v-if="selectedProducts.includes(index)">✓</text>
+              </view>
+            </view>
+            <view class="product-image">
+              <image v-if="product.image" :src="product.image" mode="aspectFill"></image>
+              <view v-else class="image-placeholder"></view>
+            </view>
+            <view class="product-detail">
+              <view class="name-container" @click.stop="showProductName(product.name)">
+                <text class="product-name">{{ product.name }}</text>
+                <text v-if="product.name.length > 12" class="expand-tip">...</text>
+              </view>
+              <view class="product-meta">
+                <text class="product-price">¥{{ (product.price || 0).toFixed(2) }}</text>
+                <text class="product-quantity-label">购买数量:x{{ product.quantity }}</text>
+              </view>
+            </view>
+          </view>
+          
+          <!-- 退款数量选择器(仅当商品被选中时显示,独立一行) -->
+          <view v-if="selectedProducts.includes(index)" class="refund-quantity-selector">
+            <view class="selector-row">
+              <text class="selector-label">退款数量</text>
+              <view class="quantity-control">
+                <view 
+                  :class="['control-btn', (product.refundQuantity || 1) > 1 ? '' : 'disabled']" 
+                  @click.stop="decreaseQuantity(index)"
+                >
+                  <text>-</text>
+                </view>
+                <view class="quantity-display">
+                  <text class="quantity-text">{{ product.refundQuantity || 1 }}</text>
+                </view>
+                <view 
+                  :class="['control-btn', (product.refundQuantity || 1) < product.quantity ? '' : 'disabled']" 
+                  @click.stop="increaseQuantity(index)"
+                >
+                  <text>+</text>
+                </view>
+              </view>
+            </view>
+            <view class="refund-amount-preview">
+              <text class="preview-label">退款金额</text>
+              <text class="preview-value">¥{{ ((product.price || 0) * (product.refundQuantity || 1)).toFixed(2) }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+      <view class="selected-tip">
+        已选择 {{ selectedProducts.length }} 件商品,共 {{ refundQuantity }} 件
+      </view>
     </view>
     
     <!-- 退款原因 -->
@@ -24,30 +84,80 @@
       <text class="section-title">退款信息</text>
       <view class="info-row">
         <text class="info-label">订单编号:</text>
-        <text class="info-value">2601081537467989</text>
+        <text class="info-value">{{ orderInfo.orderNo || '-' }}</text>
       </view>
       <view class="info-row">
         <text class="info-label">订单总金额:</text>
-        <text class="info-value">0.44</text>
+        <text class="info-value">¥{{ (orderInfo.totalAmount || 0).toFixed(2) }}</text>
       </view>
       <view class="info-row">
         <text class="info-label">退款数量:</text>
-        <text class="info-value">1</text>
+        <text class="info-value">{{ refundQuantity }}</text>
       </view>
       <view class="info-row">
         <text class="info-label">退款总金额:</text>
-        <text class="info-value">0.44</text>
+        <text class="info-value refund-amount">¥{{ refundAmount.toFixed(2) }}</text>
       </view>
     </view>
     
+    <!-- 提示信息 -->
+    <view class="section-block tip-section">
+      <text class="tip-text">• 请选择需要退款的商品</text>
+      <text class="tip-text">• 退款申请提交后,工作人员将尽快处理</text>
+      <text class="tip-text">• 退款金额将原路返回至您的支付账户</text>
+    </view>
+    
     <!-- 确认提交按钮 -->
     <button class="submit-btn" @click="submitRefund">确认提交</button>
   </view>
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from 'vue';
+import { ref, computed, onMounted } from 'vue';
+import { onLoad } from '@dcloudio/uni-app';
 import { checkAuth } from '../../utils/auth';
+import { getOrderDetail } from '../../api/order';
+import type { OrderInfo } from '../../api/order';
+import { post } from '../../utils/request';
+
+// 路由参数
+const orderId = ref<number>(0);
+
+// 订单信息
+const orderInfo = ref<OrderInfo>({
+  id: 0,
+  orderNo: '',
+  outTradeNo: '',
+  hahaOrderNo: '',
+  deviceId: '',
+  totalAmount: 0,
+  payStatus: '',
+  status: 0,
+  createTime: '',
+  products: []
+});
+
+// 选中的商品索引列表
+const selectedProducts = ref<number[]>([]);
+
+// 退款数量(根据选中商品的退款数量计算)
+const refundQuantity = computed(() => {
+  return selectedProducts.value.reduce((sum, index) => {
+    const product = orderInfo.value.products[index];
+    return sum + (product.refundQuantity || 1);
+  }, 0);
+});
+
+// 退款金额(根据选中商品的退款数量计算)
+const refundAmount = computed(() => {
+  return selectedProducts.value.reduce((sum, index) => {
+    const product = orderInfo.value.products[index];
+    if (product) {
+      return sum + (product.price || 0) * (product.refundQuantity || 1);
+    }
+    return sum;
+  }, 0);
+});
 
 const selectedReason = ref('');
 
@@ -63,14 +173,143 @@ const handleReasonChange = (e: any) => {
 };
 
 /**
- * 页面加载时检查登录状态
+ * 切换商品选中状态
+ */
+const toggleProductSelect = (index: number) => {
+  const selectedIndex = selectedProducts.value.indexOf(index);
+  if (selectedIndex > -1) {
+    // 取消选中:清除退款数量
+    orderInfo.value.products[index].refundQuantity = undefined;
+    selectedProducts.value.splice(selectedIndex, 1);
+  } else {
+    // 选中:初始化退款数量为 1
+    if (!orderInfo.value.products[index].refundQuantity) {
+      orderInfo.value.products[index].refundQuantity = 1;
+    }
+    selectedProducts.value.push(index);
+  }
+};
+
+/**
+ * 显示商品全称
+ */
+const showProductName = (productName: string) => {
+  uni.showModal({
+    title: '商品名称',
+    content: productName,
+    showCancel: false,
+    confirmText: '知道了'
+  });
+};
+
+/**
+ * 减少退款数量
+ */
+const decreaseQuantity = (index: number) => {
+  const product = orderInfo.value.products[index];
+  if (product.refundQuantity && product.refundQuantity > 1) {
+    product.refundQuantity--;
+  }
+};
+
+/**
+ * 增加退款数量
+ */
+const increaseQuantity = (index: number) => {
+  const product = orderInfo.value.products[index];
+  if ((product.refundQuantity || 1) < product.quantity) {
+    if (!product.refundQuantity) {
+      product.refundQuantity = 2;
+    } else {
+      product.refundQuantity++;
+    }
+  }
+};
+
+/**
+ * 显示商品详情
+ */
+const showProductDetail = () => {
+  // TODO: 跳转到商品详情页或弹出商品详情弹窗
+  uni.showToast({
+    title: '商品详情功能开发中',
+    icon: 'none'
+  });
+};
+
+/**
+ * 加载订单详情
+ */
+const loadOrderDetail = async () => {
+  if (!orderId.value) {
+    uni.showToast({
+      title: '订单 ID 缺失',
+      icon: 'none'
+    });
+    setTimeout(() => {
+      uni.navigateBack();
+    }, 1500);
+    return;
+  }
+
+  try {
+    const detail = await getOrderDetail({ orderId: orderId.value });
+    orderInfo.value = detail;
+    
+    // 默认不选中任何商品(移除之前的默认全选逻辑)
+    selectedProducts.value = [];
+  } catch (error) {
+    console.error('加载订单详情失败:', error);
+    uni.showToast({
+      title: '加载订单详情失败',
+      icon: 'none'
+    });
+    setTimeout(() => {
+      uni.navigateBack();
+    }, 1500);
+  }
+};
+
+/**
+ * 页面加载时检查登录状态并加载订单详情
  */
 onMounted(() => {
   // 检查登录状态,未登录会自动跳转到登录页
   checkAuth();
 });
 
-const submitRefund = () => {
+/**
+ * 监听页面加载参数
+ */
+onLoad((options) => {
+  if (options && options.orderId) {
+    orderId.value = parseInt(options.orderId as string);
+    loadOrderDetail();
+  } else {
+    uni.showToast({
+      title: '订单 ID 缺失',
+      icon: 'none'
+    });
+    setTimeout(() => {
+      uni.navigateBack();
+    }, 1500);
+  }
+});
+
+/**
+ * 提交退款申请
+ */
+const submitRefund = async () => {
+  // 校验是否选择了商品
+  if (selectedProducts.value.length === 0) {
+    uni.showToast({
+      title: '请选择要退款的商品',
+      icon: 'none'
+    });
+    return;
+  }
+
+  // 校验退款原因
   if (!selectedReason.value) {
     uni.showToast({
       title: '请选择退款原因',
@@ -78,15 +317,48 @@ const submitRefund = () => {
     });
     return;
   }
-  
-  uni.showToast({
-    title: '退款申请提交成功',
-    icon: 'success'
+
+  // 映射前端退款原因到后端描述
+  const reasonMap: Record<string, string> = {
+    '1': '订单扣款错误',
+    '2': '扣款金额与实际金额不符',
+    '3': '商品过期或变质',
+    '4': '其他'
+  };
+
+  const reasonText = reasonMap[selectedReason.value] || '用户申请退款';
+
+  // 构建选中的商品信息(包含退款数量)
+  const selectedProductIds = selectedProducts.value.map(index => {
+    const product = orderInfo.value.products[index];
+    return {
+      productId: product.id,
+      productName: product.name,
+      quantity: product.refundQuantity || 1, // 使用用户选择的退款数量
+      price: product.price
+    };
   });
-  
-  setTimeout(() => {
-    uni.navigateBack();
-  }, 1500);
+
+  try {
+    // 调用后端退款 API
+    const result = await post('/payment/refund', {
+      orderId: orderId.value,
+      reason: reasonText,
+      products: selectedProductIds // 传递选中的商品信息
+    });
+
+    uni.showToast({
+      title: '退款申请成功',
+      icon: 'success'
+    });
+
+    setTimeout(() => {
+      uni.navigateBack();
+    }, 1500);
+  } catch (error: any) {
+    console.error('退款申请失败:', error);
+    // 错误提示已在 request 拦截器中处理
+  }
 };
 </script>
 
@@ -95,6 +367,264 @@ const submitRefund = () => {
   min-height: 100vh;
   background-color: #f5f5f5;
   padding-top: 20rpx;
+  padding-bottom: 40rpx;
+}
+
+/* 商品选择区域 */
+.product-section {
+  margin-bottom: 20rpx;
+  padding-bottom: 20rpx;
+}
+
+.product-list {
+  display: flex;
+  flex-direction: column;
+  gap: 20rpx;
+}
+
+.product-item {
+  display: flex;
+  flex-direction: column;
+  background-color: #ffffff;
+  border-radius: 12rpx;
+  padding: 24rpx;
+  border: 2rpx solid transparent;
+  transition: all 0.3s ease;
+}
+
+.product-item-selected {
+  border-color: #07c160;
+  background-color: #f6ffed;
+}
+
+.product-item:active {
+  background-color: #fafafa;
+}
+
+.product-main {
+  display: flex;
+  align-items: center;
+  width: 100%;
+}
+
+.product-checkbox {
+  margin-right: 20rpx;
+  flex-shrink: 0;
+}
+
+.checkbox-icon {
+  width: 44rpx;
+  height: 44rpx;
+  border-radius: 50%;
+  border: 3rpx solid #dddddd;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.3s ease;
+}
+
+.checkbox-icon.checked {
+  background-color: #07c160;
+  border-color: #07c160;
+  box-shadow: 0 4rpx 12rpx rgba(7, 193, 96, 0.3);
+}
+
+.checkbox-icon text {
+  color: #ffffff;
+  font-size: 26rpx;
+  font-weight: bold;
+}
+
+.product-image {
+  width: 140rpx;
+  height: 140rpx;
+  border-radius: 12rpx;
+  overflow: hidden;
+  flex-shrink: 0;
+  margin-right: 20rpx;
+  background-color: #f5f5f5;
+  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
+}
+
+.product-image image {
+  width: 100%;
+  height: 100%;
+}
+
+.image-placeholder {
+  width: 100%;
+  height: 100%;
+  background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  color: #999999;
+  font-size: 40rpx;
+}
+
+.product-detail {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  min-width: 0;
+  gap: 8rpx;
+}
+
+.name-container {
+  position: relative;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  max-width: 100%;
+}
+
+.product-name {
+  font-size: 28rpx;
+  color: #333333;
+  line-height: 1.4;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  flex: 1;
+  min-width: 0;
+}
+
+.expand-tip {
+  font-size: 26rpx;
+  color: #1890ff;
+  margin-left: 8rpx;
+  flex-shrink: 0;
+  padding: 4rpx 12rpx;
+  background-color: #e6f7ff;
+  border-radius: 6rpx;
+}
+
+.product-meta {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 8rpx;
+}
+
+.product-price {
+  font-size: 32rpx;
+  color: #ff4400;
+  font-weight: 600;
+}
+
+.product-quantity {
+  font-size: 24rpx;
+  color: #999999;
+}
+
+.product-quantity-label {
+  font-size: 24rpx;
+  color: #999999;
+  background-color: #f5f5f5;
+  padding: 4rpx 12rpx;
+  border-radius: 6rpx;
+}
+
+/* 退款数量选择器 */
+.refund-quantity-selector {
+  margin-top: 20rpx;
+  margin-left: 56rpx;
+  padding: 20rpx 24rpx;
+  background-color: #fafffe;
+  border-radius: 12rpx;
+  border: 1rpx solid #d9f7be;
+}
+
+.selector-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.selector-label {
+  font-size: 28rpx;
+  color: #52c41a;
+  font-weight: 500;
+  flex-shrink: 0;
+}
+
+.quantity-control {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+}
+
+.control-btn {
+  width: 60rpx;
+  height: 60rpx;
+  border-radius: 12rpx;
+  background-color: #ffffff;
+  border: 2rpx solid #e0e0e0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 36rpx;
+  color: #333333;
+  transition: all 0.2s;
+}
+
+.control-btn:active {
+  background-color: #f0f0f0;
+  transform: scale(0.95);
+}
+
+.control-btn.disabled {
+  background-color: #fafafa;
+  border-color: #f0f0f0;
+  color: #cccccc;
+  cursor: not-allowed;
+}
+
+.control-btn.disabled:active {
+  background-color: #fafafa;
+  transform: none;
+}
+
+.quantity-display {
+  min-width: 80rpx;
+  height: 60rpx;
+  text-align: center;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #ffffff;
+  border: 2rpx solid #07c160;
+  border-radius: 12rpx;
+  padding: 0 16rpx;
+}
+
+.quantity-text {
+  font-size: 32rpx;
+  color: #07c160;
+  font-weight: 600;
+}
+
+/* 退款金额预览 */
+.refund-amount-preview {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: 16rpx;
+  padding-top: 16rpx;
+  border-top: 1rpx dashed #d9f7be;
+}
+
+.preview-label {
+  font-size: 26rpx;
+  color: #999999;
+}
+
+.preview-value {
+  font-size: 30rpx;
+  color: #ff4400;
+  font-weight: 600;
 }
 
 /* 退款商品选择器 */
@@ -203,4 +733,43 @@ radio-group {
 .submit-btn::after {
   border: none;
 }
+
+/* 商品信息 */
+.product-info {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+}
+
+.refund-amount {
+  color: #ff4400;
+  font-weight: 500;
+}
+
+/* 提示信息 */
+.tip-section {
+  background-color: #fffbea;
+  border: 1rpx solid #ffe58f;
+  border-radius: 8rpx;
+  padding: 24rpx 30rpx;
+  margin: 30rpx;
+}
+
+.tip-text {
+  display: block;
+  font-size: 24rpx;
+  color: #faad14;
+  line-height: 1.8;
+}
+
+.selected-tip {
+  text-align: right;
+  font-size: 26rpx;
+  color: #07c160;
+  margin-top: 20rpx;
+  padding: 16rpx 20rpx;
+  background-color: #f6ffed;
+  border-radius: 8rpx;
+  font-weight: 500;
+}
 </style>