| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 |
- <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: $mat-green-800;
- }
- .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>
|