uni-app 订单系统

核心知识点

1. 订单系统架构

  • 前端组件:订单创建、订单列表、订单详情、订单操作
  • 后端服务:订单 API、订单处理、状态管理
  • 数据层:订单数据模型、状态流转、历史记录
  • 支付集成:支付网关对接、订单支付、退款处理

2. 订单生命周期

  • 订单创建:购物车转订单、直接创建订单
  • 订单待支付:等待用户支付、支付超时处理
  • 订单处理中:商家确认、商品准备、物流安排
  • 订单已完成:交易完成、评价、售后
  • 订单已取消:用户取消、系统取消、超时取消

3. 订单状态管理

  • 状态定义:明确订单状态及流转规则
  • 状态更新:前端状态同步、后端状态变更
  • 状态监听:实时监听订单状态变化
  • 状态回滚:处理异常情况的状态回滚

4. 订单支付流程

  • 支付方式集成:微信支付、支付宝支付、银行卡支付
  • 支付请求:生成支付参数、调用支付接口
  • 支付回调:处理支付结果、更新订单状态
  • 支付异常:处理支付失败、重复支付

5. 订单管理功能

  • 订单列表:按状态、时间、类型筛选
  • 订单详情:商品信息、收货信息、支付信息
  • 订单操作:取消订单、确认收货、申请退款
  • 订单统计:订单数量、金额、状态分布

实用案例

实现电商订单系统

1. 订单创建组件

<template>
  <view class="order-create">
    <view class="section">
      <text class="section-title">收货信息</text>
      <view class="address-info" @click="selectAddress">
        <uni-icons type="location" size="24" color="#FF6600" />
        <view class="address-content" v-if="selectedAddress">
          <text class="consignee">{{ selectedAddress.consignee }} {{ selectedAddress.phone }}</text>
          <text class="address-detail">{{ selectedAddress.province }}{{ selectedAddress.city }}{{ selectedAddress.district }}{{ selectedAddress.detail }}</text>
        </view>
        <view class="address-content" v-else>
          <text class="no-address">请选择收货地址</text>
        </view>
        <uni-icons type="arrowright" size="20" color="#999" />
      </view>
    </view>
    
    <view class="section">
      <text class="section-title">商品信息</text>
      <view class="product-list">
        <view 
          v-for="(product, index) in products" 
          :key="index"
          class="product-item"
        >
          <image :src="product.image" mode="aspectFill" class="product-image" />
          <view class="product-info">
            <text class="product-title">{{ product.title }}</text>
            <text class="product-spec">{{ product.spec }}</text>
            <view class="product-price-count">
              <text class="product-price">¥{{ product.price.toFixed(2) }}</text>
              <text class="product-count">x{{ product.count }}</text>
            </view>
          </view>
        </view>
      </view>
    </view>
    
    <view class="section">
      <text class="section-title">支付方式</text>
      <uni-radio-group v-model="selectedPayment">
        <label 
          v-for="(payment, index) in paymentMethods" 
          :key="index"
          class="payment-item"
        >
          <uni-radio :value="payment.value" />
          <text class="payment-label">{{ payment.label }}</text>
        </label>
      </uni-radio-group>
    </view>
    
    <view class="section">
      <text class="section-title">订单备注</text>
      <textarea 
        v-model="remark" 
        placeholder="请输入订单备注" 
        placeholder-class="placeholder"
        class="remark-input"
      />
    </view>
    
    <view class="order-summary">
      <view class="summary-item">
        <text class="label">商品金额</text>
        <text class="value">¥{{ subtotal.toFixed(2) }}</text>
      </view>
      <view class="summary-item">
        <text class="label">运费</text>
        <text class="value">¥{{ shippingFee.toFixed(2) }}</text>
      </view>
      <view class="summary-item total">
        <text class="label">订单总额</text>
        <text class="value">¥{{ total.toFixed(2) }}</text>
      </view>
    </view>
    
    <view class="bottom-bar">
      <view class="total-price">
        <text class="label">合计:</text>
        <text class="price">¥{{ total.toFixed(2) }}</text>
      </view>
      <button class="submit-btn" @click="submitOrder">提交订单</button>
    </view>
  </view>
</template>

<script>
import orderApi from '@/api/order';

export default {
  data() {
    return {
      selectedAddress: null,
      products: [],
      selectedPayment: 'wechat',
      paymentMethods: [
        { label: '微信支付', value: 'wechat' },
        { label: '支付宝', value: 'alipay' },
        { label: '银行卡', value: 'bank' }
      ],
      remark: '',
      subtotal: 0,
      shippingFee: 0,
      total: 0
    };
  },
  onLoad(options) {
    // 从购物车或商品详情页获取商品信息
    if (options.products) {
      this.products = JSON.parse(options.products);
    } else {
      // 模拟商品数据
      this.products = [
        {
          id: 1,
          title: 'uni-app 开发实战',
          spec: '纸质书',
          price: 59.90,
          count: 1,
          image: 'https://example.com/book1.jpg'
        },
        {
          id: 2,
          title: 'Vue.js 实战',
          spec: '纸质书',
          price: 49.90,
          count: 1,
          image: 'https://example.com/book2.jpg'
        }
      ];
    }
    
    this.calculatePrice();
    this.loadDefaultAddress();
  },
  methods: {
    calculatePrice() {
      // 计算商品总价
      this.subtotal = this.products.reduce((sum, product) => {
        return sum + product.price * product.count;
      }, 0);
      
      // 计算运费
      this.shippingFee = this.subtotal >= 99 ? 0 : 10;
      
      // 计算订单总额
      this.total = this.subtotal + this.shippingFee;
    },
    
    loadDefaultAddress() {
      // 实际项目中应该从地址管理中获取默认地址
      this.selectedAddress = {
        id: 1,
        consignee: '张三',
        phone: '13800138000',
        province: '北京市',
        city: '北京市',
        district: '朝阳区',
        detail: '望京SOHO T1 C座 2801'
      };
    },
    
    selectAddress() {
      // 跳转到地址选择页面
      uni.navigateTo({
        url: '/pages/address/select',
        success: (res) => {
          res.eventChannel.on('selectAddress', (data) => {
            this.selectedAddress = data.address;
          });
        }
      });
    },
    
    submitOrder() {
      if (!this.selectedAddress) {
        uni.showToast({
          title: '请选择收货地址',
          icon: 'none'
        });
        return;
      }
      
      uni.showLoading({
        title: '提交订单中...'
      });
      
      // 构建订单数据
      const orderData = {
        addressId: this.selectedAddress.id,
        products: this.products.map(product => ({
          productId: product.id,
          quantity: product.count,
          price: product.price
        })),
        paymentMethod: this.selectedPayment,
        remark: this.remark,
        amount: {
          subtotal: this.subtotal,
          shippingFee: this.shippingFee,
          total: this.total
        }
      };
      
      orderApi.createOrder(orderData).then(res => {
        if (res.success) {
          const orderId = res.data.orderId;
          // 跳转到支付页面
          uni.navigateTo({
            url: `/pages/payment/index?orderId=${orderId}&amount=${this.total}`
          });
        } else {
          uni.showToast({
            title: '订单创建失败',
            icon: 'none'
          });
        }
      }).catch(error => {
        console.error('提交订单失败:', error);
        uni.showToast({
          title: '网络错误,请稍后重试',
          icon: 'none'
        });
      }).finally(() => {
        uni.hideLoading();
      });
    }
  }
};
</script>

<style scoped>
.order-create {
  min-height: 100vh;
  background-color: #F5F5F5;
  padding-bottom: 120rpx;
}

.section {
  background-color: #FFFFFF;
  margin-bottom: 10rpx;
  padding: 20rpx;
}

.section-title {
  font-size: 28rpx;
  font-weight: bold;
  color: #333;
  margin-bottom: 20rpx;
}

.address-info {
  display: flex;
  align-items: flex-start;
  padding: 15rpx 0;
}

.address-content {
  flex: 1;
  margin-left: 15rpx;
}

.consignee {
  font-size: 28rpx;
  color: #333;
  margin-bottom: 10rpx;
  display: block;
}

.address-detail {
  font-size: 24rpx;
  color: #666;
  line-height: 1.4;
}

.no-address {
  font-size: 28rpx;
  color: #999;
}

.product-list {
  padding: 10rpx 0;
}

.product-item {
  display: flex;
  margin-bottom: 20rpx;
}

.product-image {
  width: 120rpx;
  height: 120rpx;
  border-radius: 8rpx;
}

.product-info {
  flex: 1;
  margin-left: 15rpx;
}

.product-title {
  font-size: 28rpx;
  color: #333;
  margin-bottom: 10rpx;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.product-spec {
  font-size: 24rpx;
  color: #999;
  margin-bottom: 15rpx;
}

.product-price-count {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.product-price {
  font-size: 28rpx;
  color: #FF6600;
  font-weight: bold;
}

.product-count {
  font-size: 24rpx;
  color: #666;
}

.payment-item {
  display: flex;
  align-items: center;
  margin-bottom: 20rpx;
}

.payment-label {
  margin-left: 15rpx;
  font-size: 26rpx;
  color: #333;
}

.remark-input {
  width: 100%;
  height: 150rpx;
  padding: 15rpx;
  border: 1rpx solid #E0E0E0;
  border-radius: 8rpx;
  font-size: 26rpx;
  color: #333;
  background-color: #F9F9F9;
}

.placeholder {
  color: #999;
}

.order-summary {
  background-color: #FFFFFF;
  margin-bottom: 10rpx;
  padding: 20rpx;
}

.summary-item {
  display: flex;
  justify-content: space-between;
  margin-bottom: 15rpx;
  font-size: 26rpx;
}

.summary-item.total {
  font-weight: bold;
  margin-top: 10rpx;
  padding-top: 15rpx;
  border-top: 1rpx solid #E0E0E0;
}

.label {
  color: #666;
}

.value {
  color: #333;
}

.summary-item.total .value {
  color: #FF6600;
}

.bottom-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20rpx;
  background-color: #FFFFFF;
  border-top: 1rpx solid #E0E0E0;
  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
}

.total-price {
  display: flex;
  align-items: center;
}

.total-price .label {
  font-size: 26rpx;
  color: #333;
}

.total-price .price {
  font-size: 32rpx;
  color: #FF6600;
  font-weight: bold;
  margin-left: 10rpx;
}

.submit-btn {
  width: 200rpx;
  height: 80rpx;
  background-color: #FF6600;
  color: #FFFFFF;
  border-radius: 40rpx;
  font-size: 28rpx;
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>

2. 订单 API 服务

// api/order.js
import request from './request';

export default {
  // 创建订单
  async createOrder(orderData) {
    return request({
      url: '/api/order/create',
      method: 'POST',
      data: orderData
    });
  },
  
  // 获取订单列表
  async getOrderList(params = {}) {
    return request({
      url: '/api/order/list',
      method: 'GET',
      params: {
        status: '',
        page: 1,
        pageSize: 10,
        ...params
      }
    });
  },
  
  // 获取订单详情
  async getOrderDetail(orderId) {
    return request({
      url: `/api/order/detail/${orderId}`,
      method: 'GET'
    });
  },
  
  // 取消订单
  async cancelOrder(orderId, reason = '') {
    return request({
      url: `/api/order/cancel/${orderId}`,
      method: 'POST',
      data: {
        reason
      }
    });
  },
  
  // 确认收货
  async confirmReceipt(orderId) {
    return request({
      url: `/api/order/confirm/${orderId}`,
      method: 'POST'
    });
  },
  
  // 申请退款
  async applyRefund(orderId, data) {
    return request({
      url: `/api/order/refund/${orderId}`,
      method: 'POST',
      data
    });
  },
  
  // 获取订单状态
  async getOrderStatus(orderId) {
    return request({
      url: `/api/order/status/${orderId}`,
      method: 'GET'
    });
  },
  
  // 支付订单
  async payOrder(orderId, paymentData) {
    return request({
      url: `/api/order/pay/${orderId}`,
      method: 'POST',
      data: paymentData
    });
  },
  
  // 订单统计
  async getOrderStats() {
    return request({
      url: '/api/order/stats',
      method: 'GET'
    });
  }
};

3. 订单列表页面

<template>
  <view class="order-list">
    <view class="tab-bar">
      <uni-segmented-control 
        :values="['全部', '待支付', '待发货', '待收货', '已完成', '已取消']" 
        :current="activeTab"
        @clickItem="handleTabChange"
        style-type="button"
        active-color="#FF6600"
      />
    </view>
    
    <uni-refresher 
      v-model="refreshing" 
      @refresh="onRefresh"
      :contentdown="{ content: '下拉刷新' }"
      :contentover="{ content: '释放刷新' }"
      :contentrefresh="{ content: '刷新中...' }"
    >
      <view v-if="loading" class="loading-state">
        <uni-icons type="spinner" size="30" color="#FF6600" animation="spin" />
        <text class="loading-text">加载中...</text>
      </view>
      
      <view v-else-if="orders.length > 0" class="order-items">
        <view 
          v-for="(order, index) in orders" 
          :key="order.id"
          class="order-item"
        >
          <view class="order-header">
            <text class="order-time">{{ formatDate(order.createdAt) }}</text>
            <text :class="['order-status', order.status]"
              >{{ getStatusText(order.status) }}</text>
          </view>
          
          <view class="order-products">
            <view 
              v-for="(product, pIndex) in order.products" 
              :key="pIndex"
              class="product-item"
            >
              <image :src="product.image" mode="aspectFill" class="product-image" />
              <view class="product-info">
                <text class="product-title">{{ product.title }}</text>
                <text class="product-spec">{{ product.spec }}</text>
                <view class="product-price-count">
                  <text class="product-price">¥{{ product.price.toFixed(2) }}</text>
                  <text class="product-count">x{{ product.quantity }}</text>
                </view>
              </view>
            </view>
          </view>
          
          <view class="order-footer">
            <view class="order-amount">
              <text class="label">共{{ order.products.length }}件商品</text>
              <text class="amount">合计:¥{{ order.amount.total.toFixed(2) }}</text>
            </view>
            
            <view class="order-actions">
              <button 
                v-if="order.status === 'pending_payment'"
                class="action-btn secondary"
                @click="cancelOrder(order.id)"
              >
                取消订单
              </button>
              <button 
                v-if="order.status === 'pending_payment'"
                class="action-btn primary"
                @click="payOrder(order.id, order.amount.total)"
              >
                去支付
              </button>
              <button 
                v-if="order.status === 'pending_delivery'"
                class="action-btn primary"
                @click="viewLogistics(order.id)"
              >
                查看物流
              </button>
              <button 
                v-if="order.status === 'pending_receipt'"
                class="action-btn primary"
                @click="confirmReceipt(order.id)"
              >
                确认收货
              </button>
              <button 
                v-if="order.status === 'completed'"
                class="action-btn secondary"
                @click="viewOrderDetail(order.id)"
              >
                查看详情
              </button>
              <button 
                v-if="order.status === 'completed'"
                class="action-btn primary"
                @click="reviewOrder(order.id)"
              >
                去评价
              </button>
            </view>
          </view>
        </view>
        
        <view v-if="hasMore" class="load-more" @click="loadMore">
          <text>加载更多</text>
        </view>
      </view>
      
      <view v-else class="empty-state">
        <uni-icons type="document" size="60" color="#CCCCCC" />
        <text class="empty-text">暂无订单</text>
      </view>
    </uni-refresher>
  </view>
</template>

<script>
import orderApi from '@/api/order';

export default {
  data() {
    return {
      activeTab: 0,
      orders: [],
      loading: false,
      refreshing: false,
      page: 1,
      pageSize: 10,
      hasMore: true,
      statusMap: {
        0: '',
        1: 'pending_payment',
        2: 'pending_delivery',
        3: 'pending_receipt',
        4: 'completed',
        5: 'cancelled'
      }
    };
  },
  onLoad() {
    this.loadOrders();
  },
  methods: {
    loadOrders() {
      this.loading = true;
      
      const status = this.statusMap[this.activeTab];
      
      orderApi.getOrderList({
        status,
        page: this.page,
        pageSize: this.pageSize
      }).then(res => {
        if (res.success) {
          const newOrders = res.data.orders || [];
          if (this.page === 1) {
            this.orders = newOrders;
          } else {
            this.orders = [...this.orders, ...newOrders];
          }
          this.hasMore = newOrders.length === this.pageSize;
        }
      }).catch(error => {
        console.error('加载订单失败:', error);
      }).finally(() => {
        this.loading = false;
        this.refreshing = false;
      });
    },
    
    handleTabChange(e) {
      this.activeTab = e.current;
      this.page = 1;
      this.orders = [];
      this.hasMore = true;
      this.loadOrders();
    },
    
    onRefresh() {
      this.refreshing = true;
      this.page = 1;
      this.loadOrders();
    },
    
    loadMore() {
      if (this.loading || !this.hasMore) return;
      
      this.page++;
      this.loadOrders();
    },
    
    getStatusText(status) {
      const statusTextMap = {
        'pending_payment': '待支付',
        'pending_delivery': '待发货',
        'pending_receipt': '待收货',
        'completed': '已完成',
        'cancelled': '已取消'
      };
      return statusTextMap[status] || status;
    },
    
    formatDate(dateString) {
      const date = new Date(dateString);
      return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
    },
    
    cancelOrder(orderId) {
      uni.showModal({
        title: '确认取消',
        content: '确定要取消该订单吗?',
        success: (res) => {
          if (res.confirm) {
            orderApi.cancelOrder(orderId).then(res => {
              if (res.success) {
                uni.showToast({
                  title: '订单已取消',
                  icon: 'success'
                });
                this.onRefresh();
              }
            });
          }
        }
      });
    },
    
    payOrder(orderId, amount) {
      uni.navigateTo({
        url: `/pages/payment/index?orderId=${orderId}&amount=${amount}`
      });
    },
    
    viewLogistics(orderId) {
      uni.navigateTo({
        url: `/pages/logistics/index?orderId=${orderId}`
      });
    },
    
    confirmReceipt(orderId) {
      uni.showModal({
        title: '确认收货',
        content: '确定已收到商品吗?',
        success: (res) => {
          if (res.confirm) {
            orderApi.confirmReceipt(orderId).then(res => {
              if (res.success) {
                uni.showToast({
                  title: '收货成功',
                  icon: 'success'
                });
                this.onRefresh();
              }
            });
          }
        }
      });
    },
    
    viewOrderDetail(orderId) {
      uni.navigateTo({
        url: `/pages/order/detail?id=${orderId}`
      });
    },
    
    reviewOrder(orderId) {
      uni.navigateTo({
        url: `/pages/order/review?id=${orderId}`
      });
    }
  }
};
</script>

<style scoped>
.order-list {
  min-height: 100vh;
  background-color: #F5F5F5;
}

.tab-bar {
  background-color: #FFFFFF;
  padding: 10rpx;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  position: sticky;
  top: 0;
  z-index: 10;
}

.loading-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 100rpx 0;
}

.loading-text {
  margin-top: 20rpx;
  font-size: 28rpx;
  color: #666;
}

.order-items {
  padding: 10rpx;
}

.order-item {
  background-color: #FFFFFF;
  border-radius: 10rpx;
  margin-bottom: 20rpx;
  overflow: hidden;
  box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}

.order-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20rpx;
  border-bottom: 1rpx solid #F0F0F0;
}

.order-time {
  font-size: 24rpx;
  color: #999;
}

.order-status {
  font-size: 26rpx;
  font-weight: bold;
}

.order-status.pending_payment {
  color: #FF9800;
}

.order-status.pending_delivery {
  color: #2196F3;
}

.order-status.pending_receipt {
  color: #4CAF50;
}

.order-status.completed {
  color: #9E9E9E;
}

.order-status.cancelled {
  color: #9E9E9E;
}

.order-products {
  padding: 20rpx;
}

.product-item {
  display: flex;
  margin-bottom: 20rpx;
}

.product-item:last-child {
  margin-bottom: 0;
}

.product-image {
  width: 120rpx;
  height: 120rpx;
  border-radius: 8rpx;
}

.product-info {
  flex: 1;
  margin-left: 15rpx;
}

.product-title {
  font-size: 26rpx;
  color: #333;
  margin-bottom: 10rpx;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.product-spec {
  font-size: 24rpx;
  color: #999;
  margin-bottom: 15rpx;
}

.product-price-count {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.product-price {
  font-size: 26rpx;
  color: #333;
  font-weight: bold;
}

.product-count {
  font-size: 24rpx;
  color: #666;
}

.order-footer {
  padding: 20rpx;
  border-top: 1rpx solid #F0F0F0;
}

.order-amount {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  margin-bottom: 20rpx;
  font-size: 26rpx;
}

.order-amount .label {
  color: #666;
  margin-right: 10rpx;
}

.order-amount .amount {
  color: #333;
  font-weight: bold;
}

.order-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10rpx;
}

.action-btn {
  padding: 10rpx 20rpx;
  border-radius: 20rpx;
  font-size: 24rpx;
  min-width: 120rpx;
}

.action-btn.primary {
  background-color: #FF6600;
  color: #FFFFFF;
}

.action-btn.secondary {
  background-color: #FFFFFF;
  color: #666;
  border: 1rpx solid #E0E0E0;
}

.load-more {
  text-align: center;
  padding: 30rpx 0;
  color: #666;
  font-size: 28rpx;
}

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 150rpx 0;
  color: #999;
}

.empty-text {
  margin-top: 20rpx;
  font-size: 28rpx;
}
</style>

4. 订单详情页面

<template>
  <view class="order-detail">
    <view class="section">
      <view class="order-status">
        <text class="status-icon">{{ getStatusIcon(order.status) }}</text>
        <text class="status-text">{{ getStatusText(order.status) }}</text>
        <text class="status-desc">{{ getStatusDesc(order.status) }}</text>
      </view>
    </view>
    
    <view class="section">
      <text class="section-title">收货信息</text>
      <view class="address-info">
        <uni-icons type="location" size="24" color="#FF6600" />
        <view class="address-content">
          <text class="consignee">{{ order.address.consignee }} {{ order.address.phone }}</text>
          <text class="address-detail">{{ order.address.province }}{{ order.address.city }}{{ order.address.district }}{{ order.address.detail }}</text>
        </view>
      </view>
    </view>
    
    <view class="section">
      <text class="section-title">商品信息</text>
      <view class="product-list">
        <view 
          v-for="(product, index) in order.products" 
          :key="index"
          class="product-item"
        >
          <image :src="product.image" mode="aspectFill" class="product-image" />
          <view class="product-info">
            <text class="product-title">{{ product.title }}</text>
            <text class="product-spec">{{ product.spec }}</text>
            <view class="product-price-count">
              <text class="product-price">¥{{ product.price.toFixed(2) }}</text>
              <text class="product-count">x{{ product.quantity }}</text>
            </view>
          </view>
        </view>
      </view>
    </view>
    
    <view class="section">
      <text class="section-title">订单信息</text>
      <view class="order-info">
        <view class="info-item">
          <text class="label">订单编号</text>
          <text class="value">{{ order.orderNo }}</text>
        </view>
        <view class="info-item">
          <text class="label">创建时间</text>
          <text class="value">{{ formatDate(order.createdAt) }}</text>
        </view>
        <view class="info-item" v-if="order.paymentTime">
          <text class="label">支付时间</text>
          <text class="value">{{ formatDate(order.paymentTime) }}</text>
        </view>
        <view class="info-item" v-if="order.deliveryTime">
          <text class="label">发货时间</text>
          <text class="value">{{ formatDate(order.deliveryTime) }}</text>
        </view>
        <view class="info-item" v-if="order.completeTime">
          <text class="label">完成时间</text>
          <text class="value">{{ formatDate(order.completeTime) }}</text>
        </view>
        <view class="info-item">
          <text class="label">支付方式</text>
          <text class="value">{{ getPaymentText(order.paymentMethod) }}</text>
        </view>
        <view class="info-item" v-if="order.remark">
          <text class="label">订单备注</text>
          <text class="value">{{ order.remark }}</text>
        </view>
      </view>
    </view>
    
    <view class="section">
      <text class="section-title">费用明细</text>
      <view class="cost-detail">
        <view class="cost-item">
          <text class="label">商品金额</text>
          <text class="value">¥{{ order.amount.subtotal.toFixed(2) }}</text>
        </view>
        <view class="cost-item">
          <text class="label">运费</text>
          <text class="value">¥{{ order.amount.shippingFee.toFixed(2) }}</text>
        </view>
        <view class="cost-item total">
          <text class="label">订单总额</text>
          <text class="value">¥{{ order.amount.total.toFixed(2) }}</text>
        </view>
      </view>
    </view>
    
    <view class="bottom-bar" v-if="showActions">
      <view class="actions">
        <button 
          v-if="order.status === 'pending_payment'"
          class="action-btn secondary"
          @click="cancelOrder"
        >
          取消订单
        </button>
        <button 
          v-if="order.status === 'pending_payment'"
          class="action-btn primary"
          @click="payOrder"
        >
          去支付
        </button>
        <button 
          v-if="order.status === 'pending_delivery'"
          class="action-btn primary"
          @click="viewLogistics"
        >
          查看物流
        </button>
        <button 
          v-if="order.status === 'pending_receipt'"
          class="action-btn primary"
          @click="confirmReceipt"
        >
          确认收货
        </button>
        <button 
          v-if="order.status === 'completed'"
          class="action-btn primary"
          @click="reviewOrder"
        >
          去评价
        </button>
      </view>
    </view>
  </view>
</template>

<script>
import orderApi from '@/api/order';

export default {
  data() {
    return {
      order: {},
      loading: true
    };
  },
  computed: {
    showActions() {
      const status = this.order.status;
      return ['pending_payment', 'pending_delivery', 'pending_receipt', 'completed'].includes(status);
    }
  },
  onLoad(options) {
    if (options.id) {
      this.loadOrderDetail(options.id);
    }
  },
  methods: {
    loadOrderDetail(orderId) {
      this.loading = true;
      orderApi.getOrderDetail(orderId).then(res => {
        if (res.success) {
          this.order = res.data;
        } else {
          uni.showToast({
            title: '加载订单失败',
            icon: 'none'
          });
        }
      }).catch(error => {
        console.error('加载订单详情失败:', error);
      }).finally(() => {
        this.loading = false;
      });
    },
    
    getStatusText(status) {
      const statusTextMap = {
        'pending_payment': '待支付',
        'pending_delivery': '待发货',
        'pending_receipt': '待收货',
        'completed': '已完成',
        'cancelled': '已取消'
      };
      return statusTextMap[status] || status;
    },
    
    getStatusIcon(status) {
      const iconMap = {
        'pending_payment': 'time',
        'pending_delivery': 'truck',
        'pending_receipt': 'package',
        'completed': 'checkmarkcircle',
        'cancelled': 'closecircle'
      };
      return iconMap[status] || 'document';
    },
    
    getStatusDesc(status) {
      const descMap = {
        'pending_payment': '请尽快完成支付',
        'pending_delivery': '商家正在处理您的订单',
        'pending_receipt': '商品正在配送中',
        'completed': '交易已完成',
        'cancelled': '订单已取消'
      };
      return descMap[status] || '';
    },
    
    getPaymentText(method) {
      const paymentMap = {
        'wechat': '微信支付',
        'alipay': '支付宝',
        'bank': '银行卡支付'
      };
      return paymentMap[method] || method;
    },
    
    formatDate(dateString) {
      const date = new Date(dateString);
      return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
    },
    
    cancelOrder() {
      uni.showModal({
        title: '确认取消',
        content: '确定要取消该订单吗?',
        success: (res) => {
          if (res.confirm) {
            orderApi.cancelOrder(this.order.id).then(res => {
              if (res.success) {
                uni.showToast({
                  title: '订单已取消',
                  icon: 'success'
                });
                this.loadOrderDetail(this.order.id);
              }
            });
          }
        }
      });
    },
    
    payOrder() {
      uni.navigateTo({
        url: `/pages/payment/index?orderId=${this.order.id}&amount=${this.order.amount.total}`
      });
    },
    
    viewLogistics() {
      uni.navigateTo({
        url: `/pages/logistics/index?orderId=${this.order.id}`
      });
    },
    
    confirmReceipt() {
      uni.showModal({
        title: '确认收货',
        content: '确定已收到商品吗?',
        success: (res) => {
          if (res.confirm) {
            orderApi.confirmReceipt(this.order.id).then(res => {
              if (res.success) {
                uni.showToast({
                  title: '收货成功',
                  icon: 'success'
                });
                this.loadOrderDetail(this.order.id);
              }
            });
          }
        }
      });
    },
    
    reviewOrder() {
      uni.navigateTo({
        url: `/pages/order/review?id=${this.order.id}`
      });
    }
  }
};
</script>

<style scoped>
.order-detail {
  min-height: 100vh;
  background-color: #F5F5F5;
}

.section {
  background-color: #FFFFFF;
  margin-bottom: 10rpx;
  padding: 20rpx;
}

.section-title {
  font-size: 28rpx;
  font-weight: bold;
  color: #333;
  margin-bottom: 20rpx;
}

.order-status {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 30rpx 0;
  background-color: #FFF3E0;
  border-radius: 10rpx;
  margin-bottom: 20rpx;
}

.status-icon {
  font-size: 60rpx;
  color: #FF6600;
  margin-bottom: 15rpx;
}

.status-text {
  font-size: 32rpx;
  font-weight: bold;
  color: #333;
  margin-bottom: 10rpx;
}

.status-desc {
  font-size: 24rpx;
  color: #666;
}

.address-info {
  display: flex;
  align-items: flex-start;
}

.address-content {
  flex: 1;
  margin-left: 15rpx;
}

.consignee {
  font-size: 26rpx;
  color: #333;
  margin-bottom: 10rpx;
  display: block;
}

.address-detail {
  font-size: 24rpx;
  color: #666;
  line-height: 1.4;
}

.product-list {
  padding: 10rpx 0;
}

.product-item {
  display: flex;
  margin-bottom: 20rpx;
}

.product-item:last-child {
  margin-bottom: 0;
}

.product-image {
  width: 120rpx;
  height: 120rpx;
  border-radius: 8rpx;
}

.product-info {
  flex: 1;
  margin-left: 15rpx;
}

.product-title {
  font-size: 26rpx;
  color: #333;
  margin-bottom: 10rpx;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.product-spec {
  font-size: 24rpx;
  color: #999;
  margin-bottom: 15rpx;
}

.product-price-count {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.product-price {
  font-size: 26rpx;
  color: #333;
  font-weight: bold;
}

.product-count {
  font-size: 24rpx;
  color: #666;
}

.order-info {
  padding: 10rpx 0;
}

.info-item {
  display: flex;
  margin-bottom: 15rpx;
  font-size: 26rpx;
}

.info-item .label {
  width: 120rpx;
  color: #666;
}

.info-item .value {
  flex: 1;
  color: #333;
}

.cost-detail {
  padding: 10rpx 0;
}

.cost-item {
  display: flex;
  justify-content: space-between;
  margin-bottom: 15rpx;
  font-size: 26rpx;
}

.cost-item.total {
  font-weight: bold;
  margin-top: 10rpx;
  padding-top: 15rpx;
  border-top: 1rpx solid #E0E0E0;
}

.cost-item .label {
  color: #666;
}

.cost-item .value {
  color: #333;
}

.cost-item.total .value {
  color: #FF6600;
}

.bottom-bar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background-color: #FFFFFF;
  border-top: 1rpx solid #E0E0E0;
  padding: 20rpx;
  box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
}

.actions {
  display: flex;
  justify-content: flex-end;
  gap: 10rpx;
}

.action-btn {
  padding: 10rpx 20rpx;
  border-radius: 20rpx;
  font-size: 24rpx;
  min-width: 120rpx;
}

.action-btn.primary {
  background-color: #FF6600;
  color: #FFFFFF;
}

.action-btn.secondary {
  background-color: #FFFFFF;
  color: #666;
  border: 1rpx solid #E0E0E0;
}
</style>

实用技巧

1. 订单状态管理

  • 状态枚举:使用枚举定义订单状态,确保状态一致性
  • 状态流转:定义清晰的状态流转规则,避免状态混乱
  • 状态监听:使用 WebSocket 或轮询实时监听状态变化
  • 状态持久化:确保状态变更持久化到后端数据库

2. 订单支付安全

  • 签名验证:确保支付请求和回调的签名验证
  • 订单金额校验:前端显示金额与后端计算金额一致性校验
  • 重复支付防护:防止用户重复支付同一订单
  • 支付超时处理:设置合理的支付超时时间

3. 订单系统性能优化

  • 分页加载:订单列表使用分页加载,避免一次性加载过多数据
  • 数据缓存:缓存订单列表和详情数据,减少重复请求
  • 异步处理:订单创建、支付等操作使用异步处理
  • 批量操作:支持批量订单操作,提高处理效率

4. 订单异常处理

  • 网络异常:处理网络不稳定导致的订单操作失败
  • 支付异常:处理支付失败、退款等异常情况
  • 物流异常:处理物流信息异常、包裹丢失等情况
  • 系统异常:处理系统崩溃、数据丢失等极端情况

5. 用户体验优化

  • 订单状态清晰:使用直观的状态名称和图标
  • 操作引导:为用户提供清晰的操作引导
  • 进度追踪:提供订单处理进度的实时追踪
  • 通知提醒:重要状态变更时及时通知用户

总结

通过本教程的学习,你已经掌握了 uni-app 订单系统的完整实现方法,包括:

  1. 订单系统架构设计:了解了订单系统的前端组件、后端服务、数据层、支付集成等核心组成部分

  2. 订单生命周期管理:掌握了订单从创建到完成的完整生命周期管理

  3. 订单状态管理:学会了订单状态的定义、更新、监听和回滚

  4. 订单支付流程:掌握了支付方式集成、支付请求、支付回调、异常处理等

  5. 订单管理功能:实现了订单列表、订单详情、订单操作等核心功能

  6. 性能优化和用户体验:应用了多种性能优化策略和用户体验提升技巧

订单系统是电商应用的核心功能之一,它不仅关系到用户的购物体验,还直接影响到商家的运营效率。在实际项目中,你可以根据具体业务需求对本教程中的实现进行扩展和优化,构建更加完善的订单系统。

学习目标

  • 掌握 uni-app 订单系统的完整实现方法
  • 理解订单系统的架构设计和数据流程
  • 学会订单状态管理和支付流程集成
  • 掌握订单系统的性能优化和异常处理
  • 实现订单创建、支付、物流、售后等完整功能
  • 构建安全、高效、用户友好的订单系统

通过本教程的学习,你已经具备了开发高质量订单系统的能力,可以在实际项目中灵活应用这些知识,为用户提供流畅的购物体验。

« 上一篇 uni-app 推荐系统 下一篇 » uni-app 购物车系统