Prechádzať zdrojové kódy

refactor: rename FamilyRelationshipSection -> OctopusGraph; fix: health chart shows zeros when no data

User 2 mesiacov pred
rodič
commit
5e64094623

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/ActivityDTO.java

@@ -12,6 +12,8 @@ public class ActivityDTO {
     private String description;
     private String coverImage;
     private String dimensionCode;
+    /** JSON: {"body":0,"mind":0,"wisdom":0,"action":0,"wealth":0} */
+    private String dimensionWeights;
     private String activityType;
     private String status;
     private String startTime;
@@ -38,6 +40,7 @@ public class ActivityDTO {
         dto.setDescription(activity.getDescription());
         dto.setCoverImage(activity.getCoverImage());
         dto.setDimensionCode(activity.getDimensionCode());
+        dto.setDimensionWeights(activity.getDimensionWeights());
         dto.setActivityType(activity.getActivityType());
         dto.setStatus(activity.getStatus());
         dto.setStartTime(activity.getStartTime() != null ? activity.getStartTime().toString() : null);

+ 3 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Activity.java

@@ -24,6 +24,9 @@ public class Activity implements Serializable {
     /** body/mind/wisdom/action/wealth */
     private String dimensionCode;
 
+    /** JSON: {"body":0,"mind":0,"wisdom":0,"action":0,"wealth":0} */
+    private String dimensionWeights;
+
     /** offline/online/campaign */
     private String activityType;
 

+ 36 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DimensionWeightService.java

@@ -3,6 +3,7 @@ package com.etotem.cfc.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.entity.DimensionWeight;
 import com.etotem.cfc.mapper.DimensionWeightMapper;
+import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -16,6 +17,9 @@ public class DimensionWeightService {
     @Resource
     private DimensionWeightMapper dimensionWeightMapper;
 
+    @Resource
+    private JdbcTemplate jdbcTemplate;
+
     public List<DimensionWeight> getByTarget(String targetType, Long targetId) {
         return dimensionWeightMapper.selectList(
                 new LambdaQueryWrapper<DimensionWeight>()
@@ -25,6 +29,7 @@ public class DimensionWeightService {
 
     @Transactional
     public void saveWeights(String targetType, Long targetId, List<DimensionWeight> weights) {
+        // Save to dimension_weights table
         dimensionWeightMapper.delete(
                 new LambdaQueryWrapper<DimensionWeight>()
                         .eq(DimensionWeight::getTargetType, targetType)
@@ -40,6 +45,37 @@ public class DimensionWeightService {
                 dimensionWeightMapper.insert(w);
             }
         }
+
+        // Also update the dimension_weights JSON column on the main entity table
+        // so that frontend filtering queries (JSON_EXTRACT) reflect the changes
+        String tableName = getTableName(targetType);
+        if (tableName != null) {
+            if (weights != null && !weights.isEmpty()) {
+                StringBuilder json = new StringBuilder("{");
+                for (int i = 0; i < weights.size(); i++) {
+                    DimensionWeight w = weights.get(i);
+                    if (i > 0) json.append(",");
+                    json.append("\"").append(w.getDimension()).append("\":");
+                    json.append(w.getWeight() != null ? w.getWeight() : 0);
+                }
+                json.append("}");
+                String sql = "UPDATE " + tableName + " SET dimension_weights = CAST(? AS JSON) WHERE id = ?";
+                jdbcTemplate.update(sql, json.toString(), targetId);
+            } else {
+                // No weights = clear the JSON column
+                String sql = "UPDATE " + tableName + " SET dimension_weights = NULL WHERE id = ?";
+                jdbcTemplate.update(sql, targetId);
+            }
+        }
+    }
+
+    private String getTableName(String targetType) {
+        switch (targetType) {
+            case "activity": return "activities";
+            case "product": return "products";
+            case "article": return "articles";
+            default: return null;
+        }
     }
 
     @Transactional

+ 19 - 4
cfc-frontend/components/DimensionArticles.vue

@@ -4,11 +4,26 @@
       <text class="section-title">🔥 推荐阅读</text>
       <text class="section-more" @click="$emit('moreArticles')">更多 ›</text>
     </view>
-    <view v-if="!displayArticles || displayArticles.length === 0" class="empty-state">
-      <text class="empty-tip">{{ isLoggedIn ? '暂无阅读推荐' : '登录后查看更多文章' }}</text>
-    </view>
     <view class="article-list">
-      <view class="article-card" v-for="article in displayArticles" :key="article.id" @click="$emit('articleClick', article)">
+      <!-- 无数据时显示占位卡片 -->
+      <template v-if="!displayArticles || displayArticles.length === 0">
+        <view class="article-card" v-for="i in 3" :key="'ph-' + i">
+          <view class="article-category" :style="{ background: colors[(i-1) % colors.length] }">
+            <text class="article-category-text">推荐文章</text>
+          </view>
+          <text class="article-title">暂无推荐文章</text>
+          <text class="article-summary">精彩内容即将上线,敬请期待</text>
+          <view class="article-meta">
+            <view class="meta-item">
+              <text class="meta-icon">&#x1F4D6;</text>
+              <text class="meta-text">0</text>
+            </view>
+            <text class="article-date">--</text>
+          </view>
+        </view>
+      </template>
+      <!-- 有数据时显示文章卡片 -->
+      <view v-else class="article-card" v-for="article in displayArticles" :key="article.id" @click="$emit('articleClick', article)">
         <view class="article-category" :style="{ background: article.categoryColor || defaultColors[0] }">
           <text class="article-category-text">{{ article.category || '推荐文章' }}</text>
         </view>

+ 0 - 323
cfc-frontend/components/FamilyRelationshipSection.vue

@@ -1,323 +0,0 @@
-<template>
-  <view class="family-relationship-wrapper">
-    <!-- ===== 重要关系维护 ===== -->
-    <view class="contact-section" v-if="isLoggedIn">
-    <view class="contact-header">
-      <text class="contact-section-title">重要关系</text>
-      <view class="contact-header-right">
-        <text class="contact-more" @click="$emit('refresh')">刷新</text>
-        <text class="contact-import-btn" @click="$emit('import')">+ 导入</text>
-        <text class="contact-manage-btn" @click="$emit('manage')">⚙️</text>
-      </view>
-    </view>
-    <scroll-view class="contact-scroll" scroll-x enable-flex>
-      <view class="contact-scroll-inner">
-        <view class="contact-card-wrap" v-for="item in contactList" :key="item.id" @click="$emit('contactClick', item)">
-          <ContactCard :contact="item" />
-        </view>
-        <view class="contact-card-wrap contact-add-card" @click="$emit('import')">
-          <view class="add-card-inner">
-            <text class="add-card-icon">+</text>
-            <text class="add-card-label">导入通讯录</text>
-          </view>
-        </view>
-      </view>
-    </scroll-view>
-    <view class="contact-empty" v-if="contactList.length === 0">
-      <text class="contact-empty-text">暂无重要联系人,点击导入添加</text>
-    </view>
-  </view>
-
-  <!-- ===== 关系健康概览 ===== -->
-  <view class="rq-section" v-if="isLoggedIn && (healthAlerts.length > 0 || milestones.length > 0)">
-    <view class="rq-header">
-      <text class="rq-title">关系健康</text>
-      <text class="rq-more" @click="$emit('questionnaire')">去做问卷</text>
-    </view>
-    <!-- 健康预警列表 -->
-    <view class="rq-alerts" v-if="healthAlerts.length > 0">
-      <view class="rq-alert-card" v-for="alert in healthAlerts" :key="alert.memberId" @click="$emit('interactionLog', alert.memberId)">
-        <view class="rq-alert-icon">
-          <text class="rq-alert-emoji">⚠️</text>
-        </view>
-        <view class="rq-alert-info">
-          <text class="rq-alert-name">{{ alert.memberName }}</text>
-          <text class="rq-alert-desc">已{{ alert.daysSinceInteraction }}天无互动</text>
-        </view>
-        <view class="rq-alert-action">
-          <text class="rq-alert-btn">去互动</text>
-        </view>
-      </view>
-    </view>
-    <!-- 里程碑列表 -->
-    <view class="rq-milestones" v-if="milestones.length > 0">
-      <view class="rq-milestone-card" v-for="m in milestones" :key="m.memberId">
-        <view class="rq-milestone-icon">
-          <text class="rq-milestone-emoji">🎂</text>
-        </view>
-        <view class="rq-milestone-info">
-          <text class="rq-milestone-name">{{ m.memberName }}</text>
-          <text class="rq-milestone-desc">{{ m.daysUntil > 0 ? m.daysUntil + '天后生日' : '今天生日' }}</text>
-        </view>
-      </view>
-    </view>
-    <!-- 快捷操作 -->
-    <view class="rq-quick-actions">
-      <view class="rq-action-btn" @click="$emit('interactionLog')">
-        <text class="rq-action-icon">📝</text>
-        <text class="rq-action-label">记录互动</text>
-      </view>
-      <view class="rq-action-btn" @click="$emit('questionnaire')">
-        <text class="rq-action-icon">🤝</text>
-        <text class="rq-action-label">做关系问卷</text>
-      </view>
-    </view>
-  </view>
-  </view>
-</template>
-
-<script>
-export default {
-  props: {
-    contactList: { type: Array, default: function() { return [] } },
-    healthAlerts: { type: Array, default: function() { return [] } },
-    milestones: { type: Array, default: function() { return [] } },
-    isLoggedIn: { type: Boolean, default: false }
-  }
-}
-</script>
-
-<style scoped>
-/* ===== 重要关系 ===== */
-.contact-section {
-  margin: 20rpx 20rpx 0;
-}
-.contact-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 16rpx;
-}
-.contact-section-title {
-  font-size: 30rpx;
-  font-weight: 600;
-  color: #333;
-}
-.contact-header-right {
-  display: flex;
-  gap: 16rpx;
-  align-items: center;
-}
-.contact-more {
-  font-size: 24rpx;
-  color: #999;
-}
-.contact-import-btn {
-  font-size: 24rpx;
-  color: #F97316;
-  font-weight: 500;
-}
-.contact-manage-btn {
-  font-size: 30rpx;
-  color: #999;
-  padding: 4rpx;
-}
-.contact-scroll {
-  white-space: nowrap;
-  overflow: hidden;
-}
-.contact-scroll-inner {
-  display: flex;
-  flex-direction: row;
-  gap: 16rpx;
-  padding: 8rpx 0 16rpx;
-}
-.contact-card-wrap {
-  flex-shrink: 0;
-  width: 220rpx;
-}
-.contact-card-wrap:active {
-  opacity: 0.8;
-}
-.contact-add-card {
-  width: 160rpx;
-}
-.add-card-inner {
-  width: 160rpx;
-  height: 280rpx;
-  background: #fff;
-  border-radius: 16rpx;
-  border: 2rpx dashed #ddd;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  gap: 12rpx;
-}
-.add-card-icon {
-  font-size: 48rpx;
-  color: #ccc;
-}
-.add-card-label {
-  font-size: 22rpx;
-  color: #999;
-}
-.contact-empty {
-  padding: 40rpx 0;
-  text-align: center;
-}
-.contact-empty-text {
-  font-size: 26rpx;
-  color: #ccc;
-}
-
-/* ===== 关系健康概览 ===== */
-.rq-section {
-  margin: 20rpx 20rpx 0;
-}
-.rq-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 16rpx;
-}
-.rq-title {
-  font-size: 30rpx;
-  font-weight: 600;
-  color: #333;
-}
-.rq-more {
-  font-size: 24rpx;
-  color: #F97316;
-  font-weight: 500;
-}
-/* 健康预警卡片 */
-.rq-alerts {
-  display: flex;
-  flex-direction: column;
-  gap: 12rpx;
-  margin-bottom: 16rpx;
-}
-.rq-alert-card {
-  background: #FFF7ED;
-  border-radius: 16rpx;
-  padding: 20rpx;
-  display: flex;
-  align-items: center;
-  gap: 16rpx;
-  border: 1rpx solid #FED7AA;
-}
-.rq-alert-card:active {
-  opacity: 0.8;
-}
-.rq-alert-icon {
-  width: 64rpx;
-  height: 64rpx;
-  border-radius: 50%;
-  background: #fff;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex-shrink: 0;
-}
-.rq-alert-emoji {
-  font-size: 32rpx;
-}
-.rq-alert-info {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  gap: 4rpx;
-}
-.rq-alert-name {
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #333;
-}
-.rq-alert-desc {
-  font-size: 24rpx;
-  color: #F97316;
-}
-.rq-alert-action {
-  flex-shrink: 0;
-}
-.rq-alert-btn {
-  font-size: 24rpx;
-  color: #fff;
-  background: #F97316;
-  padding: 8rpx 20rpx;
-  border-radius: 20rpx;
-}
-/* 里程碑卡片 */
-.rq-milestones {
-  display: flex;
-  flex-direction: column;
-  gap: 12rpx;
-  margin-bottom: 16rpx;
-}
-.rq-milestone-card {
-  background: #F0F9FF;
-  border-radius: 16rpx;
-  padding: 20rpx;
-  display: flex;
-  align-items: center;
-  gap: 16rpx;
-  border: 1rpx solid #BAE6FD;
-}
-.rq-milestone-icon {
-  width: 64rpx;
-  height: 64rpx;
-  border-radius: 50%;
-  background: #fff;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  flex-shrink: 0;
-}
-.rq-milestone-emoji {
-  font-size: 32rpx;
-}
-.rq-milestone-info {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  gap: 4rpx;
-}
-.rq-milestone-name {
-  font-size: 28rpx;
-  font-weight: 600;
-  color: #333;
-}
-.rq-milestone-desc {
-  font-size: 24rpx;
-  color: #0EA5E9;
-}
-/* 快捷操作 */
-.rq-quick-actions {
-  display: flex;
-  gap: 16rpx;
-  margin-top: 8rpx;
-}
-.rq-action-btn {
-  flex: 1;
-  background: #fff;
-  border-radius: 16rpx;
-  padding: 20rpx;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  gap: 10rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-}
-.rq-action-btn:active {
-  opacity: 0.8;
-}
-.rq-action-icon {
-  font-size: 36rpx;
-}
-.rq-action-label {
-  font-size: 26rpx;
-  color: #333;
-  font-weight: 500;
-}
-</style>

+ 21 - 8
cfc-frontend/components/HealthDimensionsSection.vue

@@ -10,16 +10,11 @@
       <text class="loading-text">加载中...</text>
     </view>
 
-    <!-- 空数据 -->
-    <view v-else-if="!healthData.dimensions || healthData.dimensions.length === 0" class="empty-state">
-      <text class="empty-tip">暂无健康数据</text>
-    </view>
-
-    <!-- 维度列表 -->
+    <!-- 维度列表(无数据时显示7项默认值,分数为0) -->
     <view v-else class="dimension-grid">
       <view
         class="dimension-item"
-        v-for="dim in healthData.dimensions"
+        v-for="dim in displayDimensions"
         :key="dim.dimension || dim.key"
         @click="goDimensionDetail(dim)">
         <text class="dimension-icon">{{ getDimIcon(dim.dimension || dim.key) }}</text>
@@ -27,7 +22,7 @@
         <view class="dimension-score-bar">
           <view
             class="dimension-score-fill"
-            :style="'width:' + Math.min(100, dim.score || 0) + '%'"></view>
+            :style="'width:' + Math.min(100, (dim.score || 0)) + '%'"></view>
         </view>
         <text class="dimension-score">{{ dim.score || 0 }}分</text>
       </view>
@@ -53,11 +48,29 @@ var DIM_ICONS = {
   family: '\u{1F46A}'
 }
 
+var DEFAULT_DIMENSIONS = [
+  { dimension: 'growth', label: '成长发育', score: 0 },
+  { dimension: 'sleep', label: '睡眠', score: 0 },
+  { dimension: 'vision', label: '视力', score: 0 },
+  { dimension: 'immunity', label: '免疫力', score: 0 },
+  { dimension: 'nutrition', label: '营养', score: 0 },
+  { dimension: 'gut', label: '肠道', score: 0 },
+  { dimension: 'exercise', label: '运动', score: 0 }
+]
+
 export default {
   props: {
     healthData: { type: Object, default: null },
     activeChildId: { type: [Number, String], default: null }
   },
+  computed: {
+    displayDimensions: function() {
+      if (this.healthData && this.healthData.dimensions && this.healthData.dimensions.length > 0) {
+        return this.healthData.dimensions
+      }
+      return DEFAULT_DIMENSIONS
+    }
+  },
   methods: {
     getDimIcon: function(key) {
       return DIM_ICONS[key] || '\u{2B50}'

+ 3 - 3
cfc-frontend/pages/action/index.vue

@@ -83,7 +83,7 @@
       @moreArticles="goMoreArticles" />
 
     <!-- ===== 重要关系维护 + 关系健康概览 ===== -->
-    <FamilyRelationshipSection
+    <OctopusGraph
       :contactList="contactList"
       :healthAlerts="healthAlerts"
       :milestones="milestones"
@@ -123,13 +123,13 @@ import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionArticles from '../../components/DimensionArticles.vue'
 import ContactCard from '../../components/ContactCard.vue'
 import ContactImport from '../../components/ContactImport.vue'
-import FamilyRelationshipSection from '../../components/FamilyRelationshipSection.vue'
+import OctopusGraph from '../../components/OctopusGraph.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import WuxingSandbox from '../../components/wuxing-sandbox.vue'
 import { getVisibleSections, getEnergyOverview, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren, getContactList, getVisibleFamilyMembers, getFeaturedArticles, getHealthAlerts, getMilestones } from '../../utils/api.js'
 
 export default {
-  components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, ContactCard, ContactImport, FamilyRelationGraph, FamilyRelationshipSection, WuxingSandbox },
+  components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProducts, DimensionArticles, ContactCard, ContactImport, FamilyRelationGraph, OctopusGraph, WuxingSandbox },
   data() {
     return {
       showTabTransition: true,

+ 1 - 1
cfc-frontend/pages/body/index.vue

@@ -61,7 +61,7 @@
 
     <!-- 七维健康全景(嵌入卡片) -->
     <HealthDimensionsSection
-      v-if="isLoggedIn && dimensionData && dimensionData.dimensions"
+      v-if="isLoggedIn && dimensionData"
       :healthData="dimensionData"
       :activeChildId="activeChildId" />
 

+ 7 - 6
cfc-frontend/pages/health/report-upload.vue

@@ -231,12 +231,8 @@ export default {
     if (options.reportType && ['physical_exam', 'gut_flora', 'tongue'].indexOf(options.reportType) !== -1) {
       this.form.reportType = options.reportType
     }
-    // 默认设为今天
-    var now = new Date()
-    var y = now.getFullYear()
-    var m = ('' + (now.getMonth() + 1)).padStart(2, '0')
-    var d = ('' + now.getDate()).padStart(2, '0')
-    this.form.reportDate = y + '-' + m + '-' + d
+    // 不默认填充日期 — 待解析后从报告中提取
+    this.form.reportDate = ''
   },
   onShow: function() {
     // 每次显示时从store获取familyId和家庭成员
@@ -390,6 +386,10 @@ export default {
           self.showMemberSelector = true
           self.loadFamilyMembers()
         }
+        // 从解析结果自动填充报告日期
+        if (detail.extractedReportDate) {
+          self.form.reportDate = detail.extractedReportDate
+        }
       }).catch(function(e) {
         var msg = (e && e.message) || '解析失败'
         uni.showToast({ title: msg, icon: 'none' })
@@ -467,6 +467,7 @@ export default {
           + '&matchConfidence=' + (self.parsedInfo.confidence || '')
           + '&matchLabel=' + encodeURIComponent(self.parsedInfo.matchLabel || '')
           + '&candidates=' + candidatesStr
+          + '&extractedReportDate=' + encodeURIComponent(self.parsedInfo.reportDate || '')
         if (self.parsedInfo.matchedMemberId) {
           params += '&matchedMemberId=' + self.parsedInfo.matchedMemberId
         }