mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp.git
synced 2026-08-06 10:57:25 +08:00
- 将多个组件转换为 `<script setup>` 语法,提升可读性和性能 - 优化状态管理和事件处理逻辑,简化代码结构 - 更新样式和布局以适应新组件结构 - 添加新功能和修复已知问题,提升用户体验
457 lines
11 KiB
Vue
457 lines
11 KiB
Vue
<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>
|