| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 |
- <template>
- <scroll-view class="container" scroll-y>
- <!-- 顶部能量提示 -->
- <view class="energy-tip">
- <text class="energy-tip-text">⚡ 每次打卡可获得 5 财富能量</text>
- <text class="energy-tip-sub">坚持打卡,养成理财好习惯</text>
- </view>
- <!-- 当月日历打卡 -->
- <view class="calendar-card">
- <view class="calendar-header">
- <text class="calendar-title">{{ currentYear }}年{{ currentMonth }}月</text>
- <view class="calendar-nav">
- <text class="nav-btn" @click="changeMonth(-1)">‹</text>
- <text class="nav-btn" @click="changeMonth(1)">›</text>
- </view>
- </view>
- <view class="calendar-weekdays">
- <text class="weekday" v-for="(w, i) in weekdays" :key="i">{{ w }}</text>
- </view>
- <view class="calendar-days">
- <view class="day-cell empty" v-for="(e, i) in emptyDays" :key="getEmptyKey(i)"></view>
- <view
- v-for="(day, i) in daysInMonth"
- :key="getDayKey(i)"
- class="day-cell"
- :class="{
- checked: checkinDates[day],
- today: day === currentDay && currentMonth === realMonth && currentYear === realYear
- }"
- @click="onDayClick(day)">
- <text class="day-num">{{ day }}</text>
- <text class="day-dot" v-if="checkinDates[day]">●</text>
- </view>
- </view>
- <view class="calendar-stats">
- <text>本月打卡 <text class="stat-highlight">{{ checkinDaysThisMonth }}</text> 天</text>
- <text>连续 <text class="stat-highlight">{{ streakDays }}</text> 天</text>
- </view>
- </view>
- <!-- 快速打卡 -->
- <view class="quick-checkin">
- <view class="section-title-row">
- <text class="section-title">快速打卡</text>
- <text class="section-subtitle">今日已打卡 {{ todayCount }} 次</text>
- </view>
- <view class="checkin-types">
- <view
- v-for="(item, index) in checkinTypes"
- :key="index"
- class="checkin-btn"
- :class="{ disabled: item.disabled }"
- @click="showCheckinDialog(item.type)">
- <text class="checkin-icon">{{ item.icon }}</text>
- <text class="checkin-label">{{ item.label }}</text>
- </view>
- </view>
- </view>
- <!-- 统计卡片 -->
- <view class="stats-card">
- <view class="stat-item">
- <text class="stat-value">{{ stats.totalCheckins || 0 }}</text>
- <text class="stat-label">总打卡</text>
- </view>
- <view class="stat-divider"></view>
- <view class="stat-item">
- <text class="stat-value">{{ stats.totalEnergy || 0 }}</text>
- <text class="stat-label">总能量</text>
- </view>
- <view class="stat-divider"></view>
- <view class="stat-item">
- <text class="stat-value">{{ stats.dailyPlanCount || 0 }}</text>
- <text class="stat-label">规划</text>
- </view>
- <view class="stat-divider"></view>
- <view class="stat-item">
- <text class="stat-value">{{ stats.savingCount || 0 }}</text>
- <text class="stat-label">储蓄</text>
- </view>
- </view>
- <!-- 打卡记录 -->
- <view class="record-section">
- <view class="section-title-row">
- <text class="section-title">打卡记录</text>
- <text class="section-subtitle" @click="toggleRecords">
- {{ showAllRecords ? '收起' : '查看全部' }}
- </text>
- </view>
- <view v-if="displayRecords.length === 0" class="empty-state">
- <text class="empty-icon">📝</text>
- <text class="empty-text">暂无打卡记录,开始打卡吧!</text>
- </view>
- <view
- v-for="(item, index) in displayRecords"
- :key="index"
- class="record-item"
- @longpress="confirmDelete(item)">
- <view class="record-left">
- <text class="record-icon">{{ getTypeIcon(item.checkinType) }}</text>
- <view class="record-info">
- <text class="record-type">{{ getTypeLabel(item.checkinType) }}</text>
- <text class="record-date">{{ formatDate(item.createdAt) }}</text>
- <text class="record-note" v-if="item.note">{{ item.note }}</text>
- </view>
- </view>
- <view class="record-right">
- <text class="record-energy">+{{ item.earnedEnergy || 5 }}</text>
- <text class="record-mood" v-if="item.mood">{{ item.mood }}</text>
- </view>
- </view>
- </view>
- <!-- 打卡弹窗 -->
- <view class="dialog-mask" v-if="showDialog" @click="closeDialog">
- <view class="dialog-card" @click.stop>
- <text class="dialog-title">{{ dialogType ? getTypeLabel(dialogType) : '' }}打卡</text>
- <view class="dialog-form">
- <view class="form-item">
- <text class="form-label">金额(选填)</text>
- <input class="form-input" type="digit" v-model="formAmount" placeholder="输入金额" />
- </view>
- <view class="form-item">
- <text class="form-label">分类</text>
- <picker class="form-picker" mode="selector" :range="categoryOptions" @change="onCategoryChange">
- <text class="picker-text">{{ formCategory || '选择分类' }}</text>
- </picker>
- </view>
- <view class="form-item">
- <text class="form-label">备注</text>
- <input class="form-input" type="text" v-model="formNote" placeholder="备注信息" />
- </view>
- <view class="form-item">
- <text class="form-label">心情</text>
- <view class="mood-picker">
- <text
- v-for="(m, i) in moodOptions"
- :key="i"
- class="mood-item"
- :class="{ selected: formMood === m.value }"
- @click="selectMood(m.value)">{{ m.icon }}</text>
- </view>
- </view>
- </view>
- <view class="dialog-actions">
- <button class="dialog-btn cancel" @click="closeDialog">取消</button>
- <button class="dialog-btn confirm" @click="doCheckin">确认打卡</button>
- </view>
- </view>
- </view>
- <!-- 能量奖励动画 -->
- <view class="energy-animation" v-if="showEnergyAnim">
- <text class="energy-anim-text">+{{ earnedEnergy }} ⚡</text>
- </view>
- </scroll-view>
- </template>
- <script>
- import { getCheckinList, getCheckinStats, createCheckin, deleteCheckin } from '../../utils/api'
- export default {
- data() {
- return {
- checkinTypes: [
- { type: 'daily_plan', label: '记账规划', icon: '📋' },
- { type: 'saving', label: '储蓄记录', icon: '💰' },
- { type: 'expense', label: '消费记录', icon: '🛒' },
- { type: 'review', label: '财务回顾', icon: '📊' }
- ],
- checkins: [],
- stats: {},
- todayCount: 0,
- todayTimer: null,
- // 日历
- currentYear: 0,
- currentMonth: 0,
- realMonth: 0,
- realYear: 0,
- currentDay: 0,
- weekdays: ['日', '一', '二', '三', '四', '五', '六'],
- checkinDates: {},
- streakDays: 0,
- // 弹窗
- showDialog: false,
- dialogType: '',
- formAmount: '',
- formCategory: '',
- formNote: '',
- formMood: '',
- categoryOptions: ['日常', '餐饮', '交通', '购物', '娱乐', '教育', '医疗', '其他'],
- moodOptions: [
- { icon: '😊', value: 'happy' },
- { icon: '😐', value: 'neutral' },
- { icon: '😔', value: 'sad' },
- { icon: '😤', value: 'angry' },
- { icon: '🎉', value: 'excited' }
- ],
- // 能量动画
- showEnergyAnim: false,
- earnedEnergy: 5,
- showAllRecords: false,
- childId: null
- }
- },
- computed: {
- daysInMonth: function() {
- return new Date(this.currentYear, this.currentMonth, 0).getDate()
- },
- emptyDays: function() {
- var firstDay = new Date(this.currentYear, this.currentMonth - 1, 1).getDay()
- return firstDay
- },
- checkinDaysThisMonth: function() {
- return Object.keys(this.checkinDates).length
- },
- displayRecords: function() {
- if (this.showAllRecords) return this.checkins
- return this.checkins.slice(0, 10)
- }
- },
- onShow() {
- var now = new Date()
- this.currentYear = now.getFullYear()
- this.currentMonth = now.getMonth() + 1
- this.realMonth = this.currentMonth
- this.realYear = this.currentYear
- this.currentDay = now.getDate()
- this.childId = this.$route && this.$route.query && this.$route.query.childId
- this.loadData()
- this.loadCalendarData()
- },
- methods: {
- async loadData() {
- try {
- var params = {}
- if (this.childId) params.childId = this.childId
- var res = await getCheckinList(params)
- if (res && res.code === 200) {
- this.checkins = res.data || []
- }
- var statsRes = await getCheckinStats(params)
- if (statsRes && statsRes.code === 200) {
- this.stats = statsRes.data || {}
- }
- this.calcTodayCount()
- } catch (e) {
- console.error('加载打卡数据失败', e)
- }
- },
- async loadCalendarData() {
- try {
- var ym = this.currentYear + '-' + (this.currentMonth < 10 ? '0' : '') + this.currentMonth
- var params = { yearMonth: ym }
- if (this.childId) params.childId = this.childId
- var res = await getCheckinList(params)
- if (res && res.code === 200) {
- var list = res.data || []
- var dates = {}
- for (var i = 0; i < list.length; i++) {
- var d = list[i]
- if (d.checkinDate) {
- var day = parseInt(d.checkinDate.substring(8, 10))
- dates[day] = true
- }
- }
- this.checkinDates = dates
- this.calcStreak(list)
- }
- } catch (e) {
- console.error('加载日历数据失败', e)
- }
- },
- calcStreak: function(list) {
- if (!list || list.length === 0) { this.streakDays = 0; return }
- var dates = []
- for (var i = 0; i < list.length; i++) {
- if (list[i].checkinDate) dates.push(list[i].checkinDate)
- }
- dates.sort().reverse()
- var streak = 0
- var today = new Date()
- var checkDate = new Date(today.getFullYear(), today.getMonth(), today.getDate())
- for (var j = 0; j < dates.length; j++) {
- var d = new Date(dates[j])
- var diff = Math.round((checkDate - d) / 86400000)
- if (diff === streak) {
- streak++
- } else if (diff > streak) {
- break
- }
- }
- this.streakDays = streak
- },
- calcTodayCount: function() {
- var today = new Date()
- var todayStr = today.toISOString().split('T')[0]
- this.todayCount = this.checkins.filter(function(c) {
- if (!c.createdAt) return false
- return c.createdAt.indexOf(todayStr) === 0
- }).length
- },
- changeMonth: function(delta) {
- this.currentMonth += delta
- if (this.currentMonth > 12) { this.currentMonth = 1; this.currentYear++ }
- if (this.currentMonth < 1) { this.currentMonth = 12; this.currentYear-- }
- this.loadCalendarData()
- },
- onDayClick: function(day) {
- // 点击日期跳转到当天或者查看记录
- },
- toggleRecords: function() {
- this.showAllRecords = !this.showAllRecords
- },
- selectMood: function(value) {
- this.formMood = value
- },
- closeDialog: function() {
- this.showDialog = false
- },
- showCheckinDialog: function(type) {
- this.dialogType = type
- this.formAmount = ''
- this.formCategory = ''
- this.formNote = ''
- this.formMood = ''
- this.showDialog = true
- },
- onCategoryChange: function(e) {
- this.formCategory = this.categoryOptions[e.detail.value]
- },
- async doCheckin() {
- if (this.todayTimer) {
- uni.showToast({ title: '操作太频繁,请稍后', icon: 'none' })
- return
- }
- this.todayTimer = setTimeout(function() { this.todayTimer = null }.bind(this), 3000)
- var params = {
- checkinType: this.dialogType,
- checkinDate: new Date().toISOString().split('T')[0],
- earnedEnergy: 5
- }
- if (this.childId) params.childId = this.childId
- if (this.formAmount) params.amount = parseInt(this.formAmount)
- if (this.formCategory) params.category = this.formCategory
- if (this.formNote) params.note = this.formNote
- if (this.formMood) params.mood = this.formMood
- try {
- var res = await createCheckin(params)
- if (res && res.code === 200) {
- this.showDialog = false
- this.earnedEnergy = 5
- this.showEnergyAnim = true
- setTimeout(function() { this.showEnergyAnim = false }.bind(this), 1500)
- uni.showToast({ title: '打卡成功!+5能量', icon: 'success' })
- this.loadData()
- this.loadCalendarData()
- } else {
- uni.showToast({ title: res && res.message || '打卡失败', icon: 'none' })
- }
- } catch (e) {
- uni.showToast({ title: '网络错误', icon: 'none' })
- }
- },
- confirmDelete: function(item) {
- var self = this
- uni.showModal({
- title: '提示',
- content: '确定删除此打卡记录?',
- success: function(res) {
- if (res.confirm) self.deleteRecord(item)
- }
- })
- },
- async deleteRecord(item) {
- try {
- var res = await deleteCheckin(item.id)
- if (res && res.code === 200) {
- uni.showToast({ title: '已删除', icon: 'success' })
- this.loadData()
- this.loadCalendarData()
- }
- } catch (e) {
- console.error('删除失败', e)
- }
- },
- getTypeIcon: function(type) {
- var map = { daily_plan: '📋', saving: '💰', expense: '🛒', review: '📊' }
- return map[type] || '📌'
- },
- getTypeLabel: function(type) {
- var map = { daily_plan: '记账规划', saving: '储蓄记录', expense: '消费记录', review: '财务回顾' }
- return map[type] || type
- },
- formatDate: function(dateStr) {
- if (!dateStr) return ''
- return dateStr.substring(0, 16).replace('T', ' ')
- },
- getEmptyKey: function(i) {
- return 'e' + i
- },
- getDayKey: function(i) {
- return 'd' + i
- }
- }
- }
- </script>
- <style>
- .container { min-height: 100vh; background: #f5f7fa; padding: 20rpx 30rpx 120rpx; }
- /* 能量提示 */
- .energy-tip {
- background: linear-gradient(135deg, #F59E0B, #F97316);
- border-radius: 20rpx; padding: 24rpx 30rpx; margin-bottom: 24rpx;
- }
- .energy-tip-text { color: #fff; font-size: 28rpx; font-weight: 600; }
- .energy-tip-sub { color: rgba(255,255,255,0.8); font-size: 22rpx; margin-top: 6rpx; }
- /* 日历 */
- .calendar-card {
- background: #fff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 24rpx;
- }
- .calendar-header {
- display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx;
- }
- .calendar-title { font-size: 32rpx; font-weight: 700; color: #333; }
- .nav-btn { font-size: 40rpx; color: #F97316; padding: 0 16rpx; }
- .calendar-weekdays {
- display: flex; flex-direction: row; margin-bottom: 12rpx;
- }
- .weekday { width: 14.28%; text-align: center; font-size: 24rpx; color: #999; }
- .calendar-days { display: flex; flex-direction: row; flex-wrap: wrap; }
- .day-cell {
- width: 14.28%; height: 72rpx; display: flex; flex-direction: column;
- align-items: center; justify-content: center; position: relative;
- }
- .day-cell.today .day-num {
- background: #F97316; color: #fff; border-radius: 50%;
- width: 48rpx; height: 48rpx; text-align: center; line-height: 48rpx;
- }
- .day-num { font-size: 26rpx; color: #333; }
- .day-dot { font-size: 16rpx; color: #F97316; position: absolute; bottom: 4rpx; }
- .day-cell.empty { height: 72rpx; }
- .calendar-stats {
- display: flex; justify-content: space-between; margin-top: 16rpx;
- padding-top: 16rpx; border-top: 2rpx solid #f5f5f5;
- font-size: 24rpx; color: #999;
- }
- .stat-highlight { color: #F97316; font-weight: 700; font-size: 28rpx; }
- /* 快速打卡 */
- .quick-checkin { background: #fff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 24rpx; }
- .section-title-row {
- display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx;
- }
- .section-title { font-size: 30rpx; font-weight: 700; color: #333; }
- .section-subtitle { font-size: 22rpx; color: #999; }
- .checkin-types { display: flex; flex-direction: row; justify-content: space-between; }
- .checkin-btn {
- display: flex; flex-direction: column; align-items: center; justify-content: center;
- width: 150rpx; height: 130rpx; background: #FFFBEB; border-radius: 16rpx;
- border: 2rpx solid #FDE68A;
- }
- .checkin-btn:active { background: #FEF3C7; }
- .checkin-btn.disabled { opacity: 0.5; }
- .checkin-icon { font-size: 44rpx; margin-bottom: 8rpx; }
- .checkin-label { font-size: 24rpx; color: #92400E; font-weight: 500; }
- /* 统计 */
- .stats-card {
- background: #fff; border-radius: 20rpx; padding: 24rpx;
- display: flex; flex-direction: row; justify-content: space-around;
- align-items: center; margin-bottom: 24rpx;
- }
- .stat-item { display: flex; flex-direction: column; align-items: center; }
- .stat-value { font-size: 38rpx; font-weight: 800; color: #F59E0B; }
- .stat-label { font-size: 22rpx; color: #999; margin-top: 4rpx; }
- .stat-divider { width: 2rpx; height: 60rpx; background: #eee; }
- /* 记录 */
- .record-section { background: #fff; border-radius: 20rpx; padding: 24rpx; }
- .empty-state { text-align: center; padding: 48rpx 0; }
- .empty-icon { font-size: 48rpx; }
- .empty-text { color: #999; font-size: 26rpx; margin-top: 12rpx; }
- .record-item {
- display: flex; flex-direction: row; justify-content: space-between;
- align-items: center; padding: 20rpx 0; border-bottom: 2rpx solid #f5f5f5;
- }
- .record-item:last-child { border-bottom: none; }
- .record-left { display: flex; flex-direction: row; align-items: center; flex: 1; }
- .record-icon { font-size: 36rpx; margin-right: 16rpx; }
- .record-info { display: flex; flex-direction: column; }
- .record-type { font-size: 28rpx; color: #333; font-weight: 500; }
- .record-date { font-size: 22rpx; color: #999; margin-top: 2rpx; }
- .record-note { font-size: 22rpx; color: #666; margin-top: 2rpx; max-width: 300rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
- .record-right { display: flex; flex-direction: column; align-items: flex-end; }
- .record-energy { font-size: 28rpx; color: #F59E0B; font-weight: 700; }
- .record-mood { font-size: 28rpx; margin-top: 4rpx; }
- /* 打卡弹窗 */
- .dialog-mask {
- position: fixed; top: 0; left: 0; right: 0; bottom: 0;
- background: rgba(0,0,0,0.5); z-index: 999; display: flex;
- align-items: center; justify-content: center;
- }
- .dialog-card {
- background: #fff; border-radius: 24rpx; width: 620rpx; padding: 32rpx;
- }
- .dialog-title { font-size: 34rpx; font-weight: 700; color: #333; text-align: center; margin-bottom: 28rpx; }
- .form-item { margin-bottom: 24rpx; }
- .form-label { font-size: 26rpx; color: #666; margin-bottom: 8rpx; display: block; }
- .form-input {
- border: 2rpx solid #E5E7EB; border-radius: 12rpx; padding: 16rpx 20rpx;
- font-size: 26rpx; width: 100%; box-sizing: border-box;
- }
- .form-picker {
- border: 2rpx solid #E5E7EB; border-radius: 12rpx; padding: 16rpx 20rpx;
- font-size: 26rpx; color: #333;
- }
- .picker-text { color: #333; }
- .mood-picker { display: flex; flex-direction: row; gap: 16rpx; }
- .mood-item { font-size: 44rpx; padding: 8rpx; border-radius: 12rpx; border: 2rpx solid transparent; }
- .mood-item.selected { border-color: #F59E0B; background: #FFFBEB; }
- .dialog-actions { display: flex; flex-direction: row; gap: 20rpx; margin-top: 32rpx; }
- .dialog-btn { flex: 1; padding: 20rpx; border-radius: 12rpx; font-size: 28rpx; text-align: center; }
- .dialog-btn.cancel { background: #f5f5f5; color: #666; }
- .dialog-btn.confirm { background: linear-gradient(135deg, #F59E0B, #F97316); color: #fff; }
- /* 能量动画 */
- .energy-animation {
- position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
- z-index: 1000; pointer-events: none;
- }
- .energy-anim-text {
- font-size: 80rpx; font-weight: 900; color: #F59E0B;
- text-shadow: 0 4rpx 20rpx rgba(245,158,11,0.5);
- animation: floatUp 1.5s ease-out forwards;
- }
- @keyframes floatUp {
- 0% { opacity: 1; transform: scale(0.5); }
- 50% { opacity: 1; transform: scale(1.2); }
- 100% { opacity: 0; transform: translateY(-200rpx) scale(1); }
- }
- </style>
|