浏览代码

feat: 小程序先天画像轨迹页+心维度首页入口+补create接口

Xiaogang Liao 3 周之前
父节点
当前提交
beedd50068

+ 6 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/NumSoulDetailConfigController.java

@@ -28,6 +28,12 @@ public class NumSoulDetailConfigController {
         return Result.success(numSoulDetailConfigMapper.selectList(wrapper));
     }
 
+    @PostMapping("/create")
+    public Result<Void> create(@RequestBody NumSoulDetailConfig config) {
+        numSoulDetailConfigMapper.insert(config);
+        return Result.success(null);
+    }
+
     @PostMapping("/update")
     public Result<Void> update(@RequestBody NumSoulDetailConfig config) {
         if (config.getId() == null) {

+ 15 - 2
cfc-frontend/pages/mind-detail/index.vue

@@ -234,7 +234,8 @@ components: { TabTransition, PageBanner, RadarChart, FamilyEnergyBar, PsychCrisi
         { icon: '\u{1F4C8}', label: '情绪趋势', needLogin: true, page: '/pages/mind-detail/emotion-trend' },
         { icon: '\u{1F9E9}', label: '心理自评', needLogin: true, page: '/pages/mind-detail/screening' },
         { icon: '\u{1F3E0}', label: '家庭天盘', needLogin: true, page: '/pages/mind-detail/family-dashboard' },
-        { icon: '\u{1F9D8}', label: '疏导工具', needLogin: true, page: '/pages/mind-detail/soothe-toolbox' }
+        { icon: '\u{1F9D8}', label: '疏导工具', needLogin: true, page: '/pages/mind-detail/soothe-toolbox' },
+        { icon: '\u{1F31F}', label: '先天画像', needLogin: true, page: '/pages/mind-detail/innate-portrait' }
       ],
       articles: [],
 
@@ -725,7 +726,19 @@ var descList = descMap[this.dominantElement] || descMap.wood
         })
         return
       }
-      if (item.page) nav.goPage(item.page)
+      if (item.page) {
+        var url = item.page
+        if (item.page.indexOf('innate-portrait') !== -1 || item.page.indexOf('innate-trajectory') !== -1) {
+          var memberId = this.currentChildId
+          if (!memberId) {
+            uni.showToast({ title: '请先选择家庭成员', icon: 'none' })
+            return
+          }
+          var familyId = (this.$store.state && this.$store.state.familyId) || uni.getStorageSync('familyId') || ''
+          url += '?memberId=' + memberId + '&memberType=child&familyId=' + familyId
+        }
+        nav.goPage(url)
+      }
     },
     // ===== 孩子列表与维度数据 =====
     loadChildren: function() {

+ 215 - 0
cfc-frontend/pages/mind-detail/innate-trajectory.vue

@@ -0,0 +1,215 @@
+<template>
+  <view class="trajectory-container">
+    <view class="loading-state" v-if="loading">
+      <text>加载中...</text>
+    </view>
+
+    <template v-if="!loading && trajectory">
+      <view class="section">
+        <view class="section-title">先天 vs 后天</view>
+        <view class="compare-row">
+          <view class="compare-item">
+            <text class="compare-label">先天心能量</text>
+            <text class="compare-value">{{ trajectory.innateVsCurrent.mindInnate || 0 }}</text>
+          </view>
+          <view class="compare-item">
+            <text class="compare-label">当前心能量</text>
+            <text class="compare-value">{{ trajectory.innateVsCurrent.mindCurrent || 0 }}</text>
+          </view>
+          <view class="compare-item">
+            <text class="compare-label">成长</text>
+            <text class="compare-value" :style="{ color: growthColor }">{{ trajectory.innateVsCurrent.growth || '0' }}</text>
+          </view>
+        </view>
+      </view>
+
+      <view class="section" v-if="hasInnateScores">
+        <view class="section-title">五维先天基础分</view>
+        <view class="dim-row" v-for="(score, dim) in trajectory.innateDimensionScores" :key="dim">
+          <text class="dim-label">{{ dimLabel(dim) }}</text>
+          <view class="dim-track">
+            <view class="dim-fill" :style="{ width: score + '%' }"></view>
+          </view>
+          <text class="dim-value">{{ score }}</text>
+        </view>
+      </view>
+
+      <view class="section" v-if="trajectory.growthTrend">
+        <view class="section-title">成长趋势</view>
+        <text class="trend-text">{{ trajectory.growthTrend }}</text>
+      </view>
+
+      <view class="section" v-if="trajectory.advice">
+        <view class="section-title">成长建议</view>
+        <text class="advice-text">{{ trajectory.advice }}</text>
+      </view>
+    </template>
+
+    <view class="empty-state" v-if="!loading && !trajectory && loadError">
+      <text>{{ loadError }}</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getInnateTrajectory } from '../../utils/api'
+export default {
+  data() {
+    return {
+      memberId: null,
+      memberType: 'child',
+      familyId: null,
+      loading: false,
+      trajectory: null,
+      loadError: '',
+      dimNames: { body: '身', mind: '心', wisdom: '智', action: '行', wealth: '富' }
+    }
+  },
+  computed: {
+    hasInnateScores() {
+      return !!this.trajectory && !!this.trajectory.innateDimensionScores
+    },
+    growthColor() {
+      if (!this.trajectory || !this.trajectory.innateVsCurrent) return '#999'
+      var g = this.trajectory.innateVsCurrent.growth || '0'
+      if (g.indexOf('-') === 0) return '#f56c6c'
+      return '#07c160'
+    }
+  },
+  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
+      getInnateTrajectory({ memberId: self.memberId, memberType: self.memberType, familyId: self.familyId }).then(function (res) {
+        self.loading = false
+        if (res && res.code === 200) {
+          self.trajectory = res.data
+        } else {
+          self.loadError = (res && res.message) || '加载失败'
+        }
+      }).catch(function () {
+        self.loading = false
+        self.loadError = '网络异常'
+      })
+    },
+    dimLabel(dim) {
+      return this.dimNames[dim] || dim
+    }
+  }
+}
+</script>
+
+<style scoped>
+.trajectory-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;
+}
+
+.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;
+}
+
+.compare-row {
+  display: flex;
+  justify-content: space-around;
+}
+
+.compare-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.compare-label {
+  font-size: 24rpx;
+  color: #999;
+  margin-bottom: 8rpx;
+}
+
+.compare-value {
+  font-size: 48rpx;
+  font-weight: 700;
+  color: #333;
+}
+
+.dim-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+
+.dim-label {
+  font-size: 28rpx;
+  font-weight: 600;
+  width: 48rpx;
+  text-align: center;
+}
+
+.dim-track {
+  flex: 1;
+  height: 24rpx;
+  background: #F0F0F0;
+  border-radius: 12rpx;
+  overflow: hidden;
+  margin: 0 16rpx;
+}
+
+.dim-fill {
+  height: 100%;
+  border-radius: 12rpx;
+  background: linear-gradient(90deg, #FF6B9D, #FF8C42);
+}
+
+.dim-value {
+  font-size: 24rpx;
+  color: #666;
+  width: 60rpx;
+  text-align: right;
+}
+
+.trend-text, .advice-text {
+  font-size: 28rpx;
+  color: #444;
+  line-height: 1.8;
+}
+
+.empty-state {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 120rpx 0;
+  color: #999;
+  font-size: 28rpx;
+}
+</style>