Files
hgmj_h5/src/components/TabBar/AiDramaFloatBubble.vue
T
谢宇宁 eada71e211 [Human] chore(project): 初始化导入 91dj H5 客户端项目代码
从本地工程首次入库;.npmrc(含私有 npm 源 token)与 tsbuildinfo 已加入 .gitignore,不随仓库提交。
2026-10-08 14:22:35 +07:00

171 lines
4.2 KiB
Vue

<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>