CheckoutPage.vue 15 KB
<template>
  <AppLayout title="" :hide-bottom-nav="true" @back-click="router.back()">
    <div v-if="cartItems.length === 0 && !orderComplete" class="h-screen flex flex-col items-center justify-center px-4">
      <FrostedGlass class="p-6 rounded-xl text-center">
        <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 text-gray-400 mx-auto mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
        </svg>
        <h2 class="text-xl font-bold mb-2">购物车为空</h2>
        <p class="text-gray-600 mb-6">您的购物车中没有任何商品</p>
        <button
          @click="router.push('/courses')"
          class="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-xl font-medium"
        >
          浏览课程
        </button>
      </FrostedGlass>
    </div>

    <div v-else-if="!orderComplete" class="pb-20">
      <!-- Order Summary -->
      <div class="p-4 bg-gradient-to-r from-green-500/10 to-blue-500/10">
        <FrostedGlass class="rounded-xl p-4">
          <h3 class="font-medium mb-3">订单摘要</h3>
          <div class="space-y-3">
            <div v-for="item in cartItems" :key="`${item.type}-${item.id}`" class="flex justify-between">
              <div class="flex flex-1">
                <div class="w-12 h-12 rounded-lg overflow-hidden mr-3 flex-shrink-0">
                  <img
                    :src="item.cover || 'https://cdn.ipadbiz.cn/mlaj/images/default_block.png'"
                    :alt="item.title"
                    class="w-full h-full object-cover"
                    @error="handleImageError"
                  />
                </div>
                <div class="flex-1">
                  <p class="font-medium text-sm line-clamp-1">{{ item.title }}</p>
                  <p class="text-xs text-gray-500">
                    {{ item.type === 'course' ? '课程' : '活动' }} · {{ item?.quantity }} 份
                  </p>
                </div>
                <button
                  v-if="mode === 'multiple'"
                  @click="() => {
                    itemToDelete = item
                    showConfirmDialog = true
                  }"
                  class="text-red-500 hover:text-red-600 p-1 -mr-1"
                  title="删除"
                >
                  <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
                  </svg>
                </button>
              </div>
              <div class="ml-2 text-right">
                <p class="font-medium text-sm">{{ formatPrice(item?.price * item.quantity) }}</p>
                <p class="text-xs text-gray-500">{{ formatPrice(item.price) }} / 份</p>
              </div>
            </div>
          </div>

          <div class="mt-4 pt-3 border-t border-gray-200">
            <!-- <div class="flex justify-between items-center text-sm">
              <span class="text-gray-600">小计</span>
              <span class="font-medium">{{ formatPrice(getTotalPrice()) }}</span>
            </div>
            <div class="flex justify-between items-center text-sm mt-1">
              <span class="text-gray-600">优惠</span>
              <span class="text-red-500">- ¥0.00</span>
            </div> -->
            <div class="flex justify-between items-center mt-2 font-medium">
              <span>总计</span>
              <span class="text-lg text-green-600">{{ formatPrice(getTotalPrice()) }}</span>
            </div>
          </div>
        </FrostedGlass>
      </div>

      <!-- Checkout Form -->
      <div class="px-4 pt-4">
        <form @submit.prevent="handleSubmit">
          <FrostedGlass class="rounded-xl p-4 mb-4">
            <h3 class="font-medium mb-3">个人信息</h3>

            <div class="space-y-3">
              <div>
                <label class="block text-sm text-gray-600 mb-1">姓名 <span class="text-red-500">*</span></label>
                <input
                  v-model="formData.receive_name"
                  type="text"
                  class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm"
                  placeholder="请输入您的姓名"
                  required
                />
              </div>

              <div>
                <label class="block text-sm text-gray-600 mb-1">手机号码 <span class="text-red-500">*</span></label>
                <input
                  v-model="formData.receive_phone"
                  type="tel"
                  class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm"
                  placeholder="请输入您的手机号码"
                  required
                />
              </div>

              <!-- <div>
                <label class="block text-sm text-gray-600 mb-1">电子邮箱</label>
                <input
                  v-model="formData.receive_email"
                  type="email"
                  class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm"
                  placeholder="请输入您的邮箱(选填)"
                />
              </div> -->

              <div>
                <!-- <label class="block text-sm text-gray-600 mb-1">联系地址 <span class="text-red-500">*</span></label> -->
                <label class="block text-sm text-gray-600 mb-1">联系地址</label>
                <input
                  v-model="formData.receive_address"
                  type="text"
                  class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm"
                  placeholder="请输入您的详细地址"
                />
              </div>

              <div>
                <label class="block text-sm text-gray-600 mb-1">备注</label>
                <textarea
                  v-model="formData.notes"
                  class="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm resize-none h-20"
                  placeholder="有什么需要我们注意的事项?(选填)"
                />
              </div>
            </div>
          </FrostedGlass>

          <FrostedGlass class="rounded-xl p-4 mb-6">
            <h3 class="font-medium mb-3">支付方式</h3>
            <div class="space-y-2">
              <label class="flex items-center p-3 border border-gray-200 rounded-lg bg-white/50">
                <input
                  v-model="formData.pay_type"
                  type="radio"
                  value="WeChat"
                  class="mr-3"
                />
                <span class="flex-1">微信支付</span>
                <svg class="h-6 w-6 text-green-500" viewBox="0 0 24 24" fill="currentColor">
                  <path d="M9.5,8.5m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
                  <path d="M14.5,8.5m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
                  <path d="M9.5,14.5m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
                  <path d="M14.5,14.5m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
                  <path d="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10s10,-4.5 10,-10S17.5,2 12,2zM19.8,11c-0.2,4.6 -4.1,7.8 -8.8,7.5c-3.8,-0.2 -7.2,-3.2 -7.5,-7.1C3.2,7 6.9,3.1 11.5,3C16.1,3 20,6.9 19.8,11z" />
                </svg>
              </label>

              <!-- <label class="flex items-center p-3 border border-gray-200 rounded-lg bg-white/50">
                <input
                  v-model="formData.paymentMethod"
                  type="radio"
                  value="alipay"
                  class="mr-3"
                />
                <span class="flex-1">支付宝</span>
                <svg class="h-6 w-6 text-blue-500" viewBox="0 0 24 24" fill="currentColor">
                  <path d="M12,2C6.5,2 2,6.5 2,12s4.5,10 10,10s10,-4.5 10,-10S17.5,2 12,2zM12,20.5c-4.7,0 -8.5,-3.8 -8.5,-8.5S7.3,3.5 12,3.5s8.5,3.8 8.5,8.5S16.7,20.5 12,20.5z" />
                  <path d="M12,6c-3.3,0 -6,2.7 -6,6s2.7,6 6,6s6,-2.7 6,-6S15.3,6 12,6zM16,13h-3v3h-2v-3H8v-2h3V8h2v3h3V13z" />
                </svg>
              </label> -->

              <!-- <label class="flex items-center p-3 border border-gray-200 rounded-lg bg-white/50">
                <input
                  v-model="formData.paymentMethod"
                  type="radio"
                  value="bank"
                  class="mr-3"
                />
                <span class="flex-1">银行卡</span>
                <svg class="h-6 w-6 text-gray-500" viewBox="0 0 24 24" fill="currentColor">
                  <path d="M20,4H4C2.9,4 2,4.9 2,6v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6C22,4.9 21.1,4 20,4zM20,18H4V12h16V18zM20,8H4V6h16V8z" />
                  <path d="M5,13h2v2h-2z" />
                  <path d="M9,13h2v2h-2z" />
                  <path d="M13,13h2v2h-2z" />
                  <path d="M17,13h2v2h-2z" />
                </svg>
              </label> -->
            </div>
          </FrostedGlass>

          <div class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 shadow-md px-4 py-3">
            <div class="flex justify-between items-center mb-3">
              <div>
                <span class="text-sm text-gray-500">总计:</span>
                <span class="text-lg font-bold text-green-600">{{ formatPrice(getTotalPrice()) }}</span>
              </div>
              <button
                v-if="mode === 'multiple'"
                type="button"
                @click="() => {
                  showConfirmDialog = true
                  itemToDelete = null
                }"
                class="text-sm text-red-500"
              >
                清空课程
              </button>
            </div>
            <button
              type="submit"
              :disabled="isProcessing"
              class="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-xl font-medium disabled:opacity-70 flex items-center justify-center"
            >
              <template v-if="isProcessing">
                <div class="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div>
                处理中...
              </template>
              <template v-else>
                确认支付
              </template>
            </button>
          </div>
        </form>
      </div>
    </div>

    <!-- Confirm Dialog -->
    <ConfirmDialog
      v-model:show="showConfirmDialog"
      title="确认删除"
      :message="itemToDelete ? `确定要删除 ${itemToDelete.title || '此商品'} 吗?` : '确定要清空课程吗?'"
      @confirm="() => {
        if (itemToDelete) {
          if (cartItems.length === 1) {
            clearCart()
          } else {
            removeFromCart(itemToDelete.id, itemToDelete.type)
          }
          itemToDelete = null
        } else {
          clearCart()
        }
      }"
      @cancel="itemToDelete = null"
    />


    <!-- 支付弹窗 -->
    <van-popup
      v-model:show="showPayment"
      position="bottom"
      round
      :style="{ height: '50%' }"
    >
      <div class="bg-gradient-to-r from-green-500/10 to-blue-500/10 h-full">
        <WechatPayment
          v-if="showPayment && orderId"
          :order-id="orderId"
          cancelBtnText="关闭"
          @success="handlePaymentSuccess"
          @failed="handlePaymentFailed"
          @processing="handlePaymentProcessing"
          @cancel="showPayment = false"
        >
          <template #success-action>
            <button
              @click="handleBackToHome"
              class="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-3 rounded-xl font-medium"
            >
              返回首页
            </button>
          </template>
          <template #failed-action>
            <button
              @click="showPayment = false"
              class="w-full bg-gray-100 text-gray-700 py-3 sm:py-3.5 rounded-xl font-medium text-base sm:text-lg hover:bg-gray-200 transition-colors"
            >
              关闭
            </button>
          </template>
        </WechatPayment>
      </div>
    </van-popup>
  </AppLayout>
</template>

<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import AppLayout from '@/components/layout/AppLayout.vue'
import FrostedGlass from '@/components/ui/FrostedGlass.vue'
import ConfirmDialog from '@/components/ui/ConfirmDialog.vue'
import WechatPayment from '@/components/payment/WechatPayment.vue'
import { useCart } from '@/contexts/cart'
import { useTitle } from '@vueuse/core'

const $route = useRoute()
const $router = useRouter()
useTitle($route.meta.title)
const router = useRouter()
const { items: cartItems, mode, getTotalPrice, handleCheckout, clearCart, removeFromCart } = useCart()

// Form state
const formData = ref({
  receive_name: '',
  receive_phone: '',
  receive_email: '',
  receive_address: '',
  notes: '',
  pay_type: 'WeChat'
})

// 支付相关状态
const showPayment = ref(false)
const orderId = ref('')
const isProcessing = ref(false)
const orderComplete = ref(false)

// 确认对话框状态
const showConfirmDialog = ref(false)
const itemToDelete = ref(null)

// Format price with Chinese Yuan symbol
const formatPrice = (price) => {
  if (!price && price !== 0) return '¥0.00'
  return `¥${Number(price).toFixed(2)}`
}

// Handle image error
const handleImageError = (e) => {
  e.target.src = '/assets/images/course-placeholder.jpg'
}

// 处理表单提交
const handleSubmit = async (e) => {
  try {
    // 表单验证
    if (!formData.value.receive_name?.trim()) {
      throw new Error('请输入姓名')
    }
    if (!formData.value.receive_phone?.trim()) {
      throw new Error('请输入手机号码')
    }
    if (!formData.value.pay_type) {
      throw new Error('请选择支付方式')
    }

    isProcessing.value = true

    // 创建订单
    const result = await handleCheckout(formData.value)
    if (!result?.success) {
      throw new Error(result?.message || '订单创建失败,请重试')
    }

    orderId.value = result.orderId || ''
    if (!orderId.value) {
      throw new Error('订单号获取失败,请重试')
    }

    // 显示支付组件
    showPayment.value = true

  } catch (error) {
    console.error('订单创建失败:', error)
    alert(error.message || '订单创建失败,请重试')
  } finally {
    isProcessing.value = false
  }
}

// 处理支付成功
const handlePaymentSuccess = () => {
  orderComplete.value = true
  clearCart();
  // 返回上一页
  router.go(-1)
}

// 处理支付失败
const handlePaymentFailed = (error) => {
  console.error('支付失败:', error)
  // showPayment.value = false
}

// 处理支付处理中
const handlePaymentProcessing = () => {
  console.log('支付处理中...')
}

// Handle navigation back to home after order completion
const handleBackToHome = () => {
  router.push('/')
}
</script>