feat: 年卡相关页面(列表/详情/购买/激活/入园码/绑定)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2293e67f8c
commit
b4981587a2
@@ -1,3 +1,106 @@
|
||||
<template>
|
||||
<view><text>页面占位 - 后续任务实现</text></view>
|
||||
<view class="page">
|
||||
<!-- 年卡信息头部 -->
|
||||
<view class="card-info">
|
||||
<text class="card-name">{{ cardName }}</text>
|
||||
<text class="card-tip">请填写实名信息以激活年卡</text>
|
||||
</view>
|
||||
<!-- 实名表单 -->
|
||||
<view class="form-card">
|
||||
<view class="form-item">
|
||||
<text class="label">真实姓名</text>
|
||||
<input class="input" v-model="form.realName" placeholder="请输入真实姓名" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">身份证号</text>
|
||||
<input class="input" v-model="form.idCard" placeholder="请输入身份证号" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="label">手机号码</text>
|
||||
<input class="input" v-model="form.phone" placeholder="请输入手机号码" type="number" />
|
||||
</view>
|
||||
</view>
|
||||
<!-- 激活按钮 -->
|
||||
<view class="btn-area">
|
||||
<button class="btn-activate" @click="handleActivate" :disabled="activating">
|
||||
{{ activating ? '激活中...' : '立即激活' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useCardStore } from '../../../../store/card'
|
||||
import { showToast } from '../../../../utils'
|
||||
|
||||
const cardStore = useCardStore()
|
||||
const cardId = ref('')
|
||||
const cardName = ref('')
|
||||
const activating = ref(false)
|
||||
|
||||
/** 实名信息表单 */
|
||||
const form = reactive({
|
||||
realName: '',
|
||||
idCard: '',
|
||||
phone: ''
|
||||
})
|
||||
|
||||
/** 页面加载时解析路由参数 */
|
||||
onLoad((options) => {
|
||||
cardId.value = options?.cardId || ''
|
||||
cardName.value = decodeURIComponent(options?.name || '')
|
||||
})
|
||||
|
||||
/** 表单校验 */
|
||||
function validate(): boolean {
|
||||
if (!form.realName.trim()) {
|
||||
showToast('请输入真实姓名')
|
||||
return false
|
||||
}
|
||||
if (!/^\d{17}[\dXx]$/.test(form.idCard)) {
|
||||
showToast('请输入正确的身份证号')
|
||||
return false
|
||||
}
|
||||
if (!/^1\d{10}$/.test(form.phone)) {
|
||||
showToast('请输入正确的手机号码')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** 提交激活 */
|
||||
async function handleActivate() {
|
||||
if (!validate()) return
|
||||
if (!cardId.value) return
|
||||
activating.value = true
|
||||
try {
|
||||
await cardStore.activateCard(cardId.value, form)
|
||||
showToast('激活成功', 'success')
|
||||
// 激活成功后跳转入园码页
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({ url: `/packages/user/pages/card/qrcode?cardId=${cardId.value}` })
|
||||
}, 1500)
|
||||
} catch (err: any) {
|
||||
showToast(err.message || '激活失败')
|
||||
} finally {
|
||||
activating.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #f5f5f5; padding: 20rpx; box-sizing: border-box; }
|
||||
.card-info { background: #4A90D9; border-radius: 16rpx; padding: 30rpx; margin-bottom: 20rpx; }
|
||||
.card-name { font-size: 34rpx; font-weight: bold; color: #fff; display: block; margin-bottom: 8rpx; }
|
||||
.card-tip { font-size: 24rpx; color: rgba(255,255,255,0.8); }
|
||||
.form-card { background: #fff; border-radius: 16rpx; padding: 10rpx 30rpx; }
|
||||
.form-item { padding: 24rpx 0; border-bottom: 1rpx solid #f0f0f0; }
|
||||
.form-item:last-child { border-bottom: none; }
|
||||
.label { display: block; font-size: 26rpx; color: #666; margin-bottom: 12rpx; }
|
||||
.input { width: 100%; height: 80rpx; font-size: 30rpx; color: #333; box-sizing: border-box; }
|
||||
.btn-area { padding: 40rpx 20rpx; }
|
||||
.btn-activate { width: 100%; height: 96rpx; line-height: 96rpx; background: #4A90D9; color: #fff; border-radius: 48rpx; font-size: 32rpx; border: none; }
|
||||
.btn-activate[disabled] { background: #A5C8E8; }
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,66 @@
|
||||
<template>
|
||||
<view><text>页面占位 - 后续任务实现</text></view>
|
||||
<view class="page">
|
||||
<!-- 页面头部 -->
|
||||
<view class="header">
|
||||
<text class="title">绑定年卡</text>
|
||||
<text class="desc">输入卡号绑定已有年卡</text>
|
||||
</view>
|
||||
<!-- 卡号输入表单 -->
|
||||
<view class="form-card">
|
||||
<view class="form-item">
|
||||
<text class="label">卡号</text>
|
||||
<input class="input" v-model="cardNo" placeholder="请输入年卡卡号" />
|
||||
</view>
|
||||
</view>
|
||||
<!-- 绑定按钮 -->
|
||||
<view class="btn-area">
|
||||
<button class="btn-bind" @click="handleBind" :disabled="binding">
|
||||
{{ binding ? '绑定中...' : '立即绑定' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useCardStore } from '../../../../store/card'
|
||||
import { showToast } from '../../../../utils'
|
||||
|
||||
const cardStore = useCardStore()
|
||||
const cardNo = ref('')
|
||||
const binding = ref(false)
|
||||
|
||||
/** 执行绑定 */
|
||||
async function handleBind() {
|
||||
if (!cardNo.value.trim()) {
|
||||
showToast('请输入年卡卡号')
|
||||
return
|
||||
}
|
||||
binding.value = true
|
||||
try {
|
||||
await cardStore.bindCard(cardNo.value.trim())
|
||||
showToast('绑定成功', 'success')
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} catch (err: any) {
|
||||
showToast(err.message || '绑定失败')
|
||||
} finally {
|
||||
binding.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #f5f5f5; padding: 20rpx; box-sizing: border-box; }
|
||||
.header { padding: 20rpx 10rpx 30rpx; }
|
||||
.title { font-size: 36rpx; font-weight: bold; color: #333; display: block; margin-bottom: 8rpx; }
|
||||
.desc { font-size: 26rpx; color: #999; }
|
||||
.form-card { background: #fff; border-radius: 16rpx; padding: 10rpx 30rpx; }
|
||||
.form-item { padding: 24rpx 0; }
|
||||
.label { display: block; font-size: 26rpx; color: #666; margin-bottom: 12rpx; }
|
||||
.input { width: 100%; height: 80rpx; font-size: 30rpx; color: #333; box-sizing: border-box; }
|
||||
.btn-area { padding: 40rpx 20rpx; }
|
||||
.btn-bind { width: 100%; height: 96rpx; line-height: 96rpx; background: #4A90D9; color: #fff; border-radius: 48rpx; font-size: 32rpx; border: none; }
|
||||
.btn-bind[disabled] { background: #A5C8E8; }
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,106 @@
|
||||
<template>
|
||||
<view><text>页面占位 - 后续任务实现</text></view>
|
||||
<view class="page">
|
||||
<!-- 订单信息卡片 -->
|
||||
<view class="order-card">
|
||||
<text class="card-title">订单信息</text>
|
||||
<view class="order-row">
|
||||
<text class="label">年卡名称</text>
|
||||
<text class="value">{{ cardName }}</text>
|
||||
</view>
|
||||
<view class="order-row">
|
||||
<text class="label">有效期</text>
|
||||
<text class="value">365天</text>
|
||||
</view>
|
||||
<view class="order-row">
|
||||
<text class="label">价格</text>
|
||||
<text class="value price">¥{{ price }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 支付方式 -->
|
||||
<view class="pay-card">
|
||||
<text class="card-title">支付方式</text>
|
||||
<view class="pay-option active">
|
||||
<text class="pay-icon">💳</text>
|
||||
<text class="pay-name">模拟支付(测试)</text>
|
||||
<text class="pay-check">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 底部支付栏 -->
|
||||
<view class="bottom-bar">
|
||||
<view class="bottom-price">
|
||||
<text class="label">应付金额</text>
|
||||
<text class="value">¥{{ price }}</text>
|
||||
</view>
|
||||
<button class="btn-pay" @click="handlePay" :disabled="paying">
|
||||
{{ paying ? '支付中...' : '确认支付' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { createOrder, mockPay } from '../../../../api/order'
|
||||
import { showToast } from '../../../../utils'
|
||||
|
||||
const cardId = ref('')
|
||||
const cardName = ref('')
|
||||
const price = ref(0)
|
||||
const paying = ref(false)
|
||||
|
||||
/** 页面加载时解析路由参数 */
|
||||
onLoad((options) => {
|
||||
cardId.value = options?.id || ''
|
||||
cardName.value = decodeURIComponent(options?.name || '')
|
||||
price.value = Number(options?.price || 0)
|
||||
})
|
||||
|
||||
/** 执行支付流程 */
|
||||
async function handlePay() {
|
||||
if (!cardId.value) return
|
||||
paying.value = true
|
||||
try {
|
||||
// 1. 创建订单
|
||||
const order = await createOrder({ cardId: cardId.value })
|
||||
// 2. 模拟支付
|
||||
const payResult = await mockPay(order.id)
|
||||
if (payResult.success) {
|
||||
showToast('支付成功', 'success')
|
||||
// 3. 支付成功后跳转激活页
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({
|
||||
url: `/packages/user/pages/card/activate?cardId=${cardId.value}&name=${encodeURIComponent(cardName.value)}`
|
||||
})
|
||||
}, 1500)
|
||||
} else {
|
||||
showToast('支付失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
showToast(err.message || '支付失败')
|
||||
} finally {
|
||||
paying.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #f5f5f5; padding: 20rpx; padding-bottom: 140rpx; box-sizing: border-box; }
|
||||
.order-card, .pay-card { background: #fff; border-radius: 16rpx; padding: 24rpx 30rpx; margin-bottom: 20rpx; }
|
||||
.card-title { font-size: 30rpx; font-weight: bold; color: #333; display: block; margin-bottom: 20rpx; padding-bottom: 16rpx; border-bottom: 1rpx solid #f0f0f0; }
|
||||
.order-row { display: flex; justify-content: space-between; align-items: center; padding: 16rpx 0; }
|
||||
.label { font-size: 28rpx; color: #666; }
|
||||
.value { font-size: 28rpx; color: #333; }
|
||||
.value.price { color: #E74C3C; font-weight: bold; font-size: 36rpx; }
|
||||
.pay-option { display: flex; align-items: center; padding: 20rpx 0; }
|
||||
.pay-option.active { color: #4A90D9; }
|
||||
.pay-icon { font-size: 36rpx; margin-right: 16rpx; }
|
||||
.pay-name { flex: 1; font-size: 28rpx; }
|
||||
.pay-check { font-size: 32rpx; color: #4A90D9; font-weight: bold; }
|
||||
.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; display: flex; align-items: center; padding: 16rpx 30rpx; padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); border-top: 1rpx solid #eee; }
|
||||
.bottom-price { flex: 1; display: flex; flex-direction: column; }
|
||||
.bottom-price .label { font-size: 22rpx; color: #999; }
|
||||
.bottom-price .value { font-size: 40rpx; color: #E74C3C; font-weight: bold; }
|
||||
.btn-pay { background: #E74C3C; color: #fff; font-size: 30rpx; padding: 20rpx 60rpx; border-radius: 44rpx; border: none; }
|
||||
.btn-pay[disabled] { background: #F5A8A3; }
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,115 @@
|
||||
<template>
|
||||
<view><text>页面占位 - 后续任务实现</text></view>
|
||||
<view class="page" v-if="card">
|
||||
<!-- 封面图 -->
|
||||
<image class="cover" :src="card.coverImage" mode="aspectFill" />
|
||||
<!-- 基本信息 -->
|
||||
<view class="info-card">
|
||||
<text class="card-name">{{ card.name }}</text>
|
||||
<view class="price-row">
|
||||
<text class="price">¥{{ card.price }}</text>
|
||||
<text class="original-price" v-if="card.originalPrice">¥{{ card.originalPrice }}</text>
|
||||
</view>
|
||||
<text class="card-desc">{{ card.description }}</text>
|
||||
</view>
|
||||
<!-- 权益说明 -->
|
||||
<view class="section" v-if="card.benefits?.length">
|
||||
<text class="section-title">权益说明</text>
|
||||
<view class="benefit-list">
|
||||
<view class="benefit-item" v-for="(benefit, idx) in card.benefits" :key="idx">
|
||||
<text class="benefit-icon">✅</text>
|
||||
<text class="benefit-text">{{ benefit }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 权益景区 -->
|
||||
<view class="section" v-if="card.scenicSpots?.length">
|
||||
<text class="section-title">权益景区({{ card.scenicSpots.length }}个)</text>
|
||||
<scroll-view scroll-x class="scenic-scroll">
|
||||
<view class="scenic-list">
|
||||
<view class="scenic-card" v-for="spot in card.scenicSpots" :key="spot.id">
|
||||
<image class="scenic-img" :src="spot.image" mode="aspectFill" />
|
||||
<text class="scenic-name">{{ spot.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- 年卡详情(富文本) -->
|
||||
<view class="section" v-if="card.detail">
|
||||
<text class="section-title">年卡详情</text>
|
||||
<rich-text :nodes="card.detail" />
|
||||
</view>
|
||||
<!-- 底部购买栏 -->
|
||||
<view class="bottom-bar">
|
||||
<view class="bottom-price">
|
||||
<text class="label">价格</text>
|
||||
<text class="value">¥{{ card.price }}</text>
|
||||
</view>
|
||||
<button class="btn-buy" @click="handleBuy">立即购买</button>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 加载中状态 -->
|
||||
<view v-else class="loading-page"><text>加载中...</text></view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useCardStore } from '../../../../store/card'
|
||||
import type { CardDetail } from '../../../../api/types'
|
||||
|
||||
const cardStore = useCardStore()
|
||||
const card = ref<CardDetail | null>(null)
|
||||
|
||||
/** 页面加载时获取年卡详情 */
|
||||
onLoad((options) => {
|
||||
if (options?.id) {
|
||||
loadDetail(options.id)
|
||||
}
|
||||
})
|
||||
|
||||
/** 加载年卡详情数据 */
|
||||
async function loadDetail(id: string) {
|
||||
try {
|
||||
await cardStore.fetchCardDetail(id)
|
||||
card.value = cardStore.currentCard
|
||||
} catch (err) {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
/** 跳转购买页 */
|
||||
function handleBuy() {
|
||||
if (!card.value) return
|
||||
uni.navigateTo({
|
||||
url: `/packages/user/pages/card/buy?id=${card.value.id}&name=${encodeURIComponent(card.value.name)}&price=${card.value.price}`
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #f5f5f5; padding-bottom: 120rpx; }
|
||||
.cover { width: 100%; height: 400rpx; background: #ddd; }
|
||||
.info-card { background: #fff; padding: 24rpx 30rpx; margin-bottom: 16rpx; }
|
||||
.card-name { font-size: 36rpx; font-weight: bold; color: #333; display: block; }
|
||||
.price-row { display: flex; align-items: baseline; gap: 12rpx; margin: 12rpx 0; }
|
||||
.price { font-size: 44rpx; color: #E74C3C; font-weight: bold; }
|
||||
.original-price { font-size: 26rpx; color: #ccc; text-decoration: line-through; }
|
||||
.card-desc { font-size: 26rpx; color: #666; }
|
||||
.section { background: #fff; padding: 24rpx 30rpx; margin-bottom: 16rpx; }
|
||||
.section-title { font-size: 30rpx; font-weight: bold; color: #333; display: block; margin-bottom: 16rpx; }
|
||||
.benefit-list { display: flex; flex-direction: column; gap: 12rpx; }
|
||||
.benefit-item { display: flex; align-items: center; gap: 12rpx; }
|
||||
.benefit-icon { font-size: 28rpx; }
|
||||
.benefit-text { font-size: 26rpx; color: #666; }
|
||||
.scenic-scroll { white-space: nowrap; }
|
||||
.scenic-list { display: flex; gap: 16rpx; }
|
||||
.scenic-card { min-width: 200rpx; display: flex; flex-direction: column; }
|
||||
.scenic-img { width: 200rpx; height: 140rpx; border-radius: 12rpx; background: #eee; }
|
||||
.scenic-name { font-size: 24rpx; color: #333; margin-top: 8rpx; text-align: center; }
|
||||
.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; display: flex; align-items: center; padding: 16rpx 30rpx; padding-bottom: calc(16rpx + env(safe-area-inset-bottom)); border-top: 1rpx solid #eee; }
|
||||
.bottom-price { flex: 1; display: flex; flex-direction: column; }
|
||||
.bottom-price .label { font-size: 22rpx; color: #999; }
|
||||
.bottom-price .value { font-size: 36rpx; color: #E74C3C; font-weight: bold; }
|
||||
.btn-buy { background: #E74C3C; color: #fff; font-size: 30rpx; padding: 20rpx 60rpx; border-radius: 44rpx; border: none; }
|
||||
.loading-page { display: flex; align-items: center; justify-content: center; min-height: 100vh; color: #999; }
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,82 @@
|
||||
<template>
|
||||
<view><text>页面占位 - 后续任务实现</text></view>
|
||||
<view class="page">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<uni-search-bar v-model="keyword" placeholder="搜索年卡" @confirm="handleSearch" @clear="handleSearch" />
|
||||
</view>
|
||||
<!-- 分类筛选 -->
|
||||
<view class="filter-bar">
|
||||
<view
|
||||
v-for="cat in categories"
|
||||
:key="cat.value"
|
||||
class="filter-tag"
|
||||
:class="{ active: currentCategory === cat.value }"
|
||||
@click="selectCategory(cat.value)"
|
||||
>
|
||||
<text>{{ cat.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 年卡列表 -->
|
||||
<view class="card-list">
|
||||
<view v-if="cardStore.loading" class="loading"><text>加载中...</text></view>
|
||||
<view v-else-if="cardStore.cardList.length === 0">
|
||||
<empty-state icon="🎫" text="暂无符合条件的年卡" />
|
||||
</view>
|
||||
<view v-else>
|
||||
<card-item v-for="card in cardStore.cardList" :key="card.id" :card="card" @click="goDetail" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useCardStore } from '../../../../store/card'
|
||||
import CardItem from '../../../../components/card-item.vue'
|
||||
import EmptyState from '../../../../components/empty-state.vue'
|
||||
import type { CardInfo } from '../../../../api/types'
|
||||
|
||||
const cardStore = useCardStore()
|
||||
const keyword = ref('')
|
||||
const currentCategory = ref('')
|
||||
|
||||
/** 分类选项 */
|
||||
const categories = [
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '旅游年卡', value: 'travel' },
|
||||
{ label: '亲子年卡', value: 'family' },
|
||||
{ label: '情侣年卡', value: 'couple' }
|
||||
]
|
||||
|
||||
/** 页面加载时获取年卡列表 */
|
||||
onLoad(() => {
|
||||
cardStore.fetchCardList()
|
||||
})
|
||||
|
||||
/** 执行搜索 */
|
||||
function handleSearch() {
|
||||
cardStore.fetchCardList({ keyword: keyword.value, category: currentCategory.value })
|
||||
}
|
||||
|
||||
/** 选择分类 */
|
||||
function selectCategory(value: string) {
|
||||
currentCategory.value = value
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
/** 跳转年卡详情 */
|
||||
function goDetail(card: CardInfo) {
|
||||
uni.navigateTo({ url: `/packages/user/pages/card/detail?id=${card.id}` })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #f5f5f5; }
|
||||
.search-bar { background: #fff; padding: 12rpx 20rpx; }
|
||||
.filter-bar { display: flex; gap: 16rpx; padding: 16rpx 20rpx; background: #fff; border-bottom: 1rpx solid #f0f0f0; }
|
||||
.filter-tag { padding: 10rpx 24rpx; border-radius: 30rpx; font-size: 24rpx; color: #666; background: #f5f5f5; }
|
||||
.filter-tag.active { background: #4A90D9; color: #fff; }
|
||||
.card-list { padding: 20rpx; }
|
||||
.loading { text-align: center; padding: 60rpx; color: #999; font-size: 28rpx; }
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,89 @@
|
||||
<template>
|
||||
<view><text>页面占位 - 后续任务实现</text></view>
|
||||
<view class="page">
|
||||
<!-- 年卡信息头部 -->
|
||||
<view class="card-top">
|
||||
<text class="card-label">{{ qrInfo?.cardName || '年卡' }}</text>
|
||||
<text class="user-name">{{ qrInfo?.userName || '---' }}</text>
|
||||
<text class="expire-date">有效期至 {{ qrInfo?.expireDate || '---' }}</text>
|
||||
</view>
|
||||
<!-- 二维码区域 -->
|
||||
<view class="qr-card">
|
||||
<view class="qr-area">
|
||||
<image v-if="qrInfo?.qrCode" class="qr-image" :src="qrInfo.qrCode" mode="aspectFit" />
|
||||
<view v-else class="qr-placeholder"><text>加载中...</text></view>
|
||||
</view>
|
||||
<text class="qr-tip">扫码入园 · 每60秒自动刷新</text>
|
||||
</view>
|
||||
<!-- 操作按钮 -->
|
||||
<view class="actions">
|
||||
<button class="btn-secondary" @click="showComingSoon">预约景区</button>
|
||||
<button class="btn-primary" @click="refreshQrCode">刷新二维码</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onBeforeUnmount } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useCardStore } from '../../../../store/card'
|
||||
import { showToast } from '../../../../utils'
|
||||
import type { QrCodeInfo } from '../../../../api/types'
|
||||
|
||||
const cardStore = useCardStore()
|
||||
const cardId = ref('')
|
||||
const qrInfo = ref<QrCodeInfo | null>(null)
|
||||
let refreshTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
/** 页面加载时获取入园码并启动定时刷新 */
|
||||
onLoad((options) => {
|
||||
cardId.value = options?.cardId || ''
|
||||
if (cardId.value) {
|
||||
loadQrCode()
|
||||
// 每60秒自动刷新二维码
|
||||
refreshTimer = setInterval(() => {
|
||||
loadQrCode()
|
||||
}, 60000)
|
||||
}
|
||||
})
|
||||
|
||||
/** 页面销毁时清除定时器 */
|
||||
onBeforeUnmount(() => {
|
||||
if (refreshTimer) clearInterval(refreshTimer)
|
||||
})
|
||||
|
||||
/** 加载入园码 */
|
||||
async function loadQrCode() {
|
||||
try {
|
||||
qrInfo.value = await cardStore.getEntryQrCode(cardId.value)
|
||||
} catch (err: any) {
|
||||
showToast(err.message || '获取入园码失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 手动刷新二维码 */
|
||||
function refreshQrCode() {
|
||||
loadQrCode()
|
||||
showToast('已刷新', 'success')
|
||||
}
|
||||
|
||||
/** 预约景区(暂未开放) */
|
||||
function showComingSoon() {
|
||||
showToast('即将上线,敬请期待')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #f5f5f5; padding: 20rpx; box-sizing: border-box; }
|
||||
.card-top { background: linear-gradient(135deg, #4A90D9, #357ABD); border-radius: 20rpx; padding: 30rpx; color: #fff; margin-bottom: -20rpx; }
|
||||
.card-label { font-size: 24rpx; opacity: 0.8; display: block; }
|
||||
.user-name { font-size: 36rpx; font-weight: bold; display: block; margin: 12rpx 0 8rpx; }
|
||||
.expire-date { font-size: 22rpx; opacity: 0.7; display: block; }
|
||||
.qr-card { background: #fff; border-radius: 16rpx; padding: 30rpx; margin-top: 30rpx; box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.08); text-align: center; }
|
||||
.qr-area { width: 360rpx; height: 360rpx; margin: 20rpx auto; display: flex; align-items: center; justify-content: center; background: #f8f8f8; border-radius: 12rpx; }
|
||||
.qr-image { width: 320rpx; height: 320rpx; }
|
||||
.qr-placeholder { color: #999; font-size: 28rpx; }
|
||||
.qr-tip { font-size: 24rpx; color: #999; display: block; margin-top: 16rpx; }
|
||||
.actions { display: flex; gap: 20rpx; padding: 40rpx 20rpx; }
|
||||
.btn-secondary { flex: 1; height: 88rpx; line-height: 88rpx; background: #fff; color: #4A90D9; border: 2rpx solid #4A90D9; border-radius: 44rpx; font-size: 28rpx; }
|
||||
.btn-primary { flex: 1; height: 88rpx; line-height: 88rpx; background: #4A90D9; color: #fff; border-radius: 44rpx; font-size: 28rpx; border: none; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user