|
|
@@ -1,42 +1,54 @@
|
|
|
<!--
|
|
|
首页预览-秒杀专区/团购专区
|
|
|
- 只读展示:按 layoutCols(1/2/3) 渲染活动商品,含倒计时;秒杀按钮为「立即抢购」,团购按钮为「去开团」
|
|
|
+ 只读展示:按 layoutCols(1/2/3) 渲染活动商品,含倒计时(支持天数);
|
|
|
+ 仅在活动有效期内(startTime~endTime)展示,未开始或结束后整块隐藏;
|
|
|
+ 结构/样式与 mallApp 真实首页 activitySection 完全一致,不响应点击跳转。
|
|
|
-->
|
|
|
<template>
|
|
|
- <view v-if="data && data.enabled == 1 && data.goods && data.goods.length" class="pv-activity">
|
|
|
+ <view v-if="isActive && data && data.enabled == 1 && data.goods && data.goods.length" class="pv-activity">
|
|
|
<view class="pv-activity-head">
|
|
|
- <view class="pv-activity-title-row">
|
|
|
- <text class="pv-activity-title">{{ data.title || (isGroupBuy ? '团购优惠' : '限时秒杀') }}</text>
|
|
|
- <view v-if="data.showCountdown == 1 && data.status == 1" class="pv-countdown">
|
|
|
- <text class="pv-countdown-unit">{{ countdown.h }}</text>
|
|
|
- <text class="pv-countdown-colon">:</text>
|
|
|
- <text class="pv-countdown-unit">{{ countdown.m }}</text>
|
|
|
- <text class="pv-countdown-colon">:</text>
|
|
|
- <text class="pv-countdown-unit">{{ countdown.s }}</text>
|
|
|
+ <view class="pv-activity-head-row">
|
|
|
+ <view class="pv-activity-title-row">
|
|
|
+ <text class="pv-activity-title">{{ data.title || (isGroupBuy ? '团购优惠' : '限时秒杀') }}</text>
|
|
|
+ <view v-if="data.showCountdown == 1 && data.status == 1" class="pv-countdown">
|
|
|
+ <!-- 剩余超过 24 小时时展示「天」单位,避免小时数无限增大 -->
|
|
|
+ <template v-if="countdown.showDay">
|
|
|
+ <text class="pv-countdown-unit">{{ countdown.d }}</text>
|
|
|
+ <text class="pv-countdown-day">天</text>
|
|
|
+ </template>
|
|
|
+ <text class="pv-countdown-unit">{{ countdown.h }}</text>
|
|
|
+ <text class="pv-countdown-colon">:</text>
|
|
|
+ <text class="pv-countdown-unit">{{ countdown.m }}</text>
|
|
|
+ <text class="pv-countdown-colon">:</text>
|
|
|
+ <text class="pv-countdown-unit">{{ countdown.s }}</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
+ <text v-if="showMore" class="pv-section-more">更多 ></text>
|
|
|
</view>
|
|
|
- <text class="pv-activity-more">{{ data.subtitle || (isGroupBuy ? '多人拼团更实惠' : '每日精选 限时特惠') }}</text>
|
|
|
+ <text class="pv-activity-subtitle">{{ data.subtitle || (isGroupBuy ? '多人拼团更实惠' : '每日精选 限时特惠') }}</text>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 1排1列:横向列表 -->
|
|
|
+ <!-- 1排1列:左图右文,底部价格与按钮同行 -->
|
|
|
<view v-if="cols === 1" class="pv-list-1">
|
|
|
- <view v-for="g in data.goods" :key="g.goodsId" class="pv-row-card">
|
|
|
+ <view v-for="g in data.goods" :key="g.id || g.goodsId" class="pv-row-card">
|
|
|
<image class="pv-row-cover" :src="g.coverUrl" mode="aspectFill" />
|
|
|
- <view class="pv-row-body">
|
|
|
+ <view class="pv-row-content">
|
|
|
<text class="pv-row-name">{{ g.name }}</text>
|
|
|
<text class="pv-row-desc">{{ isGroupBuy ? (g.groupSize + '人团') : '限时特惠' }}</text>
|
|
|
+ <view class="pv-row-bottom">
|
|
|
+ <view class="pv-row-price-wrap">
|
|
|
+ <text class="pv-price">¥{{ formatPrice(g.price) }}</text>
|
|
|
+ <text class="pv-origin-price">¥{{ formatPrice(g.originPrice) }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="pv-action-btn">{{ actionText }} ></view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
- <view class="pv-row-price-wrap">
|
|
|
- <text class="pv-price">¥{{ formatPrice(g.price) }}</text>
|
|
|
- <text class="pv-origin-price">¥{{ formatPrice(g.originPrice) }}</text>
|
|
|
- </view>
|
|
|
- <view class="pv-action-btn">{{ actionText }} ></view>
|
|
|
</view>
|
|
|
</view>
|
|
|
|
|
|
<!-- 1排2列 / 1排3列:网格 -->
|
|
|
- <view v-else class="pv-grid" :class="'cols-' + cols">
|
|
|
- <view v-for="g in data.goods" :key="g.goodsId" class="pv-grid-card">
|
|
|
+ <view v-else class="pv-goods-grid" :class="'cols-' + cols">
|
|
|
+ <view v-for="g in data.goods" :key="g.id || g.goodsId" class="pv-grid-card">
|
|
|
<view class="pv-grid-cover-wrap">
|
|
|
<image class="pv-grid-cover" :src="g.coverUrl" mode="aspectFill" />
|
|
|
<view v-if="isGroupBuy" class="pv-group-badge">{{ g.groupSize }}人团</view>
|
|
|
@@ -68,7 +80,10 @@ export default {
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
- countdown: { h: '00', m: '00', s: '00' },
|
|
|
+ // showDay:剩余 ≥24h 时展示天;d/h/m/s 为补零后的展示字符串
|
|
|
+ countdown: { showDay: false, d: '00', h: '00', m: '00', s: '00' },
|
|
|
+ /** 是否处于活动有效期,控制整块专区显隐 */
|
|
|
+ isActive: false,
|
|
|
_timer: null
|
|
|
}
|
|
|
},
|
|
|
@@ -82,18 +97,26 @@ export default {
|
|
|
cols() {
|
|
|
const c = parseInt(this.data && this.data.layoutCols, 10)
|
|
|
return [1, 2, 3].includes(c) ? c : 3
|
|
|
+ },
|
|
|
+ /** 展示数量小于商品总数时显示「更多」文案(预览只读,不可点击) */
|
|
|
+ showMore() {
|
|
|
+ return Number(this.data && this.data.goodsTotal || 0) > ((this.data && this.data.goods) || []).length
|
|
|
}
|
|
|
},
|
|
|
watch: {
|
|
|
- 'data.endTime': {
|
|
|
+ // 活动起止时间变更时重算有效期与倒计时(接口刷新后同步)
|
|
|
+ 'data.startTime': {
|
|
|
immediate: true,
|
|
|
handler() {
|
|
|
this.restartCountdown()
|
|
|
}
|
|
|
+ },
|
|
|
+ 'data.endTime'() {
|
|
|
+ this.restartCountdown()
|
|
|
}
|
|
|
},
|
|
|
beforeDestroy() {
|
|
|
- if (this._timer) clearInterval(this._timer)
|
|
|
+ this.clearTimer()
|
|
|
},
|
|
|
methods: {
|
|
|
formatPrice(val) {
|
|
|
@@ -101,24 +124,82 @@ export default {
|
|
|
if (isNaN(n)) return '0'
|
|
|
return n % 1 === 0 ? String(n) : n.toFixed(2)
|
|
|
},
|
|
|
+ /** 清除倒计时定时器,避免页面销毁或活动结束后空转 */
|
|
|
+ clearTimer() {
|
|
|
+ if (this._timer) {
|
|
|
+ clearInterval(this._timer)
|
|
|
+ this._timer = null
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 重启倒计时:先立即算一次有效期,再每秒刷新;
|
|
|
+ * 未开始时定时器继续跑以便到点展示,结束后停表并隐藏。
|
|
|
+ */
|
|
|
restartCountdown() {
|
|
|
- if (this._timer) clearInterval(this._timer)
|
|
|
+ this.clearTimer()
|
|
|
this.tickCountdown()
|
|
|
- this._timer = setInterval(this.tickCountdown, 1000)
|
|
|
+ // 已结束则无需再开定时器;未开始/进行中继续每秒刷新
|
|
|
+ if (this._shouldKeepTimer()) {
|
|
|
+ this._timer = setInterval(this.tickCountdown, 1000)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 是否还需维持定时器:时间配置有效且未到结束时刻
|
|
|
+ * (未开始也要继续跑,以便到点自动展示)
|
|
|
+ * @returns {boolean}
|
|
|
+ */
|
|
|
+ _shouldKeepTimer() {
|
|
|
+ const start = parseInt(this.data && this.data.startTime, 10) || 0
|
|
|
+ const end = parseInt(this.data && this.data.endTime, 10) || 0
|
|
|
+ if (start <= 0 || end <= 0) return false
|
|
|
+ const now = Math.floor(Date.now() / 1000)
|
|
|
+ return now < end
|
|
|
},
|
|
|
+ /**
|
|
|
+ * 按 startTime/endTime 刷新专区显隐与倒计时数字;
|
|
|
+ * 与后端 calcActivityStatus 一致:仅 now ∈ [start, end] 时展示;
|
|
|
+ * 剩余 ≥24h 时拆出天单位,小时限制在 0–23。
|
|
|
+ */
|
|
|
tickCountdown() {
|
|
|
+ const start = parseInt(this.data && this.data.startTime, 10) || 0
|
|
|
const end = parseInt(this.data && this.data.endTime, 10) || 0
|
|
|
const now = Math.floor(Date.now() / 1000)
|
|
|
+
|
|
|
+ // 无有效时间或已超过结束时间:隐藏专区并停表
|
|
|
+ if (start <= 0 || end <= 0 || now > end) {
|
|
|
+ this.isActive = false
|
|
|
+ this.countdown = { showDay: false, d: '00', h: '00', m: '00', s: '00' }
|
|
|
+ this.clearTimer()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 未到开始时间:暂不展示,定时器继续等待到点
|
|
|
+ if (now < start) {
|
|
|
+ this.isActive = false
|
|
|
+ this.countdown = { showDay: false, d: '00', h: '00', m: '00', s: '00' }
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 活动进行中:展示专区并更新剩余天/时/分/秒(≥24h 拆出天)
|
|
|
+ this.isActive = true
|
|
|
let diff = end - now
|
|
|
if (diff < 0) diff = 0
|
|
|
- const h = Math.floor(diff / 3600)
|
|
|
+ const d = Math.floor(diff / 86400)
|
|
|
+ const h = Math.floor((diff % 86400) / 3600)
|
|
|
const m = Math.floor((diff % 3600) / 60)
|
|
|
const s = diff % 60
|
|
|
this.countdown = {
|
|
|
+ showDay: d > 0,
|
|
|
+ d: String(d).padStart(2, '0'),
|
|
|
h: String(h).padStart(2, '0'),
|
|
|
m: String(m).padStart(2, '0'),
|
|
|
s: String(s).padStart(2, '0')
|
|
|
}
|
|
|
+ // 刚好走到结束点时下一秒会隐藏;diff 为 0 时也立即隐藏,避免残留 00:00:00
|
|
|
+ if (diff === 0) {
|
|
|
+ this.isActive = false
|
|
|
+ this.clearTimer()
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -126,17 +207,24 @@ export default {
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
.pv-activity {
|
|
|
- margin: 20upx 20upx 0;
|
|
|
+ margin-top: 3upx;
|
|
|
padding: 24upx;
|
|
|
- border-radius: 16upx;
|
|
|
background: #fff;
|
|
|
box-shadow: 0 4upx 20upx rgba(255, 107, 136, 0.08);
|
|
|
}
|
|
|
.pv-activity-head {
|
|
|
margin-bottom: 20upx;
|
|
|
}
|
|
|
+.pv-activity-head-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
.pv-activity-title-row {
|
|
|
- @include disFlex(center, space-between);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
}
|
|
|
.pv-activity-title {
|
|
|
font-size: 32upx;
|
|
|
@@ -144,7 +232,9 @@ export default {
|
|
|
color: #FF4D6D;
|
|
|
}
|
|
|
.pv-countdown {
|
|
|
- @include disFlex(center, flex-end);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-left: 22upx;
|
|
|
}
|
|
|
.pv-countdown-unit {
|
|
|
min-width: 44upx;
|
|
|
@@ -161,16 +251,29 @@ export default {
|
|
|
color: #FF4D6D;
|
|
|
font-weight: 700;
|
|
|
}
|
|
|
-.pv-activity-more {
|
|
|
+.pv-countdown-day {
|
|
|
+ margin: 0 6upx 0 4upx;
|
|
|
+ font-size: 22upx;
|
|
|
+ color: #FF4D6D;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+.pv-section-more {
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-left: 16upx;
|
|
|
+ font-size: 22upx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+.pv-activity-subtitle {
|
|
|
display: block;
|
|
|
margin-top: 8upx;
|
|
|
font-size: 22upx;
|
|
|
- color: $fontColor3;
|
|
|
+ color: #999;
|
|
|
}
|
|
|
|
|
|
-/* 1排1列 */
|
|
|
+/* 1排1列:左图 + 右侧标题/描述/价格按钮 */
|
|
|
.pv-row-card {
|
|
|
- @include disFlex(center, flex-start);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
padding: 16upx 0;
|
|
|
border-bottom: 1px solid #f5f5f5;
|
|
|
&:last-child {
|
|
|
@@ -184,61 +287,87 @@ export default {
|
|
|
flex-shrink: 0;
|
|
|
background: #f5f5f5;
|
|
|
}
|
|
|
-.pv-row-body {
|
|
|
+.pv-row-content {
|
|
|
flex: 1;
|
|
|
min-width: 0;
|
|
|
- padding: 0 16upx;
|
|
|
+ margin-left: 16upx;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: space-between;
|
|
|
+ min-height: 140upx;
|
|
|
}
|
|
|
.pv-row-name {
|
|
|
display: block;
|
|
|
- font-size: 28upx;
|
|
|
+ font-size: 30upx;
|
|
|
color: #333;
|
|
|
- font-weight: 600;
|
|
|
- margin-bottom: 8upx;
|
|
|
+ font-weight: 700;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
.pv-row-desc {
|
|
|
+ display: block;
|
|
|
+ margin-top: 8upx;
|
|
|
font-size: 22upx;
|
|
|
- color: $fontColor3;
|
|
|
+ color: #999;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+.pv-row-bottom {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-top: 12upx;
|
|
|
}
|
|
|
.pv-row-price-wrap {
|
|
|
- flex-shrink: 0;
|
|
|
- text-align: right;
|
|
|
- margin-right: 16upx;
|
|
|
+ display: flex;
|
|
|
+ align-items: baseline;
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ margin-right: 12upx;
|
|
|
}
|
|
|
-.pv-price {
|
|
|
- display: block;
|
|
|
- font-size: 30upx;
|
|
|
+.pv-row-price-wrap .pv-price {
|
|
|
+ font-size: 36upx;
|
|
|
color: #FF4D6D;
|
|
|
font-weight: 700;
|
|
|
+ margin-right: 8upx;
|
|
|
}
|
|
|
-.pv-origin-price {
|
|
|
+.pv-row-price-wrap .pv-origin-price {
|
|
|
font-size: 22upx;
|
|
|
color: #bbb;
|
|
|
text-decoration: line-through;
|
|
|
}
|
|
|
.pv-action-btn {
|
|
|
flex-shrink: 0;
|
|
|
- padding: 12upx 22upx;
|
|
|
- font-size: 24upx;
|
|
|
+ padding: 10upx 20upx;
|
|
|
+ font-size: 22upx;
|
|
|
color: #fff;
|
|
|
background: linear-gradient(135deg, #FF8FA3, #FF4D6D);
|
|
|
border-radius: 30upx;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
+.pv-price {
|
|
|
+ font-size: 30upx;
|
|
|
+ color: #FF4D6D;
|
|
|
+ font-weight: 700;
|
|
|
+}
|
|
|
+.pv-origin-price {
|
|
|
+ font-size: 22upx;
|
|
|
+ color: #bbb;
|
|
|
+ text-decoration: line-through;
|
|
|
+}
|
|
|
|
|
|
/* 网格 2/3 列 */
|
|
|
-.pv-grid {
|
|
|
+.pv-goods-grid {
|
|
|
display: flex;
|
|
|
flex-wrap: wrap;
|
|
|
margin: 0 -8upx;
|
|
|
}
|
|
|
-.pv-grid.cols-2 .pv-grid-card {
|
|
|
+.pv-goods-grid.cols-2 .pv-grid-card {
|
|
|
width: 50%;
|
|
|
}
|
|
|
-.pv-grid.cols-3 .pv-grid-card {
|
|
|
+.pv-goods-grid.cols-3 .pv-grid-card {
|
|
|
width: 33.33%;
|
|
|
}
|
|
|
.pv-grid-card {
|
|
|
@@ -281,7 +410,8 @@ export default {
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
.pv-grid-price-row {
|
|
|
- @include disFlex(center, flex-start);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
margin-top: 6upx;
|
|
|
}
|
|
|
.pv-grid-btn {
|