index.vue 9.12 KB
<!--
 * @Date: 2024-01-15 16:25:51
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2026-01-24 12:44:25
 * @FilePath: /xyxBooking-weapp/src/pages/submit/index.vue
 * @Description: 预约人员信息
-->
<template>
  <view class="submit-page">
    <view @tap="goToBooking" class="visit-time">
      <view>参访时间</view>
      <view class="flex items-center">
        <text style="font-size: 30rpx">{{ date }} {{ time }}</text>
        <IconFont name="rect-right" class="ml-1" />
      </view>
    </view>
    <view @tap="goToVisitor" class="add-visitors">
      <view class="flex items-center justify-center">
        <IconFont name="plus" class="mr-1" /> 添加参观者
      </view>
    </view>
    <view v-if="visitorList.length" class="visitors-list">
      <view
        v-for="(item, index) in visitorList"
        :key="index"
        @tap="addVisitor(item)"
        class="visitor-item"
      >
        <view style="margin-right: 32rpx">
          <image
            v-if="!checked_visitors.includes(item.id)"
            :src="icon_check1"
            style="width: 38rpx; height: 38rpx"
          />
          <image v-else :src="icon_check2" style="width: 38rpx; height: 38rpx" />
        </view>
        <view>
          <view style="color: #a67939">{{ item.name }}</view>
          <view>证件号:{{ formatId(item.id_number) }}</view>
          <view
            v-if="item.is_reserve === RESERVE_STATUS.ENABLE"
            style="color: #9c9a9a; font-size: 26rpx"
            >*已预约过{{ date }}参观,请不要重复预约</view
          >
        </view>
      </view>
    </view>
    <view v-else class="no-visitors-list">
      <image
        src="https://cdn.ipadbiz.cn/xys/booking/%E6%9A%82%E6%97%A0@2x.png"
        style="width: 320rpx; height: 320rpx"
      />
      <view class="no-visitors-list-title">您还没有添加过参观者</view>
    </view>
    <view style="height: 160rpx"></view>
    <view class="submit-wrapper">
      <view class="control-wrapper">
        <view class="left">
          <view style="margin-left: 32rpx; display: flex; align-items: center">
            订单金额&nbsp;&nbsp;<view style="color: #ff1919; display: inline-block"
              >¥<view style="font-size: 48rpx; display: inline-block">&nbsp;{{ total }}</view>
            </view>
          </view>
        </view>
        <view @tap="submitBtn" class="right">提交订单</view>
      </view>
      <view style="font-size: 27rpx; margin-left: 32rpx; color: #ff1919; margin-bottom: 32rpx"
        >提交后请在10分钟内完成支付</view
      >
    </view>
  </view>
</template>

<script setup>
import { ref, computed } from 'vue'
import Taro, { useDidShow, useRouter as useTaroRouter } from '@tarojs/taro'
import { IconFont } from '@nutui/icons-vue-taro'
import { useGo } from '@/hooks/useGo'
import icon_check1 from '@/assets/images/多选01@2x.png'
import icon_check2 from '@/assets/images/多选02@2x.png'
import { personListAPI, addReserveAPI } from '@/api/index'
import { wechat_pay } from '@/utils/wechatPay'
import { mask_id_number } from '@/utils/tools'
import { showError, showLoading, hideLoading } from '@/utils/toast'

const router = useTaroRouter()
const go = useGo()

const visitorList = ref([])
const date = ref('')
const time = ref('')
const price = ref(0)
const period_type = ref('')
const formatId = id => mask_id_number(id)

/**
 * @description 当天预约标记
 * - ENABLE 表示该参观者今天已预约,不允许重复预约
 * @readonly
 */
const RESERVE_STATUS = {
  ENABLE: '1'
}

const checked_visitors = ref([])
const is_submitting = ref(false) // 是否正在提交订单

/**
 * @description 选择/取消选择参观者
 * - 已预约过当天参观的参观者不允许选择
 * @param {Object} item 参观者数据
 * @returns {void} 无返回值
 */
const addVisitor = item => {
  if (item.is_reserve === RESERVE_STATUS.ENABLE) {
    // 今天已经预约
    showError('已预约过参观,请不要重复预约')
    return
  }
  if (checked_visitors.value.includes(item.id)) {
    checked_visitors.value = checked_visitors.value.filter(v => v !== item.id)
  } else {
    checked_visitors.value.push(item.id)
  }
}

const total = computed(() => {
  return price.value * checked_visitors.value.length
})

/**
 * @description 返回重新选择参访时间页
 * @returns {void} 无返回值
 */
const goToBooking = () => {
  go('/booking')
}

/**
 * @description 跳转新增参观者页
 * @returns {void} 无返回值
 */
const goToVisitor = () => {
  go('/addVisitor')
}

// 待支付订单ID
const pending_pay_id = ref(null)
// 待支付订单是否需要支付
const pending_need_pay = ref(null)

/**
 * @description 刷新参观者列表(并同步“当天已预约”标记)
 * @param {Object} options 选项
 * @param {boolean} options.reset_checked 是否清空已勾选参观者
 * @returns {Promise<void>} 无返回值
 */

const refreshVisitorList = async options => {
  if (!date.value || !time.value) {
    return
  }
  const res = await personListAPI({
    reserve_date: date.value,
    begin_time: time.value.split('-')[0],
    end_time: time.value.split('-')[1],
    period_type: period_type.value
  })
  if (res && res.code) {
    visitorList.value = res.data || []
    if (options?.reset_checked) {
      checked_visitors.value = []
    }
  }
}

/**
 * @description 提交订单
 * - 先创建预约单拿 pay_id(支持"待支付订单"复用)
 * - need_pay=1 时拉起微信支付
 * @returns {Promise<void>} 无返回值
 */
const submitBtn = async () => {
  if (is_submitting.value) {
    return
  }
  if (!checked_visitors.value.length) {
    showError('请先添加参观者')
    return
  }

  is_submitting.value = true
  try {
    let pay_id = pending_pay_id.value
    let need_pay = pending_need_pay.value

    if (!pay_id) {
      // TAG: 提交订单, 如果没有待支付订单ID, 则创建一个新的订单
      showLoading('提交中...')
      let reserve_res = null
      try {
        reserve_res = await addReserveAPI({
          reserve_date: date.value,
          begin_time: time.value.split('-')[0],
          end_time: time.value.split('-')[1],
          person_id_list: JSON.stringify(checked_visitors.value),
          period_type: period_type.value
        })
      } finally {
        hideLoading()
      }

      if (!reserve_res || reserve_res.code !== 1) {
        return
      }
      pay_id = reserve_res.data.pay_id
      pending_pay_id.value = pay_id
      need_pay = reserve_res.data?.need_pay
      pending_need_pay.value = need_pay
      await refreshVisitorList({ reset_checked: true })
    }

    // 以接口返回的 need_pay 为准:1=需要支付,0=不需要支付
    if (Number(need_pay) === 1 || need_pay === true) {
      const pay_res = await wechat_pay({ pay_id })
      if (pay_res && pay_res.code === 1) {
        pending_pay_id.value = null
        pending_need_pay.value = null
        go('/success', { pay_id })
        return
      }
      // 支付失败/取消后,刷新参观者列表
      refreshVisitorList({ reset_checked: true }).catch(() => {})
      // wechat_pay 内部已经处理了弹窗和返回上一页,这里不需要额外处理
    } else {
      pending_pay_id.value = null
      pending_need_pay.value = null
      go('/success', { pay_id })
    }
  } finally {
    is_submitting.value = false
  }
}

useDidShow(async () => {
  const params = router.params
  date.value = params.date || ''
  time.value = params.time || ''
  price.value = params.price || 0
  period_type.value = params.period_type || ''

  await refreshVisitorList()
})
</script>

<style lang="less">
.submit-page {
  margin: 32rpx;
  position: relative;

  .visit-time {
    background-color: #fff;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 24rpx;
    border-radius: 16rpx;
  }

  .add-visitors {
    border: 2rpx dashed #a67939;
    color: #a67939;
    border-radius: 10rpx;
    text-align: center;
    padding: 21rpx 0;
    margin: 32rpx 0;
    font-size: 37rpx;
  }

  .visitors-list {
    .visitor-item {
      background-color: #fff;
      border-radius: 16rpx;
      padding: 32rpx;
      margin-bottom: 32rpx;
      display: flex;
      align-items: center;
    }
  }

  .no-visitors-list {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;

    img {
      margin-top: 32rpx;
      margin-bottom: 32rpx;
      width: 320rpx;
    }

    .no-visitors-list-title {
      color: #a67939;
      font-size: 34rpx;
    }
  }

  .submit-wrapper {
    position: fixed;
    bottom: 0;
    left: 0;
    width: 750rpx;
    display: flex;
    background-color: #fff;
    // padding: 32rpx;
    justify-content: space-between;
    flex-direction: column;
    box-shadow: 0 -10rpx 8rpx 0 rgba(0, 0, 0, 0.12);

    .control-wrapper {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .left {
      display: flex;
      justify-content: center;
      align-items: center;
      flex-wrap: nowrap;
    }

    .right {
      background-color: #a67939;
      color: #fff;
      margin: 32rpx;
      padding: 26rpx 96rpx;
      border-radius: 5px;
      font-size: 35rpx;
      margin-bottom: 0;
    }
  }
}
</style>