更新记录

1.0.0(2026-10-22)

首个版本:

  • 支持激励视频、插屏广告、插屏式激励视频、开屏广告(仅 iOS)与 Banner 横幅组件
  • Android 端为纯 UTS 实现(三方依赖走 config.json 的 Maven dependencies),iOS 端为 UTS + Swift 混编,随包 GoogleMobileAds 12.11.0

平台兼容性

uni-app(3.99)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
√ √ × × √ √ 5.0 12 ×
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
× × × × × × × × × × × ×

admob-uts(Google AdMob 广告插件 UTS 版)

Google AdMob 广告插件,支持 激励视频、插屏广告、插屏式激励视频、开屏广告(仅 iOS) 与 Banner 横幅组件,Android / iOS 双端。

本插件为完全明文源码,不做任何加密(package.json 中 uni_modules.encrypt 为空数组,插件目录内也不存在 encrypt 文件),可自由二次开发。

一、能力一览

能力 函数 / 组件 Android iOS
激励视频 loadRewardedAd / showRewardedAd √ √
插屏广告 loadInterstitialAd / showInterstitialAd √ √
插屏式激励视频 loadRewardedInterstitialAd / showRewardedInterstitialAd √ √
开屏广告 loadAppOpenAD / showAppOpenAD ×(直接回调"仅支持 iOS") √
Banner 横幅 组件 admob-banner(仅 nvue / uvue 页面) √ √

三、接入前配置

调试原生插件必须使用自定义基座。

1. 配置 App ID

iOS:项目根目录新建 Info.plist(注意大小写)

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>GADApplicationIdentifier</key>
    <string>这里填写你应用 iOS 的 APPID</string>
  </dict>
</plist>

Android:项目根目录新建 AndroidManifest.xml(注意大小写)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
  package="io.dcloud.nativeresouce">
    <application>
        <meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="这里填写你应用安卓的APPID"/>
    </application>
</manifest>

2. 引用插件

import {
    loadAppOpenAD,
    showAppOpenAD,
    loadRewardedAd,
    showRewardedAd,
    loadInterstitialAd,
    showInterstitialAd,
    loadRewardedInterstitialAd,
    showRewardedInterstitialAd
} from '@/uni_modules/admob-uts'

3. 注意

插件测试过程中,请按谷歌要求使用测试广告位 ID 进行测试,以免账号被封停:

类型 Android iOS
激励视频 ca-app-pub-3940256099942544/5224354917 ca-app-pub-3940256099942544/1712485313
插屏广告 ca-app-pub-3940256099942544/1033173712 ca-app-pub-3940256099942544/4411468910
插屏式激励视频 ca-app-pub-3940256099942544/5354046379 ca-app-pub-3940256099942544/6978759866
开屏广告 ca-app-pub-3940256099942544/5575463023 ca-app-pub-3940256099942544/5575463023
Banner ca-app-pub-3940256099942544/6300978111 ca-app-pub-3940256099942544/2934735716

四、回调数据格式

所有回调均为 JSON 字符串:

{
    "errorCode": 0,
    "code": 100,
    "msg": "激励视频加载成功",
    "data": {}
}
字段 说明
errorCode 0 成功,1 失败
code 事件码,见下表
msg 文本提示,可直接用于 toast
data 附加数据(可选,仅激励发放 / Banner 为空)

事件码:

code 含义
100 加载成功
101 广告曝光(Impression)
102 广告点击
103 广告关闭
104 加载失败
105 激励发放(仅激励视频 / 插屏式激励视频)
106 展示失败
107 广告已展示(打开全屏内容)

load* 方法只要 errorCode == 0 即代表加载完成,可以调用对应的 show* 播放; show* 方法在广告播放过程中会持续回调(曝光 / 点击 / 关闭 / 激励发放)。

五、API 说明

5.1 激励视频

loadRewardedAd({
    adid: 'ca-app-pub-3940256099942544/5224354917'
}, res => {
    const resJson = JSON.parse(res)
    if (resJson.errorCode == 0) {
        console.log('广告 load 完成,可以播放')
    } else {
        console.log('广告 load 失败:' + resJson.msg)
    }
})

// 加载成功后播放;customData 会在激励发放时原样返回
showRewardedAd({
    adid: 'ca-app-pub-3940256099942544/5224354917',
    customData: '123'
}, res => {
    const resJson = JSON.parse(res)
    if (resJson.code == 105) {
        // 激励发放,此时才可给用户发放奖励
        console.log('奖励发放', resJson.data)
    }
})

5.2 插屏广告

loadInterstitialAd({
    adid: 'ca-app-pub-3940256099942544/1033173712'
}, res => {
    console.log('插屏加载进度更新 = ' + res)
})

showInterstitialAd({
    adid: 'ca-app-pub-3940256099942544/1033173712'
}, res => {
    console.log('插屏播放进度更新 = ' + res)
})

5.3 插屏式激励视频

loadRewardedInterstitialAd({
    adid: 'ca-app-pub-3940256099942544/5354046379'
}, res => {
    console.log('插屏式激励视频加载进度更新 = ' + res)
})

showRewardedInterstitialAd({
    adid: 'ca-app-pub-3940256099942544/5354046379',
    customData: '123'
}, res => {
    console.log('插屏式激励视频播放进度更新 = ' + res)
})

5.4 开屏广告(仅 iOS)

loadAppOpenAD({
    adid: 'ca-app-pub-3940256099942544/5575463023'
}, res => {
    console.log('开屏广告加载进度更新 = ' + res)
})

showAppOpenAD({
    adid: 'ca-app-pub-3940256099942544/5575463023'
}, res => {
    console.log('开屏广告播放进度更新 = ' + res)
})

5.5 辅助方法

import { isAdReady, resetAd } from '@/uni_modules/admob-uts'

isAdReady('rewarded')   // 是否有已加载待播放的激励视频
resetAd('rewarded')     // 清空已缓存的激励视频(切换广告位 ID 时使用)

format 取值:rewarded / interstitial / rewardedInterstitial / appOpen。

5.6 Banner 横幅广告

Banner 为原生组件,仅支持 nvue / uvue 页面。

<admob-banner ref="admobBanner" @eventCallBack="eventCallBack"
    style="width:375px;height:100px;background-color:aqua;"></admob-banner>
// 加载广告:参数为(广告位 ID, 宽, 高),宽高单位为 px,需与 style 中保持一致
this.$refs['admobBanner'].loadAD('ca-app-pub-3940256099942544/9214589741', 375, 100)

eventCallBack(res) {
    console.log(res.detail) // JSON 字符串,与上述回调格式一致
}

Banner 事件码沿用统一事件码(100 加载成功、101 曝光、102 点击、103 关闭、104 加载失败、105 打开、106 滑动点击), 其中 105/106 仅 Banner 组件会产生。

六、常见问题

  • xxx not found:先确认 @/uni_modules/admob-uts 路径正确,且已使用自定义基座(标准基座不含本插件)。

  • 加载失败且 msg 提示 Application ID 缺失:检查项目根目录的 AndroidManifest.xml / Info.plist 是否配置了 App ID。

  • 当前无可用 Activity(Android):请勿在 onLaunch 阶段立即调用,等页面 onReady 之后再调用。

  • 激励视频没有回调 code == 105:用户未完整观看(提前关闭)时不会发放奖励,属正常行为;请仅在 105 时发放奖励。

  • 报 callback回调函数已释放,不能再次执行(两端都可能): HBuilderX 4.25 起,UTS 插件导出方法里的回调参数改为「触发一次后立即自动回收」。 本插件的回调天然要触发多次(107 已展示 → 101 曝光 → 102 点击 → 105 激励发放 → 103 关闭), 所以两端实现里的导出方法都带了 @UTSJS.keepAlive 装饰器 (官方文档)。 改动代码时不要删这个装饰器,也不要把 export function 改回 export const xxx: AdMobLoad = ... —— 装饰器不支持那种导出写法。注意 app-android / app-ios 两端都要配,少一端就会在那一端复现。 该装饰器会让回调常驻内存,因此请勿高频调用 load/show(按需加载即可)。

  • Android 报「激励视频加载异常」:所有 SDK 调用都已切到主线程 (UTSAndroid.getDispatcher("main").async(...),Google Mobile Ads SDK 要求在主线程初始化)。 若仍失败,用 adb logcat | grep admob-uts 看完整异常堆栈 —— 详情只写日志、不放回调文案, 因为 UTS 里 catch (e) 的 e 是 any,不能取属性、也不能参与字符串拼接。

  • iOS 报 激励视频加载失败…:Request Error: Invalid request.(先别改代码,八成不是代码问题): iOS 实现会把诊断信息一起回给 JS(adUnitId / appId / initStatus / netProbe / sdklog),照着看即可:

    • netProbe: googleads.g.doubleclick.net=失败(TLS错误导致安全连接失败。) ⇒ 设备网络连不上 Google 广告服务,SDK 拉不到初始化配置 (initStatus 里 GADMobileAds 会是 Not Ready: Could not retrieve application configuration data.), 请求在本地就被判为无效。此时 detail 里的 Response ID 必然是 (null)、Adapter Response 为空。 修法在网络层(开代理 / 换网络),与本插件代码、广告位、App ID 都无关。
    • 中国大陆网络环境下 iOS 端 AdMob 无法稳定变现(TLS 会被阻断), 面向国内用户的 App 建议 iOS 走国内广告联盟,或只对可访问 Google 的地区投放。
    • appId= 显示 (未配置) ⇒ 基座里的 Info.plist 没生效;initStatus 里出现 Not Ready ⇒ 初始化没成功。
    • sdklog 字段是用 OSLogStore 读本进程 os_log(iOS 15+)拿到的 Google SDK 自身日志 —— 这些行默认只在 Xcode/设备日志里,这样就能直接在 HBuilderX 控制台看到。
    • 广告位必须按平台取:iOS 与 Android 的测试广告位不通用(见上文对照表), 两端不要共用一个常量;经典 uni-app 的 vue 服务层里 #ifdef APP-IOS 不生效, 要用 uni.getSystemInfoSync().platform 做运行时判断。

七、最简可运行 Demo

不想读文档、只想先跑起来看效果的话,把下面这份代码整段覆盖到 pages/index/index.vue, 用自定义基座跑到手机上就能出测试广告。它覆盖了插件全部能力, 也把「激励视频优先 + 插屏式激励视频兜底」这个最容易踩坑的组合写全了。

跑之前确认三件事(缺一跑不出来):

  1. 项目根目录的 AndroidManifest.xml / Info.plist 已填好 App ID(测试阶段用 Google 官方测试 App ID 即可);
  2. 用自定义基座运行,标准基座不含本插件,会报 admob-uts not found;
  3. 别在 onLaunch 阶段调用,等页面 onLoad 之后再调(Android 需要 Activity 就绪)。

预期表现:进页面后三个(iOS 四个)状态点陆续变绿=广告加载成功;点「看广告解锁」会弹出全屏激励视频; 点「加载 / 展示」可单独测试每种广告;页面底部实时打印插件回调的原始事件码。

<template>
    <view class="page">
        <view class="header">
            <view class="title">AdMob UTS 插件 · 最简 Demo</view>
            <view class="sub">当前平台:{{ platformText }}(广告位已按平台自动切换)</view>
        </view>

        <!-- 广告位就绪状态 -->
        <view class="card">
            <view class="card-title">广告位状态</view>
            <view class="status-row" v-for="item in adList" :key="item.format">
                <view class="status-name">{{ item.name }}</view>
                <view class="dot" :class="ready[item.format] ? 'dot-on' : 'dot-off'"></view>
                <view class="status-text" :class="ready[item.format] ? 'text-on' : 'text-off'">
                    {{ ready[item.format] ? '已就绪' : '未就绪' }}
                </view>
            </view>
        </view>

        <!-- 场景一:看广告解锁 -->
        <view class="card">
            <view class="card-title">场景一 · 看广告解锁(激励视频优先,插屏式激励兜底)</view>
            <view class="unlock-text" :class="unlocked ? 'text-on' : 'text-off'">
                {{ unlocked ? '第 1 集已解锁' : '第 1 集未解锁,需完整观看激励类广告' }}
            </view>
            <view class="btn btn-primary" :class="{ 'btn-disabled': unlocking }" @click="adUnlock">
                {{ unlocking ? '广告播放中…' : '看广告解锁' }}
            </view>
        </view>

        <!-- 场景二:单独加载 / 展示 -->
        <view class="card">
            <view class="card-title">场景二 · 单独加载 / 展示</view>
            <view class="ad-row" v-for="item in adList" :key="item.format">
                <view class="ad-name">{{ item.name }}</view>
                <view class="btn btn-mini" @click="loadAd(item.format)">加载</view>
                <view class="btn btn-mini btn-primary" :class="{ 'btn-disabled': !ready[item.format] }"
                    @click="showAd(item.format)">展示</view>
            </view>
        </view>

        <!-- 回调日志 -->
        <view class="card">
            <view class="card-title">
                <text>回调日志(最新在最上面)</text>
                <text class="clear-btn" @click="logs = []">清空</text>
            </view>
            <scroll-view class="log-box" scroll-y>
                <view class="log-line" v-for="line in logs" :key="line.id">{{ line.text }}</view>
                <view class="log-empty" v-if="!logs.length">暂无日志,点上面的按钮试试</view>
            </scroll-view>
        </view>
    </view>
</template>

<script>
    import {
        loadAppOpenAD,
        showAppOpenAD,
        loadRewardedAd,
        showRewardedAd,
        loadInterstitialAd,
        showInterstitialAd,
        loadRewardedInterstitialAd,
        showRewardedInterstitialAd
    } from '@/uni_modules/admob-uts'

    // ------------------------------------------------------------------ 平台与广告位
    //
    // ⚠️ 用运行时判断,不要用 `#ifdef APP-IOS` / `#ifdef APP-ANDROID`。
    //    经典 uni-app 的 vue 服务层两端共用同一份 app-service.js,
    //    条件编译分支会被整体丢弃,写了等于没写。
    const IS_IOS = (() => {
        try {
            return uni.getSystemInfoSync().platform === 'ios'
        } catch (e) {
            return false
        }
    })()

    // 测试广告位:Google 官方测试 ID,上线前整体替换成自己后台的正式广告位。
    // ⚠️ 两端广告位**不通用**,必须按平台取;用错平台会返回
    //    「Request Error: Invalid request.」,广告永远拉不起来。
    const AD_UNIT_IDS = {
        rewarded: IS_IOS ? 'ca-app-pub-3940256099942544/1712485313' : 'ca-app-pub-3940256099942544/5224354917',
        rewardedInterstitial: IS_IOS ? 'ca-app-pub-3940256099942544/6978759866' : 'ca-app-pub-3940256099942544/5354046379',
        interstitial: IS_IOS ? 'ca-app-pub-3940256099942544/4411468910' : 'ca-app-pub-3940256099942544/1033173712',
        appOpen: IS_IOS ? 'ca-app-pub-3940256099942544/5575463023' : 'ca-app-pub-3940256099942544/9257395921'
    }

    /** 事件码,与插件 utssdk/interface.uts 中的定义一致 */
    const CODE = {
        LOADED: 100,
        IMPRESSION: 101,
        CLICK: 102,
        CLOSED: 103,
        LOAD_FAILED: 104,
        REWARD: 105,
        SHOW_FAILED: 106,
        OPENED: 107
    }

    const CODE_NAME = {
        100: '加载成功',
        101: '曝光',
        102: '点击',
        103: '关闭',
        104: '加载失败',
        105: '激励发放',
        106: '展示失败',
        107: '已展示'
    }

    /** 全部广告位,Android 上会过滤掉开屏广告(插件在 Android 端直接回调失败) */
    const ALL_ADS = [
        { format: 'rewarded', name: '激励视频' },
        { format: 'rewardedInterstitial', name: '插屏式激励视频' },
        { format: 'interstitial', name: '插屏广告' },
        { format: 'appOpen', name: '开屏广告(仅 iOS)' }
    ]

    const pad2 = (n) => (n < 10 ? '0' + n : '' + n)

    export default {
        data() {
            return {
                platformText: IS_IOS ? 'iOS' : 'Android',
                // 每个广告位的加载状态:true = 已加载好,可以展示
                ready: {
                    rewarded: false,
                    rewardedInterstitial: false,
                    interstitial: false,
                    appOpen: false
                },
                unlocking: false, // 「看广告解锁」流程进行中,防重复点击
                unlocked: false, // 演示用的解锁结果
                pendingUnlock: false, // 本次广告播放是否在等解锁奖励
                rewardGranted: false, // 本次播放是否已发放奖励(只有 code=105 才置 true)
                rewardFormat: '', // 当前播放的是哪个激励类广告位
                autoShowAppOpen: IS_IOS, // iOS:开屏广告加载完成后自动展示一次
                logs: [], // 回调日志
                logId: 0
            }
        },
        computed: {
            adList() {
                // Android 不支持开屏广告,直接从列表里去掉,避免误导
                return IS_IOS ? ALL_ADS : ALL_ADS.filter((item) => item.format !== 'appOpen')
            }
        },
        onLoad() {
            // 广告是异步加载的,提前预加载好才能在需要时立即展示
            this.loadAd('rewarded') // 广告解锁优先使用
            this.loadAd('rewardedInterstitial') // 广告解锁兜底
            this.loadAd('interstitial') // 切集插屏
            if (IS_IOS) {
                this.loadAd('appOpen') // 开屏广告仅 iOS 支持
            }
        },
        methods: {
            // ---------------------------------------------------------- 通用工具
            log(text) {
                const now = new Date()
                const time = pad2(now.getHours()) + ':' + pad2(now.getMinutes()) + ':' + pad2(now.getSeconds())
                this.logId++
                this.logs.unshift({ id: this.logId, text: '[' + time + '] ' + text })
                if (this.logs.length > 60) this.logs.pop()
                console.log('[admob-demo]', text)
            },

            // 插件所有回调都是 JSON 字符串,这里统一解析;顺手兼容对象形式
            parse(res) {
                if (res && typeof res === 'object') return res
                try {
                    return JSON.parse(res)
                } catch (e) {
                    return {}
                }
            },

            codeName(code) {
                return CODE_NAME[code] || '未知事件 ' + code
            },

            // ---------------------------------------------------------- 加载 / 展示
            /**
             * 加载广告
             * @param {String} format rewarded / rewardedInterstitial / interstitial / appOpen
             * @param {Function} callback 可选,参数 ready:Boolean
             */
            loadAd(format, callback) {
                if (format === 'appOpen' && !IS_IOS) {
                    this.log('开屏广告仅 iOS 支持,Android 跳过')
                    if (typeof callback === 'function') callback(false)
                    return
                }
                const options = { adid: AD_UNIT_IDS[format] }
                const done = (res) => {
                    const resJson = this.parse(res)
                    const ok = resJson.errorCode === 0
                    this.ready[format] = ok
                    this.log(format + ' 加载' + (ok ? '成功' : '失败') + (resJson.msg ? ':' + resJson.msg : ''))
                    if (typeof callback === 'function') callback(ok)
                    // iOS 开屏广告:加载完成后自动展示一次,模拟真实「开屏」场景
                    if (ok && format === 'appOpen' && this.autoShowAppOpen) {
                        this.autoShowAppOpen = false
                        this.showAd('appOpen')
                    }
                }
                if (format === 'rewarded') {
                    loadRewardedAd(options, done)
                } else if (format === 'rewardedInterstitial') {
                    loadRewardedInterstitialAd(options, done)
                } else if (format === 'interstitial') {
                    loadInterstitialAd(options, done)
                } else {
                    loadAppOpenAD(options, done)
                }
            },

            /**
             * 展示广告(一次性消耗品:展示后必须重新 load 才能再次播放)
             * @param {String} format 同上
             */
            showAd(format) {
                if (!this.ready[format]) {
                    uni.showToast({ title: '广告还没加载好,先点「加载」', icon: 'none' })
                    return
                }
                // 激励类广告每次播放前重置奖励标记,只有收到 code=105 才算发放
                if (format === 'rewarded' || format === 'rewardedInterstitial') {
                    this.rewardGranted = false
                    this.rewardFormat = format
                }
                this.ready[format] = false // 播放后即被消耗,关闭/失败时再重新加载
                const options = {
                    adid: AD_UNIT_IDS[format],
                    // customData 会在激励发放时原样返回,真实业务里一般传剧集 ID
                    customData: 'demo-episode-1'
                }
                const onEvent = (res) => this.onAdEvent(format, res)
                if (format === 'rewarded') {
                    showRewardedAd(options, onEvent)
                } else if (format === 'rewardedInterstitial') {
                    showRewardedInterstitialAd(options, onEvent)
                } else if (format === 'interstitial') {
                    showInterstitialAd(options, onEvent)
                } else {
                    showAppOpenAD(options, onEvent)
                }
            },

            /**
             * 播放过程中的事件统一处理
             * 100 加载成功 / 101 曝光 / 102 点击 / 103 关闭 / 104 加载失败 / 105 激励发放 /
             * 106 展示失败 / 107 已展示
             */
            onAdEvent(format, res) {
                const resJson = this.parse(res)
                const code = resJson.code
                this.log(format + ' → code=' + code + '(' + this.codeName(code) + ')' +
                    (resJson.msg ? ' ' + resJson.msg : ''))

                // 激励发放:只有这个事件码才能给用户发奖励
                if (code === CODE.REWARD) {
                    this.rewardGranted = true
                    this.log('激励发放完成' + (resJson.data ? ',customData=' + resJson.data : ''))
                    if (this.pendingUnlock) {
                        this.pendingUnlock = false
                        this.unlocking = false
                        this.unlocked = true
                        uni.showToast({ title: '解锁成功', icon: 'none' })
                    }
                    return
                }

                // 广告关闭:重新预加载下一次
                if (code === CODE.CLOSED) {
                    if (this.pendingUnlock && !this.rewardGranted) {
                        // 没看完就关了,不发奖励,属正常行为
                        this.pendingUnlock = false
                        this.unlocking = false
                        uni.showToast({ title: '未完整观看,未发放奖励', icon: 'none' })
                    }
                    this.loadAd(format)
                    return
                }

                // 展示失败 / 加载失败
                if (code === CODE.SHOW_FAILED || resJson.errorCode !== 0) {
                    if (this.pendingUnlock) {
                        this.pendingUnlock = false
                        this.unlocking = false
                        uni.showToast({ title: resJson.msg || '广告展示失败', icon: 'none' })
                    }
                    this.loadAd(format)
                }
            },

            // ---------------------------------------------------------- 场景一
            // 看广告解锁:激励视频优先,拉不起来时用插屏式激励视频兜底
            adUnlock() {
                if (this.unlocking) return
                this.unlocking = true
                this.unlocked = false
                this.pendingUnlock = true
                this.log('开始「看广告解锁」流程…')

                this.ensureRewardAd((format) => {
                    if (!format) {
                        this.pendingUnlock = false
                        this.unlocking = false
                        this.log('两种激励类广告都没能加载出来')
                        uni.showToast({ title: '广告暂不可用,请稍后重试', icon: 'none' })
                        return
                    }
                    this.log('准备播放:' + format)
                    this.showAd(format)
                })
            },

            /**
             * 确保有一个可播放的激励类广告
             * @param {Function} callback 参数为可播放的 format,都没有则回传空字符串
             */
            ensureRewardAd(callback) {
                if (this.ready.rewarded) {
                    callback('rewarded')
                    return
                }
                if (this.ready.rewardedInterstitial) {
                    callback('rewardedInterstitial')
                    return
                }
                // 都没就绪,先补一次加载:激励视频 → 插屏式激励视频
                this.loadAd('rewarded', (ok) => {
                    if (ok) {
                        callback('rewarded')
                        return
                    }
                    this.loadAd('rewardedInterstitial', (ok2) => {
                        callback(ok2 ? 'rewardedInterstitial' : '')
                    })
                })
            }
        }
    }
</script>

<style>
    .page {
        min-height: 100vh;
        background-color: #f2f3f5;
        padding: 24rpx 24rpx 60rpx;
        box-sizing: border-box;
    }

    .header {
        padding: 16rpx 8rpx 24rpx;
    }

    .title {
        font-size: 40rpx;
        font-weight: bold;
        color: #1f2329;
    }

    .sub {
        margin-top: 8rpx;
        font-size: 24rpx;
        color: #8a8f99;
    }

    .card {
        background-color: #ffffff;
        border-radius: 16rpx;
        padding: 28rpx;
        margin-bottom: 24rpx;
    }

    .card-title {
        display: flex;
        justify-content: space-between;
        align-items: center;
        font-size: 28rpx;
        font-weight: bold;
        color: #1f2329;
        margin-bottom: 20rpx;
    }

    .clear-btn {
        font-size: 24rpx;
        font-weight: normal;
        color: #8a8f99;
    }

    /* 广告位状态 */
    .status-row,
    .ad-row {
        display: flex;
        align-items: center;
        margin-bottom: 16rpx;
    }

    .status-row:last-child,
    .ad-row:last-child {
        margin-bottom: 0;
    }

    .status-name,
    .ad-name {
        flex: 1;
        font-size: 26rpx;
        color: #4b5058;
    }

    .dot {
        width: 14rpx;
        height: 14rpx;
        border-radius: 50%;
        margin-right: 10rpx;
    }

    .dot-on {
        background-color: #07c160;
    }

    .dot-off {
        background-color: #c9cdd4;
    }

    .status-text {
        width: 110rpx;
        text-align: right;
        font-size: 24rpx;
    }

    .text-on {
        color: #07c160;
    }

    .text-off {
        color: #8a8f99;
    }

    /* 解锁区 */
    .unlock-text {
        font-size: 26rpx;
        margin-bottom: 20rpx;
    }

    /* 按钮 */
    .btn {
        display: flex;
        align-items: center;
        justify-content: center;
        height: 72rpx;
        border-radius: 12rpx;
        font-size: 28rpx;
        background-color: #f2f3f5;
        color: #1f2329;
    }

    .btn-primary {
        background-color: #1677ff;
        color: #ffffff;
    }

    .btn-mini {
        width: 120rpx;
        height: 60rpx;
        font-size: 26rpx;
        margin-left: 16rpx;
    }

    .btn-disabled {
        opacity: 0.45;
    }

    /* 日志 */
    .log-box {
        height: 460rpx;
        background-color: #1f2329;
        border-radius: 12rpx;
        padding: 16rpx;
        box-sizing: border-box;
    }

    .log-line {
        font-size: 22rpx;
        line-height: 34rpx;
        color: #a6c8ff;
        font-family: Menlo, Consolas, monospace;
        word-break: break-all;
    }

    .log-empty {
        font-size: 24rpx;
        color: #6b7280;
    }
</style>

7.1 这份 Demo 里值得抄走的几条规则

规则 原因
广告位用 IS_IOS 运行时三元取,不用条件编译 经典 uni-app 的 vue 服务层两端同一份 app-service.js,#ifdef 会被丢弃
奖励只在 code == 105 时发放 提前关闭(103)不会给 105,这是正常行为,别用「关闭」当成功
每次 show* 后都重新 load* 激励类广告是一次性消耗品,不重新加载第二次点就是「加载失败」
加载失败/展示失败静默处理,不阻塞用户 海外网络不稳时广告拉不起来很常见,业务逻辑不能依赖广告成功
加载与展示分离(先 ready 再展示) load 是异步的,边点边加载必然黑屏或丢广告

隐私、权限声明

1. 本插件需要申请的系统权限列表:

无

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

无

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

有,Google AdMob,具体详情见官网:https://developers.google.com/admob

暂无用户评论。