<template>
|
<view class="page">
|
<view class="top-glow"></view>
|
<view class="custom-nav">
|
<view class="safe-space" :style="'height:' + navTop + 'px'"></view>
|
<view class="nav-row" :style="'height:' + navHeight + 'px;padding-right:' + navRight + 'px'">
|
<view class="back-button" hover-class="back-button-active" @tap="goBack"><image src="/static/news-assets/arrow-left.svg" mode="aspectFit"></image></view>
|
<text class="nav-title">免费权益</text>
|
<view class="capsule-space" :style="'width:' + navWidth + 'px'"></view>
|
</view>
|
</view>
|
|
<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>
|
<view v-else class="benefit-card">
|
<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 != '' ? '免费券码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>{{quotaDetail != '' ? quotaDetail : description}}</text></view>
|
</view>
|
</view>
|
<view class="content-bottom"></view>
|
</scroll-view>
|
</view>
|
</template>
|
|
<script setup lang="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 navTop = ref(28)
|
const navHeight = ref(32)
|
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
|
const capsule = uni.getMenuButtonBoundingClientRect()
|
const systemInfo = uni.getSystemInfoSync()
|
navTop.value = capsule.top
|
navHeight.value = capsule.height
|
navWidth.value = capsule.width
|
navRight.value = systemInfo.windowWidth - capsule.right
|
// #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'
|
loadBenefit()
|
})
|
|
onShow(() => {
|
if (available.value && !loading.value) refreshCode()
|
})
|
onHide(() => clearCodeTimer())
|
onUnload(() => clearCodeTimer())
|
|
const clearCodeTimer = () => {
|
if (codeRefreshTimer != null) { clearTimeout(codeRefreshTimer); codeRefreshTimer = null }
|
if (countdownTimer != null) { clearInterval(countdownTimer); countdownTimer = null }
|
countdown.value = 0
|
}
|
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 = ''
|
codeText.value = ''
|
getBenefitQuota('HOTEL', talentId.value, cardId.value, merchantId.value, (data: any) => {
|
loading.value = false
|
if (data == null) {
|
available.value = false
|
unavailableReason.value = '未查询到当前酒店权益额度'
|
return
|
}
|
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
|
available.value = false
|
unavailableReason.value = error.message == '' ? '酒店权益额度查询失败' : error.message
|
})
|
}
|
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 = ''
|
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
|
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
|
}
|
const handleCodeImageError = () => { codeImage.value = ''; codeFailed.value = false }
|
const goBack = () => safeBack('/pages/stay-benefits/stay-benefits')
|
</script>
|
|
<style>
|
.page { display: flex; width: 100%; height: 100vh; position: relative; overflow: hidden; background-color: #FFFFFF; flex-direction: column; }
|
.top-glow { position: absolute; left: 0; right: 0; top: 0; height: 300rpx; background-image: linear-gradient(180deg, #FFE5B8 0%, #FFF4DF 68%, rgba(255,255,255,0) 100%); }
|
.custom-nav { position: relative; z-index: 2; flex-shrink: 0; }
|
.safe-space { height: 28px; }
|
.nav-row { display: flex; height: 32px; box-sizing: border-box; padding-left: 20rpx; padding-right: 10px; position: relative; flex-direction: row; align-items: center; justify-content: space-between; }
|
.back-button { display: flex; width: 70rpx; height: 64rpx; margin-left: -4rpx; justify-content: center; align-items: center; }
|
.back-button image { width: 36rpx; height: 36rpx; }
|
.back-button-active { opacity: .55; }
|
.nav-title { position: absolute; left: 50%; top: 50%; color: #1C2128; font-size: 36rpx; line-height: 48rpx; font-weight: 700; transform: translate(-50%, -50%); white-space: nowrap; }
|
.capsule-space { width: 88px; height: 100%; flex-shrink: 0; }
|
.content { min-height: 0; position: relative; z-index: 1; flex: 1; }
|
.benefit-card { min-height: 760rpx; margin: 44rpx 25rpx 0; overflow: hidden; border-radius: 19rpx; background-color: #FFFFFF; box-shadow: 0 10rpx 25rpx rgba(67, 77, 91, .06); }
|
.benefit-name { display: block; margin: 38rpx 32rpx 0; color: #161C26; font-size: 32rpx; line-height: 45rpx; font-weight: 700; text-align: center; }
|
.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; }
|
.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; }
|
.detail-title { display: block; color: #252A32; font-size: 29rpx; line-height: 40rpx; font-weight: 700; }
|
.detail-content { min-height: 150rpx; margin-top: 24rpx; box-sizing: border-box; padding: 31rpx 35rpx; border-radius: 13rpx; background-color: #F7F7F8; }
|
.detail-content text { color: #343B46; font-size: 23rpx; line-height: 35rpx; white-space: pre-wrap; }
|
.page-state { display: flex; min-height: 520rpx; margin: 44rpx 25rpx 0; color: #65707D; font-size: 28rpx; line-height: 41rpx; text-align: center; flex-direction: column; justify-content: center; align-items: center; }
|
.empty-state { padding: 0 58rpx; }
|
.empty-title { color: #333B47; font-size: 32rpx; line-height: 46rpx; font-weight: 700; }
|
.empty-copy { margin-top: 14rpx; color: #8B94A0; font-size: 24rpx; line-height: 36rpx; }
|
.retry-text { margin-top: 20rpx; color: #F27637; font-size: 25rpx; line-height: 36rpx; }
|
.loading-spinner { width: 46rpx; height: 46rpx; margin-bottom: 20rpx; box-sizing: border-box; border: 5rpx solid #F9DFC7; border-top-color: #F47A35; border-radius: 50%; animation: free-benefit-spin 850ms linear infinite; }
|
.small-spinner { width: 38rpx; height: 38rpx; margin-bottom: 15rpx; }
|
.content-bottom { height: 40rpx; }
|
@keyframes free-benefit-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
</style>
|