Просмотр исходного кода

feat(frontend): 新增家庭传统命理页面+入口卡片

iwt 1 месяц назад
Родитель
Сommit
e1a8410482

+ 6 - 0
cfc-frontend/pages.json

@@ -1400,6 +1400,12 @@
             "navigationBarTitleText": "相关事项",
             "navigationStyle": "custom"
           }
+        },
+        {
+          "path": "traditional",
+          "style": {
+            "navigationBarTitleText": "家庭传统命理"
+          }
         }
       ]
     },

+ 34 - 2
cfc-frontend/pages/tianpan/index.vue

@@ -87,6 +87,17 @@
       <view class="fortune-arrow">查看详情 ›</view>
     </view>
 
+    <!-- 传统命理入口卡片 -->
+    <view class="fortune-card tradition-card" @click="goToTraditional">
+      <view class="fortune-header">
+        <text class="fortune-title">家庭传统命理</text>
+      </view>
+      <view class="fortune-content">
+        <text class="fortune-text">查看家庭成员八字、星座、灵数与五维基础能量分</text>
+      </view>
+      <view class="fortune-arrow">进入命理详情 ›</view>
+    </view>
+
     <!-- 成员详情弹窗 -->
     <view class="member-popup" v-if="showMemberPopup" @touchmove.stop>
       <view class="popup-overlay" @click="closeMemberPopup"></view>
@@ -642,6 +653,12 @@ export default {
       })
     },
     
+    goToTraditional: function() {
+      uni.navigateTo({
+        url: '/pages/tianpan/traditional'
+      })
+    },
+    
     loadDailyFortune: function() {
       var today = new Date()
       var dateStr = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate()
@@ -982,8 +999,23 @@ export default {
   font-size: 16px;
 }
 
-/* 加载状态 */
-.loading-overlay {
+/* 传统命理入口卡片 */
+.tradition-card {
+  background: linear-gradient(135deg, #F5F0FF 0%, #E0D4FF 100%);
+  box-shadow: 0 4px 12px rgba(139, 92, 246, 0.2);
+}
+
+.tradition-card .fortune-title {
+  color: #6D28D9;
+}
+
+.tradition-card .fortune-text {
+  color: #7C3AED;
+}
+
+.tradition-card .fortune-arrow {
+  color: #8B5CF6;
+}
   position: fixed;
   top: 0;
   left: 0;

+ 438 - 0
cfc-frontend/pages/tianpan/traditional.vue

@@ -0,0 +1,438 @@
+<template>
+  <view class="traditional-container">
+    <!-- 自定义导航栏 -->
+    <view class="custom-nav">
+      <view class="nav-back" @click="goBack">‹ 返回</view>
+      <text class="nav-title">家庭传统命理</text>
+    </view>
+
+    <!-- 加载状态 -->
+    <view class="loading-state" v-if="loading">
+      <view class="loading-spinner"></view>
+      <text>加载中...</text>
+    </view>
+
+    <!-- 成员列表 -->
+    <view class="member-list" v-if="!loading && members && members.length > 0">
+      <view class="member-card" v-for="member in members" :key="member.memberId">
+        <!-- 头部 -->
+        <view class="card-header">
+          <view class="avatar" :style="{ backgroundColor: member.colorHex || '#6B7280' }">
+            <text class="avatar-text">{{ member.nickname ? member.nickname.charAt(0) : 'M' }}</text>
+          </view>
+          <view class="member-info">
+            <text class="member-name">{{ member.nickname || member.name }}</text>
+            <text class="member-role">{{ getRoleLabel(member.memberType) }}</text>
+          </view>
+        </view>
+
+        <!-- 八字四柱 -->
+        <view class="section" v-if="member.eightCharacters">
+          <text class="section-title">八字四柱</text>
+          <view class="bazi-grid">
+            <view class="bazi-col" v-for="(val, key) in member.eightCharacters" :key="key">
+              <text class="bazi-label">{{ getPillarLabel(key) }}</text>
+              <text class="bazi-value">{{ val }}</text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 五行能量 -->
+        <view class="section" v-if="member.wuxingElements">
+          <text class="section-title">五行能量</text>
+          <view class="wuxing-list">
+            <view class="wuxing-item" v-for="wx in getWuxingData(member.wuxingElements)" :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>
+
+        <!-- 生肖星座 -->
+        <view class="section" v-if="member.zodiacName || member.westernSign">
+          <text class="section-title">生肖星座</text>
+          <view class="tag-row">
+            <view class="tag" v-if="member.zodiacName">
+              <text>{{ member.zodiacName }}</text>
+            </view>
+            <view class="tag" v-if="member.westernSign">
+              <text>{{ member.westernSign }}</text>
+            </view>
+          </view>
+        </view>
+
+        <!-- 生命灵数 -->
+        <view class="section" v-if="member.lifeNumber">
+          <text class="section-title">生命灵数</text>
+          <view class="numsoul-display">
+            <view class="numsoul-number" :style="{ background: getLifeNumberColor(member.lifeNumber) }">
+              <text>{{ member.lifeNumber }}</text>
+            </view>
+            <text class="numsoul-title">{{ getLifeNumberTitle(member.lifeNumber) }}</text>
+          </view>
+        </view>
+
+        <!-- 五维基础分 -->
+        <view class="section" v-if="member.dimensionBaseScores">
+          <text class="section-title">五维基础分</text>
+          <view class="dimension-bars">
+            <view class="dim-item" v-for="(val, key) in member.dimensionBaseScores" :key="key">
+              <text class="dim-label">{{ getDimLabel(key) }}</text>
+              <view class="dim-bar-track">
+                <view class="dim-bar-fill" :style="{ width: val + '%', background: getDimColor(key) }"></view>
+              </view>
+              <text class="dim-value">{{ val }}</text>
+            </view>
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 空状态 -->
+    <view class="empty-state" v-if="!loading && (!members || members.length === 0)">
+      <text>暂无家庭成员数据</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { mapState, mapGetters, mapActions } from 'vuex'
+
+export default {
+  data: function() {
+    return {
+      loading: false,
+      members: []
+    }
+  },
+  computed: {
+    ...mapState('tianpan', ['loading']),
+    ...mapGetters('tianpan', ['members'])
+  },
+  onLoad: function() {
+    this.loadMembers()
+  },
+  methods: {
+    ...mapActions('tianpan', ['loadDashboard']),
+
+    loadMembers: function() {
+      this.loading = true
+      if (!this.members || this.members.length === 0) {
+        this.loadDashboard({ year: new Date().getFullYear(), daysAhead: 7 })
+          .then(this.fetchMembers)
+          .finally(function() {
+            this.loading = false
+          }.bind(this))
+      } else {
+        this.fetchMembers()
+      }
+    },
+
+    fetchMembers: function() {
+      this.members = this.members || []
+    },
+
+    goBack: function() {
+      uni.navigateBack()
+    },
+
+    getPillarLabel: function(key) {
+      var labels = { year: '年柱', month: '月柱', day: '日柱', hour: '时柱' }
+      return labels[key] || key
+    },
+
+    getWuxingData: function(wuxingElements) {
+      if (!wuxingElements) return []
+      var colorMap = { wood: '#10B981', fire: '#FF6B9D', earth: '#FF8C42', metal: '#6366F1', water: '#3B82F6' }
+      var labelMap = { wood: '木', fire: '火', earth: '土', metal: '金', water: '水' }
+      var result = []
+      for (var key in wuxingElements) {
+        if (wuxingElements.hasOwnProperty(key)) {
+          result.push({ key: key, label: labelMap[key] || key, value: wuxingElements[key], color: colorMap[key] || '#94A3B8' })
+        }
+      }
+      return result.sort(function(a, b) { return b.value - a.value })
+    },
+
+    getRoleLabel: function(type) {
+      var labels = { parent: '家长', child: '孩子', teacher: '规划师' }
+      return labels[type] || type
+    },
+
+    getLifeNumberColor: function(num) {
+      var colors = { 1: '#F97316', 2: '#EC4899', 3: '#8B5CF6', 4: '#F59E0B', 5: '#3B82F6', 6: '#10B981', 7: '#6366F1', 8: '#EF4444', 9: '#14B8A6' }
+      return colors[num] || '#6B7280'
+    },
+
+    getLifeNumberTitle: function(num) {
+      var titles = { 1: '开创', 2: '协调', 3: '表达', 4: '稳定', 5: '自由', 6: '责任', 7: '求知', 8: '权力', 9: '博爱' }
+      return titles[num] || ''
+    },
+
+    getDimLabel: function(key) {
+      var labels = { body: '身', mind: '心', wisdom: '智', action: '行', wealth: '富' }
+      return labels[key] || key
+    },
+
+    getDimColor: function(key) {
+      var colors = { body: '#FF8C42', mind: '#FF6B9D', wisdom: '#6366F1', action: '#10B981', wealth: '#F59E0B' }
+      return colors[key] || '#6B7280'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.traditional-container {
+  min-height: 100vh;
+  background: #FFF8F0;
+  padding-bottom: 40px;
+}
+
+.custom-nav {
+  background: linear-gradient(135deg, #F97316 0%, #FB923C 100%);
+  padding: 44px 16px 16px;
+  display: flex;
+  align-items: center;
+}
+
+.nav-back {
+  font-size: 16px;
+  color: #FFFFFF;
+  margin-right: 16px;
+}
+
+.nav-title {
+  font-size: 18px;
+  color: #FFFFFF;
+  font-weight: bold;
+}
+
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  padding: 120px 0;
+}
+
+.loading-spinner {
+  width: 40px;
+  height: 40px;
+  border: 4px solid #E5E7EB;
+  border-top-color: #F97316;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+  margin-bottom: 16px;
+}
+
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+
+.member-list {
+  padding: 16px;
+}
+
+.member-card {
+  background: #FFFFFF;
+  border-radius: 16px;
+  padding: 20px;
+  margin-bottom: 16px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
+.card-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+}
+
+.avatar {
+  width: 64px;
+  height: 64px;
+  border-radius: 50%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-right: 16px;
+}
+
+.avatar-text {
+  color: #FFFFFF;
+  font-size: 28px;
+  font-weight: bold;
+}
+
+.member-info {
+  flex: 1;
+}
+
+.member-name {
+  font-size: 18px;
+  font-weight: bold;
+  color: #1F2937;
+  display: block;
+}
+
+.member-role {
+  font-size: 14px;
+  color: #6B7280;
+}
+
+.section {
+  margin-bottom: 20px;
+}
+
+.section-title {
+  font-size: 14px;
+  color: #9CA3AF;
+  margin-bottom: 12px;
+  font-weight: 500;
+}
+
+.bazi-grid {
+  display: flex;
+  justify-content: space-around;
+}
+
+.bazi-col {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.bazi-label {
+  font-size: 12px;
+  color: #9CA3AF;
+  margin-bottom: 4px;
+}
+
+.bazi-value {
+  font-size: 24px;
+  font-weight: bold;
+  color: #8B5CF6;
+}
+
+.wuxing-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.wuxing-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.wuxing-label {
+  font-size: 14px;
+  width: 24px;
+  text-align: center;
+}
+
+.wuxing-bar-track {
+  flex: 1;
+  height: 8px;
+  background: #E5E7EB;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.wuxing-bar-fill {
+  height: 100%;
+  border-radius: 4px;
+}
+
+.wuxing-value {
+  font-size: 12px;
+  color: #6B7280;
+  width: 36px;
+  text-align: right;
+}
+
+.tag-row {
+  display: flex;
+  gap: 12px;
+  flex-wrap: wrap;
+}
+
+.tag {
+  background: #F3F4F6;
+  padding: 6px 16px;
+  border-radius: 20px;
+  font-size: 14px;
+  color: #374151;
+}
+
+.numsoul-display {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.numsoul-number {
+  width: 64px;
+  height: 64px;
+  border-radius: 50%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 28px;
+  font-weight: bold;
+  color: #FFFFFF;
+}
+
+.numsoul-title {
+  font-size: 16px;
+  color: #374151;
+  font-weight: 500;
+}
+
+.dimension-bars {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.dim-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.dim-label {
+  font-size: 14px;
+  width: 24px;
+}
+
+.dim-bar-track {
+  flex: 1;
+  height: 8px;
+  background: #E5E7EB;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.dim-bar-fill {
+  height: 100%;
+  border-radius: 4px;
+}
+
+.dim-value {
+  font-size: 12px;
+  color: #6B7280;
+  width: 36px;
+  text-align: right;
+}
+
+.empty-state {
+  text-align: center;
+  padding: 120px 0;
+  color: #9CA3AF;
+}
+</style>