713 lines
22 KiB
Vue
713 lines
22 KiB
Vue
<template>
|
||
<view class="page">
|
||
<!-- 未登录状态 -->
|
||
<view class="empty-state" v-if="notLoggedIn">
|
||
<view class="empty-icon-card">
|
||
<view class="empty-icon-stripe" />
|
||
<view class="empty-icon-stripe short" />
|
||
</view>
|
||
<text class="empty-text">请先登录</text>
|
||
<text class="empty-desc">登录后可查看您的旅游卡</text>
|
||
<view class="empty-btn" @click="goLogin">
|
||
<text class="empty-btn-text">立即登录</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 加载中 -->
|
||
<view class="loading-state" v-else-if="loading">
|
||
<view class="loading-spinner" />
|
||
<text class="loading-text">加载中...</text>
|
||
</view>
|
||
|
||
<!-- 未持卡状态 -->
|
||
<view class="empty-state" v-else-if="!hasCard && loaded">
|
||
<view class="empty-icon-card">
|
||
<view class="empty-icon-stripe" />
|
||
<view class="empty-icon-stripe short" />
|
||
</view>
|
||
<text class="empty-text">您还没有旅游卡</text>
|
||
<text class="empty-desc">购买或绑定旅游卡后即可使用入园码</text>
|
||
<view class="empty-btn" @click="goBuyCard">
|
||
<text class="empty-btn-text">去购买</text>
|
||
</view>
|
||
<view class="empty-btn-secondary" @click="goBindCard">
|
||
<text class="empty-btn-secondary-text">绑定已有旅游卡</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 持卡状态 -->
|
||
<scroll-view scroll-y class="scroll-content" v-else>
|
||
<!-- 卡面展示区域 -->
|
||
<view class="card-swiper-wrap">
|
||
<swiper
|
||
class="card-swiper"
|
||
:current="currentCardIndex"
|
||
@change="onCardChange"
|
||
circular
|
||
previous-margin="120rpx"
|
||
next-margin="120rpx"
|
||
>
|
||
<swiper-item v-for="(card, index) in userCards" :key="card.cardNo">
|
||
<view
|
||
class="card-face"
|
||
:class="{ 'card-face--active': index === currentCardIndex }"
|
||
>
|
||
<view class="card-face-inner">
|
||
<image
|
||
class="card-bg"
|
||
:src="card.cardModelPic || 'https://hn12301.oss-cn-shenzhen.aliyuncs.com/hn12301/%E5%88%9D%E5%BF%83%E6%BA%90%20%E6%B9%98%E9%84%82%E8%B5%A3%E6%96%87%E6%97%85%E4%B8%80%E5%8D%A1%E9%80%9A_1777447994254.png'"
|
||
mode="aspectFill"
|
||
/>
|
||
</view>
|
||
<view class="card-dots" v-if="userCards.length > 1">
|
||
<view
|
||
v-for="(_, idx) in userCards"
|
||
:key="idx"
|
||
class="card-dot"
|
||
:class="{ active: idx === index }"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</swiper-item>
|
||
</swiper>
|
||
</view>
|
||
|
||
<!-- 功能入口 -->
|
||
<view class="feature-panel">
|
||
<view class="feature-grid">
|
||
<view class="feature-item" v-for="item in featureActions" :key="item.label" @click="onFeatureClick(item)">
|
||
<image class="feature-icon-img" :src="item.icon" mode="aspectFit" />
|
||
<text class="feature-label">{{ item.label }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 持卡人信息 -->
|
||
<view class="holder-panel">
|
||
<view class="avatar-wrap" @click="previewAvatar">
|
||
<image
|
||
class="holder-avatar"
|
||
:src="userInfoData?.img || '/static/logo.png'"
|
||
mode="aspectFill"
|
||
/>
|
||
<view class="avatar-zoom">
|
||
<view class="zoom-icon" />
|
||
</view>
|
||
</view>
|
||
<view class="holder-details">
|
||
<view class="detail-row">
|
||
<text class="detail-label">姓名:</text>
|
||
<text class="detail-value">{{ userInfoData?.userName || '-' }}</text>
|
||
</view>
|
||
<view class="detail-row">
|
||
<text class="detail-label">手机:</text>
|
||
<text class="detail-value">{{ userInfoData?.mobile || userStore.userInfo?.phone || '-' }}</text>
|
||
</view>
|
||
<view class="detail-row">
|
||
<text class="detail-label">卡号:</text>
|
||
<view class="detail-value-wrap">
|
||
<text class="detail-value">{{ currentCard?.cardNo }}</text>
|
||
<text class="copy-btn" @click="copyCardNo">复制</text>
|
||
</view>
|
||
</view>
|
||
<view class="detail-row">
|
||
<text class="detail-label">有效期至:</text>
|
||
<text class="detail-value">{{ formatDateOnly(currentCard?.validityTime) }}</text>
|
||
</view>
|
||
<view class="detail-row">
|
||
<text class="detail-label">{{ userInfoData?.idType || '证件' }}:</text>
|
||
<text class="detail-value">{{ userInfoData?.idNo || '-' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 入园提示(持卡人信息下方) -->
|
||
<view class="qrcode-section" v-if="!showFaceModal && !isLeaving">
|
||
<text class="qrcode-title">入园提示</text>
|
||
<view v-if="!entryNotice" class="qrcode-fallback">为确保信息安全,现入园核销只能使用人脸核销。</view>
|
||
<view v-else class="qrcode-content">
|
||
<rich-text :nodes="entryNotice" />
|
||
</view>
|
||
</view>
|
||
<!-- 底部占位(滑到底时与tabBar的间隔) -->
|
||
<view style="height: 48rpx;"/>
|
||
</scroll-view>
|
||
|
||
<!-- 人脸绑定提醒弹窗 -->
|
||
<uni-popup
|
||
ref="faceModalPopup"
|
||
type="center"
|
||
:mask-click="false"
|
||
@change="(e: any) => showFaceModal = e.show"
|
||
>
|
||
<view class="face-modal">
|
||
<view class="face-modal__title">人脸核销升级通知</view>
|
||
|
||
<view class="face-modal__row face-modal__row--spec">
|
||
<image class="face-modal__icon" src="https://aliyunoss.hn12301.net/sd/ticket_icon1.png" mode="aspectFit" />
|
||
<view class="face-modal__col">
|
||
<text class="face-modal__sub-title">拍照规范</text>
|
||
<view class="face-modal__desc">仅可现场真人实拍正面头像,禁止艺术写真、美颜修图、远景全身、特写大头照;人脸要求完整。</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="face-modal__row">
|
||
<image class="face-modal__icon" src="https://aliyunoss.hn12301.net/sd/ticket_icon3.png" mode="aspectFit" />
|
||
<view class="face-modal__col">
|
||
<text class="face-modal__sub-title">隐私说明</text>
|
||
<view class="face-modal__desc">采集人脸仅用于本人入园身份核验,信息加密严格保密,不会挪作其他用途。</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="face-modal__tip">
|
||
<image class="face-modal__tip-icon" src="https://aliyunoss.hn12301.net/sd/ticket_icon2.png" mode="aspectFit" />
|
||
<view class="face-modal__tip-text">完成人脸重新绑定后,才可正常使用旅游卡</br>全部入园权益功能</view>
|
||
</view>
|
||
|
||
<view class="face-modal__btn" @click="onFaceModalConfirm">立即重新绑定</view>
|
||
<view class="face-modal__hint">请使用本人实拍照片</view>
|
||
</view>
|
||
</uni-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch, onUnmounted } from 'vue'
|
||
import { onShow, onHide } from '@dcloudio/uni-app'
|
||
import { useUserStore } from '../../store/user'
|
||
import { getUserCardList, getQrAndUserInfo } from '../../api/card'
|
||
import type { UserCardListItem, QrAndUserInfoResult, BannerItem } from '../../api/types'
|
||
import { post } from '../../api/request'
|
||
|
||
const userStore = useUserStore()
|
||
|
||
const notLoggedIn = ref(false)
|
||
const loading = ref(true)
|
||
const loaded = ref(false)
|
||
const userCards = ref<UserCardListItem[]>([])
|
||
const userInfoData = ref<QrAndUserInfoResult | null>(null)
|
||
const showFaceModal = ref(false)
|
||
const faceModalPopup = ref<any>(null)
|
||
watch(showFaceModal, (v) => {
|
||
if (v) faceModalPopup.value?.open()
|
||
else faceModalPopup.value?.close()
|
||
})
|
||
const isLeaving = ref(false)
|
||
const entryNotice = ref('')
|
||
const entryNoticeLoaded = ref(false)
|
||
|
||
const hasCard = computed(() => userCards.value.length > 0)
|
||
const currentCardIndex = ref(0)
|
||
const currentCard = computed(() => userCards.value[currentCardIndex.value])
|
||
const countdown = ref(20)
|
||
let timer: ReturnType<typeof setInterval> | null = null
|
||
|
||
/** 获取用户年卡列表 */
|
||
async function fetchCards() {
|
||
const mobile = (uni.getStorageSync('phone') as string) || userStore.userInfo?.phone || ''
|
||
loading.value = true
|
||
fetchEntryNotice()
|
||
try {
|
||
const list = (await getUserCardList({ mobile, saleChannel: 'xgh' })) || []
|
||
const allowedIds = ['820093340659013', '820093438328133']
|
||
userCards.value = list.filter((c: any) => allowedIds.includes(String(c.cardModelId)))
|
||
currentCardIndex.value = 0
|
||
|
||
const firstCard = userCards.value[0]
|
||
if (firstCard?.needFaceImgFlag) {
|
||
showFaceModal.value = true
|
||
}
|
||
|
||
if (list.length > 0) {
|
||
fetchUserInfo(firstCard)
|
||
}
|
||
} catch {
|
||
userCards.value = []
|
||
} finally {
|
||
loaded.value = true
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
/** 获取入园提示内容(ENTRY_NOTICE 公告) */
|
||
async function fetchEntryNotice() {
|
||
if (entryNoticeLoaded.value) return
|
||
try {
|
||
const list = await post<BannerItem[]>('/mpContent/queryList', { channel: 'pingxiang', type: 'ENTRY_NOTICE' })
|
||
entryNotice.value = list?.[0]?.content || ''
|
||
} catch {
|
||
entryNotice.value = ''
|
||
} finally {
|
||
entryNoticeLoaded.value = true
|
||
}
|
||
}
|
||
|
||
/** 刷新当前卡的人脸绑定状态(从接口获取最新数据) */
|
||
async function refreshNeedFace() {
|
||
const mobile = (uni.getStorageSync('phone') as string) || userStore.userInfo?.phone || ''
|
||
try {
|
||
const list = (await getUserCardList({ mobile, saleChannel: 'xgh' })) || []
|
||
const allowedIds = ['820093340659013', '820093438328133']
|
||
userCards.value = list.filter((c: any) => allowedIds.includes(String(c.cardModelId)))
|
||
currentCardIndex.value = 0
|
||
|
||
const current = userCards.value[currentCardIndex.value]
|
||
if (current) {
|
||
fetchUserInfo(current)
|
||
if (current.needFaceImgFlag) {
|
||
showFaceModal.value = true
|
||
}
|
||
}
|
||
} catch {
|
||
// 静默失败,沿用旧数据
|
||
}
|
||
}
|
||
|
||
/** 获取持卡人信息和入园码 */
|
||
async function fetchUserInfo(card: UserCardListItem) {
|
||
const mobile = uni.getStorageSync('phone') as string
|
||
if (!mobile || !card.cardNo || !card.thirdId) return
|
||
try {
|
||
const res = await getQrAndUserInfo({
|
||
cardNum: card.cardNo,
|
||
thirdId: card.thirdId,
|
||
mobile,
|
||
channel: 'Channel_SZH',
|
||
scenicId: '',
|
||
})
|
||
userInfoData.value = res
|
||
} catch {
|
||
userInfoData.value = null
|
||
}
|
||
}
|
||
|
||
function onCardChange(e: any) {
|
||
currentCardIndex.value = e.detail.current
|
||
resetCountdown()
|
||
const card = userCards.value[currentCardIndex.value]
|
||
if (card) {
|
||
fetchUserInfo(card)
|
||
if (card.needFaceImgFlag) {
|
||
showFaceModal.value = true
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 有效期只显示年月日(去掉时间部分) */
|
||
function formatDateOnly(time?: string): string {
|
||
if (!time) return '-'
|
||
return time.split(' ')[0]
|
||
}
|
||
|
||
function previewAvatar() {
|
||
if (!userInfoData.value?.img) return
|
||
uni.previewImage({
|
||
urls: [userInfoData.value.img],
|
||
current: 0,
|
||
})
|
||
}
|
||
|
||
function resetCountdown() {
|
||
countdown.value = 20
|
||
}
|
||
|
||
function startTimer() {
|
||
timer = setInterval(() => {
|
||
countdown.value--
|
||
if (countdown.value <= 0) {
|
||
const card = currentCard.value
|
||
if (card) fetchUserInfo(card)
|
||
resetCountdown()
|
||
}
|
||
}, 1000)
|
||
}
|
||
|
||
function goLogin() {
|
||
uni.navigateTo({ url: '/pages/login/login' })
|
||
}
|
||
|
||
function goBuyCard() {
|
||
uni.navigateTo({ url: '/packages/user/pages/buy' })
|
||
}
|
||
|
||
function copyCardNo() {
|
||
const card = currentCard.value
|
||
if (!card) return
|
||
uni.setClipboardData({
|
||
data: card.cardNo,
|
||
success: () => { uni.showToast({ title: '已复制', icon: 'success' }) },
|
||
})
|
||
}
|
||
|
||
function goBindCard() {
|
||
uni.navigateTo({ url: '/packages/user/pages/bind' })
|
||
}
|
||
|
||
const idTypeLabelMap: Record<string, string> = {
|
||
'身份证': '1',
|
||
'护照': '2',
|
||
'港澳居民来往内地通行证': '3',
|
||
'台湾居民来往大陆通行证': '4',
|
||
}
|
||
|
||
function onFaceModalConfirm() {
|
||
isLeaving.value = true
|
||
showFaceModal.value = false
|
||
|
||
const card = currentCard.value
|
||
const info = userInfoData.value
|
||
if (!card || !info) {
|
||
goBindCard()
|
||
return
|
||
}
|
||
|
||
const idTypeValue = idTypeLabelMap[info.idType || ''] || '1'
|
||
|
||
uni.navigateTo({
|
||
url: `/packages/user/pages/bind?faceBind=1&cardNo=${card.cardNo}&userName=${info.userName || ''}&mobile=${info.mobile || ''}&idType=${idTypeValue}&idNo=${info.idNo || ''}`
|
||
})
|
||
}
|
||
|
||
const featureActions = [
|
||
{ label: '我要绑定', icon: 'https://aliyunoss.hn12301.net/pxmall/card.png' },
|
||
{ label: '我要预约', icon: 'https://aliyunoss.hn12301.net/pxmall/reserve.png' },
|
||
{ label: '预约/使用记录', icon: 'https://aliyunoss.hn12301.net/pxmall/record.png' },
|
||
{ label: '使用说明', icon: 'https://aliyunoss.hn12301.net/pxmall/scenic.png' },
|
||
]
|
||
|
||
function onFeatureClick(item: { label: string }) {
|
||
if (item.label === '我要绑定') {
|
||
uni.navigateTo({ url: '/packages/user/pages/bind' })
|
||
return
|
||
}
|
||
if (item.label === '我要预约') {
|
||
const card = currentCard.value
|
||
if (!card) {
|
||
uni.showToast({ title: '请先选择一张旅游卡', icon: 'none' })
|
||
return
|
||
}
|
||
post<any>('/mpCard/checkFaceBind', { cardModelId: card.cardModelId, cardNumber: card.cardNo })
|
||
.then((res) => {
|
||
if (res?.needBindFace) {
|
||
showFaceModal.value = true
|
||
} else {
|
||
const url = card.thirdId
|
||
? `/packages/content/pages/scenic-list?cardModelId=${card.thirdId}`
|
||
: '/packages/content/pages/scenic-list'
|
||
uni.navigateTo({ url })
|
||
}
|
||
})
|
||
.catch(() => {
|
||
const url = card.thirdId
|
||
? `/packages/content/pages/scenic-list?cardModelId=${card.thirdId}`
|
||
: '/packages/content/pages/scenic-list'
|
||
uni.navigateTo({ url })
|
||
})
|
||
return
|
||
}
|
||
if (item.label === '使用说明') {
|
||
uni.navigateTo({ url: '/packages/content/pages/usage-guide' })
|
||
return
|
||
}
|
||
if (item.label === '预约/使用记录') {
|
||
uni.navigateTo({ url: '/packages/user/pages/reserve/list' })
|
||
return
|
||
}
|
||
uni.showToast({ title: `${item.label}功能开发中`, icon: 'none' })
|
||
}
|
||
|
||
onShow(() => {
|
||
notLoggedIn.value = !userStore.isLogin
|
||
if (!userStore.isLogin) return
|
||
isLeaving.value = false
|
||
if (loaded.value) {
|
||
refreshNeedFace()
|
||
} else {
|
||
fetchCards()
|
||
}
|
||
if (!timer) startTimer()
|
||
})
|
||
onHide(() => {
|
||
if (timer) { clearInterval(timer); timer = null }
|
||
})
|
||
onUnmounted(() => { if (timer) clearInterval(timer) })
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
|
||
.page { min-height: 100vh; background: $color-background; font-family: $font-family-body; }
|
||
.scroll-content { }
|
||
|
||
/* === 人脸绑定提醒弹窗 === */
|
||
.face-modal {
|
||
width: 600rpx;
|
||
padding: 24rpx;
|
||
position: relative;
|
||
box-sizing: border-box;
|
||
background: #ffffff;
|
||
border-radius: 24rpx;
|
||
}
|
||
.face-modal__title {
|
||
color: #000;
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
text-align: center;
|
||
}
|
||
.face-modal__row {
|
||
width: 552rpx;
|
||
display: flex;
|
||
padding-bottom: 24rpx;
|
||
margin-top: 24rpx;
|
||
}
|
||
.face-modal__row--spec {
|
||
border-bottom: 2rpx solid #f2f2f7;
|
||
margin-top: 36rpx;
|
||
}
|
||
.face-modal__icon {
|
||
width: 50rpx;
|
||
height: 50rpx;
|
||
}
|
||
.face-modal__col {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-left: 12rpx;
|
||
}
|
||
.face-modal__sub-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
}
|
||
.face-modal__desc {
|
||
width: 490rpx;
|
||
line-height: 44rpx;
|
||
margin-top: 24rpx;
|
||
}
|
||
.face-modal__tip {
|
||
width: 552rpx;
|
||
display: flex;
|
||
margin-top: 24rpx;
|
||
padding: 7rpx;
|
||
background: #f6f9fe;
|
||
box-sizing: border-box;
|
||
}
|
||
.face-modal__tip-icon {
|
||
width: 36rpx;
|
||
height: 36rpx;
|
||
}
|
||
.face-modal__tip-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-left: 16rpx;
|
||
font-size: 24rpx;
|
||
line-height: 36rpx;
|
||
}
|
||
.face-modal__btn {
|
||
width: 552rpx;
|
||
height: 80rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 32rpx;
|
||
border-radius: 40rpx;
|
||
color: #fff;
|
||
margin-top: 32rpx;
|
||
background: #0088ff;
|
||
}
|
||
.face-modal__hint {
|
||
width: 552rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-top: 24rpx;
|
||
color: #404040;
|
||
}
|
||
|
||
/* === 加载中 === */
|
||
.loading-state {
|
||
display: flex; flex-direction: column; align-items: center;
|
||
justify-content: center; min-height: 100vh; gap: 24rpx;
|
||
}
|
||
.loading-spinner {
|
||
width: 48rpx; height: 48rpx;
|
||
border: 4rpx solid #e0e0e0;
|
||
border-top-color: $uv-primary;
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
}
|
||
.loading-text { font-size: $font-size-base; color: $uv-tips-color; }
|
||
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
/* === 空状态 === */
|
||
.empty-state {
|
||
display: flex; flex-direction: column; align-items: center;
|
||
justify-content: center; min-height: 100vh; padding: 40rpx;
|
||
}
|
||
.empty-icon-card {
|
||
width: 120rpx; height: 80rpx; border-radius: 24rpx;
|
||
background: linear-gradient(135deg, $uv-primary, #398ade);
|
||
display: flex; flex-direction: column; justify-content: center;
|
||
padding: 16rpx; gap: 8rpx; margin-bottom: 32rpx;
|
||
}
|
||
.empty-icon-stripe { height: 8rpx; background: rgba(255,255,255,0.6); border-radius: 4rpx; width: 100%; }
|
||
.empty-icon-stripe.short { width: 60%; }
|
||
.empty-text { font-size: $font-size-lg; font-weight: $font-weight-bold; color: $uv-main-color; margin-bottom: 12rpx; }
|
||
.empty-desc { font-size: $font-size-sm; color: $uv-tips-color; margin-bottom: 40rpx; }
|
||
.empty-btn {
|
||
background: $uv-primary; border-radius: 44rpx; padding: 20rpx 80rpx;
|
||
min-height: 88rpx; display: flex; align-items: center; justify-content: center;
|
||
margin-bottom: 20rpx; transition: opacity 150ms;
|
||
}
|
||
.empty-btn:active { opacity: 0.7; }
|
||
.empty-btn-text { font-size: $font-size-lg; color: #fff; font-weight: $font-weight-medium; }
|
||
.empty-btn-secondary {
|
||
padding: 20rpx 60rpx; border-radius: 44rpx;
|
||
border: 2rpx solid #eeeeee; background: $color-card;
|
||
transition: opacity 150ms;
|
||
}
|
||
.empty-btn-secondary:active { opacity: 0.7; }
|
||
.empty-btn-secondary-text { font-size: $font-size-base; color: $uv-content-color; }
|
||
|
||
/* === 卡面展示区域(3D Coverflow) === */
|
||
.card-swiper-wrap { margin: 14rpx 14rpx 14rpx; overflow: visible; perspective: 1200rpx; }
|
||
.card-swiper { height: 360rpx; }
|
||
.card-face {
|
||
height: 340rpx;
|
||
margin: 0 8rpx;
|
||
border-radius: 16rpx;
|
||
background: #fff;
|
||
position: relative;
|
||
transform: scale(0.88);
|
||
opacity: 0.6;
|
||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||
will-change: transform, opacity;
|
||
}
|
||
.card-face--active {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
box-shadow: 0 8rpx 32rpx rgba(15, 23, 42, 0.12);
|
||
}
|
||
.card-face-inner {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 16rpx;
|
||
overflow: hidden;
|
||
}
|
||
.card-bg {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
}
|
||
|
||
/* 指示点(卡内右下角) */
|
||
.card-dots {
|
||
position: absolute; bottom: 48rpx; right: 48rpx; z-index: 2;
|
||
display: flex; gap: 10rpx;
|
||
}
|
||
.card-dot {
|
||
width: 10rpx; height: 10rpx; border-radius: 50%;
|
||
background: rgba(255,255,255,0.4); transition: all 300ms;
|
||
}
|
||
.card-dot.active { background: $color-card; width: 22rpx; border-radius: 5rpx; }
|
||
|
||
/* === 持卡人信息 === */
|
||
.holder-panel {
|
||
display: flex; align-items: flex-start; gap: 12px;
|
||
margin: 0 20rpx; background: $color-card;
|
||
border-radius: 24rpx; padding: 28rpx;
|
||
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.06);
|
||
}
|
||
.avatar-wrap { position: relative; width: 288rpx; height: 358.4rpx; flex-shrink: 0; }
|
||
.holder-avatar {
|
||
width: 288rpx; height: 358.4rpx; border-radius: 0;
|
||
border: 3rpx solid #eeeeee;
|
||
}
|
||
.avatar-zoom {
|
||
position: absolute; bottom: -2rpx; right: -2rpx;
|
||
width: 34rpx; height: 34rpx; background: $color-card;
|
||
border-radius: 50%; display: flex; align-items: center;
|
||
justify-content: center; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||
}
|
||
.zoom-icon {
|
||
width: 18rpx; height: 18rpx;
|
||
border: 1rpx solid $uv-primary; border-radius: 50%;
|
||
position: relative;
|
||
}
|
||
.zoom-icon::after {
|
||
content: ''; width: 6rpx; height: 2rpx;
|
||
background: $uv-primary; position: absolute;
|
||
bottom: -2rpx; right: -2rpx;
|
||
transform: rotate(45deg); border-radius: 1rpx;
|
||
}
|
||
.holder-details { flex: 1; min-width: 0; align-self: stretch; display: flex; flex-direction: column; justify-content: space-between; gap: 16rpx; }
|
||
.detail-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 12rpx; }
|
||
.detail-label { font-size: $font-size-sm; color: #909399; flex-shrink: 0; }
|
||
.detail-value-wrap { display: flex; align-items: center; gap: 12rpx; }
|
||
.detail-value { font-size: $font-size-sm; color: $uv-content-color; word-break: break-all; text-align: right; }
|
||
.copy-btn {
|
||
font-size: 22rpx; color: $uv-primary;
|
||
padding: 4rpx 16rpx; border: 1rpx solid $uv-primary;
|
||
border-radius: 8rpx; transition: opacity 150ms;
|
||
}
|
||
.copy-btn:active { opacity: 0.6; }
|
||
|
||
/* === 二维码区域 === */
|
||
.qrcode-section {
|
||
display: flex; flex-direction: column; align-items: center;
|
||
margin: 20rpx 20rpx 20rpx; background: $color-card;
|
||
border-radius: 24rpx; padding: 28rpx;
|
||
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.06);
|
||
}
|
||
.qrcode-title {
|
||
font-size: $font-size-lg; font-weight: $font-weight-medium; color: $uv-main-color;
|
||
display: block; margin-bottom: 20rpx; align-self: flex-start;
|
||
}
|
||
.qrcode-content {
|
||
font-size: $font-size-base; color: $uv-content-color;
|
||
line-height: 1.7; word-break: break-all; align-self: flex-start;
|
||
}
|
||
.qrcode-fallback {
|
||
font-size: $font-size-base; color: $uv-content-color;
|
||
line-height: 1.7; word-break: break-all; align-self: flex-start;
|
||
}
|
||
.qrcode-box {
|
||
width: 360rpx; height: 360rpx; background: #f8fafc;
|
||
border-radius: 16rpx; display: flex; align-items: center;
|
||
justify-content: center; border: 1rpx solid #eeeeee;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
.qrcode-img { width: 300rpx; height: 300rpx; }
|
||
.qrcode-countdown { display: flex; align-items: center; gap: 8rpx; }
|
||
.countdown-icon {
|
||
width: 20rpx; height: 20rpx;
|
||
border: 3rpx solid #666666; border-radius: 50%;
|
||
position: relative;
|
||
}
|
||
.countdown-icon::before {
|
||
content: ''; position: absolute;
|
||
width: 3rpx; height: 8rpx; background: #666666;
|
||
top: 2rpx; left: 50%; transform: translateX(-50%);
|
||
border-radius: 1rpx;
|
||
}
|
||
.countdown-text { font-size: $font-size-sm; color: $uv-tips-color; }
|
||
|
||
/* === 功能入口 === */
|
||
.feature-panel {
|
||
margin: 0 20rpx 20rpx; background: $color-card;
|
||
border-radius: 24rpx; padding: 28rpx;
|
||
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.06);
|
||
}
|
||
.feature-grid {
|
||
display: flex; justify-content: space-around;
|
||
}
|
||
.feature-item {
|
||
display: flex; flex-direction: column; align-items: center;
|
||
min-width: 110rpx; transition: opacity 150ms;
|
||
}
|
||
.feature-item:active { opacity: 0.6; }
|
||
.feature-icon-img {
|
||
width: 88rpx; height: 88rpx;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
.feature-label { font-size: $font-size-sm; color: $uv-content-color; }
|
||
</style>
|