|
|
@@ -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>
|