[Human] chore(project): 初始化导入 91dj H5 客户端项目代码
从本地工程首次入库;.npmrc(含私有 npm 源 token)与 tsbuildinfo 已加入 .gitignore,不随仓库提交。
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<van-popup
|
||||
v-model:show="state.popupShow"
|
||||
class="accountCredentialsPopup"
|
||||
:style="{ background: 'transparent', overflow: 'visible' }"
|
||||
>
|
||||
<div class="popupBody">
|
||||
<Promote />
|
||||
<div class="promoteBtnBox">
|
||||
<div class="promoteBtn saved" @click="close">我已保存</div>
|
||||
<div class="promoteBtn primary" @click="saveImg">保存图片</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from 'vue';
|
||||
import { useAppStore } from '@/stores/app';
|
||||
import { showToast } from 'vant';
|
||||
|
||||
const appStore = useAppStore();
|
||||
const state = reactive({
|
||||
popupShow: appStore.isCertificate,
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
state.popupShow = false;
|
||||
appStore.setCertificate();
|
||||
};
|
||||
|
||||
const saveImg = () => {
|
||||
showToast('请截图保存图片');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.popupBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 0 8px;
|
||||
}
|
||||
|
||||
.promoteBtnBox {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.promoteBtn {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 500;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.saved {
|
||||
width: 94px;
|
||||
border-radius: 90px;
|
||||
background: #dcdcdc;
|
||||
color: #292929;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
width: 130px;
|
||||
border-radius: 999px;
|
||||
background: #ffc000;
|
||||
color: #1f2029;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="adv_swiper">
|
||||
<div class="adv-container" v-if="props.list.length > 0" ref="advRef">
|
||||
<van-swipe
|
||||
:show-indicators="isShowIndicators"
|
||||
:autoplay="autoplay"
|
||||
:lazy-render="lazyRender"
|
||||
>
|
||||
<van-swipe-item
|
||||
@click="handleJumpAdv(item, props.clickType)"
|
||||
v-for="item in props.list"
|
||||
:key="item.id"
|
||||
:style="{ height: height }"
|
||||
class="adv-swiper-slide"
|
||||
>
|
||||
<DecryptImg @load="load" :src="item.cover" />
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { AdImpressionType, setAdImpression } from '@/utils/eventReporter/adImpression';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reportAdClickEvent } from '@/utils/eventReporter/adClick';
|
||||
|
||||
const route = useRoute();
|
||||
const emit = defineEmits(['load']);
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
list: Types.App.AdsInfoListItem[];
|
||||
height?: string;
|
||||
isShowIndicators?: boolean;
|
||||
clickType?: 'ads' | 'recreation';
|
||||
autoplay?: number;
|
||||
adImpression?: AdImpressionType;
|
||||
/** 为 true 时仅挂载当前/相邻项,减轻首屏 DecryptImg 与解密压力 */
|
||||
lazyRender?: boolean;
|
||||
}>(),
|
||||
{
|
||||
list: () => [],
|
||||
height: '156px',
|
||||
isShowIndicators: true,
|
||||
clickType: 'ads',
|
||||
autoplay: 5000,
|
||||
lazyRender: true,
|
||||
},
|
||||
);
|
||||
|
||||
const load = () => {
|
||||
emit('load');
|
||||
};
|
||||
// 广告展示埋点
|
||||
const advRef = ref();
|
||||
watch(
|
||||
() => props.list,
|
||||
() => {
|
||||
if (props.list.length) {
|
||||
nextTick(() => {
|
||||
setAdImpression(advRef.value, props.list, {
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'horizontal_cover',
|
||||
}).setupObserver();
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const handleJumpAdv = (item: Types.App.AdsInfoListItem, clickType?: 'ads' | 'recreation') => {
|
||||
let params = {
|
||||
...item,
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'horizontal_cover',
|
||||
};
|
||||
reportAdClickEvent(params);
|
||||
jumpAdv(item, clickType);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.van-swipe__indicators) {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
padding-right: 12px;
|
||||
}
|
||||
.adv_swiper {
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
.adv-container {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
.adv-swiper-slide {
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.van-swipe__indicator) {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border: none;
|
||||
background: rgba(225, 53, 31, 1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
:deep(.van-swipe__indicator--active) {
|
||||
width: 12px;
|
||||
height: 6px;
|
||||
border-radius: 4px;
|
||||
opacity: 1;
|
||||
background: rgba(225, 53, 31, 1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="aiIcon-new">
|
||||
<img class="bg" src="@/assets/gif/cartoon-gif.gif" alt="" @click.stop="$router.push('aiUndress')" />
|
||||
<!-- <div v-if="visibleText" class="text-box">
|
||||
<p class="text" v-html="aiText"></p>
|
||||
</div> -->
|
||||
<div class="ai-text" v-if="configStore.appConfig.aiCharacterImg">
|
||||
<div class="text ellipsis2">
|
||||
{{ configStore.appConfig.aiCharacterImg }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="props.isShowClose" class="aiClose" @click.stop="emit('close')"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'AiFloat',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
const configStore = useConfigStore();
|
||||
const emit = defineEmits(['close']);
|
||||
const route = useRoute();
|
||||
|
||||
const props = defineProps({
|
||||
isShowClose: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
routeName: {
|
||||
type: String,
|
||||
default: 'home',
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.name,
|
||||
(toName) => {
|
||||
if (toName === props.routeName) {
|
||||
setText();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const visibleText = ref(true);
|
||||
const aiText = ref('');
|
||||
let timer = null as any;
|
||||
|
||||
const setText = () => {
|
||||
const arr = configStore.appConfig.aiBubble || [];
|
||||
const len = arr.length;
|
||||
if (len === 0) {
|
||||
aiText.value = '';
|
||||
} else {
|
||||
aiText.value = arr[Math.floor(Math.random() * len)].replace('\n', '<br>');
|
||||
}
|
||||
visibleText.value = true;
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
visibleText.value = false;
|
||||
}, 3000);
|
||||
};
|
||||
setText();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.aiIcon-new {
|
||||
width: 61.6px;
|
||||
height: 56px;
|
||||
position: fixed;
|
||||
bottom: 110px;
|
||||
right: 20px;
|
||||
z-index: 10;
|
||||
.bg {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: -10px;
|
||||
z-index: 1;
|
||||
width: 61.6px;
|
||||
height: 56px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.ai-text {
|
||||
width: 60px;
|
||||
height: 36px;
|
||||
position: absolute;
|
||||
left: -14px;
|
||||
top: -10px;
|
||||
background: url('@/assets/images/png/ai-text.png') no-repeat;
|
||||
background-size: contain;
|
||||
z-index: 10;
|
||||
.text {
|
||||
font-size: 7px;
|
||||
color: #000;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 52px;
|
||||
}
|
||||
}
|
||||
.text-box {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: -10px;
|
||||
z-index: 2;
|
||||
width: 76px;
|
||||
height: 54px;
|
||||
background: url('@/assets/images/png/AiGirlFriend-float-txt.png') 0 0 / contain no-repeat;
|
||||
padding: 10px 6.8px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-self: center;
|
||||
.text {
|
||||
color: #000;
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
max-height: 26px;
|
||||
}
|
||||
}
|
||||
.aiClose {
|
||||
position: absolute;
|
||||
top: -14px;
|
||||
right: -10px;
|
||||
z-index: 1;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
background: url('@/assets/images/png/aiClose.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<!-- <van-overlay v-model:show="isShow" z-index="2001"> -->
|
||||
<van-popup
|
||||
v-model:show="isShow"
|
||||
close-on-click-overlay
|
||||
z-index="2001"
|
||||
position="bottom"
|
||||
:style="{ height: '350px' }"
|
||||
@close="close"
|
||||
>
|
||||
<div class="wrapper_gold">
|
||||
<div class="popup-title">
|
||||
<div class="title">使用抵扣券</div>
|
||||
</div>
|
||||
<div class="wrap">
|
||||
<div class="couponMain overflow-y-auto">
|
||||
<div class="couponItem" v-for="item in state.couponList" :key="item.id">
|
||||
<!-- <div class="aiLogo"></div> -->
|
||||
<div class="coupon-name">{{ item.goodsName }}</div>
|
||||
<div class="couponInfo">
|
||||
<div class="coupon-value">{{ item.goodsDesc }}</div>
|
||||
<!-- <div class="faceValue">{{ item.goodsValue }}金币</div>
|
||||
<div class="time">
|
||||
有效期:<span>{{ dateFormatYyyyMmDd(item.expiredTime) }}</span>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="use-btn" @click="handleUseCoupon(item)">立即使用</div>
|
||||
<!-- <div class="selectedCoupon" v-if="item.id == state.couponId"></div>
|
||||
<div class="selectCoupon" v-else></div> -->
|
||||
</div>
|
||||
</div>
|
||||
<Empty v-if="state.couponList.length === 0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import useLoading from '@/hooks/loading';
|
||||
import VideoApi from '@/service/video';
|
||||
import { nextTick, reactive, watch, ref } from 'vue';
|
||||
import { ProductType } from '@/enums/app';
|
||||
import { showToast } from 'vant';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useRouter } from 'vue-router';
|
||||
import MineApi from '@/service/mine';
|
||||
import { dateFormatYyyyMmDd } from '@/utils/index.ts';
|
||||
|
||||
const props = defineProps({
|
||||
gold: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
isAv: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
isCoupon: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let state = reactive({
|
||||
couponSelect: -1,
|
||||
couponIndex: -1,
|
||||
couponList: [],
|
||||
couponId: '',
|
||||
});
|
||||
|
||||
const userStore = useUserStore();
|
||||
const userInfo = useUserStore().userInfo;
|
||||
const router = useRouter();
|
||||
const { openLoading, closeLoading } = useLoading();
|
||||
const emit = defineEmits(['close', 'buySuccess', 'update:isOpen', 'useCoupon']);
|
||||
const isShow = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
(newVal) => {
|
||||
isShow.value = newVal;
|
||||
},
|
||||
);
|
||||
|
||||
nextTick(() => {
|
||||
getAiCoupon();
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取抵用卷
|
||||
*/
|
||||
const getAiCoupon = async () => {
|
||||
let res: any = await MineApi.getAibackpack({ page: 1, limit: 50, status: 2, type: 3 });
|
||||
console.log(res);
|
||||
state.couponList = res || [];
|
||||
};
|
||||
|
||||
const select = (item: any) => {
|
||||
if (item.id == state.couponId) {
|
||||
// 说明已经选了此优惠券,想要取消
|
||||
state.couponId = '';
|
||||
} else {
|
||||
state.couponId = item.id;
|
||||
}
|
||||
setTimeout(() => {
|
||||
// state.showCouponPopup = false;
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const selectCoupon = (index: any) => {
|
||||
if (state.couponSelect == state.couponIndex) {
|
||||
state.couponSelect = -1;
|
||||
state.couponIndex = -1;
|
||||
} else {
|
||||
state.couponIndex = index;
|
||||
}
|
||||
};
|
||||
|
||||
const unlockNowCoupon = (type: any) => {
|
||||
if (state.couponIndex > -1 && type) {
|
||||
emit('buySuccess', state.couponList[state.couponIndex]['coupon']['id']);
|
||||
} else {
|
||||
emit('buySuccess', null);
|
||||
}
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
emit('close');
|
||||
emit('update:isOpen', false);
|
||||
};
|
||||
|
||||
const handleUseCoupon = (item: any) => {
|
||||
console.log(item);
|
||||
emit('useCoupon', item);
|
||||
close();
|
||||
};
|
||||
// const selectCoupon = (item: any) => {
|
||||
// let money = userInfo.isVip ? props.videoInfo.coins : props.videoInfo.originCoins;
|
||||
// if (item.gold < money) {
|
||||
// showToast('观影券面值过小,请选择其他支付方式');
|
||||
// } else {
|
||||
// unlockNow(item.gold);
|
||||
// }
|
||||
// };
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.wrapper_gold {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
background: rgba(3, 15, 24, 1);
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
.wrap {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
// border-radius: 5px;
|
||||
// padding: 20px 0;
|
||||
}
|
||||
.popup-title {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.buyAiTitleBg {
|
||||
.title {
|
||||
font-size: 16px;
|
||||
}
|
||||
.close {
|
||||
position: absolute;
|
||||
font-size: 18px;
|
||||
right: 15px;
|
||||
top: -5px;
|
||||
z-index: 5;
|
||||
}
|
||||
}
|
||||
|
||||
.couponMain {
|
||||
padding-top: 16px;
|
||||
height: 388px;
|
||||
.couponItem {
|
||||
height: 68px;
|
||||
width: 343px;
|
||||
margin: 0 auto 14px;
|
||||
background: url('@/assets/images/png/aiCouponBg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.coupon-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(126, 68, 68, 1);
|
||||
margin-left: 12px;
|
||||
}
|
||||
.coupon-value {
|
||||
font-size: 14px;
|
||||
color: rgba(126, 68, 68, 1);
|
||||
opacity: 0.6;
|
||||
margin-left: 12px;
|
||||
}
|
||||
// .aiLogo {
|
||||
// height: 48px;
|
||||
// width: 43.2px;
|
||||
// background: url("@/assets/images/png/ai.png") no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// position: absolute;
|
||||
// left: 30px;
|
||||
// top: 20px;
|
||||
// }
|
||||
.couponInfo {
|
||||
// position: absolute;
|
||||
// left: 124px;
|
||||
// top: 14px;
|
||||
.faceValue {
|
||||
font-size: 23px;
|
||||
font-weight: 500;
|
||||
color: rgb(242, 106, 80);
|
||||
}
|
||||
.time {
|
||||
font-size: 14px;
|
||||
color: rgb(100, 94, 91);
|
||||
margin-top: 7px;
|
||||
span {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.right {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 12px;
|
||||
transform: translateY(-50%);
|
||||
.use-btn {
|
||||
width: 72px;
|
||||
height: 26px;
|
||||
background: rgba(255, 220, 179, 1);
|
||||
border-radius: 50px;
|
||||
font-size: 14px;
|
||||
color: rgba(127, 0, 0, 1);
|
||||
text-align: center;
|
||||
line-height: 26px;
|
||||
}
|
||||
// .selectCoupon {
|
||||
// height: 16px;
|
||||
// width: 16px;
|
||||
// background: url("@/assets/images/png/selectCoupon.png") no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// }
|
||||
// .selectedCoupon {
|
||||
// height: 16px;
|
||||
// width: 16px;
|
||||
// background: url("@/assets/images/png/selectedCoupon.png")
|
||||
// no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div class="horizontalVideoBox">
|
||||
<AnimationHorizontalVideo
|
||||
:videoInfo="props.videoInfo"
|
||||
:mediaInfo="props.mediaInfo"
|
||||
@getFreeCount="getFreeCount"
|
||||
@payShow="visiblePayForWatch = true"
|
||||
@showPreviewTime="showPreviewTime"
|
||||
ref="verticalVideoRef"
|
||||
class="videoBox"
|
||||
@play="play"
|
||||
/>
|
||||
<div
|
||||
class="tipBox"
|
||||
v-if="!isVip && userFreeCount.watchCount > 0 && parentInfo.price == 0 && parentInfo.permission != 2"
|
||||
>
|
||||
免费观影数: {{ userFreeCount.watchCount }}次
|
||||
</div>
|
||||
<div class="tipBox" v-else-if="isVip && parentInfo.permission == 0 && isVipFreeShow">
|
||||
<span>已享VIP免费特权</span>
|
||||
</div>
|
||||
<div class="tipBox" v-else-if="parentInfo.permission == 1 && isVipFreeShow && vipLevel >= 2">
|
||||
<span>已享金币视频免费特权</span>
|
||||
</div>
|
||||
<PayForWatch
|
||||
ref="dvPayForWatch"
|
||||
:isOpen="visiblePayForWatch"
|
||||
:info="props.videoInfo"
|
||||
@close="visiblePayForWatch = false"
|
||||
@buySuccess="buySuccess"
|
||||
></PayForWatch>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Ref, ref, inject } from 'vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
// import PayForWatch from '@/views/acgModule/components/pay/PayForWatch.vue';
|
||||
import PayForWatch from './pay/PayForWatch.vue';
|
||||
import type { PayInfo } from '@/views/acgModule/utils/pay-mitt';
|
||||
const parentInfo = inject('historyData') as Ref<Types.Media.MediaInfo>;
|
||||
const verticalVideoRef = ref();
|
||||
let previewTime = ref(0); // 预览视频时间
|
||||
const isVipFreeShow = ref(true);
|
||||
const props = defineProps<{
|
||||
videoInfo: Types.Acg.MediaContentListItem;
|
||||
mediaInfo: Types.Acg.ListItem;
|
||||
}>();
|
||||
const visiblePayForWatch = ref(false);
|
||||
const emit = defineEmits(['play', 'buySuccess']);
|
||||
const { isVip, vipLevel } = useUserStore();
|
||||
let userFreeCount: Ref<Types.Video.UserFreeCount> = ref({
|
||||
isCan: false,
|
||||
watchCount: 0,
|
||||
});
|
||||
|
||||
//免费观看视频次数
|
||||
const getFreeCount = (userCount: Types.Video.UserFreeCount) => {
|
||||
userFreeCount.value = userCount;
|
||||
};
|
||||
const showPreviewTime = (time: number) => {
|
||||
previewTime.value = parseInt(time.toString());
|
||||
};
|
||||
let isFirst = false;
|
||||
const play = (bool: boolean) => {
|
||||
if (!isFirst) {
|
||||
isFirst = true;
|
||||
let timer = setTimeout(() => {
|
||||
isVipFreeShow.value = false;
|
||||
clearTimeout(timer);
|
||||
}, 3000);
|
||||
}
|
||||
emit('play', bool);
|
||||
};
|
||||
/**
|
||||
* @description 视频购买成功
|
||||
*/
|
||||
const buySuccess = (obj: PayInfo) => {
|
||||
visiblePayForWatch.value = false;
|
||||
verticalVideoRef.value.success();
|
||||
emit('buySuccess', obj);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.horizontalVideoBox {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.videoBox {
|
||||
height: 212px;
|
||||
}
|
||||
.tipBox {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
font-size: 12px;
|
||||
color: rgb(242, 242, 242);
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
span {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.memberPopup {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(12, 12, 15, 0.61);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
text-align: center;
|
||||
.close {
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
background: url('./../../assets/images/png/movieClose.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 19px;
|
||||
}
|
||||
.countUsed {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #eeeeee;
|
||||
margin-top: 60px;
|
||||
}
|
||||
.unlimited {
|
||||
font-size: 12px;
|
||||
color: #eeeeee;
|
||||
//margin: 6px 0 25px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.btnBox {
|
||||
margin-top: 25px;
|
||||
.btn {
|
||||
width: 106px;
|
||||
height: 39px;
|
||||
line-height: 38px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
color: rgb(244, 244, 244);
|
||||
border: 1px solid white;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.openVip {
|
||||
background-color: #fd2d55;
|
||||
border: 1px solid #fd2d55;
|
||||
margin-left: 19px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goldPopup {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(12, 12, 15, 0.61);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
text-align: center;
|
||||
.close {
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
background: url('./../../assets/images/png/movieClose.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 19px;
|
||||
}
|
||||
.countUsed {
|
||||
font-size: 15px;
|
||||
//font-weight: 600;
|
||||
color: #eeeeee;
|
||||
margin-top: 60px;
|
||||
|
||||
.amountBox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
svg {
|
||||
margin-left: 2px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.unlimited {
|
||||
font-size: 12px;
|
||||
color: #eeeeee;
|
||||
//margin: 6px 0 25px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.btnBox {
|
||||
margin-top: 25px;
|
||||
.btn {
|
||||
width: 106px;
|
||||
height: 39px;
|
||||
line-height: 38px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
color: rgb(244, 244, 244);
|
||||
border: 1px solid white;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.openVip {
|
||||
background-color: #fd2d55;
|
||||
border: 1px solid #fd2d55;
|
||||
margin-left: 19px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,249 @@
|
||||
<template>
|
||||
<div v-if="visiblePermission1" class="goldPopup">
|
||||
<div class="title">试看结束</div>
|
||||
<div class="content">开通VIP 全站视频免费看</div>
|
||||
<div class="btnBox flex-center-center">
|
||||
<div class="btn" @click="showGoldPayPopup = true">{{ historyData.price }}金币解锁</div>
|
||||
<div class="btn primary" @click="showPaymentStatusPopup = true">开通VIP免费看</div>
|
||||
</div>
|
||||
<!-- 观影卷 -->
|
||||
<div class="couponBox" v-if="useCoupon?.gold" @click.stop="nowBuy('all', true)">
|
||||
使用观影券
|
||||
<span>x{{ userStore.userInfo.goldVideoCoupon?.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="visiblePermission0" class="goldPopup">
|
||||
<div class="title">试看结束</div>
|
||||
<div class="content">开通VIP 全站视频免费看</div>
|
||||
<div class="btnBox flex-center-center">
|
||||
<div class="btn" @click="router.push('/share')">邀请得3日VIP</div>
|
||||
<div class="btn primary" @click="showPaymentStatusPopup = true">开通VIP免费看</div>
|
||||
</div>
|
||||
<!-- 观影卷 -->
|
||||
<div class="couponBox" v-if="useCoupon?.gold" @click.stop="nowBuy('all', true)">
|
||||
使用观影券
|
||||
<span>x{{ useCouponNum }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 开通会员 -->
|
||||
<PaymentStatusPopup
|
||||
:show="showPaymentStatusPopup"
|
||||
:video-id="String(historyData?.id || '')"
|
||||
@close="closePaymentStatusPopup"
|
||||
/>
|
||||
<GoldPopup
|
||||
:is-open="showGoldPayPopup"
|
||||
:video-info="props.info"
|
||||
confirm-only
|
||||
@close="showGoldPayPopup = false"
|
||||
@confirm="handleGoldConfirm"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch, onActivated, inject, Ref, nextTick, defineAsyncComponent } from 'vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import UserApi from '@/service/user';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { showToast } from 'vant';
|
||||
import VideoApi from '@/service/video';
|
||||
import { ProductType } from '@/enums/app';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import GoldPopup from '@/views/acgModule/components/pay/GoldPopup.vue';
|
||||
const { openLoading, closeLoading } = useLoading();
|
||||
const PaymentStatusPopup = defineAsyncComponent(() => import('@/components/paymentStatusPopup/index.vue'));
|
||||
const router = useRouter();
|
||||
const showPaymentStatusPopup = ref(false);
|
||||
const showGoldPayPopup = ref(false);
|
||||
const closePaymentStatusPopup = () => {
|
||||
showPaymentStatusPopup.value = false;
|
||||
};
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
info: any; // Types.Media.MediaInfo | Types.Acg.MediaContentListItem;
|
||||
isOpen: boolean;
|
||||
}>(),
|
||||
{
|
||||
info: () => <any>{},
|
||||
isOpen: () => false,
|
||||
},
|
||||
);
|
||||
|
||||
const historyData = inject('historyData') as Ref<Types.Media.MediaInfo>;
|
||||
|
||||
const userStore = useUserStore();
|
||||
const isTypeVip = computed(() => {
|
||||
return userStore.isVip;
|
||||
});
|
||||
const emit = defineEmits(['close', 'buySuccess']);
|
||||
|
||||
const visiblePermission0 = ref(false);
|
||||
const visiblePermission1 = ref(false);
|
||||
// const visibleTodayFree = ref(false);
|
||||
|
||||
// permission 收听权限 0:会员 1:金币购买 2:免费
|
||||
const permission = computed(() => {
|
||||
return historyData.value.permission;
|
||||
});
|
||||
const recharge = computed(() => {
|
||||
return (userStore.walletInfo.amount * 100 + userStore.walletInfo.income * 100) / 100;
|
||||
});
|
||||
const gold = computed(() => {
|
||||
return historyData.value.price;
|
||||
});
|
||||
const useCoupon: any = computed(() => {
|
||||
if (userStore.userInfo.goldVideoCoupon?.length) {
|
||||
return userStore.userInfo.goldVideoCoupon.find((item: any) => item.gold >= gold.value) || {};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
const useCouponNum = computed(() => {
|
||||
if (userStore.userInfo.goldVideoCoupon?.length) {
|
||||
return userStore.userInfo.goldVideoCoupon.reduce((sum: number, item: any) => sum + item.count, 0);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
const handleGoldConfirm = () => {
|
||||
showGoldPayPopup.value = false;
|
||||
nowBuy('all');
|
||||
};
|
||||
/**
|
||||
* @description 视频购买
|
||||
*/
|
||||
const nowBuy = (type: 'all', coupon?: boolean) => {
|
||||
try {
|
||||
if (recharge.value < gold.value) {
|
||||
emit('close');
|
||||
router.push('/wallet');
|
||||
return;
|
||||
}
|
||||
let productID = historyData.value.id;
|
||||
if (!productID) {
|
||||
showToast('购买出错~');
|
||||
return;
|
||||
}
|
||||
openLoading();
|
||||
|
||||
VideoApi.videoBuying({
|
||||
productType: ProductType.AcgAll,
|
||||
productID,
|
||||
goldVideoCouponNum: coupon ? useCoupon.value.gold : 0,
|
||||
})
|
||||
.then(async () => {
|
||||
showToast('购买成功');
|
||||
userStore.updateWallet();
|
||||
emit('buySuccess', { type, productID });
|
||||
if (coupon) {
|
||||
userStore.updateUserInfo();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.toString().indexOf('8000') != -1) {
|
||||
emit('close');
|
||||
nextTick(() => {
|
||||
router.push({
|
||||
path: '/wallet',
|
||||
query: {
|
||||
activeTab: 1,
|
||||
id: historyData.value.id,
|
||||
title: historyData.value.title || '无标题',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
closeLoading();
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
(bool) => {
|
||||
if (!bool) {
|
||||
// visibleTodayFree.value = false;
|
||||
visiblePermission0.value = false;
|
||||
visiblePermission1.value = false;
|
||||
showGoldPayPopup.value = false;
|
||||
return;
|
||||
} else {
|
||||
if (permission.value === undefined) {
|
||||
emit('close');
|
||||
return;
|
||||
}
|
||||
if (permission.value === 1) {
|
||||
visiblePermission1.value = true;
|
||||
} else if (permission.value === 0) {
|
||||
visiblePermission0.value = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
|
||||
onActivated(() => {
|
||||
// 会员类型要从接口取用户最新信息
|
||||
if (permission.value === 0 && !isTypeVip.value) {
|
||||
UserApi.queryUserInfo(null).then((res) => {
|
||||
userStore.saveUserInfo(res);
|
||||
if (res.isVip) {
|
||||
emit('close');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.goldPopup {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #fff;
|
||||
padding-top: 42px;
|
||||
text-align: center;
|
||||
.content {
|
||||
margin: 26px 0;
|
||||
span {
|
||||
color: rgb(62, 100, 234);
|
||||
}
|
||||
}
|
||||
.btnBox {
|
||||
.btn {
|
||||
padding: 6px 18px;
|
||||
line-height: 20px;
|
||||
border-radius: 30px;
|
||||
background: linear-gradient(90deg, #ffe8be 1.65%, #e6b764 99.23%);
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #694923;
|
||||
font-weight: 400;
|
||||
|
||||
&.primary {
|
||||
margin-left: 15px;
|
||||
background: linear-gradient(320deg, #ff4d4d 0%, #ff6e6e 100%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
.couponBox {
|
||||
font-family: 'PingFang SC';
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
margin-top: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
span {
|
||||
color: #f52c56;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div class="animationImg">
|
||||
<transition name="bounceFade">
|
||||
<img
|
||||
v-show="isLoad"
|
||||
:src="decyImg"
|
||||
@load="load"
|
||||
alt="加载失败"
|
||||
:class="{ round: round }"
|
||||
:style="{ borderRadius: borderRadius + 'px' }"
|
||||
/>
|
||||
</transition>
|
||||
<transition name="fade">
|
||||
<div
|
||||
class="loading"
|
||||
v-if="!isLoad"
|
||||
:class="{ round: round }"
|
||||
:style="{ borderRadius: borderRadius + 'px' }"
|
||||
></div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import AppApi from '@/service/app';
|
||||
const props = defineProps({
|
||||
//图片地址
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
default() {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
isNeedDecypt: {
|
||||
//是否需要解密
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
round: {
|
||||
//图片是否显示为圆形
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
//图片圆角
|
||||
type: Number,
|
||||
required: false,
|
||||
default() {
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['load']);
|
||||
|
||||
let isLoad = ref(false);
|
||||
let decyImg = ref('');
|
||||
const configStore = useConfigStore();
|
||||
|
||||
const load = () => {
|
||||
isLoad.value = true;
|
||||
emit('load');
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
isLoad.value = false;
|
||||
decyImg.value = '';
|
||||
|
||||
if (!props.src) {
|
||||
return;
|
||||
}
|
||||
if (!props.isNeedDecypt) {
|
||||
decyImg.value = props.src;
|
||||
return;
|
||||
}
|
||||
|
||||
if (/^(blob:|data:)/.test(props.src)) {
|
||||
decyImg.value = props.src;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const isHttp = /^https?:\/\//.test(props.src);
|
||||
const imgSrc = isHttp ? props.src : configStore.sourceInfo.IMAGE + '/' + props.src;
|
||||
AppApi.imgDecypt(imgSrc)
|
||||
.then((url: any) => {
|
||||
decyImg.value = url;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('imgDecypt error', err);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('imgDecypt catch', error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.src, props.isNeedDecypt],
|
||||
() => {
|
||||
init();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 1s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
.bounceFade-enter-active {
|
||||
animation: bounceFade-in 0.4s;
|
||||
}
|
||||
.bounceFade-leave-active {
|
||||
animation: bounceFade-in 0.4s;
|
||||
}
|
||||
|
||||
@keyframes bounceFade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.round {
|
||||
border-radius: 50% !important;
|
||||
}
|
||||
.loading {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('@/assets/images/png/placeholder.png') no-repeat;
|
||||
background-size: 50% auto;
|
||||
background-position: 50% 50%;
|
||||
background-color: #141414;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
object-fit: cover !important;
|
||||
}
|
||||
}
|
||||
.animationImg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="announcementPopup">
|
||||
<van-overlay
|
||||
v-if="currentAnnouncement && currentAnnouncement.type == 0"
|
||||
:show="true"
|
||||
z-index="2000"
|
||||
:lock-scroll="false"
|
||||
@click="close"
|
||||
>
|
||||
<div class="wrapper" @click.stop>
|
||||
<div class="content app-popup-surface">
|
||||
<div class="title app-popup-title">{{ currentAnnouncement.title }}</div>
|
||||
<div class="horizontalLine app-popup-divider"></div>
|
||||
<div class="text app-popup-body">
|
||||
{{ currentAnnouncement.content }}
|
||||
</div>
|
||||
<div class="btnList app-popup-primary" @click.stop="close">我知道了</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
<van-overlay v-if="currentAnnouncement && currentAnnouncement.type == 1" :show="true" z-index="1999" @click="close">
|
||||
<div class="imgWrapper" @click.stop>
|
||||
<lazyImg @click.stop="jumpAdv(currentAnnouncement)" class="imgBox" :src="currentAnnouncement.cover" />
|
||||
<div class="close" @click.stop="close"></div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
announList: Types.App.AnnounListItem[];
|
||||
}>(),
|
||||
{
|
||||
announList: () => [],
|
||||
},
|
||||
);
|
||||
const emit = defineEmits(['close']);
|
||||
const state = reactive({
|
||||
announList: <Array<Types.App.AnnounListItem>>props.announList,
|
||||
});
|
||||
const currentAnnouncement = computed(() => state.announList[state.announList.length - 1]);
|
||||
const close = () => {
|
||||
state.announList.pop();
|
||||
if (!state.announList.length) {
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.van-overlay) {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
.announcementPopup {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.wrapper {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 311px;
|
||||
box-sizing: border-box;
|
||||
.content {
|
||||
padding: 30px 24px 20px 24px;
|
||||
border-radius: 12px;
|
||||
background: #030f18;
|
||||
width: 311px;
|
||||
margin: 0 auto;
|
||||
color: #656565;
|
||||
font-family: 'PingFang SC';
|
||||
font-size: 14px;
|
||||
.title {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
text-align: center;
|
||||
font-family: PingFang SC;
|
||||
font-size: 20px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
height: 40px;
|
||||
}
|
||||
// .horizontalLine {
|
||||
// height: 1px;
|
||||
// background-color: rgba(255, 255, 255, 0.1);
|
||||
// margin: 18px 0;
|
||||
// }
|
||||
.text {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
white-space: pre-line;
|
||||
height: 260px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
.btnList {
|
||||
margin: 6px auto 0;
|
||||
height: 44px;
|
||||
width: 263px;
|
||||
text-align: center;
|
||||
line-height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border-radius: 3px;
|
||||
background: #e1351f;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.imgWrapper {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.imgBox {
|
||||
width: 325px;
|
||||
height: 325px;
|
||||
:deep(.animationImg) {
|
||||
img {
|
||||
object-fit: contain !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.close {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
background: url('./../../assets/images/png/closeDialog.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
margin: 10px auto;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<van-overlay class="share-overlay" z-index="2000" :show="visible" @click="emit('close')">
|
||||
<div class="wrapper flex-center-center" @click.stop="emit('close')">
|
||||
<div class="share-box" :class="[props.theme]" @click.stop>
|
||||
<div class="content">
|
||||
<div class="app">
|
||||
<img class="logo" src="@/assets/images/ui/share-logo@2x.png" alt="" />
|
||||
<div class="info">
|
||||
<span class="name">91短剧</span>
|
||||
<span class="desc">百万大神短剧原创</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cover-box" :class="[props.coverType]">
|
||||
<lazyImg
|
||||
:src="cover || globalStore.shareInfo.cover"
|
||||
:key="cover || globalStore.shareInfo.cover"
|
||||
class="cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="qrCodeBg">
|
||||
<div class="qrCodeBox flex-center-center">
|
||||
<qrcode-vue id="qrCode" class="qrCodeImg" v-if="state.qrUrl" :value="state.qrUrl" level="M" />
|
||||
<van-loading size="50px" type="spinner" color="#c0c9ea" v-else />
|
||||
</div>
|
||||
<div class="share-info">
|
||||
<div class="invite-code">
|
||||
<span>邀请码</span>
|
||||
<span>{{ state.userInfo.promotionCode }}</span>
|
||||
</div>
|
||||
<div class="tips-link">
|
||||
<p class="tips">
|
||||
每邀请
|
||||
<span>1人</span>
|
||||
送
|
||||
<span>1天</span>
|
||||
会员
|
||||
</p>
|
||||
<p class="link">{{ displayLink }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="promoteBtnBox">
|
||||
<div class="btn" @click="copyLink">复制链接</div>
|
||||
<div class="btn primary" @click="saveImg">保存图片</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <img
|
||||
class="close-icon"
|
||||
src="@/assets/images/png/community/rule-popup-close.svg"
|
||||
alt=""
|
||||
@click.stop="emit('close')"
|
||||
/> -->
|
||||
</div>
|
||||
</van-overlay>
|
||||
</Teleport>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, computed } from 'vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { showToast } from 'vant';
|
||||
import toClipboard from '@/utils/clipboard';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
isOpen: boolean;
|
||||
isNormalLink?: boolean;
|
||||
theme?: string;
|
||||
cover?: string;
|
||||
coverType?: string;
|
||||
}>(),
|
||||
{
|
||||
isOpen: false,
|
||||
isNormalLink: false,
|
||||
theme: 'black',
|
||||
},
|
||||
);
|
||||
const userStore = useUserStore();
|
||||
const emit = defineEmits(['close', 'getLink']);
|
||||
const configStore = useConfigStore();
|
||||
const globalStore = useGlobalStore();
|
||||
const state = reactive({
|
||||
visible: true,
|
||||
userInfo: <Types.User.UserInfoType>userStore.userInfo,
|
||||
qrUrl: <string>'',
|
||||
});
|
||||
|
||||
const init = () => {
|
||||
if (props.isNormalLink) {
|
||||
state.qrUrl = configStore.sourceInfo.LAND;
|
||||
} else {
|
||||
state.qrUrl = state.userInfo.promoteURL;
|
||||
emit('getLink', state.qrUrl);
|
||||
}
|
||||
};
|
||||
init();
|
||||
const visible = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
const displayLink = computed(() => {
|
||||
return configStore.sourceInfo.GUIDE || '';
|
||||
});
|
||||
const copyLink = async () => {
|
||||
try {
|
||||
if (!state.qrUrl) {
|
||||
showToast('复制失败');
|
||||
return;
|
||||
}
|
||||
await toClipboard(state.qrUrl);
|
||||
showToast('复制成功');
|
||||
} catch (e: any) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
const saveImg = () => {
|
||||
showToast('请截图保存图片');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.share-overlay {
|
||||
background: rgba(0, 0, 0, 0.7) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.share-box {
|
||||
width: 311px;
|
||||
border-radius: 12px;
|
||||
background: #0f0f0f;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
padding: 32px 24px 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.app {
|
||||
width: 265px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
.name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cover-box {
|
||||
width: 261px;
|
||||
height: 123px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 9px;
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.portrait {
|
||||
width: 211px;
|
||||
height: 281px;
|
||||
}
|
||||
}
|
||||
|
||||
.qrCodeBg {
|
||||
width: 265px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.qrCodeBox {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background-color: #fff;
|
||||
|
||||
.qrCodeImg {
|
||||
width: 94px !important;
|
||||
height: 94px !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.share-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.invite-code {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
|
||||
span {
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tips-link {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
.tips {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
|
||||
span {
|
||||
color: #ffc000;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
margin: 0;
|
||||
word-break: break-all;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.promoteBtnBox {
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.btn {
|
||||
height: 44px;
|
||||
padding: 0 24px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 88px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #989898;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.primary {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #1f2029;
|
||||
background: #ffc000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,521 @@
|
||||
<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分钟后重试支付。如支付未到账,请
|
||||
<span @click.stop="goCustomerService">反馈客服订单号</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="coinVideoPayPopup__footer">
|
||||
<div
|
||||
class="coinVideoPayPopup__payBtn"
|
||||
:class="{ 'coinVideoPayPopup__payBtn--disabled': !canClickPay }"
|
||||
@click="handlePay"
|
||||
>
|
||||
{{ payBtnText }}
|
||||
</div>
|
||||
<div class="coinVideoPayPopup__service">
|
||||
支付问题反馈,点击联系
|
||||
<span @click.stop="goCustomerService">在线客服</span>
|
||||
</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 goCustomerService = () => {
|
||||
emitClose();
|
||||
router.push('/customerService');
|
||||
};
|
||||
|
||||
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: var(--app-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 44px;
|
||||
text-align: center;
|
||||
color: var(--app-primary-text);
|
||||
}
|
||||
.coinVideoPayPopup__payBtn--disabled {
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
}
|
||||
.coinVideoPayPopup__service {
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
color: #dcdcdc;
|
||||
span {
|
||||
color: #ffd460;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,634 @@
|
||||
<template>
|
||||
<!-- 新用户倒计时 -->
|
||||
<div
|
||||
class="newUserBg"
|
||||
v-if="displayType === 'newUser' && newUserTime.countdownSec > 0"
|
||||
@click="$emit('openMemberPay')"
|
||||
>
|
||||
<lazyImg
|
||||
class="newUserBannerImg"
|
||||
:src="configStore.appConfig?.newUserBannerImg"
|
||||
:key="configStore.appConfig?.newUserBannerImg"
|
||||
v-if="configStore.appConfig?.newUserBannerImg"
|
||||
/>
|
||||
<img src="@/assets/images/png/home/newUserBg.png" class="newUserBannerImg" v-else />
|
||||
<div class="hour">{{ newUserTime.hr }}</div> <span class="hourColon">:</span>
|
||||
<div class="minute">{{ newUserTime.min }}</div> <span class="minuteColon">:</span>
|
||||
<div class="second">{{ newUserTime.sec }}</div>
|
||||
<!-- <div class="nowOpenBtn" @click="$emit('openMemberPay')"></div> -->
|
||||
</div>
|
||||
<!-- 限时特惠倒计时 -->
|
||||
<div
|
||||
class="newUserBg specialOfferBg"
|
||||
v-else-if="displayType === 'specialOffer' && specialOfferTime.countdownSec > 0"
|
||||
@click="handleSpecialOfferClick"
|
||||
>
|
||||
<lazyImg
|
||||
class="newUserBannerImg"
|
||||
:src="
|
||||
specialOfferType === 'newbie' ? configStore.appConfig?.specialBannerImg : configStore.appConfig?.backBannerImg
|
||||
"
|
||||
:key="
|
||||
specialOfferType === 'newbie' ? configStore.appConfig?.specialBannerImg : configStore.appConfig?.backBannerImg
|
||||
"
|
||||
v-if="
|
||||
(specialOfferType === 'newbie' && configStore.appConfig?.specialBannerImg) ||
|
||||
(specialOfferType === 'old' && configStore.appConfig?.backBannerImg)
|
||||
"
|
||||
/>
|
||||
<img src="@/assets/images/png/home/newUserBg.png" class="newUserBannerImg" v-else />
|
||||
<div class="hour">{{ specialOfferTime.hr }}</div> <span class="hourColon">:</span>
|
||||
<div class="minute">{{ specialOfferTime.min }}</div> <span class="minuteColon">:</span>
|
||||
<div class="second">{{ specialOfferTime.sec }}</div>
|
||||
<div class="nowOpenBtn"></div>
|
||||
</div>
|
||||
<!-- 活动banner倒计时 -->
|
||||
<div
|
||||
class="bannerBox"
|
||||
v-else-if="displayType === 'activity' && activeTime.countdownSec > 0"
|
||||
@click="jumpAdv(configStore.appConfig.bannerJump && configStore.appConfig.bannerJump[1])"
|
||||
>
|
||||
<lazyImg
|
||||
class="bannerImg"
|
||||
:src="configStore.appConfig.bannerJump && configStore.appConfig.bannerJump[1]?.banner"
|
||||
:key="configStore.appConfig.bannerJump && configStore.appConfig.bannerJump[1]?.banner"
|
||||
/>
|
||||
<div class="timeBox">{{ activeTime.timeStr }}</div>
|
||||
</div>
|
||||
<!-- 付费分层 playPage banner -->
|
||||
<div
|
||||
class="newUserBg paymentPlayPageBg"
|
||||
v-else-if="showPaymentPlayPage"
|
||||
@click="openDirectPay(paymentVipCardId)"
|
||||
>
|
||||
<lazyImg class="newUserBannerImg" :src="paymentPlayPageImg" :key="paymentPlayPageImg" />
|
||||
<template v-if="showPaymentCountdown">
|
||||
<div class="hour">{{ paymentPlayPageTime.hr }}</div>
|
||||
<span class="hourColon">:</span>
|
||||
<div class="minute">{{ paymentPlayPageTime.min }}</div>
|
||||
<span class="minuteColon">:</span>
|
||||
<div class="second">{{ paymentPlayPageTime.sec }}</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- <div
|
||||
class="newUserBg"
|
||||
v-else-if="configStore.appConfig.dailyBannerImg && !isVip"
|
||||
@click="handleToMember(configStore.appConfig?.dailyBannerVipCardId)"
|
||||
>
|
||||
<lazyImg
|
||||
class="newUserBannerImg"
|
||||
:src="configStore.appConfig.dailyBannerImg"
|
||||
:key="configStore.appConfig.dailyBannerImg"
|
||||
/>
|
||||
</div>
|
||||
<div class="vipBox" v-else-if="displayType === 'vipBox'" @click="handleToMember('')">
|
||||
<img src="@/assets/images/png/up_vip_icon.png" alt="" class="vipIcon">
|
||||
<div class="vipContent">
|
||||
<span class="vipName">{{ userInfo.vipName || '普通会员' }}</span>
|
||||
<span class="vipTime">到期时间:{{ moment(userInfo.vipExpireDate).format('YYYY.MM.DD') }}</span>
|
||||
</div>
|
||||
<img src="@/assets/images/png/up_vip_btn.png" alt="" class="vipBtn">
|
||||
</div> -->
|
||||
<RechargeBox
|
||||
ref="rechargeBoxRef"
|
||||
:is-open="isRecharge"
|
||||
@closePay="handleClosePay"
|
||||
@paySuccess="handlePaySuccess"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onMounted, onUnmounted, watch, defineAsyncComponent } from 'vue';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
import { useRouter } from 'vue-router';
|
||||
import moment from 'moment';
|
||||
import { usePaymentStatusDirectPay } from '@/hooks/usePaymentStatusDirectPay';
|
||||
import {
|
||||
hasAnyPaymentConfigField,
|
||||
isPaymentPlayPageStatus,
|
||||
parseLastDiscountTime,
|
||||
resolvePaymentConfigImg,
|
||||
} from '@/utils/paymentStatusPopup';
|
||||
|
||||
const RechargeBox = defineAsyncComponent(() => import('@/components/RechargeBox/index.vue'));
|
||||
const configStore = useConfigStore();
|
||||
const { isNewUser, isVip, userInfo } = useUserStore();
|
||||
const router = useRouter();
|
||||
const props = defineProps({
|
||||
// 新用户倒计时秒数
|
||||
newUserCountdownSec: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
// 活动倒计时秒数
|
||||
activeCountdownSec: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
// 限时特惠倒计时毫秒数
|
||||
specialOfferCountdownMs: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
// 限时特惠类型:newbie-新人特惠, old-返场特惠
|
||||
specialOfferType: {
|
||||
type: String,
|
||||
default: 'newbie',
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['openMemberPay', 'openSpecialOffer']);
|
||||
|
||||
// 新用户倒计时
|
||||
const newUserTime = ref({
|
||||
countdownSec: props.newUserCountdownSec,
|
||||
hr: '',
|
||||
min: '',
|
||||
sec: '',
|
||||
});
|
||||
|
||||
// 活动倒计时
|
||||
const activeTime = ref({
|
||||
countdownSec: props.activeCountdownSec,
|
||||
timeStr: '',
|
||||
});
|
||||
|
||||
// 限时特惠倒计时
|
||||
const specialOfferTime = ref({
|
||||
countdownSec: Math.floor(props.specialOfferCountdownMs / 1000),
|
||||
hr: '',
|
||||
min: '',
|
||||
sec: '',
|
||||
});
|
||||
|
||||
// 付费分层 playPage 倒计时
|
||||
const paymentPlayPageTime = ref({
|
||||
countdownSec: 0,
|
||||
hr: '',
|
||||
min: '',
|
||||
sec: '',
|
||||
});
|
||||
|
||||
const paymentCountdownExpired = ref(false);
|
||||
const { rechargeBoxRef, isRecharge, openDirectPay, handlePaySuccess, handleClosePay } = usePaymentStatusDirectPay({
|
||||
sourcePage: 'VIDEO_BOTTOM_BANNER',
|
||||
});
|
||||
|
||||
let newUserTimer: any = null;
|
||||
let activeTimer: any = null;
|
||||
let oldReturnTimer: any = null;
|
||||
let specialOfferTimer: any = null;
|
||||
let paymentPlayPageTimer: any = null;
|
||||
|
||||
// 格式化时分秒
|
||||
const formatTime = (timer: number) => {
|
||||
if (timer <= 0) return { hr: '00', min: '00', sec: '00' };
|
||||
|
||||
const hr = Math.floor(timer / 60 / 60);
|
||||
const min = Math.floor((timer / 60) % 60);
|
||||
const sec = Math.floor(timer % 60);
|
||||
|
||||
return {
|
||||
hr: hr > 9 ? hr.toString() : '0' + hr,
|
||||
min: min > 9 ? min.toString() : '0' + min,
|
||||
sec: sec > 9 ? sec.toString() : '0' + sec,
|
||||
};
|
||||
};
|
||||
|
||||
// 格式化活动倒计时(天时分秒)
|
||||
const formatActiveTime = (timer: number) => {
|
||||
if (timer <= 0) return '';
|
||||
|
||||
const day = Math.floor(timer / 60 / 60 / 24);
|
||||
const hr = Math.floor((timer - day * 24 * 60 * 60) / 60 / 60);
|
||||
const min = Math.floor((timer / 60) % 60);
|
||||
const sec = Math.floor(timer % 60);
|
||||
|
||||
return `${day}天${String(hr).padStart(2, '0')}:${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// 新用户倒计时
|
||||
const startNewUserCountdown = () => {
|
||||
if (newUserTime.value.countdownSec <= 0) return;
|
||||
|
||||
const updateTime = () => {
|
||||
const formatted = formatTime(newUserTime.value.countdownSec);
|
||||
newUserTime.value.hr = formatted.hr;
|
||||
newUserTime.value.min = formatted.min;
|
||||
newUserTime.value.sec = formatted.sec;
|
||||
|
||||
if (newUserTime.value.countdownSec > 0) {
|
||||
newUserTimer = setTimeout(() => {
|
||||
newUserTime.value.countdownSec--;
|
||||
updateTime();
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
updateTime();
|
||||
};
|
||||
|
||||
// 活动倒计时
|
||||
const startActiveCountdown = () => {
|
||||
if (activeTime.value.countdownSec <= 0) return;
|
||||
|
||||
const updateTime = () => {
|
||||
activeTime.value.timeStr = formatActiveTime(activeTime.value.countdownSec);
|
||||
|
||||
if (activeTime.value.countdownSec > 0) {
|
||||
activeTimer = setTimeout(() => {
|
||||
activeTime.value.countdownSec--;
|
||||
updateTime();
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
updateTime();
|
||||
};
|
||||
|
||||
// 限时特惠倒计时
|
||||
const startSpecialOfferCountdown = () => {
|
||||
if (specialOfferTime.value.countdownSec <= 0) return;
|
||||
|
||||
const updateTime = () => {
|
||||
const formatted = formatTime(specialOfferTime.value.countdownSec);
|
||||
specialOfferTime.value.hr = formatted.hr;
|
||||
specialOfferTime.value.min = formatted.min;
|
||||
specialOfferTime.value.sec = formatted.sec;
|
||||
|
||||
if (specialOfferTime.value.countdownSec > 0) {
|
||||
specialOfferTimer = setTimeout(() => {
|
||||
specialOfferTime.value.countdownSec--;
|
||||
updateTime();
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
updateTime();
|
||||
};
|
||||
|
||||
// 付费分层 playPage 倒计时
|
||||
const startPaymentPlayPageCountdown = () => {
|
||||
clearTimeout(paymentPlayPageTimer);
|
||||
if (paymentPlayPageTime.value.countdownSec <= 0) {
|
||||
paymentPlayPageTime.value.hr = '';
|
||||
paymentPlayPageTime.value.min = '';
|
||||
paymentPlayPageTime.value.sec = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const updateTime = () => {
|
||||
const formatted = formatTime(paymentPlayPageTime.value.countdownSec);
|
||||
paymentPlayPageTime.value.hr = formatted.hr;
|
||||
paymentPlayPageTime.value.min = formatted.min;
|
||||
paymentPlayPageTime.value.sec = formatted.sec;
|
||||
|
||||
if (paymentPlayPageTime.value.countdownSec > 0) {
|
||||
paymentPlayPageTimer = setTimeout(() => {
|
||||
paymentPlayPageTime.value.countdownSec--;
|
||||
updateTime();
|
||||
}, 1000);
|
||||
} else {
|
||||
paymentCountdownExpired.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
updateTime();
|
||||
};
|
||||
|
||||
// 新用户 / 限时特惠 / 活动倒计时是否有任一可展示
|
||||
const hasLegacyCountdown = computed(() => {
|
||||
// if (isVip) return false;
|
||||
|
||||
// const hasNewUser = isNewUser && newUserTime.value.countdownSec > 0;
|
||||
// const hasSpecialOffer = specialOfferTime.value.countdownSec > 0;
|
||||
const hasActivity =
|
||||
!!configStore.appConfig.bannerJump?.[1]?.banner && activeTime.value.countdownSec > 0;
|
||||
|
||||
return hasActivity;
|
||||
});
|
||||
|
||||
const paymentPlayPageImg = computed(() =>
|
||||
resolvePaymentConfigImg(configStore.appConfig?.paymentStatusPopupConfig?.playPage),
|
||||
);
|
||||
|
||||
const paymentDiscountDeadline = computed(() =>
|
||||
parseLastDiscountTime(configStore.appConfig?.paymentStatusPopupConfig?.lastDiscountTime),
|
||||
);
|
||||
|
||||
const paymentCountdownSec = computed(() => {
|
||||
if (!paymentDiscountDeadline.value || paymentCountdownExpired.value) return 0;
|
||||
const remaining = Math.floor((paymentDiscountDeadline.value - Date.now()) / 1000);
|
||||
return remaining > 0 ? remaining : 0;
|
||||
});
|
||||
|
||||
const showPaymentCountdown = computed(() => paymentCountdownSec.value > 0);
|
||||
|
||||
const showPaymentPlayPage = computed(() => {
|
||||
// if (hasLegacyCountdown.value) return false;
|
||||
if (!isPaymentPlayPageStatus(configStore.appConfig?.paymentStatusPopup)) return false;
|
||||
if (!hasAnyPaymentConfigField(configStore.appConfig?.paymentStatusPopupConfig, ['playPage', 'vipCard'])) {
|
||||
return false;
|
||||
}
|
||||
// if (paymentDiscountDeadline.value && paymentCountdownSec.value <= 0) return false;
|
||||
return !!paymentPlayPageImg.value;
|
||||
});
|
||||
|
||||
const paymentVipCardId = computed(() => configStore.appConfig?.paymentStatusPopupConfig?.vipCard || '');
|
||||
|
||||
// 当前锁定的显示类型
|
||||
const lockedDisplayType = ref<string | null>(null);
|
||||
|
||||
// 计算显示哪个倒计时(选择剩余时间最短的,但一旦选择就锁定直到结束)
|
||||
const displayType = computed(() => {
|
||||
const times = [];
|
||||
|
||||
//检查新用户倒计时
|
||||
// if (isNewUser && newUserTime.value.countdownSec > 0 && !isVip) {
|
||||
// times.push({ type: 'newUser', sec: newUserTime.value.countdownSec });
|
||||
// }
|
||||
|
||||
// // 检查限时特惠倒计时
|
||||
// if (specialOfferTime.value.countdownSec > 0 && !isVip) {
|
||||
// times.push({ type: 'specialOffer', sec: specialOfferTime.value.countdownSec });
|
||||
// }
|
||||
|
||||
// 检查活动倒计时
|
||||
if (
|
||||
configStore.appConfig.bannerJump &&
|
||||
configStore.appConfig.bannerJump[1]?.banner &&
|
||||
activeTime.value.countdownSec > 0
|
||||
) {
|
||||
times.push({ type: 'activity', sec: activeTime.value.countdownSec });
|
||||
}
|
||||
|
||||
// if (isVip && userInfo.vipLevel == 1) {
|
||||
// times.push({ type: 'vipBox', sec: 0 });
|
||||
// }
|
||||
|
||||
// 如果没有有效的倒计时,返回 null
|
||||
if (times.length === 0) {
|
||||
lockedDisplayType.value = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果当前锁定的类型仍然有效,继续使用它
|
||||
if (lockedDisplayType.value) {
|
||||
const lockedType = times.find((t) => t.type === lockedDisplayType.value);
|
||||
if (lockedType && lockedType.sec > 0) {
|
||||
return lockedDisplayType.value;
|
||||
}
|
||||
// 锁定的类型已失效,清除锁定
|
||||
lockedDisplayType.value = null;
|
||||
}
|
||||
|
||||
// 返回剩余时间最短的倒计时类型,并锁定它
|
||||
times.sort((a, b) => a.sec - b.sec);
|
||||
const selectedType = times[0].type;
|
||||
lockedDisplayType.value = selectedType;
|
||||
return selectedType;
|
||||
});
|
||||
|
||||
|
||||
const handleToMember = (vipCardId?: string) => {
|
||||
router.push(`/member${vipCardId ? `?id=${vipCardId}` : ''}`);
|
||||
};
|
||||
|
||||
// 处理特惠 banner 点击事件
|
||||
const handleSpecialOfferClick = () => {
|
||||
// 根据特惠类型获取对应的 VIP 卡 ID
|
||||
const vipCardId =
|
||||
props.specialOfferType === 'newbie'
|
||||
? configStore.appConfig?.specialBannerVipCardId
|
||||
: configStore.appConfig?.backBannerVipCardId;
|
||||
|
||||
emit('openSpecialOffer', vipCardId);
|
||||
};
|
||||
|
||||
// 监听 props 变化,重新初始化倒计时
|
||||
watch(
|
||||
() => props.newUserCountdownSec,
|
||||
(newVal) => {
|
||||
if (newVal !== newUserTime.value.countdownSec) {
|
||||
clearTimeout(newUserTimer);
|
||||
newUserTime.value.countdownSec = newVal;
|
||||
startNewUserCountdown();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.activeCountdownSec,
|
||||
(newVal) => {
|
||||
if (newVal !== activeTime.value.countdownSec) {
|
||||
clearTimeout(activeTimer);
|
||||
activeTime.value.countdownSec = newVal;
|
||||
startActiveCountdown();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.specialOfferCountdownMs,
|
||||
(newVal) => {
|
||||
const newSec = Math.floor(newVal / 1000);
|
||||
if (newSec !== specialOfferTime.value.countdownSec) {
|
||||
clearTimeout(specialOfferTimer);
|
||||
specialOfferTime.value.countdownSec = newSec;
|
||||
startSpecialOfferCountdown();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
paymentCountdownSec,
|
||||
(newSec) => {
|
||||
if (newSec !== paymentPlayPageTime.value.countdownSec) {
|
||||
clearTimeout(paymentPlayPageTimer);
|
||||
paymentPlayPageTime.value.countdownSec = newSec;
|
||||
startPaymentPlayPageCountdown();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => configStore.appConfig?.paymentStatusPopupConfig?.lastDiscountTime,
|
||||
() => {
|
||||
paymentCountdownExpired.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
// 组件挂载时启动倒计时
|
||||
onMounted(() => {
|
||||
startNewUserCountdown();
|
||||
startActiveCountdown();
|
||||
startSpecialOfferCountdown();
|
||||
});
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
clearTimeout(newUserTimer);
|
||||
clearTimeout(activeTimer);
|
||||
clearTimeout(oldReturnTimer);
|
||||
clearTimeout(specialOfferTimer);
|
||||
clearTimeout(paymentPlayPageTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bannerBox {
|
||||
height: 46px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.bannerImg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.timeBox {
|
||||
height: 22px;
|
||||
width: 110px;
|
||||
border-radius: 22px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 16px;
|
||||
transform: translate(0, -50%);
|
||||
z-index: 1;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.newUserBg {
|
||||
height: 46px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
padding-left: 82px;
|
||||
padding-right: 12px;
|
||||
color: #e24359;
|
||||
position: relative;
|
||||
.newUserBannerImg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.hour,
|
||||
.minute,
|
||||
.second {
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
background: rgba(27, 27, 27, 1);
|
||||
border-radius: 3px;
|
||||
border: 0.5px solid rgba(255, 227, 129, 1);
|
||||
}
|
||||
.hour {
|
||||
left: 162px;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
.minute {
|
||||
left: 194.5px;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
.second {
|
||||
left: 227px;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
.nowOpenBtn {
|
||||
height: 21px;
|
||||
width: 64px;
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.hourColon{
|
||||
position: absolute;
|
||||
left: 187px;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
color: #fff;
|
||||
}
|
||||
.minuteColon{
|
||||
position: absolute;
|
||||
left: 219px;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.specialOfferBg {
|
||||
.hour,
|
||||
.minute,
|
||||
.second {
|
||||
// color: rgba(123, 0, 0, 1);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.paymentPlayPageBg {
|
||||
.hour,
|
||||
.minute,
|
||||
.second {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.vipBox {
|
||||
height: 46px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding-right: 12px;
|
||||
color: rgba(255, 190, 93, 1);
|
||||
position: relative;
|
||||
background: url('@/assets/images/png/up_vip_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
.vipIcon {
|
||||
width: 15px;
|
||||
height: 14px;
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.vipContent {
|
||||
padding-left: 30px;
|
||||
.vipTime {
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.vipName {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.vipBtn {
|
||||
width: 72px;
|
||||
height: 24px;
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="img-decypt">
|
||||
<van-image :src="decyImg" class="vanImage" :fit="fit" :round="round" @load="emit('load')" @error="emit('error')">
|
||||
<template v-slot:loading>
|
||||
<div class="loading" v-if="!isSplash" />
|
||||
<div class="splash-loading-img" v-else></div>
|
||||
</template>
|
||||
<template v-slot:error>
|
||||
<div class="loading" v-if="!isSplash" />
|
||||
<div class="splash-error-img" v-else></div>
|
||||
</template>
|
||||
<slot></slot>
|
||||
</van-image>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Image as VanImage } from 'vant';
|
||||
import { ref, watch, type PropType } from 'vue';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import AppApi from '@/service/app';
|
||||
const props = defineProps({
|
||||
fit: {
|
||||
type: String as PropType<'cover' | 'contain'>,
|
||||
default: 'cover',
|
||||
},
|
||||
//图片地址
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
default() {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
isNeedDecypt: {
|
||||
//是否需要解密
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
isSplash: {
|
||||
//是否来自启动页
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
round: {
|
||||
//图片是否显示为圆形
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
});
|
||||
let decyImg = ref('');
|
||||
const configStore = useConfigStore();
|
||||
const emit = defineEmits(['load', 'error']);
|
||||
|
||||
const init = () => {
|
||||
decyImg.value = '';
|
||||
if (!props.src) {
|
||||
return;
|
||||
}
|
||||
if (!props.isNeedDecypt) {
|
||||
decyImg.value = props.src;
|
||||
return;
|
||||
}
|
||||
if (/^(blob:|data:)/.test(props.src)) {
|
||||
decyImg.value = props.src;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const isHttp = /^https?:\/\//.test(props.src);
|
||||
const imgSrc = isHttp ? props.src : configStore.sourceInfo.IMAGE + '/' + props.src;
|
||||
AppApi.imgDecypt(imgSrc)
|
||||
.then((url: any) => {
|
||||
decyImg.value = url;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('imgDecypt error', err);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('imgDecypt catch', error);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.src, props.isNeedDecypt],
|
||||
() => {
|
||||
init();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.van-image__loading) {
|
||||
// background-color: #2d2d2d !important;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.splash-loading-img,
|
||||
.splash-error-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('../../assets/images/webp/splash.webp') no-repeat;
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.img-decypt {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.vanImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// background-color: #2d2d2d !important;
|
||||
|
||||
.loading {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('./../../assets/images/png/placeholder.png') no-repeat;
|
||||
background-size: 50% auto;
|
||||
background-position: 50% 50%;
|
||||
background-color: #343434;
|
||||
}
|
||||
|
||||
:deep(.van-image__error) {
|
||||
// background-color: #2d2d2d !important;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<van-popup v-model:show="showDrawer" position="right" z-index="99999" @close="close">
|
||||
<div class="drawerBox">
|
||||
<div class="title-box flex-center-between">
|
||||
导航列表
|
||||
<span class="reset" @click.stop="resetList">恢复默认</span>
|
||||
</div>
|
||||
<div class="tips">长按拖动排序</div>
|
||||
|
||||
<draggable
|
||||
class="menuBox overflow-y-auto"
|
||||
v-model="list"
|
||||
@start="drag = true"
|
||||
@end="drag = false"
|
||||
@change="change"
|
||||
item-key="id"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<div
|
||||
class="menu"
|
||||
:class="activeIndex == index ? 'menued' : undefined"
|
||||
@click.stop="goTab(index, element.moduleName)"
|
||||
>
|
||||
{{ element.moduleName }}
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, nextTick } from 'vue';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
import draggable from 'vuedraggable-es';
|
||||
const props = defineProps({
|
||||
elType: String,
|
||||
});
|
||||
const globalStore = useGlobalStore();
|
||||
const list = ref<Array<Types.Home.HomeTabItem>>([]);
|
||||
switch (props.elType) {
|
||||
case 'home':
|
||||
list.value = [...globalStore.homePageList];
|
||||
break;
|
||||
case 'darkNet':
|
||||
list.value = [...globalStore.darkNetPageList];
|
||||
break;
|
||||
}
|
||||
|
||||
const showDrawer = computed(() => {
|
||||
let show = false;
|
||||
switch (props.elType) {
|
||||
case 'home':
|
||||
show = globalStore.showHomeDrawer;
|
||||
break;
|
||||
case 'darkNet':
|
||||
show = globalStore.showDarkNetDrawer;
|
||||
break;
|
||||
}
|
||||
return show;
|
||||
});
|
||||
|
||||
const activeIndex = computed(() => {
|
||||
let index;
|
||||
switch (props.elType) {
|
||||
case 'home':
|
||||
index = globalStore.activeIndex;
|
||||
break;
|
||||
case 'darkNet':
|
||||
index = globalStore.darkNetActiveIndex;
|
||||
break;
|
||||
}
|
||||
return index;
|
||||
});
|
||||
|
||||
const goTab = (index: number, title: any) => {
|
||||
switch (props.elType) {
|
||||
case 'home':
|
||||
globalStore.setPreIndex(globalStore.activeIndex, props.elType as string);
|
||||
break;
|
||||
case 'darkNet':
|
||||
globalStore.setPreIndex(globalStore.preDarkNetIndex, props.elType as string);
|
||||
break;
|
||||
}
|
||||
globalStore.setActiveIndex(index, props.elType as string);
|
||||
nextTick(() => {
|
||||
close();
|
||||
});
|
||||
};
|
||||
|
||||
const drag = ref(false);
|
||||
let isChange = false;
|
||||
const change = (move: any) => {
|
||||
isChange = true;
|
||||
};
|
||||
const close = () => {
|
||||
globalStore.setShowDrawer(false, props.elType as string);
|
||||
if (isChange) {
|
||||
switch (props.elType) {
|
||||
case 'home':
|
||||
localStorage.setItem('homePageIds', list.value.map((item: any) => item.id).join(','));
|
||||
break;
|
||||
case 'darkNet':
|
||||
localStorage.setItem('darkNetPageIds', list.value.map((item: any) => item.id).join(','));
|
||||
break;
|
||||
}
|
||||
globalStore.updateHomePageList(list.value, props.elType as string);
|
||||
}
|
||||
};
|
||||
const resetList = () => {
|
||||
const { homePage, deepWeb } = globalStore.tabObj;
|
||||
switch (props.elType) {
|
||||
case 'home':
|
||||
if (homePage) {
|
||||
list.value = homePage || [];
|
||||
}
|
||||
break;
|
||||
case 'darkNet':
|
||||
if (homePage) {
|
||||
list.value = deepWeb || [];
|
||||
}
|
||||
break;
|
||||
}
|
||||
nextTick(() => {
|
||||
close();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.van-popup) {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
:deep(.van-overlay) {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.drawerBox {
|
||||
width: 218px;
|
||||
height: 100vh;
|
||||
background-color: var(--app-surface);
|
||||
backdrop-filter: blur(6.5px);
|
||||
padding: 56px 10px 0px;
|
||||
.title-box {
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
.reset {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
}
|
||||
.tips {
|
||||
margin-top: 8px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 18px; /* 150% */
|
||||
}
|
||||
|
||||
.menuBox {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-column-gap: 6px;
|
||||
grid-row-gap: 10px;
|
||||
max-height: 100%;
|
||||
padding-top: 30px;
|
||||
padding-bottom: 80px;
|
||||
.menu {
|
||||
height: 40px;
|
||||
width: 84px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 0 12px rgba(15, 15, 15, 0.05);
|
||||
}
|
||||
.menued {
|
||||
color: var(--app-primary-text);
|
||||
background: var(--app-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<div class="richText" @click.stop="delegateClick">
|
||||
<template v-for="(node, index) in parsedContent" :key="index">
|
||||
<!-- 动态渲染组件 -->
|
||||
<component v-if="node.type === 'component'" :is="node.name" v-bind="node.props" @lz-image-click="lzImageClick" />
|
||||
|
||||
<!-- 渲染普通 HTML -->
|
||||
<div class="text_box" v-else v-html="node.html" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import LazyImg from '../lazyImg/index.vue';
|
||||
import { innerJump, compatibleAppStoreJump } from '@/utils/index';
|
||||
// Props
|
||||
const props = defineProps({
|
||||
html: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 解析 HTML 内容
|
||||
const parseHtml = (html: string) => {
|
||||
const regex = /<img\s+[^>]*src="([^"]+)"[^>]*>/g;
|
||||
const segments: Array<{ type: 'div' | 'component'; html?: string; name?: any; props?: any }> = [];
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
// 遍历匹配 <img> 标签
|
||||
while ((match = regex.exec(html))) {
|
||||
// 保存普通 HTML
|
||||
if (match.index > lastIndex) {
|
||||
segments.push({
|
||||
type: 'div',
|
||||
html: html.slice(lastIndex, match.index),
|
||||
});
|
||||
}
|
||||
|
||||
// 替换 img 为 LazyImg 组件
|
||||
segments.push({
|
||||
type: 'component',
|
||||
name: LazyImg,
|
||||
props: { src: match[1], borderRadius: 8 },
|
||||
});
|
||||
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
|
||||
// 添加剩余的普通 HTML
|
||||
if (lastIndex < html.length) {
|
||||
segments.push({
|
||||
type: 'div',
|
||||
html: html.slice(lastIndex),
|
||||
});
|
||||
}
|
||||
|
||||
return segments;
|
||||
};
|
||||
const updatedText = ref(props.html);
|
||||
// a标题标签替换为span标签
|
||||
updatedText.value = updatedText.value.replace(
|
||||
/<a href="(.*?)">(.*?)<\/a>/g,
|
||||
`<span style="color: #ffc000;" data-href="$1">$2</span>`,
|
||||
);
|
||||
|
||||
// 正则替换 font 为 span
|
||||
updatedText.value = updatedText.value.replace(/<font([^>]*)>(.*?)<\/font>/gi, (match, attributes, content) => {
|
||||
// 提取 font 标签的各属性并构造 style
|
||||
const inlineStyles: string[] = [];
|
||||
// 提取颜色
|
||||
const colorMatch = attributes.match(/color=["'](.*?)["']/i);
|
||||
if (colorMatch) {
|
||||
inlineStyles.push(`color:${colorMatch[1]}`);
|
||||
}
|
||||
// 提取字体大小
|
||||
const sizeMatch = attributes.match(/size=["'](.*?)["']/i);
|
||||
if (sizeMatch) {
|
||||
const fontSizeMap: any = {
|
||||
'1': '14',
|
||||
'2': '17',
|
||||
'3': '20',
|
||||
'4': '23',
|
||||
'5': '26',
|
||||
'6': '29',
|
||||
'7': '32',
|
||||
};
|
||||
inlineStyles.push(`font-size:${fontSizeMap[sizeMatch[1]] || sizeMatch[1]}px`);
|
||||
}
|
||||
|
||||
// 提取字体样式(font-family)
|
||||
const faceMatch = attributes.match(/face=["'](.*?)["']/i);
|
||||
if (faceMatch) {
|
||||
inlineStyles.push(`font-family:${faceMatch[1]}`);
|
||||
}
|
||||
|
||||
// 提取现有 style 属性
|
||||
const styleMatch = attributes.match(/style=["'](.*?)["']/i);
|
||||
if (styleMatch) {
|
||||
inlineStyles.push(styleMatch[1]);
|
||||
}
|
||||
|
||||
// 合并生成最终的 style 属性
|
||||
const finalStyle = inlineStyles.join('; ');
|
||||
|
||||
// 返回新的 span 标签
|
||||
return `<span style="${finalStyle}">${content}</span>`;
|
||||
});
|
||||
|
||||
// 解析后的内容
|
||||
const parsedContent = computed(() => parseHtml(updatedText.value));
|
||||
|
||||
// 处理点击事件
|
||||
const emit = defineEmits(['lz-image-click']);
|
||||
const lzImageClick = (src: string) => {
|
||||
emit('lz-image-click', src);
|
||||
};
|
||||
|
||||
//span 标签跳转
|
||||
const delegateClick = (event: any) => {
|
||||
const target = event.target.closest('span[data-href]');
|
||||
if (event.target.tagName === 'SPAN' && target) {
|
||||
const href = target.getAttribute('data-href');
|
||||
console.log(href);
|
||||
const index = href.indexOf('yinseinner://');
|
||||
if (index != -1) {
|
||||
innerJump(href.substring(13));
|
||||
return;
|
||||
}
|
||||
compatibleAppStoreJump(href);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.richText {
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: rgb(206, 206, 206);
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
:deep(a) {
|
||||
color: #3e64ea;
|
||||
}
|
||||
:deep(b) {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
:deep(.lazy_img) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.text_box {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
:deep(h1),
|
||||
:deep(h2),
|
||||
:deep(h3),
|
||||
:deep(h4),
|
||||
:deep(h5),
|
||||
:deep(h6) {
|
||||
all: revert;
|
||||
color: #fff;
|
||||
}
|
||||
:deep(i) {
|
||||
all: revert;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="empty_warp">
|
||||
<div class="empty_box">
|
||||
<div class="noDataIcon"></div>
|
||||
<div class="noData">暂无数据</div>
|
||||
<p>请稍后再试~</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.empty_warp {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 60px;
|
||||
padding-bottom: 120px;
|
||||
.noDataIcon {
|
||||
width: 170px;
|
||||
height: 120px;
|
||||
background: url('./../../assets/images/png/nodata3.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.noData {
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
color: rgb(102, 102, 102);
|
||||
}
|
||||
p {
|
||||
padding-top: 8px;
|
||||
text-align: center;
|
||||
color: rgb(153, 153, 153);
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<van-overlay :show="isShow" z-index="999">
|
||||
<div class="warp">
|
||||
<div class="content_warp app-popup-surface">
|
||||
<div class="title"></div>
|
||||
<div class="content app-popup-body">
|
||||
<h2>APP线路检测失败</h2>
|
||||
<h3>解决方案:</h3>
|
||||
<p>1.请检测您的网络是否正常,手机是否开了VPN或10分钟后再尝试访问。</p>
|
||||
<p>
|
||||
2.前往官网地址
|
||||
<span @click="download2">91PORN01.CC</span>
|
||||
下载最新版。
|
||||
</p>
|
||||
<p>
|
||||
官方备用:
|
||||
<span @click="download">91PORN01.CC</span>
|
||||
</p>
|
||||
<p>3.前往Telegram官方交流群</p>
|
||||
<div class="footer-box">
|
||||
<div class="btn app-popup-primary" @click="toPotato">前往群聊</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { compatibleAppStoreJump } from '@/utils/index';
|
||||
const isShow = ref(true);
|
||||
const download = () => {
|
||||
compatibleAppStoreJump('https://91porn01.cc');
|
||||
};
|
||||
const download1 = () => {
|
||||
compatibleAppStoreJump('https://lolii.fm');
|
||||
};
|
||||
const download2 = () => {
|
||||
compatibleAppStoreJump('https://91porn01.cc');
|
||||
};
|
||||
const toPotato = () => {
|
||||
compatibleAppStoreJump('https://t.me/hougong9527');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.warp {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.content_warp {
|
||||
width: 311px;
|
||||
background: url('@/assets/images/png/systemPromptBg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
border-radius: 12px;
|
||||
padding: 30px 0 10px;
|
||||
.title {
|
||||
height: 24px;
|
||||
width: 96.6px;
|
||||
background: url('@/assets/images/png/systemPromptTitle.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
font-size: 14px;
|
||||
border-radius: 20px;
|
||||
padding: 18px 22px 27px;
|
||||
color: #fff;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
font-family: 'PingFang SC';
|
||||
h2 {
|
||||
color: #f52c56;
|
||||
line-height: 20px;
|
||||
}
|
||||
h3 {
|
||||
margin: 12px 0;
|
||||
line-height: 20px;
|
||||
}
|
||||
p {
|
||||
span {
|
||||
color: #3476ff;
|
||||
}
|
||||
}
|
||||
.footer-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.btn {
|
||||
font-family: 'PingFang SC';
|
||||
height: 42px;
|
||||
width: 230px;
|
||||
background: #f68804;
|
||||
border-radius: 3px;
|
||||
text-align: center;
|
||||
line-height: 42px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin: 19px auto 0;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="fiveGridAdv">
|
||||
<div class="fiveGrid" @touchstart.stop @touchmove.stop @touchend.stop v-if="state.advList.length > 0" ref="advRef">
|
||||
<div class="rectAdvBox" @click="handleJumpAdv(item)" v-for="item in state.advList" :key="item.id">
|
||||
<lazyImg class="rectAdvIcon" :src="item.cover" />
|
||||
<div class="rectName">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { nextTick, reactive, ref, watch } from 'vue';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
import { getAdItem } from '@/utils/getConfig';
|
||||
import { AdImpressionType, setAdImpression } from '@/utils/eventReporter/adImpression';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reportAdClickEvent } from '@/utils/eventReporter/adClick';
|
||||
|
||||
const route = useRoute();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
advType: number;
|
||||
adImpression?: AdImpressionType;
|
||||
}>(),
|
||||
{
|
||||
advType: 2,
|
||||
},
|
||||
);
|
||||
const state = reactive({
|
||||
advList: <any>(getAdItem(props.advType) || []), // 广告数据
|
||||
});
|
||||
// 广告展示埋点
|
||||
const advRef = ref();
|
||||
watch(
|
||||
() => state.advList,
|
||||
() => {
|
||||
if (state.advList.length) {
|
||||
nextTick(() => {
|
||||
setAdImpression(advRef.value, state.advList, {
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'small_grid',
|
||||
}).setupObserver();
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const handleJumpAdv = (item: Types.App.AdsInfoListItem) => {
|
||||
let params = {
|
||||
...item,
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'small_grid',
|
||||
};
|
||||
reportAdClickEvent(params);
|
||||
jumpAdv(item);
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.fiveGridAdv {
|
||||
.fiveGrid {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
margin: 0 0 18px;
|
||||
padding-left: 10px;
|
||||
.rectAdvBox {
|
||||
width: 60px;
|
||||
margin-right: 6px;
|
||||
.rectAdvIcon {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rectName {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.rectAdvBox:last-child {
|
||||
margin-right: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="fiveGridAdv">
|
||||
<div class="fiveGrid" v-if="advList.length > 0" ref="advRef">
|
||||
<div class="rectAdvBox" @click="handleJumpAdv(item)" v-for="item in advList" :key="item.id">
|
||||
<lazyImg class="rectAdvIcon" :src="item.cover" />
|
||||
<div class="rectName">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
import { AdImpressionType, setAdImpression } from '@/utils/eventReporter/adImpression';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reportAdClickEvent } from '@/utils/eventReporter/adClick';
|
||||
|
||||
const route = useRoute();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
advList: any;
|
||||
adImpression?: AdImpressionType;
|
||||
}>(),
|
||||
{
|
||||
advList: [],
|
||||
},
|
||||
);
|
||||
// 广告展示埋点
|
||||
const advRef = ref();
|
||||
watch(
|
||||
() => props.advList,
|
||||
() => {
|
||||
if (props.advList.length) {
|
||||
nextTick(() => {
|
||||
setAdImpression(advRef.value, props.advList, {
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'small_grid',
|
||||
}).setupObserver();
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const handleJumpAdv = (item: Types.App.AdsInfoListItem) => {
|
||||
let params = {
|
||||
...item,
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'small_grid',
|
||||
};
|
||||
reportAdClickEvent(params);
|
||||
jumpAdv(item);
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.fiveGridAdv {
|
||||
.fiveGrid {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
margin: 18px 0;
|
||||
.rectAdvBox {
|
||||
width: 60px;
|
||||
margin-right: 12px;
|
||||
.rectAdvIcon {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rectName {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.rectAdvBox:last-child {
|
||||
margin-right: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<!-- 浮窗 -->
|
||||
<div
|
||||
class="square"
|
||||
id="moveDiv"
|
||||
:class="{ closeSquare: state.isClose, squareHome: state.isHome }"
|
||||
@mousedown="down()"
|
||||
@touchstart="down()"
|
||||
@mousemove.prevent.stop="move()"
|
||||
@touchmove.prevent.stop="move()"
|
||||
@mouseup="end()"
|
||||
@touchend="end()"
|
||||
@click="jumpUrl()"
|
||||
v-if="state.advObj"
|
||||
>
|
||||
<LazyImg class="squareImg" :src="state.advObj?.cover" />
|
||||
<div class="close" @click.stop="close" v-if="!state.isClose && state.showClose"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue';
|
||||
import { AdType } from '@/enums/app';
|
||||
import { getAdItem } from '@/utils/getConfig';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
showClose?: boolean;
|
||||
isHome?: boolean;
|
||||
}>(),
|
||||
{
|
||||
showClose: () => true,
|
||||
isHome: () => false,
|
||||
},
|
||||
);
|
||||
const state = reactive({
|
||||
showClose: props.showClose,
|
||||
isHome: props.isHome,
|
||||
flags: false,
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
nx: 0,
|
||||
ny: 0,
|
||||
dx: 0,
|
||||
dy: 0,
|
||||
xPum: 0,
|
||||
yPum: 0,
|
||||
advObj: <any>{},
|
||||
isClose: false,
|
||||
});
|
||||
// 初始化广告
|
||||
init();
|
||||
function init() {
|
||||
state.advObj = getAdItem(AdType.FLOAT)[0];
|
||||
}
|
||||
// 实现移动端拖拽
|
||||
function down() {
|
||||
state.flags = true;
|
||||
let touch;
|
||||
if ((event as any).touches) {
|
||||
touch = (event as any).touches[0];
|
||||
} else {
|
||||
touch = event;
|
||||
}
|
||||
state.position.x = touch.clientX;
|
||||
state.position.y = touch.clientY;
|
||||
state.dx = (document.getElementById('moveDiv') as HTMLElement).offsetLeft;
|
||||
state.dy = (document.getElementById('moveDiv') as HTMLElement).offsetTop;
|
||||
}
|
||||
function move() {
|
||||
if (state.flags) {
|
||||
let touch;
|
||||
if ((event as any).touches) {
|
||||
touch = (event as any).touches[0];
|
||||
} else {
|
||||
touch = event;
|
||||
}
|
||||
state.nx = touch.clientX - state.position.x;
|
||||
state.ny = touch.clientY - state.position.y;
|
||||
state.xPum = state.dx + state.nx;
|
||||
state.yPum = state.dy + state.ny;
|
||||
//添加限制:只允许在屏幕内拖动
|
||||
const maxWidth = document.body.clientWidth - 90; //屏幕宽度减去悬浮框宽高
|
||||
const maxHeight = document.body.clientHeight - 90;
|
||||
if (state.xPum < 0) {
|
||||
//屏幕x限制
|
||||
state.xPum = 0;
|
||||
} else if (state.xPum > maxWidth) {
|
||||
state.xPum = maxWidth;
|
||||
}
|
||||
if (state.yPum < 0) {
|
||||
//屏幕y限制
|
||||
state.yPum = 0;
|
||||
} else if (state.yPum > maxHeight - 100) {
|
||||
state.yPum = maxHeight - 100;
|
||||
}
|
||||
(document.getElementById('moveDiv') as HTMLElement).style.left = state.xPum + 'px';
|
||||
(document.getElementById('moveDiv') as HTMLElement).style.top = state.yPum + 'px';
|
||||
//阻止页面的滑动默认事件
|
||||
// document.addEventListener(
|
||||
// "touchmove",
|
||||
// function () {
|
||||
// // 1.2 如果碰到滑动问题,请注意是否获取到 touchmove
|
||||
// (event as any).preventDefault(); //jq 阻止冒泡事件
|
||||
// // event.stopPropagation(); // 如果没有引入jq 就用 stopPropagation()
|
||||
// },
|
||||
// { passive: true }
|
||||
// );
|
||||
}
|
||||
}
|
||||
//鼠标释放时候的函数
|
||||
function end() {
|
||||
state.flags = false;
|
||||
}
|
||||
function close() {
|
||||
state.isClose = true;
|
||||
}
|
||||
function jumpUrl() {
|
||||
if (state.isClose) return (state.isClose = false);
|
||||
jumpAdv(state.advObj);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.square {
|
||||
height: 90px;
|
||||
width: 90px;
|
||||
z-index: 1000;
|
||||
position: fixed;
|
||||
top: 50px;
|
||||
left: 5px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
:deep(img) {
|
||||
border-radius: 10px;
|
||||
}
|
||||
.squareImg {
|
||||
height: 90px;
|
||||
width: 90px;
|
||||
}
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
border-radius: 50%;
|
||||
background: url('../../assets/images/png/appsClose.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
.squareHome {
|
||||
top: unset;
|
||||
left: unset;
|
||||
bottom: 100px;
|
||||
right: 14px;
|
||||
}
|
||||
.closeSquare {
|
||||
width: 20px;
|
||||
border-radius: 20px;
|
||||
.squareImg {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="fullScreenGallery">
|
||||
<van-swipe
|
||||
ref="swipeRef"
|
||||
class="my-swipe"
|
||||
indicator-color="white"
|
||||
:loop="isLoop"
|
||||
:autoplay="autoTime"
|
||||
:lazy-render="true"
|
||||
@change="change"
|
||||
>
|
||||
<van-swipe-item class="my-swier-item" v-for="url in videoInfo.seriesCover" :key="url" @click="jumpAdv(videoInfo)">
|
||||
<div class="fullScreen_item" @touchstart="touchstart" @touchmove="touchmove">
|
||||
<AnimationImg :src="url" />
|
||||
</div>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { SwipeInstance } from 'vant';
|
||||
import { ref } from 'vue';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
|
||||
const props = defineProps<{
|
||||
videoInfo: Types.Video.VInfosItem;
|
||||
}>();
|
||||
|
||||
const autoTime = ref(2000);
|
||||
const isLoop = ref(true);
|
||||
|
||||
const swipeRef = ref<SwipeInstance>();
|
||||
|
||||
function change(index: number) {
|
||||
console.log(index);
|
||||
}
|
||||
const startX = ref();
|
||||
const startY = ref();
|
||||
function touchstart(e: any) {
|
||||
// 如果你要阻止点击事件,请反注释下一行代码
|
||||
// e.preventDefault()
|
||||
startX.value = e.touches[0].clientX;
|
||||
startY.value = e.touches[0].clientY;
|
||||
}
|
||||
function touchmove(e: any) {
|
||||
// e.preventDefault()
|
||||
let moveX = e.touches[0].clientX;
|
||||
if (startX.value - moveX != 0) {
|
||||
// 右滑触发
|
||||
// do something
|
||||
isLoop.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.van-swipe__indicator) {
|
||||
width: 28px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
:deep(.van-swipe__indicators) {
|
||||
bottom: 1%;
|
||||
}
|
||||
.fullScreenGallery {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.my-swipe,
|
||||
.van-swipe-item,
|
||||
.my-swier-item {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.fullScreen_item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
:deep(.van-image) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #000000 !important;
|
||||
}
|
||||
:deep(img) {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,339 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<van-popup :show="isShow" z-index="2001" position="bottom" @click-overlay="emit('close')">
|
||||
<div class="wrapper flex-center-center">
|
||||
<div class="content" :class="[props.theme]" @click.stop="void 0">
|
||||
<div class="line_top" />
|
||||
<div class="title">{{ props.title }}</div>
|
||||
<div class="priceList">
|
||||
<div class="leader">作品解锁</div>
|
||||
<div class="price gold flex-vcenter">
|
||||
<!-- <SvgIcon name="gold" /> -->
|
||||
{{ gold }} 金币
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="props.isSale">
|
||||
<div class="priceList">
|
||||
<div class="leader">作品原价</div>
|
||||
<div class="price through flex-vcenter">{{ props.videoInfo.originCoins }}金币</div>
|
||||
</div>
|
||||
<div v-if="userInfo.isVip" class="priceList vip">
|
||||
<div class="leader">您当前享受{{ sale }}折扣优惠</div>
|
||||
<div class="price underline flex-vcenter" @click.stop="$router.push('member'), emit('close')">
|
||||
升级会员
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="priceList vip">
|
||||
<div class="leader">您当前不享受免费观看</div>
|
||||
<div class="price underline flex-vcenter" @click.stop="$router.push('member'), emit('close')">
|
||||
购买VIP享免费观看
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="priceList split">
|
||||
<div class="leader">我的金币</div>
|
||||
<div class="price flex-vcenter">
|
||||
{{ recharge }}
|
||||
<!-- 100 -->
|
||||
<span class="go" @click.stop="$router.push('wallet'), emit('close')">充值</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="couponBox" v-if="userGoldVideoCoupon.length">
|
||||
<div
|
||||
class="couponItem flex-center-between"
|
||||
:class="index === changeGoldVideoCouponIndex ? 'active' : ''"
|
||||
v-for="(item, index) in userGoldVideoCoupon"
|
||||
:key="index"
|
||||
@click.stop="selectCoupon(item, index)"
|
||||
>
|
||||
<div>观影券({{ item.num }}金币)</div>
|
||||
<div class="count">剩余{{ item.newcount }}张</div>
|
||||
<div class="checkbox"></div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="btnBox flex-center-between">
|
||||
<div class="btn flex-center-center" :class="!showVipBtn ? 'themeColor' : ''" @click="unlockNow">
|
||||
{{ !showVipBtn ? `${gold}金币/立即支付` : `${gold}金币支付` }}
|
||||
</div>
|
||||
<div v-if="showVipBtn" class="btn vip-btn flex-center-center" @click.stop="clickVipHandle">
|
||||
开通会员免费看
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</Teleport>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import VideoApi from '@/service/video';
|
||||
import { ProductType } from '@/enums/app';
|
||||
import { showToast } from 'vant';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
videoInfo: Types.Video.VInfosItem;
|
||||
isOpen: boolean;
|
||||
productType?: ProductType;
|
||||
theme?: string; //black 为黑色主题,默认为白色主题
|
||||
title?: string; //弹窗标题
|
||||
isSale?: boolean; //是否显示折扣区
|
||||
showVipBtn?: boolean; // 是否显示购买vip按钮
|
||||
membershipActivation?: boolean;
|
||||
}>(),
|
||||
{
|
||||
title: '购买作品',
|
||||
theme: 'black',
|
||||
productType: ProductType.VIDEO,
|
||||
isSale: true,
|
||||
showVipBtn: false,
|
||||
membershipActivation: false,
|
||||
},
|
||||
);
|
||||
const userStore = useUserStore();
|
||||
const userInfo = userStore.userInfo;
|
||||
|
||||
const router = useRouter();
|
||||
const { openLoading, closeLoading } = useLoading();
|
||||
const emit = defineEmits(['close', 'buySuccess', 'membershipActivationShow']);
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
// const changeGoldVideoCouponIndex = ref(-1);
|
||||
// const userGoldVideoCoupon = computed(() => {
|
||||
// const mergedData: any = [];
|
||||
// (userInfo.userGoldVideoCoupon || [])
|
||||
// .filter((item: any) => !item.used)
|
||||
// .forEach((item: any) => {
|
||||
// const existingItem = mergedData.find((element: any) => element.num === item.num);
|
||||
// if (existingItem) {
|
||||
// existingItem.newcount += 1;
|
||||
// } else {
|
||||
// item.newcount = 1;
|
||||
// mergedData.push(item);
|
||||
// }
|
||||
// });
|
||||
// return mergedData;
|
||||
// });
|
||||
const gold = computed(() => {
|
||||
// return userInfo.isVip ? props.videoInfo.coins : props.videoInfo.originCoins;
|
||||
return props.isSale ? props.videoInfo.coins : props.videoInfo.originCoins;
|
||||
});
|
||||
|
||||
const sale = computed(() => {
|
||||
return userStore.userInfo.payVidDiscount;
|
||||
});
|
||||
|
||||
const recharge = computed(() => {
|
||||
const balance = ((userStore.walletInfo.amount || 0) * 100 + (userStore.walletInfo.income || 0) * 100) / 100;
|
||||
return balance === 0 ? '0' : balance.toFixed(1);
|
||||
});
|
||||
// const selectCoupon = (item: any, index: number) => {
|
||||
// if (changeGoldVideoCouponIndex.value === index) {
|
||||
// changeGoldVideoCouponIndex.value = -1;
|
||||
// return;
|
||||
// }
|
||||
// let money = gold.value;
|
||||
// if (item.num < money) return showToast('观影券面值过小,请选择其他支付方式');
|
||||
// changeGoldVideoCouponIndex.value = index;
|
||||
// let productID = props.videoInfo.id;
|
||||
// if (!productID) {
|
||||
// showToast('购买出错~');
|
||||
// return;
|
||||
// }
|
||||
// };
|
||||
|
||||
const clickVipHandle = () => {
|
||||
emit('close');
|
||||
if (props.membershipActivation) {
|
||||
emit('membershipActivationShow');
|
||||
return;
|
||||
}
|
||||
router.push('/member');
|
||||
};
|
||||
|
||||
const unlockNow = () => {
|
||||
try {
|
||||
openLoading();
|
||||
// const money =
|
||||
// changeGoldVideoCouponIndex.value > -1 ? userGoldVideoCoupon.value[changeGoldVideoCouponIndex.value].num : 0;
|
||||
let productID = props.videoInfo.id;
|
||||
if (!productID) {
|
||||
showToast('购买出错~');
|
||||
return;
|
||||
}
|
||||
VideoApi.videoBuying({
|
||||
productType: props.productType, //props.productType ||
|
||||
productID: productID,
|
||||
goldVideoCouponNum: 0, //money || 0,
|
||||
})
|
||||
.then(async () => {
|
||||
showToast('购买成功');
|
||||
userStore.updateWallet();
|
||||
emit('buySuccess');
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.toString().indexOf('8000') != -1) {
|
||||
emit('close');
|
||||
nextTick(() => {
|
||||
router.push({
|
||||
path: '/wallet',
|
||||
query: {
|
||||
activeTab: 1,
|
||||
id: props.videoInfo.id,
|
||||
title: props.videoInfo.title || '无标题',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
closeLoading();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.content {
|
||||
color: #000;
|
||||
width: 100%;
|
||||
padding: 18px 10px 0;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
border-radius: 10px 10px 0 0;
|
||||
padding-bottom: 24px;
|
||||
.line_top {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
background: #f5f5f5;
|
||||
margin: 0 auto;
|
||||
border-radius: 99px;
|
||||
}
|
||||
.title {
|
||||
padding: 18px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
.priceList {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 18px 0;
|
||||
&.vip {
|
||||
.leader,
|
||||
.price {
|
||||
color: #ac7e6e;
|
||||
}
|
||||
}
|
||||
&.split {
|
||||
border-top: $borderSplit;
|
||||
margin-top: 18px;
|
||||
padding-top: 18px;
|
||||
}
|
||||
.leader {
|
||||
color: #757575;
|
||||
font-size: 14px;
|
||||
}
|
||||
.price {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
> svg {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.go {
|
||||
text-decoration-line: underline;
|
||||
color: #ac7e6e;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
.gold {
|
||||
color: #ff9000;
|
||||
}
|
||||
.through {
|
||||
text-decoration: line-through;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
.underline {
|
||||
text-decoration: underline;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
&.black {
|
||||
background-color: #0f0f0f;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
.line_top {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.leader {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.text {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
margin: 30px 0 18px;
|
||||
span {
|
||||
font-size: 28px;
|
||||
color: $themeColor;
|
||||
}
|
||||
|
||||
&.line {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.text-center {
|
||||
position: relative;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 30%;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
p {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.btnBox {
|
||||
margin-top: 26px;
|
||||
|
||||
.btn {
|
||||
height: 44px;
|
||||
width: 100%;
|
||||
background: linear-gradient(165deg, #f8f3ae 0%, #ddac44 100%);
|
||||
text-align: center;
|
||||
line-height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #7b0000;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.vip-btn {
|
||||
background: var(--app-primary);
|
||||
color: var(--app-primary-text);
|
||||
margin-left: 18px;
|
||||
}
|
||||
.themeColor {
|
||||
background: var(--app-primary);
|
||||
color: var(--app-primary-text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<van-overlay :show="isShow" z-index="2001" @click="emit('close')">
|
||||
<div class="wrapper flex-center-center">
|
||||
<div class="content">
|
||||
<p>金币购买本视频解锁精彩完整版!</p>
|
||||
<p style="color: #3f57b9">{{ videoInfo.originCoins }}金币</p>
|
||||
<p>可用金币{{ (userWallet.amount * 100 + userWallet.income * 100) / 100 || 0 }}金币</p>
|
||||
<div class="btnBox flex-center-between">
|
||||
<div class="btn" @click="unlockNow(0)">立即购买</div>
|
||||
<div class="btn" @click="$router.push('freeBenefits')">做任务得VIP</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import useLoading from '@/hooks/loading';
|
||||
import VideoApi from '@/service/video';
|
||||
import { computed } from 'vue';
|
||||
import { ProductType } from '@/enums/app';
|
||||
import { showToast } from 'vant';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
|
||||
const props = defineProps<{
|
||||
videoInfo: Types.Video.VInfosItem;
|
||||
isOpen: boolean;
|
||||
}>();
|
||||
const emit = defineEmits(['close', 'buySuccess']);
|
||||
const userInfo = useUserStore().userInfo;
|
||||
const userWallet = useUserStore().walletInfo;
|
||||
const { openLoading, closeLoading } = useLoading();
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description 视频购买
|
||||
*/
|
||||
const unlockNow = (money: any) => {
|
||||
try {
|
||||
openLoading();
|
||||
let productID = props.videoInfo.id;
|
||||
if (!productID) {
|
||||
showToast('购买出错~');
|
||||
return;
|
||||
}
|
||||
VideoApi.videoBuying({ productType: ProductType.VIDEO, productID: productID, goldVideoCouponNum: money || 0 })
|
||||
.then(async () => {
|
||||
showToast('购买成功');
|
||||
if (money) {
|
||||
for (let i = 0; i < userInfo.goldVideoCoupon.length; i++) {
|
||||
if (money == userInfo.goldVideoCoupon[i].gold) {
|
||||
if (userInfo.goldVideoCoupon[i].count > 1) {
|
||||
userInfo.goldVideoCoupon[i].count--;
|
||||
} else {
|
||||
userInfo.goldVideoCoupon.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
useUserStore().updateWallet();
|
||||
emit('buySuccess');
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.toString().indexOf('8000') != -1) {
|
||||
router.push({
|
||||
path: '/wallet',
|
||||
query: {
|
||||
id: props.videoInfo.id,
|
||||
title: props.videoInfo.title || '无标题',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
closeLoading();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
.content {
|
||||
width: 313px;
|
||||
height: 293px;
|
||||
background: url('@/assets/images/png/popVip.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
padding: 135px 34px 0;
|
||||
position: relative;
|
||||
p {
|
||||
font-size: 12px;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.btnBox {
|
||||
width: 235px;
|
||||
margin-top: 21px;
|
||||
.btn {
|
||||
height: 31px;
|
||||
width: 92px;
|
||||
background: #3476ff;
|
||||
text-align: center;
|
||||
line-height: 31px;
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="header flex-center-between" :style="{ background: headerBg }">
|
||||
<slot name="left">
|
||||
<SvgIcon v-if="leftIcon" :name="leftIcon" class="back" :color="leftIconColor" @click="router.go(-1)" />
|
||||
</slot>
|
||||
<div v-if="title" class="title">{{ title }}</div>
|
||||
<div v-else class="custom">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="right-btn" v-if="rightIcon || rightTitle" @click="rightBtn">
|
||||
<SvgIcon class="rightIcon" :name="rightIcon" v-if="rightIcon" />
|
||||
<div v-if="rightTitle">{{ rightTitle }}</div>
|
||||
</div>
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
const props: any = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
default() {
|
||||
return '标题';
|
||||
},
|
||||
},
|
||||
leftIcon: {
|
||||
type: String,
|
||||
default() {
|
||||
return 'back_white';
|
||||
},
|
||||
},
|
||||
leftIconColor: {
|
||||
type: String,
|
||||
default() {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
rightTitle: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
rightIcon: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
isIcon: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
headerBg: {
|
||||
type: String,
|
||||
default: '#000',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['rightFuc']);
|
||||
const rightBtn = () => {
|
||||
if (props.path) {
|
||||
router.push(props.path);
|
||||
} else {
|
||||
emit('rightFuc');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
height: 44px;
|
||||
padding: 0 14px;
|
||||
// background: #000;
|
||||
.back {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--app-text-primary);
|
||||
font-size: 16px;
|
||||
max-width: 220px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
font-weight: 500;
|
||||
}
|
||||
.custom {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
.right-btn {
|
||||
color: #ffffff73;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rightIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div class="headerAdv" v-if="state.advList.length" ref="advRef">
|
||||
<!-- 5宫格广告 -->
|
||||
<div class="fiveGrid" v-if="props.type === 'five'">
|
||||
<div class="rectAdvBox" @click="handleJumpAdv(item)" v-for="item in state.advList" :key="item.id">
|
||||
<lazyImg class="rectAdvIcon" :src="item.cover" />
|
||||
<div class="rectName">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<AdvSwiper class="swiper" v-else-if="props.type === 'swiper'" :list="state.advList" height="18.933vw"></AdvSwiper>
|
||||
<div class="rows-adv" v-else-if="props.type === 'rows'">
|
||||
<div class="box" @touchstart.stop @touchmove.stop @touchend.stop v-if="state.advList.length > 0">
|
||||
<div class="rectAdvBox" @click="handleJumpAdv(item)" v-for="item in state.advList" :key="item.id">
|
||||
<lazyImg class="rectAdvIcon" :src="item.cover" />
|
||||
<div class="rectName">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { nextTick, reactive, ref, watch } from 'vue';
|
||||
import { getAdItem } from '@/utils/getConfig';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
import { AdImpressionType, setAdImpression } from '@/utils/eventReporter/adImpression';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reportAdClickEvent } from '@/utils/eventReporter/adClick';
|
||||
|
||||
const route = useRoute();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
advType?: number;
|
||||
type?: 'five' | 'swiper' | 'rows';
|
||||
length?: number; //广告获取长度0-不限制
|
||||
random?: boolean; //是否随机获取
|
||||
adImpression?: AdImpressionType;
|
||||
}>(),
|
||||
{
|
||||
advType: 28,
|
||||
type: 'five',
|
||||
length: 5,
|
||||
},
|
||||
);
|
||||
const state = reactive({
|
||||
advList: <any>[], // 广告数据
|
||||
});
|
||||
const init = () => {
|
||||
const arr = getAdItem(props.advType) || [];
|
||||
if (props.length > 0) {
|
||||
state.advList = arr.slice(0, props.length);
|
||||
} else {
|
||||
if (props.random) {
|
||||
const shuffled = arr.slice().sort(() => 0.5 - Math.random());
|
||||
state.advList = shuffled.slice(0, 5);
|
||||
} else {
|
||||
state.advList = arr;
|
||||
}
|
||||
}
|
||||
};
|
||||
init();
|
||||
// 广告展示埋点
|
||||
const advRef = ref();
|
||||
watch(
|
||||
() => state.advList,
|
||||
() => {
|
||||
if (state.advList.length) {
|
||||
nextTick(() => {
|
||||
setAdImpression(advRef.value, state.advList, {
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'small_grid',
|
||||
}).setupObserver();
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const handleJumpAdv = (item: Types.App.AdsInfoListItem) => {
|
||||
let params = {
|
||||
...item,
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'small_grid',
|
||||
};
|
||||
reportAdClickEvent(params);
|
||||
jumpAdv(item);
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.headerAdv {
|
||||
width: 100%;
|
||||
.fiveGrid {
|
||||
margin: 8px 0 12px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
||||
grid-row-gap: 12px;
|
||||
grid-column-gap: 10px;
|
||||
box-sizing: border-box;
|
||||
.rectAdvBox {
|
||||
width: 60px;
|
||||
.rectAdvIcon {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rectName {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.swiper {
|
||||
padding: 0 10px;
|
||||
}
|
||||
.rows-adv {
|
||||
.box {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
padding: 18px 0;
|
||||
.rectAdvBox {
|
||||
width: 60px;
|
||||
margin-right: 6px;
|
||||
.rectAdvIcon {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rectName {
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.rectAdvBox:last-child {
|
||||
margin-right: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,203 @@
|
||||
<!--
|
||||
@description 顶部搜索栏:首页按 Figma 展示搜索图标与轮播文字,其他页面保留原有入口。
|
||||
@figma https://www.figma.com/design/ju3V3C1DiVqw9qK6gIpXVd?node-id=42199-184055
|
||||
-->
|
||||
<template>
|
||||
<div class="header_search" :class="{ 'header_search--home': variant === 'home' }">
|
||||
<div class="header_logo" />
|
||||
<div class="search-input" @click.stop="toSearchPage">
|
||||
<img
|
||||
v-if="variant === 'home'"
|
||||
class="search-icon"
|
||||
src="@/assets/images/ui/header-search.svg"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<van-notice-bar :scrollable="false" v-if="searchHintWord.length" class="notice-bar">
|
||||
<van-swipe
|
||||
vertical
|
||||
class="notice-swipe"
|
||||
:autoplay="3000"
|
||||
:touchable="false"
|
||||
@change="noticeChange"
|
||||
:show-indicators="false"
|
||||
>
|
||||
<van-swipe-item v-for="(item, index) in searchHintWord" :key="index">
|
||||
{{ item }}
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
</van-notice-bar>
|
||||
<span v-else class="search-placeholder">热搜短剧轮播</span>
|
||||
</div>
|
||||
<div class="search_box">
|
||||
<template v-if="getTheme.holidays !== 'default'">
|
||||
<div class="holidays_sign_icon" @click="router.push('/task_sign_day')"></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<lazyImg
|
||||
v-if="configStore.appConfig.signIcon"
|
||||
:src="configStore.appConfig.signIcon"
|
||||
class="sign_icon"
|
||||
@click="router.push('/task_sign_day')"
|
||||
/>
|
||||
</template>
|
||||
<!-- <div v-if="variant !== 'home'" class="menu" @click="emit('menuChange')" />-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import setTheme from '@/assets/theme/index';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
withDefaults(defineProps<{ variant?: 'default' | 'home' }>(), { variant: 'default' });
|
||||
const currentKeywordIndex = ref(0);
|
||||
const configStore = useConfigStore();
|
||||
const getTheme = setTheme(configStore.appConfig.festivalUi);
|
||||
|
||||
const router = useRouter();
|
||||
const emit = defineEmits(['menuChange']);
|
||||
const searchHintWord = computed(() => configStore.appConfig.searchHintWord || []);
|
||||
const noticeChange = (index: number) => {
|
||||
currentKeywordIndex.value = index;
|
||||
};
|
||||
const toSearchPage = () => {
|
||||
if (searchHintWord.value.length) {
|
||||
router.push(`/searchPage?text=${searchHintWord.value[currentKeywordIndex.value]}`);
|
||||
} else {
|
||||
router.push('/searchPage');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.header_search {
|
||||
padding: 4px 16px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.header_logo {
|
||||
height: 36px;
|
||||
width: 109px;
|
||||
background: url('@/assets/images/ui/header-logo@2x.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.search-input {
|
||||
flex: 1;
|
||||
margin: 0 12px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
text-indent: 12px;
|
||||
|
||||
/* Body/Medium */
|
||||
font-family: 'PingFang SC';
|
||||
font-size: 14px;
|
||||
.notice-bar {
|
||||
height: 32px;
|
||||
background: transparent;
|
||||
padding: 0 6px;
|
||||
.notice-swipe {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
:deep(.van-swipe-item) {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
.search_box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.menu {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background: url('@/assets/images/ui/menu.svg') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.sign_icon {
|
||||
width: 20px;
|
||||
height: 35px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header_search--home {
|
||||
.header_logo,
|
||||
.search_box {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
margin: 0 0 0 12px;
|
||||
padding: 0 12px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
text-indent: 0;
|
||||
|
||||
.search-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.notice-bar {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 18px;
|
||||
padding: 0;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
|
||||
:deep(.van-notice-bar__wrap) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.van-notice-bar__content) {
|
||||
position: static;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.notice-swipe {
|
||||
width: 100%;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
:deep(.van-swipe-item) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search_box {
|
||||
.sign_icon,
|
||||
.holidays_sign_icon {
|
||||
margin: 0 0 0 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="highlighted" v-html="highlightedText" @click="handleKeywordClick"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { innerJump, compatibleAppStoreJump } from '@/utils/index';
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
default() {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
keyword: {
|
||||
type: String,
|
||||
required: false,
|
||||
default() {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
linkType: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default() {
|
||||
return 1;
|
||||
},
|
||||
},
|
||||
linkStr: {
|
||||
type: String,
|
||||
required: false,
|
||||
default() {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleKeywordClick = (e: Event) => {
|
||||
const { keyword, linkType, linkStr } = props;
|
||||
if (!keyword || !linkStr) return;
|
||||
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.tagName === 'SPAN') {
|
||||
const index = linkStr.indexOf('yinseinner://');
|
||||
if (linkType === 2 || index !== -1) {
|
||||
innerJump(linkStr.substring(13));
|
||||
} else {
|
||||
compatibleAppStoreJump(linkStr);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const highlightedText = computed(() => {
|
||||
const { keyword } = props;
|
||||
if (!keyword) return props.text;
|
||||
let result = '';
|
||||
let currentIndex = 0;
|
||||
|
||||
// for (const keyword of props.keywords) {
|
||||
const index = props.text.indexOf(keyword, currentIndex);
|
||||
if (index !== -1) {
|
||||
result += props.text.slice(currentIndex, index);
|
||||
result += `<span>${keyword}</span>`;
|
||||
currentIndex = index + keyword.length;
|
||||
}
|
||||
// }
|
||||
|
||||
result += props.text.slice(currentIndex);
|
||||
return result;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.highlighted {
|
||||
font-size: 14px;
|
||||
:deep(span) {
|
||||
font-size: 14px;
|
||||
color: var(--app-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,236 @@
|
||||
import { computed, ref, watch, type ComputedRef } from 'vue';
|
||||
import AppApi from '@/service/app';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
|
||||
/** 浮窗倒计时显示阶段 */
|
||||
export type HongbaoRainPhase = 'countdown' | 'active';
|
||||
|
||||
export interface HongbaoRainStatus {
|
||||
/** 浮窗上展示的文字(未开始:HH:mm:ss;进行中:立即参与) */
|
||||
text: string;
|
||||
phase: HongbaoRainPhase;
|
||||
}
|
||||
|
||||
/** 服务端单查空数据错误码:-1010 时浮窗已下线或暂无下一场,需移除 */
|
||||
const CODE_EMPTY_DATA = '-1010';
|
||||
|
||||
/**
|
||||
* 把 banner.url 上的 query 解析出来,兼容只在 # 后带参数的写法
|
||||
*/
|
||||
const getBannerUrlSearchParams = (url: string) => {
|
||||
const queryIndex = url.indexOf('?');
|
||||
if (queryIndex !== -1) {
|
||||
const hashIndex = url.indexOf('#', queryIndex + 1);
|
||||
const query = hashIndex === -1 ? url.slice(queryIndex + 1) : url.slice(queryIndex + 1, hashIndex);
|
||||
return new URLSearchParams(query);
|
||||
}
|
||||
const hashIndex = url.indexOf('#');
|
||||
if (hashIndex !== -1 && url.slice(hashIndex + 1).includes('=')) {
|
||||
return new URLSearchParams(url.slice(hashIndex + 1));
|
||||
}
|
||||
return new URLSearchParams();
|
||||
};
|
||||
|
||||
/**
|
||||
* 红包雨外链:url 上带 type=hongbaoRain(及活动 id)即视为红包雨入口;
|
||||
* 不要求 url 的 id 与 banner.id 字符串完全一致
|
||||
*/
|
||||
export const isHongbaoRainBanner = (banner: Types.App.BannerJump | undefined | null) => {
|
||||
if (!banner?.id || !banner?.url) return false;
|
||||
try {
|
||||
const type = getBannerUrlSearchParams(banner.url).get('type');
|
||||
if (type?.toLowerCase() === 'hongbaorain') return true;
|
||||
return /(?:^|[?&#])type=hongbaoRain(?:&|$|[#])/i.test(banner.url);
|
||||
} catch {
|
||||
return /(?:^|[?&#])type=hongbaoRain(?:&|$|[#])/i.test(banner.url);
|
||||
}
|
||||
};
|
||||
|
||||
/** HH:mm:ss 格式化剩余时间 */
|
||||
const formatCountdown = (remainMs: number) => {
|
||||
const totalSec = Math.max(0, Math.floor(remainMs / 1000));
|
||||
const hour = Math.floor(totalSec / 3600);
|
||||
const minute = Math.floor((totalSec % 3600) / 60);
|
||||
const second = totalSec % 60;
|
||||
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const isAbortError = (error: unknown) => {
|
||||
if (!error || typeof error !== 'object') return false;
|
||||
const err = error as { code?: string; name?: string };
|
||||
return err.code === 'ERR_CANCELED' || err.name === 'AbortError' || err.name === 'CanceledError';
|
||||
};
|
||||
|
||||
/**
|
||||
* 浮窗「红包雨场次倒计时」状态管理:
|
||||
* - 维护各浮窗的倒计时文案 / 进行中文案
|
||||
* - 每秒刷新一次;场次结束时按 banner.id 单查刷新,处理下一场或下线
|
||||
*
|
||||
* 抽离原因:包含定时器、AbortController、跨场次副作用,且与展示组件其它业务无耦合,
|
||||
* 需要独立的启动/停止时机,符合"多个副作用块需抽离"的规范。
|
||||
*/
|
||||
export const useHongbaoRainCountdown = (bannerList: ComputedRef<Types.App.BannerJump[]>) => {
|
||||
const configStore = useConfigStore();
|
||||
const statusMap = ref<Record<string, HongbaoRainStatus>>({});
|
||||
/** 已处理过"场次结束"事件的 banner,避免每秒重复单查 */
|
||||
const endedHandledIds = ref<Set<string>>(new Set());
|
||||
const refreshingBannerId = ref('');
|
||||
let countdownTimer: number | null = null;
|
||||
let isActive = false;
|
||||
let refreshAbort: AbortController | null = null;
|
||||
|
||||
const hongbaoRainBannerList = computed(() => bannerList.value.filter(isHongbaoRainBanner));
|
||||
|
||||
const getBannerStatus = (bannerId: string) => statusMap.value[bannerId];
|
||||
|
||||
const setStatus = (bannerId: string, status: HongbaoRainStatus) => {
|
||||
statusMap.value = { ...statusMap.value, [bannerId]: status };
|
||||
};
|
||||
|
||||
const cancelRefresh = () => {
|
||||
refreshAbort?.abort();
|
||||
refreshAbort = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 用单查结果替换列表中的 banner;
|
||||
* 传 null 表示该 banner 已下线,从列表中移除
|
||||
*/
|
||||
const replaceBannerById = (bannerId: string, banner: Types.App.BannerJump | null) => {
|
||||
const oldList = configStore.appConfig?.bannerJumpList || [];
|
||||
const nextList = oldList.filter((item) => item?.id !== bannerId);
|
||||
if (banner) {
|
||||
nextList.push(banner);
|
||||
}
|
||||
configStore.appConfig.bannerJumpList = nextList;
|
||||
};
|
||||
|
||||
/**
|
||||
* 场次结束 → 单查最新场次;
|
||||
* - 成功:用新的 startAt/endAt 接续倒计时
|
||||
* - -1010:移除该 banner
|
||||
*/
|
||||
const refreshBanner = async (bannerId: string) => {
|
||||
if (!isActive || !bannerId || refreshingBannerId.value === bannerId) return;
|
||||
|
||||
cancelRefresh();
|
||||
const abortController = new AbortController();
|
||||
refreshAbort = abortController;
|
||||
refreshingBannerId.value = bannerId;
|
||||
|
||||
try {
|
||||
const latestBanner = await AppApi.queryBannerById(bannerId, abortController.signal);
|
||||
if (!isActive) return;
|
||||
if (latestBanner?.id) {
|
||||
replaceBannerById(bannerId, latestBanner);
|
||||
endedHandledIds.value.delete(bannerId);
|
||||
updateStatus();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (isAbortError(error) || !isActive) return;
|
||||
const errorCode = String((error as Error)?.message || '');
|
||||
if (errorCode === CODE_EMPTY_DATA) {
|
||||
replaceBannerById(bannerId, null);
|
||||
}
|
||||
} finally {
|
||||
if (refreshAbort === abortController) {
|
||||
refreshAbort = null;
|
||||
}
|
||||
refreshingBannerId.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
/** 单条 banner 状态更新:未开始 / 进行中 / 已结束 */
|
||||
const updateStatusForBanner = (banner: Types.App.BannerJump) => {
|
||||
const bannerId = banner.id;
|
||||
if (!isHongbaoRainBanner(banner)) return;
|
||||
|
||||
// 非红包雨倒计时类型(兜底兼容):直接展示"立即参与"
|
||||
if (Number(banner.countdownType) !== 1) {
|
||||
setStatus(bannerId, { text: '立即参与', phase: 'active' });
|
||||
return;
|
||||
}
|
||||
|
||||
const startAt = new Date(banner.startAt).getTime();
|
||||
const endAt = new Date(banner.endAt).getTime();
|
||||
if (!Number.isFinite(startAt) || !Number.isFinite(endAt)) {
|
||||
setStatus(bannerId, { text: '', phase: 'countdown' });
|
||||
return;
|
||||
}
|
||||
const now = Date.now();
|
||||
if (now < startAt) {
|
||||
setStatus(bannerId, {
|
||||
text: formatCountdown(startAt - now),
|
||||
phase: 'countdown',
|
||||
});
|
||||
endedHandledIds.value.delete(bannerId);
|
||||
return;
|
||||
}
|
||||
if (now < endAt) {
|
||||
setStatus(bannerId, { text: '立即参与', phase: 'active' });
|
||||
endedHandledIds.value.delete(bannerId);
|
||||
return;
|
||||
}
|
||||
// 场次已结束:先清空文案;同一 id 只在边界触发一次单查
|
||||
setStatus(bannerId, { text: '', phase: 'countdown' });
|
||||
if (!endedHandledIds.value.has(bannerId)) {
|
||||
endedHandledIds.value = new Set(endedHandledIds.value).add(bannerId);
|
||||
refreshBanner(bannerId);
|
||||
}
|
||||
};
|
||||
|
||||
const updateStatus = () => {
|
||||
if (!isActive) return;
|
||||
const list = hongbaoRainBannerList.value;
|
||||
if (!list.length) {
|
||||
statusMap.value = {};
|
||||
endedHandledIds.value = new Set();
|
||||
return;
|
||||
}
|
||||
// 清理已不在列表中的状态/已处理标记,避免内存堆积
|
||||
const activeIds = new Set(list.map((item) => item.id));
|
||||
statusMap.value = Object.fromEntries(
|
||||
Object.entries(statusMap.value).filter(([id]) => activeIds.has(id)),
|
||||
);
|
||||
endedHandledIds.value = new Set([...endedHandledIds.value].filter((id) => activeIds.has(id)));
|
||||
list.forEach((banner) => updateStatusForBanner(banner));
|
||||
};
|
||||
|
||||
const startCountdown = () => {
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
}
|
||||
isActive = true;
|
||||
updateStatus();
|
||||
countdownTimer = window.setInterval(() => {
|
||||
updateStatus();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const stopCountdown = () => {
|
||||
isActive = false;
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
}
|
||||
cancelRefresh();
|
||||
};
|
||||
|
||||
/** 配置侧的 bannerJumpList 变更时(如其它入口刷新配置)同步更新状态 */
|
||||
watch(
|
||||
() => [hongbaoRainBannerList.value, configStore.appConfig?.bannerJumpList] as const,
|
||||
() => {
|
||||
if (!isActive) return;
|
||||
updateStatus();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
return {
|
||||
statusMap,
|
||||
getBannerStatus,
|
||||
isHongbaoRainBanner,
|
||||
startCountdown,
|
||||
stopCountdown,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<!-- 商店包下载按钮 -->
|
||||
<div class="iosAppStoreBtn" @click="download" v-if="activeLayerKey === 'shopIosLink'">
|
||||
<SvgIcon name="closeIosAppStore" @click.stop="closeShopIosLink" />
|
||||
</div>
|
||||
<!-- 弹窗广告 -->
|
||||
<HomePopupAdv v-if="activeLayerKey === 'homeAdv'" :advList="state.advList" @close="closeHomePopupAdv" />
|
||||
<!-- APP公告 -->
|
||||
<AnnouncementPopup
|
||||
v-if="activeLayerKey === 'announcement'"
|
||||
:announList="state.announList"
|
||||
@close="closeAnnouncementPopup"
|
||||
/>
|
||||
<!-- 系统公告 -->
|
||||
<!-- <SystemAnnouncement /> -->
|
||||
<!-- 九宫格广告 -->
|
||||
<NineGridPopup v-if="activeLayerKey === 'nineGrid'" :is-open="true" @close="closeNineGridPopup" />
|
||||
<!-- 浮窗广告 -->
|
||||
<!-- <FloatingBlock :show-close="false" :is-home="true" /> -->
|
||||
<!-- 预售浮窗 -->
|
||||
<PersalePopup v-if="activeLayerKey === 'persale'" :is-open="true" @close="closePersale" />
|
||||
<!-- 支付预售尾款弹框 -->
|
||||
<PayPresalePopup
|
||||
v-if="activeLayerKey === 'payPresale'"
|
||||
:is-open="true"
|
||||
@close="closePayPersale"
|
||||
ref="PayPresalePopupRef"
|
||||
/>
|
||||
<!-- 游戏活动弹框 -->
|
||||
<JointGameing v-if="activeLayerKey === 'jointGame'" :is-open="true" @close="closeJointGameingPop" />
|
||||
<!-- 首页付费分层弹窗 -->
|
||||
<HomepagePaymentPopup v-if="activeLayerKey === 'paymentHome'" @close="closePaymentHomePopup" />
|
||||
<!-- 轮播浮窗(不参与弹窗顺序队列;短剧页可关) -->
|
||||
<SwipeFloat v-if="showSwipeFloat && !activeLayerKey" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, onDeactivated, reactive, ref, watch } from 'vue';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { AdType } from '@/enums/app';
|
||||
import { getAdItem } from '@/utils/getConfig';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { judgeTime } from '@/utils/index';
|
||||
import { isPaymentPlayPageStatus, resolvePaymentConfigImg } from '@/utils/paymentStatusPopup';
|
||||
import SwipeFloat from './swipeFloat.vue';
|
||||
|
||||
const HomepagePaymentPopup = defineAsyncComponent(() => import('@/components/HomepagePaymentPopup/index.vue'));
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 商店下载条;短剧进页关闭 */
|
||||
showShopDownload?: boolean;
|
||||
/** 队列结束后的轮播浮窗;短剧进页关闭 */
|
||||
showSwipeFloat?: boolean;
|
||||
}>(),
|
||||
{
|
||||
showShopDownload: true,
|
||||
showSwipeFloat: true,
|
||||
},
|
||||
);
|
||||
|
||||
const PayPresalePopupRef = ref();
|
||||
const router = useRouter();
|
||||
const globalStore = useGlobalStore();
|
||||
const configStore = useConfigStore();
|
||||
|
||||
const state = reactive({
|
||||
tabList: <Array<Types.Home.HomeTabItem>>[],
|
||||
tabActive: 0,
|
||||
isShowAdvPopup: !globalStore.homeAdvFirst, // 首页弹窗广告
|
||||
isShowPaymentHomePopup: !globalStore.homePaymentPopupFirst, // 首页付费分层弹窗
|
||||
advList: <Array<Types.App.AdsInfoListItem>>[], // 弹窗广告数据
|
||||
isShowAnnouncementPopup: !globalStore.announcementFirst, // app公告
|
||||
announList: <Array<Types.App.AnnounListItem>>[], //公告数据
|
||||
isShowNineGridPopup: !globalStore.nineGridFirst, // 九宫格广告
|
||||
nineGridList: <Array<Types.App.AdsInfoListItem>>[], // 九宫格广告数据
|
||||
isShowPersalePopup: !globalStore.presaleFirst, //预售弹框
|
||||
isHasPresale: false, //是否开启预售活动
|
||||
isShowPayPersalePopup: !globalStore.payPresaleFirst, //支付预售尾款弹框
|
||||
isPayPresale: false, //是否支付预售尾款
|
||||
scrollDistance: <any>0, // 滚动距离
|
||||
isShowGamePopup: !globalStore.joinGameingFirst, //游戏活动弹框
|
||||
isHaveGameActive: false, //是否包含游戏活动
|
||||
showLuckIcon: true, //是否显示抽奖图标
|
||||
showAiIcon: false, //是否显示Ai图标
|
||||
closeAiAcount: 0, // 关闭ai脱衣次数
|
||||
showShopIosLink: false,
|
||||
isChangeList: false,
|
||||
homeTabActive: 1,
|
||||
});
|
||||
|
||||
type ModalLayerKey = 'nineGrid' | 'homeAdv' | 'announcement' | 'jointGame' | 'payPresale' | 'persale' | 'shopIosLink' | 'paymentHome';
|
||||
|
||||
const shouldShowPaymentHomePopup = computed(() => {
|
||||
if (!isPaymentPlayPageStatus(configStore.appConfig?.paymentStatusPopup)) return false;
|
||||
return !!resolvePaymentConfigImg(configStore.appConfig?.paymentStatusPopupConfig?.homepage);
|
||||
});
|
||||
|
||||
const isModalLayerReady = (key: ModalLayerKey): boolean => {
|
||||
const advancePage = configStore.appConfig.advancePage;
|
||||
switch (key) {
|
||||
case 'nineGrid':
|
||||
return state.isShowNineGridPopup && state.nineGridList.length > 0;
|
||||
case 'homeAdv':
|
||||
return state.isShowAdvPopup && state.advList.length > 0;
|
||||
case 'announcement':
|
||||
return state.isShowAnnouncementPopup && state.announList.length > 0;
|
||||
case 'jointGame':
|
||||
return state.isShowGamePopup && state.isHaveGameActive;
|
||||
case 'payPresale':
|
||||
return state.isShowPayPersalePopup && state.isPayPresale && !!advancePage?.popBgProc;
|
||||
case 'persale':
|
||||
return state.isShowPersalePopup && state.isHasPresale && !!advancePage?.popBgWait;
|
||||
case 'shopIosLink':
|
||||
return props.showShopDownload && state.showShopIosLink && !!configStore.appConfig.shopIosLink;
|
||||
case 'paymentHome':
|
||||
return state.isShowPaymentHomePopup && shouldShowPaymentHomePopup.value;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const modalLayerQueue: { key: ModalLayerKey }[] = [
|
||||
{ key: 'nineGrid' },
|
||||
{ key: 'homeAdv' },
|
||||
{ key: 'announcement' },
|
||||
{ key: 'jointGame' },
|
||||
{ key: 'payPresale' },
|
||||
{ key: 'persale' },
|
||||
{ key: 'paymentHome' },
|
||||
{ key: 'shopIosLink' },
|
||||
];
|
||||
|
||||
const activeLayerKey = computed(() => {
|
||||
return modalLayerQueue.find((item) => isModalLayerReady(item.key))?.key || '';
|
||||
});
|
||||
|
||||
/** 须用运行时数组声明:Vue 3.2 的 type-only defineEmits 不会生成 emits,多根节点会触发 fallthrough warning */
|
||||
const emit = defineEmits(['queue-busy']);
|
||||
|
||||
watch(
|
||||
activeLayerKey,
|
||||
(key) => {
|
||||
emit('queue-busy', !!key);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
/** 无配置时清掉展示标记,避免占队列位置 */
|
||||
const skipUnconfiguredLayers = () => {
|
||||
if (state.nineGridList.length === 0) {
|
||||
state.isShowNineGridPopup = false;
|
||||
}
|
||||
if (state.advList.length === 0) {
|
||||
state.isShowAdvPopup = false;
|
||||
}
|
||||
if (state.announList.length === 0) {
|
||||
state.isShowAnnouncementPopup = false;
|
||||
}
|
||||
if (!state.isHaveGameActive) {
|
||||
state.isShowGamePopup = false;
|
||||
}
|
||||
const advancePage = configStore.appConfig.advancePage;
|
||||
if (!state.isHasPresale || !advancePage?.popBgWait) {
|
||||
state.isShowPersalePopup = false;
|
||||
state.isHasPresale = false;
|
||||
}
|
||||
if (!state.isPayPresale || !advancePage?.popBgProc) {
|
||||
state.isShowPayPersalePopup = false;
|
||||
state.isPayPresale = false;
|
||||
}
|
||||
if (!configStore.appConfig.shopIosLink) {
|
||||
state.showShopIosLink = false;
|
||||
}
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
state.showShopIosLink = props.showShopDownload && !localStorage.getItem('isAppStore');
|
||||
// 预售弹框
|
||||
const advancePage = configStore.appConfig.advancePage;
|
||||
let data = configStore.appConfig.advanceStatus;
|
||||
if (data && data.activityPopUp) {
|
||||
if (data.status == 0 && advancePage?.popBgWait) {
|
||||
state.isHasPresale = true;
|
||||
globalStore.setPresaleFirst();
|
||||
} else if (
|
||||
(data.status == 2 || data.status == 3) &&
|
||||
judgeTime(configStore.appConfig.advanceStatus.startTime) &&
|
||||
advancePage?.popBgProc
|
||||
) {
|
||||
state.isPayPresale = true;
|
||||
globalStore.setPayPresaleFirst();
|
||||
}
|
||||
}
|
||||
state.nineGridList = getAdItem(AdType.QualityApplication) || [];
|
||||
if (state.nineGridList.length > 0) {
|
||||
globalStore.setNineGridFirst();
|
||||
}
|
||||
// 游戏活动弹框
|
||||
if (data && data.gameActivityPopUp) {
|
||||
state.isHaveGameActive = true;
|
||||
globalStore.setJoinGameingFirst();
|
||||
}
|
||||
state.advList = getAdItem(AdType.HOMEPOPUPADV);
|
||||
if (state.advList && state.advList.length > 0) {
|
||||
globalStore.setHomeAdvFirst();
|
||||
}
|
||||
if (configStore.appConfig.announList && configStore.appConfig.announList.length > 0) {
|
||||
state.announList = JSON.parse(JSON.stringify(configStore.appConfig.announList));
|
||||
globalStore.setAnnouncementFirst();
|
||||
}
|
||||
if (shouldShowPaymentHomePopup.value) {
|
||||
globalStore.setHomePaymentPopupFirst();
|
||||
}
|
||||
skipUnconfiguredLayers();
|
||||
};
|
||||
init();
|
||||
|
||||
const download = () => {
|
||||
window.location.href = configStore.appConfig.shopIosLink;
|
||||
};
|
||||
const closeShopIosLink = () => {
|
||||
state.showShopIosLink = false;
|
||||
};
|
||||
const closePersale = (type: any) => {
|
||||
if (!type) {
|
||||
router.push('/presale');
|
||||
}
|
||||
state.isShowPersalePopup = false;
|
||||
state.isHasPresale = false;
|
||||
};
|
||||
const closePayPersale = (type: any) => {
|
||||
if (!type) {
|
||||
router.push('/presale');
|
||||
}
|
||||
state.isShowPayPersalePopup = false;
|
||||
state.isPayPresale = false;
|
||||
};
|
||||
const closeJointGameingPop = (type: any) => {
|
||||
if (!type) {
|
||||
router.push('/jointGamingActivitie');
|
||||
}
|
||||
state.isShowGamePopup = false;
|
||||
state.isHaveGameActive = false;
|
||||
};
|
||||
|
||||
const closeHomePopupAdv = () => {
|
||||
state.isShowAdvPopup = false;
|
||||
};
|
||||
const closeAnnouncementPopup = () => {
|
||||
state.isShowAnnouncementPopup = false;
|
||||
};
|
||||
const closeNineGridPopup = () => {
|
||||
state.isShowNineGridPopup = false;
|
||||
};
|
||||
const closePaymentHomePopup = () => {
|
||||
state.isShowPaymentHomePopup = false;
|
||||
};
|
||||
|
||||
onDeactivated(() => {
|
||||
if (PayPresalePopupRef.value) {
|
||||
clearTimeout(PayPresalePopupRef.value.timeoutTimerPay);
|
||||
PayPresalePopupRef.value.timeoutTimerPay = <any>null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.iosAppStoreBtn {
|
||||
height: 36px;
|
||||
width: 284px;
|
||||
background: url('@/assets/gif/iosAppStoreBtn.gif') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
top: 443px;
|
||||
z-index: 99;
|
||||
> svg {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
top: -8px;
|
||||
}
|
||||
}
|
||||
.persalebox {
|
||||
position: fixed;
|
||||
bottom: 30%;
|
||||
right: 10px;
|
||||
.persaleImg {
|
||||
width: 56px;
|
||||
animation: shake 1.5s infinite;
|
||||
}
|
||||
}
|
||||
@keyframes shake {
|
||||
0%,
|
||||
65% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
70% {
|
||||
transform: rotate(0deg) scale(1.1);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(6deg) scale(1.1);
|
||||
}
|
||||
80% {
|
||||
transform: rotate(-6deg) scale(1.1);
|
||||
}
|
||||
85% {
|
||||
transform: rotate(6deg) scale(1.1);
|
||||
}
|
||||
90% {
|
||||
transform: rotate(-6deg) scale(1.1);
|
||||
}
|
||||
95% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,348 @@
|
||||
<template>
|
||||
<!-- 轮播浮框 -->
|
||||
<div class="rightBottomBox" :style="{ display: display }">
|
||||
<!-- 轮播浮框1 -->
|
||||
<div class="floatingBox1" v-if="$route.name != 'mine' && hasFloatingBox1Items">
|
||||
<van-swipe class="my-swipe1" :autoplay="3000" indicator-color="white" :show-indicators="false">
|
||||
<van-swipe-item class="swipe-item" v-if="state.showLimitedTime && !userStore.isVip">
|
||||
<!-- 限时优惠 -->
|
||||
<limitedTime @exShowEl="state.showLimitedTime = false" />
|
||||
</van-swipe-item>
|
||||
<van-swipe-item
|
||||
class="swipe-item"
|
||||
v-if="state.activityDetail && (state.isShowPersaleFC == 'ys' || state.isShowPersaleFC == 'pay')"
|
||||
>
|
||||
<!-- 预售 +支付尾款 浮窗 -->
|
||||
<div class="persalebox">
|
||||
<lazyImg
|
||||
class="persaleImg"
|
||||
v-if="state.isShowPersaleFC == 'ys'"
|
||||
@click="$router.push('/presale')"
|
||||
:src="configStore?.appConfig?.advancePage?.enterBgWait"
|
||||
:key="configStore?.appConfig?.advancePage?.enterBgWait"
|
||||
/>
|
||||
<lazyImg
|
||||
class="persaleImg"
|
||||
v-if="state.isShowPersaleFC == 'pay'"
|
||||
@click="$router.push('/presale')"
|
||||
:src="configStore?.appConfig?.advancePage?.enterBgProc"
|
||||
:key="configStore?.appConfig?.advancePage?.enterBgProc"
|
||||
/>
|
||||
</div>
|
||||
</van-swipe-item>
|
||||
<van-swipe-item class="swipe-item" v-if="paymentFloatShouldShow">
|
||||
<HomepagePaymentFloat embedded :legacy-float-visible="state.showLimitedTime && !userStore.isVip" />
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
</div>
|
||||
<!-- 轮播浮框2 -->
|
||||
<div
|
||||
class="floatingBox2"
|
||||
v-if="floatingBox2List.length || (configStore.appConfig.luckyDrawH5 && state.showLuckIcon)"
|
||||
>
|
||||
<van-swipe class="my-swipe2" :autoplay="3000" indicator-color="white" :show-indicators="false">
|
||||
<van-swipe-item
|
||||
class="swipe-item"
|
||||
@click="openFloatAdv(item)"
|
||||
v-for="item in floatingBox2List"
|
||||
:key="item.id || item.banner"
|
||||
>
|
||||
<div class="countdownBanner">
|
||||
<lazyImg class="cover" :src="item.banner" :key="item.banner" />
|
||||
<div
|
||||
v-if="isHongbaoRainBanner(item) && getBannerStatus(item.id)?.text"
|
||||
class="countdownText"
|
||||
:class="getBannerStatus(item.id)?.phase === 'countdown' ? 'countdownText--timer' : 'countdownText--join'"
|
||||
>
|
||||
{{ getBannerStatus(item.id)?.text }}
|
||||
</div>
|
||||
</div>
|
||||
</van-swipe-item>
|
||||
<van-swipe-item v-if="configStore.appConfig.luckyDrawH5 && state.showLuckIcon">
|
||||
<div class="luckyDraw" @click.stop="$router.push('/lotteryPage')">
|
||||
<lazyImg
|
||||
class="lotteryIcon"
|
||||
v-if="configStore?.appConfig?.luckyDrawIcon"
|
||||
:src="configStore?.appConfig?.luckyDrawIcon"
|
||||
:key="configStore?.appConfig?.luckyDrawIcon"
|
||||
/>
|
||||
<div class="lotteryImg" v-else></div>
|
||||
</div>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent, nextTick, onActivated, onDeactivated, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
||||
import HomeApi from '@/service/home';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { judgeTime, jumpAdv } from '@/utils/index';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { hasAnyPaymentConfigField, isActivePaymentStatus } from '@/utils/paymentStatusPopup';
|
||||
import { useHongbaoRainCountdown } from './hooks/useHongbaoRainCountdown';
|
||||
|
||||
const HomepagePaymentFloat = defineAsyncComponent(() => import('@/components/HomepagePaymentFloat/index.vue'));
|
||||
|
||||
/** 浮窗2 固定显示在首页右下角的位置 */
|
||||
const FLOAT_POSITION_HOME = 3;
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const userStore = useUserStore();
|
||||
let display = ref('block');
|
||||
const state = reactive({
|
||||
showLuckIcon: true, //是否显示抽奖图标
|
||||
activityDetail: <any>'', //预售详情
|
||||
isShowPersaleFC: '', //浮窗展示
|
||||
showLimitedTime: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* 浮窗2 列表:直接从配置派生,便于 hook 内单查刷新后自动反映到 UI;
|
||||
* 避免 init 时一次性快照,错过场次切换/下线
|
||||
*/
|
||||
const floatingBox2List = computed<Types.App.BannerJump[]>(() => {
|
||||
const list = configStore.appConfig?.bannerJumpList || [];
|
||||
return list.filter((item) => item?.position === FLOAT_POSITION_HOME && item?.banner);
|
||||
});
|
||||
|
||||
const paymentFloatShouldShow = computed(() => {
|
||||
const paymentConfig = configStore.appConfig?.paymentStatusPopupConfig;
|
||||
if (!isActivePaymentStatus(configStore.appConfig?.paymentStatusPopup)) return false;
|
||||
return hasAnyPaymentConfigField(paymentConfig, ['homepageFlot', 'vipCard']);
|
||||
});
|
||||
|
||||
const presaleFloatVisible = computed(
|
||||
() => !!(state.activityDetail && (state.isShowPersaleFC == 'ys' || state.isShowPersaleFC == 'pay')),
|
||||
);
|
||||
|
||||
const hasFloatingBox1Items = computed(
|
||||
() =>
|
||||
(state.showLimitedTime && !userStore.isVip) ||
|
||||
presaleFloatVisible.value ||
|
||||
paymentFloatShouldShow.value,
|
||||
);
|
||||
|
||||
const { getBannerStatus, isHongbaoRainBanner, startCountdown, stopCountdown } =
|
||||
useHongbaoRainCountdown(floatingBox2List);
|
||||
|
||||
/** 浮窗2 内链;跳会员中心时带 HOME_FLOAT_WINDOW */
|
||||
const openFloatAdv = (item: Types.App.BannerJump) => {
|
||||
void jumpAdv(item, { sourcePage: 'HOME_FLOAT_WINDOW' });
|
||||
};
|
||||
|
||||
const isNoWatchVideoFirst = computed(() => !!localStorage.getItem(`secondViewing${userStore.userInfo.uid}`));
|
||||
const isNoWatchVideoOld = computed(() => !!localStorage.getItem(`secondOldViewing${userStore.userInfo.uid}`));
|
||||
|
||||
watch(
|
||||
[isNoWatchVideoFirst, isNoWatchVideoOld],
|
||||
([newFirst, newOld]) => {
|
||||
console.log(newFirst, 'isNoWatchVideoFirst');
|
||||
console.log(newOld, 'isNoWatchVideoOld');
|
||||
|
||||
if (newFirst || newOld) {
|
||||
state.showLimitedTime = true;
|
||||
} else {
|
||||
state.showLimitedTime = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const getPresaleData = async () => {
|
||||
try {
|
||||
let req = {
|
||||
pageNumber: 1,
|
||||
pageSize: 1,
|
||||
};
|
||||
let res: any = await HomeApi.getPresaleList(req);
|
||||
nextTick(() => {
|
||||
state.activityDetail = res.activityDetail;
|
||||
// 预售浮窗
|
||||
if (state.activityDetail.status == 0 || state.activityDetail.status == 1) {
|
||||
state.isShowPersaleFC = 'ys';
|
||||
} else if (
|
||||
(state.activityDetail.status == 2 || state.activityDetail.status == 3) &&
|
||||
judgeTime(state.activityDetail.startTime)
|
||||
) {
|
||||
//支付尾款浮窗
|
||||
state.isShowPersaleFC = 'pay';
|
||||
} else if (
|
||||
(state.activityDetail.status == 2 || state.activityDetail.status == 3) &&
|
||||
!judgeTime(state.activityDetail.startTime)
|
||||
) {
|
||||
//预售
|
||||
state.isShowPersaleFC = 'ys';
|
||||
} else {
|
||||
state.isShowPersaleFC = '';
|
||||
}
|
||||
// 判断剩余支付时间是否过期
|
||||
const endTime = new Date(state.activityDetail.endTime).getTime();
|
||||
const newTime = new Date().getTime();
|
||||
const diffTime = (endTime - newTime) / 1000;
|
||||
if (diffTime < 0) {
|
||||
console.log('过期了');
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
// SwipeFloat 在 HomeAdv 内通过 v-if 在弹窗关闭后才挂载,
|
||||
// 此时 keep-alive 父级已激活,onActivated 不会再触发,
|
||||
// 必须用 onMounted 兜底首次启动倒计时
|
||||
const startupCountdown = () => {
|
||||
if (configStore.appConfig.advanceStatus && configStore.appConfig.advanceStatus.activityStatus) {
|
||||
getPresaleData();
|
||||
}
|
||||
startCountdown();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
startupCountdown();
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
startupCountdown();
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
stopCountdown();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopCountdown();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.rightBottomBox {
|
||||
position: fixed;
|
||||
bottom: 140px;
|
||||
right: 16px;
|
||||
z-index: 10;
|
||||
.floatingBox1 {
|
||||
width: 60px;
|
||||
margin-bottom: 20px;
|
||||
.my-swipe1 {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
:deep(.van-swipe__track) {
|
||||
align-items: center;
|
||||
}
|
||||
.swipe-item {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.persalebox {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.persaleImg {
|
||||
width: 56px;
|
||||
animation: shake 1.5s infinite;
|
||||
}
|
||||
@keyframes shake {
|
||||
0%,
|
||||
65% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
70% {
|
||||
transform: rotate(0deg) scale(1.1);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(6deg) scale(1.1);
|
||||
}
|
||||
80% {
|
||||
transform: rotate(-6deg) scale(1.1);
|
||||
}
|
||||
85% {
|
||||
transform: rotate(6deg) scale(1.1);
|
||||
}
|
||||
90% {
|
||||
transform: rotate(-6deg) scale(1.1);
|
||||
}
|
||||
95% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.floatingBox2 {
|
||||
width: 60px;
|
||||
.my-swipe2 {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
:deep(.van-swipe__track) {
|
||||
align-items: center;
|
||||
}
|
||||
.swipe-item {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.cover {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.countdownBanner {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
position: relative;
|
||||
.cover {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.countdownText {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translateX(-50%);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 52px;
|
||||
height: 14px;
|
||||
padding: 0 4px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
letter-spacing: -0.2px;
|
||||
font-family: 'Alimama ShuHeiTi';
|
||||
}
|
||||
.countdownText--timer {
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, #d000ff 0%, #40c0ff 100%);
|
||||
}
|
||||
.countdownText--join {
|
||||
min-width: 44px;
|
||||
color: #fff;
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.luckyDraw {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
.lotteryIcon {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.lotteryImg {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: url('@/assets/gif/lottery.gif') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<div class="homeHeader" :data-type="state.advCoverType" v-if="state.advList?.length" ref="advRef">
|
||||
<!-- 小方块布局 -->
|
||||
<template v-if="state.advCoverType === 'small-cube'">
|
||||
<div class="firstRow" v-if="firstRowList.length">
|
||||
<div class="rectAdvBox" @click="handleJumpAdv(item)" v-for="item in firstRowList" :key="item.id">
|
||||
<lazyImg class="rectAdvIcon" :src="item.cover" />
|
||||
<div class="rectName">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="secondRowList.length >= 6">
|
||||
<div
|
||||
:class="['adv-line-2', 'flex', isStopAnimation ? 'stopAnima' : '']"
|
||||
@touchmove.stop="onTouchMove"
|
||||
@touchend="onTouchEnd"
|
||||
ref="scrollableRef"
|
||||
:style="{ '--animation-duration': `${secondRowList.length * 2}s` }"
|
||||
>
|
||||
<div class="fiveGrid secondLine shrink animation">
|
||||
<div
|
||||
class="rectAdvBox"
|
||||
v-for="item in secondRowList"
|
||||
:key="'first-' + item.id"
|
||||
@click="handleJumpAdv(item)"
|
||||
>
|
||||
<lazyImg class="rectAdvIcon" :src="item.cover" />
|
||||
<div class="rectName">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fiveGrid secondLine shrink append animation">
|
||||
<div
|
||||
class="rectAdvBox"
|
||||
v-for="item in secondRowList"
|
||||
:key="'second-' + item.id"
|
||||
@click="handleJumpAdv(item)"
|
||||
>
|
||||
<lazyImg class="rectAdvIcon" :src="item.cover" />
|
||||
<div class="rectName">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="firstRow" v-if="secondRowList?.length">
|
||||
<div class="rectAdvBox" v-for="item in secondRowList" :key="item.id" @click="handleJumpAdv(item)">
|
||||
<lazyImg class="rectAdvIcon" :src="item.cover" />
|
||||
<div class="rectName">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else-if="state.advCoverType === 'vertical-cube'">
|
||||
<div
|
||||
:class="['adv-line-2 flex verticalCube', isStopAnimation ? 'stopAnima' : '']"
|
||||
@touchmove.stop="onTouchMove"
|
||||
@touchend="onTouchEnd"
|
||||
ref="scrollableRef"
|
||||
:style="{ '--animation-duration': `${state.advList.length * 3}s` }"
|
||||
>
|
||||
<div class="fiveGrid secondLine shrink animation">
|
||||
<div class="rectAdvBox" v-for="item in state.advList" :key="'first-' + item.id" @click="handleJumpAdv(item)">
|
||||
<lazyImg class="rectAdvIcon" :src="item.cover" />
|
||||
<div class="rectName">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fiveGrid secondLine shrink append animation">
|
||||
<div class="rectAdvBox" v-for="item in state.advList" :key="'second-' + item.id" @click="handleJumpAdv(item)">
|
||||
<lazyImg class="rectAdvIcon" :src="item.cover" />
|
||||
<div class="rectName">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 大图轮播 -->
|
||||
<div class="banner_box" v-else-if="state.advCoverType.indexOf('banner-') > -1">
|
||||
<van-swipe :autoplay="2000" :indicator-color="indicatorColor" class="swipe_box">
|
||||
<van-swipe-item v-for="(item, index) in state.advList" @click="handleJumpAdv(item)" :key="index">
|
||||
<DecryptImg :src="item.cover" :key="item.cover" />
|
||||
<div class="title" v-if="showTitle">{{ item.title }}</div>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { reactive, computed, ref, watch, nextTick } from 'vue';
|
||||
import { getAdItem } from '@/utils/getConfig';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
import { AdImpressionType, setAdImpression } from '@/utils/eventReporter/adImpression';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reportAdClickEvent } from '@/utils/eventReporter/adClick';
|
||||
|
||||
const route = useRoute();
|
||||
const isStopAnimation = ref(false);
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
advType: number;
|
||||
showTitle?: boolean;
|
||||
indicatorColor?: string;
|
||||
adImpression?: AdImpressionType;
|
||||
}>(),
|
||||
{
|
||||
indicatorColor: '#fff',
|
||||
showTitle: false,
|
||||
},
|
||||
);
|
||||
const state = reactive({
|
||||
advList: <Array<Types.App.AdsInfoListItem>>[], // 所有广告
|
||||
advCoverType: 'small-cube', // 广告类型
|
||||
});
|
||||
// 第一行广告
|
||||
const firstRowList = computed(() => {
|
||||
return state.advList.slice(0, 5);
|
||||
});
|
||||
// 第二行广告
|
||||
const secondRowList = computed(() => {
|
||||
return state.advList.slice(5, state.advList.length);
|
||||
});
|
||||
const advTypeText = ref('');
|
||||
const init = () => {
|
||||
let list: any = [];
|
||||
if (props.advType) {
|
||||
list = getAdItem(props.advType) || [];
|
||||
}
|
||||
state.advList = list.slice(0, list.length);
|
||||
state.advCoverType = list?.[0]?.coverType || list?.[0]?.coverImgSize || 'small-cube';
|
||||
if (state.advCoverType === 'small-cube') {
|
||||
advTypeText.value = 'small_grid';
|
||||
}
|
||||
if (state.advCoverType === 'vertical-cube') {
|
||||
advTypeText.value = 'vertical_grid';
|
||||
}
|
||||
if (state.advCoverType.indexOf('banner-') > -1) {
|
||||
advTypeText.value = 'large_carousel';
|
||||
}
|
||||
};
|
||||
init();
|
||||
// 广告展示埋点
|
||||
const advRef = ref();
|
||||
watch(
|
||||
() => state.advList,
|
||||
() => {
|
||||
if (state.advList.length) {
|
||||
nextTick(() => {
|
||||
setAdImpression(advRef.value, state.advList, {
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || advTypeText.value,
|
||||
}).setupObserver();
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const handleJumpAdv = (item: Types.App.AdsInfoListItem) => {
|
||||
let params = {
|
||||
...item,
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || advTypeText.value,
|
||||
};
|
||||
reportAdClickEvent(params);
|
||||
jumpAdv(item);
|
||||
};
|
||||
const onTouchMove = () => {
|
||||
isStopAnimation.value = true;
|
||||
};
|
||||
|
||||
let timer: any = null;
|
||||
const scrollableRef = ref<HTMLElement>();
|
||||
const onTouchEnd = async () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
scrollableRef.value!.scrollLeft = 0;
|
||||
isStopAnimation.value = false;
|
||||
}, 3000);
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.homeHeader {
|
||||
--size-width: 60px;
|
||||
--size-height: 60px;
|
||||
.rectAdvBox {
|
||||
width: var(--size-width);
|
||||
.rectAdvIcon {
|
||||
height: var(--size-height);
|
||||
width: var(--size-width);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rectName {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
.firstRow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 9px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.adv-line-2 {
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
.fiveGrid {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
&.secondLine {
|
||||
&.animation {
|
||||
animation: scroll var(--animation-duration) linear 0s infinite;
|
||||
}
|
||||
}
|
||||
&.append {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.stopAnima {
|
||||
.animation {
|
||||
animation: none !important;
|
||||
animation-play-state: paused !important;
|
||||
}
|
||||
}
|
||||
// 竖方块
|
||||
.verticalCube {
|
||||
--size-width: 100px;
|
||||
--size-height: 150px;
|
||||
.fiveGrid {
|
||||
gap: 5px;
|
||||
}
|
||||
.rectAdvBox {
|
||||
.rectName {
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scroll {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(-100% - 10px));
|
||||
}
|
||||
}
|
||||
.banner_box {
|
||||
.swipe_box {
|
||||
width: 100%;
|
||||
height: 143px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
:deep(.van-swipe__indicators) {
|
||||
transform: translateX(0);
|
||||
left: unset;
|
||||
right: 9px;
|
||||
.van-swipe__indicator {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.van-swipe__indicator--active {
|
||||
width: 12px;
|
||||
height: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="announcementPopup">
|
||||
<van-overlay :show="true" z-index="2001" @click="close()">
|
||||
<div class="imgWrapper" ref="advRef">
|
||||
<lazyImg
|
||||
@click.stop="handleJumpAdv(currentAds[0])"
|
||||
class="imgBox"
|
||||
:src="currentAds[0]?.cover"
|
||||
:key="currentAds[0]?.id"
|
||||
/>
|
||||
<lazyImg
|
||||
v-if="currentAds[1]"
|
||||
@click.stop="handleJumpAdv(currentAds[1])"
|
||||
class="imgBox"
|
||||
:src="currentAds[1]?.cover"
|
||||
:key="currentAds[1]?.id"
|
||||
/>
|
||||
<div class="close"></div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, reactive, ref, watch, computed } from 'vue';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
import { AdImpressionType, setAdImpression } from '@/utils/eventReporter/adImpression';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reportAdClickEvent } from '@/utils/eventReporter/adClick';
|
||||
|
||||
const route = useRoute();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
advList: Types.App.AdsInfoListItem[];
|
||||
adImpression?: AdImpressionType;
|
||||
}>(),
|
||||
{
|
||||
advList: () => [],
|
||||
},
|
||||
);
|
||||
const emit = defineEmits(['close']);
|
||||
const state = reactive({
|
||||
advList: <Array<Types.App.AdsInfoListItem>>props.advList,
|
||||
});
|
||||
const currentIndex = ref(0);
|
||||
const currentAds = computed(() => {
|
||||
return state.advList.slice(currentIndex.value, currentIndex.value + 2);
|
||||
});
|
||||
const advRef = ref();
|
||||
watch(
|
||||
() => currentAds.value,
|
||||
() => {
|
||||
if (currentAds.value.length) {
|
||||
nextTick(() => {
|
||||
setAdImpression(
|
||||
advRef.value,
|
||||
currentAds.value,
|
||||
{
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'popup',
|
||||
},
|
||||
// 'payload',
|
||||
).setupObserver();
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const handleJumpAdv = (item: Types.App.AdsInfoListItem) => {
|
||||
let params = {
|
||||
...item,
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'popup',
|
||||
};
|
||||
reportAdClickEvent(params);
|
||||
jumpAdv(item);
|
||||
};
|
||||
const close = () => {
|
||||
currentIndex.value += 2;
|
||||
// 如果没有更多广告(即当前索引超出或等于广告总数),关闭弹窗
|
||||
if (currentIndex.value >= state.advList.length) {
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.van-overlay) {
|
||||
background-color: rgba(0, 0, 0, 1);
|
||||
}
|
||||
.announcementPopup {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.imgWrapper {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
.imgBox {
|
||||
height: 169px;
|
||||
width: 300px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.close {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
background: url('./../../assets/images/png/closeDialog.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
margin: 30px auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="homepagePaymentFloat"
|
||||
:class="[{ embedded }, isStopScroll ? '' : 'scrollHomepagePaymentFloat']"
|
||||
:style="{ display: displayStyle }"
|
||||
@click.stop="handleToMember"
|
||||
>
|
||||
<lazyImg v-if="floatIcon" class="floatImg" :src="floatIcon" :key="floatIcon" />
|
||||
<img v-else class="floatImg" src="@/assets/images/png/limitedTime.png" alt="" />
|
||||
<div v-if="showCountdown" class="floatTime">
|
||||
<van-count-down :time="countdownMs" @finish="onCountdownFinish" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import {
|
||||
hasAnyPaymentConfigField,
|
||||
isActivePaymentStatus,
|
||||
parseLastDiscountTime,
|
||||
resolvePaymentConfigImg,
|
||||
} from '@/utils/paymentStatusPopup';
|
||||
|
||||
const props = defineProps({
|
||||
/** 新老用户限时浮窗是否正在展示,展示时本组件不显示 */
|
||||
legacyFloatVisible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isStopScroll: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
displayStyle: {
|
||||
type: String,
|
||||
default: 'block',
|
||||
},
|
||||
/** 嵌入首页浮窗轮播时使用,取消 fixed 定位 */
|
||||
embedded: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const configStore = useConfigStore();
|
||||
const countdownExpired = ref(false);
|
||||
|
||||
const paymentConfig = computed(() => configStore.appConfig?.paymentStatusPopupConfig);
|
||||
|
||||
const discountDeadline = computed(() => parseLastDiscountTime(paymentConfig.value?.lastDiscountTime));
|
||||
|
||||
const countdownMs = computed(() => {
|
||||
if (!discountDeadline.value || countdownExpired.value) return 0;
|
||||
const remaining = discountDeadline.value - Date.now();
|
||||
return remaining > 0 ? remaining : 0;
|
||||
});
|
||||
|
||||
const floatIcon = computed(() => resolvePaymentConfigImg(paymentConfig.value?.homepageFlot));
|
||||
|
||||
const vipCardId = computed(() => paymentConfig.value?.vipCard || '');
|
||||
|
||||
const showCountdown = computed(() => countdownMs.value > 0);
|
||||
|
||||
const visible = computed(() => {
|
||||
if (props.legacyFloatVisible) return false;
|
||||
if (!isActivePaymentStatus(configStore.appConfig?.paymentStatusPopup)) return false;
|
||||
if (!hasAnyPaymentConfigField(paymentConfig.value, ['homepageFlot', 'vipCard'])) {
|
||||
return false;
|
||||
}
|
||||
if (discountDeadline.value && countdownMs.value <= 0) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
watch(
|
||||
visible,
|
||||
(value) => {
|
||||
emit('update:visible', value);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const handleToMember = () => {
|
||||
const id = vipCardId.value;
|
||||
router.push({
|
||||
path: '/member',
|
||||
query: {
|
||||
...(id ? { id } : {}),
|
||||
sourcePage: 'HOME_FLOAT_WINDOW',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onCountdownFinish = () => {
|
||||
countdownExpired.value = true;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => paymentConfig.value?.lastDiscountTime,
|
||||
() => {
|
||||
countdownExpired.value = false;
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.homepagePaymentFloat {
|
||||
// position: fixed;
|
||||
// bottom: 24%;
|
||||
// right: 0;
|
||||
// z-index: 10;
|
||||
|
||||
&.embedded {
|
||||
position: relative;
|
||||
bottom: auto;
|
||||
right: auto;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.floatImg {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.floatTime {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 47px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Alimama FangYuanTi VF', sans-serif;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
--van-count-down-text-color: rgba(255, 240, 209, 1);
|
||||
--van-count-down-font-size: 8.71px;
|
||||
--van-count-down-line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollHomepagePaymentFloat {
|
||||
transform: translateX(80px);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div class="homepagePaymentPopup">
|
||||
<van-overlay :show="true" z-index="2001" @click="close">
|
||||
<div class="imgWrapper">
|
||||
<div class="popupContent" @click.stop="handleClick">
|
||||
<lazyImg class="imgBox" :src="homepageImg" :key="homepageImg" />
|
||||
<div v-if="showCountdown" class="countdownTimer">{{ countdownText }}</div>
|
||||
</div>
|
||||
<div class="close" @click="close"></div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { parseLastDiscountTime, resolvePaymentConfigImg } from '@/utils/paymentStatusPopup';
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const router = useRouter();
|
||||
const configStore = useConfigStore();
|
||||
const countdownExpired = ref(false);
|
||||
const countdownText = ref('00:00:00');
|
||||
let countdownTimer: number | null = null;
|
||||
|
||||
const paymentConfig = computed(() => configStore.appConfig?.paymentStatusPopupConfig);
|
||||
|
||||
const homepageImg = computed(() => resolvePaymentConfigImg(paymentConfig.value?.homepage));
|
||||
|
||||
const discountDeadline = computed(() => parseLastDiscountTime(paymentConfig.value?.lastDiscountTime));
|
||||
|
||||
const countdownSec = computed(() => {
|
||||
if (!discountDeadline.value || countdownExpired.value) return 0;
|
||||
const remaining = Math.floor((discountDeadline.value - Date.now()) / 1000);
|
||||
return remaining > 0 ? remaining : 0;
|
||||
});
|
||||
|
||||
const showCountdown = computed(() => discountDeadline.value > 0 && countdownSec.value > 0);
|
||||
|
||||
const vipCardId = computed(() => paymentConfig.value?.vipCard || '');
|
||||
|
||||
const formatCountdown = (totalSec: number) => {
|
||||
const sec = Math.max(0, totalSec);
|
||||
const hour = Math.floor(sec / 3600);
|
||||
const minute = Math.floor((sec % 3600) / 60);
|
||||
const second = sec % 60;
|
||||
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const updateCountdownText = () => {
|
||||
if (!discountDeadline.value) {
|
||||
countdownText.value = '00:00:00';
|
||||
return;
|
||||
}
|
||||
const remaining = Math.floor((discountDeadline.value - Date.now()) / 1000);
|
||||
if (remaining <= 0) {
|
||||
countdownText.value = '00:00:00';
|
||||
countdownExpired.value = true;
|
||||
stopCountdown();
|
||||
return;
|
||||
}
|
||||
countdownText.value = formatCountdown(remaining);
|
||||
};
|
||||
|
||||
const startCountdown = () => {
|
||||
stopCountdown();
|
||||
updateCountdownText();
|
||||
if (!showCountdown.value) return;
|
||||
countdownTimer = window.setInterval(updateCountdownText, 1000);
|
||||
};
|
||||
|
||||
const stopCountdown = () => {
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
/** 跳转会员中心并选中分层配置的会员卡,不直接拉起支付 */
|
||||
const handleClick = () => {
|
||||
const id = vipCardId.value;
|
||||
close();
|
||||
router.push({
|
||||
path: '/member',
|
||||
query: {
|
||||
...(id ? { id } : {}),
|
||||
sourcePage: 'HOME_USER_SEGMENT',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
watch(
|
||||
() => paymentConfig.value?.lastDiscountTime,
|
||||
() => {
|
||||
countdownExpired.value = false;
|
||||
startCountdown();
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
startCountdown();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopCountdown();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.van-overlay) {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.homepagePaymentPopup {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
.imgWrapper {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.popupContent {
|
||||
position: relative;
|
||||
width: 285px;
|
||||
}
|
||||
|
||||
.imgBox {
|
||||
width: 285px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.countdownTimer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
min-width: 103px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #e03017 0%, #f27952 100%);
|
||||
border: 0.52px solid rgba(234, 184, 101, 1);
|
||||
color: rgba(255, 240, 209, 1);
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.close {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
background: url('@/assets/images/png/closeDialog.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
margin: 10px auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="horizontalGameBox" @click="openVideo">
|
||||
<lazyImg class="gameCover" :src="state.videoInfo.cover">
|
||||
<div class="box flex-vcenter">
|
||||
<SvgIcon name="eyeWhite" class="icon" />
|
||||
{{ watchCount(state.videoInfo.pageViewCount) }}
|
||||
</div>
|
||||
</lazyImg>
|
||||
<div class="gameTitle ellipsis">
|
||||
{{ state.videoInfo.title }}
|
||||
</div>
|
||||
<div class="tagBox ellipsis">
|
||||
<div class="tag" @click.stop="jumpTag(tag)" v-for="tag in state.videoInfo.tags" :key="tag.id">
|
||||
{{ tag.name }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 编辑时 -->
|
||||
<!-- <div class="checked" @click.stop v-if="isEdit">
|
||||
<van-checkbox :name="videoInfo.id">
|
||||
<template #icon="props">
|
||||
<img class="imgIcon" :src="props.checked ? activeIcon : inactiveIcon" />
|
||||
</template>
|
||||
</van-checkbox>
|
||||
</div> -->
|
||||
<div class="delBtn" v-if="isEdit" @click.stop="handleDel">
|
||||
<SvgIcon name="clear-red" class="selectActive" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import activeIcon from '@/assets/images/png/mine/icheck-active.png';
|
||||
import inactiveIcon from '@/assets/images/png/mine/icheck.png';
|
||||
import { reactive, inject, computed, type ComputedRef } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { watchCount, jumpAdv } from '@/utils';
|
||||
|
||||
const emit = defineEmits(['select', 'del']);
|
||||
const router = useRouter();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
videoInfo: Types.Video.VInfosItem;
|
||||
isEdit?: boolean;
|
||||
}>(),
|
||||
{
|
||||
videoInfo: () => <Types.Video.VInfosItem>{},
|
||||
isEdit: false,
|
||||
},
|
||||
);
|
||||
const injectedEdit = inject<ComputedRef<boolean> | boolean>('isEdit', false);
|
||||
const isEdit = computed(() => {
|
||||
if (props.isEdit) return true;
|
||||
if (typeof injectedEdit === 'object' && injectedEdit && 'value' in injectedEdit) {
|
||||
return Boolean(injectedEdit.value);
|
||||
}
|
||||
return Boolean(injectedEdit);
|
||||
});
|
||||
const state = reactive({
|
||||
videoInfo: props.videoInfo,
|
||||
});
|
||||
const openVideo = () => {
|
||||
// 编辑时返回id
|
||||
if (isEdit.value) {
|
||||
emit('select', state.videoInfo?.id);
|
||||
return;
|
||||
}
|
||||
if (state.videoInfo.type) {
|
||||
jumpAdv(state.videoInfo);
|
||||
return;
|
||||
}
|
||||
if (state.videoInfo.newsType == 'SEED_LINK') {
|
||||
router.push(`gameDetails?id=${state.videoInfo.id}`);
|
||||
} else if (state.videoInfo.newsType == 'COVER') {
|
||||
router.push(`postDetail?id=${state.videoInfo.id}`);
|
||||
} else {
|
||||
router.push({
|
||||
path: '/movieDetails',
|
||||
query: {
|
||||
vid: state.videoInfo.id,
|
||||
uid: state.videoInfo.publisher?.uid,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
// 标签跳转
|
||||
const jumpTag = (item: any) => {
|
||||
// router.push({
|
||||
// name: 'gameTagDetails',
|
||||
// query: {
|
||||
// id: item.id,
|
||||
// name: item.name,
|
||||
// },
|
||||
// });
|
||||
};
|
||||
const handleDel = () => {
|
||||
emit('del', state.videoInfo?.id);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.horizontalGameBox {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.gameCover {
|
||||
width: 100%;
|
||||
height: 101px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 6px;
|
||||
position: relative;
|
||||
.box {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.6) 100%);
|
||||
padding: 0 8px;
|
||||
|
||||
font-size: 10px;
|
||||
.icon {
|
||||
margin-right: 2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.gameTitle {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.tagBox {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
.tag {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
font-size: 10px;
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
.checked {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 6px;
|
||||
:deep(.van-checkbox__icon) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.imgIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.delBtn {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
.selectActive {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,553 @@
|
||||
<template>
|
||||
<div class="horizontalVideoBox" @click="openVideo">
|
||||
<div class="coverBox">
|
||||
<!-- 暗网权限膜层 -->
|
||||
<div class="awPermissions-popup" v-if="showAwPopup">
|
||||
<div class="blur-background"></div>
|
||||
<div class="awPermissions-content">
|
||||
<div>
|
||||
需要开通
|
||||
<span>{{ configStore.appConfig.darkWebVipName }}</span>
|
||||
</div>
|
||||
<div>即可观看</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isEdit && editMode === 'select'"
|
||||
class="selectBtn"
|
||||
:class="{ active: selected }"
|
||||
@click.stop="toggleSelect"
|
||||
/>
|
||||
<div class="delBtn" v-else-if="isEdit" @click.stop="del(videoInfo)">
|
||||
<SvgIcon name="clear-red" class="selectActive" />
|
||||
</div>
|
||||
<lazyImg
|
||||
class="avatar"
|
||||
:src="videoInfo?.cover || (acgHorizontal ? videoInfo?.horizontalCover : videoInfo?.verticalCover)"
|
||||
/>
|
||||
<!-- <div-->
|
||||
<!-- class="vip_icon free_icon"-->
|
||||
<!-- v-if="-->
|
||||
<!-- !videoInfo.type &&-->
|
||||
<!-- configStore.appConfig.freeMark &&-->
|
||||
<!-- ((isLongFormVideo(videoInfo) && !videoInfo.mediaType && videoInfo.freeArea === true) ||-->
|
||||
<!-- (['video', 'image', 'text'].includes(videoInfo.mediaType) && videoInfo.permission === 2))-->
|
||||
<!-- "-->
|
||||
<!-- >-->
|
||||
<!-- 免费-->
|
||||
<!-- </div>-->
|
||||
<div class="vip_icon free_trial_icon" v-if="showFreeTrialIcon">免费</div>
|
||||
<template v-else-if="['video', 'image', 'text'].includes(videoInfo.mediaType)">
|
||||
<div class="vip_icon" v-if="configStore.appConfig.vipMark && videoInfo.permission == 0">VIP</div>
|
||||
<div
|
||||
class="vip_icon coins_icon"
|
||||
v-if="configStore.appConfig.coinMark && videoInfo.permission === 1 && videoInfo.price"
|
||||
>
|
||||
金币
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
class="vip_icon"
|
||||
v-if="configStore.appConfig.vipMark && videoInfo?.originCoins == 0 && !videoInfo?.freeArea"
|
||||
>
|
||||
VIP
|
||||
</div>
|
||||
<div
|
||||
class="vip_icon coins_icon"
|
||||
v-if="configStore.appConfig.coinMark && videoInfo?.originCoins != 0 && !videoInfo?.freeArea"
|
||||
>
|
||||
金币
|
||||
</div>
|
||||
</template>
|
||||
<div class="videoInfo flex-center-between" v-if="!videoInfo.type && showTimeAndComment">
|
||||
<div class="playCount flex-vcenter">
|
||||
<SvgIcon name="playCountIcon" color="#fff" />
|
||||
{{ watchCount(videoInfo?.playCount) }}
|
||||
</div>
|
||||
<div class="playDuration flex-vcenter">
|
||||
<template v-if="isPic">
|
||||
<SvgIcon v-if="isPic" name="cover_pic" color="#fff" style="font-size: 16px; margin-right: 2px" />
|
||||
{{ watchCount(videoInfo.seriesCover?.length) || 0 }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ videoTime(videoInfo?.playTime) }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="subscriptInfoAdv" v-else-if="videoInfo.type">广告</div>
|
||||
</div>
|
||||
<div
|
||||
class="videoTitleBox"
|
||||
:class="{
|
||||
'videoTitleBox--compact':
|
||||
hideVideoCardTagsAndComments && !videoInfo.type && !videoInfo.mediaType && showType !== 5,
|
||||
}"
|
||||
>
|
||||
<div class="videoTitle">{{ videoInfo?.title }}</div>
|
||||
<!-- acg -->
|
||||
<div class="acg-info" v-if="['video', 'image'].includes(videoInfo.mediaType)">
|
||||
<span v-if="videoInfo.updateStatus != 1">共{{ videoInfo.totalEpisode }}话 · 已完结</span>
|
||||
<span class="loading" v-else>更新{{ videoInfo.currentEpisode || videoInfo.totalEpisode }}话 · 连载中</span>
|
||||
</div>
|
||||
<div
|
||||
class="videoTitleInfo flex-center-between"
|
||||
v-else-if="
|
||||
!videoInfo.type &&
|
||||
showTimeAndComment &&
|
||||
(!hideVideoCardTagsAndComments || showType === 5) &&
|
||||
videoInfo.newsType !== 'PIC'
|
||||
"
|
||||
>
|
||||
<template v-if="showType && [5].includes(showType)">
|
||||
<span class="collectCount">收藏:{{ watchCount(videoInfo?.collectCount) }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="tagBox ellipsis" @click.stop="jumpTag()" v-if="getTag()">
|
||||
{{ getTag()?.name }}
|
||||
</div>
|
||||
<div v-else></div>
|
||||
<div class="commentCount">
|
||||
评论{{ watchCount(videoInfo?.commentCount > 0 ? videoInfo?.commentCount : 0) }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { watchCount, videoTime, jumpAdv, isLongFormVideo } from '@/utils/index';
|
||||
import { inject, computed, onMounted, type ComputedRef } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useVideoStore } from '@/stores/video';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { sharedFreeWatchCount, ensureSharedFreeWatchCount } from '@/utils/sharedFreeWatchCount';
|
||||
const configStore = useConfigStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const videoStore = useVideoStore();
|
||||
const hideVideoCardTagsAndComments = inject('hideVideoCardTagsAndComments', false);
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
videoInfo: any;
|
||||
videoList: any[];
|
||||
isMovie?: boolean;
|
||||
isNoDelPageCache?: string; // 是否需要不删除缓存
|
||||
checkIsOpen?: boolean;
|
||||
showTimeAndComment?: boolean; // 是否展示评论数和上架时间 默认true
|
||||
awPermissions?: boolean; // 是否开启暗网视频权限, 默认false
|
||||
isPic?: boolean; // 是否是图集 默认false
|
||||
showType?: number;
|
||||
acgHorizontal?: boolean;
|
||||
/** 外部可强制编辑态,优先于 inject */
|
||||
isEdit?: boolean;
|
||||
/** delete: 单项删除图标;select: 多选圆圈 */
|
||||
editMode?: 'delete' | 'select';
|
||||
selected?: boolean;
|
||||
}>(),
|
||||
{
|
||||
videoInfo: () => <any>{},
|
||||
videoList: () => [],
|
||||
isMovie: () => false,
|
||||
isNoDelPageCache: () => '',
|
||||
checkIsOpen: () => false,
|
||||
showTimeAndComment: () => true,
|
||||
awPermissions: () => false,
|
||||
isPic: () => false,
|
||||
isEdit: undefined,
|
||||
editMode: undefined,
|
||||
selected: () => false,
|
||||
},
|
||||
);
|
||||
/** getPlayCount.watchCount <= 1 时不展示免费试看角标 */
|
||||
const showFreeTrialIcon = computed(() => {
|
||||
if (!configStore.appConfig.freeMark || !props.videoInfo?.showFreeTrialBadge) return false;
|
||||
const remaining =
|
||||
sharedFreeWatchCount.value !== null && sharedFreeWatchCount.value >= 0
|
||||
? sharedFreeWatchCount.value
|
||||
: Number(props.videoInfo.freeTrialRemaining);
|
||||
if (Number.isFinite(remaining) && remaining <= 1) return false;
|
||||
return true;
|
||||
});
|
||||
const showAwPopup = computed(() => {
|
||||
if (props.awPermissions && props.videoInfo?.tags?.length && userStore.userInfo.vipLevel < 3) {
|
||||
let tags = props.videoInfo.tags;
|
||||
return tags.some((item: any) => item?.name?.indexOf('暗网') > -1);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const emit = defineEmits(['changeVideo', 'del', 'select', 'click']);
|
||||
onMounted(() => {
|
||||
if (configStore.appConfig.freeMark && props.videoInfo?.showFreeTrialBadge) {
|
||||
ensureSharedFreeWatchCount(props.videoInfo?.id);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取标签:默认第二个,第二个没有就取第一个,第一个没有就空
|
||||
const getTag = () => {
|
||||
const tags = props.videoInfo?.tags;
|
||||
if (tags?.length) {
|
||||
return tags[0] || '';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const jumpTag = () => {
|
||||
router.push({
|
||||
path: '/tagVideoList',
|
||||
query: {
|
||||
id: getTag()?.id,
|
||||
tagName: getTag()?.name,
|
||||
newsType: 'SP',
|
||||
},
|
||||
});
|
||||
};
|
||||
const buildVideoQuery = (video: Types.Video.VInfosItem) => {
|
||||
const query: Record<string, string> = {
|
||||
vid: video.id,
|
||||
uid: String(video.publisher?.uid || ''),
|
||||
};
|
||||
if (video.searchAccessTicket) {
|
||||
query.searchAccessTicket = video.searchAccessTicket;
|
||||
}
|
||||
return query;
|
||||
};
|
||||
const openVideo = () => {
|
||||
if (isEdit.value) {
|
||||
if (editMode.value === 'select') {
|
||||
toggleSelect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
emit('click');
|
||||
// 必须读 props:列表刷新后组件可能复用,禁止缓存 setup 时的 videoInfo
|
||||
const videoInfo = props.videoInfo;
|
||||
if (videoInfo.type) {
|
||||
jumpAdv(videoInfo);
|
||||
return;
|
||||
}
|
||||
// 点击的暗网视频,需要权限
|
||||
if (showAwPopup.value) {
|
||||
router.push(`/member?id=${configStore.appConfig.darkWebVipId}`);
|
||||
return;
|
||||
}
|
||||
if (videoInfo.newsType == 'COVER') {
|
||||
router.push({
|
||||
path: '/postDetail',
|
||||
query: {
|
||||
postid: videoInfo.id,
|
||||
},
|
||||
});
|
||||
} else if (videoInfo.mediaType == 'video') {
|
||||
router.push({
|
||||
path: '/AcgVideoDetail',
|
||||
query: {
|
||||
id: videoInfo.id,
|
||||
},
|
||||
});
|
||||
} else if (videoInfo.mediaType == 'image') {
|
||||
router.push({
|
||||
path: '/AcgImageDetail',
|
||||
query: {
|
||||
id: videoInfo.id,
|
||||
},
|
||||
});
|
||||
} else if (videoInfo.newsType == 'PIC') {
|
||||
router.push({ path: '/coverPreview', query: { index: 1, id: videoInfo.id } });
|
||||
} else if (videoInfo.mediaType == 'text') {
|
||||
router.push({ path: '/AcgTextDetail', query: { id: videoInfo.id } });
|
||||
} else {
|
||||
if (isLongFormVideo(videoInfo)) {
|
||||
if (props.isMovie) {
|
||||
emit('changeVideo', videoInfo);
|
||||
} else {
|
||||
router.push({
|
||||
path: '/movieDetails',
|
||||
query: buildVideoQuery(videoInfo),
|
||||
});
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
videoStore.deleteVideoList();
|
||||
videoStore.saveVideoList(props.videoList, videoInfo);
|
||||
const query: Record<string, string> = { id: videoInfo.id };
|
||||
if (videoInfo.searchAccessTicket) {
|
||||
query.searchAccessTicket = videoInfo.searchAccessTicket;
|
||||
}
|
||||
router.push({ path: '/shortVideoDetails', query });
|
||||
}
|
||||
}
|
||||
};
|
||||
const injectedEdit = inject<ComputedRef<boolean> | boolean>('isEdit', false);
|
||||
const injectedEditMode = inject<'delete' | 'select' | ComputedRef<'delete' | 'select'>>('editMode', 'delete');
|
||||
const isEdit = computed(() => {
|
||||
if (typeof props.isEdit === 'boolean') return props.isEdit;
|
||||
if (typeof injectedEdit === 'object' && injectedEdit && 'value' in injectedEdit) {
|
||||
return Boolean(injectedEdit.value);
|
||||
}
|
||||
return Boolean(injectedEdit);
|
||||
});
|
||||
const editMode = computed(() => {
|
||||
if (props.editMode === 'select' || props.editMode === 'delete') return props.editMode;
|
||||
if (typeof injectedEditMode === 'object' && injectedEditMode && 'value' in injectedEditMode) {
|
||||
return injectedEditMode.value || 'delete';
|
||||
}
|
||||
return injectedEditMode || 'delete';
|
||||
});
|
||||
|
||||
const toggleSelect = () => {
|
||||
emit('select', props.videoInfo?.id);
|
||||
};
|
||||
|
||||
const del = async (info: any) => {
|
||||
emit('del', info.id);
|
||||
return;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.horizontalVideoBox {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.coverBox {
|
||||
height: 98px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
transform: translateZ(0px);
|
||||
.awPermissions-popup {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 20;
|
||||
backdrop-filter: blur(8px);
|
||||
.blur-background {
|
||||
position: absolute;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 30;
|
||||
}
|
||||
.awPermissions-content {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
z-index: 88;
|
||||
color: #fafafa;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
width: 100%;
|
||||
span {
|
||||
color: #f68804;
|
||||
}
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
position: relative;
|
||||
border-radius: 8px 8px 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.vip_icon {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 2;
|
||||
width: 27px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
border-radius: 3px;
|
||||
font-family: 'PingFang SC';
|
||||
color: #fff;
|
||||
background: linear-gradient(72deg, #FF8000 10.74%, #FF7A01 66.04%);
|
||||
// 免费
|
||||
&.free_icon {
|
||||
width: 32px;
|
||||
color: #fff;
|
||||
background: #1CD302;
|
||||
}
|
||||
// 免费试看
|
||||
&.free_trial_icon {
|
||||
color: #fff;
|
||||
background: rgba(28, 211, 2, 1);
|
||||
}
|
||||
// 金币
|
||||
&.coins_icon {
|
||||
color: #1f2029;
|
||||
background: linear-gradient(135deg, #FFE580 0%, #FACC15 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.videoInfo {
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.6));
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding: 0 9px 0 4px;
|
||||
transform: translateZ(0px);
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
.playCount {
|
||||
> svg {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.subscriptInfo {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
height: 16px;
|
||||
width: 34px;
|
||||
border-radius: 0 0 4px 0;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
line-height: 16px;
|
||||
.vipLabel {
|
||||
width: 34px;
|
||||
height: 18px;
|
||||
border-radius: 0 0 4px 0;
|
||||
background: #fb415c;
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.goldLabel {
|
||||
height: 19px;
|
||||
background: #fb415c;
|
||||
padding: 0 6px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 0 0 4px 0;
|
||||
img {
|
||||
width: 9px;
|
||||
}
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
padding-left: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.subscriptInfoAdv {
|
||||
transform: translateZ(0px);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
height: 16px;
|
||||
width: 34px;
|
||||
border-radius: 0 0 4px 0;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: rgb(0, 0, 0);
|
||||
background-color: rgb(255, 235, 58);
|
||||
text-align: center;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
.videoTitleBox {
|
||||
margin-bottom: 12px;
|
||||
.videoTitle {
|
||||
height: 35px;
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
letter-spacing: 0.24px;
|
||||
font-weight: 500;
|
||||
margin: 4px 0;
|
||||
line-height: 18px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
&--compact .videoTitle {
|
||||
height: auto;
|
||||
}
|
||||
.acg-info {
|
||||
span {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
font-size: 11px;
|
||||
}
|
||||
.loading {
|
||||
color: #eec76b;
|
||||
}
|
||||
}
|
||||
.videoTitleInfo {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
.tagBox {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 3px;
|
||||
padding: 4px 6px 4px;
|
||||
max-width: 95px;
|
||||
}
|
||||
.collectCount {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.delBtn {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 50;
|
||||
.selectActive {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
z-index: 100;
|
||||
}
|
||||
}
|
||||
.selectBtn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 50;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid #989898;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&.active {
|
||||
border-color: #ffc000;
|
||||
background: #ffc000;
|
||||
&::after {
|
||||
content: '';
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
border: 2px solid #1f2029;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
transform: rotate(45deg) translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,368 @@
|
||||
<template>
|
||||
<div class="horizontalVideoBox">
|
||||
<HorizontalVideo
|
||||
:videoInfo="videoInfo"
|
||||
:searchAccessTicket="searchAccessTicket"
|
||||
@getFreeCount="getFreeCount"
|
||||
@jumpOverHandle="jumpOverHandle"
|
||||
@showPreviewTime="showPreviewTime"
|
||||
@timeupdate="emit('timeupdate', $event)"
|
||||
:hasDarkWebTag="hasDarkWebTag"
|
||||
@play="play"
|
||||
ref="verticalVideoRef"
|
||||
class="videoBox"
|
||||
/>
|
||||
<div v-if="isPreSaleCoin" v-show="isVipFreeShow" class="tipBox">
|
||||
免费金币观影数: {{ useGlobalStore().advanceStatus.privilegeLimit.remain.todayCoinVideoCount }}次
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="tipBox" v-if="videoInfo.freeArea && isVipFreeShow">免费视频</div>
|
||||
<div
|
||||
class="tipBox"
|
||||
v-else-if="
|
||||
!isVip && userFreeCount.watchCount > 0 && videoInfo.originCoins == 0 && !videoInfo.freeArea && isVipFreeShow
|
||||
"
|
||||
>
|
||||
免费观影数: {{ userFreeCount.watchCount
|
||||
}}{{ userFreeCount.totalWatchCount ? `/${userFreeCount.totalWatchCount}` : '' }}次
|
||||
</div>
|
||||
<div class="tipBox" v-else-if="isVip && videoInfo.originCoins == 0 && !videoInfo.freeArea && isVipFreeShow">
|
||||
已享VIP免费特权
|
||||
</div>
|
||||
<div
|
||||
class="tipBox"
|
||||
v-else-if="!isVip && userFreeCount.isCan && videoInfo.originCoins == 0 && !videoInfo.freeArea && isVipFreeShow"
|
||||
>
|
||||
已享免费特权
|
||||
</div>
|
||||
<div
|
||||
class="tipBox"
|
||||
v-else-if="
|
||||
((videoInfo.originCoins != 0 && videoInfo.coins == 0) ||
|
||||
(isVip && userInfo.vipLevel >= 2 && isVipFreeShow && !hasDarkWebTag) ||
|
||||
(hasDarkWebTag && userInfo.vipLevel == 4)) &&
|
||||
isVipFreeShow
|
||||
"
|
||||
>
|
||||
已享金币视频免费特权
|
||||
</div>
|
||||
<div class="tipBox" v-else-if="videoInfo.vidStatus.hasPaid && isVipFreeShow">已购买完整版</div>
|
||||
<div
|
||||
class="tipBox"
|
||||
@click="jumpOverHandle('manual')"
|
||||
v-else-if="
|
||||
isFreeCountLoaded &&
|
||||
!isVip &&
|
||||
!userFreeCount.isCan &&
|
||||
videoInfo.originCoins == 0 &&
|
||||
videoInfo.freeArea == false &&
|
||||
!videoInfo.vidStatus.hasPaid
|
||||
"
|
||||
>
|
||||
跳过预览
|
||||
</div>
|
||||
<!-- 金币视频 -->
|
||||
<template
|
||||
v-else-if="
|
||||
videoInfo.originCoins > 0 &&
|
||||
!videoInfo.vidStatus.hasPaid &&
|
||||
!videoInfo.freeArea &&
|
||||
!(isVip && userInfo.vipLevel >= 2)
|
||||
"
|
||||
>
|
||||
<div class="tipBox" @click="jumpOverHandle('manual')" v-if="!(videoInfo.originCoins != 0 && videoInfo.coins == 0)">
|
||||
跳过预览
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<VideoPermissionMask
|
||||
:visible="showVideoMask"
|
||||
:videoInfo="videoInfo"
|
||||
:triggerSource="videoMaskTriggerSource"
|
||||
enable-coin-pay-popup
|
||||
@buySuccess="buySuccess"
|
||||
@showVipPopUpImmediately="openPaymentStatusPopup"
|
||||
@openCoinPayPopup="showCoinVideoPayPopup = true"
|
||||
/>
|
||||
<CoinVideoPayPopup
|
||||
:is-open="showCoinVideoPayPopup"
|
||||
:video-info="videoInfo"
|
||||
@close="showCoinVideoPayPopup = false"
|
||||
@buySuccess="buySuccess"
|
||||
/>
|
||||
<AppSharePopup :is-open="visibleShare" @close="visibleShare = false" :cover="props.videoInfo.cover"></AppSharePopup>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Ref, ref, computed, onDeactivated, watch } from 'vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { showToast } from 'vant';
|
||||
import { ProductType } from '@/enums/app';
|
||||
import { useRouter } from 'vue-router';
|
||||
import VideoApi from '@/service/video';
|
||||
import { judgeTime } from '@/utils/index';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
import MineApi from '@/service/mine';
|
||||
|
||||
const verticalVideoRef = ref();
|
||||
const configStore = useConfigStore();
|
||||
const globalStore = useGlobalStore();
|
||||
const router = useRouter();
|
||||
let isPreview = ref(false); // 是否预览视频
|
||||
let previewTime = ref(0); // 预览视频时间
|
||||
let isVipFreeShow = ref(true); //VIP免费特权
|
||||
let isPreSaleCoin = ref(false); // 是否显示预售金币免费提示
|
||||
const showVideoMask = ref(false); // 是否显示视频遮罩
|
||||
const showCoinVideoPayPopup = ref(false);
|
||||
const videoMaskTriggerSource = ref<'manual' | 'previewEnd'>('previewEnd');
|
||||
const props = defineProps<{
|
||||
videoInfo: Types.Video.VInfosItem;
|
||||
searchAccessTicket?: string;
|
||||
}>();
|
||||
const userStore = useUserStore();
|
||||
const { isVip, freeVideo30, userInfo, walletInfo, updateWallet } = userStore;
|
||||
let userFreeCount: Ref<Types.Video.UserFreeCount> = ref({
|
||||
isCan: false,
|
||||
watchCount: 0,
|
||||
});
|
||||
// 免费次数接口返回前不渲染会员视频「跳过预览」,避免先闪再切成免费观影数
|
||||
const isFreeCountLoaded = ref(false);
|
||||
const emit = defineEmits([
|
||||
'getFreeCount',
|
||||
'play',
|
||||
'showVipPopUp',
|
||||
'showVipPopUpImmediately',
|
||||
'buySuccess',
|
||||
'timeupdate',
|
||||
]);
|
||||
// 是否含有暗网
|
||||
const hasDarkWebTag = computed(() => props.videoInfo?.tags?.some((item) => item.name.indexOf('暗网') > -1));
|
||||
/**
|
||||
* @description 判断是否预览视频
|
||||
*/
|
||||
const checkPreview = () => {
|
||||
if (
|
||||
!props.videoInfo.vidStatus.hasPaid &&
|
||||
!(freeVideo30 && props.videoInfo.originCoins < 30 && props.videoInfo.originCoins != 0) &&
|
||||
!(props.videoInfo.originCoins === 0 && isVip) &&
|
||||
!(userInfo.uid === props.videoInfo.publisher.uid) &&
|
||||
!(userFreeCount.value.isCan === true && props.videoInfo.originCoins === 0) &&
|
||||
props.videoInfo.previewURL
|
||||
) {
|
||||
isPreview.value = true;
|
||||
} else {
|
||||
isPreview.value = false;
|
||||
}
|
||||
};
|
||||
let isFirst = false;
|
||||
const play = () => {
|
||||
emit('play');
|
||||
if (!isFirst) {
|
||||
isFirst = true;
|
||||
setTimeout(() => {
|
||||
isVipFreeShow.value = false;
|
||||
}, 3000);
|
||||
}
|
||||
};
|
||||
checkPreview();
|
||||
//免费观看视频次数
|
||||
const getFreeCount = (userCount: Types.Video.UserFreeCount) => {
|
||||
userFreeCount.value = userCount;
|
||||
isFreeCountLoaded.value = true;
|
||||
emit('getFreeCount', userCount);
|
||||
};
|
||||
const showPreviewTime = (time: number) => {
|
||||
previewTime.value = parseInt(time.toString());
|
||||
};
|
||||
const shouldAutoShowMemberPopup = () => {
|
||||
const info = props.videoInfo;
|
||||
if (isVip || info.freeArea || info.vidStatus.hasPaid) return false;
|
||||
return info.originCoins === 0;
|
||||
};
|
||||
|
||||
const jumpOverHandle = (source: 'manual' | 'previewEnd' = 'previewEnd') => {
|
||||
verticalVideoRef.value?.pause?.();
|
||||
updateWallet();
|
||||
videoMaskTriggerSource.value = source;
|
||||
// 遮罩已展示时半屏可能刚被用户关掉;勿重复 emit,否则点遮罩外会「关了又弹」
|
||||
const alreadyShowingMask = showVideoMask.value;
|
||||
showVideoMask.value = true;
|
||||
// 试看结束:由详情页分流(无免费次数 → VIP 列表;其余仅遮罩)
|
||||
if (source === 'previewEnd' && shouldAutoShowMemberPopup() && !alreadyShowingMask) {
|
||||
emit('showVipPopUp');
|
||||
}
|
||||
};
|
||||
|
||||
const openPaymentStatusPopup = () => {
|
||||
verticalVideoRef.value?.pause?.();
|
||||
emit('showVipPopUpImmediately');
|
||||
};
|
||||
//获取当前视频价格
|
||||
const getGoldCoin = () => {
|
||||
return isVip ? props.videoInfo.coins : props.videoInfo.originCoins;
|
||||
};
|
||||
/**
|
||||
* @description 视频购买成功
|
||||
*/
|
||||
const buySuccess = () => {
|
||||
showToast('购买成功');
|
||||
showVideoMask.value = false;
|
||||
showCoinVideoPayPopup.value = false;
|
||||
useUserStore().updateWallet();
|
||||
verticalVideoRef.value.success();
|
||||
emit('buySuccess');
|
||||
};
|
||||
/**
|
||||
* @description 视频购买
|
||||
*/
|
||||
const nowBuy = () => {
|
||||
try {
|
||||
let productID = props.videoInfo.id;
|
||||
if (!productID) {
|
||||
showToast('购买出错~');
|
||||
return;
|
||||
}
|
||||
VideoApi.videoBuying({ productType: ProductType.VIDEO, productID: productID })
|
||||
.then(async () => {
|
||||
buySuccess();
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.toString().indexOf('8000') != -1) {
|
||||
router.push({
|
||||
path: '/wallet',
|
||||
query: {
|
||||
id: props.videoInfo.id,
|
||||
title: props.videoInfo.title || '无标题',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
// 预售活动相关
|
||||
const requestPrivilegeConsume = async () => {
|
||||
if (useGlobalStore().presaleObj.status != 0 && globalStore.advanceStatus.privilegeLimit.hasLimit) {
|
||||
if (globalStore.advanceStatus.privilegeLimit.remain.todayCoinVideoCount > 0) {
|
||||
try {
|
||||
let req = {
|
||||
count: 1,
|
||||
privilegeType: 'coinVideo',
|
||||
};
|
||||
await MineApi.privilegeConsume(req);
|
||||
isPreSaleCoin.value = true;
|
||||
globalStore.setPreSaleCoinVideoPlay(true);
|
||||
globalStore.advanceStatus.privilegeLimit.remain.todayCoinVideoCount -= 1;
|
||||
} catch (e) {
|
||||
globalStore.setPreSaleCoinVideoPlay(false);
|
||||
}
|
||||
} else {
|
||||
globalStore.setPreSaleCoinVideoPlay(false);
|
||||
}
|
||||
} else {
|
||||
globalStore.setPreSaleCoinVideoPlay(false);
|
||||
}
|
||||
};
|
||||
const init = async () => {
|
||||
// 预售活动开启了,获取预售信息
|
||||
if (configStore.appConfig.advanceStatus && configStore.appConfig.advanceStatus.activityStatus) {
|
||||
if (
|
||||
!(props.videoInfo.originCoins != 0 && props.videoInfo.coins == 0) &&
|
||||
props.videoInfo.originCoins > 0 &&
|
||||
!props.videoInfo.vidStatus.hasPaid &&
|
||||
!props.videoInfo.freeArea
|
||||
) {
|
||||
requestPrivilegeConsume();
|
||||
}
|
||||
}
|
||||
};
|
||||
init();
|
||||
const closeOverlays = () => {
|
||||
showCoinVideoPayPopup.value = false;
|
||||
showVideoMask.value = false;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => userStore.isVip,
|
||||
(vip) => {
|
||||
if (!vip || !showVideoMask.value) return;
|
||||
const info = props.videoInfo;
|
||||
if (info.originCoins === 0 && !info.freeArea && !info.vidStatus.hasPaid) {
|
||||
showVideoMask.value = false;
|
||||
verticalVideoRef.value?.play?.();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onDeactivated(closeOverlays);
|
||||
defineExpose({
|
||||
buySuccess,
|
||||
closeOverlays,
|
||||
pause: () => verticalVideoRef.value?.pause?.(),
|
||||
play: () => verticalVideoRef.value?.play?.(),
|
||||
});
|
||||
|
||||
const visibleShare = ref(false);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.horizontalVideoBox {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.tipBox {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: rgb(255, 255, 255);
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
padding: 0 12px;
|
||||
border-radius: 6px;
|
||||
span {
|
||||
color: rgb(255, 13, 44);
|
||||
}
|
||||
}
|
||||
.goldPopup {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 999;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
font-size: 14px;
|
||||
padding-top: 42px;
|
||||
text-align: center;
|
||||
.content {
|
||||
margin: 26px 0;
|
||||
span {
|
||||
color: rgb(62, 100, 234);
|
||||
}
|
||||
}
|
||||
.btnBox {
|
||||
.btn {
|
||||
padding: 6px 18px;
|
||||
line-height: 20px;
|
||||
border-radius: 30px;
|
||||
background: linear-gradient(90deg, #ffe8be 1.65%, #e6b764 99.23%);
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #694923;
|
||||
font-weight: 400;
|
||||
|
||||
&.primary {
|
||||
margin-left: 15px;
|
||||
background: linear-gradient(286deg, #ff4d4d 0%, #ff6e6e 100%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div v-if="!closed && ads.length" class="im-chat-notify-wrap">
|
||||
<van-swipe
|
||||
class="im-chat-notify-swipe"
|
||||
vertical
|
||||
:autoplay="ads.length > 1 ? 3000 : undefined"
|
||||
:show-indicators="false"
|
||||
:touchable="ads.length > 1"
|
||||
>
|
||||
<van-swipe-item v-for="ad in ads" :key="ad.id">
|
||||
<div class="im-chat-notify" @click="onClaim(ad)">
|
||||
<LazyImg class="im-chat-notify__cover" :src="ad.cover" :is-need-decypt="true" round />
|
||||
<div class="im-chat-notify__body">
|
||||
<div class="im-chat-notify__title-row">
|
||||
<span class="im-chat-notify__title">{{ getAdTitle(ad) }}</span>
|
||||
<span class="im-chat-notify__tag">广告</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="im-chat-notify__btn" @click.stop="onClaim(ad)">
|
||||
{{ getActionText(ad) }}
|
||||
</button>
|
||||
</div>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
<button type="button" class="im-chat-notify__close-btn" aria-label="关闭" @click.stop="onClose">
|
||||
<SvgIcon name="im-adv-close" class="im-chat-notify__close" color="#fff" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { IM_AD_POS, useImAdStore } from '@/stores/imAd';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'visible-change', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
const imAdStore = useImAdStore();
|
||||
const closed = ref(false);
|
||||
|
||||
const ads = computed(() => imAdStore.getByPosition(IM_AD_POS.CHAT_NOTIFY));
|
||||
const isVisible = computed(() => !closed.value && ads.value.length > 0);
|
||||
|
||||
watch(
|
||||
isVisible,
|
||||
(visible) => {
|
||||
emit('visible-change', visible);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function getAdTitle(ad: Types.Im.AdItem) {
|
||||
return ad.title || ad.description || '精选推荐';
|
||||
}
|
||||
|
||||
function getActionText(ad: Types.Im.AdItem) {
|
||||
const desc = ad.description?.trim();
|
||||
if (desc && desc.length <= 4) return desc;
|
||||
return '领取';
|
||||
}
|
||||
|
||||
const onClaim = (ad: Types.Im.AdItem) => {
|
||||
imAdStore.markClicked(ad.id);
|
||||
jumpAdv(ad);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
closed.value = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.im-chat-notify-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: #241317;
|
||||
}
|
||||
|
||||
.im-chat-notify-swipe {
|
||||
height: 68px;
|
||||
|
||||
:deep(.van-swipe__track) {
|
||||
height: 68px;
|
||||
}
|
||||
|
||||
:deep(.van-swipe-item) {
|
||||
height: 68px;
|
||||
}
|
||||
}
|
||||
|
||||
.im-chat-notify {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
height: 68px;
|
||||
padding: 10px 36px 10px 16px;
|
||||
border-radius: 0;
|
||||
background: rgba(229, 54, 92, 0.1);
|
||||
box-sizing: border-box;
|
||||
|
||||
&__cover {
|
||||
flex-shrink: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
|
||||
:deep(.lazy_img),
|
||||
:deep(.laz_com),
|
||||
:deep(.animationImg),
|
||||
:deep(img) {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
flex-shrink: 0;
|
||||
padding: 1px 6px;
|
||||
border-radius: 6px;
|
||||
background: #3d2f2f;
|
||||
font-size: 10px;
|
||||
line-height: 16px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
height: 24px;
|
||||
width: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 12px;
|
||||
background: #ff8800;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__close-btn {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
z-index: 2;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__close {
|
||||
flex-shrink: 0;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div v-if="!closed && ads.length" class="session-ad-float">
|
||||
<van-swipe class="ad-swipe" :autoplay="4000" :show-indicators="false" :loop="ads.length > 1">
|
||||
<van-swipe-item v-for="(item, index) in ads" :key="item.id || index">
|
||||
<DecryptImg :src="item.cover" class="ad-cover" round @click="onClick(item)" />
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
<div class="ad-close" @click.stop="closed = true">×</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { IM_AD_POS, useImAdStore } from '@/stores/imAd';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
|
||||
const imAdStore = useImAdStore();
|
||||
const closed = ref(false);
|
||||
const ads = computed(() => imAdStore.getByPosition(IM_AD_POS.FLOAT));
|
||||
|
||||
const onClick = (item: Types.Im.AdItem) => {
|
||||
jumpAdv(item);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.session-ad-float {
|
||||
position: fixed;
|
||||
right: 10px;
|
||||
@include chat-layout-above-input;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.ad-swipe {
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ad-cover {
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.img-decypt),
|
||||
:deep(.vanImage),
|
||||
:deep(.van-image),
|
||||
:deep(.van-image__img) {
|
||||
width: 60px !important;
|
||||
height: 60px !important;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ad-close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
line-height: 13px;
|
||||
text-align: center;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 1);
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<Transition name="im-notify-slide">
|
||||
<div v-if="notify" class="im-message-notify" @click="onOpen">
|
||||
<LazyImg class="im-message-notify__avatar" :src="notify.peerAvatar" round eager />
|
||||
<div class="im-message-notify__body">
|
||||
<p class="im-message-notify__title">{{ notify.peerName }}</p>
|
||||
<p class="im-message-notify__preview">{{ formatSessionPreview(notify.preview) }}</p>
|
||||
</div>
|
||||
<button type="button" class="im-message-notify__close" aria-label="关闭" @click.stop="onClose">×</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { showToast } from 'vant';
|
||||
import ImApi from '@/service/im';
|
||||
import { dismissImMessageNotify, imMessageNotify } from '@/utils/im/notify';
|
||||
import { formatSessionPreview } from '@/utils/im/sessionList';
|
||||
|
||||
const router = useRouter();
|
||||
const notify = computed(() => imMessageNotify.value);
|
||||
|
||||
const onClose = () => {
|
||||
dismissImMessageNotify();
|
||||
};
|
||||
|
||||
const onOpen = async () => {
|
||||
const payload = notify.value;
|
||||
if (!payload) return;
|
||||
dismissImMessageNotify();
|
||||
try {
|
||||
const uid = await ImApi.resolveBusinessUid(payload.peerImUserId);
|
||||
await router.push({
|
||||
path: '/privateLetter',
|
||||
query: { uid: String(uid) },
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('[IM] 通知跳转失败:', e);
|
||||
showToast('无法打开私信');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.im-message-notify {
|
||||
position: fixed;
|
||||
top: calc(12px + env(safe-area-inset-top));
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
z-index: 300000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
box-sizing: border-box;
|
||||
|
||||
&__avatar {
|
||||
flex-shrink: 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
|
||||
:deep(.lazy_img),
|
||||
:deep(.laz_com),
|
||||
:deep(.animationImg),
|
||||
:deep(img) {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
&__preview {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__close {
|
||||
flex-shrink: 0;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
color: #666;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.im-notify-slide-enter-active,
|
||||
.im-notify-slide-leave-active {
|
||||
transition: transform 0.25s ease, opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.im-notify-slide-enter-from,
|
||||
.im-notify-slide-leave-to {
|
||||
transform: translateY(-120%);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div
|
||||
class="adv_swiper"
|
||||
:class="showTitle ? 'showTitleBox' : ''"
|
||||
v-if="props.list.length > 0"
|
||||
@touchstart.stop
|
||||
@touchmove.stop
|
||||
@touchend.stop
|
||||
ref="advRef"
|
||||
>
|
||||
<div class="adv-container">
|
||||
<van-swipe
|
||||
class="my-swipe"
|
||||
:show-indicators="isShowIndicators"
|
||||
:indicator-color="indicatorColor"
|
||||
:autoplay="2000"
|
||||
:key="props.swiperKey"
|
||||
>
|
||||
<van-swipe-item
|
||||
@click="handleJumpAdv(item, props.clickType)"
|
||||
v-for="item in props.list"
|
||||
:key="item.id"
|
||||
class="adv-swiper-slide"
|
||||
>
|
||||
<lazyImg :src="item.cover" class="cover" />
|
||||
<div class="advTitle ellipsis2" v-if="showTitle">{{ item.title }}</div>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import { AdImpressionType, setAdImpression } from '@/utils/eventReporter/adImpression';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reportAdClickEvent } from '@/utils/eventReporter/adClick';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
list: Types.App.AdsInfoListItem[];
|
||||
height?: string;
|
||||
isShowIndicators?: boolean;
|
||||
clickType?: 'ads' | 'recreation';
|
||||
indicatorColor?: string;
|
||||
showTitle?: boolean;
|
||||
swiperKey?: string;
|
||||
adImpression?: AdImpressionType;
|
||||
}>(),
|
||||
{
|
||||
list: () => [],
|
||||
isShowIndicators: true,
|
||||
clickType: 'ads',
|
||||
indicatorColor: '#fff',
|
||||
showTitle: () => true,
|
||||
swiperKey: '',
|
||||
},
|
||||
);
|
||||
// 广告展示埋点
|
||||
const advRef = ref();
|
||||
watch(
|
||||
() => props.list,
|
||||
() => {
|
||||
if (props.list.length) {
|
||||
nextTick(() => {
|
||||
setAdImpression(advRef.value, props.list, {
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'horizontal_cover',
|
||||
}).setupObserver();
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const handleJumpAdv = (item: Types.App.AdsInfoListItem, clickType?: 'ads' | 'recreation') => {
|
||||
let params = {
|
||||
...item,
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'horizontal_cover',
|
||||
};
|
||||
reportAdClickEvent(params);
|
||||
jumpAdv(item, clickType);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.van-swipe__indicator--active) {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
padding-right: 12px;
|
||||
}
|
||||
.adv_swiper {
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
.adv-container {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
:deep(.van-swipe__track) {
|
||||
width: 100% !important;
|
||||
}
|
||||
.adv-swiper-slide {
|
||||
width: 100% !important;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
.cover {
|
||||
height: 96px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.advTitle {
|
||||
color: #fff;
|
||||
padding: 4px 0;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.24px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.van-swipe__indicator) {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
border: none;
|
||||
background-color: #fff;
|
||||
}
|
||||
:deep(.van-swipe__indicator--active) {
|
||||
width: 4px;
|
||||
height: 2px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.showTitleBox {
|
||||
:deep(.van-swipe__indicators) {
|
||||
top: 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="persalePopup">
|
||||
<van-overlay :show="isShow" z-index="1998" @click="emit('close',true)">
|
||||
<div class="wrapper" @click.stop>
|
||||
<div class="btn" @click="emit('close',false)"></div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive,computed,nextTick } from 'vue';
|
||||
const state = reactive({
|
||||
|
||||
|
||||
});
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
}>();
|
||||
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.van-overlay) {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
.persalePopup {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.wrapper {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 342px;
|
||||
height: 456px;
|
||||
box-sizing: border-box;
|
||||
background: url('@/assets/images/png/jointGameing/popup.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
.btn{
|
||||
position: absolute;
|
||||
width: 185px;
|
||||
height: 50px;
|
||||
bottom: 32px;
|
||||
left: 50%;
|
||||
border-radius: 30px;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="kingKongBox" v-if="kingKongList.length">
|
||||
<div class="kingkongScroll" @touchstart.stop @touchmove.stop @touchend.stop @scroll="kingkongScroll">
|
||||
<div class="kingKong flex-center-between" v-for="item in kingKongList" :key="item.id" @click="jumpAdv(item)">
|
||||
<lazyImg class="kingKongIcon" :src="item.img" :key="item.id" />
|
||||
<div class="kingKongName">{{ item.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <van-progress pivot-text="" color="#FD2D55" :percentage="percentage" /> -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
const configStore = useConfigStore();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
moduleId: string;
|
||||
}>(),
|
||||
{},
|
||||
);
|
||||
const percentage = ref(0);
|
||||
const kingKongList = computed(() => {
|
||||
let arr = configStore.appConfig?.jgArea || []; // 获取金刚区数据
|
||||
if (props.moduleId) {
|
||||
return arr.filter((item) => item.mid == props.moduleId);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const kingkongScroll = (event: any) => {
|
||||
let scrollLeft = event.target.scrollLeft;
|
||||
let scrollWidth = event.target.scrollWidth;
|
||||
let clientWidth = event.target.clientWidth;
|
||||
percentage.value = (scrollLeft / (scrollWidth - clientWidth)) * 100;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.kingKongBox {
|
||||
width: 100%;
|
||||
margin: 12px 0 8px 0px;
|
||||
position: relative;
|
||||
.kingkongScroll {
|
||||
padding-left: 16px;
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.kingKong {
|
||||
margin-right: 20px;
|
||||
flex-direction: column;
|
||||
.kingKongIcon {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
:deep(.loading) {
|
||||
background-size: 18px 18px;
|
||||
}
|
||||
}
|
||||
.kingKongName {
|
||||
width: 48px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
}
|
||||
:deep(.van-progress) {
|
||||
margin: 10px auto 0;
|
||||
width: 26px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,372 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<van-popup :show="show" round position="bottom" @click-overlay="emit('close')" @open="open">
|
||||
<div class="limitedTimeOfferBox">
|
||||
<div class="line"></div>
|
||||
<div class="title-box">
|
||||
<div :class="`title_${state.title}`"></div>
|
||||
<div class="time-box">
|
||||
<van-count-down :time="countdown">
|
||||
<template #default="timeData">
|
||||
<div class="h">{{ displayHours(timeData) }}</div>
|
||||
<span>:</span>
|
||||
<div class="m">{{ timeData.minutes < 10 ? '0' + timeData.minutes : timeData.minutes }}</div>
|
||||
<span>:</span>
|
||||
<div class="s">{{ timeData.seconds < 10 ? '0' + timeData.seconds : timeData.seconds }}</div>
|
||||
</template>
|
||||
</van-count-down>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="state.currentCard && Object.keys(state.currentCard).length">
|
||||
<div class="permissions" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<div class="item" v-for="(item, index) in state.currentCard.newPrivilege" :key="index">
|
||||
<lazyImg class="item-img" :src="item.img" :key="item.img"></lazyImg>
|
||||
{{ item.privilegeName }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-box">
|
||||
<div class="item" v-for="item in state.vipCards" :key="item.productID" @click="selectCard(item)">
|
||||
<lazyImg class="img" :src="item.newBgImg" :key="item.newBgImg"></lazyImg>
|
||||
<SvgIcon name="radio-select" color="#F68804" v-if="item.productID == state.currentCard.productID" />
|
||||
<SvgIcon name="radio-unselect" color="rgb(255,255,255)" v-else />
|
||||
</div>
|
||||
</div>
|
||||
<div class="price-box">
|
||||
<div class="item">
|
||||
<span class="l">原价</span>
|
||||
<span class="r">¥{{ state.currentCard.originalPrice / 10 }}</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="l">限时优惠</span>
|
||||
<span class="r">
|
||||
¥{{
|
||||
((state.currentCard.originalPrice || 0) * 1000 - (state.currentCard.discountedPriceIos || 0) * 1000) /
|
||||
10000
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="item total">
|
||||
<span class="l">总计</span>
|
||||
<span class="r">¥{{ state.currentCard.discountedPriceIos / 10 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footter">
|
||||
<div class="btn" @click.stop="showPay">立即支付</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="isLoading">
|
||||
<Loading />
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</Teleport>
|
||||
<RechargeBox ref="rechargeBoxRef" :is-open="state.isRecharge" @closePay="handleClosePay" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import MineApi from '@/service/mine';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const userStore = useUserStore();
|
||||
const countdown = ref(0);
|
||||
const rechargeBoxRef = ref();
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['close']);
|
||||
const state = reactive({
|
||||
vipCards: <any>[],
|
||||
dcInfo: <any>{},
|
||||
isRecharge: false,
|
||||
currentCard: <any>{},
|
||||
loading: false,
|
||||
title: 'new',
|
||||
isNewbie: true,
|
||||
});
|
||||
const selectCard = (item: any) => {
|
||||
state.currentCard = item;
|
||||
};
|
||||
// 限时优惠
|
||||
const newcomerActivities = async () => {
|
||||
let createAt = new Date(userStore.userInfo.createdAt).getTime();
|
||||
let now = Date.now();
|
||||
if (now - createAt < configStore.appConfig?.newbieSaleTime * 60 * 60 * 1000) {
|
||||
let newbieSaleTime = configStore.appConfig?.newbieSaleTime * 60 * 60 * 1000;
|
||||
countdown.value = newbieSaleTime + createAt - now;
|
||||
state.title = 'new';
|
||||
state.isNewbie = true;
|
||||
} else if (now - createAt > 3600000 * 24) {
|
||||
let oldReturnSaleTime = configStore.appConfig?.oldReturnSaleTime * 60 * 60 * 1000;
|
||||
let firstLoginTime = localStorage.getItem(`first_login_time_${userStore.userInfo.uid}`);
|
||||
if (firstLoginTime) {
|
||||
countdown.value = oldReturnSaleTime + Number(firstLoginTime) - now;
|
||||
}
|
||||
state.title = 'return';
|
||||
state.isNewbie = false;
|
||||
}
|
||||
};
|
||||
// 查询会员卡列表
|
||||
const getVipList = async () => {
|
||||
let res: any = await MineApi.queryVipProduct({
|
||||
isNewUser: true,
|
||||
});
|
||||
let arr: any = [];
|
||||
if (res?.list?.length) {
|
||||
res.list.forEach((item: any) =>
|
||||
item.list.forEach((v: any) => {
|
||||
if (state.isNewbie) {
|
||||
if (configStore.appConfig?.recommendVipIds && configStore.appConfig?.recommendVipIds?.includes(v.productID)) {
|
||||
arr.push(v);
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
configStore.appConfig?.returnSaleVipIds &&
|
||||
configStore.appConfig?.returnSaleVipIds?.includes(v.productID)
|
||||
) {
|
||||
arr.push(v);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
state.vipCards = arr;
|
||||
state.currentCard = arr[0] ?? {};
|
||||
state.dcInfo = res.daichong;
|
||||
}
|
||||
};
|
||||
const open = () => {
|
||||
newcomerActivities();
|
||||
};
|
||||
const showPay = () => {
|
||||
let payData = {
|
||||
rechargeList: state.currentCard.rchgType, // 支付方式列表
|
||||
dcInfo: state.dcInfo, // 代充信息
|
||||
popType: 'vip', // 弹窗类型
|
||||
productType: state.currentCard.productType, // 商品类型
|
||||
productID: state.currentCard.productID, // 商品ID
|
||||
dcType: 1, // 代充商品类型
|
||||
buyType: 4, // 商品类型 1-是金币 2-是游戏币 3-是果币 4-是商品
|
||||
discountedPrice: state.currentCard.discountedPrice / 10, // 充值金额
|
||||
isAmountPay: state.currentCard.isAmountPay, // 是否支持金币兑换
|
||||
finalPayStatus: null, //预售卡是否支付尾款
|
||||
sourcePage: 'VIDEO_BOTTOM_SHEET' as const,
|
||||
};
|
||||
rechargeBoxRef.value.changeItem(payData);
|
||||
state.isRecharge = true;
|
||||
};
|
||||
const handleClosePay = () => {
|
||||
state.isRecharge = false;
|
||||
};
|
||||
|
||||
const displayHours = (timeData: any) => {
|
||||
if (timeData.days && timeData.days > 0) {
|
||||
const total = timeData.days * 24 + timeData.hours;
|
||||
return total < 10 ? '0' + total : total;
|
||||
}
|
||||
return timeData.hours < 10 ? '0' + timeData.hours : timeData.hours;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
async (newVal: boolean) => {
|
||||
if (newVal) {
|
||||
state.loading = true;
|
||||
await newcomerActivities();
|
||||
await getVipList();
|
||||
state.loading = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.limitedTimeOfferBox {
|
||||
border-radius: 20px 20px 0px 0px;
|
||||
background: linear-gradient(180deg, #0f0f0f 50.09%, #251d0b 99.95%);
|
||||
padding: 14px 16px 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-height: 80vh;
|
||||
.line {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
border-radius: 100px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.title-box {
|
||||
margin: 14px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.title_new {
|
||||
background: url('@/assets/images/png/new-discount-title.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
width: 123px;
|
||||
height: 22px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.title_return {
|
||||
background: url('@/assets/images/png/return-discount-title.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
width: 123px;
|
||||
height: 22px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.time-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
:deep(.van-count-down) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
span {
|
||||
font-size: 20px;
|
||||
margin: 0 5px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
.h,
|
||||
.m,
|
||||
.s {
|
||||
border-radius: 3px;
|
||||
background: #f68804;
|
||||
display: flex;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-feature-settings: 'liga' off, 'clig' off;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
.isLoading {
|
||||
height: 400px;
|
||||
}
|
||||
.permissions {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
.item {
|
||||
flex-shrink: 0;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
.item-img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.card-box {
|
||||
margin-top: 14px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
:deep(.van-swipe) {
|
||||
width: 100%;
|
||||
height: 152px;
|
||||
}
|
||||
:deep(.van-swipe-item) {
|
||||
height: 152px;
|
||||
}
|
||||
// padding: 0 30px;
|
||||
.item {
|
||||
width: 118px;
|
||||
height: 152px;
|
||||
position: relative;
|
||||
margin-right: 18px;
|
||||
svg {
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
right: 6px;
|
||||
top: 6px;
|
||||
}
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.price-box {
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 12px;
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 14px;
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
&:nth-of-type(1) {
|
||||
.r {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
&:nth-of-type(2) {
|
||||
.r {
|
||||
color: #ff9000;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
&:nth-of-type(3) {
|
||||
.r {
|
||||
// color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
.total {
|
||||
margin-bottom: 0;
|
||||
padding-top: 14px;
|
||||
border-top: 0.5px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
.footter {
|
||||
padding: 14px 0;
|
||||
width: 100%;
|
||||
.btn {
|
||||
width: 100%;
|
||||
border-radius: 3px;
|
||||
background: #f68804;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="loading_box">
|
||||
<img class="loading" :src="loadingSrc" rel="external nofollow" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import loadingSrc from '@/assets/gif/loading.gif';
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.loading_box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
img {
|
||||
width: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<!-- 大横屏无限制 -->
|
||||
<template>
|
||||
<div class="bigHorizontalScreenUnlimit">
|
||||
<HorizontalVideoBox :videoInfo="item" :videoList="videoList" v-for="item in videoList" :key="item.id" />
|
||||
<!-- 首页仅保留 101、102、103 的底部操作,107 暂时停用。
|
||||
<div class="layoutDisplay-optArea flex-center-between">
|
||||
<div class="optBox flex-center-center" @click="jumpMore">
|
||||
<div class="optName">更多片源</div>
|
||||
</div>
|
||||
<div class="optBox change flex-center-center" @click="change">
|
||||
<div class="optName">更换一批</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { showToast } from 'vant';
|
||||
import { useRouter } from 'vue-router';
|
||||
import HomeApi from '@/service/home';
|
||||
import { computed } from 'vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const props = defineProps<{
|
||||
section: Types.Home.SectionsItem;
|
||||
}>();
|
||||
const state = {
|
||||
section: props.section,
|
||||
lock: false, // 换一换的锁
|
||||
};
|
||||
const videoList = computed(() => state.section.allVideoInfo || state.section.allMediaInfo || []);
|
||||
const change = async () => {
|
||||
if (state.lock) return showToast('请勿频繁操作');
|
||||
state.lock = true;
|
||||
// let svg: any = document.querySelector('#refresh' + state.section.sectionID);
|
||||
// svg.classList.add('refresh-icon');
|
||||
try {
|
||||
let res: any;
|
||||
let list: any = [];
|
||||
if (state.section?.allMediaInfo?.length) {
|
||||
res = await HomeApi.queryMediaChange(state.section.sectionID);
|
||||
list = res.medias || [];
|
||||
} else {
|
||||
res = await HomeApi.queryVidChange(state.section.sectionID);
|
||||
list = res.videos || [];
|
||||
let num = userStore.userInfo.uid % 2;
|
||||
list = list.filter(
|
||||
(item: any) => !item.showType || (num == 0 && item.showType == 2) || (num == 1 && item.showType == 1),
|
||||
);
|
||||
}
|
||||
if (list?.length) {
|
||||
state.section.allVideoInfo = list;
|
||||
} else {
|
||||
showToast('没有更多数据了');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
state.lock = false;
|
||||
}
|
||||
};
|
||||
const jumpMore = () => {
|
||||
if (state.section?.allMediaInfo?.length) {
|
||||
router.push({
|
||||
path: '/AcgTopicList',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
title: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
router.push({
|
||||
path: '/tagShortVideo',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
name: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.horizontalVideoBox .coverBox) {
|
||||
height: 197px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<!-- 大横屏 + 横版滑动(小)布局 -->
|
||||
<template>
|
||||
<div class="bigScreenHorizontalSlidingSmall">
|
||||
<HorizontalVideoBox :videoList="videoInfoList" :videoInfo="videoInfo" :key="videoInfo.id" />
|
||||
<div class="slideBox" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<HorizontalVideoBox
|
||||
class="slideItem"
|
||||
:videoList="videoInfoList"
|
||||
:videoInfo="item"
|
||||
v-for="item in videoInfoList"
|
||||
:key="item.id"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="holidays_qw_area_bg flex-center-between">
|
||||
<div class="flex-center-center" @click="jumpMore">
|
||||
<div class="holidays_qw_vmore" />
|
||||
<div class="holidays_qw_area_name">看更多</div>
|
||||
</div>
|
||||
<div class="holidays_qw_vertical" />
|
||||
<div class="flex-center-center" @click="change">
|
||||
<div class="holidays_qw_refresh" :id="'refresh' + state.section.sectionID" />
|
||||
<div class="holidays_qw_area_name">换一批</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { showToast } from 'vant';
|
||||
import { useRouter } from 'vue-router';
|
||||
import HomeApi from '@/service/home';
|
||||
|
||||
const router = useRouter();
|
||||
const props = defineProps<{
|
||||
section: Types.Home.SectionsItem;
|
||||
}>();
|
||||
const state = {
|
||||
section: props.section,
|
||||
lock: false, // 换一换的锁
|
||||
};
|
||||
const videoInfo: any = computed(() => {
|
||||
if (state.section.videoInfo) {
|
||||
if (state.section.videoInfo.length > 1) {
|
||||
return state.section.videoInfo[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
const videoInfoList: any = computed(() => {
|
||||
if (state.section.videoInfo) {
|
||||
if (state.section.videoInfo.length > 1) {
|
||||
return state.section.videoInfo.slice(1, state.section.videoInfo.length);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const change = async () => {
|
||||
if (state.lock) return showToast('请勿频繁操作');
|
||||
state.lock = true;
|
||||
let svg: any = document.querySelector('#refresh' + state.section.sectionID);
|
||||
svg.classList.add('refresh-icon');
|
||||
try {
|
||||
let res: any = await HomeApi.queryVidChange(state.section.sectionID);
|
||||
if (res.videos) {
|
||||
state.section.videoInfo = res.videos;
|
||||
} else {
|
||||
showToast('没有更多数据了');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
svg?.classList.remove('refresh-icon');
|
||||
state.lock = false;
|
||||
}
|
||||
};
|
||||
const jumpMore = () => {
|
||||
router.push({
|
||||
path: '/tagShortVideo',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
name: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.horizontalVideoBox .coverBox) {
|
||||
height: 197px;
|
||||
}
|
||||
:deep(.slideBox .horizontalVideoBox .coverBox) {
|
||||
height: 94px;
|
||||
width: 147px;
|
||||
}
|
||||
:deep(.slideBox .horizontalVideoBox .videoTitle) {
|
||||
width: 147px;
|
||||
}
|
||||
.bigScreenHorizontalSlidingSmall {
|
||||
.slideBox {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
box-sizing: border-box;
|
||||
.slideItem {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,105 @@
|
||||
<!-- 四宫格(横) -->
|
||||
<template>
|
||||
<div class="fourGridHorizontal">
|
||||
<HorizontalVideoBox
|
||||
class="videoBox"
|
||||
:videoList="videoList"
|
||||
:videoInfo="item"
|
||||
v-for="item in videoList"
|
||||
:key="item.id"
|
||||
acgHorizontal
|
||||
:showTimeAndComment="!item.mediaType"
|
||||
/>
|
||||
<!-- 此组件也用于未知 showType 的兜底,底部操作仅在 102 时保留。 -->
|
||||
<div v-if="section.showType == 102" class="layoutDisplay-optArea flex-center-between">
|
||||
<div class="optBox flex-center-center" @click="jumpMore">
|
||||
<!-- <SvgIcon name="viewMore" class="optIcon"></SvgIcon> -->
|
||||
<div class="optName">更多片源</div>
|
||||
</div>
|
||||
<div class="optBox change flex-center-center" @click="change">
|
||||
<!-- <SvgIcon name="refreshIcon" class="optIcon" :id="'refresh' + section.sectionID"></SvgIcon> -->
|
||||
<div class="optName">更换一批</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
import { showToast } from 'vant';
|
||||
import { useRouter } from 'vue-router';
|
||||
import HomeApi from '@/service/home';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
const userStore = useUserStore();
|
||||
|
||||
const router = useRouter();
|
||||
const props = defineProps<{
|
||||
section: Types.Home.SectionsItem;
|
||||
}>();
|
||||
const state = reactive({
|
||||
section: props.section,
|
||||
lock: false, // 换一换的锁
|
||||
});
|
||||
const videoList = computed(() => {
|
||||
let arr = state.section.allVideoInfo || state.section.allMediaInfo || [];
|
||||
return arr.slice(0, 4);
|
||||
});
|
||||
const change = async () => {
|
||||
if (state.lock) return showToast('请勿频繁操作');
|
||||
state.lock = true;
|
||||
// let svg: any = document.querySelector('#refresh' + state.section.sectionID);
|
||||
// svg.classList.add('refresh-icon');
|
||||
try {
|
||||
let res: any;
|
||||
let list: any = [];
|
||||
if (state.section?.allMediaInfo?.length) {
|
||||
res = await HomeApi.queryMediaChange(state.section.sectionID);
|
||||
list = res.medias || [];
|
||||
} else {
|
||||
res = await HomeApi.queryVidChange(state.section.sectionID);
|
||||
list = res.videos || [];
|
||||
let num = userStore.userInfo.uid % 2;
|
||||
list = list.filter(
|
||||
(item: any) => !item.showType || (num == 0 && item.showType == 2) || (num == 1 && item.showType == 1),
|
||||
);
|
||||
}
|
||||
if (list?.length) {
|
||||
state.section.allVideoInfo = list;
|
||||
} else {
|
||||
showToast('没有更多数据了');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
state.lock = false;
|
||||
}
|
||||
};
|
||||
const jumpMore = () => {
|
||||
if (state.section?.allMediaInfo?.length) {
|
||||
router.push({
|
||||
path: '/AcgTopicList',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
title: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
router.push({
|
||||
path: '/tagShortVideo',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
name: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.fourGridHorizontal {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
.videoBox {
|
||||
width: 168px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,119 @@
|
||||
<!-- 四宫格(竖) -->
|
||||
<template>
|
||||
<div class="fourGridVertical">
|
||||
<HorizontalVideoBox
|
||||
class="videoBox"
|
||||
:videoList="videoInfoList"
|
||||
:videoInfo="item"
|
||||
v-for="item in videoInfoList"
|
||||
:key="item.id"
|
||||
:isCollection="true"
|
||||
:showTimeAndComment="false"
|
||||
/>
|
||||
<!-- 首页仅保留 101、102、103 的底部操作,201 暂时停用。
|
||||
<div class="layoutDisplay-optArea flex-center-between">
|
||||
<div class="optBox flex-center-center" @click="jumpMore">
|
||||
<div class="optName">更多片源</div>
|
||||
</div>
|
||||
<div class="optBox change flex-center-center" @click="change">
|
||||
<div class="optName">更换一批</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { showToast } from 'vant';
|
||||
import { useRouter } from 'vue-router';
|
||||
import HomeApi from '@/service/home';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
const userStore = useUserStore();
|
||||
|
||||
const router = useRouter();
|
||||
const props = defineProps<{
|
||||
section: Types.Home.SectionsItem;
|
||||
}>();
|
||||
const videoInfoList: any = computed(() => {
|
||||
let arr = state.section.allVideoInfo || state.section.allMediaInfo;
|
||||
if (arr) {
|
||||
return arr.slice(0, 4);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const state = {
|
||||
section: props.section,
|
||||
lock: false, // 换一换的锁
|
||||
};
|
||||
const change = async () => {
|
||||
if (state.lock) return showToast('请勿频繁操作');
|
||||
state.lock = true;
|
||||
// let svg: any = document.querySelector('#refresh' + state.section.sectionID);
|
||||
// svg.classList.add('refresh-icon');
|
||||
try {
|
||||
let res: any;
|
||||
let list: any = [];
|
||||
if (state.section?.allMediaInfo?.length) {
|
||||
res = await HomeApi.queryMediaChange(state.section.sectionID);
|
||||
list = res.medias || [];
|
||||
} else {
|
||||
res = await HomeApi.queryVidChange(state.section.sectionID);
|
||||
list = res.videos || [];
|
||||
let num = userStore.userInfo.uid % 2;
|
||||
list = list.filter(
|
||||
(item: any) => !item.showType || (num == 0 && item.showType == 2) || (num == 1 && item.showType == 1),
|
||||
);
|
||||
}
|
||||
if (list?.length) {
|
||||
state.section.allVideoInfo = list;
|
||||
} else {
|
||||
showToast('没有更多数据了');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
state.lock = false;
|
||||
}
|
||||
};
|
||||
const jumpMore = () => {
|
||||
if (state.section?.allMediaInfo?.length) {
|
||||
router.push({
|
||||
path: '/AcgTopicList',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
title: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
router.push({
|
||||
path: '/tagShortVideo',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
name: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.horizontalVideoBox) {
|
||||
.coverBox {
|
||||
height: 224px;
|
||||
}
|
||||
.videoTitleBox {
|
||||
.videoTitle {
|
||||
height: 20px;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.fourGridVertical {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
.videoBox {
|
||||
width: 168px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div class="guessYouLike">
|
||||
<div class="title">
|
||||
<div class="title-icon"></div>
|
||||
</div>
|
||||
<van-tabs
|
||||
v-model:active="state.tabActive"
|
||||
class="guess-tabs"
|
||||
title-active-color="rgba(255, 255, 255, 0.90)"
|
||||
title-inactive-color="rgba(255, 255, 255, 0.45)"
|
||||
animated
|
||||
:scrollspy="false"
|
||||
:lazy-render="true"
|
||||
@change="getList()"
|
||||
>
|
||||
<van-tab :name="item.key" title="" v-for="(item, index) in state.tabList" :key="index">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<div class="moreMoviesBox">
|
||||
<HorizontalVideoBox
|
||||
class="videoBox"
|
||||
:videoInfo="item"
|
||||
:videoList="state.list"
|
||||
v-for="item in state.list"
|
||||
:key="item.id"
|
||||
/>
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, watch } from 'vue';
|
||||
import VideoApi from '@/service/video';
|
||||
const props = defineProps<{
|
||||
moduleId: number | string;
|
||||
}>();
|
||||
const state = reactive({
|
||||
tabActive: 0,
|
||||
tabList: [
|
||||
// {
|
||||
// label: '最多收藏',
|
||||
// key: 3,
|
||||
// },
|
||||
// {
|
||||
// label: '最新上架',
|
||||
// key: 1,
|
||||
// },
|
||||
{
|
||||
label: '最多观看',
|
||||
key: 0,
|
||||
},
|
||||
],
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
list: <any>[],
|
||||
});
|
||||
const moreParams = reactive({
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
isNoData: false,
|
||||
error: false,
|
||||
isHigehtMax: false,
|
||||
});
|
||||
const getList = async () => {
|
||||
let req = {
|
||||
pageNumber: state.pageNumber,
|
||||
pageSize: state.pageSize,
|
||||
type: state.tabActive,
|
||||
};
|
||||
try {
|
||||
let res: any = await VideoApi.getGuessYouLike({ id: props.moduleId, data: req });
|
||||
moreParams.loading = false;
|
||||
moreParams.refreshing = false;
|
||||
let list = res.allVideoInfo || [];
|
||||
if (state.pageNumber === 1) {
|
||||
state.list = list;
|
||||
} else {
|
||||
state.list = state.list.concat(list);
|
||||
}
|
||||
|
||||
moreParams.isNoData = state.list.length === 0;
|
||||
if (!res.hasNext) {
|
||||
moreParams.finished = true;
|
||||
}
|
||||
} catch (e) {
|
||||
moreParams.loading = false;
|
||||
moreParams.error = true;
|
||||
}
|
||||
};
|
||||
watch(
|
||||
() => props.moduleId,
|
||||
(v) => {
|
||||
if (v) {
|
||||
getList();
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
const onLoad = () => {
|
||||
if (moreParams.refreshing) return;
|
||||
moreParams.loading = true;
|
||||
state.pageNumber++;
|
||||
getList();
|
||||
};
|
||||
const onRefresh = () => {
|
||||
state.pageNumber = 1;
|
||||
moreParams.refreshing = true;
|
||||
moreParams.finished = false;
|
||||
getList();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.guessYouLike {
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, rgba(225, 53, 31, 0) 0%, rgba(225, 53, 31, 0.2) 50.5%, rgba(225, 53, 31, 0) 62%);
|
||||
background-size: 375px 212px;
|
||||
background-position: -16px 0px;
|
||||
.title {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 18px;
|
||||
margin-bottom: 10px;
|
||||
height: 52px;
|
||||
background: url('@/assets/images/png/guessYouLike-bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
position: relative;
|
||||
.title-icon {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: url('@/assets/images/png/guessYouLike-title.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
width: 63.5px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
.guess-tabs {
|
||||
:deep(.van-tabs__wrap) {
|
||||
display: none;
|
||||
padding-right: 70px;
|
||||
.van-tabs__nav {
|
||||
.van-tab {
|
||||
font-size: 14px !important;
|
||||
font-weight: 400;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.van-tabs__line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.moreMoviesBox {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-column-gap: 7px;
|
||||
padding: 0;
|
||||
.videoBox {
|
||||
width: 174px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<!-- 横版滑动(大)布局 -->
|
||||
<template>
|
||||
<div class="horizontalSlidingBig">
|
||||
<div class="slideBox" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<HorizontalVideoBox
|
||||
class="slideItem"
|
||||
:videoInfo="item"
|
||||
:videoList="videoList"
|
||||
v-for="item in videoList"
|
||||
:key="item.id"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="holidays_qw_area_bg flex-center-between">
|
||||
<div class="flex-center-center" @click="jumpMore">
|
||||
<div class="holidays_qw_vmore" />
|
||||
<div class="holidays_qw_area_name">看更多</div>
|
||||
</div>
|
||||
<div class="holidays_qw_vertical" />
|
||||
<div class="flex-center-center" @click="change">
|
||||
<div class="holidays_qw_refresh" :id="'refresh' + state.section.sectionID" />
|
||||
<div class="holidays_qw_area_name">换一批</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { showToast } from 'vant';
|
||||
import { useRouter } from 'vue-router';
|
||||
import HomeApi from '@/service/home';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const router = useRouter();
|
||||
const props = defineProps<{
|
||||
section: Types.Home.SectionsItem;
|
||||
}>();
|
||||
const state = {
|
||||
section: props.section,
|
||||
lock: false, // 换一换的锁
|
||||
};
|
||||
const videoList = computed(() => state.section.allVideoInfo || state.section.allMediaInfo || []);
|
||||
const change = async () => {
|
||||
if (state.lock) return showToast('请勿频繁操作');
|
||||
state.lock = true;
|
||||
let svg: any = document.querySelector('#refresh' + state.section.sectionID);
|
||||
svg.classList.add('refresh-icon');
|
||||
try {
|
||||
let res: any = await HomeApi.queryVidChange(state.section.sectionID);
|
||||
if (res.videos) {
|
||||
state.section.videoInfo = res.videos;
|
||||
} else {
|
||||
showToast('没有更多数据了');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
svg?.classList.remove('refresh-icon');
|
||||
state.lock = false;
|
||||
}
|
||||
};
|
||||
const jumpMore = () => {
|
||||
router.push({
|
||||
path: '/tagShortVideo',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
name: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.horizontalVideoBox) {
|
||||
width: 235px;
|
||||
.coverBox {
|
||||
height: 132px;
|
||||
}
|
||||
}
|
||||
.horizontalSlidingBig {
|
||||
width: 100dvw;
|
||||
.slideBox {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
.slideItem {
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<!-- 横版滑动(小)布局 -->
|
||||
<template>
|
||||
<div class="horizontalSlidingSmall">
|
||||
<div class="slideBox" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<HorizontalVideoBox
|
||||
class="slideItem"
|
||||
:videoInfo="item"
|
||||
:videoList="videoList"
|
||||
v-for="item in videoList"
|
||||
:key="item.id"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="holidays_qw_area_bg flex-center-between">
|
||||
<div class="flex-center-center" @click="jumpMore">
|
||||
<div class="holidays_qw_vmore" />
|
||||
<div class="holidays_qw_area_name">看更多</div>
|
||||
</div>
|
||||
<div class="holidays_qw_vertical" />
|
||||
<div class="flex-center-center" @click="change">
|
||||
<div class="holidays_qw_refresh" :id="'refresh' + state.section.sectionID" />
|
||||
<div class="holidays_qw_area_name">换一批</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { showToast } from 'vant';
|
||||
import { useRouter } from 'vue-router';
|
||||
import HomeApi from '@/service/home';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const router = useRouter();
|
||||
const props = defineProps<{
|
||||
section: Types.Home.SectionsItem;
|
||||
}>();
|
||||
const state = {
|
||||
section: props.section,
|
||||
lock: false, // 换一换的锁
|
||||
};
|
||||
const videoList = computed(() => state.section.allVideoInfo || state.section.allMediaInfo || []);
|
||||
const change = async () => {
|
||||
if (state.lock) return showToast('请勿频繁操作');
|
||||
state.lock = true;
|
||||
let svg: any = document.querySelector('#refresh' + state.section.sectionID);
|
||||
svg.classList.add('refresh-icon');
|
||||
try {
|
||||
let res: any = await HomeApi.queryVidChange(state.section.sectionID);
|
||||
if (res.videos) {
|
||||
state.section.videoInfo = res.videos;
|
||||
} else {
|
||||
showToast('没有更多数据了');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
svg?.classList.remove('refresh-icon');
|
||||
state.lock = false;
|
||||
}
|
||||
};
|
||||
const jumpMore = () => {
|
||||
router.push({
|
||||
path: '/tagShortVideo',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
name: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.horizontalVideoBox) {
|
||||
width: 147px;
|
||||
.coverBox {
|
||||
height: 82px;
|
||||
}
|
||||
}
|
||||
.horizontalSlidingSmall {
|
||||
width: 100dvw;
|
||||
.slideBox {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
box-sizing: border-box;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
width: 100%;
|
||||
.slideItem {
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<!-- 三横版布局 -->
|
||||
<template>
|
||||
<div class="horizontalThreeBig">
|
||||
<div class="slideBox openX">
|
||||
<HorizontalVideoBox
|
||||
:videoInfo="item"
|
||||
:videoList="videoList"
|
||||
v-for="item in videoList.slice(0, 3)"
|
||||
:key="item.id"
|
||||
:showType="5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useVideoStore } from '@/stores/video';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const router = useRouter();
|
||||
const videoStore = useVideoStore();
|
||||
const props = defineProps<{
|
||||
section: Types.Home.SectionsItem;
|
||||
}>();
|
||||
const state = {
|
||||
pageNumber: 1,
|
||||
pageSize: 6,
|
||||
section: props.section,
|
||||
};
|
||||
const videoList = computed(() => state.section.allVideoInfo || state.section.allMediaInfo || []);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.horizontalThreeBig {
|
||||
.slideBox {
|
||||
:deep(.horizontalVideoBox) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
.coverBox {
|
||||
height: 96px;
|
||||
width: 170px;
|
||||
}
|
||||
.videoTitleBox {
|
||||
flex: 1;
|
||||
height: 96px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding-left: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<!-- 一大四小布局 -->
|
||||
<template>
|
||||
<div class="oneBigFourSmall">
|
||||
<HorizontalVideoBox :videoList="videoInfoPlayList" :videoInfo="allVideoInfo" :key="allVideoInfo?.id" />
|
||||
<div class="fourGrid">
|
||||
<HorizontalVideoBox
|
||||
class="videoBox"
|
||||
:videoInfo="item"
|
||||
:videoList="videoInfoPlayList"
|
||||
v-for="item in videoInfoList"
|
||||
:key="item.id"
|
||||
/>
|
||||
</div>
|
||||
<div class="layoutDisplay-optArea flex-center-between">
|
||||
<div class="optBox flex-center-center" @click="jumpMore">
|
||||
<!-- <SvgIcon name="viewMore" class="optIcon"></SvgIcon> -->
|
||||
<div class="optName">更多片源</div>
|
||||
</div>
|
||||
<div class="optBox change flex-center-center" @click="change">
|
||||
<!-- <SvgIcon name="refreshIcon" class="optIcon" :id="'refresh' + section.sectionID"></SvgIcon> -->
|
||||
<div class="optName">更换一批</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { showToast } from 'vant';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import HomeApi from '@/service/home';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const props = defineProps<{
|
||||
section: Types.Home.SectionsItem;
|
||||
}>();
|
||||
const allVideoInfo: any = computed(() => {
|
||||
let arr = state.section.allVideoInfo || state.section.allMediaInfo;
|
||||
if (arr) {
|
||||
if (arr.length > 0) {
|
||||
return arr[0];
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
const videoInfoList: any = computed(() => {
|
||||
let arr = state.section.allVideoInfo || state.section.allMediaInfo;
|
||||
if (arr) {
|
||||
return arr.slice(1, 5) || [];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const videoInfoPlayList: any = computed(() => {
|
||||
let arr = state.section.allVideoInfo || state.section.allMediaInfo;
|
||||
if (arr) {
|
||||
return arr.slice(0, 5);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const state = {
|
||||
section: props.section,
|
||||
lock: false, // 换一换的锁
|
||||
};
|
||||
const change = async () => {
|
||||
if (state.lock) return showToast('请勿频繁操作');
|
||||
state.lock = true;
|
||||
// let svg: any = document.querySelector('#refresh' + state.section.sectionID);
|
||||
// svg.classList.add('refresh-icon');
|
||||
try {
|
||||
let res: any;
|
||||
let list: any = [];
|
||||
if (state.section?.allMediaInfo?.length) {
|
||||
res = await HomeApi.queryMediaChange(state.section.sectionID);
|
||||
list = res.medias || [];
|
||||
} else {
|
||||
res = await HomeApi.queryVidChange(state.section.sectionID);
|
||||
list = res.videos || [];
|
||||
let num = userStore.userInfo.uid % 2;
|
||||
list = list.filter(
|
||||
(item: any) => !item.showType || (num == 0 && item.showType == 2) || (num == 1 && item.showType == 1),
|
||||
);
|
||||
}
|
||||
if (list?.length) {
|
||||
state.section.allVideoInfo = list;
|
||||
} else {
|
||||
showToast('没有更多数据了');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
state.lock = false;
|
||||
}
|
||||
};
|
||||
const jumpMore = () => {
|
||||
if (state.section?.allMediaInfo?.length) {
|
||||
router.push({
|
||||
path: '/AcgTopicList',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
title: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
router.push({
|
||||
path: '/tagShortVideo',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
name: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.horizontalVideoBox .coverBox) {
|
||||
height: 197px !important;
|
||||
}
|
||||
:deep(.fourGrid .horizontalVideoBox .coverBox) {
|
||||
height: 94px !important;
|
||||
width: 168px;
|
||||
}
|
||||
.oneBigFourSmall {
|
||||
.fourGrid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
.videoBox {
|
||||
width: 168px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,106 @@
|
||||
<!-- 六宫格 -->
|
||||
<template>
|
||||
<div class="sixGrid">
|
||||
<HorizontalVideoBox
|
||||
class="videoBox"
|
||||
:videoInfo="item"
|
||||
:videoList="videoInfoList"
|
||||
v-for="item in videoInfoList"
|
||||
:key="item.id"
|
||||
/>
|
||||
<div class="layoutDisplay-optArea flex-center-between">
|
||||
<div class="optBox flex-center-center" @click="jumpMore">
|
||||
<!-- <SvgIcon name="viewMore" class="optIcon"></SvgIcon> -->
|
||||
<div class="optName">更多片源</div>
|
||||
</div>
|
||||
<div class="optBox change flex-center-center" @click="change">
|
||||
<!-- <SvgIcon name="refreshIcon" class="optIcon" :id="'refresh' + section.sectionID"></SvgIcon> -->
|
||||
<div class="optName">更换一批</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { showToast } from 'vant';
|
||||
import { useRouter } from 'vue-router';
|
||||
import HomeApi from '@/service/home';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
const userStore = useUserStore();
|
||||
|
||||
const router = useRouter();
|
||||
const props = defineProps<{
|
||||
section: Types.Home.SectionsItem;
|
||||
}>();
|
||||
const videoInfoList: any = computed(() => {
|
||||
let arr = state.section.allVideoInfo || state.section.allMediaInfo;
|
||||
if (arr) {
|
||||
return arr.length > 6 ? arr.slice(0, 6) : arr;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const state = {
|
||||
section: props.section,
|
||||
lock: false, // 换一换的锁
|
||||
};
|
||||
const change = async () => {
|
||||
if (state.lock) return showToast('请勿频繁操作');
|
||||
state.lock = true;
|
||||
// let svg: any = document.querySelector('#refresh' + state.section.sectionID);
|
||||
// svg.classList.add('refresh-icon');
|
||||
try {
|
||||
let res: any;
|
||||
let list: any = [];
|
||||
if (state.section?.allMediaInfo?.length) {
|
||||
res = await HomeApi.queryMediaChange(state.section.sectionID);
|
||||
list = res.medias || [];
|
||||
} else {
|
||||
res = await HomeApi.queryVidChange(state.section.sectionID);
|
||||
list = res.videos || [];
|
||||
let num = userStore.userInfo.uid % 2;
|
||||
list = list.filter(
|
||||
(item: any) => !item.showType || (num == 0 && item.showType == 2) || (num == 1 && item.showType == 1),
|
||||
);
|
||||
}
|
||||
if (list?.length) {
|
||||
state.section.allVideoInfo = list;
|
||||
} else {
|
||||
showToast('没有更多数据了');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
state.lock = false;
|
||||
}
|
||||
};
|
||||
const jumpMore = () => {
|
||||
if (state.section?.allMediaInfo?.length) {
|
||||
router.push({
|
||||
path: '/AcgTopicList',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
title: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
router.push({
|
||||
path: '/tagShortVideo',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
name: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.sixGrid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
.videoBox {
|
||||
width: 168px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<div class="updateDaily" v-if="state.list && state.list.length">
|
||||
<div class="title">{{ moduleName }}精品-每日更新</div>
|
||||
<div class="main">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad">
|
||||
<div class="videoSlide">
|
||||
<HorizontalVideoBox
|
||||
class="videoBox"
|
||||
:videoInfo="item"
|
||||
:videoList="state.list"
|
||||
v-for="item in state.list"
|
||||
:key="item.id"
|
||||
/>
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue';
|
||||
import HomeApi from '@/service/home';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
tagId: string;
|
||||
moduleName: string;
|
||||
}>(),
|
||||
{
|
||||
tagId: '',
|
||||
moduleName: '',
|
||||
},
|
||||
);
|
||||
const state = reactive({
|
||||
list: <Array<Types.Video.VInfosItem>>[],
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
const moreParams = reactive({
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
isNoData: false,
|
||||
error: false,
|
||||
isHigehtMax: false,
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
// 获取数据
|
||||
const getList = async (type: any) => {
|
||||
try {
|
||||
let req = {
|
||||
type: 0,
|
||||
model: 1,
|
||||
tag: props.tagId || undefined,
|
||||
paymentType: 0,
|
||||
pageNumber: state.pageNumber,
|
||||
pageSize: state.pageSize,
|
||||
time: new Date().toISOString(),
|
||||
};
|
||||
let res: any = await HomeApi.queryRecommendVidList(req);
|
||||
moreParams.loading = false;
|
||||
moreParams.refreshing = false;
|
||||
let list = res.list || [];
|
||||
if (type === 'refresh') {
|
||||
state.list = list;
|
||||
} else {
|
||||
state.list = state.list.concat(list);
|
||||
}
|
||||
if (state.pageNumber == 1 && state.list.length == 0) {
|
||||
moreParams.isNoData = true;
|
||||
}
|
||||
if (!res.hasNext) {
|
||||
moreParams.finished = true;
|
||||
}
|
||||
} catch (e) {
|
||||
moreParams.loading = false;
|
||||
moreParams.error = true;
|
||||
}
|
||||
};
|
||||
getList('');
|
||||
const onLoad = () => {
|
||||
moreParams.loading = true;
|
||||
state.pageNumber++;
|
||||
getList(null);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.updateDaily {
|
||||
padding: 0 14px;
|
||||
height: 100%;
|
||||
.title {
|
||||
font-size: 14px;
|
||||
color: $titleColor;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.main {
|
||||
height: calc(100vh - 100px);
|
||||
.videoSlide {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
.videoBox {
|
||||
width: 168px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,134 @@
|
||||
<!-- 竖排9宫格 -->
|
||||
<template>
|
||||
<div class="verticalNineGrid">
|
||||
<div class="videoBox">
|
||||
<HorizontalVideoBox
|
||||
class="slideItem"
|
||||
:videoInfo="item"
|
||||
:videoList="videoList"
|
||||
v-for="item in videoList"
|
||||
:key="item.id"
|
||||
:isCollection="true"
|
||||
:showTimeAndComment="false"
|
||||
/>
|
||||
<!-- 首页仅保留 101、102、103 的底部操作,203 暂时停用。
|
||||
<div class="layoutDisplay-optArea flex-center-between">
|
||||
<div class="optBox flex-center-center" @click="jumpMore">
|
||||
<div class="optName">更多片源</div>
|
||||
</div>
|
||||
<div class="optBox change flex-center-center" @click="change">
|
||||
<div class="optName">更换一批</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { showToast } from 'vant';
|
||||
import { useRouter } from 'vue-router';
|
||||
import HomeApi from '@/service/home';
|
||||
const router = useRouter();
|
||||
const props = defineProps<{
|
||||
section: Types.Home.SectionsItem;
|
||||
}>();
|
||||
const state = {
|
||||
section: props.section,
|
||||
lock: false, // 换一换的锁
|
||||
};
|
||||
const videoList: any = computed(() => {
|
||||
let arr = state.section.allVideoInfo || state.section.allMediaInfo;
|
||||
if (arr) {
|
||||
return arr.length > 9 ? arr.slice(0, 9) : arr;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const change = async () => {
|
||||
if (state.lock) return showToast('请勿频繁操作');
|
||||
state.lock = true;
|
||||
try {
|
||||
let res: any;
|
||||
let list: any = [];
|
||||
if (state.section?.allMediaInfo?.length) {
|
||||
res = await HomeApi.queryMediaChange(state.section.sectionID);
|
||||
list = res.medias || [];
|
||||
} else {
|
||||
res = await HomeApi.queryVidChange(state.section.sectionID);
|
||||
list = res.videos || [];
|
||||
}
|
||||
|
||||
if (list?.length) {
|
||||
state.section.allVideoInfo = list;
|
||||
} else {
|
||||
showToast('没有更多数据了');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
state.lock = false;
|
||||
}
|
||||
};
|
||||
const jumpMore = () => {
|
||||
if (state.section?.allMediaInfo?.length) {
|
||||
router.push({
|
||||
path: '/AcgTopicList',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
title: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
router.push({
|
||||
path: '/tagShortVideo',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
name: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.horizontalVideoBox) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
:deep(.horizontalVideoBox .coverBox) {
|
||||
height: 150px;
|
||||
}
|
||||
.verticalNineGrid {
|
||||
width: 100%;
|
||||
.videoBox {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
.slideItem {
|
||||
width: 111px;
|
||||
:deep(.coverBox) {
|
||||
height: 148px;
|
||||
}
|
||||
:deep(.videoTitleBox) {
|
||||
.videoTitle {
|
||||
height: 20px;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// width: 100dvw;
|
||||
// .videoBox {
|
||||
// width: 100%;
|
||||
// display: flex;
|
||||
// overflow-x: auto;
|
||||
// -webkit-overflow-scrolling: touch;
|
||||
// box-sizing: border-box;
|
||||
// .slideItem {
|
||||
// flex-shrink: 0;
|
||||
// margin-right: 10px;
|
||||
// }
|
||||
// }
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
<!-- 竖排六宫格 -->
|
||||
<template>
|
||||
<div class="verticalSixGrid">
|
||||
<div class="videoBox">
|
||||
<HorizontalVideoBox
|
||||
class="videoItem"
|
||||
:videoInfo="item"
|
||||
:videoList="videoInfoList"
|
||||
v-for="item in videoInfoList"
|
||||
:key="item.id"
|
||||
:isCollection="true"
|
||||
:showTimeAndComment="false"
|
||||
/>
|
||||
</div>
|
||||
<!-- 首页仅保留 101、102、103 的底部操作,202 暂时停用。
|
||||
<div class="layoutDisplay-optArea flex-center-between">
|
||||
<div class="optBox flex-center-center" @click="jumpMore">
|
||||
<div class="optName">更多片源</div>
|
||||
</div>
|
||||
<div class="optBox change flex-center-center" @click="change">
|
||||
<div class="optName">更换一批</div>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { showToast } from 'vant';
|
||||
import { useRouter } from 'vue-router';
|
||||
import HomeApi from '@/service/home';
|
||||
|
||||
const router = useRouter();
|
||||
const props = defineProps<{
|
||||
section: Types.Home.SectionsItem;
|
||||
}>();
|
||||
const videoInfoList: any = computed(() => {
|
||||
let arr = state.section.allVideoInfo || state.section.allMediaInfo;
|
||||
if (arr) {
|
||||
return arr.length > 6 ? arr.slice(0, 6) : arr;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const state = {
|
||||
section: props.section,
|
||||
lock: false, // 换一换的锁
|
||||
};
|
||||
const change = async () => {
|
||||
if (state.lock) return showToast('请勿频繁操作');
|
||||
state.lock = true;
|
||||
// let svg: any = document.querySelector('#refresh' + state.section.sectionID);
|
||||
// svg.classList.add('refresh-icon');
|
||||
try {
|
||||
let res: any;
|
||||
let list: any = [];
|
||||
if (state.section?.allMediaInfo?.length) {
|
||||
res = await HomeApi.queryMediaChange(state.section.sectionID);
|
||||
list = res.medias || [];
|
||||
} else {
|
||||
res = await HomeApi.queryVidChange(state.section.sectionID);
|
||||
list = res.videos || [];
|
||||
}
|
||||
if (list?.length) {
|
||||
state.section.allVideoInfo = list;
|
||||
} else {
|
||||
showToast('没有更多数据了');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
state.lock = false;
|
||||
}
|
||||
};
|
||||
const jumpMore = () => {
|
||||
if (state.section?.allMediaInfo?.length) {
|
||||
router.push({
|
||||
path: '/AcgTopicList',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
title: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
router.push({
|
||||
path: '/tagShortVideo',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
name: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.horizontalVideoBox .coverBox) {
|
||||
height: 162px;
|
||||
}
|
||||
.verticalSixGrid {
|
||||
.videoBox {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-column-gap: 5px;
|
||||
.videoItem {
|
||||
width: 111px;
|
||||
:deep(.coverBox) {
|
||||
height: 148px;
|
||||
}
|
||||
:deep(.videoTitleBox) {
|
||||
.videoTitle {
|
||||
height: 20px;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,106 @@
|
||||
<!-- 竖版滑动(大)布局 -->
|
||||
<template>
|
||||
<div class="verticalSlideBig">
|
||||
<div class="videoBox" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<HorizontalVideoBox
|
||||
class="slideItem"
|
||||
:videoInfo="item"
|
||||
:videoList="videoList"
|
||||
v-for="item in videoList"
|
||||
:key="item.id"
|
||||
:isCollection="true"
|
||||
:showTimeAndComment="false"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="holidays_qw_area_bg flex-center-between">
|
||||
<div class="flex-center-center" @click="jumpMore">
|
||||
<div class="holidays_qw_vmore" />
|
||||
<div class="holidays_qw_area_name">看更多</div>
|
||||
</div>
|
||||
<div class="holidays_qw_vertical" />
|
||||
<div class="flex-center-center" @click="change">
|
||||
<div class="holidays_qw_refresh" :id="'refresh' + state.section.sectionID" />
|
||||
<div class="holidays_qw_area_name">换一批</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { showToast } from 'vant';
|
||||
import { useRouter } from 'vue-router';
|
||||
import HomeApi from '@/service/home';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const router = useRouter();
|
||||
const props = defineProps<{
|
||||
section: Types.Home.SectionsItem;
|
||||
}>();
|
||||
const state = {
|
||||
section: props.section,
|
||||
lock: false, // 换一换的锁
|
||||
};
|
||||
|
||||
const videoList = computed(() => state.section.allVideoInfo || state.section.allMediaInfo || []);
|
||||
const change = async () => {
|
||||
if (state.lock) return showToast('请勿频繁操作');
|
||||
state.lock = true;
|
||||
let svg: any = document.querySelector('#refresh' + state.section.sectionID);
|
||||
svg.classList.add('refresh-icon');
|
||||
try {
|
||||
let res: any;
|
||||
if (state.section?.allMediaInfo?.length) {
|
||||
res = await HomeApi.queryMediaChange(state.section.sectionID);
|
||||
} else {
|
||||
res = await HomeApi.queryVidChange(state.section.sectionID);
|
||||
}
|
||||
if (res.videos) {
|
||||
state.section.videoInfo = res.videos;
|
||||
} else {
|
||||
showToast('没有更多数据了');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
svg?.classList.remove('refresh-icon');
|
||||
state.lock = false;
|
||||
}
|
||||
};
|
||||
const jumpMore = () => {
|
||||
router.push({
|
||||
path: '/tagShortVideo',
|
||||
query: {
|
||||
id: state.section.sectionID,
|
||||
name: state.section.sectionName,
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.horizontalVideoBox .coverBox) {
|
||||
height: 205px;
|
||||
width: 146px;
|
||||
}
|
||||
:deep(.horizontalVideoBox) {
|
||||
width: 146px;
|
||||
}
|
||||
.verticalSlideBig {
|
||||
width: 100dvw;
|
||||
.videoBox {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
box-sizing: border-box;
|
||||
.slideItem {
|
||||
flex-shrink: 0;
|
||||
margin-right: 10px;
|
||||
:deep(.videoTitleBox) {
|
||||
.videoTitle {
|
||||
height: 20px;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,215 @@
|
||||
<!-- 二竖屏左右滑动 -->
|
||||
<template>
|
||||
<div class="horizontalTwoSlid" v-if="state.newArr?.length" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<swiper class="swiper" :loop="true" :effect="'creative'" :slidesPerView="'auto'">
|
||||
<swiper-slide class="swiperSlide" v-for="(arr, index) in state.newArr" :key="index">
|
||||
<div class="actorBox" v-for="item in arr" :key="item.id" @click="jumpPage(item)">
|
||||
<lazyImg class="actorAvatar" :src="item.cover ?? item.verticalCover" :key="item.cover ?? item.verticalCover">
|
||||
<template v-if="['video', 'image', 'text'].includes(item.mediaType)">
|
||||
<div class="vip_icon free_icon" v-if="configStore.appConfig.freeMark && item.permission === 2">免费</div>
|
||||
<div class="vip_icon" v-if="configStore.appConfig.vipMark && item.permission == 0">VIP</div>
|
||||
<div
|
||||
class="vip_icon coins_icon"
|
||||
v-if="configStore.appConfig.coinMark && item.permission === 1 && item.price"
|
||||
>
|
||||
金币
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="vip_icon" v-if="configStore.appConfig.vipMark && item?.originCoins == 0 && !item?.freeArea">
|
||||
VIP
|
||||
</div>
|
||||
<div
|
||||
class="vip_icon coins_icon"
|
||||
v-if="configStore.appConfig.coinMark && item?.originCoins != 0 && !item?.freeArea"
|
||||
>
|
||||
金币
|
||||
</div>
|
||||
</template>
|
||||
</lazyImg>
|
||||
<div class="infoBox">
|
||||
<div class="actorName ellipsis2">{{ item.title }}</div>
|
||||
<p class="desc" v-if="item.mediaType">
|
||||
共{{ item.totalEpisode }}话 · {{ item.updateStatus === 2 ? '已完结' : '更新中' }}
|
||||
</p>
|
||||
<p class="desc" v-else></p>
|
||||
</div>
|
||||
</div>
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Swiper, SwiperSlide } from 'swiper/vue';
|
||||
import 'swiper/swiper.min.css';
|
||||
import { jumpAdv, isLongFormVideo } from '@/utils';
|
||||
import { useVideoStore } from '@/stores/video';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
const configStore = useConfigStore();
|
||||
|
||||
const videoStore = useVideoStore();
|
||||
const props = defineProps<{
|
||||
section: any;
|
||||
}>();
|
||||
const router = useRouter();
|
||||
const state = reactive({
|
||||
section: props.section,
|
||||
newArr: <Array<any>>[],
|
||||
});
|
||||
// 切割数组
|
||||
const group = (array: any, subGroupLength: any) => {
|
||||
let index = 0;
|
||||
let newArray = [];
|
||||
while (index < array.length) {
|
||||
newArray.push(array.slice(index, (index += subGroupLength)));
|
||||
}
|
||||
return newArray;
|
||||
};
|
||||
const init = () => {
|
||||
state.newArr = group(state.section.allMediaInfo || state.section.allVideoInfo || [], 2);
|
||||
};
|
||||
|
||||
init();
|
||||
const jumpPage = (videoInfo: any) => {
|
||||
if (videoInfo.mediaType) {
|
||||
if (videoInfo.mediaType == 'video') {
|
||||
router.push({
|
||||
path: '/AcgVideoDetail',
|
||||
query: {
|
||||
id: videoInfo?.id,
|
||||
},
|
||||
});
|
||||
return;
|
||||
} else if (videoInfo.mediaType == 'image') {
|
||||
router.push({
|
||||
path: '/AcgImageDetail',
|
||||
query: {
|
||||
id: videoInfo?.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (videoInfo.type) {
|
||||
jumpAdv(videoInfo);
|
||||
return;
|
||||
}
|
||||
if (videoInfo.newsType == 'COVER') {
|
||||
router.push({
|
||||
path: '/postDetail',
|
||||
query: {
|
||||
postid: videoInfo.id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (isLongFormVideo(videoInfo)) {
|
||||
router.push({
|
||||
path: '/movieDetails',
|
||||
query: {
|
||||
vid: videoInfo.id,
|
||||
uid: videoInfo.publisher?.uid,
|
||||
},
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
videoStore.deleteVideoList();
|
||||
videoStore.saveVideoList(state.section.allMediaInfo || state.section.allVideoInfo, videoInfo);
|
||||
router.push(`/shortVideoDetails?id=${videoInfo.id}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.horizontalTwoSlid {
|
||||
margin-top: 18px;
|
||||
.titleBox {
|
||||
padding: 0 10px;
|
||||
.title {
|
||||
color: #333333;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
span {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
.moreRight {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: rgba(51, 51, 51, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
// :deep(.van-swipe-item) {
|
||||
// width: 320px !important;
|
||||
// }
|
||||
.swiperSlide {
|
||||
width: 276px;
|
||||
}
|
||||
.actorBox {
|
||||
display: flex;
|
||||
//margin-left: 10px;
|
||||
//margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
width: 256px;
|
||||
.actorAvatar {
|
||||
height: 110px;
|
||||
width: 82px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
.vip_icon {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 27px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
border-radius: 3px;
|
||||
font-family: 'PingFang SC';
|
||||
color: #292929;
|
||||
background: linear-gradient(135deg, #f8f3ae 0%, #ddac44 100%);
|
||||
// 免费
|
||||
&.free_icon {
|
||||
width: 32px;
|
||||
color: #fff;
|
||||
background: #1cd302;
|
||||
}
|
||||
// 金币
|
||||
&.coins_icon {
|
||||
color: #1f2029;
|
||||
background: linear-gradient(135deg, #ffe580 0%, #facc15 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.infoBox {
|
||||
padding-left: 12px;
|
||||
width: 160px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
.actorName {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 3px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.desc {
|
||||
line-height: 18px;
|
||||
//overflow: hidden;
|
||||
//text-overflow: ellipsis;
|
||||
//white-space: nowrap;
|
||||
color: rgba(152, 152, 152, 1);
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
// width: 228px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,370 @@
|
||||
<template>
|
||||
<div class="homeItem" ref="homeItemRef" :id="state.setId">
|
||||
<van-pull-refresh
|
||||
v-if="!state.loading"
|
||||
v-model="moreParams.refreshing"
|
||||
@refresh="onRefresh"
|
||||
:disabled="moreParams.disabled"
|
||||
success-text="刷新成功"
|
||||
:class="[{ 'my-refresh': moreParams.isHigehtMax, none: moreParams.refreshing }]"
|
||||
>
|
||||
<ModuleBanners :banners="banners" :moduleType="type" />
|
||||
<HomeHeader :advType="AdType.HOME" class="advBox" v-if="!props.pureVersion" />
|
||||
<!-- 公告 -->
|
||||
<!-- <MarqueeBar :announce="state.announce" /> -->
|
||||
<!-- 金刚区 -->
|
||||
<KingKong :moduleId="moduleId" />
|
||||
<van-list
|
||||
v-if="!moreParams.isNoData"
|
||||
v-model:loading="moreParams.loading"
|
||||
:finished="moreParams.finished"
|
||||
finished-text="没有更多数据了"
|
||||
@load="onLoad"
|
||||
:offset="10"
|
||||
v-model:error="moreParams.error"
|
||||
:immediate-check="false"
|
||||
error-text="请求失败,点击重新加载"
|
||||
>
|
||||
<!-- <template v-slot:finished>
|
||||
<div class="noMoreData" v-if="!userInfo.isVip && moreParams.isFindMore" @click="$router.push('member')">
|
||||
点击发现更多刺激内容
|
||||
</div>
|
||||
</template> -->
|
||||
<div class="listBox" v-for="(item, index) in state.list" :key="item.sectionID">
|
||||
<div class="titleBox flex-center-between" @click="jumpMore(item)" v-if="item.showType != 301">
|
||||
<div class="title" :class="{ red: state.isAlong }">
|
||||
{{ item.sectionName }}
|
||||
<div class="titleLine" v-if="state.isAlong"></div>
|
||||
</div>
|
||||
<div class="moreText">
|
||||
<!-- 更多 -->
|
||||
<SvgIcon class="moreBox" v-if="state.isAlong" name="moreIcon1" />
|
||||
<SvgIcon class="moreBox" v-else name="moreIcon" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 一大四小布局 -->
|
||||
<OneBigFourSmall :section="item" v-if="item.showType == 101" />
|
||||
<!-- 四宫格(横) -->
|
||||
<FourGridHorizontal :section="item" v-else-if="item.showType == 102" />
|
||||
<!-- 六宫格(横-长视频) -->
|
||||
<SixGrid :section="item" v-else-if="item.showType == 103" />
|
||||
<!-- 横屏1.2滑动 -->
|
||||
<HorizontalSlidingSmall :section="item" v-else-if="item.showType == 104" />
|
||||
<!-- 横屏2.2滑动 -->
|
||||
<HorizontalSlidingBig :section="item" v-else-if="item.showType == 105" />
|
||||
<!-- 三横版样式布局 -->
|
||||
<HorizontalThreeBig :section="item" v-else-if="item.showType == 106" />
|
||||
<!-- 大横屏无限制 -->
|
||||
<BigHorizontalScreenUnlimit :section="item" v-else-if="item.showType == 107" />
|
||||
<!-- 四宫格(竖) -->
|
||||
<FourGridVertical :section="item" v-else-if="item.showType == 201" />
|
||||
<!-- 竖排六宫格 -->
|
||||
<VerticalSixGrid :section="item" v-else-if="item.showType == 202" />
|
||||
<!-- acg九竖版布局 -->
|
||||
<VerticalNineGrid :section="item" v-else-if="item.showType == 203" />
|
||||
<!-- 竖版滑动(大)布局 -->
|
||||
<VerticalSlideBig :section="item" v-else-if="item.showType == 204" />
|
||||
<!-- acg二竖版左右滑动布局 -->
|
||||
<VerticalTwoSlid :section="item" v-else-if="item.showType == 205" />
|
||||
<!-- 猜你喜欢 -->
|
||||
<GuessYouLike :moduleId="moduleId" v-else-if="item.showType == 301" />
|
||||
<!-- 大横屏 + 横版滑动(小)布局 -->
|
||||
<!-- <BigScreenHorizontalSlidingSmall :section="item" v-else-if="item.showType == 8" /> -->
|
||||
<!-- 四宫格(横) -->
|
||||
<FourGridHorizontal :section="item" v-else />
|
||||
<!-- 广告 -->
|
||||
<template v-if="state.advsList.length && !props.pureVersion">
|
||||
<FiveGridAdvVal
|
||||
v-if="index > 0 && (index + 1) % 2 == 0"
|
||||
:adv-list="state.gridAdvsList[((index - 1) / 2) % state.gridAdvsList.length]"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</van-list>
|
||||
<Empty class="p14" v-if="moreParams.isNoData" />
|
||||
<div class="sizeBox"></div>
|
||||
</van-pull-refresh>
|
||||
<Loading v-else />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, ref, provide } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { AdType } from '@/enums/app';
|
||||
import { getAdItem } from '@/utils/getConfig';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
import { sliceArray } from '@/utils/index';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import HomeApi from '@/service/home';
|
||||
import BigHorizontalScreenUnlimit from './components/BigHorizontalScreenUnlimit.vue';
|
||||
import BigScreenHorizontalSlidingSmall from './components/BigScreenHorizontalSlidingSmall.vue';
|
||||
import FourGridHorizontal from './components/FourGridHorizontal.vue';
|
||||
import FourGridVertical from './components/FourGridVertical.vue';
|
||||
import HorizontalSlidingBig from './components/HorizontalSlidingBig.vue';
|
||||
import HorizontalSlidingSmall from './components/HorizontalSlidingSmall.vue';
|
||||
import OneBigFourSmall from './components/OneBigFourSmall.vue';
|
||||
import SixGrid from './components/SixGrid.vue';
|
||||
import VerticalSixGrid from './components/VerticalSixGrid.vue';
|
||||
import VerticalSlideBig from './components/VerticalSlideBig.vue';
|
||||
import HorizontalThreeBig from './components/HorizontalThreeBig.vue';
|
||||
import VerticalNineGrid from './components/VerticalNineGrid.vue';
|
||||
import VerticalTwoSlid from './components/VerticalTwoSlid.vue';
|
||||
import GuessYouLike from './components/GuessYouLike.vue';
|
||||
import useCommon from '@/hooks/common';
|
||||
provide('hideVideoCardTagsAndComments', true);
|
||||
const { routeLeaveRecord, routeBackRecord } = useCommon();
|
||||
const homeItemRef = ref();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
moduleId: string;
|
||||
isAlong?: boolean;
|
||||
tabIndex: number;
|
||||
type: number;
|
||||
pureVersion?: boolean;
|
||||
banners?: Types.Home.ModuleBanner[] | null;
|
||||
}>(),
|
||||
{
|
||||
moduleId: '',
|
||||
isAlong: false,
|
||||
tabIndex: 0,
|
||||
pureVersion: false,
|
||||
},
|
||||
);
|
||||
const router = useRouter();
|
||||
const globalStore = useGlobalStore();
|
||||
const configStore = useConfigStore();
|
||||
const userInfo = useUserStore().userInfo;
|
||||
const state = reactive({
|
||||
list: <Array<Types.Home.SectionsItem>>[],
|
||||
// sectionPage: 1,
|
||||
// sectionSize: 10,
|
||||
pageSize: 6,
|
||||
pageNumber: 1,
|
||||
announce: {},
|
||||
advsList: <Array<Types.App.AdsInfoListItem>>[], // 专栏之间的广告
|
||||
gridAdvsList: <any>[],
|
||||
isNoticeShow: true,
|
||||
loading: true, // loading
|
||||
isAlong: props.isAlong,
|
||||
setId: '', // id,用于缓存
|
||||
});
|
||||
const moreParams = reactive({
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
isNoData: false,
|
||||
error: false,
|
||||
isHigehtMax: false,
|
||||
disabled: false,
|
||||
isFindMore: true,
|
||||
});
|
||||
onMounted(() => {
|
||||
routeLeaveRecord(homeItemRef.value);
|
||||
routeBackRecord(homeItemRef.value);
|
||||
});
|
||||
// 获取模块数据
|
||||
const getList = async () => {
|
||||
const data = {
|
||||
id: props.moduleId,
|
||||
data: {
|
||||
pageNumber: state.pageNumber,
|
||||
pageSize: state.pageSize,
|
||||
},
|
||||
};
|
||||
try {
|
||||
let res: any = await HomeApi.getModuleList(data);
|
||||
state.loading = false;
|
||||
moreParams.loading = false;
|
||||
moreParams.refreshing = false;
|
||||
let list = res.allSection || [];
|
||||
if (list[0]?.allVideoInfo?.length) {
|
||||
let num = userInfo.uid % 2;
|
||||
list = list.map((item: any) => ({
|
||||
...item,
|
||||
allVideoInfo: item.allVideoInfo.filter(
|
||||
(v: any) => !v.showType || (num == 0 && v.showType == 2) || (num == 1 && v.showType == 1),
|
||||
),
|
||||
}));
|
||||
}
|
||||
if (state.pageNumber == 1) {
|
||||
state.list = list;
|
||||
} else {
|
||||
state.list = state.list.concat(list);
|
||||
}
|
||||
if (state.pageNumber == 1 && state.list.length == 0) {
|
||||
moreParams.isNoData = true;
|
||||
}
|
||||
if (!res.hasNext) {
|
||||
moreParams.finished = true;
|
||||
}
|
||||
} catch (e) {
|
||||
state.loading = false;
|
||||
moreParams.loading = false;
|
||||
moreParams.error = true;
|
||||
}
|
||||
};
|
||||
// 初始化
|
||||
const init = () => {
|
||||
// 专题之间的广告
|
||||
state.advsList = getAdItem(AdType.TopicGridAdv);
|
||||
state.gridAdvsList = sliceArray(state.advsList, 5);
|
||||
// 跑马灯
|
||||
// state.announce = getAnnounce(1);
|
||||
state.setId = 'mainItem' + props.tabIndex;
|
||||
getList();
|
||||
};
|
||||
// const getAnnounce = (position: number) => {
|
||||
// let obj: any = {};
|
||||
// if (globalStore.announceArr.length) {
|
||||
// globalStore.announceArr.map((item: any) => {
|
||||
// if (item.type == position) {
|
||||
// obj = item;
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// return obj;
|
||||
// };
|
||||
init();
|
||||
const jumpMore = (item: any) => {
|
||||
if ([4, 5].includes(props.type)) {
|
||||
router.push({
|
||||
path: '/AcgTopicList',
|
||||
query: {
|
||||
id: item.sectionID,
|
||||
title: item.sectionName,
|
||||
// showType: ![201, 202, 203, 204, 205].includes(item.showType) ? '0' : '',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
router.push({
|
||||
path: '/tagShortVideo',
|
||||
query: {
|
||||
id: item.sectionID,
|
||||
name: item.sectionName,
|
||||
showType: [201, 202, 203, 204, 205].includes(item.showType) ? 'vertical' : '',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onLoad = () => {
|
||||
if (moreParams.refreshing) return;
|
||||
moreParams.loading = true;
|
||||
state.pageNumber++;
|
||||
getList();
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
state.pageNumber = 1;
|
||||
moreParams.refreshing = true;
|
||||
moreParams.finished = false;
|
||||
getList();
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.homeItem {
|
||||
width: 100%;
|
||||
height: calc(100vh - 44px * 2 - 55px);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow-y: scroll;
|
||||
:deep(.my-refresh) {
|
||||
padding: 0;
|
||||
}
|
||||
.my-refresh {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
:deep(.advBox) {
|
||||
margin: 12px 0;
|
||||
.firstRow {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.adv-line-2 {
|
||||
padding: 0 16px;
|
||||
}
|
||||
.banner_box {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
.fiveGrid {
|
||||
padding: 0 16px;
|
||||
margin: 12px 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
||||
grid-row-gap: 12px;
|
||||
grid-column-gap: 10px;
|
||||
.rectAdvBox {
|
||||
width: 60px;
|
||||
.rectAdvIcon {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
border-radius: 14.6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rectName {
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
margin-top: 4px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.listBox {
|
||||
width: 100%;
|
||||
padding: 0 14px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
.titleBox {
|
||||
font-size: 18px;
|
||||
color: rgb(255, 255, 255);
|
||||
font-weight: 500;
|
||||
margin: 10px 0 14px;
|
||||
box-sizing: border-box;
|
||||
.title {
|
||||
position: relative;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
.titleLine {
|
||||
height: 11px;
|
||||
width: 36px;
|
||||
background: url('./../../assets/images/png/titleLine.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: -12px;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
}
|
||||
.red {
|
||||
font-family: 'AaLongYanTi';
|
||||
color: rgb(146, 7, 7);
|
||||
}
|
||||
.moreText {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #989898;
|
||||
font-size: 12px;
|
||||
}
|
||||
.moreBox {
|
||||
height: 20px;
|
||||
width: 26px;
|
||||
}
|
||||
}
|
||||
.advImg {
|
||||
height: 72.3px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-top: 20px;
|
||||
transform: translateZ(0px);
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
.sizeBox {
|
||||
height: 70px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<!-- 跑马灯 -->
|
||||
<div class="homeNoticeBox flex-vcenter" v-if="state.marquee && state.marquee.length && state.showMarquee">
|
||||
<div class="not_horn">
|
||||
<img src="@/assets/images/ui/notice.svg" alt="公告" />
|
||||
</div>
|
||||
<van-notice-bar scrollable class="homeNotice" color="#ffffff" :speed="60">
|
||||
<template #default>
|
||||
<span v-for="(item, index) in state.marquee" :key="item.id" @click="jumpAdv(item)">
|
||||
{{ item.content }}
|
||||
<span v-if="index < state.marquee.length - 1">
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</van-notice-bar>
|
||||
<img src="@/assets/images/ui/close.svg" alt="关闭公告" class="closeMarquee" @click="closeMarquee" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from 'vue';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type?: number;
|
||||
/** 与接口下发的 item.active 对应,用于筛选当前场景跑马灯 */
|
||||
active?: boolean | number | string;
|
||||
}>(),
|
||||
{
|
||||
type: 0,
|
||||
},
|
||||
);
|
||||
const globalStore = useGlobalStore();
|
||||
const state = reactive({
|
||||
marquee: <any[]>[],
|
||||
showMarquee: true,
|
||||
});
|
||||
const syncMarquee = () => {
|
||||
const list = globalStore.marqueeDatas;
|
||||
if (!Array.isArray(list) || !list.length) {
|
||||
state.marquee = [];
|
||||
return;
|
||||
}
|
||||
state.marquee = list.filter((item: any) => {
|
||||
if (item.type != props.type) return false;
|
||||
// 未传 active 时只按 type 筛;传入后精确匹配 item.active
|
||||
if (props.active === undefined) return true;
|
||||
return item.active ;
|
||||
});
|
||||
};
|
||||
watch(() => [globalStore.marqueeDatas, props.type, props.active] as const, syncMarquee, {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
});
|
||||
const route = useRoute();
|
||||
const emit = defineEmits(['closeIcon']);
|
||||
const closeMarquee = () => {
|
||||
state.showMarquee = false;
|
||||
emit('closeIcon', route.name);
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.homeNoticeBox {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 0 16px;
|
||||
width: 100%;
|
||||
.not_horn {
|
||||
position: relative;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
> img {
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
.homeNotice {
|
||||
padding: 0 8px;
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
}
|
||||
.closeMarquee {
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
color: rgba(93, 101, 115, 1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div class="meTabPaymentBanner" @click="handleClick">
|
||||
<lazyImg class="bannerImg" :src="bannerImg" :key="bannerImg" />
|
||||
<div v-if="showCountdown" class="countdown">
|
||||
<van-count-down :time="countdownMs" @finish="onCountdownFinish">
|
||||
<template #default="timeData">
|
||||
<div class="h">{{ formatCountdownUnit(timeData.hours, timeData.days) }}</div>
|
||||
<span class="colon">:</span>
|
||||
<div class="m">{{ formatCountdownUnit(timeData.minutes) }}</div>
|
||||
<span class="colon">:</span>
|
||||
<div class="s">{{ formatCountdownUnit(timeData.seconds) }}</div>
|
||||
</template>
|
||||
</van-count-down>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import {
|
||||
parseLastDiscountTime,
|
||||
resolvePaymentConfigImg,
|
||||
} from '@/utils/paymentStatusPopup';
|
||||
import defaultBannerImg from '@/assets/images/png/payment/me-tab-banner-vip200.png';
|
||||
|
||||
const router = useRouter();
|
||||
const configStore = useConfigStore();
|
||||
const countdownExpired = ref(false);
|
||||
|
||||
const paymentConfig = computed(() => configStore.appConfig?.paymentStatusPopupConfig);
|
||||
|
||||
const bannerImg = computed(
|
||||
() => resolvePaymentConfigImg(paymentConfig.value?.meTab) || defaultBannerImg,
|
||||
);
|
||||
|
||||
const discountDeadline = computed(() => parseLastDiscountTime(paymentConfig.value?.lastDiscountTime));
|
||||
|
||||
const countdownMs = computed(() => {
|
||||
if (!discountDeadline.value || countdownExpired.value) return 0;
|
||||
const remaining = discountDeadline.value - Date.now();
|
||||
return remaining > 0 ? remaining : 0;
|
||||
});
|
||||
|
||||
const showCountdown = computed(() => countdownMs.value > 0);
|
||||
|
||||
const formatCountdownUnit = (value: number, days = 0) => {
|
||||
const total = days > 0 ? days * 24 + value : value;
|
||||
return total < 10 ? `0${total}` : `${total}`;
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
const id = paymentConfig.value?.vipCard || '';
|
||||
router.push(`/member${id ? `?id=${id}` : ''}`);
|
||||
};
|
||||
|
||||
const onCountdownFinish = () => {
|
||||
countdownExpired.value = true;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => paymentConfig.value?.lastDiscountTime,
|
||||
() => {
|
||||
countdownExpired.value = false;
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.meTabPaymentBanner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
|
||||
.bannerImg {
|
||||
width: 100%;
|
||||
display: block;
|
||||
line-height: 0;
|
||||
|
||||
:deep(.lazy_img),
|
||||
:deep(.laz_com) {
|
||||
width: 100%;
|
||||
display: block;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.countdown {
|
||||
position: absolute;
|
||||
left: 20.55%;
|
||||
bottom: 7.35%;
|
||||
z-index: 1;
|
||||
height: 19px;
|
||||
|
||||
:deep(.van-count-down) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 19px;
|
||||
}
|
||||
|
||||
.h,
|
||||
.m,
|
||||
.s {
|
||||
min-width: 21px;
|
||||
height: 19px;
|
||||
line-height: 17px;
|
||||
padding: 1px 3px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 3px;
|
||||
background: #1b1b1b;
|
||||
border: 0.5px solid #ffe381;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
font-family: 'PingFang SC', sans-serif;
|
||||
}
|
||||
|
||||
.colon {
|
||||
width: 11px;
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 19px;
|
||||
text-align: center;
|
||||
font-family: 'PingFang SC', sans-serif;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,734 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<van-popup :show="show" round position="bottom" @click-overlay="emit('close')" style="height: 90%">
|
||||
<div class="limitedTimeOfferBox">
|
||||
<div class="line"></div>
|
||||
<div class="main overflow-y-auto" v-if="!state.loading" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<div class="title-box">完整版视频需要成为VIP会员才能观看! 请选择会员类型</div>
|
||||
<div class="card-box">
|
||||
<div class="swiper pop_swiper" ref="mySwiper">
|
||||
<div class="swiper-wrapper">
|
||||
<div class="swiper-slide" v-for="(item, index) in state.vipList" :key="item.productID">
|
||||
<div class="vipCard">
|
||||
<div class="gift ellipsis" v-if="item.actionDesc">{{ item.actionDesc }}</div>
|
||||
<AnimationImg
|
||||
v-show="state.selectedItem.productID == item.productID"
|
||||
:src="
|
||||
item.productType == 21 && state.payOrreserve == 'pay' && item.endBgSelectImg
|
||||
? item.endBgSelectImg
|
||||
: item.newBgImg || item.bgImg
|
||||
"
|
||||
class="bgImg"
|
||||
:key="index"
|
||||
/>
|
||||
<AnimationImg
|
||||
v-show="state.selectedItem.productID != item.productID"
|
||||
:src="
|
||||
item.productType == 21 && state.payOrreserve == 'pay' && item.endBgImg
|
||||
? item.endBgImg
|
||||
: item.bgImg
|
||||
"
|
||||
:key="index"
|
||||
class="bgImg"
|
||||
/>
|
||||
|
||||
<!-- v-if="item.productType == 5" -->
|
||||
<div class="countdownBox" v-if="item.productType == 5 && state.timeStr">
|
||||
{{ state.timeStr }}
|
||||
<!-- <div class="hour">{{ state.hr }}</div>
|
||||
<div class="minute">{{ state.min }}</div>
|
||||
<div class="second">{{ state.sec }}</div> -->
|
||||
</div>
|
||||
<div
|
||||
class="timeStrPay"
|
||||
v-if="item.productType == 21 && state.payOrreserve == 'pay' && state.showPresal"
|
||||
>
|
||||
{{ state.timeStrPay }}
|
||||
</div>
|
||||
<div class="timeStrPay" v-if="item.productType == 21 && !state.showPresal">活动已结束</div>
|
||||
<div class="timeStrPay alBooked" v-if="item.productType == 21 && state.payOrreserve == 'wait'">
|
||||
已预定 请在{{ dateFormatMmDd(state.activityDetail.startTime) }}支付尾款
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="horizontalLine"></div>
|
||||
<!-- 预售入口 -->
|
||||
<template
|
||||
v-if="
|
||||
configStore.appConfig.advanceStatus &&
|
||||
configStore.appConfig.advanceStatus.activityStatus &&
|
||||
state.activityDetail.status != 4 &&
|
||||
state.showPresal
|
||||
"
|
||||
>
|
||||
<!-- 未购买 -->
|
||||
<lazyImg
|
||||
class="preSaleEntBox"
|
||||
:src="configStore?.appConfig?.advancePage?.banner"
|
||||
:key="configStore?.appConfig?.advancePage?.banner"
|
||||
@click="$router.push('/presale')"
|
||||
v-if="!state.payOrreserve"
|
||||
/>
|
||||
<!-- 已预定 -->
|
||||
<div class="preSaledEntBox" @click="$router.push('/presale')" v-else>
|
||||
<lazyImg
|
||||
:src="configStore?.appConfig?.advancePage?.bannerWait"
|
||||
:key="configStore?.appConfig?.advancePage?.bannerWait"
|
||||
class="payBtn"
|
||||
v-if="state.payOrreserve == 'wait'"
|
||||
></lazyImg>
|
||||
<lazyImg
|
||||
:src="configStore?.appConfig?.advancePage?.bannerProc"
|
||||
:key="configStore?.appConfig?.advancePage?.bannerProc"
|
||||
class="payBtn"
|
||||
v-else
|
||||
>
|
||||
<div class="time">{{ state.timeStrPay }}</div>
|
||||
</lazyImg>
|
||||
</div>
|
||||
</template>
|
||||
<div class="rights-box">
|
||||
<div class="subTitleBox">
|
||||
<img src="@/assets/images/png/mine/privilege_text.png" alt="" />
|
||||
</div>
|
||||
<div class="privilegeBox overflow-y-auto" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<div class="privilegeItem" v-for="item in state.currentPrivilege" :key="item.privilegeName">
|
||||
<LazyImg :src="item.img" class="cover" />
|
||||
<div class="subTitle">{{ item.privilegeName }}</div>
|
||||
<div class="desc ellipsis2" v-html="item.privilegeDesc.replace(/\n/g, '<br>')"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Loading class="loading" v-else />
|
||||
<div class="bottomBox">
|
||||
<div class="payBtnBox" v-if="state.selectedItem.productType == 21">
|
||||
<template v-if="state.payOrreserve == 'wait'">
|
||||
<DecryptImg
|
||||
@click="showToast('尾款支付暂未开放,请您耐心等待~')"
|
||||
:src="configStore?.appConfig?.advancePage?.buttonWaitBackground"
|
||||
:key="configStore?.appConfig?.advancePage?.buttonWaitBackground"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="state.payOrreserve != 'pay'">
|
||||
<template v-if="state.selectedItem.isUpgrade">
|
||||
<div class="nowPay" @click="showPay">{{ state.selectedItem.balanceAmount / 10 }}元/立即升级</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<DecryptImg
|
||||
@click="showPay"
|
||||
:src="configStore?.appConfig?.advancePage?.buttonBackground"
|
||||
:key="configStore?.appConfig?.advancePage?.buttonBackground"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="state.payOrreserve == 'pay'">
|
||||
<DecryptImg
|
||||
@click="showPay"
|
||||
:src="configStore?.appConfig?.advancePage?.buttonProcBackground"
|
||||
:key="configStore?.appConfig?.advancePage?.buttonProcBackground"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 普通会员卡支付按钮 -->
|
||||
<div class="nowPay" v-else @click="showPay">
|
||||
{{ state.selectedItem.discountedPrice / 10 }}元/{{ state.selectedItem.isUpgrade ? '立即升级' : '立即支付' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</Teleport>
|
||||
<RechargeBox ref="rechargeBoxRef" :is-open="state.isRecharge" @closePay="closePay" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, onDeactivated, watch } from 'vue';
|
||||
import MineApi from '@/service/mine';
|
||||
import HomeApi from '@/service/home';
|
||||
import AppApi from '@/service/app';
|
||||
import { showToast } from 'vant';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { dateFormatMmDd, judgeTime, dateFormatYyyyMmDd } from '@/utils/index';
|
||||
import { parseVipProductResponse } from '@/utils/vipProductList';
|
||||
import Swiper from 'swiper';
|
||||
import 'swiper/swiper.min.css';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const rechargeBoxRef = ref();
|
||||
const configStore = useConfigStore();
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: () => false,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['close']);
|
||||
const mySwiper = ref();
|
||||
const state = reactive({
|
||||
activeValue: 'member',
|
||||
userWallet: <Types.User.WalletInfo>{},
|
||||
vipList: <Array<Types.Recharge.VipItem>>[], // vip列表
|
||||
currentPrivilege: <Array<Types.Recharge.PrivilegeItem>>[], // 当前vip卡特权
|
||||
integralList: <Array<Types.Recharge.IntegralList>>[], // 积分列表
|
||||
dcInfo: <Types.Recharge.Daichong>{}, // 代充信息
|
||||
isRecharge: false, // 充值界面
|
||||
selectedItem: <Types.Recharge.VipItem>{}, // 选中的会员卡
|
||||
payData: <any>{}, // 支付对象
|
||||
countdownSec: 0, // 新手倒计时时间 单位秒
|
||||
day: '', // 新手倒计时-天
|
||||
hr: '', // 新手倒计时-小时
|
||||
min: '', // 新手倒计时-分钟
|
||||
sec: '', // 新手倒计时-秒
|
||||
timeStr: '',
|
||||
timeoutTimer: <any>null, // 倒计时计时器
|
||||
timeStrPay: '',
|
||||
timeoutTimerPay: <any>null, // 支付尾款倒计时
|
||||
loading: true, // 加载loading
|
||||
activityDetail: <any>{},
|
||||
payOrreserve: '', //支付尾款或者已预定
|
||||
isPayPresal: false, //是否支付尾款
|
||||
nowData: <any>{},
|
||||
showPresal: true, // 默认true
|
||||
});
|
||||
const initSwiper = () => {
|
||||
new Swiper('.pop_swiper', {
|
||||
direction: 'horizontal',
|
||||
// loop: true,
|
||||
slidesPerView: 'auto',
|
||||
spaceBetween: 6,
|
||||
// centeredSlides: true,
|
||||
// watchSlidesProgress: true,
|
||||
on: {
|
||||
click: (swiper) => {
|
||||
if (mySwiper.value.swiper) {
|
||||
let index = swiper.clickedIndex % state.vipList.length;
|
||||
let vip = state.vipList[index];
|
||||
selectVip(vip);
|
||||
swiper.slideTo(index);
|
||||
}
|
||||
},
|
||||
slideChange: (swiper) => {
|
||||
// 滑动结束后切换选中的vip
|
||||
let vip = state.vipList[swiper.realIndex];
|
||||
selectVip(vip);
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
// 图片解密
|
||||
const imgDecypt = (list: any) => {
|
||||
try {
|
||||
let newBgImg = '',
|
||||
bgImg = '',
|
||||
endBgSelectImg = '',
|
||||
endBgImg = '';
|
||||
list.forEach((item: any) => {
|
||||
const isHttp = new RegExp('http').test(item.newBgImg);
|
||||
isHttp == true ? (newBgImg = item.newBgImg) : (newBgImg = configStore.sourceInfo.IMAGE + '/' + item.newBgImg);
|
||||
AppApi.imgDecypt(newBgImg).then((url: any) => {
|
||||
item.newBgImg = url;
|
||||
});
|
||||
const isHttp1 = new RegExp('http').test(item.bgImg);
|
||||
isHttp1 == true ? (bgImg = item.bgImg) : (bgImg = configStore.sourceInfo.IMAGE + '/' + item.bgImg);
|
||||
AppApi.imgDecypt(bgImg).then((url: any) => {
|
||||
item.bgImg = url;
|
||||
});
|
||||
if (item.endBgSelectImg) {
|
||||
const isHttp2 = new RegExp('http').test(item.endBgSelectImg);
|
||||
isHttp2 == true
|
||||
? (endBgSelectImg = item.endBgSelectImg)
|
||||
: (endBgSelectImg = configStore.sourceInfo.IMAGE + '/' + item.endBgSelectImg);
|
||||
AppApi.imgDecypt(endBgSelectImg).then((url: any) => {
|
||||
item.endBgSelectImg = url;
|
||||
});
|
||||
}
|
||||
if (item.endBgImg) {
|
||||
const isHttp3 = new RegExp('http').test(item.endBgImg);
|
||||
isHttp3 == true ? (endBgImg = item.endBgImg) : (endBgImg = configStore.sourceInfo.IMAGE + '/' + item.endBgImg);
|
||||
AppApi.imgDecypt(endBgImg).then((url: any) => {
|
||||
item.endBgImg = url;
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
};
|
||||
// 查询会员卡列表
|
||||
const getVipList = async () => {
|
||||
const res = await MineApi.queryVipProduct();
|
||||
state.loading = false;
|
||||
state.integralList = res.integralList;
|
||||
const { cards, dcInfo } = parseVipProductResponse(res, {
|
||||
includePresale: true,
|
||||
presaleActivityActive: !!configStore.appConfig.advanceStatus?.activityStatus,
|
||||
showPresal: state.showPresal,
|
||||
activityDetailStatus: state.activityDetail?.status,
|
||||
});
|
||||
state.vipList = cards;
|
||||
state.dcInfo = dcInfo;
|
||||
};
|
||||
|
||||
// 查询预售信息
|
||||
const getPresaleData = async () => {
|
||||
try {
|
||||
let req = {
|
||||
pageNumber: 1,
|
||||
pageSize: 1,
|
||||
};
|
||||
let res: any = await HomeApi.getPresaleList(req);
|
||||
state.activityDetail = res.activityDetail;
|
||||
// 判断预售状态 如果章台是2或者3可进行尾款支付 但是要判断时间
|
||||
// 付尾款的时间未开始,但是已经预定了 就展示已预订
|
||||
if (state.activityDetail.status == 2 || state.activityDetail.status == 3) {
|
||||
// 如果当前时间大于尾款支付时间 -- 支付尾款 反之 已预定
|
||||
if (judgeTime(state.activityDetail.startTime)) {
|
||||
state.payOrreserve = 'pay';
|
||||
// 尾款倒计时
|
||||
const timers = new Date(state.activityDetail.endTime).getTime();
|
||||
countdownPay(timers);
|
||||
} else {
|
||||
state.payOrreserve = 'wait';
|
||||
}
|
||||
}
|
||||
// 判断剩余支付时间是否过期
|
||||
const endTime = new Date(state.activityDetail.endTime).getTime();
|
||||
const newTime = new Date().getTime();
|
||||
const diffTime = (endTime - newTime) / 1000;
|
||||
if (diffTime < 0) {
|
||||
state.showPresal = false;
|
||||
}
|
||||
// 用于支付传参判断
|
||||
if (!state.activityDetail.balancePayment) {
|
||||
//如果是预付
|
||||
state.isPayPresal = false;
|
||||
} else {
|
||||
//支付尾款
|
||||
state.isPayPresal = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
async (newVal: boolean) => {
|
||||
if (newVal) {
|
||||
// 如果有预售活动则调用
|
||||
if (configStore.appConfig.advanceStatus && configStore.appConfig.advanceStatus.activityStatus) {
|
||||
await getPresaleData();
|
||||
}
|
||||
state.userWallet = userStore.walletInfo;
|
||||
await getVipList();
|
||||
await initSwiper();
|
||||
selectVip(state.vipList[0]);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// 选中会员卡
|
||||
const selectVip = (item: any) => {
|
||||
state.selectedItem = item;
|
||||
state.currentPrivilege = item.newPrivilege;
|
||||
let payData = {
|
||||
rechargeList: item.rchgType, // 支付方式列表
|
||||
dcInfo: state.dcInfo, // 代充信息
|
||||
popType: 'vip', // 弹窗类型
|
||||
productType: item.productType, // 商品类型
|
||||
productID: item.productID, // 商品ID
|
||||
dcType: 1, // 代充商品类型
|
||||
buyType: 4, // 商品类型 1-是金币 2-是游戏币 3-是果币 4-是商品
|
||||
discountedPrice:
|
||||
item.productType == 21
|
||||
? state.isPayPresal
|
||||
? item.balanceAmount / 10
|
||||
: item.advanceAmount / 10
|
||||
: item.discountedPrice / 10, // 充值金额
|
||||
isAmountPay: item.isAmountPay, // 是否支持金币兑换
|
||||
finalPayStatus: item.productType == 21 ? state.isPayPresal : null, //预售卡是否支付尾款
|
||||
};
|
||||
state.payData = payData;
|
||||
};
|
||||
const showPay = () => {
|
||||
if (state.payData.productType == 21 && state.payOrreserve == 'wait') {
|
||||
showToast('尾款支付暂未开放,请您耐心等待~');
|
||||
return false;
|
||||
}
|
||||
if (state.payData.productType == 21 && state.activityDetail.status == 4) {
|
||||
showToast('尾款已支付,请勿重复购买~');
|
||||
return false;
|
||||
}
|
||||
if (state.payData.productType == 21 && !state.showPresal) {
|
||||
showToast('活动已结束');
|
||||
return false;
|
||||
}
|
||||
rechargeBoxRef.value.changeItem(state.payData);
|
||||
state.isRecharge = true;
|
||||
};
|
||||
// 查询新手优惠时间
|
||||
const queryTiroCountdown = async () => {
|
||||
try {
|
||||
let res: any = await MineApi.queryNewcomerCountdown();
|
||||
state.countdownSec = res.countdownSec;
|
||||
countdown(res.countdownSec);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
// 倒计时
|
||||
const countdown = (timer: number) => {
|
||||
if (timer <= 0) return;
|
||||
// 相差的毫秒数
|
||||
const msec: any = timer;
|
||||
// 计算时分秒数
|
||||
let hr: any = parseInt((msec / 60 / 60).toString());
|
||||
let min: any = parseInt(((msec / 60) % 60).toString());
|
||||
let sec: any = parseInt((msec % 60).toString());
|
||||
// 个位数前补零
|
||||
hr = hr > 9 ? hr : '0' + hr;
|
||||
min = min > 9 ? min : '0' + min;
|
||||
sec = sec > 9 ? sec : '0' + sec;
|
||||
state.timeStr = `倒计时:${hr}:${min}:${sec}`;
|
||||
// 一秒后递归
|
||||
state.timeoutTimer = setTimeout(() => {
|
||||
timer--;
|
||||
countdown(timer);
|
||||
}, 1000);
|
||||
};
|
||||
queryTiroCountdown();
|
||||
|
||||
// 支付尾款剩余时间--倒计时
|
||||
const countdownPay = (timer: number) => {
|
||||
const endTime = timer;
|
||||
const newTime = new Date().getTime();
|
||||
const diffTime = (endTime - newTime) / 1000;
|
||||
const day = parseInt((diffTime / 60 / 60 / 24).toString());
|
||||
const honur = parseInt(((diffTime / 60 / 60) % 24).toString());
|
||||
const min = parseInt(((diffTime / 60) % 60).toString());
|
||||
const sen = parseInt((diffTime % 60).toString());
|
||||
if (diffTime > 0) {
|
||||
state.timeStrPay = `剩余时间:${showTime(day)}天${showTime(honur)}:${showTime(min)}:${showTime(sen)}`;
|
||||
} else {
|
||||
state.showPresal = false;
|
||||
}
|
||||
// 一秒后递归
|
||||
state.timeoutTimerPay = setTimeout(() => {
|
||||
timer--;
|
||||
countdownPay(timer);
|
||||
}, 1000);
|
||||
};
|
||||
const showTime = (t: number) => {
|
||||
const time = t >= 10 ? t : '0' + t;
|
||||
return time;
|
||||
};
|
||||
const closePay = () => {
|
||||
state.isRecharge = false;
|
||||
};
|
||||
onDeactivated(() => {
|
||||
clearTimeout(state.timeoutTimer);
|
||||
clearTimeout(state.timeoutTimerPay);
|
||||
state.timeoutTimer = <any>null;
|
||||
state.timeoutTimerPay = <any>null;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.limitedTimeOfferBox {
|
||||
border-radius: 20px 20px 0px 0px;
|
||||
background: linear-gradient(180deg, #0f0f0f 50.09%, #251d0b 99.95%);
|
||||
padding: 14px 16px 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
.line {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
border-radius: 100px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.main {
|
||||
height: calc(100% - 30px);
|
||||
padding: 0px 0 70px;
|
||||
box-sizing: border-box;
|
||||
.title-box {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
width: 271px;
|
||||
text-align: center;
|
||||
line-height: 22px;
|
||||
margin: 16px auto;
|
||||
}
|
||||
.horizontalLine {
|
||||
height: 1px;
|
||||
width: 355px;
|
||||
margin: 10px auto;
|
||||
}
|
||||
.rights-box {
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.05);
|
||||
width: 343px;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.subTitleBox {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
img {
|
||||
height: 25px;
|
||||
}
|
||||
span {
|
||||
color: rgba(255, 224, 207, 1);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.gift {
|
||||
height: 20px;
|
||||
// background: linear-gradient(94deg, #eabfa0 0%, #f2d3b9 100%);
|
||||
background: rgba(225, 53, 31, 1);
|
||||
border-radius: 12px 0 12px 0;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
max-width: 111px;
|
||||
line-height: 20px;
|
||||
padding: 0 5px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.privilegeBox {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-column-gap: 10px;
|
||||
grid-row-gap: 12px;
|
||||
margin-top: 18px;
|
||||
height: 202px;
|
||||
.privilegeItem {
|
||||
// width: 66px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.cover {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transform: translateZ(0px);
|
||||
:deep(.loading) {
|
||||
background-size: 28px 21px;
|
||||
}
|
||||
}
|
||||
.subTitle {
|
||||
// font-size: 14px;
|
||||
font-size: 10.91px;
|
||||
margin-top: 6px;
|
||||
line-height: 16px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
.desc {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
// font-size: 12px;
|
||||
font-size: 9.35px;
|
||||
margin-top: 6px;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
.integralCardArea {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding-left: 10px;
|
||||
.integralCardBox {
|
||||
.integralCard {
|
||||
height: 100px;
|
||||
width: 111px;
|
||||
background: linear-gradient(148deg, #ffeedc 0%, #eec7b2 101.54%, #faddc2 101.55%);
|
||||
border-radius: 8px;
|
||||
color: #421a11;
|
||||
text-align: center;
|
||||
padding: 25px 0;
|
||||
margin-right: 11px;
|
||||
.integralTitle {
|
||||
font-size: 16px;
|
||||
}
|
||||
.integralDesc {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
margin-top: 14px;
|
||||
}
|
||||
}
|
||||
.exchangeBtn {
|
||||
height: 28px;
|
||||
width: 72px;
|
||||
background-color: rgb(35, 42, 60);
|
||||
text-align: center;
|
||||
line-height: 28px;
|
||||
border-radius: 28px;
|
||||
margin: 8px auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.loading {
|
||||
height: calc(100vh - 120px);
|
||||
}
|
||||
.bottomBox {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
padding: 11px 10px;
|
||||
.nowPay {
|
||||
width: 343px;
|
||||
background: #f68804;
|
||||
border-radius: 3px;
|
||||
text-align: center;
|
||||
line-height: 42px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin: 0 auto;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.payBtnBox {
|
||||
width: 343px;
|
||||
// height: 44px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
.preSaleEntBox {
|
||||
height: 72px;
|
||||
width: 343px;
|
||||
margin: 18px auto;
|
||||
}
|
||||
.preSaledEntBox {
|
||||
height: 72px;
|
||||
width: 343px;
|
||||
margin: 18px auto;
|
||||
position: relative;
|
||||
.payBtn {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
.time {
|
||||
position: absolute;
|
||||
bottom: 14.5px;
|
||||
left: 87px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.4px;
|
||||
background: linear-gradient(180deg, #fffefd 0%, #fefdda 47.5%, #fdfcd4 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.pop_swiper {
|
||||
margin-top: 10px;
|
||||
overflow: unset;
|
||||
padding-right: 180px;
|
||||
.swiper-wrapper {
|
||||
width: 140px;
|
||||
height: 200px;
|
||||
.swiper-slide {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// transform: scale(0.9);
|
||||
.vipCard {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
.bgImg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// background: rgba(37, 31, 21, 1);
|
||||
}
|
||||
.countdownBox {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: -11px;
|
||||
height: 22px;
|
||||
min-width: 86px;
|
||||
background: #f68804;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
color: rgba(0, 0, 0, 1);
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
// background: url('@/assets/images/png/countdownBg.png') no-repeat;
|
||||
// background-size: 100% 100%;
|
||||
.hour,
|
||||
.minute,
|
||||
.second {
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.hour {
|
||||
left: 5px;
|
||||
top: 5px;
|
||||
}
|
||||
.minute {
|
||||
left: 36px;
|
||||
top: 5px;
|
||||
}
|
||||
.second {
|
||||
left: 70px;
|
||||
top: 5px;
|
||||
}
|
||||
}
|
||||
.timeStrPay {
|
||||
position: absolute;
|
||||
left: 42px;
|
||||
bottom: 13.5px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 9px;
|
||||
}
|
||||
.alBooked {
|
||||
left: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.swiper-slide-active,
|
||||
.swiper-slide-duplicate-active {
|
||||
// transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
<!-- 模块内容轮播:没有有效图片时不渲染,也不保留占位。 -->
|
||||
<template>
|
||||
<div v-if="visibleBanners.length" class="module-banners" @touchstart.stop @touchmove.stop @touchend.stop>
|
||||
<van-swipe :key="bannerKey" :autoplay="visibleBanners.length > 1 ? 5000 : 0">
|
||||
<van-swipe-item v-for="(banner, index) in visibleBanners" :key="index" @click="openContent(banner)">
|
||||
<LazyImg :src="banner.image" />
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { getModuleBannerTarget } from '@/utils/moduleBanner';
|
||||
|
||||
const props = defineProps<{
|
||||
banners?: Types.Home.ModuleBanner[] | null;
|
||||
moduleType: number;
|
||||
imageFit?: 'cover' | 'contain';
|
||||
}>();
|
||||
const router = useRouter();
|
||||
const visibleBanners = computed(() =>
|
||||
Array.isArray(props.banners) ? props.banners.filter((banner) => banner?.image?.trim()) : [],
|
||||
);
|
||||
const bannerKey = computed(() => visibleBanners.value.map((banner) => `${banner.contentId}:${banner.image}`).join('|'));
|
||||
const openContent = (banner: Types.Home.ModuleBanner) => {
|
||||
const target = getModuleBannerTarget(props.moduleType, banner.contentId);
|
||||
if (target) void router.push(target);
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.module-banners {
|
||||
padding: 12px 16px;
|
||||
.van-swipe {
|
||||
width: 100%;
|
||||
height: 193px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.van-swipe-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 50px;
|
||||
background: linear-gradient(to bottom, transparent, #000);
|
||||
pointer-events: none;
|
||||
}
|
||||
:deep(.img-decypt) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
:deep(.van-swipe__indicators) {
|
||||
transform: translateX(0);
|
||||
left: auto;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
.van-swipe__indicator {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
opacity: 0.3;
|
||||
&:not(:last-child) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
.van-swipe__indicator--active {
|
||||
width: 12px;
|
||||
background: $themeColor;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="nineGridPopup" v-if="appList.length > 0">
|
||||
<van-overlay :show="isShow" z-index="2001" @click="emit('close')">
|
||||
<div class="wrapper" ref="advRef">
|
||||
<!-- <AdvSwiper
|
||||
v-if="topList.length"
|
||||
@click.stop
|
||||
class="topAdv"
|
||||
:isShowIndicators="topList.length != 1"
|
||||
:list="topList"
|
||||
/> -->
|
||||
<div class="appList overflow-y-auto" @touchmove.stop>
|
||||
<div v-for="item in appList" :key="item.id">
|
||||
<div class="appItem" @click.stop="handleJumpAdv(item)">
|
||||
<lazyImg class="appIcon" :src="item.cover" />
|
||||
<div class="download">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="close"></div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { getAdItem } from '@/utils/getConfig';
|
||||
import { AdType } from '@/enums/app';
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
import { AdImpressionType, setAdImpression } from '@/utils/eventReporter/adImpression';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reportAdClickEvent } from '@/utils/eventReporter/adClick';
|
||||
|
||||
const route = useRoute();
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
adImpression?: AdImpressionType;
|
||||
}>();
|
||||
const emit = defineEmits(['close']);
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
const appList = ref<Types.App.AdsInfoListItem[]>([]);
|
||||
appList.value = getAdItem(AdType.QualityApplication) || [];
|
||||
// const topList = getAdItem(AdType.TopAdvsList);
|
||||
// 广告展示埋点
|
||||
const advRef = ref();
|
||||
watch(
|
||||
appList,
|
||||
() => {
|
||||
if (appList.value.length) {
|
||||
nextTick(() => {
|
||||
setAdImpression(advRef.value, appList.value, {
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'multi_grid',
|
||||
}).setupObserver();
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const handleJumpAdv = (item: Types.App.AdsInfoListItem) => {
|
||||
let params = {
|
||||
...item,
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'multi_grid',
|
||||
};
|
||||
reportAdClickEvent(params);
|
||||
jumpAdv(item);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.van-overlay) {
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
:deep(.adv-container) {
|
||||
height: 104px !important;
|
||||
.van-swipe-item {
|
||||
height: 104px !important;
|
||||
}
|
||||
}
|
||||
.nineGridPopup {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
// .topAdv {
|
||||
// width: 384px;
|
||||
// height: 104px;
|
||||
// border-radius: 4px;
|
||||
// overflow: hidden;
|
||||
// margin: 20px auto;
|
||||
// box-sizing: border-box;
|
||||
// }
|
||||
.appList {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-row-gap: 6px;
|
||||
grid-column-gap: 5px;
|
||||
padding: 0 25px;
|
||||
max-height: 470px;
|
||||
.appItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
// height: 140px;
|
||||
.appIcon {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.download {
|
||||
height: 30px;
|
||||
width: 70px;
|
||||
// background-color: #f4454d;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
line-height: 26px;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.close {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
background: url('./../../assets/images/png/closeDialog.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
margin: 20px auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="persalePopup">
|
||||
<van-overlay :show="isShow" z-index="1998" @click="emit('close', true)">
|
||||
<div class="wrapper" @click.stop>
|
||||
<!-- <div class="cartoon-gif"></div> -->
|
||||
<lazyImg
|
||||
class="bg"
|
||||
:src="configStore?.appConfig?.advancePage?.popBgProc"
|
||||
:key="configStore?.appConfig?.advancePage?.popBgProc"
|
||||
@click="emit('close', false)"
|
||||
/>
|
||||
<!-- <div class="mess">尾款剩余时间 {{ state.timeStrPay }}</div> -->
|
||||
</div>
|
||||
</van-overlay>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, computed, nextTick } from 'vue';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
|
||||
const state = reactive({
|
||||
timeStrPay: '',
|
||||
timeoutTimerPay: <any>null, // 支付尾款倒计时
|
||||
});
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
}>();
|
||||
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
const setTime = () => {
|
||||
const timers = new Date(configStore.appConfig.advanceStatus.endTime).getTime();
|
||||
countdownPay(timers);
|
||||
};
|
||||
nextTick(() => {
|
||||
setTime();
|
||||
});
|
||||
|
||||
// 支付尾款剩余时间--倒计时
|
||||
const countdownPay = (timer: number) => {
|
||||
const endTime = timer;
|
||||
const newTime = new Date().getTime();
|
||||
const diffTime = (endTime - newTime) / 1000;
|
||||
const day = parseInt((diffTime / 60 / 60 / 24).toString());
|
||||
const honur = parseInt(((diffTime / 60 / 60) % 24).toString());
|
||||
const min = parseInt(((diffTime / 60) % 60).toString());
|
||||
const sen = parseInt((diffTime % 60).toString());
|
||||
state.timeStrPay = `${showTime(day)}天 ${showTime(honur)}:${showTime(min)}:${showTime(sen)}`;
|
||||
// 一秒后递归
|
||||
state.timeoutTimerPay = setTimeout(() => {
|
||||
timer--;
|
||||
countdownPay(timer);
|
||||
}, 1000);
|
||||
};
|
||||
const showTime = (t: number) => {
|
||||
const time = t >= 10 ? t : '0' + t;
|
||||
return time;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.van-overlay) {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
.persalePopup {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.wrapper {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -60%);
|
||||
width: 283px;
|
||||
height: 381px;
|
||||
box-sizing: border-box;
|
||||
.cartoon-gif {
|
||||
width: 107px;
|
||||
height: 107px;
|
||||
background: url('@/assets/gif/cartoon-gif.gif') no-repeat;
|
||||
background-size: contain;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 20px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2000;
|
||||
}
|
||||
.mess {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
bottom: 130px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
width: 170px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.bg {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<van-popup
|
||||
:show="show"
|
||||
position="bottom"
|
||||
teleport="body"
|
||||
class="guideBottomSheet"
|
||||
:style="{ background: 'transparent' }"
|
||||
:z-index="2001"
|
||||
@click-overlay="emit('close')"
|
||||
>
|
||||
<div class="panel">
|
||||
<div class="indicator" />
|
||||
<div class="title">
|
||||
<p v-for="(line, lineIndex) in titleLines" :key="lineIndex">
|
||||
<span v-for="(part, partIndex) in line" :key="partIndex" :class="part.tone">{{ part.text }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="freeCount" v-if="freeCountText">{{ freeCountText }}</div>
|
||||
<lazyImg v-if="cardCover" class="card" :src="cardCover" :key="cardCover" />
|
||||
<img v-else class="card" :src="defaultVipCardImg" alt="" />
|
||||
<p class="desc">{{ descText }}</p>
|
||||
<div class="payBtn" @click="emit('pay')">{{ payBtnText }}</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import defaultVipCardImg from '@/assets/images/png/paymentGuide/vip-card.png';
|
||||
import type { GuideTitleLine } from '../types';
|
||||
|
||||
defineProps<{
|
||||
show: boolean;
|
||||
titleLines: GuideTitleLine[];
|
||||
descText: string;
|
||||
payBtnText: string;
|
||||
freeCountText?: string;
|
||||
/** 接口下发的会员卡封面,空则用本地默认图 */
|
||||
cardCover?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['close', 'pay']);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.guideBottomSheet {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 18px 16px calc(18px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid rgba(228, 187, 109, 0.3);
|
||||
border-radius: 20px 20px 0 0;
|
||||
background: linear-gradient(180deg, #0f0f0f 0%, #000000 24.81%);
|
||||
overflow: hidden;
|
||||
|
||||
.indicator {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
border-radius: 100px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
word-break: break-word;
|
||||
|
||||
.danger {
|
||||
color: #e94335;
|
||||
}
|
||||
|
||||
.gold {
|
||||
background: linear-gradient(90deg, #ffe8be 0%, #e6b764 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.freeCount {
|
||||
width: 271px;
|
||||
padding: 4px 0;
|
||||
border: 1px solid rgba(231, 71, 42, 0.2);
|
||||
border-radius: 20px;
|
||||
background: rgba(233, 67, 53, 0.1);
|
||||
font-size: 16px;
|
||||
line-height: 26px;
|
||||
color: #e94335;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: block;
|
||||
width: 270px;
|
||||
height: 122px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
|
||||
:deep(.lazy_img),
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
word-break: break-word;
|
||||
background: linear-gradient(90deg, #ffffff 0%, #efd394 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.payBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
border-radius: 26px;
|
||||
background: linear-gradient(90deg, #ffb03f 0%, #f2680c 100%);
|
||||
box-shadow: 0 6px 18px 0 rgba(242, 104, 12, 0.45);
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<van-overlay :show="true" :z-index="zIndex" class="guideCenterModal" @click="emit('close')">
|
||||
<div class="modalStack" @click.stop>
|
||||
<div class="wrapper">
|
||||
<lazyImg v-if="cardCover" class="card" :src="cardCover" :key="cardCover" />
|
||||
<img v-else class="card" :src="defaultVipCardImg" alt="" />
|
||||
<div class="panel">
|
||||
<div class="divider" />
|
||||
<div class="body">
|
||||
<div class="titleRow">
|
||||
<span class="normal">现在下单获得</span>
|
||||
<span class="highlight">优惠</span>
|
||||
<span class="normal">福利</span>
|
||||
</div>
|
||||
<div v-if="remainSec > 0" class="countdownRow">
|
||||
<span class="label">距优惠结束</span>
|
||||
<span class="unit">{{ padStart(hours) }}</span>
|
||||
<span class="colon">:</span>
|
||||
<span class="unit">{{ padStart(minutes) }}</span>
|
||||
<span class="colon">:</span>
|
||||
<span class="unit">{{ padStart(seconds) }}</span>
|
||||
</div>
|
||||
<div class="payBtn" @click="emit('pay')">{{ payBtnText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="closeBtn" aria-label="关闭" @click="emit('close')">
|
||||
<SvgIcon name="payment-guide-close" class="closeIcon" />
|
||||
</button>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 仅由父级在 show=true 时挂载;关闭即销毁。
|
||||
* 不在 keep-alive 缓存树上复用,避免二次打开倒计时行消失 / 停表。
|
||||
*/
|
||||
import { computed, onBeforeUnmount, ref } from 'vue';
|
||||
import defaultVipCardImg from '@/assets/images/png/paymentGuide/vip-card.png';
|
||||
import {
|
||||
ensureDiscountCountdownDeadlineMs,
|
||||
getDiscountCountdownRemainingMs,
|
||||
} from '@/utils/discountCountdownLocal';
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
payBtnText: string;
|
||||
/** 打开瞬间剩余毫秒;优先用 window 单例校准 */
|
||||
countdownMs: number;
|
||||
cardCover?: string;
|
||||
zIndex?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['close', 'pay']);
|
||||
|
||||
const zIndex = computed(() => props.zIndex ?? 2001);
|
||||
|
||||
ensureDiscountCountdownDeadlineMs();
|
||||
const remainMs = ref(
|
||||
(() => {
|
||||
const local = getDiscountCountdownRemainingMs();
|
||||
if (local > 0) return local;
|
||||
return Number(props.countdownMs) > 0 ? Number(props.countdownMs) : 0;
|
||||
})(),
|
||||
);
|
||||
|
||||
const remainSec = computed(() => Math.floor(remainMs.value / 1000));
|
||||
const hours = computed(() => Math.floor(remainSec.value / 3600));
|
||||
const minutes = computed(() => Math.floor((remainSec.value % 3600) / 60));
|
||||
const seconds = computed(() => remainSec.value % 60);
|
||||
|
||||
const padStart = (value: number) => String(value).padStart(2, '0');
|
||||
|
||||
/** 打开后短暂忽略到期关窗,避免挂载瞬间误关 */
|
||||
let allowFinishClose = false;
|
||||
const allowFinishTimer = window.setTimeout(() => {
|
||||
allowFinishClose = true;
|
||||
}, 800);
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
const local = getDiscountCountdownRemainingMs();
|
||||
remainMs.value = local > 0 ? local : Math.max(0, remainMs.value - 250);
|
||||
if (allowFinishClose && remainMs.value <= 0) {
|
||||
emit('close');
|
||||
}
|
||||
}, 250);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.clearInterval(timer);
|
||||
window.clearTimeout(allowFinishTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.guideCenterModal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(2px);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.modalStack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
width: 311px;
|
||||
padding-top: 61px;
|
||||
|
||||
.card {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 0;
|
||||
display: block;
|
||||
width: 270px;
|
||||
height: 122px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
z-index: 1;
|
||||
|
||||
:deep(.lazy_img),
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 311px;
|
||||
min-height: 261px;
|
||||
padding: 80px 24px 24px;
|
||||
border-radius: 16px;
|
||||
background: #0f0f0f;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border-top: 0.5px solid rgba(255, 255, 255, 0.1);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
font-weight: 900;
|
||||
white-space: nowrap;
|
||||
line-height: normal;
|
||||
|
||||
.normal {
|
||||
font-size: 20px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
font-size: 27px;
|
||||
background: linear-gradient(90deg, #ffe3a0 0%, #f0a93c 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.countdownRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #e85b4f;
|
||||
line-height: normal;
|
||||
|
||||
.label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #b8ae9c;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.unit {
|
||||
padding: 4px 7px;
|
||||
border-radius: 6px;
|
||||
background: #e23b2e;
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.colon {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.payBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 269px;
|
||||
height: 52px;
|
||||
border-radius: 26px;
|
||||
background: linear-gradient(90deg, #ffb03f 0%, #f2680c 100%);
|
||||
box-shadow: 0 6px 18px 0 rgba(242, 104, 12, 0.45);
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.closeBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
|
||||
:deep(.closeIcon) {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<van-popup
|
||||
:show="show"
|
||||
position="top"
|
||||
teleport="body"
|
||||
class="guideTopSheet"
|
||||
:style="{ background: 'transparent' }"
|
||||
:z-index="2001"
|
||||
:duration="popupDuration"
|
||||
:close-on-click-overlay="false"
|
||||
:lock-scroll="false"
|
||||
>
|
||||
<div
|
||||
class="panel"
|
||||
:class="{ 'panel--dismissing': dismissing }"
|
||||
:style="panelStyle"
|
||||
@touchstart.passive="onTouchStart"
|
||||
@touchmove.passive="onTouchMove"
|
||||
@touchend="onTouchEnd"
|
||||
@touchcancel="onTouchEnd"
|
||||
>
|
||||
<img class="hotTag" :src="hotTagImg" alt="" />
|
||||
<p class="title">{{ titleText }}</p>
|
||||
<div class="videoBox">
|
||||
<div class="videoList">
|
||||
<div class="videoItem" v-for="item in videos" :key="item.id" @click="emit('videoClick', item)">
|
||||
<div class="videoItem__cover">
|
||||
<lazyImg :src="item.coverThumb || item.cover" />
|
||||
<div class="videoItem__meta">
|
||||
<span class="videoItem__hot">
|
||||
<SvgIcon name="playCountIcon" color="#fff" />
|
||||
{{ watchCount(item.playCount) }}
|
||||
</span>
|
||||
<span>{{ videoTime(item.playTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="videoItem__title">{{ item.title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="payBtn" @click="emit('pay')">{{ payBtnText }}</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import hotTagImg from '@/assets/images/png/paymentGuide/hot-tag.png';
|
||||
import { watchCount, videoTime } from '@/utils/index';
|
||||
|
||||
const SWIPE_UP_DISMISS_PX = 48;
|
||||
const DISMISS_MS = 220;
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
titleText: string;
|
||||
payBtnText: string;
|
||||
videos: Types.Payment.GuideVideoItem[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['close', 'pay', 'videoClick']);
|
||||
|
||||
const dragOffsetY = ref(0);
|
||||
const dismissing = ref(false);
|
||||
let touchStartY = 0;
|
||||
let tracking = false;
|
||||
let dismissTimer: number | undefined;
|
||||
|
||||
/** 自定义上滑淡出后再关:禁用 van-popup 离场动画,避免淡出后又滑一次闪一下 */
|
||||
const popupDuration = computed(() => (dismissing.value ? 0 : 0.3));
|
||||
|
||||
const panelStyle = computed(() => {
|
||||
if (!dragOffsetY.value && !dismissing.value) return undefined;
|
||||
return {
|
||||
transform: `translateY(${dragOffsetY.value}px)`,
|
||||
transition: dismissing.value ? `transform ${DISMISS_MS}ms ease-out, opacity ${DISMISS_MS}ms ease-out` : 'none',
|
||||
opacity: dismissing.value ? 0 : Math.max(0.35, 1 + dragOffsetY.value / 160),
|
||||
};
|
||||
});
|
||||
|
||||
const resetDismissState = () => {
|
||||
if (dismissTimer != null) {
|
||||
window.clearTimeout(dismissTimer);
|
||||
dismissTimer = undefined;
|
||||
}
|
||||
dragOffsetY.value = 0;
|
||||
dismissing.value = false;
|
||||
tracking = false;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
// 再次打开时才复位;关闭过程中保持淡出态,避免闪回
|
||||
resetDismissState();
|
||||
return;
|
||||
}
|
||||
if (dismissTimer != null) {
|
||||
window.clearTimeout(dismissTimer);
|
||||
dismissTimer = undefined;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
if (!props.show || dismissing.value) return;
|
||||
const touch = e.touches[0];
|
||||
if (!touch) return;
|
||||
touchStartY = touch.clientY;
|
||||
tracking = true;
|
||||
dragOffsetY.value = 0;
|
||||
};
|
||||
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
if (!tracking || dismissing.value) return;
|
||||
const touch = e.touches[0];
|
||||
if (!touch) return;
|
||||
// 仅允许上滑位移(负向)
|
||||
dragOffsetY.value = Math.min(0, touch.clientY - touchStartY);
|
||||
};
|
||||
|
||||
const onTouchEnd = () => {
|
||||
if (!tracking || dismissing.value) return;
|
||||
tracking = false;
|
||||
if (dragOffsetY.value <= -SWIPE_UP_DISMISS_PX) {
|
||||
dismissing.value = true;
|
||||
dragOffsetY.value = -220;
|
||||
dismissTimer = window.setTimeout(() => {
|
||||
dismissTimer = undefined;
|
||||
emit('close');
|
||||
// 保持 dismissing/offset,等 show=false 再复位
|
||||
}, DISMISS_MS);
|
||||
return;
|
||||
}
|
||||
dragOffsetY.value = 0;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.guideTopSheet {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: relative;
|
||||
height: 242px;
|
||||
border-bottom: 1px solid #b71e03;
|
||||
border-radius: 0 0 20px 20px;
|
||||
background: #000000;
|
||||
overflow: hidden;
|
||||
touch-action: pan-y;
|
||||
will-change: transform, opacity;
|
||||
|
||||
&--dismissing {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hotTag {
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
top: 44px;
|
||||
width: 113px;
|
||||
height: 42px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: absolute;
|
||||
left: 156px;
|
||||
right: 29px;
|
||||
top: 69px;
|
||||
z-index: 1;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: linear-gradient(89.38deg, #ffe8be 1.65%, #e6b764 99.23%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.videoBox {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
top: 61px;
|
||||
height: 102px;
|
||||
border: 1px solid #b71e03;
|
||||
border-radius: 20px;
|
||||
background: rgba(183, 30, 3, 0.1);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.videoList {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
padding: 35px 10px 0;
|
||||
}
|
||||
|
||||
.videoItem {
|
||||
width: 76px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&__cover {
|
||||
position: relative;
|
||||
width: 76px;
|
||||
height: 42.5px;
|
||||
border-radius: 3.6px;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.lazy_img),
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__meta {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 3.6px 2.3px;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.6) 100%);
|
||||
font-size: 7px;
|
||||
line-height: 12px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&__hot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
|
||||
:deep(.svg-icon) {
|
||||
flex-shrink: 0;
|
||||
font-size: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-top: 4px;
|
||||
font-size: 8px;
|
||||
font-weight: 500;
|
||||
line-height: 8px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.payBtn {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 183px;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 39px;
|
||||
padding: 0 78px;
|
||||
border-radius: 19.5px;
|
||||
background: linear-gradient(90deg, #ffb03f 0%, #f2680c 100%);
|
||||
box-shadow: 0 4.5px 13.5px 0 rgba(242, 104, 12, 0.45);
|
||||
font-size: 13.5px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<GuideTopSheet
|
||||
v-if="styleType === PaymentGuideStyle.TOP_SHEET"
|
||||
:show="show"
|
||||
:title-text="topTitleText"
|
||||
:pay-btn-text="payBtnText"
|
||||
:videos="videos"
|
||||
@close="emit('close')"
|
||||
@pay="handlePay"
|
||||
@videoClick="handleVideoClick"
|
||||
/>
|
||||
<!-- 仅打开时挂载:关掉销毁,避免详情 keep-alive 复用导致倒计时二次不显示 -->
|
||||
<GuideCenterModal
|
||||
v-else-if="styleType === PaymentGuideStyle.CENTER_MODAL && show"
|
||||
:key="`center-${countdownOpenSeq}`"
|
||||
:show="true"
|
||||
:pay-btn-text="payBtnText"
|
||||
:countdown-ms="countdownMs"
|
||||
:card-cover="cardCover"
|
||||
:z-index="centerModalZIndex"
|
||||
@close="emit('close')"
|
||||
@pay="handlePay"
|
||||
/>
|
||||
<GuideBottomSheet
|
||||
v-else-if="styleType === PaymentGuideStyle.BOTTOM_SHEET"
|
||||
:show="show"
|
||||
:title-lines="titleLines"
|
||||
:desc-text="descText"
|
||||
:pay-btn-text="payBtnText"
|
||||
:free-count-text="freeCountText"
|
||||
:card-cover="cardCover"
|
||||
@close="emit('close')"
|
||||
@pay="handlePay"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import VideoApi from '@/service/video';
|
||||
import { PaymentGuideScene, PaymentGuideStyle } from '@/enums/home';
|
||||
import { usePaymentStatusDirectPay } from '@/hooks/usePaymentStatusDirectPay';
|
||||
import { getDiscountCountdownRemainingMs } from '@/utils/discountCountdownLocal';
|
||||
import GuideBottomSheet from './components/GuideBottomSheet.vue';
|
||||
import GuideCenterModal from './components/GuideCenterModal.vue';
|
||||
import GuideTopSheet from './components/GuideTopSheet.vue';
|
||||
import type { GuideTitleLine } from './types';
|
||||
|
||||
const MAX_TOP_SHEET_VIDEOS = 4;
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
guide: Types.Payment.PaymentGuide | null;
|
||||
/** 触发场景,用于缺省文案 */
|
||||
scene?: PaymentGuideScene | '';
|
||||
/** 免费试看次数,传入后展示「剩余免费次数」样式 */
|
||||
freeCount?: Types.Payment.GuideFreeCount | null;
|
||||
}>();
|
||||
|
||||
/** 须用运行时数组:Vue 3.2 type-only defineEmits 易被 Volar 推成无调用签名的 __VLS_Emit */
|
||||
const emit = defineEmits(['close', 'pay']);
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { prepareCard } = usePaymentStatusDirectPay();
|
||||
|
||||
const videos = ref<Types.Payment.GuideVideoItem[]>([]);
|
||||
const priceText = ref('');
|
||||
|
||||
/** 会员卡图:优先接口 cover,空则子组件回退本地默认图 */
|
||||
const cardCover = computed(() => props.guide?.cover?.trim() || '');
|
||||
|
||||
/**
|
||||
* 弹层样式只跟场景 UI,不读接口 style:
|
||||
* - VIP 上新 → 顶部横幅
|
||||
* - 返回拦截 / 会员中心优惠 / 优惠倒计时 → 居中弹窗
|
||||
* - 试看结束 / 免费次数 / 首页新老用户 → 底部半屏
|
||||
*/
|
||||
const styleType = computed(() => {
|
||||
if (props.scene === PaymentGuideScene.VIP_CONTENT_UPDATE) {
|
||||
return PaymentGuideStyle.TOP_SHEET;
|
||||
}
|
||||
if (
|
||||
props.scene === PaymentGuideScene.VIDEO_BACK ||
|
||||
props.scene === PaymentGuideScene.VIP_CENTER ||
|
||||
props.scene === PaymentGuideScene.DISCOUNT_COUNTDOWN
|
||||
) {
|
||||
return PaymentGuideStyle.CENTER_MODAL;
|
||||
}
|
||||
return PaymentGuideStyle.BOTTOM_SHEET;
|
||||
});
|
||||
|
||||
/** 返回引导需盖过播放器/遮罩等高层级元素 */
|
||||
const centerModalZIndex = computed(() =>
|
||||
props.scene === PaymentGuideScene.VIDEO_BACK ? 100000 : 2001,
|
||||
);
|
||||
|
||||
const isVideoScene = computed(
|
||||
() => props.scene === PaymentGuideScene.VIDEO_PREVIEW_END || props.scene === PaymentGuideScene.VIDEO_BACK,
|
||||
);
|
||||
|
||||
const vipCardId = computed(() => {
|
||||
const action = props.guide?.action;
|
||||
if (action?.type === 'VIP_PRODUCT' && action.value) return action.value;
|
||||
return props.guide?.productId || '';
|
||||
});
|
||||
|
||||
/**
|
||||
* 每次打开居中弹窗递增,配合 v-if + :key 强制新实例。
|
||||
* countdownMs 在打开瞬间取剩余,不随关窗清成 0(关窗时子组件已卸载)。
|
||||
*/
|
||||
const countdownOpenSeq = ref(0);
|
||||
const countdownMs = ref(0);
|
||||
watch(
|
||||
() => [props.show, props.scene, props.guide?.durationSeconds] as const,
|
||||
([visible]) => {
|
||||
if (!visible) return;
|
||||
countdownOpenSeq.value += 1;
|
||||
const fromGuide = Number(props.guide?.durationSeconds) || 0;
|
||||
if (
|
||||
props.scene === PaymentGuideScene.VIDEO_BACK ||
|
||||
props.scene === PaymentGuideScene.VIP_CENTER ||
|
||||
props.scene === PaymentGuideScene.DISCOUNT_COUNTDOWN
|
||||
) {
|
||||
const local = getDiscountCountdownRemainingMs();
|
||||
countdownMs.value = local > 0 ? local : fromGuide > 0 ? fromGuide * 1000 : 0;
|
||||
return;
|
||||
}
|
||||
countdownMs.value = fromGuide > 0 ? fromGuide * 1000 : 0;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
/** 是否拿到过免费次数数据(含剩余 0) */
|
||||
const hasFreeCountInfo = computed(() => props.freeCount != null);
|
||||
|
||||
/** 免费次数引导:传入 freeCount,或场景为 HOME_NEW_USER_FREE_TRIAL */
|
||||
const useFixedFreeCountCopy = computed(
|
||||
() => hasFreeCountInfo.value || props.scene === PaymentGuideScene.HOME_NEW_USER_FREE_TRIAL,
|
||||
);
|
||||
|
||||
/** 剩余次数:只认 watchCount,不认 isCan(已扣过次的片也会 isCan=true) */
|
||||
const freeRemaining = computed(() => {
|
||||
const n = Number(props.freeCount?.watchCount);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
});
|
||||
const freeTotal = computed(() => {
|
||||
const n = Number(props.freeCount?.totalWatchCount);
|
||||
return Number.isFinite(n) && n > 0 ? n : 0;
|
||||
});
|
||||
const hasRemainingFreeQuota = computed(() => hasFreeCountInfo.value && freeRemaining.value > 0);
|
||||
|
||||
const FREE_COUNT_DESC = '开通会员即可 畅看 “VIP全部视频”';
|
||||
|
||||
/** 免费次数红条文案:有次数「免费观看次数剩余1/2次」;用尽「免费看额度没有了」 */
|
||||
const freeCountText = computed(() => {
|
||||
if (!hasFreeCountInfo.value) return '';
|
||||
if (!hasRemainingFreeQuota.value) return '免费看额度没有了';
|
||||
if (!freeTotal.value) return `免费观看次数剩余${freeRemaining.value}次`;
|
||||
return `免费观看次数剩余${freeRemaining.value}/${freeTotal.value}次`;
|
||||
});
|
||||
|
||||
/** 标题优先读接口 title,缺省按场景兜底 */
|
||||
const titleLines = computed<GuideTitleLine[]>(() => {
|
||||
const apiTitle = props.guide?.title?.trim();
|
||||
if (apiTitle) {
|
||||
return [[{ text: apiTitle }]];
|
||||
}
|
||||
if (hasRemainingFreeQuota.value) {
|
||||
return [[{ text: '免费观看次数 ' }, { text: '即将用尽!', tone: 'danger' }]];
|
||||
}
|
||||
if (useFixedFreeCountCopy.value) {
|
||||
return [[{ text: '免费观看次数' }]];
|
||||
}
|
||||
if (isVideoScene.value) {
|
||||
// Figma 21694:90491 试看结束前置购买提示缺省文案
|
||||
return [[{ text: '该视频为' }, { text: 'VIP内容', tone: 'gold' }], [{ text: '只能观看试看内容哦~' }]];
|
||||
}
|
||||
if (props.scene === PaymentGuideScene.HOME_OLD_USER) {
|
||||
return [[{ text: 'VIP影片可以试看' }]];
|
||||
}
|
||||
return [[{ text: 'VIP影片免费试看' }]];
|
||||
});
|
||||
|
||||
/** 描述优先读接口 description,缺省按场景兜底 */
|
||||
const descText = computed(() => {
|
||||
const apiDesc = props.guide?.description?.trim();
|
||||
if (apiDesc) return apiDesc;
|
||||
if (useFixedFreeCountCopy.value) return FREE_COUNT_DESC;
|
||||
// Figma 21694:90491 试看结束前置购买提示缺省文案
|
||||
if (isVideoScene.value) return '想要查看完整内容,需要开通会员';
|
||||
if (props.scene === PaymentGuideScene.HOME_OLD_USER) return '查看完整影片需要购买会员';
|
||||
return '开通会员 查看完整影片';
|
||||
});
|
||||
|
||||
const topTitleText = computed(
|
||||
() => props.guide?.description?.trim() || 'VIP会员“特享内容”更新上架啦~',
|
||||
);
|
||||
|
||||
const payBtnText = computed(() => {
|
||||
if (styleType.value === PaymentGuideStyle.TOP_SHEET) return '开通会员 · 查看劲爆视频';
|
||||
if (priceText.value) return `立即开通 · 仅需¥${priceText.value}`;
|
||||
return '开通会员 · 查看完整内容';
|
||||
});
|
||||
|
||||
const formatAmount = (amount: number) => (Number.isInteger(amount) ? String(amount) : amount.toFixed(2));
|
||||
|
||||
const mapGuideVideos = (list: Types.Payment.GuideVideoItem[]) =>
|
||||
list.slice(0, MAX_TOP_SHEET_VIDEOS).map((item) => ({
|
||||
...item,
|
||||
cover: item.coverThumb || item.cover,
|
||||
}));
|
||||
|
||||
/** 优先用接口直出的 videos;无 videos 时回退 videoIds 拉详情(兼容旧响应) */
|
||||
const loadTopSheetVideos = async () => {
|
||||
const guideVideos = props.guide?.videos;
|
||||
if (guideVideos?.length) {
|
||||
videos.value = mapGuideVideos(guideVideos);
|
||||
return;
|
||||
}
|
||||
const videoIds = props.guide?.videoIds || [];
|
||||
if (!videoIds.length) {
|
||||
videos.value = [];
|
||||
return;
|
||||
}
|
||||
const list = await Promise.all(
|
||||
videoIds.slice(0, MAX_TOP_SHEET_VIDEOS).map((videoID) =>
|
||||
VideoApi.queryVideoInfo({ videoID }).catch((e) => {
|
||||
console.log(e);
|
||||
return null;
|
||||
}),
|
||||
),
|
||||
);
|
||||
videos.value = list.filter(Boolean).map((item) => {
|
||||
const video = item as unknown as Types.Video.VInfosItem;
|
||||
return {
|
||||
id: video.id,
|
||||
title: video.title,
|
||||
cover: video.coverThumb || video.cover,
|
||||
coverThumb: video.coverThumb,
|
||||
playTime: video.playTime,
|
||||
playCount: video.playCount,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** 开通按钮:跳转会员中心并选中引导配置的卡,不直接拉起支付 */
|
||||
const handlePay = async () => {
|
||||
const id = vipCardId.value ? String(vipCardId.value) : '';
|
||||
emit('pay');
|
||||
emit('close');
|
||||
// 等弹窗关闭(含 van-count-down 卸载)后再跳转,避免详情 keep-alive 失活时倒计时进入 deactivated 残留
|
||||
await nextTick();
|
||||
// 已在会员页:只更新选中卡,避免再 push 一层 /member
|
||||
if (route.path === '/member') {
|
||||
if (!id) return;
|
||||
if (String(route.query.id || '') !== id) {
|
||||
router.replace({ path: '/member', query: { ...route.query, id } });
|
||||
}
|
||||
return;
|
||||
}
|
||||
router.push(`/member${id ? `?id=${id}` : ''}`);
|
||||
};
|
||||
|
||||
const handleVideoClick = (video: Types.Payment.GuideVideoItem) => {
|
||||
emit('close');
|
||||
router.push(`/movieDetails?vid=${video.id}`);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
async (visible) => {
|
||||
if (!visible) return;
|
||||
priceText.value = '';
|
||||
const context = await prepareCard(vipCardId.value);
|
||||
priceText.value = context && context.amount > 0 ? formatAmount(context.amount) : '';
|
||||
if (styleType.value === PaymentGuideStyle.TOP_SHEET) {
|
||||
loadTopSheetVideos();
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
/** 标题高亮色调:gold=金色渐变,danger=警示红 */
|
||||
export type GuideTitleTone = 'gold' | 'danger';
|
||||
|
||||
export interface GuideTitlePart {
|
||||
text: string;
|
||||
tone?: GuideTitleTone;
|
||||
}
|
||||
|
||||
export type GuideTitleLine = GuideTitlePart[];
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div class="persalePopup">
|
||||
<van-overlay :show="isShow" z-index="1998" @click="emit('close', true)">
|
||||
<div class="wrapper" @click.stop>
|
||||
<!-- <div class="cartoon-gif"></div> -->
|
||||
<lazyImg
|
||||
class="bg"
|
||||
:src="configStore?.appConfig?.advancePage?.popBgWait"
|
||||
:key="configStore?.appConfig?.advancePage?.popBgWait"
|
||||
@click="emit('close', false)"
|
||||
/>
|
||||
<!-- <div class="btn" @click="emit('close', false)"></div> -->
|
||||
</div>
|
||||
</van-overlay>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
const configStore: any = useConfigStore();
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
}>();
|
||||
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.van-overlay) {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
.persalePopup {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.wrapper {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -60%);
|
||||
width: 283px;
|
||||
height: 381px;
|
||||
box-sizing: border-box;
|
||||
.cartoon-gif {
|
||||
width: 107px;
|
||||
height: 107px;
|
||||
background: url('@/assets/gif/cartoon-gif.gif') no-repeat;
|
||||
background-size: contain;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 20px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2000;
|
||||
}
|
||||
.btn {
|
||||
position: absolute;
|
||||
width: 90%;
|
||||
height: 50px;
|
||||
bottom: 40px;
|
||||
left: 20px;
|
||||
}
|
||||
.bg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,700 @@
|
||||
<template>
|
||||
<!-- <div class="post-box" @click="$router.push({ path: '/postDetail', query: { postid: state.postInfo.id } })"> -->
|
||||
<div class="post-box" @click="jumpDetail">
|
||||
<div class="rank-box" v-if="rank">
|
||||
<div v-if="rank < 4" class="rank-icon" :class="`rank-icon-${rank}`"></div>
|
||||
<div v-else class="sortbox">
|
||||
<div class="sortbox-icon"></div>
|
||||
NO.{{ props.rank }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- header-->
|
||||
<div class="post-top">
|
||||
<div class="post-top-left flex-center-center">
|
||||
<div class="post-avatar">
|
||||
<lazyImg
|
||||
:src="state.postInfo.publisher?.portrait"
|
||||
round
|
||||
:key="state.postInfo.publisher?.portrait"
|
||||
@click.stop="toUserpage"
|
||||
/>
|
||||
</div>
|
||||
<div class="post-info">
|
||||
<div class="username_box ellipsis">
|
||||
<span>{{ state.postInfo.publisher?.name }}</span>
|
||||
<img v-if="state.postInfo.publisher?.superUser" src="@/assets/images/png/isCentral.png" alt="" />
|
||||
<!-- <SvgIcon class="vipIcon icon" name="vipIcon" v-if="state.publisher?.vipLevel > 0" />-->
|
||||
</div>
|
||||
<!--显示时间-->
|
||||
<div class="location-box flex-vcenter">
|
||||
<!-- <span v-if="state.postInfo.publisher?.vipName">{{state.postInfo.publisher?.vipName}}</span> -->
|
||||
{{ formatPostTime(state.postInfo.reviewAt) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 关注 -->
|
||||
<!-- <div
|
||||
@click.stop="followUp"
|
||||
v-if="userStore.userInfo.uid != state.postInfo.publisher?.uid && route.path != '/personal'"
|
||||
>
|
||||
<div class="post-top-right-follow flex-center-center" v-if="state.postInfo.publisher?.hasFollowed">已关注</div>
|
||||
<div class="post-top-right flex-center-center" v-else>
|
||||
<svgIcon name="follow_add"></svgIcon>
|
||||
关注
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- 审核状态 -->
|
||||
<div
|
||||
class="statusClass"
|
||||
v-if="userStore.userInfo.uid == state.postInfo.publisher?.uid && route.path == '/minepostpage'"
|
||||
>
|
||||
<div @click.stop="state.postInfo?.status == 2 ? showReason() : ''">
|
||||
{{ setText(state.postInfo?.status) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--content -->
|
||||
<div class="post-text">
|
||||
<TextOverflow
|
||||
:text="state.postInfo.title"
|
||||
:maxLines="3"
|
||||
:isChoosen="state.postInfo.chosen"
|
||||
:isTopping="state.postInfo.isTopping"
|
||||
>
|
||||
<template #default="{ clickToggle, expanded }">
|
||||
<div @click.stop="clickToggle" class="text-btn">
|
||||
{{ expanded ? '收起' : '全文' }}
|
||||
</div>
|
||||
</template>
|
||||
</TextOverflow>
|
||||
</div>
|
||||
<!-- 图片+视频 -->
|
||||
<div
|
||||
class="post-media"
|
||||
:class="{ image_one: displaySeriesCover.length == 1 && !state.postInfo.sourceURL }"
|
||||
>
|
||||
<!-- 如果只有图片 没有视频 -->
|
||||
<template v-if="displaySeriesCover.length && !state.postInfo.sourceURL">
|
||||
<lazyImg
|
||||
:src="src"
|
||||
v-for="(src, index) in displaySeriesCover.slice(0, 3)"
|
||||
:key="src + index"
|
||||
class="img_item"
|
||||
>
|
||||
<div v-if="index == 2 && displaySeriesCover.length > 3" class="filter">
|
||||
+{{ displaySeriesCover.length - 3 }}
|
||||
</div>
|
||||
</lazyImg>
|
||||
</template>
|
||||
<!-- 有图片也有视频:seriesCover 已剔除封面 -->
|
||||
<template v-else-if="displaySeriesCover.length && state.postInfo.sourceURL">
|
||||
<template v-if="displaySeriesCover.length >= 3">
|
||||
<lazyImg
|
||||
:src="src"
|
||||
v-for="(src, index) in displaySeriesCover.slice(0, 2)"
|
||||
:key="src + index"
|
||||
class="img_item"
|
||||
/>
|
||||
<lazyImg :src="state.postInfo.cover" :key="state.postInfo.cover" class="img_item">
|
||||
<div class="play_box">
|
||||
<SvgIcon name="playIcon" />
|
||||
</div>
|
||||
</lazyImg>
|
||||
</template>
|
||||
<template v-else>
|
||||
<lazyImg
|
||||
:src="src"
|
||||
v-for="(src, index) in displaySeriesCover"
|
||||
:key="src + index"
|
||||
class="img_item"
|
||||
></lazyImg>
|
||||
<lazyImg :src="state.postInfo.cover" :key="state.postInfo.cover" class="img_item">
|
||||
<div class="play_box">
|
||||
<SvgIcon name="playIcon" />
|
||||
</div>
|
||||
</lazyImg>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 只有视频(无图,或图全是封面已被剔除) -->
|
||||
<template v-else-if="state.postInfo.sourceURL">
|
||||
<lazyImg :src="state.postInfo.cover" :key="state.postInfo.cover" class="img_item img_item2">
|
||||
<div class="play_box">
|
||||
<SvgIcon name="playIcon" />
|
||||
</div>
|
||||
</lazyImg>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 操作栏 -->
|
||||
<div class="post_menu">
|
||||
<div class="post_menu_left">
|
||||
<div class="item">
|
||||
<SvgIcon class="icon" name="post_eye" />
|
||||
<span class="ellipsis">{{ watchCount(state.postInfo.pageViewCount) }}</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<SvgIcon class="community_info icon" name="post_commit" />
|
||||
<span class="ellipsis">
|
||||
{{ watchCount(state.postInfo.commentCount > 0 ? state.postInfo.commentCount : 0) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<SvgIcon class="icon" v-if="state.postInfo.vidStatus?.hasLiked" name="post_inheart" />
|
||||
<SvgIcon class="icon" v-else name="post_heart" />
|
||||
<span class="ellipsis">{{ watchCount(state.postInfo.likeCount) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="post_menu_right" @click.stop="goTagPage" v-if="!props.isNoShowTags">
|
||||
{{ state.postInfo.tags && state.postInfo.tags.length ? '#' + state.postInfo.tags[0].name : '' }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 新增删除 / 多选 -->
|
||||
<div
|
||||
v-if="isEdit && editMode === 'select'"
|
||||
class="selectBtn"
|
||||
:class="{ active: selected }"
|
||||
@click.stop="emitSelect"
|
||||
/>
|
||||
<div class="delBtn" v-else-if="isEdit" @click.stop="del(state.postInfo)">
|
||||
<SvgIcon name="clear-red" class="selectActive" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject, nextTick, reactive, computed, type ComputedRef } from 'vue';
|
||||
import { watchCount } from '@/utils/index';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { showToast } from 'vant';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import CommunityApi from '@/service/community';
|
||||
import { LikeType } from '@/enums/app';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
import UserApi from '@/service/user';
|
||||
import moment from 'moment';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const globalStore = useGlobalStore();
|
||||
|
||||
const props = defineProps<{
|
||||
postInfo: any;
|
||||
currentIndex: any;
|
||||
isNoShowTags?: any;
|
||||
list: Array<Types.Video.VInfosItem>;
|
||||
checkIsOpen?: boolean;
|
||||
rank?: number;
|
||||
isListMode?: boolean; //列表or 详情
|
||||
isEdit?: boolean;
|
||||
editMode?: 'delete' | 'select';
|
||||
selected?: boolean;
|
||||
}>();
|
||||
|
||||
const state = reactive({
|
||||
postInfo: props.postInfo,
|
||||
isListMode: props.isListMode || false,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['showReason', 'setNewData', 'del', 'select', 'click']);
|
||||
|
||||
/** 有视频且封面在 seriesCover 中时,展示前剔除 cover,避免封面重复 */
|
||||
const displaySeriesCover = computed(() => {
|
||||
const list = state.postInfo?.seriesCover || [];
|
||||
const cover = state.postInfo?.cover;
|
||||
if (!state.postInfo?.sourceURL || !cover) return list;
|
||||
if (!list.includes(cover)) return list;
|
||||
return list.filter((src: string) => src !== cover);
|
||||
});
|
||||
|
||||
/** 帖子相对时间:刚刚 / n分钟前 / n小时前 / n天前 / n周前 / 月日 / 年月日 */
|
||||
const formatPostTime = (time: any) => {
|
||||
if (!time) return '';
|
||||
const now = moment();
|
||||
const target = moment(time);
|
||||
if (!target.isValid()) return '';
|
||||
|
||||
const diffSec = Math.max(now.diff(target, 'seconds'), 0);
|
||||
if (diffSec < 60) return '刚刚';
|
||||
|
||||
const diffMin = now.diff(target, 'minutes');
|
||||
if (diffMin < 60) return `${diffMin}分钟前`;
|
||||
|
||||
const diffHour = now.diff(target, 'hours');
|
||||
if (diffHour < 24) return `${diffHour}小时前`;
|
||||
|
||||
const diffDay = now.diff(target, 'days');
|
||||
if (diffDay < 7) return `${diffDay}天前`;
|
||||
|
||||
const diffMonth = now.diff(target, 'months');
|
||||
if (diffMonth < 1) {
|
||||
const diffWeek = Math.max(now.diff(target, 'weeks'), 1);
|
||||
return `${diffWeek}周前`;
|
||||
}
|
||||
|
||||
if (diffMonth < 12) {
|
||||
return target.format('MM月DD日');
|
||||
}
|
||||
|
||||
return target.format('YYYY年MM月DD日');
|
||||
};
|
||||
|
||||
const goTagPage = () => {
|
||||
const [oneTag] = state.postInfo.tags;
|
||||
if (oneTag) {
|
||||
router.push({
|
||||
path: '/communityTagDeatil',
|
||||
query: { id: oneTag.id, title: oneTag.name, newsType: state.postInfo.newsType },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const toUserpage = () => {
|
||||
router.push({ path: '/userInfo', query: { uid: state.postInfo.publisher.uid } });
|
||||
localStorage.removeItem('isChange');
|
||||
};
|
||||
|
||||
const setText = (data: any) => {
|
||||
if (data == 0) {
|
||||
return '待审核';
|
||||
} else if (data == 1) {
|
||||
return '已通过';
|
||||
} else if (data == 2) {
|
||||
return '未过审·查看原因';
|
||||
}
|
||||
};
|
||||
|
||||
const showReason = () => {
|
||||
emit('showReason', state.postInfo?.reason);
|
||||
};
|
||||
const jumpDetail = () => {
|
||||
if (isEdit.value) {
|
||||
if (editMode.value === 'select') {
|
||||
emit('select', state.postInfo?.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
emit('click');
|
||||
globalStore.savePostList(props.list, props.currentIndex);
|
||||
router.push(`/postDetail?postid=${state.postInfo.id}`);
|
||||
};
|
||||
const injectedEdit = inject<ComputedRef<boolean> | boolean>('isEdit', false);
|
||||
const injectedEditMode = inject<'delete' | 'select' | ComputedRef<'delete' | 'select'>>('editMode', 'delete');
|
||||
const isEdit = computed(() => {
|
||||
if (typeof props.isEdit === 'boolean') return props.isEdit;
|
||||
if (typeof injectedEdit === 'object' && injectedEdit && 'value' in injectedEdit) {
|
||||
return Boolean(injectedEdit.value);
|
||||
}
|
||||
return Boolean(injectedEdit);
|
||||
});
|
||||
const editMode = computed(() => {
|
||||
if (props.editMode === 'select' || props.editMode === 'delete') return props.editMode;
|
||||
if (typeof injectedEditMode === 'object' && injectedEditMode && 'value' in injectedEditMode) {
|
||||
return injectedEditMode.value || 'delete';
|
||||
}
|
||||
return injectedEditMode || 'delete';
|
||||
});
|
||||
const selected = computed(() => Boolean(props.selected));
|
||||
const emitSelect = () => {
|
||||
emit('select', state.postInfo?.id);
|
||||
};
|
||||
// const del = async (info: any) => {
|
||||
// // 二次确认弹窗
|
||||
// showConfirmDialog({
|
||||
// title: '收藏记录',
|
||||
// message: '是否确认删除该收藏',
|
||||
// })
|
||||
// .then(async () => {
|
||||
// showToast('正在清除,请等待...');
|
||||
// let params = {
|
||||
// objID: info.id,
|
||||
// isCollect: false,
|
||||
// type: 'img',
|
||||
// };
|
||||
// try {
|
||||
// const res = await UserApi.delCollect(params); // 假设这是你的清除操作
|
||||
// await nextTick();
|
||||
// emit('del');
|
||||
// showToast('清除成功');
|
||||
// } catch (error) {
|
||||
// showToast('清除失败');
|
||||
// console.error('清除操作失败:', error);
|
||||
// }
|
||||
// })
|
||||
// .catch(() => {
|
||||
// console.log('取消');
|
||||
// });
|
||||
// };
|
||||
|
||||
//
|
||||
const del = async (info: any) => {
|
||||
emit('del', info.id);
|
||||
return;
|
||||
// if (props.checkIsOpen) {
|
||||
// emit('del', info.id);
|
||||
// return;
|
||||
// }
|
||||
// showToast('正在清除,请等待...');
|
||||
// let params = {
|
||||
// objID: info.id,
|
||||
// isCollect: false,
|
||||
// type: 'img',
|
||||
// };
|
||||
// try {
|
||||
// const res = await UserApi.delCollect(params); // 假设这是你的清除操作
|
||||
// await nextTick();
|
||||
// emit('del');
|
||||
// showToast('清除成功');
|
||||
// } catch (error) {
|
||||
// showToast('清除失败');
|
||||
// console.error('清除操作失败:', error);
|
||||
// }
|
||||
};
|
||||
|
||||
// 点赞
|
||||
const clickLike = async () => {
|
||||
// 列表不显示是否点赞,只有详情页才有这种操作
|
||||
if (state.isListMode) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (!state.postInfo.vidStatus.hasLiked) {
|
||||
let req = {
|
||||
type: LikeType.COVER,
|
||||
objID: state.postInfo.id,
|
||||
};
|
||||
await CommunityApi.zan(req);
|
||||
state.postInfo.vidStatus.hasLiked = true;
|
||||
state.postInfo.likeCount++;
|
||||
showToast('点赞成功');
|
||||
localStorage.setItem('operateData', JSON.stringify(state.postInfo));
|
||||
} else {
|
||||
let req = {
|
||||
type: LikeType.COVER,
|
||||
objIDs: [state.postInfo.id],
|
||||
};
|
||||
await CommunityApi.cancleZan(req);
|
||||
state.postInfo.vidStatus.hasLiked = false;
|
||||
state.postInfo.likeCount--;
|
||||
showToast('取消成功');
|
||||
localStorage.setItem('operateData', JSON.stringify(state.postInfo));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.post-box {
|
||||
width: 100%;
|
||||
padding: 12px 0;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
.rank-box {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 16px;
|
||||
|
||||
.rank-icon {
|
||||
width: 59px;
|
||||
height: 24px;
|
||||
|
||||
&-1 {
|
||||
background: url('@/assets/images/png/rank_one.png') no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
&-2 {
|
||||
background: url('@/assets/images/png/rank_two.png') no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
&-3 {
|
||||
background: url('@/assets/images/png/rank_three.png') no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
&-4 {
|
||||
background: url('@/assets/images/png/rank_four.png') no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.sortbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #bdbdbd;
|
||||
font-family: 'PingFang SC';
|
||||
font-size: 14px;
|
||||
|
||||
.sortbox-icon {
|
||||
margin-right: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: url('@/assets/images/png/rank_other.png') no-repeat center center;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
|
||||
.post-top-left {
|
||||
.post-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
|
||||
:deep(.loading) {
|
||||
background-size: 28px 21px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-info {
|
||||
width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.username_box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.36px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.location-box {
|
||||
padding-top: 5px;
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
|
||||
span {
|
||||
font-size: 10px;
|
||||
color: rgb(102, 56, 0);
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(to right, rgb(238, 217, 180), rgb(174, 138, 95));
|
||||
padding: 2px 5px;
|
||||
box-sizing: border-box;
|
||||
margin: 0 5px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post-top-right {
|
||||
svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin: 0 3px 0 0;
|
||||
}
|
||||
|
||||
background: #ffc000;
|
||||
border: solid 1px #ffc000;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
color: #1f2029;
|
||||
width: 62px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.post-top-right-follow {
|
||||
width: 62px;
|
||||
height: 24px;
|
||||
color: #989898;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-text {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post-media {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-row-gap: 8px;
|
||||
grid-column-gap: 5px;
|
||||
&.image_one {
|
||||
grid-template-columns: 1fr;
|
||||
.img_item {
|
||||
width: 100%;
|
||||
height: 193px;
|
||||
}
|
||||
}
|
||||
|
||||
.img_item {
|
||||
height: 111px;
|
||||
width: 111px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.filter {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
right: 5px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
color: #141414;
|
||||
padding: 2px 8px;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
border-radius: 99px;
|
||||
}
|
||||
|
||||
.play_box {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
> svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.img_item2 {
|
||||
width: 343px;
|
||||
height: 193px;
|
||||
|
||||
.play_box {
|
||||
> svg {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post_menu {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
|
||||
.post_menu_left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 22px;
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 3px;
|
||||
color: #989898;
|
||||
}
|
||||
|
||||
.ellipsis {
|
||||
font-size: 12px;
|
||||
color: #989898;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post_menu_right {
|
||||
font-size: 12px;
|
||||
padding: 0 6px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
color: #7c8287;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.statusClass {
|
||||
padding: 10px 0 0 0;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
margin: 10px 0 0 0;
|
||||
color: rgb(62, 100, 234);
|
||||
|
||||
p {
|
||||
margin: 6px 0 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除按钮
|
||||
.delBtn {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
|
||||
.selectActive {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
}
|
||||
.selectBtn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 10;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid #989898;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&.active {
|
||||
border-color: #ffc000;
|
||||
background: #ffc000;
|
||||
&::after {
|
||||
content: '';
|
||||
width: 6px;
|
||||
height: 10px;
|
||||
border: 2px solid #1f2029;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
transform: rotate(45deg) translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<van-overlay :show="isShow" z-index="2001" @click.stop="emit('close')" :lock-scroll="false">
|
||||
<div class="wrapper flex-center-center">
|
||||
<div class="content">
|
||||
<div class="title">提示</div>
|
||||
<div class="horizontalLine"></div>
|
||||
<div class="text">当前免费观影次数已用完!</div>
|
||||
<div class="text">支付尾款 享受完整超级会员特权</div>
|
||||
<div class="btnBox flex-center-between">
|
||||
<div class="inviteBtn" @click.stop="unlockNow">{{ gold }}金币支付</div>
|
||||
<div class="btn flex-center-center" @click.stop="$router.push('/presale')">前往活动</div>
|
||||
</div>
|
||||
<template v-if="useGlobalStore().presaleObj.status == 2">
|
||||
<div class="startActivity">
|
||||
{{ dateFormatMmDd(useGlobalStore().presaleObj.activityDetail.startTime) }} 限时开启
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="useGlobalStore().presaleObj.status == 3">
|
||||
<div class="countdown">{{ useGlobalStore().presaleObj.timeStrCoinPay }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import useLoading from '@/hooks/loading';
|
||||
import VideoApi from '@/service/video';
|
||||
import { computed } from 'vue';
|
||||
import { ProductType } from '@/enums/app';
|
||||
import { showToast } from 'vant';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { dateFormatMmDd } from '@/utils/index';
|
||||
|
||||
const props = defineProps<{
|
||||
videoInfo: Types.Video.VInfosItem;
|
||||
isOpen: boolean;
|
||||
productType?: ProductType;
|
||||
}>();
|
||||
const userStore = useUserStore();
|
||||
const userInfo = userStore.userInfo;
|
||||
|
||||
const router = useRouter();
|
||||
const { openLoading, closeLoading } = useLoading();
|
||||
const emit = defineEmits(['close', 'buySuccess']);
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
const gold = computed(() => {
|
||||
return userInfo.isVip ? props.videoInfo.coins : props.videoInfo.originCoins;
|
||||
});
|
||||
const unlockNow = () => {
|
||||
try {
|
||||
openLoading();
|
||||
let productID = props.videoInfo.id;
|
||||
if (!productID) {
|
||||
showToast('购买出错~');
|
||||
return;
|
||||
}
|
||||
VideoApi.videoBuying({ productType: ProductType.VIDEO, productID: productID })
|
||||
.then(async () => {
|
||||
showToast('购买成功');
|
||||
useUserStore().updateWallet();
|
||||
emit('buySuccess');
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.toString().indexOf('8000') != -1) {
|
||||
router.push({
|
||||
path: '/wallet',
|
||||
query: {
|
||||
id: props.videoInfo.id,
|
||||
title: props.videoInfo.title || '无标题',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
closeLoading();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
.content {
|
||||
width: 311px;
|
||||
height: 240px;
|
||||
background-color: #202133;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
padding: 18px;
|
||||
.title {
|
||||
color: #3476ff;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
.horizontalLine {
|
||||
height: 1px;
|
||||
background-color: rgb(54, 55, 70);
|
||||
margin: 18px 0;
|
||||
}
|
||||
.text {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
}
|
||||
.btnBox {
|
||||
margin-top: 18px;
|
||||
.inviteBtn {
|
||||
height: 42px;
|
||||
width: 112px;
|
||||
text-align: center;
|
||||
line-height: 42px;
|
||||
font-size: 16px;
|
||||
border-radius: 42px;
|
||||
color: #e0e0e0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.btn {
|
||||
height: 42px;
|
||||
width: 147px;
|
||||
text-align: center;
|
||||
line-height: 42px;
|
||||
font-size: 16px;
|
||||
border-radius: 42px;
|
||||
background: #3476ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
.startActivity {
|
||||
color: #ffcc14;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
padding: 12px 22px 0 0;
|
||||
}
|
||||
.countdown {
|
||||
opacity: 0.9;
|
||||
background: var(---, linear-gradient(135deg, #35debc 0%, #22bb9c 100%));
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
padding: 12px 22px 0 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<van-overlay :show="isShow" z-index="2001" @click.stop="emit('close')" :lock-scroll="false">
|
||||
<div class="wrapper flex-center-center">
|
||||
<div class="content">
|
||||
<div class="title">提示</div>
|
||||
<div class="horizontalLine"></div>
|
||||
<div class="text">开通限时预售 全站视频免费观看</div>
|
||||
<div class="text">查看更多会员特权</div>
|
||||
<div class="btnBox flex-center-between">
|
||||
<div class="inviteBtn" @click.stop="unlockNow">{{ gold }}金币支付</div>
|
||||
<div class="btn flex-center-center" @click.stop="$router.push('/presale')">前往活动</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import useLoading from '@/hooks/loading';
|
||||
import VideoApi from '@/service/video';
|
||||
import { computed } from 'vue';
|
||||
import { ProductType } from '@/enums/app';
|
||||
import { showToast } from 'vant';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const props = defineProps<{
|
||||
videoInfo: Types.Video.VInfosItem;
|
||||
isOpen: boolean;
|
||||
productType?: ProductType;
|
||||
}>();
|
||||
const userStore = useUserStore();
|
||||
const userInfo = userStore.userInfo;
|
||||
|
||||
const router = useRouter();
|
||||
const { openLoading, closeLoading } = useLoading();
|
||||
const emit = defineEmits(['close', 'buySuccess']);
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
const gold = computed(() => {
|
||||
return userInfo.isVip ? props.videoInfo.coins : props.videoInfo.originCoins;
|
||||
});
|
||||
const unlockNow = () => {
|
||||
try {
|
||||
openLoading();
|
||||
let productID = props.videoInfo.id;
|
||||
if (!productID) {
|
||||
showToast('购买出错~');
|
||||
return;
|
||||
}
|
||||
VideoApi.videoBuying({ productType: ProductType.VIDEO, productID: productID })
|
||||
.then(async () => {
|
||||
showToast('购买成功');
|
||||
useUserStore().updateWallet();
|
||||
emit('buySuccess');
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.toString().indexOf('8000') != -1) {
|
||||
router.push({
|
||||
path: '/wallet',
|
||||
query: {
|
||||
id: props.videoInfo.id,
|
||||
title: props.videoInfo.title || '无标题',
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
closeLoading();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
.content {
|
||||
width: 311px;
|
||||
height: 240px;
|
||||
background-color: #202133;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
padding: 18px;
|
||||
.title {
|
||||
color: #3476ff;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
.horizontalLine {
|
||||
height: 1px;
|
||||
background-color: rgb(54, 55, 70);
|
||||
margin: 18px 0;
|
||||
}
|
||||
.text {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
}
|
||||
.btnBox {
|
||||
margin-top: 18px;
|
||||
.inviteBtn {
|
||||
height: 42px;
|
||||
width: 112px;
|
||||
text-align: center;
|
||||
line-height: 42px;
|
||||
font-size: 16px;
|
||||
border-radius: 42px;
|
||||
color: #e0e0e0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.btn {
|
||||
height: 42px;
|
||||
width: 147px;
|
||||
text-align: center;
|
||||
line-height: 42px;
|
||||
font-size: 16px;
|
||||
border-radius: 42px;
|
||||
background: #3476ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<van-overlay :show="isShow" z-index="2001" @click.stop="emit('close')">
|
||||
<div class="wrapper flex-center-center">
|
||||
<div class="content">
|
||||
<div class="title">提示</div>
|
||||
<div class="horizontalLine"></div>
|
||||
<div class="text">开通限时预售 全站视频免费观看</div>
|
||||
<div class="text">查看更多会员特权</div>
|
||||
<div class="btnBox flex-center-between">
|
||||
<div class="inviteBtn" @click.stop="$router.push('/member')">会员中心</div>
|
||||
<div class="btn flex-center-center" @click.stop="$router.push('/presale')">前往活动</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
}>();
|
||||
const emit = defineEmits(['close']);
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
.content {
|
||||
width: 311px;
|
||||
height: 240px;
|
||||
background-color: #202133;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
padding: 18px;
|
||||
.title {
|
||||
color: #3476ff;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
.horizontalLine {
|
||||
height: 1px;
|
||||
background-color: rgb(54, 55, 70);
|
||||
margin: 18px 0;
|
||||
}
|
||||
.text {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
}
|
||||
.btnBox {
|
||||
margin-top: 18px;
|
||||
.inviteBtn {
|
||||
height: 42px;
|
||||
width: 112px;
|
||||
text-align: center;
|
||||
line-height: 42px;
|
||||
font-size: 16px;
|
||||
border-radius: 42px;
|
||||
color: #e0e0e0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.btn {
|
||||
height: 42px;
|
||||
width: 147px;
|
||||
text-align: center;
|
||||
line-height: 42px;
|
||||
font-size: 16px;
|
||||
border-radius: 42px;
|
||||
background: #3476ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="promote_box flex-vcenter">
|
||||
<lazyImg :src="userStore.userInfo.portrait" round class="promote_portrait" />
|
||||
<div class="promote_code">
|
||||
账号凭证
|
||||
<div>ID {{ userStore.userInfo.uid }}</div>
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="invit_quantity">提示:可通过凭证在设置找回账号</div>
|
||||
<div class="qr_code_bg">
|
||||
<div class="code_box flex-center-center">
|
||||
<qrcode-vue id="qrCode" class="code_img" v-if="state.qrUrl" :value="state.qrUrl" level="M" />
|
||||
<van-loading size="50px" type="spinner" color="#c0c9ea" v-else />
|
||||
</div>
|
||||
</div>
|
||||
<div class="promote_info">
|
||||
APP更新可能会导致会员账号掉线,
|
||||
<span>请勿删除此凭证</span>
|
||||
,此凭证是您永久有效的登陆途径。
|
||||
</div>
|
||||
<div class="promote_info">
|
||||
永久官方地址 <span class="guide">{{ configStore.sourceInfo.GUIDE }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, reactive } from 'vue';
|
||||
import UserApi from '@/service/user';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const configStore = useConfigStore();
|
||||
|
||||
const state = reactive({
|
||||
qrUrl: <string>'',
|
||||
});
|
||||
nextTick(async () => {
|
||||
let ret: any = await UserApi.queryCertificate();
|
||||
state.qrUrl = ret.content;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.promote_box {
|
||||
height: 474px;
|
||||
width: 296px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: url('@/assets/images/png/mine/figma/ticket-bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
margin: 0 auto 24px;
|
||||
padding: 27px 24px 0;
|
||||
box-sizing: border-box;
|
||||
.promote_portrait {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border: 4px solid rgba(243, 121, 82, 0.45);
|
||||
border-radius: 99px;
|
||||
margin-bottom: 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.promote_code {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
margin-bottom: 12px;
|
||||
div {
|
||||
margin-top: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
}
|
||||
.line {
|
||||
background: url('@/assets/images/png/mine/share_line.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
width: 248px;
|
||||
height: 1px;
|
||||
opacity: 0.3;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.invit_quantity {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #f37952;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr_code_bg {
|
||||
height: 120px;
|
||||
width: 120px;
|
||||
margin: 12px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: url('@/assets/images/png/frame.png') no-repeat;
|
||||
background-size: contain;
|
||||
.code_box {
|
||||
background-color: white;
|
||||
width: 108px;
|
||||
height: 108px;
|
||||
.code_img {
|
||||
width: 98px !important;
|
||||
height: 98px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.promote_info {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
width: 196px;
|
||||
> span {
|
||||
color: #f52c56;
|
||||
}
|
||||
.guide {
|
||||
font-weight: 500;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.promote_info + .promote_info {
|
||||
margin-top: 4px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<van-popup v-model:show="isShow">
|
||||
<div class="warp">
|
||||
<div class="content_warp">
|
||||
<div class="content">
|
||||
<p class="title">
|
||||
发布成功
|
||||
</p>
|
||||
<div class="mess">
|
||||
帖子发布成功后将在24小时内进行审核,您可在【我的】·【我的帖子】中进行查看
|
||||
</div>
|
||||
|
||||
<div class="btn_box">
|
||||
<div class="ISee flex-center-center" @click="emit('close')">确定</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.warp {
|
||||
color:#fff;
|
||||
.content_warp {
|
||||
width: 350px;
|
||||
border-radius: 10px;
|
||||
background: #23262f;
|
||||
.content {
|
||||
font-size: 12px;
|
||||
.title {
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
padding: 30px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.mess{
|
||||
width: 100%;
|
||||
font-size: 15px;
|
||||
padding: 0 30px;
|
||||
box-sizing: border-box;
|
||||
line-height: 20px;
|
||||
color: #a2a4ac;
|
||||
}
|
||||
}
|
||||
.btn_box {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
margin: 30px 0 0 0;
|
||||
padding-bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
.ISee {
|
||||
width: 205px;
|
||||
height: 35px;
|
||||
background: linear-gradient(to right,rgb( 132, 164, 249),rgb( 43, 93, 222));
|
||||
color: #fff;
|
||||
margin: 0 10px 0 0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.ISee:nth-child(2){
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="refreshing_warp">
|
||||
<van-pull-refresh
|
||||
v-model="params.refreshing"
|
||||
@refresh="onRefresh"
|
||||
:disabled="params.disabled"
|
||||
success-text="刷新成功"
|
||||
:class="[{ 'my-refresh': params.isHigehtMax, none: params.refreshing }, params.className]"
|
||||
:id="params.id"
|
||||
>
|
||||
<van-list
|
||||
v-if="!params.isNoData"
|
||||
v-model:loading="loadingc"
|
||||
:finished="params.finished"
|
||||
:finished-text="finishedText"
|
||||
@load="onLoad"
|
||||
:offset="100"
|
||||
v-model:error="params.error"
|
||||
:immediate-check="false"
|
||||
error-text="请求失败,点击重新加载"
|
||||
>
|
||||
<slot></slot>
|
||||
</van-list>
|
||||
<div class="sizeBox" v-if="!params.isNoData"></div>
|
||||
<Empty v-if="params.isNoData" />
|
||||
<!-- <div v-if="params.isNoData" class="dataNull">暂无数据</div> -->
|
||||
</van-pull-refresh>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, toRefs, watchEffect } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
finishedText?: string;
|
||||
params: {
|
||||
className?: string; //自定义class
|
||||
disabled?: boolean; //是否禁止下拉刷新 默认为false
|
||||
refreshing: boolean; //下拉刷新loading
|
||||
loading: boolean; //上滑刷新loading
|
||||
finished: boolean; //列表数据是否记载完毕
|
||||
isNoData: boolean; //数据是否为空
|
||||
error?: boolean; //请求错误
|
||||
/**
|
||||
* 占满父级100%
|
||||
* 组件内部 overflow-y:auto
|
||||
* 此参数设置为true的话,父级不要设置overflow-y:auto
|
||||
*/
|
||||
isHigehtMax?: boolean;
|
||||
id?: string; //自定义class
|
||||
};
|
||||
}>(),
|
||||
{
|
||||
finishedText: '没有更多数据了',
|
||||
},
|
||||
);
|
||||
const { params } = toRefs(props);
|
||||
|
||||
const loadingc = computed({
|
||||
get() {
|
||||
return params.value.loading;
|
||||
},
|
||||
set() {
|
||||
return params.value.loading;
|
||||
},
|
||||
});
|
||||
|
||||
let isNeedOnLoad = false;
|
||||
watchEffect(() => {
|
||||
if (!props.params.refreshing && isNeedOnLoad) {
|
||||
isNeedOnLoad = false;
|
||||
onLoad();
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['onLoad', 'onRefresh']);
|
||||
function onLoad() {
|
||||
if (props.params.refreshing) {
|
||||
isNeedOnLoad = true;
|
||||
return;
|
||||
}
|
||||
if (props.params.finished) return;
|
||||
emit('onLoad');
|
||||
}
|
||||
function onRefresh() {
|
||||
emit('onRefresh');
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
:deep(.van-loading__circular) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.none {
|
||||
:deep(.van-list) {
|
||||
.van-loading {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.van-loading__spinner) {
|
||||
background: url('../../assets/gif/loading.gif');
|
||||
width: 25px !important;
|
||||
height: 25px !important;
|
||||
background-size: cover;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.refreshing_warp {
|
||||
height: 100%;
|
||||
.my-refresh {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 11px 16px;
|
||||
}
|
||||
|
||||
.loading_warp {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.loading_box {
|
||||
width: 200px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.loading {
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.loading_text {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.sizeBox {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,764 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="recharge-box">
|
||||
<!-- 支付方式弹窗 -->
|
||||
<van-popup
|
||||
class="recharge-popup"
|
||||
v-model:show="isOpenRecharge"
|
||||
position="bottom"
|
||||
:z-index="zIndex"
|
||||
@close="emits('closePay')"
|
||||
>
|
||||
<div class="recharge-bg">
|
||||
<div class="recharge_line" />
|
||||
<div class="recharge_title flex-center-center">选择支付方式</div>
|
||||
<van-radio-group v-model="state.activeItem" direction="horizontal" class="rechargeList">
|
||||
<div v-for="item in state.rechargeList" :key="item.channel" @click.stop="selectPay(item)">
|
||||
<div class="rechargeItem" v-if="item.type !== 'daichong'">
|
||||
<SvgIcon class="payIcon" :name="item.type"></SvgIcon>
|
||||
<div class="payText">{{ item.typeName }}</div>
|
||||
<van-radio checked-color="#F68804" icon-size="16px" :name="item.type"></van-radio>
|
||||
</div>
|
||||
<!-- 代充 -->
|
||||
<div v-if="state.dcInfo.traders && state.dcInfo.traders.length > 0 && item.type === 'daichong'">
|
||||
<div
|
||||
v-for="dcItem in state.dcInfo.traders[0].payInfos"
|
||||
:key="dcItem.payMethod"
|
||||
@click="selectDcPay(item, dcItem)"
|
||||
>
|
||||
<div class="rechargeItem" v-if="item.type == 'daichong'">
|
||||
<SvgIcon class="payIcon" :name="getSvgPayIcon(dcItem.payMethod)"></SvgIcon>
|
||||
<div class="payText">
|
||||
{{ getPayName(dcItem.payMethod) }}
|
||||
<span>(人工充值)</span>
|
||||
</div>
|
||||
<van-radio checked-color="#F68804" icon-size="16px" :name="'dc' + dcItem.payMethod"></van-radio>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rechargeItem" v-if="state.popType == 'vip' && state.isAmountPay" @click="selectGoldPay">
|
||||
<div class="goldIcon"></div>
|
||||
<div class="payText">金币</div>
|
||||
<van-radio checked-color="#F68804" icon-size="16px" name="gold"></van-radio>
|
||||
</div>
|
||||
</van-radio-group>
|
||||
<div class="couponBox" v-if="state.couponList.length > 0">
|
||||
<div class="couponTitle">加赠券</div>
|
||||
<van-list
|
||||
class="couponList"
|
||||
v-model="state.loading"
|
||||
:finished="state.finished"
|
||||
:offset="10"
|
||||
v-if="state.couponList.length > 0"
|
||||
>
|
||||
<div class="coupon-item" v-for="couponItem in state.couponList" :key="couponItem.id">
|
||||
<div class="coupon-title">{{ couponItem.source }}</div>
|
||||
<div class="coupon-amount">{{ couponItem.amount }}金币</div>
|
||||
<div class="coupon-selset">
|
||||
<van-radio-group v-model="state.radioValue">
|
||||
<van-radio
|
||||
:name="couponItem.id"
|
||||
checked-color="#E1351F"
|
||||
icon-size="16px"
|
||||
@click="clickRadio(couponItem.id)"
|
||||
></van-radio>
|
||||
</van-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
</div>
|
||||
<div class="subTitle">支付小贴士:</div>
|
||||
<div class="payTip">1.因超时支付无法到账,请重新发起。</div>
|
||||
<div class="payTip">2.每天发起支付不能超过5次,连续发起且未支付,账号可能被加入黑名单。</div>
|
||||
<div class="payBtn" @click="toPay">¥{{ state.discountedPrice }}/{{ payBtnLabel }}</div>
|
||||
<div class="customerService">
|
||||
支付中如有问题,请咨询
|
||||
<router-link class="contactCustomer" to="/customerService">在线客服</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<!-- 支付提示(暂时省去该流程) -->
|
||||
<!-- <van-popup class="kindTips" v-model:show="state.showKindTips">
|
||||
<div class="kindTipsBox">
|
||||
<div class="title flex-center-center">温馨提示</div>
|
||||
<div class="contents">
|
||||
<p>1.订单支付后,一般会在1-10分钟内到账,如超时未到账,请联系在线客服为您处理。</p>
|
||||
<p>2.受特殊行业限制,如支付失败可尝试重新发起订单,系统将会随机切换备用的支付通道。</p>
|
||||
<p>3.本APP有稳定的广告收益,产品稳定安全,请放心充值使用,如遇报毒提示忽略即可。</p>
|
||||
</div>
|
||||
<div class="messbg"></div>
|
||||
<div class="cBtn" @click="comfinPay" v-if="!state.isPayLoading">确认支付</div>
|
||||
<div class="cBtn" v-else>
|
||||
<van-loading size="20px" type="spinner" color="rgb(255,255,255)">支付中...</van-loading>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup> -->
|
||||
|
||||
<!-- 订单确认弹窗 -->
|
||||
<van-popup class="payTipPop" v-model:show="state.showPayTip">
|
||||
<div class="payTipBox app-popup-surface">
|
||||
<div class="paySubTitle app-popup-title">支付确认</div>
|
||||
<div class="tipItem">1.订单支付后,一般会在3-5分钟内到账,如超时未到账,请联系在线客服为您处理。</div>
|
||||
<div class="tipItem">2.受特殊行业限制,如支付失败可尝试重新发起订单,系统将会随机切换备用的支付通道。</div>
|
||||
<div class="tipItem">3.本APP有稳定的广告收益,产品稳定安全,请放心充值使用。</div>
|
||||
<div class="question flex-center-center" @click="$router.push('paymentIssues')">
|
||||
支付遇到问题
|
||||
<SvgIcon name="greyRightArrow"></SvgIcon>
|
||||
</div>
|
||||
<div class="confirmBtn app-popup-primary" @click="confirmPaySuccess">支付成功</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import MineApi from '@/service/mine';
|
||||
import { Buffer } from 'buffer';
|
||||
import { showToast } from 'vant';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { compatibleAppStoreJump } from '@/utils/index';
|
||||
import { refreshPaymentGuideConfig } from '@/utils/paymentGuide';
|
||||
import { refreshPaymentPopupConfig } from '@/utils/paymentStatusPopup';
|
||||
const route = useRoute();
|
||||
const userStore = useUserStore();
|
||||
const userInfo = userStore.userInfo;
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
isOpen: boolean;
|
||||
color?: string;
|
||||
zIndex?: number;
|
||||
}>(),
|
||||
{
|
||||
zIndex: 2000,
|
||||
},
|
||||
);
|
||||
const emits = defineEmits(['closePay', 'paySuccess']);
|
||||
const isOpenRecharge = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
const state = reactive({
|
||||
rechargeList: <Array<Types.Recharge.RchgTypeItem>>[], // 支付方式列表
|
||||
activeItem: '', // 支付方式选中
|
||||
dcInfo: <Types.Recharge.Daichong>{}, // 代充信息
|
||||
dcType: 1, // 代充商品类型 会员-1 金币-0
|
||||
popType: 'vip', // 弹窗类型 会员卡-vip 金币-gold
|
||||
payMethodType: 'dc', // 支付方式类型 支付渠道-channel 代充-dc 金币兑换-gold
|
||||
productType: '', // 商品类型
|
||||
productID: '', // 商品ID
|
||||
buyType: '', // 商品类型
|
||||
discountedPrice: 0, // 充值金额
|
||||
payInfo: <any>{}, // 支付渠道信息
|
||||
rchgType: <any>{}, // 充值类型
|
||||
dcPayInfo: <any>{}, // 代充支付信息
|
||||
showPayTip: false, // 支付提示弹窗状态
|
||||
showKindTips: false, // 温馨提示
|
||||
isAmountPay: false, // 是否支持金币兑换
|
||||
graphicsId: '', // 埋点ID
|
||||
graphicsTitle: '', // 埋点标题
|
||||
isOpenRecharge: false,
|
||||
couponList: <Array<Types.Recharge.CouponItem>>[], // 金币加购券列表
|
||||
pageNumber: 1,
|
||||
loading: false,
|
||||
finished: false,
|
||||
radioValue: '',
|
||||
curRadioValue: '', // 金币加购券
|
||||
show: false,
|
||||
isPayLoading: false, // 支付加载中
|
||||
finalPayStatus: <any>'no', //是否支付尾款
|
||||
isUpgrade: false, // 当前会员卡是否补差价升级
|
||||
payTracking: <Types.Recharge.PayTrackingFields>{}, // 下单来源 / A/B 追踪字段
|
||||
});
|
||||
|
||||
const payBtnLabel = computed(() => {
|
||||
if (state.popType === 'vip' && state.isUpgrade) {
|
||||
return '补差价升级';
|
||||
}
|
||||
return '立即支付';
|
||||
});
|
||||
// 查询优惠券
|
||||
const queryCouponList = async () => {
|
||||
let req = {
|
||||
pageSize: '50',
|
||||
pageNumber: state.pageNumber,
|
||||
int: 1, // 0-全部 1-有效
|
||||
};
|
||||
state.loading = true;
|
||||
try {
|
||||
let res: any = await MineApi.getCouponList(req);
|
||||
|
||||
state.couponList = res.list.filter((item: any) => {
|
||||
if (new Date(item.expired).getTime() > new Date().getTime() && !item.used) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
state.finished = true;
|
||||
setTimeout(() => {
|
||||
state.loading = false;
|
||||
}, 200);
|
||||
} catch (error) {
|
||||
state.loading = false;
|
||||
state.finished = true;
|
||||
}
|
||||
};
|
||||
const init = () => {
|
||||
if (state.popType == 'gold') {
|
||||
queryCouponList();
|
||||
}
|
||||
};
|
||||
init();
|
||||
/**
|
||||
* @description 显示支付方式弹窗
|
||||
* @param rechargeList - 支付方式列表
|
||||
* @param dcInfo - 代充信息
|
||||
* @param popType - 弹窗类型 会员卡-vip 金币-gold
|
||||
*/
|
||||
const changeItem = (payData: any) => {
|
||||
state.rechargeList = payData.rechargeList;
|
||||
if (payData.dcInfo) {
|
||||
state.dcInfo = payData.dcInfo;
|
||||
}
|
||||
state.popType = payData.popType;
|
||||
state.dcType = payData.dcType;
|
||||
state.productType = payData.productType;
|
||||
state.productID = payData.productID;
|
||||
state.discountedPrice = payData.discountedPrice;
|
||||
state.buyType = payData.buyType;
|
||||
state.isAmountPay = payData.isAmountPay;
|
||||
state.isUpgrade = !!payData.isUpgrade;
|
||||
const trackingEntries = Object.entries({
|
||||
sourcePage: payData.sourcePage || 'UNKNOWN',
|
||||
sourceRef: payData.sourceRef,
|
||||
videoId: payData.videoId,
|
||||
mediaId: payData.mediaId,
|
||||
contentId: payData.contentId,
|
||||
activityId: payData.activityId,
|
||||
experimentId: payData.experimentId,
|
||||
experimentVariant: payData.experimentVariant,
|
||||
sessionId: payData.sessionId,
|
||||
}).filter(([, value]) => value !== undefined && value !== '');
|
||||
state.payTracking = Object.fromEntries(trackingEntries) as Types.Recharge.PayTrackingFields;
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
state.finalPayStatus = payData.hasOwnProperty('finalPayStatus') ? payData.finalPayStatus : 'no';
|
||||
let dcArr = state.rechargeList.filter((i: any) => {
|
||||
return i.type == 'daichong';
|
||||
});
|
||||
let channel = state.rechargeList.filter((i: any) => {
|
||||
return i.type != 'daichong';
|
||||
});
|
||||
if (channel.length > 0) {
|
||||
selectPay(channel[0]);
|
||||
} else if (dcArr.length > 0 && state.dcInfo.traders.length > 0 && state.dcInfo.traders[0].payInfos.length > 0) {
|
||||
let arr = state.rechargeList.filter((i: any) => {
|
||||
return i.type == 'daichong';
|
||||
});
|
||||
selectDcPay(arr[0], state.dcInfo.traders[0].payInfos[0]);
|
||||
} else {
|
||||
if (state.isAmountPay) {
|
||||
selectGoldPay();
|
||||
}
|
||||
}
|
||||
};
|
||||
// 选中支付方式
|
||||
const selectPay = (item: any) => {
|
||||
if (item.type != 'daichong') {
|
||||
state.payInfo = item;
|
||||
state.rchgType = {};
|
||||
state.dcPayInfo = {};
|
||||
state.payMethodType = 'channel';
|
||||
state.activeItem = item.type;
|
||||
}
|
||||
};
|
||||
// 选中代充支付方式
|
||||
const selectDcPay = (rchgType: any, dcPayInfo: Types.Recharge.Daichong) => {
|
||||
state.payInfo = {};
|
||||
state.rchgType = rchgType;
|
||||
state.dcPayInfo = dcPayInfo;
|
||||
state.payMethodType = 'dc';
|
||||
state.activeItem = 'dc' + state.dcPayInfo.payMethod;
|
||||
};
|
||||
// 选中金币兑换
|
||||
const selectGoldPay = () => {
|
||||
state.payInfo = { typeName: '金币' };
|
||||
state.rchgType = {};
|
||||
state.dcPayInfo = {};
|
||||
state.payMethodType = 'gold';
|
||||
state.activeItem = 'gold';
|
||||
};
|
||||
// 点击金币加购券
|
||||
const clickRadio = (id: any) => {
|
||||
if (state.curRadioValue === id) {
|
||||
state.radioValue = '';
|
||||
state.curRadioValue = '';
|
||||
} else {
|
||||
state.curRadioValue = id;
|
||||
}
|
||||
};
|
||||
// 立即支付
|
||||
const toPay = () => {
|
||||
if (!state.rechargeList.length && state.payMethodType != 'gold') return showToast('暂无支付方式');
|
||||
if (state.payMethodType == 'gold') {
|
||||
goldExchange();
|
||||
return;
|
||||
}
|
||||
// 暂时省去温馨提示流程,直接发起支付
|
||||
// state.showKindTips = true;
|
||||
comfinPay();
|
||||
};
|
||||
const comfinPay = () => {
|
||||
state.isPayLoading = true;
|
||||
if (state.payMethodType == 'channel') {
|
||||
channelPay();
|
||||
} else if (state.payMethodType == 'dc') {
|
||||
dcToPay();
|
||||
}
|
||||
};
|
||||
const onPurchaseSuccess = async () => {
|
||||
emits('paySuccess');
|
||||
emits('closePay');
|
||||
await Promise.all([refreshPaymentPopupConfig(), refreshPaymentGuideConfig()]);
|
||||
};
|
||||
|
||||
const confirmPaySuccess = async () => {
|
||||
state.showPayTip = false;
|
||||
await onPurchaseSuccess();
|
||||
};
|
||||
|
||||
// 金币兑换(会员购买时附带 A/B 实验追踪字段,与 /mine/topay 对齐)
|
||||
const goldExchange = async () => {
|
||||
const { experimentId, experimentVariant, sessionId } = state.payTracking;
|
||||
const trackingEntries = Object.entries({ experimentId, experimentVariant, sessionId }).filter(
|
||||
([, value]) => value !== undefined && value !== '',
|
||||
);
|
||||
let req: Record<string, unknown> = {
|
||||
productType: state.productType,
|
||||
productID: state.productID,
|
||||
isH5: true,
|
||||
...Object.fromEntries(trackingEntries),
|
||||
};
|
||||
if (state.finalPayStatus != 'no') {
|
||||
Object.assign(req, { finalPayStatus: state.finalPayStatus });
|
||||
}
|
||||
try {
|
||||
userStore.updateWallet();
|
||||
let walletInfo = userStore.walletInfo;
|
||||
let walletBalance = ((walletInfo.amount || 0) + (walletInfo.income || 0)).toFixed(2);
|
||||
if (state.discountedPrice * 10 > Number(walletBalance)) {
|
||||
showToast('金币不足请充值');
|
||||
return;
|
||||
}
|
||||
await MineApi.goldExchange(req);
|
||||
showToast('购买成功');
|
||||
await onPurchaseSuccess();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
const goToPayUrl = (payUrl: string) => {
|
||||
state.show = false;
|
||||
state.showKindTips = false;
|
||||
state.isPayLoading = false;
|
||||
emits('closePay');
|
||||
if (localStorage.getItem('isAppStore')) {
|
||||
window.webkit?.messageHandlers?.getMessage?.postMessage({
|
||||
id: 'openOutLink',
|
||||
data: { url: payUrl },
|
||||
});
|
||||
} else {
|
||||
window.location.href = payUrl;
|
||||
}
|
||||
};
|
||||
|
||||
// 支付渠道支付
|
||||
const channelPay = async () => {
|
||||
state.show = true;
|
||||
let req: Types.Recharge.PayTrackingFields & Record<string, unknown> = {
|
||||
uid: userInfo.uid,
|
||||
rechargeType: state.payInfo.type,
|
||||
productID: state.productID,
|
||||
buyType: state.buyType,
|
||||
goldExtraID: state.curRadioValue ? state.curRadioValue : undefined,
|
||||
...state.payTracking,
|
||||
};
|
||||
if (state.finalPayStatus != 'no') {
|
||||
Object.assign(req, { finalPayStatus: state.finalPayStatus });
|
||||
}
|
||||
try {
|
||||
let ret: any = await MineApi.getPay(req);
|
||||
state.payInfo = {};
|
||||
state.curRadioValue = '';
|
||||
state.finalPayStatus = 'no';
|
||||
goToPayUrl(ret.payUrl);
|
||||
} catch (e) {
|
||||
state.isPayLoading = false;
|
||||
state.show = false;
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
// 代充
|
||||
const dcToPay = () => {
|
||||
state.show = true;
|
||||
let payMoney = state.discountedPrice;
|
||||
if (state.finalPayStatus != 'no') {
|
||||
Object.assign(state.dcInfo, { finalPayStatus: state.finalPayStatus });
|
||||
}
|
||||
let dcData = JSON.parse(JSON.stringify(state.dcInfo)); // 代充信息
|
||||
let payInfoModel = JSON.parse(JSON.stringify(state.dcPayInfo)); // 代充支付类型
|
||||
let channel = state.rchgType.channel;
|
||||
let productInfo = {
|
||||
id: state.productID,
|
||||
type: state.dcType,
|
||||
version: '3.0.0',
|
||||
};
|
||||
//设置默认值
|
||||
if (dcData.limit == 0) {
|
||||
dcData.limit = 500;
|
||||
}
|
||||
if (payInfoModel.payType.includes(2) && payInfoModel.payType.includes(3)) {
|
||||
if (payMoney > dcData.limit) {
|
||||
payInfoModel.payType = [3];
|
||||
} else {
|
||||
payInfoModel.payType = [2];
|
||||
}
|
||||
}
|
||||
if (payInfoModel.payType.length >= 3) {
|
||||
if (payMoney > dcData.limit) {
|
||||
payInfoModel.payType = [1, 3];
|
||||
} else {
|
||||
payInfoModel.payType = [1, 2];
|
||||
}
|
||||
}
|
||||
dcData.traders[0].payInfos = [payInfoModel];
|
||||
dcData.productInfo = productInfo;
|
||||
dcData.chargeMoney = payMoney;
|
||||
dcData.channel = channel;
|
||||
let models = JSON.stringify(dcData);
|
||||
state.isPayLoading = false;
|
||||
let payUrl = `${channel}/?data=${encodeBase64(models)}&token=${userStore.token}`;
|
||||
state.finalPayStatus = 'no';
|
||||
goToPayUrl(payUrl);
|
||||
};
|
||||
// 转base64
|
||||
const encodeBase64 = (str: any) => {
|
||||
let base64Str = Buffer.from(str, 'utf-8').toString('base64');
|
||||
return base64Str;
|
||||
};
|
||||
// 获取支付方式名称
|
||||
const getPayName = (type: any) => {
|
||||
switch (type) {
|
||||
case 'alipay':
|
||||
return '支付宝';
|
||||
case 'wechat':
|
||||
return '微信';
|
||||
case 'union':
|
||||
return '银联';
|
||||
case 101:
|
||||
return '支付宝';
|
||||
case 102:
|
||||
return '微信';
|
||||
case 103:
|
||||
return '银行卡';
|
||||
case 104:
|
||||
return '信用卡';
|
||||
case 105:
|
||||
return '花呗';
|
||||
case 106:
|
||||
return '云闪付';
|
||||
case 107:
|
||||
return 'QQ钱包';
|
||||
case 108:
|
||||
return '京东支付';
|
||||
default:
|
||||
return '金币兑换';
|
||||
}
|
||||
};
|
||||
// 获取支付方式图标
|
||||
const getSvgPayIcon = (type: any) => {
|
||||
switch (type) {
|
||||
case 'alipay':
|
||||
return 'alipay';
|
||||
case 'wechat':
|
||||
return 'wechat';
|
||||
case 'union':
|
||||
return 'bankcard';
|
||||
case 101:
|
||||
return 'alipay';
|
||||
case 102:
|
||||
return 'wechat';
|
||||
case 103:
|
||||
return 'bankcard';
|
||||
case 104:
|
||||
return 'bankcard';
|
||||
case 105:
|
||||
return 'alipay';
|
||||
case 106:
|
||||
return 'bankcard';
|
||||
case 107:
|
||||
return 'bankcard';
|
||||
case 108:
|
||||
return 'bankcard';
|
||||
default:
|
||||
return 'gold';
|
||||
}
|
||||
};
|
||||
defineExpose({
|
||||
changeItem,
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.van-overlay) {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
:deep(.van-popup) {
|
||||
background-color: transparent;
|
||||
}
|
||||
.recharge-box {
|
||||
.recharge-popup {
|
||||
.recharge-bg {
|
||||
padding-top: 20px;
|
||||
border-radius: 18px 18px 0 0;
|
||||
color: white;
|
||||
background: #0f0f0f;
|
||||
.recharge_line {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
background: #ffffff33;
|
||||
border-radius: 99px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.recharge_title {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
height: 65px;
|
||||
}
|
||||
.payMethod {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
span {
|
||||
font-size: 16px;
|
||||
color: #fb415c;
|
||||
}
|
||||
}
|
||||
.title {
|
||||
font-size: 12px;
|
||||
color: #aab2b7;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.rechargeList {
|
||||
max-height: 310px;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 8px 20px 8px 42px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.rechargeItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 8px 0;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
.payIcon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.goldIcon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: url('@/assets/images/png/goldIcon.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.payText {
|
||||
flex: 2;
|
||||
font-size: 14px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
.couponBox {
|
||||
padding: 0 30px;
|
||||
box-sizing: border-box;
|
||||
height: 90px;
|
||||
.couponTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: $titleColor;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.couponList {
|
||||
height: 60px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.coupon-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
.coupon-title {
|
||||
font-size: 14px;
|
||||
color: $greyDeepColor;
|
||||
}
|
||||
.coupon-amount {
|
||||
flex: 2;
|
||||
text-align: right;
|
||||
margin: 0 10px;
|
||||
font-size: 14px;
|
||||
color: $titleColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
.subTitle {
|
||||
padding: 0 42px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.payTip {
|
||||
padding: 0 42px;
|
||||
box-sizing: border-box;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
.payBtn {
|
||||
background: #ffc000;
|
||||
color: #270000;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
width: 343px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 43px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
.customerService {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
padding-bottom: 30px;
|
||||
.contactCustomer {
|
||||
color: #ffc000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.van-popup) {
|
||||
overflow-y: visible;
|
||||
}
|
||||
.kindTips {
|
||||
border-radius: 8px;
|
||||
.kindTipsBox {
|
||||
width: 311px;
|
||||
height: 486px;
|
||||
background: url('@/assets/images/png/recharge-bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
border-radius: 12px;
|
||||
padding: 32px 24px;
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
margin-bottom: 6px;
|
||||
background: linear-gradient(91.16deg, #fff0c2 35.22%, #ffffff 64.61%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.horizontalLine {
|
||||
height: 1px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.contents {
|
||||
font-size: 14px;
|
||||
padding: 10px 0px;
|
||||
line-height: 24px;
|
||||
p {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
.messbg {
|
||||
width: 271px;
|
||||
height: 101px;
|
||||
margin: 0 auto 22px;
|
||||
background: url('@/assets/images/png/messbg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.cBtn {
|
||||
width: 263px;
|
||||
height: 44px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
border-radius: 43px;
|
||||
margin: 0 auto;
|
||||
background: #ffc000;
|
||||
text-align: center;
|
||||
line-height: 44px;
|
||||
color: #270000;
|
||||
}
|
||||
.close {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: 0;
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.payTipPop {
|
||||
.payTipBox {
|
||||
height: 361px;
|
||||
width: 303px;
|
||||
background-color: rgb(34, 34, 34);
|
||||
border-radius: 8px;
|
||||
padding: 14px 12px;
|
||||
.paySubTitle {
|
||||
font-size: 18px;
|
||||
color: rgb(251, 219, 165);
|
||||
text-align: center;
|
||||
}
|
||||
.tipItem {
|
||||
font-size: 12px;
|
||||
color: rgb(255, 255, 255);
|
||||
line-height: 25px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.question {
|
||||
font-size: 12px;
|
||||
color: rgb(153, 153, 153);
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
> svg {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
.confirmBtn {
|
||||
width: 279px;
|
||||
height: 41px;
|
||||
font-size: 14px;
|
||||
border-radius: 41px;
|
||||
margin: 25px auto 0;
|
||||
color: rgb(0, 0, 0);
|
||||
background: linear-gradient(to right, rgb(253, 239, 165), rgb(249, 180, 91));
|
||||
text-align: center;
|
||||
line-height: 41px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="recommendedList">
|
||||
<van-tabs
|
||||
v-model:active="state.tabActive"
|
||||
class="oepnX"
|
||||
title-active-color="rgba(255, 255, 255, 0.90)"
|
||||
title-inactive-color="rgba(255, 255, 255, 0.55)"
|
||||
:scrollspy="false"
|
||||
:swipeable="false"
|
||||
:lazy-render="true"
|
||||
>
|
||||
<van-tab :name="index" :title="item.label" v-for="(item, index) in tabList" :key="item.value">
|
||||
<Video
|
||||
:type="type"
|
||||
:tagId="tagId"
|
||||
:currentType="item.value"
|
||||
v-if="index == state.tabActive"
|
||||
@changeVideo="
|
||||
(videoInfo) => {
|
||||
emit('changeVideo', videoInfo);
|
||||
}
|
||||
"
|
||||
/>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { reactive, computed } from 'vue';
|
||||
import Video from './video.vue';
|
||||
const emit = defineEmits(['changeVideo']);
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type: string;
|
||||
tagId?: string;
|
||||
}>(),
|
||||
{
|
||||
type: 'SP',
|
||||
},
|
||||
);
|
||||
const state = reactive({
|
||||
tabActive: 0,
|
||||
});
|
||||
const tabList = computed(() => {
|
||||
if (props.type == 'video') {
|
||||
return [
|
||||
{
|
||||
label: '动漫推荐',
|
||||
value: 'video',
|
||||
},
|
||||
{
|
||||
label: '视频推荐',
|
||||
value: 'SP',
|
||||
},
|
||||
{
|
||||
label: '漫画推荐',
|
||||
value: 'image',
|
||||
},
|
||||
];
|
||||
}
|
||||
if (props.type == 'image') {
|
||||
return [
|
||||
{
|
||||
label: '漫画推荐',
|
||||
value: 'image',
|
||||
},
|
||||
{
|
||||
label: '视频推荐',
|
||||
value: 'SP',
|
||||
},
|
||||
{
|
||||
label: '动漫推荐',
|
||||
value: 'video',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
label: '视频推荐',
|
||||
value: 'SP',
|
||||
},
|
||||
{
|
||||
label: '动漫推荐',
|
||||
value: 'video',
|
||||
},
|
||||
{
|
||||
label: '漫画推荐',
|
||||
value: 'image',
|
||||
},
|
||||
];
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.recommendedList {
|
||||
height: calc(100vh - 250px);
|
||||
--van-tabs-line-height: 44px;
|
||||
:deep(.van-tabs) {
|
||||
.van-tabs__wrap {
|
||||
position: sticky;
|
||||
top: -1px;
|
||||
background-color: var(--app-background);
|
||||
border-bottom: 0 none;
|
||||
padding: 0 10px;
|
||||
z-index: 1;
|
||||
.van-tabs__nav {
|
||||
background: unset;
|
||||
}
|
||||
.van-tab {
|
||||
flex: 1 !important;
|
||||
width: unset !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
.van-tabs__line {
|
||||
width: 13px;
|
||||
height: 3px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(90deg, rgba(255, 192, 0, 0) 0%, var(--app-primary) 100%);
|
||||
bottom: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="videos">
|
||||
<PullRefresh :params="moreParams" @onLoad="onLoad" @onRefresh="onRefresh">
|
||||
<div class="moreMoviesBox" :class="['video', 'image'].includes(currentType) ? 'moreAcgBox' : ''">
|
||||
<HorizontalVideoBox
|
||||
class="videoBox"
|
||||
:videoList="state.list"
|
||||
:videoInfo="item"
|
||||
:isMovie="type == 'SP' && type == currentType"
|
||||
v-for="item in state.list"
|
||||
:key="item.id"
|
||||
:showTimeAndComment="currentType == 'SP'"
|
||||
@changeVideo="changeVideo"
|
||||
/>
|
||||
</div>
|
||||
</PullRefresh>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import VideoApi from '@/service/video';
|
||||
import AcgApi from '@/service/acg';
|
||||
import { reactive } from 'vue';
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type: string;
|
||||
tagId?: string;
|
||||
currentType: string;
|
||||
}>(),
|
||||
{
|
||||
type: 'SP',
|
||||
},
|
||||
);
|
||||
const state = reactive({
|
||||
tabActive: 0,
|
||||
pageNumber: 1,
|
||||
pageSize: 10,
|
||||
list: <any>[],
|
||||
});
|
||||
const moreParams = reactive({
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
finished: false,
|
||||
isNoData: false,
|
||||
error: false,
|
||||
isHigehtMax: false,
|
||||
});
|
||||
const emit = defineEmits(['changeVideo']);
|
||||
const changeVideo = (videoInfo: any) => {
|
||||
emit('changeVideo', videoInfo);
|
||||
};
|
||||
// 获取更多视频
|
||||
const getList = async () => {
|
||||
try {
|
||||
let req: any = {
|
||||
pageNumber: state.pageNumber,
|
||||
pageSize: state.pageSize,
|
||||
};
|
||||
let ajax: any = VideoApi.queryRecommend;
|
||||
if (props.currentType == 'video' || props.currentType == 'image') {
|
||||
req.mediaType = props.currentType;
|
||||
if (props.type == props.currentType) {
|
||||
req.tagId = props.tagId || null;
|
||||
}
|
||||
ajax = AcgApi.getMediaRecommend;
|
||||
} else {
|
||||
req.newsType = props.currentType;
|
||||
if (props.type == 'SP') {
|
||||
req.tagId = props.tagId || null;
|
||||
}
|
||||
}
|
||||
|
||||
let res: any = await ajax(req);
|
||||
let list = res.list || [];
|
||||
if (state.pageNumber == 1) {
|
||||
state.list = list;
|
||||
} else {
|
||||
state.list = state.list.concat(list);
|
||||
}
|
||||
if (state.pageNumber == 1 && state.list.length == 0) {
|
||||
moreParams.isNoData = true;
|
||||
}
|
||||
if (!res.hasNext) {
|
||||
moreParams.finished = true;
|
||||
}
|
||||
} catch (e) {
|
||||
moreParams.error = true;
|
||||
} finally {
|
||||
moreParams.loading = false;
|
||||
moreParams.refreshing = false;
|
||||
}
|
||||
};
|
||||
const onLoad = () => {
|
||||
if (moreParams.refreshing) return;
|
||||
moreParams.loading = true;
|
||||
state.pageNumber++;
|
||||
getList();
|
||||
};
|
||||
const onRefresh = () => {
|
||||
state.pageNumber = 1;
|
||||
moreParams.refreshing = true;
|
||||
moreParams.finished = false;
|
||||
moreParams.isNoData = false;
|
||||
getList();
|
||||
};
|
||||
const init = () => {
|
||||
getList();
|
||||
};
|
||||
init();
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.videos {
|
||||
.moreMoviesBox {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-column-gap: 7px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
.videoBox {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
:deep(.coverBox) {
|
||||
height: 93px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.moreAcgBox {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-column-gap: 5px;
|
||||
.videoBox {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
:deep(.coverBox) {
|
||||
height: 148px;
|
||||
}
|
||||
:deep(.videoTitleBox) {
|
||||
.videoTitle {
|
||||
height: 20px;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="rich-preview">
|
||||
<div ref="content" class="content" v-html="html" @click="onElementClick"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, ref } from 'vue';
|
||||
import AppApi from '@/service/app';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import placeholderImage from '@/assets/images/png/placeholder.png';
|
||||
import { jumpAdv } from '@/utils';
|
||||
|
||||
function isFullURL(text: string) {
|
||||
return /^(http(s)?:\/\/)\w+[^\s]+(\.[^\s]+){1,}$/.test(text);
|
||||
}
|
||||
|
||||
const props = defineProps<{ text?: string }>();
|
||||
const configStore = useConfigStore();
|
||||
|
||||
const html = computed(() => {
|
||||
nextTick(handleDynamicElements);
|
||||
return replaceImageSrc(props.text || '');
|
||||
});
|
||||
|
||||
const content = ref<HTMLElement>();
|
||||
const handleDynamicElements = () => {
|
||||
const imgs = content.value?.getElementsByTagName('img');
|
||||
observeImages(imgs);
|
||||
};
|
||||
|
||||
const onElementClick = (e: Event) => {
|
||||
const ele = e.target as HTMLElement;
|
||||
const aTag = findNearestElement('a', ele);
|
||||
if (aTag) {
|
||||
const href = aTag.getAttribute('href');
|
||||
jumpAdv({ href });
|
||||
}
|
||||
};
|
||||
|
||||
const findNearestElement = (nodeName: string, element: HTMLElement | null) => {
|
||||
const upperNodeName = nodeName.toUpperCase();
|
||||
if (!element) return null;
|
||||
if (element.nodeName === upperNodeName) return element;
|
||||
return findNearestElement(upperNodeName, element.parentElement);
|
||||
};
|
||||
|
||||
// 缓存监听的元素
|
||||
const observedElements: HTMLElement[] = [];
|
||||
const observeImages = (imgs?: HTMLCollectionOf<HTMLImageElement>) => {
|
||||
// 先移除原来的监听
|
||||
unobserveImages();
|
||||
if (!imgs) return;
|
||||
for (let index = 0; index < imgs.length; index++) {
|
||||
const element = imgs[index];
|
||||
observer.observe(element);
|
||||
observedElements.push(element);
|
||||
}
|
||||
};
|
||||
const unobserveImages = () => {
|
||||
observedElements.forEach((v) => observer.unobserve(v));
|
||||
observedElements.splice(0);
|
||||
};
|
||||
const unobserveImage = (img: HTMLElement) => {
|
||||
observer.unobserve(img);
|
||||
const idx = observedElements.indexOf(img);
|
||||
if (idx >= 0) {
|
||||
observedElements.splice(idx, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const unobserveEntry = (entry: IntersectionObserverEntry) => {
|
||||
unobserveImage(entry.target as HTMLElement);
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unobserveImages();
|
||||
observer.disconnect();
|
||||
});
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
function (entries) {
|
||||
entries.forEach((entry) => {
|
||||
observeEntry(entry);
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1 },
|
||||
);
|
||||
|
||||
const observeEntry = async (entry: IntersectionObserverEntry) => {
|
||||
const { intersectionRatio } = entry;
|
||||
if (intersectionRatio <= 0) return;
|
||||
const target = entry.target as HTMLElement;
|
||||
const tempSrc = target.getAttribute('temp-src');
|
||||
if (!tempSrc) {
|
||||
// 没有临时路径,清除监听
|
||||
unobserveEntry(entry);
|
||||
return;
|
||||
}
|
||||
// 是完整url,无需解密直接src加载
|
||||
if (isFullURL(tempSrc)) {
|
||||
target.style.height = target.style.height || 'auto';
|
||||
target.setAttribute('src', tempSrc);
|
||||
target.removeAttribute('temp-src');
|
||||
unobserveEntry(entry);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const url = await AppApi.imgDecypt(configStore.sourceInfo.IMAGE + '/' + tempSrc);
|
||||
target.style.height = target.style.height || 'auto';
|
||||
target.setAttribute('src', url);
|
||||
target.removeAttribute('temp-src');
|
||||
unobserveEntry(entry);
|
||||
} catch (error) {
|
||||
console.error('img decypt error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const replaceImageSrc = (htmlContent: string) => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlContent, 'text/html');
|
||||
const images = doc.getElementsByTagName('img');
|
||||
for (const img of Array.from(images)) {
|
||||
let imgSrc = img.getAttribute('src') || '';
|
||||
img.setAttribute('src', placeholderImage);
|
||||
img.setAttribute('temp-src', imgSrc);
|
||||
}
|
||||
return doc.documentElement.outerHTML;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rich-preview {
|
||||
.content {
|
||||
margin: 0 0 10px 0;
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
font-weight: 400;
|
||||
white-space: pre-line;
|
||||
|
||||
:deep() {
|
||||
img {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<van-overlay :show="isShow" z-index="2001">
|
||||
<div class="wrapper flex-center-center">
|
||||
<div class="content">
|
||||
<p>充值VIP,获取会员线路</p>
|
||||
<div class="name">蜜桃传媒</div>
|
||||
<div class="close" @click="emit('close')">
|
||||
<img src="@/assets/images/png/pop_close.png" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="btnBox flex-center-between">
|
||||
<div class="btn" @click="$router.push('/share')">邀请好友</div>
|
||||
<div class="btn nowOpenBtn" @click="$router.push('/member')">立即开通</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
}>();
|
||||
const emit = defineEmits(['close']);
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
.content {
|
||||
width: 281px;
|
||||
height: 334px;
|
||||
background: url('./../../assets/images/png/popVip.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
color: #383838;
|
||||
padding: 135px 30px 0 50px;
|
||||
position: relative;
|
||||
p {
|
||||
font-size: 17px;
|
||||
line-height: 22px;
|
||||
}
|
||||
.name {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
padding-right: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.close {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
> img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.btnBox {
|
||||
.btn {
|
||||
height: 46px;
|
||||
width: 126px;
|
||||
background: url('./../../assets/images/png/popBtn2.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
text-align: center;
|
||||
line-height: 46px;
|
||||
font-size: 17px;
|
||||
}
|
||||
.nowOpenBtn {
|
||||
background: url('./../../assets/images/png/popBtn.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
margin-left: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<!-- 选择路线弹窗 -->
|
||||
<Teleport to="body">
|
||||
<van-popup class="selectionPopup" v-model:show="isOpenSelectionLine" position="bottom" @close="emits('close')">
|
||||
<div class="selectionLineBox">
|
||||
<!-- <div class="titleBox">
|
||||
选择播放线路
|
||||
<SvgIcon name="closeBlack" class="close" @click="emits('close')"></SvgIcon>
|
||||
</div> -->
|
||||
<div class="line_top" />
|
||||
<div class="lineBox">
|
||||
<div
|
||||
class="line"
|
||||
:class="{ selectedLine: item.desc == globalStore.videoRoadLine.desc }"
|
||||
@click="selectLine(item)"
|
||||
v-for="item in state.roadLines"
|
||||
:key="item.desc"
|
||||
>
|
||||
{{ item.desc }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
<!-- 线路vip弹框 -->
|
||||
<VipLinePopup :is-open="state.visibleVipLinePopup" @close="closeVipLine"></VipLinePopup>
|
||||
</Teleport>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
import { showToast } from 'vant';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
const { isVip } = useUserStore();
|
||||
const configStore = useConfigStore();
|
||||
const globalStore = useGlobalStore();
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
}>();
|
||||
const emits = defineEmits(['close', 'closeSelectLine']);
|
||||
const isOpenSelectionLine = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
const state = reactive({
|
||||
roadLines: <any>configStore.sourceInfo.VID,
|
||||
visibleVipLinePopup: false,
|
||||
});
|
||||
const closeVipLine = () => {
|
||||
state.visibleVipLinePopup = false;
|
||||
};
|
||||
const selectLine = (item: any) => {
|
||||
if (!isVip && globalStore.videoRoadLine.desc !== item.desc) {
|
||||
state.visibleVipLinePopup = true;
|
||||
emits('close');
|
||||
return;
|
||||
}
|
||||
globalStore.setVideoRoadLine(item);
|
||||
showToast('切换成功');
|
||||
emits('closeSelectLine');
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/styles/mixins.scss';
|
||||
:deep(.van-overlay) {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
:deep(.van-popup) {
|
||||
background-color: transparent;
|
||||
}
|
||||
.selectionPopup {
|
||||
.selectionLineBox {
|
||||
background: #0f0f0f;
|
||||
background-size: 150%;
|
||||
background-position-y: -20px;
|
||||
padding: 18px 14px;
|
||||
border-radius: 18px 18px 0 0;
|
||||
|
||||
@include SafeArea('padding', 'bottom', 18px);
|
||||
// .titleBox {
|
||||
// color: black;
|
||||
// font-size: 18px;
|
||||
// font-weight: 500;
|
||||
// height: 50px;
|
||||
// text-align: center;
|
||||
// line-height: 50px;
|
||||
// border-bottom: 1px solid #f1f3f5;
|
||||
// position: relative;
|
||||
// .close {
|
||||
// height: 15px;
|
||||
// width: 15px;
|
||||
// position: absolute;
|
||||
// right: 10px;
|
||||
// top: 10px;
|
||||
// }
|
||||
// }
|
||||
.line_top {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
border-radius: 99px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
margin: 0 auto;
|
||||
}
|
||||
.lineBox {
|
||||
.line {
|
||||
padding: 18px 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
border-bottom: $borderSplit;
|
||||
}
|
||||
.selectedLine {
|
||||
color: $themeColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<van-popup
|
||||
:show="isShow"
|
||||
position="bottom"
|
||||
round
|
||||
class="set-price-popup"
|
||||
@click-overlay="emit('close')"
|
||||
@update:show="(v) => !v && emit('close')"
|
||||
>
|
||||
<div class="content">
|
||||
<div class="line" />
|
||||
<p class="title">设置价格</p>
|
||||
<div class="mess">
|
||||
<div
|
||||
:class="['item', state.activeData.id == item.id ? 'active' : '']"
|
||||
v-for="(item, index) in state.list"
|
||||
@click="setPrice(item)"
|
||||
:key="index"
|
||||
>
|
||||
<img src="@/assets/images/png/coins.png" alt="" />
|
||||
<span>{{ item.num }}金币</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mess2 flex-center-center">
|
||||
<span>自定义</span>
|
||||
<van-field @blur="blur" class="titleText" v-model="state.price" placeholder="" type="number" />
|
||||
<span>金币</span>
|
||||
</div>
|
||||
<div class="btn_box">
|
||||
<div class="ISee flex-center-center" @click="emit('close', state.priceVal)">确定设置</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
|
||||
const state = reactive({
|
||||
list: [
|
||||
{ num: 10, id: 1 },
|
||||
{ num: 20, id: 2 },
|
||||
{ num: 30, id: 3 },
|
||||
{ num: 40, id: 4 },
|
||||
{ num: 50, id: 5 },
|
||||
{ num: 60, id: 6 },
|
||||
{ num: 70, id: 7 },
|
||||
{ num: 80, id: 8 },
|
||||
],
|
||||
price: '',
|
||||
activeData: <any>{},
|
||||
priceVal: '',
|
||||
});
|
||||
const props = defineProps({
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const isShow = computed(() => props.isOpen);
|
||||
|
||||
const setPrice = (data: any) => {
|
||||
if (state.activeData.id == data.id) {
|
||||
state.activeData = {};
|
||||
state.priceVal = '';
|
||||
} else {
|
||||
state.activeData = data;
|
||||
state.priceVal = data.num;
|
||||
}
|
||||
};
|
||||
|
||||
const blur = () => {
|
||||
state.price ? (state.activeData = {} && (state.priceVal = state.price)) : (state.priceVal = '');
|
||||
emit('blur');
|
||||
};
|
||||
|
||||
const emit = defineEmits(['close', 'blur']);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
width: 100%;
|
||||
background: #0f0f0f;
|
||||
border-radius: 20px 20px 0 0;
|
||||
padding: 18px 16px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
@include SafeArea('padding', 'bottom', 21px);
|
||||
|
||||
.line {
|
||||
width: 36px;
|
||||
height: 5px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 100px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 28px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mess {
|
||||
width: 300px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 66px);
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
|
||||
.item {
|
||||
width: 66px;
|
||||
height: 72px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.active {
|
||||
border-color: transparent;
|
||||
background: #ffc000;
|
||||
color: #fefce8;
|
||||
}
|
||||
}
|
||||
|
||||
.mess2 {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
gap: 0;
|
||||
|
||||
.titleText {
|
||||
width: 47px;
|
||||
height: 24px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 0;
|
||||
margin: 0 6px;
|
||||
border-radius: 5px;
|
||||
|
||||
:deep(.van-field__control) {
|
||||
height: 24px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.55) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn_box {
|
||||
width: 100%;
|
||||
|
||||
.ISee {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
border-radius: 39px;
|
||||
background: #ffc000;
|
||||
color: #231600;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.set-price-popup {
|
||||
background: #0f0f0f !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<div
|
||||
class="vertical_video_warp"
|
||||
:class="{
|
||||
'vertical_video_warp--hide-progress': props.hideProgress,
|
||||
'vertical_video_warp--hide-fullscreen': props.hideFullscreen,
|
||||
'vertical_video_warp--hide-background': props.hideBackground,
|
||||
}"
|
||||
>
|
||||
<div id="video">
|
||||
<div class="avoid_sliding" @click="pause"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeUnmount, onMounted } from 'vue';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import HlsJSPlayer from 'xgplayer-hls.js';
|
||||
import { bindH265PlaybackFallback, resolvePlayResourceKey } from '@/utils/h265Playback';
|
||||
|
||||
const props = defineProps<{
|
||||
sourceURL: string;
|
||||
/** H.265 资源 key;有值且设备支持时优先播放 */
|
||||
h265Url?: string | null;
|
||||
hideProgress?: boolean;
|
||||
hideFullscreen?: boolean;
|
||||
hideBackground?: boolean;
|
||||
}>();
|
||||
let player: any = null; //播放器实例
|
||||
let playResourceKey = '';
|
||||
let isPlayingH265 = false;
|
||||
let forceH264 = false;
|
||||
let pendingResumeAt = 0;
|
||||
let unbindH265Fallback: (() => void) | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
//初始化播放器
|
||||
initVideo();
|
||||
});
|
||||
/**
|
||||
* @description 暂停
|
||||
*/
|
||||
const pause = () => {
|
||||
player.pause();
|
||||
};
|
||||
|
||||
const resolveCurrentResourceKey = () => {
|
||||
const resolved = resolvePlayResourceKey({
|
||||
h264Key: props.sourceURL,
|
||||
h265Key: props.h265Url,
|
||||
forceH264,
|
||||
});
|
||||
playResourceKey = resolved.resourceKey;
|
||||
isPlayingH265 = resolved.isH265;
|
||||
};
|
||||
|
||||
const destroyPlayer = () => {
|
||||
unbindH265Fallback?.();
|
||||
unbindH265Fallback = null;
|
||||
if (!player) return;
|
||||
try {
|
||||
player.src = '';
|
||||
player.destroy(true);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
player = null;
|
||||
};
|
||||
|
||||
const fallbackH265ToH264 = (resumeAt: number) => {
|
||||
if (forceH264 || !isPlayingH265) return;
|
||||
forceH264 = true;
|
||||
isPlayingH265 = false;
|
||||
pendingResumeAt = resumeAt;
|
||||
destroyPlayer();
|
||||
initVideo();
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 初始化播放器
|
||||
*/
|
||||
const initVideo = () => {
|
||||
if (player) destroyPlayer();
|
||||
resolveCurrentResourceKey();
|
||||
player = new HlsJSPlayer({
|
||||
id: 'video',
|
||||
url: getPath(),
|
||||
fluid: true,
|
||||
playsinline: true,
|
||||
videoInit: true,
|
||||
loop: true,
|
||||
autoplay: true,
|
||||
controls: true,
|
||||
lang: 'zh-cn',
|
||||
ignores: ['i18n', 'play', 'volume', 'definition'],
|
||||
allowPlayAfterEnded: true,
|
||||
allowSeekAfterEnded: true,
|
||||
closeVideoPreventDefault: true,
|
||||
closeVideoStopPropagation: true,
|
||||
closeInactive: true,
|
||||
enableVideoDbltouch: true,
|
||||
});
|
||||
unbindH265Fallback = bindH265PlaybackFallback(player, {
|
||||
shouldFallback: () => isPlayingH265 && !forceH264,
|
||||
onFallback: fallbackH265ToH264,
|
||||
});
|
||||
player.on('loadedmetadata', () => {
|
||||
if (pendingResumeAt > 0) {
|
||||
const resumeAt = pendingResumeAt;
|
||||
pendingResumeAt = 0;
|
||||
try {
|
||||
player.currentTime = resumeAt;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
/**
|
||||
* @description 获取播放链接
|
||||
*/
|
||||
const getPath = () => {
|
||||
const { baseUrl } = useConfigStore();
|
||||
return `${baseUrl}/api/app/vid/h5/m3u8/${playResourceKey || props.sourceURL}?token=${getToken()}`;
|
||||
};
|
||||
/**
|
||||
* @description 获取token
|
||||
*/
|
||||
const getToken = () => {
|
||||
const { token } = useUserStore();
|
||||
return token;
|
||||
};
|
||||
onBeforeUnmount(() => {
|
||||
destroyPlayer();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.vertical_video_warp {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
background: #000000;
|
||||
align-items: center;
|
||||
:deep(.xgplayer-progress-played) {
|
||||
background: rgb(39, 213, 236);
|
||||
}
|
||||
:deep(.xgplayer-progress) {
|
||||
top: 10px;
|
||||
}
|
||||
:deep(.xgplayer-start),
|
||||
:deep(.xgplayer-controls) {
|
||||
transform: translate3d(0, 0, 0);
|
||||
-webkit-transform: translate3d(0, 0, 0);
|
||||
}
|
||||
:deep(.xgplayer-progress-btn) {
|
||||
display: none !important;
|
||||
}
|
||||
:deep(.xgplayer-time-current:after) {
|
||||
content: ' ';
|
||||
}
|
||||
:deep(.xgplayer-progress) {
|
||||
left: 43%;
|
||||
right: 0;
|
||||
width: 60%;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
:deep(.xgplayer-time) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
// margin-top: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
:deep(.xgplayer-time-current)::after {
|
||||
display: none;
|
||||
}
|
||||
&.vertical_video_warp--hide-progress {
|
||||
:deep(.xgplayer-progress),
|
||||
:deep(.xgplayer-time) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
&.vertical_video_warp--hide-fullscreen {
|
||||
:deep(.xgplayer-fullscreen) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
&.vertical_video_warp--hide-background {
|
||||
background: transparent;
|
||||
}
|
||||
// :deep(.xgplayer-icon-play) {
|
||||
// > svg {
|
||||
// height: 40px;
|
||||
// width: 40px;
|
||||
// }
|
||||
// }
|
||||
// :deep(.xgplayer-skin-default .xgplayer-start) {
|
||||
// height: 40px;
|
||||
// width: 40px;
|
||||
// margin: -20px;
|
||||
// }
|
||||
#video {
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: relative;
|
||||
:deep(.xgplayer-enter-spinner) {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
.avoid_sliding {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="adv_swiper">
|
||||
<div class="adv-container" v-if="props.list.length > 0" ref="advRef">
|
||||
<van-swipe ref="swipeRef" :show-indicators="false" :autoplay="autoTime" class="custom-swipe" @change="onChange">
|
||||
<van-swipe-item
|
||||
@click="handleJumpAdv(item, props.clickType)"
|
||||
v-for="item in props.list"
|
||||
:key="item.id"
|
||||
:style="{ height: props.height }"
|
||||
class="adv-swiper-slide"
|
||||
>
|
||||
<DecryptImg @load="load" :src="item.cover" />
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
<!-- 自定义指示器 -->
|
||||
<div class="indicator-wrapper" v-if="isShowIndicators">
|
||||
<div class="custom-indicators">
|
||||
<div
|
||||
v-for="(_, index) in props.list"
|
||||
:key="index"
|
||||
:class="['indicator-dot', { active: currentIndex === index }]"
|
||||
@click="swipeRef?.swipeTo(index)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
import type { SwipeInstance } from 'vant';
|
||||
import { AdImpressionType, setAdImpression } from '@/utils/eventReporter/adImpression';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { reportAdClickEvent } from '@/utils/eventReporter/adClick';
|
||||
|
||||
const route = useRoute();
|
||||
const emit = defineEmits(['load']);
|
||||
const swipeRef = ref<SwipeInstance>();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
list: Types.App.AdsInfoListItem[];
|
||||
height?: string;
|
||||
isShowIndicators?: boolean;
|
||||
clickType?: 'ads' | 'recreation';
|
||||
autoTime: number;
|
||||
adImpression?: AdImpressionType;
|
||||
}>(),
|
||||
{
|
||||
list: () => [],
|
||||
height: '156px',
|
||||
isShowIndicators: true,
|
||||
clickType: 'ads',
|
||||
autoTime: 5000,
|
||||
},
|
||||
);
|
||||
|
||||
const currentIndex = ref(0);
|
||||
|
||||
const onChange = (index: number) => {
|
||||
currentIndex.value = index;
|
||||
};
|
||||
|
||||
const load = () => {
|
||||
emit('load');
|
||||
};
|
||||
// 广告展示埋点
|
||||
const advRef = ref();
|
||||
watch(
|
||||
() => props.list,
|
||||
() => {
|
||||
if (props.list.length) {
|
||||
nextTick(() => {
|
||||
setAdImpression(advRef.value, props.list, {
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'fullscreen',
|
||||
}).setupObserver();
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
const handleJumpAdv = (item: Types.App.AdsInfoListItem, clickType?: 'ads' | 'recreation') => {
|
||||
let params = {
|
||||
...item,
|
||||
page_key: props.adImpression?.page_key || (route.meta.pageKey as string) || '',
|
||||
page_name: props.adImpression?.page_name || (route.meta.pageName as string) || '',
|
||||
ad_type: props.adImpression?.ad_type || 'fullscreen',
|
||||
};
|
||||
reportAdClickEvent(params);
|
||||
jumpAdv(item, clickType);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.adv_swiper {
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.adv-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
.custom-swipe {
|
||||
height: 100%;
|
||||
.adv-swiper-slide {
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(img) {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.indicator-wrapper {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
pointer-events: none;
|
||||
z-index: 99;
|
||||
|
||||
.custom-indicators {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
right: 12px;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
pointer-events: auto;
|
||||
|
||||
.indicator-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
width: 17px;
|
||||
background: #3476ff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="svg-icon"
|
||||
:width="props.size"
|
||||
:height="props.size"
|
||||
:style="props.color ? { color: props.color } : undefined"
|
||||
>
|
||||
<use :xlink:href="symbolId" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
const props = defineProps({
|
||||
prefix: {
|
||||
type: String,
|
||||
default: 'icon',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: '1em',
|
||||
},
|
||||
});
|
||||
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
|
||||
</script>
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="showing"
|
||||
class="ai-drama-float-bubble"
|
||||
:style="{ '--tab-count': String(tabCount) }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="ai-drama-float-bubble__hit"
|
||||
aria-label="AI爽剧来袭"
|
||||
@click.stop="onClick"
|
||||
>
|
||||
<img
|
||||
class="ai-drama-float-bubble__img"
|
||||
src="@/assets/images/png/short-drama/ai-drama-float-bubble.png"
|
||||
alt="AI爽剧来袭!"
|
||||
width="131"
|
||||
height="34"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { unlockShortVideoAudio } from '@/utils/shortVideoAudioUnlock';
|
||||
|
||||
/** 终身只展示 1 次(含老用户更新后的首次启动) */
|
||||
const STORAGE_KEY = 'ai_drama_float_bubble_once_v1';
|
||||
|
||||
const props = defineProps<{
|
||||
/** 底部 Tab 数量(含暗网时为 6,否则 5) */
|
||||
tabCount?: number;
|
||||
/** 当前是否已在短剧 Tab */
|
||||
activeOnShortVideo?: boolean;
|
||||
}>();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const configStore = useConfigStore();
|
||||
|
||||
const tabCount = computed(() => Math.max(5, Number(props.tabCount) || 6));
|
||||
|
||||
const isOnShortVideo = () =>
|
||||
Boolean(props.activeOnShortVideo) || route.name === 'shortVideo';
|
||||
|
||||
const readOnceFlag = () => {
|
||||
try {
|
||||
return localStorage.getItem(STORAGE_KEY) === '1';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const writeOnceFlag = () => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, '1');
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
/** 本次是否正在展示(仅首次启动且非短剧 Tab) */
|
||||
const showing = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
// 已消耗过首次机会 → 永不出现
|
||||
if (readOnceFlag()) {
|
||||
showing.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Ping 运营全局关闭:不展示,也不消耗本地 once(日后重新开启仍可出现一次)
|
||||
if (configStore.appConfig?.entryPopupEnabled === false) {
|
||||
showing.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 消耗首次机会(老用户更新后第一次进 App 也算)
|
||||
writeOnceFlag();
|
||||
|
||||
// 启动时默认就在短剧 Tab → 不出现
|
||||
if (isOnShortVideo()) {
|
||||
showing.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
showing.value = true;
|
||||
});
|
||||
|
||||
// 进入短剧 Tab 后立刻消失
|
||||
watch(
|
||||
() => [props.activeOnShortVideo, route.name] as const,
|
||||
() => {
|
||||
if (isOnShortVideo()) {
|
||||
showing.value = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const onClick = () => {
|
||||
unlockShortVideoAudio();
|
||||
showing.value = false;
|
||||
writeOnceFlag();
|
||||
void router.replace({ path: '/shortVideo', query: { type: '4' } });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/*
|
||||
* 与 TabBar 同坐标系:fixed + max-width:780 居中。
|
||||
* 原先 absolute 相对 tab_wrapper,和 fixed 底栏/安全区错位,真机易乱。
|
||||
* Figma 23190:99055:三角 Vector x=46 w=12,尖端约在气泡左侧 52px,对准第 2 个 Tab「短剧」中心。
|
||||
*/
|
||||
.ai-drama-float-bubble {
|
||||
--tab-count: 6;
|
||||
/* 气泡左缘 → 三角尖端(46 + 12/2) */
|
||||
--arrow-offset: 6px;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 780px;
|
||||
/* 与 :deep(.van-tabbar) height:55px + safe-area 对齐,再上抬 6px */
|
||||
bottom: calc(55px + constant(safe-area-inset-bottom) + 6px);
|
||||
bottom: calc(55px + env(safe-area-inset-bottom, 0px) + 6px);
|
||||
z-index: 1002;
|
||||
height: 34px;
|
||||
pointer-events: none;
|
||||
|
||||
&__hit {
|
||||
position: absolute;
|
||||
/* 第 2 个 Tab 中心:兼容部分 WebView 对 calc 除法顺序的限制 */
|
||||
left: calc(100% * 1.5 / var(--tab-count));
|
||||
bottom: 0;
|
||||
width: 50px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transform: translateX(calc(-1 * var(--arrow-offset)));
|
||||
animation: ai-drama-bubble-bob 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&__img {
|
||||
display: block;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ai-drama-bubble-bob {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateX(calc(-1 * var(--arrow-offset))) translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(calc(-1 * var(--arrow-offset))) translateY(-3px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,346 @@
|
||||
<template>
|
||||
<div class="tab_wrapper">
|
||||
<AiDramaFloatBubble :tab-count="tabCount" :active-on-short-video="activeName === 'shortVideo'" />
|
||||
<van-tabbar
|
||||
v-model="activeName"
|
||||
route
|
||||
fixed
|
||||
placeholder
|
||||
:border="false"
|
||||
:safe-area-inset-bottom="true"
|
||||
@change="onChange"
|
||||
data-ignore-track
|
||||
>
|
||||
<!-- :icon="activeName == 'home' ? homeIconActived : homeIcon" -->
|
||||
<van-tabbar-item to="/home" name="home" replace>
|
||||
<span class="tabbar-item-text" v-if="isHomeTop" @click="backTop('home')">顶部</span>
|
||||
<span v-else class="tabbar-item-text">首页</span>
|
||||
<template #icon="props">
|
||||
<div
|
||||
class="tabbar-item"
|
||||
@click="backTop('home')"
|
||||
:class="[props.active ? (isHomeTop ? 'top' : 'homeActive') : 'home']"
|
||||
></div>
|
||||
</template>
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item to="/shortVideo" name="shortVideo" replace>
|
||||
<span class="tabbar-item-text">短剧</span>
|
||||
<template #icon="props">
|
||||
<div class="tabbar-item" :class="[props.active ? 'liveActive' : 'live']"></div>
|
||||
</template>
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item to="/darkNet" name="darkNet" replace v-if="darkWebEnable">
|
||||
<span class="tabbar-item-text" v-if="isDarkNetTop" @click="backTop('darkNet')">顶部</span>
|
||||
<span v-else class="tabbar-item-text">
|
||||
<template v-if="darkWebIconName">
|
||||
{{ darkWebIconName }}
|
||||
</template>
|
||||
<template v-else>暗网</template>
|
||||
</span>
|
||||
<template #icon="props">
|
||||
<template v-if="darkWebIcon">
|
||||
<lazyImg :src="darkWebIcon" alt="darkWebIcon" v-if="props.active && !isDarkNetTop" />
|
||||
<div
|
||||
v-else
|
||||
class="tabbar-item"
|
||||
@click="backTop('darkNet')"
|
||||
:class="[props.active ? (isDarkNetTop ? 'top' : 'darkNetActive') : 'darkNet']"
|
||||
></div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
class="tabbar-item"
|
||||
@click="backTop('darkNet')"
|
||||
:class="[props.active ? (isDarkNetTop ? 'top' : 'darkNetActive') : 'darkNet']"
|
||||
></div>
|
||||
</template>
|
||||
</template>
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item to="/aiUndress2" name="aiUndress2" replace>
|
||||
<span class="tabbar-item-text">AI</span>
|
||||
<template #icon="props">
|
||||
<div class="tabbar-item" :class="[props.active ? 'aiActive' : 'ai']"></div>
|
||||
</template>
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item to="/community" name="community" replace>
|
||||
<span class="tabbar-item-text" v-if="isCommunityTop" @click="backTop('community')">顶部</span>
|
||||
<span v-else class="tabbar-item-text">社区</span>
|
||||
<template #icon="props">
|
||||
<div
|
||||
class="tabbar-item"
|
||||
@click="backTop('community')"
|
||||
:class="[props.active ? (isCommunityTop ? 'top' : 'communityActive') : 'community']"
|
||||
></div>
|
||||
</template>
|
||||
</van-tabbar-item>
|
||||
<van-tabbar-item to="/mine" name="mine" replace>
|
||||
<span class="tabbar-item-text">我的</span>
|
||||
<template #icon="props">
|
||||
<div class="tabbar-item" :class="[props.active ? 'mineActive' : 'mine']"></div>
|
||||
</template>
|
||||
<div v-if="showRedDot" class="newsTip"></div>
|
||||
</van-tabbar-item>
|
||||
</van-tabbar>
|
||||
<!-- warning提示 -->
|
||||
<van-popup class="waringBox" v-model:show="state.showWarning" :close-on-click-overlay="false">
|
||||
<div class="waringBoxTip">
|
||||
<div class="title">WANRNING</div>
|
||||
<div class="content">该板块内容极度危险,心理承受弱者请自行离开,切记挑战自己好奇害死猫,请您确认还要继续吗?</div>
|
||||
<div class="btnBox flex-center-between">
|
||||
<div class="btn" @click.stop="leave">离开</div>
|
||||
<div class="btn red" @click="state.showWarning = false">确认</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TabBar',
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import { ref, toRefs, watch, reactive, onActivated, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import UserApi from '@/service/user';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useConfigStore } from '@/stores/config';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
import { useImAdStore } from '@/stores/imAd';
|
||||
import { imTotalUnread } from '@/utils/im/sdk';
|
||||
import { analyticsEventNavigation } from '@/utils/eventReporter/analytics';
|
||||
import setTheme from '@/assets/theme/index';
|
||||
import { unlockShortVideoAudio } from '@/utils/shortVideoAudioUnlock';
|
||||
import AiDramaFloatBubble from './AiDramaFloatBubble.vue';
|
||||
|
||||
const configStore = useConfigStore();
|
||||
const globalStore = useGlobalStore();
|
||||
setTheme(configStore.appConfig.festivalUi);
|
||||
const darkWebEnable = computed(() => {
|
||||
return configStore.appConfig.darkWebEnable;
|
||||
});
|
||||
const darkWebIcon = computed(() => {
|
||||
return configStore.appConfig.darkWebIcon;
|
||||
});
|
||||
const darkWebIconName = computed(() => {
|
||||
return configStore.appConfig.darkWebIconName;
|
||||
});
|
||||
/** 底部 Tab 数量:暗网开启时 6,否则 5(用于悬浮气泡对准「短剧」) */
|
||||
const tabCount = computed(() => (darkWebEnable.value ? 6 : 5));
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const activeName = ref('home');
|
||||
|
||||
const state = reactive({
|
||||
showWarning: false,
|
||||
userInfo: <Types.User.UserInfoType>{},
|
||||
});
|
||||
|
||||
const refreshRoute = (name: any) => {
|
||||
activeName.value = name;
|
||||
};
|
||||
|
||||
refreshRoute(route.name);
|
||||
|
||||
watch(route, (newVal) => {
|
||||
refreshRoute(newVal.name);
|
||||
});
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @param uid
|
||||
*/
|
||||
const getInfo = async () => {
|
||||
try {
|
||||
let res: any = await UserApi.queryUserInfo(null);
|
||||
state.userInfo = res;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
getInfo();
|
||||
const change = (activeName: any) => {
|
||||
if (activeName == 'darkNet') {
|
||||
if (state.userInfo.isVip) {
|
||||
state.showWarning = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
const leave = () => {
|
||||
activeName.value = 'home';
|
||||
router.replace('/home');
|
||||
state.showWarning = false;
|
||||
};
|
||||
|
||||
const UserStore = useUserStore();
|
||||
onActivated(() => {
|
||||
refreshRoute(route.name);
|
||||
UserStore.getCheckMessageTip();
|
||||
});
|
||||
const onChange = (name: string) => {
|
||||
UserStore.getCheckMessageTip();
|
||||
if (name == 'home') {
|
||||
analyticsEventNavigation('Home', '首页');
|
||||
} else if (name == 'shortVideo') {
|
||||
// 底部进短视频页:解锁会话有声,抖音/短剧后续竖滑可带声
|
||||
unlockShortVideoAudio();
|
||||
analyticsEventNavigation('ShortVideo', '抖音');
|
||||
} else if (name == 'darkNet') {
|
||||
analyticsEventNavigation('DarkWeb', '暗网');
|
||||
} else if (name == 'aiUndress2') {
|
||||
analyticsEventNavigation('AI', 'AI');
|
||||
} else if (name == 'community') {
|
||||
analyticsEventNavigation('Community', '社区');
|
||||
} else if (name == 'mine') {
|
||||
analyticsEventNavigation('Mine', '我的');
|
||||
}
|
||||
};
|
||||
const { isNewsTip } = toRefs(UserStore);
|
||||
|
||||
const imAdStore = useImAdStore();
|
||||
const { listHasUnclicked } = storeToRefs(imAdStore);
|
||||
|
||||
/** 我的页红点:业务未读 / IM 私信未读 / IM 列表广告未点击 */
|
||||
const showRedDot = computed(() => {
|
||||
return Boolean(isNewsTip.value) || Number(imTotalUnread.value) > 0 || listHasUnclicked.value;
|
||||
});
|
||||
|
||||
const backTop = (path: string) => {
|
||||
let dom: any = null;
|
||||
if (path == 'home') {
|
||||
if (!isHomeTop.value) return;
|
||||
dom = document.getElementById(path + globalStore.activeIndex);
|
||||
} else if (path == 'darkNet') {
|
||||
if (!isDarkNetTop.value) return;
|
||||
dom = document.getElementById(path + globalStore.darkNetActiveIndex);
|
||||
} else if (path == 'community') {
|
||||
if (!isCommunityTop.value) return;
|
||||
dom = document.getElementById(path + globalStore.communityActiveIndex);
|
||||
}
|
||||
dom?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
const arr = globalStore.keepScroll[`/${path}`];
|
||||
arr.map((item: any) => {
|
||||
if (item.scrollObj == globalStore.activeIndex) {
|
||||
item.showBackTopBtn = false;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
globalStore.keepScroll[`/${path}`] = arr;
|
||||
};
|
||||
|
||||
const isHomeTop = computed(() => {
|
||||
return (
|
||||
activeName.value == 'home' &&
|
||||
globalStore.keepScroll['/home']?.find((item) => item.scrollObj == globalStore.activeIndex)?.showBackTopBtn
|
||||
);
|
||||
});
|
||||
|
||||
const isDarkNetTop = computed(() => {
|
||||
return (
|
||||
activeName.value == 'darkNet' &&
|
||||
globalStore.keepScroll['/darkNet']?.find((item) => item.scrollObj == globalStore.darkNetActiveIndex)?.showBackTopBtn
|
||||
);
|
||||
});
|
||||
|
||||
const isCommunityTop = computed(() => {
|
||||
return (
|
||||
activeName.value == 'community' &&
|
||||
globalStore.keepScroll['/community']?.find((item) => item.scrollObj == globalStore.communityActiveIndex)
|
||||
?.showBackTopBtn
|
||||
);
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@media (min-height: 700px) {
|
||||
.tab_wrapper {
|
||||
.marqueeBar {
|
||||
top: -64px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.van-tabbar) {
|
||||
background: rgb(0, 0, 0);
|
||||
height: 55px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.van-tabbar-item--active {
|
||||
background: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
:deep(.van-tabbar--fixed) {
|
||||
max-width: 780px;
|
||||
margin: 0 auto;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
:deep(.van-tabbar__placeholder) {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
:deep(.van-icon__image) {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
:deep(.van-tabbar-item__text) {
|
||||
font-size: 12px;
|
||||
}
|
||||
.tab_wrapper {
|
||||
position: relative;
|
||||
.marqueeBar {
|
||||
height: 31px;
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -32px;
|
||||
z-index: 999;
|
||||
}
|
||||
}
|
||||
.waringBox {
|
||||
.waringBoxTip {
|
||||
height: 235px;
|
||||
width: 307px;
|
||||
background-color: rgb(34, 34, 34);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
.title {
|
||||
height: 60px;
|
||||
background-color: rgb(255, 50, 48);
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
line-height: 60px;
|
||||
}
|
||||
.content {
|
||||
font-size: 16px;
|
||||
padding: 23px 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.btnBox {
|
||||
padding: 0 20px;
|
||||
.btn {
|
||||
font-size: 15px;
|
||||
height: 40px;
|
||||
width: 120px;
|
||||
background-color: rgb(61, 61, 61);
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
border-radius: 40px;
|
||||
}
|
||||
.red {
|
||||
background-color: rgb(255, 50, 48);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.newsTip {
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
right: 0;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #fd0563;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<div ref="textOverflow" class="text-overflow" :style="boxStyle">
|
||||
<!-- <span ref="overEllipsis" class="overEllipsis">{{ realText }}</span> -->
|
||||
<span v-if="isTopping" class="choosen">置顶</span>
|
||||
<span v-else-if="!isTopping && isChoosen" class="choosen">精华</span>
|
||||
<span ref="overEllipsis" class="overEllipsis" v-html="rearrangeText(realText)"></span>
|
||||
<span class="slot-box" ref="slotRef" v-if="state.showSlotNode">
|
||||
<slot :click-toggle="toggle" :expanded="state.expanded"></slot>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
import { ref, reactive, computed, nextTick, onMounted } from 'vue';
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
maxLines: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
isChoosen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isTopping: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const overEllipsis = ref<HTMLElement>();
|
||||
const slotRef = ref<HTMLElement>();
|
||||
const textOverflow = ref<HTMLElement>();
|
||||
const state = reactive({
|
||||
offset: props.text.length,
|
||||
expanded: false,
|
||||
slotBoxWidth: 0,
|
||||
textBoxWidth: props.width,
|
||||
showSlotNode: false,
|
||||
isChoosen: props.isChoosen,
|
||||
});
|
||||
const boxStyle = computed(() => {
|
||||
if (props.width) {
|
||||
return {
|
||||
width: props.width + 'px',
|
||||
};
|
||||
}
|
||||
return '0px';
|
||||
});
|
||||
const realText = computed(() => {
|
||||
// 是否被截取
|
||||
const isCutOut = state.offset !== props.text.length;
|
||||
let realText = props.text;
|
||||
if (isCutOut && !state.expanded) {
|
||||
realText = props.text.slice(0, state.offset) + '...';
|
||||
}
|
||||
return realText;
|
||||
});
|
||||
const calculateOffset = (from: number, to: number) => {
|
||||
nextTick(() => {
|
||||
if (Math.abs(from - to) <= 1) return;
|
||||
if (isOverflow()) {
|
||||
to = state.offset;
|
||||
} else {
|
||||
from = state.offset;
|
||||
}
|
||||
state.offset = Math.floor((from + to) / 2);
|
||||
calculateOffset(from, to);
|
||||
});
|
||||
};
|
||||
const isOverflow = () => {
|
||||
const { len, lastWidth } = getLines();
|
||||
if (len < props.maxLines) {
|
||||
return false;
|
||||
}
|
||||
if (props.maxLines) {
|
||||
// 超出部分 行数 > 最大行数 或则 已经是最大行数但最后一行宽度 + 后面内容超出正常宽度
|
||||
const lastLineOver = !!(len === props.maxLines && lastWidth + state.slotBoxWidth > state.textBoxWidth);
|
||||
if (len > props.maxLines || lastLineOver) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const getLines = () => {
|
||||
const clientRects: any = overEllipsis.value?.getClientRects();
|
||||
return {
|
||||
len: clientRects.length,
|
||||
lastWidth: clientRects[clientRects.length - 1]?.width,
|
||||
};
|
||||
};
|
||||
const toggle = () => {
|
||||
state.expanded = !state.expanded;
|
||||
};
|
||||
const rearrangeText = (text: any) => {
|
||||
let originalText = '';
|
||||
if (props.text && props.text.indexOf('##') != -1) {
|
||||
let startIndex = props.text.indexOf('{');
|
||||
let endIndex = props.text.indexOf('}');
|
||||
let jsonText = props.text.substring(startIndex, endIndex + 1);
|
||||
let jsonObj = JSON.parse(jsonText);
|
||||
let texts = '##' + jsonText + '##';
|
||||
originalText = text.replace(
|
||||
texts,
|
||||
`<div onclick="jumpKeywords('${jsonObj.type}', '${jsonObj.id}')" style='color:rgb(162,203,240);display: inline-block;'>${jsonObj.txt}<div style="height: 9px;width: 9px;display: inline-block;margin-bottom: 3px;background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAYCAMAAADJYP15AAAAOVBMVEVHcEyh0fuh0fyi0f2h0fyg0Pyf1f+f0/yfz/+h0fyfz/+h0Pyg0Pyh0fyf0vyi0vyh0fyh0fyg0fw8+yy6AAAAEXRSTlMAkLBwgPAwPxDGIODfoFBgXzlWcyEAAAC6SURBVCjPbZDbEsMgCEQNarxU027+/2MLFQ3pZF9kduQA65wof7aCRmF3VqlB1fMy84ZLLU1b3JdPrh6dq6igxDVpb3oBZSCYSwtYIxCkeDPhmuM8Y5Ts7VaMqfwUIFmbgIMfHni7IQy4Njmzrr+alhTKTd24WaE7bjNp3sMXt2oGYlyfeWgcq2c6zzPMWKMkR4Ejx/os+GKSXREKMg6rSGcxEdUjeFloj3OVuzhRbA++/09J1R9///QF6MAK320wOcMAAAAASUVORK5CYII=');background-size: 100% 100%;"></div></div>`,
|
||||
);
|
||||
return originalText;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
function jumpKeywords(type: any, id: any) {
|
||||
window.event.stopPropagation();
|
||||
let item = {
|
||||
href: '',
|
||||
};
|
||||
switch (type) {
|
||||
case '1':
|
||||
item.href = 'yinseinner://memberCentre';
|
||||
break;
|
||||
case '2':
|
||||
item.href = 'yinseinner://sharePromote';
|
||||
break;
|
||||
case '3':
|
||||
item.href = 'yinseinner://incomeCenter';
|
||||
break;
|
||||
case '4':
|
||||
item.href = 'yinseinner://taskHall';
|
||||
break;
|
||||
case '5':
|
||||
item.href = `yinseinner://taskDetails?id=${id}`;
|
||||
break;
|
||||
case '6':
|
||||
item.href = 'yinseinner://wishList';
|
||||
break;
|
||||
case '7':
|
||||
item.href = 'yinseinner://creationCenter';
|
||||
break;
|
||||
case '8':
|
||||
item.href = 'yinseinner://myCertification';
|
||||
break;
|
||||
case '9':
|
||||
item.href = 'yinseinner://announcement';
|
||||
break;
|
||||
case '10':
|
||||
item.href = 'yinseinner://kefu';
|
||||
break;
|
||||
case '11':
|
||||
item.href = `yinseinner://horizontalVideo?id=${id}`;
|
||||
break;
|
||||
case '12':
|
||||
item.href = `yinseinner://userHomePage?uid=${id}`;
|
||||
break;
|
||||
case '13':
|
||||
item.href = 'yinseinner://gameWallet';
|
||||
break;
|
||||
// case '14':
|
||||
// case '15':
|
||||
// item.href = `yinseinner://postDetails?id=${id}`;
|
||||
// break;
|
||||
}
|
||||
jumpAdv(item);
|
||||
}
|
||||
onMounted(() => {
|
||||
window.jumpKeywords = jumpKeywords;
|
||||
const { len } = getLines();
|
||||
if (len > props.maxLines) {
|
||||
state.showSlotNode = true;
|
||||
nextTick(() => {
|
||||
state.slotBoxWidth = <number>slotRef.value?.clientWidth;
|
||||
state.textBoxWidth = <number>textOverflow.value?.clientWidth;
|
||||
calculateOffset(0, props.text.length);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.text-overflow {
|
||||
line-height: 20px;
|
||||
}
|
||||
.slot-box {
|
||||
display: inline-block;
|
||||
color: rgba(62, 100, 234);
|
||||
}
|
||||
.overEllipsis {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.choosen {
|
||||
font-size: 12px;
|
||||
height: 18px;
|
||||
width: 37px;
|
||||
text-align: center;
|
||||
line-height: 18px;
|
||||
border-radius: 3px;
|
||||
color: #ffffff;
|
||||
// background: linear-gradient(to right, rgb(132, 164, 249), rgb(43, 93, 222));
|
||||
background: #f52c56;
|
||||
display: inline-block;
|
||||
margin: 0px 5px 0px 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div ref="textOverflow" class="text-overflow" :style="boxStyle">
|
||||
<span ref="overEllipsis" class="overEllipsis" v-html="rearrangeText(realText)"></span>
|
||||
<span class="slot-box" ref="slotRef" v-if="state.showSlotNode">
|
||||
<slot :click-toggle="toggle" :expanded="state.expanded"></slot>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { jumpAdv } from '@/utils/index';
|
||||
import { ref, reactive, computed, nextTick, onMounted } from 'vue';
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
maxLines: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
isChoosen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const overEllipsis = ref<HTMLElement>();
|
||||
const slotRef = ref<HTMLElement>();
|
||||
const textOverflow = ref<HTMLElement>();
|
||||
const state = reactive({
|
||||
offset: props.text.length,
|
||||
expanded: true,
|
||||
slotBoxWidth: 0,
|
||||
textBoxWidth: props.width,
|
||||
showSlotNode: false,
|
||||
isChoosen: props.isChoosen,
|
||||
});
|
||||
const boxStyle = computed(() => {
|
||||
if (props.width) {
|
||||
return {
|
||||
width: props.width + 'px',
|
||||
};
|
||||
}
|
||||
return '0px';
|
||||
});
|
||||
const realText = computed(() => {
|
||||
// 是否被截取
|
||||
const isCutOut = state.offset !== props.text.length;
|
||||
let realText = props.text;
|
||||
if (isCutOut && !state.expanded) {
|
||||
realText = props.text.slice(0, state.offset) + '...';
|
||||
}
|
||||
return realText;
|
||||
});
|
||||
const calculateOffset = (from: number, to: number) => {
|
||||
nextTick(() => {
|
||||
if (Math.abs(from - to) <= 1) return;
|
||||
if (isOverflow()) {
|
||||
to = state.offset;
|
||||
} else {
|
||||
from = state.offset;
|
||||
}
|
||||
state.offset = Math.floor((from + to) / 2);
|
||||
calculateOffset(from, to);
|
||||
});
|
||||
};
|
||||
const isOverflow = () => {
|
||||
const { len, lastWidth } = getLines();
|
||||
if (len < props.maxLines) {
|
||||
return false;
|
||||
}
|
||||
if (props.maxLines) {
|
||||
// 超出部分 行数 > 最大行数 或则 已经是最大行数但最后一行宽度 + 后面内容超出正常宽度
|
||||
const lastLineOver = !!(len === props.maxLines && lastWidth + state.slotBoxWidth > state.textBoxWidth);
|
||||
if (len > props.maxLines || lastLineOver) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const getLines = () => {
|
||||
const clientRects: any = overEllipsis.value?.getClientRects();
|
||||
return {
|
||||
len: clientRects.length,
|
||||
lastWidth: clientRects[clientRects.length - 1]?.width,
|
||||
};
|
||||
};
|
||||
const toggle = () => {
|
||||
state.expanded = !state.expanded;
|
||||
if (!state.expanded) {
|
||||
calculateOffset(0, props.text.length);
|
||||
}
|
||||
};
|
||||
const rearrangeText = (text: any) => {
|
||||
let originalText = '';
|
||||
if (props.text && props.text.indexOf('##') != -1) {
|
||||
let startIndex = props.text.indexOf('{');
|
||||
let endIndex = props.text.indexOf('}');
|
||||
let jsonText = props.text.substring(startIndex, endIndex + 1);
|
||||
let jsonObj = JSON.parse(jsonText);
|
||||
let texts = '##' + jsonText + '##';
|
||||
originalText = text.replace(
|
||||
texts,
|
||||
`<div onclick="jumpKeywords('${jsonObj.type}', '${jsonObj.id}')" style='color:rgb(162,203,240);display: inline-block;'>${jsonObj.txt}<div style="height: 9px;width: 9px;display: inline-block;margin-bottom: 3px;background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAYCAMAAADJYP15AAAAOVBMVEVHcEyh0fuh0fyi0f2h0fyg0Pyf1f+f0/yfz/+h0fyfz/+h0Pyg0Pyh0fyf0vyi0vyh0fyh0fyg0fw8+yy6AAAAEXRSTlMAkLBwgPAwPxDGIODfoFBgXzlWcyEAAAC6SURBVCjPbZDbEsMgCEQNarxU027+/2MLFQ3pZF9kduQA65wof7aCRmF3VqlB1fMy84ZLLU1b3JdPrh6dq6igxDVpb3oBZSCYSwtYIxCkeDPhmuM8Y5Ts7VaMqfwUIFmbgIMfHni7IQy4Njmzrr+alhTKTd24WaE7bjNp3sMXt2oGYlyfeWgcq2c6zzPMWKMkR4Ejx/os+GKSXREKMg6rSGcxEdUjeFloj3OVuzhRbA++/09J1R9///QF6MAK320wOcMAAAAASUVORK5CYII=');background-size: 100% 100%;"></div></div>`,
|
||||
);
|
||||
return originalText;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
function jumpKeywords(type: any, id: any) {
|
||||
window.event.stopPropagation();
|
||||
let item = {
|
||||
href: '',
|
||||
};
|
||||
switch (type) {
|
||||
case '1':
|
||||
item.href = 'yinseinner://memberCentre';
|
||||
break;
|
||||
case '2':
|
||||
item.href = 'yinseinner://sharePromote';
|
||||
break;
|
||||
case '3':
|
||||
item.href = 'yinseinner://incomeCenter';
|
||||
break;
|
||||
case '4':
|
||||
item.href = 'yinseinner://taskHall';
|
||||
break;
|
||||
case '5':
|
||||
item.href = `yinseinner://taskDetails?id=${id}`;
|
||||
break;
|
||||
case '6':
|
||||
item.href = 'yinseinner://wishList';
|
||||
break;
|
||||
case '7':
|
||||
item.href = 'yinseinner://creationCenter';
|
||||
break;
|
||||
case '8':
|
||||
item.href = 'yinseinner://myCertification';
|
||||
break;
|
||||
case '9':
|
||||
item.href = 'yinseinner://announcement';
|
||||
break;
|
||||
case '10':
|
||||
item.href = 'yinseinner://kefu';
|
||||
break;
|
||||
case '11':
|
||||
item.href = `yinseinner://horizontalVideo?id=${id}`;
|
||||
break;
|
||||
case '12':
|
||||
item.href = `yinseinner://userHomePage?uid=${id}`;
|
||||
break;
|
||||
case '13':
|
||||
item.href = 'yinseinner://gameWallet';
|
||||
break;
|
||||
// case '14':
|
||||
// case '15':
|
||||
// item.href = `yinseinner://postDetails?id=${id}`;
|
||||
// break;
|
||||
}
|
||||
jumpAdv(item);
|
||||
}
|
||||
onMounted(() => {
|
||||
window.jumpKeywords = jumpKeywords;
|
||||
const { len } = getLines();
|
||||
if (len > props.maxLines) {
|
||||
console.log(state.expanded);
|
||||
state.showSlotNode = true;
|
||||
nextTick(() => {
|
||||
state.slotBoxWidth = <number>slotRef.value?.clientWidth;
|
||||
state.textBoxWidth = <number>textOverflow.value?.clientWidth;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.slot-box {
|
||||
display: inline-block;
|
||||
color: rgba(62, 100, 234);
|
||||
}
|
||||
.overEllipsis {
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
// color: $greyDeepColor;
|
||||
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.choosen {
|
||||
font-size: 12px;
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(to right, rgb(132, 164, 249), rgb(43, 93, 222));
|
||||
display: inline-block;
|
||||
margin: 0 5px 0 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<van-popup v-model:show="isShow" @close="emit('close')">
|
||||
<div class="warp" :class="{ 'warp--glow': glow }">
|
||||
<div class="content_warp" :class="{ 'app-popup-surface': glow }">
|
||||
<div class="title_box">
|
||||
<div class="title" :class="{ 'app-popup-title': glow }">{{ props.title }}</div>
|
||||
</div>
|
||||
<div class="divider" :class="{ 'app-popup-divider': glow }" />
|
||||
<div class="content" :class="{ 'app-popup-body': glow }">
|
||||
<p class="tip" v-html="props.tip"></p>
|
||||
<div class="btn_box">
|
||||
<div
|
||||
v-if="showCancel"
|
||||
class="ISee flex-center-center"
|
||||
:class="glow ? 'app-popup-secondary' : ''"
|
||||
@click="emit('close')"
|
||||
>
|
||||
{{ cancelText }}
|
||||
</div>
|
||||
<div
|
||||
class="ISee flex-center-center"
|
||||
:class="[glow ? 'app-popup-primary' : '', { 'wd-full': !showCancel }]"
|
||||
@click="jumpPath('/member')"
|
||||
>
|
||||
{{ btnText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</Teleport>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps({
|
||||
// Only the matching Figma nickname/avatar prompts opt into the glow style.
|
||||
glow: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tip: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消',
|
||||
},
|
||||
btnText: {
|
||||
type: String,
|
||||
default: '立即开通',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '温馨提示',
|
||||
},
|
||||
});
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
const emit = defineEmits(['close']);
|
||||
function jumpPath(path: any) {
|
||||
router.push(path);
|
||||
emit('close');
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.warp {
|
||||
color: #fff;
|
||||
|
||||
.content_warp {
|
||||
width: 303px;
|
||||
background: #0f0f0f;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.title_box {
|
||||
.title {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
margin: 10px 0 5px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
margin-top: 20px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.content {
|
||||
font-size: 16px;
|
||||
margin-top: 20px;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
|
||||
.tip {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn_box {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
margin: 20px 0 0;
|
||||
gap: 10px;
|
||||
|
||||
.ISee {
|
||||
width: 105px;
|
||||
height: 44px;
|
||||
background: var(--app-primary);
|
||||
color: var(--app-primary-text);
|
||||
border-radius: 3px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.wd-full {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Figma 33115:139452 — VIP 开通提示弹窗
|
||||
&.warp--glow {
|
||||
.content_warp {
|
||||
width: 311px;
|
||||
min-height: 236px;
|
||||
padding: 30px 24px 24px;
|
||||
border-radius: 16px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.title_box {
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
height: 1px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.tip {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.btn_box {
|
||||
margin-top: auto;
|
||||
padding-top: 24px;
|
||||
gap: 0;
|
||||
justify-content: center;
|
||||
|
||||
.ISee {
|
||||
width: 100%;
|
||||
max-width: 263px;
|
||||
height: 42px;
|
||||
border-radius: 22px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #1f2029;
|
||||
background: #ffc000;
|
||||
}
|
||||
|
||||
.wd-full {
|
||||
width: 100%;
|
||||
max-width: 263px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<van-popup v-model:show="isShow">
|
||||
<div class="warp">
|
||||
<div class="content_warp">
|
||||
<div class="content">
|
||||
<p class="title">{{props.tip}}</p>
|
||||
|
||||
<div class="btn_box">
|
||||
<div class="ISee flex-center-center" @click="emit('close')">取消</div>
|
||||
<div class="ISee flex-center-center" @click="jumpPath('/member')">立即开通</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tip:{
|
||||
type: String,
|
||||
default: '',
|
||||
}
|
||||
});
|
||||
const isShow = computed(() => {
|
||||
return props.isOpen;
|
||||
});
|
||||
const emit = defineEmits(['close']);
|
||||
function jumpPath(path:any) {
|
||||
router.push(path);
|
||||
emit('close');
|
||||
}
|
||||
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.warp {
|
||||
padding-bottom: 80px;
|
||||
color:#fff;
|
||||
.content_warp {
|
||||
width: 357px;
|
||||
height: 334px;
|
||||
background: url('@/assets/images/png/tip_bg.png') no-repeat;
|
||||
background-size: cover;
|
||||
padding-top: 200px;
|
||||
.content {
|
||||
height: 205px;
|
||||
font-size: 12px;
|
||||
.title {
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
}
|
||||
.btn_box {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
margin: 50px 0 0 0;
|
||||
.ISee {
|
||||
width: 105px;
|
||||
height: 35px;
|
||||
background: linear-gradient(to right,rgb( 132, 164, 249),rgb( 43, 93, 222));
|
||||
color: #fff;
|
||||
margin: 0 10px 0 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.ISee:nth-child(2){
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<van-overlay :show="isShow" z-index="999">
|
||||
<div class="warp">
|
||||
<div class="content_warp app-popup-surface">
|
||||
<div class="title app-popup-title">系统已经升级</div>
|
||||
<div class="content app-popup-body">
|
||||
<h2>请更新最新版本</h2>
|
||||
</div>
|
||||
<div class="btnBox">
|
||||
<div class="btn app-popup-primary" @click="toUpgrade">立即升级</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-overlay>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
const isShow = ref(true);
|
||||
const toUpgrade = () => {
|
||||
window.location.href = 'https://d2yvsvw55p1b3a.cloudfront.net/qaz/wsx/edc/mobileconfig/qy2';
|
||||
};
|
||||
const toPotato = () => {
|
||||
window.location.href = 'https://psslk.org/haijiaoshequ99';
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.warp {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.content_warp {
|
||||
background-color: #202133;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px 50px 30px;
|
||||
box-sizing: border-box;
|
||||
.title {
|
||||
font-size: 32px;
|
||||
text-align: center;
|
||||
color: #009fe8;
|
||||
}
|
||||
.content {
|
||||
margin-top: 24px;
|
||||
font-size: 12px;
|
||||
h2 {
|
||||
line-height: 45px;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
color: rgb(255, 153, 0);
|
||||
}
|
||||
}
|
||||
.btnBox {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
.btn {
|
||||
height: 38px;
|
||||
width: 184px;
|
||||
margin: 0 auto;
|
||||
line-height: 38px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
background-color: #009fe8;
|
||||
border-radius: 38px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.small {
|
||||
height: 38px;
|
||||
width: 108px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div class="upload-img" @click.stop>
|
||||
<van-uploader
|
||||
v-model="state.fileList"
|
||||
:after-read="afterRead"
|
||||
@delete="delImg"
|
||||
:multiple="multiple"
|
||||
:before-read="beforeRead"
|
||||
:max-count="maxCount"
|
||||
>
|
||||
<div class="addBox">
|
||||
<div class="upload-text" v-if="props.isCover">
|
||||
<SvgIcon name="upload_add" height="24px" width="24px" style="color: #9a9a9a"></SvgIcon>
|
||||
<span>{{ text }}</span>
|
||||
</div>
|
||||
<div class="upload-text" v-else>
|
||||
<SvgIcon name="upload_add" height="24px" width="24px" style="color: #9a9a9a"></SvgIcon>
|
||||
</div>
|
||||
</div>
|
||||
<template #preview-delete>
|
||||
<SvgIcon name="upload_del" height="24px" width="24px" style="color: #000" />
|
||||
</template>
|
||||
</van-uploader>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import AppApi from '@/service/app';
|
||||
import { showToast } from 'vant';
|
||||
import { reactive } from 'vue';
|
||||
const props = defineProps({
|
||||
maxCount: {
|
||||
type: String,
|
||||
default: '99',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isCover: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: '上传图片',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['uploadSuccess', 'delImg']);
|
||||
const state = reactive({
|
||||
fileList: [],
|
||||
maxSize: 1024 * 1024 * 10,
|
||||
});
|
||||
const beforeRead = (file: any) => {
|
||||
if (file.length > props.maxCount) {
|
||||
showToast(`一次最多上传${props.maxCount}张图`);
|
||||
return false;
|
||||
}
|
||||
if (file.length) {
|
||||
file.map(async (item: any) => {
|
||||
if (item.size > state.maxSize) {
|
||||
showToast('单个图片最大为10M');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (file.size > state.maxSize) {
|
||||
showToast('单个图片最大为10M');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
// 图片上传
|
||||
const afterRead = async (file: any) => {
|
||||
if (file.length) {
|
||||
file.map(async (item: any) => {
|
||||
let newFile = await compress(item.file);
|
||||
item.file = newFile;
|
||||
await uploadImg(item);
|
||||
});
|
||||
} else {
|
||||
let newFile = await compress(file.file);
|
||||
file.file = newFile;
|
||||
uploadImg(file);
|
||||
}
|
||||
};
|
||||
//图片上传
|
||||
const uploadImg = async (fileInfo: any) => {
|
||||
let req = new FormData();
|
||||
req.append('upload', fileInfo.file);
|
||||
fileInfo.status = 'uploading';
|
||||
fileInfo.message = '上传中...';
|
||||
try {
|
||||
let ret: any = await AppApi.uploadFile(req);
|
||||
fileInfo.status = 'done';
|
||||
fileInfo.message = '上传成功';
|
||||
emit('uploadSuccess', ret.coverImg);
|
||||
} catch (e) {
|
||||
fileInfo.status = 'failed';
|
||||
fileInfo.message = '上传失败';
|
||||
}
|
||||
};
|
||||
//将base64转换为文件对象
|
||||
const convertBase64ToBlob = (base64: any) => {
|
||||
var base64Arr = base64.split(',');
|
||||
var imgtype = '';
|
||||
var base64String = '';
|
||||
if (base64Arr.length > 1) {
|
||||
//如果是图片base64,去掉头信息
|
||||
base64String = base64Arr[1];
|
||||
imgtype = base64Arr[0].substring(base64Arr[0].indexOf(':') + 1, base64Arr[0].indexOf(';'));
|
||||
}
|
||||
// 将base64解码
|
||||
var bytes = atob(base64String);
|
||||
//var bytes = base64;
|
||||
var bytesCode = new ArrayBuffer(bytes.length);
|
||||
// 转换为类型化数组
|
||||
var byteArray = new Uint8Array(bytesCode);
|
||||
|
||||
// 将base64转换为ascii码
|
||||
for (var i = 0; i < bytes.length; i++) {
|
||||
byteArray[i] = bytes.charCodeAt(i);
|
||||
}
|
||||
// 生成Blob对象(文件对象)
|
||||
return new Blob([bytesCode], { type: imgtype });
|
||||
};
|
||||
// 压缩图片
|
||||
const compress = (file: any) => {
|
||||
if (!file || !window.FileReader) return;
|
||||
if (/^image/.test(file.type)) {
|
||||
let reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
return new Promise((resolve) => {
|
||||
reader.onloadend = function () {
|
||||
let result: any = reader.result;
|
||||
let img: any = new Image();
|
||||
img.src = result;
|
||||
if (result.length <= 200 * 1024) {
|
||||
let blob = convertBase64ToBlob(result);
|
||||
let files = new File([blob], file.name, { type: blob.type });
|
||||
resolve(files);
|
||||
return;
|
||||
}
|
||||
img.onload = function () {
|
||||
let canvas = document.createElement('canvas');
|
||||
let ctx: any = canvas.getContext('2d');
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
// 铺底色
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
//进行最小压缩
|
||||
let ndata = canvas.toDataURL('image/jpeg', 0.3);
|
||||
let blob = convertBase64ToBlob(ndata);
|
||||
let files = new File([blob], file.name, { type: blob.type });
|
||||
resolve(files);
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
// 删除回调
|
||||
const delImg = (_file: any, detail: any) => {
|
||||
emit('delImg', detail.index);
|
||||
};
|
||||
// 清理图片列表
|
||||
const clearImg = () => {
|
||||
state.fileList = [];
|
||||
};
|
||||
defineExpose({
|
||||
clearImg,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload-img {
|
||||
.addBox {
|
||||
width: 111px;
|
||||
height: 111px;
|
||||
border-radius: 9px;
|
||||
border: dashed 1px #989898;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
|
||||
.upload-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.van-uploader__preview-delete) {
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.van-uploader__preview-delete-icon {
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.van-uploader__wrapper) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
:deep(.van-uploader__upload) {
|
||||
margin: 0;
|
||||
width: 111px;
|
||||
height: 111px;
|
||||
}
|
||||
|
||||
:deep(.van-uploader__mask) {
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
:deep(.van-uploader__preview) {
|
||||
width: 111px;
|
||||
height: 111px;
|
||||
border-radius: 9px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.van-image) {
|
||||
width: 111px;
|
||||
height: 111px;
|
||||
border-radius: 9px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<div class="upload-img" @click.stop>
|
||||
<van-uploader
|
||||
v-model="state.fileList"
|
||||
:after-read="afterRead"
|
||||
@delete="delImg"
|
||||
:multiple="multiple"
|
||||
:before-read="beforeRead"
|
||||
:max-count="maxCount"
|
||||
:preview-image="props.isPreview"
|
||||
>
|
||||
<div :class="[props.commentType]" />
|
||||
</van-uploader>
|
||||
<div :class="[props.commentType]" v-if="state.isShow && !props.isPreview" />
|
||||
<!-- <img
|
||||
:src="state.img"
|
||||
alt=""
|
||||
style="border-radius: 6px"
|
||||
v-else-if="state.isShow && props.isPreview && state.img"
|
||||
/>
|
||||
<img
|
||||
v-if="state.isShow && props.isPreview && state.img"
|
||||
src="@/assets/images/png/pop_close.png"
|
||||
class="del"
|
||||
name="previewDelete"
|
||||
@click.stop="delImg"
|
||||
/> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import AppApi from '@/service/app';
|
||||
import { showToast } from 'vant';
|
||||
import { reactive } from 'vue';
|
||||
const props = defineProps({
|
||||
maxCount: {
|
||||
type: [String, Number],
|
||||
default: '99',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isCover: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isPreview: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
commentType: {
|
||||
type: String, //normal默认底部样式,short短视频底部样式
|
||||
default: 'normal',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['uploadSuccess', 'delImg']);
|
||||
const state = reactive({
|
||||
fileList: [],
|
||||
maxSize: 1024 * 1024 * 10,
|
||||
isShow: false,
|
||||
img: '',
|
||||
});
|
||||
const beforeRead = (file: any) => {
|
||||
if (file.length > props.maxCount) {
|
||||
showToast(`一次最多上传${props.maxCount}张图`);
|
||||
return false;
|
||||
}
|
||||
if (file.length) {
|
||||
file.map(async (item: any) => {
|
||||
if (item.size > state.maxSize) {
|
||||
showToast('单个图片最大为10M');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (file.size > state.maxSize) {
|
||||
showToast('单个图片最大为10M');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
// 图片上传
|
||||
const afterRead = async (file: any) => {
|
||||
if (file.length) {
|
||||
file.map(async (item: any) => {
|
||||
let newFile = await compress(item.file);
|
||||
item.file = newFile;
|
||||
await uploadImg(item);
|
||||
});
|
||||
} else {
|
||||
let newFile = await compress(file.file);
|
||||
file.file = newFile;
|
||||
uploadImg(file);
|
||||
}
|
||||
};
|
||||
//图片上传
|
||||
const uploadImg = async (fileInfo: any) => {
|
||||
let req = new FormData();
|
||||
req.append('upload', fileInfo.file);
|
||||
fileInfo.status = 'uploading';
|
||||
fileInfo.message = '上传中...';
|
||||
state.isShow = true;
|
||||
if (props.isPreview && !state.img) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e: any) {
|
||||
state.img = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(fileInfo.file);
|
||||
}
|
||||
try {
|
||||
let ret: any = await AppApi.uploadFile(req);
|
||||
fileInfo.status = 'done';
|
||||
fileInfo.message = '上传成功';
|
||||
emit('uploadSuccess', ret.coverImg);
|
||||
} catch (e) {
|
||||
fileInfo.status = 'failed';
|
||||
fileInfo.message = '上传失败';
|
||||
}
|
||||
};
|
||||
//将base64转换为文件对象
|
||||
const convertBase64ToBlob = (base64: any) => {
|
||||
var base64Arr = base64.split(',');
|
||||
var imgtype = '';
|
||||
var base64String = '';
|
||||
if (base64Arr.length > 1) {
|
||||
//如果是图片base64,去掉头信息
|
||||
base64String = base64Arr[1];
|
||||
imgtype = base64Arr[0].substring(base64Arr[0].indexOf(':') + 1, base64Arr[0].indexOf(';'));
|
||||
}
|
||||
// 将base64解码
|
||||
var bytes = atob(base64String);
|
||||
//var bytes = base64;
|
||||
var bytesCode = new ArrayBuffer(bytes.length);
|
||||
// 转换为类型化数组
|
||||
var byteArray = new Uint8Array(bytesCode);
|
||||
|
||||
// 将base64转换为ascii码
|
||||
for (var i = 0; i < bytes.length; i++) {
|
||||
byteArray[i] = bytes.charCodeAt(i);
|
||||
}
|
||||
// 生成Blob对象(文件对象)
|
||||
return new Blob([bytesCode], { type: imgtype });
|
||||
};
|
||||
// 压缩图片
|
||||
const compress = (file: any) => {
|
||||
if (!file || !window.FileReader) return;
|
||||
if (/^image/.test(file.type)) {
|
||||
let reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
return new Promise((resolve) => {
|
||||
reader.onloadend = function () {
|
||||
let result: any = reader.result;
|
||||
let img: any = new Image();
|
||||
img.src = result;
|
||||
if (result.length <= 200 * 1024) {
|
||||
let blob = convertBase64ToBlob(result);
|
||||
let files = new File([blob], file.name, { type: blob.type });
|
||||
resolve(files);
|
||||
return;
|
||||
}
|
||||
img.onload = function () {
|
||||
let canvas = document.createElement('canvas');
|
||||
let ctx: any = canvas.getContext('2d');
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
// 铺底色
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
//进行最小压缩
|
||||
let ndata = canvas.toDataURL('image/jpeg', 0.3);
|
||||
let blob = convertBase64ToBlob(ndata);
|
||||
let files = new File([blob], file.name, { type: blob.type });
|
||||
resolve(files);
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
// 删除回调
|
||||
const delImg = (_file: any, detail: any) => {
|
||||
emit('delImg', detail.index);
|
||||
};
|
||||
// 清理图片列表
|
||||
const clearImg = () => {
|
||||
state.fileList = [];
|
||||
};
|
||||
const setIMG = () => {
|
||||
state.isShow = false;
|
||||
state.img = '';
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
clearImg();
|
||||
setIMG();
|
||||
};
|
||||
defineExpose({
|
||||
clearImg,
|
||||
setIMG,
|
||||
reset,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload-img {
|
||||
display: flex;
|
||||
.normal,
|
||||
.short {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
background: url('@/assets/icons/comment_image.svg') no-repeat center / contain;
|
||||
}
|
||||
.addBox {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
|
||||
.upload-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
span {
|
||||
font-size: 9px;
|
||||
color: rgb(129, 129, 129);
|
||||
margin-top: 5.4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.van-uploader__preview-delete) {
|
||||
border-radius: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// border: 1px solid #ffffff;
|
||||
background: #fff;
|
||||
.van-uploader__preview-delete-icon {
|
||||
top: 0;
|
||||
right: 0;
|
||||
// transform: scale(0.7);
|
||||
color: #000;
|
||||
top: -1px;
|
||||
right: -1px;
|
||||
}
|
||||
}
|
||||
:deep(.van-uploader__mask) {
|
||||
border-radius: 5px;
|
||||
}
|
||||
:deep(.van-uploader__preview) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 5px;
|
||||
margin: 0;
|
||||
}
|
||||
:deep(.van-image) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:deep(.van-uploader__mask-message) {
|
||||
display: none;
|
||||
}
|
||||
// .preview-box {
|
||||
// position: relative;
|
||||
// .del {
|
||||
// position: absolute;
|
||||
// top: -2px;
|
||||
// right: -2px;
|
||||
// z-index: 2;
|
||||
// width: 10px;
|
||||
// height: 10px;
|
||||
// flex-shrink: 0;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,336 @@
|
||||
<template>
|
||||
<div class="upload-video" @click.stop>
|
||||
<van-uploader accept="video/*" :max-count="1" :after-read="afterVideo" v-if="!state.startUploadVideo">
|
||||
<div class="upload-box">
|
||||
<SvgIcon name="upload_add" height="24px" width="24px" style="color: #9a9a9a"></SvgIcon>
|
||||
<span>添加视频</span>
|
||||
</div>
|
||||
</van-uploader>
|
||||
<!-- 上传中样式 -->
|
||||
<div v-if="state.startUploadVideo" class="uploading">
|
||||
<video class="previewVideo" id="video" style="object-fit: cover"></video>
|
||||
<div class="video-mask"></div>
|
||||
<van-circle
|
||||
v-model:current-rate="state.currentRate"
|
||||
:rate="state.uploadStatue.percent"
|
||||
:speed="100"
|
||||
color="#00b95b"
|
||||
:text="state.currentRate + '%'"
|
||||
size="50px"
|
||||
class="circle"
|
||||
/>
|
||||
<p :class="state.uploadStatue.statusText == '上传成功' ? 'green' : 'F5164E'">
|
||||
{{ state.uploadStatue.statusText }}
|
||||
</p>
|
||||
<div
|
||||
class="close"
|
||||
v-if="state.uploadStatue.statusText == '上传成功' || state.uploadStatue.statusText == '上传失败'"
|
||||
@click="uploadDelete"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import SparkMD5 from 'spark-md5';
|
||||
import { Buffer } from 'buffer';
|
||||
import { reactive, nextTick } from 'vue';
|
||||
import CommunityApi from '@/service/community';
|
||||
|
||||
const state = reactive({
|
||||
requestAll: <any>[], //总请求
|
||||
requestNum: 0, //正在请求的数量
|
||||
requestMaxNum: 6, //同时最大请求数量
|
||||
chunk_size: 1024 * 1024 * 2, //分片大小
|
||||
startUploadVideo: false, //视频是否上传中
|
||||
currentRate: 0,
|
||||
uploadStatue: {
|
||||
statusText: '上传中,请勿退出',
|
||||
percent: 0, //当前上传文件执行的百分比
|
||||
},
|
||||
totalProgress: <any>{}, //总进度
|
||||
sendVideoSucNum: 0, //分片上传视频,成功收到的返回
|
||||
videoHeight: 0,
|
||||
uploadData: {
|
||||
newsType: 'SP', //上传类型 COVER-图集 SP-视频
|
||||
tags: [], //视频标签数组
|
||||
playTime: 0, //视频时长
|
||||
cover: '', //封面
|
||||
coverThumb: '', //封面小图
|
||||
seriesCover: [], // 图集图片地址
|
||||
via: '', //视频来源
|
||||
coins: 0, //观看金币
|
||||
size: 0, //文件大小
|
||||
mimeType: '', //文件类型
|
||||
actor: '', //演员名字
|
||||
sourceURL: '', //资源url 为SP时 必传
|
||||
sourceID: '', //上��视频成功后 返回���ID 为SP时必传
|
||||
filename: '', //文件名
|
||||
resolution: '', //分辨率
|
||||
ratio: 1, //视频比例
|
||||
md5: '', //文件摘要
|
||||
freeTime: 0, //免费观影时长
|
||||
location: {
|
||||
city: '',
|
||||
}, //地理位置信息
|
||||
isActivity: false, //是否参加大赛
|
||||
},
|
||||
newUpload: {
|
||||
sourceID: '', //上��视频成功后 返回���ID
|
||||
md5: '', //文件摘要
|
||||
type: 'vid',
|
||||
src: '',
|
||||
height: '',
|
||||
width: '',
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['uploadVideoSuccess']);
|
||||
const fileReaderPromisify = (file: any) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let reader = new FileReader();
|
||||
let Spark = new SparkMD5.ArrayBuffer();
|
||||
reader.readAsArrayBuffer(file);
|
||||
reader.onload = function (e: any) {
|
||||
Spark.append(e.target.result);
|
||||
let md5 = Spark.end();
|
||||
resolve(md5);
|
||||
};
|
||||
reader.onerror = (err) => {
|
||||
reject(err);
|
||||
};
|
||||
});
|
||||
};
|
||||
// 上传视频
|
||||
const afterVideo = async (e: any) => {
|
||||
state.startUploadVideo = true;
|
||||
let file = e.file;
|
||||
let fileMd5: any = await fileReaderPromisify(file);
|
||||
let file_chunks = getFileChunks(file);
|
||||
let length = file_chunks.length;
|
||||
for (let index in file_chunks) {
|
||||
let chunk = file_chunks[Number(index)];
|
||||
let opts = {
|
||||
pos: Number(index) + 1,
|
||||
totalPos: length,
|
||||
data: Buffer.from(await chunk.arrayBuffer(), 'utf8').toString('base64'),
|
||||
id: fileMd5,
|
||||
};
|
||||
state.uploadData.md5 = fileMd5;
|
||||
state.newUpload.md5 = fileMd5;
|
||||
state.requestAll.push(opts);
|
||||
}
|
||||
getVideoPlayerInfo(file); //获取视频宽高
|
||||
requestConcurrency();
|
||||
};
|
||||
//限制请求数量
|
||||
const requestConcurrency = () => {
|
||||
while (state.requestAll.length > 0 && state.requestNum <= state.requestMaxNum && state.requestNum >= 0) {
|
||||
let opts = state.requestAll.shift();
|
||||
upload(opts);
|
||||
}
|
||||
};
|
||||
// 将视频分片
|
||||
const getFileChunks = (file: any) => {
|
||||
let { size } = file;
|
||||
let total_chunks = Math.ceil(size / state.chunk_size);
|
||||
let file_chunks = [];
|
||||
if (size > state.chunk_size) {
|
||||
for (let i = 0; i < total_chunks; i++) {
|
||||
let start = i * state.chunk_size;
|
||||
let end = (i + 1) * state.chunk_size;
|
||||
let chunk = file.slice(start, end);
|
||||
state.totalProgress[i] = 0;
|
||||
file_chunks.push(chunk);
|
||||
}
|
||||
} else {
|
||||
file_chunks.push(file);
|
||||
state.totalProgress[0] = 0;
|
||||
}
|
||||
return file_chunks;
|
||||
};
|
||||
// 开始上传
|
||||
const upload = async (options: any) => {
|
||||
try {
|
||||
state.requestNum++;
|
||||
let callback = (processCallback: any) => {
|
||||
let percent = parseInt(((processCallback.loaded / processCallback.total) * 100).toString());
|
||||
state.totalProgress[options.pos - 1] = percent.toFixed(2);
|
||||
let strArr = Object.values(state.totalProgress);
|
||||
let numberArr = strArr.map(Number);
|
||||
state.uploadStatue.percent = Number(average(numberArr).toFixed(1));
|
||||
};
|
||||
let ret: any = await CommunityApi.uploadVideo(options, callback);
|
||||
state.sendVideoSucNum++;
|
||||
if (state.uploadStatue.percent == 100) {
|
||||
// 所有分片请求发送完毕,等待返回
|
||||
state.uploadStatue.statusText = '处理中,请勿退出';
|
||||
}
|
||||
if (state.uploadStatue.percent == 100 && state.sendVideoSucNum == options.totalPos) {
|
||||
state.uploadStatue.statusText = '上传成功';
|
||||
state.uploadData.sourceURL = ret.videoUri;
|
||||
state.uploadData.sourceID = ret.id;
|
||||
state.newUpload.sourceID = ret.id;
|
||||
state.newUpload.src = ret.videoUri;
|
||||
emit('uploadVideoSuccess', state.uploadData);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
state.currentRate = 0;
|
||||
state.uploadStatue.percent = 0;
|
||||
state.uploadStatue.statusText = '上传失败';
|
||||
state.sendVideoSucNum = 0;
|
||||
} finally {
|
||||
state.requestNum--;
|
||||
requestConcurrency();
|
||||
}
|
||||
};
|
||||
// 获取视频信息
|
||||
const getVideoPlayerInfo = (file: any) => {
|
||||
state.uploadData.size = file.size;
|
||||
state.uploadData.filename = file.name;
|
||||
state.uploadData.mimeType = file.type;
|
||||
// state.newUpload.type = file.type;
|
||||
nextTick(() => {
|
||||
const videoElement: any = document.getElementById('video');
|
||||
videoElement.src = URL.createObjectURL(file);
|
||||
videoElement.addEventListener('loadedmetadata', () => {
|
||||
state.uploadData.playTime = parseInt(videoElement.duration);
|
||||
state.videoHeight = videoElement.videoHeight;
|
||||
state.uploadData.ratio = Number((videoElement.videoWidth / videoElement.videoHeight).toFixed(2));
|
||||
state.uploadData.resolution = videoElement.videoWidth + 'x' + videoElement.videoHeight;
|
||||
state.newUpload.height = videoElement.videoHeight;
|
||||
state.newUpload.width = videoElement.videoWidth;
|
||||
});
|
||||
});
|
||||
};
|
||||
// 还原默认数据
|
||||
const reductionData = () => {
|
||||
state.uploadData = {
|
||||
newsType: 'SP', //上传类型 COVER-图集 SP-视频
|
||||
tags: [], //视频标签数组
|
||||
playTime: 0, //视频时长
|
||||
cover: '', //封面
|
||||
coverThumb: '', //封面小图
|
||||
seriesCover: [], // 图集图片地址
|
||||
via: '', //视频来源
|
||||
coins: 0, //观看金币
|
||||
size: 0, //文件大小
|
||||
mimeType: '', //文件类型
|
||||
actor: '', //演员名字
|
||||
sourceURL: '', //资源url 为SP时 必传
|
||||
sourceID: '', //上��视频成功后 返回���ID 为SP时必传
|
||||
filename: '', //文件名
|
||||
resolution: '', //分辨率
|
||||
ratio: 1, //视频比例
|
||||
md5: '', //文件摘要
|
||||
freeTime: 0, //免费观影时长
|
||||
location: {
|
||||
city: '',
|
||||
}, //地理位置信息
|
||||
isActivity: false, //是否参加大赛
|
||||
};
|
||||
state.newUpload = {
|
||||
sourceID: '',
|
||||
md5: '', //文件摘要
|
||||
type: 'vid',
|
||||
src: '',
|
||||
height: '',
|
||||
width: '',
|
||||
};
|
||||
};
|
||||
// 删除视频
|
||||
const uploadDelete = () => {
|
||||
state.newUpload.src = 'vid';
|
||||
state.uploadData.sourceURL = '';
|
||||
state.uploadStatue.statusText = '上传中,请勿退出';
|
||||
state.uploadStatue.percent = 0;
|
||||
state.currentRate = 0;
|
||||
state.sendVideoSucNum = 0;
|
||||
state.startUploadVideo = false;
|
||||
};
|
||||
// 平均数
|
||||
const average = (nums: any) => {
|
||||
return nums.reduce((a: any, b: any) => a + b) / nums.length;
|
||||
};
|
||||
defineExpose({
|
||||
reductionData,
|
||||
uploadDelete,
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.upload-video {
|
||||
.upload-box {
|
||||
width: 111px;
|
||||
height: 111px;
|
||||
border-radius: 9px;
|
||||
border: dashed 1px #989898;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.upload-box span {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.uploadCover {
|
||||
padding-left: 0.16rem;
|
||||
}
|
||||
/* 上传中 */
|
||||
.uploading {
|
||||
width: 111px;
|
||||
height: 111px;
|
||||
border-radius: 9px;
|
||||
color: #fff;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.previewVideo {
|
||||
width: 111px;
|
||||
height: 111px;
|
||||
border-radius: 9px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.video-mask {
|
||||
width: 111px;
|
||||
height: 111px;
|
||||
border-radius: 9px;
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.circle {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.uploading .close {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: url('@/assets/images/png/community_close.png') center center no-repeat;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
:deep(.van-circle__text) {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user