392 行
9.4 KiB
Vue
392 行
9.4 KiB
Vue
<template>
|
||
<view class="ticket-container">
|
||
<view class="ticket-card">
|
||
<!-- 票据头部 -->
|
||
<view class="ticket-header">
|
||
<view class="game-badge">{{ ticketData.game }}</view>
|
||
<view class="stamp-badge" :class="{ verified: isCheckedIn }">
|
||
{{ isCheckedIn ? '已核销入队' : '待核销' }}
|
||
</view>
|
||
</view>
|
||
|
||
<view class="ticket-title">{{ ticketData.title }}</view>
|
||
|
||
<view class="ticket-meta">
|
||
<view class="meta-row">
|
||
<text class="meta-k">开黑时间</text>
|
||
<text class="meta-v">{{ ticketData.startTime }}</text>
|
||
</view>
|
||
<view class="meta-row">
|
||
<text class="meta-k">语音频道</text>
|
||
<text class="meta-v">{{ ticketData.location }}</text>
|
||
</view>
|
||
<view class="meta-row">
|
||
<text class="meta-k">房主昵称</text>
|
||
<text class="meta-v">{{ ticketData.creatorNickname }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 撕票齿轮分割虚线与两侧凹槽 -->
|
||
<view class="ticket-punch-wrap">
|
||
<view class="punch punch-left"></view>
|
||
<view class="dash-line"></view>
|
||
<view class="punch punch-right"></view>
|
||
</view>
|
||
|
||
<!-- 核心亮点:4位数字核销码专属卡槽 -->
|
||
<view class="code-section">
|
||
<view class="code-tips">进房向房主出示 4 位入队核销码</view>
|
||
<view class="code-slots">
|
||
<view v-for="(char, idx) in verifyCodeDigits" :key="idx" class="digit-box">
|
||
<text class="digit-text">{{ char }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 条形码与二维码模拟区域 -->
|
||
<view class="qrcode-section">
|
||
<view class="barcode-graphic">
|
||
<view class="bar" v-for="i in 36" :key="i" :style="{ width: (i % 3 === 0 ? '6rpx' : '2rpx'), marginRight: (i % 2 === 0 ? '4rpx' : '2rpx') }"></view>
|
||
</view>
|
||
<text class="barcode-num">TICKET-2026-9025</text>
|
||
</view>
|
||
|
||
<!-- 动态防伪时间与安全保障 -->
|
||
<view class="security-footer">
|
||
<view class="security-time">
|
||
<text class="dot-green"></text>
|
||
<text class="time-text">动态防伪时间:{{ currentTime }}</text>
|
||
</view>
|
||
<view class="anti-counterfeit-bar"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 演示操作按钮 -->
|
||
<view class="action-wrap">
|
||
<button class="btn-verify-demo btn-active" @click="toggleCheckIn">
|
||
{{ isCheckedIn ? '重置为待核销状态' : '模拟房主核销此门票' }}
|
||
</button>
|
||
<button class="btn-back btn-active" @click="goBack">返回活动详情</button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||
import { ACTIVITIES_LIST } from '@/common/mock.js';
|
||
|
||
const ticketData = ref(ACTIVITIES_LIST[0]);
|
||
const verifyCode = ref('8392');
|
||
const isCheckedIn = ref(false);
|
||
const currentTime = ref('');
|
||
|
||
const verifyCodeDigits = computed(() => {
|
||
return verifyCode.value.split('');
|
||
});
|
||
|
||
let timer = null;
|
||
const updateTime = () => {
|
||
const now = new Date();
|
||
const pad = (n) => String(n).padStart(2, '0');
|
||
const y = now.getFullYear();
|
||
const m = pad(now.getMonth() + 1);
|
||
const d = pad(now.getDate());
|
||
const hh = pad(now.getHours());
|
||
const mm = pad(now.getMinutes());
|
||
const ss = pad(now.getSeconds());
|
||
currentTime.value = `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
|
||
};
|
||
|
||
onMounted(() => {
|
||
updateTime();
|
||
timer = setInterval(updateTime, 1000);
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
if (timer) clearInterval(timer);
|
||
});
|
||
|
||
const toggleCheckIn = () => {
|
||
isCheckedIn.value = !isCheckedIn.value;
|
||
uni.showToast({
|
||
title: isCheckedIn.value ? '房主已成功核销该4位门票码!' : '状态已重置为待核销',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
};
|
||
|
||
const goBack = () => {
|
||
uni.navigateBack();
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.ticket-container {
|
||
min-height: 100vh;
|
||
background-color: #0A0F1D;
|
||
padding: 40rpx 36rpx calc(60rpx + env(safe-area-inset-bottom));
|
||
}
|
||
|
||
.ticket-card {
|
||
background: #151D30;
|
||
border-radius: 32rpx;
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.5);
|
||
overflow: hidden;
|
||
position: relative;
|
||
|
||
.ticket-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 32rpx 32rpx 16rpx;
|
||
|
||
.game-badge {
|
||
background: rgba(99, 102, 241, 0.2);
|
||
color: #818CF8;
|
||
font-size: 22rpx;
|
||
padding: 6rpx 16rpx;
|
||
border-radius: 8rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.stamp-badge {
|
||
font-size: 22rpx;
|
||
padding: 6rpx 20rpx;
|
||
border-radius: 24rpx;
|
||
background: rgba(245, 158, 11, 0.15);
|
||
color: #F59E0B;
|
||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||
font-weight: 700;
|
||
transition: all 0.3s ease;
|
||
|
||
&.verified {
|
||
background: rgba(16, 185, 129, 0.15);
|
||
color: #10B981;
|
||
border-color: rgba(16, 185, 129, 0.5);
|
||
box-shadow: 0 0 16rpx rgba(16, 185, 129, 0.3);
|
||
animation: stampIn 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
|
||
}
|
||
}
|
||
}
|
||
|
||
.ticket-title {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: #FFFFFF;
|
||
padding: 0 32rpx 24rpx;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.ticket-meta {
|
||
padding: 0 32rpx 32rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12rpx;
|
||
|
||
.meta-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 24rpx;
|
||
|
||
.meta-k {
|
||
color: #64748B;
|
||
}
|
||
.meta-v {
|
||
color: #CBD5E1;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
|
||
.ticket-punch-wrap {
|
||
position: relative;
|
||
height: 48rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.punch {
|
||
position: absolute;
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
border-radius: 50%;
|
||
background-color: #0A0F1D;
|
||
|
||
&.punch-left {
|
||
left: -24rpx;
|
||
}
|
||
&.punch-right {
|
||
right: -24rpx;
|
||
}
|
||
}
|
||
|
||
.dash-line {
|
||
width: 84%;
|
||
height: 2rpx;
|
||
border-bottom: 2rpx dashed rgba(255, 255, 255, 0.15);
|
||
}
|
||
}
|
||
|
||
.code-section {
|
||
padding: 36rpx 32rpx 24rpx;
|
||
text-align: center;
|
||
|
||
.code-tips {
|
||
font-size: 22rpx;
|
||
color: #94A3B8;
|
||
margin-bottom: 24rpx;
|
||
}
|
||
|
||
.code-slots {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 20rpx;
|
||
|
||
.digit-box {
|
||
width: 110rpx;
|
||
height: 130rpx;
|
||
background: rgba(10, 15, 29, 0.85);
|
||
border: 2rpx solid #10B981;
|
||
border-radius: 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 0 24rpx rgba(16, 185, 129, 0.4), inset 0 0 16rpx rgba(16, 185, 129, 0.15);
|
||
animation: slotPulse 2.5s ease-in-out infinite alternate;
|
||
|
||
.digit-text {
|
||
font-size: 64rpx;
|
||
font-weight: 800;
|
||
color: #10B981;
|
||
font-family: 'Courier New', Courier, monospace;
|
||
font-variant-numeric: tabular-nums;
|
||
text-shadow: 0 0 16rpx rgba(16, 185, 129, 0.7);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.qrcode-section {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 16rpx 32rpx 28rpx;
|
||
|
||
.barcode-graphic {
|
||
display: flex;
|
||
height: 60rpx;
|
||
align-items: center;
|
||
opacity: 0.8;
|
||
|
||
.bar {
|
||
height: 100%;
|
||
background-color: #F8FAFC;
|
||
}
|
||
}
|
||
|
||
.barcode-num {
|
||
font-size: 20rpx;
|
||
color: #64748B;
|
||
letter-spacing: 4rpx;
|
||
margin-top: 10rpx;
|
||
}
|
||
}
|
||
|
||
.security-footer {
|
||
background: rgba(10, 15, 29, 0.5);
|
||
padding: 20rpx 32rpx;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.04);
|
||
|
||
.security-time {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12rpx;
|
||
|
||
.dot-green {
|
||
width: 12rpx;
|
||
height: 12rpx;
|
||
border-radius: 50%;
|
||
background-color: #10B981;
|
||
box-shadow: 0 0 8rpx #10B981;
|
||
}
|
||
|
||
.time-text {
|
||
font-size: 20rpx;
|
||
color: #94A3B8;
|
||
font-family: 'Courier New', monospace;
|
||
font-variant-numeric: tabular-nums;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
}
|
||
|
||
.anti-counterfeit-bar {
|
||
height: 4rpx;
|
||
margin-top: 12rpx;
|
||
background: linear-gradient(90deg, #6366F1, #10B981, #F59E0B, #6366F1);
|
||
background-size: 200% 100%;
|
||
border-radius: 2rpx;
|
||
animation: gradientMove 3s infinite linear;
|
||
}
|
||
}
|
||
}
|
||
|
||
.action-wrap {
|
||
margin-top: 48rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20rpx;
|
||
|
||
.btn-verify-demo {
|
||
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
|
||
color: #FFFFFF;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
height: 88rpx;
|
||
line-height: 88rpx;
|
||
border-radius: 44rpx;
|
||
box-shadow: 0 4rpx 20rpx rgba(99, 102, 241, 0.4);
|
||
}
|
||
|
||
.btn-back {
|
||
background: #151D30;
|
||
color: #94A3B8;
|
||
font-size: 26rpx;
|
||
height: 84rpx;
|
||
line-height: 84rpx;
|
||
border-radius: 44rpx;
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
}
|
||
}
|
||
|
||
@keyframes gradientMove {
|
||
0% { background-position: 0% 50%; }
|
||
100% { background-position: 200% 50%; }
|
||
}
|
||
|
||
@keyframes stampIn {
|
||
0% {
|
||
transform: scale(2.4) rotate(-22deg);
|
||
opacity: 0;
|
||
}
|
||
70% {
|
||
transform: scale(0.92) rotate(-6deg);
|
||
opacity: 1;
|
||
}
|
||
100% {
|
||
transform: scale(1) rotate(-4deg);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@keyframes slotPulse {
|
||
0% {
|
||
box-shadow: 0 0 16rpx rgba(16, 185, 129, 0.3), inset 0 0 10rpx rgba(16, 185, 129, 0.1);
|
||
}
|
||
100% {
|
||
box-shadow: 0 0 28rpx rgba(16, 185, 129, 0.6), inset 0 0 18rpx rgba(16, 185, 129, 0.25);
|
||
}
|
||
}
|
||
</style>
|