<template>
	<view class="stage">
		<!-- 封面底图 -->
		<image
			class="stage-poster"
			:src="posterSrc"
			:style="fullFrameStyle"
			mode="aspectFill"
		/>

		<!-- 播放区 -->
		<view v-if="shouldRenderPlayer" class="stage-player">
			<!-- 多房间：三屏竖向 pager，始终复位到中间屏 -->
			<swiper
				v-if="pagerEnabled"
				class="v-pager"
				vertical="true"
				:acceleration="true"
				:current="pagerSlot"
				:duration="pagerAnimDuration"
				:disable-touch="pagerTouchLocked"
				:style="fullFrameStyle"
				@change="onPagerSlotChange"
			>
				<swiper-item>
					<view class="v-pager-cell" :style="fullFrameStyle">
						<view v-if="priorRoomKey" class="v-pager-neighbor">
							<image
								v-if="priorRoomCover"
								class="v-pager-cover"
								:src="priorRoomCover"
								:style="fullFrameStyle"
								mode="aspectFill"
							/>
							<view v-else class="v-pager-fallback" :style="fullFrameStyle" />
							<view class="v-pager-hint-box">
								<text class="v-pager-hint">↑</text>
							</view>
						</view>
						<view v-else class="v-pager-edge" :style="fullFrameStyle">
							<text class="v-pager-edge-txt">{{ copy.noPrior }}</text>
						</view>
					</view>
				</swiper-item>
				<swiper-item>
					<view class="v-pager-cell" :style="fullFrameStyle">
						<stream-player
							:key="playerRenderToken"
							:source="streamSrc"
							:phase="sessionPhase"
							:frame-width="viewport.w"
							:frame-height="viewport.h"
						/>
					</view>
				</swiper-item>
				<swiper-item>
					<view class="v-pager-cell" :style="fullFrameStyle">
						<view v-if="nextRoomKey" class="v-pager-neighbor">
							<image
								v-if="nextRoomCover"
								class="v-pager-cover"
								:src="nextRoomCover"
								:style="fullFrameStyle"
								mode="aspectFill"
							/>
							<view v-else class="v-pager-fallback" :style="fullFrameStyle" />
							<view class="v-pager-hint-box">
								<text class="v-pager-hint">↓</text>
							</view>
						</view>
						<view v-else class="v-pager-edge" :style="fullFrameStyle">
							<text class="v-pager-edge-txt">{{ copy.noNext }}</text>
						</view>
					</view>
				</swiper-item>
			</swiper>
			<stream-player
				v-else
				:key="playerRenderToken"
				:source="streamSrc"
				:phase="sessionPhase"
				:frame-width="viewport.w"
				:frame-height="viewport.h"
			/>
		</view>

		<!-- 状态层 -->
		<view v-if="statusHint && !paywallOn" class="stage-hint">
			<text class="stage-hint-txt">{{ statusHint }}</text>
		</view>

		<!-- 诊断 banner（仅 debug 模式显示，方便排查"看不了"的根因） -->
		<!-- <view v-if="diagText" class="stage-diag" :style="{ top: (viewport.statusBar + 90) + 'px' }">
			<text class="stage-diag-txt">{{ diagText }}</text>
		</view> -->

		<!-- 礼物层必须在 vue 子组件之前（同 broadcast / 参考工程）：
		     nvue 里 vue 会生成原生 webview 容器，后挂的 native 飘条易被盖住 -->
		<!-- 礼物显示列表（最多3个） -->
		<view class="gift-display-list" v-if="displayGiftList.length > 0">
			<view
				v-for="(item, index) in displayGiftList"
				:key="item.id"
				class="gift-display-item"
				:class="{ 'gift-display-item-fade-out': item.isFadingOut }"
				:style="getGiftItemStyle(item)"
			>
				<view class="gift-display-content">
					<text class="gift-display-name">{{ item.userName }}</text>
					<image
						class="gift-display-image"
						:src="toCdn(item.giftImage)"
						mode="aspectFit"
					/>
					<text class="gift-display-count">✖{{ item.count }}</text>
				</view>
			</view>
		</view>

		<!-- 打赏动画 - SVGA -->
		<!-- #ifndef APP-NVUE -->
		<!-- #ifndef H5 -->
		<view class="gift-svga-container" v-if="currentGift.show && currentGift.svgaUrl">
			<c-svga
				ref="giftSvga"
				:key="giftCanvasId"
				:canvasId="giftCanvasId"
				:src="currentGift.svgaUrl"
				:loops="1"
				:auto-play="true"
				width="750rpx"
				height="750rpx"
				@finished="onGiftAnimationEnd"
				@loaded="onGiftSvgaLoaded"
			/>
			<view class="gift-text-overlay" v-if="currentGift.text">
				<text class="gift-text">{{ currentGift.text }}</text>
			</view>
		</view>
		<!-- #endif -->
		<!-- #endif -->
		<!-- #ifdef APP-NVUE -->
		<!-- nvue：tx-svga 常驻挂载，避免每次 v-if 重建导致 ref 为空、特效播不出来 -->
		<view
			class="gift-svga-container"
			:style="nvueGiftLayerStyle"
		>
			<tx-svga
				v-if="svgaNativeReady"
				ref="giftTxSvga"
				class="gift-tx-svga"
				:style="nvueTxSvgaStyle"
				@statechange="onGiftTxSvgaStateChange"
			/>
			<view class="gift-text-overlay" v-if="currentGift.show && currentGift.text">
				<text class="gift-text">{{ currentGift.text }}</text>
			</view>
		</view>
		<!-- #endif -->

		<!-- 顶栏 -->
		<view class="stage-top" :style="{ paddingTop: viewport.statusBar + 'px' }">
			<view class="stage-back" @tap="exitRoom">
				<text class="stage-back-txt">‹</text>
			</view>
			<view class="stage-anchor">
				<image class="stage-anchor-av" :src="anchorCover" mode="aspectFill" />
				<view class="stage-anchor-meta">
					<text class="stage-anchor-name">{{ anchorTitle }}</text>
					<text class="stage-anchor-online">{{ metrics.online }} {{ copy.watching }}</text>
				</view>
				<view
					class="stage-follow-btn"
					:class="anchorFollowed ? 'stage-follow-btn--on' : ''"
					@tap.stop="tapFollow"
				>
					<text
						class="stage-follow-btn-txt"
						:class="anchorFollowed ? 'stage-follow-btn-txt--on' : ''"
					>{{ anchorFollowed ? copy.followed : copy.follow }}</text>
				</view>
			</view>
		</view>

		<!-- 弹幕区 -->
		<live-chat-panel
			v-if="sessionPhase === 'onair' && !paywallOn"
			ref="chatPanel"
			class="stage-chat"
			:style="{ bottom: chatBottomPx + 'px' }"
		/>

		<!-- 底部操作坞：线框容器，黑边区域纳入框内（付费遮罩时隐藏，避免挡点击） -->
		<view
			v-if="sessionPhase === 'onair' && !paywallOn"
			class="stage-dock"
			:style="dockWrapStyle"
		>
			<view class="stage-dock-frame">
				<view class="stage-dock-body" :style="dockBodyStyle">
					<view class="stage-dock-row">
						<view v-if="!paywallOn" class="stage-composer">
							<input
								class="stage-input"
								v-model="draftMsg"
								confirm-type="send"
								:placeholder="copy.saySomething"
								placeholder-style="color:rgba(255,255,255,0.45)"
								@confirm="submitChat"
							/>
							<view class="stage-send-hit" @tap="submitChat">
								<text class="stage-send-txt">{{ copy.send }}</text>
							</view>
						</view>
						<view v-else class="stage-composer stage-composer--disabled">
							<text class="stage-composer-placeholder">{{ copy.trialEnd }}</text>
						</view>
						<!-- 购物袋 / 礼物 / 点赞 -->
						<view class="stage-like-col stage-like-col--trio">
							<view class="stage-like-btn stage-goods-btn" @tap="openGoodsSheet">
								<image class="stage-like-icon stage-goods-icon" src="/static/icon/live-shoping.png" mode="aspectFit" />
								<view v-if="liveGoods.length" class="stage-goods-badge">
									<text class="stage-goods-badge-txt">{{ liveGoodsBadge }}</text>
								</view>
							</view>
							<view class="stage-like-btn" @tap="openGiftPanel">
								<image class="stage-like-icon" src="/static/icon/live-gift.png" mode="aspectFit" />
							</view>
							<view class="stage-like-btn" @tap="tapLike">
								<image class="stage-like-icon" src="/static/icon/thumb-up.png" mode="aspectFit" />
							</view>
						</view>
					</view>
				</view>
			</view>
		</view>

		<!-- 直播购物袋弹层 -->
		<view
			v-if="goodsSheetVisible"
			class="stage-goods-mask"
			@tap="closeGoodsSheet"
		/>
		<view
			v-if="goodsSheetVisible"
			class="stage-goods-sheet"
			:style="goodsSheetStyle"
		>
			<view class="stage-goods-sheet-hd">
				<text class="stage-goods-sheet-title">{{ goodsSheetTitle }}</text>
				<view class="stage-goods-sheet-close" @tap="closeGoodsSheet">
					<text class="stage-goods-sheet-close-txt">×</text>
				</view>
			</view>
			<scroll-view scroll-y="true" class="stage-goods-sheet-scroll" :style="{ height: goodsListHeightPx + 'px' }">
				<view v-if="!liveGoods.length" class="stage-goods-empty">
					<text class="stage-goods-empty-txt">{{ copy.noGoods }}</text>
				</view>
				<view
					v-for="(item, index) in liveGoods"
					:key="item.id || index"
					class="stage-goods-row"
				>
					<image class="stage-goods-cover" :src="goodsCover(item)" mode="aspectFill" />
					<view class="stage-goods-rank" :class="'stage-goods-rank--' + Math.min(index + 1, 3)">
						<text class="stage-goods-rank-txt">{{ index + 1 }}</text>
					</view>
					<view class="stage-goods-info">
						<text class="stage-goods-title" :lines="2">{{ item.title || '' }}</text>
						<view class="stage-goods-ops">
							<text class="stage-goods-price">HKD{{ item.price }}</text>
							<view class="stage-goods-ops-btns">
								<view
									v-if="sessionPhase === 'onair'"
									class="stage-goods-explain"
									@tap="requestGoodsExplain(index)"
								>
									<text class="stage-goods-explain-txt">{{ copy.askExplain }}</text>
								</view>
								<view class="stage-goods-buy" @tap="openGoodsDetail(item)">
									<text class="stage-goods-buy-txt">{{ copy.buy }}</text>
								</view>
							</view>
						</view>
					</view>
				</view>
			</scroll-view>
			<view :style="{ height: viewport.safeBottom + 'px' }" />
		</view>

		<!-- 打赏面板：首次打开再挂载 -->
		<live-room-gift-panel
			v-if="giftPanelMounted"
			ref="giftPanel"
			:live-id="giftLiveId"
			@gift-sent="onGiftSent"
		/>

		<!-- 付费遮罩必须在最上层（iOS nvue 后绘制的兄弟节点才能点到） -->
		<view v-if="paywallOn" class="stage-paywall" :style="paywallLayerStyle">
			<!-- 封面未就绪时的底色占位，避免纯黑屏 -->
			<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" />
				<!-- 仅父级 @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"
					/>
					<text class="stage-paywall-btn-amount">{{ session.paid_money || 0 }}</text>
					<text class="stage-paywall-btn-txt">{{ copy.goToPay }}</text>
				</view>
			</view>
		</view>

		<!-- #ifndef APP-NVUE -->
		<live-room-insufficient-balance-modal
			:show="showInsufficientBalanceModal"
			:title="copy.insufficientBalance"
			:subtitle="copy.rechargeToContinue"
			:buttonText="copy.goToRecharge"
			@close="closeInsufficientBalanceModal"
			@confirm="goToLiveRecharge"
		/>
		<live-video-paypop
			:show="showLivePayPopup"
			icon="/static/imgs/icon/ic_pay_diam.png"
			:title="livePayPopupTitle"
			:subtitle="livePayPopupSubtitle"
			:confirmText="copy.confirm"
			:closeText="copy.cancel"
			@close="closeLivePayPopup"
			@confirm="handleLivePayPopupConfirm"
		/>
		<!-- #endif -->
	</view>
</template>

<script>
import appSettings from '@/core/config/app-settings'
import { STORAGE } from '@/core/constants/storage-keys'
import streamPlayer from '@/components/live-watch/stream-player'
import liveChatPanel from '@/components/live-watch/live-chat-panel'
import liveRoomGiftPanel from '@/components/live-room/gift-panel'
// #ifndef APP-NVUE
import liveRoomInsufficientBalanceModal from '@/components/live-room/insufficient-balance-modal.vue'
import liveVideoPaypop from '@/components/uni-popup/live-video-paypop.vue'
// #endif
import { isTeenModeEnabled } from '@/features/account/teen-mode-settings'
import {
	pullBroadcastDetail,
	pullBroadcastCatalog,
	pullTrialSeconds,
	unlockLiveRoom,
	notifyRoomExit,
	pushRoomComment,
	pushRoomLike,
	requestLiveGoodsExplain
} from '@/features/live/gateway'
import { fetchDiamondBalance, toggleClipFollow } from '@/features/feed/feed-gateway'
import { resolvePlaybackUrl, describeRoomPhase } from '@/features/live/stream-picker'
import { readRoomQueue, saveRoomQueue, patchRoomMeta } from '@/features/live/room-queue'
import {
	normalizeIsFollow,
	pickIsFollowFromDetail,
	pickAnchorFollowId
} from '@/features/live/follow-state'
import { locateBroadcastInDirection } from '@/features/live/room-swipe-resolver'
import { TrialMeter } from '@/features/live/trial-meter'
import {
	groupMatchesRoom,
	collectRoomGroupCandidates,
	resolveRoomGroupId,
	isBroadcastEndedSignal
} from '@/features/live/chat-lines'
import { post } from '@/core/http/http-client'
// #ifndef H5
// #ifndef APP-NVUE
import cSvga from '@/uni_modules/c-svga/components/c-svga/c-svga.vue'
// #endif
// #endif

const device = uni.getSystemInfoSync()
/** 直播 contain 模式下底部常见黑边高度（与 stream-player object-fit:contain 对应） */
const STREAM_LETTERBOX_RPX = 50
let safeInsetBottom = 10
// #ifdef APP-PLUS || H5 || MP-WEIXIN
safeInsetBottom = device.safeAreaInsets ? device.safeAreaInsets.bottom : 10
// #endif

export default {
	components: {
		streamPlayer,
		liveChatPanel,
		liveRoomGiftPanel
		// #ifndef APP-NVUE
		,liveRoomInsufficientBalanceModal
		,liveVideoPaypop
		// #endif
		// #ifndef H5
		// #ifndef APP-NVUE
		,cSvga
		// #endif
		// #endif
	},
	data() {
		return {
			roomKey: '',
			session: { shop: {}, state: 0, paid_money: 0, isFollow: false },
			metrics: { online: 0, like: 0 },
			streamSrc: '',
			streamArchive: '',
			sessionPhase: 'loading',
			paywallOn: false,
			unlocked: false,
			/** 付费锁屏封面是否已加载成功 */
			paywallCoverReady: false,
			showLivePayPopup: false,
			showInsufficientBalanceModal: false,
			unlockLiveBusy: false,
			_payPopupBusy: false,
			_payPopupAt: 0,
			draftMsg: '',
			_followBusy: false,
			playerRenderToken: 0,
			viewport: {
				w: device.screenWidth,
				h: device.screenHeight,
				statusBar: device.statusBarHeight || 0,
				safeBottom: safeInsetBottom
			},
			feedIds: [],
			feedCovers: [],
			feedCursor: 0,
			pagerSlot: 1,
			pagerAnimDuration: 300,
			_pagerRecentering: false,
			// 礼物 / 打赏
			teenModeOn: false,
			currentGift: {
				type: 'default',
				image: '',
				text: '',
				svgaUrl: '',
				show: false
			},
			giftCanvasId: 'gift-svga-' + Date.now(),
			giftQueue: [],
			isPlayingGift: false,
			giftAnimationTimeout: null,
			displayGiftList: [],
			giftIdCounter: 0,
			lastTipFingerprints: [],
			currentPlayingGiftId: null,
			svgaNativeReady: false,
			_svgaModel: null,
			giftPanelMounted: false,
			goodsSheetVisible: false,
			_pagerSwitching: false,
			trialClock: null,
			lastDetailSnap: null,
			_entryMeta: {},
			_broadcastEndHandled: false,
			copy: {}
		}
	},
	computed: {
		fullFrameStyle() {
			return { width: `${this.viewport.w}px`, height: `${this.viewport.h}px` }
		},
		posterSrc() {
			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 || ''
		},
		anchorCover() {
			return this.toCdn((this.session.shop && this.session.shop.avatar) || '')
		},
		anchorFollowed() {
			const f = this.session.isFollow
			return f === true || f === 1 || f === '1'
		},
		shouldRenderPlayer() {
			// 参考 live-room.nvue：
			//   v-if="(state == 1 || (state == 2 && playUrl)) && !isPlayStopped"
			// 付费遮罩 / 尚无拉流地址时不挂播放器，避免空壳盖住封面变成黑屏
			if (this.paywallOn) return false
			if (!this.streamSrc) return false
			if (this.sessionPhase === 'onair') return true
			if (this.sessionPhase === 'replay') return true
			return false
		},
		pagerEnabled() {
			return this.feedIds.length > 1
		},
		pagerTouchLocked() {
			return this.paywallOn
		},
		priorRoomKey() {
			if (!this.pagerEnabled || this.feedCursor <= 0) return ''
			return this.feedIds[this.feedCursor - 1] || ''
		},
		nextRoomKey() {
			if (!this.pagerEnabled || this.feedCursor >= this.feedIds.length - 1) return ''
			return this.feedIds[this.feedCursor + 1] || ''
		},
		priorRoomCover() {
			if (!this.priorRoomKey) return ''
			return this.toCdn(this.feedCovers[this.feedCursor - 1] || '')
		},
		nextRoomCover() {
			if (!this.nextRoomKey) return ''
			return this.toCdn(this.feedCovers[this.feedCursor + 1] || '')
		},
		statusHint() {
			if (this.paywallOn) return ''
			const map = {
				loading: this.copy.loading,
				preparing: this.copy.preparing,
				offline: this.copy.offline
			}
			return map[this.sessionPhase] || ''
		},
		chatBottomPx() {
			return this.dockTotalHeightPx + uni.upx2px(16)
		},
		letterboxPx() {
			return uni.upx2px(STREAM_LETTERBOX_RPX)
		},
		dockContentHeightPx() {
			return uni.upx2px(112)
		},
		dockTotalHeightPx() {
			return this.viewport.safeBottom + this.dockContentHeightPx + this.letterboxPx + uni.upx2px(24)
		},
		dockWrapStyle() {
			return {
				paddingBottom: `${this.viewport.safeBottom}px`
			}
		},
		dockBodyStyle() {
			return {
				paddingBottom: `${this.letterboxPx}px`
			}
		},
		nvueGiftLayerStyle() {
			const show = !!(this.currentGift && this.currentGift.show && this.currentGift.svgaUrl)
			// 隐藏时必须收成 0 尺寸：iOS nvue 对 pointer-events:none 支持不稳，全屏透明层会吞掉「去支付」点击
			return {
				opacity: show ? 1 : 0,
				zIndex: show ? 120 : 0,
				width: show ? `${this.viewport.w}px` : '0px',
				height: show ? `${this.viewport.h}px` : '0px',
				overflow: 'hidden'
			}
		},
		nvueTxSvgaStyle() {
			const show = !!(this.currentGift && this.currentGift.show && this.currentGift.svgaUrl)
			return {
				width: show ? `${this.viewport.w}px` : '0px',
				height: show ? `${this.viewport.h}px` : '0px'
			}
		},
		paywallLayerStyle() {
			return {
				width: `${this.viewport.w}px`,
				height: `${this.viewport.h}px`,
				zIndex: 300
			}
		},
		payOverlayStyle() {
			return {
				width: `${this.viewport.w}px`,
				height: `${this.viewport.h}px`,
				zIndex: 320
			}
		},
		showBottomBarLikeCount() {
			return !!appSettings.showBottomBarLikeCount
		},
		likeCountText() {
			const n = Number(this.metrics.like) || 0
			if (n <= 0) return this.copy.like || ''
			if (n > 9999) return `${Math.floor((n - n % 1000) / 10000)}w`
			return String(n)
		},
		/** 直播挂载商品（play 详情 / IM update） */
		liveGoods() {
			const list = this.session && this.session.goods
			return Array.isArray(list) ? list.filter(Boolean) : []
		},
		liveGoodsBadge() {
			const n = this.liveGoods.length
			if (n <= 0) return ''
			return n > 9999 ? `${Math.floor((n - (n % 1000)) / 10000)}w` : String(n)
		},
		goodsListHeightPx() {
			return Math.round(this.viewport.h * 0.42)
		},
		goodsSheetStyle() {
			return {
				width: `${this.viewport.w}px`
			}
		},
		goodsSheetTitle() {
			const tpl = this.copy.goodsCount || '{count}'
			return tpl.replace('{count}', String(this.liveGoods.length))
		},
		platformLabel() {
			// 仅用于日志诊断，区分当前运行端
			// #ifdef H5
			return 'H5'
			// #endif
			// #ifdef APP-PLUS
			return 'APP-PLUS'
			// #endif
			// #ifdef MP-WEIXIN
			return 'MP-WEIXIN'
			// #endif
			return 'UNKNOWN'
		},
		/**
		 * 用屏内 banner 暴露播放链路关键变量，开启 traceVerbose 后即可在测试机直观看到。
		 * 上线版不会出现（依赖 appSettings.debug）。
		 */
		diagText() {
			if (!appSettings.debug) return ''
			if (!this.lastDetailSnap) return `LIVE ROOM=${this.roomKey || '-'} phase=${this.sessionPhase}`
			const s = this.lastDetailSnap
			const hasSrc = s.src ? 'Y' : 'N'
			const url = String(s.src || '')
			// 展示头 + 尾，方便同时看到协议和文件后缀
			const urlBrief = url.length > 80
				? `${url.slice(0, 32)}...${url.slice(-36)}`
				: url
			const head = `LIVE id=${this.roomKey} state=${s.state} phase=${s.phase} src=${hasSrc}`
			const tail = s.src ? ` | ${urlBrief}` : ` | liveurl=${String(s.liveurl || '').slice(-60)}`
			return head + tail
		},
		/** 打赏面板用的直播间数字 ID（与 sendgift 的 id 一致） */
		giftLiveId() {
			const n = Number(this.session.id || this.roomKey || 0)
			return Number.isFinite(n) ? n : 0
		},
		livePayPopupTitle() {
			const paidAmount = this.session.paid_money || 0
			if (this.copy.payDiamonds) {
				return this.copy.payDiamonds.replace('{amount}', paidAmount)
			}
			return String(paidAmount)
		},
		livePayPopupSubtitle() {
			const paidAmount = this.session.paid_money || 0
			if (this.copy.confirmUnlockContent) {
				return this.copy.confirmUnlockContent.replace('{amount}', paidAmount)
			}
			return this.copy.unlockFullLive || ''
		}
	},
	onLoad(query) {
		this.initLanguageStrings()
		this.roomKey = query.id || query.roomId || ''
		const entryUserNo = (query.user_no || query.userNo || '').toString()
		this._entryMeta = {
			userNo: entryUserNo,
			isFollow: query.isFollow != null ? normalizeIsFollow(query.isFollow) : null
		}
		this.teenModeOn = !!isTeenModeEnabled()
		// #ifdef APP-NVUE
		try {
			const m = uni.requireNativePlugin('tx-svgamodel')
			if (m) {
				this._svgaModel = m
				this.svgaNativeReady = true
				console.log('[gift] tx-svgamodel ready')
			} else {
				console.warn('[gift] tx-svgamodel 返回空，全屏礼物特效不可用（需含 tx-svga 的自定义基座）')
			}
		} catch (e) {
			console.warn('[gift] tx-svgamodel 未配置，礼物 SVGA 动画将跳过', e)
		}
		// #endif
		this.bootstrapFeed()
		if (this.$store) this.$store.dispatch('im/resume').catch(() => {})
		this.enterSession()
		uni.$off('onLiveMessage', this.onSocketLive)
		uni.$on('onLiveMessage', this.onSocketLive)
		uni.$off('im:connected', this.onImConnected)
		uni.$on('im:connected', this.onImConnected)
		if ((query.autoSendGift === '1' || query.autoSendGift === 1) && !this.teenModeOn) {
			const giftId = query.giftId
			const count = query.count || 1
			if (giftId) {
				setTimeout(() => {
					this.autoSendGiftAfterRecharge(giftId, count)
				}, 1500)
			}
		}
	},
	onShow() {
		this.initLanguageStrings()
		this.teenModeOn = !!isTeenModeEnabled()
		if (this.$store) {
			this.$store.dispatch('im/resume').catch(() => {})
		}
		if (this.roomKey && this.sessionPhase === 'onair') {
			this.refreshRoomBinding().catch(() => {})
		}
		// 充值返回：自动发送待送礼物 / 刷新面板余额
		const pendingGift = uni.getStorageSync('pending_gift')
		if (pendingGift && pendingGift.liveId && Number(pendingGift.liveId) === Number(this.giftLiveId)) {
			uni.removeStorageSync('pending_gift')
			if (!this.teenModeOn) {
				setTimeout(() => {
					this.autoSendGiftAfterRecharge(pendingGift.giftId, pendingGift.count)
				}, 1500)
			}
		}
		const fromRecharge = uni.getStorageSync('from_recharge_page')
		if (fromRecharge) {
			uni.removeStorageSync('from_recharge_page')
			this.$nextTick(() => {
				if (this.$refs.giftPanel && typeof this.$refs.giftPanel.loadBalance === 'function') {
					this.$refs.giftPanel.loadBalance()
				}
			})
		}
	},
	onUnload() {
		uni.$off('onLiveMessage', this.onSocketLive)
		uni.$off('im:connected', this.onImConnected)
		this.trialClock?.stop()
		this.resetGiftUiState()
		if (this.session.liveid) {
			notifyRoomExit(resolveRoomGroupId(this.session, this.roomKey)).catch(() => {})
		}
	},
	methods: {
		onImConnected() {
			// WS 握手晚于 live/play 时，需再刷一次进房绑定，否则收不到群内 tip_done
			if (this.roomKey) {
				this.refreshRoomBinding().catch(() => {})
			}
		},
		toCdn(path) {
			if (!path) return ''
			if (/^https?:\/\//.test(path)) return path
			return `${appSettings.cdnurl}${path}`
		},
		/** 初始化语言文本 */
		initLanguageStrings() {
			let locale = ''
			try {
				locale = uni.getLocale() || 'zh-Hans'
			} catch (_) {
				locale = 'zh-Hans'
			}
			let savedLanguage = ''
			try {
				savedLanguage = uni.getStorageSync(STORAGE.language) || uni.getStorageSync('language') || ''
			} catch (_) {
				savedLanguage = ''
			}
			let currentLang = savedLanguage || locale || 'zh-Hans'
			if (currentLang === 'auto') currentLang = locale || 'zh-Hans'
			if (currentLang === 'zh-HK' || currentLang === 'zh-TW' || String(currentLang).indexOf('Hant') >= 0) {
				currentLang = 'zh-Hant'
			} else if (String(currentLang).indexOf('zh') === 0) {
				currentLang = 'zh-Hans'
			} else if (String(currentLang).indexOf('en') === 0) {
				currentLang = currentLang === 'en-SG' || currentLang === 'en-MY' ? currentLang : 'en'
			}

			const languageMap = {
				'zh-Hans': {
					paidLivePleasePay: '付费直播，请先付费',
					loading: '加载中...',
					transcoding: '转码中...',
					paidLive: '付费直播',
					freeTimeExpired: '您的免费观看时长已用完，需要付费才能继续观看。是否前往支付？',
					goToPay: '去支付',
					cancel: '关闭',
					svgaDownloadFailed: '礼物特效下载失败',
					trialEnd: '试看已结束',
					insufficientBalance: '钻石余额不足',
					rechargeToContinue: '储值后才可继续购买',
					goToRecharge: '去储值',
					confirmUnlock: '确认解锁',
					confirmUnlockContent: '确认支付 {amount} 钻石解锁观看？',
					confirm: '确认',
					unlocking: '解锁中...',
					unlockSuccess: '解锁成功',
					unlockFailed: '解锁失败',
					payDiamonds: '支付{amount}钻石',
					unlockFullLive: '解锁观看完整直播',
					noPrior: '已经是第一个',
					noNext: '没有更多了',
					skipped: '部分直播已结束，已为您跳过',
					noLiveAhead: '该方向暂无正在直播',
					teenModeLiveBlocked: '青少年模式下无法使用打赏功能',
					watching: '人在看',
					follow: '关注',
					followed: '已关注',
					offline: '直播已结束',
					preparing: '主播准备中…',
					needLogin: '请先登录',
					unlock: '解锁',
					liveRoom: '直播间',
					like: '点赞',
					saySomething: '说点什么…',
					send: '发送',
					goodsBtn: '购',
					goodsCount: '{count} 件商品',
					noGoods: '暂无商品',
					askExplain: '求讲解',
					buy: '买',
					roomParamMissing: '房间参数缺失',
					enterRoomFailed: '进入直播间失败',
					liveParamMissing: '直播间参数缺失',
					roomNotReady: '房间未就绪',
					me: '我',
					sendFailed: '发送失败',
					explainSent: '已发送求讲解',
					explainFailed: '求讲解失败',
					anchorInfoMissing: '暂无法获取主播信息',
					actionFailed: '操作失败',
					gift: '礼物',
					user: '用户',
					giftPanelLoadFailed: '礼物面板加载失败',
					giftSendSuccess: '礼物发送成功',
					giftSendFailed: '发送礼物失败'
				},
				'zh-Hant': {
					paidLivePleasePay: '付費直播，請先付費',
					loading: '加載中...',
					transcoding: '轉碼中...',
					paidLive: '付費直播',
					freeTimeExpired: '您的免費觀看時長已用完，需要付費才能繼續觀看。是否前往支付？',
					goToPay: '去支付',
					cancel: '關閉',
					svgaDownloadFailed: '禮物特效下載失敗',
					trialEnd: '試看已結束',
					insufficientBalance: '鑽石餘額不足',
					rechargeToContinue: '儲值後才可繼續購買',
					goToRecharge: '去儲值',
					confirmUnlock: '確認解鎖',
					confirmUnlockContent: '確認支付 {amount} 鑽石解鎖觀看？',
					confirm: '確認',
					unlocking: '解鎖中...',
					unlockSuccess: '解鎖成功',
					unlockFailed: '解鎖失敗',
					payDiamonds: '支付{amount}鑽石',
					unlockFullLive: '解鎖觀看完整直播',
					noPrior: '已經是第一個',
					noNext: '沒有更多了',
					skipped: '部分直播已結束，已為您跳過',
					noLiveAhead: '該方向暫無正在直播',
					teenModeLiveBlocked: '青少年模式下無法使用打賞功能',
					watching: '人在看',
					follow: '關注',
					followed: '已關注',
					offline: '直播已結束',
					preparing: '主播準備中…',
					needLogin: '請先登入',
					unlock: '解鎖',
					liveRoom: '直播間',
					like: '點讚',
					saySomething: '說點什麼…',
					send: '發送',
					goodsBtn: '購',
					goodsCount: '{count} 件商品',
					noGoods: '暫無商品',
					askExplain: '求講解',
					buy: '買',
					roomParamMissing: '房間參數缺失',
					enterRoomFailed: '進入直播間失敗',
					liveParamMissing: '直播間參數缺失',
					roomNotReady: '房間未就緒',
					me: '我',
					sendFailed: '發送失敗',
					explainSent: '已發送求講解',
					explainFailed: '求講解失敗',
					anchorInfoMissing: '暫無法取得主播資訊',
					actionFailed: '操作失敗',
					gift: '禮物',
					user: '用戶',
					giftPanelLoadFailed: '禮物面板加載失敗',
					giftSendSuccess: '禮物發送成功',
					giftSendFailed: '發送禮物失敗'
				},
				en: {
					paidLivePleasePay: 'Paid live stream, please pay first',
					loading: 'Loading...',
					transcoding: 'Transcoding...',
					paidLive: 'Paid Live Stream',
					freeTimeExpired: 'Your free viewing time has expired. Payment is required to continue watching. Go to payment?',
					goToPay: 'Pay',
					cancel: 'Close',
					svgaDownloadFailed: 'Gift effect download failed',
					trialEnd: 'Trial Ended',
					insufficientBalance: 'Insufficient Diamond Balance',
					rechargeToContinue: 'Please top up to continue',
					goToRecharge: 'Top Up',
					confirmUnlock: 'Confirm Unlock',
					confirmUnlockContent: 'Pay {amount} diamonds to unlock?',
					confirm: 'Confirm',
					unlocking: 'Unlocking...',
					unlockSuccess: 'Unlocked',
					unlockFailed: 'Unlock failed',
					payDiamonds: 'Pay {amount} Diamonds',
					unlockFullLive: 'Unlock full live stream',
					noPrior: 'This is the first live',
					noNext: 'No more lives',
					skipped: 'Some streams ended; skipped ahead',
					noLiveAhead: 'No live stream in this direction',
					teenModeLiveBlocked: 'Tipping is disabled in Teen Mode',
					watching: 'watching',
					follow: 'Follow',
					followed: 'Following',
					offline: 'Live ended',
					preparing: 'Host is preparing…',
					needLogin: 'Please sign in first',
					unlock: 'Unlock',
					liveRoom: 'Live room',
					like: 'Like',
					saySomething: 'Say something…',
					send: 'Send',
					goodsBtn: 'Shop',
					goodsCount: '{count} items',
					noGoods: 'No products',
					askExplain: 'Ask to explain',
					buy: 'Buy',
					roomParamMissing: 'Room id missing',
					enterRoomFailed: 'Failed to enter live room',
					liveParamMissing: 'Live room params missing',
					roomNotReady: 'Room not ready',
					me: 'Me',
					sendFailed: 'Send failed',
					explainSent: 'Explain request sent',
					explainFailed: 'Explain request failed',
					anchorInfoMissing: 'Host info unavailable',
					actionFailed: 'Action failed',
					gift: 'Gift',
					user: 'User',
					giftPanelLoadFailed: 'Failed to load gift panel',
					giftSendSuccess: 'Gift sent',
					giftSendFailed: 'Failed to send gift'
				}
			}
			languageMap['en-SG'] = languageMap.en
			languageMap['en-MY'] = languageMap.en

			const texts = languageMap[currentLang] || languageMap['zh-Hans']
			this.copy = { ...texts }
		},
		async bootstrapFeed() {
			const cached = readRoomQueue(this.roomKey)
			if (cached.ids.length) {
				this.feedIds = cached.ids
				this.feedCovers = cached.thumbs
				this.feedCursor = cached.index
				if (cached.meta && typeof cached.meta === 'object') {
					this._entryMeta = {
						...cached.meta,
						...this._entryMeta
					}
					if (this._entryMeta.isFollow != null) {
						this._entryMeta.isFollow = normalizeIsFollow(this._entryMeta.isFollow)
					}
				}
				this.seedPosterFromFeed()
				return
			}
			try {
				const rows = await pullBroadcastCatalog()
				const pairs = (rows || [])
					.map((row) => ({
						id: String(row.id || row.liveid || ''),
						cover: row.thumb || row.image || ''
					}))
					.filter((p) => p.id)
				if (!pairs.length) return
				this.feedIds = pairs.map((p) => p.id)
				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' })
				return
			}
			this.sessionPhase = 'loading'
			this.paywallOn = false
			this.paywallCoverReady = false
			this.seedPosterFromFeed()
			// 与详情并行拉取试看秒数，付费房据此决定锁屏 / 开播（-1=无限）
			const trialPromise = pullTrialSeconds(this.roomKey).catch((err) => {
				if (appSettings.debug) console.warn('[live] pullTrialSeconds failed', err)
				return null
			})
			let detail = null
			try {
				detail = await pullBroadcastDetail(this.roomKey)
			} catch (err) {
				console.error('=== [LIVE-DIAG] pullBroadcastDetail FAILED ===', err && (err.message || err))
				this.sessionPhase = 'offline'
				uni.showToast({ title: this.copy.enterRoomFailed, icon: 'none' })
				return
			}
			// === 关键诊断（请把整段日志贴给开发者） ===
			console.log('=== [LIVE-DIAG] detail begin ===')
			try {
				console.log('[LIVE-DIAG] roomKey =', this.roomKey)
				console.log('[LIVE-DIAG] detail object =', detail)
				console.log('[LIVE-DIAG] detail JSON =', JSON.stringify(detail))
				console.log('[LIVE-DIAG] detail keys =', detail && Object.keys(detail))
				console.log('[LIVE-DIAG] state =', detail && detail.state, ' typeof =', detail && typeof detail.state)
				console.log('[LIVE-DIAG] liveurl =', detail && detail.liveurl)
				console.log('[LIVE-DIAG] recordurl =', detail && detail.recordurl)
				console.log('[LIVE-DIAG] liveid =', detail && detail.liveid)
				console.log('[LIVE-DIAG] paid_money =', detail && detail.paid_money)
			} catch (logErr) {
				console.warn('[LIVE-DIAG] log failed', logErr)
			}
			console.log('=== [LIVE-DIAG] detail end ===')

			this.session = detail || {}
			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()
			if (entryNo && !pickAnchorFollowId(this.session)) {
				this.$set(this.session, 'user_no', entryNo)
			}
			this.applyFollowState(detail)
			this.metrics.like = Number(detail.like) || 0
			this.metrics.online = Number(detail.online) || 1
			this.sessionPhase = describeRoomPhase(detail.state)
			const url = resolvePlaybackUrl(detail)
			console.log('=== [LIVE-DIAG] picked stream url ===')
			console.log('[LIVE-DIAG] resolved url =', url)
			console.log('[LIVE-DIAG] url length =', url ? url.length : 0)
			console.log('[LIVE-DIAG] sessionPhase =', this.sessionPhase)
			console.log('[LIVE-DIAG] platform =', this.platformLabel)

			this.streamArchive = url
			// 先不拉流：等 getLastTime，避免已解锁闪锁 / 未解锁先裸播
			this.streamSrc = ''
			this.paywallOn = false
			this.playerRenderToken += 1
			this.lastDetailSnap = {
				state: detail && detail.state,
				liveurl: detail && detail.liveurl,
				recordurl: detail && detail.recordurl,
				phase: this.sessionPhase,
				src: this.streamSrc
			}
			console.log('=== [LIVE-DIAG] banner snap ===', this.lastDetailSnap)

			const paidAmount = Number(detail && detail.paid_money) || 0
			try {
				const sec = await trialPromise
				console.log('[LIVE-DIAG] getLastTime last_time =', sec, ' paid_money =', paidAmount)
				this.applyTrialSeconds(sec, { paidAmount })
			} catch (err) {
				console.warn('[LIVE-DIAG] applyTrialSeconds failed', err)
				if (paidAmount > 0 && !this.unlocked) {
					this.triggerPaywall()
				} else {
					this.releasePaywallForTrial()
				}
			}
			console.log(
				'=== [LIVE-DIAG] shouldRenderPlayer =',
				this.shouldRenderPlayer,
				' paywallOn =',
				this.paywallOn,
				' streamSrc=',
				!!this.streamSrc
			)

			this.resetGiftUiState()
			this.$nextTick(() => {
				this.$refs.chatPanel?.reset?.()
			})
			// 进房后再绑一次：避免 WS 尚未 shake 完成导致未加入直播 group，别人送礼收不到 tip_done
			if (this.$store && this.$store.state && this.$store.state.im && this.$store.state.im.online) {
				setTimeout(() => {
					this.refreshRoomBinding().catch(() => {})
				}, 400)
			}
		},
		async refreshRoomBinding() {
			if (!this.roomKey || this._broadcastEndHandled) return
			try {
				const detail = await pullBroadcastDetail(this.roomKey)
				if (!detail) return
				const prevPhase = this.sessionPhase
				const nextPhase = describeRoomPhase(detail.state)
				const nextUrl = resolvePlaybackUrl(detail)
				this.session = { ...this.session, ...detail }
				if (!Array.isArray(this.session.goods)) {
					this.$set(this.session, 'goods', [])
				}
				this.applyFollowState(detail)
				if (detail.like != null) this.metrics.like = Number(detail.like) || this.metrics.like
				if (detail.online != null) this.metrics.online = Number(detail.online) || this.metrics.online
				console.log('[gift] room bind refreshed', {
					roomKey: this.roomKey,
					liveid: this.session && this.session.liveid,
					groupCandidates: collectRoomGroupCandidates(this.session, this.roomKey)
				})
				// 兜底：未收到 WS end 但接口已变为非直播中且无回放地址
				if (
					prevPhase === 'onair' &&
					nextPhase !== 'onair' &&
					(nextPhase !== 'replay' || !nextUrl)
				) {
					this.handleBroadcastEnded()
				}
			} catch (_) {}
		},
		async setupTrial() {
			if (this.unlocked) {
				this.releasePaywallForTrial()
				return
			}
			let sec
			try {
				sec = await pullTrialSeconds(this.roomKey)
			} catch (err) {
				if (appSettings.debug) console.warn('[live] pullTrialSeconds failed', err)
				sec = null
			}
			this.applyTrialSeconds(sec)
		},
		/**
		 * 按 getLastTime 结果处理：
		 *   -1 => 无限播放（免费或已解锁）
		 *   >0 => 试看倒计时
		 *   <=0 => 付费锁屏
		 */
		applyTrialSeconds(sec, opts = {}) {
			const paidAmount =
				opts.paidAmount != null
					? Number(opts.paidAmount) || 0
					: Number(this.session && this.session.paid_money) || 0

			if (this.unlocked) {
				this.releasePaywallForTrial()
				return
			}

			let n = sec
			if (n !== null && n !== undefined && n !== '') {
				n = Number(n)
				if (Number.isNaN(n)) n = null
			} else {
				n = null
			}

			this.trialClock?.stop()

			// -1：无限播放
			if (n === -1) {
				this.unlocked = true
				this.releasePaywallForTrial()
				return
			}

			if (n == null) {
				if (paidAmount > 0) {
					this.triggerPaywall()
				} else {
					this.releasePaywallForTrial()
				}
				return
			}

			this.trialClock = new TrialMeter(() => this.triggerPaywall())
			if (n <= 0) {
				this.triggerPaywall()
				return
			}
			// 仍有试看
			this.releasePaywallForTrial()
			this.trialClock.start(n)
		},
		/** 关锁屏并恢复拉流（强制 remount，避免 nvue 空 src 挂载后黑屏） */
		releasePaywallForTrial() {
			this.paywallOn = false
			const url = this.streamArchive || resolvePlaybackUrl(this.session) || ''
			this.streamSrc = url
			this.playerRenderToken += 1
			if (!url) {
				console.warn('[LIVE-DIAG] releasePaywallForTrial: empty stream url')
			}
		},
		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()) {
					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
			// iOS nvue 自定义弹层点击/叠层不稳定，确认支付改用系统弹窗（与 H5 流程一致：确认 → 验余额 → unlocklive）
			uni.showModal({
				title: this.livePayPopupTitle || '解锁观看',
				content: this.livePayPopupSubtitle || '',
				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() {
			this.showLivePayPopup = false
		},
		handleLivePayPopupConfirm() {
			this.closeLivePayPopup()
			this.checkLiveBalanceAndPay()
		},
		async checkLiveBalanceAndPay() {
			try {
				const balance = await fetchDiamondBalance()
				const paidAmount = Number(this.session.paid_money || 0) || 0
				if (balance < paidAmount) {
					// #ifdef APP-PLUS
					uni.showModal({
						title: this.copy.insufficientBalance || '钻石余额不足',
						content: this.copy.rechargeToContinue || '储值后才可继续购买',
						confirmText: this.copy.goToRecharge || '去储值',
						cancelText: this.copy.cancel || '关闭',
						success: (res) => {
							if (res.confirm) this.goToLiveRecharge()
						}
					})
					return
					// #endif
					this.showInsufficientBalanceModal = true
					return
				}
				await this.unlockLive()
			} catch (error) {
				console.error('[live] checkLiveBalanceAndPay failed', error)
				// #ifdef APP-PLUS
				uni.showModal({
					title: this.copy.insufficientBalance || '钻石余额不足',
					content: this.copy.rechargeToContinue || '储值后才可继续购买',
					confirmText: this.copy.goToRecharge || '去储值',
					cancelText: this.copy.cancel || '关闭',
					success: (res) => {
						if (res.confirm) this.goToLiveRecharge()
					}
				})
				return
				// #endif
				this.showInsufficientBalanceModal = true
			}
		},
		closeInsufficientBalanceModal() {
			this.showInsufficientBalanceModal = false
		},
		goToLiveRecharge() {
			this.closeInsufficientBalanceModal()
			uni.setStorageSync('from_recharge_page', true)
			uni.navigateTo({
				url: '/pages/user/money/diamond-top-up'
			})
		},
		async unlockLive() {
			if (this.unlockLiveBusy) return
			const liveId = this.session.id || this.roomKey
			if (!liveId) {
				uni.showToast({ title: this.copy.liveParamMissing, icon: 'none' })
				return
			}
			this.unlockLiveBusy = true
			uni.showLoading({ title: this.copy.unlocking || '解锁中...' })
			try {
				await unlockLiveRoom(liveId)
				uni.hideLoading()
				uni.showToast({
					title: this.copy.unlockSuccess || '解锁成功',
					icon: 'success',
					duration: 1500
				})
				this.resumeAfterUnlock()
			} catch (error) {
				uni.hideLoading()
				console.error('[live] unlockLive failed', error)
				uni.showToast({
					title: (error && error.message) || this.copy.unlockFailed || '解锁失败',
					icon: 'none'
				})
			} finally {
				this.unlockLiveBusy = false
			}
		},
		/** 解锁成功后恢复播放（本工程不整页 redirect） */
		resumeAfterUnlock() {
					this.unlocked = true
					this.paywallOn = false
			this.showLivePayPopup = false
			this.showInsufficientBalanceModal = false
			this.trialClock?.stop()
			const url = this.streamArchive || resolvePlaybackUrl(this.session)
			this.streamSrc = url || ''
			this.playerRenderToken += 1
					this.sessionPhase = describeRoomPhase(this.session.state)
		},
		/** @deprecated 保留旧入口名，避免残留引用 */
		confirmUnlock() {
			this.openLivePayPopup()
		},
		/** 三屏 pager 无动画回到中间位 */
		recenterPager() {
			this._pagerRecentering = true
			this.pagerAnimDuration = 0
			this.pagerSlot = 1
			this.$nextTick(() => {
				setTimeout(() => {
					this.pagerAnimDuration = 300
					this._pagerRecentering = false
				}, 50)
			})
		},
		parsePagerIndex(e) {
			const d = (e && e.detail) || {}
			let idx = d.current
			if (idx === undefined && e && e.target && e.target.current !== undefined) {
				idx = e.target.current
			}
			if (idx === undefined && e && e.current !== undefined) {
				idx = e.current
			}
			return idx
		},
		onPagerSlotChange(e) {
			if (this._pagerRecentering || this._pagerSwitching) return
			const slot = this.parsePagerIndex(e)
			if (slot === undefined || slot === null) return
			if (!this.pagerEnabled) return
			if (this.paywallOn) {
				this.recenterPager()
				return
			}
			if (Number(slot) === 1) return
			if (Number(slot) === 2) {
				if (this.feedCursor >= this.feedIds.length - 1) {
					uni.showToast({ title: this.copy.noLiveAhead, icon: 'none' })
					this.recenterPager()
					return
				}
				this.jumpToFeedIndex(this.feedCursor + 1)
				return
			}
			if (Number(slot) === 0) {
				if (this.feedCursor <= 0) {
					uni.showToast({ title: this.copy.noPrior, icon: 'none' })
					this.recenterPager()
					return
				}
				this.jumpToFeedIndex(this.feedCursor - 1)
			}
		},
		async jumpToFeedIndex(requestedIndex) {
			if (this._pagerSwitching) return
			const dir = requestedIndex > this.feedCursor ? 1 : -1
			const located = await locateBroadcastInDirection(this.feedIds, requestedIndex, dir)
			if (!located) {
				uni.showToast({ title: this.copy.noLiveAhead, icon: 'none' })
				this.recenterPager()
				return
			}
			if (located.skipped) {
				uni.showToast({ title: this.copy.skipped, icon: 'none' })
			}
			this._pagerSwitching = true
			try {
				if (this.session.liveid) {
					await notifyRoomExit(resolveRoomGroupId(this.session, this.roomKey)).catch(() => {})
				}
				this._broadcastEndHandled = false
				this.trialClock?.stop()
				this.unlocked = false
				this.paywallOn = false
				this.streamSrc = ''
				this.streamArchive = ''
				this.playerRenderToken += 1
				this.resetGiftUiState()
				if (this.session && typeof this.session === 'object') {
					this.session.state = 0
					this.session.liveid = ''
					this.session.group = ''
				}
				this.roomKey = located.id
				this.feedCursor = located.index
				const swipeCached = readRoomQueue(this.roomKey)
				this._entryMeta = (swipeCached.meta && typeof swipeCached.meta === 'object')
					? { ...swipeCached.meta }
					: {}
				saveRoomQueue(this.feedIds, this.feedCovers, located.index)
				await this.enterSession()
				this.recenterPager()
			} catch (_) {
				uni.showToast({ title: this.copy.loading, icon: 'none' })
				this.recenterPager()
			} finally {
				this._pagerSwitching = false
			}
		},
		onSocketLive(msg) {
			if (!msg || typeof msg !== 'object') return
			// 兼容 message 为 JSON 字符串
			if (typeof msg.message === 'string') {
				try {
					const parsed = JSON.parse(msg.message)
					if (parsed && typeof parsed === 'object') {
						msg = { ...msg, message: parsed }
					}
				} catch (_) {}
			}
			const sameRoom = groupMatchesRoom(msg, this.session, this.roomKey)
			const innerType = (msg.message && typeof msg.message === 'object') ? msg.message.type : ''
			const outerType = msg.type
			if (appSettings.debug || outerType === 'tip_done' || innerType === 'tip_done' || innerType === 'gift') {
				console.log('[gift] onLiveMessage', {
					outerType,
					innerType,
					group: msg.group,
					sameRoom,
					candidates: collectRoomGroupCandidates(this.session, this.roomKey)
				})
			}
			// 主播下播：优先处理 message.type === 'end' → redirectTo 结束页
			if (sameRoom && isBroadcastEndedSignal(msg)) {
				this.handleBroadcastEnded()
				return
			}
			// 购物袋同步（message.type === 'update'）
			if (sameRoom && this.applyLiveGoodsUpdate(msg)) {
				if (msg.online != null) this.metrics.online = msg.online
				if (msg.like != null) this.metrics.like = msg.like
				return
			}
			// 礼物 tip_done：即使 group 一时对不上也尝试（无 group 时 groupMatchesRoom 已放行）
			if (this.handleTipDoneGift(msg, sameRoom)) {
				if (msg.online != null) this.metrics.online = msg.online
				if (msg.like != null) this.metrics.like = msg.like
				return
			}
			if (!sameRoom) {
				if (msg.online != null) this.metrics.online = msg.online
				if (msg.like != null) this.metrics.like = msg.like
				return
			}
			if (this.sessionPhase === 'onair' && !this.paywallOn) {
				this.$nextTick(() => {
					this.$refs.chatPanel && this.$refs.chatPanel.onMessage && this.$refs.chatPanel.onMessage(msg)
				})
			}
			if (msg.online != null) this.metrics.online = msg.online
			if (msg.like != null) this.metrics.like = msg.like
		},
		handleBroadcastEnded() {
			if (this._broadcastEndHandled) return
			this._broadcastEndHandled = true
			this.sessionPhase = 'offline'
			this.streamSrc = ''
			this.streamArchive = ''
			this.paywallOn = false
			this.playerRenderToken += 1
			this.trialClock?.stop()
			if (this.session && typeof this.session === 'object') {
				this.session.state = 0
			}
			const roomId = String(this.roomKey || this.session.id || '')
			const groupId = resolveRoomGroupId(this.session, this.roomKey)
			if (groupId) {
				notifyRoomExit(groupId).catch(() => {})
			}
			uni.$off('onLiveMessage', this.onSocketLive)
			uni.$off('im:connected', this.onImConnected)
			const endUrl = roomId ? `/pages/live/end?id=${roomId}` : '/pages/live/end'
			uni.redirectTo({
				url: endUrl,
				fail: () => {
					uni.showToast({ title: this.copy.offline, icon: 'none', duration: 2000 })
				}
			})
		},
		async submitChat() {
			const text = (this.draftMsg || '').trim()
			const groupId = resolveRoomGroupId(this.session, this.roomKey)
			if (!text) return
			if (!groupId) {
				uni.showToast({ title: this.copy.roomNotReady, icon: 'none' })
				return
			}
			if (this.$appKit && !this.$appKit.requireLogin()) return
			this.draftMsg = ''
			uni.hideKeyboard()
			try {
				await pushRoomComment(groupId, text)
				const user = (this.$store && this.$store.state.user) || {}
				this.$refs.chatPanel?.pushOutgoing?.({
					userId: user.id != null ? String(user.id) : '',
					user: user.nickname || user.username || this.copy.me,
					avatar: user.avatar || '',
					text
				})
			} catch (_) {
				uni.showToast({ title: this.copy.sendFailed, icon: 'none' })
			}
		},
		async tapLike() {
			try {
				await pushRoomLike(this.roomKey)
				this.metrics.like += 1
			} catch (_) {}
		},
		/** IM update → 刷新购物袋 */
		applyLiveGoodsUpdate(msg) {
			const inner = msg && msg.message
			if (!inner || typeof inner !== 'object') return false
			if (inner.type !== 'update') return false
			const rows = Array.isArray(inner.data) ? inner.data : []
			this.$set(this.session, 'goods', rows)
			return true
		},
		openGoodsSheet() {
			this.goodsSheetVisible = true
		},
		closeGoodsSheet() {
			this.goodsSheetVisible = false
		},
		goodsCover(item) {
			return this.toCdn((item && (item.image || item.image_url || item.cover)) || '')
		},
		async requestGoodsExplain(index) {
			const groupId = resolveRoomGroupId(this.session, this.roomKey)
			if (!groupId) {
				uni.showToast({ title: this.copy.roomNotReady, icon: 'none' })
				return
			}
			if (this.$appKit && !this.$appKit.requireLogin()) return
			try {
				await requestLiveGoodsExplain(groupId, Number(index) + 1)
				this.closeGoodsSheet()
				uni.showToast({ title: this.copy.explainSent, icon: 'none' })
			} catch (_) {
				uni.showToast({ title: this.copy.explainFailed, icon: 'none' })
			}
		},
		openGoodsDetail(item) {
			const id = item && item.id
			if (id == null || id === '') return
			this.closeGoodsSheet()
			uni.navigateTo({
				url: `/pages/product/goods?id=${id}`
			})
		},
		applyFollowState(detail) {
			const fromApi = pickIsFollowFromDetail(detail)
			if (fromApi !== null) {
				this.$set(this.session, 'isFollow', fromApi)
				return
			}
			const cached = this._entryMeta && this._entryMeta.isFollow
			if (cached !== null && cached !== undefined) {
				this.$set(this.session, 'isFollow', normalizeIsFollow(cached))
			}
		},
		
		async tapFollow() {
			if (this._followBusy) return
			if (this.$appKit && !this.$appKit.requireLogin()) return

			// 优先详情字段；大厅传入的 user_no 作兜底（play 接口常缺 user_no）
			const userNo =
				pickAnchorFollowId(this.session) ||
				String((this._entryMeta && this._entryMeta.userNo) || '').trim()
			if (!userNo) {
				uni.showToast({ title: this.copy.anchorInfoMissing, icon: 'none' })
				return
			}

			this._followBusy = true
			const wasFollowed = this.anchorFollowed
			const nextFollowed = !wasFollowed
			// 乐观更新（同 reel onFollowTap）
			this.$set(this.session, 'isFollow', nextFollowed ? 1 : 0)

			try {
				// POST /wanlshop/find/user/setFindUser  { id: user_no, type: 'follow' }
				const flag = await toggleClipFollow(userNo)
				let finalFollowed = nextFollowed
				if (flag === 1 || flag === 0 || flag === '1' || flag === '0') {
					const serverFollowed = flag === 1 || flag === '1'
					if (serverFollowed !== nextFollowed) {
						this.$set(this.session, 'isFollow', serverFollowed ? 1 : 0)
					}
					finalFollowed = serverFollowed
				}
				const followFlag = finalFollowed ? 1 : 0
				patchRoomMeta(this.roomKey, { isFollow: followFlag, userNo })
				this._entryMeta = { ...(this._entryMeta || {}), isFollow: followFlag, userNo }
			} catch (_) {
				this.$set(this.session, 'isFollow', wasFollowed ? 1 : 0)
				uni.showToast({ title: this.copy.actionFailed, icon: 'none' })
			} finally {
				this._followBusy = false
			}
		},
		exitRoom() {
			uni.navigateBack({
				fail: () => uni.switchTab({ url: '/pages/index/index' })
			})
		},
		resetGiftUiState() {
			this.giftQueue = []
			this.isPlayingGift = false
			this.displayGiftList = []
			this.currentPlayingGiftId = null
			this.lastTipFingerprints = []
			this.currentGift = {
				type: 'default',
				image: '',
				text: '',
				svgaUrl: '',
				show: false
			}
			if (this.giftAnimationTimeout) {
				clearTimeout(this.giftAnimationTimeout)
				this.giftAnimationTimeout = null
			}
		},
		/**
		 * 处理 tip_done / gift 礼物消息。返回 true 表示已消费该消息。
		 * 发送端与观看端统一只走 WebSocket 入队，避免重复显示。
		 * @param {boolean} [sameRoom=true] 是否同房；异房只消费不展示
		 */
		handleTipDoneGift(msg, sameRoom = true) {
			if (!msg) return false
			let messageObj = (msg.message && typeof msg.message === 'object') ? msg.message : null
			const outerType = msg.type
			const innerType = messageObj ? messageObj.type : ''
			const isTipDone =
				outerType === 'tip_done' ||
				innerType === 'tip_done' ||
				innerType === 'gift'
			if (!isTipDone) return false

			if (!sameRoom) {
				console.warn('[gift] tip_done ignored (group mismatch)', {
					group: msg.group,
					candidates: collectRoomGroupCandidates(this.session, this.roomKey)
				})
				return true
			}

			// 旧文档格式：message.type=gift + message.data
			const giftPayload =
				(messageObj && messageObj.type === 'gift' && messageObj.data && typeof messageObj.data === 'object')
					? messageObj.data
					: null

			const messageText =
				(messageObj && (messageObj.msg || messageObj.message) && typeof (messageObj.msg || messageObj.message) !== 'object')
					? String(messageObj.msg || messageObj.message)
					: (typeof msg.message === 'string' ? msg.message : '')
			const fileUrl =
				(giftPayload && (giftPayload.file_url || giftPayload.svga)) ? (giftPayload.file_url || giftPayload.svga)
					: (messageObj && messageObj.file_url) ? messageObj.file_url
						: (msg && msg.file_url) ? msg.file_url
							: ''
			const giftId =
				(giftPayload && (giftPayload.id || giftPayload.gift_id)) ? (giftPayload.id || giftPayload.gift_id)
					: (messageObj && (messageObj.id || messageObj.gift_id)) ? (messageObj.id || messageObj.gift_id)
						: (msg && msg.gift_id) ? msg.gift_id
							: 0
			const isSvga =
				(messageObj && messageObj.is_svga !== undefined && messageObj.is_svga !== null) ? Number(messageObj.is_svga)
					: (msg && msg.is_svga !== undefined && msg.is_svga !== null) ? Number(msg.is_svga)
						: 0

			let giftName =
				(giftPayload && giftPayload.gift_name) ? String(giftPayload.gift_name)
					: (msg && msg.gift_name) ? String(msg.gift_name)
						: (messageObj && messageObj.gift_name) ? String(messageObj.gift_name)
							: (this.copy.gift || '')
			if (giftName === (this.copy.gift || '') && messageText) {
				const match = messageText.match(/tipped a (.+)$/i)
				if (match && match[1]) giftName = match[1].trim()
			}
			const giftImage =
				(giftPayload && (giftPayload.gift_image || giftPayload.image)) ? (giftPayload.gift_image || giftPayload.image)
					: (msg && msg.gift_image) ? msg.gift_image
						: (messageObj && messageObj.image) ? messageObj.image
							: ''
			const giftCount =
				(giftPayload && giftPayload.count != null) ? Number(giftPayload.count)
					: (messageObj && messageObj.number !== undefined && messageObj.number !== null) ? Number(messageObj.number)
						: (messageObj && (messageObj.count !== undefined || messageObj.num !== undefined)) ? Number(messageObj.count || messageObj.num)
							: (msg && msg.count !== undefined && msg.count !== null) ? Number(msg.count)
								: 1

			const msgGroup = msg.group || msg.liveid || ''
			const tipFp = `${msgGroup}|${(msg.form && msg.form.id) || ''}|${giftId}|${giftCount}|${giftName}|${String(giftImage || '').substring(0, 80)}`
			const now = Date.now()
			const recent = (this.lastTipFingerprints || []).filter((e) => now - e.t < 800)
			if (recent.some((e) => e.fp === tipFp)) {
				return true
			}
			this.lastTipFingerprints = recent.concat([{ fp: tipFp, t: now }]).slice(-30)

			const giftData = {
				gift_id: giftId,
				gift_name: giftName,
				gift_image: giftImage,
				gift_type: (giftPayload && giftPayload.gift_type) || msg.gift_type || 'default',
				count: giftCount,
				user_name:
					(giftPayload && giftPayload.user_name) ||
					(msg.form && msg.form.nickname) ||
					this.copy.user,
				file_url: fileUrl,
				svga: fileUrl
			}
			// is_svga==1 或无 file_url → 只飘条；否则入队播特效
			const shouldPlayAnimation =
				isSvga !== 1 && fileUrl !== null && fileUrl !== undefined && String(fileUrl).trim() !== ''
			console.log('[gift] tip_done', {
				giftId,
				giftName,
				giftCount,
				isSvga,
				fileUrl,
				shouldPlayAnimation,
				svgaNativeReady: this.svgaNativeReady
			})
			if (shouldPlayAnimation) {
				const baseGiftForQueue = { ...giftData, count: 1 }
				this.addGiftToQueue(baseGiftForQueue, { displayDelta: giftCount })
				for (let i = 1; i < giftCount; i++) {
					this.addGiftToQueue(baseGiftForQueue, { displayDelta: 0 })
				}
			} else {
				this.addGiftToDisplayOnly(giftData, giftCount)
			}
			return true
		},
		async openGiftPanel() {
			if (this.teenModeOn) {
				uni.showToast({ title: this.copy.teenModeLiveBlocked, icon: 'none' })
				return
			}
			if (this.$appKit && !this.$appKit.requireLogin()) return
			if (!this.giftLiveId) {
				uni.showToast({ title: this.copy.roomNotReady, icon: 'none' })
				return
			}
			if (!this.giftPanelMounted) {
				this.giftPanelMounted = true
				await this.$nextTick()
				// nvue 嵌套 vue 组件需要多等一帧再取 ref
				await new Promise((resolve) => setTimeout(resolve, 50))
			}
			try {
				if (this.$refs.giftPanel && typeof this.$refs.giftPanel.loadBalance === 'function') {
					this.$refs.giftPanel.loadBalance()
				}
			} catch (_) {}
			if (this.$refs.giftPanel && typeof this.$refs.giftPanel.open === 'function') {
				this.$refs.giftPanel.open()
			} else {
				uni.showToast({ title: this.copy.giftPanelLoadFailed, icon: 'none' })
			}
		},
		async autoSendGiftAfterRecharge(giftId, count) {
			if (this.teenModeOn) return
			try {
				await this.$nextTick()
				await new Promise((resolve) => setTimeout(resolve, 1000))
				await this.sendGiftDirectly(giftId, count)
			} catch (error) {
				console.error('自动发送礼物异常:', error)
				this.sendGiftDirectly(giftId, count)
			}
		},
		async sendGiftDirectly(giftId, count) {
			if (!this.giftLiveId || !giftId) return
			try {
				await post('/wanlshop/pay/sendgift', {
					id: Number(this.giftLiveId),
					gift_id: Number(giftId),
					number: Number(count) || 1
				})
				uni.showToast({ title: this.copy.giftSendSuccess, icon: 'none' })
				if (this.$refs.giftPanel && typeof this.$refs.giftPanel.loadBalance === 'function') {
					this.$refs.giftPanel.loadBalance()
				}
			} catch (error) {
				console.error('直接发送礼物异常:', error)
				uni.showToast({
					title: (error && error.message) ? error.message : (this.copy.giftSendFailed || ''),
					icon: 'none'
				})
			}
		},
		// 打赏发送成功：不在此处入队，统一等 tip_done
		onGiftSent() {},
		addGiftToQueue(gift, options = {}) {
			const displayDeltaRaw = options && options.displayDelta !== undefined ? options.displayDelta : undefined
			let giftData = {}
			if (gift.gift_id || gift.gift_name || gift.file_url) {
				giftData = {
					image: gift.gift_image || gift.image || '',
					name: gift.gift_name || gift.name || this.copy.gift,
					userName: gift.user_name || (gift.form && gift.form.nickname) || this.copy.user,
					svga: gift.file_url || gift.svga || gift.show_image || gift.animation || ''
				}
			} else {
				giftData = {
					image: gift.image || '',
					name: gift.name || this.copy.gift,
					userName: gift.userName || gift.user_name || this.copy.user,
					svga: gift.svga || gift.show_image || gift.animation || ''
				}
			}
			const displayDelta = typeof displayDeltaRaw === 'number' ? displayDeltaRaw : 1
			const giftKey = `${giftData.userName}__${giftData.name}__${giftData.image}`
			const existingItem = this.displayGiftList.find((item) => item && item.giftKey === giftKey && !item.isFadingOut)

			let displayItemId
			if (existingItem) {
				displayItemId = existingItem.id
				if (displayDelta > 0) {
					this.$set(existingItem, 'count', (Number(existingItem.count || 0) || 0) + displayDelta)
					this.$set(existingItem, 'remainingPlays', (Number(existingItem.remainingPlays || 0) || 0) + displayDelta)
				}
			} else {
				const initDelta = displayDelta > 0 ? displayDelta : 1
				if (this.displayGiftList.length >= 3) {
					this.displayGiftList.shift()
				}
				displayItemId = ++this.giftIdCounter
				this.displayGiftList.push({
					id: displayItemId,
					giftKey,
					giftName: giftData.name,
					userName: giftData.userName,
					giftImage: giftData.image,
					count: initDelta,
					remainingPlays: initDelta,
					isAnimating: false,
					isFadingOut: false,
					svgaUrl: giftData.svga
				})
			}
			const giftWithId = { ...gift, _displayId: displayItemId }
			this.giftQueue.push(giftWithId)
			if (!this.isPlayingGift) {
				this.processGiftQueue()
			}
		},
		addGiftToDisplayOnly(gift, giftCount) {
			let giftData = {}
			if (gift.gift_id || gift.gift_name || gift.file_url) {
				giftData = {
					image: gift.gift_image || gift.image || '',
					name: gift.gift_name || gift.name || this.copy.gift,
					count: giftCount || 1,
					userName: gift.user_name || (gift.form && gift.form.nickname) || this.copy.user,
					svga: gift.file_url || gift.svga || gift.show_image || gift.animation || ''
				}
			} else {
				giftData = {
					image: gift.image || '',
					name: gift.name || this.copy.gift,
					count: giftCount || 1,
					userName: gift.userName || gift.user_name || this.copy.user,
					svga: gift.svga || gift.show_image || gift.animation || ''
				}
			}
			const giftKey = `${giftData.userName}__${giftData.name}__${giftData.image}`
			const existingItem = this.displayGiftList.find((item) => item && item.giftKey === giftKey && !item.isFadingOut)
			if (existingItem) {
				const nextCount = (Number(existingItem.count || 0) || 0) + giftCount
				this.$set(existingItem, 'count', nextCount)
				this.$set(existingItem, 'remainingPlays', 0)
				this.$set(existingItem, 'isFadingOut', true)
				setTimeout(() => {
					const index = this.displayGiftList.findIndex((item) => item && item.id === existingItem.id)
					if (index !== -1) {
						this.displayGiftList.splice(index, 1)
						// #ifdef APP-NVUE
						this.displayGiftList = [...this.displayGiftList]
						// #endif
					}
				}, 1000)
			} else {
				if (this.displayGiftList.length >= 3) {
					this.displayGiftList.shift()
				}
				const displayItemId = ++this.giftIdCounter
				this.displayGiftList.push({
					id: displayItemId,
					giftKey,
					giftName: giftData.name,
					userName: giftData.userName,
					giftImage: giftData.image,
					count: giftCount,
					remainingPlays: 0,
					isAnimating: false,
					isFadingOut: false,
					svgaUrl: giftData.svga
				})
				setTimeout(() => {
					const index = this.displayGiftList.findIndex((item) => item && item.id === displayItemId)
					if (index !== -1) {
						const item = this.displayGiftList[index]
						this.$set(item, 'isFadingOut', true)
						setTimeout(() => {
							const removeIndex = this.displayGiftList.findIndex((i) => i && i.id === displayItemId)
							if (removeIndex !== -1) {
								this.displayGiftList.splice(removeIndex, 1)
								// #ifdef APP-NVUE
								this.displayGiftList = [...this.displayGiftList]
								// #endif
							}
						}, 1000)
					}
				}, 3000)
			}
		},
		processGiftQueue() {
			if (this.giftQueue.length === 0 || this.isPlayingGift) return
			const gift = this.giftQueue.shift()
			this.playGiftAnimation(gift)
		},
		playGiftAnimation(gift) {
			if (gift && gift._displayId) {
				this.currentPlayingGiftId = gift._displayId
				const idx = this.displayGiftList.findIndex((x) => x && x.id === gift._displayId)
				if (idx !== -1) {
					this.$set(this.displayGiftList[idx], 'isAnimating', true)
				}
			}
			this.isPlayingGift = true
			if (this.giftAnimationTimeout) {
				clearTimeout(this.giftAnimationTimeout)
				this.giftAnimationTimeout = null
			}
			this.giftAnimationTimeout = setTimeout(() => {
				this.onGiftAnimationEnd()
			}, 8000)

			let giftData = {}
			if (gift.gift_id || gift.gift_name || gift.file_url) {
				giftData = {
					type: gift.gift_type || gift.type || 'default',
					image: gift.gift_image || gift.image || '',
					name: gift.gift_name || gift.name || this.copy.gift,
					count: gift.count || 1,
					userName: gift.user_name || (gift.form && gift.form.nickname) || '',
					svga: gift.file_url || gift.svga || gift.show_image || gift.animation || ''
				}
			} else {
				giftData = {
					type: gift.type || 'default',
					image: gift.image || '',
					name: gift.name || this.copy.gift,
					count: gift.count || 1,
					svga: gift.svga || gift.show_image || gift.animation || ''
				}
			}

			let displayText = `${giftData.name}`
			if (giftData.userName) {
				displayText = `${giftData.userName} 送出了 ${displayText}`
			}

			let svgaUrl = ''
			if (giftData.svga) {
				if (/^(http|https):\/\/.+/.test(giftData.svga)) {
					svgaUrl = giftData.svga
				} else {
					svgaUrl = this.toCdn(giftData.svga)
				}
			}

			this.giftCanvasId = 'gift-svga-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9)
			this.currentGift.show = false
			this.$nextTick(() => {
				this.currentGift = {
					type: giftData.type,
					image: giftData.image,
					text: displayText,
					svgaUrl,
					show: !!svgaUrl
				}
				if (svgaUrl) {
					// #ifdef APP-NVUE
					this.playNvueSvga(svgaUrl, 0)
					// #endif
				} else {
					setTimeout(() => this.onGiftAnimationEnd(), 2000)
				}
			})
		},
		/** nvue 播放 SVGA：等待原生组件就绪并下载后播放 */
		playNvueSvga(svgaUrl, retry = 0) {
			// #ifdef APP-NVUE
			if (!this._svgaModel || !this.svgaNativeReady) {
				console.warn('[gift] tx-svgamodel 不可用，跳过全屏特效（请确认自定义基座已打包 tx-svga 插件）')
				// 仍保留飘条，2 秒后结束队列项
				setTimeout(() => this.onGiftAnimationEnd(), 2000)
				return
			}
			const player = this.$refs.giftTxSvga
			if (!player) {
				if (retry < 15) {
					setTimeout(() => this.playNvueSvga(svgaUrl, retry + 1), 80)
					return
				}
				console.warn('[gift] giftTxSvga ref 仍不可用')
				setTimeout(() => this.onGiftAnimationEnd(), 2000)
				return
			}
			const startPlay = (filePath) => {
				try {
					player.playAnimation({ filePath })
					console.log('[gift] playAnimation', filePath)
				} catch (e) {
					console.error('[gift] playAnimation failed', e)
					setTimeout(() => this.onGiftAnimationEnd(), 2000)
				}
			}
			if (typeof svgaUrl === 'string' && svgaUrl.indexOf('/static/') === 0) {
				try {
					startPlay(plus.io.convertLocalFileSystemURL(svgaUrl))
				} catch (e) {
					setTimeout(() => this.onGiftAnimationEnd(), 2000)
				}
				return
			}
			try {
				this._svgaModel.downloadUrls(svgaUrl, (result) => {
					if (result && result.filePath) {
						startPlay(result.filePath)
					} else {
						console.warn('[gift] SVGA 下载失败', result)
						uni.showToast({ title: this.copy.svgaDownloadFailed, icon: 'none' })
						setTimeout(() => this.onGiftAnimationEnd(), 2000)
					}
				})
			} catch (e) {
				console.error('[gift] downloadUrls error', e)
				setTimeout(() => this.onGiftAnimationEnd(), 2000)
			}
			// #endif
		},
		onGiftSvgaLoaded() {
			// #ifndef APP-NVUE
			this.$nextTick(() => {
				if (this.$refs.giftSvga) {
					try {
						this.$refs.giftSvga.call('startAnimation')
					} catch (error) {
						console.error('Error calling startAnimation:', error)
					}
				}
			})
			// #endif
		},
		onGiftAnimationEnd() {
			if (this.giftAnimationTimeout) {
				clearTimeout(this.giftAnimationTimeout)
				this.giftAnimationTimeout = null
			}
			// #ifndef APP-NVUE
			if (this.$refs.giftSvga) {
				try { this.$refs.giftSvga.call('stopAnimation') } catch (_) {}
			}
			// #endif
			// #ifdef APP-NVUE
			if (this.$refs.giftTxSvga) {
				try { this.$refs.giftTxSvga.cancelAnimation() } catch (_) {}
			}
			// #endif
			this.currentGift = {
				type: 'default',
				image: '',
				text: '',
				svgaUrl: '',
				show: false
			}
			const finishedDisplayId = this.currentPlayingGiftId
			if (finishedDisplayId) {
				const idx = this.displayGiftList.findIndex((x) => x && x.id === finishedDisplayId)
				if (idx !== -1) {
					const item = this.displayGiftList[idx]
					const nextRemain = Math.max((Number(item.remainingPlays || 0) || 0) - 1, 0)
					this.$set(item, 'remainingPlays', nextRemain)
					this.$set(item, 'isAnimating', false)
					if (nextRemain <= 0) {
						this.$set(item, 'isFadingOut', true)
						setTimeout(() => {
							const removeIdx = this.displayGiftList.findIndex((x) => x && x.id === finishedDisplayId)
							if (removeIdx !== -1) this.displayGiftList.splice(removeIdx, 1)
						}, 1000)
					}
				}
				this.currentPlayingGiftId = null
			}
			this.isPlayingGift = false
			setTimeout(() => {
				this.processGiftQueue()
				// 队列播完后不要立刻清掉飘条：剩余项由 remainingPlays / 淡出计时自行移除
				// （原先 clearAllDisplayItems 会在 SVGA 插件缺失时把左上角数量一并抹掉）
			}, 100)
		},
		clearAllDisplayItems() {
			const itemsToFadeOut = this.displayGiftList.filter((item) => item && !item.isFadingOut)
			itemsToFadeOut.forEach((item) => {
				this.$set(item, 'isFadingOut', true)
				setTimeout(() => {
					const idx = this.displayGiftList.findIndex((x) => x && x.id === item.id)
					if (idx !== -1) this.displayGiftList.splice(idx, 1)
				}, 1000)
			})
		},
		getGiftItemStyle(item) {
			if (item && item.isFadingOut) {
				return { opacity: 0, transform: 'translateX(-50rpx)' }
			}
			return { opacity: 1, transform: 'translateX(0)' }
		},
		onGiftTxSvgaStateChange(e) {
			const result = e.detail || e
			if (result && (result.state === 'finished' || result.state === 'end' || result.state === 'error')) {
				this.onGiftAnimationEnd()
			}
		}
	}
}
</script>

<style>
.stage {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	background-color: #101018;
}
.stage-poster {
	position: absolute;
	top: 0;
	left: 0;
}
.stage-player {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
}
.v-pager {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
}
.v-pager-cell {
	position: relative;
	background-color: #1c1e3d;
}
.v-pager-edge {
	background-color: #1c1e3d;
	justify-content: center;
	align-items: center;
}
.v-pager-edge-txt {
	color: rgba(255, 255, 255, 0.5);
	font-size: 28rpx;
	text-align: center;
}
.v-pager-neighbor {
	flex: 1;
	position: relative;
	background-color: #1c1e3d;
	justify-content: center;
	align-items: center;
}
.v-pager-cover {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
}
.v-pager-fallback {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	background-color: #1c1e3d;
}
.v-pager-hint-box {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	justify-content: center;
	align-items: center;
}
.v-pager-hint {
	color: rgba(255, 255, 255, 0.45);
	font-size: 40rpx;
}
.stage-hint {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	justify-content: center;
	align-items: center;
	background-color: rgba(0, 0, 0, 0.45);
}
.stage-hint-txt {
	color: #ffffff;
	font-size: 30rpx;
}
.stage-diag {
	position: absolute;
	left: 12rpx;
	right: 12rpx;
	background-color: rgba(255, 255, 0, 0.85);
	padding-top: 10rpx;
	padding-bottom: 10rpx;
	padding-left: 16rpx;
	padding-right: 16rpx;
	border-radius: 8rpx;
}
.stage-diag-txt {
	color: #000000;
	font-size: 22rpx;
	lines: 2;
}
.stage-paywall {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 80;
}
.stage-paywall-placeholder {
	position: absolute;
	top: 0;
	left: 0;
	background-color: #1c1e3d;
}
.stage-paywall-bg {
	position: absolute;
	top: 0;
	left: 0;
}
.stage-paywall-mask {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
}
.stage-paywall-body {
	position: absolute;
	left: 0;
	right: 0;
	bottom: 0;
	top: 0;
	align-items: center;
	justify-content: center;
	padding-bottom: 80rpx;
}
.stage-paywall-lock-img {
	width: 96rpx;
	height: 96rpx;
	margin-bottom: 24rpx;
}
.stage-paywall-title {
	color: #ffffff;
	font-size: 32rpx;
	margin-bottom: 40rpx;
}
.stage-paywall-btn {
	flex-direction: row;
	align-items: center;
	justify-content: center;
	background-color: #e6162c;
	border-radius: 50rpx;
	padding-top: 24rpx;
	padding-bottom: 24rpx;
	padding-left: 48rpx;
	padding-right: 48rpx;
	min-width: 280rpx;
}
.stage-paywall-btn-icon {
	width: 36rpx;
	height: 36rpx;
	margin-right: 12rpx;
}
.stage-paywall-btn-amount {
	color: #ffffff;
	font-size: 30rpx;
	font-weight: bold;
	margin-right: 12rpx;
}
.stage-paywall-btn-txt {
	color: #ffffff;
	font-size: 30rpx;
	font-weight: bold;
}
.stage-top {
	position: absolute;
	left: 0;
	right: 0;
	top: 0;
	flex-direction: row;
	align-items: center;
	padding-left: 24rpx;
	padding-right: 24rpx;
}
.stage-back {
	width: 64rpx;
	height: 64rpx;
	border-radius: 32rpx;
	background-color: rgba(0, 0, 0, 0.35);
	justify-content: center;
	align-items: center;
	margin-right: 16rpx;
}
.stage-back-txt {
	color: #ffffff;
	font-size: 44rpx;
	line-height: 64rpx;
	text-align: center;
}
.stage-anchor {
	flex: 1;
	flex-direction: row;
	align-items: center;
	background-color: rgba(0, 0, 0, 0.4);
	border-radius: 55rpx;
	padding: 8rpx 20rpx 8rpx 8rpx;
}
.stage-anchor-av {
	width: 110rpx;
	height: 110rpx;
	border-radius: 75rpx;
	margin-right: 12rpx;
}
.stage-anchor-meta {
	flex: 1;
}
.stage-anchor-name {
	color: #ffffff;
	font-size: 28rpx;
	font-weight: 600;
}
.stage-anchor-online {
	color: rgba(255, 255, 255, 0.75);
	font-size: 22rpx;
	margin-top: 4rpx;
}
.stage-follow-btn {
	min-width: 86rpx;
	height: 50rpx;
	padding-left: 12rpx;
	padding-right: 12rpx;
	margin-left: 16rpx;
	border-radius: 18rpx;
	background-color: #ff4400;
	justify-content: center;
	align-items: center;
	flex-shrink: 0;
}
.stage-follow-btn--on {
	background-color: rgba(0, 0, 0, 0);
	border-width: 2rpx;
	border-color: rgba(255, 255, 255, 0.85);
	border-style: solid;
}
.stage-follow-btn-txt {
	color: #ffffff;
	font-size: 24rpx;
}
.stage-follow-btn-txt--on {
	color: rgba(255, 255, 255, 0.95);
}
.stage-chat {
	position: absolute;
	left: 24rpx;
}
.stage-dock {
	position: absolute;
	left: 20rpx;
	right: 20rpx;
	bottom: 0;
}
.stage-dock-frame {
	position: relative;
	border-width: 1rpx;
	border-color: rgba(255, 255, 255, 0.24);
	border-style: solid;
	border-top-left-radius: 24rpx;
	border-top-right-radius: 24rpx;
	border-bottom-left-radius: 16rpx;
	border-bottom-right-radius: 16rpx;
	background-color: rgba(0, 0, 0, 0.82);
	/* nvue 勿 overflow:hidden，窄屏会把礼物/点赞列裁掉 */
}
.stage-dock-body {
	padding-top: 16rpx;
	padding-left: 12rpx;
	padding-right: 12rpx;
}
.stage-dock-row {
	flex-direction: row;
	align-items: center;
}
.stage-composer {
	flex: 1;
	flex-direction: row;
	align-items: center;
	height: 72rpx;
	background-color: rgba(255, 255, 255, 0.06);
	border-radius: 36rpx;
	border-width: 1rpx;
	border-color: rgba(255, 255, 255, 0.1);
	border-style: solid;
	padding-left: 20rpx;
	padding-right: 6rpx;
}
.stage-composer--disabled {
	justify-content: center;
	padding-left: 0;
	padding-right: 0;
	background-color: rgba(255, 255, 255, 0.04);
}
.stage-composer-placeholder {
	color: rgba(255, 255, 255, 0.42);
	font-size: 26rpx;
}
.stage-input {
	flex: 1;
	height: 72rpx;
	color: #ffffff;
	font-size: 28rpx;
	padding-top: 0;
	padding-bottom: 0;
	padding-left: 0;
	padding-right: 12rpx;
}
.stage-send-hit {
	height: 60rpx;
	padding-left: 22rpx;
	padding-right: 22rpx;
	border-radius: 30rpx;
	background-color: rgba(255, 86, 136, 0.94);
	justify-content: center;
	align-items: center;
	flex-shrink: 0;
}
.stage-send-txt {
	color: #ffffff;
	font-size: 24rpx;
	font-weight: 600;
	line-height: 60rpx;
}
.stage-like-col {
	width: 84rpx;
	align-items: center;
	flex-shrink: 0;
}
.stage-like-col--duo {
	width: 168rpx;
	flex-direction: row;
	align-items: center;
	justify-content: space-between;
}
.stage-like-col--trio {
	width: 252rpx;
	flex-direction: row;
	align-items: center;
	justify-content: space-between;
}
.stage-like-btn {
	width: 72rpx;
	height: 72rpx;
	border-radius: 36rpx;
	background-color: rgba(255, 255, 255, 0.06);
	border-width: 1rpx;
	border-color: rgba(255, 255, 255, 0.1);
	border-style: solid;
	justify-content: center;
	align-items: center;
}
.stage-like-icon {
	width: 48rpx;
	height: 48rpx;
}
.stage-like-num {
	margin-top: 4rpx;
	color: rgba(255, 255, 255, 0.75);
	font-size: 20rpx;
	line-height: 26rpx;
	text-align: center;
}
.stage-goods-btn {
	position: relative;
}
.stage-goods-icon {
	width: 56rpx;
	height: 56rpx;
}
.stage-goods-badge {
	position: absolute;
	top: -6rpx;
	right: -6rpx;
	min-width: 28rpx;
	height: 28rpx;
	padding-left: 6rpx;
	padding-right: 6rpx;
	border-radius: 14rpx;
	background-color: #f72b36;
	justify-content: center;
	align-items: center;
}
.stage-goods-badge-txt {
	color: #ffffff;
	font-size: 18rpx;
	line-height: 28rpx;
	text-align: center;
}
.stage-goods-mask {
	position: fixed;
	left: 0;
	right: 0;
	top: 0;
	bottom: 0;
	background-color: rgba(0, 0, 0, 0.45);
	z-index: 280;
}
.stage-goods-sheet {
	position: fixed;
	left: 0;
	bottom: 0;
	z-index: 281;
	background-color: rgba(255, 255, 255, 0.96);
	border-top-left-radius: 18rpx;
	border-top-right-radius: 18rpx;
	overflow: hidden;
}
.stage-goods-sheet-hd {
	height: 100rpx;
	padding-left: 28rpx;
	padding-right: 20rpx;
	flex-direction: row;
	align-items: center;
	justify-content: space-between;
}
.stage-goods-sheet-title {
	color: #222222;
	font-size: 32rpx;
	font-weight: 600;
}
.stage-goods-sheet-close {
	width: 64rpx;
	height: 64rpx;
	justify-content: center;
	align-items: center;
}
.stage-goods-sheet-close-txt {
	color: #666666;
	font-size: 44rpx;
	line-height: 44rpx;
}
.stage-goods-sheet-scroll {
	padding-left: 18rpx;
	padding-right: 18rpx;
}
.stage-goods-empty {
	padding-top: 80rpx;
	padding-bottom: 80rpx;
	align-items: center;
	justify-content: center;
}
.stage-goods-empty-txt {
	color: #999999;
	font-size: 28rpx;
}
.stage-goods-row {
	position: relative;
	flex-direction: row;
	background-color: #ffffff;
	padding: 18rpx;
	margin-bottom: 18rpx;
	border-radius: 18rpx;
}
.stage-goods-cover {
	width: 180rpx;
	height: 180rpx;
	border-radius: 12rpx;
	margin-right: 20rpx;
	background-color: #f7f7f7;
	flex-shrink: 0;
}
.stage-goods-rank {
	position: absolute;
	left: 18rpx;
	top: 18rpx;
	width: 40rpx;
	height: 40rpx;
	border-radius: 8rpx;
	background-color: rgba(0, 0, 0, 0.45);
	justify-content: center;
	align-items: center;
}
.stage-goods-rank--1 {
	background-color: #f72b36;
}
.stage-goods-rank--2 {
	background-color: #ff8a00;
}
.stage-goods-rank--3 {
	background-color: #ffb020;
}
.stage-goods-rank-txt {
	color: #ffffff;
	font-size: 22rpx;
	font-weight: 600;
}
.stage-goods-info {
	flex: 1;
	justify-content: space-between;
	min-width: 0;
}
.stage-goods-title {
	color: #222222;
	font-size: 28rpx;
	lines: 2;
	text-overflow: ellipsis;
}
.stage-goods-ops {
	flex-direction: row;
	align-items: center;
	justify-content: space-between;
}
.stage-goods-price {
	color: #f72b36;
	font-size: 32rpx;
	font-weight: 600;
}
.stage-goods-ops-btns {
	flex-direction: row;
	align-items: center;
}
.stage-goods-explain {
	margin-right: 18rpx;
	height: 54rpx;
	padding-left: 18rpx;
	padding-right: 18rpx;
	border-radius: 27rpx;
	border-width: 2rpx;
	border-style: solid;
	border-color: #6200ff;
	justify-content: center;
	align-items: center;
}
.stage-goods-explain-txt {
	color: #6200ff;
	font-size: 24rpx;
}
.stage-goods-buy {
	width: 54rpx;
	height: 54rpx;
	border-radius: 27rpx;
	background-color: #f72b36;
	justify-content: center;
	align-items: center;
}
.stage-goods-buy-txt {
	color: #ffffff;
	font-size: 24rpx;
	font-weight: 600;
}

/* SVGA 礼物动画容器 */
.gift-svga-container {
	position: absolute;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	z-index: 100;
	/* #ifndef APP-NVUE */
	display: flex;
	/* #endif */
	flex-direction: column;
	align-items: center;
	justify-content: center;
	pointer-events: none;
}
.gift-text-overlay {
	position: absolute;
	bottom: 200rpx;
	/* #ifndef APP-NVUE */
	display: flex;
	/* #endif */
	align-items: center;
	justify-content: center;
	pointer-events: none;
}
.gift-text {
	color: #ffffff;
	font-size: 32rpx;
	font-weight: bold;
	text-shadow: 2rpx 2rpx 4rpx rgba(0, 0, 0, 0.8);
	/* #ifndef APP-NVUE */
	display: block;
	/* #endif */
}
/* #ifdef APP-NVUE */
.gift-tx-svga {
	width: 750rpx;
	height: 100%;
}
/* #endif */
.gift-display-list {
	position: absolute;
	top: 200rpx;
	left: 20rpx;
	z-index: 90;
	/* #ifndef APP-NVUE */
	display: flex;
	/* #endif */
	flex-direction: column;
	align-items: flex-start;
	justify-content: flex-start;
	pointer-events: none;
}
.gift-display-item {
	margin-bottom: 20rpx;
	/* #ifndef APP-NVUE */
	display: flex;
	/* #endif */
	align-items: center;
	opacity: 1;
	transform: translateX(0);
	/* #ifndef APP-NVUE */
	transition: opacity 1s ease-out, transform 1s ease-out;
	/* #endif */
}
.gift-display-item-fade-out {
	opacity: 0;
	transform: translateX(-50rpx);
}
.gift-display-content {
	background-color: rgba(0, 0, 0, 0.6);
	border-radius: 50rpx;
	padding: 12rpx 24rpx;
	/* #ifndef APP-NVUE */
	display: flex;
	/* #endif */
	flex-direction: row;
	align-items: center;
	justify-content: flex-start;
}
.gift-display-name {
	color: #ffffff;
	font-size: 24rpx;
	margin-right: 12rpx;
	max-width: 200rpx;
	/* #ifndef APP-NVUE */
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
	/* #endif */
}
.gift-display-image {
	width: 48rpx;
	height: 48rpx;
	margin-right: 8rpx;
}
.gift-display-count {
	color: #ffd700;
	font-size: 24rpx;
	font-weight: bold;
}

/* #ifdef APP-NVUE */
.nvue-pay-mask {
	position: absolute;
	top: 0;
	left: 0;
	z-index: 200;
	background-color: rgba(0, 0, 0, 0.55);
	align-items: center;
	justify-content: center;
}
.nvue-pay-card {
	width: 600rpx;
	background-color: #ffffff;
	border-radius: 32rpx;
	padding-top: 48rpx;
	padding-bottom: 40rpx;
	padding-left: 40rpx;
	padding-right: 40rpx;
	align-items: center;
}
.nvue-pay-icon {
	width: 160rpx;
	height: 160rpx;
	margin-bottom: 28rpx;
}
.nvue-pay-title {
	color: #333333;
	font-size: 34rpx;
	font-weight: bold;
	text-align: center;
	margin-bottom: 16rpx;
}
.nvue-pay-sub {
	color: #666666;
	font-size: 26rpx;
	text-align: center;
	margin-bottom: 40rpx;
	lines: 3;
}
.nvue-pay-actions {
	width: 520rpx;
	flex-direction: row;
	align-items: center;
	justify-content: space-between;
}
.nvue-pay-actions--single {
	justify-content: center;
}
.nvue-pay-btn {
	height: 80rpx;
	border-radius: 40rpx;
	align-items: center;
	justify-content: center;
	padding-left: 24rpx;
	padding-right: 24rpx;
	flex: 1;
}
.nvue-pay-btn--cancel {
	background-color: #f5f5f5;
	border-width: 1rpx;
	border-style: solid;
	border-color: #cccccc;
	margin-right: 20rpx;
}
.nvue-pay-btn--ok {
	background-color: #19be6b;
}
.nvue-pay-btn--wide {
	flex: 0;
	min-width: 360rpx;
}
.nvue-pay-btn-txt {
	font-size: 30rpx;
	font-weight: 500;
}
.nvue-pay-btn-txt--cancel {
	color: #333333;
}
.nvue-pay-btn-txt--ok {
	color: #ffffff;
}
/* #endif */
</style>
