|
|
@@ -298,33 +298,31 @@
|
|
|
|
|
|
<!-- 付费遮罩必须在最上层(iOS nvue 后绘制的兄弟节点才能点到) -->
|
|
|
<view v-if="paywallOn" class="stage-paywall" :style="paywallLayerStyle">
|
|
|
- <image class="stage-paywall-bg" :src="posterSrc" :style="fullFrameStyle" mode="aspectFill" />
|
|
|
- <view class="stage-paywall-mask" :style="fullFrameStyle" />
|
|
|
+ <!-- 封面未就绪时的底色占位,避免纯黑屏 -->
|
|
|
+ <view class="stage-paywall-placeholder" :style="fullFrameStyle" />
|
|
|
+ <image
|
|
|
+ v-if="posterSrc"
|
|
|
+ class="stage-paywall-bg"
|
|
|
+ :src="posterSrc"
|
|
|
+ :style="fullFrameStyle"
|
|
|
+ mode="aspectFill"
|
|
|
+ :lazy-load="false"
|
|
|
+ @load="onPaywallCoverLoad"
|
|
|
+ @error="onPaywallCoverError"
|
|
|
+ />
|
|
|
+ <view class="stage-paywall-mask" :style="paywallMaskStyle" />
|
|
|
<view class="stage-paywall-body" :style="fullFrameStyle">
|
|
|
<image class="stage-paywall-lock-img" src="/static/imgs/icon/ic_lock.png" mode="aspectFit" />
|
|
|
<text class="stage-paywall-title">{{ copy.trialEnd }}</text>
|
|
|
- <view
|
|
|
- class="stage-paywall-btn"
|
|
|
- @click="openLivePayPopup"
|
|
|
- @tap="openLivePayPopup"
|
|
|
- >
|
|
|
+ <!-- 仅父级 @tap,避免 iOS 上 click+tap / 子节点冒泡连弹两次 -->
|
|
|
+ <view class="stage-paywall-btn" @tap="openLivePayPopup">
|
|
|
<image
|
|
|
class="stage-paywall-btn-icon"
|
|
|
src="/static/imgs/icon/ic_d.png"
|
|
|
mode="aspectFit"
|
|
|
- @click.stop="openLivePayPopup"
|
|
|
- @tap.stop="openLivePayPopup"
|
|
|
/>
|
|
|
- <text
|
|
|
- class="stage-paywall-btn-amount"
|
|
|
- @click.stop="openLivePayPopup"
|
|
|
- @tap.stop="openLivePayPopup"
|
|
|
- >{{ session.paid_money || 0 }}</text>
|
|
|
- <text
|
|
|
- class="stage-paywall-btn-txt"
|
|
|
- @click.stop="openLivePayPopup"
|
|
|
- @tap.stop="openLivePayPopup"
|
|
|
- >{{ copy.goToPay }}</text>
|
|
|
+ <text class="stage-paywall-btn-amount">{{ session.paid_money || 0 }}</text>
|
|
|
+ <text class="stage-paywall-btn-txt">{{ copy.goToPay }}</text>
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
@@ -430,9 +428,13 @@ export default {
|
|
|
sessionPhase: 'loading',
|
|
|
paywallOn: false,
|
|
|
unlocked: false,
|
|
|
+ /** 付费锁屏封面是否已加载成功 */
|
|
|
+ paywallCoverReady: false,
|
|
|
showLivePayPopup: false,
|
|
|
showInsufficientBalanceModal: false,
|
|
|
unlockLiveBusy: false,
|
|
|
+ _payPopupBusy: false,
|
|
|
+ _payPopupAt: 0,
|
|
|
draftMsg: '',
|
|
|
_followBusy: false,
|
|
|
playerRenderToken: 0,
|
|
|
@@ -482,7 +484,27 @@ export default {
|
|
|
return { width: `${this.viewport.w}px`, height: `${this.viewport.h}px` }
|
|
|
},
|
|
|
posterSrc() {
|
|
|
- return this.toCdn(this.session.image)
|
|
|
+ const fromSession = this.toCdn(this.session && this.session.image)
|
|
|
+ if (fromSession) return fromSession
|
|
|
+ const fromFeed = this.toCdn(
|
|
|
+ (this.feedCovers && this.feedCovers[this.feedCursor]) || ''
|
|
|
+ )
|
|
|
+ if (fromFeed) return fromFeed
|
|
|
+ const fromAvatar = this.toCdn(
|
|
|
+ (this.session && this.session.shop && this.session.shop.avatar) || ''
|
|
|
+ )
|
|
|
+ if (fromAvatar) return fromAvatar
|
|
|
+ return ''
|
|
|
+ },
|
|
|
+ paywallMaskStyle() {
|
|
|
+ const base = this.fullFrameStyle || {}
|
|
|
+ return {
|
|
|
+ ...base,
|
|
|
+ // 封面未出时用深色蓝底,避免纯黑;就绪后再加深遮罩
|
|
|
+ backgroundColor: this.paywallCoverReady
|
|
|
+ ? 'rgba(0, 0, 0, 0.55)'
|
|
|
+ : 'rgba(28, 30, 61, 0.55)'
|
|
|
+ }
|
|
|
},
|
|
|
anchorTitle() {
|
|
|
return (this.session.shop && this.session.shop.shopname) || this.copy.liveRoom || ''
|
|
|
@@ -982,6 +1004,7 @@ export default {
|
|
|
this._entryMeta.isFollow = normalizeIsFollow(this._entryMeta.isFollow)
|
|
|
}
|
|
|
}
|
|
|
+ this.seedPosterFromFeed()
|
|
|
return
|
|
|
}
|
|
|
try {
|
|
|
@@ -997,8 +1020,18 @@ export default {
|
|
|
this.feedCovers = pairs.map((p) => p.cover)
|
|
|
const hit = this.feedIds.indexOf(String(this.roomKey))
|
|
|
this.feedCursor = hit >= 0 ? hit : 0
|
|
|
+ this.seedPosterFromFeed()
|
|
|
} catch (_) {}
|
|
|
},
|
|
|
+ /** 列表进房时先灌封面,详情返回前锁屏/底图不至于空黑 */
|
|
|
+ seedPosterFromFeed() {
|
|
|
+ const cover = (this.feedCovers && this.feedCovers[this.feedCursor]) || ''
|
|
|
+ if (!cover) return
|
|
|
+ if (!this.session) this.session = { shop: {}, state: 0, paid_money: 0, isFollow: false }
|
|
|
+ if (!this.session.image) {
|
|
|
+ this.$set(this.session, 'image', cover)
|
|
|
+ }
|
|
|
+ },
|
|
|
async enterSession() {
|
|
|
if (!this.roomKey) {
|
|
|
uni.showToast({ title: this.copy.roomParamMissing, icon: 'none' })
|
|
|
@@ -1006,6 +1039,8 @@ export default {
|
|
|
}
|
|
|
this.sessionPhase = 'loading'
|
|
|
this.paywallOn = false
|
|
|
+ this.paywallCoverReady = false
|
|
|
+ this.seedPosterFromFeed()
|
|
|
let detail = null
|
|
|
try {
|
|
|
detail = await pullBroadcastDetail(this.roomKey)
|
|
|
@@ -1036,6 +1071,11 @@ export default {
|
|
|
if (!Array.isArray(this.session.goods)) {
|
|
|
this.$set(this.session, 'goods', [])
|
|
|
}
|
|
|
+ // 详情若无封面,保留列表带入的 thumb
|
|
|
+ if (!this.session.image) {
|
|
|
+ this.seedPosterFromFeed()
|
|
|
+ }
|
|
|
+ this.paywallCoverReady = false
|
|
|
this.goodsSheetVisible = false
|
|
|
// 大厅带入的 user_no 写入 session,供关注接口使用(play 详情常缺此字段)
|
|
|
const entryNo = String((this._entryMeta && this._entryMeta.userNo) || '').trim()
|
|
|
@@ -1054,7 +1094,16 @@ export default {
|
|
|
console.log('[LIVE-DIAG] platform =', this.platformLabel)
|
|
|
|
|
|
this.streamArchive = url
|
|
|
- this.streamSrc = url || ''
|
|
|
+ const paidAmount = Number(detail && detail.paid_money) || 0
|
|
|
+ // 付费未解锁:进房立刻锁屏,避免先出画面再等 getLastTime 才遮罩
|
|
|
+ if (paidAmount > 0 && !this.unlocked) {
|
|
|
+ this.paywallCoverReady = false
|
|
|
+ this.paywallOn = true
|
|
|
+ this.streamSrc = ''
|
|
|
+ } else {
|
|
|
+ this.paywallOn = false
|
|
|
+ this.streamSrc = url || ''
|
|
|
+ }
|
|
|
// 强制让 <stream-player> 重新挂载,等价源代码 onShow 里 `playComponentKey = Date.now()`,
|
|
|
// 解决 nvue + weex video 在 src 由空变非空时偶发不自动 reload 的问题。
|
|
|
this.playerRenderToken += 1
|
|
|
@@ -1068,8 +1117,7 @@ export default {
|
|
|
console.log('=== [LIVE-DIAG] banner snap ===', this.lastDetailSnap)
|
|
|
console.log('=== [LIVE-DIAG] shouldRenderPlayer =', this.shouldRenderPlayer, ' paywallOn =', this.paywallOn)
|
|
|
|
|
|
- // 试看时长检测必须脱离主流程:源码里 checkFreeTime 也是不 await 的,
|
|
|
- // 失败不能让 sessionPhase 倒退回 offline,否则正在直播的房间会被误判为已结束。
|
|
|
+ // 试看检测不阻塞进房;付费房已先锁屏,有剩余试看时再放开
|
|
|
this.setupTrial().catch((err) => {
|
|
|
console.warn('[LIVE-DIAG] setupTrial failed', err)
|
|
|
})
|
|
|
@@ -1121,28 +1169,73 @@ export default {
|
|
|
sec = await pullTrialSeconds(this.roomKey)
|
|
|
} catch (err) {
|
|
|
if (appSettings.debug) console.warn('[live] pullTrialSeconds failed', err)
|
|
|
+ // 接口失败:付费房保持/打开锁屏,避免裸播
|
|
|
+ if ((Number(this.session.paid_money) || 0) > 0 && !this.unlocked) {
|
|
|
+ this.triggerPaywall()
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // -1:免费全场
|
|
|
+ if (sec === -1) {
|
|
|
+ this.releasePaywallForTrial()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (sec == null) {
|
|
|
+ if ((Number(this.session.paid_money) || 0) > 0 && !this.unlocked) {
|
|
|
+ this.triggerPaywall()
|
|
|
+ }
|
|
|
return
|
|
|
}
|
|
|
- if (sec === -1 || sec == null) return
|
|
|
this.trialClock?.stop()
|
|
|
this.trialClock = new TrialMeter(() => this.triggerPaywall())
|
|
|
if (sec <= 0) {
|
|
|
this.triggerPaywall()
|
|
|
return
|
|
|
}
|
|
|
+ // 仍有试看:关锁屏、开播并倒计时
|
|
|
+ this.releasePaywallForTrial()
|
|
|
this.trialClock.start(sec)
|
|
|
},
|
|
|
+ /** 有试看时长时放开锁屏并恢复拉流 */
|
|
|
+ releasePaywallForTrial() {
|
|
|
+ if (this.unlocked) return
|
|
|
+ this.paywallOn = false
|
|
|
+ const url = this.streamArchive || resolvePlaybackUrl(this.session)
|
|
|
+ if (!url) return
|
|
|
+ if (this.streamSrc !== url) {
|
|
|
+ this.streamSrc = url
|
|
|
+ this.playerRenderToken += 1
|
|
|
+ }
|
|
|
+ },
|
|
|
triggerPaywall() {
|
|
|
+ this.paywallCoverReady = false
|
|
|
this.paywallOn = true
|
|
|
this.streamSrc = ''
|
|
|
this.trialClock?.stop()
|
|
|
},
|
|
|
+ onPaywallCoverLoad() {
|
|
|
+ this.paywallCoverReady = true
|
|
|
+ },
|
|
|
+ onPaywallCoverError() {
|
|
|
+ this.paywallCoverReady = false
|
|
|
+ },
|
|
|
openLivePayPopup() {
|
|
|
+ const now = Date.now()
|
|
|
+ // iOS nvue 偶发 tap 连发;短时防抖避免连弹两个系统确认框
|
|
|
+ if (this._payPopupBusy || (now - (this._payPopupAt || 0) < 900)) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ this._payPopupBusy = true
|
|
|
+ this._payPopupAt = now
|
|
|
console.log('[live] openLivePayPopup tap')
|
|
|
if (this.$appKit && typeof this.$appKit.requireLogin === 'function') {
|
|
|
- if (!this.$appKit.requireLogin()) return
|
|
|
+ if (!this.$appKit.requireLogin()) {
|
|
|
+ this._payPopupBusy = false
|
|
|
+ return
|
|
|
+ }
|
|
|
} else if (!(this.$store && this.$store.state && this.$store.state.user && this.$store.state.user.isLogin)) {
|
|
|
uni.showToast({ title: this.copy.needLogin, icon: 'none' })
|
|
|
+ this._payPopupBusy = false
|
|
|
return
|
|
|
}
|
|
|
// #ifdef APP-PLUS
|
|
|
@@ -1153,12 +1246,23 @@ export default {
|
|
|
confirmText: this.copy.confirm || '确认',
|
|
|
cancelText: this.copy.cancel || '关闭',
|
|
|
success: (res) => {
|
|
|
+ this._payPopupBusy = false
|
|
|
+ this._payPopupAt = 0
|
|
|
if (res.confirm) this.checkLiveBalanceAndPay()
|
|
|
+ },
|
|
|
+ fail: () => {
|
|
|
+ this._payPopupBusy = false
|
|
|
+ this._payPopupAt = 0
|
|
|
}
|
|
|
})
|
|
|
+ // 兜底:部分机型 success 偶发不回调时释放锁
|
|
|
+ setTimeout(() => {
|
|
|
+ this._payPopupBusy = false
|
|
|
+ }, 1200)
|
|
|
return
|
|
|
// #endif
|
|
|
this.showLivePayPopup = true
|
|
|
+ this._payPopupBusy = false
|
|
|
},
|
|
|
noop() {},
|
|
|
closeLivePayPopup() {
|
|
|
@@ -2199,6 +2303,12 @@ export default {
|
|
|
bottom: 0;
|
|
|
z-index: 80;
|
|
|
}
|
|
|
+.stage-paywall-placeholder {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ background-color: #1c1e3d;
|
|
|
+}
|
|
|
.stage-paywall-bg {
|
|
|
position: absolute;
|
|
|
top: 0;
|
|
|
@@ -2210,7 +2320,6 @@ export default {
|
|
|
left: 0;
|
|
|
right: 0;
|
|
|
bottom: 0;
|
|
|
- background-color: rgba(0, 0, 0, 0.55);
|
|
|
}
|
|
|
.stage-paywall-body {
|
|
|
position: absolute;
|