Bläddra i källkod

refactor: 统一五维首页商品推荐组件为 DimensionProducts

- 删除 DimensionProductList/RecommendedSection/recommend-mixin
- 五维首页(index-home/body/wisdom/mind/wealth)与 action 页统一使用 DimensionProducts
- 统一商品跳转 goProductDetail/goMoreProducts,移除登录跳转冗余分支
- 修复 DimensionProducts 空态文案依赖 isLoggedIn 的逻辑
Xiaogang Liao 1 månad sedan
förälder
incheckning
4f7d4d1e69

+ 0 - 202
cfc-frontend/components/DimensionProductList.vue

@@ -1,202 +0,0 @@
-<template>
-  <view class="dimension-product-list">
-    <view class="section-header">
-      <text class="section-title">{{ title }}</text>
-    </view>
-    <view class="product-grid" v-if="products.length > 0">
-      <view class="product-card" v-for="product in products" :key="product.id" @click="goProduct(product)">
-        <image class="product-cover" :src="product.coverImage || '/static/default-product.png'" mode="aspectFill"></image>
-        <view class="product-info">
-          <text class="product-name">{{ product.name }}</text>
-          <view class="product-price-row">
-            <text class="product-price">¥{{ (product.price / 100).toFixed(2) }}</text>
-            <text class="product-reason" v-if="product.reason">{{ product.reason }}</text>
-          </view>
-          <view class="match-badge" v-if="product.matchScore">
-            <text class="match-score">{{ product.matchScore }}分</text>
-          </view>
-        </view>
-      </view>
-    </view>
-    <view class="empty-state" v-else-if="!loading">
-      <text class="empty-text">{{ familyId ? '暂无推荐商品' : '登录后查看推荐商品' }}</text>
-    </view>
-    <view class="loading-state" v-if="loading">
-      <text class="loading-text">加载中...</text>
-    </view>
-  </view>
-</template>
-
-<script>
-import { getDimensionProducts, clickRepurchaseReminder } from '../utils/api.js'
-
-export default {
-  props: {
-    dimensionCode: { type: String, required: true },
-    familyId: { type: [Number, String], default: null },
-    memberScores: { type: Object, default: function() { return {} } },
-    title: { type: String, default: '为你推荐' }
-  },
-  data: function() {
-    return {
-      products: [],
-      loading: false
-    }
-  },
-  mounted: function() {
-    // 无 familyId(未登录/未选择孩子)时不请求推荐接口,展示引导文案
-    if (!this.familyId) {
-      this.loading = false
-      return
-    }
-    this.loadProducts()
-  },
-  methods: {
-    loadProducts: function() {
-      var self = this
-      self.loading = true
-      var params = {
-        dimensionCode: self.dimensionCode,
-        familyId: self.familyId,
-        limit: 6
-      }
-      if (self.memberScores && Object.keys(self.memberScores).length > 0) {
-        params.memberScores = self.memberScores
-      }
-      
-      getDimensionProducts(params).then(function(res) {
-        self.loading = false
-        if (res.code === 200 && res.data) {
-          self.products = Array.isArray(res.data) ? res.data : (res.data.records || [])
-        }
-      }).catch(function() {
-        self.loading = false
-      })
-    },
-    goProduct: function(product) {
-      if (!product || !product.id) return
-
-      if (product.source === 'repurchase' && product.id) {
-        clickRepurchaseReminder(product.id).catch(function() {})
-      }
-
-      uni.navigateTo({
-        url: '/pages/discover-detail/product-detail/product-detail?id=' + product.productId + '&from=dimension'
-      })
-    }
-  }
-}
-</script>
-
-<style scoped>
-.dimension-product-list {
-  margin: 20rpx 20rpx;
-}
-
-.section-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 20rpx;
-}
-
-.section-title {
-  font-size: 30rpx;
-  font-weight: bold;
-  color: #333;
-}
-
-.product-grid {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 20rpx;
-}
-
-.product-card {
-  width: calc(50% - 10rpx);
-  background: #fff;
-  border-radius: 16rpx;
-  overflow: hidden;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-}
-
-.product-cover {
-  width: 100%;
-  height: 240rpx;
-  background: #f0f0f0;
-}
-
-.product-info {
-  padding: 16rpx;
-}
-
-.product-name {
-  font-size: 26rpx;
-  color: #333;
-  display: block;
-  margin-bottom: 12rpx;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  display: -webkit-box;
-  -webkit-line-clamp: 2;
-  -webkit-box-orient: vertical;
-}
-
-.product-price-row {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 8rpx;
-}
-
-.product-price {
-  font-size: 28rpx;
-  color: #F97316;
-  font-weight: bold;
-}
-
-.product-reason {
-  font-size: 22rpx;
-  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-top: 8rpx;
-}
-
-.match-score {
-  font-size: 22rpx;
-  color: #fff;
-  font-weight: 500;
-}
-
-.empty-state {
-  display: flex;
-  justify-content: center;
-  padding: 60rpx 0;
-}
-
-.empty-text {
-  font-size: 26rpx;
-  color: #999;
-}
-
-.loading-state {
-  display: flex;
-  justify-content: center;
-  padding: 40rpx 0;
-}
-
-.loading-text {
-  font-size: 24rpx;
-  color: #999;
-}
-</style>

+ 117 - 17
cfc-frontend/components/DimensionProducts.vue

@@ -1,28 +1,39 @@
 <template>
   <view class="section">
     <view class="section-header">
-      <text class="section-title">🛍️ 推荐商品</text>
-      <text class="section-more" @click="$emit('moreProducts')">更多 ›</text>
+      <text class="section-title">🛍️ {{ title }}</text>
+      <text class="section-more" v-if="showMore" @click="$emit('moreProducts')">更多 ›</text>
     </view>
-    <view v-if="!displayProducts || displayProducts.length === 0" class="empty-state">
-      <text class="empty-tip">{{ isLoggedIn ? '暂无商品' : '登录后查看更多商品' }}</text>
+    <view v-if="!loading && displayProducts.length === 0" class="empty-state">
+      <text class="empty-tip">{{ emptyText }}</text>
     </view>
-    <view class="product-list" v-if="displayProducts && displayProducts.length > 0">
-      <view class="product-card" v-for="prod in displayProducts" :key="prod.id" @click="$emit('productClick', prod)">
+    <view class="product-list" v-if="displayProducts.length > 0">
+      <view class="product-card" v-for="prod in displayProducts" :key="prod.id" @click="onProductClick(prod)">
         <image class="product-img" :src="prod.coverImage || '/static/default-product.png'" mode="aspectFill" />
         <text class="product-name line-clamp-2">{{ prod.name }}</text>
         <view class="weight-badges" v-if="prod._badges && prod._badges.length">
           <text class="weight-badge" v-for="badge in prod._badges" :key="badge.name" :style="{ background: badge.color + '20', color: badge.color }">{{ badge.name }}{{ badge.weight }}%</text>
         </view>
-        <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>
+        <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>
 </template>
 
 <script>
+import { getDimensionProducts, clickRepurchaseReminder } from '../utils/api.js'
+
 var DIMENSION_MAP = {
   body: { name: '身', color: '#FF8C42' },
   mind: { name: '心', color: '#FF6B9D' },
@@ -34,27 +45,82 @@ var DIMENSION_MAP = {
 export default {
   props: {
     dimensionCode: { type: String, default: '' },
+    // 兼容模式:父组件直接传入商品数组(member-detail 等页面)
     products: { type: Array, default: function() { return [] } },
-    isLoggedIn: { type: Boolean, default: false }
+    // 自加载模式:传入 familyId 后组件内部调用 getDimensionProducts
+    familyId: { type: [Number, String], default: null },
+    isLoggedIn: { type: Boolean, default: false },
+    title: { type: String, default: '推荐商品' },
+    showMore: { type: Boolean, default: true }
   },
   data: function() {
     return {
+      selfProducts: [],
+      loading: false
     }
   },
   computed: {
     displayProducts: function() {
+      var list = this.products && this.products.length > 0 ? this.products : this.selfProducts
       var self = this
-      return (this.products || []).map(function(item) {
+      return (list || []).map(function(item) {
         item._badges = self.parseWeights(item.dimensionWeights)
         return item
       })
+    },
+    emptyText: function() {
+      // 有 familyId(已登录且选中孩子)但推荐接口无数据
+      if (this.familyId) {
+        return '暂无推荐商品'
+      }
+      return this.isLoggedIn ? '暂无商品' : '登录后查看更多商品'
+    }
+  },
+  mounted: function() {
+    // 自加载模式:有 familyId 且父组件未传入 products 时才请求推荐接口
+    if (this.familyId && (!this.products || this.products.length === 0)) {
+      this.loadDimensionProducts()
+    }
+  },
+  watch: {
+    familyId: function(val) {
+      // 切换孩子/登录态变化时重新加载
+      if (val && (!this.products || this.products.length === 0)) {
+        this.loadDimensionProducts()
+      }
     }
   },
   methods: {
-formatPriceWithSymbol: function(price) {
-if (price == null) return '免费'
-return '¥' + (Number(price) / 100).toFixed(2)
-},
+    loadDimensionProducts: function() {
+      var self = this
+      self.loading = true
+      var params = {
+        dimensionCode: self.dimensionCode,
+        familyId: self.familyId,
+        limit: 6
+      }
+      getDimensionProducts(params).then(function(res) {
+        self.loading = false
+        if (res.code === 200 && res.data) {
+          self.selfProducts = Array.isArray(res.data) ? res.data : (res.data.records || [])
+        }
+      }).catch(function() {
+        self.loading = false
+        self.selfProducts = []
+      })
+    },
+    onProductClick: function(prod) {
+      if (!prod || !prod.id) return
+      // 复购提醒追踪(推荐商品点击行为上报)
+      if (prod.source === 'repurchase' && prod.id) {
+        clickRepurchaseReminder(prod.id).catch(function() {})
+      }
+      this.$emit('productClick', prod)
+    },
+    formatPriceWithSymbol: function(price) {
+      if (price == null) return '免费'
+      return '¥' + (Number(price) / 100).toFixed(2)
+    },
     parseWeights: function(str) {
       if (!str) return []
       try {
@@ -132,12 +198,37 @@ return '¥' + (Number(price) / 100).toFixed(2)
   border-radius: 6rpx;
   font-weight: 500;
 }
+.price-row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: space-between;
+  padding: 8rpx 16rpx 16rpx;
+}
 .product-price {
   font-size: 28rpx;
   color: #F97316;
   font-weight: bold;
-  padding: 8rpx 16rpx 16rpx;
-  display: block;
+}
+.product-reason {
+  font-size: 20rpx;
+  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 16rpx 16rpx;
+}
+.match-score {
+  font-size: 22rpx;
+  color: #fff;
+  font-weight: 500;
 }
 .line-clamp-2 {
   overflow: hidden;
@@ -155,4 +246,13 @@ return '¥' + (Number(price) / 100).toFixed(2)
   font-size: 24rpx;
   color: #999;
 }
+.loading-state {
+  display: flex;
+  justify-content: center;
+  padding: 40rpx 0;
+}
+.loading-text {
+  font-size: 24rpx;
+  color: #999;
+}
 </style>

+ 0 - 180
cfc-frontend/components/RecommendedSection.vue

@@ -1,180 +0,0 @@
-<template>
-  <view class="recommended-section">
-    <view class="section-header">
-      <text class="section-icon">✨</text>
-      <text class="section-title">为你推荐</text>
-      <text class="section-more" v-if="hasMore && items && items.length > 0" @click="$emit('more')">更多 ›</text>
-    </view>
-    <scroll-view class="scroll-x" scroll-x show-scrollbar="false" v-if="items && items.length > 0">
-      <view class="card-list">
-        <view
-          class="card-item"
-          v-for="(item, idx) in items"
-          :key="idx"
-          @click="onItemClick(item)"
-          hover-class="card-hover"
-        >
-          <image class="card-img" :src="item.coverImage || '/static/default.png'" mode="aspectFill" />
-          <view class="card-body">
-            <text class="card-title line-clamp-2">{{ item.title }}</text>
-            <view class="card-footer">
-              <text class="card-reason line-clamp-1">{{ item.recommendReason }}</text>
-              <text class="card-badge" v-if="item.badge" :class="'badge-' + item.badge">{{ badgeLabel(item.badge) }}</text>
-            </view>
-          </view>
-        </view>
-      </view>
-    </scroll-view>
-    <view class="empty-state" v-else>
-      <text class="empty-text">暂无推荐内容</text>
-    </view>
-  </view>
-</template>
-
-<script>
-export default {
-  props: {
-    items: {
-      type: Array,
-      default: function() { return [] }
-    },
-    hasMore: {
-      type: Boolean,
-      default: false
-    }
-  },
-  methods: {
-    onItemClick(item) {
-      this.$emit('itemClick', item)
-    },
-    badgeLabel(badge) {
-      var map = {
-        hot: '热门',
-        new: '最新',
-        renew: '续费',
-        task: '任务'
-      }
-      return map[badge] || badge
-    }
-  }
-}
-</script>
-
-<style scoped>
-.recommended-section {
-  margin: 20rpx 30rpx;
-}
-.section-header {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  margin-bottom: 16rpx;
-}
-.section-icon {
-  font-size: 28rpx;
-  margin-right: 8rpx;
-}
-.section-title {
-  font-size: 30rpx;
-  font-weight: 700;
-  color: #333;
-  flex: 1;
-}
-.section-more {
-  font-size: 24rpx;
-  color: #999;
-}
-.scroll-x {
-  white-space: nowrap;
-  overflow: hidden;
-}
-.card-list {
-  display: flex;
-  flex-direction: row;
-  gap: 16rpx;
-}
-.card-item {
-  display: flex;
-  flex-direction: column;
-  width: 240rpx;
-  background: #fff;
-  border-radius: 16rpx;
-  overflow: hidden;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-  flex-shrink: 0;
-}
-.card-hover {
-  opacity: 0.85;
-}
-.card-img {
-  width: 240rpx;
-  height: 160rpx;
-  background: #f5f5f5;
-}
-.card-body {
-  padding: 12rpx;
-  display: flex;
-  flex-direction: column;
-  gap: 8rpx;
-}
-.card-title {
-  font-size: 26rpx;
-  font-weight: 600;
-  color: #333;
-  line-height: 1.4;
-  height: 72rpx;
-}
-.card-footer {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-}
-.card-reason {
-  font-size: 20rpx;
-  color: #999;
-  flex: 1;
-  margin-right: 8rpx;
-}
-.card-badge {
-  font-size: 20rpx;
-  padding: 2rpx 10rpx;
-  border-radius: 6rpx;
-  flex-shrink: 0;
-}
-.badge-hot {
-  background: #fff1f0;
-  color: #f56c6c;
-}
-.badge-new {
-  background: #e6f7ff;
-  color: #1890ff;
-}
-.badge-renew {
-  background: #fff7e6;
-  color: #fa8c16;
-}
-.badge-task {
-  background: #f6ffed;
-  color: #52c41a;
-}
-.line-clamp-1 {
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-.line-clamp-2 {
-  display: -webkit-box;
-  -webkit-box-orient: vertical;
-  -webkit-line-clamp: 2;
-  overflow: hidden;
-}
-.empty-state {
-  padding: 40rpx 0;
-  text-align: center;
-}
-.empty-text {
-  font-size: 26rpx;
-  color: #ccc;
-}
-</style>

+ 0 - 122
cfc-frontend/mixins/recommend-mixin.js

@@ -1,122 +0,0 @@
-/**
- * 推荐系统 Mixin — 为维度页面提供个性化推荐加载与行为追踪
- *
- * 用法:
- *   import recommendMixin from '../../mixins/recommend-mixin'
- *   export default {
- *     mixins: [recommendMixin],
- *     methods: {
- *       loadDimensionData: function() {
- *         this.loadRecommendations('action')  // 传入当前维度代码
- *       }
- *     }
- *   }
- *
- * 模板:
- *   <RecommendedSection
- *     v-if="recommendItems.length > 0"
- *     :items="recommendItems"
- *     :hasMore="recommendHasMore"
- *     @itemClick="onRecommendItemClick"
- *     @more="$emit('more')" />
- */
-import { getRecommendList, trackRecommendAction } from '../utils/api'
-
-var recommendMixin = {
-  data: function() {
-    return {
-      recommendItems: [],
-      recommendHasMore: false,
-      _recommendDimension: null
-    }
-  },
-  methods: {
-    /**
-     * 加载个性化推荐列表
-     * @param {String} dimensionCode 维度代码 body/mind/wisdom/action/wealth
-     * @param {Number} limit 推荐数量
-     */
-    loadRecommendations: function(dimensionCode, limit) {
-      limit = limit || 6
-      this._recommendDimension = dimensionCode
-      var self = this
-      getRecommendList({
-        dimensionCode: dimensionCode,
-        size: limit
-      }).then(function(res) {
-        if (res.code === 200 && res.data && res.data.items) {
-          self.recommendItems = res.data.items || []
-          self.recommendHasMore = res.data.hasMore || false
-        }
-      }).catch(function() {
-        // 静默失败,不阻塞页面
-      })
-    },
-
-    /**
-     * 推荐项点击处理 — 跳转 + 行为追踪
-     */
-    onRecommendItemClick: function(item) {
-      if (!item) return
-      // 追踪点击行为
-      this._trackRecommend(item, 'click')
-      // 根据内容类型跳转
-      this._navigateToRecommend(item)
-    },
-
-    /**
-     * 内部:追踪推荐行为
-     */
-    _trackRecommend: function(item, action) {
-      trackRecommendAction({
-        contentType: item.contentType,
-        contentId: item.id,
-        action: action
-      }).catch(function() {
-        // 静默
-      })
-    },
-
-    /**
-     * 内部:根据内容类型导航
-     */
-    _navigateToRecommend: function(item) {
-      var url = ''
-      switch (item.contentType) {
-        case 'article':
-          url = '/pages/article-center/article-detail/article-detail?id=' + item.id
-          break
-        case 'task':
-          url = '/pages/tasks/task-detail/task-detail?id=' + item.id
-          break
-        case 'product':
-          url = '/pages/shop/product-detail/product-detail?id=' + item.id
-          break
-        case 'activity':
-          url = '/pages/activity/activity-detail/activity-detail?id=' + item.id
-          break
-        default:
-          return
-      }
-      if (url) {
-        uni.navigateTo({ url: url })
-      }
-    }
-  },
-
-  /**
-   * 页面显示时展示推荐内容(曝光追踪)
-   */
-  onShow: function() {
-    if (this.recommendItems && this.recommendItems.length > 0) {
-      var self = this
-      // 只追踪前3条曝光(避免过多请求)
-      var showItems = this.recommendItems.slice(0, 3)
-      showItems.forEach(function(item) {
-        self._trackRecommend(item, 'show')
-      })
-    }
-  }
-}
-
-export default recommendMixin

+ 6 - 18
cfc-frontend/pages/action/index.vue

@@ -68,10 +68,12 @@
       @activityClick="goActivityDetail"
       @moreActivities="goMoreActivities" />
 
-    <DimensionProductList
+    <DimensionProducts
       dimensionCode="action"
       :familyId="activeChildId"
-      title="为你推荐" />
+      title="为你推荐"
+      @productClick="goProductDetail"
+      @moreProducts="goMoreProducts" />
 
     <!-- 推荐阅读 -->
     <ArticleBookshelf
@@ -111,13 +113,6 @@
           </view>
         </view>
 
-        <!-- 推荐 -->
-        <RecommendedSection
-          :items="recommendItems"
-          :hasMore="recommendHasMore"
-          @itemClick="onRecommendItemClick"
-          @more="goMoreRecommend" />
-
         <!-- 圈子详情弹窗 -->
         <CircleDetail
           :visible="circleDetailVisible"
@@ -151,20 +146,17 @@ import LoginGuideCard from '../../components/LoginGuideCard.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
-import DimensionProductList from '../../components/DimensionProductList.vue'
+import DimensionProducts from '../../components/DimensionProducts.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
 import CircleCard from '../../components/CircleCard.vue'
 import CircleDetail from '../../components/CircleDetail.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
 import WuxingSandbox from '../../components/wuxing-sandbox.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
-import RecommendedSection from '../../components/RecommendedSection.vue'
-import recommendMixin from '../../mixins/recommend-mixin'
 import { getVisibleSections, getEnergyOverview, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren, getVisibleFamilyMembers, getFeaturedArticles, getMyCircles, discoverCircles, joinGeneralCircle, leaveGeneralCircle } from '../../utils/api.js'
 
 export default {
-  mixins: [recommendMixin],
-  components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProductList, ArticleBookshelf, FamilyRelationGraph, WuxingSandbox, FamilyMemberStrip, CircleCard, CircleDetail, RecommendedSection },
+  components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProducts, ArticleBookshelf, FamilyRelationGraph, WuxingSandbox, FamilyMemberStrip, CircleCard, CircleDetail },
   data() {
     return {
       showTabTransition: true,
@@ -322,10 +314,6 @@ export default {
       this.loadDimensionActivities()
       this.loadDimensionProducts()
       this.loadCircleData()
-      this.loadRecommendations('action')
-    },
-    goMoreRecommend: function() {
-      uni.navigateTo({ url: '/pages/discover/index' })
     },
     loadEnergyData: function() {
       var self = this

+ 12 - 17
cfc-frontend/pages/body/index.vue

@@ -107,10 +107,12 @@
       @moreActivities="goMoreActivities" />
 
     <!-- 商品推荐 -->
-    <DimensionProductList
+    <DimensionProducts
       dimensionCode="body"
       :familyId="activeChildId"
-      title="为你推荐" />
+      title="为你推荐"
+      @productClick="goProductDetail"
+      @moreProducts="goMoreProducts" />
 
     <!-- 推荐阅读 -->
     <ArticleBookshelf
@@ -119,13 +121,6 @@
       @articleClick="goArticleDetail"
       @moreArticles="goMoreArticles" />
 
-    <!-- 个性化推荐 -->
-    <RecommendedSection
-      :items="recommendItems"
-      :hasMore="recommendHasMore"
-      @itemClick="onRecommendItemClick"
-      @more="goMoreRecommend" />
-
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
     <AIFloatingAvatar />
@@ -139,18 +134,15 @@ import LoginGuideCard from '../../components/LoginGuideCard.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
-import DimensionProductList from '../../components/DimensionProductList.vue'
+import DimensionProducts from '../../components/DimensionProducts.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
-import RecommendedSection from '../../components/RecommendedSection.vue'
-import recommendMixin from '../../mixins/recommend-mixin'
 import { getVisibleSections, getEnergyOverview, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getVisibleFamilyMembers, getDimensionOverview, getFeaturedArticles } from '../../utils/api.js'
 
 export default {
-  mixins: [recommendMixin],
-  components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProductList, ArticleBookshelf, RadarChart, AIFloatingAvatar, FamilyMemberStrip, RecommendedSection },
+  components: { TabTransition, PageBanner, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProducts, ArticleBookshelf, RadarChart, AIFloatingAvatar, FamilyMemberStrip },
   data() {
     return {
       isLoggedIn: false,
@@ -300,10 +292,13 @@ export default {
       this.loadDimensionProducts()
       this.loadSandboxData()
       this.loadHealthDimensions()
-      this.loadRecommendations('body')
     },
-    goMoreRecommend: function() {
-      uni.navigateTo({ url: '/pages/discover/index' })
+    goProductDetail: function(prod) {
+      if (!prod || !prod.id) return
+      uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
+    },
+    goMoreProducts: function() {
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     loadHealthDimensions: function() {
       var memberId = this.activeChildId || (this.userInfo && this.userInfo.userId)

+ 1 - 16
cfc-frontend/pages/index-home/index.vue

@@ -146,13 +146,6 @@
       </view>
     </view>
 
-    <!-- 个性化推荐 -->
-    <RecommendedSection
-      :items="recommendItems"
-      :hasMore="recommendHasMore"
-      @itemClick="onRecommendItemClick"
-      @more="goMoreRecommend" />
-
     <!-- 圈子详情弹窗 -->
     <CircleDetail
       :visible="circleDetailVisible"
@@ -359,8 +352,6 @@ import CircleCard from '../../components/CircleCard.vue'
 import CircleDetail from '../../components/CircleDetail.vue'
 import LoginGuideCard from '../../components/LoginGuideCard.vue'
 import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
-import RecommendedSection from '../../components/RecommendedSection.vue'
-import recommendMixin from '../../mixins/recommend-mixin'
 import { acceptParentInvite, productList, getActivityList, getFeaturedArticles, getFamilyEnergySandbox, getVisibleFamilyMembers, getTodayTasksByCategory, getChildren, getEnergyOverview, getProductsByDomain, getChallengeList, getMyCircles, discoverCircles, joinGeneralCircle, leaveGeneralCircle } from '../../utils/api.js'
 
 var DIMENSIONS = [
@@ -372,7 +363,6 @@ var DIMENSIONS = [
 ]
 
 export default {
-  mixins: [recommendMixin],
   components: {
     PageBanner,
     WuxingSandbox,
@@ -386,8 +376,7 @@ export default {
     CircleCard,
     CircleDetail,
     LoginGuideCard,
-    FamilyChallengeCard,
-    RecommendedSection
+    FamilyChallengeCard
   },
   data() {
     // 从 storage 预取登录态/角色,确保首次渲染即正确(WeChat 渲染层/逻辑层分离架构下 onLoad 执行前已渲染)
@@ -572,10 +561,6 @@ export default {
       this.loadChallenges()
       this.loadMyCircles()
       this.loadDiscoverCircles()
-      this.loadRecommendations('action')
-    },
-    goMoreRecommend: function() {
-      uni.navigateTo({ url: '/pages/discover/index' })
     },
     loadEnergyData: function() {
       var self = this

+ 12 - 17
cfc-frontend/pages/mind/index.vue

@@ -113,10 +113,12 @@
       @activityClick="goActivityDetail"
       @moreActivities="goMoreActivities" />
 
-    <DimensionProductList
+    <DimensionProducts
       dimensionCode="mind"
       :familyId="currentChildId"
-      title="为你推荐" />
+      title="为你推荐"
+      @productClick="goProductDetail"
+      @moreProducts="goMoreProducts" />
 
     <!-- 推荐阅读 -->
     <ArticleBookshelf
@@ -150,13 +152,6 @@
       </view>
     </view>
 
-    <!-- 个性化推荐 -->
-    <RecommendedSection
-      :items="recommendItems"
-      :hasMore="recommendHasMore"
-      @itemClick="onRecommendItemClick"
-      @more="goMoreRecommend" />
-
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
     <AIFloatingAvatar />
@@ -171,7 +166,7 @@ import RadarChart from '../../components/RadarChart.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
-import DimensionProductList from '../../components/DimensionProductList.vue'
+import DimensionProducts from '../../components/DimensionProducts.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
 import PsychCrisisBanner from '../../components/PsychCrisisBanner.vue'
 import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
@@ -179,12 +174,9 @@ import FamilyTianpanCard from '../../components/FamilyTianpanCard.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
 import { getVisibleSections, getFeaturedArticles, getEmiReport, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getChildren, getVisibleFamilyMembers, getEnergyOverview, getContactList, getHealthAlerts, getMilestones, getDanReportByChild, getFamilyFortune } from '../../utils/api.js'
 import config from '../../config.js'
-import RecommendedSection from '../../components/RecommendedSection.vue'
-import recommendMixin from '../../mixins/recommend-mixin'
 
 export default {
-  mixins: [recommendMixin],
-components: { TabTransition, PageBanner, RadarChart, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProductList, ArticleBookshelf, PsychCrisisBanner, AIFloatingAvatar, FamilyTianpanCard, FamilyMemberStrip, RecommendedSection },
+components: { TabTransition, PageBanner, RadarChart, LoginGuideCard, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProducts, ArticleBookshelf, PsychCrisisBanner, AIFloatingAvatar, FamilyTianpanCard, FamilyMemberStrip },
   data() {
     return {
       isLoggedIn: false,
@@ -454,7 +446,6 @@ components: { TabTransition, PageBanner, RadarChart, LoginGuideCard, FamilyEnerg
     }
     // 游客也能浏览活动和商品
     this.loadDimensionActivities()
-    this.loadRecommendations('mind')
     this.loadFamilyFortune()
     this._watchPageReady()
   },
@@ -755,8 +746,12 @@ components: { TabTransition, PageBanner, RadarChart, LoginGuideCard, FamilyEnerg
       var id = article.id || article
       uni.navigateTo({ url: '/pages/article-center/article-detail?id=' + id })
     },
-    goMoreRecommend: function() {
-      uni.navigateTo({ url: '/pages/discover/index' })
+    goProductDetail: function(prod) {
+      if (!prod || !prod.id) return
+      uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
+    },
+    goMoreProducts: function() {
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     // ===== 孩子列表与维度数据 =====
     loadChildren: function() {

+ 16 - 19
cfc-frontend/pages/wealth/index.vue

@@ -10,9 +10,11 @@
         quote="富足源于利他与坚持" />
 
       <!-- 商品推荐 -->
-      <DimensionProductList
+      <DimensionProducts
         dimensionCode="wealth"
-        title="为你推荐" />
+        title="为你推荐"
+        @productClick="goProductDetail"
+        @moreProducts="goMoreProducts" />
 
       <!-- 推荐阅读 -->
       <ArticleBookshelf
@@ -167,10 +169,12 @@
       </view>
 
       <!-- 商品推荐 -->
-      <DimensionProductList
+      <DimensionProducts
         dimensionCode="wealth"
         :familyId="activeChildId"
-        title="为你推荐" />
+        title="为你推荐"
+        @productClick="goProductDetail"
+        @moreProducts="goMoreProducts" />
 
       <!-- 推荐活动 -->
       <DimensionActivities
@@ -187,13 +191,6 @@
         @articleClick="goArticleDetail"
         @moreArticles="goMoreArticles" />
 
-      <!-- 个性化推荐 -->
-      <RecommendedSection
-        :items="recommendItems"
-        :hasMore="recommendHasMore"
-        @itemClick="onRecommendItemClick"
-        @more="goMoreRecommend" />
-
       <!-- 底部菜单 -->
       <view class="menu-list">
         <view class="menu-item" @click="goToProfile">
@@ -217,13 +214,11 @@
 import TabTransition from '../../components/tab-transition.vue'
 import PageBanner from '../../components/PageBanner.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
-import DimensionProductList from '../../components/DimensionProductList.vue'
+import DimensionProducts from '../../components/DimensionProducts.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import WealthSubScores from '../../components/WealthSubScores.vue'
 import BodyWealthAlert from '../../components/BodyWealthAlert.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
-import RecommendedSection from '../../components/RecommendedSection.vue'
-import recommendMixin from '../../mixins/recommend-mixin'
 import {
   verifyPassword,
   getChildren,
@@ -237,8 +232,7 @@ import {
 } from '../../utils/api.js'
 
 export default {
-  mixins: [recommendMixin],
-  components: { TabTransition, PageBanner, DimensionProductList, DimensionActivities, WealthSubScores, BodyWealthAlert, FamilyMemberStrip, RecommendedSection, ArticleBookshelf },
+  components: { TabTransition, PageBanner, DimensionProducts, DimensionActivities, WealthSubScores, BodyWealthAlert, FamilyMemberStrip, ArticleBookshelf },
   data() {
     return {
       showTabTransition: true,
@@ -312,7 +306,6 @@ export default {
     this.loadWealthDetail()
     this.loadWealthData()
     this.loadFamilyMembersVisible()
-    this.loadRecommendations('wealth')
     this.loadWealthArticles()
     this.loadDimensionActivities()
     this._watchPageReady()
@@ -321,8 +314,12 @@ export default {
     onFamilyMemberSelect(member) {
       this.selectedMemberId = member.id
     },
-    goMoreRecommend: function() {
-      uni.navigateTo({ url: '/pages/discover/index' })
+    goProductDetail: function(prod) {
+      if (!prod || !prod.id) return
+      uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
+    },
+    goMoreProducts: function() {
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     loadDimensionActivities: function() {
       var self = this

+ 12 - 17
cfc-frontend/pages/wisdom/index.vue

@@ -99,10 +99,12 @@
       @moreActivities="goMoreActivities" />
 
     <!-- 9. 维度商品 -->
-    <DimensionProductList
+    <DimensionProducts
       dimensionCode="wisdom"
       :familyId="activeChildId"
-      title="为你推荐" />
+      title="为你推荐"
+      @productClick="goProductDetail"
+      @moreProducts="goMoreProducts" />
 
     <!-- 10. 推荐阅读 -->
     <ArticleBookshelf
@@ -132,13 +134,6 @@
       </view>
     </view>
 
-    <!-- 个性化推荐 -->
-    <RecommendedSection
-      :items="recommendItems"
-      :hasMore="recommendHasMore"
-      @itemClick="onRecommendItemClick"
-      @more="goMoreRecommend" />
-
     <!-- 底部占位 -->
     <view class="bottom-spacer"></view>
     <AIFloatingAvatar />
@@ -153,17 +148,14 @@ import RadarChart from '../../components/RadarChart.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import DimensionTasks from '../../components/DimensionTasks.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
-import DimensionProductList from '../../components/DimensionProductList.vue'
+import DimensionProducts from '../../components/DimensionProducts.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
 import AIFloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
-import RecommendedSection from '../../components/RecommendedSection.vue'
-import recommendMixin from '../../mixins/recommend-mixin'
 import { getVisibleSections, getChildren, getTodayTasksByCategory, getActivityList, getProductsByDomain, getFamilyEnergySandbox, getAssessmentLatestResult, getFeaturedArticles, getDanReportByChild } from '../../utils/api.js'
 
 export default {
-  mixins: [recommendMixin],
-  components: { TabTransition, PageBanner, LoginGuideCard, RadarChart, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProductList, ArticleBookshelf, AIFloatingAvatar, FamilyMemberStrip, RecommendedSection },
+  components: { TabTransition, PageBanner, LoginGuideCard, RadarChart, FamilyEnergyBar, DimensionTasks, DimensionActivities, DimensionProducts, ArticleBookshelf, AIFloatingAvatar, FamilyMemberStrip },
   data() {
     return {
       isLoggedIn: false,
@@ -247,12 +239,15 @@ export default {
     this.loadDimensionProducts()
     // 游客也能浏览文章
     this.loadFeaturedArticles()
-    this.loadRecommendations('wisdom')
     this._watchPageReady()
   },
   methods: {
-    goMoreRecommend: function() {
-      uni.navigateTo({ url: '/pages/discover/index' })
+    goProductDetail: function(prod) {
+      if (!prod || !prod.id) return
+      uni.navigateTo({ url: '/pages/discover-detail/product-detail/product-detail?id=' + prod.id })
+    },
+    goMoreProducts: function() {
+      uni.navigateTo({ url: '/pages/shop/index' })
     },
     onFamilyMemberSelect: function(member) {
       this.selectedMemberId = member.id