فهرست منبع

feat: 小程序先天画像API封装+页面注册+报告页

Xiaogang Liao 2 ماه پیش
والد
کامیت
d09ed317de
3فایلهای تغییر یافته به همراه341 افزوده شده و 0 حذف شده
  1. 12 0
      cfc-frontend/pages.json
  2. 323 0
      cfc-frontend/pages/mind-detail/innate-portrait.vue
  3. 6 0
      cfc-frontend/utils/api.js

+ 12 - 0
cfc-frontend/pages.json

@@ -1105,6 +1105,18 @@
           "style": {
             "navigationBarTitleText": "双人合盘"
           }
+        },
+        {
+          "path": "innate-portrait",
+          "style": {
+            "navigationBarTitleText": "先天画像"
+          }
+        },
+        {
+          "path": "innate-trajectory",
+          "style": {
+            "navigationBarTitleText": "先天成长轨迹"
+          }
         }
       ]
     },

+ 323 - 0
cfc-frontend/pages/mind-detail/innate-portrait.vue

@@ -0,0 +1,323 @@
+<template>
+  <view class="portrait-container">
+    <view class="loading-state" v-if="loading">
+      <text>解读生成中...</text>
+    </view>
+
+    <template v-if="!loading && report">
+      <view class="portrait-header">
+        <text class="header-score">{{ report.portrait.mindBaseScore || 0 }}</text>
+        <text class="header-label">心维度先天能量</text>
+      </view>
+
+      <view class="section">
+        <view class="section-title">先天画像</view>
+        <view class="bazi-grid" v-if="hasEightChars">
+          <view class="bazi-column" v-for="(pillar, key) in report.portrait.eightCharacters" :key="key">
+            <text class="bazi-label">{{ pillarLabel(key) }}</text>
+            <text class="bazi-value">{{ pillar }}</text>
+          </view>
+        </view>
+        <view class="info-row" v-if="report.portrait.zodiac">
+          <text class="info-label">生肖</text>
+          <text class="info-value">{{ report.portrait.zodiac }}</text>
+        </view>
+        <view class="info-row" v-if="report.portrait.bloodType">
+          <text class="info-label">血型</text>
+          <text class="info-value">{{ report.portrait.bloodType }}</text>
+        </view>
+        <view class="info-row" v-if="numSoulData">
+          <text class="info-label">生命灵数</text>
+          <text class="info-value">{{ numSoulData.lifePath }} {{ numSoulData.lifePathTitle || '' }}</text>
+        </view>
+      </view>
+
+      <view class="section" v-if="report.aiReading">
+        <view class="section-title">
+          <text>AI 解读</text>
+          <text class="fallback-tag" v-if="report.fallbackUsed">模板解读</text>
+        </view>
+        <text class="reading-text">{{ report.aiReading }}</text>
+      </view>
+
+      <view class="section" v-if="hasWuxing">
+        <view class="section-title">五行能量</view>
+        <view class="wuxing-item" v-for="(wv, wk) in report.portrait.wuxingElements" :key="wk">
+          <text class="wuxing-label">{{ wuxingLabel(wk) }}</text>
+          <view class="wuxing-track">
+            <view class="wuxing-fill" :style="{ width: wv + '%', background: wuxingColor(wk) }"></view>
+          </view>
+          <text class="wuxing-value">{{ wv }}%</text>
+        </view>
+      </view>
+
+      <view class="trajectory-entry" @click="goTrajectory">
+        <text>查看先天+后天成长轨迹</text>
+        <text class="arrow">›</text>
+      </view>
+    </template>
+
+    <view class="empty-state" v-if="!loading && !report && loadError">
+      <text>{{ loadError }}</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getInnateReading } from '../../utils/api'
+export default {
+  data() {
+    return {
+      memberId: null,
+      memberType: 'child',
+      familyId: null,
+      loading: false,
+      report: null,
+      loadError: '',
+      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: {
+    numSoulData() {
+      if (!this.report || !this.report.portrait || !this.report.portrait.numSoul) return null
+      return this.report.portrait.numSoul
+    },
+    hasEightChars() {
+      return !!this.report && !!this.report.portrait && !!this.report.portrait.eightCharacters
+    },
+    hasWuxing() {
+      return !!this.report && !!this.report.portrait && !!this.report.portrait.wuxingElements
+    }
+  },
+  onLoad(options) {
+    this.memberId = options.memberId ? Number(options.memberId) : null
+    this.memberType = options.memberType || 'child'
+    this.familyId = options.familyId ? Number(options.familyId) : null
+    if (!this.memberId) {
+      this.loadError = '缺少成员ID'
+      return
+    }
+    this.load()
+  },
+  methods: {
+    load() {
+      var self = this
+      self.loading = true
+      getInnateReading({ memberId: self.memberId, memberType: self.memberType, familyId: self.familyId }).then(function (res) {
+        self.loading = false
+        if (res && res.code === 200) {
+          self.report = res.data
+        } else {
+          self.loadError = (res && res.message) || '加载失败'
+        }
+      }).catch(function () {
+        self.loading = false
+        self.loadError = '网络异常'
+      })
+    },
+    pillarLabel(key) {
+      return this.pillarLabels[key] || key
+    },
+    wuxingLabel(key) {
+      var cfg = this.wuxingConfig[key]
+      return cfg ? cfg.label : key
+    },
+    wuxingColor(key) {
+      var cfg = this.wuxingConfig[key]
+      return cfg ? cfg.color : '#ccc'
+    },
+    goTrajectory() {
+      uni.navigateTo({
+        url: '/pages/mind-detail/innate-trajectory?memberId=' + this.memberId + '&memberType=' + this.memberType + '&familyId=' + (this.familyId || '')
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.portrait-container {
+  min-height: 100vh;
+  background: #F8F6F0;
+  padding-bottom: 60rpx;
+}
+
+.loading-state {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 0;
+  color: #999;
+  font-size: 28rpx;
+}
+
+.portrait-header {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 60rpx 32rpx 40rpx;
+  background: linear-gradient(135deg, #FF6B9D 0%, #FF8C42 100%);
+}
+
+.header-score {
+  font-size: 96rpx;
+  font-weight: 700;
+  color: #fff;
+  line-height: 1.1;
+}
+
+.header-label {
+  font-size: 28rpx;
+  color: rgba(255,255,255,0.9);
+  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-title {
+  font-size: 30rpx;
+  font-weight: 600;
+  color: #333;
+  margin-bottom: 20rpx;
+  display: flex;
+  align-items: center;
+}
+
+.fallback-tag {
+  font-size: 20rpx;
+  color: #999;
+  background: #F0F0F0;
+  border-radius: 8rpx;
+  padding: 4rpx 12rpx;
+  margin-left: 16rpx;
+}
+
+.bazi-grid {
+  display: flex;
+  justify-content: space-around;
+  padding: 16rpx 0;
+}
+
+.bazi-column {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.bazi-label {
+  font-size: 22rpx;
+  color: #999;
+  margin-bottom: 8rpx;
+}
+
+.bazi-value {
+  font-size: 36rpx;
+  font-weight: 700;
+  color: #8B5CF6;
+  background: #F5F0FF;
+  padding: 12rpx 16rpx;
+  border-radius: 12rpx;
+}
+
+.info-row {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16rpx 0;
+  border-bottom: 1rpx solid #F5F5F5;
+}
+
+.info-row:last-child {
+  border-bottom: none;
+}
+
+.info-label {
+  font-size: 28rpx;
+  color: #999;
+}
+
+.info-value {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+}
+
+.reading-text {
+  font-size: 28rpx;
+  color: #444;
+  line-height: 1.8;
+}
+
+.wuxing-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+
+.wuxing-label {
+  font-size: 28rpx;
+  font-weight: 600;
+  width: 48rpx;
+  text-align: center;
+}
+
+.wuxing-track {
+  flex: 1;
+  height: 24rpx;
+  background: #F0F0F0;
+  border-radius: 12rpx;
+  overflow: hidden;
+  margin: 0 16rpx;
+}
+
+.wuxing-fill {
+  height: 100%;
+  border-radius: 12rpx;
+}
+
+.wuxing-value {
+  font-size: 24rpx;
+  color: #666;
+  width: 60rpx;
+  text-align: right;
+}
+
+.trajectory-entry {
+  margin: 24rpx 32rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  color: #FF6B9D;
+  font-size: 28rpx;
+  font-weight: 500;
+}
+
+.arrow {
+  font-size: 36rpx;
+}
+
+.empty-state {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 0;
+  color: #999;
+  font-size: 28rpx;
+}
+</style>

+ 6 - 0
cfc-frontend/utils/api.js

@@ -2087,6 +2087,12 @@ export const getNutritionTrend = (memberId) => request('/api/health/nutrition/tr
 export const getHealthAnalysis = (memberId) => request('/api/health/analysis', 'POST', { memberId })
 export const getNutritionMappings = (data) => request('/api/health/nutrition/mappings', 'POST', data || {})
 
+// ===== 先天画像 =====
+export const getInnatePortrait = (data) => request('/api/mind/innate/portrait', 'POST', data)
+export const getInnateReading = (data) => request('/api/mind/innate/reading', 'POST', data)
+export const getInnateNumsoul = (data) => request('/api/mind/innate/numsoul', 'POST', data)
+export const getInnateTrajectory = (data) => request('/api/mind/innate/trajectory', 'POST', data)
+
 // ===== 健康维度 =====
 export const getDimensionOverview = (data) => request('/api/dimension/overview', 'POST', data)
 export const uploadDimensionScore = (data) => request('/api/dimension/upload', 'POST', data)