Browse Source

feat: 新增肠脑轴情绪打卡功能

新增情绪打卡/情绪趋势/传统文化/疏导工具4个页面及导航入口,mind首页增加肠脑情绪关联卡片展示,DatabaseInitializer添加emotion_checkin表自动迁移。

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Xiaogang Liao 4 months ago
parent
commit
6b264e7ae4

+ 24 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -2612,6 +2612,30 @@ log.info("已添加template_id列到tasks表");
             log.info("已添加 big_five_overall_score 列到 dan_assessment_results 表");
         } catch (Exception e) { /* 列已存在,忽略 */ }
 
+        // Migration: 创建 emotion_checkin 表(情绪打卡)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS emotion_checkin (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
+                    "mood_weather VARCHAR(20) NOT NULL COMMENT '天气类比: sunny/cloudy/rainy/stormy/rainbow', " +
+                    "mood_score INT COMMENT '情绪值 1-10', " +
+                    "emotion_tags VARCHAR(200) COMMENT '情绪标签JSON', " +
+                    "emotion_type VARCHAR(50) COMMENT '情绪类型', " +
+                    "stress_level INT COMMENT '压力值 1-10', " +
+                    "photo_url VARCHAR(500) COMMENT '拍照URL', " +
+                    "note TEXT COMMENT '文字记录', " +
+                    "energy_awarded INT DEFAULT 5 COMMENT '本次获得心能量', " +
+                    "checkin_date DATE NOT NULL COMMENT '打卡日期', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "INDEX idx_child_date (child_id, checkin_date), " +
+                    "INDEX idx_child_weather (child_id, mood_weather)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='情绪打卡记录'");
+            log.info("已创建 emotion_checkin 表");
+        } catch (Exception e) {
+            log.warn("创建 emotion_checkin 表失败: {}", e.getMessage());
+        }
+
         log.info("数据库迁移完成");
 
         // ==================== 健康维度 Phase 1: health_dimension_score / health_data_source_record / health_norm_reference ====================

+ 24 - 0
cfc-frontend/pages.json

@@ -145,6 +145,30 @@
         "navigationStyle": "custom"
       }
     },
+    {
+      "path": "pages/mind/emotion-checkin",
+      "style": {
+        "navigationBarTitleText": "情绪打卡"
+      }
+    },
+    {
+      "path": "pages/mind/emotion-trend",
+      "style": {
+        "navigationBarTitleText": "情绪趋势"
+      }
+    },
+    {
+      "path": "pages/mind/traditional-mirror",
+      "style": {
+        "navigationBarTitleText": "传统文化镜"
+      }
+    },
+    {
+      "path": "pages/mind/soothe-toolbox",
+      "style": {
+        "navigationBarTitleText": "疏导工具箱"
+      }
+    },
     {
       "path": "pages/action/index",
       "style": {

+ 485 - 0
cfc-frontend/pages/mind/emotion-checkin.vue

@@ -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">&#x1F525;</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">&#x1F4F7;</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">&#x2764;</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>

+ 441 - 0
cfc-frontend/pages/mind/emotion-trend.vue

@@ -0,0 +1,441 @@
+<template>
+  <view class="trend-container">
+    <!-- 统计概览卡片 -->
+    <view class="stats-overview">
+      <view class="stat-item">
+        <text class="stat-value">{{ stats.totalCheckins || 0 }}</text>
+        <text class="stat-label">总打卡</text>
+      </view>
+      <view class="stat-item">
+        <text class="stat-value">{{ stats.currentStreak || 0 }}</text>
+        <text class="stat-label">连续天数</text>
+      </view>
+      <view class="stat-item">
+        <text class="stat-value">{{ stats.thisMonthCheckins || 0 }}</text>
+        <text class="stat-label">本月打卡</text>
+      </view>
+      <view class="stat-item">
+        <text class="stat-value">{{ stats.totalEnergy || 0 }}</text>
+        <text class="stat-label">心能量</text>
+      </view>
+    </view>
+
+    <!-- 周期切换 -->
+    <view class="period-tabs">
+      <view
+        :class="['period-tab', period === 'week' ? 'period-active' : '']"
+        @click="switchPeriod('week')">
+        <text>周</text>
+      </view>
+      <view
+        :class="['period-tab', period === 'month' ? 'period-active' : '']"
+        @click="switchPeriod('month')">
+        <text>月</text>
+      </view>
+    </view>
+
+    <!-- 情绪日历 -->
+    <view class="calendar-card">
+      <text class="card-title">情绪日历</text>
+      <view class="calendar-grid">
+        <view class="calendar-weekday" v-for="wd in weekdays" :key="wd">
+          <text>{{ wd }}</text>
+        </view>
+        <view
+          v-for="(day, idx) in calendarDays"
+          :key="idx"
+          :class="['calendar-day', 'weather-' + (day.weather || 'none'), day.isToday ? 'is-today' : '']">
+          <text class="day-num">{{ day.dayNum }}</text>
+          <text v-if="day.weather" class="day-emoji">{{ weatherEmoji(day.weather) }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 情绪分布 -->
+    <view class="distribution-card">
+      <text class="card-title">情绪分布</text>
+      <view class="distribution-list">
+        <view class="dist-item" v-for="dist in distributionData" :key="dist.weather">
+          <text class="dist-emoji">{{ dist.icon }}</text>
+          <text class="dist-label">{{ dist.label }}</text>
+          <view class="dist-bar-track">
+            <view class="dist-bar-fill" :style="{ width: dist.percent + '%', background: dist.color }"></view>
+          </view>
+          <text class="dist-count">{{ dist.count }}次</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 按天详情 -->
+    <view class="daily-list-card" v-if="dailyData.length > 0">
+      <text class="card-title">每日记录</text>
+      <view class="daily-item" v-for="(day, idx) in dailyData" :key="idx">
+        <text class="daily-date">{{ formatDate(day.date) }}</text>
+        <text class="daily-emoji">{{ weatherEmoji(day.weather) }}</text>
+        <text class="daily-weather">{{ weatherLabel(day.weather) }}</text>
+      </view>
+    </view>
+
+    <view class="empty-state" v-if="!stats || stats.totalCheckins === 0">
+      <text class="empty-icon">&#x1F4AD;</text>
+      <text class="empty-text">还没有情绪打卡记录</text>
+      <button class="empty-btn" @click="goCheckin">去打卡</button>
+    </view>
+  </view>
+</template>
+
+<script>
+import config from '../../config'
+
+export default {
+  data() {
+    return {
+      period: 'week',
+      stats: null,
+      trend: null,
+      weekdays: ['日', '一', '二', '三', '四', '五', '六'],
+      weatherConfig: {
+        sunny: { icon: '\u2600\uFE0F', label: '心情很好', color: '#FFB347' },
+        cloudy: { icon: '\u26C5', label: '一般般', color: '#A0A0A0' },
+        rainy: { icon: '\uD83C\uDF27\uFE0F', label: '有点难过', color: '#6B9BD2' },
+        stormy: { icon: '\u26A1', label: '很烦躁', color: '#8B5CF6' },
+        rainbow: { icon: '\uD83C\uDF08', label: '特别开心', color: '#FF6B9D' }
+      }
+    }
+  },
+
+  computed: {
+    calendarDays() {
+      const days = []
+      const now = new Date()
+      const year = now.getFullYear()
+      const month = now.getMonth()
+      const firstDay = new Date(year, month, 1).getDay()
+      const daysInMonth = new Date(year, month + 1, 0).getDate()
+      const today = now.getDate()
+
+      // Build weather map from daily data
+      const weatherMap = {}
+      if (this.trend && this.trend.dailyData) {
+        for (const d of this.trend.dailyData) {
+          if (d.date) {
+            const dateObj = new Date(d.date)
+            weatherMap[dateObj.getDate()] = d.weather
+          }
+        }
+      }
+
+      // Empty leading cells
+      for (let i = 0; i < firstDay; i++) {
+        days.push({ dayNum: '', weather: null, isToday: false })
+      }
+
+      for (let d = 1; d <= daysInMonth; d++) {
+        days.push({
+          dayNum: d,
+          weather: weatherMap[d] || null,
+          isToday: d === today
+        })
+      }
+
+      return days
+    },
+
+    distributionData() {
+      const dist = this.trend && this.trend.weatherDistribution
+      if (!dist) return []
+      const total = Object.values(dist).reduce((a, b) => a + b, 0)
+      if (total === 0) return []
+
+      return Object.keys(dist).map(w => {
+        const cfg = this.weatherConfig[w] || { icon: '?', label: w, color: '#ccc' }
+        return {
+          weather: w,
+          icon: cfg.icon,
+          label: cfg.label,
+          color: cfg.color,
+          count: dist[w],
+          percent: Math.round(dist[w] / total * 100)
+        }
+      })
+    },
+
+    dailyData() {
+      return this.trend && this.trend.dailyData || []
+    }
+  },
+
+  onLoad() {
+    this.loadData()
+  },
+
+  methods: {
+    loadData() {
+      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) {
+            this.stats = res.data.data
+          }
+        }
+      })
+
+      uni.request({
+        url: config.API_BASE_URL + '/api/mind/checkin/trend',
+        method: 'POST',
+        data: { childId, period: this.period },
+        success: (res) => {
+          if (res.data && res.data.code === 200) {
+            this.trend = res.data.data
+          }
+        }
+      })
+    },
+
+    switchPeriod(p) {
+      this.period = p
+      this.loadData()
+    },
+
+    weatherEmoji(weather) {
+      const cfg = this.weatherConfig[weather]
+      return cfg ? cfg.icon : ''
+    },
+
+    weatherLabel(weather) {
+      const cfg = this.weatherConfig[weather]
+      return cfg ? cfg.label : weather
+    },
+
+    formatDate(dateStr) {
+      if (!dateStr) return ''
+      const d = new Date(dateStr)
+      return (d.getMonth() + 1) + '/' + d.getDate()
+    },
+
+    goCheckin() {
+      uni.navigateTo({ url: '/pages/mind/emotion-checkin' })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.trend-container {
+  min-height: 100vh;
+  background: #F8F8F8;
+  padding-bottom: 40rpx;
+}
+
+.stats-overview {
+  display: flex;
+  margin: 24rpx 32rpx;
+  background: linear-gradient(135deg, #FF6B9D, #FF8CB3);
+  border-radius: 20rpx;
+  padding: 28rpx 16rpx;
+}
+
+.stat-item {
+  flex: 1;
+  text-align: center;
+}
+
+.stat-value {
+  font-size: 36rpx;
+  font-weight: 700;
+  color: #fff;
+  display: block;
+}
+
+.stat-label {
+  font-size: 22rpx;
+  color: rgba(255, 255, 255, 0.8);
+  margin-top: 6rpx;
+  display: block;
+}
+
+.period-tabs {
+  display: flex;
+  margin: 0 32rpx;
+  background: #fff;
+  border-radius: 12rpx;
+  overflow: hidden;
+}
+
+.period-tab {
+  flex: 1;
+  text-align: center;
+  padding: 16rpx 0;
+  font-size: 26rpx;
+  color: #666;
+}
+
+.period-active {
+  background: #FF6B9D;
+  color: #fff;
+  font-weight: 600;
+}
+
+.calendar-card,
+.distribution-card,
+.daily-list-card {
+  margin: 24rpx 32rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.card-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  display: block;
+  margin-bottom: 20rpx;
+}
+
+.calendar-grid {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+.calendar-weekday {
+  width: 14.28%;
+  text-align: center;
+  font-size: 22rpx;
+  color: #999;
+  padding: 10rpx 0;
+}
+
+.calendar-day {
+  width: 14.28%;
+  text-align: center;
+  padding: 8rpx 0;
+  min-height: 80rpx;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.is-today {
+  background: #FFF0F5;
+  border-radius: 12rpx;
+}
+
+.day-num {
+  font-size: 26rpx;
+  color: #333;
+}
+
+.day-emoji {
+  font-size: 24rpx;
+  margin-top: 4rpx;
+}
+
+.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; }
+
+.distribution-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+
+.dist-item {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+}
+
+.dist-emoji {
+  font-size: 32rpx;
+  width: 48rpx;
+  text-align: center;
+}
+
+.dist-label {
+  font-size: 24rpx;
+  color: #666;
+  width: 120rpx;
+}
+
+.dist-bar-track {
+  flex: 1;
+  height: 20rpx;
+  background: #F0F0F0;
+  border-radius: 10rpx;
+  overflow: hidden;
+}
+
+.dist-bar-fill {
+  height: 100%;
+  border-radius: 10rpx;
+  transition: width 0.3s;
+}
+
+.dist-count {
+  font-size: 24rpx;
+  color: #999;
+  width: 60rpx;
+  text-align: right;
+}
+
+.daily-item {
+  display: flex;
+  align-items: center;
+  padding: 16rpx 0;
+  border-bottom: 2rpx solid #F5F5F5;
+  gap: 16rpx;
+}
+
+.daily-date {
+  font-size: 24rpx;
+  color: #999;
+  width: 100rpx;
+}
+
+.daily-emoji {
+  font-size: 32rpx;
+}
+
+.daily-weather {
+  font-size: 26rpx;
+  color: #666;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 120rpx 32rpx;
+}
+
+.empty-icon {
+  font-size: 80rpx;
+  margin-bottom: 20rpx;
+}
+
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+  margin-bottom: 40rpx;
+}
+
+.empty-btn {
+  background: #FF6B9D;
+  color: #fff;
+  padding: 16rpx 60rpx;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+}
+</style>

+ 144 - 2
cfc-frontend/pages/mind/index.vue

@@ -146,6 +146,28 @@
       </view>
     </view>
 
+    <!-- 肠脑轴情绪卡片 -->
+    <view class="section" v-if="gutIndicators && gutIndicators.length > 0">
+      <view class="section-header">
+        <text class="section-title">&#x1F9E0; 肠脑情绪关联</text>
+      </view>
+      <view class="gut-insight-text" v-if="gutInsight">
+        <text>{{ gutInsight }}</text>
+      </view>
+      <view class="gut-indicator-list">
+        <view class="gut-indicator-item" v-for="ind in gutIndicators" :key="ind.id">
+          <view class="gut-indicator-left">
+            <text class="gut-indicator-name">{{ ind.indicatorName || ind.name }}</text>
+          </view>
+          <view class="gut-indicator-bar-track">
+            <view class="gut-indicator-bar-fill" :style="{ width: (ind.value || 0) + '%', background: indicatorColor(ind.status) }"></view>
+          </view>
+          <text class="gut-indicator-value">{{ ind.value }}{{ ind.unit || '%' }}</text>
+          <text :class="['gut-indicator-status', 'status-' + (ind.status || 'normal')]">{{ statusLabel(ind.status) }}</text>
+        </view>
+      </view>
+    </view>
+
     <!-- 推荐阅读 -->
     <view class="section" v-if="sectionVisible('recommended_reading')">
       <view class="section-header">
@@ -209,7 +231,6 @@
           <text class="score-big-label">认知总分</text>
           <text class="score-big-value">{{ assessmentData.overallScore || 0 }}</text>
         </view>
-        </view>
         <view class="score-big-cell">
           <text class="score-big-label">上次测评</text>
           <text class="score-big-value small">{{ assessmentData.assessmentDate || '-' }}</text>
@@ -451,10 +472,18 @@ export default {
         { icon: '\u{1F3AE}', label: '小游戏', needLogin: true, page: '/pages/games/list' },
         { icon: '\u{1F4CA}', label: '测评报告', needLogin: true, page: '/pages/mind/emotion-report' },
         { icon: '\u{1F9EA}', label: '训练中心', needLogin: true, page: '/pages/mind/training' },
-        { 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/emotion-checkin' },
+        { icon: '\u{1F4C8}', label: '情绪趋势', needLogin: true, page: '/pages/mind/emotion-trend' },
+        { icon: '\u{2637}', label: '传统文化', needLogin: true, page: '/pages/mind/traditional-mirror' },
+        { icon: '\u{1F9D8}', label: '疏导工具', needLogin: true, page: '/pages/mind/soothe-toolbox' }
       ],
       articles: [],
 
+      // 肠脑轴数据
+      gutIndicators: null,
+      gutInsight: '',
+
       // 五维组件数据
       userName: '',
       children: [],
@@ -552,6 +581,7 @@ export default {
       this.loadEmiData()
       this.loadAssessmentData()
       this.loadReadingStats()
+      this.loadGutData()
       this.loadFamilyCognitiveData()
       this.loadEnergyData()
     }
@@ -698,6 +728,52 @@ export default {
       }).catch(function() {})
     },
 
+    // ===== 肠脑轴情绪指标 =====
+    loadGutData: function() {
+      var self = this
+      var childId = self.currentChildId
+      if (!childId) return
+      var token = uni.getStorageSync('token')
+      // 获取神经递质指标
+      uni.request({
+        url: config.API_BASE_URL + '/api/mind/neurotransmitters',
+        method: 'POST',
+        header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+        data: { childId: childId },
+        success: function(r) {
+          if (r.statusCode === 200 && r.data && r.data.code === 200) {
+            var list = r.data.data
+            if (Array.isArray(list) && list.length > 0) {
+              self.gutIndicators = list.slice(0, 4)
+            }
+          }
+        }
+      })
+      // 获取情绪洞察摘要
+      uni.request({
+        url: config.API_BASE_URL + '/api/mind/gut-emotion-insight',
+        method: 'POST',
+        header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
+        data: { childId: childId },
+        success: function(r) {
+          if (r.statusCode === 200 && r.data && r.data.code === 200 && r.data.data) {
+            self.gutInsight = r.data.data.summary || ''
+          }
+        }
+      })
+    },
+    indicatorColor: function(status) {
+      if (status === 'abnormal' || status === '偏高' || status === '偏低') return '#EF4444'
+      if (status === 'warning' || status === '偏高' || status === '偏低') return '#F59E0B'
+      return '#10B981'
+    },
+    statusLabel: function(status) {
+      if (status === 'normal') return '正常'
+      if (status === 'abnormal') return '异常'
+      if (status === 'warning') return '警讯'
+      return status || '正常'
+    },
+
     // ===== 文章 =====
     loadFeaturedArticles: function() {
       var self = this
@@ -1617,4 +1693,70 @@ export default {
   color: #ccc;
   margin-left: 12rpx;
 }
+
+/* ===== 肠脑轴 ===== */
+.gut-insight-text {
+  font-size: 24rpx;
+  color: #666;
+  margin-bottom: 16rpx;
+  padding: 12rpx 16rpx;
+  background: #F0FFF4;
+  border-radius: 12rpx;
+  line-height: 1.6;
+}
+.gut-indicator-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+}
+.gut-indicator-item {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+}
+.gut-indicator-left {
+  width: 80rpx;
+  flex-shrink: 0;
+}
+.gut-indicator-name {
+  font-size: 22rpx;
+  color: #666;
+}
+.gut-indicator-bar-track {
+  flex: 1;
+  height: 16rpx;
+  background: #F0F0F0;
+  border-radius: 8rpx;
+  overflow: hidden;
+}
+.gut-indicator-bar-fill {
+  height: 100%;
+  border-radius: 8rpx;
+  transition: width 0.3s;
+}
+.gut-indicator-value {
+  font-size: 22rpx;
+  color: #666;
+  width: 60rpx;
+  text-align: right;
+}
+.gut-indicator-status {
+  font-size: 20rpx;
+  padding: 2rpx 12rpx;
+  border-radius: 10rpx;
+  width: 60rpx;
+  text-align: center;
+}
+.status-normal {
+  background: #D1FAE5;
+  color: #059669;
+}
+.status-abnormal {
+  background: #FEE2E2;
+  color: #DC2626;
+}
+.status-warning {
+  background: #FEF3C7;
+  color: #D97706;
+}
 </style>

+ 396 - 0
cfc-frontend/pages/mind/soothe-toolbox.vue

@@ -0,0 +1,396 @@
+<template>
+  <view class="toolbox-container">
+    <!-- ===== 4-7-8 呼吸练习 ===== -->
+    <view class="tool-section">
+      <view class="section-header">
+        <text class="section-emoji">&#x1F9D8;</text>
+        <text class="section-title">4-7-8 呼吸练习</text>
+      </view>
+      <text class="section-desc">用深呼吸让自己平静下来</text>
+
+      <view class="breathing-area" @click="toggleBreathing">
+        <view :class="['breath-circle', breathPhase]">
+          <text class="breath-text">{{ breathInstruction }}</text>
+        </view>
+      </view>
+      <text class="breath-hint" v-if="!breathing">点击开始呼吸练习</text>
+      <text class="breath-count" v-if="breathing">第 {{ breathRound }} 轮</text>
+    </view>
+
+    <!-- ===== 正念练习 ===== -->
+    <view class="tool-section">
+      <view class="section-header">
+        <text class="section-emoji">&#x1F4A1;</text>
+        <text class="section-title">正念练习</text>
+      </view>
+      <view class="mindfulness-cards">
+        <view
+          :class="['mind-card', activeMindCard === idx ? 'mind-card-active' : '']"
+          v-for="(item, idx) in mindfulnessItems"
+          :key="idx"
+          @click="activeMindCard = idx">
+          <text class="mind-card-title">{{ item.title }}</text>
+          <text class="mind-card-desc">{{ item.desc }}</text>
+        </view>
+      </view>
+      <view class="mindfulness-content" v-if="activeMindCard !== -1">
+        <text class="mindfulness-text">{{ mindfulnessItems[activeMindCard].content }}</text>
+      </view>
+    </view>
+
+    <!-- ===== 情绪涂鸦 ===== -->
+    <view class="tool-section">
+      <view class="section-header">
+        <text class="section-emoji">&#x1F3A8;</text>
+        <text class="section-title">情绪涂鸦</text>
+      </view>
+      <text class="section-desc">在纸上自由画下你现在的情绪</text>
+      <view class="doodle-suggestions">
+        <view class="doodle-item" v-for="(item, idx) in doodlePrompts" :key="idx">
+          <text class="doodle-icon">{{ item.icon }}</text>
+          <text class="doodle-text">{{ item.text }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- ===== 运动释压 ===== -->
+    <view class="tool-section">
+      <view class="section-header">
+        <text class="section-emoji">&#x1F3CB;</text>
+        <text class="section-title">运动释压</text>
+      </view>
+      <text class="section-desc">简单的伸展动作,释放身体紧张</text>
+      <view class="exercise-list">
+        <view class="exercise-item" v-for="(ex, idx) in exercises" :key="idx">
+          <text class="exercise-name">{{ ex.name }}</text>
+          <text class="exercise-desc">{{ ex.desc }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 底部提示 -->
+    <view class="disclaimer">
+      <text class="disclaimer-text">以上内容仅供参考,不能替代专业心理咨询或医疗建议。如有需要,请寻求专业帮助。</text>
+    </view>
+  </view>
+</template>
+
+<script>
+export default {
+  data() {
+    return {
+      breathing: false,
+      breathPhase: 'inhal',
+      breathInstruction: '吸气',
+      breathRound: 0,
+      breathTimer: null,
+      activeMindCard: -1,
+      mindfulnessItems: [
+        {
+          title: '5-4-3-2-1 感官练习',
+          desc: '快速回归当下',
+          content: '找一个舒服的姿势坐下,深呼吸三次。然后:\n\n看:说出你看到的 5 样东西\n听:说出你听到的 4 种声音\n触:感受你身体的 3 处触觉\n闻:闻到的 2 种气味\n味:品尝到的 1 种味道\n\n这个练习能帮你快速从焦虑中回到当下。'
+        },
+        {
+          title: '身体扫描',
+          desc: '放松全身',
+          content: '闭上眼睛,把注意力带到脚趾。感受脚趾的温度和触感。\n\n慢慢向上移动注意力:脚掌 → 脚踝 → 小腿 → 膝盖 → 大腿 → 腹部 → 胸部 → 手指 → 手臂 → 肩膀 → 脖子 → 面部 → 头顶。\n\n在每个部位停留 3-5 秒,感受那里的感觉。'
+        },
+        {
+          title: '感恩三件事',
+          desc: '培养积极心态',
+          content: '想一想今天让你感到感恩的三件事。可以是很小的事:\n\n1. 今天吃到了一顿美味的饭\n2. 有人对你说了一句温暖的话\n3. 今天的天气很好\n4. 你学会了一样新东西\n5. 帮助了别人\n\n在心里默默感谢这些美好。'
+        }
+      ],
+      doodlePrompts: [
+        { icon: '\u{1F308}', text: '画出你今天的情绪彩虹' },
+        { icon: '\u{1F33F}', text: '画一棵代表你内心的小树' },
+        { icon: '\u{1F30A}', text: '画出心情波浪' },
+        { icon: '\u{1F31F}', text: '画出你心中的星星' }
+      ],
+      exercises: [
+        { name: '耸肩放松', desc: '双肩用力向上耸起(靠近耳朵),保持 5 秒,然后突然放松。重复 5 次。' },
+        { name: '颈部伸展', desc: '慢慢将头向右侧倾斜,右手轻压头部,保持 15 秒。换左侧重复。' },
+        { name: '猫牛式', desc: '双手双膝着地,吸气时塌腰抬头(牛式),呼气时弓背低头(猫式)。重复 10 次。' },
+        { name: '蝴蝶式', desc: '坐姿,双脚掌相对,双手握住脚踝,膝盖向两侧打开轻轻上下抖动,像蝴蝶扇动翅膀。' }
+      ]
+    }
+  },
+
+  methods: {
+    toggleBreathing() {
+      if (this.breathing) {
+        this.stopBreathing()
+      } else {
+        this.startBreathing()
+      }
+    },
+
+    startBreathing() {
+      this.breathing = true
+      this.breathRound = 1
+      this.doBreathPhase('inhal', 4)
+    },
+
+    doBreathPhase(phase, seconds) {
+      var self = this
+      if (!self.breathing) return
+
+      self.breathPhase = phase
+      var instructions = {
+        inhal: '吸气...',
+        hold: '屏息...',
+        exhale: '呼气...'
+      }
+      self.breathInstruction = instructions[phase] || ''
+
+      self.breathTimer = setTimeout(function() {
+        if (!self.breathing) return
+        if (phase === 'inhal') {
+          self.doBreathPhase('hold', 7)
+        } else if (phase === 'hold') {
+          self.doBreathPhase('exhale', 8)
+        } else {
+          // exhale done, start next round
+          var nextRound = self.breathRound + 1
+          self.breathRound = nextRound
+          if (nextRound <= 5) {
+            self.doBreathPhase('inhal', 4)
+          } else {
+            self.stopBreathing()
+            uni.showToast({ title: '完成一组呼吸', icon: 'none' })
+          }
+        }
+      }, seconds * 1000)
+    },
+
+    stopBreathing() {
+      this.breathing = false
+      if (this.breathTimer) {
+        clearTimeout(this.breathTimer)
+        this.breathTimer = null
+      }
+      this.breathPhase = 'inhal'
+      this.breathInstruction = '吸气'
+      this.breathRound = 0
+    }
+  },
+
+  onUnload() {
+    this.stopBreathing()
+  }
+}
+</script>
+
+<style scoped>
+.toolbox-container {
+  min-height: 100vh;
+  background: linear-gradient(180deg, #F0FDF4 0%, #FFFFFF 50%, #FFF7ED 100%);
+  padding-bottom: 40rpx;
+}
+
+.tool-section {
+  margin: 24rpx 32rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.section-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8rpx;
+}
+
+.section-emoji {
+  font-size: 36rpx;
+  margin-right: 12rpx;
+}
+
+.section-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.section-desc {
+  font-size: 24rpx;
+  color: #999;
+  display: block;
+  margin-bottom: 20rpx;
+}
+
+/* ===== 呼吸练习 ===== */
+.breathing-area {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 40rpx 0;
+}
+
+.breath-circle {
+  width: 280rpx;
+  height: 280rpx;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #A7F3D0, #6EE7B7);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: all 0.3s;
+  box-shadow: 0 8rpx 40rpx rgba(16, 185, 129, 0.2);
+}
+
+.breath-circle.inhal {
+  animation: breatheIn 4s ease-in-out;
+}
+
+.breath-circle.hold {
+  background: linear-gradient(135deg, #FDE68A, #F59E0B);
+  box-shadow: 0 8rpx 40rpx rgba(245, 158, 11, 0.3);
+}
+
+.breath-circle.exhale {
+  animation: breatheOut 8s ease-in-out;
+}
+
+.breath-text {
+  font-size: 36rpx;
+  font-weight: 600;
+  color: #fff;
+}
+
+.breath-hint {
+  display: block;
+  text-align: center;
+  font-size: 24rpx;
+  color: #999;
+}
+
+.breath-count {
+  display: block;
+  text-align: center;
+  font-size: 24rpx;
+  color: #10B981;
+  margin-top: 8rpx;
+}
+
+/* ===== 正念练习 ===== */
+.mindfulness-cards {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+
+.mind-card {
+  padding: 20rpx;
+  background: #F9F9F9;
+  border-radius: 12rpx;
+  border: 2rpx solid transparent;
+}
+
+.mind-card-active {
+  background: #F0FDF4;
+  border-color: #10B981;
+}
+
+.mind-card-title {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #333;
+  display: block;
+}
+
+.mind-card-desc {
+  font-size: 22rpx;
+  color: #999;
+  display: block;
+  margin-top: 4rpx;
+}
+
+.mindfulness-content {
+  margin-top: 16rpx;
+  padding: 20rpx;
+  background: #F0FDF4;
+  border-radius: 12rpx;
+  border-left: 6rpx solid #10B981;
+}
+
+.mindfulness-text {
+  font-size: 24rpx;
+  color: #555;
+  line-height: 1.8;
+  white-space: pre-wrap;
+}
+
+/* ===== 情绪涂鸦 ===== */
+.doodle-suggestions {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16rpx;
+}
+
+.doodle-item {
+  width: calc(50% - 8rpx);
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  padding: 20rpx;
+  background: #FFFBEB;
+  border-radius: 12rpx;
+  box-sizing: border-box;
+}
+
+.doodle-icon {
+  font-size: 36rpx;
+}
+
+.doodle-text {
+  font-size: 24rpx;
+  color: #666;
+  flex: 1;
+}
+
+/* ===== 运动释压 ===== */
+.exercise-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+
+.exercise-item {
+  padding: 20rpx;
+  background: #F0F9FF;
+  border-radius: 12rpx;
+  border-left: 6rpx solid #3B82F6;
+}
+
+.exercise-name {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #2563EB;
+  display: block;
+  margin-bottom: 6rpx;
+}
+
+.exercise-desc {
+  font-size: 24rpx;
+  color: #666;
+  line-height: 1.6;
+  display: block;
+}
+
+/* ===== 免责声明 ===== */
+.disclaimer {
+  margin: 32rpx;
+  padding: 20rpx;
+  background: #FEF3C7;
+  border-radius: 12rpx;
+}
+
+.disclaimer-text {
+  font-size: 22rpx;
+  color: #92400E;
+  line-height: 1.6;
+}
+</style>

+ 398 - 0
cfc-frontend/pages/mind/traditional-mirror.vue

@@ -0,0 +1,398 @@
+<template>
+  <view class="mirror-container">
+    <!-- 加载状态 -->
+    <view class="loading-state" v-if="loading">
+      <text>加载中...</text>
+    </view>
+
+    <template v-if="!loading && dataLoaded">
+      <!-- 八字四柱 -->
+      <view class="mirror-section">
+        <view class="section-header">
+          <text class="section-icon">&#x2637;</text>
+          <text class="section-title">八字四柱</text>
+        </view>
+        <view class="bazi-grid" v-if="eightChars">
+          <view class="bazi-column" v-for="(pillar, key) in eightChars" :key="key">
+            <text class="bazi-pillar-label">{{ pillarLabel(key) }}</text>
+            <text class="bazi-pillar-value">{{ pillar }}</text>
+          </view>
+        </view>
+        <view class="empty-data" v-else>
+          <text>暂未设置出生信息,无法排八字</text>
+        </view>
+      </view>
+
+      <!-- 五行能量 -->
+      <view class="mirror-section">
+        <view class="section-header">
+          <text class="section-icon">&#x267E;</text>
+          <text class="section-title">五行能量</text>
+        </view>
+        <view class="wuxing-list" v-if="wuxingData">
+          <view class="wuxing-item" v-for="wx in wuxingData" :key="wx.key">
+            <text class="wuxing-label">{{ wx.label }}</text>
+            <view class="wuxing-bar-track">
+              <view class="wuxing-bar-fill" :style="{ width: wx.value + '%', background: wx.color }"></view>
+            </view>
+            <text class="wuxing-value">{{ wx.value }}%</text>
+          </view>
+        </view>
+
+        <!-- 五行解读 -->
+        <view class="reading-card" v-if="wuxingReading">
+          <text class="reading-label">五行解读</text>
+          <text class="reading-text" v-if="wuxingReading.personalTrait">{{ wuxingReading.personalTrait }}</text>
+          <text class="reading-text" v-if="wuxingReading.mindGrowthAdvice">{{ wuxingReading.mindGrowthAdvice }}</text>
+        </view>
+      </view>
+
+      <!-- 太阳星座 -->
+      <view class="mirror-section" v-if="zodiac">
+        <view class="section-header">
+          <text class="section-icon">&#x2600;</text>
+          <text class="section-title">太阳星座</text>
+        </view>
+        <view class="zodiac-display">
+          <text class="zodiac-name">{{ zodiac }}</text>
+        </view>
+      </view>
+
+      <!-- 生命灵数 -->
+      <view class="mirror-section" v-if="numSoulData">
+        <view class="section-header">
+          <text class="section-icon">&#x1F52E;</text>
+          <text class="section-title">生命灵数</text>
+        </view>
+        <view class="numsoul-display">
+          <view class="numsoul-number" :style="{ background: numSoulData.colorHex || '#FF6B9D' }">
+            <text class="numsoul-num">{{ lifeNumber }}</text>
+          </view>
+          <view class="numsoul-info">
+            <text class="numsoul-title">{{ numSoulData.title || '' }}</text>
+            <text class="numsoul-keywords" v-if="numSoulData.keywords">{{ numSoulData.keywords }}</text>
+            <text class="numsoul-advice" v-if="numSoulData.mindGrowthAdvice">{{ numSoulData.mindGrowthAdvice }}</text>
+          </view>
+        </view>
+      </view>
+    </template>
+
+    <!-- 无数据 -->
+    <view class="empty-state" v-if="!loading && !dataLoaded">
+      <text class="empty-icon">&#x1F4AD;</text>
+      <text class="empty-text">暂无传统文化数据</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import config from '../../config'
+
+export default {
+  data() {
+    return {
+      loading: true,
+      dataLoaded: false,
+      eightChars: null,
+      wuxingElements: null,
+      zodiac: '',
+      lifeNumber: null,
+      wuxingReading: null,
+      numSoulData: null,
+      wuxingConfig: {
+        wood: { label: '木', color: '#10B981' },
+        fire: { label: '火', color: '#FF6B9D' },
+        earth: { label: '土', color: '#FF8C42' },
+        metal: { label: '金', color: '#6366F1' },
+        water: { label: '水', color: '#3B82F6' }
+      },
+      pillarLabels: { year: '年柱', month: '月柱', day: '日柱', hour: '时柱' }
+    }
+  },
+
+  computed: {
+    wuxingData() {
+      if (!this.wuxingElements) return null
+      var keys = ['wood', 'fire', 'earth', 'metal', 'water']
+      var result = []
+      for (var i = 0; i < keys.length; i++) {
+        var k = keys[i]
+        if (this.wuxingElements[k] !== undefined) {
+          result.push({
+            key: k,
+            label: this.wuxingConfig[k].label,
+            color: this.wuxingConfig[k].color,
+            value: this.wuxingElements[k]
+          })
+        }
+      }
+      return result.sort(function(a, b) { return b.value - a.value })
+    }
+  },
+
+  onLoad() {
+    this.loadData()
+  },
+
+  methods: {
+    loadData() {
+      var self = this
+      var app = getApp()
+      var memberId = this.$mp.query.memberId || app.globalData.currentChildId
+      if (!memberId) {
+        // Try to get from storage
+        memberId = uni.getStorageSync('currentChildId')
+      }
+      if (!memberId) {
+        self.loading = false
+        return
+      }
+
+      uni.request({
+        url: config.API_BASE_URL + '/api/mind/traditional/mirror',
+        method: 'POST',
+        data: { memberId: memberId, memberType: 'child' },
+        success: function(res) {
+          self.loading = false
+          if (res.data && res.data.code === 200 && res.data.data) {
+            var d = res.data.data
+            self.eightChars = d.eightCharacters || null
+            self.wuxingElements = d.wuxingElements || null
+            self.zodiac = d.zodiac || ''
+            self.lifeNumber = d.lifeNumber || null
+            self.wuxingReading = d.wuxingReading || null
+            self.numSoulData = d.numSoul || null
+            self.dataLoaded = !!(self.eightChars || self.wuxingElements || self.zodiac || self.numSoulData)
+          } else {
+            self.dataLoaded = false
+          }
+        },
+        fail: function() {
+          self.loading = false
+        }
+      })
+    },
+
+    pillarLabel(key) {
+      return this.pillarLabels[key] || key
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mirror-container {
+  min-height: 100vh;
+  background: #F8F6F0;
+  padding-bottom: 40rpx;
+}
+
+.loading-state {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 0;
+  color: #999;
+  font-size: 28rpx;
+}
+
+.mirror-section {
+  margin: 24rpx 32rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+}
+
+.section-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20rpx;
+}
+
+.section-icon {
+  font-size: 36rpx;
+  margin-right: 12rpx;
+}
+
+.section-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+
+.bazi-grid {
+  display: flex;
+  justify-content: space-around;
+  padding: 16rpx 0;
+}
+
+.bazi-column {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.bazi-pillar-label {
+  font-size: 22rpx;
+  color: #999;
+  margin-bottom: 8rpx;
+}
+
+.bazi-pillar-value {
+  font-size: 36rpx;
+  font-weight: 700;
+  color: #8B5CF6;
+  background: #F5F0FF;
+  padding: 12rpx 16rpx;
+  border-radius: 12rpx;
+}
+
+.wuxing-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+
+.wuxing-item {
+  display: flex;
+  align-items: center;
+  gap: 16rpx;
+}
+
+.wuxing-label {
+  font-size: 28rpx;
+  font-weight: 600;
+  width: 48rpx;
+  text-align: center;
+}
+
+.wuxing-bar-track {
+  flex: 1;
+  height: 24rpx;
+  background: #F0F0F0;
+  border-radius: 12rpx;
+  overflow: hidden;
+}
+
+.wuxing-bar-fill {
+  height: 100%;
+  border-radius: 12rpx;
+}
+
+.wuxing-value {
+  font-size: 24rpx;
+  color: #666;
+  width: 60rpx;
+  text-align: right;
+}
+
+.reading-card {
+  margin-top: 20rpx;
+  padding: 16rpx;
+  background: #FFFBEB;
+  border-radius: 12rpx;
+  border-left: 6rpx solid #F59E0B;
+}
+
+.reading-label {
+  font-size: 24rpx;
+  font-weight: 600;
+  color: #D97706;
+  display: block;
+  margin-bottom: 8rpx;
+}
+
+.reading-text {
+  font-size: 24rpx;
+  color: #666;
+  line-height: 1.6;
+  display: block;
+  margin-top: 8rpx;
+}
+
+.zodiac-display {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 20rpx;
+}
+
+.zodiac-name {
+  font-size: 40rpx;
+  font-weight: 700;
+  color: #F59E0B;
+}
+
+.numsoul-display {
+  display: flex;
+  gap: 24rpx;
+  align-items: center;
+}
+
+.numsoul-number {
+  width: 100rpx;
+  height: 100rpx;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.numsoul-num {
+  font-size: 44rpx;
+  font-weight: 700;
+  color: #fff;
+}
+
+.numsoul-info {
+  flex: 1;
+}
+
+.numsoul-title {
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #333;
+  display: block;
+}
+
+.numsoul-keywords {
+  font-size: 24rpx;
+  color: #666;
+  display: block;
+  margin-top: 6rpx;
+}
+
+.numsoul-advice {
+  font-size: 24rpx;
+  color: #8B5CF6;
+  display: block;
+  margin-top: 8rpx;
+  line-height: 1.5;
+}
+
+.empty-data {
+  padding: 20rpx;
+  text-align: center;
+  color: #999;
+  font-size: 24rpx;
+}
+
+.empty-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 120rpx 32rpx;
+}
+
+.empty-icon {
+  font-size: 80rpx;
+  margin-bottom: 20rpx;
+}
+
+.empty-text {
+  font-size: 28rpx;
+  color: #999;
+}
+</style>