Files
pingxiangcard/src/pages/qrcode/index.vue
T
jacobxu666andClaude 4ad874adf9 refactor: 全项目 UI/UX 标准化修复
- P0: uni.scss 全局 Token 修正(字体SourceHanSerifCN、色彩#111/#333/#666/#999/#eeeeee、删除600字重)
- P0: 分包6页面色彩迁移(#4A90D9→#3c9cff、#E74C3C→#e53935、圆角统一24rpx)
- P1: qrcode页面全量SCSS迁移(12个硬编码色值替换、emoji图标替换、圆角统一)
- P1: index页面价格色修正(#f9ae3d→#e53935、按钮主色统一、硬编码字号对齐)
- P2: 10个页面字号/圆角/字体统一(notice-list/scenic/login/profile/faq/card-bind等)
- P3: 全局 font-weight:600 替换为500/700(5处),删除semibold变量和工具类
- 新增 -size-title: 38rpx Token
- 构建验证通过

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-26 02:50:21 +08:00

413 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<!-- 未持卡状态 -->
<view class="empty-state" v-if="!hasCard">
<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"
previous-margin="24rpx"
next-margin="24rpx"
>
<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"
/>
<!-- 多卡指示点(卡内右下角) -->
<view class="card-dots" v-if="userCards.length > 1">
<view
v-for="(_, idx) in userCards"
:key="idx"
class="card-dot"
:class="{ active: idx === currentCardIndex }"
/>
</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="qrcode-section">
<text class="qrcode-title">入园码</text>
<view class="qrcode-box">
<image
class="qrcode-img"
src="/static/qr-demo.png"
mode="aspectFit"
/>
</view>
<view class="qrcode-countdown">
<view class="countdown-icon" />
<text class="countdown-text">{{ countdown }}秒后自动刷新</text>
</view>
</view>
<!-- 持卡人信息(页面底部) -->
<view class="holder-panel">
<view class="holder-header">
<view class="avatar-wrap" @click="previewAvatar">
<image
class="holder-avatar"
src="https://hn12301.oss-cn-shenzhen.aliyuncs.com/hn12301/1782285689132_1782285690033.png"
mode="aspectFill"
/>
<view class="avatar-zoom">
<view class="zoom-icon" />
</view>
</view>
<view class="holder-main">
<text class="holder-name">{{ currentCard.userName }}</text>
<text class="holder-tag">已激活</text>
</view>
</view>
<view class="holder-details">
<view class="detail-row">
<text class="detail-label">卡种名称</text>
<text class="detail-value">{{ currentCard.cardName }}</text>
</view>
<view class="detail-row">
<text class="detail-label">旅游卡卡号</text>
<view class="detail-value-wrap">
<text class="detail-value">PX00002489</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>
</view>
<view class="detail-row">
<text class="detail-label">证件类型</text>
<text class="detail-value">{{ currentCard.idType }}</text>
</view>
<view class="detail-row">
<text class="detail-label">证件号码</text>
<text class="detail-value">{{ currentCard.idNumber }}</text>
</view>
<view class="detail-row">
<text class="detail-label">手机号码</text>
<text class="detail-value">{{ currentCard.phone }}</text>
</view>
</view>
</view>
<!-- 底部占位(滑到底时与tabBar的间隔) -->
<view style="height: 48rpx;" />
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useUserStore } from '../../store/user'
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 hasCard = computed(() => userCards.value.length > 0)
const currentCardIndex = ref(0)
const currentCard = computed(() => userCards.value[currentCardIndex.value])
const countdown = ref(60)
let timer: ReturnType<typeof setInterval> | null = null
function onCardChange(e: any) {
currentCardIndex.value = e.detail.current
resetCountdown()
}
function previewAvatar() {
uni.previewImage({
urls: ['https://hn12301.oss-cn-shenzhen.aliyuncs.com/hn12301/1782285689132_1782285690033.png'],
current: 0,
})
}
function resetCountdown() {
countdown.value = 60
}
function startTimer() {
timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
resetCountdown()
}
}, 1000)
}
function goBuyCard() {
uni.navigateTo({ url: '/packages/user/pages/card/list' })
}
function copyCardNo() {
uni.setClipboardData({
data: 'PX00002489',
success: () => { uni.showToast({ title: '已复制', icon: 'success' }) },
})
}
function goBindCard() {
uni.navigateTo({ url: '/pages/card-bind/card-bind' })
}
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' },
]
function onFeatureClick(item: { label: string }) {
if (item.label === '我要绑定') {
uni.navigateTo({ url: '/pages/card-bind/card-bind' })
return
}
if (item.label === '我要预约') {
uni.navigateTo({ url: '/pages/scenic-list/scenic-list' })
return
}
if (item.label === '使用说明') {
uni.navigateTo({ url: '/pages/usage-guide/usage-guide' })
return
}
if (item.label === '使用记录') {
uni.navigateTo({ url: '/pages/reserve/reserve-list' })
return
}
uni.showToast({ title: `${item.label}功能开发中`, icon: 'none' })
}
onMounted(() => { startTimer() })
onUnmounted(() => { if (timer) clearInterval(timer) })
</script>
<style lang="scss" scoped>
$font: 'SourceHanSerifCN', '思源宋体', -apple-system, 'PingFang SC', sans-serif;
$primary: #3c9cff;
.page { min-height: 100vh; background: #f7f8fa; font-family: $font; }
.scroll-content { height: 100vh; }
/* === 空状态 === */
.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, $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: 32rpx; font-weight: 700; color: #111111; margin-bottom: 12rpx; }
.empty-desc { font-size: 26rpx; color: #666666; margin-bottom: 40rpx; }
.empty-btn {
background: $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: 30rpx; color: #fff; font-weight: 500; }
.empty-btn-secondary {
padding: 20rpx 60rpx; border-radius: 44rpx;
border: 2rpx solid #eeeeee; background: #fff;
transition: opacity 150ms;
}
.empty-btn-secondary:active { opacity: 0.7; }
.empty-btn-secondary-text { font-size: 28rpx; color: #333333; }
/* === 卡面展示区域 === */
.card-swiper-wrap { margin: 20rpx 20rpx 20rpx; overflow: visible; }
.card-swiper { height: 440rpx; }
.card-face {
margin: 0 12rpx;
border-radius: 24rpx;
position: relative; overflow: hidden;
background: #222222;
display: flex; align-items: center; justify-content: center;
}
.card-bg {
width: 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: #fff; width: 22rpx; border-radius: 5rpx; }
/* === 持卡人信息 === */
.holder-panel {
margin: 0 20rpx; background: #fff;
border-radius: 24rpx; padding: 28rpx;
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.06);
}
.holder-header {
display: flex; align-items: center; gap: 20rpx;
padding-bottom: 24rpx; margin-bottom: 20rpx;
border-bottom: 1rpx solid #f7f8fa;
}
.avatar-wrap { position: relative; width: 96rpx; height: 96rpx; flex-shrink: 0; }
.holder-avatar {
width: 96rpx; height: 96rpx; border-radius: 50%;
border: 3rpx solid #eeeeee;
}
.avatar-zoom {
position: absolute; bottom: -2rpx; right: -2rpx;
width: 34rpx; height: 34rpx; background: #fff;
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: 24rpx; height: 24rpx;
border: 3rpx solid $primary; border-radius: 50%;
position: relative;
}
.zoom-icon::after {
content: ''; width: 8rpx; height: 3rpx;
background: $primary; position: absolute;
bottom: -4rpx; right: -4rpx;
transform: rotate(45deg); border-radius: 2rpx;
}
.holder-main { flex: 1; }
.holder-name { font-size: 32rpx; font-weight: 500; color: #111111; display: block; }
.holder-tag {
display: inline-block; font-size: 22rpx; color: #00b42a;
background: #f0fff4; padding: 2rpx 12rpx; border-radius: 6rpx; margin-top: 6rpx;
}
.holder-details { display: flex; flex-direction: column; gap: 14rpx; }
.detail-row { display: flex; justify-content: space-between; align-items: center; }
.detail-label { font-size: 26rpx; color: #666666; }
.detail-value-wrap { display: flex; align-items: center; gap: 12rpx; }
.detail-value { font-size: 26rpx; color: #333333; }
.copy-btn {
font-size: 22rpx; color: $primary;
padding: 4rpx 16rpx; border: 1rpx solid $primary;
border-radius: 8rpx; transition: opacity 150ms;
}
.copy-btn:active { opacity: 0.6; }
/* === 二维码区域 === */
.qrcode-section {
display: flex; flex-direction: column; align-items: center;
margin: 0 20rpx 20rpx; background: #fff;
border-radius: 24rpx; padding: 28rpx;
box-shadow: 0 4rpx 20rpx rgba(15, 23, 42, 0.06);
}
.qrcode-title {
font-size: 30rpx; font-weight: 500; color: #111111;
display: block; margin-bottom: 20rpx; 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: 24rpx; color: #666666; }
/* === 功能入口 === */
.feature-panel {
margin: 0 20rpx 20rpx; background: #fff;
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: 24rpx; color: #333333; }
</style>