第一个版本

This commit is contained in:
lcj
2026-07-06 09:17:41 +08:00
parent 1b4dcabad2
commit 203d129ffb
37 changed files with 2625 additions and 1199 deletions
+241 -77
View File
@@ -1,7 +1,26 @@
<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-if="!hasCard">
<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" />
@@ -24,23 +43,28 @@
class="card-swiper"
:current="currentCardIndex"
@change="onCardChange"
previous-margin="24rpx"
next-margin="24rpx"
circular
previous-margin="60rpx"
next-margin="60rpx"
>
<swiper-item v-for="(card, index) in userCards" :key="card.id">
<view class="card-face">
<image
class="card-bg"
src="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="widthFix"
/>
<!-- 多卡指示点(卡内右下角) -->
<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 === currentCardIndex }"
:class="{ active: idx === index }"
/>
</view>
</view>
@@ -59,12 +83,12 @@
</view>
<!-- 入园码二维码 -->
<view class="qrcode-section">
<view class="qrcode-section" v-if="!showFaceModal && !isLeaving">
<text class="qrcode-title">入园码</text>
<view class="qrcode-box">
<image
class="qrcode-img"
src="/static/qr-demo.png"
:src="userInfoData?.qrCode || '/static/qr-demo.png'"
mode="aspectFit"
/>
</view>
@@ -80,7 +104,7 @@
<view class="avatar-wrap" @click="previewAvatar">
<image
class="holder-avatar"
src="https://hn12301.oss-cn-shenzhen.aliyuncs.com/hn12301/1782285689132_1782285690033.png"
:src="userInfoData?.img || '/static/logo.png'"
mode="aspectFill"
/>
<view class="avatar-zoom">
@@ -88,124 +112,190 @@
</view>
</view>
<view class="holder-main">
<text class="holder-name">{{ currentCard.userName }}</text>
<text class="holder-tag">已激活</text>
<text class="holder-name">{{ userInfoData?.userName || '-' }}</text>
</view>
</view>
<view class="holder-details">
<view class="detail-row">
<text class="detail-label">卡种名称</text>
<text class="detail-value">{{ currentCard.cardName }}</text>
<text class="detail-value">{{ currentCard.cardModelName }}</text>
</view>
<view class="detail-row">
<text class="detail-label">旅游卡卡号</text>
<view class="detail-value-wrap">
<text class="detail-value">PX00002489</text>
<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">{{ currentCard.expireDate }}</text>
<text class="detail-value">{{ currentCard.validityTime }}</text>
</view>
<view class="detail-row">
<text class="detail-label">证件类型</text>
<text class="detail-value">{{ currentCard.idType }}</text>
<text class="detail-value">{{ userInfoData?.idType || '-' }}</text>
</view>
<view class="detail-row">
<text class="detail-label">证件号码</text>
<text class="detail-value">{{ currentCard.idNumber }}</text>
<text class="detail-value">{{ userInfoData?.idNo || '-' }}</text>
</view>
<view class="detail-row">
<text class="detail-label">手机号码</text>
<text class="detail-value">{{ currentCard.phone }}</text>
<text class="detail-value">{{ userInfoData?.mobile || userStore.userInfo?.phone || '-' }}</text>
</view>
</view>
</view>
<!-- 底部占位(滑到底时与tabBar的间隔) -->
<view style="height: 48rpx;" />
<view style="height: 48rpx;"/>
</scroll-view>
<!-- 人脸绑定提醒弹窗 -->
<ConfirmModal
v-model="showFaceModal"
title="系统安全升级提醒"
:content="'为保障您的旅游卡信息安全、规范入园使用,平台已启动人脸核验升级。\n根据规则要求,使用旅游卡前需完成人脸绑定,核验仅用于身份核实,人脸信息严格保密,不会挪作他用。\n请立即完成人脸绑定,方可继续使用全部功能。'"
confirmText="前往人脸绑定"
@confirm="onFaceModalConfirm"
/>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ref, computed, 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 } from '../../api/types'
import ConfirmModal from '../../components/confirm-modal.vue'
const userStore = useUserStore()
interface UserCardData {
id: string
cardName: string
cardNo: string
userName: string
expireDate: string
idType: string
idNumber: string
phone: string
}
const userCards = ref<UserCardData[]>([
{
id: '1',
cardName: '赣湘全域旅游年卡',
cardNo: 'HN2026-0001-8888',
userName: '张三',
expireDate: '2027.06.24',
idType: '身份证',
idNumber: '360102199001011234',
phone: '13812345678',
},
{
id: '2',
cardName: '超级玩家卡',
cardNo: 'HN2026-0002-6666',
userName: '张三',
expireDate: '2027.06.24',
idType: '身份证',
idNumber: '360102199001011234',
phone: '13812345678',
},
])
const notLoggedIn = ref(false)
const loading = ref(false)
const loaded = ref(false)
const userCards = ref<UserCardListItem[]>([])
const userInfoData = ref<QrAndUserInfoResult | null>(null)
const showFaceModal = ref(false)
const isLeaving = ref(false)
const hasCard = computed(() => userCards.value.length > 0)
const currentCardIndex = ref(0)
const currentCard = computed(() => userCards.value[currentCardIndex.value])
const countdown = ref(60)
const countdown = ref(20)
let timer: ReturnType<typeof setInterval> | null = null
/** 获取用户年卡列表 */
async function fetchCards() {
const mobile = uni.getStorageSync('phone') as string
if (!mobile) {
loaded.value = true
return
}
loading.value = true
try {
const list = (await getUserCardList({ mobile, saleChannel: 'xgh' })) || []
userCards.value = list
const firstCard = list[0]
if (firstCard?.needFaceImgFlag) {
showFaceModal.value = true
}
if (list.length > 0) {
fetchUserInfo(firstCard)
}
} catch {
userCards.value = []
} finally {
loaded.value = true
loading.value = false
}
}
/** 刷新当前卡的人脸绑定状态(从接口获取最新数据) */
async function refreshNeedFace() {
const mobile = uni.getStorageSync('phone') as string
if (!mobile) return
try {
const list = (await getUserCardList({ mobile, saleChannel: 'xgh' })) || []
userCards.value = list
const current = list[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 previewAvatar() {
if (!userInfoData.value?.img) return
uni.previewImage({
urls: ['https://hn12301.oss-cn-shenzhen.aliyuncs.com/hn12301/1782285689132_1782285690033.png'],
urls: [userInfoData.value.img],
current: 0,
})
}
function resetCountdown() {
countdown.value = 60
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/card/list' })
}
function copyCardNo() {
uni.setClipboardData({
data: 'PX00002489',
data: currentCard.value.cardNo,
success: () => { uni.showToast({ title: '已复制', icon: 'success' }) },
})
}
@@ -214,11 +304,36 @@ 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: '/static/icon/card.png' },
{ label: '我要预约', icon: '/static/icon/reserve.png' },
{ label: '使用记录', icon: '/static/icon/record.png' },
{ label: '使用说明', icon: '/static/icon/scenic.png' },
{ label: '我要绑定', icon: 'https://aliyunoss.hntgov.net/pxmall/card.png' },
{ label: '我要预约', icon: 'https://aliyunoss.hntgov.net/pxmall/reserve.png' },
{ label: '使用记录', icon: 'https://aliyunoss.hntgov.net/pxmall/record.png' },
{ label: '使用说明', icon: 'https://aliyunoss.hntgov.net/pxmall/scenic.png' },
]
function onFeatureClick(item: { label: string }) {
@@ -227,7 +342,10 @@ function onFeatureClick(item: { label: string }) {
return
}
if (item.label === '我要预约') {
uni.navigateTo({ url: '/packages/content/pages/scenic-list' })
const url = currentCard.value?.thirdId
? `/packages/content/pages/scenic-list?cardModelId=${currentCard.value.thirdId}`
: '/packages/content/pages/scenic-list'
uni.navigateTo({ url })
return
}
if (item.label === '使用说明') {
@@ -241,7 +359,20 @@ function onFeatureClick(item: { label: string }) {
uni.showToast({ title: `${item.label}功能开发中`, icon: 'none' })
}
onMounted(() => { startTimer() })
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>
@@ -250,6 +381,22 @@ onUnmounted(() => { if (timer) clearInterval(timer) })
.page { min-height: 100vh; background: $color-background; font-family: $font-family-body; }
.scroll-content { height: 100vh; }
/* === 加载中 === */
.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;
@@ -280,18 +427,35 @@ onUnmounted(() => { if (timer) clearInterval(timer) })
.empty-btn-secondary:active { opacity: 0.7; }
.empty-btn-secondary-text { font-size: $font-size-base; color: $uv-content-color; }
/* === 卡面展示区域 === */
.card-swiper-wrap { margin: 20rpx 20rpx 20rpx; overflow: visible; }
.card-swiper { height: 440rpx; }
/* === 卡面展示区域(3D Coverflow) === */
.card-swiper-wrap { margin: 14rpx 14rpx 14rpx; overflow: visible; perspective: 1200rpx; }
.card-swiper { height: 360rpx; }
.card-face {
margin: 0 12rpx;
border-radius: 24rpx;
position: relative; overflow: hidden;
background: #222222;
display: flex; align-items: center; justify-content: center;
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%; display: block;
width: 100%;
height: 100%;
display: block;
}
/* 指示点(卡内右下角) */