655 lines
15 KiB
Vue
655 lines
15 KiB
Vue
<template>
|
||
<view class="page-container">
|
||
<template v-if="actOptions && actOptions.length > 0">
|
||
<t-dropdown-menu class="top-dropdown">
|
||
<t-dropdown-item :options="actOptions" :value="selectActId" @change="getSignInfo"/>
|
||
</t-dropdown-menu>
|
||
<view class="content-container">
|
||
<view class="scan-area animate__animated animate__fadeIn" @click="handleScan">
|
||
<view class="scan-icon-wrapper pulse-animation">
|
||
<t-icon name="scan" size="80" color="#f45a5a"></t-icon>
|
||
<view class="scan-circles"></view>
|
||
</view>
|
||
<t-button shape="round"
|
||
theme="primary"
|
||
size="medium"
|
||
class="scan-button">扫描二维码签到
|
||
</t-button>
|
||
</view>
|
||
<view class="guide-area animate__animated animate__fadeInUp">
|
||
<view class="guide-title">签到说明</view>
|
||
<view class="guide-list">
|
||
<view class="guide-item animate__animated animate__fadeInLeft" style="animation-delay: 0.1s">
|
||
<t-icon name="notification" size="16" color="#ff5f5c" class="guide-icon"></t-icon>
|
||
<text class="guide-text">请对准活动二维码进行扫描</text>
|
||
</view>
|
||
<view class="guide-item animate__animated animate__fadeInLeft" style="animation-delay: 0.2s">
|
||
<t-icon name="notification" size="16" color="#ff5f5c" class="guide-icon"></t-icon>
|
||
<text class="guide-text">确保手机相机权限已开启</text>
|
||
</view>
|
||
<view class="guide-item animate__animated animate__fadeInLeft" style="animation-delay: 0.3s">
|
||
<t-icon name="notification" size="16" color="#ff5f5c" class="guide-icon"></t-icon>
|
||
<text class="guide-text">保持二维码清晰可见</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="current-status-area animate__animated animate__fadeInUp" style="animation-delay: 0.2s">
|
||
<view class="status-header">
|
||
<text class="status-title">当前签到状态</text>
|
||
</view>
|
||
<view v-if="signInfo" class="status-content">
|
||
<template v-if="signInfo.isSignIn">
|
||
<view class="sign-success">
|
||
<div class="success-icon-wrapper">
|
||
<t-icon name="task-checked-1" size="48" color="#07c160"></t-icon>
|
||
<svg class="checkmark-circle" viewBox="0 0 52 52">
|
||
<circle class="checkmark-circle__circle" cx="26" cy="26" r="25" fill="none" />
|
||
</svg>
|
||
</div>
|
||
<text class="success-text">已完成签到</text>
|
||
<view class="sign-detail">
|
||
<view class="detail-item">
|
||
<uni-icons type="calendar" size="14" color="#666666"/>
|
||
<text>签到时间:{{ signInfo.signInTime }}</text>
|
||
</view>
|
||
<!-- <view class="detail-item">-->
|
||
<!-- <uni-icons type="location" size="14" color="#666666"/>-->
|
||
<!-- <text>签到地点:{{ signInfo.location }}</text>-->
|
||
<!-- </view>-->
|
||
</view>
|
||
</view>
|
||
</template>
|
||
<template v-else>
|
||
<view class="sign-pending">
|
||
<t-icon name="info-circle" size="48" color="#f45a5a"></t-icon>
|
||
<text class="pending-text">待签到</text>
|
||
<text class="pending-tip">请使用扫码签到完成签到</text>
|
||
</view>
|
||
</template>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<div class="bg-decoration">
|
||
<svg class="wave" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320">
|
||
<path fill="#f45a5a" fill-opacity="0.1" d="M0,160L48,154.7C96,149,192,139,288,144C384,149,480,171,576,176C672,181,768,171,864,154.7C960,139,1056,117,1152,128C1248,139,1344,181,1392,202.7L1440,224L1440,0L1392,0C1344,0,1248,0,1152,0C1056,0,960,0,864,0C768,0,672,0,576,0C480,0,384,0,288,0C192,0,96,0,48,0L0,0Z"></path>
|
||
</svg>
|
||
<svg class="dots" width="100" height="100" viewBox="0 0 100 100">
|
||
<circle fill="#f45a5a" fill-opacity="0.2" cx="10" cy="10" r="3" />
|
||
<circle fill="#f45a5a" fill-opacity="0.2" cx="30" cy="10" r="3" />
|
||
<circle fill="#f45a5a" fill-opacity="0.2" cx="50" cy="10" r="3" />
|
||
<circle fill="#f45a5a" fill-opacity="0.2" cx="70" cy="10" r="3" />
|
||
<circle fill="#f45a5a" fill-opacity="0.2" cx="90" cy="10" r="3" />
|
||
<circle fill="#f45a5a" fill-opacity="0.2" cx="10" cy="30" r="3" />
|
||
<circle fill="#f45a5a" fill-opacity="0.2" cx="30" cy="30" r="3" />
|
||
<circle fill="#f45a5a" fill-opacity="0.2" cx="50" cy="30" r="3" />
|
||
<circle fill="#f45a5a" fill-opacity="0.2" cx="70" cy="30" r="3" />
|
||
<circle fill="#f45a5a" fill-opacity="0.2" cx="90" cy="30" r="3" />
|
||
<circle fill="#f45a5a" fill-opacity="0.2" cx="10" cy="50" r="3" />
|
||
<circle fill="#f45a5a" fill-opacity="0.2" cx="30" cy="50" r="3" />
|
||
<circle fill="#f45a5a" fill-opacity="0.2" cx="50" cy="50" r="3" />
|
||
<circle fill="#f45a5a" fill-opacity="0.2" cx="70" cy="50" r="3" />
|
||
<circle fill="#f45a5a" fill-opacity="0.2" cx="90" cy="50" r="3" />
|
||
</svg>
|
||
</div>
|
||
</template>
|
||
|
||
<view style="flex: 1;display: flex;justify-content: center;align-items: center">
|
||
<t-empty v-if="actOptions && actOptions.length === 0"
|
||
image="/static/img/empty.png"
|
||
description="报名活动后才能扫码签到"
|
||
style="flex: 1">
|
||
<template #action>
|
||
<t-button theme="primary" shape="round" size="large" @click="navigateToSignUp">去报名列表看看</t-button>
|
||
</template>
|
||
</t-empty>
|
||
</view>
|
||
</view>
|
||
|
||
<ScanQRCode
|
||
ref="qrScannerRef"
|
||
@scan-success="handleScanSuccess"
|
||
/>
|
||
|
||
</template>
|
||
|
||
<script setup>
|
||
import {ref, computed, onMounted} from 'vue';
|
||
import {useRequest} from '@/utils/request';
|
||
import {Dialog, Toast} from 'tdesign-mobile-vue';
|
||
import moment from 'moment';
|
||
import ScanQRCode from '@/components/scanQrCode.vue'
|
||
|
||
const request = useRequest()
|
||
|
||
const list = ref([])
|
||
const signInfo = ref({})
|
||
const actOptions = computed(() => {
|
||
return list.value.map(item => {
|
||
return {
|
||
value: item.id,
|
||
label: item.title
|
||
}
|
||
})
|
||
})
|
||
const selectActId = ref(null)
|
||
const qrScannerRef = ref()
|
||
|
||
onMounted(() => {
|
||
listMineAct();
|
||
});
|
||
|
||
// 获取活动列表
|
||
async function listMineAct() {
|
||
try {
|
||
const {data, code, msg} = await request.get('/client/activity/list-mine-act')
|
||
if (code === 0) {
|
||
list.value = data
|
||
if (list.value.length) {
|
||
selectActId.value = list.value[0].id
|
||
console.log(selectActId.value)
|
||
await getSignInfo();
|
||
}
|
||
} else {
|
||
Toast.error(msg)
|
||
}
|
||
} catch (e) {
|
||
console.error(e)
|
||
Toast.error(e.message)
|
||
}
|
||
}
|
||
|
||
// 获取签到信息
|
||
async function getSignInfo(val) {
|
||
try {
|
||
const {code, data, msg} = await request.get('/client/activity/sign-in-info/' + (val ? val : selectActId.value))
|
||
if (code === 0) {
|
||
signInfo.value = data
|
||
} else {
|
||
Toast.error(msg)
|
||
}
|
||
} catch (e) {
|
||
console.error(e)
|
||
Toast.error(e.message)
|
||
}
|
||
}
|
||
|
||
// 扫码签到
|
||
function handleScan() {
|
||
qrScannerRef.value.startScan()
|
||
}
|
||
|
||
// 扫码成功回调
|
||
function handleScanSuccess(val) {
|
||
console.log('扫描成功,活动ID为' + val, "当前选择活动ID为" + selectActId.value)
|
||
if (!val) {
|
||
Toast.error("请扫描合法的二维码")
|
||
return
|
||
}
|
||
if (String(val) !== String(selectActId.value)) {
|
||
Toast.error('请扫码签到当前活动,扫描成功,活动ID为' + val + ",当前选择活动ID为" + selectActId.value)
|
||
return
|
||
}
|
||
signIn()
|
||
}
|
||
|
||
// 签到
|
||
async function signIn() {
|
||
try {
|
||
await uni.showLoading({
|
||
title: '签到中...',
|
||
mask: true,
|
||
})
|
||
const {code, msg} = await request.post('/client/activity/sign-in/' + selectActId.value)
|
||
uni.hideLoading()
|
||
Dialog.alert({
|
||
title: code === 0 ? '签到成功' : '签到失败',
|
||
content: code === 0 ? '祝您活动顺利' : msg,
|
||
confirmBtn: '确定'
|
||
})
|
||
if (code === 0) {
|
||
await getSignInfo()
|
||
}
|
||
} catch (e) {
|
||
Toast.error(e.message)
|
||
console.error(e)
|
||
} finally {
|
||
uni.hideLoading()
|
||
}
|
||
}
|
||
|
||
// 去报名列表
|
||
function navigateToSignUp(){
|
||
uni.redirectTo({
|
||
url: '/pages/activity/list/index'
|
||
})
|
||
}
|
||
|
||
</script>
|
||
|
||
<style scoped lang="less">
|
||
@keyframes pulse {
|
||
0% {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
transform: scale(1.1);
|
||
opacity: 0.8;
|
||
}
|
||
100% {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@keyframes ripple {
|
||
0% {
|
||
transform: scale(0.8);
|
||
opacity: 1;
|
||
}
|
||
100% {
|
||
transform: scale(1.5);
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
@keyframes checkmark-circle {
|
||
0% {
|
||
stroke-dashoffset: 166;
|
||
}
|
||
100% {
|
||
stroke-dashoffset: 0;
|
||
}
|
||
}
|
||
|
||
@keyframes float {
|
||
0% {
|
||
transform: translateY(0);
|
||
}
|
||
50% {
|
||
transform: translateY(-10px);
|
||
}
|
||
100% {
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.top-dropdown {
|
||
flex-shrink: 0;
|
||
z-index: 10;
|
||
position: relative;
|
||
}
|
||
|
||
.content-container {
|
||
padding: 15px;
|
||
z-index: 9;
|
||
position: relative;
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.scan-area {
|
||
background-color: #ffffff;
|
||
border-radius: 16px;
|
||
padding: 64px 0;
|
||
margin-bottom: 32rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: all 0.3s ease;
|
||
|
||
&:active {
|
||
transform: scale(0.98);
|
||
}
|
||
}
|
||
|
||
.scan-icon-wrapper {
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 32rpx;
|
||
position: relative;
|
||
}
|
||
|
||
.pulse-animation {
|
||
animation: pulse 2s infinite ease-in-out;
|
||
}
|
||
|
||
.scan-circles {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
width: 100%;
|
||
height: 100%;
|
||
transform: translate(-50%, -50%);
|
||
|
||
&:before, &:after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
border-radius: 50%;
|
||
border: 2px solid rgba(244, 90, 90, 0.3);
|
||
animation: ripple 2s infinite;
|
||
}
|
||
|
||
&:after {
|
||
animation-delay: 0.5s;
|
||
}
|
||
}
|
||
|
||
.scan-button {
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: all 0.3s ease;
|
||
|
||
&:active {
|
||
transform: translateY(2px);
|
||
}
|
||
|
||
&:after {
|
||
content: '';
|
||
position: absolute;
|
||
top: -50%;
|
||
left: -50%;
|
||
width: 200%;
|
||
height: 200%;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
transform: rotate(30deg);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
&:hover:after {
|
||
transform: rotate(30deg) translate(-30%, -30%);
|
||
}
|
||
}
|
||
|
||
.guide-area {
|
||
background-color: #ffffff;
|
||
border-radius: 16rpx;
|
||
padding: 32rpx;
|
||
margin-bottom: 32rpx;
|
||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.guide-title {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
color: #333333;
|
||
margin-bottom: 24rpx;
|
||
position: relative;
|
||
|
||
&:after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: -8rpx;
|
||
left: 0;
|
||
width: 40rpx;
|
||
height: 4rpx;
|
||
background-color: #f45a5a;
|
||
border-radius: 2rpx;
|
||
}
|
||
}
|
||
|
||
.guide-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
padding: 12rpx 16rpx;
|
||
border-radius: 8rpx;
|
||
transition: all 0.3s ease;
|
||
|
||
&:hover {
|
||
background-color: rgba(244, 90, 90, 0.05);
|
||
}
|
||
}
|
||
|
||
.guide-icon {
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
margin-right: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.guide-text {
|
||
font-size: 14px;
|
||
color: #666666;
|
||
}
|
||
|
||
.current-status-area {
|
||
background-color: #ffffff;
|
||
border-radius: 16rpx;
|
||
padding: 32rpx;
|
||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.status-header {
|
||
margin-bottom: 32rpx;
|
||
}
|
||
|
||
.status-title {
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
color: #333333;
|
||
margin-bottom: 0.75rem;
|
||
position: relative;
|
||
|
||
&:after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: -8rpx;
|
||
left: 0;
|
||
width: 40rpx;
|
||
height: 4rpx;
|
||
background-color: #f45a5a;
|
||
border-radius: 2rpx;
|
||
}
|
||
}
|
||
|
||
.status-content {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 48rpx 0;
|
||
}
|
||
|
||
.sign-success,
|
||
.sign-pending {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
text-align: center;
|
||
}
|
||
|
||
.success-icon-wrapper {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 96rpx;
|
||
height: 96rpx;
|
||
}
|
||
|
||
.checkmark-circle {
|
||
width: 96rpx;
|
||
height: 96rpx;
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
|
||
&__circle {
|
||
stroke-dasharray: 166;
|
||
stroke-dashoffset: 166;
|
||
stroke-width: 2;
|
||
stroke: #07c160;
|
||
stroke-miterlimit: 10;
|
||
fill: none;
|
||
animation: checkmark-circle 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
|
||
animation-delay: 0.2s;
|
||
}
|
||
}
|
||
|
||
.success-text {
|
||
font-size: 20px;
|
||
color: #07c160;
|
||
font-weight: 500;
|
||
margin: 24rpx 0 32rpx;
|
||
}
|
||
|
||
.sign-detail {
|
||
margin-top: 32rpx;
|
||
background-color: rgba(7, 193, 96, 0.05);
|
||
padding: 16rpx 24rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
|
||
.detail-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
color: #666666;
|
||
font-size: 14px;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.detail-item uni-icons {
|
||
margin-right: 12rpx;
|
||
}
|
||
|
||
.pending-text {
|
||
font-size: 20px;
|
||
color: #f45a5a;
|
||
font-weight: 500;
|
||
margin: 24px 0 16px;
|
||
}
|
||
|
||
.pending-tip {
|
||
font-size: 14px;
|
||
color: #999999;
|
||
}
|
||
|
||
.bg-decoration {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
pointer-events: none;
|
||
z-index: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.wave {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 200px;
|
||
}
|
||
|
||
.dots {
|
||
position: absolute;
|
||
bottom: 5%;
|
||
right: 5%;
|
||
animation: float 4s ease-in-out infinite;
|
||
}
|
||
|
||
/* 导入Animate.css的部分动画 */
|
||
.animate__animated {
|
||
animation-duration: 1s;
|
||
animation-fill-mode: both;
|
||
}
|
||
|
||
.animate__fadeIn {
|
||
animation-name: fadeIn;
|
||
}
|
||
|
||
.animate__fadeInUp {
|
||
animation-name: fadeInUp;
|
||
}
|
||
|
||
.animate__fadeInLeft {
|
||
animation-name: fadeInLeft;
|
||
}
|
||
|
||
.animate__bounceIn {
|
||
animation-name: bounceIn;
|
||
}
|
||
|
||
.animate__pulse {
|
||
animation-name: pulse;
|
||
}
|
||
|
||
.animate__infinite {
|
||
animation-iteration-count: infinite;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@keyframes fadeInUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translate3d(0, 40px, 0);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translate3d(0, 0, 0);
|
||
}
|
||
}
|
||
|
||
@keyframes fadeInLeft {
|
||
from {
|
||
opacity: 0;
|
||
transform: translate3d(-40px, 0, 0);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translate3d(0, 0, 0);
|
||
}
|
||
}
|
||
|
||
@keyframes bounceIn {
|
||
from, 20%, 40%, 60%, 80%, to {
|
||
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||
}
|
||
0% {
|
||
opacity: 0;
|
||
transform: scale3d(0.3, 0.3, 0.3);
|
||
}
|
||
20% {
|
||
transform: scale3d(1.1, 1.1, 1.1);
|
||
}
|
||
40% {
|
||
transform: scale3d(0.9, 0.9, 0.9);
|
||
}
|
||
60% {
|
||
opacity: 1;
|
||
transform: scale3d(1.03, 1.03, 1.03);
|
||
}
|
||
80% {
|
||
transform: scale3d(0.97, 0.97, 0.97);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: scale3d(1, 1, 1);
|
||
}
|
||
}
|
||
</style>
|