diff --git a/pages/my/index.vue b/pages/my/index.vue index 0863d22..ba5071a 100644 --- a/pages/my/index.vue +++ b/pages/my/index.vue @@ -33,6 +33,48 @@ + + + + + + + + + + + + + + + + + @@ -186,6 +228,18 @@ export default { return { userInfo: {}, currentTab: 0, + signInfo: { + signedToday: false, + continuousDays: 0, + totalPoints: 0, + todayReward: 0, + nextRewardText: '', + monthLabel: '', + monthSignedDays: 0 + }, + signCalendar: [], + signLoading: false, + weekdayLabels: ['日', '一', '二', '三', '四', '五', '六'], stats: [ { icon: '🎁', value: '1280', label: '当前积分', color: '#7B43FF' }, { icon: '🛍️', value: '23', label: '总订单数', color: '#2877FF' }, @@ -315,6 +369,7 @@ export default { return; } else { this.loadProfile(); + this.loadSignInfo(); } }, methods: { @@ -331,6 +386,140 @@ export default { console.warn('加载个人信息失败', error); } }, + loadSignInfo() { + try { + return request('xcx/sign_in', 'GET').then(res => { + const info = res || {}; + const monthLabel = info.current_month || this.getMonthLabel(); + const calendar = this.buildMonthlyCalendar(info.month_records || info.recent_records, monthLabel); + this.signCalendar = calendar; + const updatedSignInfo = { + ...this.signInfo, + signedToday: !!info.signed_today, + continuousDays: info.continuous_days || 0, + totalPoints: info.total_points || 0, + todayReward: info.today_reward || 0, + nextRewardText: info.next_reward_text || '', + monthLabel, + monthSignedDays: calendar.filter(day => day.signed && !day.isPlaceholder).length + }; + this.signInfo = updatedSignInfo; + if (this.stats && this.stats.length) { + const newPoints = updatedSignInfo.totalPoints; + this.$set(this.stats, 0, { + ...this.stats[0], + value: newPoints ? String(newPoints) : this.stats[0].value + }); + } + }).catch(error => { + console.warn('加载签到信息失败', error); + this.signCalendar = this.buildMonthlyCalendar(); + this.signInfo = { + ...this.signInfo, + monthLabel: this.getMonthLabel(), + monthSignedDays: 0 + }; + }); + } catch (error) { + console.warn('获取签到状态异常', error); + this.signCalendar = this.buildMonthlyCalendar(); + this.signInfo = { + ...this.signInfo, + monthLabel: this.getMonthLabel(), + monthSignedDays: 0 + }; + } + }, + buildMonthlyCalendar(list = [], referenceMonth = '') { + const today = new Date(); + const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + let year = today.getFullYear(); + let month = today.getMonth() + 1; + if (referenceMonth && /^\d{4}-\d{2}$/.test(referenceMonth)) { + const parts = referenceMonth.split('-'); + year = parseInt(parts[0], 10) || year; + month = parseInt(parts[1], 10) || month; + } + const firstDay = new Date(year, month - 1, 1); + const totalDays = new Date(year, month, 0).getDate(); + const prefixDays = firstDay.getDay(); + const recordMap = {}; + if (Array.isArray(list)) { + list.forEach(item => { + const dateStr = item?.date || item?.day || item?.label; + if (dateStr) { + const status = + item?.checked ?? + item?.signed ?? + (typeof item?.status !== 'undefined' ? item.status === 1 : undefined) ?? + item?.is_signed ?? + false; + recordMap[dateStr] = !!status; + } + }); + } + const calendar = []; + for (let i = 0; i < prefixDays; i++) { + calendar.push({ label: '', isPlaceholder: true }); + } + for (let day = 1; day <= totalDays; day++) { + const dateStr = `${year}-${this.padZero(month)}-${this.padZero(day)}`; + const targetDate = new Date(year, month - 1, day); + const isToday = this.isSameDate(targetDate, todayStart); + const isFuture = targetDate.getTime() > todayStart.getTime(); + const signed = !!recordMap[dateStr]; + calendar.push({ + label: day, + date: dateStr, + signed, + isToday, + isFuture, + isPlaceholder: false, + missed: !signed && !isFuture && !isToday + }); + } + while (calendar.length % 7 !== 0) { + calendar.push({ label: '', isPlaceholder: true }); + } + return calendar; + }, + getMonthLabel() { + const today = new Date(); + return `${today.getFullYear()}-${this.padZero(today.getMonth() + 1)}`; + }, + padZero(num) { + return num < 10 ? `0${num}` : `${num}`; + }, + isSameDate(dateA, dateB) { + return ( + dateA.getFullYear() === dateB.getFullYear() && + dateA.getMonth() === dateB.getMonth() && + dateA.getDate() === dateB.getDate() + ); + }, + async handleSignIn() { + if (this.signInfo.signedToday || this.signLoading) { + return; + } + this.signLoading = true; + try { + const res = await request('xcx/sign_in', 'POST'); + const rewardText = res?.message || '签到成功'; + uni.showToast({ + title: rewardText, + icon: 'success' + }); + await this.loadSignInfo(); + } catch (error) { + console.error('签到失败', error); + uni.showToast({ + title: '签到失败,请稍后再试', + icon: 'none' + }); + } finally { + this.signLoading = false; + } + }, uploadAvatar() { console.log('uploadAvatar 被点击'); uni.chooseImage({ @@ -418,6 +607,181 @@ export default { padding-bottom: 24rpx; } +.signin-card { + margin: 0 24rpx; + padding: 32rpx; + background: linear-gradient(120deg, #f6f1ff 0%, #ffeef5 100%); + border-radius: 28rpx; + box-shadow: 0 16rpx 32rpx rgba(139, 64, 255, 0.12); + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.signin-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.signin-headline { + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.signin-title { + font-size: 36rpx; + font-weight: 700; + color: #33195d; +} + +.signin-desc { + font-size: 26rpx; + color: #7b43ff; +} + +.signin-badge { + padding: 12rpx 28rpx; + border-radius: 999rpx; + background: rgba(255, 60, 141, 0.12); + color: #ff3c8d; + font-size: 26rpx; + font-weight: 600; +} + +.signin-calendar { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.signin-calendar-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.signin-month { + font-size: 32rpx; + font-weight: 600; + color: #33195d; +} + +.signin-month-summary { + font-size: 24rpx; + color: #7b43ff; +} + +.signin-weekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + font-size: 24rpx; + color: #7b43ff; + text-align: center; +} + +.signin-weekday { + opacity: 0.9; +} + +.signin-calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 16rpx 12rpx; +} + +.signin-date { + height: 80rpx; + border-radius: 18rpx; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(255, 255, 255, 0.7); + border: 2rpx dashed rgba(139, 64, 255, 0.16); + color: #4a2d71; + font-size: 28rpx; +} + +.signin-date.placeholder { + background: transparent; + border-color: transparent; +} + +.signin-date.signed { + background: linear-gradient(135deg, #8b40ff 0%, #ff3c8d 100%); + color: #fff; + border-style: solid; +} + +.signin-date.today { + border-color: #ff3c8d; + box-shadow: 0 0 0 3rpx rgba(255, 60, 141, 0.3); +} + +.signin-date.missed { + border-style: solid; + border-color: rgba(255, 60, 141, 0.3); + color: rgba(51, 25, 93, 0.4); +} + +.signin-date-number { + font-weight: 600; +} + +.signin-footer { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.signin-stats { + display: flex; + gap: 32rpx; +} + +.signin-stat { + flex: 1; + background-color: rgba(255, 255, 255, 0.7); + border-radius: 24rpx; + padding: 16rpx; + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.signin-stat-value { + font-size: 36rpx; + font-weight: 700; + color: #33195d; +} + +.signin-stat-label { + font-size: 24rpx; + color: #7b43ff; +} + +.signin-btn { + width: 100%; + padding: 24rpx 0; + border-radius: 999rpx; + background: linear-gradient(135deg, #8b40ff 0%, #ff3c8d 100%); + color: #ffffff; + font-size: 32rpx; + font-weight: 600; + text-align: center; +} + +.signin-btn.disabled { + opacity: 0.6; +} + +.signin-tip { + font-size: 24rpx; + color: #7b43ff; + text-align: center; +} + .profile-header { background: linear-gradient(135deg, #8b40ff 0%, #ff3c8d 100%); /* border-radius: 24rpx 24rpx 0 0; */ diff --git a/pages/subscribe/index.vue b/pages/subscribe/index.vue index ee0653e..a98cd60 100644 --- a/pages/subscribe/index.vue +++ b/pages/subscribe/index.vue @@ -2,13 +2,16 @@