|
|
@@ -0,0 +1,485 @@
|
|
|
+<template>
|
|
|
+ <view class="checkin-container">
|
|
|
+ <!-- 顶部:日期 + 连续打卡 -->
|
|
|
+ <view class="checkin-header">
|
|
|
+ <text class="checkin-date">{{ todayDisplay }}</text>
|
|
|
+ <view class="streak-badge" v-if="stats && stats.currentStreak > 0">
|
|
|
+ <text class="streak-icon">🔥</text>
|
|
|
+ <text class="streak-text">连续{{ stats.currentStreak }}天</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 天气选择 -->
|
|
|
+ <view class="section">
|
|
|
+ <text class="section-title">今天心情怎么样?</text>
|
|
|
+ <view class="weather-grid">
|
|
|
+ <view
|
|
|
+ v-for="w in weatherOptions"
|
|
|
+ :key="w.value"
|
|
|
+ :class="['weather-btn', selectedWeather === w.value ? 'weather-active' : '']"
|
|
|
+ :style="{ borderColor: selectedWeather === w.value ? w.color : 'transparent' }"
|
|
|
+ @click="selectedWeather = w.value">
|
|
|
+ <text class="weather-icon">{{ w.icon }}</text>
|
|
|
+ <text class="weather-label">{{ w.label }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 情绪标签 -->
|
|
|
+ <view class="section">
|
|
|
+ <text class="section-title">情绪标签(可多选)</text>
|
|
|
+ <view class="tag-grid">
|
|
|
+ <view
|
|
|
+ v-for="tag in emotionTags"
|
|
|
+ :key="tag"
|
|
|
+ :class="['tag-btn', selectedTags.indexOf(tag) !== -1 ? 'tag-active' : '']"
|
|
|
+ @click="toggleTag(tag)">
|
|
|
+ <text>{{ tag }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="custom-tag-row" v-if="showCustomTagInput">
|
|
|
+ <input class="custom-tag-input" v-model="customTag" placeholder="输入自定义情绪" />
|
|
|
+ <text class="custom-tag-add" @click="addCustomTag">添加</text>
|
|
|
+ </view>
|
|
|
+ <text class="custom-tag-trigger" v-if="!showCustomTagInput" @click="showCustomTagInput = true">+ 自定义标签</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 拍照 -->
|
|
|
+ <view class="section">
|
|
|
+ <text class="section-title">拍一张照片记录心情</text>
|
|
|
+ <view class="photo-area" @click="takePhoto">
|
|
|
+ <image v-if="photoPath" :src="photoPath" mode="aspectFill" class="photo-preview" />
|
|
|
+ <view v-else class="photo-placeholder">
|
|
|
+ <text class="photo-icon">📷</text>
|
|
|
+ <text class="photo-hint">点击拍照</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <text class="photo-note">照片仅对自己可见</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 文字记录 -->
|
|
|
+ <view class="section">
|
|
|
+ <text class="section-title">记录一下(选填)</text>
|
|
|
+ <textarea
|
|
|
+ class="note-input"
|
|
|
+ v-model="note"
|
|
|
+ placeholder="今天发生了什么?"
|
|
|
+ maxlength="500" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 提交按钮 -->
|
|
|
+ <view class="submit-area">
|
|
|
+ <button
|
|
|
+ class="submit-btn"
|
|
|
+ :class="{ 'submit-disabled': !canSubmit }"
|
|
|
+ :disabled="!canSubmit"
|
|
|
+ @click="submitCheckin">
|
|
|
+ <text class="submit-icon">❤</text>
|
|
|
+ <text>记录心情</text>
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 提交中 loading -->
|
|
|
+ <view class="loading-mask" v-if="submitting">
|
|
|
+ <view class="loading-box">
|
|
|
+ <text class="loading-text">记录中...</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import config from '../../config'
|
|
|
+
|
|
|
+export default {
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ selectedWeather: '',
|
|
|
+ selectedTags: [],
|
|
|
+ customTag: '',
|
|
|
+ showCustomTagInput: false,
|
|
|
+ photoPath: '',
|
|
|
+ note: '',
|
|
|
+ submitting: false,
|
|
|
+ stats: null,
|
|
|
+ weatherOptions: [
|
|
|
+ { value: 'sunny', icon: '\u2600\uFE0F', label: '心情很好', color: '#FFB347' },
|
|
|
+ { value: 'cloudy', icon: '\u26C5', label: '一般般', color: '#A0A0A0' },
|
|
|
+ { value: 'rainy', icon: '\uD83C\uDF27\uFE0F', label: '有点难过', color: '#6B9BD2' },
|
|
|
+ { value: 'stormy', icon: '\u26A1', label: '很烦躁', color: '#8B5CF6' },
|
|
|
+ { value: 'rainbow', icon: '\uD83C\uDF08', label: '特别开心', color: '#FF6B9D' }
|
|
|
+ ],
|
|
|
+ emotionTags: ['开心', '期待', '平静', '惊喜', '难过', '生气', '紧张', '无聊', '疲惫', '害怕']
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ computed: {
|
|
|
+ todayDisplay() {
|
|
|
+ const now = new Date()
|
|
|
+ const y = now.getFullYear()
|
|
|
+ const m = now.getMonth() + 1
|
|
|
+ const d = now.getDate()
|
|
|
+ const weekdays = ['日', '一', '二', '三', '四', '五', '六']
|
|
|
+ return y + '年' + m + '月' + d + '日 周' + weekdays[now.getDay()]
|
|
|
+ },
|
|
|
+
|
|
|
+ canSubmit() {
|
|
|
+ return !!this.selectedWeather
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ onLoad() {
|
|
|
+ this.loadStats()
|
|
|
+ },
|
|
|
+
|
|
|
+ methods: {
|
|
|
+ loadStats() {
|
|
|
+ const app = getApp()
|
|
|
+ const childId = app.globalData.currentChildId
|
|
|
+ if (!childId) return
|
|
|
+ uni.request({
|
|
|
+ url: config.API_BASE_URL + '/api/mind/checkin/stats',
|
|
|
+ method: 'POST',
|
|
|
+ data: { childId },
|
|
|
+ success: (res) => {
|
|
|
+ if (res.data && res.data.code === 200 && res.data.data) {
|
|
|
+ this.stats = res.data.data
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ toggleTag(tag) {
|
|
|
+ const idx = this.selectedTags.indexOf(tag)
|
|
|
+ if (idx !== -1) {
|
|
|
+ this.selectedTags.splice(idx, 1)
|
|
|
+ } else {
|
|
|
+ this.selectedTags.push(tag)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ addCustomTag() {
|
|
|
+ const tag = this.customTag.trim()
|
|
|
+ if (tag && this.emotionTags.indexOf(tag) === -1 && this.selectedTags.indexOf(tag) === -1) {
|
|
|
+ this.selectedTags.push(tag)
|
|
|
+ }
|
|
|
+ this.customTag = ''
|
|
|
+ this.showCustomTagInput = false
|
|
|
+ },
|
|
|
+
|
|
|
+ takePhoto() {
|
|
|
+ uni.chooseImage({
|
|
|
+ count: 1,
|
|
|
+ sourceType: ['camera', 'album'],
|
|
|
+ success: (res) => {
|
|
|
+ this.photoPath = res.tempFilePaths[0]
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+
|
|
|
+ submitCheckin() {
|
|
|
+ if (!this.canSubmit || this.submitting) return
|
|
|
+ this.submitting = true
|
|
|
+
|
|
|
+ const app = getApp()
|
|
|
+ const childId = app.globalData.currentChildId
|
|
|
+
|
|
|
+ // Upload photo first if exists, then submit
|
|
|
+ const doSubmit = (photoUrl) => {
|
|
|
+ uni.request({
|
|
|
+ url: config.API_BASE_URL + '/api/mind/checkin/create',
|
|
|
+ method: 'POST',
|
|
|
+ data: {
|
|
|
+ childId: childId,
|
|
|
+ moodWeather: this.selectedWeather,
|
|
|
+ emotionTags: JSON.stringify(this.selectedTags),
|
|
|
+ photoUrl: photoUrl || '',
|
|
|
+ note: this.note
|
|
|
+ },
|
|
|
+ success: (res) => {
|
|
|
+ this.submitting = false
|
|
|
+ if (res.data && res.data.code === 200) {
|
|
|
+ uni.showToast({
|
|
|
+ title: '+5 \u5FC3\u80FD\u91CF',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 2000
|
|
|
+ })
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.navigateTo({ url: '/pages/mind/emotion-trend' })
|
|
|
+ }, 1500)
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: res.data && res.data.message || '提交失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: () => {
|
|
|
+ this.submitting = false
|
|
|
+ uni.showToast({ title: '网络错误', icon: 'none' })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.photoPath) {
|
|
|
+ uni.uploadFile({
|
|
|
+ url: config.API_BASE_URL + '/api/upload',
|
|
|
+ filePath: this.photoPath,
|
|
|
+ name: 'file',
|
|
|
+ success: (uploadRes) => {
|
|
|
+ let photoUrl = ''
|
|
|
+ try {
|
|
|
+ const data = JSON.parse(uploadRes.data)
|
|
|
+ photoUrl = data.data || data.url || ''
|
|
|
+ } catch (e) {
|
|
|
+ photoUrl = uploadRes.data
|
|
|
+ }
|
|
|
+ doSubmit(photoUrl)
|
|
|
+ },
|
|
|
+ fail: () => doSubmit('')
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ doSubmit('')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.checkin-container {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: linear-gradient(180deg, #FFF0F5 0%, #FFFFFF 100%);
|
|
|
+ padding-bottom: 40rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.checkin-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 30rpx 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.checkin-date {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+
|
|
|
+.streak-badge {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background: linear-gradient(135deg, #FF6B9D, #FF8CB3);
|
|
|
+ border-radius: 30rpx;
|
|
|
+ padding: 8rpx 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.streak-icon {
|
|
|
+ font-size: 28rpx;
|
|
|
+ margin-right: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.streak-text {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #fff;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.section {
|
|
|
+ margin: 20rpx 32rpx;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ padding: 28rpx;
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(255, 107, 157, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+
|
|
|
+.weather-grid {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 16rpx;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.weather-btn {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ width: 120rpx;
|
|
|
+ padding: 16rpx 8rpx;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ border: 3rpx solid transparent;
|
|
|
+ background: #FAFAFA;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.weather-active {
|
|
|
+ background: #FFF0F5;
|
|
|
+}
|
|
|
+
|
|
|
+.weather-icon {
|
|
|
+ font-size: 44rpx;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.weather-label {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+
|
|
|
+.tag-grid {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.tag-btn {
|
|
|
+ padding: 12rpx 28rpx;
|
|
|
+ border-radius: 30rpx;
|
|
|
+ background: #F5F5F5;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #666;
|
|
|
+ border: 2rpx solid transparent;
|
|
|
+}
|
|
|
+
|
|
|
+.tag-active {
|
|
|
+ background: #FFF0F5;
|
|
|
+ color: #FF6B9D;
|
|
|
+ border-color: #FF6B9D;
|
|
|
+}
|
|
|
+
|
|
|
+.custom-tag-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-top: 16rpx;
|
|
|
+ gap: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.custom-tag-input {
|
|
|
+ flex: 1;
|
|
|
+ height: 60rpx;
|
|
|
+ border: 2rpx solid #E0E0E0;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ padding: 0 16rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.custom-tag-add {
|
|
|
+ padding: 12rpx 24rpx;
|
|
|
+ background: #FF6B9D;
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.custom-tag-trigger {
|
|
|
+ display: inline-block;
|
|
|
+ margin-top: 16rpx;
|
|
|
+ color: #FF6B9D;
|
|
|
+ font-size: 26rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.photo-area {
|
|
|
+ width: 200rpx;
|
|
|
+ height: 200rpx;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #F5F5F5;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.photo-preview {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.photo-placeholder {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.photo-icon {
|
|
|
+ font-size: 60rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.photo-hint {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999;
|
|
|
+ margin-top: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.photo-note {
|
|
|
+ display: block;
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #999;
|
|
|
+ margin-top: 12rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.note-input {
|
|
|
+ width: 100%;
|
|
|
+ min-height: 140rpx;
|
|
|
+ background: #F9F9F9;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ padding: 20rpx;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #333;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.submit-area {
|
|
|
+ margin: 40rpx 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.submit-btn {
|
|
|
+ width: 100%;
|
|
|
+ height: 88rpx;
|
|
|
+ line-height: 88rpx;
|
|
|
+ background: linear-gradient(135deg, #FF6B9D, #FF8CB3);
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 44rpx;
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.submit-disabled {
|
|
|
+ opacity: 0.5;
|
|
|
+}
|
|
|
+
|
|
|
+.submit-icon {
|
|
|
+ margin-right: 8rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-mask {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.3);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 999;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-box {
|
|
|
+ background: #fff;
|
|
|
+ padding: 40rpx 60rpx;
|
|
|
+ border-radius: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-text {
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+</style>
|