Prechádzať zdrojové kódy

feat(frontend): 推荐商品/推荐阅读改四列网格; fix(backend): 家庭成员列表按userId去重

- DimensionProducts 组件: 两列改四列, 显示4个商品, 对齐商城推荐卡片风格(图+名+价)
- ArticleBookshelf 组件: 书册横滑改四列网格, 显示4篇文章
- index-home: 推荐阅读移到推荐商品下方, 推荐商品请求数量 2->4
- FamilyMemberStrip: 无 props 用法时自我加载可见家庭成员
- UserService: 家庭成员列表按 userId 去重(创建者双表记录)
asus 1 mesiac pred
rodič
commit
8c9850d885
32 zmenil súbory, kde vykonal 464 pridanie a 834 odobranie
  1. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
  2. 92 289
      cfc-frontend/components/ArticleBookshelf.vue
  3. 30 77
      cfc-frontend/components/DimensionProducts.vue
  4. 110 0
      cfc-frontend/components/FamilyFeed.vue
  5. 26 1
      cfc-frontend/components/FamilyMemberStrip.vue
  6. 2 16
      cfc-frontend/components/member-detail-sections/mind-section.vue
  7. 6 25
      cfc-frontend/components/wuxing-sandbox.vue
  8. 0 10
      cfc-frontend/pages/action-detail/interaction-log.vue
  9. 0 9
      cfc-frontend/pages/action-detail/member-action-detail.vue
  10. 1 12
      cfc-frontend/pages/assessment/purchase.vue
  11. 1 20
      cfc-frontend/pages/body-detail/index.vue
  12. 0 9
      cfc-frontend/pages/body-detail/member-body-detail.vue
  13. 0 9
      cfc-frontend/pages/body/member-body-detail.vue
  14. 1 10
      cfc-frontend/pages/energy/detail.vue
  15. 3 20
      cfc-frontend/pages/growth-main/index.vue
  16. 1 11
      cfc-frontend/pages/growth/create.vue
  17. 1 12
      cfc-frontend/pages/health/gut-index.vue
  18. 18 33
      cfc-frontend/pages/home-pages/child-index.vue
  19. 4 37
      cfc-frontend/pages/home-pages/parent-index.vue
  20. 109 53
      cfc-frontend/pages/index-home/index.vue
  21. 46 68
      cfc-frontend/pages/mind-detail/index.vue
  22. 0 9
      cfc-frontend/pages/mind-detail/member-mind-detail.vue
  23. 0 9
      cfc-frontend/pages/mind/member-mind-detail.vue
  24. 0 10
      cfc-frontend/pages/parent/child-detail.vue
  25. 1 13
      cfc-frontend/pages/parent/plans/index.vue
  26. 0 10
      cfc-frontend/pages/parent/wishes/index.vue
  27. 0 11
      cfc-frontend/pages/rewards/badge.vue
  28. 3 13
      cfc-frontend/pages/share/index.vue
  29. 0 10
      cfc-frontend/pages/teacher/teacher-profile.vue
  30. 0 12
      cfc-frontend/pages/wisdom-detail/member-wisdom-detail.vue
  31. 0 12
      cfc-frontend/pages/wisdom/member-wisdom-detail.vue
  32. 0 4
      cfc-frontend/utils/api.js

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java

@@ -789,6 +789,10 @@ private FamilyInvitationService familyInvitationService;
         Long creatorId = (family != null) ? family.getCreatorId() : null;
         boolean isAdmin = creatorId != null && creatorId.equals(userId);
 
+        // 已加入的 userId 集合:家长部分与成员部分按 userId 去重
+        // (创建者在 users 表(parent) 和 family_members 表均有记录,避免同一人出现两次)
+        Set<Long> seenUserIds = new HashSet<>();
+
         // 家长:show_to_family=1 的同家庭家长(自己始终可见)
         List<User> parents = userMapper.selectList(new LambdaQueryWrapper<User>()
                 .eq(User::getFamilyId, familyId)
@@ -810,6 +814,7 @@ private FamilyInvitationService familyInvitationService;
                 vo.setShowToFamily(p.getShowToFamily() != null ? p.getShowToFamily() : 1);
                 vo.setIsAdmin(isAdmin);
                 vo.setIsSelf(p.getId().equals(userId));
+                seenUserIds.add(p.getId());
                 result.add(vo);
             }
         }
@@ -818,6 +823,10 @@ private FamilyInvitationService familyInvitationService;
         // 再按 show_to_family 过滤(自己始终可见)
         List<FamilyMemberVO> members = familyMemberService.listMembers(userId);
         for (FamilyMemberVO vo : members) {
+            // 该成员已在家长部分返回过(如创建者同时存在于 users 表和 family_members 表),跳过避免重复
+            if (vo.getUserId() != null && seenUserIds.contains(vo.getUserId())) {
+                continue;
+            }
             boolean isSelf = vo.getUserId() != null && vo.getUserId().equals(userId);
             if (isSelf || vo.getShowToFamily() == null || vo.getShowToFamily() == 1) {
                 vo.setIsSelf(isSelf);

+ 92 - 289
cfc-frontend/components/ArticleBookshelf.vue

@@ -7,83 +7,48 @@
 
     <!-- 骨架屏 -->
     <view v-else-if="loading && (!articles || articles.length === 0)" class="bs-shelf-skeleton">
-      <view class="bs-skeleton-shelf">
-        <view v-for="n in 3" :key="n" class="bs-skeleton-book"></view>
+      <view class="bs-skeleton-grid">
+        <view v-for="n in 4" :key="n" class="bs-skeleton-card">
+          <view class="bs-skeleton-cover"></view>
+          <view class="bs-skeleton-line"></view>
+        </view>
       </view>
     </view>
 
-    <!-- 书册展示区 -->
-    <view v-else class="bs-bookcase">
-      <!-- 书架装饰线 -->
-      <view class="bs-shelf-top-border"></view>
-
-      <!-- 书籍横向滚动(每屏3本) -->
-      <scroll-view
-        scroll-x
-        class="bs-books-scroll"
-        :scroll-left="scrollLeft"
-        scroll-with-animation
-        :show-scrollbar="false"
-        @scroll="onScroll"
+    <!-- 四列文章网格 -->
+    <view v-else class="bs-grid">
+      <view
+        v-for="(article, idx) in displayArticles"
+        :key="idx"
+        class="bs-card"
+        @click="$emit('articleClick', article)"
       >
-        <view class="bs-books-inner">
-          <view class="bs-book-spacer"></view>
-
-          <view
-            v-for="(article, idx) in displayArticles"
-            :key="idx"
-            class="bs-book-wrap"
-            @click="$emit('articleClick', article)"
-          >
-            <view
-              class="bs-book-cover"
-              :style="{
-                background: getBookColor(article, idx)
-              }"
-            >
-              <image
-                v-if="article.coverImage"
-                class="bs-cover-img"
-                :src="getImageUrl(article.coverImage)"
-                mode="aspectFill"
-              />
-
-              <view v-else class="bs-cover-text">
-                <text
-                  class="bs-cover-title"
-                  :style="{ color: getTextColor(article, idx) }"
-                >{{ article.title }}</text>
-              </view>
-
-              <view v-if="article.coverImage" class="bs-cover-title-bottom">
-                <text class="bs-cover-title-overlay">{{ article.title }}</text>
-              </view>
-
-              <view class="bs-cover-edge"></view>
-            </view>
+        <view
+          class="bs-card-cover"
+          :style="{
+            background: getBookColor(article, idx)
+          }"
+        >
+          <image
+            v-if="article.coverImage"
+            class="bs-cover-img"
+            :src="getImageUrl(article.coverImage)"
+            mode="aspectFill"
+          />
 
-            <view class="bs-book-shadow"></view>
+          <view v-else class="bs-cover-text">
+            <text
+              class="bs-cover-title"
+              :style="{ color: getTextColor(article, idx) }"
+            >{{ article.title }}</text>
           </view>
 
-          <view class="bs-book-spacer"></view>
+          <view v-if="article.coverImage" class="bs-cover-title-bottom">
+            <text class="bs-cover-title-overlay">{{ article.title }}</text>
+          </view>
         </view>
-      </scroll-view>
 
-      <view class="bs-shelf-bottom">
-        <view class="bs-shelf-plank"></view>
-        <view class="bs-shelf-bracket bs-shelf-bracket-left"></view>
-        <view class="bs-shelf-bracket bs-shelf-bracket-right"></view>
-      </view>
-
-      <view class="bs-page-dots" v-if="displayArticles.length > 3">
-        <view
-          v-for="n in dotCount"
-          :key="n"
-          class="bs-dot"
-          :class="{ 'bs-dot-active': isActiveDot(n) }"
-        >
-          <text class="bs-dot-text">{{ (n - 1) * 3 + 1 }}-{{ Math.min(n * 3, displayArticles.length) }}</text>
-        </view>
+        <text class="bs-card-name">{{ article.title }}</text>
       </view>
     </view>
   </view>
@@ -118,16 +83,9 @@ export default {
     isLoggedIn: { type: Boolean, default: false }
   },
   data: function() {
-    return {
-      scrollLeft: 0,
-      activeDot: 1
-    }
+    return {}
   },
   computed: {
-    // 每屏 3 本,指示器数量 = ceil(n/3)
-    dotCount: function() {
-      return Math.max(1, Math.ceil(this.displayArticles.length / 3))
-    },
     displayArticles: function() {
       if (!this.articles || this.articles.length === 0) {
         return [
@@ -136,9 +94,9 @@ export default {
           { title: '亲子沟通的心理学智慧', category: '心理养育', summary: '有效的倾听与表达技巧' },
           { title: '规划师推荐的书单', category: '成长规划', summary: '培养未来领导力的阅读路径' },
           { title: '儿童营养均衡完全指南', category: '营养健康', summary: '0-12岁各阶段营养需求' }
-        ]
+        ].slice(0, 4)
       }
-      return this.articles.slice(0, 3)
+      return this.articles.slice(0, 4)
     }
   },
   methods: {
@@ -187,17 +145,6 @@ export default {
     },
     getImageUrl: function(path) {
       return config.API_BASE_URL + path
-    },
-    // 滚动时计算当前指示器位置(每 3 本一组)
-    onScroll: function(e) {
-      var scrollLeft = e.detail.scrollLeft || 0
-      // 每本封面宽 200rpx + 间距 24rpx ≈ 224rpx/本;scrollLeft 为 px,需转 px 后按 3 本一组
-      var groupWidth = uni.upx2px(224) * 3
-      var groupIndex = Math.floor(scrollLeft / groupWidth)
-      this.activeDot = Math.min(groupIndex + 1, this.dotCount)
-    },
-    isActiveDot: function(dotIndex) {
-      return dotIndex === this.activeDot
     }
   }
 }
@@ -205,7 +152,7 @@ export default {
 
 <style scoped>
 /* ======================================
-   ArticleBookshelf — 书册风格推荐阅读组件
+   ArticleBookshelf — 四列网格推荐阅读组件
    浠艾福 (Care Family) 设计系统
    ====================================== */
 
@@ -215,61 +162,6 @@ export default {
   padding: 0 0 16rpx;
 }
 
-/* ---- Header ---- */
-.bs-header {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  padding: 0 32rpx 20rpx;
-}
-.bs-header-left {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-}
-.bs-book-icon {
-  font-size: 48rpx;
-  margin-right: 14rpx;
-  line-height: 1;
-}
-.bs-header-text {
-  display: flex;
-  flex-direction: column;
-}
-.bs-title {
-  font-size: 34rpx;
-  font-weight: 700;
-  color: #1a1a2e;
-  letter-spacing: 2rpx;
-  line-height: 1.2;
-}
-.bs-subtitle {
-  font-size: 20rpx;
-  color: #9CA3AF;
-  font-family: 'Georgia', serif;
-  font-style: italic;
-  margin-top: 2rpx;
-}
-.bs-header-right {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  padding: 8rpx 16rpx;
-  background: rgba(91, 155, 213, 0.08);
-  border-radius: 20rpx;
-}
-.bs-more-text {
-  font-size: 22rpx;
-  color: #5B9BD5;
-  font-weight: 500;
-}
-.bs-more-arrow {
-  font-size: 28rpx;
-  color: #5B9BD5;
-  margin-left: 4rpx;
-}
-
 /* ---- Empty State ---- */
 .bs-empty {
   padding: 40rpx 0;
@@ -279,102 +171,74 @@ export default {
 .bs-empty-text {
   font-size: 24rpx;
   color: #9CA3AF;
-  font-family: 'Georgia', serif;
-  font-style: italic;
 }
 
 /* ---- Skeleton ---- */
 .bs-shelf-skeleton {
   padding: 0 28rpx;
 }
-.bs-skeleton-shelf {
+.bs-skeleton-grid {
   display: flex;
-  flex-direction: row;
-  align-items: flex-start;
-  gap: 24rpx;
+  flex-wrap: wrap;
   padding-top: 16rpx;
 }
-.bs-skeleton-book {
-  width: 200rpx;
-  height: 280rpx;
-  border-radius: 8rpx;
+.bs-skeleton-card {
+  width: calc(25% - 12rpx);
+  margin-right: 16rpx;
+  margin-bottom: 16rpx;
+}
+.bs-skeleton-card:nth-child(4n) {
+  margin-right: 0;
+}
+.bs-skeleton-cover {
+  width: 100%;
+  height: 160rpx;
+  border-radius: 12rpx;
+  background: linear-gradient(180deg, #e8e8e8 0%, #f0f0f0 50%, #e8e8e8 100%);
+  background-size: 200% 100%;
+  animation: skeleton-wave 1.4s ease-in-out infinite;
+}
+.bs-skeleton-line {
+  height: 24rpx;
+  margin-top: 10rpx;
+  border-radius: 6rpx;
   background: linear-gradient(180deg, #e8e8e8 0%, #f0f0f0 50%, #e8e8e8 100%);
   background-size: 200% 100%;
   animation: skeleton-wave 1.4s ease-in-out infinite;
-  flex-shrink: 0;
 }
 @keyframes skeleton-wave {
   0% { background-position: 200% 0; }
   100% { background-position: -200% 0; }
 }
 
-/* ---- Bookcase ---- */
-.bs-bookcase {
-  position: relative;
-}
-
-/* 书架顶部装饰线 */
-.bs-shelf-top-border {
-  height: 6rpx;
-  margin: 0 28rpx;
-  background: linear-gradient(90deg,
-    transparent 0%,
-    #8B7355 10%,
-    #A0896A 30%,
-    #C4A882 50%,
-    #A0896A 70%,
-    #8B7355 90%,
-    transparent 100%
-  );
-  border-radius: 3rpx;
-}
-
-/* ---- Books Scroll ---- */
-.bs-books-scroll {
-  white-space: nowrap;
-  padding: 20rpx 0 0;
-  width: 100%;
-}
-.bs-books-inner {
-  display: inline-flex;
-  flex-direction: row;
-  align-items: flex-start;
+/* ---- 四列文章网格 ---- */
+.bs-grid {
+  display: flex;
+  flex-wrap: wrap;
 }
-.bs-book-spacer {
-  width: 28rpx;
-  flex-shrink: 0;
+.bs-card {
+  width: calc(25% - 12rpx);
+  margin-right: 16rpx;
+  margin-bottom: 16rpx;
 }
-
-/* ---- Single Book (竖版封面) ---- */
-.bs-book-wrap {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  flex-shrink: 0;
-  margin-right: 24rpx;
-  position: relative;
+.bs-card:nth-child(4n) {
+  margin-right: 0;
 }
-.bs-book-cover {
-  width: 200rpx;
-  height: 280rpx;
-  border-radius: 8rpx 8rpx 4rpx 4rpx;
+.bs-card-cover {
+  width: 100%;
+  height: 160rpx;
+  border-radius: 12rpx;
   position: relative;
   display: flex;
   align-items: center;
   justify-content: center;
   overflow: hidden;
-  box-shadow:
-    4rpx 0 10rpx rgba(0,0,0,0.18),
-    -2rpx 0 4rpx rgba(0,0,0,0.08),
-    0 6rpx 14rpx rgba(0,0,0,0.12);
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.12);
   transition: transform 0.18s, box-shadow 0.18s;
 }
-.bs-book-wrap:active .bs-book-cover {
-  transform: translateY(-6rpx) scale(1.02);
-  box-shadow:
-    6rpx 0 14rpx rgba(0,0,0,0.25),
-    -3rpx 0 6rpx rgba(0,0,0,0.1),
-    0 10rpx 22rpx rgba(0,0,0,0.18);
+.bs-card:active .bs-card-cover {
+  transform: translateY(-4rpx) scale(1.02);
+  box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.18);
 }
 
 /* 封面图 */
@@ -390,11 +254,11 @@ export default {
   display: flex;
   align-items: center;
   justify-content: center;
-  padding: 20rpx 16rpx;
+  padding: 16rpx 12rpx;
   box-sizing: border-box;
 }
 .bs-cover-title {
-  font-size: 24rpx;
+  font-size: 22rpx;
   font-weight: 700;
   line-height: 1.5;
   letter-spacing: 1rpx;
@@ -402,7 +266,7 @@ export default {
   overflow: hidden;
   text-overflow: ellipsis;
   display: -webkit-box;
-  -webkit-line-clamp: 4;
+  -webkit-line-clamp: 3;
   -webkit-box-orient: vertical;
   word-break: break-all;
 }
@@ -416,11 +280,11 @@ export default {
   display: flex;
   justify-content: center;
   align-items: center;
-  padding: 14rpx 12rpx 10rpx;
+  padding: 10rpx 8rpx 8rpx;
   background: linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.55) 100%);
 }
 .bs-cover-title-overlay {
-  font-size: 20rpx;
+  font-size: 18rpx;
   font-weight: 700;
   line-height: 1.4;
   color: #FFFFFF;
@@ -431,79 +295,18 @@ export default {
   -webkit-line-clamp: 2;
   -webkit-box-orient: vertical;
   word-break: break-all;
-  max-width: 176rpx;
+  max-width: 90%;
 }
 
-/* 右侧书页切边(3D效果) */
-.bs-cover-edge {
-  position: absolute;
-  top: 10rpx;
-  bottom: 10rpx;
-  right: 0;
-  width: 6rpx;
-  background: linear-gradient(180deg,
-    rgba(0,0,0,0.25) 0%,
-    rgba(0,0,0,0.1) 50%,
-    rgba(0,0,0,0.2) 100%
-  );
-  border-radius: 0 4rpx 4rpx 0;
-}
-
-/* 封面底部投影(书架效果) */
-.bs-book-shadow {
-  width: 160rpx;
-  height: 14rpx;
+/* 卡片底部标题 */
+.bs-card-name {
+  display: block;
   margin-top: 8rpx;
-  border-radius: 50%;
-  background: radial-gradient(ellipse at center, rgba(0,0,0,0.18) 0%, rgba(0,0,0,0) 70%);
-  flex-shrink: 0;
-}
-
-/* ---- 书架底板 ---- */
-.bs-shelf-bottom {
-  position: relative;
-  height: 24rpx;
-  margin: 0 20rpx;
-}
-.bs-shelf-plank {
-  position: absolute;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  height: 14rpx;
-  background: linear-gradient(180deg, #C4A882 0%, #A0896A 100%);
-  border-radius: 2rpx;
-  box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.15);
-}
-.bs-shelf-bracket {
-  position: absolute;
-  bottom: 6rpx;
-  width: 16rpx;
-  height: 20rpx;
-  background: #8B7355;
-  border-radius: 2rpx 2rpx 0 0;
-}
-.bs-shelf-bracket-left { left: 32rpx; }
-.bs-shelf-bracket-right { right: 32rpx; }
-
-/* ---- Page Dots ---- */
-.bs-page-dots {
-  display: flex;
-  flex-direction: row;
-  justify-content: center;
-  padding-top: 16rpx;
-  gap: 8rpx;
-}
-.bs-dot {
-  width: 10rpx;
-  height: 10rpx;
-  border-radius: 50%;
-  background: #D1D5DB;
-  transition: all 0.25s;
-}
-.bs-dot-active {
-  background: #8B7355;
-  width: 24rpx;
-  border-radius: 5rpx;
+  font-size: 22rpx;
+  color: #333;
+  line-height: 1.4;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 </style>

+ 30 - 77
cfc-frontend/components/DimensionProducts.vue

@@ -1,30 +1,23 @@
 <template>
   <view>
-    <view v-if="hasFamily">
-      <view v-if="!loading && displayProducts.length === 0" class="empty-state">
-        <text class="empty-tip">{{ emptyText }}</text>
-      </view>
-      <view v-else class="product-list">
-        <view class="product-card" v-for="prod in displayProducts" :key="prod.id" @click="onProductClick(prod)">
-          <image class="product-img" :src="getImageUrl(prod.coverImage) || '/static/default-product.png'" mode="aspectFill" />
-          <text class="product-name line-clamp-2">{{ prod.name }}</text>
+    <view v-if="!loading && displayProducts.length === 0" class="empty-state">
+      <text class="empty-tip">{{ emptyText }}</text>
+    </view>
+    <view v-else class="product-list">
+      <view class="product-card" v-for="prod in displayProducts" :key="prod.id" @click="onProductClick(prod)">
+        <image class="product-img" :src="getImageUrl(prod.coverImage) || '/static/default-product.png'" mode="aspectFill" />
+        <view class="product-body">
+          <text class="product-name">{{ prod.name }}</text>
           <view class="price-row">
             <text class="product-price" v-if="prod.priceLabel">{{ prod.priceLabel }}</text>
             <text class="product-price" v-else-if="prod.price">{{ formatPriceWithSymbol(prod.price) }}</text>
             <text class="product-price" v-else>免费</text>
-            <text class="product-reason" v-if="prod.reason">{{ prod.reason }}</text>
-          </view>
-          <view class="match-badge" v-if="prod.matchScore">
-            <text class="match-score">{{ prod.matchScore }}分</text>
           </view>
         </view>
       </view>
-      <view class="loading-state" v-if="loading">
-        <text class="loading-text">加载中...</text>
-      </view>
     </view>
-    <view v-else>
-      <FamilyEmptyState type="card" />
+    <view class="loading-state" v-if="loading">
+      <text class="loading-text">加载中...</text>
     </view>
   </view>
 </template>
@@ -51,25 +44,8 @@ export default {
     }
   },
   computed: {
-    hasFamily: function() {
-      // 应急修复(2026-08-07,对齐 body/index.vue):store.familyId 在 familyId 后端派生改造后
-      // 不再被写入,hasFamily 恒为 false 导致商品区被 v-else 空态吞掉。
-      // 改为基于组件自身数据判断:有 familyId(自加载模式)或父组件传入商品(兼容模式)
-      // 即认为有家庭,store getter 仅作兜底。
-      if (this.familyId) {
-        return true
-      }
-      if (this.products && this.products.length > 0) {
-        return true
-      }
-      // 父组件已控制整体可见性(如 index-home 的行页面),信任父组件
-      if (this.isLoggedIn) {
-        return true
-      }
-      return this.$store.getters.hasFamily
-    },
     displayProducts: function() {
-      return (this.products && this.products.length > 0 ? this.products : this.selfProducts).slice(0, 2)
+      return (this.products && this.products.length > 0 ? this.products : this.selfProducts).slice(0, 4)
     },
     emptyText: function() {
       // 有 familyId(已登录且选中孩子)但推荐接口无数据
@@ -136,67 +112,44 @@ export default {
   flex-wrap: wrap;
 }
 .product-card {
-  width: calc(50% - 10rpx);
+  width: calc(25% - 12rpx);
   background: #fff;
-  border-radius: 14rpx;
+  border-radius: 12rpx;
   overflow: hidden;
-  box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.06);
-  margin-right: 14rpx;
-  margin-bottom: 14rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
+  margin-right: 16rpx;
+  margin-bottom: 16rpx;
 }
-.product-card:nth-child(2n) {
+.product-card:nth-child(4n) {
   margin-right: 0;
 }
 .product-img {
   width: 100%;
-  height: 180rpx;
+  height: 160rpx;
   background: #f0f0f0;
 }
+.product-body {
+  padding: 8rpx 10rpx 12rpx;
+}
 .product-name {
-  font-size: 24rpx;
+  display: block;
+  font-size: 22rpx;
   color: #333;
-  padding: 8rpx 14rpx 0;
-  line-height: 1.3;
+  line-height: 1.4;
+  margin-bottom: 4rpx;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 .price-row {
   display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-  padding: 4rpx 14rpx 12rpx;
+  align-items: baseline;
 }
 .product-price {
-  font-size: 26rpx;
+  font-size: 24rpx;
   color: #F97316;
   font-weight: bold;
 }
-.product-reason {
-  font-size: 18rpx;
-  color: #999;
-  max-width: 60%;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-.match-badge {
-  display: inline-block;
-  background: linear-gradient(135deg, #6366F1, #818CF8);
-  padding: 4rpx 16rpx;
-  border-radius: 20rpx;
-  margin: 0 10rpx 10rpx;
-}
-.match-score {
-  font-size: 22rpx;
-  color: #fff;
-  font-weight: 500;
-}
-.line-clamp-2 {
-  overflow: hidden;
-  text-overflow: ellipsis;
-  display: -webkit-box;
-  -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;
-}
 .empty-state {
   display: flex;
   justify-content: center;

+ 110 - 0
cfc-frontend/components/FamilyFeed.vue

@@ -0,0 +1,110 @@
+<template>
+  <view class="family-feed">
+    <view class="feed-header" v-if="showHeader">
+      <text class="feed-icon">📢</text>
+      <text class="feed-title">家庭动态</text>
+    </view>
+    <scroll-view scroll-x class="feed-scroll" scroll-with-animation :show-scrollbar="false">
+      <view class="feed-list">
+        <view class="feed-item" v-for="(item, idx) in feedItems" :key="idx">
+          <text class="feed-msg">{{ item.text }}</text>
+          <text class="feed-time">{{ item.time }}</text>
+        </view>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+export default {
+  name: 'FamilyFeed',
+  props: {
+    items: {
+      type: Array,
+      default: function() { return [] }
+    },
+    showHeader: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data: function() {
+    return {
+      scrollLeft: 0
+    }
+  },
+  computed: {
+    feedItems: function() {
+      return this.items.length > 0 ? this.items : [
+        { text: '暂无动态,先做几个任务吧', time: '' }
+      ]
+    }
+  }
+}
+</script>
+
+<style>
+.family-feed {
+  margin: 12rpx 30rpx 0;
+  background: #fff;
+  border-radius: 16rpx;
+  padding: 12rpx 16rpx 14rpx;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
+}
+
+.feed-header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 6rpx;
+  margin-bottom: 8rpx;
+}
+
+.feed-icon {
+  font-size: 22rpx;
+}
+
+.feed-title {
+  font-size: 22rpx;
+  font-weight: bold;
+  color: #333;
+}
+
+.feed-scroll {
+  width: 100%;
+  white-space: nowrap;
+}
+
+.feed-list {
+  display: inline-flex;
+  flex-direction: row;
+  gap: 12rpx;
+  padding: 2rpx 0;
+}
+
+.feed-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  flex-shrink: 0;
+  background: #F8FAFC;
+  border-radius: 20rpx;
+  padding: 6rpx 14rpx;
+  white-space: nowrap;
+}
+
+.feed-msg {
+  font-size: 20rpx;
+  color: #333;
+  margin-right: 4rpx;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.feed-time {
+  font-size: 16rpx;
+  color: #999;
+  flex-shrink: 0;
+}
+</style>

+ 26 - 1
cfc-frontend/components/FamilyMemberStrip.vue

@@ -132,7 +132,7 @@
 <script>
 import FamilyMemberCard from '@/components/FamilyMemberCard'
 import SharePoster from '@/components/SharePoster.vue'
-import { kickFamilyMemberById, createFamily, generateFamilyInviteQrCode, getPairwiseRelationships } from '@/utils/api'
+import { kickFamilyMemberById, createFamily, generateFamilyInviteQrCode, getPairwiseRelationships, getVisibleFamilyMembers } from '@/utils/api'
 import { parseDate } from '@/utils/format.js'
 
 export default {
@@ -173,6 +173,15 @@ export default {
       immediate: true
     }
   },
+  // 未显式传入 members 时(如 growth-main/health-main 无 props 用法),自动加载可见家庭成员。
+  // 注意:mp-weixin 平台微信 properties 会对未传入的 props 填充默认值,
+  // $options.propsData.members 恒为 [](而非 undefined),不能用它判断父组件是否传参。
+  // 改为按实际数据判断:父组件传入非空 members 时不自我加载,否则调用接口兜底。
+  created: function() {
+    if (!this.members || this.members.length === 0) {
+      this.loadVisibleMembers()
+    }
+  },
   computed: {
     canSwitch: function() {
       // 仅家庭成员记录(source=family_member)且已关联账号时才能切换视角;
@@ -296,6 +305,22 @@ export default {
       if (!member || !this.energyMap) return null
       return this.energyMap[member.id] || this.energyMap[member.memberId] || null
     },
+    // 自我加载可见家庭成员(无 props 用法兜底,替代父组件传 members)
+    loadVisibleMembers: function() {
+      var self = this
+      var token = uni.getStorageSync('token')
+      if (!token) return
+      getVisibleFamilyMembers().then(function(res) {
+        if (res && res.code === 200) {
+          var arr = res.data || []
+          if (arr instanceof Array && arr.length > 0) {
+            self.renderedMembers = arr
+          }
+        }
+      }).catch(function() {
+        // 静默失败,保持空态
+      })
+    },
     onCreateFamily: function() {
       var self = this
       if (this.creatingFamily) return

+ 2 - 16
cfc-frontend/components/member-detail-sections/mind-section.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="mind-section" v-if="hasFamily">
+  <view class="mind-section">
     <!-- 亲密关系评分 -->
     <view class="section-card" v-if="relationScores && relationScores.scores">
       <view class="section-header">
@@ -55,10 +55,6 @@
     <!-- 加载中 -->
     <view class="loading" v-if="loading">加载中...</view>
   </view>
-  <!-- FamilyEmptyState type="block" for no-family case -->
-  <view v-else>
-    <FamilyEmptyState type="block" />
-  </view>
 </template>
 
 <script>
@@ -81,17 +77,7 @@ export default {
       milestones: []
     }
   },
-  computed: {
-    hasFamily: function() {
-      // 应急修复(2026-08-07,对齐 body/index.vue):store.familyId 在 familyId 后端派生改造后
-      // 不再被写入,hasFamily 恒为 false 导致内容区被 v-else 空态吞掉。
-      // 成员详情页必然处于家庭上下文中,基于 props 判断即可,store getter 仅兜底。
-      if (this.memberId || (this.memberInfo && this.memberInfo.familyId)) {
-        return true
-      }
-      return this.$store.getters.hasFamily
-    }
-  },
+  computed: {},
   mounted: function() {
     this.loadData()
   },

+ 6 - 25
cfc-frontend/components/wuxing-sandbox.vue

@@ -21,7 +21,7 @@
     <!-- Canvas 点击感应层 -->
     <view class="canvas-tap-area" @click="onCanvasTap"></view>
 
-    <!-- 顶点:从 sortedDimensions 顺时针排 [顶, 右, 右下, 左下, 左] -->
+    <!-- 顶点:从 sortedDimensions 顺时针排 [心, 身, 智, 富, 行] -->
     <view class="star-point star-point--top star-point--row" @click="onPointClick(sortedDimensions[0].code)">
       <view class="point-dot" :class="'point-dot--' + dimElementColor(sortedDimensions[0].code)">
         <text class="point-icon">{{ sortedDimensions[0].icon }}</text>
@@ -249,18 +249,16 @@ export default {
       return m ? (m.overallScore || 0) : 0
     },
     /** 按五行顺序绕圈排列
-     *  五行固定序:身(土)→智(金)→富(水)→行(木)→心(火)
-     *  最小值放最上面,其余按五行序顺时针依次排:左边→右边→左下→右下
-     *  预览/无数据时以身为起点
+     *  固定序:心(火)→身(土)→智(金)→富(水)→行(木),心始终在顶部
+     *  其余按五行相生序顺时针依次排:左边→右边→左下→右下
      */
     sortedDimensions: function() {
-      var order = ['body', 'wisdom', 'wealth', 'action', 'mind']
+      var order = ['mind', 'body', 'wisdom', 'wealth', 'action']
       var dimMap = {}
       for (var i = 0; i < this.dimensions.length; i++) {
         var d = this.dimensions[i]
         if (d && d.code) dimMap[d.code] = d
       }
-      // 构建完整数据(含兜底)
       var all = []
       for (var j = 0; j < order.length; j++) {
         var code = order[j]
@@ -275,25 +273,8 @@ export default {
           all.push({ code: code, name: meta.name || '', icon: meta.icon || '', element: meta.element || '', energy: 0, healthIndex: 0 })
         }
       }
-      // all 此时为 [身, 智, 富, 行, 心](五行固定序)
-      // 找最小值所在的索引
-      var startIdx = 0  // 默认以身为起点
-      if (this.mode === 'live' && all.some(function(d) { return d.energy > 0 })) {
-        var minEnergy = all[0].energy
-        for (var i = 1; i < all.length; i++) {
-          if (all[i].energy < minEnergy) {
-            minEnergy = all[i].energy
-            startIdx = i
-          }
-        }
-      }
-      // 从最小值开始,按五行序顺时针绕圈填入 [顶, 右, 右下, 左下, 左]
-      var result = []
-      for (var k = 0; k < 5; k++) {
-        result.push(all[(startIdx + k) % 5])
-      }
-      return result
-      // result = [最小值→顶, 下一位→右, 再→右下, 再→左下, 再→左]
+      return all
+      // all = [心→顶, 身→右, 智→右下, 富→左下, 行→左]
     }
   },
   watch: {

+ 0 - 10
cfc-frontend/pages/action-detail/interaction-log.vue

@@ -5,7 +5,6 @@
     </view>
 
     <!-- 家庭数据区 -->
-    <view v-if="hasFamily">
     <!-- 选择互动对象 -->
     <view class="form-section">
       <text class="section-label">互动对象</text>
@@ -57,10 +56,6 @@
     <view class="submit-bar">
       <button class="btn-submit" @click="onSubmit" :disabled="!canSubmit">保存记录</button>
     </view>
-    </view>
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
   </view>
 </template>
 
@@ -97,11 +92,6 @@ export default {
     }
   },
   computed: {
-    hasFamily: function() {
-      var token = uni.getStorageSync('token')
-      if (token) return true
-      return this.$store.getters.hasFamily
-    },
     canSubmit() {
       return this.selectedMemberId && this.selectedType
     }

+ 0 - 9
cfc-frontend/pages/action-detail/member-action-detail.vue

@@ -9,7 +9,6 @@
     </view>
 
     <!-- 五维能量条 -->
-    <view v-if="hasFamily">
     <FamilyEnergyBar
       dimensionCode="action"
       :sandboxData="sandboxData"
@@ -62,10 +61,6 @@
 
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
-    </view>
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
   </view>
 </template>
 
@@ -92,10 +87,6 @@ export default {
     }
   },
   computed: {
-    hasFamily: function() {
-      if (this.memberId) return true
-      return this.$store.getters.hasFamily
-    },
     graphMembers: function() {
       if (!this.sandboxData || !this.sandboxData.members) return []
       return this.sandboxData.members.map(function(m) {

+ 1 - 12
cfc-frontend/pages/assessment/purchase.vue

@@ -6,7 +6,6 @@
     </view>
 
     <!-- 家庭数据区 -->
-    <view v-if="hasFamily">
     <!-- 成长规划师选择 -->
     <view class="section" v-if="!guideId">
       <view class="section-title">选择成长规划师</view>
@@ -83,10 +82,6 @@
 
     <!-- 提交按钮 -->
     <button class="btn-submit" @click="submit" :disabled="!selectedPackageId || submitting" :loading="submitting">确认购买</button>
-    </view>
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
   </view>
 </template>
 
@@ -111,13 +106,7 @@ export default {
       submitting: false
     }
   },
-  computed: {
-    hasFamily: function() {
-      var token = uni.getStorageSync('token')
-      if (token) return true
-      return this.$store.getters.hasFamily
-    }
-  },
+  computed: {},
   onLoad(options) {
     // 如果有邀请码参数,自动填充
     if (options.inviteCode) {

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

@@ -48,7 +48,6 @@
     </template>
 
     <!-- 登录后:五维能量条 -->
-    <view v-if="hasFamily">
     <FamilyEnergyBar
       v-if="isLoggedIn"
       dimensionCode="body"
@@ -95,10 +94,6 @@
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
     <AIFloatingAvatar />
-    </view>
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
   </view>
 </template>
 
@@ -125,7 +120,7 @@ export default {
       userName: '',
       children: [],
       familyMembersVisible: [],
-      // 接口数据是否已检查(用于 hasFamily 判断,替代已失效的 store.familyId
+      // 接口数据是否已检查(避免"暂无数据"闪烁
       childrenChecked: false,
       membersChecked: false,
       sandboxData: null,
@@ -146,20 +141,6 @@ export default {
     }
   },
   computed: {
-    hasFamily: function() {
-      // 应急修复(2026-08-07):store.familyId 在 familyId 后端派生改造后不再被写入,
-      // hasFamily 恒为 false 导致内容区被 v-else 空态吞掉。改为基于接口数据判断:
-      // 有孩子成员或可见家庭成员即认为有家庭。
-      if (!this.isLoggedIn) {
-        return false
-      }
-      // 接口未返回前乐观显示内容区,避免"暂无数据"闪烁
-      if (!this.childrenChecked || !this.membersChecked) {
-        return true
-      }
-      return (this.children && this.children.length > 0) ||
-             (this.familyMembersVisible && this.familyMembersVisible.length > 0)
-    },
     /** 七维雷达图:维度标签(API无数据时使用默认标签) */
     radarDimensions: function() {
       var dims = this.dimensionData && this.dimensionData.dimensions

+ 0 - 9
cfc-frontend/pages/body-detail/member-body-detail.vue

@@ -9,7 +9,6 @@
     </view>
 
     <!-- 五维能量条 -->
-    <view v-if="hasFamily">
     <FamilyEnergyBar
       dimensionCode="body"
       :sandboxData="sandboxData"
@@ -62,10 +61,6 @@
 
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
-    </view>
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
   </view>
 </template>
 
@@ -92,10 +87,6 @@ export default {
     }
   },
   computed: {
-    hasFamily: function() {
-      if (this.memberId) return true
-      return this.$store.getters.hasFamily
-    },
     graphMembers: function() {
       if (!this.sandboxData || !this.sandboxData.members) return []
       return this.sandboxData.members.map(function(m) {

+ 0 - 9
cfc-frontend/pages/body/member-body-detail.vue

@@ -9,7 +9,6 @@
     </view>
 
     <!-- 五维能量条 -->
-    <view v-if="hasFamily">
     <FamilyEnergyBar
       dimensionCode="body"
       :sandboxData="sandboxData"
@@ -62,10 +61,6 @@
 
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
-    </view>
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
   </view>
 </template>
 
@@ -92,10 +87,6 @@ export default {
     }
   },
   computed: {
-    hasFamily: function() {
-      if (this.memberId) return true
-      return this.$store.getters.hasFamily
-    },
     graphMembers: function() {
       if (!this.sandboxData || !this.sandboxData.members) return []
       return this.sandboxData.members.map(function(m) {

+ 1 - 10
cfc-frontend/pages/energy/detail.vue

@@ -17,7 +17,7 @@
     <BaseLoading :loading="loading" text="加载中..." />
 
     <!-- 流水列表 -->
-    <view class="logs-section" v-if="hasFamily && !loading">
+    <view class="logs-section" v-if="!loading">
       <view class="section-header">
         <text class="section-title">📋 能量流水</text>
       </view>
@@ -41,9 +41,6 @@
         <text>加载更多...</text>
       </view>
     </view>
-    <view v-else-if="!loading">
-      <FamilyEmptyState type="list" />
-    </view>
   </view>
 </template>
 
@@ -74,12 +71,6 @@ export default {
       elementColor: 'fire'
     }
   },
-  computed: {
-    hasFamily: function() {
-      if (this.memberId) return true
-      return this.$store.getters.hasFamily
-    }
-  },
   onLoad(options) {
     if (options.memberId) this.memberId = options.memberId
     if (options.code) {

+ 3 - 20
cfc-frontend/pages/growth-main/index.vue

@@ -2,14 +2,8 @@
   <view class="container">
     <PageBanner theme="action" tagline="方案 · 执行 · 成长" quote="知行合一,每天进步一点点" />
 
-    <!-- 无家庭状态 -->
-    <view v-if="!hasFamily">
-      <FamilyEmptyState type="card" />
-    </view>
-
-    <view v-else>
-      <!-- 家庭成员选择条 -->
-      <FamilyMemberStrip />
+    <!-- 家庭成员选择条(登录即自动建家庭,无需空态判断) -->
+    <FamilyMemberStrip />
 
       <!-- 今日待办卡片 -->
       <view class="section">
@@ -106,11 +100,6 @@
               <text class="arrow-icon">›</text>
             </view>
           </view>
-          <view class="empty-state" v-if="children.length === 0 && !loading">
-            <text class="empty-icon">📋</text>
-            <text class="empty-text">还没有添加孩子</text>
-            <text class="empty-hint">请先在"我的"页面添加孩子</text>
-          </view>
         </view>
 
         <!-- 档案记录 -->
@@ -154,6 +143,7 @@
           </view>
           <view class="empty-state" v-if="records.length === 0">
             <text class="empty-text">暂无成长档案记录</text>
+            <text class="empty-hint">点击下方按钮创建第一个成长档案</text>
           </view>
         </view>
 
@@ -165,7 +155,6 @@
           <button class="btn-invite-guide" open-type="share" @click="prepareGuideInvite">邀请成长规划师</button>
         </view>
       </view>
-    </view>
   </view>
 </template>
 
@@ -199,12 +188,6 @@ export default {
     }
   },
   computed: {
-    hasFamily: function() {
-      if (this.memberId) return true
-      if (this.children && this.children.length > 0) return true
-      if (this.records && this.records.length > 0) return true
-      return this.$store.getters.hasFamily
-    },
     filteredRecords: function() {
       if (this.activeFilter === 'all') return this.records
       var filter = this.activeFilter

+ 1 - 11
cfc-frontend/pages/growth/create.vue

@@ -5,7 +5,6 @@
       <text class="subtitle">选择创建方式</text>
     </view>
 
-    <view v-if="hasFamily">
       <view class="source-type-section">
         <view class="source-card" :class="sourceType === 'manual' ? 'source-active' : ''" @click="sourceType = 'manual'">
           <text class="source-icon">📝</text>
@@ -105,10 +104,6 @@
         </view>
       </view>
     </view>
-
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
   </view>
 </template>
 
@@ -148,12 +143,7 @@ export default {
       this.formData.recordTitle = '成长档案 - ' + this.getCurrentDate()
     }
   },
-  computed: {
-    hasFamily: function() {
-      if (this.targetChildId || (this.children && this.children.length > 0)) return true
-      return this.$store.getters.hasFamily
-    }
-  },
+  computed: {},
   methods: {
     getCurrentDate() {
       var d = new Date()

+ 1 - 12
cfc-frontend/pages/health/gut-index.vue

@@ -7,7 +7,7 @@
       <text class="nav-title">菌群</text>
     </view>
 
-    <scroll-view class="content" scroll-y v-if="hasFamily">
+    <scroll-view class="content" scroll-y>
       <view class="card">
         <view class="card-accent"></view>
         <view class="card-body">
@@ -89,10 +89,6 @@
         </view>
       </view>
     </scroll-view>
-
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
   </view>
 </template>
 
@@ -129,13 +125,6 @@ export default {
   data() {
     return {}
   },
-  computed: {
-    hasFamily: function() {
-      var token = uni.getStorageSync('token')
-      if (token) return true
-      return this.$store.getters.hasFamily
-    }
-  },
   methods: {
     goBack: function() {
       uni.navigateBack()

+ 18 - 33
cfc-frontend/pages/home-pages/child-index.vue

@@ -34,28 +34,23 @@
   quote="给全家的一站式幸福提案" />
 
 <!-- 五维能量沙盘 -->
-<view v-if="hasFamily">
-  <WuxingSandbox
-    v-if="sandboxData"
-    mode="live"
-    :scores="sandboxScores"
-    :overallScore="sandboxData.overallScore || 0"
-    :members="sandboxData.members || []"
-    :dimensions="energyDimensions"
-    :totalEnergy="totalEnergy"
-    :totalHealthIndex="totalHealthIndex"
-    starHeight="400rpx"
-    showDimensionBar
-    @point-click="onDomainClick" />
-  <!-- 家庭能量横条 -->
-  <FamilyEnergyBar
-    v-if="hasChildren"
-    dimensionCode="child"
-    :sandboxData="sandboxData" />
-</view>
-<view v-else>
-  <FamilyEmptyState type="card" />
-</view>
+<WuxingSandbox
+  v-if="sandboxData"
+  mode="live"
+  :scores="sandboxScores"
+  :overallScore="sandboxData.overallScore || 0"
+  :members="sandboxData.members || []"
+  :dimensions="energyDimensions"
+  :totalEnergy="totalEnergy"
+  :totalHealthIndex="totalHealthIndex"
+  starHeight="400rpx"
+  showDimensionBar
+  @point-click="onDomainClick" />
+<!-- 家庭能量横条 -->
+<FamilyEnergyBar
+  v-if="hasChildren"
+  dimensionCode="child"
+  :sandboxData="sandboxData" />
 
 <!-- 家庭成员选择条 -->
 <FamilyMemberStrip
@@ -273,7 +268,7 @@ B) 积分卡片 — Claymorphism 暖橙黏土风格
       <!-- ============================================================
       F) 最近能量变动
       ============================================================ -->
-      <view class="energy-log-section animate-slide-up animate-stagger-6" v-if="hasFamily && energyLogs.length > 0">
+      <view class="energy-log-section animate-slide-up animate-stagger-6" v-if="energyLogs.length > 0">
         <view class="section-header">
           <text class="section-title">💧 最近能量变动</text>
           <text class="section-link" @click="goToEnergyDetail">查看详情 →</text>
@@ -293,9 +288,6 @@ B) 积分卡片 — Claymorphism 暖橙黏土风格
           </view>
         </view>
       </view>
-      <view v-else>
-        <FamilyEmptyState type="card" />
-      </view>
 
       <!-- ===== 推荐商品+活动(混合信息流) ===== -->
       <RecommendedFeed
@@ -397,13 +389,6 @@ export default {
     }
   },
   computed: {
-    hasFamily: function() {
-      if (this.loading) return true
-      if (this.hasChildren) return true
-      if (this.sandboxData) return true
-      if (this.familyMembersVisible && this.familyMembersVisible.length > 0) return true
-      return this.$store.getters.hasFamily
-    },
     // ---------- 能量沙盘 ----------
     sandboxScores() {
       if (!this.sandboxData) return { body: 0, mind: 0, wisdom: 0, action: 0, wealth: 0 }

+ 4 - 37
cfc-frontend/pages/home-pages/parent-index.vue

@@ -32,18 +32,13 @@
   @dismiss="onAlertDismiss" />
 
       <!-- ===== 家庭健康排行榜(P2-2) ===== -->
-      <view v-if="hasFamily">
         <FamilyHealthRanking
           v-if="rankingMembers && rankingMembers.length > 0"
           :list="rankingMembers"
           :totalScore="rankingTotalScore" />
-      </view>
-      <view v-else>
-        <FamilyEmptyState type="card" />
-      </view>
 
       <!-- ===== 家庭挑战区域 ===== -->
-      <view class="challenge-section" v-if="hasFamily">
+      <view class="challenge-section">
         <view class="challenge-section-header" @click="goToChallengeManage">
           <text class="challenge-section-title">家庭挑战</text>
           <text class="challenge-section-action">管理 →</text>
@@ -53,12 +48,9 @@
           :key="ch.id"
           :challenge="ch" />
       </view>
-      <view v-else>
-        <FamilyEmptyState type="card" />
-      </view>
 
     <!-- ===== 健康圈入口(P3:社区扩散) ===== -->
-    <view class="circle-entry-section" v-if="hasFamily">
+    <view class="circle-entry-section">
       <!-- 有健康圈数据:显示排行榜预览 -->
       <PlayfulCard v-if="circleName && circleRankings.length > 0" variant="flat" shadow="sm" :clickable="true" @click="goToHealthCircle" class="circle-card-wide">
         <view class="circle-card-content">
@@ -93,9 +85,6 @@
         </view>
       </PlayfulCard>
     </view>
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
 
     <!-- ===== 五维家庭自检入口(P0-1/P0-2) ===== -->
     <view class="selfcheck-entry-section">
@@ -157,7 +146,7 @@
       </view>
 
       <!-- ===== c) 成长数据 ===== -->
-      <view class="stats-section animate-fade-in animate-stagger-5" v-if="hasFamily">
+      <view class="stats-section animate-fade-in animate-stagger-5">
         <view class="section-title">成长数据</view>
         <view class="stats-row">
           <!-- 任务完成率:自己的 + 全家平均 -->
@@ -218,9 +207,6 @@
           </PlayfulCard>
         </view>
       </view>
-      <view v-else>
-        <FamilyEmptyState type="card" />
-      </view>
 
       <!-- ===== d) 今日重点 ===== -->
       <view class="today-section animate-fade-in animate-stagger-7">
@@ -273,7 +259,7 @@
       </view>
 
       <!-- ===== f) 待处理事项 ===== -->
-      <view class="pending-section animate-fade-in animate-stagger-9" v-if="hasFamily">
+      <view class="pending-section animate-fade-in animate-stagger-9">
         <view class="section-title">📋 待处理事项</view>
         <view class="pending-grid">
           <view class="pending-card" @click="goToReview">
@@ -314,9 +300,6 @@
           </view>
         </view>
       </view>
-      <view v-else>
-        <FamilyEmptyState type="card" />
-      </view>
 
       <!-- ===== 重要关系(外部通讯录) ===== -->
       <view class="contact-section" v-if="!loading && familyMembersVisible.length > 0">
@@ -532,7 +515,6 @@ export default {
       totalHealthIndex: 0,
       recommendedActivities: [],
       recommendedProducts: [],
-      hasFamilyData: false,
       sandboxData: null,
       errorEnergy: false,
       errorTasks: false,
@@ -579,20 +561,6 @@ export default {
     }
   },
   computed: {
-    hasFamily: function() {
-      // 应急修复(2026-08-07,对齐 body/index.vue):store.familyId 在 familyId 后端派生改造后
-      // 不再被写入,hasFamily 恒为 false 导致内容区被 v-else 空态吞掉。
-      // 改为基于接口数据判断:能量沙盘有成员或家庭成员可见列表非空即认为有家庭。
-      if (!this.isLoggedIn) {
-        return false
-      }
-      // 接口未返回前乐观显示内容区,避免"暂无数据"闪烁
-      if (this.loading) {
-        return true
-      }
-      return this.hasFamilyData ||
-             (this.familyMembersVisible && this.familyMembersVisible.length > 0)
-    },
     isFamilyAdmin() {
       if (!this.familyMembersVisible || this.familyMembersVisible.length === 0) return false
       return this.familyMembersVisible[0].isAdmin === true
@@ -846,7 +814,6 @@ export default {
           const energyRes = await getFamilyEnergySandbox()
           this.energyData = energyRes.data || null
           this.sandboxData = energyRes.data || null
-          this.hasFamilyData = !!(energyRes.data && energyRes.data.members && energyRes.data.members.length > 0)
         } catch (e) {
           console.log('获取能量沙盘数据失败', e)
           this.energyData = null

+ 109 - 53
cfc-frontend/pages/index-home/index.vue

@@ -51,29 +51,20 @@
       tagline="知行合一 · 快乐成长"
       quote="千里之行,始于足下" />
 
-    <!-- 家庭成员选择条 -->
-    <FamilyMemberStrip
-      :members="familyMembersVisible"
-      :selectedMemberId="selectedMemberId"
-      :isParent="isFamilyAdmin"
-      @select="onFamilyMemberSelect"
-      @switch="onFamilyMemberSwitch"
-      @removed="loadFamilyMembers" />
-
     <!-- 能量沙盘 — Canvas 五行星图 -->
-    <view v-if="hasFamily">
-      <WuxingSandbox
-        v-if="sandboxData"
-        mode="live"
-        :scores="sandboxScores"
-        :overallScore="sandboxData.overallScore || 0"
-        :members="sandboxData.members || []"
-        showDimensionBar
-        starHeight="480rpx" />
-    </view>
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
+    <WuxingSandbox
+      v-if="sandboxData"
+      mode="live"
+      :scores="sandboxScores"
+      :overallScore="sandboxData.overallScore || 0"
+      :members="sandboxData.members || []"
+      showDimensionBar
+      starHeight="480rpx" />
+
+    <!-- 家庭动态 -->
+    <FamilyFeed
+      :items="familyFeed"
+      :showHeader="true" />
 
     <!-- 会员入口:非会员显示开通 / 快到期显示续费 -->
     <view v-if="showMemberEntry" class="member-entry" @click="goMembership">
@@ -141,7 +132,7 @@
     </view>
 
     <!-- 家庭挑战任务 -->
-    <view v-if="hasFamily" class="section">
+    <view class="section">
       <view class="section-header">
         <text class="section-title">🔥 家庭挑战</text>
         <text class="section-more" @click="goChallengeManage">管理 ›</text>
@@ -153,21 +144,11 @@
         :showCheckin="showCheckinBtn"
         @checkin="onChallengeCheckin"
         @respond="onChallengeRespond" />
-    </view>
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
-
-    <!-- 推荐文章 -->
-    <view class="section">
-      <view class="section-header">
-        <text class="section-title">📖 推荐阅读</text>
-        <text class="section-more" @click="goMoreArticles">更多 ›</text>
+      <view v-if="activeChallenges.length === 0" class="challenge-empty">
+        <text class="challenge-empty-icon">🏆</text>
+        <text class="challenge-empty-text">您还没有挑战,去添加吧</text>
+        <button class="challenge-add-btn" @click="goChallengeManage">去添加</button>
       </view>
-      <ArticleBookshelf
-        :articles="actionArticles"
-        :isLoggedIn="true"
-        @articleClick="goArticleDetail" />
     </view>
 
     <!-- 推荐商品 -->
@@ -184,6 +165,18 @@
         @productClick="goProductDetail" />
     </view>
 
+    <!-- 推荐文章 -->
+    <view class="section">
+      <view class="section-header">
+        <text class="section-title">📖 推荐阅读</text>
+        <text class="section-more" @click="goMoreArticles">更多 ›</text>
+      </view>
+      <ArticleBookshelf
+        :articles="actionArticles"
+        :isLoggedIn="true"
+        @articleClick="goArticleDetail" />
+    </view>
+
     <!-- 底部占位 -->
     <view class="bottom-spacer" style="height: 40rpx;"></view>
   </view>
@@ -330,18 +323,18 @@
 import store from '../../store/index.js'
 import WuxingSandbox from '../../components/wuxing-sandbox.vue'
 import PageBanner from '../../components/PageBanner.vue'
-import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
+import FamilyFeed from '../../components/FamilyFeed.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
 import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
-import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getVisibleFamilyMembers, getChildren, getProductsByDomain, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi } from '../../utils/api.js'
+import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getVisibleFamilyMembers, getChildren, getProductsByDomain, getChallengeList, switchToFamilyMember, getMyMembership, updateChallengeProgress, respondChallenge, getTodayTasks, completeTask as completeTaskApi, getTodayParentTasks, completeParentTask as completeParentTaskApi, getTaskHistory } from '../../utils/api.js'
 import config from '@/config.js'
 
 export default {
   components: {
     PageBanner,
     WuxingSandbox,
-    FamilyMemberStrip,
+    FamilyFeed,
     DimensionProducts,
     ArticleBookshelf,
     FamilyChallengeCard
@@ -388,7 +381,7 @@ export default {
 
       // 登录态数据
       familyMembersVisible: [],
-      // 接口数据是否已检查(用于 hasFamily 判断,替代已失效的 store.familyId
+      // 接口数据是否已检查(避免"暂无数据"闪烁
       childrenChecked: false,
       membersChecked: false,
       selectedMemberId: null,
@@ -399,6 +392,7 @@ export default {
       currentChildId: null,
       activeChallenges: [],
       pendingTasks: [],
+      familyFeed: [],
       loggedInInitDone: false,
 
       // 会员状态(沙盘下方会员入口使用)
@@ -407,16 +401,6 @@ export default {
     }
   },
   computed: {
-    hasFamily: function() {
-      if (!this.isLoggedIn()) return false
-      // 接口未返回前乐观显示内容区,避免"暂无数据"闪烁
-      if (!this.childrenChecked || !this.membersChecked) return true
-      // 有孩子成员或可见家庭成员即认为有家庭
-      if (this.sandboxData) return true
-      if (this.children && this.children.length > 0) return true
-      if (this.familyMembersVisible && this.familyMembersVisible.length > 0) return true
-      return this.$store.getters.hasFamily
-    },
     isFamilyAdmin: function() {
       if (!this.familyMembersVisible || this.familyMembersVisible.length === 0) return false
       return this.familyMembersVisible[0].isAdmin === true
@@ -536,6 +520,7 @@ export default {
         }
         self.childrenChecked = true
         self.loadTodayTasks()
+        self.loadFamilyFeed()
       }).catch(function(e) {
         console.log('获取孩子列表失败', e)
         self.childrenChecked = true
@@ -559,10 +544,10 @@ export default {
     },
     loadActionProducts: function() {
       var self = this
-      getProductsByDomain('action', 1, 2).then(function(res) {
+      getProductsByDomain('action', 1, 4).then(function(res) {
         if (res && res.data) {
           var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
-          self.dimensionProducts = list.slice(0, 2)
+          self.dimensionProducts = list.slice(0, 4)
         }
       }).catch(function() { self.dimensionProducts = [] })
     },
@@ -622,6 +607,45 @@ export default {
         self.pendingTasks = []
       })
     },
+    loadFamilyFeed: function() {
+      var self = this
+      var children = self.children || []
+      if (children.length === 0) {
+        self.familyFeed = []
+        return
+      }
+      var promises = children.map(function(child) {
+        return getTaskHistory(child.id, 1, 3).then(function(res) {
+          var tasks = []
+          if (res.code === 200) {
+            var data = res.data
+            tasks = (data && data.records) ? data.records : (Array.isArray(data) ? data : [])
+          }
+          return tasks.filter(function(t) { return t.status === 'completed' }).map(function(t) {
+            var points = t.pointsEarned || t.points || 0
+            var timeStr = ''
+            if (t.completedAt && typeof t.completedAt === 'string') {
+              timeStr = t.completedAt.substring(11, 16)
+            }
+            return {
+              text: '✅ ' + (child.nickname || '小名') + ' 完成「' + (t.title || '任务') + '」 +' + points + '分',
+              time: timeStr
+            }
+          })
+        }).catch(function() { return [] })
+      })
+      Promise.all(promises).then(function(results) {
+        var all = []
+        for (var i = 0; i < results.length; i++) {
+          for (var j = 0; j < results[i].length; j++) {
+            all.push(results[i][j])
+          }
+        }
+        self.familyFeed = all.slice(0, 10)
+      }).catch(function() {
+        self.familyFeed = []
+      })
+    },
     quickCompleteTask: function(task) {
       var self = this
       completeTaskApi(task.id, self.currentChildId, '').then(function(res) {
@@ -1587,4 +1611,36 @@ export default {
   border: none;
 }
 
+/* ===== 家庭挑战空态 ===== */
+.challenge-empty {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 40rpx 0 20rpx;
+}
+.challenge-empty-icon {
+  font-size: 64rpx;
+  margin-bottom: 12rpx;
+  line-height: 1;
+}
+.challenge-empty-text {
+  font-size: 26rpx;
+  color: #999;
+  margin-bottom: 20rpx;
+}
+.challenge-add-btn {
+  padding: 10rpx 32rpx;
+  background: linear-gradient(135deg, #F97316, #FB923C);
+  color: #fff;
+  font-size: 24rpx;
+  font-weight: bold;
+  border-radius: 24rpx;
+  border: none;
+  line-height: 1;
+  height: 48rpx;
+}
+.challenge-add-btn::after {
+  border: none;
+}
+
 </style>

+ 46 - 68
cfc-frontend/pages/mind-detail/index.vue

@@ -38,81 +38,66 @@
     </template>
 
         <!-- 登录后:五维能量条 -->
-        <view v-if="hasFamily">
-          <FamilyEnergyBar
-            v-if="isLoggedIn"
-            dimensionCode="mind"
-            :sandboxData="sandboxData" />
-        </view>
-        <view v-else>
-          <FamilyEmptyState type="card" />
-        </view>
+        <FamilyEnergyBar
+          v-if="isLoggedIn"
+          dimensionCode="mind"
+          :sandboxData="sandboxData" />
 
         <!-- 心理报告卡片(登录后可见) -->
-        <view v-if="hasFamily">
-          <view class="emi-card" v-if="isLoggedIn && emiData">
-            <view class="emi-header">
-              <text class="emi-title">心理测评报告</text>
-              <view class="emi-overall-wrap">
-                <text class="emi-overall-label">综合EQ分</text>
-                <text class="emi-overall-value">{{ emiData.overallScore || 0 }}</text>
-                <text :class="['emi-overall-level', 'level-' + getScoreLevel(emiData.overallScore || 0)]">
-                  {{ getScoreLevelText(emiData.overallScore || 0) }}
-                </text>
-              </view>
+        <view class="emi-card" v-if="isLoggedIn && emiData">
+          <view class="emi-header">
+            <text class="emi-title">心理测评报告</text>
+            <view class="emi-overall-wrap">
+              <text class="emi-overall-label">综合EQ分</text>
+              <text class="emi-overall-value">{{ emiData.overallScore || 0 }}</text>
+              <text :class="['emi-overall-level', 'level-' + getScoreLevel(emiData.overallScore || 0)]">
+                {{ getScoreLevelText(emiData.overallScore || 0) }}
+              </text>
             </view>
-            <view class="emi-dimensions">
-              <view class="emi-dimension" v-for="dim in emiDimensions" :key="dim.key">
-                <view class="emi-dim-header">
-                  <text class="emi-dim-label">{{ dim.label }}</text>
-                  <view class="emi-dim-values">
-                    <text class="emi-dim-score">{{ emiData[dim.key] || 0 }}</text>
-                    <text :class="['emi-dim-level', 'level-' + getScoreLevel(emiData[dim.key] || 0)]">
-                      {{ getScoreLevelText(emiData[dim.key] || 0) }}
-                    </text>
-                  </view>
-                </view>
-                <view class="emi-dim-bar">
-                  <view class="emi-dim-bar-fill" :style="{ width: (emiData[dim.key] || 0) + '%', background: dim.color }"></view>
+          </view>
+          <view class="emi-dimensions">
+            <view class="emi-dimension" v-for="dim in emiDimensions" :key="dim.key">
+              <view class="emi-dim-header">
+                <text class="emi-dim-label">{{ dim.label }}</text>
+                <view class="emi-dim-values">
+                  <text class="emi-dim-score">{{ emiData[dim.key] || 0 }}</text>
+                  <text :class="['emi-dim-level', 'level-' + getScoreLevel(emiData[dim.key] || 0)]">
+                    {{ getScoreLevelText(emiData[dim.key] || 0) }}
+                  </text>
                 </view>
               </view>
-            </view>
-            <view class="emi-footer" @click="goEmotionReport">
-              <text class="emi-footer-text">查看详情</text>
-              <text class="emi-footer-icon">&#x2192;</text>
+              <view class="emi-dim-bar">
+                <view class="emi-dim-bar-fill" :style="{ width: (emiData[dim.key] || 0) + '%', background: dim.color }"></view>
+              </view>
             </view>
           </view>
-        </view>
-        <view v-else>
-          <FamilyEmptyState type="card" />
+          <view class="emi-footer" @click="goEmotionReport">
+            <text class="emi-footer-text">查看详情</text>
+            <text class="emi-footer-icon">&#x2192;</text>
+          </view>
         </view>
 
         <!-- 肠脑轴情绪卡片 -->
-        <view v-if="hasFamily">
-          <view class="section" v-if="gutIndicators && gutIndicators.length > 0">
-            <view class="section-header">
-              <text class="section-title">&#x1F9E0; 肠脑情绪关联</text>
-            </view>
-            <view class="gut-insight-text" v-if="gutInsight">
-              <text>{{ gutInsight }}</text>
-            </view>
-            <view class="gut-indicator-list">
-              <view class="gut-indicator-item" v-for="ind in gutIndicators" :key="ind.id">
-                <view class="gut-indicator-left">
-                  <text class="gut-indicator-name">{{ ind.indicatorName || ind.name }}</text>
-                </view>
-                <view class="gut-indicator-bar-track">
-                  <view class="gut-indicator-bar-fill" :style="{ width: (ind.value || 0) + '%', background: indicatorColor(ind.status) }"></view>
-                </view>
-                <text class="gut-indicator-value">{{ ind.value }}{{ ind.unit || '%' }}</text>
-                <text :class="['gut-indicator-status', 'status-' + (ind.status || 'normal')]">{{ statusLabel(ind.status) }}</text>
+        <view class="section" v-if="gutIndicators && gutIndicators.length > 0">
+          <view class="section-header">
+            <text class="section-title">&#x1F9E0; 肠脑情绪关联</text>
+          </view>
+          <view class="gut-insight-text" v-if="gutInsight">
+            <text>{{ gutInsight }}</text>
+          </view>
+          <view class="gut-indicator-list">
+            <view class="gut-indicator-item" v-for="ind in gutIndicators" :key="ind.id">
+              <view class="gut-indicator-left">
+                <text class="gut-indicator-name">{{ ind.indicatorName || ind.name }}</text>
+              </view>
+              <view class="gut-indicator-bar-track">
+                <view class="gut-indicator-bar-fill" :style="{ width: (ind.value || 0) + '%', background: indicatorColor(ind.status) }"></view>
               </view>
+              <text class="gut-indicator-value">{{ ind.value }}{{ ind.unit || '%' }}</text>
+              <text :class="['gut-indicator-status', 'status-' + (ind.status || 'normal')]">{{ statusLabel(ind.status) }}</text>
             </view>
           </view>
         </view>
-        <view v-else>
-          <FamilyEmptyState type="card" />
-        </view>
 
     
 
@@ -245,13 +230,6 @@ components: { TabTransition, PageBanner, RadarChart, LoginGuideCard, FamilyEnerg
     }
   },
   computed: {
-    hasFamily: function() {
-      // 应急修复(2026-08-07,对齐 body/index.vue):store.familyId 在 familyId 后端派生改造后
-      // 不再被写入,hasFamily 恒为 false 导致内容区被 v-else 空态吞掉。
-      // 后端所有登录路径均兜底自动创建家庭(UserService.ensureUserHasFamily),
-      // 故登录用户视为有家庭,内容区由内部组件基于接口数据自行渲染。
-      return this.isLoggedIn
-    },
     personalityScores: function() {
       if (!this.emiData) return []
       return [

+ 0 - 9
cfc-frontend/pages/mind-detail/member-mind-detail.vue

@@ -9,7 +9,6 @@
     </view>
 
     <!-- 五维能量条 -->
-    <view v-if="hasFamily">
     <FamilyEnergyBar
       dimensionCode="mind"
       :sandboxData="sandboxData"
@@ -62,10 +61,6 @@
 
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
-    </view>
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
   </view>
 </template>
 
@@ -92,10 +87,6 @@ export default {
     }
   },
   computed: {
-    hasFamily: function() {
-      if (this.memberId) return true
-      return this.$store.getters.hasFamily
-    },
     graphMembers: function() {
       if (!this.sandboxData || !this.sandboxData.members) return []
       return this.sandboxData.members.map(function(m) {

+ 0 - 9
cfc-frontend/pages/mind/member-mind-detail.vue

@@ -9,7 +9,6 @@
     </view>
 
     <!-- 五维能量条 -->
-    <view v-if="hasFamily">
     <FamilyEnergyBar
       dimensionCode="mind"
       :sandboxData="sandboxData"
@@ -62,10 +61,6 @@
 
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
-    </view>
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
   </view>
 </template>
 
@@ -92,10 +87,6 @@ export default {
     }
   },
   computed: {
-    hasFamily: function() {
-      if (this.memberId) return true
-      return this.$store.getters.hasFamily
-    },
     graphMembers: function() {
       if (!this.sandboxData || !this.sandboxData.members) return []
       return this.sandboxData.members.map(function(m) {

+ 0 - 10
cfc-frontend/pages/parent/child-detail.vue

@@ -1,6 +1,5 @@
 <template>
   <view class="container">
-    <view v-if="hasFamily">
     <!-- 加载状态 -->
     <view class="loading-container" v-if="loading">
       <text class="loading-text">加载中...</text>
@@ -104,11 +103,6 @@
         </view>
       </view>
     </template>
-    </view>
-
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
   </view>
 </template>
 
@@ -128,10 +122,6 @@ export default {
     }
   },
   computed: {
-    hasFamily: function() {
-      if (this.memberId) return true
-      return this.$store.getters.hasFamily
-    },
     completedCount() {
       return this.todayTasks.filter(t => t.status === 'completed').length
     },

+ 1 - 13
cfc-frontend/pages/parent/plans/index.vue

@@ -1,6 +1,5 @@
 <template>
   <view class="container">
-    <view v-if="hasFamily">
     <view class="header">
       <text class="title">我的计划</text>
       <text class="subtitle">查看和管理家庭成长计划</text>
@@ -34,11 +33,6 @@
         </view>
       </view>
     </view>
-    </view>
-
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
   </view>
 </template>
 
@@ -52,13 +46,7 @@ export default {
       plans: []
     }
   },
-  computed: {
-    hasFamily: function() {
-      if (this.loading) return true
-      if (this.plans && this.plans.length > 0) return true
-      return this.$store.getters.hasFamily
-    }
-  },
+  computed: {},
   onShow() {
     this.loadPlans()
   },

+ 0 - 10
cfc-frontend/pages/parent/wishes/index.vue

@@ -1,6 +1,5 @@
 <template>
   <view class="container">
-    <view v-if="hasFamily">
     <!-- 标签页切换 -->
     <view class="tabs">
       <view 
@@ -138,11 +137,6 @@
         </view>
       </view>
     </view>
-    </view>
-
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
   </view>
 </template>
 
@@ -170,10 +164,6 @@ export default {
     }
   },
   computed: {
-    hasFamily: function() {
-      if (this.memberId) return true
-      return this.$store.getters.hasFamily
-    },
     pendingCount() {
       return this.pendingWishes.length
     },

+ 0 - 11
cfc-frontend/pages/rewards/badge.vue

@@ -1,6 +1,5 @@
 <template>
   <view class="badge-container">
-    <view v-if="hasFamily">
     <!-- 统计概览 -->
     <view class="stats-card">
       <view class="stat-item">
@@ -95,11 +94,6 @@
         </view>
       </view>
     </view>
-    </view>
-
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
   </view>
 </template>
 
@@ -138,11 +132,6 @@ export default {
     }
   },
   computed: {
-    hasFamily: function() {
-      var token = uni.getStorageSync('token')
-      if (token) return true
-      return this.$store.getters.hasFamily
-    },
     detailIcon() {
       return this.detailBadge ? this.detailBadge.icon : ''
     },

+ 3 - 13
cfc-frontend/pages/share/index.vue

@@ -1,10 +1,10 @@
 <template>
   <view class="share-page">
-    <view class="sp-header" v-if="hasFamily && familyName">
+    <view class="sp-header" v-if="familyName">
       <text class="sp-title">{{ familyName }} · 健康足迹</text>
     </view>
 
-    <view class="sp-body" v-if="hasFamily && members && members.length > 0">
+    <view class="sp-body" v-if="members && members.length > 0">
       <view class="sp-member" v-for="m in members" :key="m.name">
         <image class="sp-avatar" :src="m.avatar || '/static/default-avatar.png'" mode="aspectFill"></image>
         <view class="sp-member-info">
@@ -19,14 +19,10 @@
       </view>
     </view>
 
-    <view class="sp-footer" v-if="hasFamily && totalCheckinDays">
+    <view class="sp-footer" v-if="totalCheckinDays">
       <text class="sp-slogan">我们坚持健康打卡 {{ totalCheckinDays }} 天了!</text>
     </view>
 
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
-
     <view class="sp-qr">
       <text class="sp-qr-text">扫码加入我们一起健康</text>
     </view>
@@ -41,12 +37,6 @@
 import { getFamilyFootprint } from '../../utils/api.js'
 
 export default {
-  computed: {
-    hasFamily: function() {
-      if (this.familyName || (this.members && this.members.length > 0)) return true
-      return this.$store.getters.hasFamily
-    }
-  },
   data() {
     return {
       familyName: '',

+ 0 - 10
cfc-frontend/pages/teacher/teacher-profile.vue

@@ -5,7 +5,6 @@
       <text>🔄 切换家长端</text>
     </view>
 
-    <view v-if="hasFamily">
     <view class="user-card">
       <view class="avatar">👤</view>
       <view class="user-info">
@@ -47,10 +46,6 @@
         <text class="arrow">›</text>
       </view>
     </view>
-    </view>
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
 
     <!-- 密码设置弹窗 -->
     <view class="modal-mask" v-if="showPasswordModal" @click="showPasswordModal = false">
@@ -92,11 +87,6 @@ export default {
     statusText() {
       const map = { pending: '审核中', approved: '已通过', rejected: '已拒绝' }
       return map[this.teacherStatus] || this.teacherStatus
-    },
-    hasFamily: function() {
-      var token = uni.getStorageSync('token')
-      if (token) return true
-      return this.$store.getters.hasFamily
     }
   },
   onLoad() {

+ 0 - 12
cfc-frontend/pages/wisdom-detail/member-wisdom-detail.vue

@@ -8,7 +8,6 @@
       <text class="nav-title">智 · 成员详情</text>
     </view>
 
-    <view v-if="hasFamily">
     <!-- 五维能量条 -->
     <FamilyEnergyBar
       dimensionCode="wisdom"
@@ -59,11 +58,6 @@
       :products="dimensionProducts"
       @productClick="goProductDetail"
       @moreProducts="goMoreProducts" />
-    </view>
-
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
 
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
@@ -95,12 +89,6 @@ export default {
       sandboxData: null
     }
   },
-  computed: {
-    hasFamily: function() {
-      if (this.memberId) return true
-      return this.$store.getters.hasFamily
-    }
-  },
   onLoad: function(options) {
     if (options && options.memberId) {
       this.selfId = options.memberId

+ 0 - 12
cfc-frontend/pages/wisdom/member-wisdom-detail.vue

@@ -8,7 +8,6 @@
       <text class="nav-title">智 · 成员详情</text>
     </view>
 
-    <view v-if="hasFamily">
     <!-- 五维能量条 -->
     <FamilyEnergyBar
       dimensionCode="wisdom"
@@ -59,11 +58,6 @@
       :products="dimensionProducts"
       @productClick="goProductDetail"
       @moreProducts="goMoreProducts" />
-    </view>
-
-    <view v-else>
-      <FamilyEmptyState type="card" />
-    </view>
 
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
@@ -95,12 +89,6 @@ export default {
       sandboxData: null
     }
   },
-  computed: {
-    hasFamily: function() {
-      if (this.memberId) return true
-      return this.$store.getters.hasFamily
-    }
-  },
   onLoad: function(options) {
     if (options && options.memberId) {
       this.selfId = options.memberId

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

@@ -550,10 +550,6 @@ export const acceptTask = (taskId, memberId) => {
   return request('/api/tasks/accept', 'POST', { taskId, memberId })
 }
 
-export const rejectTask = (taskId, memberId) => {
-  return request(`/api/tasks/reject/${taskId}`, 'POST', { memberId })
-}
-
 // ===
 // 家长心愿单模块
 // ===