概述
全屏广告组件是一个用于在应用中展示全屏广告的Vue组件,支持自动显示、倒计时关闭、点击跳转等功能。该组件主要用于应用启动时或特定场景下展示推广内容。
效果
组件结构
主要文件
src/components/fullscreen-ad/index.vue
- 主组件文件
src/components/fullscreen-ad/examples/ad-example.vue
- 广告内容示例组件
功能特性
1. 自动显示控制
- 自动显示: 支持组件加载后自动显示广告
- 显示频率控制: 基于本地存储控制每日显示频率,避免重复打扰用户
- 延迟显示: 支持延迟1秒后显示,确保页面加载完成
2. 倒计时功能
- 自动倒计时: 广告显示后开始倒计时,到达指定时间后自动关闭
- 倒计时显示: 在广告界面显示剩余时间,用户可清楚了解关闭时间
- 可配置时长: 支持自定义倒计时时长(默认30秒)
3. 交互功能
- 手动关闭: 用户可通过"跳过"按钮手动关闭广告
- 点击跳转: 点击广告内容可跳转到指定的微信小程序
- 背景关闭: 点击广告外部区域可关闭广告
4. 小程序跳转
- 微信小程序跳转: 支持跳转到指定的微信小程序
- 跳转参数配置: 支持配置目标小程序的AppID和页面路径
- 跳转状态处理: 处理跳转成功、失败、取消等不同状态
- 自动关闭: 跳转成功后自动关闭广告
5. 图片处理
- 动态图片: 支持配置广告图片URL
- 加载状态: 监听图片加载成功和失败状态
- 错误处理: 图片加载失败时显示占位内容
- 点击响应: 图片和占位内容都支持点击跳转
6. 响应式设计
- 全屏适配: 支持不同屏幕尺寸的全屏显示
- 安全区域适配: 自动适配设备的安全区域(刘海屏、底部指示器等)
- 动画效果: 包含淡入动画和滑入动画效果
- 毛玻璃效果: 背景支持毛玻璃模糊效果
7. 样式特性
- 渐变背景: 使用线性渐变背景提升视觉效果
- 现代化UI: 采用圆角、阴影等现代化设计元素
- 服务特色展示: 展示服务特色图标和文字说明
- 品牌展示: 包含应用logo、名称和slogan展示
组件属性 (Props)
属性名 |
类型 |
默认值 |
说明 |
imageUrl |
String |
"https://picsum.photos/400/600" |
广告图片URL |
appId |
String |
"wx1234567890abcdef" |
目标微信小程序AppID |
path |
String |
"pages/index/index" |
目标小程序页面路径 |
countdown |
Number |
30 |
倒计时时长(秒) |
autoShow |
Boolean |
true |
是否自动显示广告 |
组件事件 (Events)
事件名 |
说明 |
回调参数 |
close |
广告关闭时触发 |
无 |
click |
广告被点击时触发 |
无 |
使用示例
需要的位置调用,一般是首页
html
复制代码
<template>
<!-- 全屏广告组件 -->
<fullscreen-ad
:imageUrl="adConfig.imageUrl"
:appId="adConfig.appId"
:path="adConfig.path"
:countdown="adConfig.countdown"
:autoShow="true"
@close="handleAdClose"
@click="handleAdClick"
/>
</template>
<script setup>
import FullscreenAd from '@/components/fullscreen-ad/index.vue'
// 广告配置
const adConfig = {
imageUrl: 'https://example.com/ad-image.jpg',
appId: 'wx1234567890abcdef',
path: 'pages/index/index',
countdown: 30
}
// 处理广告关闭
const handleAdClose = () => {
console.log('广告已关闭')
}
// 处理广告点击
const handleAdClick = () => {
console.log('用户点击了广告')
}
</script>
广告的核心逻辑
- src/components/fullscreen-ad/index.vue
html
复制代码
<template>
<!-- 全屏广告弹出层 -->
<view
class="fullscreen-ad-overlay"
v-if="adData.show"
@click="closeAd"
:style="containerStyle"
>
<ad-example
:adData="adData"
@close="closeAd"
@click="handleAdClick"
style="width: 100%"
></ad-example>
</view>
</template>
<script lang="ts" setup>
import { reactive, onUnmounted, ref, computed } from "vue";
import { storages } from "@/support/storages";
import adExample from "./examples/ad-example.vue";
// 定义组件属性
interface Props {
imageUrl?: string;
appId?: string;
path?: string;
countdown?: number;
autoShow?: boolean;
}
// 定义事件
interface Emits {
(e: "close"): void;
(e: "click"): void;
}
const props = withDefaults(defineProps<Props>(), {
imageUrl: "https://picsum.photos/400/600",
appId: "wx1234567890abcdef",
path: "pages/index/index",
countdown: 30,
autoShow: true,
});
const emit = defineEmits<Emits>();
// 广告数据
const adData = reactive({
show: false,
countdown: props.countdown,
imageUrl: props.imageUrl,
imageError: false,
appId: props.appId,
path: props.path,
timer: null as any,
// 显示广告
showAd: () => {
console.log("🚀 [广告组件] 开始检查是否显示广告");
// 检查是否已经显示过广告
const today = new Date().toDateString();
const lastShownDate = storages.get("fullscreen_ad_shown_date");
console.log("🚀 [广告组件] 今天日期:", today);
console.log("🚀 [广告组件] 上次显示日期:", lastShownDate);
// 临时注释掉日期检查,确保每次都显示广告用于调试
if (true) {
// 临时改为总是显示
console.log("🚀 [广告组件] 准备显示广告");
adData.show = true;
adData.countdown = props.countdown;
adData.startCountdown();
// 记录今天已显示过广告
storages.set("fullscreen_ad_shown_date", today);
console.log("🚀 [广告组件] 广告已显示,倒计时开始");
} else {
console.log("🚀 [广告组件] 今天已显示过广告,跳过");
}
},
// 开始倒计时
startCountdown: () => {
console.log("🚀 [广告组件] 开始倒计时,初始值:", adData.countdown);
adData.timer = setInterval(() => {
adData.countdown--;
console.log("🚀 [广告组件] 倒计时:", adData.countdown);
if (adData.countdown <= 0) {
console.log("🚀 [广告组件] 倒计时结束,自动关闭广告");
adData.closeAd();
}
}, 1000);
},
// 关闭广告
closeAd: () => {
console.log("🚀 [广告组件] 执行关闭广告操作");
adData.show = false;
if (adData.timer) {
console.log("🚀 [广告组件] 清理倒计时定时器");
clearInterval(adData.timer);
adData.timer = null;
}
console.log("🚀 [广告组件] 广告已关闭");
emit("close");
},
});
// 关闭广告
const closeAd = () => {
console.log("🚀 [广告组件] 用户手动关闭广告");
adData.closeAd();
};
// 处理广告点击
const handleAdClick = () => {
console.log("🚀 [广告组件] 用户点击了广告");
emit("click");
// 跳转到微信小程序
uni.navigateToMiniProgram({
appId: adData.appId,
path: adData.path,
success: (res) => {
console.log("🚀 [广告组件] 跳转小程序成功", res);
// 跳转成功后关闭广告
adData.closeAd();
},
fail: (err) => {
console.error("🚀 [广告组件] 跳转小程序失败", err);
// 如果是用户取消操作,不显示失败提示
if (err.errMsg && err.errMsg.includes("cancel")) {
console.log("🚀 [广告组件] 用户取消跳转");
return;
}
uni.showToast({
title: "跳转失败",
icon: "none",
});
},
});
};
// 处理图片加载错误
const handleImageError = (e: any) => {
console.log("🚀 [广告组件] 图片加载失败", e);
adData.imageError = true;
};
// 处理图片加载成功
const handleImageLoad = (e: any) => {
console.log("🚀 [广告组件] 图片加载成功", e);
adData.imageError = false;
};
// 暴露方法给父组件
defineExpose({
showAd: adData.showAd,
closeAd: adData.closeAd,
});
// 组件卸载时清理定时器
onUnmounted(() => {
if (adData.timer) {
clearInterval(adData.timer);
adData.timer = null;
}
});
// 获取系统信息,用于安全区域适配
const systemInfo = ref<any>({});
// 获取系统信息
const getSystemInfo = () => {
try {
const info = uni.getSystemInfoSync();
systemInfo.value = info;
console.log("🚀 [广告组件] 系统信息:", info);
// 在微信小程序中,直接使用系统信息来计算安全区域
if (info.safeAreaInsets) {
const { top, bottom, left, right } = info.safeAreaInsets;
console.log("🚀 [广告组件] 安全区域:", { top, bottom, left, right });
// 将安全区域信息保存到响应式数据中
safeAreaInsets.value = { top, bottom, left, right };
} else if (info.statusBarHeight) {
// 兼容旧版本,使用状态栏高度
console.log("🚀 [广告组件] 状态栏高度:", info.statusBarHeight);
safeAreaInsets.value = {
top: info.statusBarHeight,
bottom: 0,
left: 0,
right: 0,
};
} else {
// 默认值
safeAreaInsets.value = { top: 0, bottom: 0, left: 0, right: 0 };
}
} catch (error) {
console.error("🚀 [广告组件] 获取系统信息失败:", error);
// 设置默认值
safeAreaInsets.value = { top: 0, bottom: 0, left: 0, right: 0 };
}
};
// 安全区域数据
const safeAreaInsets = ref({ top: 0, bottom: 0, left: 0, right: 0 });
// 计算样式
const containerStyle = computed(() => ({
// paddingTop: `${safeAreaInsets.value.top}px`,
paddingBottom: `${safeAreaInsets.value.bottom}px`,
paddingLeft: `${safeAreaInsets.value.left}px`,
paddingRight: `${safeAreaInsets.value.right}px`,
}));
// const adContainerStyle = computed(() => ({
// height: `calc(100vh - ${safeAreaInsets.value.top}px - ${safeAreaInsets.value.bottom}px)`
// }));
const headerStyle = computed(() => ({
paddingTop: `${30 + safeAreaInsets.value.top}px`, // 60rpx ≈ 30px
}));
const countdownStyle = computed(() => ({
top: `${20 + safeAreaInsets.value.top}px`, // 40rpx ≈ 20px
}));
const closeBtnStyle = computed(() => ({
top: `${20 + safeAreaInsets.value.top}px`, // 40rpx ≈ 20px
}));
// 初始化时获取系统信息
getSystemInfo();
// 如果设置了自动显示,则延迟显示广告
if (props.autoShow) {
setTimeout(() => {
adData.showAd();
}, 1000);
}
</script>
<style lang="scss" scoped>
/* 全屏广告样式 */
.fullscreen-ad-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
// background: linear-gradient(135deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.9) 100%);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.4s ease-in-out;
backdrop-filter: blur(10rpx);
/* 安全区域通过JavaScript动态设置 */
box-sizing: border-box;
}
</style>
广告的交互页面
src/components/fullscreen-ad/examples/ad-example.vue
- 广告内容示例组件
html
复制代码
<template>
<!-- 广告内容 -->
<view class="fullscreen-ad-container" @click.stop>
<!-- 头部区域 -->
<view class="ad-header">
<view class="ad-title">高价回收手机</view>
<view class="ad-subtitle">30分钟免费上门回收</view>
</view>
<!-- 内容区域 -->
<view class="ad-content">
<!-- 广告图片 -->
<image
class="banner-image"
:src="adData.imageUrl"
mode="aspectFill"
@click="handleAdClick"
@error="handleImageError"
@load="handleImageLoad"
/>
<!-- 图片加载失败时的占位内容 -->
<view
class="ad-placeholder"
v-if="adData.imageError"
@click="handleAdClick"
>
<text class="placeholder-text">广告图片加载失败</text>
<text class="placeholder-subtitle">点击此处跳转小程序</text>
</view>
<!-- 服务特色 -->
<view class="service-features">
<view class="feature-item">
<view class="feature-icon">⏰</view>
<text class="feature-text">30分钟上门</text>
</view>
<view class="feature-item">
<view class="feature-icon">🔒</view>
<text class="feature-text">安全保障</text>
</view>
<view class="feature-item">
<view class="feature-icon">⚡</view>
<text class="feature-text">极速打款</text>
</view>
</view>
</view>
<!-- 底部操作区 -->
<view class="action-bar">
<view class="submit-btn" @click="handleAdClick"> 戳我换钱 </view>
</view>
<!-- 底部导航 -->
<view class="bottom-nav">
<view class="nav-left">
<text class="page-number">{{ adData.countdown }}</text>
</view>
<view class="nav-center">
<view class="app-logo">🦆</view>
<view class="app-info">
<text class="app-name">出手鸭</text>
<text class="app-slogan">该出手时就出手</text>
</view>
</view>
<view class="nav-right" @click="closeAd">
<text class="skip-text">跳过</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { reactive, onUnmounted, ref, computed } from "vue";
import { storages } from "@/support/storages";
// 定义组件属性
interface Props {
adData?: boolean;
}
// 定义事件
interface Emits {
(e: "close"): void;
(e: "click"): void;
}
const props = withDefaults(defineProps<Props>(), {
adData: {},
});
const emit = defineEmits<Emits>();
// 关闭广告
const closeAd = () => {
console.log("🚀 [广告组件] 用户手动关闭广告");
emit("close");
};
// 处理广告点击
const handleAdClick = () => {
console.log("🚀 [广告组件] 用户点击了广告");
emit("click");
};
// 处理图片加载错误
const handleImageError = (e: any) => {
console.log("🚀 [广告组件] 图片加载失败", e);
// adData.imageError = true;
};
// 处理图片加载成功
const handleImageLoad = (e: any) => {
console.log("🚀 [广告组件] 图片加载成功", e);
// adData.imageError = false;
};
</script>
<style lang="scss">
.fullscreen-ad-container {
position: relative;
width: 100%;
/* 高度通过JavaScript动态设置 */
height: 100vh;
border-radius: 0;
overflow: hidden;
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
animation: slideIn 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
display: flex;
flex-direction: column;
}
/* 头部区域 */
.ad-header {
/* padding通过JavaScript动态设置 */
padding: 200rpx 40rpx 40rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
overflow: hidden;
}
.ad-header::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="20" cy="20" r="2" fill="rgba(255,255,255,0.1)"/><circle cx="80" cy="40" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="40" cy="80" r="1.5" fill="rgba(255,255,255,0.1)"/></svg>')
repeat;
opacity: 0.3;
}
.ad-title {
font-size: 64rpx;
font-weight: 800;
color: #ffffff;
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
letter-spacing: 2rpx;
position: relative;
z-index: 1;
}
.ad-subtitle {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.9);
margin-top: 16rpx;
font-weight: 500;
position: relative;
z-index: 1;
}
.countdown-container {
position: absolute;
/* top通过JavaScript动态设置 */
top: 40rpx;
right: 40rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 40rpx;
padding: 12rpx 24rpx;
backdrop-filter: blur(10rpx);
border: 1rpx solid rgba(255, 255, 255, 0.3);
}
.countdown-text {
color: #fff;
font-size: 24rpx;
font-weight: 600;
}
/* 内容区域 */
.ad-content {
flex: 1;
padding: 40rpx;
background: #ffffff;
position: relative;
}
.banner-image {
width: 100%;
height: 600rpx;
border-radius: 24rpx;
margin-bottom: 40rpx;
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.15);
object-fit: cover;
}
.ad-placeholder {
width: 100%;
height: 360rpx;
border-radius: 24rpx;
margin-bottom: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
cursor: pointer;
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.15);
position: relative;
overflow: hidden;
}
.ad-placeholder::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(
circle at 30% 30%,
rgba(255, 255, 255, 0.1) 0%,
transparent 50%
);
}
.placeholder-text {
color: #fff;
font-size: 36rpx;
font-weight: 700;
margin-bottom: 16rpx;
position: relative;
z-index: 1;
}
.placeholder-subtitle {
color: rgba(255, 255, 255, 0.8);
font-size: 28rpx;
position: relative;
z-index: 1;
}
/* 服务特色 */
.service-features {
display: flex;
justify-content: space-around;
margin-bottom: 40rpx;
background: #f8f9ff;
border-radius: 20rpx;
padding: 30rpx 20rpx;
}
.feature-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.feature-icon {
font-size: 48rpx;
margin-bottom: 12rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.feature-text {
font-size: 24rpx;
color: #666666;
font-weight: 500;
text-align: center;
}
/* 底部操作区 */
.action-bar {
padding: 30rpx 40rpx;
background: #ffffff;
}
.submit-btn {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 48rpx;
color: #ffffff;
font-size: 32rpx;
font-weight: 700;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.submit-btn::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.2),
transparent
);
transition: left 0.5s ease;
}
.submit-btn:active::before {
left: 100%;
}
/* 底部导航 */
.bottom-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 40rpx;
background: #ffffff;
border-top: 1rpx solid #f0f0f0;
height: 180rpx;
}
.nav-left {
width: 56rpx;
height: 56rpx;
background: linear-gradient(135deg, #f0f0f0 0%, #e0e0e0 100%);
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.page-number {
font-size: 24rpx;
color: #666666;
font-weight: 600;
}
.nav-center {
display: flex;
align-items: center;
}
.app-logo {
font-size: 48rpx;
margin-right: 16rpx;
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.1));
}
.app-info {
display: flex;
flex-direction: column;
}
.app-name {
font-size: 28rpx;
font-weight: 700;
color: #333333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.app-slogan {
font-size: 20rpx;
color: #999999;
margin-top: 4rpx;
font-weight: 500;
}
.nav-right {
width: 100rpx;
height: 56rpx;
background: linear-gradient(135deg, #f0f0f0 0%, #e0e0e0 100%);
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.nav-right:active {
transform: scale(0.95);
}
.skip-text {
font-size: 24rpx;
color: #666666;
font-weight: 600;
}
/* 关闭按钮 */
.close-btn {
position: absolute;
/* top通过JavaScript动态设置 */
top: 40rpx;
right: 40rpx;
width: 56rpx;
height: 56rpx;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 30;
backdrop-filter: blur(10rpx);
border: 1rpx solid rgba(255, 255, 255, 0.3);
transition: all 0.3s ease;
}
.close-btn:active {
transform: scale(0.9);
background: rgba(255, 255, 255, 0.3);
}
.close-icon {
color: #fff;
font-size: 32rpx;
font-weight: 700;
line-height: 1;
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
backdrop-filter: blur(0);
}
to {
opacity: 1;
backdrop-filter: blur(10rpx);
}
}
@keyframes slideIn {
from {
transform: scale(0.9) translateY(60rpx);
opacity: 0;
}
to {
transform: scale(1) translateY(0);
opacity: 1;
}
}
/* 响应式适配 */
@media screen and (max-width: 750rpx) {
.ad-title {
font-size: 56rpx;
}
.ad-subtitle {
font-size: 28rpx;
}
.banner-image {
height: 320rpx;
}
.ad-placeholder {
height: 320rpx;
}
.submit-btn {
height: 88rpx;
font-size: 30rpx;
}
}
@media screen and (max-width: 600rpx) {
.ad-header {
/* padding通过JavaScript动态设置 */
padding: 50rpx 30rpx 30rpx;
}
.ad-content {
padding: 30rpx;
}
.action-bar {
padding: 24rpx 30rpx;
}
.bottom-nav {
padding: 20rpx 30rpx;
}
.countdown-container {
/* top通过JavaScript动态设置 */
top: 30rpx;
right: 30rpx;
}
.close-btn {
/* top通过JavaScript动态设置 */
top: 30rpx;
right: 30rpx;
}
}
</style>
技术实现
核心技术栈
- Vue 3: 使用Composition API
- TypeScript: 类型安全的开发体验
- uni-app: 跨平台开发框架
- SCSS: CSS预处理器
关键实现
- 状态管理: 使用reactive响应式数据管理广告状态
- 定时器管理: 使用setInterval实现倒计时,组件卸载时自动清理
- 本地存储: 使用uni-app的存储API记录广告显示状态
- 系统信息获取: 获取设备信息进行安全区域适配
- 小程序跳转: 使用uni.navigateToMiniProgram API实现跳转
生命周期管理
- 组件挂载: 自动获取系统信息,设置自动显示
- 组件卸载: 自动清理定时器,防止内存泄漏
- 错误处理: 完善的错误捕获和用户提示
注意事项
- 权限要求: 跳转微信小程序需要相应的平台权限配置
- 网络依赖: 广告图片加载依赖网络连接
- 存储空间: 组件会使用本地存储记录显示状态
- 性能考虑: 大图片可能影响加载性能,建议优化图片大小
- 用户体验: 建议合理设置显示频率,避免过度打扰用户
扩展建议
- 多广告支持: 支持配置多个广告轮播显示
- 统计功能: 添加广告展示和点击统计
- A/B测试: 支持不同广告内容的A/B测试
- 动态配置: 支持从服务端动态获取广告配置
- 更多跳转方式: 支持跳转到H5页面、应用内页面等