| | |
| | | <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> |
| | |
| | | </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' |
| | |
| | | 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 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 |
| | |
| | | // #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' |
| | |
| | | |
| | | 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) : '-' |
| | | 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 |
| | | 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> |
| | | |
| | |
| | | .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; } |