payCard.vue 10.3 KB
<!--
 * @Date: 2023-12-20 14:11:11
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2025-08-08 10:33:57
 * @FilePath: /jgdl/src/components/paycard.vue
 * @Description: 文件描述
-->
<template>
  <div class="pay-card">
    <nut-action-sheet v-model:visible="visible" title="" @close="onClose">
      <view style="padding: 2rem 1rem; text-align: center;">
        <view style="font-size: 32rpx;">实付金额</view>
        <view style="color: red; margin: 10rpx 0;"><text style="font-size: 50rpx;">¥</text><text style="font-size: 80rpx;">{{ price }}</text></view>
        <view style="font-size: 28rpx; margin-bottom: 20rpx;">支付剩余时间 <text style="color: red;">{{ formatTime(remain_time) }}</text></view>

        <!-- 协议勾选区域 -->
        <div v-if="!hasAgreed" class="agreement-section">
          <nut-checkbox v-model="isChecked" class="agreement-checkbox">
            <view class="checkbox-text">
              <text>我已阅读并同意</text>
              <text class="agreement-link" @tap.stop="showProtocol">
                《支付协议》
              </text>
            </view>
          </nut-checkbox>
        </div>

        <nut-button
          block
          color="#fb923c"
          :disabled="(!hasAgreed && !isChecked) || isPaymentLoading"
          :loading="isPaymentLoading"
          @tap="goToPay"
        >
          {{ isPaymentLoading ? '处理中...' : '立即支付' }}
        </nut-button>
      </view>
    </nut-action-sheet>

    <!-- 支付协议弹框 -->
    <nut-popup
      v-model:visible="protocolVisible"
      position="right"
      :close-on-click-overlay="true"
      :safe-area-inset-bottom="true"
      :style="{ width: '100%', height: '100%' }"
      @close="protocolVisible = false"
    >
      <view class="protocol-container">
        <!-- 标题栏 -->
        <view class="protocol-header">
          <text class="protocol-title">支付协议</text>
          <view class="close-btn" @click="protocolVisible = false">
            <text class="close-text">×</text>
          </view>
        </view>

        <!-- 内容区域 -->
        <scroll-view class="protocol-scroll" :scroll-y="true">
          <view class="protocol-body">
            <view class="protocol-text">{{ protocolContent }}</view>
          </view>
        </scroll-view>
      </view>
    </nut-popup>
  </div>
</template>

<script setup>
import Taro from '@tarojs/taro'
import { ref, watch, onMounted, onUnmounted } from 'vue'
import { getCurrentPageUrl } from "@/utils/weapp";
import { payAPI, payCheckAPI, getProfileAPI, updateProfileAPI } from '@/api/index'
import { useUserStore } from '@/stores/user'

/**
 * 格式化时间
 * @param {*} seconds
 */
function formatTime(seconds) {
  const hours = Math.floor(seconds / 3600); // 计算小时数
  const minutes = Math.floor((seconds % 3600) / 60); // 计算分钟数
  const remainingSeconds = seconds % 60; // 计算剩余的秒数

  const formattedHours = String(hours).padStart(2, "0"); // 格式化小时数,保证两位数
  const formattedMinutes = String(minutes).padStart(2, "0"); // 格式化分钟数,保证两位数
  const formattedSeconds = String(remainingSeconds).padStart(2, "0"); // 格式化剩余的秒数,保证两位数

  return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
}

const props = defineProps({
  visible: {
    type: Boolean,
    default: false,
  },
  data: {
    type: Object,
    default: () => ({}),
  },
});

const emit = defineEmits(['close', 'paySuccess']);

const userStore = useUserStore()

const visible = ref(false);

// 协议相关状态
const isChecked = ref(false);
const hasAgreed = ref(false);
const protocolVisible = ref(false);

// 支付loading状态
const isPaymentLoading = ref(false);

// 支付协议内容
const protocolContent = ref(`
1. 用户在使用捡个电驴支付服务时,需遵守相关法律法规。
2. 平台有权对异常交易进行风险控制。
3. 用户应确保支付信息的真实性和准确性。
4. 平台将按照约定收取相应的服务费用。
5. 如有争议,双方应友好协商解决。
`)

const onClose = () => {
  visible.value = false;
}

const id = ref('');
const price = ref('');
const remain_time = ref('');

let timeId = null;

watch(
  () => props.visible,
  async (val) => {
    visible.value = val;
    if (val) {
      id.value = props.data.id;
      price.value = props.data.price;
      remain_time.value = props.data.remain_time;
      // 检查用户协议状态
      await checkAgreementStatus();
    }
  }
)

watch(
  () => visible.value,
  (val) => {
    if (!val) {
      emit('close');
    }
  }
)

onMounted(() => {
  // 进入页面后,开始倒计时
  timeId = setInterval(() => {
    remain_time.value ? remain_time.value -= 1 : 0;
    if (remain_time.value === 0) { // 倒计时结束
      clearInterval(timeId);
      visible.value = false;
    }
  }, 1000);
})

onUnmounted(() => {
  timeId && clearInterval(timeId);
})

/**
 * 检查用户是否已同意过协议
 */
const checkAgreementStatus = async () => {
  try {
    // 调用API获取用户信息
    const result = await getProfileAPI()

    if (result.code && result.data) {
      hasAgreed.value = result.data.is_signed || false
      // 更新用户store中的状态
      if (userStore.userInfo) {
        userStore.userInfo.is_signed = result.data.is_signed
      }
    } else {
      // 如果API调用失败,使用store中的数据作为备选
      hasAgreed.value = userStore.userInfo?.is_signed || false
    }
  } catch (error) {
    console.error('获取用户协议状态失败:', error)
    // 如果API调用失败,使用store中的数据作为备选
    hasAgreed.value = userStore.userInfo?.is_signed || false
  }
}

/**
 * 显示支付协议
 */
const showProtocol = () => {
  protocolVisible.value = true
}

/**
 * 处理协议同意
 */
const handleAgreeProtocol = async () => {
  if (!isChecked.value) return

  try {
    // 调用API更新用户协议状态
    const result = await updateProfileAPI({
      is_signed: true
    })

    if (result.code) {
      hasAgreed.value = true
      // 更新用户store中的状态
      if (userStore.userInfo) {
        userStore.userInfo.is_signed = true
      }
    }
  } catch (error) {
    console.error('更新协议状态失败:', error)
    throw error // 重新抛出错误,让goToPay函数处理
  }
}

const goToPay = async () => {
  // 防止重复点击
  if (isPaymentLoading.value) return

  // 检查协议状态
  if (!hasAgreed.value && !isChecked.value) {
    Taro.showToast({
      title: '请先同意支付协议',
      icon: 'none'
    })
    return
  }

  // 设置loading状态
  isPaymentLoading.value = true

  try {
    // 如果用户勾选了协议但还未提交,先处理协议同意
    if (!hasAgreed.value && isChecked.value) {
      await handleAgreeProtocol()
    }

    if (price.value > 0) { // 金额大于0
    // 获取支付参数
    const { code, data } = await payAPI({ order_id: id.value });
    if (code) {
      let pay = data.payargs;
      // 触发微信支付操作
      Taro.requestPayment({
        timeStamp: pay.timeStamp,
        nonceStr: pay.nonceStr,
        package: pay.package,
        signType: pay.signType,
        paySign: pay.paySign,
        success: async () => {
          visible.value = false; // 关闭支付弹框
          Taro.showToast({
            title: '支付成功',
            icon: 'success',
            duration: 1000
          });
          // 支付成功后,调用检查接口
          const pay_success = await payCheckAPI({ order_id: id.value });
          if (pay_success.code) {
            let current_page = getCurrentPageUrl();
            if (current_page === 'pages/myOrders/index') { // 我的订单打开
              // 发出支付成功事件,通知父组件更新订单状态
              emit('paySuccess', { orderId: id.value });
            }
            if (current_page === 'pages/productDetail/index') { // 详情页打开
              // 跳转订单成功页
              Taro.navigateTo({
                url: '/pages/myOrders/index',
              });
            }
            if (current_page === 'pages/setAuthCar/index') { // 发布认证页打开
              // 发出支付成功事件,通知父组件更新订单状态
              emit('paySuccess', { orderId: id.value });
            }
          }
        },
        fail: () => {
          let current_page = getCurrentPageUrl();
          if (current_page === 'pages/setAuthCar/index') { // 发布认证页打开
            Taro.showModal({
              title: '温馨提示',
              content: '需要付费才能认证',
              showCancel: false,
            });
          }
        },
        complete: () => {
          // 支付完成后清除loading状态
          isPaymentLoading.value = false
        }
      });
    }
    }
  } catch (error) {
    console.error('支付过程出错:', error)
    Taro.showToast({
      title: '支付失败,请重试',
      icon: 'error'
    })
  } finally {
    // 确保loading状态被清除
    isPaymentLoading.value = false
  }
}
</script>

<style lang="less">
.pay-card {
  .agreement-section {
    margin: 20rpx 0;
    padding: 0 20rpx;

    .agreement-checkbox {
      // display: flex;
      // align-items: center;
      // justify-content: center;

      :deep(.nut-checkbox__label) {
        margin-left: 8rpx;
      }

      .checkbox-text {
        font-size: 28rpx;
        color: #666;

        .agreement-link {
          color: #fb923c;
          text-decoration: underline;
          margin-left: 8rpx;
        }
      }
    }
  }
}

.protocol-container {
  display: flex;
  flex-direction: column;
  height: 100%;
  background-color: #fff;

  .protocol-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 20rpx 30rpx;
    border-bottom: 1rpx solid #eee;

    .protocol-title {
      font-size: 36rpx;
      font-weight: bold;
      color: #333;
    }

    .close-btn {
      width: 60rpx;
      height: 60rpx;
      display: flex;
      align-items: center;
      justify-content: center;

      .close-text {
        font-size: 48rpx;
        color: #999;
        line-height: 1;
      }
    }
  }

  .protocol-scroll {
    flex: 1;

    .protocol-body {
      padding: 30rpx;

      .protocol-text {
        font-size: 28rpx;
        line-height: 1.6;
        color: #333;
        white-space: pre-line;
      }
    }
  }
}
</style>