فهرست منبع

feat(mind): add Canvas-based trend charts and index page updates

Enhance emotion-trend with Canvas scatter plot (valence×arousal), mood score trend line (Y-axis 1-10), and heatmap calendar with mood-score tinted backgrounds. Add PsychCrisisBanner integration. Update index.vue with screening funcList entry and crisis banner.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Xiaogang Liao 4 ماه پیش
والد
کامیت
a5a1790bfc
3فایلهای تغییر یافته به همراه855 افزوده شده و 47 حذف شده
  1. 430 23
      cfc-frontend/pages/mind-detail/emotion-trend.vue
  2. 419 23
      cfc-frontend/pages/mind/emotion-trend.vue
  3. 6 1
      cfc-frontend/pages/mind/index.vue

+ 430 - 23
cfc-frontend/pages/mind-detail/emotion-trend.vue

@@ -34,7 +34,24 @@
       </view>
       </view>
     </view>
     </view>
 
 
-    <!-- 情绪日历 -->
+    <!-- Phase 1.2: 情绪风险告警 -->
+    <PsychCrisisBanner v-if="hasChildId" :childId="currentChildId" source="checkin" />
+
+    <!-- ===== Phase 1.1: 情绪评分折线图 ===== -->
+    <view class="chart-card" v-if="moodTrendData.length > 0">
+      <text class="card-title">情绪评分趋势</text>
+      <text class="card-subtitle" v-if="trend && trend.moodScoreAvg">周均分: {{ trend.moodScoreAvg }}</text>
+      <canvas class="line-chart-canvas" canvas-id="moodTrendCanvas" @ready="drawMoodTrend"></canvas>
+    </view>
+
+    <!-- ===== Phase 1.1: 效价-激活度散点图 ===== -->
+    <view class="chart-card" v-if="scatterData.length > 0">
+      <text class="card-title">情绪空间分布</text>
+      <text class="card-subtitle">每个点代表一次打卡</text>
+      <canvas class="scatter-canvas" canvas-id="scatterCanvas" @ready="drawScatter"></canvas>
+    </view>
+
+    <!-- ===== 情绪日历(升级为热力版) ===== -->
     <view class="calendar-card">
     <view class="calendar-card">
       <text class="card-title">情绪日历</text>
       <text class="card-title">情绪日历</text>
       <view class="calendar-grid">
       <view class="calendar-grid">
@@ -44,10 +61,25 @@
         <view
         <view
           v-for="(day, idx) in calendarDays"
           v-for="(day, idx) in calendarDays"
           :key="idx"
           :key="idx"
-          :class="['calendar-day', 'weather-' + (day.weather || 'none'), day.isToday ? 'is-today' : '']">
+          :class="['calendar-day', day.isToday ? 'is-today' : '', day.moodScore ? 'has-data' : '']"
+          :style="day.bgStyle"
+          @click="day.moodScore && showDayDetail(day)">
           <text class="day-num">{{ day.dayNum }}</text>
           <text class="day-num">{{ day.dayNum }}</text>
-          <text v-if="day.weather" class="day-emoji">{{ weatherEmoji(day.weather) }}</text>
+          <text v-if="day.weather" class="day-emoji-small">{{ weatherEmoji(day.weather) }}</text>
+          <text v-if="day.moodScore" class="day-score">{{ day.moodScore }}</text>
+        </view>
+      </view>
+      <!-- 热力图例 -->
+      <view class="heatmap-legend">
+        <text class="legend-label">低</text>
+        <view class="legend-bar">
+          <view class="legend-step" style="background:#FFE0E0"></view>
+          <view class="legend-step" style="background:#FFB0B0"></view>
+          <view class="legend-step" style="background:#FFD9A0"></view>
+          <view class="legend-step" style="background:#C8E6C9"></view>
+          <view class="legend-step" style="background:#81C784"></view>
         </view>
         </view>
+        <text class="legend-label">高</text>
       </view>
       </view>
     </view>
     </view>
 
 
@@ -66,13 +98,19 @@
       </view>
       </view>
     </view>
     </view>
 
 
-    <!-- 按天详情 -->
+    <!-- 按天详情(含情绪精细数据) -->
     <view class="daily-list-card" v-if="dailyData.length > 0">
     <view class="daily-list-card" v-if="dailyData.length > 0">
       <text class="card-title">每日记录</text>
       <text class="card-title">每日记录</text>
-      <view class="daily-item" v-for="(day, idx) in dailyData" :key="idx">
+      <view class="daily-item" v-for="(day, idx) in dailyData" :key="idx" @click="viewDayCheckin(day)">
         <text class="daily-date">{{ formatDate(day.date) }}</text>
         <text class="daily-date">{{ formatDate(day.date) }}</text>
         <text class="daily-emoji">{{ weatherEmoji(day.weather) }}</text>
         <text class="daily-emoji">{{ weatherEmoji(day.weather) }}</text>
         <text class="daily-weather">{{ weatherLabel(day.weather) }}</text>
         <text class="daily-weather">{{ weatherLabel(day.weather) }}</text>
+        <!-- Phase 1.1 精细数据标签 -->
+        <view class="daily-tags" v-if="day.moodScore">
+          <text class="daily-tag score-tag">{{ day.moodScore }}分</text>
+          <text class="daily-tag type-tag" v-if="day.emotionType">{{ emotionTypeLabel(day.emotionType) }}</text>
+          <text class="daily-tag arousal-tag" v-if="day.arousalLevel">激活{{ day.arousalLevel }}</text>
+        </view>
       </view>
       </view>
     </view>
     </view>
 
 
@@ -86,10 +124,13 @@
 
 
 <script>
 <script>
 import config from '../../config'
 import config from '../../config'
+import PsychCrisisBanner from '../../components/PsychCrisisBanner.vue'
 
 
 export default {
 export default {
+  components: { PsychCrisisBanner },
   data() {
   data() {
     return {
     return {
+      currentChildId: null,
       period: 'week',
       period: 'week',
       stats: null,
       stats: null,
       trend: null,
       trend: null,
@@ -100,11 +141,27 @@ export default {
         rainy: { icon: '\uD83C\uDF27\uFE0F', label: '有点难过', color: '#6B9BD2' },
         rainy: { icon: '\uD83C\uDF27\uFE0F', label: '有点难过', color: '#6B9BD2' },
         stormy: { icon: '\u26A1', label: '很烦躁', color: '#8B5CF6' },
         stormy: { icon: '\u26A1', label: '很烦躁', color: '#8B5CF6' },
         rainbow: { icon: '\uD83C\uDF08', label: '特别开心', color: '#FF6B9D' }
         rainbow: { icon: '\uD83C\uDF08', label: '特别开心', color: '#FF6B9D' }
-      }
+      },
+      // Plutchik 情绪中文映射
+      emotionTypeLabels: {
+        'joy': '开心', 'trust': '信任', 'fear': '恐惧', 'surprise': '惊讶',
+        'sadness': '悲伤', 'disgust': '厌恶', 'anger': '愤怒', 'anticipation': '期待'
+      },
+
+      // 散点/折线图数据
+      scatterData: [],
+      moodTrendData: [],
+
+      // Canvas 上下文
+      scatterCtx: null,
+      trendCtx: null
     }
     }
   },
   },
 
 
   computed: {
   computed: {
+    hasChildId() {
+      return !!this.currentChildId
+    },
     calendarDays() {
     calendarDays() {
       const days = []
       const days = []
       const now = new Date()
       const now = new Date()
@@ -114,27 +171,59 @@ export default {
       const daysInMonth = new Date(year, month + 1, 0).getDate()
       const daysInMonth = new Date(year, month + 1, 0).getDate()
       const today = now.getDate()
       const today = now.getDate()
 
 
-      // Build weather map from daily data
-      const weatherMap = {}
+      // Build data map from dailyData
+      const dataMap = {}
       if (this.trend && this.trend.dailyData) {
       if (this.trend && this.trend.dailyData) {
         for (const d of this.trend.dailyData) {
         for (const d of this.trend.dailyData) {
           if (d.date) {
           if (d.date) {
             const dateObj = new Date(d.date)
             const dateObj = new Date(d.date)
-            weatherMap[dateObj.getDate()] = d.weather
+            dataMap[dateObj.getDate()] = d
           }
           }
         }
         }
       }
       }
 
 
       // Empty leading cells
       // Empty leading cells
       for (let i = 0; i < firstDay; i++) {
       for (let i = 0; i < firstDay; i++) {
-        days.push({ dayNum: '', weather: null, isToday: false })
+        days.push({ dayNum: '', weather: null, moodScore: null, isToday: false, bgStyle: '' })
       }
       }
 
 
       for (let d = 1; d <= daysInMonth; d++) {
       for (let d = 1; d <= daysInMonth; d++) {
+        const data = dataMap[d]
+        var bgStyle = ''
+        if (data && data.moodScore) {
+          // Heatmap intensity: moodScore 1-10 mapped to color
+          var score = data.moodScore
+          var r, g, b
+          if (score <= 3) {
+            // 1-3: red tint
+            var intensity1 = (score - 1) / 2
+            r = 255
+            g = Math.round(180 + 75 * intensity1)
+            b = Math.round(180 + 75 * intensity1)
+          } else if (score <= 6) {
+            // 4-6: orange/yellow tint
+            var intensity2 = (score - 4) / 2
+            r = 255
+            g = Math.round(200 + 55 * intensity2)
+            b = Math.round(160 - 60 * intensity2)
+          } else {
+            // 7-10: green tint
+            var intensity3 = (score - 7) / 3
+            r = Math.round(200 - 120 * intensity3)
+            g = Math.round(230 - 20 * intensity3)
+            b = Math.round(200 - 120 * intensity3)
+          }
+          bgStyle = 'background: rgba(' + r + ',' + g + ',' + b + ',0.4)'
+        }
+
         days.push({
         days.push({
           dayNum: d,
           dayNum: d,
-          weather: weatherMap[d] || null,
-          isToday: d === today
+          weather: data ? data.weather : null,
+          moodScore: data ? data.moodScore : null,
+          emotionType: data ? data.emotionType : null,
+          arousalLevel: data ? data.arousalLevel : null,
+          isToday: d === today,
+          bgStyle: bgStyle
         })
         })
       }
       }
 
 
@@ -166,6 +255,8 @@ export default {
   },
   },
 
 
   onLoad() {
   onLoad() {
+    const app = getApp()
+    this.currentChildId = app.globalData.currentChildId || null
     this.loadData()
     this.loadData()
   },
   },
 
 
@@ -193,6 +284,14 @@ export default {
         success: (res) => {
         success: (res) => {
           if (res.data && res.data.code === 200) {
           if (res.data && res.data.code === 200) {
             this.trend = res.data.data
             this.trend = res.data.data
+            // Extract scatter & trend data
+            this.scatterData = this.trend && this.trend.scatterData || []
+            this.moodTrendData = this.trend && this.trend.moodTrend || []
+            // Draw charts after data is ready
+            this.$nextTick(function() {
+              this.drawMoodTrend()
+              this.drawScatter()
+            })
           }
           }
         }
         }
       })
       })
@@ -213,6 +312,10 @@ export default {
       return cfg ? cfg.label : weather
       return cfg ? cfg.label : weather
     },
     },
 
 
+    emotionTypeLabel(type) {
+      return this.emotionTypeLabels[type] || type
+    },
+
     formatDate(dateStr) {
     formatDate(dateStr) {
       if (!dateStr) return ''
       if (!dateStr) return ''
       const d = new Date(dateStr)
       const d = new Date(dateStr)
@@ -221,6 +324,222 @@ export default {
 
 
     goCheckin() {
     goCheckin() {
       uni.navigateTo({ url: '/pages/mind/emotion-checkin' })
       uni.navigateTo({ url: '/pages/mind/emotion-checkin' })
+    },
+
+    showDayDetail(day) {
+      // 点击日历格子可以跳转到当天的打卡详情
+      // 简单 toast 显示评分
+      uni.showToast({ title: '情绪评分: ' + day.moodScore + '/10', icon: 'none' })
+    },
+
+    viewDayCheckin(day) {
+      // 查看单条打卡详情(当前简单响应)
+      if (day.moodScore) {
+        uni.showToast({ title: '情绪评分: ' + day.moodScore + '/10', icon: 'none' })
+      }
+    },
+
+    // ===== Canvas 绘图 =====
+
+    drawMoodTrend() {
+      if (this.moodTrendData.length < 1) return
+
+      var canvasId = 'moodTrendCanvas'
+      var query = uni.createSelectorQuery()
+      var self = this
+
+      query.select('#' + canvasId).fields({ node: true, size: true }).exec(function(res) {
+        // Fallback: use context API for older uni-app
+        var ctx = uni.createCanvasContext(canvasId, self)
+        var dpr = 2
+        // Get canvas dimensions from the element
+        query.select('#' + canvasId).boundingClientRect(function(rect) {
+          if (!rect) return
+          var width = rect.width || 300
+          var height = rect.height || 200
+          var canvasWidth = width * dpr
+          var canvasHeight = height * dpr
+
+          // Clear
+          ctx.clearRect(0, 0, canvasWidth, canvasHeight)
+
+          var data = self.moodTrendData
+          var count = data.length
+          if (count === 0) return
+
+          var padding = { top: 20 * dpr, right: 20 * dpr, bottom: 30 * dpr, left: 30 * dpr }
+          var plotW = canvasWidth - padding.left - padding.right
+          var plotH = canvasHeight - padding.top - padding.bottom
+
+          // Draw axes
+          ctx.setStrokeStyle('#E0E0E0')
+          ctx.setLineWidth(1 * dpr)
+          ctx.beginPath()
+          ctx.moveTo(padding.left, padding.top)
+          ctx.lineTo(padding.left, padding.top + plotH)
+          ctx.lineTo(padding.left + plotW, padding.top + plotH)
+          ctx.stroke()
+
+          // Draw Y axis labels
+          ctx.setFontSize(10 * dpr)
+          ctx.setFillStyle('#999')
+          ctx.setTextAlign('right')
+          for (var yVal = 2; yVal <= 10; yVal += 2) {
+            var yPos = padding.top + plotH - (yVal / 10) * plotH
+            ctx.fillText(yVal + '', padding.left - 6 * dpr, yPos + 3 * dpr)
+            // Grid line
+            ctx.setStrokeStyle('#F0F0F0')
+            ctx.setLineWidth(1 * dpr)
+            ctx.beginPath()
+            ctx.moveTo(padding.left, yPos)
+            ctx.lineTo(padding.left + plotW, yPos)
+            ctx.stroke()
+          }
+
+          // Determine trend line color based on trend direction
+          var avg = 0
+          for (var si = 0; si < count; si++) {
+            avg += data[si].moodScore
+          }
+          avg /= count
+          var lineColor = avg >= 5 ? '#10B981' : '#FF6B9D'
+
+          // Draw data points and lines
+          ctx.setStrokeStyle(lineColor)
+          ctx.setLineWidth(3 * dpr)
+          ctx.setFillStyle(lineColor)
+
+          for (var i = 0; i < count; i++) {
+            var x = padding.left + (i / Math.max(count - 1, 1)) * plotW
+            var y = padding.top + plotH - (data[i].moodScore / 10) * plotH
+
+            if (i === 0) {
+              ctx.beginPath()
+              ctx.moveTo(x, y)
+            } else {
+              ctx.lineTo(x, y)
+            }
+
+            // Draw point circle
+            ctx.beginPath()
+            ctx.arc(x, y, 4 * dpr, 0, 2 * Math.PI)
+            ctx.setFillStyle(lineColor)
+            ctx.fill()
+          }
+          ctx.stroke()
+
+          // Draw X axis labels (dates)
+          ctx.setFontSize(9 * dpr)
+          ctx.setFillStyle('#999')
+          ctx.setTextAlign('center')
+          var step = Math.max(1, Math.floor(count / 6))
+          for (var di = 0; di < count; di += step) {
+            var labelX = padding.left + (di / Math.max(count - 1, 1)) * plotW
+            var labelText = self.formatDate(data[di].date)
+            ctx.fillText(labelText, labelX, padding.top + plotH + 18 * dpr)
+          }
+
+          ctx.draw()
+        }).exec()
+      })
+    },
+
+    drawScatter() {
+      if (this.scatterData.length < 3) return
+
+      var canvasId = 'scatterCanvas'
+      var query = uni.createSelectorQuery()
+      var self = this
+
+      query.select('#' + canvasId).boundingClientRect(function(rect) {
+        if (!rect) return
+        var ctx = uni.createCanvasContext(canvasId, self)
+        var dpr = 2
+        var width = rect.width || 300
+        var height = rect.height || 200
+        var canvasWidth = width * dpr
+        var canvasHeight = height * dpr
+
+        ctx.clearRect(0, 0, canvasWidth, canvasHeight)
+
+        var padding = { top: 20 * dpr, right: 20 * dpr, bottom: 30 * dpr, left: 30 * dpr }
+        var plotW = canvasWidth - padding.left - padding.right
+        var plotH = canvasHeight - padding.top - padding.bottom
+
+        // Draw background with 4 quadrants
+        // Q1 (high arousal, high valence): top-right
+        var midX = padding.left + plotW / 2
+        var midY = padding.top + plotH / 2
+
+        // Quadrant backgrounds
+        ctx.setFillStyle('rgba(255,240,240,0.3)')
+        ctx.fillRect(midX, padding.top, plotW / 2, plotH / 2)  // high arousal, high valence
+        ctx.setFillStyle('rgba(240,240,255,0.3)')
+        ctx.fillRect(padding.left, padding.top, plotW / 2, plotH / 2)  // high arousal, low valence
+        ctx.setFillStyle('rgba(255,248,240,0.3)')
+        ctx.fillRect(midX, midY, plotW / 2, plotH / 2)  // low arousal, high valence
+        ctx.setFillStyle('rgba(245,245,245,0.3)')
+        ctx.fillRect(padding.left, midY, plotW / 2, plotH / 2)  // low arousal, low valence
+
+        // Quadrant labels
+        ctx.setFontSize(10 * dpr)
+        ctx.setFillStyle('rgba(0,0,0,0.15)')
+        ctx.setTextAlign('center')
+        ctx.fillText('兴奋', padding.left + plotW * 0.75, padding.top + plotH * 0.3)
+        ctx.fillText('烦躁', padding.left + plotW * 0.25, padding.top + plotH * 0.3)
+        ctx.fillText('平静', padding.left + plotW * 0.75, padding.top + plotH * 0.8)
+        ctx.fillText('低落', padding.left + plotW * 0.25, padding.top + plotH * 0.8)
+
+        // Axes
+        ctx.setStrokeStyle('#E0E0E0')
+        ctx.setLineWidth(1 * dpr)
+        ctx.beginPath()
+        ctx.moveTo(padding.left, padding.top + plotH / 2)
+        ctx.lineTo(padding.left + plotW, padding.top + plotH / 2)
+        ctx.stroke()
+        ctx.beginPath()
+        ctx.moveTo(padding.left + plotW / 2, padding.top)
+        ctx.lineTo(padding.left + plotW / 2, padding.top + plotH)
+        ctx.stroke()
+
+        // Axis labels
+        ctx.setFontSize(9 * dpr)
+        ctx.setFillStyle('#999')
+        ctx.setTextAlign('center')
+        ctx.fillText('效价 →', padding.left + plotW / 2, padding.top + plotH + 18 * dpr)
+        ctx.save()
+        ctx.translate(padding.left - 4 * dpr, padding.top + plotH / 2)
+        ctx.rotate(-Math.PI / 2)
+        ctx.setTextAlign('center')
+        ctx.fillText('激活度', 0, 0)
+        ctx.restore()
+
+        // Draw scatter points
+        for (var i = 0; i < self.scatterData.length; i++) {
+          var pt = self.scatterData[i]
+          var px = padding.left + (pt.valence / 10) * plotW
+          var py = padding.top + plotH - (pt.arousal / 10) * plotH
+
+          // Color based on valence
+          var color
+          if (pt.valence >= 7) {
+            color = '#10B981'
+          } else if (pt.valence >= 5) {
+            color = '#FFD93D'
+          } else {
+            color = '#FF6B9D'
+          }
+
+          ctx.beginPath()
+          ctx.arc(px, py, 5 * dpr, 0, 2 * Math.PI)
+          ctx.setFillStyle(color)
+          ctx.setGlobalAlpha(0.7)
+          ctx.fill()
+          ctx.setGlobalAlpha(1)
+        }
+
+        ctx.draw()
+      }).exec()
     }
     }
   }
   }
 }
 }
@@ -282,6 +601,38 @@ export default {
   font-weight: 600;
   font-weight: 600;
 }
 }
 
 
+/* ===== 图表卡片 ===== */
+.chart-card {
+  margin: 24rpx 32rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.chart-card .card-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  display: block;
+  margin-bottom: 4rpx;
+}
+
+.chart-card .card-subtitle {
+  font-size: 22rpx;
+  color: #999;
+  display: block;
+  margin-bottom: 16rpx;
+}
+
+.line-chart-canvas,
+.scatter-canvas {
+  width: 100%;
+  height: 360rpx;
+  border-radius: 12rpx;
+  background: #FCFCFC;
+}
+
 .calendar-card,
 .calendar-card,
 .distribution-card,
 .distribution-card,
 .daily-list-card {
 .daily-list-card {
@@ -322,29 +673,61 @@ export default {
   flex-direction: column;
   flex-direction: column;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
+  border-radius: 8rpx;
+}
+
+.calendar-day.has-data {
+  cursor: pointer;
 }
 }
 
 
 .is-today {
 .is-today {
-  background: #FFF0F5;
-  border-radius: 12rpx;
+  border: 2rpx solid #FF6B9D;
+  box-sizing: border-box;
 }
 }
 
 
 .day-num {
 .day-num {
-  font-size: 26rpx;
+  font-size: 24rpx;
   color: #333;
   color: #333;
 }
 }
 
 
-.day-emoji {
-  font-size: 24rpx;
-  margin-top: 4rpx;
+.day-emoji-small {
+  font-size: 20rpx;
+  margin-top: 2rpx;
 }
 }
 
 
-.weather-sunny { background: #FFF8E1; border-radius: 8rpx; }
-.weather-cloudy { background: #F5F5F5; border-radius: 8rpx; }
-.weather-rainy { background: #E3F2FD; border-radius: 8rpx; }
-.weather-stormy { background: #EDE7F6; border-radius: 8rpx; }
-.weather-rainbow { background: #FCE4EC; border-radius: 8rpx; }
+.day-score {
+  font-size: 18rpx;
+  color: #888;
+  margin-top: 2rpx;
+}
 
 
+/* 热力图例 */
+.heatmap-legend {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 12rpx;
+  margin-top: 16rpx;
+}
+
+.legend-label {
+  font-size: 20rpx;
+  color: #999;
+}
+
+.legend-bar {
+  display: flex;
+  gap: 2rpx;
+  border-radius: 6rpx;
+  overflow: hidden;
+}
+
+.legend-step {
+  width: 24rpx;
+  height: 16rpx;
+}
+
+/* ===== 情绪分布 ===== */
 .distribution-list {
 .distribution-list {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
@@ -390,6 +773,7 @@ export default {
   text-align: right;
   text-align: right;
 }
 }
 
 
+/* ===== 每日记录 ===== */
 .daily-item {
 .daily-item {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -402,17 +786,40 @@ export default {
   font-size: 24rpx;
   font-size: 24rpx;
   color: #999;
   color: #999;
   width: 100rpx;
   width: 100rpx;
+  flex-shrink: 0;
 }
 }
 
 
 .daily-emoji {
 .daily-emoji {
   font-size: 32rpx;
   font-size: 32rpx;
+  width: 40rpx;
+  text-align: center;
+  flex-shrink: 0;
 }
 }
 
 
 .daily-weather {
 .daily-weather {
   font-size: 26rpx;
   font-size: 26rpx;
   color: #666;
   color: #666;
+  width: 120rpx;
+  flex-shrink: 0;
+}
+
+.daily-tags {
+  display: flex;
+  gap: 8rpx;
+  flex-wrap: wrap;
 }
 }
 
 
+.daily-tag {
+  font-size: 20rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 12rpx;
+  color: #fff;
+}
+
+.score-tag { background: #FF6B9D; }
+.type-tag { background: #8B5CF6; }
+.arousal-tag { background: #3B82F6; }
+
 .empty-state {
 .empty-state {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;

+ 419 - 23
cfc-frontend/pages/mind/emotion-trend.vue

@@ -34,7 +34,21 @@
       </view>
       </view>
     </view>
     </view>
 
 
-    <!-- 情绪日历 -->
+    <!-- ===== Phase 1.1: 情绪评分折线图 ===== -->
+    <view class="chart-card" v-if="moodTrendData.length > 0">
+      <text class="card-title">情绪评分趋势</text>
+      <text class="card-subtitle" v-if="trend && trend.moodScoreAvg">周均分: {{ trend.moodScoreAvg }}</text>
+      <canvas class="line-chart-canvas" canvas-id="moodTrendCanvas" @ready="drawMoodTrend"></canvas>
+    </view>
+
+    <!-- ===== Phase 1.1: 效价-激活度散点图 ===== -->
+    <view class="chart-card" v-if="scatterData.length > 0">
+      <text class="card-title">情绪空间分布</text>
+      <text class="card-subtitle">每个点代表一次打卡</text>
+      <canvas class="scatter-canvas" canvas-id="scatterCanvas" @ready="drawScatter"></canvas>
+    </view>
+
+    <!-- ===== 情绪日历(升级为热力版) ===== -->
     <view class="calendar-card">
     <view class="calendar-card">
       <text class="card-title">情绪日历</text>
       <text class="card-title">情绪日历</text>
       <view class="calendar-grid">
       <view class="calendar-grid">
@@ -44,10 +58,25 @@
         <view
         <view
           v-for="(day, idx) in calendarDays"
           v-for="(day, idx) in calendarDays"
           :key="idx"
           :key="idx"
-          :class="['calendar-day', 'weather-' + (day.weather || 'none'), day.isToday ? 'is-today' : '']">
+          :class="['calendar-day', day.isToday ? 'is-today' : '', day.moodScore ? 'has-data' : '']"
+          :style="day.bgStyle"
+          @click="day.moodScore && showDayDetail(day)">
           <text class="day-num">{{ day.dayNum }}</text>
           <text class="day-num">{{ day.dayNum }}</text>
-          <text v-if="day.weather" class="day-emoji">{{ weatherEmoji(day.weather) }}</text>
+          <text v-if="day.weather" class="day-emoji-small">{{ weatherEmoji(day.weather) }}</text>
+          <text v-if="day.moodScore" class="day-score">{{ day.moodScore }}</text>
+        </view>
+      </view>
+      <!-- 热力图例 -->
+      <view class="heatmap-legend">
+        <text class="legend-label">低</text>
+        <view class="legend-bar">
+          <view class="legend-step" style="background:#FFE0E0"></view>
+          <view class="legend-step" style="background:#FFB0B0"></view>
+          <view class="legend-step" style="background:#FFD9A0"></view>
+          <view class="legend-step" style="background:#C8E6C9"></view>
+          <view class="legend-step" style="background:#81C784"></view>
         </view>
         </view>
+        <text class="legend-label">高</text>
       </view>
       </view>
     </view>
     </view>
 
 
@@ -66,13 +95,19 @@
       </view>
       </view>
     </view>
     </view>
 
 
-    <!-- 按天详情 -->
+    <!-- 按天详情(含情绪精细数据) -->
     <view class="daily-list-card" v-if="dailyData.length > 0">
     <view class="daily-list-card" v-if="dailyData.length > 0">
       <text class="card-title">每日记录</text>
       <text class="card-title">每日记录</text>
-      <view class="daily-item" v-for="(day, idx) in dailyData" :key="idx">
+      <view class="daily-item" v-for="(day, idx) in dailyData" :key="idx" @click="viewDayCheckin(day)">
         <text class="daily-date">{{ formatDate(day.date) }}</text>
         <text class="daily-date">{{ formatDate(day.date) }}</text>
         <text class="daily-emoji">{{ weatherEmoji(day.weather) }}</text>
         <text class="daily-emoji">{{ weatherEmoji(day.weather) }}</text>
         <text class="daily-weather">{{ weatherLabel(day.weather) }}</text>
         <text class="daily-weather">{{ weatherLabel(day.weather) }}</text>
+        <!-- Phase 1.1 精细数据标签 -->
+        <view class="daily-tags" v-if="day.moodScore">
+          <text class="daily-tag score-tag">{{ day.moodScore }}分</text>
+          <text class="daily-tag type-tag" v-if="day.emotionType">{{ emotionTypeLabel(day.emotionType) }}</text>
+          <text class="daily-tag arousal-tag" v-if="day.arousalLevel">激活{{ day.arousalLevel }}</text>
+        </view>
       </view>
       </view>
     </view>
     </view>
 
 
@@ -100,7 +135,20 @@ export default {
         rainy: { icon: '\uD83C\uDF27\uFE0F', label: '有点难过', color: '#6B9BD2' },
         rainy: { icon: '\uD83C\uDF27\uFE0F', label: '有点难过', color: '#6B9BD2' },
         stormy: { icon: '\u26A1', label: '很烦躁', color: '#8B5CF6' },
         stormy: { icon: '\u26A1', label: '很烦躁', color: '#8B5CF6' },
         rainbow: { icon: '\uD83C\uDF08', label: '特别开心', color: '#FF6B9D' }
         rainbow: { icon: '\uD83C\uDF08', label: '特别开心', color: '#FF6B9D' }
-      }
+      },
+      // Plutchik 情绪中文映射
+      emotionTypeLabels: {
+        'joy': '开心', 'trust': '信任', 'fear': '恐惧', 'surprise': '惊讶',
+        'sadness': '悲伤', 'disgust': '厌恶', 'anger': '愤怒', 'anticipation': '期待'
+      },
+
+      // 散点/折线图数据
+      scatterData: [],
+      moodTrendData: [],
+
+      // Canvas 上下文
+      scatterCtx: null,
+      trendCtx: null
     }
     }
   },
   },
 
 
@@ -114,27 +162,59 @@ export default {
       const daysInMonth = new Date(year, month + 1, 0).getDate()
       const daysInMonth = new Date(year, month + 1, 0).getDate()
       const today = now.getDate()
       const today = now.getDate()
 
 
-      // Build weather map from daily data
-      const weatherMap = {}
+      // Build data map from dailyData
+      const dataMap = {}
       if (this.trend && this.trend.dailyData) {
       if (this.trend && this.trend.dailyData) {
         for (const d of this.trend.dailyData) {
         for (const d of this.trend.dailyData) {
           if (d.date) {
           if (d.date) {
             const dateObj = new Date(d.date)
             const dateObj = new Date(d.date)
-            weatherMap[dateObj.getDate()] = d.weather
+            dataMap[dateObj.getDate()] = d
           }
           }
         }
         }
       }
       }
 
 
       // Empty leading cells
       // Empty leading cells
       for (let i = 0; i < firstDay; i++) {
       for (let i = 0; i < firstDay; i++) {
-        days.push({ dayNum: '', weather: null, isToday: false })
+        days.push({ dayNum: '', weather: null, moodScore: null, isToday: false, bgStyle: '' })
       }
       }
 
 
       for (let d = 1; d <= daysInMonth; d++) {
       for (let d = 1; d <= daysInMonth; d++) {
+        const data = dataMap[d]
+        var bgStyle = ''
+        if (data && data.moodScore) {
+          // Heatmap intensity: moodScore 1-10 mapped to color
+          var score = data.moodScore
+          var r, g, b
+          if (score <= 3) {
+            // 1-3: red tint
+            var intensity1 = (score - 1) / 2
+            r = 255
+            g = Math.round(180 + 75 * intensity1)
+            b = Math.round(180 + 75 * intensity1)
+          } else if (score <= 6) {
+            // 4-6: orange/yellow tint
+            var intensity2 = (score - 4) / 2
+            r = 255
+            g = Math.round(200 + 55 * intensity2)
+            b = Math.round(160 - 60 * intensity2)
+          } else {
+            // 7-10: green tint
+            var intensity3 = (score - 7) / 3
+            r = Math.round(200 - 120 * intensity3)
+            g = Math.round(230 - 20 * intensity3)
+            b = Math.round(200 - 120 * intensity3)
+          }
+          bgStyle = 'background: rgba(' + r + ',' + g + ',' + b + ',0.4)'
+        }
+
         days.push({
         days.push({
           dayNum: d,
           dayNum: d,
-          weather: weatherMap[d] || null,
-          isToday: d === today
+          weather: data ? data.weather : null,
+          moodScore: data ? data.moodScore : null,
+          emotionType: data ? data.emotionType : null,
+          arousalLevel: data ? data.arousalLevel : null,
+          isToday: d === today,
+          bgStyle: bgStyle
         })
         })
       }
       }
 
 
@@ -193,6 +273,14 @@ export default {
         success: (res) => {
         success: (res) => {
           if (res.data && res.data.code === 200) {
           if (res.data && res.data.code === 200) {
             this.trend = res.data.data
             this.trend = res.data.data
+            // Extract scatter & trend data
+            this.scatterData = this.trend && this.trend.scatterData || []
+            this.moodTrendData = this.trend && this.trend.moodTrend || []
+            // Draw charts after data is ready
+            this.$nextTick(function() {
+              this.drawMoodTrend()
+              this.drawScatter()
+            })
           }
           }
         }
         }
       })
       })
@@ -213,6 +301,10 @@ export default {
       return cfg ? cfg.label : weather
       return cfg ? cfg.label : weather
     },
     },
 
 
+    emotionTypeLabel(type) {
+      return this.emotionTypeLabels[type] || type
+    },
+
     formatDate(dateStr) {
     formatDate(dateStr) {
       if (!dateStr) return ''
       if (!dateStr) return ''
       const d = new Date(dateStr)
       const d = new Date(dateStr)
@@ -221,6 +313,222 @@ export default {
 
 
     goCheckin() {
     goCheckin() {
       uni.navigateTo({ url: '/pages/mind/emotion-checkin' })
       uni.navigateTo({ url: '/pages/mind/emotion-checkin' })
+    },
+
+    showDayDetail(day) {
+      // 点击日历格子可以跳转到当天的打卡详情
+      // 简单 toast 显示评分
+      uni.showToast({ title: '情绪评分: ' + day.moodScore + '/10', icon: 'none' })
+    },
+
+    viewDayCheckin(day) {
+      // 查看单条打卡详情(当前简单响应)
+      if (day.moodScore) {
+        uni.showToast({ title: '情绪评分: ' + day.moodScore + '/10', icon: 'none' })
+      }
+    },
+
+    // ===== Canvas 绘图 =====
+
+    drawMoodTrend() {
+      if (this.moodTrendData.length < 1) return
+
+      var canvasId = 'moodTrendCanvas'
+      var query = uni.createSelectorQuery()
+      var self = this
+
+      query.select('#' + canvasId).fields({ node: true, size: true }).exec(function(res) {
+        // Fallback: use context API for older uni-app
+        var ctx = uni.createCanvasContext(canvasId, self)
+        var dpr = 2
+        // Get canvas dimensions from the element
+        query.select('#' + canvasId).boundingClientRect(function(rect) {
+          if (!rect) return
+          var width = rect.width || 300
+          var height = rect.height || 200
+          var canvasWidth = width * dpr
+          var canvasHeight = height * dpr
+
+          // Clear
+          ctx.clearRect(0, 0, canvasWidth, canvasHeight)
+
+          var data = self.moodTrendData
+          var count = data.length
+          if (count === 0) return
+
+          var padding = { top: 20 * dpr, right: 20 * dpr, bottom: 30 * dpr, left: 30 * dpr }
+          var plotW = canvasWidth - padding.left - padding.right
+          var plotH = canvasHeight - padding.top - padding.bottom
+
+          // Draw axes
+          ctx.setStrokeStyle('#E0E0E0')
+          ctx.setLineWidth(1 * dpr)
+          ctx.beginPath()
+          ctx.moveTo(padding.left, padding.top)
+          ctx.lineTo(padding.left, padding.top + plotH)
+          ctx.lineTo(padding.left + plotW, padding.top + plotH)
+          ctx.stroke()
+
+          // Draw Y axis labels
+          ctx.setFontSize(10 * dpr)
+          ctx.setFillStyle('#999')
+          ctx.setTextAlign('right')
+          for (var yVal = 2; yVal <= 10; yVal += 2) {
+            var yPos = padding.top + plotH - (yVal / 10) * plotH
+            ctx.fillText(yVal + '', padding.left - 6 * dpr, yPos + 3 * dpr)
+            // Grid line
+            ctx.setStrokeStyle('#F0F0F0')
+            ctx.setLineWidth(1 * dpr)
+            ctx.beginPath()
+            ctx.moveTo(padding.left, yPos)
+            ctx.lineTo(padding.left + plotW, yPos)
+            ctx.stroke()
+          }
+
+          // Determine trend line color based on trend direction
+          var avg = 0
+          for (var si = 0; si < count; si++) {
+            avg += data[si].moodScore
+          }
+          avg /= count
+          var lineColor = avg >= 5 ? '#10B981' : '#FF6B9D'
+
+          // Draw data points and lines
+          ctx.setStrokeStyle(lineColor)
+          ctx.setLineWidth(3 * dpr)
+          ctx.setFillStyle(lineColor)
+
+          for (var i = 0; i < count; i++) {
+            var x = padding.left + (i / Math.max(count - 1, 1)) * plotW
+            var y = padding.top + plotH - (data[i].moodScore / 10) * plotH
+
+            if (i === 0) {
+              ctx.beginPath()
+              ctx.moveTo(x, y)
+            } else {
+              ctx.lineTo(x, y)
+            }
+
+            // Draw point circle
+            ctx.beginPath()
+            ctx.arc(x, y, 4 * dpr, 0, 2 * Math.PI)
+            ctx.setFillStyle(lineColor)
+            ctx.fill()
+          }
+          ctx.stroke()
+
+          // Draw X axis labels (dates)
+          ctx.setFontSize(9 * dpr)
+          ctx.setFillStyle('#999')
+          ctx.setTextAlign('center')
+          var step = Math.max(1, Math.floor(count / 6))
+          for (var di = 0; di < count; di += step) {
+            var labelX = padding.left + (di / Math.max(count - 1, 1)) * plotW
+            var labelText = self.formatDate(data[di].date)
+            ctx.fillText(labelText, labelX, padding.top + plotH + 18 * dpr)
+          }
+
+          ctx.draw()
+        }).exec()
+      })
+    },
+
+    drawScatter() {
+      if (this.scatterData.length < 3) return
+
+      var canvasId = 'scatterCanvas'
+      var query = uni.createSelectorQuery()
+      var self = this
+
+      query.select('#' + canvasId).boundingClientRect(function(rect) {
+        if (!rect) return
+        var ctx = uni.createCanvasContext(canvasId, self)
+        var dpr = 2
+        var width = rect.width || 300
+        var height = rect.height || 200
+        var canvasWidth = width * dpr
+        var canvasHeight = height * dpr
+
+        ctx.clearRect(0, 0, canvasWidth, canvasHeight)
+
+        var padding = { top: 20 * dpr, right: 20 * dpr, bottom: 30 * dpr, left: 30 * dpr }
+        var plotW = canvasWidth - padding.left - padding.right
+        var plotH = canvasHeight - padding.top - padding.bottom
+
+        // Draw background with 4 quadrants
+        // Q1 (high arousal, high valence): top-right
+        var midX = padding.left + plotW / 2
+        var midY = padding.top + plotH / 2
+
+        // Quadrant backgrounds
+        ctx.setFillStyle('rgba(255,240,240,0.3)')
+        ctx.fillRect(midX, padding.top, plotW / 2, plotH / 2)  // high arousal, high valence
+        ctx.setFillStyle('rgba(240,240,255,0.3)')
+        ctx.fillRect(padding.left, padding.top, plotW / 2, plotH / 2)  // high arousal, low valence
+        ctx.setFillStyle('rgba(255,248,240,0.3)')
+        ctx.fillRect(midX, midY, plotW / 2, plotH / 2)  // low arousal, high valence
+        ctx.setFillStyle('rgba(245,245,245,0.3)')
+        ctx.fillRect(padding.left, midY, plotW / 2, plotH / 2)  // low arousal, low valence
+
+        // Quadrant labels
+        ctx.setFontSize(10 * dpr)
+        ctx.setFillStyle('rgba(0,0,0,0.15)')
+        ctx.setTextAlign('center')
+        ctx.fillText('兴奋', padding.left + plotW * 0.75, padding.top + plotH * 0.3)
+        ctx.fillText('烦躁', padding.left + plotW * 0.25, padding.top + plotH * 0.3)
+        ctx.fillText('平静', padding.left + plotW * 0.75, padding.top + plotH * 0.8)
+        ctx.fillText('低落', padding.left + plotW * 0.25, padding.top + plotH * 0.8)
+
+        // Axes
+        ctx.setStrokeStyle('#E0E0E0')
+        ctx.setLineWidth(1 * dpr)
+        ctx.beginPath()
+        ctx.moveTo(padding.left, padding.top + plotH / 2)
+        ctx.lineTo(padding.left + plotW, padding.top + plotH / 2)
+        ctx.stroke()
+        ctx.beginPath()
+        ctx.moveTo(padding.left + plotW / 2, padding.top)
+        ctx.lineTo(padding.left + plotW / 2, padding.top + plotH)
+        ctx.stroke()
+
+        // Axis labels
+        ctx.setFontSize(9 * dpr)
+        ctx.setFillStyle('#999')
+        ctx.setTextAlign('center')
+        ctx.fillText('效价 →', padding.left + plotW / 2, padding.top + plotH + 18 * dpr)
+        ctx.save()
+        ctx.translate(padding.left - 4 * dpr, padding.top + plotH / 2)
+        ctx.rotate(-Math.PI / 2)
+        ctx.setTextAlign('center')
+        ctx.fillText('激活度', 0, 0)
+        ctx.restore()
+
+        // Draw scatter points
+        for (var i = 0; i < self.scatterData.length; i++) {
+          var pt = self.scatterData[i]
+          var px = padding.left + (pt.valence / 10) * plotW
+          var py = padding.top + plotH - (pt.arousal / 10) * plotH
+
+          // Color based on valence
+          var color
+          if (pt.valence >= 7) {
+            color = '#10B981'
+          } else if (pt.valence >= 5) {
+            color = '#FFD93D'
+          } else {
+            color = '#FF6B9D'
+          }
+
+          ctx.beginPath()
+          ctx.arc(px, py, 5 * dpr, 0, 2 * Math.PI)
+          ctx.setFillStyle(color)
+          ctx.setGlobalAlpha(0.7)
+          ctx.fill()
+          ctx.setGlobalAlpha(1)
+        }
+
+        ctx.draw()
+      }).exec()
     }
     }
   }
   }
 }
 }
@@ -282,6 +590,38 @@ export default {
   font-weight: 600;
   font-weight: 600;
 }
 }
 
 
+/* ===== 图表卡片 ===== */
+.chart-card {
+  margin: 24rpx 32rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.chart-card .card-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  display: block;
+  margin-bottom: 4rpx;
+}
+
+.chart-card .card-subtitle {
+  font-size: 22rpx;
+  color: #999;
+  display: block;
+  margin-bottom: 16rpx;
+}
+
+.line-chart-canvas,
+.scatter-canvas {
+  width: 100%;
+  height: 360rpx;
+  border-radius: 12rpx;
+  background: #FCFCFC;
+}
+
 .calendar-card,
 .calendar-card,
 .distribution-card,
 .distribution-card,
 .daily-list-card {
 .daily-list-card {
@@ -322,29 +662,61 @@ export default {
   flex-direction: column;
   flex-direction: column;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
+  border-radius: 8rpx;
+}
+
+.calendar-day.has-data {
+  cursor: pointer;
 }
 }
 
 
 .is-today {
 .is-today {
-  background: #FFF0F5;
-  border-radius: 12rpx;
+  border: 2rpx solid #FF6B9D;
+  box-sizing: border-box;
 }
 }
 
 
 .day-num {
 .day-num {
-  font-size: 26rpx;
+  font-size: 24rpx;
   color: #333;
   color: #333;
 }
 }
 
 
-.day-emoji {
-  font-size: 24rpx;
-  margin-top: 4rpx;
+.day-emoji-small {
+  font-size: 20rpx;
+  margin-top: 2rpx;
 }
 }
 
 
-.weather-sunny { background: #FFF8E1; border-radius: 8rpx; }
-.weather-cloudy { background: #F5F5F5; border-radius: 8rpx; }
-.weather-rainy { background: #E3F2FD; border-radius: 8rpx; }
-.weather-stormy { background: #EDE7F6; border-radius: 8rpx; }
-.weather-rainbow { background: #FCE4EC; border-radius: 8rpx; }
+.day-score {
+  font-size: 18rpx;
+  color: #888;
+  margin-top: 2rpx;
+}
 
 
+/* 热力图例 */
+.heatmap-legend {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 12rpx;
+  margin-top: 16rpx;
+}
+
+.legend-label {
+  font-size: 20rpx;
+  color: #999;
+}
+
+.legend-bar {
+  display: flex;
+  gap: 2rpx;
+  border-radius: 6rpx;
+  overflow: hidden;
+}
+
+.legend-step {
+  width: 24rpx;
+  height: 16rpx;
+}
+
+/* ===== 情绪分布 ===== */
 .distribution-list {
 .distribution-list {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
@@ -390,6 +762,7 @@ export default {
   text-align: right;
   text-align: right;
 }
 }
 
 
+/* ===== 每日记录 ===== */
 .daily-item {
 .daily-item {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
@@ -402,17 +775,40 @@ export default {
   font-size: 24rpx;
   font-size: 24rpx;
   color: #999;
   color: #999;
   width: 100rpx;
   width: 100rpx;
+  flex-shrink: 0;
 }
 }
 
 
 .daily-emoji {
 .daily-emoji {
   font-size: 32rpx;
   font-size: 32rpx;
+  width: 40rpx;
+  text-align: center;
+  flex-shrink: 0;
 }
 }
 
 
 .daily-weather {
 .daily-weather {
   font-size: 26rpx;
   font-size: 26rpx;
   color: #666;
   color: #666;
+  width: 120rpx;
+  flex-shrink: 0;
+}
+
+.daily-tags {
+  display: flex;
+  gap: 8rpx;
+  flex-wrap: wrap;
 }
 }
 
 
+.daily-tag {
+  font-size: 20rpx;
+  padding: 4rpx 12rpx;
+  border-radius: 12rpx;
+  color: #fff;
+}
+
+.score-tag { background: #FF6B9D; }
+.type-tag { background: #8B5CF6; }
+.arousal-tag { background: #3B82F6; }
+
 .empty-state {
 .empty-state {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;

+ 6 - 1
cfc-frontend/pages/mind/index.vue

@@ -257,6 +257,9 @@
       @productClick="goProductDetail"
       @productClick="goProductDetail"
       @moreProducts="goMoreProducts" />
       @moreProducts="goMoreProducts" />
 
 
+    <!-- ===== Phase 1.2: 心理告警横幅 ===== -->
+    <PsychCrisisBanner v-if="isLoggedIn" :childId="currentChildId" source="screening" />
+
     <!-- ===== 底部:功能入口 Grid(通用) ===== -->
     <!-- ===== 底部:功能入口 Grid(通用) ===== -->
     <view class="func-section" v-if="sectionVisible('func_entries')">
     <view class="func-section" v-if="sectionVisible('func_entries')">
       <view class="func-grid">
       <view class="func-grid">
@@ -285,11 +288,12 @@ import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
+import PsychCrisisBanner from '../../components/PsychCrisisBanner.vue'
 import { getVisibleSections, getFeaturedArticles, getEmiReport, getDailyTip, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren, getVisibleFamilyMembers, getEnergyOverview } from '../../utils/api.js'
 import { getVisibleSections, getFeaturedArticles, getEmiReport, getDailyTip, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren, getVisibleFamilyMembers, getEnergyOverview } from '../../utils/api.js'
 import config from '../../config.js'
 import config from '../../config.js'
 
 
 export default {
 export default {
-  components: { TabTransition, PageBanner, RadarChart, LoginGuideCard, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, FamilyRelationGraph },
+  components: { TabTransition, PageBanner, RadarChart, LoginGuideCard, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, FamilyRelationGraph, PsychCrisisBanner },
   data() {
   data() {
     return {
     return {
       isLoggedIn: false,
       isLoggedIn: false,
@@ -328,6 +332,7 @@ export default {
         { icon: '\u{1F4DD}', label: '成长档案', needLogin: true, page: '/pages/growth/index' },
         { icon: '\u{1F4DD}', label: '成长档案', needLogin: true, page: '/pages/growth/index' },
         { icon: '\u{2728}', label: '情绪打卡', needLogin: true, page: '/pages/mind-detail/emotion-checkin' },
         { icon: '\u{2728}', label: '情绪打卡', needLogin: true, page: '/pages/mind-detail/emotion-checkin' },
         { icon: '\u{1F4C8}', label: '情绪趋势', needLogin: true, page: '/pages/mind-detail/emotion-trend' },
         { icon: '\u{1F4C8}', label: '情绪趋势', needLogin: true, page: '/pages/mind-detail/emotion-trend' },
+        { icon: '\u{1F9E9}', label: '心理自评', needLogin: true, page: '/pages/mind-detail/screening' },
         { icon: '\u{2637}', label: '传统文化', needLogin: true, page: '/pages/mind-detail/traditional-mirror' },
         { icon: '\u{2637}', label: '传统文化', needLogin: true, page: '/pages/mind-detail/traditional-mirror' },
         { icon: '\u{1F9D8}', label: '疏导工具', needLogin: true, page: '/pages/mind-detail/soothe-toolbox' }
         { icon: '\u{1F9D8}', label: '疏导工具', needLogin: true, page: '/pages/mind-detail/soothe-toolbox' }
       ],
       ],