> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.onesignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 构建按钮驱动的引导轮播

> 使用带有按钮导航和自动关闭功能的 HTML 应用内消息创建多步骤引导流程。

## 概述

本教程向您展示如何使用单个 HTML 应用内消息创建**多步骤引导轮播**。与依赖滑动手势的传统轮播不同，此方法使用**按钮驱动导航**，并将所有步骤保持在一条消息中。

**您将构建的内容：**

* 包含图片、文本和按钮的两步引导流程
* 按钮导航（点击"下一步"前进，点击"开始使用"关闭）
* 进度指示点
* 步骤之间的平滑淡入淡出过渡

<Frame caption="带有按钮驱动导航的示例引导轮播">
  <img src="https://mintcdn.com/onesignal/t_Vr4vLAhranGx-O/images/docs/onboarding-carousel-demo.gif?s=39418d8af9896323694f46f9cb6eebec" alt="展示欢迎屏幕、图片、文本和下一步按钮的引导轮播" style={{width: '200px', maxWidth: '100%'}} width="768" height="1660" data-path="images/docs/onboarding-carousel-demo.gif" />
</Frame>

**在以下情况使用此方法：**

* 引导用户完成**简短的引导或教育流程**（2-5 步）
* 要求用户**明确点击按钮**才能继续（无滑动手势）
* 将所有内容保持在**一个 HTML 应用内消息**中以简化操作
* 流程完成时自动关闭消息

<Info>
  本指南使用 HTML 应用内消息以获得完全控制。您也可以[使用拖放编辑器构建基于卡片的引导流程](./design-your-in-app-message#carousels)——这些卡片支持滑动但定制性较低。
</Info>

***

## 前提条件

开始之前，请确保您具备：

* 一个已启用应用内消息的活跃 OneSignal 应用
* [创建或编辑 HTML 应用内消息的权限](./manage-team-members#team-members)
* 在您的移动应用中安装了 [Mobile SDK](./mobile-sdk-setup)
* 对 HTML、CSS 和 JavaScript 有基本了解

***

## 多步骤流程的工作原理

在深入代码之前，了解技术方法很重要。此实现使用**一个 HTML 应用内消息**，通过**显示和隐藏内容**在步骤之间切换，而不是加载多个单独的消息。

该架构依赖于**四个核心组件**：

<Steps>
  <Step title="每个步骤的卡片容器">
    每个步骤都包装在一个带有 `card` 类和唯一 ID 的 `<div>` 中：

    ```html theme={null}
    <div id="card-0" class="card active">...</div>
    <div id="card-1" class="card">...</div>
    ```

    * 所有卡片同时存在于 DOM 中
    * 一次只有一张卡片可见（由 `active` 类控制）
  </Step>

  <Step title="CSS 可见性控制">
    CSS 使用透明度和指针事件处理显示/隐藏逻辑：

    ```css theme={null}
    .card {
      opacity: 0;
      pointer-events: none;  /* 防止与隐藏卡片的交互 */
      transition: opacity .25s ease;
    }

    .card.active {
      opacity: 1;
      pointer-events: auto;  /* 允许与可见卡片的交互 */
    }
    ```

    **为什么这很重要：**

    * `opacity: 0` 在视觉上隐藏卡片但保留其在布局中的位置
    * `pointer-events: none` 防止意外点击隐藏的卡片
    * `transition` 创建平滑的淡入淡出效果
  </Step>

  <Step title="JavaScript 状态管理">
    `setActive(i)` 函数控制哪张卡片可见：

    ```javascript theme={null}
    function setActive(i) {
      // 更新卡片可见性
      document.getElementById("card-0").className = i === 0 ? "card active" : "card";
      document.getElementById("card-1").className = i === 1 ? "card active" : "card";

      // 更新进度点
      var dots = document.getElementById("dots").children;
      dots[0].classList.toggle("active", i === 0);
      dots[1].classList.toggle("active", i === 1);
    }
    ```

    此函数：

    * 从所有卡片中移除 `active`
    * 将 `active` 添加到目标卡片
    * 更新进度指示点
  </Step>

  <Step title="按钮事件监听器">
    按钮触发导航或关闭：

    ```javascript theme={null}
    // 前进到下一步
    document.getElementById("next-0").addEventListener("click", function () {
      setActive(1);
    });

    // 关闭应用内消息
    document.getElementById("done").addEventListener("click", function (e) {
      if (window.OneSignalIamApi && OneSignalIamApi.close) {
        OneSignalIamApi.close(e);
      }
    });
    ```

    **重要提示：** `OneSignalIamApi.close(e)` 是从 HTML 内部关闭应用内消息的 OneSignal SDK 方法。
  </Step>
</Steps>

<Note>
  **关键见解：** 这是应用内消息中的**单页应用 (SPA)** 模式。所有内容一次性加载，JavaScript 管理状态变化而无需重新加载。
</Note>

***

## 步骤 1：创建新的 HTML 应用内消息

1. 在 OneSignal 仪表板中，转到 **Messages → In-App Messages**
2. 点击 **New In-App Message**
3. 选择 **HTML** 作为消息类型
4. 选择 **Full Screen** 或 **Large** 布局（推荐用于引导以最大化视觉效果）
5. 继续进入 HTML 编辑器

<Note>
  HTML 编辑器预览可能无法完全反映运行时行为。请始终在真实设备或测试用户上进行测试，以验证动画、按钮行为和关闭操作。
</Note>

***

## 步骤 2：添加 HTML 模板

用以下模板替换编辑器内容。此模板包括：

* **自包含代码：** 所有 HTML、CSS 和 JavaScript 在一个文件中
* **按钮驱动导航：** 无滑动手势（跨设备更可靠）
* **淡入淡出过渡：** 步骤之间平滑的透明度变化
* **OneSignal SDK 集成：** 使用 `OneSignalIamApi.close(e)` 关闭消息
* **移动端优化：** 带有 viewport meta 标签的响应式布局

<Accordion title="查看完整 HTML 模板">
  ```html theme={null}
  <!doctype html>
  <html>
  <head>
    <meta charset="UTF-8" />
    <!-- viewport-fit=cover 确保在带刘海的设备上覆盖安全区域 -->
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
    <style>
      /* 基础样式 - 重置和系统字体 */
      html, body {
        margin: 0;
        padding: 0;
        background: #ffffff;
        font-family: -apple-system, system-ui;
      }

      /* 带内边距的主容器 */
      .wrap {
        padding: 28px 22px 24px;
      }

      /* 舞台容器 - 将所有卡片保持在同一位置 */
      .stage {
        position: relative;
        min-height: 74vh;  /* 确保足够的垂直空间 */
      }

      /* 卡片 - 引导流程的每个步骤 */
      .card {
        position: absolute;  /* 所有卡片重叠在同一位置 */
        inset: 0;            /* 完全覆盖舞台 */
        display: flex;
        flex-direction: column;
        align-items: center;
        opacity: 0;               /* 默认隐藏 */
        pointer-events: none;     /* 隐藏时防止点击 */
        transition: opacity .25s ease;  /* 平滑淡入淡出效果 */
      }

      /* 活动卡片可见且可交互 */
      .card.active {
        opacity: 1;
        pointer-events: auto;
      }

      /* 排版 */
      h1 {
        margin: 44px 0 12px;
        font-size: 26px;
        text-align: center;
      }

      p {
        margin: 0;
        color: #6b7280;
        text-align: center;
        max-width: 260px;
        line-height: 1.35;
      }

      /* 图片容器 - 带圆角的正方形 */
      .image {
        width: 240px;
        height: 240px;
        border-radius: 16px;
        margin: 24px 0 12px;
        background-size: cover;
        background-position: center;
      }

      /* 主按钮 */
      .btn {
        margin-top: auto;  /* 将按钮推到卡片底部 */
        width: 100%;
        max-width: 260px;
        height: 52px;
        border: 0;
        border-radius: 12px;
        background: #3b82f6;  /* 蓝色 - 可根据您的品牌定制 */
        color: #fff;
        font-size: 18px;
        font-weight: 600;
      }

      /* 进度指示点 */
      .dots {
        display: flex;
        justify-content: center;
        gap: 8px;
        padding: 12px 0 8px;
      }

      .dot {
        width: 8px;
        height: 8px;
        border-radius: 999px;
        background: #d1d5db;  /* 非活动点颜色 */
      }

      .dot.active {
        background: #6b7280;  /* 活动点颜色 */
        transform: scale(1.15);  /* 活动时略大 */
      }
    </style>
  </head>

  <body>
    <div class="wrap">
      <div class="stage">

        <!-- 步骤 1：欢迎卡片（以 "active" 类开始可见） -->
        <div id="card-0" class="card active">
          <h1>欢迎</h1>
          <div
            class="image"
            style="background-image: url('https://images.pexels.com/photos/6153129/pexels-photo-6153129.jpeg');">
          </div>
          <p>几分钟内养成平静的日常习惯。</p>
          <button
            id="next-0"
            class="btn"
            data-onesignal-unique-label="onboarding_next_0">
            下一步
          </button>
        </div>

        <!-- 步骤 2：呼吸卡片（开始隐藏，用户点击"下一步"时显示） -->
        <div id="card-1" class="card">
          <h1>呼吸</h1>
          <div
            class="image"
            style="background-image: url('https://images.pexels.com/photos/417173/pexels-photo-417173.jpeg');">
          </div>
          <p>随时进行引导式呼吸来重置状态。</p>
          <button
            id="done"
            class="btn"
            data-onesignal-unique-label="onboarding_done">
            开始使用
          </button>
        </div>

      </div>

      <!-- 进度指示器：2 个点，第一个开始时为活动状态 -->
      <div class="dots" id="dots">
        <div class="dot active"></div>
        <div class="dot"></div>
      </div>
    </div>

    <script>
      (function () {
        /**
         * 通过切换 "active" 类在卡片之间切换
         * @param {number} i - 要显示的卡片索引（0 或 1）
         */
        function setActive(i) {
          // 更新卡片可见性
          document.getElementById("card-0").className = i === 0 ? "card active" : "card";
          document.getElementById("card-1").className = i === 1 ? "card active" : "card";

          // 更新进度点
          var dots = document.getElementById("dots").children;
          dots[0].classList.toggle("active", i === 0);
          dots[1].classList.toggle("active", i === 1);
        }

        // 按钮：下一步（卡片 0 → 卡片 1）
        document.getElementById("next-0").addEventListener("click", function () {
          setActive(1);
        });

        // 按钮：开始使用（关闭应用内消息）
        document.getElementById("done").addEventListener("click", function (e) {
          // 检查 OneSignal IAM API 是否可用
          if (window.OneSignalIamApi && OneSignalIamApi.close) {
            OneSignalIamApi.close(e);  // 关闭消息
          }
        });
      })();
    </script>
  </body>
  </html>
  ```
</Accordion>

***

## 步骤 3：自定义您的内容

### 可安全自定义的内容

您可以修改这些元素而不会破坏功能：

**内容：**

* `<h1>` 标签中的标题文本
* `<p>` 标签中的正文内容
* 按钮标签（`下一步`、`开始使用`）
* `background-image: url('...')` 样式中的图片 URL

**视觉样式：**

* 颜色：更改 `.btn` 背景色、文本颜色或点的颜色
* 间距：调整内边距和外边距
* 排版：修改 font-family、font-size、font-weight
* 圆角：更新按钮和图片的 `border-radius` 值

### 添加更多步骤

要添加第三个步骤，请按照以下模式操作：

1. **添加 HTML 卡片：**

```html theme={null}
<div id="card-2" class="card">
  <h1>您的标题</h1>
  <div class="image" style="background-image: url('your-image-url');"></div>
  <p>您的描述</p>
  <button id="next-2" class="btn">下一步</button>
</div>
```

2. **添加进度点：**

```html theme={null}
<div class="dots" id="dots">
  <div class="dot active"></div>
  <div class="dot"></div>
  <div class="dot"></div> <!-- 新点 -->
</div>
```

3. **更新 `setActive()` 函数：**

```javascript theme={null}
function setActive(i) {
  document.getElementById("card-0").className = i === 0 ? "card active" : "card";
  document.getElementById("card-1").className = i === 1 ? "card active" : "card";
  document.getElementById("card-2").className = i === 2 ? "card active" : "card"; // 新卡片

  var dots = document.getElementById("dots").children;
  dots[0].classList.toggle("active", i === 0);
  dots[1].classList.toggle("active", i === 1);
  dots[2].classList.toggle("active", i === 2); // 新点
}
```

4. **更新上一步骤的按钮 ID：**
   将卡片 1 按钮的 `id="done"` 更改为 `id="next-1"`，然后添加点击监听器：

```javascript theme={null}
document.getElementById("next-1").addEventListener("click", function () {
  setActive(2);
});
```

5. **将关闭按钮添加到新的最后一张卡片（card-2）：**

```javascript theme={null}
document.getElementById("done").addEventListener("click", function (e) {
  if (window.OneSignalIamApi && OneSignalIamApi.close) {
    OneSignalIamApi.close(e);
  }
});
```

<Warning>
  保持引导流程简短（最多 2-4 步）。用户在较长的流程中会快速流失。使用[点击跟踪](./in-app-message-api#click-name)测试完成率。
</Warning>

***

## 步骤 4：测试应用内消息

### 测试清单

1. 在 OneSignal 仪表板中**保存**消息
2. **配置发送设置：**
   * 设置触发条件（例如，会话开始、特定页面浏览）
   * 选择目标受众或选择测试用户
3. **发送到测试设备：**
   * 使用[测试用户](./test-users)预览而不影响生产用户
   * 在物理设备上安装您的应用（推荐而非模拟器以获得准确行为）
4. **验证功能：**
   * ✓ 第一张卡片显示正确内容
   * ✓ "下一步"按钮前进到卡片 2
   * ✓ 进度点正确更新
   * ✓ 淡入淡出过渡平滑
   * ✓ "开始使用"按钮关闭消息
   * ✓ 消息不会立即重新出现（检查频率限制设置）

<Note>
  模拟器可能无法准确反映真实设备行为，特别是触摸交互和 SDK 集成。在发布到生产环境之前，请务必在物理设备上进行测试。
</Note>

### 常见问题故障排除

| 问题        | 可能原因                  | 解决方案                                        |
| --------- | --------------------- | ------------------------------------------- |
| 消息未出现     | 未满足触发条件               | 检查[应用内消息触发器](./iam-triggers)并验证您的测试用户是否满足条件 |
| 按钮不起作用    | JavaScript 错误或 ID 不匹配 | 检查浏览器控制台是否有错误；验证按钮 ID 与事件监听器 ID 是否匹配        |
| 图片未加载     | CORS 问题或无效 URL        | 使用 HTTPS URL；首先在浏览器中测试图片 URL                |
| 消息出现但无法关闭 | OneSignal SDK 未加载     | 验证 [Mobile SDK 设置](./mobile-sdk-setup)是否完成  |

***

## 后续步骤

**跟踪用户参与度：**

* 使用 [`data-onesignal-unique-label`](./in-app-message-api#click-name) 属性添加点击跟踪（模板中已包含）以衡量步骤之间的流失率
* 在 **Messages → In-App Messages → \[您的消息] → Analytics** 中查看点击分析

**个性化体验：**

* 为完成引导的用户[添加标签](./in-app-message-api#tag-user)（例如，`onboarding_completed: true`）
* 使用标签[细分用户](./segmentation)并防止重复显示引导流程
* [添加用户数据](./add-user-data-tags)以个性化未来消息中的内容

**高级自定义：**

* 关闭后将用户[深度链接](./deep-linking#send-in-app-messages-with-deep-links)到特定屏幕
* 使用 [Liquid 语法](./using-liquid-syntax)用用户名或属性个性化标题
* 使用不同的引导流程实施 A/B 测试以优化完成率
