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

feat(ui): 首页/成长页「推荐阅读」统一左图右文列表,成长页「推荐商品」对齐首页两列大卡片

任务: ui-article-feed-unify(首页与成长页推荐阅读显示风格统一 + 成长页推荐商品按首页显示风格)

- 新增 components/ArticleFeed.vue:复刻 RelatedContent「相关文章」区块视觉
  (白卡 16rpx 圆角 / 左图右文 / 图 160x120rpx / 标题 27rpx 两行截断 / 摘要 23rpx 两行截断),
  含空态「暂无推荐文章」与 3 条骨架卡片;封面 URL 解析同 RelatedContent.articleCover
  (空/null/undefined//null -> /static/default-article.png)
- pages/index-home/index.vue:登录态「推荐阅读」ArticleBookshelf -> ArticleFeed
- pages/growth-main/index.vue:「推荐阅读」ArticleBookshelf -> ArticleFeed;
  「推荐商品」DimensionProducts 追加 layout="mall"(两列大卡片,与首页一致)

未改动:health-main / parent-index / article-center 的 ArticleBookshelf 书封网格,
首页未登录态 article-card 引流区块。

验证:npm run build:mp-weixin exit 0;verify-vue-compile 3/3 PASS;
audit-mp-antipatterns ERROR 0;jest 与 origin/cfclub 基线一致(存量 3 套件 4 用例失败)
Sisyphus Agent 1 неделя назад
Родитель
Сommit
b608a1bbf7

+ 162 - 0
cfc-frontend/components/ArticleFeed.vue

@@ -0,0 +1,162 @@
+<template>
+  <view class="af-section">
+    <!-- 空态 -->
+    <view v-if="(!articles || articles.length === 0) && !loading" class="af-empty">
+      <text class="af-empty-text">暂无推荐文章</text>
+    </view>
+
+    <!-- 加载态骨架:3 条左图右文卡片 -->
+    <view v-else-if="loading && (!articles || articles.length === 0)" class="af-list">
+      <view v-for="n in 3" :key="n" class="af-item">
+        <view class="af-cover af-skeleton-block"></view>
+        <view class="af-body">
+          <view class="af-skeleton-line af-skeleton-line-title"></view>
+          <view class="af-skeleton-line"></view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 左图右文列表(与文章详情「相关文章」视觉一致) -->
+    <view v-else class="af-list">
+      <view
+        v-for="item in articles"
+        :key="item.id"
+        class="af-item"
+        hover-class="af-item-pressed"
+        @click="onArticleClick(item)"
+      >
+        <image class="af-cover" :src="articleCover(item)" mode="aspectFill" lazy-load />
+        <view class="af-body">
+          <text class="af-title">{{ item.title }}</text>
+          <text class="af-summary" v-if="item.summary">{{ item.summary }}</text>
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import config from '@/config.js'
+
+/**
+ * ArticleFeed — 文章「关联文章」同款左图右文列表,用于首页/成长页「推荐阅读」统一视觉。
+ * 纯展示组件:数据由父页面传入,点击通过 articleClick 事件上抛文章对象。
+ */
+export default {
+  name: 'ArticleFeed',
+  props: {
+    articles: { type: Array, default: function() { return [] } },
+    loading: { type: Boolean, default: false }
+  },
+  methods: {
+    onArticleClick: function(item) {
+      this.$emit('articleClick', item)
+    },
+    // 封面图 URL 解析:空 / null / undefined / '/null' → 默认图(与 RelatedContent 同口径,增补 '/null' 兜底)
+    articleCover: function(item) {
+      if (!item || !item.coverImage) return '/static/default-article.png'
+      return this.resolveUrl(item.coverImage) || '/static/default-article.png'
+    },
+    resolveUrl: function(path) {
+      var s = String(path || '').trim()
+      if (!s || s === 'null' || s === 'undefined' || s === '/null') return ''
+      if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0 || s.indexOf('/static/') === 0) return s
+      return config.API_BASE_URL + (s.charAt(0) === '/' ? s : '/' + s)
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.af-section {
+  background: $app-surface;
+  border-radius: 16rpx;
+  padding: 24rpx;
+}
+
+/* ---- 空态 ---- */
+.af-empty {
+  display: flex;
+  justify-content: center;
+  padding: 40rpx 0;
+}
+.af-empty-text {
+  font-size: calc(24rpx * var(--font-scale, 1));
+  color: $facade-text-gray-400;
+}
+
+/* ---- 列表 ---- */
+.af-list {
+  display: flex;
+  flex-direction: column;
+}
+.af-item {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  padding: 14rpx 0;
+  border-radius: 16rpx;
+}
+.af-item-pressed {
+  background: $facade-fill-gray-100;
+}
+.af-cover {
+  width: 160rpx;
+  height: 120rpx;
+  border-radius: 16rpx;
+  background: $facade-fill-gray-100;
+  flex-shrink: 0;
+}
+.af-body {
+  flex: 1;
+  min-width: 0;
+  margin-left: 16rpx;
+  display: flex;
+  flex-direction: column;
+}
+.af-title {
+  font-size: calc(27rpx * var(--font-scale, 1));
+  font-weight: 500;
+  color: $facade-text-gray-800;
+  line-height: 1.4;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  overflow: hidden;
+}
+.af-summary {
+  font-size: calc(23rpx * var(--font-scale, 1));
+  color: $facade-text-gray-400;
+  line-height: 1.4;
+  margin-top: 6rpx;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  overflow: hidden;
+}
+
+/* ---- 骨架屏(3 条左图右文占位) ---- */
+.af-skeleton-block {
+  background: linear-gradient(180deg, $facade-fill-4 0%, $facade-fill 50%, $facade-fill-4 100%);
+  background-size: 200% 100%;
+  animation: af-skeleton-wave 1.4s ease-in-out infinite;
+}
+.af-skeleton-line {
+  width: 100%;
+  height: 28rpx;
+  border-radius: 8rpx;
+  margin-top: 10rpx;
+  background: linear-gradient(180deg, $facade-fill-4 0%, $facade-fill 50%, $facade-fill-4 100%);
+  background-size: 200% 100%;
+  animation: af-skeleton-wave 1.4s ease-in-out infinite;
+}
+.af-skeleton-line-title {
+  width: 72%;
+  height: 32rpx;
+  margin-top: 0;
+}
+@keyframes af-skeleton-wave {
+  0% { background-position: 200% 0; }
+  100% { background-position: -200% 0; }
+}
+</style>

+ 4 - 4
cfc-frontend/pages/growth-main/index.vue

@@ -251,7 +251,7 @@
           <text class="section-title">📖 推荐阅读</text>
           <text class="section-more" @click="goMoreArticles">更多 ›</text>
         </view>
-        <ArticleBookshelf :articles="recommendArticles" :isLoggedIn="true" @articleClick="goToArticle" />
+        <ArticleFeed :articles="recommendArticles" @articleClick="goToArticle" />
       </view>
 
       <!-- 推荐商品(放最底部) -->
@@ -260,7 +260,7 @@
           <text class="section-title">🛍️ 推荐商品</text>
           <text class="section-more" @click="goMoreProducts">更多 ›</text>
         </view>
-        <DimensionProducts :isLoggedIn="true" :showMore="false" @productClick="goToProduct" />
+        <DimensionProducts :isLoggedIn="true" :showMore="false" layout="mall" @productClick="goToProduct" />
       </view>
 
       <view class="bottom-spacer" style="height: 40rpx;"></view>
@@ -272,14 +272,14 @@
 import LoginGuideCard from '../../components/LoginGuideCard.vue'
 import DimensionActivities from '../../components/DimensionActivities.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
-import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
+import ArticleFeed from '../../components/ArticleFeed.vue'
 import { healthCheckinList, getHealthMealList, getGrowthRecommendations, getExerciseRecords, getSleepRecords, getEmotionCheckinList, getCheckinList, getTodayTasks, reportAppEnter } from '../../utils/api.js'
 import { parseDate } from '../../utils/format.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 import shareMixin from '../../components/share-mixin.js'
 export default {
   mixins: [fontScaleMixin, shareMixin],
-  components: { LoginGuideCard, DimensionActivities, DimensionProducts, ArticleBookshelf },
+  components: { LoginGuideCard, DimensionActivities, DimensionProducts, ArticleFeed },
   data() { return { memberId: '', todayDate: '', hasIncomplete: false, incompleteCount: 0, progressItems: [], recentTasks: [], todayTaskList: [], streakDays: 0, activeChallenges: 0, completeness: 0,
       healthArticles: [],
       growthArticles: [],

+ 3 - 2
cfc-frontend/pages/index-home/index.vue

@@ -263,9 +263,8 @@
         <text class="section-title">📖 推荐阅读</text>
         <text class="section-more" @click="goMoreArticles">更多 ›</text>
       </view>
-      <ArticleBookshelf
+      <ArticleFeed
         :articles="actionArticles"
-        :isLoggedIn="true"
         @articleClick="goArticleDetail" />
     </view>
 
@@ -484,6 +483,7 @@ import GateLock from '../../components/gate-lock.vue'
 import FamilyFeed from '../../components/FamilyFeed.vue'
 import DimensionProducts from '../../components/DimensionProducts.vue'
 import ArticleBookshelf from '../../components/ArticleBookshelf.vue'
+import ArticleFeed from '../../components/ArticleFeed.vue'
 import FamilyChallengeCard from '../../components/FamilyChallengeCard.vue'
 import DailyTaskCard from '../../components/daily-task-card.vue'
 import ReportUploadCard from '../../components/report-upload-card.vue'
@@ -506,6 +506,7 @@ components: {
     FamilyFeed,
     DimensionProducts,
     ArticleBookshelf,
+    ArticleFeed,
     FamilyChallengeCard,
     DailyTaskCard,
     ReportUploadCard,