Files
lilishop-uniapp/pages/mine/signIn.vue
Yer11214 349ffa4f34 refactor: 重构多个组件以支持 Vue 3 语法和功能
- 将多个组件转换为 `<script setup>` 语法,提升可读性和性能
- 优化状态管理和事件处理逻辑,简化代码结构
- 更新样式和布局以适应新组件结构
- 添加新功能和修复已知问题,提升用户体验
2026-07-08 18:37:11 +08:00

457 lines
11 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="sign-in">
<view class="date-card">
<div class="box">
<div class="circle-box">
<div class="cricle" @click="handleSignIn()">
<span v-if="!hasSignedToday" :class="{ active: signAnimating || hasSignedToday }">签到</span>
<span v-else :class="{ active: signAnimating || hasSignedToday }"
:style="hasSignedToday ? 'transform: rotateY(0deg);' : ''">已签</span>
</div>
</div>
<text class="tips">坚持每天连续签到可以获多重奖励哦</text>
</div>
</view>
<div class="date-card">
<view class="date-con">
<view class="date-tit">
<div class="current-month">
<div class="day">每日记录<span> ({{ currentMonth }})</span></div>
</div>
</view>
<view class="week">
<text v-for="item in weekArr" :key="item.id">{{ item }}</text>
</view>
<view class="date" v-for="(obj, rowIndex) in calendarRows" :key="rowIndex">
<view class="item" v-for="(item, dayIndex) in obj" :key="dayIndex" :class="item == '' ? 'hide' : ''"
:animation="item == currentDay ? animationData : ''">
<view class="just" :class="signedDays.indexOf(item) != -1 ? 'active' : ''">
<view class="top">{{ item }} </view>
<view class="bottom">
<u-icon name="error" v-if="item <= currentDay" size="24" color="#999"></u-icon>
</view>
</view>
<view class="back" :class="signedDays.indexOf(item) != -1 ? 'active' : ''" :style="
signedDays.indexOf(item) != -1 && hasSignedToday
? 'transform: rotateY(0deg);'
: signedDays.indexOf(item) != -1 && item != currentDay
? 'transform: rotateY(0deg);'
: ''
">
<view class="top">{{ item }}</view>
<view class="bottom">
<u-icon name="checkmark" size="24" :color="aiderLightColor"></u-icon>
</view>
</view>
</view>
</view>
</view>
</div>
<view class="mask" :class="{ show: showSuccessMask, trans: maskClosing }" ref="mask">
<view class="mask-header">
<text class="close"></text>
<text>签到成功</text>
<text class="close" @click="closeSuccessMask">×</text>
</view>
<view class="mask-con">
<u-icon size="120" style="margin: 50rpx 0" :color="aiderLightColor" name="checkmark"></u-icon>
<text class="text">连续签到可获得额外奖励哦</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useStore } from '@/store'
import { sign, signTime } from '@/api/point.js'
const store = useStore()
const aiderLightColor = computed(() => store.getters.aiderLightColor)
const signAnimating = ref(false)
const animationData = ref({})
const showSuccessMask = ref(false)
const maskClosing = ref(false)
const weekArr = ['日', '一', '二', '三', '四', '五', '六']
const monthLabels = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
const currentMonth = ref('')
const currentMonthIndex = ref(0)
const currentYear = ref(0)
const currentDay = ref(0)
const calendarRows = ref<any[][]>([])
const signedDays = ref<number[]>([])
const signRecords = ref<any[]>([])
const hasSignedToday = ref(false)
onLoad(async () => {
const response = await signTime(
`${new Date().getFullYear()}${padZero(new Date().getMonth() + 1)}`
)
signRecords.value = response.data.result
buildCalendar()
})
function padZero(val: number) {
return val >= 10 ? String(val) : `0${val}`
}
async function handleSignIn() {
if (hasSignedToday.value || signAnimating.value) return
const response = await sign()
if (response.data.code !== 200) {
uni.showToast({
title: response.data.message,
duration: 2000,
icon: 'none',
})
return
}
const animation = uni.createAnimation({ duration: 200, timingFunction: 'linear' })
signedDays.value.push(currentDay.value)
animation.rotateY(0).step()
animationData.value = animation.export()
setTimeout(() => {
signAnimating.value = true
showSuccessMask.value = true
hasSignedToday.value = true
animation.rotateY(0).step()
animationData.value = animation.export()
}, 200)
}
function closeSuccessMask() {
showSuccessMask.value = false
maskClosing.value = true
setTimeout(() => {
maskClosing.value = false
}, 500)
}
function buildCalendar() {
const date = new Date()
const monthIndex = date.getMonth()
currentYear.value = date.getFullYear()
currentMonth.value = monthLabels[monthIndex]
currentMonthIndex.value = monthIndex + 1
currentDay.value = date.getDate()
syncSignedDays()
const firstWeekDay = getWeekByDay(`${currentYear.value}-${monthIndex + 1}-1`)
buildMonthDays(monthIndex, firstWeekDay)
}
function syncSignedDays() {
const date = new Date()
const monthIndex = date.getMonth()
signedDays.value = []
hasSignedToday.value = false
signRecords.value.forEach((item) => {
const datePart = item.createTime.split(' ')[0]
const parts = datePart.split('-')
if (
Number(parts[0]) === currentYear.value &&
Number(parts[1]) === currentMonthIndex.value
) {
signedDays.value.push(Number(parts[2]))
}
if (
Number(parts[0]) === date.getFullYear() &&
Number(parts[1]) === monthIndex + 1 &&
Number(parts[2]) === date.getDate()
) {
hasSignedToday.value = true
}
})
}
function buildMonthDays(monthIndex: number, firstWeekDay: number) {
const daysInRow: any[] = []
const rows: any[][] = []
for (let i = 0; i < firstWeekDay; i++) {
daysInRow.push('')
}
const isLongMonth = [0, 2, 4, 6, 7, 9, 11].includes(monthIndex)
const isFebruary = monthIndex === 1
let totalDays = isLongMonth ? 31 : 30
if (isFebruary) {
const year = currentYear.value
const isLeap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0
totalDays = isLeap ? 29 : 28
}
for (let day = 1; day <= totalDays; day++) {
daysInRow.push(day)
}
while (daysInRow.length) {
const row = daysInRow.splice(0, 7)
while (row.length < 7) {
row.push('')
}
rows.push(row)
}
calendarRows.value = rows
}
function getWeekByDay(dayValue: string) {
return new Date(Date.parse(dayValue.replace(/-/g, '/'))).getDay()
}
</script>
<style scoped>
page {
background: #f7f7f7;
}
</style>
<style lang="scss" scoped>
.date-card {
padding: 0 32rpx;
margin: 32rpx 0;
box-shadow: 0 4rpx 24rpx 0 rgba($color: #f6f6f6, $alpha: 1);
}
.tips {
margin-top: 54rpx;
color: #999;
font-size: 24rpx;
letter-spacing: 1rpx;
}
.circle-box {
width: 200rpx;
height: 200rpx;
border-radius: 50%;
background: $aider-light-color;
box-shadow: 0 4rpx 24rpx 0 rgba($color: $aider-light-color, $alpha: 1);
display: flex;
justify-content: center; //这个是X轴居中
align-items: center; //这个是 Y轴居中
}
.cricle {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background: $aider-light-color;
text-align: center;
line-height: 160rpx;
color: #fff;
font-size: 40rpx;
}
.current-month {
width: 100%;
margin: 20rpx 0;
}
.box {
display: flex;
flex-direction: column;
align-items: center;
padding:64rpx 32rpx;
background: #fff;
border-radius: 20rpx;
}
.sign-in {
color: #333;
.date-con {
background: #fff;
min-height: 730rpx;
border-radius: 20rpx;
padding: 0 28rpx;
}
.day {
font-size: 36rpx;
> span {
font-size: 30rpx;
color: #999;
margin-left: 20rpx;
}
}
.date-tit {
display: flex;
justify-content: space-between;
margin: 0 0 30rpx 0;
}
.week {
display: flex;
justify-content: space-between;
color: #a6a6a6;
font-size: 26rpx;
text {
width: 66rpx;
text-align: center;
}
}
.date {
margin: 10rpx 0 36rpx;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
.item {
width: 66rpx;
height: 96rpx;
border-radius: 5px;
overflow: hidden;
position: relative;
&.hide {
opacity: 0;
}
.just,
.back {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
position: absolute;
.top {
position: relative;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
line-height: 1.2;
min-height: 0;
&:before {
content: "";
width: 40rpx;
height: 40rpx;
position: absolute;
left: 50%;
transform: translateX(-50%);
top: 50%;
margin-top: -28rpx;
}
}
}
.just {
&.active {
display: none;
}
}
.back {
display: none;
&.active {
display: flex;
}
.top {
color: $aider-light-color;
}
}
.bottom {
color: #999;
font-size: 20rpx;
height: 36rpx;
min-height: 36rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
}
}
.mask {
position: fixed;
top: 0;
bottom: 0;
left: -100%;
right: 100%;
background: rgba(0, 0, 0, 0.2);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.5s;
&.show {
opacity: 1;
left: 0;
right: 0;
}
&.trans {
left: 0;
right: 0;
}
.mask-header {
width: 540rpx;
height: 130rpx;
line-height: 130rpx;
background: $aider-light-color;
color: #fff;
font-size: 40rpx;
font-weight: 500;
display: flex;
justify-content: space-between;
.close {
width: 60rpx;
font-size: 66rpx;
font-weight: 400;
line-height: 60rpx;
}
}
.mask-con {
width: 540rpx;
height: 380rpx;
background: #fff;
display: flex;
flex-direction: column;
align-items: center;
color: #999;
font-size: 24rpx;
border-radius: 0 0 9px 9px;
.keep-sign {
font-size: 30rpx;
margin-top: 30rpx;
text {
font-size: 46rpx;
font-weight: 500;
color: #999;
padding: 0 6rpx 0 8rpx;
}
}
.mark {
// flex: 1;
display: flex;
align-items: flex-end;
position: relative;
margin-bottom: 16rpx;
text {
margin-left: 4rpx;
color: #999;
}
}
.text {
color: #6f6f6f;
height: 90rpx;
}
}
}
}
</style>