Browse Source

feat: product order hybrid payment + shop page refactor + DatabaseInitializer merge conflict resolution

- product_orders: add points_used, points_cost, money_amount columns
- ProductOrderController/Service: support mixed payment (points + cash)
- PointsService: integrate points deduction for orders
- shop pages: refactor detail, index, order-list, order-detail
- DatabaseInitializer: resolve merge conflict (keep both migrations 38-40)
Xiaogang Liao 3 months ago
parent
commit
9df8e4c585

+ 7 - 2
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -4125,7 +4125,12 @@ try {
             log.warn("迁移family_members.generation数据失败: {}", e.getMessage());
         }
 
-        // 迁移38: 创建供应商体系相关表
+        // 迁移38: product_orders表添加points_used/points_cost/money_amount列(混合支付支持)
+        ensureColumn("product_orders", "points_used", "INT DEFAULT 0 COMMENT '使用的积分数量'");
+        ensureColumn("product_orders", "points_cost", "INT DEFAULT 0 COMMENT '积分抵扣金额(分)'");
+        ensureColumn("product_orders", "money_amount", "INT DEFAULT 0 COMMENT '现金支付金额(分)'");
+
+        // 迁移39: 创建供应商体系相关表
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS supply_system (" +
                 "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
@@ -4205,7 +4210,7 @@ try {
             log.warn("创建supply_settlement_detail表失败: {}", e.getMessage());
         }
 
-        // 迁移39: 添加supplier_admin角色到users.role ENUM
+        // 迁移40: 添加supplier_admin角色到users.role ENUM
         try {
             jdbcTemplate.execute("ALTER TABLE users MODIFY COLUMN role ENUM('parent','child','teacher','supplier_admin') NOT NULL DEFAULT 'parent' COMMENT '当前活跃角色'");
             log.info("已添加supplier_admin到users.role ENUM");

+ 7 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java

@@ -54,7 +54,13 @@ public class ProductOrderController {
     public Result<List<ProductOrderDTO>> myOrders(@RequestBody Map<String, Object> params,
                                                  @RequestAttribute("userId") Long userId) {
         String status = (String) params.get("status");
-        return orderService.myOrders(userId, status);
+        String keyword = (String) params.get("keyword");
+        return orderService.myOrders(userId, status, keyword);
+    }
+
+    @PostMapping("/my/counts")
+    public Result<Map<String, Long>> myOrderCounts(@RequestAttribute("userId") Long userId) {
+        return orderService.myOrderCounts(userId);
     }
 
     @PostMapping("/detail")

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/CreateProductOrderDTO.java

@@ -15,6 +15,7 @@ public class CreateProductOrderDTO {
     private Integer discountAmount;
     private Long couponId;
     private Integer deliveryMethod = 1; // 1=快递 2=自提
+    private Integer pointsUsed; // 积分抵扣数量
 
     // Explicit getters needed due to Lombok issues
     public List<OrderItemVO> getItems() { return items; }
@@ -26,4 +27,5 @@ public class CreateProductOrderDTO {
     public Long getCouponId() { return couponId; }
     public String getAddressSnapshot() { return addressSnapshot; }
     public Integer getDeliveryMethod() { return deliveryMethod; }
+    public Integer getPointsUsed() { return pointsUsed; }
 }

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrder.java

@@ -50,6 +50,10 @@ public class ProductOrder implements Serializable {
     private Long supplySystemId;       // 供应商体系ID
     private Long supplierId;           // 供应商ID
     private Integer supplyCommission;  // 分销佣金(分)
+    // 混合支付字段
+    private Integer pointsUsed;        // 使用的积分数量
+    private Integer pointsCost;         // 积分抵扣金额(分)
+    private Integer moneyAmount;       // 现金支付金额(分)
     private Date createdAt;
     private Date updatedAt;
 
@@ -70,4 +74,7 @@ public class ProductOrder implements Serializable {
     public void setSupplySystemId(Long supplySystemId) { this.supplySystemId = supplySystemId; }
     public void setSupplierId(Long supplierId) { this.supplierId = supplierId; }
     public void setSupplyCommission(Integer supplyCommission) { this.supplyCommission = supplyCommission; }
+    public void setPointsUsed(Integer pointsUsed) { this.pointsUsed = pointsUsed; }
+    public void setPointsCost(Integer pointsCost) { this.pointsCost = pointsCost; }
+    public void setMoneyAmount(Integer moneyAmount) { this.moneyAmount = moneyAmount; }
 }

+ 28 - 11
cfc-backend/src/main/java/com/etotem/cfc/service/PointsService.java

@@ -6,8 +6,10 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.PointsLog;
+import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.PointsLogMapper;
+import com.etotem.cfc.mapper.UserMapper;
 import javax.annotation.Resource;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -25,6 +27,9 @@ public class PointsService implements PointsServiceInterface {
     @Resource
     private PointsLogMapper pointsLogMapper;
 
+    @Resource
+    private UserMapper userMapper;
+
     public Map<String, Object> getBalance(Long childId) {
         FamilyMember child = familyMemberMapper.selectById(childId);
         if (child == null) {
@@ -192,19 +197,31 @@ public class PointsService implements PointsServiceInterface {
         return awardSystemPoints(childId, amount, "心愿兑换奖励");
     }
 
-    /**
-     * 扣除心愿兑换积分
-     * @param childId 孩子ID
-     * @param amount 扣除积分
-     * @return 扣除成功返回true,余额不足返回false
-     */
     @Transactional
-    public boolean deductForWish(Long childId, int amount) {
-        int currentBalance = getSystemPointsBalance(childId);
-        if (currentBalance < amount) {
+    public boolean deductUserPoints(Long userId, int amount, String reason) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
             return false;
         }
-        int result = deductSystemPoints(childId, amount, "心愿兑换扣除");
-        return result != -1;
+        int current = user.getTotalPoints() == null ? 0 : user.getTotalPoints();
+        if (current < amount) {
+            return false;
+        }
+        user.setTotalPoints(current - amount);
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
+        return true;
+    }
+
+    @Transactional
+    public void refundUserPoints(Long userId, int amount, String reason) {
+        User user = userMapper.selectById(userId);
+        if (user == null) {
+            return;
+        }
+        int current = user.getTotalPoints() == null ? 0 : user.getTotalPoints();
+        user.setTotalPoints(current + amount);
+        user.setUpdatedAt(new Date());
+        userMapper.updateById(user);
     }
 }

+ 66 - 7
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -58,6 +58,9 @@ public class ProductOrderService {
     @Resource
     private UserService userService;
 
+    @Resource
+    private PointsService pointsService;
+
     public Result<ProductOrderDTO> create(CreateProductOrderDTO dto, Long buyerId) {
         if (buyerId == null) {
             return Result.error("请先登录");
@@ -92,6 +95,11 @@ public class ProductOrderService {
         order.setQuantity(dto.getQuantity());
         order.setUnitPrice(product.getPrice());
         order.setTotalAmount(product.getPrice() * dto.getQuantity());
+        order.setPointsUsed(dto.getPointsUsed() != null ? dto.getPointsUsed() : 0);
+        int pointsCost = dto.getPointsUsed() != null ? dto.getPointsUsed() : 0;
+        order.setPointsCost(pointsCost);
+        int moneyAmount = order.getTotalAmount() - pointsCost;
+        order.setMoneyAmount(moneyAmount > 0 ? moneyAmount : 0);
         order.setStatus("pending");
         order.setPaymentMethod(dto.getPaymentMethod() != null ? dto.getPaymentMethod() : "wechat");
         order.setRemark(dto.getRemark());
@@ -149,6 +157,11 @@ public class ProductOrderService {
         order.setQuantity(totalQuantity);
         order.setUnitPrice(firstProduct.getPrice());
         order.setTotalAmount(totalAmount);
+        order.setPointsUsed(dto.getPointsUsed() != null ? dto.getPointsUsed() : 0);
+        int pointsCost = dto.getPointsUsed() != null ? dto.getPointsUsed() : 0;
+        order.setPointsCost(pointsCost);
+        int moneyAmount = totalAmount - pointsCost;
+        order.setMoneyAmount(moneyAmount > 0 ? moneyAmount : 0);
         order.setStatus("pending");
         order.setPaymentMethod(dto.getPaymentMethod() != null ? dto.getPaymentMethod() : "wechat");
         order.setRemark(dto.getRemark());
@@ -175,15 +188,32 @@ public class ProductOrderService {
         if (!"pending".equals(order.getStatus())) {
             return Result.error("订单状态异常");
         }
-        User user = userMapper.selectById(userId);
-        if (user == null || user.getOpenid() == null || user.getOpenid().isEmpty()) {
-            return Result.error("用户openid不存在,无法支付");
+        int pointsUsed = order.getPointsUsed() != null ? order.getPointsUsed() : 0;
+        if (pointsUsed > 0) {
+            boolean deducted = pointsService.deductUserPoints(userId, pointsUsed, "商品购买:" + orderNo);
+            if (!deducted) {
+                return Result.error("积分不足,无法使用积分支付");
+            }
+        }
+        Integer moneyAmount = order.getMoneyAmount();
+        if (moneyAmount != null && moneyAmount > 0) {
+            User user = userMapper.selectById(userId);
+            if (user == null || user.getOpenid() == null || user.getOpenid().isEmpty()) {
+                if (pointsUsed > 0) {
+                    pointsService.refundUserPoints(userId, pointsUsed, "支付失败退款:" + orderNo);
+                }
+                return Result.error("用户openid不存在,无法支付");
+            }
+            Map<String, Object> wechatPayParams = paymentService.createWechatPrepay(
+                    orderNo, order.getProductName(), moneyAmount, user.getOpenid());
+            Map<String, Object> result = new HashMap<>();
+            result.put("wechatPayParams", wechatPayParams);
+            result.put("pointsUsed", pointsUsed);
+            return Result.success(result);
         }
-        Map<String, Object> wechatPayParams = paymentService.createWechatPrepay(
-                orderNo, order.getProductName(), order.getTotalAmount(), user.getOpenid());
-        // Do NOT change status or deduct stock here — WeChat callback confirms payment
         Map<String, Object> result = new HashMap<>();
-        result.put("wechatPayParams", wechatPayParams);
+        result.put("pointsUsed", pointsUsed);
+        result.put("noMoneyPayment", true);
         return Result.success(result);
     }
 
@@ -373,16 +403,45 @@ public class ProductOrderService {
     }
 
     public Result<List<ProductOrderDTO>> myOrders(Long buyerId, String status) {
+        return myOrders(buyerId, status, null);
+    }
+
+    public Result<List<ProductOrderDTO>> myOrders(Long buyerId, String status, String keyword) {
         LambdaQueryWrapper<ProductOrder> wrapper = new LambdaQueryWrapper<ProductOrder>()
             .eq(ProductOrder::getBuyerId, buyerId)
             .orderByDesc(ProductOrder::getCreatedAt);
         if (status != null && !status.isEmpty()) {
             wrapper.eq(ProductOrder::getStatus, status);
         }
+        if (keyword != null && !keyword.trim().isEmpty()) {
+            String kw = keyword.trim();
+            wrapper.and(w -> w.like(ProductOrder::getProductName, kw).or().like(ProductOrder::getOrderNo, kw));
+        }
         List<ProductOrder> list = orderMapper.selectList(wrapper);
         return Result.success(list.stream().map(ProductOrderDTO::from).collect(Collectors.toList()));
     }
 
+    public Result<Map<String, Long>> myOrderCounts(Long buyerId) {
+        Map<String, Long> counts = new java.util.LinkedHashMap<>();
+        String[] statuses = {"pending", "paid", "shipped", "completed", "refunding"};
+        long total = 0;
+        for (String s : statuses) {
+            long cnt = orderMapper.selectCount(new LambdaQueryWrapper<ProductOrder>()
+                .eq(ProductOrder::getBuyerId, buyerId)
+                .eq(ProductOrder::getStatus, s));
+            counts.put(s, cnt);
+            total += cnt;
+        }
+        long cancelled = orderMapper.selectCount(new LambdaQueryWrapper<ProductOrder>()
+            .eq(ProductOrder::getBuyerId, buyerId).eq(ProductOrder::getStatus, "cancelled"));
+        long refunded = orderMapper.selectCount(new LambdaQueryWrapper<ProductOrder>()
+            .eq(ProductOrder::getBuyerId, buyerId).eq(ProductOrder::getStatus, "refunded"));
+        counts.put("total", total + cancelled + refunded);
+        counts.put("cancelled", cancelled);
+        counts.put("refunded", refunded);
+        return Result.success(counts);
+    }
+
     public Result<ProductOrderDTO> detail(String orderNo, Long userId) {
         ProductOrder order = orderMapper.selectOne(
             new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));

+ 3 - 0
cfc-backend/src/main/resources/schema.sql

@@ -1086,6 +1086,9 @@ CREATE TABLE IF NOT EXISTS product_orders (
     supply_system_id BIGINT COMMENT '所属供应商体系ID',
     supplier_id BIGINT COMMENT '收款供应商用户ID',
     supply_commission INT COMMENT '分销佣金(分)',
+    points_used INT DEFAULT 0 COMMENT '使用的积分数量',
+    points_cost INT DEFAULT 0 COMMENT '积分抵扣金额(分)',
+    money_amount INT DEFAULT 0 COMMENT '现金支付金额(分)',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     INDEX idx_user_id (user_id),

+ 6 - 425
cfc-frontend/pages/shop/detail/detail.vue

@@ -1,437 +1,18 @@
 <template>
-  <view class="container">
-    <view v-if="loading" class="loading-wrap">
-      <text class="loading-text">加载中...</text>
-    </view>
-    <scroll-view v-else-if="product && product.id" scroll-y class="content">
-      <image
-        class="cover"
-        :src="product.coverImage || product.image || '/static/default-product.png'"
-        mode="widthFix"
-      />
-      <view class="info-section">
-        <view class="name-row">
-          <text class="product-name">{{ product.name }}</text>
-        </view>
-        <view class="price-row">
-          <text class="price">{{ formatPriceWithSymbol(currentPrice) }}</text>
-          <text v-if="product.memberPrice" class="member-price">会员价 {{ formatPriceWithSymbol(product.memberPrice) }}</text>
-        </view>
-        <view class="meta-row">
-          <text v-if="product.productType" class="meta-item">{{ typeLabel }}</text>
-          <text v-if="product.domain" class="meta-item">{{ domainLabel }}</text>
-          <text class="meta-item">库存 {{ currentStock }}</text>
-        </view>
-        <view v-if="product.brandName" class="vendor-row">
-          <text class="vendor-label">品牌:</text>
-          <text class="vendor-name">{{ product.brandName }}</text>
-        </view>
-      </view>
-
-      <!-- SKU Selector -->
-      <view v-if="product.hasSku && skuList.length > 0" class="sku-section">
-        <text class="section-title">选择规格</text>
-        <view class="sku-list">
-          <view v-for="sku in skuList" :key="sku.id" class="sku-item">
-            <view
-              :class="['sku-chip', selectedSkuId === sku.id ? 'sku-chip-active' : '']"
-              @click="selectSku(sku)"
-            >
-              <text class="sku-chip-text">{{ getSkuDisplayName(sku) }}</text>
-              <text class="sku-chip-price">{{ formatPriceWithSymbol(sku.price) }}</text>
-            </view>
-          </view>
-        </view>
-      </view>
-
-      <!-- Quantity Selector -->
-      <view class="qty-section">
-        <text class="section-title">数量</text>
-        <view class="qty-stepper">
-          <text class="qty-btn" @click="decreaseQty">−</text>
-          <text class="qty-value">{{ quantity }}</text>
-          <text class="qty-btn" @click="increaseQty">+</text>
-        </view>
-      </view>
-
-      <view class="desc-section">
-        <text class="section-title">商品介绍</text>
-        <text class="desc-text">{{ product.description || '暂无介绍' }}</text>
-      </view>
-
-      <view v-if="product.intro" class="intro-section">
-        <text class="section-title">详细介绍</text>
-        <text class="intro-text">{{ product.intro }}</text>
-      </view>
-
-      <view class="bottom-bar">
-        <view class="price-info">
-          <text class="bottom-price">{{ formatPriceWithSymbol(currentPrice) }}</text>
-          <text v-if="product.memberPrice" class="bottom-member">会员{{ formatPriceWithSymbol(product.memberPrice) }}</text>
-        </view>
-        <view class="action-btns">
-          <button class="btn-cart" @click="onAddToCart">加入购物车</button>
-          <button class="btn-buy" @click="onBuy">立即购买</button>
-        </view>
-      </view>
-    </scroll-view>
-    <view v-else class="empty-wrap">
-      <text class="empty-text">商品不存在或已下架</text>
-    </view>
-  </view>
+  <view class="redirect"></view>
 </template>
 
 <script>
-import { productDetail, productSkuList } from '@/utils/api.js'
-import config from '@/config.js'
-
 export default {
-  data() {
-    return {
-      product: {},
-      loading: false,
-      productId: null,
-      skuList: [],
-      selectedSkuId: null,
-      selectedSku: null,
-      quantity: 1
-    }
-  },
-  computed: {
-    typeLabel() {
-      var map = { activity: '活动', course: '课程', physical: '实物', digital: '数字', service: '服务' }
-      return map[this.product.productType] || this.product.productType
-    },
-    domainLabel() {
-      var map = { action: '行动', mind: '心智', body: '身体', wisdom: '智慧', wealth: '财富' }
-      return map[this.product.domain] || this.product.domain
-    },
-    currentPrice() {
-      if (this.selectedSku && this.selectedSku.price) {
-        return this.selectedSku.price
-      }
-      return this.product.price
-    },
-    currentStock() {
-      if (this.selectedSku && this.selectedSku.stock !== undefined) {
-        return this.selectedSku.stock
-      }
-      return this.product.stock || 0
-    },
-    skuSpecNames() {
-      if (!this.product.skuSpecNames) return []
-      try {
-        return JSON.parse(this.product.skuSpecNames)
-      } catch (e) {
-        return []
-      }
-    }
-  },
   onLoad(options) {
-    if (options.id) {
-      this.productId = options.id
-      this.loadDetail(options.id)
-    }
-  },
-  methods: {
-    loadDetail(id) {
-      this.loading = true
-      var that = this
-      productDetail({ id: parseInt(id) }).then(function(res) {
-        that.loading = false
-        if (res.code === 200 && res.data) {
-          that.product = res.data
-          if (that.product.hasSku) {
-            that.loadSkuList(id)
-          }
-        }
-      }).catch(function() {
-        that.loading = false
-      })
-    },
-    loadSkuList(productId) {
-      var that = this
-      productSkuList({ productId: parseInt(productId) }).then(function(res) {
-        if (res.code === 200 && res.data) {
-          that.skuList = res.data
-        }
-      }).catch(function() {})
-    },
-    selectSku(sku) {
-      this.selectedSkuId = sku.id
-      this.selectedSku = sku
-    },
-    getSkuDisplayName(sku) {
-      if (!sku.specs) return ''
-      try {
-        var specs = JSON.parse(sku.specs)
-        var displayParts = []
-        for (var i = 0; i < specs.length; i++) {
-          displayParts.push(specs[i].value)
-        }
-        return displayParts.join(' / ')
-      } catch (e) {
-        return sku.skuCode || ''
-      }
-    },
-    onBuy() {
-      var userId = uni.getStorageSync('userId')
-      if (!userId) {
-        uni.navigateTo({ url: '/pages/login/login' })
-        return
-      }
-      if (this.product.hasSku && !this.selectedSkuId) {
-        uni.showToast({ title: '请选择规格', icon: 'none' })
-        return
-      }
-      var url = '/pages/shop/checkout/checkout?productId=' + this.productId + '&quantity=' + this.quantity
-      url += '&productName=' + encodeURIComponent(this.product.name)
-      url += '&coverImage=' + encodeURIComponent(this.product.coverImage || '')
-      url += '&price=' + (this.selectedSku ? this.selectedSku.price : this.product.price)
-      if (this.selectedSkuId) {
-        url += '&skuId=' + this.selectedSkuId
-      }
-      uni.navigateTo({ url: url })
-    },
-    onAddToCart() {
-      var userId = uni.getStorageSync('userId')
-      if (!userId) {
-        uni.navigateTo({ url: '/pages/login/login' })
-        return
-      }
-      if (this.product.hasSku && !this.selectedSkuId) {
-        uni.showToast({ title: '请选择规格', icon: 'none' })
-        return
-      }
-      var that = this
-      uni.request({
-        url: config.api('/api/cart/add'),
-        method: 'POST',
-        data: { productId: this.productId },
-        header: {
-          'Content-Type': 'application/json',
-          'Authorization': 'Bearer ' + uni.getStorageSync('token')
-        },
-        success: function(res) {
-          if (res.data && res.data.code === 200) {
-            uni.showToast({ title: '已加入购物车', icon: 'success' })
-          } else {
-            uni.showToast({ title: res.data.message || '添加失败', icon: 'none' })
-          }
-        },
-        fail: function() {
-          uni.showToast({ title: '网络请求失败', icon: 'none' })
-        }
-      })
-    },
-    increaseQty() {
-      var stock = this.currentStock
-      if (this.quantity >= stock) {
-        uni.showToast({ title: '已达最大库存', icon: 'none' })
-        return
-      }
-      this.quantity = this.quantity + 1
-    },
-    decreaseQty() {
-      if (this.quantity <= 1) return
-      this.quantity = this.quantity - 1
-    }
+    const id = options.id || ''
+    uni.redirectTo({
+      url: '/pages/discover/product-detail/product-detail?id=' + id + '&source=shop'
+    })
   }
 }
 </script>
 
 <style scoped>
-.container {
-  min-height: 100vh;
-  background: #f5f5f5;
-}
-.loading-wrap,
-.empty-wrap {
-  display: flex;
-  justify-content: center;
-  padding-top: 200rpx;
-}
-.loading-text,
-.empty-text {
-  font-size: 28rpx;
-  color: #999;
-}
-.content {
-  height: calc(100vh - 120rpx);
-  padding-bottom: 120rpx;
-}
-.cover {
-  width: 100%;
-  background: #f0f0f0;
-}
-.info-section {
-  background: #fff;
-  padding: 30rpx;
-  margin-bottom: 20rpx;
-}
-.name-row {
-  margin-bottom: 16rpx;
-}
-.product-name {
-  font-size: 34rpx;
-  font-weight: bold;
-  color: #333;
-  line-height: 1.4;
-}
-.price-row {
-  display: flex;
-  align-items: baseline;
-  margin-bottom: 16rpx;
-}
-.price {
-  font-size: 40rpx;
-  color: #F97316;
-  font-weight: bold;
-  margin-right: 20rpx;
-}
-.member-price {
-  font-size: 26rpx;
-  color: #999;
-}
-.meta-row {
-  display: flex;
-  flex-wrap: wrap;
-  margin-bottom: 12rpx;
-}
-.meta-item {
-  font-size: 22rpx;
-  color: #999;
-  background: #f5f5f5;
-  padding: 4rpx 16rpx;
-  border-radius: 4rpx;
-  margin-right: 12rpx;
-  margin-bottom: 8rpx;
-}
-.vendor-row {
-  display: flex;
-  align-items: center;
-}
-.vendor-label {
-  font-size: 24rpx;
-  color: #999;
-}
-.vendor-name {
-  font-size: 24rpx;
-  color: #666;
-}
-.desc-section,
-.intro-section {
-  background: #fff;
-  padding: 30rpx;
-  margin-bottom: 20rpx;
-}
-.section-title {
-  font-size: 28rpx;
-  font-weight: bold;
-  color: #333;
-  margin-bottom: 16rpx;
-  display: block;
-}
-.desc-text,
-.intro-text {
-  font-size: 26rpx;
-  color: #666;
-  line-height: 1.6;
-  display: block;
-}
-.bottom-bar {
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  height: 100rpx;
-  background: #fff;
-  border-top: 1rpx solid #eee;
-  display: flex;
-  align-items: center;
-  padding: 0 20rpx;
-  z-index: 100;
-}
-.price-info {
-  display: flex;
-  align-items: baseline;
-  flex-shrink: 0;
-}
-.bottom-price {
-  font-size: 36rpx;
-  color: #F97316;
-  font-weight: bold;
-}
-.bottom-member {
-  font-size: 24rpx;
-  color: #999;
-  margin-left: 10rpx;
-}
-.action-btns {
-  display: flex;
-  align-items: center;
-  margin-left: auto;
-}
-.btn-buy {
-  background: linear-gradient(135deg, #F97316, #FB923C);
-  color: #fff;
-  font-size: 26rpx;
-  font-weight: bold;
-  padding: 0 40rpx;
-  height: 60rpx;
-  line-height: 60rpx;
-  border-radius: 30rpx;
-  border: none;
-  margin-left: 16rpx;
-}
-.btn-buy::after {
-  border: none;
-}
-.btn-cart {
-  background: #fff;
-  color: #F97316;
-  font-size: 26rpx;
-  font-weight: bold;
-  padding: 0 30rpx;
-  height: 60rpx;
-  line-height: 60rpx;
-  border-radius: 30rpx;
-  border: 2rpx solid #F97316;
-}
-.btn-cart::after {
-  border: none;
-}
-.qty-section {
-  background: #fff;
-  padding: 30rpx;
-  margin-bottom: 20rpx;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-}
-.qty-stepper {
-  display: flex;
-  align-items: center;
-  border: 1rpx solid #eee;
-  border-radius: 8rpx;
-}
-.qty-btn {
-  width: 60rpx;
-  height: 60rpx;
-  line-height: 60rpx;
-  text-align: center;
-  font-size: 32rpx;
-  color: #333;
-  background: #f9f9f9;
-}
-.qty-value {
-  width: 80rpx;
-  height: 60rpx;
-  line-height: 60rpx;
-  text-align: center;
-  font-size: 30rpx;
-  color: #333;
-  border-left: 1rpx solid #eee;
-  border-right: 1rpx solid #eee;
-}
+.redirect { background: #fff; min-height: 100vh; }
 </style>

+ 57 - 3
cfc-frontend/pages/shop/index/index.vue

@@ -47,14 +47,22 @@
       :refresher-triggered="refreshing"
       @refresherrefresh="onRefresh"
     >
-      <view v-if="loading && products.length === 0" class="loading-wrap">
-        <text class="loading-text">加载中...</text>
+      <!-- Loading skeleton -->
+      <view v-if="loading && products.length === 0" class="skeleton-grid">
+        <view class="skeleton-card" v-for="n in 6" :key="n">
+          <view class="skeleton-img"></view>
+          <view class="skeleton-body">
+            <view class="skeleton-line skeleton-title"></view>
+            <view class="skeleton-line skeleton-price"></view>
+            <view class="skeleton-line skeleton-vendor"></view>
+          </view>
+        </view>
       </view>
       <view v-else-if="products.length === 0" class="empty-wrap">
         <text class="empty-icon">📦</text>
         <text class="empty-text">暂无商品</text>
       </view>
-      <view v-else class="grid">
+      <view v-else class="grid fade-in">
         <view
           v-for="item in products"
           :key="item.id"
@@ -398,4 +406,50 @@ export default {
   font-size: 24rpx;
   color: #ccc;
 }
+/* Loading skeleton grid */
+.skeleton-grid {
+  display: flex;
+  flex-wrap: wrap;
+  padding: 20rpx;
+  justify-content: space-between;
+}
+.skeleton-card {
+  width: 49%;
+  background: #fff;
+  border-radius: 16rpx;
+  overflow: hidden;
+  margin-bottom: 20rpx;
+}
+.skeleton-img {
+  width: 100%;
+  height: 320rpx;
+  background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
+  background-size: 200% 100%;
+  animation: shimmer 1.5s infinite;
+}
+.skeleton-body {
+  padding: 16rpx;
+}
+.skeleton-line {
+  height: 24rpx;
+  border-radius: 6rpx;
+  background: linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%);
+  background-size: 200% 100%;
+  animation: shimmer 1.5s infinite;
+}
+.skeleton-title { width: 70%; margin-bottom: 12rpx; }
+.skeleton-price { width: 40%; margin-bottom: 8rpx; }
+.skeleton-vendor { width: 50%; }
+@keyframes shimmer {
+  0% { background-position: 200% 0; }
+  100% { background-position: -200% 0; }
+}
+/* Product grid fade-in */
+.grid.fade-in {
+  animation: gridFadeIn 0.3s ease-out;
+}
+@keyframes gridFadeIn {
+  from { opacity: 0; transform: translateY(10rpx); }
+  to { opacity: 1; transform: translateY(0); }
+}
 </style>

+ 61 - 8
cfc-frontend/pages/shop/order-detail/order-detail.vue

@@ -5,10 +5,12 @@
     </view>
     <scroll-view v-else-if="order.id" scroll-y class="content">
       <view class="order-status-section">
-        <text :class="['status-text', 'status-' + order.status]">{{ statusLabel(order.status) }}</text>
+        <text :class="['status-text', 'status-' + getDisplayStatus()]">{{ getDisplayStatusText() }}</text>
         <text v-if="order.status === 'paid'" class="status-hint">商家正在准备中,请耐心等待</text>
         <text v-if="order.status === 'completed'" class="status-hint">订单已完成,感谢购买</text>
         <text v-if="order.status === 'cancelled'" class="status-hint">订单已取消</text>
+        <text v-if="order.refundStatus === 1" class="status-hint">退款申请处理中,请耐心等待</text>
+        <text v-if="order.refundStatus === 2" class="status-hint">退款已完成,金额将原路返回</text>
       </view>
 
       <view class="section">
@@ -34,6 +36,12 @@
       <view class="section">
         <text class="section-title">商品信息</text>
         <view class="product-card">
+          <image
+            class="product-cover"
+            :src="order.coverImage || defaultCover(order.productType)"
+            mode="aspectFill"
+            @error="onCoverError"
+          />
           <text class="product-name">{{ order.productName }}</text>
           <view class="product-meta">
             <text class="product-type">{{ typeLabel(order.productType) }}</text>
@@ -92,7 +100,7 @@
 </template>
 
 <script>
-import { productOrderDetail, productOrderConfirm } from '@/utils/api.js'
+import { productOrderDetail, productOrderConfirm, productOrderCancel } from '@/utils/api.js'
 
 export default {
   data() {
@@ -181,16 +189,39 @@ export default {
         uni.showToast({ title: '订单信息缺失', icon: 'none' })
       }
     },
-    statusLabel(status) {
+    defaultCover(type) {
+      const map = {
+        activity: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 160 160%22%3E%3Crect fill=%22%23FFF7E6%22 width=%22160%22 height=%22160%22/%3E%3Ctext x=%2280%22 y=%2290%22 text-anchor=%22middle%22 font-size=%2260%22%3E🎯%3C/text%3E%3C/svg%3E',
+        course: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 160 160%22%3E%3Crect fill=%22%23EDE9FE%22 width=%22160%22 height=%22160%22/%3E%3Ctext x=%2280%22 y=%2290%22 text-anchor=%22middle%22 font-size=%2260%22%3E📖%3C/text%3E%3C/svg%3E',
+        physical: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 160 160%22%3E%3Crect fill=%22%23E0F2FE%22 width=%22160%22 height=%22160%22/%3E%3Ctext x=%2280%22 y=%2290%22 text-anchor=%22middle%22 font-size=%2260%22%3E📦%3C/text%3E%3C/svg%3E',
+        digital: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 160 160%22%3E%3Crect fill=%22%23F0FDF4%22 width=%22160%22 height=%22160%22/%3E%3Ctext x=%2280%22 y=%2290%22 text-anchor=%22middle%22 font-size=%2260%22%3E💎%3C/text%3E%3C/svg%3E',
+        service: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 160 160%22%3E%3Crect fill=%22%23FEF3C7%22 width=%22160%22 height=%22160%22/%3E%3Ctext x=%2280%22 y=%2290%22 text-anchor=%22middle%22 font-size=%2260%22%3E🎓%3C/text%3E%3C/svg%3E'
+      }
+      return map[type] || map.physical
+    },
+    onCoverError(e) {
+      if (this.order) {
+        this.order.coverImage = ''
+      }
+    },
+    getDisplayStatus() {
+      if (this.order.refundStatus === 1) return 'refunding'
+      if (this.order.refundStatus === 2) return 'refunded'
+      if (this.order.refundStatus === 3) return 'refund-rejected'
+      return this.order.status
+    },
+    getDisplayStatusText() {
       const map = {
         pending: '待支付',
         paid: '待发货',
         shipped: '待收货',
         completed: '已完成',
+        refunding: '退款中',
+        refunded: '已退款',
         cancelled: '已取消',
-        refunded: '已退款'
+        'refund-rejected': '退款驳回'
       }
-      return map[status] || status
+      return map[this.getDisplayStatus()] || this.order.status
     },
     paymentLabel(method) {
       const map = { wechat: '微信支付', points: '积分支付', membership: '会员折扣' }
@@ -239,22 +270,30 @@ export default {
   height: calc(100vh - 120rpx);
 }
 .order-status-section {
-  background: linear-gradient(135deg, #F97316, #FB923C);
   padding: 40rpx 30rpx;
   display: flex;
   flex-direction: column;
   align-items: center;
 }
-.status-text {
+.order-status-section .status-text {
   font-size: 36rpx;
   font-weight: bold;
   color: #fff;
   margin-bottom: 10rpx;
 }
-.status-hint {
+.order-status-section .status-hint {
   font-size: 24rpx;
   color: rgba(255,255,255,0.85);
 }
+.order-status-section { background: linear-gradient(135deg, #F97316, #FB923C); }
+.order-status-section .status-pending { background: linear-gradient(135deg, #F97316, #FB923C); }
+.order-status-section .status-paid { background: linear-gradient(135deg, #0284C7, #38BDF8); }
+.order-status-section .status-shipped { background: linear-gradient(135deg, #0284C7, #38BDF8); }
+.order-status-section .status-completed { background: linear-gradient(135deg, #16A34A, #4ADE80); }
+.order-status-section .status-refunding { background: linear-gradient(135deg, #D97706, #FBBF24); }
+.order-status-section .status-refunded { background: linear-gradient(135deg, #6B7280, #9CA3AF); }
+.order-status-section .status-cancelled { background: linear-gradient(135deg, #6B7280, #9CA3AF); }
+.order-status-section .status-refund-rejected { background: linear-gradient(135deg, #DC2626, #F87171); }
 .section {
   background: #fff;
   margin: 20rpx;
@@ -300,6 +339,20 @@ export default {
   background: #fafafa;
   border-radius: 12rpx;
   padding: 20rpx;
+  display: flex;
+  gap: 20rpx;
+  align-items: flex-start;
+}
+.product-cover {
+  width: 160rpx;
+  height: 160rpx;
+  border-radius: 10rpx;
+  background: #eee;
+  flex-shrink: 0;
+}
+.product-info {
+  flex: 1;
+  min-width: 0;
 }
 .product-name {
   display: block;

+ 358 - 175
cfc-frontend/pages/shop/order-list/order-list.vue

@@ -1,16 +1,35 @@
 <template>
   <view class="container">
-    <view class="status-tabs">
+    <!-- 搜索栏 -->
+    <view class="search-bar">
+      <view class="search-input-wrap">
+        <text class="search-icon">🔍</text>
+        <input
+          class="search-input"
+          v-model="keyword"
+          placeholder="搜索商品名称/订单号"
+          placeholder-class="search-placeholder"
+          @confirm="onSearch"
+          confirm-type="search"
+        />
+        <text v-if="keyword" class="search-clear" @click="clearSearch">✕</text>
+      </view>
+    </view>
+
+    <!-- 状态 Tabs -->
+    <scroll-view scroll-x class="status-tabs" enhanced :show-scrollbar="false">
       <view
         v-for="item in statusList"
         :key="item.value"
         :class="['tab', currentStatus === item.value ? 'active' : '']"
         @click="onStatusChange(item.value)"
       >
-        {{ item.label }}
+        <text class="tab-label">{{ item.label }}</text>
+        <text v-if="statusCounts[item.value] > 0" class="tab-badge">{{ formatCount(statusCounts[item.value]) }}</text>
       </view>
-    </view>
+    </scroll-view>
 
+    <!-- 订单列表 -->
     <scroll-view
       scroll-y
       class="order-list"
@@ -24,7 +43,8 @@
       </view>
       <view v-else-if="orders.length === 0" class="empty-wrap">
         <text class="empty-icon">📦</text>
-        <text class="empty-text">暂无订单</text>
+        <text class="empty-text">{{ keyword ? '未找到相关订单' : '暂无订单' }}</text>
+        <text v-if="keyword" class="empty-hint">试试其他关键词</text>
       </view>
       <view v-else>
         <view
@@ -33,47 +53,49 @@
           class="order-card"
           @click="goDetail(item.orderNo)"
         >
-          <view class="order-header">
-            <text class="order-no">{{ item.orderNo }}</text>
-            <text :class="['status-badge', 'status-' + item.status]">{{ statusLabel(item.status) }}</text>
-          </view>
-          <view class="order-body">
-            <view class="product-info">
-              <text class="product-name">{{ item.productName }}</text>
-              <text class="product-type">{{ typeLabel(item.productType) }}</text>
-            </view>
-            <view class="order-meta">
-              <text class="order-amount">{{ formatPriceWithSymbol(item.totalAmount) }}</text>
-              <text class="order-qty">x{{ item.quantity }}</text>
+          <view class="card-body">
+            <image
+              class="product-img"
+              :src="item.coverImage || defaultCover(item.productType)"
+              mode="aspectFill"
+              @error="onImgError($event, item)"
+            />
+            <view class="product-section">
+              <view class="product-header">
+                <text class="product-name">{{ item.productName }}</text>
+                <text :class="['status-badge', 'status-' + getDisplayStatus(item)]">
+                  {{ getDisplayStatusText(item) }}
+                </text>
+              </view>
+              <view class="product-meta">
+                <view :class="['type-tag', 'type-' + item.productType]">
+                  <text class="type-icon">{{ typeIcon(item.productType) }}</text>
+                  <text class="type-text">{{ typeLabel(item.productType) }}</text>
+                </view>
+                <text class="order-no">No.{{ item.orderNo }}</text>
+              </view>
+              <view class="price-row">
+                <text class="price">{{ formatPriceWithSymbol(item.totalAmount) }}</text>
+                <text class="qty">x{{ item.quantity }}</text>
+              </view>
             </view>
           </view>
-          <view class="order-footer">
-            <text class="order-time">{{ formatTime(item.createdAt) }}</text>
-            <view class="order-actions">
-              <button
-                v-if="item.status === 'pending'"
-                class="btn-small btn-cancel"
-                @click.stop="onCancel(item.orderNo)"
-              >取消</button>
-              <button
-                v-if="item.status === 'pending'"
-                class="btn-small btn-pay"
-                @click.stop="onPay(item.orderNo, item.totalAmount / 100)"
-              >支付</button>
-              <button
-                v-if="item.status === 'shipped'"
-                class="btn-small btn-confirm"
-                @click.stop="onConfirm(item.orderNo)"
-              >确认收货</button>
-              <button
-                v-if="(item.status === 'paid' || item.status === 'shipped') && item.refundStatus !== 1 && item.refundStatus !== 2"
-                class="btn-small btn-refund"
-                @click.stop="goAfterSales(item.id, item.orderNo)"
-              >申请退款</button>
+
+          <view class="card-footer">
+            <text class="order-time" :title="formatFullTime(item.createdAt)">
+              {{ formatRelativeTime(item.createdAt) }}
+            </text>
+            <view class="order-actions" @click.stop>
+              <button v-if="item.status === 'pending'" class="btn-sm btn-cancel" @click="onCancel(item.orderNo)">取消</button>
+              <button v-if="item.status === 'pending'" class="btn-sm btn-pay" @click="onPay(item.orderNo, item.totalAmount / 100)">去支付</button>
+              <button v-if="item.status === 'paid'" class="btn-sm btn-refund" @click="goAfterSales(item.id, item.orderNo)">申请退款</button>
+              <button v-if="item.status === 'shipped'" class="btn-sm btn-confirm" @click="onConfirm(item.orderNo)">确认收货</button>
+              <button v-if="item.status === 'completed'" class="btn-sm btn-rebuy" @click="goDetail(item.orderNo)">再次购买</button>
             </view>
           </view>
         </view>
       </view>
+
       <view v-if="loadingMore" class="loading-more">
         <text class="loading-text">加载中...</text>
       </view>
@@ -85,11 +107,21 @@
 </template>
 
 <script>
-import { productOrderMy, productOrderCancel, productOrderConfirm, productOrderRefundApply } from '@/utils/api.js'
+import { productOrderMy, productOrderCounts, productOrderCancel, productOrderConfirm } from '@/utils/api.js'
+
+var TYPE_ICONS = { activity: '🎯', course: '📖', physical: '📦', digital: '💎', service: '🎓' }
+var DEFAULT_COVER = {
+  activity: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 160 160%22%3E%3Crect fill=%22%23FFF7E6%22 width=%22160%22 height=%22160%22/%3E%3Ctext x=%2280%22 y=%2290%22 text-anchor=%22middle%22 font-size=%2260%22%3E🎯%3C/text%3E%3C/svg%3E',
+  course: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 160 160%22%3E%3Crect fill=%22%23EDE9FE%22 width=%22160%22 height=%22160%22/%3E%3Ctext x=%2280%22 y=%2290%22 text-anchor=%22middle%22 font-size=%2260%22%3E📖%3C/text%3E%3C/svg%3E',
+  physical: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 160 160%22%3E%3Crect fill=%22%23E0F2FE%22 width=%22160%22 height=%22160%22/%3E%3Ctext x=%2280%22 y=%2290%22 text-anchor=%22middle%22 font-size=%2260%22%3E📦%3C/text%3E%3C/svg%3E',
+  digital: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 160 160%22%3E%3Crect fill=%22%23F0FDF4%22 width=%22160%22 height=%22160%22/%3E%3Ctext x=%2280%22 y=%2290%22 text-anchor=%22middle%22 font-size=%2260%22%3E💎%3C/text%3E%3C/svg%3E',
+  service: 'data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 160 160%22%3E%3Crect fill=%22%23FEF3C7%22 width=%22160%22 height=%22160%22/%3E%3Ctext x=%2280%22 y=%2290%22 text-anchor=%22middle%22 font-size=%2260%22%3E🎓%3C/text%3E%3C/svg%3E'
+}
 
 export default {
   data() {
     return {
+      keyword: '',
       statusList: [
         { label: '全部', value: '' },
         { label: '待支付', value: 'pending' },
@@ -99,93 +131,130 @@ export default {
         { label: '退款中', value: 'refunding' }
       ],
       currentStatus: '',
+      statusCounts: { total: 0, pending: 0, paid: 0, shipped: 0, completed: 0, refunding: 0 },
       orders: [],
       page: 1,
       size: 20,
       loading: false,
       loadingMore: false,
       refreshing: false,
-      noMore: false
+      noMore: false,
+      searchTimer: null
     }
   },
   onLoad() {
+    this.loadCounts()
     this.loadOrders()
   },
   methods: {
-    onStatusChange(value) {
-      this.currentStatus = value
-      this.page = 1
-      this.orders = []
-      this.noMore = false
-      this.loadOrders()
+    loadCounts() {
+      var self = this
+      productOrderCounts().then(function(res) {
+        if (res.code === 200 && res.data) {
+          self.statusCounts = res.data
+        }
+      }).catch(function() {})
     },
     loadOrders() {
       if (this.loading) return
       this.loading = true
-      const params = { page: this.page, size: this.size }
+      var params = { page: this.page, size: this.size }
       if (this.currentStatus) {
         params.status = this.currentStatus
       }
-      productOrderMy(params).then(res => {
-        this.loading = false
-        this.refreshing = false
+      if (this.keyword && this.keyword.trim()) {
+        params.keyword = this.keyword.trim()
+      }
+      var self = this
+      productOrderMy(params).then(function(res) {
+        self.loading = false
+        self.refreshing = false
         if (res.code === 200 && res.data) {
-          const list = res.data || []
-          if (this.page === 1) {
-            this.orders = list
+          var list = res.data || []
+          if (self.page === 1) {
+            self.orders = list
           } else {
-            this.orders = this.orders.concat(list)
+            self.orders = self.orders.concat(list)
           }
-          this.noMore = list.length < this.size
+          self.noMore = list.length < self.size
         }
-      }).catch(() => {
-        this.loading = false
-        this.refreshing = false
+      }).catch(function() {
+        self.loading = false
+        self.refreshing = false
       })
     },
     onRefresh() {
       this.refreshing = true
       this.page = 1
       this.noMore = false
+      this.loadCounts()
       this.loadOrders()
     },
     onLoadMore() {
       if (this.noMore || this.loadingMore) return
       this.page = this.page + 1
       this.loadingMore = true
-      const params = { page: this.page, size: this.size }
+      var params = { page: this.page, size: this.size }
       if (this.currentStatus) {
         params.status = this.currentStatus
       }
-      productOrderMy(params).then(res => {
-        this.loadingMore = false
+      if (this.keyword && this.keyword.trim()) {
+        params.keyword = this.keyword.trim()
+      }
+      var self = this
+      productOrderMy(params).then(function(res) {
+        self.loadingMore = false
         if (res.code === 200 && res.data) {
-          const list = res.data || []
-          this.orders = this.orders.concat(list)
-          this.noMore = list.length < this.size
+          var list = res.data || []
+          self.orders = self.orders.concat(list)
+          self.noMore = list.length < self.size
         }
-      }).catch(() => {
-        this.loadingMore = false
+      }).catch(function() {
+        self.loadingMore = false
       })
     },
+    onStatusChange(value) {
+      this.currentStatus = value
+      this.page = 1
+      this.orders = []
+      this.noMore = false
+      this.loadOrders()
+    },
+    onSearch() {
+      var self = this
+      if (this.searchTimer) clearTimeout(this.searchTimer)
+      this.searchTimer = setTimeout(function() {
+        self.page = 1
+        self.orders = []
+        self.noMore = false
+        self.loadOrders()
+      }, 300)
+    },
+    clearSearch() {
+      this.keyword = ''
+      this.page = 1
+      this.orders = []
+      this.noMore = false
+      this.loadOrders()
+    },
     goDetail(orderNo) {
       uni.navigateTo({ url: '/pages/shop/order-detail/order-detail?orderNo=' + orderNo })
     },
     onPay(orderNo, amount) {
-      uni.navigateTo({
-        url: '/pages/shop/payment/payment?orderNo=' + orderNo + '&amount=' + (amount || 0)
-      })
+      uni.navigateTo({ url: '/pages/shop/payment/payment?orderNo=' + orderNo + '&amount=' + (amount || 0) })
     },
     onCancel(orderNo) {
+      var self = this
       uni.showModal({
         title: '确认取消',
         content: '确定取消该订单吗?',
-        success: (confirm) => {
+        success: function(confirm) {
           if (confirm.confirm) {
-            productOrderCancel({ orderNo }).then(res => {
+            productOrderCancel({ orderNo: orderNo }).then(function(res) {
               if (res.code === 200) {
                 uni.showToast({ title: '订单已取消', icon: 'success' })
-                this.onRefresh()
+                self.loadCounts()
+                self.onRefresh()
               } else {
                 uni.showToast({ title: res.message || '取消失败', icon: 'none' })
               }
@@ -195,33 +264,17 @@ export default {
       })
     },
     onConfirm(orderNo) {
-      productOrderConfirm({ orderNo }).then(res => {
+      var self = this
+      productOrderConfirm({ orderNo: orderNo }).then(function(res) {
         if (res.code === 200) {
           uni.showToast({ title: '收货成功', icon: 'success' })
-          this.onRefresh()
+          self.loadCounts()
+          self.onRefresh()
         } else {
           uni.showToast({ title: res.message || '操作失败', icon: 'none' })
         }
       })
     },
-    onApplyRefund(orderNo) {
-      uni.showModal({
-        title: '申请退款',
-        content: '确定要申请退款吗?',
-        success: (confirm) => {
-          if (confirm.confirm) {
-            productOrderRefundApply({ orderNo: orderNo }).then(res => {
-              if (res.code === 200) {
-                uni.showToast({ title: '退款申请已提交', icon: 'success' })
-                this.onRefresh()
-              } else {
-                uni.showToast({ title: res.message || '申请失败', icon: 'none' })
-              }
-            })
-          }
-        }
-      })
-    },
     goAfterSales(orderId, orderNo) {
       var url = '/pages/shop/after-sales-apply/after-sales-apply?orderId=' + orderId
       if (orderNo) {
@@ -229,39 +282,71 @@ export default {
       }
       uni.navigateTo({ url: url })
     },
-    statusLabel(status) {
-      const map = {
+    getDisplayStatus(item) {
+      if (item.refundStatus === 1) return 'refunding'
+      if (item.refundStatus === 2) return 'refunded'
+      if (item.refundStatus === 3) return 'refund-rejected'
+      return item.status
+    },
+    getDisplayStatusText(item) {
+      var map = {
         pending: '待支付',
         paid: '待发货',
-        pending_shipment: '待发货',
         shipped: '待收货',
-        pending_receipt: '待收货',
         completed: '已完成',
-        refunded: '已退款',
         refunding: '退款中',
+        refunded: '已退款',
         cancelled: '已取消',
-        abnormal: '异常'
+        'refund-rejected': '退款驳回'
       }
-      return map[status] || status
+      return map[this.getDisplayStatus(item)] || item.status
     },
     typeLabel(type) {
-      const map = {
-        activity: '活动',
-        course: '课程',
-        physical: '实物',
-        digital: '数字',
-        service: '服务'
-      }
-      return map[type] || type
+      var map = { activity: '活动', course: '课程', physical: '实物', digital: '数字', service: '服务' }
+      return map[type] || '商品'
+    },
+    typeIcon(type) {
+      return TYPE_ICONS[type] || '📦'
     },
     formatPriceWithSymbol(amount) {
       if (!amount && amount !== 0) return ''
       return '¥' + (amount / 100).toFixed(2)
     },
-    formatTime(time) {
+    formatRelativeTime(time) {
       if (!time) return ''
-      const d = new Date(time)
+      var now = Date.now()
+      var ts = new Date(time).getTime()
+      var diff = now - ts
+      var minute = 60 * 1000
+      var hour = 60 * minute
+      var day = 24 * hour
+      if (diff < minute) return '刚刚'
+      if (diff < hour) return Math.floor(diff / minute) + '分钟前'
+      if (diff < day) return Math.floor(diff / hour) + '小时前'
+      if (diff < 3 * day) return Math.floor(diff / day) + '天前'
+      var d = new Date(time)
       return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
+    },
+    formatFullTime(time) {
+      if (!time) return ''
+      var d = new Date(time)
+      var y = d.getFullYear()
+      var mo = String(d.getMonth() + 1).padStart(2, '0')
+      var da = String(d.getDate()).padStart(2, '0')
+      var h = String(d.getHours()).padStart(2, '0')
+      var mi = String(d.getMinutes()).padStart(2, '0')
+      var s = String(d.getSeconds()).padStart(2, '0')
+      return y + '-' + mo + '-' + da + ' ' + h + ':' + mi + ':' + s
+    },
+    formatCount(n) {
+      if (n > 99) return '99+'
+      return String(n)
+    },
+    defaultCover(type) {
+      return DEFAULT_COVER[type] || DEFAULT_COVER.physical
+    },
+    onImgError(e, item) {
+      item.coverImage = ''
     }
   }
 }
@@ -274,37 +359,92 @@ export default {
   height: 100vh;
   background: #f5f5f5;
 }
+.search-bar {
+  background: #fff;
+  padding: 16rpx 24rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.search-input-wrap {
+  display: flex;
+  align-items: center;
+  background: #f5f5f5;
+  border-radius: 40rpx;
+  padding: 12rpx 24rpx;
+  height: 72rpx;
+  box-sizing: border-box;
+}
+.search-icon {
+  font-size: 28rpx;
+  margin-right: 12rpx;
+  flex-shrink: 0;
+}
+.search-input {
+  flex: 1;
+  font-size: 26rpx;
+  color: #333;
+  height: 48rpx;
+  line-height: 48rpx;
+}
+.search-placeholder {
+  color: #bbb;
+  font-size: 26rpx;
+}
+.search-clear {
+  font-size: 24rpx;
+  color: #ccc;
+  padding: 8rpx;
+  flex-shrink: 0;
+}
 .status-tabs {
   display: flex;
   background: #fff;
-  padding: 20rpx 0;
+  padding: 0;
   border-bottom: 1rpx solid #eee;
-  position: sticky;
-  top: 0;
-  z-index: 10;
+  white-space: nowrap;
+  height: 88rpx;
 }
 .tab {
-  flex: 1;
-  text-align: center;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  padding: 0 24rpx;
+  height: 88rpx;
+  position: relative;
+}
+.tab-label {
   font-size: 26rpx;
   color: #666;
-  padding: 10rpx 0;
+  transition: color 0.2s;
 }
-.tab.active {
+.tab.active .tab-label {
   color: #F97316;
   font-weight: bold;
-  border-bottom: 4rpx solid #F97316;
+}
+.tab-badge {
+  font-size: 18rpx;
+  background: #EF4444;
+  color: #fff;
+  border-radius: 20rpx;
+  padding: 2rpx 10rpx;
+  margin-top: 4rpx;
+  min-width: 32rpx;
+  text-align: center;
+  line-height: 1.4;
+}
+.tab.active .tab-badge {
+  background: #F97316;
 }
 .order-list {
   flex: 1;
-  height: calc(100vh - 100rpx);
+  height: calc(100vh - 88rpx - 104rpx);
 }
 .loading-wrap,
 .empty-wrap {
   display: flex;
   flex-direction: column;
   align-items: center;
-  padding-top: 200rpx;
+  padding-top: 160rpx;
 }
 .loading-text {
   font-size: 26rpx;
@@ -318,6 +458,11 @@ export default {
   font-size: 28rpx;
   color: #999;
 }
+.empty-hint {
+  font-size: 24rpx;
+  color: #bbb;
+  margin-top: 12rpx;
+}
 .order-card {
   background: #fff;
   margin: 20rpx;
@@ -325,96 +470,126 @@ export default {
   padding: 24rpx;
   box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
 }
-.order-header {
+.card-body {
+  display: flex;
+  gap: 20rpx;
+  margin-bottom: 20rpx;
+}
+.product-img {
+  width: 160rpx;
+  height: 160rpx;
+  border-radius: 12rpx;
+  background: #f5f5f5;
+  flex-shrink: 0;
+}
+.product-section {
+  flex: 1;
+  min-width: 0;
   display: flex;
+  flex-direction: column;
   justify-content: space-between;
-  align-items: center;
-  margin-bottom: 16rpx;
 }
-.order-no {
-  font-size: 24rpx;
-  color: #999;
+.product-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  gap: 12rpx;
+}
+.product-name {
+  font-size: 28rpx;
+  color: #1a1a1a;
+  font-weight: 500;
+  line-height: 1.4;
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
 }
 .status-badge {
-  font-size: 22rpx;
-  padding: 4rpx 16rpx;
+  font-size: 20rpx;
+  padding: 4rpx 14rpx;
   border-radius: 20rpx;
+  flex-shrink: 0;
+  line-height: 1.5;
 }
-.status-pending {
-  background: #fff7e6;
-  color: #fa8c16;
+.status-pending { background: #FFF7E6; color: #F97316; }
+.status-paid { background: #E0F2FE; color: #0284C7; }
+.status-shipped { background: #E0F2FE; color: #0284C7; }
+.status-completed { background: #F0FDF4; color: #16A34A; }
+.status-refunding { background: #FEF3C7; color: #D97706; }
+.status-refunded { background: #f5f5f5; color: #999; }
+.status-cancelled { background: #f5f5f5; color: #999; }
+.status-refund-rejected { background: #FEE2E2; color: #DC2626; }
+.product-meta {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  margin-top: 10rpx;
 }
-.status-paid {
-  background: #e6f7ff;
-  color: #1890ff;
+.type-tag {
+  display: inline-flex;
+  align-items: center;
+  padding: 3rpx 12rpx;
+  border-radius: 20rpx;
+  font-size: 20rpx;
 }
-.status-shipped {
-  background: #e6f7ff;
-  color: #1890ff;
+.type-icon {
+  font-size: 20rpx;
+  margin-right: 4rpx;
 }
-.status-completed {
-  background: #f6ffed;
-  color: #52c41a;
+.type-text {
+  font-size: 20rpx;
 }
-.status-cancelled,
-.status-refunded {
-  background: #f5f5f5;
-  color: #999;
+.type-activity { background: #FFF7E6; color: #F97316; }
+.type-course { background: #EDE9FE; color: #7C3AED; }
+.type-physical { background: #E0F2FE; color: #0284C7; }
+.type-digital { background: #F0FDF4; color: #16A34A; }
+.type-service { background: #FEF3C7; color: #D97706; }
+.order-no {
+  font-size: 20rpx;
+  color: #bbb;
 }
-.order-body {
+.price-row {
   display: flex;
-  justify-content: space-between;
-  margin-bottom: 16rpx;
+  align-items: baseline;
+  gap: 8rpx;
+  margin-top: 8rpx;
 }
-.product-info {
-  flex: 1;
-}
-.product-name {
-  display: block;
-  font-size: 28rpx;
-  color: #333;
-  margin-bottom: 6rpx;
-}
-.product-type {
-  font-size: 22rpx;
-  color: #999;
-}
-.order-meta {
-  text-align: right;
-}
-.order-amount {
-  display: block;
-  font-size: 30rpx;
+.price {
+  font-size: 32rpx;
   color: #F97316;
   font-weight: bold;
 }
-.order-qty {
+.qty {
   font-size: 22rpx;
   color: #999;
 }
-.order-footer {
+.card-footer {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  border-top: 1rpx solid #f0f0f0;
+  border-top: 1rpx solid #f5f5f5;
   padding-top: 16rpx;
 }
 .order-time {
   font-size: 22rpx;
-  color: #999;
+  color: #bbb;
 }
 .order-actions {
   display: flex;
   gap: 12rpx;
+  align-items: center;
 }
-.btn-small {
+.btn-sm {
   font-size: 22rpx;
   padding: 8rpx 24rpx;
   border-radius: 30rpx;
   border: none;
   line-height: 1.5;
 }
-.btn-small::after {
+.btn-sm::after {
   border: none;
 }
 .btn-cancel {
@@ -426,8 +601,16 @@ export default {
   color: #fff;
 }
 .btn-confirm {
-  background: #e6f7ff;
-  color: #1890ff;
+  background: #E0F2FE;
+  color: #0284C7;
+}
+.btn-refund {
+  background: #FEF3C7;
+  color: #D97706;
+}
+.btn-rebuy {
+  background: #f5f5f5;
+  color: #666;
 }
 .loading-more,
 .no-more {

+ 4 - 0
cfc-frontend/utils/api.js

@@ -1081,6 +1081,10 @@ export const productOrderMy = (params) => {
   return request('/api/product/order/my', 'POST', params)
 }
 
+export const productOrderCounts = () => {
+  return request('/api/product/order/my/counts', 'POST', {})
+}
+
 // 璁㈠崟璇︽儏
 export const productOrderDetail = (params) => {
   return request('/api/product/order/detail', 'POST', params)