Răsfoiți Sursa

feat(frontend): 情绪打卡合并成长页心情记录,支持近期心情历史查看

- 删除 pages/growth/growth-emotion-checkin.vue,移除 pages.json 注册
- 成长页心情记录入口跳转至 mind-detail 情绪打卡页并透传 memberId
- 情绪打卡页新增近期心情历史列表(/api/mind/checkin/list 前5条),兼容 Plutchik 与成长页情绪词表
- 修复提交成功后跳转死链 /pages/mind/emotion-trend 与趋势页返回死链 /pages/mind/emotion-checkin
- 情绪趋势页支持 memberId 参数兜底与成长页情绪词表标签映射
liaoxg 1 lună în urmă
părinte
comite
c847dcd1cc

+ 0 - 6
cfc-frontend/pages.json

@@ -504,12 +504,6 @@
           "style": {
             "navigationBarTitleText": "睡眠打卡"
           }
-        },
-        {
-          "path": "growth-emotion-checkin",
-          "style": {
-            "navigationBarTitleText": "心情打卡 (成长)"
-          }
         }
       ]
     },

+ 3 - 1
cfc-frontend/pages/growth-main/index.vue

@@ -284,8 +284,10 @@ export default {
     handleProgressClick: function(idx) {
       var item = this.progressItems[idx]
       if (!item || !item.key) return
-      var m = { diet: '/pages/growth/diet-checkin', exercise: '/pages/growth/exercise-checkin', sleep: '/pages/growth/sleep-checkin', emotion: '/pages/growth/growth-emotion-checkin', checkin: '/pages/wealth-sub/checkin' }
+      var m = { diet: '/pages/growth/diet-checkin', exercise: '/pages/growth/exercise-checkin', sleep: '/pages/growth/sleep-checkin', emotion: '/pages/mind-detail/emotion-checkin', checkin: '/pages/wealth-sub/checkin' }
       var url = m[item.key]
+      // 情绪打卡已合并至 mind-detail 情绪打卡页,成长上下文需带上 memberId
+      if (url && item.key === 'emotion' && this.memberId) url = url + '?memberId=' + this.memberId
       if (url) uni.navigateTo({ url: url })
     },
     getTaskKey: function(task, idx) { return (task && task.id) ? ('t' + task.id) : ('tk' + idx) },

+ 0 - 416
cfc-frontend/pages/growth/growth-emotion-checkin.vue

@@ -1,416 +0,0 @@
-<template>
-  <view class="page">
-    <!-- 顶部导航栏 -->
-    <view class="nav-bar">
-      <view class="nav-left" @tap="goBack">
-        <text class="nav-icon">‹</text>
-        <text class="nav-title">心情打卡</text>
-      </view>
-    </view>
-
-    <scroll-view class="content" scroll-y>
-      <!-- 情绪选择 -->
-      <view class="card">
-        <view class="card-header">
-          <text class="card-icon">💖</text>
-          <text class="card-title">现在的心情</text>
-        </view>
-        <view class="card-body">
-          <view class="emotion-grid">
-            <view class="emotion-item" v-for="em in emotions" :key="em.type"
-              :class="{ 'emotion-active': selectedEmotion === em.type }"
-              @tap="selectedEmotion = em.type">
-              <text class="emotion-icon">{{ em.icon }}</text>
-              <text class="emotion-label">{{ em.label }}</text>
-            </view>
-          </view>
-        </view>
-      </view>
-
-      <!-- 心情强度 -->
-      <view class="card">
-        <view class="card-header">
-          <text class="card-icon">📊</text>
-          <text class="card-title">心情强度</text>
-        </view>
-        <view class="card-body">
-          <view class="slider-row">
-            <text class="slider-min">微弱</text>
-            <slider min="1" max="10" :value="intensityLevel" @change="function(e){intensityLevel=e.detail.value}" block-size="20" activeColor="#FF6B9D" />
-            <text class="slider-val">{{ intensityLevel }}</text>
-            <text class="slider-max">强烈</text>
-          </view>
-        </view>
-      </view>
-
-      <!-- 文字记录 + 媒体附件 -->
-      <view class="card">
-        <view class="card-header">
-          <text class="card-icon">📝</text>
-          <text class="card-title">文字记录</text>
-        </view>
-        <view class="card-body">
-          <textarea class="note-input" v-model="note" placeholder="今天发生了什么?说说你的感受..." maxlength="500" />
-          <!-- 媒体工具栏 -->
-          <view class="media-toolbar">
-            <view class="tool-btn" @tap="choosePhoto">
-              <text class="tool-icon">📷</text>
-              <text class="tool-label">照片</text>
-              <text class="tool-count" v-if="photos.length > 0">{{ photos.length }}/9</text>
-            </view>
-            <view class="tool-divider"></view>
-            <view class="tool-btn" @tap="toggleRecord">
-              <text class="tool-icon">🎙</text>
-              <text class="tool-label">{{ isRecording ? '录音中…' : (voicePath ? '已录制' : '录音') }}</text>
-              <text class="tool-count" v-if="isRecording">{{ recordSeconds }}s</text>
-            </view>
-          </view>
-          <!-- 照片缩略图 -->
-          <view class="photo-strip" v-if="photos.length > 0">
-            <view class="photo-item" v-for="(img, idx) in photos" :key="getItemKey(img, idx)" @tap="removePhoto(idx)">
-              <image :src="img" mode="aspectFill" class="photo-img"/>
-              <view class="photo-delete-overlay">
-                <text class="delete-icon">×</text>
-              </view>
-            </view>
-          </view>
-          <!-- 录音状态 -->
-          <view class="voice-strip" v-if="isRecording">
-            <view class="waveform">
-              <view class="wave-bar" v-for="n in 7" :key="n" :style="{animationDelay: (n * 0.1) + 's'}"></view>
-            </view>
-            <view class="voice-controls">
-              <view class="mini-btn cancel-mini" @tap="cancelRecord"><text>取消</text></view>
-              <view class="mini-btn done-mini" @tap="finishRecord"><text>完成</text></view>
-            </view>
-          </view>
-          <view class="voice-strip" v-else-if="voicePath">
-            <view class="voice-player">
-              <text class="play-icon">▶</text>
-              <text class="voice-duration">{{ recordDurationText }}</text>
-            </view>
-            <view class="delete-voice" @tap="deleteVoice"><text>×</text></view>
-          </view>
-        </view>
-      </view>
-
-      <!-- 提交按钮 -->
-      <view class="submit-wrap">
-        <view class="submit-btn" :class="{ 'submitting': submitting }" @tap="doCheckin">
-          <text>{{ submitting ? '提交中...' : '✓ 记录心情' }}</text>
-        </view>
-      </view>
-
-      <!-- 历史记录 -->
-      <view class="card" v-if="history.length > 0">
-        <view class="card-header">
-          <text class="card-icon">📜</text>
-          <text class="card-title">近期心情</text>
-        </view>
-        <view class="card-body">
-          <view class="history-list">
-            <view class="history-item" v-for="item in history" :key="item.id">
-              <view class="history-left">
-                <text class="history-type">{{ emotionIcon(item.emotionType) }} {{ emotionLabel(item.emotionType) }}</text>
-                <text class="history-content">{{ item.note || '-' }}</text>
-              </view>
-              <view class="history-right">
-                <text class="history-date">{{ formatDate(item.createdAt) }}</text>
-              </view>
-            </view>
-          </view>
-        </view>
-      </view>
-
-      <!-- 底部留白 -->
-      <view class="bottom-spacer"></view>
-    </scroll-view>
-  </view>
-</template>
-
-<script>
-import config from '../../config'
-import { parseDate } from '../../utils/format.js'
-
-export default {
-  data() {
-    return {
-      memberId: null,
-      submitting: false,
-      isRecording: false,
-      recordSeconds: 0,
-      recordTimer: null,
-      recorderManager: null,
-      history: [],
-      selectedEmotion: '',
-      intensityLevel: 5,
-      note: '',
-      photos: [],
-      voicePath: '',
-      emotions: [
-        { type: 'joy',     icon: '😊', label: '开心' },
-        { type: 'calm',    icon: '😌', label: '平静' },
-        { type: 'excited', icon: '🤩', label: '兴奋' },
-        { type: 'sad',     icon: '😢', label: '难过' },
-        { type: 'angry',   icon: '😤', label: '生气' },
-        { type: 'anxious', icon: '😰', label: '焦虑' },
-        { type: 'tired',   icon: '😴', label: '疲惫' },
-        { type: 'scared',  icon: '😨', label: '害怕' }
-      ]
-    }
-  },
-  computed: {
-    recordDurationText: function() {
-      if (!this.voicePath) return ''
-      return '录音 ' + this.recordSeconds + '秒'
-    }
-  },
-  onLoad: function() {
-    var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || 'parent'
-    if (role !== 'child') {
-      this.memberId = uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || null
-    }
-    this.recorderManager = uni.getRecorderManager()
-    this.recorderManager.onStart(() => {
-      this.isRecording = true
-      this.recordSeconds = 0
-      this.recordTimer = setInterval(() => { this.recordSeconds++ }, 1000)
-    })
-    this.recorderManager.onStop((res) => {
-      this.isRecording = false
-      if (this.recordTimer) { clearInterval(this.recordTimer); this.recordTimer = null }
-      this.voicePath = res.tempFilePath
-      this.voiceDuration = res.duration
-    })
-    this.recorderManager.onError(() => {
-      this.isRecording = false
-      if (this.recordTimer) { clearInterval(this.recordTimer); this.recordTimer = null }
-      uni.showToast({ title: '录音失败', icon: 'none' })
-    })
-    this.loadHistory()
-  },
-  onUnload: function() {
-    if (this.recordTimer) clearInterval(this.recordTimer)
-  },
-  methods: {
-    goBack: function() { uni.navigateBack() },
-    emotionLabel: function(type) {
-      var m = { joy: '开心', calm: '平静', excited: '兴奋', sad: '难过', angry: '生气', anxious: '焦虑', tired: '疲惫', scared: '害怕' }
-      return m[type] || type || ''
-    },
-    emotionIcon: function(type) {
-      var em = this.emotions.find(function(e) { return e.type === type })
-      return em ? em.icon : ''
-    },
-    formatDate: function(dateStr) {
-      if (!dateStr) return ''
-      var d = parseDate(dateStr)
-      if (!d) return dateStr
-      var m2 = (d.getMonth() + 1).toString().padStart(2, '0')
-      var day = d.getDate().toString().padStart(2, '0')
-      return m2 + '-' + day
-    },
-    loadHistory: function() {
-      var self = this
-      var token = uni.getStorageSync('token')
-      if (!token || !self.memberId) return
-      uni.request({
-        url: config.API_BASE_URL + '/api/mind/checkin/list',
-        method: 'POST',
-        data: { memberId: self.memberId, page: 1, size: 5 },
-        header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
-        success: function(res) {
-          if (res.data && res.data.code === 200 && res.data.data) {
-            self.history = res.data.data
-          }
-        }
-      })
-    },
-    choosePhoto: function() {
-      var self = this
-      uni.chooseImage({
-        count: 9 - self.photos.length,
-        sizeType: ['compressed'],
-        sourceType: ['album', 'camera'],
-        success: function(res) {
-          res.tempFilePaths.forEach(function(path) {
-            if (self.photos.length < 9) self.photos.push(path)
-          })
-        }
-      })
-    },
-    getPhotoKey: function(img, idx) {
-      return img ? img : ('p' + idx)
-    },
-    removePhoto: function(idx) {
-      this.photos.splice(idx, 1)
-    },
-    toggleRecord: function() {
-      if (this.isRecording) {
-        this.recorderManager.stop()
-      } else {
-        this.recorderManager.start({ duration: 60000, format: 'mp3' })
-      }
-    },
-    cancelRecord: function() {
-      this.recorderManager.stop()
-      this.voicePath = ''
-    },
-    finishRecord: function() {
-      this.recorderManager.stop()
-    },
-    deleteVoice: function() {
-      this.voicePath = ''
-    },
-    uploadFile: function(filePath) {
-      var self = this
-      return new Promise(function(resolve, reject) {
-        uni.uploadFile({
-          url: config.API_BASE_URL + '/api/media/upload',
-          filePath: filePath,
-          name: 'file',
-          header: { 'Authorization': 'Bearer ' + uni.getStorageSync('token') },
-          success: function(res) {
-            try { var data = JSON.parse(res.data); resolve(data.data || data.url || '') }
-            catch (e) { resolve(res.data || '') }
-          },
-          fail: function() { reject() }
-        })
-      })
-    },
-    async doCheckin() {
-      var self = this
-      if (!self.selectedEmotion) { uni.showToast({ title: '请选择当前心情', icon: 'none' }); return }
-      if (!self.memberId) { uni.showToast({ title: '孩子ID无效', icon: 'none' }); return }
-      self.submitting = true
-      try {
-        var photoUrls = []
-        if (self.photos && self.photos.length > 0) {
-          for (var i = 0; i < self.photos.length; i++) {
-            try { var url = await self.uploadFile(self.photos[i]) } catch(e) { url = '' }
-            if (url) photoUrls.push(url)
-          }
-        }
-        var voiceUrl = ''
-        if (self.voicePath) {
-          try { voiceUrl = await self.uploadFile(self.voicePath) } catch(e) { voiceUrl = '' }
-        }
-        var token = uni.getStorageSync('token')
-        uni.request({
-          url: config.API_BASE_URL + '/api/mind/checkin/create',
-          method: 'POST',
-          data: {
-            memberId: self.memberId,
-            emotionType: self.selectedEmotion,
-            intensityLevel: self.intensityLevel,
-            note: self.note,
-            photoUrls: photoUrls.join(','),
-            voiceUrl: voiceUrl
-          },
-          header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
-success: function(res) {
-              self.submitting = false
-              if (res.data && res.data.code === 200) {
-                uni.showToast({ title: '+5 心能量', icon: 'none' })
-                setTimeout(function() {
-                  uni.navigateBack()
-                }, 800)
-              } else {
-                uni.showToast({ title: (res.data && res.data.message) || '提交失败', icon: 'none' })
-              }
-            },
-          fail: function() {
-            self.submitting = false
-            uni.showToast({ title: '网络错误', icon: 'none' })
-          }
-        })
-      } catch (e) {
-        self.submitting = false
-        uni.showToast({ title: '提交失败', icon: 'none' })
-      }
-    }
-  }
-}
-</script>
-
-<style scoped>
-/* ===== 页面基础 ===== */
-.page { min-height: 100vh; background: #FFF7ED; }
-.nav-bar { height: 88rpx; padding: 24rpx 24rpx 0; display: flex; align-items: flex-end; }
-.nav-left { display: flex; align-items: center; gap: 10rpx; }
-.nav-icon { font-size: 48rpx; color: #333; line-height: 1; }
-.nav-title { font-size: 34rpx; font-weight: 600; color: #333; }
-.content { padding-bottom: 40rpx; }
-
-/* ===== 卡片通用 ===== */
-.card { margin: 16rpx 20rpx; background: #fff; border-radius: 20rpx; overflow: hidden; box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.05); }
-.card-header { display: flex; align-items: center; gap: 10rpx; padding: 20rpx 24rpx 8rpx; border-bottom: 1rpx solid #f5f5f5; background: linear-gradient(90deg, #FFF5FA, #fff); }
-.card-icon { font-size: 32rpx; }
-.card-title { font-size: 30rpx; font-weight: 600; color: #333; }
-.card-body { padding: 20rpx 24rpx 24rpx; }
-
-/* ===== 情绪网格 ===== */
-.emotion-grid { display: flex; flex-wrap: wrap; gap: 12rpx; }
-.emotion-item { width: calc(25% - 9rpx); display: flex; flex-direction: column; align-items: center; padding: 18rpx 0; border-radius: 14rpx; background: #FAFAFA; border: 2rpx solid transparent; transition: all 0.2s ease; }
-.emotion-active { background: #FFF0F5; border-color: #FF6B9D; box-shadow: 0 4rpx 12rpx rgba(255,107,157,0.15); }
-.emotion-icon { font-size: 44rpx; margin-bottom: 6rpx; }
-.emotion-label { font-size: 24rpx; color: #666; }
-.emotion-active .emotion-label { color: #FF6B9D; font-weight: 600; }
-
-/* ===== 滑块 ===== */
-.slider-row { display: flex; align-items: center; gap: 10rpx; }
-.slider-min { font-size: 22rpx; color: #999; flex-shrink: 0; }
-.slider-val { font-size: 26rpx; color: #FF6B9D; font-weight: 600; width: 36rpx; text-align: center; }
-.slider-max { font-size: 22rpx; color: #999; flex-shrink: 0; }
-
-/* ===== 文本输入 ===== */
-.note-input { width: 100%; height: 140rpx; border: 1rpx solid #e8e8e8; border-radius: 14rpx; padding: 16rpx; font-size: 26rpx; background: #FAFAFA; box-sizing: border-box; font-family: inherit; margin-bottom: 16rpx; }
-
-/* ===== 媒体工具栏 ===== */
-.media-toolbar { display: flex; align-items: center; gap: 0; padding: 12rpx 0; }
-.tool-btn { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6rpx; padding: 16rpx 0; }
-.tool-icon { font-size: 42rpx; }
-.tool-label { font-size: 24rpx; color: #666; }
-.tool-count { font-size: 20rpx; color: #FF6B9D; font-weight: 600; }
-.tool-divider { width: 1rpx; height: 56rpx; background: linear-gradient(180deg, transparent, #e8e8e8, transparent); }
-
-/* ===== 照片缩略图 ===== */
-.photo-strip { display: flex; flex-wrap: wrap; gap: 10rpx; padding: 12rpx 0 4rpx; }
-.photo-item { position: relative; width: 132rpx; height: 132rpx; border-radius: 12rpx; overflow: hidden; }
-.photo-img { width: 100%; height: 100%; object-fit: cover; }
-.photo-delete-overlay { position: absolute; top: -6rpx; right: -6rpx; width: 32rpx; height: 32rpx; background: #EF4444; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2rpx 8rpx rgba(239,68,68,0.3); z-index: 2; }
-.delete-icon { color: #fff; font-size: 22rpx; font-weight: bold; line-height: 32rpx; }
-
-/* ===== 录音展开区 ===== */
-.voice-strip { display: flex; align-items: center; justify-content: space-between; padding: 12rpx 0 4rpx; gap: 16rpx; }
-.waveform { display: flex; align-items: center; gap: 5rpx; height: 40rpx; flex: 1; }
-.wave-bar { width: 5rpx; height: 8rpx; background: #FF6B9D; border-radius: 3rpx; animation: wave-anim 0.8s ease-in-out infinite; }
-@keyframes wave-anim { 0%, 100% { height: 8rpx; } 50% { height: 32rpx; } }
-.voice-controls { display: flex; gap: 12rpx; }
-.mini-btn { padding: 10rpx 24rpx; border-radius: 22rpx; font-size: 24rpx; }
-.cancel-mini { background: #f0f0f0; color: #666; }
-.done-mini { background: #FF6B9D; color: #fff; font-weight: 600; }
-
-.voice-player { display: flex; align-items: center; gap: 10rpx; flex: 1; background: #FFF0F5; padding: 10rpx 20rpx; border-radius: 28rpx; }
-.play-icon { font-size: 26rpx; color: #FF6B9D; }
-.voice-duration { font-size: 24rpx; color: #666; }
-.delete-voice { width: 40rpx; height: 40rpx; display: flex; align-items: center; justify-content: center; color: #999; font-size: 28rpx; }
-
-/* ===== 提交按钮 ===== */
-.submit-wrap { padding: 24rpx 20rpx 32rpx; }
-.submit-btn { background: linear-gradient(135deg, #FF6B9D, #F97316); color: #fff; text-align: center; padding: 22rpx 0; border-radius: 14rpx; font-size: 30rpx; font-weight: 600; box-shadow: 0 6rpx 20rpx rgba(255,107,157,0.3); transition: all 0.2s ease; }
-.submit-btn:active { transform: scale(0.98); }
-.submitting { opacity: 0.6; }
-
-/* ===== 历史记录 ===== */
-.history-list { display: flex; flex-direction: column; gap: 12rpx; }
-.history-item { display: flex; justify-content: space-between; align-items: center; padding: 14rpx 0; border-bottom: 1rpx solid #f5f5f5; }
-.history-left { flex: 1; min-width: 0; }
-.history-type { font-size: 26rpx; font-weight: 500; color: #333; display: block; }
-.history-content { font-size: 24rpx; color: #888; display: block; margin-top: 4rpx; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-.history-right { text-align: right; margin-left: 16rpx; flex-shrink: 0; }
-.history-date { font-size: 22rpx; color: #999; }
-
-/* ===== 底部留白 ===== */
-.bottom-spacer { height: 40rpx; }
-</style>

+ 129 - 5
cfc-frontend/pages/mind-detail/emotion-checkin.vue

@@ -163,6 +163,21 @@
     <!-- Phase 1.2: 情绪风险告警 -->
     <PsychCrisisBanner v-if="currentChildId" :memberId="currentChildId" source="checkin" />
 
+    <!-- 近期心情历史(合并自成长页心情记录) -->
+    <view class="section" v-if="recentMoods.length > 0">
+      <text class="section-title">近期心情</text>
+      <view class="mood-list">
+        <view class="mood-item" v-for="(mood, idx) in recentMoods" :key="idx">
+          <text class="mood-emotion">{{ emotionIcon(mood.emotionType) }}</text>
+          <view class="mood-info">
+            <text class="mood-label">{{ emotionLabel(mood.emotionType) }}</text>
+            <text class="mood-date">{{ formatMoodDate(mood) }}</text>
+          </view>
+          <text class="mood-note" v-if="mood.note">{{ mood.note }}</text>
+        </view>
+      </view>
+    </view>
+
     <!-- 提交按钮 -->
     <view class="submit-area">
       <button
@@ -232,6 +247,9 @@ export default {
       },
       stats: null,
 
+      // 近期心情历史(合并自成长页心情记录)
+      recentMoods: [],
+
       // Plutchik 8 基本情绪
       plutchikEmotions: [
         { type: 'joy',         icon: '\uD83D\uDE04', label: '开心',   color: '#FFD93D' },
@@ -297,9 +315,12 @@ export default {
     }
   },
 
-  onLoad() {
-    this.currentChildId = uni.getStorageSync('currentChildId') || null
+  onLoad(options) {
+    // memberId 优先取页面参数(成长页跳转传入),再兜底本地存储链
+    var memberId = (options && options.memberId) || uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || null
+    this.currentChildId = memberId
     this.loadStats()
+    this.loadRecentMoods()
   },
 
   onReady() {
@@ -316,7 +337,7 @@ export default {
 
   methods: {
     loadStats() {
-      const memberId = uni.getStorageSync('currentChildId')
+      var memberId = this.currentChildId
       if (!memberId) return
       uni.request({
         url: config.API_BASE_URL + '/api/mind/checkin/stats',
@@ -330,6 +351,54 @@ export default {
       })
     },
 
+    // ===== 近期心情记录(合并自成长页心情记录) =====
+    loadRecentMoods() {
+      var self = this
+      if (!this.currentChildId) return
+      uni.request({
+        url: config.API_BASE_URL + '/api/mind/checkin/list',
+        method: 'POST',
+        data: { memberId: this.currentChildId, page: 1, size: 5 },
+        success: (res) => {
+          if (res.data && res.data.code === 200) {
+            var list = res.data.data
+            // 兼容数组 / {records} / {list} 三种返回结构
+            if (!(list instanceof Array)) {
+              list = (list && (list.list || list.records)) || []
+            }
+            self.recentMoods = list.slice(0, 5)
+          }
+        }
+      })
+    },
+
+    emotionLabel(type) {
+      var map = {
+        // Plutchik 词表(情绪打卡页)
+        joy: '开心', trust: '信任', fear: '恐惧', surprise: '惊讶',
+        sadness: '悲伤', disgust: '厌恶', anger: '愤怒', anticipation: '期待',
+        // 成长页词表(旧数据兼容)
+        calm: '平静', excited: '兴奋', sad: '难过', angry: '生气',
+        anxious: '焦虑', tired: '疲惫', scared: '害怕'
+      }
+      return map[type] || type || '心情'
+    },
+
+    emotionIcon(type) {
+      var map = {
+        joy: '\u{1F604}', trust: '\u{1F91D}', fear: '\u{1F631}', surprise: '\u{1F632}',
+        sadness: '\u{1F622}', disgust: '\u{1F922}', anger: '\u{1F621}', anticipation: '\u{1F600}',
+        calm: '\u{1F60C}', excited: '\u{1F929}', sad: '\u{1F622}', angry: '\u{1F621}',
+        anxious: '\u{1F630}', tired: '\u{1F634}', scared: '\u{1F631}'
+      }
+      return map[type] || '\u{1F604}'
+    },
+
+    formatMoodDate(mood) {
+      var raw = (mood && (mood.checkinDate || mood.createdAt)) || ''
+      return raw.substring(0, 10)
+    },
+
     // ===== 2D 效价-激活度网格 =====
     onAffectGridTap(e) {
       // 获取点击相对位置
@@ -669,7 +738,7 @@ export default {
       if (!this.canSubmit || this.submitting) return
       this.submitting = true
 
-      const memberId = uni.getStorageSync('currentChildId')
+      var memberId = this.currentChildId
 
       const doSubmit = (mediaUrl) => {
         uni.request({
@@ -700,7 +769,10 @@ export default {
                 duration: 2000
               })
               setTimeout(() => {
-                uni.navigateTo({ url: '/pages/mind/emotion-trend' })
+                var trendUrl = '/pages/mind-detail/emotion-trend'
+                // 成长页场景 memberId 可能来自参数/currentMemberId,透传给趋势页
+                if (memberId) trendUrl = trendUrl + '?memberId=' + memberId
+                uni.navigateTo({ url: trendUrl })
               }, 1500)
             } else {
               uni.showToast({ title: res.data && res.data.message || '提交失败', icon: 'none' })
@@ -1207,6 +1279,58 @@ export default {
   box-sizing: border-box;
 }
 
+/* ===== 近期心情历史(合并自成长页心情记录) ===== */
+.mood-list {
+  display: flex;
+  flex-direction: column;
+}
+
+.mood-item {
+  display: flex;
+  align-items: center;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #F5F5F5;
+}
+
+.mood-item:last-child {
+  border-bottom: none;
+}
+
+.mood-emotion {
+  font-size: 40rpx;
+  margin-right: 20rpx;
+  flex-shrink: 0;
+}
+
+.mood-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.mood-label {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+}
+
+.mood-date {
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 6rpx;
+}
+
+.mood-note {
+  font-size: 26rpx;
+  color: #666;
+  max-width: 300rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  margin-left: 20rpx;
+  flex-shrink: 0;
+}
+
 .submit-area {
   margin: 40rpx 32rpx;
 }

+ 11 - 6
cfc-frontend/pages/mind-detail/emotion-trend.vue

@@ -143,10 +143,13 @@ export default {
         stormy: { icon: '\u26A1', label: '很烦躁', color: '#8B5CF6' },
         rainbow: { icon: '\uD83C\uDF08', label: '特别开心', color: '#FF6B9D' }
       },
-      // Plutchik 情绪中文映射
+      // Plutchik 情绪中文映射 + 成长页词表(旧数据兼容)
       emotionTypeLabels: {
         'joy': '开心', 'trust': '信任', 'fear': '恐惧', 'surprise': '惊讶',
-        'sadness': '悲伤', 'disgust': '厌恶', 'anger': '愤怒', 'anticipation': '期待'
+        'sadness': '悲伤', 'disgust': '厌恶', 'anger': '愤怒', 'anticipation': '期待',
+        // 成长页(growth-emotion-checkin)历史词表
+        'calm': '平静', 'excited': '兴奋', 'sad': '难过', 'angry': '生气',
+        'anxious': '焦虑', 'tired': '疲惫', 'scared': '害怕'
       },
 
       // 散点/折线图数据
@@ -256,14 +259,16 @@ export default {
     }
   },
 
-  onLoad() {
-    this.currentChildId = uni.getStorageSync('currentChildId') || null
+  onLoad(options) {
+    // memberId 优先取页面参数(成长页/情绪打卡页跳转传入),再兜底本地存储链
+    var memberId = (options && options.memberId) || uni.getStorageSync('currentChildId') || uni.getStorageSync('currentMemberId') || uni.getStorageSync('userId') || null
+    this.currentChildId = memberId
     this.loadData()
   },
 
   methods: {
     loadData() {
-      const memberId = uni.getStorageSync('currentChildId')
+      var memberId = this.currentChildId
       if (!memberId) return
 
       uni.request({
@@ -324,7 +329,7 @@ export default {
     },
 
     goCheckin() {
-      uni.navigateTo({ url: '/pages/mind/emotion-checkin' })
+      uni.navigateTo({ url: '/pages/mind-detail/emotion-checkin' })
     },
 
     showDayDetail(day) {