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

fix(body): create missing HealthDimensionsSection component

Remote commit c52575e replaced RadarChart with HealthDimensionsSection
but the component file was not included. Create it with dimension grid
display, score bars, and navigation to detail pages.
User 4 месяцев назад
Родитель
Сommit
cbfbff50a3
1 измененных файлов с 91 добавлено и 274 удалено
  1. 91 274
      cfc-frontend/components/HealthDimensionsSection.vue

+ 91 - 274
cfc-frontend/components/HealthDimensionsSection.vue

@@ -1,330 +1,147 @@
 <template>
-  <view class="hds-section">
-    <!-- 健康指数 -->
-    <view class="hds-health-index" v-if="healthData && healthData.healthIndex">
-      <view class="index-circle">
-        <text class="index-value">{{ healthData.healthIndex }}</text>
-        <text class="index-label">健康指数</text>
-      </view>
+  <view class="health-dimensions-section">
+    <view class="section-header">
+      <text class="section-title">七维健康全景</text>
+      <text class="section-more" @click="goHealthDimensions">查看详情 ›</text>
     </view>
 
-    <!-- 七维健康雷达 -->
-    <view class="hds-radar" v-if="healthData && healthData.dimensions">
-      <view class="radar-title">七维健康雷达</view>
-      <view class="radar-wrap">
-        <view class="radar-center">
-          <view class="radar-grid">
-            <view class="grid-ring ring-3"></view>
-            <view class="grid-ring ring-2"></view>
-            <view class="grid-ring ring-1"></view>
-          </view>
-          <view class="radar-spokes">
-            <view
-              class="spoke-item"
-              v-for="(dim, idx) in healthData.dimensions"
-              :key="dim.dimension"
-              :style="spokeStyles[idx]"
-            >
-              <view class="spoke-dot" :class="'level-' + dim.level"></view>
-              <view class="spoke-label">{{ dim.label }}</view>
-              <view class="spoke-score">{{ dim.score || '--' }}</view>
-            </view>
-          </view>
-        </view>
-      </view>
+    <!-- 加载态 -->
+    <view v-if="!healthData" class="loading-state">
+      <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>
 
     <!-- 维度列表 -->
-    <view class="hds-dim-list" v-if="healthData && healthData.dimensions">
+    <view v-else class="dimension-grid">
       <view
-        class="dim-card"
+        class="dimension-item"
         v-for="dim in healthData.dimensions"
-        :key="dim.dimension"
-        @click="goDimDetail(dim)"
-      >
-        <view class="dim-header">
-          <text class="dim-label">{{ dim.label }}</text>
-          <text class="dim-score" :class="'score-' + dim.level">{{ dim.score || '--' }}分</text>
-        </view>
-        <view class="dim-bar-wrap">
-          <view class="dim-bar-bg">
-            <view
-              class="dim-bar-fill"
-              :class="'fill-' + dim.level"
-              :style="{ width: (dim.score || 0) + '%' }"
-            ></view>
-          </view>
-          <text class="dim-norms" v-if="dim.normMin && dim.normMax">
-            正常范围 {{ dim.normMin }}-{{ dim.normMax }}
-          </text>
-        </view>
-        <view class="dim-footer">
-          <text class="dim-level-tag" :class="'tag-' + dim.level">{{ getLevelText(dim.level) }}</text>
-          <text class="dim-source" v-if="dim.trend">{{ dim.trend === 'up' ? '↑ 上升' : dim.trend === 'down' ? '↓ 下降' : '→ 稳定' }}</text>
+        :key="dim.dimension || dim.key"
+        @click="goDimensionDetail(dim)">
+        <text class="dimension-icon">{{ getDimIcon(dim.dimension || dim.key) }}</text>
+        <text class="dimension-name">{{ dim.label }}</text>
+        <view class="dimension-score-bar">
+          <view
+            class="dimension-score-fill"
+            :style="'width:' + Math.min(100, dim.score || 0) + '%'"></view>
         </view>
+        <text class="dimension-score">{{ dim.score || 0 }}分</text>
       </view>
     </view>
-
-    <!-- 空数据 -->
-    <view class="hds-empty" v-if="!healthData">
-      <text class="empty-tip">暂无维度数据</text>
-      <text class="empty-sub">上传健康报告或填写问卷后可查看</text>
-    </view>
   </view>
 </template>
 
 <script>
+var DIM_ICONS = {
+  body: '\u{1F3C3}',
+  mind: '\u{1F9E0}',
+  wisdom: '\u{1F4D6}',
+  action: '\u{1F331}',
+  wealth: '\u{1F4B0}',
+  social: '\u{1F91D}',
+  family: '\u{1F46A}'
+}
+
 export default {
   props: {
-    healthData: {
-      type: Object,
-      default: null
-    },
-    activeChildId: {
-      type: [Number, String],
-      default: null
-    }
-  },
-  computed: {
-    spokeStyles: function() {
-      if (!this.healthData || !this.healthData.dimensions) return []
-      var cache = []
-      var dims = this.healthData.dimensions
-      for (var i = 0; i < dims.length; i++) {
-        var angle = i * 51.4 - 90
-        var radius = 85
-        var normalizedScore = (dims[i].score || 0) / 100
-        var translateY = -radius * normalizedScore
-        cache.push({
-          transform: 'rotate(' + angle + 'deg) translateY(' + translateY + 'px)',
-          '-webkit-transform': 'rotate(' + angle + 'deg) translateY(' + translateY + 'px)'
-        })
-      }
-      return cache
-    }
+    healthData: { type: Object, default: null },
+    activeChildId: { type: [Number, String], default: null }
   },
   methods: {
-    getLevelText: function(level) {
-      var map = {
-        'excellent': '优秀',
-        'good': '良好',
-        'fair': '一般',
-        'poor': '较差',
-        'none': '暂无数据'
+    getDimIcon: function(key) {
+      return DIM_ICONS[key] || '\u{2B50}'
+    },
+    goHealthDimensions: function() {
+      if (!this.activeChildId) {
+        uni.showToast({ title: '请先选择孩子', icon: 'none' })
+        return
       }
-      return map[level] || ''
+      uni.navigateTo({ url: '/pages/body-detail/health-dimensions?childId=' + this.activeChildId })
     },
-    goDimDetail: function(dim) {
-      if (!dim || !dim.dimension) return
-      var childId = this.activeChildId || ''
-      uni.navigateTo({
-        url: '/pages/body-detail/dimension-detail?dimension=' + dim.dimension + '&childId=' + childId
-      })
+    goDimensionDetail: function(dim) {
+      var key = dim.dimension || dim.key
+      if (!key) return
+      uni.navigateTo({ url: '/pages/body-detail/dimension-detail?dimension=' + key + '&childId=' + this.activeChildId })
     }
   }
 }
 </script>
 
 <style scoped>
-.hds-section {
-  margin: 0 30rpx 20rpx;
-}
-.hds-health-index {
+.health-dimensions-section {
+  margin: 20rpx 20rpx;
+  background: #FFFFFF;
+  border-radius: 24rpx;
+  padding: 24rpx 24rpx 32rpx;
+  box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
+}
+.section-header {
   display: flex;
-  justify-content: center;
-  padding: 30rpx;
-  background: #fff;
-  border-radius: 20rpx;
-  margin-bottom: 20rpx;
-}
-.index-circle {
-  width: 160rpx;
-  height: 160rpx;
-  border-radius: 50%;
-  background: linear-gradient(135deg, #FF8C42, #FF6B35);
-  display: flex;
-  flex-direction: column;
+  justify-content: space-between;
   align-items: center;
-  justify-content: center;
-  color: #fff;
-}
-.index-value {
-  font-size: 48rpx;
-  font-weight: bold;
-  line-height: 1;
-}
-.index-label {
-  font-size: 22rpx;
-  opacity: 0.9;
-  margin-top: 8rpx;
-}
-.hds-radar {
-  background: #fff;
-  border-radius: 20rpx;
-  padding: 30rpx;
-  margin-bottom: 20rpx;
+  margin-bottom: 24rpx;
 }
-.radar-title {
-  font-size: 32rpx;
+.section-title {
+  font-size: 30rpx;
   font-weight: bold;
   color: #333;
-  margin-bottom: 30rpx;
-  text-align: center;
 }
-.radar-wrap {
-  display: flex;
-  justify-content: center;
+.section-more {
+  font-size: 24rpx;
+  color: #FF8C42;
 }
-.radar-center {
-  width: 440rpx;
-  height: 440rpx;
-  position: relative;
+.loading-state, .empty-state {
   display: flex;
-  align-items: center;
   justify-content: center;
+  padding: 40rpx 0;
 }
-.radar-grid {
-  position: absolute;
-  width: 100%;
-  height: 100%;
-}
-.grid-ring {
-  position: absolute;
-  border-radius: 50%;
-  border: 1px solid #ddd;
-  left: 50%;
-  top: 50%;
-  transform: translate(-50%, -50%);
+.loading-text, .empty-tip {
+  font-size: 24rpx;
+  color: #999;
 }
-.ring-1 { width: 33%; height: 33%; }
-.ring-2 { width: 66%; height: 66%; }
-.ring-3 { width: 100%; height: 100%; }
-.radar-spokes {
-  position: absolute;
-  width: 100%;
-  height: 100%;
+.dimension-grid {
   display: flex;
-  align-items: center;
-  justify-content: center;
+  flex-wrap: wrap;
+  gap: 20rpx;
 }
-.spoke-item {
-  position: absolute;
+.dimension-item {
+  width: calc(33.33% - 14rpx);
   display: flex;
   flex-direction: column;
   align-items: center;
-  transform-origin: center center;
-  width: 80rpx;
-  margin-left: -40rpx;
-  top: 10rpx;
-}
-.spoke-dot {
-  width: 20rpx;
-  height: 20rpx;
-  border-radius: 50%;
-  background: #FF8C42;
-  margin-bottom: 6rpx;
-}
-.spoke-dot.level-excellent { background: #10B981; }
-.spoke-dot.level-good { background: #3B82F6; }
-.spoke-dot.level-fair { background: #F59E0B; }
-.spoke-dot.level-poor { background: #EF4444; }
-.spoke-dot.level-none { background: #ddd; }
-.spoke-label {
-  font-size: 22rpx;
-  color: #333;
-  white-space: nowrap;
-}
-.spoke-score {
-  font-size: 20rpx;
-  color: #666;
-  margin-top: 2rpx;
+  padding: 16rpx 0;
 }
-.hds-dim-list {
-  padding: 0;
+.dimension-icon {
+  font-size: 40rpx;
+  margin-bottom: 8rpx;
 }
-.dim-card {
-  background: #fff;
-  border-radius: 16rpx;
-  padding: 24rpx;
-  margin-bottom: 20rpx;
-}
-.dim-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 16rpx;
-}
-.dim-label {
-  font-size: 30rpx;
-  font-weight: bold;
+.dimension-name {
+  font-size: 24rpx;
   color: #333;
+  font-weight: 500;
+  margin-bottom: 10rpx;
 }
-.dim-score {
-  font-size: 36rpx;
-  font-weight: bold;
-  color: #FF8C42;
-}
-.dim-score.score-excellent { color: #10B981; }
-.dim-score.score-good { color: #3B82F6; }
-.dim-score.score-fair { color: #F59E0B; }
-.dim-score.score-poor { color: #EF4444; }
-.dim-bar-wrap {
-  margin-bottom: 12rpx;
-}
-.dim-bar-bg {
-  height: 16rpx;
-  background: #f0f0f0;
-  border-radius: 8rpx;
+.dimension-score-bar {
+  width: 100%;
+  height: 8rpx;
+  background: #F5F5F5;
+  border-radius: 4rpx;
   overflow: hidden;
   margin-bottom: 6rpx;
 }
-.dim-bar-fill {
+.dimension-score-fill {
   height: 100%;
-  border-radius: 8rpx;
-  background: #FF8C42;
+  background: linear-gradient(90deg, #FF8C42, #FFB088);
+  border-radius: 4rpx;
   transition: width 0.3s;
 }
-.dim-bar-fill.fill-excellent { background: #10B981; }
-.dim-bar-fill.fill-good { background: #3B82F6; }
-.dim-bar-fill.fill-fair { background: #F59E0B; }
-.dim-bar-fill.fill-poor { background: #EF4444; }
-.dim-norms {
-  font-size: 22rpx;
-  color: #999;
-}
-.dim-footer {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-.dim-level-tag {
+.dimension-score {
   font-size: 22rpx;
-  padding: 4rpx 16rpx;
-  border-radius: 20rpx;
-  background: #f0f0f0;
-  color: #666;
-}
-.dim-level-tag.tag-excellent { background: #D1FAE5; color: #10B981; }
-.dim-level-tag.tag-good { background: #DBEAFE; color: #3B82F6; }
-.dim-level-tag.tag-fair { background: #FEF3C7; color: #F59E0B; }
-.dim-level-tag.tag-poor { background: #FEE2E2; color: #EF4444; }
-.dim-source {
-  font-size: 22rpx;
-  color: #999;
-}
-.hds-empty {
-  padding: 80rpx 30rpx;
-  text-align: center;
-}
-.hds-empty .empty-tip {
-  font-size: 30rpx;
-  color: #999;
-  display: block;
-  margin-bottom: 16rpx;
-}
-.hds-empty .empty-sub {
-  font-size: 24rpx;
-  color: #ccc;
-  display: block;
+  color: #FF8C42;
+  font-weight: 500;
 }
 </style>