Bladeren bron

feat(mind): 添加家庭天盘功能到心页面

- 新增 FamilyTianpanCard.vue 组件,展示今日运势/今日心情/吉位
- 在 pages/mind/index.vue 嵌入家庭天盘模块
- 使用火元素配色 (#FF6B9D) 和 Claymorphism 卡片风格
- 添加占位数据逻辑,准备对接后端接口
- 微信小程序兼容:无可选链、无 CSS Grid、flexbox 布局
User 2 maanden geleden
bovenliggende
commit
a9403a75e3
2 gewijzigde bestanden met toevoegingen van 573 en 2 verwijderingen
  1. 554 0
      cfc-frontend/components/FamilyTianpanCard.vue
  2. 19 2
      cfc-frontend/pages/mind/index.vue

+ 554 - 0
cfc-frontend/components/FamilyTianpanCard.vue

@@ -0,0 +1,554 @@
+<template>
+  <!-- 家庭天盘卡片 — 心维度专属 (#FF6B9D 火) -->
+  <view class="tianpan-card" v-if="visible">
+    <!-- 卡片头部 -->
+    <view class="tp-header">
+      <view class="tp-header-left">
+        <view class="tp-icon-badge">
+          <text class="tp-icon-text">&#x2637;</text>
+        </view>
+        <view class="tp-header-info">
+          <text class="tp-title">家庭天盘</text>
+          <text class="tp-subtitle">五行能量 · 每日更新</text>
+        </view>
+      </view>
+      <view class="tp-header-action" @click="goDetail">
+        <text class="tp-action-text">&#x2192;</text>
+      </view>
+    </view>
+
+    <!-- 三栏展示 -->
+    <view class="tp-body">
+      <!-- 今日运势 -->
+      <view class="tp-col tp-col-fortune">
+        <text class="tp-col-icon">&#x2B50;</text>
+        <text class="tp-col-label">今日运势</text>
+        <view class="tp-fortune-badge" :class="'fortune-level-' + fortuneLevel">
+          <text class="tp-fortune-text">{{ fortuneText }}</text>
+        </view>
+        <text class="tp-fortune-desc">{{ fortuneDesc }}</text>
+      </view>
+
+      <!-- 今日心情 -->
+      <view class="tp-col tp-col-mood">
+        <text class="tp-col-icon">{{ moodEmoji }}</text>
+        <text class="tp-col-label">今日心情</text>
+        <text class="tp-mood-value">{{ moodText }}</text>
+        <view class="tp-mood-bar-track">
+          <view class="tp-mood-bar-fill" :style="{ width: moodPercent + '%' }"></view>
+        </view>
+      </view>
+
+      <!-- 吉位 -->
+      <view class="tp-col tp-col-direction">
+        <text class="tp-col-icon">&#x1F9ED;</text>
+        <text class="tp-col-label">今日吉位</text>
+        <view class="tp-compass-wrap">
+          <canvas
+            canvas-id="tianpanCompass"
+            id="tianpanCompass"
+            type="2d"
+            class="tp-compass-canvas"
+          ></canvas>
+        </view>
+        <text class="tp-direction-value">{{ luckyDirection }}</text>
+      </view>
+    </view>
+
+    <!-- 底部周报提示 -->
+    <view class="tp-footer" v-if="weeklyTip">
+      <text class="tp-footer-icon">&#x1F4A1;</text>
+      <text class="tp-footer-text">{{ weeklyTip }}</text>
+    </view>
+
+    <!-- 查看详情入口 -->
+    <view class="tp-detail-btn" @click="goDetail">
+      <text class="tp-detail-text">查看家庭天盘详情 &#x2192;</text>
+    </view>
+  </view>
+</template>
+
+<script>
+/**
+ * FamilyTianpanCard — 家庭天盘概览卡片(嵌入心维度页面)
+ *
+ * Props:
+ *   fortune    — { level: 0-4, text, description }
+ *   mood       — { emoji, text, score: 0-100 }
+ *   dominantElement — 'wood'|'fire'|'earth'|'metal'|'water'
+ *   weeklyTip  — string 本周提示
+ *   visible    — boolean 是否显示
+ *
+ * 吉位 Canvas 绘制五元素罗盘,高亮吉位方向。
+ */
+export default {
+  name: 'FamilyTianpanCard',
+  props: {
+    fortune: {
+      type: Object,
+      default: function() { return null }
+    },
+    mood: {
+      type: Object,
+      default: function() { return null }
+    },
+    dominantElement: {
+      type: String,
+      default: ''
+    },
+    weeklyTip: {
+      type: String,
+      default: ''
+    },
+    visible: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data: function() {
+    return {
+      canvasCtx: null,
+      canvasReady: false,
+      // 五行 → 方位映射
+      elementDirMap: {
+        wood: '东',
+        fire: '南',
+        earth: '中',
+        metal: '西',
+        water: '北'
+      },
+      // 方位 → 颜色映射
+      dirColorMap: {
+        '东': '#10B981',
+        '南': '#FF6B9D',
+        '中': '#FF8C42',
+        '西': '#6366F1',
+        '北': '#3B82F6'
+      },
+      // 方位 → 角度(从北顺时针,Canvas 默认从右/东开始)
+      dirAngleMap: {
+        '东': Math.PI / 2,
+        '南': Math.PI,
+        '中': 0,
+        '西': -Math.PI / 2,
+        '北': 0
+      }
+    }
+  },
+  computed: {
+    fortuneLevel: function() {
+      if (!this.fortune) return 2
+      return this.fortune.level !== undefined ? this.fortune.level : 2
+    },
+    fortuneText: function() {
+      if (!this.fortune || !this.fortune.text) return '平'
+      return this.fortune.text
+    },
+    fortuneDesc: function() {
+      if (!this.fortune || !this.fortune.description) return '今日宜静心养性'
+      return this.fortune.description
+    },
+    moodEmoji: function() {
+      if (this.mood && this.mood.emoji) return this.mood.emoji
+      return '\u{1F60A}' // 默认笑脸
+    },
+    moodText: function() {
+      if (this.mood && this.mood.text) return this.mood.text
+      return '平静'
+    },
+    moodPercent: function() {
+      if (this.mood && this.mood.score !== undefined) return this.mood.score
+      return 60
+    },
+    luckyDirection: function() {
+      if (this.dominantElement && this.elementDirMap[this.dominantElement]) {
+        return this.elementDirMap[this.dominantElement] + '方'
+      }
+      return '东方'
+    },
+    luckyElementColor: function() {
+      var dir = this.luckyDirection
+      return this.dirColorMap[dir] || '#FF6B9D'
+    }
+  },
+  watch: {
+    visible: function(val) {
+      if (val) {
+        var self = this
+        setTimeout(function() {
+          self.initCanvas()
+        }, 300)
+      }
+    },
+    dominantElement: function() {
+      if (this.canvasReady) {
+        this.drawCompass()
+      }
+    }
+  },
+  mounted: function() {
+    if (this.visible) {
+      var self = this
+      setTimeout(function() {
+        self.initCanvas()
+      }, 500)
+    }
+  },
+  methods: {
+    goDetail: function() {
+      uni.navigateTo({
+        url: '/pages/mind-detail/family-dashboard'
+      })
+    },
+
+    initCanvas: function() {
+      var self = this
+      var query = uni.createSelectorQuery().in(self)
+      query.select('#tianpanCompass')
+        .fields({ node: true, size: true })
+        .exec(function(res) {
+          if (!res || !res[0] || !res[0].node) return
+          var ctx = res[0].node.getContext('2d')
+          var dpr = uni.getSystemInfoSync().pixelRatio || 2
+          var w = res[0].width
+          var h = res[0].height
+          res[0].node.width = w * dpr
+          res[0].node.height = h * dpr
+          ctx.scale(dpr, dpr)
+          self.canvasCtx = ctx
+          self.canvasReady = true
+          self._compassW = w
+          self._compassH = h
+          self.drawCompass()
+        })
+    },
+
+    drawCompass: function() {
+      var ctx = this.canvasCtx
+      if (!ctx) return
+
+      var w = this._compassW || 100
+      var h = this._compassH || 100
+      var cx = w / 2
+      var cy = h / 2
+      var outerR = Math.min(cx, cy) - 4
+
+      // 清空
+      ctx.clearRect(0, 0, w, h)
+
+      // 五个方位: 北(上), 东(右), 南(下), 西(左), 中(中心)
+      var directions = [
+        { label: '北', element: 'water', angle: -Math.PI / 2 },
+        { label: '东', element: 'wood', angle: 0 },
+        { label: '南', element: 'fire', angle: Math.PI / 2 },
+        { label: '西', element: 'metal', angle: Math.PI },
+        { label: '中', element: 'earth', angle: null }
+      ]
+      var elementColors = {
+        wood: '#10B981',
+        fire: '#FF6B9D',
+        earth: '#FF8C42',
+        metal: '#6366F1',
+        water: '#3B82F6'
+      }
+
+      var luckyDir = this.luckyDirection.replace('方', '')
+      var luckyColor = this.luckyElementColor
+
+      // 外圈背景
+      ctx.beginPath()
+      ctx.arc(cx, cy, outerR, 0, Math.PI * 2)
+      ctx.fillStyle = '#FFF5F7'
+      ctx.fill()
+      ctx.strokeStyle = luckyColor + '40'
+      ctx.lineWidth = 1.5
+      ctx.stroke()
+
+      // 绘制四个方位的扇区
+      for (var i = 0; i < 4; i++) {
+        var d = directions[i]
+        var color = elementColors[d.element]
+
+        // 扇区线
+        ctx.beginPath()
+        ctx.moveTo(cx, cy)
+        var ex = cx + outerR * Math.cos(d.angle)
+        var ey = cy + outerR * Math.sin(d.angle)
+        ctx.lineTo(ex, ey)
+        ctx.strokeStyle = color + '30'
+        ctx.lineWidth = 1
+        ctx.stroke()
+
+        // 方位标签
+        var labelR = outerR * 0.7
+        var lx = cx + labelR * Math.cos(d.angle)
+        var ly = cy + labelR * Math.sin(d.angle)
+
+        var isLucky = d.label === luckyDir
+        if (isLucky) {
+          // 吉位高亮圆
+          ctx.beginPath()
+          ctx.arc(lx, ly, 12, 0, Math.PI * 2)
+          ctx.fillStyle = luckyColor + '20'
+          ctx.fill()
+          ctx.strokeStyle = luckyColor
+          ctx.lineWidth = 1.5
+          ctx.stroke()
+        }
+
+        ctx.fillStyle = isLucky ? luckyColor : '#9CA3AF'
+        ctx.font = (isLucky ? 'bold ' : '') + '10px sans-serif'
+        ctx.textAlign = 'center'
+        ctx.textBaseline = 'middle'
+        ctx.fillText(d.label, lx, ly)
+      }
+
+      // 中心点
+      ctx.beginPath()
+      ctx.arc(cx, cy, 5, 0, Math.PI * 2)
+      ctx.fillStyle = luckyColor
+      ctx.fill()
+      ctx.strokeStyle = '#FFFFFF'
+      ctx.lineWidth = 1.5
+      ctx.stroke()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.tianpan-card {
+  margin: 20rpx 30rpx;
+  background: #FFFFFF;
+  border-radius: 24rpx;
+  padding: 28rpx 24rpx;
+  /* Claymorphism 双阴影 */
+  box-shadow: 0 4rpx 16rpx rgba(255, 107, 157, 0.12),
+              0 2rpx 4rpx rgba(0, 0, 0, 0.04);
+  border: 1rpx solid rgba(255, 107, 157, 0.08);
+  overflow: hidden;
+}
+
+/* ===== 头部 ===== */
+.tp-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 24rpx;
+  padding-bottom: 20rpx;
+  border-bottom: 1rpx solid rgba(255, 107, 157, 0.10);
+}
+.tp-header-left {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+.tp-icon-badge {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 20rpx;
+  background: linear-gradient(135deg, #FF6B9D, #FF8FB1);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 16rpx;
+  box-shadow: 0 4rpx 12rpx rgba(255, 107, 157, 0.30);
+}
+.tp-icon-text {
+  font-size: 32rpx;
+  color: #FFFFFF;
+}
+.tp-header-info {
+  display: flex;
+  flex-direction: column;
+}
+.tp-title {
+  font-size: 30rpx;
+  font-weight: 700;
+  color: #1E293B;
+}
+.tp-subtitle {
+  font-size: 22rpx;
+  color: #94A3B8;
+  margin-top: 2rpx;
+}
+.tp-header-action {
+  width: 56rpx;
+  height: 56rpx;
+  border-radius: 28rpx;
+  background: #FFF0F3;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.tp-action-text {
+  font-size: 28rpx;
+  color: #FF6B9D;
+  font-weight: 600;
+}
+.tp-header-action:active {
+  opacity: 0.7;
+}
+
+/* ===== 三栏 body ===== */
+.tp-body {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+}
+.tp-col {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 16rpx 8rpx;
+  border-radius: 20rpx;
+  background: #FAFBFC;
+}
+.tp-col-fortune {
+  margin-right: 12rpx;
+}
+.tp-col-mood {
+  margin-left: 6rpx;
+  margin-right: 6rpx;
+}
+.tp-col-direction {
+  margin-left: 12rpx;
+}
+
+/* 列通用 */
+.tp-col-icon {
+  font-size: 36rpx;
+  margin-bottom: 8rpx;
+}
+.tp-col-label {
+  font-size: 22rpx;
+  color: #94A3B8;
+  margin-bottom: 12rpx;
+  font-weight: 500;
+}
+
+/* 运势 */
+.tp-fortune-badge {
+  padding: 6rpx 20rpx;
+  border-radius: 20rpx;
+  margin-bottom: 8rpx;
+}
+.fortune-level-0 {
+  background: #FFEBEE;
+}
+.fortune-level-0 .tp-fortune-text {
+  color: #D32F2F;
+}
+.fortune-level-1 {
+  background: #FFF3E0;
+}
+.fortune-level-1 .tp-fortune-text {
+  color: #E65100;
+}
+.fortune-level-2 {
+  background: #FFFDE7;
+}
+.fortune-level-2 .tp-fortune-text {
+  color: #F9A825;
+}
+.fortune-level-3 {
+  background: #F3E5F5;
+}
+.fortune-level-3 .tp-fortune-text {
+  color: #7B1FA2;
+}
+.fortune-level-4 {
+  background: #FFCDD2;
+}
+.fortune-level-4 .tp-fortune-text {
+  color: #B71C1C;
+}
+.tp-fortune-text {
+  font-size: 26rpx;
+  font-weight: 700;
+}
+.tp-fortune-desc {
+  font-size: 20rpx;
+  color: #94A3B8;
+  text-align: center;
+  line-height: 1.4;
+}
+
+/* 心情 */
+.tp-mood-value {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #1E293B;
+  margin-bottom: 10rpx;
+}
+.tp-mood-bar-track {
+  width: 100%;
+  height: 10rpx;
+  background: #E2E8F0;
+  border-radius: 5rpx;
+  overflow: hidden;
+}
+.tp-mood-bar-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #FF6B9D, #FF8FB1);
+  border-radius: 5rpx;
+  transition: width 0.5s;
+}
+
+/* 吉位罗盘 */
+.tp-compass-wrap {
+  width: 120rpx;
+  height: 120rpx;
+  margin-bottom: 8rpx;
+}
+.tp-compass-canvas {
+  width: 120rpx;
+  height: 120rpx;
+}
+.tp-direction-value {
+  font-size: 24rpx;
+  font-weight: 700;
+  color: #FF6B9D;
+}
+
+/* ===== 底部提示 ===== */
+.tp-footer {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  margin-top: 20rpx;
+  padding: 16rpx;
+  background: #FFF5F0;
+  border-radius: 14rpx;
+}
+.tp-footer-icon {
+  font-size: 24rpx;
+  margin-right: 8rpx;
+  flex-shrink: 0;
+}
+.tp-footer-text {
+  font-size: 22rpx;
+  color: #8B6914;
+  line-height: 1.5;
+  flex: 1;
+}
+
+/* ===== 查看详情按钮 ===== */
+.tp-detail-btn {
+  margin-top: 16rpx;
+  text-align: center;
+  padding: 14rpx;
+  background: linear-gradient(135deg, #FFF0F3, #FFE0E8);
+  border-radius: 14rpx;
+}
+.tp-detail-btn:active {
+  opacity: 0.7;
+}
+.tp-detail-text {
+  font-size: 24rpx;
+  color: #FF6B9D;
+  font-weight: 600;
+}
+</style>

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

@@ -294,6 +294,15 @@
       </view>
     </view>
 
+    <!-- ===== 家庭天盘模块(今日运势 / 今日心情 / 吉位) ===== -->
+    <FamilyTianpanCard
+      v-if="isLoggedIn"
+      :visible="isLoggedIn"
+      :fortune="dailyFortune"
+      :mood="todayMood"
+      :dominantElement="dominantElement"
+      :weeklyTip="familyWeeklyInsight" />
+
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
     <AIFloatingAvatar />
@@ -314,11 +323,12 @@ import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 
 import PsychCrisisBanner from '../../components/PsychCrisisBanner.vue'
 import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
+import FamilyTianpanCard from '../../components/FamilyTianpanCard.vue'
 import { getVisibleSections, getFeaturedArticles, getEmiReport, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren, getVisibleFamilyMembers, getEnergyOverview, getContactList, getHealthAlerts, getMilestones, getDanReportByChild } from '../../utils/api.js'
 import config from '../../config.js'
 
 export default {
-  components: { TabTransition, PageBanner, RadarChart, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, PsychCrisisBanner, AIFloatingAvatar },
+  components: { TabTransition, PageBanner, RadarChart, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, FamilyRelationGraph, PsychCrisisBanner, AIFloatingAvatar, FamilyTianpanCard },
   data() {
     return {
       isLoggedIn: false,
@@ -387,7 +397,14 @@ export default {
       contactList: [],
       healthAlerts: [],
       milestones: [],
-      danReports: []
+      danReports: [],
+
+      // 家庭天盘 - 今日心情(占位数据)
+      todayMood: {
+        emoji: '\u{1F60A}',
+        text: '平静',
+        score: 65
+      }
     }
   },
   computed: {