|
@@ -1,29 +1,61 @@
|
|
|
<template>
|
|
<template>
|
|
|
<scroll-view class="container" scroll-y>
|
|
<scroll-view class="container" scroll-y>
|
|
|
- <!-- 能量提示 -->
|
|
|
|
|
|
|
+ <!-- 顶部能量提示 -->
|
|
|
<view class="energy-tip">
|
|
<view class="energy-tip">
|
|
|
- <text class="energy-tip-text">⚡ 每次打卡可获得 5 能量值</text>
|
|
|
|
|
|
|
+ <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="'e' + i"></view>
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="(day, i) in daysInMonth"
|
|
|
|
|
+ :key="'d' + 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>
|
|
|
|
|
|
|
|
<!-- 快速打卡 -->
|
|
<!-- 快速打卡 -->
|
|
|
<view class="quick-checkin">
|
|
<view class="quick-checkin">
|
|
|
- <view class="section-title">
|
|
|
|
|
- <text class="section-title-text">今日打卡</text>
|
|
|
|
|
|
|
+ <view class="section-title-row">
|
|
|
|
|
+ <text class="section-title">快速打卡</text>
|
|
|
|
|
+ <text class="section-subtitle">今日已打卡 {{ todayCount }} 次</text>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="checkin-types">
|
|
<view class="checkin-types">
|
|
|
<view
|
|
<view
|
|
|
v-for="(item, index) in checkinTypes"
|
|
v-for="(item, index) in checkinTypes"
|
|
|
:key="index"
|
|
:key="index"
|
|
|
class="checkin-btn"
|
|
class="checkin-btn"
|
|
|
- :class="{ disabled: todayTimer }"
|
|
|
|
|
- @click="doCheckin(item.type)">
|
|
|
|
|
|
|
+ :class="{ disabled: item.disabled }"
|
|
|
|
|
+ @click="showCheckinDialog(item.type)">
|
|
|
<text class="checkin-icon">{{ item.icon }}</text>
|
|
<text class="checkin-icon">{{ item.icon }}</text>
|
|
|
<text class="checkin-label">{{ item.label }}</text>
|
|
<text class="checkin-label">{{ item.label }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
- <view v-if="todayCount > 0" class="today-count">
|
|
|
|
|
- <text>今日已打卡 {{ todayCount }} 次</text>
|
|
|
|
|
- </view>
|
|
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
<!-- 统计卡片 -->
|
|
<!-- 统计卡片 -->
|
|
@@ -40,7 +72,7 @@
|
|
|
<view class="stat-divider"></view>
|
|
<view class="stat-divider"></view>
|
|
|
<view class="stat-item">
|
|
<view class="stat-item">
|
|
|
<text class="stat-value">{{ stats.dailyPlanCount || 0 }}</text>
|
|
<text class="stat-value">{{ stats.dailyPlanCount || 0 }}</text>
|
|
|
- <text class="stat-label">计划</text>
|
|
|
|
|
|
|
+ <text class="stat-label">规划</text>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="stat-divider"></view>
|
|
<view class="stat-divider"></view>
|
|
|
<view class="stat-item">
|
|
<view class="stat-item">
|
|
@@ -51,14 +83,18 @@
|
|
|
|
|
|
|
|
<!-- 打卡记录 -->
|
|
<!-- 打卡记录 -->
|
|
|
<view class="record-section">
|
|
<view class="record-section">
|
|
|
- <view class="section-title">
|
|
|
|
|
- <text class="section-title-text">打卡记录</text>
|
|
|
|
|
|
|
+ <view class="section-title-row">
|
|
|
|
|
+ <text class="section-title">打卡记录</text>
|
|
|
|
|
+ <text class="section-subtitle" @click="showAllRecords = !showAllRecords">
|
|
|
|
|
+ {{ showAllRecords ? '收起' : '查看全部' }}
|
|
|
|
|
+ </text>
|
|
|
</view>
|
|
</view>
|
|
|
- <view v-if="checkins.length === 0" class="empty-state">
|
|
|
|
|
- <text>暂无打卡记录,开始打卡吧!</text>
|
|
|
|
|
|
|
+ <view v-if="displayRecords.length === 0" class="empty-state">
|
|
|
|
|
+ <text class="empty-icon">📝</text>
|
|
|
|
|
+ <text class="empty-text">暂无打卡记录,开始打卡吧!</text>
|
|
|
</view>
|
|
</view>
|
|
|
<view
|
|
<view
|
|
|
- v-for="(item, index) in checkins"
|
|
|
|
|
|
|
+ v-for="(item, index) in displayRecords"
|
|
|
:key="index"
|
|
:key="index"
|
|
|
class="record-item"
|
|
class="record-item"
|
|
|
@longpress="confirmDelete(item)">
|
|
@longpress="confirmDelete(item)">
|
|
@@ -67,13 +103,58 @@
|
|
|
<view class="record-info">
|
|
<view class="record-info">
|
|
|
<text class="record-type">{{ getTypeLabel(item.checkinType) }}</text>
|
|
<text class="record-type">{{ getTypeLabel(item.checkinType) }}</text>
|
|
|
<text class="record-date">{{ formatDate(item.createdAt) }}</text>
|
|
<text class="record-date">{{ formatDate(item.createdAt) }}</text>
|
|
|
|
|
+ <text class="record-note" v-if="item.note">{{ item.note }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="record-right">
|
|
<view class="record-right">
|
|
|
<text class="record-energy">+{{ item.earnedEnergy || 5 }}</text>
|
|
<text class="record-energy">+{{ item.earnedEnergy || 5 }}</text>
|
|
|
|
|
+ <text class="record-mood" v-if="item.mood">{{ item.mood }}</text>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 打卡弹窗 -->
|
|
|
|
|
+ <view class="dialog-mask" v-if="showDialog" @click="showDialog = false">
|
|
|
|
|
+ <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="formMood = m.value">{{ m.icon }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="dialog-actions">
|
|
|
|
|
+ <button class="dialog-btn cancel" @click="showDialog = false">取消</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>
|
|
</scroll-view>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
@@ -91,23 +172,79 @@ export default {
|
|
|
],
|
|
],
|
|
|
checkins: [],
|
|
checkins: [],
|
|
|
stats: {},
|
|
stats: {},
|
|
|
|
|
+ todayCount: 0,
|
|
|
todayTimer: null,
|
|
todayTimer: null,
|
|
|
- todayCount: 0
|
|
|
|
|
|
|
+ // 日历
|
|
|
|
|
+ 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() {
|
|
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.loadData()
|
|
|
|
|
+ this.loadCalendarData()
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
methods: {
|
|
methods: {
|
|
|
async loadData() {
|
|
async loadData() {
|
|
|
try {
|
|
try {
|
|
|
- const res = await getCheckinList({})
|
|
|
|
|
|
|
+ var params = {}
|
|
|
|
|
+ if (this.childId) params.childId = this.childId
|
|
|
|
|
+ var res = await getCheckinList(params)
|
|
|
if (res && res.code === 200) {
|
|
if (res && res.code === 200) {
|
|
|
this.checkins = res.data || []
|
|
this.checkins = res.data || []
|
|
|
}
|
|
}
|
|
|
- const statsRes = await getCheckinStats({})
|
|
|
|
|
|
|
+ var statsRes = await getCheckinStats(params)
|
|
|
if (statsRes && statsRes.code === 200) {
|
|
if (statsRes && statsRes.code === 200) {
|
|
|
this.stats = statsRes.data || {}
|
|
this.stats = statsRes.data || {}
|
|
|
}
|
|
}
|
|
@@ -117,29 +254,113 @@ export default {
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- calcTodayCount() {
|
|
|
|
|
- const today = new Date()
|
|
|
|
|
- const todayStr = today.toISOString().split('T')[0]
|
|
|
|
|
|
|
+ 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) {
|
|
this.todayCount = this.checkins.filter(function(c) {
|
|
|
if (!c.createdAt) return false
|
|
if (!c.createdAt) return false
|
|
|
return c.createdAt.indexOf(todayStr) === 0
|
|
return c.createdAt.indexOf(todayStr) === 0
|
|
|
}).length
|
|
}).length
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- async doCheckin(type) {
|
|
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ // 点击日期跳转到当天或者查看记录
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ 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) {
|
|
if (this.todayTimer) {
|
|
|
uni.showToast({ title: '操作太频繁,请稍后', icon: 'none' })
|
|
uni.showToast({ title: '操作太频繁,请稍后', icon: 'none' })
|
|
|
return
|
|
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 {
|
|
try {
|
|
|
- const res = await createCheckin({
|
|
|
|
|
- checkinType: type,
|
|
|
|
|
- checkinDate: new Date().toISOString().split('T')[0],
|
|
|
|
|
- earnedEnergy: 5
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ var res = await createCheckin(params)
|
|
|
if (res && res.code === 200) {
|
|
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' })
|
|
uni.showToast({ title: '打卡成功!+5能量', icon: 'success' })
|
|
|
this.loadData()
|
|
this.loadData()
|
|
|
|
|
+ this.loadCalendarData()
|
|
|
} else {
|
|
} else {
|
|
|
uni.showToast({ title: res && res.message || '打卡失败', icon: 'none' })
|
|
uni.showToast({ title: res && res.message || '打卡失败', icon: 'none' })
|
|
|
}
|
|
}
|
|
@@ -148,42 +369,39 @@ export default {
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- confirmDelete(item) {
|
|
|
|
|
|
|
+ confirmDelete: function(item) {
|
|
|
var self = this
|
|
var self = this
|
|
|
uni.showModal({
|
|
uni.showModal({
|
|
|
title: '提示',
|
|
title: '提示',
|
|
|
content: '确定删除此打卡记录?',
|
|
content: '确定删除此打卡记录?',
|
|
|
success: function(res) {
|
|
success: function(res) {
|
|
|
- if (res.confirm) {
|
|
|
|
|
- self.deleteRecord(item)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (res.confirm) self.deleteRecord(item)
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
async deleteRecord(item) {
|
|
async deleteRecord(item) {
|
|
|
try {
|
|
try {
|
|
|
- const res = await deleteCheckin(item.id)
|
|
|
|
|
|
|
+ var res = await deleteCheckin(item.id)
|
|
|
if (res && res.code === 200) {
|
|
if (res && res.code === 200) {
|
|
|
uni.showToast({ title: '已删除', icon: 'success' })
|
|
uni.showToast({ title: '已删除', icon: 'success' })
|
|
|
this.loadData()
|
|
this.loadData()
|
|
|
|
|
+ this.loadCalendarData()
|
|
|
}
|
|
}
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.error('删除失败', e)
|
|
console.error('删除失败', e)
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- getTypeIcon(type) {
|
|
|
|
|
- const map = { daily_plan: '📋', saving: '💰', expense: '🛒', review: '📊' }
|
|
|
|
|
|
|
+ getTypeIcon: function(type) {
|
|
|
|
|
+ var map = { daily_plan: '📋', saving: '💰', expense: '🛒', review: '📊' }
|
|
|
return map[type] || '📌'
|
|
return map[type] || '📌'
|
|
|
},
|
|
},
|
|
|
-
|
|
|
|
|
- getTypeLabel(type) {
|
|
|
|
|
- const map = { daily_plan: '记账规划', saving: '储蓄记录', expense: '消费记录', review: '财务回顾' }
|
|
|
|
|
|
|
+ getTypeLabel: function(type) {
|
|
|
|
|
+ var map = { daily_plan: '记账规划', saving: '储蓄记录', expense: '消费记录', review: '财务回顾' }
|
|
|
return map[type] || type
|
|
return map[type] || type
|
|
|
},
|
|
},
|
|
|
-
|
|
|
|
|
- formatDate(dateStr) {
|
|
|
|
|
|
|
+ formatDate: function(dateStr) {
|
|
|
if (!dateStr) return ''
|
|
if (!dateStr) return ''
|
|
|
return dateStr.substring(0, 16).replace('T', ' ')
|
|
return dateStr.substring(0, 16).replace('T', ' ')
|
|
|
}
|
|
}
|
|
@@ -192,175 +410,139 @@ export default {
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style>
|
|
<style>
|
|
|
-.container {
|
|
|
|
|
- min-height: 100vh;
|
|
|
|
|
- background: #f5f7fa;
|
|
|
|
|
- padding: 20rpx 30rpx 120rpx;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+.container { min-height: 100vh; background: #f5f7fa; padding: 20rpx 30rpx 120rpx; }
|
|
|
|
|
|
|
|
|
|
+/* 能量提示 */
|
|
|
.energy-tip {
|
|
.energy-tip {
|
|
|
- background: linear-gradient(135deg, #F97316, #FB923C);
|
|
|
|
|
- border-radius: 16rpx;
|
|
|
|
|
- padding: 20rpx 30rpx;
|
|
|
|
|
- margin-bottom: 24rpx;
|
|
|
|
|
|
|
+ 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; }
|
|
|
|
|
|
|
|
-.energy-tip-text {
|
|
|
|
|
- color: #fff;
|
|
|
|
|
- font-size: 26rpx;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
|
|
+/* 日历 */
|
|
|
|
|
+.calendar-card {
|
|
|
|
|
+ background: #fff; border-radius: 20rpx; padding: 24rpx; margin-bottom: 24rpx;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.section-title {
|
|
|
|
|
- margin-bottom: 20rpx;
|
|
|
|
|
|
|
+.calendar-header {
|
|
|
|
|
+ display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.section-title-text {
|
|
|
|
|
- font-size: 30rpx;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #333;
|
|
|
|
|
|
|
+.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;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.quick-checkin {
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- border-radius: 20rpx;
|
|
|
|
|
- padding: 24rpx;
|
|
|
|
|
- margin-bottom: 24rpx;
|
|
|
|
|
|
|
+.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;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.checkin-types {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: row;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
|
|
+.day-cell.today .day-num {
|
|
|
|
|
+ background: #F97316; color: #fff; border-radius: 50%;
|
|
|
|
|
+ width: 48rpx; height: 48rpx; text-align: center; line-height: 48rpx;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.checkin-btn {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- width: 150rpx;
|
|
|
|
|
- height: 120rpx;
|
|
|
|
|
- background: #FFF7ED;
|
|
|
|
|
- border-radius: 16rpx;
|
|
|
|
|
- border: 2rpx solid #FED7AA;
|
|
|
|
|
|
|
+.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; }
|
|
|
|
|
|
|
|
-.checkin-btn.disabled {
|
|
|
|
|
- opacity: 0.5;
|
|
|
|
|
|
|
+/* 快速打卡 */
|
|
|
|
|
+.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;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.checkin-icon {
|
|
|
|
|
- font-size: 40rpx;
|
|
|
|
|
- margin-bottom: 8rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.checkin-label {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: #666;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.today-count {
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- margin-top: 16rpx;
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: #999;
|
|
|
|
|
|
|
+.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 {
|
|
.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: 36rpx;
|
|
|
|
|
- font-weight: 700;
|
|
|
|
|
- color: #F97316;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.stat-label {
|
|
|
|
|
- font-size: 24rpx;
|
|
|
|
|
- color: #999;
|
|
|
|
|
- margin-top: 4rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.stat-divider {
|
|
|
|
|
- width: 2rpx;
|
|
|
|
|
- height: 60rpx;
|
|
|
|
|
- background: #eee;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.record-section {
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- border-radius: 20rpx;
|
|
|
|
|
- padding: 24rpx;
|
|
|
|
|
|
|
+ background: #fff; border-radius: 20rpx; padding: 24rpx;
|
|
|
|
|
+ display: flex; flex-direction: row; justify-content: space-around;
|
|
|
|
|
+ align-items: center; margin-bottom: 24rpx;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.empty-state {
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- padding: 48rpx 0;
|
|
|
|
|
- color: #999;
|
|
|
|
|
- font-size: 26rpx;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
|
|
+.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 {
|
|
.record-item {
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: row;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- padding: 20rpx 0;
|
|
|
|
|
- border-bottom: 2rpx solid #f5f5f5;
|
|
|
|
|
|
|
+ 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-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;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.record-left {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: row;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
|
|
+.dialog-card {
|
|
|
|
|
+ background: #fff; border-radius: 24rpx; width: 620rpx; padding: 32rpx;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.record-icon {
|
|
|
|
|
- font-size: 36rpx;
|
|
|
|
|
- margin-right: 16rpx;
|
|
|
|
|
|
|
+.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;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.record-info {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
|
|
+.form-picker {
|
|
|
|
|
+ border: 2rpx solid #E5E7EB; border-radius: 12rpx; padding: 16rpx 20rpx;
|
|
|
|
|
+ font-size: 26rpx; color: #333;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.record-type {
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- color: #333;
|
|
|
|
|
- font-weight: 500;
|
|
|
|
|
|
|
+.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;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.record-date {
|
|
|
|
|
- font-size: 22rpx;
|
|
|
|
|
- color: #999;
|
|
|
|
|
- margin-top: 4rpx;
|
|
|
|
|
|
|
+.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;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
-.record-right {}
|
|
|
|
|
-
|
|
|
|
|
-.record-energy {
|
|
|
|
|
- font-size: 28rpx;
|
|
|
|
|
- color: #F97316;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
|
|
+@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>
|
|
|
|
|
|
|
+</style>
|