Files
91fans_h5/src/components/CoinVideoPayPopup/index.vue
T
2026-09-15 22:40:55 +07:00

502 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<van-popup
class="coin-video-pay-popup"
:show="isOpen"
position="bottom"
z-index="2002"
teleport="body"
:overlay="true"
:style="{ maxHeight: '85dvh' }"
@click-overlay="emitClose"
@closed="onPopupClosed"
>
<div class="coinVideoPayPopup">
<div class="coinVideoPayPopup__scroll">
<div class="coinVideoPayPopup__handle" />
<div class="coinVideoPayPopup__title">金币支付</div>
<div class="coinVideoPayPopup__balance">
<div class="coinVideoPayPopup__balanceLabel">我的金币余额</div>
<div class="coinVideoPayPopup__balanceRow">
<div class="coinVideoPayPopup__balanceValue">
<img class="coinVideoPayPopup__coinIcon" src="@/assets/images/png/mine/coin.png" alt="" />
<span>{{ balanceText }}</span>
</div>
<div class="coinVideoPayPopup__balanceDetail" @click.stop="goTransactionRecord">余额明细</div>
</div>
</div>
<div v-if="state.amountList.length" class="coinVideoPayPopup__grid">
<div
v-for="(item, index) in state.amountList"
:key="item.id"
class="coinVideoPayPopup__package"
:class="{ active: state.selectIndex === index }"
@click="selectAmount(index)"
>
<div v-if="item.couponDesc" class="coinVideoPayPopup__packageBadge">{{ item.couponDesc }}</div>
<img class="coinVideoPayPopup__packageIcon" src="@/assets/images/png/mine/coin.png" alt="" />
<div class="coinVideoPayPopup__packageCoin">{{ item.money / 10 }}金币</div>
<div class="coinVideoPayPopup__packagePrice">¥{{ item.money / 100 }}</div>
</div>
</div>
<Loading v-else class="coinVideoPayPopup__loading" />
<div class="coinVideoPayPopup__tips">
*如提示【交易失败】【账户风险】等,可重新发起订单,或在15分钟后重试支付。
</div>
</div>
<div class="coinVideoPayPopup__footer">
<div
class="coinVideoPayPopup__payBtn"
:class="{ 'coinVideoPayPopup__payBtn--disabled': !canClickPay }"
@click="handlePay"
>
{{ payBtnText }}
</div>
</div>
</div>
</van-popup>
<!-- 与 LimitedTimeOfferPopup 一致:独立于金币弹层挂载,避免 lazy-render 导致 ref 未就绪 / 被父层遮挡 -->
<RechargeBox
ref="rechargeBoxRef"
:is-open="state.showRecharge"
:z-index="2003"
@closePay="state.showRecharge = false"
@paySuccess="onRechargeSuccess"
/>
</template>
<script lang="ts" setup>
import { computed, nextTick, reactive, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { showToast } from 'vant';
import MineApi from '@/service/mine';
import VideoApi from '@/service/video';
import { ProductType } from '@/enums/app';
import { useUserStore } from '@/stores/user';
import useLoading from '@/hooks/loading';
const props = defineProps<{
isOpen: boolean;
videoInfo: Types.Video.VInfosItem;
/**
* 为 true 时余额足够不调 videoBuying,仅通知父组件解锁
*(短剧等非长视频商品购买场景)
*/
deferUnlock?: boolean;
/** 下单来源;短剧传 DRAMA_PAYWALL */
sourcePage?: Types.Recharge.PaySourcePage;
/** 短剧整部 ID */
mediaId?: string;
/** 短剧当前子集 ID */
contentId?: string;
}>();
/** 勿对 defineEmits 结果做 `as` 断言,否则 Vue 3.2 编译器无法剥离宏,运行时报错 */
const emit = defineEmits(['close', 'buySuccess']);
const emitClose = () => {
(emit as (e: string) => void)('close');
};
const emitBuySuccess = () => {
(emit as (e: string) => void)('buySuccess');
};
const router = useRouter();
const userStore = useUserStore();
const { openLoading, closeLoading } = useLoading();
const rechargeBoxRef = ref();
const state = reactive({
amountList: [] as Types.Recharge.ListItem[],
dcInfo: {} as Types.Recharge.Daichong,
selectIndex: 0,
showRecharge: false,
packagesLoading: false,
});
const videoPrice = computed(() => {
return userStore.isVip ? props.videoInfo.coins : props.videoInfo.originCoins;
});
const balance = computed(() => {
return ((userStore.walletInfo.amount || 0) * 100 + (userStore.walletInfo.income || 0) * 100) / 100;
});
const balanceText = computed(() => (balance.value === 0 ? '0' : balance.value.toFixed(1)));
const selectedPackage = computed(() => state.amountList[state.selectIndex]);
const canPayWithCoin = computed(() => videoPrice.value > 0 && balance.value >= videoPrice.value);
const canClickPay = computed(() => !state.packagesLoading && (canPayWithCoin.value || !!selectedPackage.value));
const payBtnText = computed(() => {
if (canPayWithCoin.value) {
return `${videoPrice.value}金币/立即支付`;
}
if (!selectedPackage.value) return '立即支付';
return `¥${selectedPackage.value.money / 100}/立即支付`;
});
const buildRechargePayData = () => {
const item = selectedPackage.value;
if (!item) return null;
return {
rechargeList: [...(item.rchgType || [])],
dcInfo: state.dcInfo,
popType: 'gold',
productType: undefined,
productID: item.id,
dcType: 0,
buyType: 1,
discountedPrice: item.money / 100,
videoId: props.mediaId || props.videoInfo.id,
mediaId: props.mediaId,
contentId: props.contentId,
sourcePage: props.sourcePage || 'VIDEO_BOTTOM_SHEET',
sourceRef: props.mediaId || props.videoInfo.id,
};
};
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
/** 异步组件 + 弹层首次打开时 ref 可能晚于 nextTick,短重试确保 changeItem 能执行 */
const syncRechargePayData = async () => {
const payData = buildRechargePayData();
if (!payData) return false;
for (let i = 0; i < 10; i += 1) {
await nextTick();
if (rechargeBoxRef.value?.changeItem) {
rechargeBoxRef.value.changeItem(payData);
return true;
}
await wait(32);
}
return false;
};
const openRechargeBox = async () => {
if (!selectedPackage.value) {
showToast('暂无可选充值档位');
return;
}
const synced = await syncRechargePayData();
if (!synced) {
showToast('支付方式加载中,请稍后再试');
return;
}
state.showRecharge = true;
};
const loadCoinPackages = async () => {
state.packagesLoading = true;
try {
const res: any = await MineApi.getGoldCoinList({ type: '1' });
state.amountList = res.list || [];
state.dcInfo = JSON.parse(JSON.stringify(res.daichong || {}));
if (state.amountList.length) {
state.selectIndex = 0;
await syncRechargePayData();
}
} catch (e) {
console.log(e);
} finally {
state.packagesLoading = false;
}
};
const selectAmount = (index: number) => {
state.selectIndex = index;
void syncRechargePayData();
};
const notifyParentUnlock = () => {
// 父组件 buySuccess 内会关弹窗,避免连续 emit 触发重复更新
emitBuySuccess();
};
const unlockWithCoin = async () => {
if (balance.value < videoPrice.value) {
if (!selectedPackage.value) {
showToast('金币余额不足,请先充值');
return;
}
await openRechargeBox();
return;
}
if (props.deferUnlock) {
notifyParentUnlock();
return;
}
const productID = props.videoInfo.id;
if (!productID) {
showToast('购买出错~');
return;
}
try {
openLoading();
await VideoApi.videoBuying({
productType: ProductType.VIDEO,
productID,
goldVideoCouponNum: 0,
});
await userStore.updateWallet();
notifyParentUnlock();
} catch (e: any) {
if (e?.toString().indexOf('8000') !== -1) {
if (selectedPackage.value) {
await openRechargeBox();
} else {
showToast('金币余额不足,请先充值');
}
}
} finally {
closeLoading();
}
};
const handlePay = async () => {
if (!canClickPay.value) {
if (state.packagesLoading) {
showToast('充值档位加载中,请稍候');
} else {
showToast('暂无可选充值档位');
}
return;
}
if (canPayWithCoin.value) {
await unlockWithCoin();
return;
}
await openRechargeBox();
};
const onRechargeSuccess = async () => {
state.showRecharge = false;
await userStore.updateWallet();
if (balance.value >= videoPrice.value) {
await unlockWithCoin();
}
};
const goTransactionRecord = () => {
emitClose();
router.push('/transactionRecord');
};
const onPopupClosed = () => {
state.showRecharge = false;
};
watch(
() => props.isOpen,
(open) => {
if (!open) {
state.showRecharge = false;
return;
}
userStore.updateWallet();
loadCoinPackages();
},
);
</script>
<style lang="scss">
.coin-video-pay-popup {
background: transparent !important;
overflow: hidden;
}
</style>
<style lang="scss" scoped>
.coinVideoPayPopup {
position: relative;
display: flex;
flex-direction: column;
max-height: 85dvh;
width: 100%;
overflow: hidden;
border-top: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 24px 24px 0 0;
/* Figma 21659:85708 弹层底色 */
background: linear-gradient(180deg, #040018 0%, #060606 49.04%, #060606 100%);
/* Figma 21659:85709 Bg-Pattern:顶部金色径向光晕 */
&::before {
content: '';
position: absolute;
left: 0;
top: -1px;
z-index: 0;
width: 100%;
height: 429px;
pointer-events: none;
background: radial-gradient(
262.5px 300.3px at 50% 21px,
rgba(250, 204, 21, 0.16) 0%,
rgba(250, 204, 21, 0) 70%
);
}
> * {
position: relative;
z-index: 1;
}
}
.coinVideoPayPopup__scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 8px 16px 16px;
-webkit-overflow-scrolling: touch;
}
.coinVideoPayPopup__handle {
width: 36px;
height: 4px;
margin: 0 auto 8px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.3);
}
.coinVideoPayPopup__title {
padding: 4px 0 16px;
font-size: 17px;
font-weight: 600;
line-height: 24px;
color: #fff;
text-align: center;
}
.coinVideoPayPopup__balance {
height: 101px;
padding: 18px 17px;
border-radius: 8px;
/* 透明/黑色/010 → #ffffff1a */
background: #ffffff1a;
}
.coinVideoPayPopup__balanceLabel {
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
line-height: 18px;
color: #fff;
}
.coinVideoPayPopup__balanceRow {
display: flex;
align-items: center;
justify-content: space-between;
}
.coinVideoPayPopup__balanceValue {
display: flex;
align-items: center;
gap: 4px;
font-size: 32px;
font-weight: 600;
line-height: 1;
color: #ffd460;
}
.coinVideoPayPopup__coinIcon {
width: 36px;
height: 36px;
}
.coinVideoPayPopup__balanceDetail {
min-width: 90px;
height: 30px;
padding: 0 24px;
border-radius: 999px;
background: #ffd460;
font-size: 14px;
line-height: 30px;
text-align: center;
color: #3d3d3d;
}
.coinVideoPayPopup__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px 5px;
margin: 16px 0;
}
.coinVideoPayPopup__package {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 130px;
margin-top: 8px;
box-sizing: border-box;
border: 1px solid transparent;
border-radius: 12px;
/* 透明/黑色/005 → #ffffff0d */
background: #ffffff0d;
}
.coinVideoPayPopup__package.active {
border-color: #ffe580;
background: #ffffff0d;
}
.coinVideoPayPopup__packageBadge {
position: absolute;
top: -8px;
left: -1px;
max-width: 100%;
padding: 2px 8px;
border-radius: 12px 0 12px 0;
background: #f9c142;
font-size: 12px;
line-height: 16px;
color: #141414;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.coinVideoPayPopup__packageIcon {
width: 36px;
height: 36px;
margin-bottom: 4px;
}
.coinVideoPayPopup__packageCoin {
font-size: 14px;
line-height: 20px;
color: #989898;
}
.coinVideoPayPopup__packagePrice {
font-size: 14px;
font-weight: 500;
line-height: 20px;
color: #f9c142;
}
.coinVideoPayPopup__loading {
margin: 24px auto;
}
.coinVideoPayPopup__tips {
font-size: 12px;
line-height: 18px;
color: #989898;
text-align: justify;
span {
color: #ffd460;
}
}
.coinVideoPayPopup__footer {
flex-shrink: 0;
padding: 12px 16px calc(12px + env(safe-area-inset-bottom));
/* 透明/白色/000 → #00000000;透明/白色/100 → #000000 */
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, #000 100%);
box-shadow: 0 -4px 4px rgba(0, 0, 0, 0.05);
}
.coinVideoPayPopup__payBtn {
height: 44px;
border-radius: 3px;
background: #f68804;
font-size: 16px;
font-weight: 500;
line-height: 44px;
text-align: center;
color: #fff;
}
.coinVideoPayPopup__payBtn--disabled {
opacity: 0.45;
pointer-events: none;
}
</style>