feat: 首页弹窗广告(HOME_POPUP 类型,每次冷启动弹一次,点击跳内部页面,tabBar 页自动 switchTab,失败 toast 兜底)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jacobxu666
2026-09-17 12:01:12 +08:00
co-authored by Claude Fable 5
parent bba2571f25
commit 32f3db7e85
+175 -2
View File
@@ -125,12 +125,34 @@
</view> </view>
</scroll-view> </scroll-view>
<!-- 首页弹窗广告:每次冷启动弹一次,取「首页弹窗」类型排序最前一条 -->
<view
v-if="homePopupVisible"
class="popup-overlay"
:class="{ visible: homePopupAnim }"
@click="closeHomePopup"
>
<image
class="popup-img"
:src="homePopupData?.imageUrl || ''"
mode="widthFix"
@click.stop="onHomePopupClick"
@error="onHomePopupImgError"
/>
<view class="popup-close" @click.stop="closeHomePopup">
<view class="close-x">
<view class="close-line line-a" />
<view class="close-line line-b" />
</view>
</view>
</view>
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted, nextTick } from 'vue'
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app' import { onShow, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { post } from '../../api/request' import { post } from '../../api/request'
import { BASE_URL } from '../../config' import { BASE_URL } from '../../config'
import type { BannerItem } from '../../api/types' import type { BannerItem } from '../../api/types'
@@ -224,6 +246,71 @@ async function fetchSpots() {
} }
} }
/* ====== 首页弹窗广告(HOME_POPUP) ====== */
// xuchao: 首页为 tabBar 页,实例与进程同生命周期,实例级标志即「每次冷启动弹一次」
let homePopupHandled = false
let homePopupFetching = false
const homePopupData = ref<BannerItem | null>(null)
const homePopupVisible = ref(false)
const homePopupAnim = ref(false)
onShow(() => { checkHomePopup() })
/** 首页每次显示时检查;本次启动已弹过/请求中则跳过;接口失败不置标志,下次进首页重试 */
async function checkHomePopup() {
if (homePopupHandled || homePopupFetching) return
homePopupFetching = true
try {
const list = await post<BannerItem[]>('/mpContent/queryList', { channel: 'pingxiang', type: 'HOME_POPUP' })
homePopupHandled = true
const item = (list || []).find(c => c.imageUrl)
if (item) {
homePopupData.value = item
homePopupVisible.value = true
nextTick(() => { homePopupAnim.value = true })
}
} catch {
// 静默失败,下次进首页再试
} finally {
homePopupFetching = false
}
}
/** 点击广告图:先关弹窗,再跳配置的内部页面 */
function onHomePopupClick() {
const url = homePopupData.value?.linkUrl
homePopupAnim.value = false
homePopupVisible.value = false
if (url) navigateInnerPage(url)
}
/** 关闭弹窗(离场动画结束后移除节点) */
function closeHomePopup() {
homePopupAnim.value = false
setTimeout(() => { homePopupVisible.value = false }, 280)
}
/** 图片加载失败直接移除弹窗 */
function onHomePopupImgError() {
homePopupVisible.value = false
}
// xuchao: tabBar 页 navigateTo 会失败,必须走 switchTab
const TAB_PAGE_PATHS = ['/pages/index/index', '/pages/qrcode/index', '/pages/profile/index']
/** xuchao: 小程序内部页面智能跳转(tabBar 走 switchTab,其余 navigateTo,失败兜底提示) */
function navigateInnerPage(url: string) {
const path = url.startsWith('/') ? url : `/${url}`
if (TAB_PAGE_PATHS.includes(path.split('?')[0])) {
uni.switchTab({ url: path })
return
}
uni.navigateTo({
url: path,
fail: () => uni.showToast({ title: '页面暂未开放', icon: 'none' }),
})
}
onShareAppMessage(() => ({ onShareAppMessage(() => ({
title: '爱上萍乡旅游年卡', title: '爱上萍乡旅游年卡',
path: '/pages/index/index', path: '/pages/index/index',
@@ -823,4 +910,90 @@ function goMore() {
.main-scroll { .main-scroll {
flex: 1; flex: 1;
} }
/* ============================================
首页弹窗广告
============================================ */
.popup-overlay {
position: fixed;
inset: 0;
z-index: $z-modal;
background: rgba(0, 0, 0, 0.45);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 280ms cubic-bezier(0.16, 1, 0.3, 1);
&.visible {
opacity: 1;
.popup-img {
transform: scale(1);
opacity: 1;
}
.popup-close {
opacity: 1;
transform: scale(1);
}
}
}
/* 广告图:宽 600rpx 固定,高按原图比例(widthFix)自适应 */
.popup-img {
width: 600rpx;
border-radius: $radius-2xl;
overflow: hidden;
transform: scale(0.88);
opacity: 0;
transition: transform 320ms cubic-bezier(0.16, 1, 0.3, 1),
opacity 260ms cubic-bezier(0.16, 1, 0.3, 1);
}
.popup-close {
width: 72rpx;
height: 72rpx;
border-radius: $radius-full;
border: 2rpx solid rgba(255, 255, 255, 0.35);
background: transparent;
display: flex;
align-items: center;
justify-content: center;
margin-top: $spacing-4;
flex-shrink: 0;
opacity: 0;
transform: scale(0.8);
transition: opacity 320ms cubic-bezier(0.16, 1, 0.3, 1) 120ms,
transform 320ms cubic-bezier(0.16, 1, 0.3, 1) 120ms;
&:active {
background: rgba(255, 255, 255, 0.1);
}
}
.close-x {
width: 24rpx;
height: 24rpx;
position: relative;
}
.close-line {
position: absolute;
top: 50%;
left: 50%;
width: 28rpx;
height: 2rpx;
background: #ffffff;
border-radius: 2rpx;
&.line-a {
transform: translate(-50%, -50%) rotate(45deg);
}
&.line-b {
transform: translate(-50%, -50%) rotate(-45deg);
}
}
</style> </style>