502 lines
13 KiB
Vue
502 lines
13 KiB
Vue
<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>
|