[Human] chore(project): 初始化导入 91dj H5 客户端项目代码
从本地工程首次入库;.npmrc(含私有 npm 源 token)与 tsbuildinfo 已加入 .gitignore,不随仓库提交。
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user