Files
hmc_gbhd/frontend/src/pages/activity/scan-sign/index.vue
T
2025-04-28 15:32:41 +08:00

636 lines
14 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-container">
<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>
</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()
}
}
</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>