소스 검색

feat(mind): 新增family-dashboard家庭天盘页面+duo-compatibility双人合盘详情页

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Xiaogang Liao 4 달 전
부모
커밋
b2f0c053f5
2개의 변경된 파일과 1026개의 추가작업 그리고 0개의 파일을 삭제
  1. 517 0
      cfc-frontend/pages/mind-detail/duo-compatibility.vue
  2. 509 0
      cfc-frontend/pages/mind-detail/family-dashboard.vue

+ 517 - 0
cfc-frontend/pages/mind-detail/duo-compatibility.vue

@@ -0,0 +1,517 @@
+<template>
+  <view class="duo-container">
+    <view class="loading-state" v-if="loading">
+      <text>加载中...</text>
+    </view>
+
+    <template v-if="!loading && dataLoaded">
+      <!-- 双人头像区 -->
+      <view class="duo-header">
+        <view class="person-card">
+          <view class="person-avatar">{{ name1.charAt(0) }}</view>
+          <text class="person-name">{{ name1 }}</text>
+        </view>
+        <view class="compatibility-badge">
+          <text class="compatibility-score">{{ overallScore }}</text>
+          <text class="compatibility-label">兼容分</text>
+        </view>
+        <view class="person-card">
+          <view class="person-avatar">{{ name2.charAt(0) }}</view>
+          <text class="person-name">{{ name2 }}</text>
+        </view>
+      </view>
+
+      <!-- 维度评分条 -->
+      <view class="dimension-summary">
+        <view class="dimension-item" v-for="dim in dimensions" :key="dim.key" @click="activeTab = dim.key">
+          <text class="dimension-label">{{ dim.label }}</text>
+          <view class="dimension-bar-track">
+            <view class="dimension-bar-fill" :style="{ width: dim.score + '%', background: dim.color }"></view>
+          </view>
+          <text class="dimension-score">{{ dim.score }}</text>
+        </view>
+      </view>
+
+      <!-- Tab 切换 -->
+      <view class="tab-bar">
+        <view
+          class="tab-item"
+          v-for="tab in tabs"
+          :key="tab.key"
+          :class="activeTab === tab.key ? 'tab-active' : ''"
+          @click="activeTab = tab.key">
+          <text>{{ tab.label }}</text>
+        </view>
+      </view>
+
+      <!-- 八字合盘 Tab -->
+      <view class="tab-content" v-if="activeTab === 'zodiac'">
+        <view class="info-card">
+          <view class="info-row" v-if="data.baziPairingSummary">
+            <text class="info-label">合盘摘要</text>
+            <text class="info-value">{{ data.baziPairingSummary }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 五行互补 Tab -->
+      <view class="tab-content" v-if="activeTab === 'wuxing'">
+        <view class="wuxing-grid">
+          <view
+            class="wuxing-card"
+            v-for="wx in wuxingData"
+            :key="wx.element"
+            :style="{ background: wx.color + '18', borderColor: wx.color }">
+            <text class="wuxing-card-elem">{{ wx.elementLabel }}</text>
+            <view class="wuxing-card-bars">
+              <view class="wuxing-bar-pair">
+                <text class="wuxing-bar-label">{{ name1 }}</text>
+                <view class="wuxing-bar-track-s">
+                  <view class="wuxing-bar-fill-s" :style="{ width: wx.v1 + '%', background: wx.color }"></view>
+                </view>
+                <text class="wuxing-bar-val">{{ wx.v1 }}</text>
+              </view>
+              <view class="wuxing-bar-pair">
+                <text class="wuxing-bar-label">{{ name2 }}</text>
+                <view class="wuxing-bar-track-s">
+                  <view class="wuxing-bar-fill-s" :style="{ width: wx.v2 + '%', background: wx.color }"></view>
+                </view>
+                <text class="wuxing-bar-val">{{ wx.v2 }}</text>
+              </view>
+            </view>
+            <text class="wuxing-card-desc">{{ wx.description }}</text>
+          </view>
+        </view>
+        <view class="info-card" v-if="data.wuxingComplementarity">
+          <text class="info-label">五行互补分析</text>
+          <text class="info-value" style="margin-top:12rpx">{{ data.wuxingComplementarity.analysis }}</text>
+        </view>
+      </view>
+
+      <!-- 生肖配对 Tab -->
+      <view class="tab-content" v-if="activeTab === 'zodiac'">
+        <view class="info-card" v-if="data.zodiacCompatibility">
+          <view class="info-row">
+            <text class="info-label">生肖配对</text>
+            <text class="info-value">{{ zodLabel1 }} & {{ zodLabel2 }}</text>
+          </view>
+          <view class="info-row" v-if="data.zodiacCompatibility.affinityType">
+            <text class="info-label">配对关系</text>
+            <text class="info-value">{{ data.zodiacCompatibility.affinityType }}</text>
+          </view>
+          <view class="info-row" v-if="data.zodiacCompatibility.description">
+            <text class="info-label">详解</text>
+            <text class="info-value">{{ data.zodiacCompatibility.description }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">得分</text>
+            <text class="info-value score-high">{{ data.zodiacCompatibility.score }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 灵数关系 Tab -->
+      <view class="tab-content" v-if="activeTab === 'lifenumber'">
+        <view class="info-card" v-if="data.lifeNumberRelationship">
+          <view class="info-row">
+            <text class="info-label">{{ name1 }} 灵数</text>
+            <text class="info-value">{{ data.lifeNumberRelationship.lifeNumber1 }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">{{ name2 }} 灵数</text>
+            <text class="info-value">{{ data.lifeNumberRelationship.lifeNumber2 }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">关系数</text>
+            <text class="info-value score-primary">{{ data.lifeNumberRelationship.relationshipNumber }}</text>
+          </view>
+          <view class="info-row" v-if="data.lifeNumberRelationship.relationshipType">
+            <text class="info-label">关系类型</text>
+            <text class="info-value">{{ data.lifeNumberRelationship.relationshipType }}</text>
+          </view>
+          <view class="info-row" v-if="data.lifeNumberRelationship.description">
+            <text class="info-label">详解</text>
+            <text class="info-value">{{ data.lifeNumberRelationship.description }}</text>
+          </view>
+          <view class="info-row">
+            <text class="info-label">得分</text>
+            <text class="info-value score-high">{{ data.lifeNumberRelationship.score }}</text>
+          </view>
+        </view>
+      </view>
+    </template>
+
+    <view class="empty-state" v-if="!loading && !dataLoaded">
+      <text>暂无兼容数据</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import config from '../../config'
+
+export default {
+  data() {
+    return {
+      loading: true,
+      dataLoaded: false,
+      memberId1: null,
+      memberId2: null,
+      name1: '',
+      name2: '',
+      overallScore: 0,
+      data: {},
+      activeTab: 'wuxing',
+      tabs: [
+        { key: 'bazi', label: '八字合盘' },
+        { key: 'wuxing', label: '五行互补' },
+        { key: 'zodiac', label: '生肖配对' },
+        { key: 'lifenumber', label: '灵数关系' }
+      ],
+      dimensions: [],
+      wuxingColors: {
+        wood: '#10B981',
+        fire: '#FF6B9D',
+        earth: '#FF8C42',
+        metal: '#6366F1',
+        water: '#3B82F6'
+      },
+      wuxingLabels: {
+        wood: '木', fire: '火', earth: '土', metal: '金', water: '水'
+      },
+      zodiacNames: {
+        rat: '鼠', ox: '牛', tiger: '虎', rabbit: '兔', dragon: '龙',
+        snake: '蛇', horse: '马', sheep: '羊', monkey: '猴',
+        rooster: '鸡', dog: '狗', pig: '猪'
+      },
+      wuxingElements: ['wood', 'fire', 'earth', 'metal', 'water']
+    }
+  },
+
+  computed: {
+    zodLabel1() {
+      var z = this.data.zodiacCompatibility
+      return z ? (this.zodiacNames[z.zodiac1] || z.zodiac1) : ''
+    },
+    zodLabel2() {
+      var z = this.data.zodiacCompatibility
+      return z ? (this.zodiacNames[z.zodiac2] || z.zodiac2) : ''
+    },
+    wuxingData() {
+      var wc = this.data.wuxingComplementarity
+      if (!wc || !wc.elementScores) return []
+      var arr = []
+      for (var i = 0; i < this.wuxingElements.length; i++) {
+        var elem = this.wuxingElements[i]
+        var es = wc.elementScores[elem]
+        if (es === undefined) continue
+        arr.push({
+          element: elem,
+          elementLabel: this.wuxingLabels[elem],
+          color: this.wuxingColors[elem],
+          v1: wc.member1Elements[elem] || 0,
+          v2: wc.member2Elements[elem] || 0,
+          score: es,
+          description: this.wuxingDesc(elem, es)
+        })
+      }
+      return arr.sort(function(a, b) { return b.score - a.score })
+    }
+  },
+
+  onLoad(params) {
+    this.memberId1 = params.memberId1
+    this.memberId2 = params.memberId2
+    this.loadData()
+  },
+
+  methods: {
+    loadData() {
+      var self = this
+
+      uni.request({
+        url: config.API_BASE_URL + '/api/mind/traditional/compatibility',
+        method: 'POST',
+        data: {
+          memberId1: self.memberId1,
+          memberId2: self.memberId2,
+          memberType1: 'child',
+          memberType2: 'child'
+        },
+        success: function(res) {
+          self.loading = false
+          if (res.data && res.data.code === 200 && res.data.data) {
+            var d = res.data.data
+            self.data = d
+            self.overallScore = d.overallScore || 0
+            self.name1 = d.memberName1 || '成员1'
+            self.name2 = d.memberName2 || '成员2'
+            self.buildDimensions(d)
+            self.dataLoaded = true
+          } else {
+            self.dataLoaded = false
+          }
+        },
+        fail: function() {
+          self.loading = false
+          self.dataLoaded = false
+        }
+      })
+    },
+
+    buildDimensions(d) {
+      var dims = []
+      if (d.zodiacCompatibility && d.zodiacCompatibility.score !== undefined) {
+        dims.push({ key: 'zodiac', label: '生肖配对', score: d.zodiacCompatibility.score, color: '#8B5CF6' })
+      }
+      if (d.wuxingComplementarity && d.wuxingComplementarity.score !== undefined) {
+        dims.push({ key: 'wuxing', label: '五行互补', score: d.wuxingComplementarity.score, color: '#10B981' })
+      }
+      if (d.wuxingComplementarity && d.wuxingComplementarity.bloodTypeScore !== undefined) {
+        dims.push({ key: 'blood', label: '血型兼容', score: d.wuxingComplementarity.bloodTypeScore, color: '#FF6B9D' })
+      }
+      if (d.lifeNumberRelationship && d.lifeNumberRelationship.score !== undefined) {
+        dims.push({ key: 'lifenumber', label: '灵数关系', score: d.lifeNumberRelationship.score, color: '#F59E0B' })
+      }
+      this.dimensions = dims
+    },
+
+    wuxingDesc(elem, score) {
+      if (score >= 70) return '互补良好'
+      if (score >= 50) return '较为平衡'
+      return '有待加强'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.duo-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;
+}
+.duo-header {
+  background: linear-gradient(135deg, #8B5CF6, #A78BFA);
+  padding: 40rpx 32rpx;
+  display: flex;
+  align-items: center;
+  justify-content: space-around;
+}
+.person-card {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.person-avatar {
+  width: 88rpx;
+  height: 88rpx;
+  border-radius: 50%;
+  background: rgba(255,255,255,0.3);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 36rpx;
+  font-weight: 700;
+  color: #fff;
+  text-align: center;
+  line-height: 88rpx;
+}
+.person-name {
+  font-size: 26rpx;
+  color: rgba(255,255,255,0.9);
+  margin-top: 12rpx;
+}
+.compatibility-badge {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+.compatibility-score {
+  font-size: 64rpx;
+  font-weight: 800;
+  color: #fff;
+  line-height: 1;
+}
+.compatibility-label {
+  font-size: 22rpx;
+  color: rgba(255,255,255,0.7);
+  margin-top: 8rpx;
+}
+.dimension-summary {
+  margin: 24rpx 32rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 24rpx 28rpx;
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.dimension-item {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+}
+.dimension-label {
+  font-size: 24rpx;
+  font-weight: 500;
+  color: #666;
+  width: 96rpx;
+  flex-shrink: 0;
+}
+.dimension-bar-track {
+  flex: 1;
+  height: 16rpx;
+  background: #F0F0F0;
+  border-radius: 8rpx;
+  overflow: hidden;
+}
+.dimension-bar-fill {
+  height: 100%;
+  border-radius: 8rpx;
+  transition: width 0.3s;
+}
+.dimension-score {
+  font-size: 24rpx;
+  font-weight: 600;
+  color: #333;
+  width: 56rpx;
+  text-align: right;
+}
+.tab-bar {
+  display: flex;
+  margin: 0 32rpx;
+  background: #fff;
+  border-radius: 20rpx 20rpx 0 0;
+  overflow: hidden;
+}
+.tab-item {
+  flex: 1;
+  padding: 20rpx 0;
+  text-align: center;
+  font-size: 26rpx;
+  color: #999;
+  border-bottom: 4rpx solid transparent;
+}
+.tab-active {
+  color: #8B5CF6;
+  border-bottom-color: #8B5CF6;
+  font-weight: 600;
+}
+.tab-content {
+  margin: 0 32rpx;
+  background: #fff;
+  border-radius: 0 0 20rpx 20rpx;
+  padding: 28rpx;
+}
+.info-card {
+  padding: 20rpx;
+  background: #FAFAFA;
+  border-radius: 16rpx;
+}
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12rpx 0;
+  border-bottom: 1rpx solid #F0F0F0;
+}
+.info-row:last-child {
+  border-bottom: none;
+}
+.info-label {
+  font-size: 26rpx;
+  color: #666;
+}
+.info-value {
+  font-size: 26rpx;
+  color: #333;
+  font-weight: 500;
+  text-align: right;
+  max-width: 60%;
+}
+.score-high {
+  color: #10B981;
+  font-weight: 700;
+  font-size: 32rpx;
+}
+.score-primary {
+  color: #8B5CF6;
+  font-weight: 700;
+  font-size: 32rpx;
+}
+.wuxing-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 16rpx;
+  margin-bottom: 20rpx;
+}
+.wuxing-card {
+  padding: 20rpx;
+  border-radius: 16rpx;
+  border: 2rpx solid;
+}
+.wuxing-card-elem {
+  font-size: 28rpx;
+  font-weight: 700;
+  margin-bottom: 12rpx;
+  display: block;
+}
+.wuxing-card-bars {
+  display: flex;
+  flex-direction: column;
+  gap: 8rpx;
+}
+.wuxing-bar-pair {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+}
+.wuxing-bar-label {
+  font-size: 20rpx;
+  color: #999;
+  width: 60rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.wuxing-bar-track-s {
+  flex: 1;
+  height: 12rpx;
+  background: #F0F0F0;
+  border-radius: 6rpx;
+  overflow: hidden;
+}
+.wuxing-bar-fill-s {
+  height: 100%;
+  border-radius: 6rpx;
+}
+.wuxing-bar-val {
+  font-size: 20rpx;
+  color: #666;
+  width: 36rpx;
+  text-align: right;
+}
+.wuxing-card-desc {
+  font-size: 22rpx;
+  color: #666;
+  margin-top: 8rpx;
+  display: block;
+}
+.empty-state {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 0;
+  color: #999;
+  font-size: 28rpx;
+}
+</style>

+ 509 - 0
cfc-frontend/pages/mind-detail/family-dashboard.vue

@@ -0,0 +1,509 @@
+<template>
+  <view class="dashboard-container">
+    <view class="loading-state" v-if="loading">
+      <text>加载中...</text>
+    </view>
+
+    <template v-if="!loading && dataLoaded">
+      <!-- 家庭标识区 -->
+      <view class="family-header">
+        <text class="family-name">{{ familyName || '我的家庭' }}</text>
+        <view class="member-avatars">
+          <view class="mini-avatar" v-for="m in members" :key="m.memberId" @click="goDuo(m.memberId)">
+            <text class="mini-avatar-text">{{ m.nickname ? m.nickname.charAt(0) : '?' }}</text>
+            <text class="mini-avatar-label">{{ m.nickname }}</text>
+          </view>
+        </view>
+      </view>
+
+      <!-- 家庭五行总览 -->
+      <view class="section">
+        <view class="section-header">
+          <text class="section-icon">&#x267E;</text>
+          <text class="section-title">家庭五行总览</text>
+        </view>
+        <view class="wuxing-list" v-if="familyWuxing && familyWuxing.aggregatePercentages">
+          <view class="wuxing-stacked-bar">
+            <view
+              v-for="wx in stackedWuxing"
+              :key="wx.key"
+              class="wuxing-stack-segment"
+              :style="{ width: wx.value + '%', background: wx.color }">
+            </view>
+          </view>
+          <view class="wuxing-item" v-for="wx in flatWuxing" :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="wuxing-analysis" v-if="familyWuxing && familyWuxing.analysis">
+          <text>{{ familyWuxing.analysis }}</text>
+        </view>
+      </view>
+
+      <!-- 成员速览卡片(横向滚动) -->
+      <view class="section">
+        <view class="section-header">
+          <text class="section-icon">&#x1F465;</text>
+          <text class="section-title">家庭成员</text>
+        </view>
+        <scroll-view class="member-scroll" scroll-x="true" show-scrollbar="false">
+          <view
+            class="member-card"
+            v-for="m in members"
+            :key="m.memberId"
+            :style="{ background: memberCardBg(m) }"
+            @click="goDuo(m.memberId)">
+            <view class="member-card-avatar">{{ m.nickname ? m.nickname.charAt(0) : '?' }}</view>
+            <text class="member-card-name">{{ m.nickname }}</text>
+            <text class="member-card-detail" v-if="m.zodiac">生肖: {{ zodiacLabel(m.zodiac) }}</text>
+            <text class="member-card-detail" v-if="m.lifeNumber">灵数: {{ m.lifeNumber }}</text>
+            <text class="member-card-detail" v-if="m.bloodType">血型: {{ m.bloodType }}</text>
+          </view>
+        </scroll-view>
+      </view>
+
+      <!-- 关系速览列表 -->
+      <view class="section">
+        <view class="section-header">
+          <text class="section-icon">&#x1F91D;</text>
+          <text class="section-title">家庭关系</text>
+        </view>
+        <view
+          class="relation-card"
+          v-for="r in relationships"
+          :key="r.memberId1 + '-' + r.memberId2"
+          :class="'relation-level-' + relationLevel(r.overallScore)"
+          @click="goDuoDetail(r.memberId1, r.memberId2)">
+          <view class="relation-pair">
+            <text class="relation-name">{{ r.nickname1 }}</text>
+            <text class="relation-arrow">&#x2194;</text>
+            <text class="relation-name">{{ r.nickname2 }}</text>
+          </view>
+          <view class="relation-badge" :class="'badge-' + relationLevel(r.overallScore)">
+            <text>{{ relationLevelText(r.overallScore) }}</text>
+          </view>
+          <text class="relation-summary">{{ r.summary || r.zodiacPairingType }}</text>
+          <text class="relation-score">兼容 {{ r.overallScore }}</text>
+        </view>
+      </view>
+
+      <!-- 本周家庭提示 -->
+      <view class="weekly-insight" v-if="weeklyInsight">
+        <text class="insight-icon">&#x1F4A1;</text>
+        <text class="insight-text">{{ weeklyInsight }}</text>
+      </view>
+    </template>
+
+    <view class="empty-state" v-if="!loading && !dataLoaded">
+      <text>暂无家庭数据</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import config from '../../config'
+
+export default {
+  data() {
+    return {
+      loading: true,
+      dataLoaded: false,
+      familyId: null,
+      familyName: '',
+      members: [],
+      familyWuxing: null,
+      relationships: [],
+      weeklyInsight: '',
+      wuxingColors: {
+        wood: '#10B981',
+        fire: '#FF6B9D',
+        earth: '#FF8C42',
+        metal: '#6366F1',
+        water: '#3B82F6'
+      },
+      wuxingLabels: {
+        wood: '木', fire: '火', earth: '土', metal: '金', water: '水'
+      },
+      zodiacNames: {
+        rat: '鼠', ox: '牛', tiger: '虎', rabbit: '兔', dragon: '龙',
+        snake: '蛇', horse: '马', sheep: '羊', monkey: '猴',
+        rooster: '鸡', dog: '狗', pig: '猪'
+      },
+      elementColors: {
+        wood: '#10B981', fire: '#FF6B9D', earth: '#FF8C42',
+        metal: '#6366F1', water: '#3B82F6'
+      }
+    }
+  },
+
+  computed: {
+    flatWuxing() {
+      if (!this.familyWuxing || !this.familyWuxing.aggregatePercentages) return []
+      var pcts = this.familyWuxing.aggregatePercentages
+      var keys = ['wood', 'fire', 'earth', 'metal', 'water']
+      var result = []
+      for (var i = 0; i < keys.length; i++) {
+        var k = keys[i]
+        if (pcts[k] !== undefined) {
+          result.push({ key: k, label: this.wuxingLabels[k], color: this.wuxingColors[k], value: pcts[k] })
+        }
+      }
+      return result.sort(function(a, b) { return b.value - a.value })
+    },
+    stackedWuxing() {
+      return this.flatWuxing
+    }
+  },
+
+  onLoad() {
+    this.familyId = uni.getStorageSync('currentFamilyId')
+    if (!this.familyId) {
+      // Try to get from user info
+      var userInfo = uni.getStorageSync('userInfo')
+      if (userInfo && userInfo.familyId) {
+        this.familyId = userInfo.familyId
+      }
+    }
+    this.loadData()
+  },
+
+  methods: {
+    loadData() {
+      var self = this
+      if (!self.familyId) {
+        uni.showModal({
+          title: '提示',
+          content: '请先加入家庭',
+          success: function() { uni.navigateBack() }
+        })
+        return
+      }
+
+      uni.request({
+        url: config.API_BASE_URL + '/api/mind/traditional/family-dashboard',
+        method: 'POST',
+        data: { familyId: self.familyId },
+        success: function(res) {
+          self.loading = false
+          if (res.data && res.data.code === 200 && res.data.data) {
+            var d = res.data.data
+            self.familyName = d.familyName || ''
+            self.members = d.members || []
+            self.familyWuxing = d.familyWuxing || null
+            self.relationships = d.relationships || []
+            self.weeklyInsight = d.weeklyInsight || ''
+            self.dataLoaded = self.members.length > 0
+          } else {
+            self.dataLoaded = false
+          }
+        },
+        fail: function() {
+          self.loading = false
+          self.dataLoaded = false
+        }
+      })
+    },
+
+    goDuo(memberId) {
+      var self = this
+      var currentId = uni.getStorageSync('currentChildId')
+      if (!currentId || currentId == memberId) {
+        // If no current member or same as target, just go to first member
+        if (self.members.length >= 2) {
+          var other = self.members[0].memberId != memberId ? self.members[0] : self.members[1]
+          memberId = other.memberId
+        }
+      }
+      uni.navigateTo({
+        url: '/pages/mind-detail/duo-compatibility?memberId1=' + currentId + '&memberId2=' + memberId
+      })
+    },
+
+    goDuoDetail(id1, id2) {
+      uni.navigateTo({
+        url: '/pages/mind-detail/duo-compatibility?memberId1=' + id1 + '&memberId2=' + id2
+      })
+    },
+
+    relationLevel(score) {
+      if (score >= 80) return 'high'
+      if (score >= 60) return 'medium'
+      if (score >= 40) return 'low'
+      return 'conflict'
+    },
+
+    relationLevelText(score) {
+      if (score >= 80) return '非常融洽'
+      if (score >= 60) return '相处和谐'
+      if (score >= 40) return '需要磨合'
+      return '需包容'
+    },
+
+    memberCardBg(m) {
+      if (m.wuxingElements) {
+        var maxVal = 0
+        var maxElem = 'earth'
+        var keys = ['wood', 'fire', 'earth', 'metal', 'water']
+        for (var i = 0; i < keys.length; i++) {
+          var v = m.wuxingElements[keys[i]] || 0
+          if (v > maxVal) { maxVal = v; maxElem = keys[i] }
+        }
+        var color = this.elementColors[maxElem] || '#FF8C42'
+        return 'linear-gradient(135deg, ' + color + '22, ' + color + '44)'
+      }
+      return '#F5F0FF'
+    },
+
+    zodiacLabel(code) {
+      return this.zodiacNames[code] || code
+    }
+  }
+}
+</script>
+
+<style scoped>
+.dashboard-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;
+}
+.family-header {
+  background: linear-gradient(135deg, #8B5CF6, #A78BFA);
+  padding: 40rpx 32rpx;
+  color: #fff;
+}
+.family-name {
+  font-size: 36rpx;
+  font-weight: 700;
+  display: block;
+  margin-bottom: 24rpx;
+}
+.member-avatars {
+  display: flex;
+  gap: 20rpx;
+}
+.mini-avatar {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 96rpx;
+}
+.mini-avatar-text {
+  width: 64rpx;
+  height: 64rpx;
+  border-radius: 50%;
+  background: rgba(255,255,255,0.3);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28rpx;
+  font-weight: 600;
+  color: #fff;
+  text-align: center;
+  line-height: 64rpx;
+}
+.mini-avatar-label {
+  font-size: 20rpx;
+  color: rgba(255,255,255,0.8);
+  margin-top: 8rpx;
+}
+.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: 32rpx;
+  margin-right: 12rpx;
+}
+.section-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+}
+.wuxing-stacked-bar {
+  display: flex;
+  height: 32rpx;
+  border-radius: 16rpx;
+  overflow: hidden;
+  margin-bottom: 16rpx;
+}
+.wuxing-stack-segment {
+  height: 100%;
+}
+.wuxing-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12rpx;
+}
+.wuxing-item {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+}
+.wuxing-label {
+  font-size: 26rpx;
+  font-weight: 600;
+  width: 44rpx;
+  text-align: center;
+}
+.wuxing-bar-track {
+  flex: 1;
+  height: 20rpx;
+  background: #F0F0F0;
+  border-radius: 10rpx;
+  overflow: hidden;
+}
+.wuxing-bar-fill {
+  height: 100%;
+  border-radius: 10rpx;
+}
+.wuxing-value {
+  font-size: 22rpx;
+  color: #666;
+  width: 56rpx;
+  text-align: right;
+}
+.wuxing-analysis {
+  margin-top: 16rpx;
+  padding: 16rpx;
+  background: #FFFBEB;
+  border-radius: 12rpx;
+  font-size: 24rpx;
+  color: #D97706;
+  line-height: 1.6;
+  border-left: 6rpx solid #F59E0B;
+}
+.member-scroll {
+  display: flex;
+  white-space: nowrap;
+  padding: 8rpx 0;
+}
+.member-card {
+  display: inline-flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 24rpx 32rpx;
+  border-radius: 16rpx;
+  margin-right: 20rpx;
+  min-width: 160rpx;
+}
+.member-card-avatar {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+  background: rgba(139,92,246,0.2);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #8B5CF6;
+  margin-bottom: 12rpx;
+}
+.member-card-name {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 8rpx;
+}
+.member-card-detail {
+  font-size: 20rpx;
+  color: #999;
+  margin-top: 4rpx;
+}
+.relation-card {
+  padding: 24rpx;
+  border-radius: 16rpx;
+  margin-bottom: 16rpx;
+  position: relative;
+}
+.relation-level-high { background: #F0FFF4; border-left: 6rpx solid #10B981; }
+.relation-level-medium { background: #FFFBEB; border-left: 6rpx solid #F59E0B; }
+.relation-level-low { background: #FFF7ED; border-left: 6rpx solid #FF8C42; }
+.relation-level-conflict { background: #FEF2F2; border-left: 6rpx solid #EF4444; }
+.relation-pair {
+  display: flex;
+  align-items: center;
+  gap: 12rpx;
+  margin-bottom: 8rpx;
+}
+.relation-name {
+  font-size: 26rpx;
+  font-weight: 600;
+  color: #333;
+}
+.relation-arrow {
+  font-size: 24rpx;
+  color: #999;
+}
+.relation-badge {
+  display: inline-block;
+  padding: 4rpx 16rpx;
+  border-radius: 20rpx;
+  font-size: 20rpx;
+  margin-bottom: 8rpx;
+}
+.badge-high { background: #D1FAE5; color: #059669; }
+.badge-medium { background: #FEF3C7; color: #D97706; }
+.badge-low { background: #FFEDD5; color: #EA580C; }
+.badge-conflict { background: #FEE2E2; color: #DC2626; }
+.relation-summary {
+  font-size: 22rpx;
+  color: #666;
+  display: block;
+}
+.relation-score {
+  position: absolute;
+  right: 24rpx;
+  top: 24rpx;
+  font-size: 28rpx;
+  font-weight: 700;
+  color: #8B5CF6;
+}
+.weekly-insight {
+  margin: 24rpx 32rpx;
+  padding: 24rpx;
+  background: linear-gradient(135deg, #EEF2FF, #E0E7FF);
+  border-radius: 20rpx;
+  display: flex;
+  align-items: flex-start;
+  gap: 12rpx;
+}
+.insight-icon {
+  font-size: 36rpx;
+  flex-shrink: 0;
+}
+.insight-text {
+  font-size: 24rpx;
+  color: #4F46E5;
+  line-height: 1.6;
+}
+.empty-state {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 0;
+  color: #999;
+  font-size: 28rpx;
+}
+</style>