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

chore: auto bump version and changelog [skip ci]

iwt 2 месяцев назад
Родитель
Сommit
6ed057447e

+ 51 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/HealthReportService.java

@@ -9,6 +9,8 @@ import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.HealthDiseaseRiskMapper;
 import com.etotem.cfc.mapper.HealthGutFloraMapper;
 import com.etotem.cfc.mapper.HealthIndicatorMapper;
+import com.etotem.cfc.mapper.IndicatorDefinitionMapper;
+import com.etotem.cfc.mapper.IndicatorValueMapper;
 import com.etotem.cfc.mapper.HealthReportDraftMapper;
 import com.etotem.cfc.mapper.HealthReportEditLogMapper;
 import com.etotem.cfc.mapper.HealthReportMapper;
@@ -62,6 +64,12 @@ public class HealthReportService {
     @Resource
     private UserMapper userMapper;
 
+    @Resource
+    private IndicatorDefinitionMapper indicatorDefinitionMapper;
+
+    @Resource
+    private IndicatorValueMapper indicatorValueMapper;
+
     @Resource
     private HealthReportPermissionMapper healthReportPermissionMapper;
 
@@ -1636,4 +1644,46 @@ public class HealthReportService {
             log.info("健康报告已删除: id={}", reportId);
         }
     }
-}
+
+    /**
+     * 获取某成员某维度的最新指标值列表(用于首页卡片展示)
+     */
+    public List<Map<String, Object>> getLatestIndicatorsByDomain(Long memberId, String domain) {
+        // 1. 获取该域下所有指标定义
+        LambdaQueryWrapper<IndicatorDefinition> defW = new LambdaQueryWrapper<>();
+        defW.eq(IndicatorDefinition::getDomain, domain).eq(IndicatorDefinition::getStatus, 1);
+        defW.orderByAsc(IndicatorDefinition::getSortOrder);
+        List<IndicatorDefinition> defs = indicatorDefinitionMapper.selectList(defW);
+        if (defs.isEmpty()) return Collections.emptyList();
+
+        // 2. 对每个定义,获取该成员的最新值
+        List<Map<String, Object>> result = new java.util.ArrayList<>();
+        for (IndicatorDefinition def : defs) {
+            LambdaQueryWrapper<IndicatorValue> w = new LambdaQueryWrapper<>();
+            w.eq(IndicatorValue::getSourceId, memberId)
+             .eq(IndicatorValue::getDefinitionId, def.getId())
+             .orderByDesc(IndicatorValue::getRecordedAt)
+             .last("LIMIT 1");
+            IndicatorValue latest = indicatorValueMapper.selectOne(w);
+            Map<String, Object> item = new HashMap<>();
+            item.put("code", def.getCode());
+            item.put("name", def.getName());
+            item.put("category", def.getCategory());
+            item.put("unit", def.getUnit());
+            item.put("refRange", def.getRefRange());
+            if (latest != null) {
+                item.put("value", latest.getValue());
+                item.put("numericValue", latest.getNumericValue() != null ? latest.getNumericValue().doubleValue() : null);
+                item.put("remark", latest.getRemark());
+                item.put("recordedAt", latest.getRecordedAt());
+            } else {
+                item.put("value", null);
+                item.put("numericValue", null);
+                item.put("remark", null);
+                item.put("recordedAt", null);
+            }
+            result.add(item);
+        }
+        return result;
+    }
+}

+ 76 - 38
cfc-frontend/pages/body-detail/index.vue

@@ -33,6 +33,37 @@
         @dimensionClick="goDimensionDetail" />
     </view>
 
+    <!-- 身体指标卡片 -->
+    <view class="indicator-cards-section" v-if="isLoggedIn && bodyIndicators.length > 0">
+      <view class="section-header">
+        <text class="section-title">📊 身体指标</text>
+        <text class="section-more" @click="goToBodyDetail">更多 ›</text>
+      </view>
+      <view class="indicator-cards">
+        <view
+          v-for="(ind, i) in bodyIndicators"
+          :key="ind.code"
+          class="indicator-card"
+          :class="{ 'no-data': !ind.value }"
+          @click="goIndicatorDetail(ind)"
+        >
+          <view class="indicator-header">
+            <text class="indicator-name">{{ ind.name }}</text>
+            <text class="indicator-unit" v-if="ind.unit">{{ ind.unit }}</text>
+          </view>
+          <view class="indicator-body">
+            <text class="indicator-value" v-if="ind.value || ind.numericValue !== null">
+              {{ ind.value || formatNumeric(ind.numericValue) }}
+            </text>
+            <text class="indicator-placeholder" v-else>暂无数据</text>
+          </view>
+          <view class="indicator-footer">
+            <text class="indicator-range" v-if="ind.refRange">参考: {{ ind.refRange }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
     <!-- 家庭成员选择条 -->
     <FamilyMemberStrip
       v-if="currentView === 'self' && isLoggedIn"
@@ -53,15 +84,7 @@
       dimensionCode="body"
       :sandboxData="sandboxData"
       :dualDimension="dualDimension" />
-    <!-- 健康七维 -->
-    <dimension-sub-dims
-      v-if="isLoggedIn && healthSubDims.length > 0"
-      title="🌿 健康七维"
-      subtitle="身体各项健康指标,分数越高代表越健康"
-      accent-color="#FF8C42"
-      :dims="healthSubDims" />
-
-    <!-- 功能入口 -->
+    <!-- 身体指标卡片 -->
     <view class="func-section" v-if="sectionVisible('func_entries')">
       <view class="func-grid">
         <view class="func-item" v-for="item in funcList" :key="item.label" @click="onFuncClick(item)">
@@ -72,32 +95,6 @@
         </view>
       </view>
     </view>
-
-    <!-- ===== 精准营养(上传报告+产品+文章) ===== -->
-    <view class="section nutrition-section" v-if="isLoggedIn">
-      <view class="section-header">
-        <text class="section-title">🥗 精准营养</text>
-      </view>
-
-      <!-- 上传报告入口 -->
-      <view class="nutrition-report-card" @click="goToUploadReport">
-        <view class="nutrition-report-left">
-          <text class="nutrition-icon-large">📋</text>
-        </view>
-        <view class="nutrition-report-right">
-          <text class="nutrition-title">上传健康报告</text>
-          <text class="nutrition-desc">上传体检报告或肠道检测报告,解读分析,获取个性化营养方案</text>
-          <view class="nutrition-report-tags">
-            <text class="tag">体检报告</text>
-            <text class="tag">肠道检测</text>
-            <text class="tag">解读</text>
-          </view>
-        </view>
-        <text class="nutrition-arrow">›</text>
-      </view>
-
-      </view>
-
     <!-- 身维度介绍卡片(登录后显示) -->
     <DimensionIntroCard dimCode="body" v-if="isLoggedIn" learn-more-url="/pages/body-detail/dimension-intro" />
 
@@ -112,14 +109,14 @@ import TabTransition from '../../components/tab-transition.vue'
 import PageBanner from '../../components/PageBanner.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import RadarChart from '../../components/RadarChart.vue'
-import DimensionSubDims from '../../components/DimensionSubDims.vue'
+
 import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 import { getVisibleSections, getEnergyOverview, getChildren, getFamilyEnergySandbox, getEnergySandbox, getFamilyMemberList, getDimensionOverview } from '../../utils/api.js'
 
 export default {
-  components: { TabTransition, PageBanner, FamilyEnergyBar, RadarChart, DimensionSubDims, FloatingAvatar, FamilyMemberStrip, DimensionIntroCard },
+  components: { TabTransition, PageBanner, FamilyEnergyBar, RadarChart, FloatingAvatar, FamilyMemberStrip, DimensionIntroCard },
   data() {
     return {
       isLoggedIn: false,
@@ -135,6 +132,7 @@ export default {
       childrenChecked: false,
       membersChecked: false,
             healthSubDims: [],
+      bodyIndicators: [],
       sandboxData: null,
       currentView: 'self', // 'self' | 'family'
       dualDimension: null,
@@ -295,7 +293,7 @@ export default {
       getDimensionOverview({ memberId: memberId }).then(function(res) {
         if (res && res.data) {
           self.dimensionData = res.data
-        self.healthSubDims = (self.dimensionData.dimensions || []).map(function(d) { return {label: d.label || d.dimension, score: d.score || 0, desc: d.description || ''}; });
+          self.healthSubDims = (self.dimensionData.dimensions || []).map(function(d) { return {label: d.label || d.dimension, score: d.score || 0, desc: d.description || ''}; });
           console.log('[body] dimensionData loaded:', res.data.dimensions ? res.data.dimensions.length : 0, 'dimensions')
         } else if (res) {
           console.warn('[body] dimension API error:', res.message || res.code)
@@ -303,6 +301,18 @@ export default {
       }).catch(function(e) {
         console.error('[body] loadHealthDimensions failed', e)
       })
+      // 加载身体指标列表
+      self.loadBodyIndicators(memberId)
+    },
+    loadBodyIndicators: function(memberId) {
+      var self = this
+      getLatestIndicators({ memberId: memberId, domain: 'body' }).then(function(res) {
+        if (res.code === 200) {
+          self.bodyIndicators = res.data || []
+        }
+      }).catch(function(e) {
+        console.log('[body] loadBodyIndicators failed', e)
+      })
     },
     loadSandboxData: function() {
       var self = this
@@ -411,6 +421,22 @@ export default {
       }
       uni.navigateTo({ url: '/pages/health/nutrition-profile?memberId=' + id })
     },
+    goToBodyDetail: function() {
+      var id = this.activeChildId || uni.getStorageSync('currentChildId')
+      if (!id) return
+      uni.navigateTo({ url: '/pages/body/member-body-detail?memberId=' + id })
+    },
+    goIndicatorDetail: function(ind) {
+      var id = this.activeChildId || uni.getStorageSync('currentChildId')
+      if (!id) return
+      uni.navigateTo({ url: '/pages/body-detail/dimension-detail?dimension=' + ind.code + '&memberId=' + id })
+    },
+    formatNumeric: function(v) {
+      if (v === null || v === undefined) return ''
+      var n = parseFloat(v)
+      if (isNaN(n)) return String(v)
+      return n % 1 === 0 ? String(n) : n.toFixed(1)
+    },
     goToUploadReport: function() {
       var id = this.activeChildId || uni.getStorageSync('currentChildId') || ''
       uni.navigateTo({ url: '/pages/health/report-upload?memberId=' + id + '&from=body' })
@@ -535,6 +561,18 @@ export default {
   height: 120rpx;
 }
 
+.indicator-cards-section { margin: 20rpx 30rpx; }
+.indicator-cards { display: flex; flex-direction: row; flex-wrap: wrap; gap: 16rpx; }
+.indicator-card { width: calc(50% - 8rpx); background: #fff; border-radius: 20rpx; padding: 24rpx 20rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); }
+.indicator-card.no-data { opacity: 0.5; }
+.indicator-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12rpx; }
+.indicator-name { font-size: 26rpx; color: #666; font-weight: 500; }
+.indicator-unit { font-size: 22rpx; color: #999; }
+.indicator-body { margin-bottom: 8rpx; }
+.indicator-value { font-size: 40rpx; font-weight: bold; color: #FF8C42; }
+.indicator-placeholder { font-size: 26rpx; color: #ccc; }
+.indicator-range { font-size: 20rpx; color: #bbb; }
+
 .func-item:active, .action-btn:active {
   opacity: 0.7;
 }

+ 31 - 10
cfc-frontend/pages/mind-detail/index.vue

@@ -42,13 +42,34 @@
           v-if="isLoggedIn"
           dimensionCode="mind"
           :sandboxData="sandboxData" />
-    <!-- 心理维度 -->
-    <dimension-sub-dims
-      v-if="isLoggedIn && mindSubDims.length > 0"
-      title="🔥 心理维度"
-      subtitle="情绪与性格的综合评估"
-      accent-color="#FF6B9D"
-      :dims="mindSubDims" />
+        <!-- 心理指标卡片 -->
+        <view class="indicator-cards-section" v-if="isLoggedIn && mindIndicators.length > 0">
+          <view class="section-header">
+            <text class="section-title">🔥 心理指标</text>
+            <text class="section-more" @click="goMindDetail">更多 ›</text>
+          </view>
+          <view class="indicator-cards">
+            <view
+              v-for="(ind, i) in mindIndicators"
+              :key="ind.code"
+              class="indicator-card"
+              :class="{ 'no-data': !ind.value }"
+              @click="goMindDetail()"
+            >
+              <view class="indicator-header">
+                <text class="indicator-name">{{ ind.name }}</text>
+                <text class="indicator-unit" v-if="ind.unit">{{ ind.unit }}</text>
+              </view>
+              <view class="indicator-body">
+                <text class="indicator-value" v-if="ind.value || ind.numericValue !== null">{{ ind.value || formatNumeric(ind.numericValue) }}</text>
+                <text class="indicator-placeholder" v-else>暂无数据</text>
+              </view>
+              <view class="indicator-footer">
+                <text class="indicator-range" v-if="ind.refRange">参考: {{ ind.refRange }}</text>
+              </view>
+            </view>
+          </view>
+        </view>
 
         <!-- 心理报告卡片(登录后可见) -->
         <view class="emi-card" v-if="isLoggedIn && emiData">
@@ -167,19 +188,18 @@ import TabTransition from '../../components/tab-transition.vue'
 import PageBanner from '../../components/PageBanner.vue'
 import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import RadarChart from '../../components/RadarChart.vue'
-import DimensionSubDims from '../../components/DimensionSubDims.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import PsychCrisisBanner from '../../components/PsychCrisisBanner.vue'
 import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import FamilyTianpanCard from '../../components/FamilyTianpanCard.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
-import { getVisibleSections, getEmiReport, getFamilyEnergySandbox, getEnergySandbox, getChildren, getFamilyMemberList, getEnergyOverview, getContactList, getHealthAlerts, getMilestones, getDanReportByChild, getFamilyFortune } from '../../utils/api.js'
+import { getVisibleSections, getEmiReport, getFamilyEnergySandbox, getEnergySandbox, getChildren, getFamilyMemberList, getEnergyOverview, getContactList, getHealthAlerts, getMilestones, getDanReportByChild, getFamilyFortune, getLatestIndicators } from '../../utils/api.js'
 import nav from '../../utils/nav.js'
 import config from '../../config.js'
 import { parseDate } from '../../utils/format.js'
 
 export default {
-components: { TabTransition, PageBanner, RadarChart, DimensionSubDims, FamilyEnergyBar, PsychCrisisBanner, FloatingAvatar, FamilyTianpanCard, FamilyMemberStrip, DimensionIntroCard },
+components: { TabTransition, PageBanner, RadarChart, FamilyEnergyBar, PsychCrisisBanner, FloatingAvatar, FamilyTianpanCard, FamilyMemberStrip, DimensionIntroCard },
   data() {
     return {
       isLoggedIn: false,
@@ -240,6 +260,7 @@ components: { TabTransition, PageBanner, RadarChart, DimensionSubDims, FamilyEne
       userName: '',
       children: [],
             mindSubDims: [],
+            mindIndicators: [],
       sandboxData: null,
       currentView: 'self', // 'self' | 'family'
       contactList: [],

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

@@ -1929,6 +1929,7 @@ export const getProductsByDomain = (domain, page, size) => {
 // ===== 鍋ュ悍鎶ュ憡锛堜袱闃舵鍏ュ簱锛?=====
 export const getLatestHealthReport = (memberId) => request('/api/health/report/latest', 'POST', { memberId })
 export const getHealthIndicatorList = (memberId) => request('/api/health/indicator/list', 'POST', { memberId })
+export const getLatestIndicators = (data) => request('/api/health/indicators', 'POST', data)
 
 
 export const createHealthReport = (data) => request('/api/health/report/create', 'POST', data)

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-300007db3be406978f7ff787b7c92bf95cf7eb87
+cd5b65ac9b1173178e36557a0f3547ce136602bf

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1171",
+  "version": "1.0.1172",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1171",
+      "version": "1.0.1172",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",