概述
全屏广告组件是一个用于在应用中展示全屏广告的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 | 广告被点击时触发 | 无 |
使用示例
需要的位置调用,一般是首页
<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
<template><viewclass="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(() => ({paddingBottom: `${safeAreaInsets.value.bottom}px`,paddingLeft: `${safeAreaInsets.value.left}px`,paddingRight: `${safeAreaInsets.value.right}px`,
}));
const headerStyle = computed(() => ({paddingTop: `${30 + safeAreaInsets.value.top}px`,
}));const countdownStyle = computed(() => ({top: `${20 + safeAreaInsets.value.top}px`,
}));const closeBtnStyle = computed(() => ({top: `${20 + safeAreaInsets.value.top}px`,
}));
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);box-sizing: border-box;
}
</style>
广告的交互页面
src/components/fullscreen-ad/examples/ad-example.vue
- 广告内容示例组件
<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"><imageclass="banner-image":src="adData.imageUrl"mode="aspectFill"@click="handleAdClick"@error="handleImageError"@load="handleImageLoad"/><viewclass="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);
};
const handleImageLoad = (e: any) => {console.log("🚀 [广告组件] 图片加载成功", e);
};
</script><style lang="scss">
.fullscreen-ad-container {position: relative;width: 100%;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: 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: 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: 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: 50rpx 30rpx 30rpx;}.ad-content {padding: 30rpx;}.action-bar {padding: 24rpx 30rpx;}.bottom-nav {padding: 20rpx 30rpx;}.countdown-container {top: 30rpx;right: 30rpx;}.close-btn {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页面、应用内页面等