shinnosuke
4 天以前 5ce0d76154bf4c1d70543476cdfad9a30195b0f8
rencaika/pages/free-benefit/free-benefit.uvue
@@ -10,7 +10,7 @@
         </view>
      </view>
      <scroll-view class="content" scroll-y="true" show-scrollbar="false">
      <scroll-view class="content" scroll-y="true" :show-scrollbar="false">
         <view v-if="loading" class="page-state"><view class="loading-spinner"></view><text>正在加载免费权益</text></view>
         <view v-else-if="loadFailed" class="page-state"><text>免费权益加载失败</text><text class="retry-text" @tap="loadBenefit">重新加载</text></view>
         <view v-else-if="!available" class="page-state empty-state"><text class="empty-title">暂无可用免费券</text><text class="empty-copy">{{unavailableReason}}</text><text class="retry-text" @tap="loadBenefit">重新加载</text></view>
@@ -18,19 +18,28 @@
            <text class="benefit-name">{{couponName}}</text>
            <view class="voucher-code" hover-class="voucher-code-active" @tap="refreshCode">
               <image v-if="codeImage != ''" class="code-image" :src="codeImage" mode="aspectFit" @error="handleCodeImageError"></image>
               <text v-else-if="codeText != ''" class="code-value">{{codeText}}</text>
               <view v-else class="code-state">
                  <view v-if="codeLoading" class="loading-spinner small-spinner"></view>
                  <text>{{codeMessage}}</text>
                  <text v-if="codeFailed" class="retry-text" @tap.stop="refreshCode">点击重试</text>
               </view>
            </view>
            <text class="code-tip">{{codeImage != '' ? '动态券码将自动刷新,点击二维码可手动刷新' : '二维码不可用时请勿向商家出示'}}</text>
            <text class="remaining-count">剩余可用 {{remainingCount}} 次</text>
            <text class="valid-end">有效期至 {{validEnd}}</text>
            <text class="code-tip">{{codeImage != '' ? '免费券码60秒内有效,点击二维码可重新生成' : (codeText != '' ? '二维码不可用,请向商家出示完整核销码' : '核销码不可用时请勿向商家出示')}}</text>
            <text class="countdown-text">{{countdown > 0 ? countdown + '秒后失效' : '核销码已失效'}}</text>
            <view class="quota-overview">
               <view class="quota-item"><text class="quota-value">{{quotaTotal}}</text><text class="quota-label">可用总次数</text></view>
               <view class="quota-line"></view>
               <view class="quota-item"><text class="quota-value used-value">{{quotaUsed}}</text><text class="quota-label">已使用</text></view>
               <view class="quota-line"></view>
               <view class="quota-item"><text class="quota-value remaining-value">{{remainingCount}}</text><text class="quota-label">当前剩余</text></view>
            </view>
            <text class="reset-cycle">额度重置周期:{{resetCycle}}</text>
            <text class="valid-end">券码有效期至 {{validEnd}}</text>
            <view class="card-divider"></view>
            <view class="benefit-detail">
               <text class="detail-title">权益详情</text>
               <view class="detail-content"><text>{{description}}</text></view>
               <view class="detail-content"><text>{{quotaDetail != '' ? quotaDetail : description}}</text></view>
            </view>
         </view>
         <view class="content-bottom"></view>
@@ -39,8 +48,8 @@
</template>
<script setup lang="uts">
   import { apiEnvironment } from '../../config/api.uts'
   import { getFreeBenefit, issueFreeBenefitCode } from '../../services/portal.uts'
   import { apiEnvironment, getApiEndpoint, isApiConfigured } from '../../config/api.uts'
   import { getBenefitQuota, issueBenefitVerificationCode } from '../../services/portal.uts'
   import { resolveApiUrl } from '../../services/http.uts'
   import { getSession } from '../../services/session.uts'
   import { safeBack } from '../../utils/navigation.uts'
@@ -50,20 +59,29 @@
   const navWidth = ref(88)
   const navRight = ref(10)
   const talentId = ref('')
   const cardId = ref('')
   const merchantId = ref('')
   const loading = ref(true)
   const loadFailed = ref(false)
   const available = ref(false)
   const unavailableReason = ref('该权益当前不可使用')
   const couponName = ref('免费入住券')
   const quotaTotal = ref(0)
   const quotaUsed = ref(0)
   const remainingCount = ref(0)
   const resetCycle = ref('-')
   const countdown = ref(0)
   const validEnd = ref('-')
   const quotaDetail = ref('')
   const description = ref('')
   const codeImage = ref('')
   const codeText = ref('')
   const codeLoading = ref(false)
   const codeFailed = ref(false)
   const codeMessage = ref('正在生成动态券码')
   let codeRefreshTimer: number | null = null
   let countdownTimer: number | null = null
   const codeRequestKey = ref('')
   onLoad((options: any) => {
      // #ifdef MP-WEIXIN
@@ -76,6 +94,7 @@
      // #endif
      if (options != null && options['merchantId'] != null) merchantId.value = options['merchantId'] as string
      if (options != null && options['talentId'] != null) talentId.value = options['talentId'] as string
      if (options != null && options['cardId'] != null) cardId.value = options['cardId'] as string
      const session = getSession()
      if (talentId.value == '' && session != null && session.talentId != null) talentId.value = session.talentId as string
      if (talentId.value == '' && apiEnvironment == 'development') talentId.value = '2'
@@ -90,53 +109,116 @@
   const clearCodeTimer = () => {
      if (codeRefreshTimer != null) { clearTimeout(codeRefreshTimer); codeRefreshTimer = null }
      if (countdownTimer != null) { clearInterval(countdownTimer); countdownTimer = null }
      countdown.value = 0
   }
   const loadBenefit = () => {
   const startCountdown = (seconds: number) => {
      if (countdownTimer != null) { clearInterval(countdownTimer); countdownTimer = null }
      countdown.value = Math.max(0, seconds)
      countdownTimer = setInterval(() => {
         if (countdown.value > 0) countdown.value = countdown.value - 1
         if (countdown.value <= 0 && countdownTimer != null) { clearInterval(countdownTimer); countdownTimer = null }
      }, 1000)
   }
   const createCodeRequestKey = (): string => 'benefit-code-HOTEL-' + merchantId.value + '-' + talentId.value + '-' + Date.now() + '-' + Math.floor(Math.random() * 1000000)
   const loadBenefit = () => requestQuota()
   const refreshCode = () => requestVerificationCode(false)
   const requestQuota = () => {
      if (!isApiConfigured() || merchantId.value == '' || (talentId.value == '' && cardId.value == '')) {
         loading.value = false
         loadFailed.value = true
         return
      }
      clearCodeTimer()
      loading.value = true
      loadFailed.value = false
      available.value = false
      codeLoading.value = false
      codeFailed.value = false
      codeImage.value = ''
      getFreeBenefit(talentId.value, merchantId.value, (data: any) => {
      codeText.value = ''
      getBenefitQuota('HOTEL', talentId.value, cardId.value, merchantId.value, (data: any) => {
         loading.value = false
         available.value = data != null && data.available === true
         if (!available.value) {
            unavailableReason.value = data != null && data.reason != null ? data.reason as string : '该权益当前不可使用'
         if (data == null) {
            available.value = false
            unavailableReason.value = '未查询到当前酒店权益额度'
            return
         }
         couponName.value = data.name != null ? data.name as string : '免费入住券'
         remainingCount.value = data.remainingCount != null ? Number(data.remainingCount) : 0
         validEnd.value = data.validEnd != null ? String(data.validEnd) : '以商户说明为准'
         description.value = data.description != null && String(data.description).length > 0 ? data.description as string : '以商户说明为准'
         quotaTotal.value = data.quotaTotal != null ? Number(data.quotaTotal) : 0
         quotaUsed.value = data.quotaUsed != null ? Number(data.quotaUsed) : 0
         remainingCount.value = data.quotaRemaining != null ? Number(data.quotaRemaining) : 0
         resetCycle.value = data.quotaResetCycle != null && String(data.quotaResetCycle) != '' ? String(data.quotaResetCycle) : '-'
         quotaDetail.value = data.quotaDetail != null && String(data.quotaDetail).trim() != '' ? String(data.quotaDetail) : ''
         const familyCard = data.familyCard === true
         couponName.value = familyCard ? '亲属卡共享免费入住权益' : '人才卡免费入住权益'
         description.value = familyCard ? '当前亲属附属卡已自动关联主卡,共享主卡的酒店权益额度;核销成功后将自动扣减主卡剩余次数。' : '核销成功后将自动扣减当前主卡的酒店权益次数。'
         available.value = data.available === true && remainingCount.value > 0
         if (!available.value) {
            unavailableReason.value = remainingCount.value <= 0 ? '当前酒店权益可用次数已用完' : '该权益当前不可使用'
            return
         }
         refreshCode()
      }, () => {
      }, (error) => {
         loading.value = false
         loadFailed.value = true
         available.value = false
         unavailableReason.value = error.message == '' ? '酒店权益额度查询失败' : error.message
      })
   }
   const refreshCode = () => {
      if (!available.value || talentId.value == '' || codeLoading.value) return
   const requestVerificationCode = (initialLoad: boolean) => {
      if (!isApiConfigured() || merchantId.value == '' || (talentId.value == '' && cardId.value == '') || codeLoading.value) {
         if (initialLoad) { loading.value = false; loadFailed.value = true }
         return
      }
      clearCodeTimer()
      if (initialLoad) { loading.value = true; loadFailed.value = false; available.value = false }
      codeLoading.value = true
      codeFailed.value = false
      codeImage.value = ''
      codeMessage.value = '正在生成动态券码'
      issueFreeBenefitCode(talentId.value, merchantId.value, (data: any) => {
      codeText.value = ''
      codeMessage.value = '正在生成60秒免费券码'
      if (codeRequestKey.value == '') codeRequestKey.value = createCodeRequestKey()
      issueBenefitVerificationCode('HOTEL', talentId.value, cardId.value, merchantId.value, codeRequestKey.value, (data: any) => {
         loading.value = false
         codeLoading.value = false
         if (data == null || data.imagePath == null) { handleCodeFailure('二维码生成失败'); return }
         codeImage.value = resolveApiUrl(data.imagePath as string)
         const seconds = data.refreshAfterSeconds != null ? Number(data.refreshAfterSeconds) : 60
         codeRefreshTimer = setTimeout(() => refreshCode(), Math.max(10, seconds - 3) * 1000)
      }, (error) => handleCodeFailure(error.message))
         codeRequestKey.value = ''
         if (data == null || data.verifyCode == null) { handleCodeFailure('酒店权益核销码生成失败'); return }
         available.value = true
         const familyCard = data.familyCard === true
         couponName.value = familyCard ? '亲属卡共享免费入住权益' : '人才卡免费入住权益'
         quotaTotal.value = data.quotaTotal != null ? Number(data.quotaTotal) : quotaTotal.value
         quotaUsed.value = data.quotaUsed != null ? Number(data.quotaUsed) : quotaUsed.value
         remainingCount.value = data.quotaRemainingBefore != null ? Number(data.quotaRemainingBefore) : remainingCount.value
         resetCycle.value = data.quotaResetCycle != null && String(data.quotaResetCycle) != '' ? String(data.quotaResetCycle) : resetCycle.value
         quotaDetail.value = data.quotaDetail != null && String(data.quotaDetail).trim() != '' ? String(data.quotaDetail) : quotaDetail.value
         const expiresAt = data.expiresAt != null ? String(data.expiresAt).replace('T', ' ') : ''
         validEnd.value = expiresAt.length > 19 ? expiresAt.substring(0, 19) : (expiresAt == '' ? '生成后60秒' : expiresAt)
         description.value = familyCard ? '当前亲属附属卡已自动关联主卡,共享主卡的酒店权益额度;核销成功后将自动扣减主卡剩余次数。' : '核销成功后将自动扣减当前主卡的酒店权益次数。'
         const token = String(data.verifyCode)
         codeText.value = token
         codeImage.value = resolveApiUrl(getApiEndpoint('appFreeBenefits') + '/dynamic-codes/' + encodeURIComponent(token) + '/image')
         const seconds = data.expiresInSeconds != null ? Math.max(1, Number(data.expiresInSeconds)) : (data.refreshAfterSeconds != null ? Number(data.refreshAfterSeconds) : 60)
         startCountdown(seconds)
         codeRefreshTimer = setTimeout(() => refreshCode(), Math.max(5, seconds - 2) * 1000)
      }, (error) => {
         loading.value = false
         if (initialLoad) {
            codeLoading.value = false
            available.value = false
            unavailableReason.value = error.message == '' ? '该权益当前不可使用' : error.message
            return
         }
         handleCodeFailure(error.message)
      })
   }
   const handleCodeFailure = (message: string) => {
      clearCodeTimer()
      codeLoading.value = false
      codeImage.value = ''
      codeText.value = ''
      codeFailed.value = true
      codeMessage.value = message == '' ? '二维码已失效,点击重试' : message
      codeMessage.value = message == '' ? '核销码已失效,点击重试' : message
   }
   const handleCodeImageError = () => handleCodeFailure('二维码已失效,点击重试')
   const handleCodeImageError = () => { codeImage.value = ''; codeFailed.value = false }
   const goBack = () => safeBack('/pages/stay-benefits/stay-benefits')
</script>
@@ -157,9 +239,18 @@
   .voucher-code { display: flex; width: 260rpx; height: 260rpx; margin: 32rpx auto 0; overflow: hidden; border-radius: 14rpx; background-color: #F7F7F8; justify-content: center; align-items: center; }
   .voucher-code-active { opacity: .72; }
   .code-image { width: 100%; height: 100%; background-color: #FFFFFF; }
   .code-value { width: 220rpx; color: #313947; font-size: 20rpx; line-height: 29rpx; text-align: center; word-break: break-all; }
   .code-state { display: flex; max-width: 220rpx; color: #69727F; font-size: 25rpx; line-height: 36rpx; text-align: center; flex-direction: column; align-items: center; }
   .code-tip { display: block; margin: 14rpx 35rpx 0; color: #99A1AB; font-size: 21rpx; line-height: 30rpx; text-align: center; }
   .remaining-count { display: block; margin-top: 16rpx; color: #626C78; font-size: 25rpx; line-height: 34rpx; font-weight: 600; text-align: center; }
   .countdown-text { display: block; margin-top: 8rpx; color: #F07838; font-size: 24rpx; line-height: 34rpx; font-weight: 600; text-align: center; }
   .quota-overview { display: flex; margin: 24rpx 32rpx 0; padding: 22rpx 12rpx; border-radius: 14rpx; background-color: #FFF8EF; flex-direction: row; align-items: center; justify-content: space-around; }
   .quota-item { display: flex; min-width: 150rpx; flex-direction: column; align-items: center; }
   .quota-value { color: #313947; font-size: 36rpx; line-height: 44rpx; font-weight: 700; }
   .used-value { color: #8B94A0; }
   .remaining-value { color: #F07838; }
   .quota-label { margin-top: 5rpx; color: #8E97A4; font-size: 20rpx; line-height: 28rpx; }
   .quota-line { width: 1rpx; height: 52rpx; background-color: #EADCCB; }
   .reset-cycle { display: block; margin-top: 16rpx; color: #626C78; font-size: 23rpx; line-height: 32rpx; text-align: center; }
   .valid-end { display: block; margin-top: 7rpx; color: #8E97A4; font-size: 22rpx; line-height: 31rpx; text-align: center; }
   .card-divider { height: 1rpx; margin-top: 34rpx; background-color: #ECEEF1; }
   .benefit-detail { padding: 34rpx 20rpx 24rpx; }