<template>
|
<view class="page">
|
<view class="custom-nav" :class="isSolidNav ? 'is-solid' : ''"><view class="nav-bg"></view><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" @tap="goBack"><image src="/static/food-assets/arrow-left-white.svg" mode="aspectFit"></image></view><text class="nav-title">{{cinemaName}}</text><view class="capsule-space" :style="'width:' + navWidth + 'px'"></view></view></view>
|
<scroll-view class="content" scroll-y @scroll="handleScroll">
|
<view class="hero"><image class="hero-image" :src="cinemaHero" mode="aspectFill" @error="replaceHeroImage"></image><view class="hero-shade"></view></view>
|
<view class="merchant-shell">
|
<view v-if="detailLoading" class="detail-state"><view class="loading-spinner"></view><text>正在加载影城信息</text></view>
|
<view v-else-if="detailError" class="detail-state error-state"><text>影城信息加载失败</text><text class="retry-text" @tap="loadCinema">重新加载</text></view>
|
<view v-else class="merchant-detail">
|
<view class="merchant-info"><view class="title-row"><text class="merchant-name">{{cinemaName}}</text><view class="coupon-badge"><text>券</text></view></view><view class="service-tag"><text>{{talentCardTag}}</text></view><view class="address-row"><image class="address-icon" src="/static/benefits-assets/map-pin.svg" mode="aspectFit"></image><text class="address-text">{{cinemaAddress}}</text><view class="merchant-actions"><view class="action-button" @tap="openMap"><image src="/static/merchant-assets/navigation.svg" mode="aspectFit"></image></view><view v-if="cinemaMobile != ''" class="action-button" @tap="callCinema"><image src="/static/merchant-assets/phone.svg" mode="aspectFit"></image></view></view></view></view>
|
<view class="tabs"><view class="tab-item" :class="currentTab == 'purchase' ? 'active' : ''" @tap="changeTab('purchase')"><text>消费购买</text><view></view></view><view class="tab-item" :class="currentTab == 'rights' ? 'active' : ''" @tap="changeTab('rights')"><text>权益详情</text><view></view></view></view>
|
<view v-if="currentTab == 'purchase'" class="coupon-list"><view v-if="listLoading" class="list-state"><view class="loading-spinner"></view><text>正在加载可领电影券</text></view><view v-else-if="listError" class="list-state error-state"><text>电影券加载失败</text><text class="retry-text" @tap="loadCoupons">重新加载</text></view><view v-else-if="listLoaded && coupons.length == 0" class="list-state"><text>暂无可领取电影券</text></view><view class="coupon-item" v-for="item in coupons" :key="item.id"><image class="coupon-image" :src="item.image" mode="aspectFill" @error="replaceCouponImage(item)"></image><view class="coupon-info"><text class="coupon-name">{{item.name}}</text><view class="stock-row"><text>剩余可领</text><text>{{item.stock}}</text><text>张</text></view><view class="coupon-bottom"><view class="coupon-price"><text>¥</text><text>{{item.price}}</text></view><view class="claim-button" :class="item.claimed ? 'claimed' : (item.soldOut ? 'sold-out' : '')" @tap="requestClaimCoupon(item)"><text>{{item.claimed ? '已领取' : (item.soldOut ? '已领完' : '领券')}}</text></view></view></view></view></view>
|
<view v-else class="rights-panel"><text class="rights-title">{{rightsTitle}}</text><text class="rights-description">{{rightsDescription}}</text><view class="rule-row"><text>适用卡种</text><text>{{applicableCards}}</text></view><view class="rule-row"><text>领取价格</text><text>{{claimPrice}}</text></view><view class="rule-row"><text>使用方式</text><text>{{usage}}</text></view><view class="rule-row"><text>使用范围</text><text>{{scope}}</text></view></view>
|
</view>
|
</view>
|
<view class="content-bottom"></view>
|
</scroll-view>
|
<view v-if="showClaimDialog" class="dialog-mask" @tap="closeClaimDialog"><view class="claim-dialog" @tap.stop><view class="dialog-close" @tap="closeClaimDialog"><text>×</text></view><text class="dialog-title">是否确认领取</text><view class="dialog-message"><text>当前剩余可领取次数</text><text>{{pendingCouponStock}}</text><text>次</text></view><view class="dialog-actions"><view class="dialog-cancel" @tap="closeClaimDialog"><text>取消</text></view><view class="dialog-confirm" @tap="confirmClaimCoupon"><text>{{isSubmitting ? '领取中' : '确认领取'}}</text></view></view></view></view>
|
</view>
|
</template>
|
|
<script setup lang="uts">
|
import { apiEnvironment, isApiConfigured } from '../../config/api.uts'
|
import { getCinemaMerchant, getCinemaCoupons, claimCinemaCoupon } from '../../services/portal.uts'
|
import { resolveApiUrl } from '../../services/http.uts'
|
import { getSession } from '../../services/session.uts'
|
import { safeBack } from '../../utils/navigation.uts'
|
|
const fallbackImage = '/static/entertainment-assets/wanda-cinema.webp'
|
const navTop = ref(28); const navHeight = ref(32); const navWidth = ref(88); const navRight = ref(10)
|
const isSolidNav = ref(false); const currentTab = ref('purchase'); const merchantId = ref(''); const talentId = ref('')
|
const cinemaName = ref('影城服务'); const cinemaAddress = ref(''); const cinemaHero = ref(fallbackImage); const cinemaMobile = ref(''); const cinemaLatitude = ref(0); const cinemaLongitude = ref(0)
|
const talentCardTag = ref('人才卡专享'); const rightsTitle = ref('人才观影专属权益'); const rightsDescription = ref(''); const applicableCards = ref('以商户说明为准'); const claimPrice = ref('0元'); const usage = ref('到店出示券码'); const scope = ref('普通厅指定场次')
|
const detailLoading = ref(true); const detailError = ref(false); const coupons = ref<any[]>([]); const listLoading = ref(false); const listLoaded = ref(false); const listError = ref(false)
|
const showClaimDialog = ref(false); const isSubmitting = ref(false); const pendingCouponId = ref(''); const pendingCouponStock = ref(0)
|
|
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['name'] != null) cinemaName.value = decodeURIComponent(options['name'] as string)
|
const session = getSession(); if (session != null && session.talentId != null) talentId.value = session.talentId as string
|
if (talentId.value == '' && apiEnvironment == 'development') talentId.value = '2'
|
loadCinema()
|
})
|
|
const loadCinema = () => {
|
if (!isApiConfigured() || merchantId.value == '') { detailLoading.value = false; detailError.value = true; return }
|
detailLoading.value = true; detailError.value = false
|
getCinemaMerchant(merchantId.value, (data: any) => {
|
detailLoading.value = false
|
cinemaName.value = data.name != null ? data.name as string : cinemaName.value
|
cinemaAddress.value = data.address != null ? data.address as string : ''
|
cinemaHero.value = data.heroImage != null && data.heroImage != '' ? resolveApiUrl(data.heroImage as string) : fallbackImage
|
cinemaMobile.value = data.mobile != null ? data.mobile as string : ''
|
cinemaLatitude.value = data.latitude != null ? Number(data.latitude) : 0; cinemaLongitude.value = data.longitude != null ? Number(data.longitude) : 0
|
talentCardTag.value = data.talentCardTag != null ? data.talentCardTag as string : talentCardTag.value; rightsTitle.value = data.rightsTitle != null ? data.rightsTitle as string : rightsTitle.value; rightsDescription.value = data.rightsDescription != null ? data.rightsDescription as string : '以商户说明为准'; applicableCards.value = data.applicableCards != null ? data.applicableCards as string : applicableCards.value; claimPrice.value = data.claimPrice != null ? data.claimPrice as string : claimPrice.value; usage.value = data.usage != null ? data.usage as string : usage.value; scope.value = data.scope != null ? data.scope as string : scope.value
|
loadCoupons()
|
}, () => { detailLoading.value = false; detailError.value = true })
|
}
|
const loadCoupons = () => {
|
if (!isApiConfigured() || merchantId.value == '' || talentId.value == '') { listLoading.value = false; listError.value = true; return }
|
listLoading.value = true; listLoaded.value = false; listError.value = false
|
getCinemaCoupons(merchantId.value, talentId.value, (data: any) => {
|
const records: any[] = data instanceof Array ? data as any[] : []; const next: any[] = []
|
for (let index = 0; index < records.length; index++) { const item = records[index]; next.push({ id: '' + item.couponId, name: item.name != null ? item.name as string : '普通厅兑换券', image: item.coverUrl != null && item.coverUrl != '' ? resolveApiUrl(item.coverUrl as string) : fallbackImage, stock: item.stock != null ? Number(item.stock) : 0, price: item.claimPrice != null ? Number(item.claimPrice).toFixed(0) : '0', claimed: item.claimed === true, soldOut: item.soldOut === true }) }
|
coupons.value = next; listLoading.value = false; listLoaded.value = true
|
}, () => { listLoading.value = false; listError.value = true })
|
}
|
const goBack = () => safeBack('/pages/entertainment-benefits/entertainment-benefits')
|
const handleScroll = (event: any) => { isSolidNav.value = event.detail.scrollTop > 64 }
|
const changeTab = (tab: string) => { currentTab.value = tab }
|
const openMap = () => { if (cinemaLatitude.value <= 0 || cinemaLongitude.value <= 0) { uni.showToast({ title: '影城暂未配置导航地址', icon: 'none' }); return }; uni.openLocation({ latitude: cinemaLatitude.value, longitude: cinemaLongitude.value, name: cinemaName.value, address: cinemaAddress.value, scale: 18, fail: () => uni.showToast({ title: '暂时无法打开地图导航', icon: 'none' }) }) }
|
const callCinema = () => { if (cinemaMobile.value == '') { uni.showToast({ title: '影城暂未提供联系电话', icon: 'none' }); return }; uni.makePhoneCall({ phoneNumber: cinemaMobile.value, fail: () => uni.showToast({ title: '暂时无法拨打电话', icon: 'none' }) }) }
|
const requestClaimCoupon = (item: any) => { if (item.claimed) { uni.showToast({ title: '优惠券已领取', icon: 'none' }); return }; if (item.soldOut) { uni.showToast({ title: '电影券已领完', icon: 'none' }); return }; pendingCouponId.value = item.id; pendingCouponStock.value = item.stock; showClaimDialog.value = true }
|
const closeClaimDialog = () => { if (isSubmitting.value) return; showClaimDialog.value = false; pendingCouponId.value = ''; pendingCouponStock.value = 0 }
|
const confirmClaimCoupon = () => { if (isSubmitting.value || pendingCouponId.value == '') return; isSubmitting.value = true; claimCinemaCoupon(merchantId.value, pendingCouponId.value, talentId.value, (data: any) => { isSubmitting.value = false; showClaimDialog.value = false; loadCoupons(); uni.navigateTo({ url: '/pages/coupon-received/coupon-received?name=' + encodeURIComponent(data.couponName as string) + '&claimNo=' + encodeURIComponent(data.claimNo as string) + '&validEnd=' + encodeURIComponent(String(data.validEnd)) + '&claimId=' + encodeURIComponent(String(data.claimId)) }) }, (error) => { isSubmitting.value = false; uni.showToast({ title: error.message, icon: 'none' }) }) }
|
const replaceHeroImage = () => { cinemaHero.value = fallbackImage }; const replaceCouponImage = (item: any) => { item.image = fallbackImage }
|
</script>
|
|
<style>
|
.page, .content { width: 100%; height: 100vh; background-color: #F5F5F5; }.custom-nav { position: fixed; z-index: 30; left: 0; right: 0; top: 0; padding-bottom: 20rpx; }.nav-bg { position: absolute; left: 0; right: 0; top: 0; bottom: 0; opacity: 0; background-image: linear-gradient(180deg, #F26A38 0%, #EE542B 100%); transition: opacity 280ms ease-out; }.custom-nav.is-solid .nav-bg { opacity: 1; }.safe-space, .nav-row { position: relative; z-index: 1; }.safe-space { height: 28px; }.nav-row { display: flex; box-sizing: border-box; padding-left: 24rpx; flex-direction: row; align-items: center; justify-content: space-between; }.back-button { display: flex; width: 54rpx; height: 54rpx; position: relative; z-index: 2; border-radius: 50%; background-color: rgba(45,45,45,.52); justify-content: center; align-items: center; }.custom-nav.is-solid .back-button { background-color: rgba(45,45,45,0); }.back-button image { width: 38rpx; height: 38rpx; }.nav-title { position: absolute; z-index: 1; left: 0; right: 0; opacity: 0; color: #FFFFFF; font-size: 31rpx; font-weight: 500; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }.custom-nav.is-solid .nav-title { opacity: 1; }.capsule-space { height: 100%; flex-shrink: 0; }
|
.hero { height: 392rpx; position: relative; overflow: hidden; background-color: #222222; }.hero-image { position: absolute; left: 0; top: 0; width: 100%; height: 392rpx; }.hero-shade { position: absolute; left: 0; right: 0; top: 0; height: 230rpx; background-image: linear-gradient(180deg, rgba(0,0,0,.55) 0%, rgba(0,0,0,0) 100%); }.merchant-shell { position: relative; z-index: 3; min-height: 470rpx; margin-top: -32rpx; overflow: hidden; border-radius: 28rpx 28rpx 0 0; background-color: #F5F5F5; }.detail-state, .list-state { display: flex; height: 330rpx; color: #969CA5; font-size: 24rpx; flex-direction: row; justify-content: center; align-items: center; }.detail-state.error-state, .list-state.error-state { flex-direction: column; }.retry-text { margin-top: 20rpx; color: #F26A38; }.loading-spinner { width: 28rpx; height: 28rpx; margin-right: 13rpx; box-sizing: border-box; border: 3rpx solid #FFD6C4; border-top-color: #FF6C2B; border-radius: 50%; animation: cinema-loading 720ms linear infinite; }@keyframes cinema-loading { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
.merchant-info { padding: 27rpx 31rpx 19rpx; background-color: #FFFFFF; }.title-row { display: flex; min-height: 48rpx; flex-direction: row; align-items: center; justify-content: space-between; }.merchant-name { min-width: 0; flex: 1; color: #282828; font-size: 32rpx; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }.coupon-badge { display: flex; width: 42rpx; height: 39rpx; margin-left: 18rpx; border-radius: 7rpx; background-color: #FFF0EA; justify-content: center; align-items: center; }.coupon-badge text { color: #FF5B36; font-size: 22rpx; }.service-tag { display: flex; width: 180rpx; height: 42rpx; margin-top: 17rpx; box-sizing: border-box; border: 1rpx solid #FFB897; border-radius: 7rpx; justify-content: center; align-items: center; }.service-tag text { color: #E56843; font-size: 23rpx; }.address-row { display: flex; min-height: 58rpx; margin-top: 8rpx; flex-direction: row; align-items: center; }.address-icon { width: 27rpx; height: 27rpx; margin-right: 10rpx; flex-shrink: 0; }.address-text { min-width: 0; flex: 1; color: #858A92; font-size: 22rpx; line-height: 32rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }.merchant-actions { display: flex; width: 112rpx; margin-left: 18rpx; flex-direction: row; justify-content: space-between; flex-shrink: 0; }.action-button { display: flex; width: 48rpx; height: 48rpx; border-radius: 50%; background-color: #F4F7FA; justify-content: center; align-items: center; }.action-button image { width: 27rpx; height: 27rpx; }
|
.tabs { display: flex; height: 92rpx; box-sizing: border-box; padding: 0 31rpx; background-color: #FFFFFF; flex-direction: row; align-items: flex-end; }.tab-item { display: flex; width: 164rpx; height: 92rpx; position: relative; color: #55585D; flex-direction: column; justify-content: center; align-items: flex-start; }.tab-item text { font-size: 30rpx; }.tab-item view { position: absolute; left: 28rpx; bottom: 4rpx; width: 54rpx; height: 7rpx; opacity: 0; border-radius: 4rpx; background-image: linear-gradient(90deg, #FF4434 0%, #FFB631 100%); }.tab-item.active { color: #333333; font-weight: 700; }.tab-item.active view { opacity: 1; }
|
.coupon-list { padding: 21rpx 24rpx 8rpx; }.coupon-item { display: flex; height: 176rpx; margin-bottom: 15rpx; box-sizing: border-box; padding: 14rpx 15rpx; border-radius: 15rpx; background-color: #FFFFFF; flex-direction: row; }.coupon-image { width: 145rpx; height: 148rpx; border-radius: 13rpx; flex-shrink: 0; background-color: #E8E8E8; }.coupon-info { display: flex; min-width: 0; flex: 1; padding-left: 22rpx; flex-direction: column; }.coupon-name { color: #242424; font-size: 29rpx; line-height: 40rpx; font-weight: 700; }.stock-row { display: flex; margin-top: 10rpx; color: #8A8A8A; font-size: 21rpx; flex-direction: row; align-items: center; }.stock-row text:nth-child(2) { margin: 0 7rpx; color: #FF6037; }.coupon-bottom { display: flex; height: 56rpx; margin-top: auto; border-radius: 29rpx; background-image: linear-gradient(90deg, rgba(255,247,240,.12) 0%, rgba(255,235,218,.76) 58%, rgba(255,222,195,.96) 100%); flex-direction: row; align-items: center; justify-content: space-between; }.coupon-price { display: flex; height: 56rpx; padding-left: 5rpx; color: #FF4F2C; flex-direction: row; align-items: center; }.coupon-price text:first-child { font-size: 27rpx; }.coupon-price text:last-child { margin-left: 3rpx; font-size: 38rpx; font-weight: 700; }.claim-button { display: flex; width: 126rpx; height: 56rpx; border-radius: 29rpx; background-image: linear-gradient(90deg, #FF4433 0%, #FF8B20 100%); justify-content: center; align-items: center; }.claim-button text { color: #FFFFFF; font-size: 25rpx; }.claim-button.claimed, .claim-button.sold-out { background-image: none; background-color: #C8C8C8; }
|
.rights-panel { margin: 21rpx 24rpx 8rpx; padding: 28rpx 27rpx 18rpx; border-radius: 15rpx; background-color: #FFFFFF; }.rights-title { display: block; color: #2F2F2F; font-size: 31rpx; font-weight: 700; }.rights-description { display: block; margin-top: 19rpx; color: #737984; font-size: 26rpx; line-height: 43rpx; }.rule-row { display: flex; min-height: 72rpx; border-top: 1rpx solid #EEEEEE; color: #777D86; font-size: 25rpx; flex-direction: row; align-items: center; justify-content: space-between; }.rule-row:first-of-type { margin-top: 20rpx; }.rule-row text:last-child { max-width: 390rpx; color: #34383E; text-align: right; }.content-bottom { height: calc(30rpx + env(safe-area-inset-bottom)); }
|
.dialog-mask { display: flex; position: fixed; z-index: 100; left: 0; right: 0; top: 0; bottom: 0; background-color: rgba(24,29,38,.48); justify-content: center; align-items: center; }.claim-dialog { display: flex; width: 664rpx; height: 376rpx; position: relative; box-sizing: border-box; padding: 70rpx 48rpx 0; border-radius: 34rpx; background-color: #FFFFFF; flex-direction: column; align-items: center; }.dialog-close { display: flex; width: 64rpx; height: 64rpx; position: absolute; right: 28rpx; top: 22rpx; justify-content: center; align-items: center; }.dialog-close text { color: #AAB2BF; font-size: 43rpx; line-height: 48rpx; font-weight: 300; }.dialog-title { color: #212633; font-size: 36rpx; line-height: 50rpx; font-weight: 700; }.dialog-message { display: flex; margin-top: 24rpx; color: #A9AFBA; font-size: 25rpx; line-height: 36rpx; flex-direction: row; align-items: center; }.dialog-message text:nth-child(2) { margin: 0 7rpx; color: #FF7046; }.dialog-actions { display: flex; width: 100%; margin-top: 66rpx; padding: 0 5rpx; box-sizing: border-box; flex-direction: row; align-items: center; justify-content: space-between; }.dialog-cancel { display: flex; width: 150rpx; height: 70rpx; justify-content: center; align-items: center; }.dialog-cancel text { color: #FF752B; font-size: 27rpx; font-weight: 500; }.dialog-confirm { display: flex; width: 178rpx; height: 70rpx; border-radius: 36rpx; background-image: linear-gradient(90deg, #FF4434 0%, #FF8C21 100%); justify-content: center; align-items: center; }.dialog-confirm text { color: #FFFFFF; font-size: 27rpx; font-weight: 500; }
|
</style>
|