| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379 |
- <template>
- <view class="ad-banner">
- <!-- Empty state when no data -->
- <view v-if="!hasItems" class="ad-banner-empty">
- <text class="ad-banner-empty-icon">📢</text>
- <text class="ad-banner-empty-text">暂无推荐内容</text>
- </view>
- <!-- Swiper carousel -->
- <swiper
- v-else
- class="ad-banner-swiper"
- :indicator-dots="totalSlides > 1"
- :active-index="activeIndex"
- @change="onSwiperChange"
- autoplay
- :interval="autoPlayInterval"
- :duration="swiperDuration"
- circular
- @click="onSlideClick"
- >
- <swiper-item
- v-for="(item, index) in displayItems"
- :key="item.id"
- :data-index="index"
- >
- <view
- class="ad-banner-slide"
- :data-index="index"
- @click="onSlideClick(index)"
- >
- <!-- 封面图 -->
- <image
- class="ad-banner-cover"
- :src="getCoverImage(item)"
- mode="aspectFill"
- />
- <!-- 叠加层:标签 + 标题 -->
- <view class="ad-banner-overlay">
- <!-- 标签 -->
- <view
- class="ad-banner-tag"
- :class="item.type === 'product' ? 'tag-product' : 'tag-activity'"
- >
- {{ item.type === 'product' ? '🛍️ 商品' : '🔥 活动' }}
- </view>
- <!-- 标题 -->
- <view class="ad-banner-title" v-html="getTitleHtml(item)" />
- </view>
- </view>
- </swiper-item>
- </swiper>
- <!-- 指示点 -->
- <view v-if="totalSlides > 1" class="ad-banner-dots">
- <view
- class="ad-banner-dot"
- v-for="(dot, i) in totalSlides"
- :key="i"
- :class="{ active: activeIndex === i }"
- ></view>
- </view>
- </view>
- </template>
- <script>
- import { parseDate } from '@/utils/format.js'
- import { getHomeBanners } from '@/utils/api.js'
- export default {
- props: {
- /** 推荐活动列表(后备数据) */
- activities: {
- type: Array,
- default: function() { return [] }
- },
- /** 推荐商品列表(后备数据) */
- products: {
- type: Array,
- default: function() { return [] }
- },
- /** 自动播放间隔(毫秒) */
- autoPlayInterval: {
- type: Number,
- default: 3000
- },
- /** Swiper 切换动画时长(毫秒) */
- swiperDuration: {
- type: Number,
- default: 500
- }
- },
- data() {
- return {
- // 合并后的项目列表,交替排列:商品、活动、商品、活动...
- displayItems: [],
- activeIndex: 0,
- totalSlides: 0,
- loading: false
- }
- },
- computed: {
- /** 是否有内容 */
- hasItems: function() {
- return this.displayItems && this.displayItems.length > 0
- }
- },
- mounted: function() {
- this.loadBanners()
- },
- methods: {
- /** 优先从配置接口加载广告,失败则回退到 props */
- loadBanners: function() {
- var self = this
- self.loading = true
- getHomeBanners().then(function(res) {
- self.loading = false
- if (res && res.code === 200 && res.data && res.data.length > 0) {
- self.fromConfig(res.data)
- } else {
- self.fromProps()
- }
- }).catch(function() {
- self.loading = false
- self.fromProps()
- })
- },
- /** 使用配置接口返回的数据构建 displayItems */
- fromConfig: function(banners) {
- var items = banners.map(function(b) {
- return {
- id: b.id,
- type: b.bannerType === 'product' ? 'product' : 'activity',
- name: b.title,
- title: b.title,
- coverImage: b.coverImage || b.cover_image,
- refId: b.refId || b.ref_id,
- linkUrl: b.linkUrl || b.link_url,
- source: b
- }
- }.bind(this))
- this.displayItems = items
- this.totalSlides = items.length
- this.activeIndex = 0
- },
- /** 使用 props(activities + products)构建 displayItems(后备逻辑) */
- fromProps: function() {
- this.mergeItems()
- },
- /** 获取合并后的项目列表(后备逻辑) */
- mergeItems: function() {
- var productItems = (this.products || []).slice(0, 4).map(function(p) {
- return {
- id: p.id || p._id,
- type: 'product',
- name: p.name || '未知商品',
- coverImage: p.coverImage || p.image,
- price: p.price,
- priceLabel: p.priceLabel,
- source: p
- }
- }.bind(this))
- var activityItems = (this.activities || []).slice(0, 4).map(function(a) {
- var startTime = a.startTime
- var d
- try {
- d = parseDate(startTime)
- if (d) {
- var month = d.getMonth() + 1
- var day = d.getDate()
- var hour = d.getHours() < 10 ? '0' + d.getHours() : d.getHours()
- var minute = d.getMinutes() < 10 ? '0' + d.getMinutes() : d.getMinutes()
- startTime = month + '月' + day + '日 ' + hour + ':' + minute
- }
- } catch (e) {
- startTime = String(startTime)
- }
- return {
- id: a.id || a._id,
- type: 'activity',
- title: a.title || '未知活动',
- coverImage: a.coverImage || a.image,
- price: a.price,
- priceLabel: a.priceLabel,
- startTime: startTime,
- status: a.status,
- source: a
- }
- }.bind(this))
- // 交替合并:商品、活动、商品、活动...
- var merged = []
- var pi = 0, ai = 0
- var toggle = 0
- while ((pi < productItems.length || ai < activityItems.length) && merged.length < 8) {
- if (toggle % 2 === 0 && pi < productItems.length) {
- merged.push(productItems[pi++])
- } else if (ai < activityItems.length) {
- merged.push(activityItems[ai++])
- } else if (pi < productItems.length) {
- merged.push(productItems[pi++])
- }
- toggle++
- }
- this.displayItems = merged
- this.totalSlides = merged.length
- this.activeIndex = 0
- },
- /** 获取封面图片URL */
- getCoverImage: function(item) {
- var img = item.coverImage || item.image
- if (!img) return '/static/default-activity.png'
- var s = String(img).trim()
- if (!s || s === 'null' || s === '/null' || s === 'undefined') return '/static/default-activity.png'
- if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s
- return config.API_BASE_URL + (s.charAt(0) === '/' ? s : '/' + s)
- },
- /** 获取标题 HTML(防止 AI 腔,使用直陈) */
- getTitleHtml: function(item) {
- if (item.type === 'product') {
- return (item.name || '').substring(0, 12)
- } else {
- return (item.title || '').substring(0, 12)
- }
- },
- /** swiper 变化回调 */
- onSwiperChange: function(e) {
- this.activeIndex = e.detail.current
- },
- /** 点击轮播项 */
- onSlideClick: function(index) {
- if (isNaN(index) || index < 0 || index >= this.displayItems.length) return
- var item = this.displayItems[index]
- if (!item) return
- // 自定义链接(link_url)直接跳转,否则按类型路由
- if (item.linkUrl) {
- if (item.linkUrl.indexOf('http://') === 0 || item.linkUrl.indexOf('https://') === 0) {
- window.location.href = item.linkUrl
- return
- }
- uni.navigateTo({ url: item.linkUrl })
- return
- }
- if (item.type === 'product') {
- uni.navigateTo({
- url: '/pages/discover-detail/product-detail/product-detail?id=' + item.refId
- })
- } else if (item.type === 'activity') {
- uni.navigateTo({
- url: '/pages/activity/activity-detail/activity-detail?id=' + item.refId
- })
- }
- },
- },
- }
- </script>
- <style scoped>
- /* ===== 广告轮播组件 ===== */
- .ad-banner {
- width: 100%;
- margin: 0;
- padding: 0;
- overflow: hidden;
- }
- .ad-banner-empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 60rpx 30rpx;
- color: #666;
- }
- .ad-banner-empty-icon {
- font-size: 48rpx;
- margin-bottom: 12rpx;
- }
- .ad-banner-empty-text {
- font-size: 24rpx;
- }
- .ad-banner-swiper {
- width: 100%;
- }
- /* 幻灯片卡片 */
- .ad-banner-slide {
- position: relative;
- width: 100%;
- /* 高度保持与封面图比例一致 */
- height: 320rpx;
- }
- /* 封面图 - 统一宽高比 16:9 -> 约320rpx高 */
- .ad-banner-cover {
- width: 100%;
- height: 100%;
- display: block;
- background: linear-gradient(135deg, #FF8C42 0%, #F59E0B 100%);
- border-radius: 16rpx;
- }
- /* 叠加层 */
- .ad-banner-overlay {
- position: absolute;
- bottom: 0;
- left: 0;
- right: 0;
- padding: 24rpx 24rpx 32rpx;
- }
- .ad-banner-tag {
- display: inline-block;
- padding: 4rpx 12rpx;
- border-radius: 20rpx;
- font-size: 20rpx;
- font-weight: 700;
- margin-bottom: 12rpx;
- text-transform: uppercase;
- letter-spacing: 1rpx;
- }
- .tag-product {
- background: rgba(255, 255, 255, 0.9);
- color: #F97316;
- }
- .tag-activity {
- background: rgba(255, 255, 255, 0.9);
- color: #EA580C;
- }
- /* 标题 - 使用 v-html 防止 AI 腔,直陈具体文本 */
- .ad-banner-title {
- font-size: 32rpx;
- font-weight: 700;
- color: #FFFFFF;
- text-shadow: 0 1rpx 4rpx rgba(0,0,0,0.4);
- line-height: 1.3;
- min-height: 48rpx;
- display: flex;
- align-items: center;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- }
- /* 指示点 */
- .ad-banner-dots {
- display: flex;
- justify-content: center;
- align-items: center;
- margin-top: 12rpx;
- gap: 8rpx;
- }
- .ad-banner-dot {
- width: 8rpx;
- height: 8rpx;
- border-radius: 50%;
- background: rgba(255,255,255,0.5);
- transition: all 0.3s ease;
- }
- .ad-banner-dot.active {
- width: 24rpx;
- background: #F97316;
- }
- </style>
|