|
|
@@ -0,0 +1,431 @@
|
|
|
+<template>
|
|
|
+ <view class="related-content">
|
|
|
+ <!-- 首屏加载提示 -->
|
|
|
+ <view v-if="!loaded && loading" class="related-loading-wrap">
|
|
|
+ <text class="related-loading-text">加载中...</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 三源关联内容(任一分类无匹配整块隐藏) -->
|
|
|
+ <block v-if="loaded">
|
|
|
+ <!-- 相关文章 -->
|
|
|
+ <view v-if="articles.length > 0" class="related-section">
|
|
|
+ <view class="related-header">
|
|
|
+ <text class="related-title">相关文章</text>
|
|
|
+ <text class="related-subtitle">根据标签推荐</text>
|
|
|
+ </view>
|
|
|
+ <view class="ra-list">
|
|
|
+ <view
|
|
|
+ v-for="item in articles"
|
|
|
+ :key="item.id"
|
|
|
+ class="ra-item"
|
|
|
+ hover-class="ra-item-pressed"
|
|
|
+ @click="goArticle(item)"
|
|
|
+ >
|
|
|
+ <image class="ra-cover" :src="articleCover(item)" mode="aspectFill" lazy-load />
|
|
|
+ <view class="ra-body">
|
|
|
+ <text class="ra-title">{{ item.title }}</text>
|
|
|
+ <text class="ra-summary" v-if="item.summary">{{ item.summary }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view v-if="articlesLoading" class="related-loading-hint">加载中...</view>
|
|
|
+ <view v-else-if="!articlesHasMore" class="related-loading-hint dim">没有更多了</view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 相关活动 -->
|
|
|
+ <view v-if="activities.length > 0" class="related-section">
|
|
|
+ <view class="related-header">
|
|
|
+ <text class="related-title">相关活动</text>
|
|
|
+ <text class="related-subtitle">根据标签推荐</text>
|
|
|
+ </view>
|
|
|
+ <view class="ra-list">
|
|
|
+ <view
|
|
|
+ v-for="item in activities"
|
|
|
+ :key="item.id"
|
|
|
+ class="ra-item"
|
|
|
+ hover-class="ra-item-pressed"
|
|
|
+ @click="goActivity(item)"
|
|
|
+ >
|
|
|
+ <image class="ra-cover" :src="activityCover(item)" mode="aspectFill" lazy-load />
|
|
|
+ <view class="ra-body">
|
|
|
+ <text class="ra-title">{{ item.title }}</text>
|
|
|
+ <text class="ra-summary" v-if="item.startTime">{{ formatActivityTime(item.startTime) }}</text>
|
|
|
+ <view class="ra-bottom">
|
|
|
+ <text class="ra-status" :class="'status-' + (item._cls || 'upcoming')">{{ item._txt || '待开始' }}</text>
|
|
|
+ <text class="ra-price" v-if="item.priceLabel">{{ item.priceLabel }}</text>
|
|
|
+ <text class="ra-price" v-else-if="item.price && item.price > 0">¥{{ formatPrice(item.price) }}</text>
|
|
|
+ <text class="ra-price-free" v-else>免费</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view v-if="activitiesLoading" class="related-loading-hint">加载中...</view>
|
|
|
+ <view v-else-if="!activitiesHasMore" class="related-loading-hint dim">没有更多了</view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 相关商品(复用商城两列大卡片样式) -->
|
|
|
+ <view v-if="products.length > 0" class="related-section">
|
|
|
+ <view class="related-header">
|
|
|
+ <text class="related-title">相关商品</text>
|
|
|
+ <text class="related-subtitle">根据标签推荐</text>
|
|
|
+ </view>
|
|
|
+ <dimension-products :products="products" layout="mall" @productClick="goProduct" />
|
|
|
+ <view v-if="productsLoading" class="related-loading-hint">加载中...</view>
|
|
|
+ <view v-else-if="!productsHasMore" class="related-loading-hint dim">没有更多了</view>
|
|
|
+ </view>
|
|
|
+ </block>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { getContentRelated } from '@/utils/api.js'
|
|
|
+import config from '@/config.js'
|
|
|
+import { parseDate } from '@/utils/format.js'
|
|
|
+import DimensionProducts from '@/components/DimensionProducts.vue'
|
|
|
+
|
|
|
+/**
|
|
|
+ * 内容关联区块:以活动/文章/商品任意一类为源,按共享标签展示「相关文章 / 相关活动 / 相关商品」。
|
|
|
+ * 数据由后端 /api/content/related-items 一次返回,本组件自加载、自渲染、自翻页。
|
|
|
+ * 父页面在 scroll-view 触底时调用 ref.loadMore() 实现分段加载。
|
|
|
+ *
|
|
|
+ * Props:
|
|
|
+ * - sourceType: 'article' | 'activity' | 'product'
|
|
|
+ * - sourceId: 源内容ID
|
|
|
+ */
|
|
|
+export default {
|
|
|
+ name: 'RelatedContent',
|
|
|
+ components: { DimensionProducts },
|
|
|
+ props: {
|
|
|
+ sourceType: { type: String, default: '' },
|
|
|
+ sourceId: { type: [String, Number], default: '' }
|
|
|
+ },
|
|
|
+ data: function() {
|
|
|
+ return {
|
|
|
+ size: 4,
|
|
|
+ loaded: false,
|
|
|
+ loading: false,
|
|
|
+ // 相关文章
|
|
|
+ articles: [],
|
|
|
+ articlesPage: 1,
|
|
|
+ articlesLoading: false,
|
|
|
+ articlesHasMore: true,
|
|
|
+ // 相关活动
|
|
|
+ activities: [],
|
|
|
+ activitiesPage: 1,
|
|
|
+ activitiesLoading: false,
|
|
|
+ activitiesHasMore: true,
|
|
|
+ // 相关商品
|
|
|
+ products: [],
|
|
|
+ productsPage: 1,
|
|
|
+ productsLoading: false,
|
|
|
+ productsHasMore: true
|
|
|
+ }
|
|
|
+ },
|
|
|
+ created: function() {
|
|
|
+ this.reload()
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ sourceId: function() {
|
|
|
+ this.reload()
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ reload: function() {
|
|
|
+ if (!this.sourceType || this.sourceId === '' || this.sourceId === null || this.sourceId === undefined) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ var self = this
|
|
|
+ this.loaded = false
|
|
|
+ this.loading = true
|
|
|
+ getContentRelated({ sourceType: this.sourceType, sourceId: this.sourceId, size: this.size, page: 1 }).then(function(res) {
|
|
|
+ self.loading = false
|
|
|
+ self.loaded = true
|
|
|
+ if (res && res.code === 200 && res.data) {
|
|
|
+ var d = res.data
|
|
|
+ self.articles = Array.isArray(d.articles) ? d.articles : []
|
|
|
+ self.activities = self.decorateActivities(Array.isArray(d.activities) ? d.activities : [])
|
|
|
+ self.products = Array.isArray(d.products) ? d.products : []
|
|
|
+ self.articlesPage = 1
|
|
|
+ self.activitiesPage = 1
|
|
|
+ self.productsPage = 1
|
|
|
+ self.articlesHasMore = self.articles.length >= self.size
|
|
|
+ self.activitiesHasMore = self.activities.length >= self.size
|
|
|
+ self.productsHasMore = self.products.length >= self.size
|
|
|
+ } else {
|
|
|
+ self.articles = []
|
|
|
+ self.activities = []
|
|
|
+ self.products = []
|
|
|
+ self.articlesHasMore = false
|
|
|
+ self.activitiesHasMore = false
|
|
|
+ self.productsHasMore = false
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ self.loading = false
|
|
|
+ self.loaded = true
|
|
|
+ self.articles = []
|
|
|
+ self.activities = []
|
|
|
+ self.products = []
|
|
|
+ self.articlesHasMore = false
|
|
|
+ self.activitiesHasMore = false
|
|
|
+ self.productsHasMore = false
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // 父页面 scroll-view 触底调用:按 DOM 顺序(文章→活动→商品)翻页尚未加载完的分类
|
|
|
+ loadMore: function() {
|
|
|
+ var self = this
|
|
|
+ if (!this.loaded || this.loading) return
|
|
|
+ if (this.articlesHasMore && !this.articlesLoading) {
|
|
|
+ this.articlesLoading = true
|
|
|
+ var nextArticlePage = this.articlesPage + 1
|
|
|
+ getContentRelated({ sourceType: this.sourceType, sourceId: this.sourceId, size: this.size, page: nextArticlePage }).then(function(res) {
|
|
|
+ self.articlesLoading = false
|
|
|
+ if (res && res.code === 200 && res.data && Array.isArray(res.data.articles)) {
|
|
|
+ var more = res.data.articles
|
|
|
+ self.articles = self.articles.concat(more)
|
|
|
+ self.articlesPage = nextArticlePage
|
|
|
+ if (more.length < self.size) self.articlesHasMore = false
|
|
|
+ } else {
|
|
|
+ self.articlesHasMore = false
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ self.articlesLoading = false
|
|
|
+ self.articlesHasMore = false
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (this.activitiesHasMore && !this.activitiesLoading) {
|
|
|
+ this.activitiesLoading = true
|
|
|
+ var nextActivityPage = this.activitiesPage + 1
|
|
|
+ getContentRelated({ sourceType: this.sourceType, sourceId: this.sourceId, size: this.size, page: nextActivityPage }).then(function(res) {
|
|
|
+ self.activitiesLoading = false
|
|
|
+ if (res && res.code === 200 && res.data && Array.isArray(res.data.activities)) {
|
|
|
+ var moreActs = self.decorateActivities(res.data.activities)
|
|
|
+ self.activities = self.activities.concat(moreActs)
|
|
|
+ self.activitiesPage = nextActivityPage
|
|
|
+ if (moreActs.length < self.size) self.activitiesHasMore = false
|
|
|
+ } else {
|
|
|
+ self.activitiesHasMore = false
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ self.activitiesLoading = false
|
|
|
+ self.activitiesHasMore = false
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (this.productsHasMore && !this.productsLoading) {
|
|
|
+ this.productsLoading = true
|
|
|
+ var nextProductPage = this.productsPage + 1
|
|
|
+ getContentRelated({ sourceType: this.sourceType, sourceId: this.sourceId, size: this.size, page: nextProductPage }).then(function(res) {
|
|
|
+ self.productsLoading = false
|
|
|
+ if (res && res.code === 200 && res.data && Array.isArray(res.data.products)) {
|
|
|
+ var moreProds = res.data.products
|
|
|
+ self.products = self.products.concat(moreProds)
|
|
|
+ self.productsPage = nextProductPage
|
|
|
+ if (moreProds.length < self.size) self.productsHasMore = false
|
|
|
+ } else {
|
|
|
+ self.productsHasMore = false
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ self.productsLoading = false
|
|
|
+ self.productsHasMore = false
|
|
|
+ })
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 活动卡片展示态:已结束 / 进行中 / 待开始(活动列表页同口径)
|
|
|
+ decorateActivities: function(list) {
|
|
|
+ var self = this
|
|
|
+ return list.map(function(act) {
|
|
|
+ var item = Object.assign({}, act)
|
|
|
+ var now = new Date()
|
|
|
+ var endTime = parseDate(item.endTime)
|
|
|
+ if (item.status === 'ended' || (endTime && endTime < now)) {
|
|
|
+ item._cls = 'ended'
|
|
|
+ item._txt = '已结束'
|
|
|
+ return item
|
|
|
+ }
|
|
|
+ var startTime = parseDate(item.startTime)
|
|
|
+ if (startTime && startTime > now) {
|
|
|
+ item._cls = 'upcoming'
|
|
|
+ item._txt = '待开始'
|
|
|
+ } else {
|
|
|
+ item._cls = 'ongoing'
|
|
|
+ item._txt = '进行中'
|
|
|
+ }
|
|
|
+ return item
|
|
|
+ })
|
|
|
+ },
|
|
|
+ formatActivityTime: function(timeStr) {
|
|
|
+ var d = parseDate(timeStr)
|
|
|
+ if (!d) return ''
|
|
|
+ var month = d.getMonth() + 1
|
|
|
+ var day = d.getDate()
|
|
|
+ var hour = d.getHours()
|
|
|
+ var minute = d.getMinutes()
|
|
|
+ var hourStr = hour < 10 ? '0' + hour : hour
|
|
|
+ var minStr = minute < 10 ? '0' + minute : minute
|
|
|
+ return month + '月' + day + '日 ' + hourStr + ':' + minStr + ' 开始'
|
|
|
+ },
|
|
|
+ formatPrice: function(cents) {
|
|
|
+ if (cents === null || cents === undefined) return ''
|
|
|
+ return (cents / 100).toFixed(2)
|
|
|
+ },
|
|
|
+ articleCover: function(item) {
|
|
|
+ if (!item || !item.coverImage) return '/static/default-article.png'
|
|
|
+ return this.resolveUrl(item.coverImage)
|
|
|
+ },
|
|
|
+ activityCover: function(item) {
|
|
|
+ if (!item || !item.coverImage) return '/static/default-activity.png'
|
|
|
+ return this.resolveUrl(item.coverImage)
|
|
|
+ },
|
|
|
+ resolveUrl: function(path) {
|
|
|
+ var s = String(path || '').trim()
|
|
|
+ if (!s || s === 'null' || s === 'undefined') 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)
|
|
|
+ },
|
|
|
+ // 跳转相关文章详情
|
|
|
+ goArticle: function(item) {
|
|
|
+ if (!item || !item.id) return
|
|
|
+ uni.navigateTo({ url: '/pages/article-center/article-detail?id=' + item.id })
|
|
|
+ },
|
|
|
+ // 跳转相关活动详情
|
|
|
+ goActivity: function(item) {
|
|
|
+ if (!item || !item.id) return
|
|
|
+ uni.navigateTo({ url: '/pages/activity/activity-detail/activity-detail?id=' + item.id })
|
|
|
+ },
|
|
|
+ // 跳转商品详情(优先使用后端返回的 url)
|
|
|
+ goProduct: function(prod) {
|
|
|
+ if (!prod) return
|
|
|
+ var pid = prod.productId || prod.id
|
|
|
+ var url = prod.url || ('/pages/discover-detail/product-detail/product-detail?id=' + (pid || ''))
|
|
|
+ if (url.indexOf('/pages/') !== 0) return
|
|
|
+ uni.navigateTo({ url: url })
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+.related-content {
|
|
|
+ margin-top: 30rpx;
|
|
|
+}
|
|
|
+.related-loading-wrap {
|
|
|
+ padding: 40rpx 30rpx;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+.related-loading-text {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: $facade-text-gray-400;
|
|
|
+}
|
|
|
+.related-section {
|
|
|
+ background: $app-surface;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ padding: 24rpx;
|
|
|
+ margin-bottom: 30rpx;
|
|
|
+}
|
|
|
+.related-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+.related-title {
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: $facade-text-gray-800;
|
|
|
+ margin-right: 12rpx;
|
|
|
+}
|
|
|
+.related-subtitle {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: $facade-text-gray-400;
|
|
|
+}
|
|
|
+.ra-list {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+.ra-item {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+ align-items: flex-start;
|
|
|
+ padding: 14rpx 0;
|
|
|
+ border-radius: 12rpx;
|
|
|
+}
|
|
|
+.ra-item-pressed {
|
|
|
+ background: $facade-fill-gray-100;
|
|
|
+}
|
|
|
+.ra-cover {
|
|
|
+ width: 160rpx;
|
|
|
+ height: 120rpx;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ background: $facade-fill-gray-100;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.ra-body {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ margin-left: 16rpx;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+.ra-title {
|
|
|
+ font-size: 27rpx;
|
|
|
+ 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;
|
|
|
+}
|
|
|
+.ra-summary {
|
|
|
+ font-size: 23rpx;
|
|
|
+ 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;
|
|
|
+}
|
|
|
+.ra-bottom {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-top: 8rpx;
|
|
|
+}
|
|
|
+.ra-status {
|
|
|
+ font-size: 20rpx;
|
|
|
+ padding: 2rpx 12rpx;
|
|
|
+ border-radius: 20rpx;
|
|
|
+ margin-right: 12rpx;
|
|
|
+}
|
|
|
+.ra-status.upcoming {
|
|
|
+ background: $uni-primary-pale;
|
|
|
+ color: $brand-primary;
|
|
|
+}
|
|
|
+.ra-status.ongoing {
|
|
|
+ background: $status-success-soft-2;
|
|
|
+ color: #166534;
|
|
|
+}
|
|
|
+.ra-status.ended {
|
|
|
+ background: $facade-fill-2;
|
|
|
+ color: $facade-text-muted;
|
|
|
+}
|
|
|
+.ra-price {
|
|
|
+ font-size: 23rpx;
|
|
|
+ color: $status-danger;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.ra-price-free {
|
|
|
+ font-size: 23rpx;
|
|
|
+ color: $facade-text-gray-400;
|
|
|
+}
|
|
|
+.related-loading-hint {
|
|
|
+ text-align: center;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: $facade-text-gray-400;
|
|
|
+ padding: 16rpx 0 8rpx;
|
|
|
+}
|
|
|
+.related-loading-hint.dim {
|
|
|
+ color: $facade-text-gray-300;
|
|
|
+}
|
|
|
+</style>
|