跳转至主要内容

Shopify FAQs Shopify 中文教程

在客户即将离开 Shopify 网站时弹窗促销信息进行挽留 降低弃购

一、介绍

本期教程分享如何在 Shopify 网站中不借助任何插件实现客户挽留弹窗,当客户即将离开网站时弹出折扣促销信息进行挽留,降低弃购,提高订单转化率。

shopify-exit-intent-popup

弹窗中的内容主要由以下几个部分组成:

  • 促销信息标题和文字
  • 折扣码和一键复制
  • 带绿色小圆点的简短描述
  • 支持自定义链接的跳转按钮
  • 支持自定义时间的倒计时
shopify-exit-popup-retention

通过这个功能可以挽回即将离开的客户,提供折扣促进订单转化。同时倒计时也能制造紧迫感,让客户不想错失优惠,进而提高销售额。

主要的触发机制是:

  • 电脑端检测到鼠标往浏览器标签页区域移动时弹窗
  • 手机端由于没有鼠标检测所以设置为延时弹窗(支持自定义时间)

二、添加方法

创建文件

首先创建一个 Snippet:exit-intent-modal.liquid

如果你跟随本期视频教程在 Shopify 后台添加,复制文件名:

exit-intent-modal

代码如下:

<style>
    .exit-modal-container * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    .exit-modal-container {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
        "Helvetica Neue", Arial, sans-serif;
      line-height: 1.6;
      color: #374151;
    }

    /* 动画效果 */
    @keyframes fadeIn {
      from {
        opacity: 0;
      }
      to {
        opacity: 1;
      }
    }

    @keyframes slideIn {
      from {
        opacity: 0;
        transform: scale(0.95) translateY(-10px);
      }
      to {
        opacity: 1;
        transform: scale(1) translateY(0);
      }
    }

    .modal-backdrop {
      animation: fadeIn 0.3s ease-out;
    }

    .modal-content {
      animation: slideIn 0.3s ease-out;
    }

    .hidden {
      display: none !important;
    }

    /* 模态框样式 - 限定在 exit-modal-container 作用域内 */
    .modal-overlay {
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      z-index: 50;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    .modal-backdrop-bg {
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: rgba(0, 0, 0, 0.6);
      backdrop-filter: blur(4px);
    }

    .modal-container {
      position: relative;
      background: white;
      border-radius: 1rem;
      box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
      max-width: 500px;
      margin: 0 1rem;
      width: 100%;
    }

    @media (max-width: 768px) {
      .modal-container {
        max-width: 90%;
      }
    }

    .modal-close-btn {
      position: absolute;
      top: 1rem;
      right: 1rem;
      padding: 0.5rem 0.6rem;
      padding-top: 0.6rem;
      border-radius: 50%;
      background: transparent;
      border: none;
      cursor: pointer;
      z-index: 10;
      transition: background-color 0.2s;
    }

    .modal-close-btn:hover {
      background: #f3f4f6;
    }

    .modal-close-icon {
      width: 1.25rem;
      height: 1.25rem;
      color: #6b7280;
    }

    .modal-header {
      background: linear-gradient(
        135deg,
        #9333ea 0%,
        #ec4899 50%,
        #ef4444 100%
      );
      border-radius: 1rem 1rem 0 0;
      padding: 2.5rem;
      color: white;
      position: relative;
      overflow: hidden;
    }

    .modal-header-bg-1 {
      position: absolute;
      top: 0;
      right: 0;
      width: 8rem;
      height: 8rem;
      background: rgba(255, 255, 255, 0.1);
      border-radius: 50%;
      transform: translate(4rem, -4rem);
    }

    .modal-header-bg-2 {
      position: absolute;
      bottom: 0;
      left: 0;
      width: 6rem;
      height: 6rem;
      background: rgba(255, 255, 255, 0.1);
      border-radius: 50%;
      transform: translate(-3rem, 3rem);
    }

    .modal-header-content {
      position: relative;
    }

    .modal-header-top {
      display: flex;
      align-items: center;
      gap: 0.75rem;
      margin-bottom: 1rem;
    }

    .modal-icon-container {
        padding: 0.75rem;
        padding-top: 0.85rem;
        padding-bottom: 0.55rem;
        background: rgba(255, 255, 255, 0.2);
        border-radius: 50%;
    }

    .modal-icon {
      width: 24px;
      height: 24px;
    }

    .modal-title {
      font-size: 24px;
      font-weight: 700;
      color: #f3f4f6;
    }

    .modal-badge {
      display: flex;
      align-items: center;
      gap: 0.25rem;
      color: #fef3c7;
    }

    .modal-badge-icon {
      width: 1rem;
      height: 1rem;
    }

    .modal-badge-text {
      font-size: 14px;
    }

    .modal-discount {
      font-size: 18px;
      font-weight: 500;
      margin-bottom: 0.5rem;
    }

    .modal-discount-number {
      font-size: 30px;
      font-weight: 700;
      color: #fde047;
    }

    .modal-subtitle {
      color: #f3f4f6;
      font-size: 14px;
    }

    .modal-body {
      padding: 2.5rem;
    }

    .modal-content-title {
      text-align: center;
      margin-bottom: 1.5rem;
    }

    .modal-content-heading {
      font-size: 20px;
      font-weight: 600;
      color: #1f2937;
      margin-bottom: 0.5rem;
    }

    .modal-content-description {
      color: #6b7280;
      font-size: 14px;
      margin-bottom: 1rem;
    }

    .discount-code-box {
      background: linear-gradient(to right, #f9fafb, #f3f4f6);
      border: 2px dashed #d1d5db;
      border-radius: 0.75rem;
      padding: 1rem;
      margin-bottom: 1rem;
    }

    .discount-code {
      font-size: 24px;
      font-family: "Courier New", monospace;
      font-weight: 700;
      color: #ee476a;
      letter-spacing: 0.1em;
    }

    .copy-button {
      display: inline-flex;
      align-items: center;
      gap: 0.5rem;
      padding: 0.5rem 1rem;
      border-radius: 0.5rem;
      font-weight: 500;
      border: none;
      cursor: pointer;
      transition: all 0.2s;
      background: linear-gradient(to right, #9333ea, #ec4899);
      color: white;
    }

    .copy-button:hover {
      background: linear-gradient(to right, #7c3aed, #db2777);
      transform: scale(1.05);
    }

    .copy-button.copied {
      background: #10b981;
    }

    .copy-icon {
      width: 1rem;
      height: 1rem;
    }

    .benefits-list {
      margin-bottom: 1.5rem;
    }

    .benefit-item {
      display: flex;
      align-items: center;
      gap: 0.75rem;
      font-size: 14px;
      color: #374151;
      margin-bottom: 0.75rem;
    }

    .benefit-dot {
      width: 0.5rem;
      height: 0.5rem;
      background: #10b981;
      border-radius: 50%;
    }

    .benefit-item .benefit-dot {
        display: block;
    }

    .action-buttons {
      display: flex;
      gap: 0.75rem;
    }

    .action-button {
      flex: 1;
      padding: 0.75rem 1rem;
      border-radius: 0.75rem;
      font-weight: 500;
      border: none;
      cursor: pointer;
      transition: all 0.2s;
    }

    .action-button.secondary {
      border: 1px solid #d1d5db;
      color: #374151;
      background: white;
    }

    .action-button.secondary:hover {
      background: #f9fafb;
    }

    .action-button.primary {
      background: linear-gradient(to right, #9333ea, #ec4899);
      color: white;
    }

    .action-button.primary:hover {
      background: linear-gradient(to right, #7c3aed, #db2777);
      transform: scale(1.05);
    }

    .timer-section {
      margin-top: 2rem;
      text-align: center;
    }

    .timer-text {
      font-size: 12px;
      color: #6b7280;
    }

    .countdown {
      font-family: "Courier New", monospace;
      font-weight: 600;
      color: #dc2626;
      margin-left: 0.25rem;
    }
  </style>
  <!-- 退出意图模态框 -->
  <div id="exitModal" class="modal-overlay hidden exit-modal-container">
    <!-- 背景遮罩 -->
    <div
      class="modal-backdrop-bg modal-backdrop"
      onclick="closeModal()"
    ></div>

    <!-- 模态框 -->
    <div class="modal-container modal-content">
      <!-- 关闭按钮 -->
      <button onclick="closeModal()" class="modal-close-btn">
        <svg
          class="modal-close-icon"
          fill="none"
          stroke="currentColor"
          viewBox="0 0 24 24"
        >
          <path
            stroke-linecap="round"
            stroke-linejoin="round"
            stroke-width="2"
            d="M6 18L18 6M6 6l12 12"
          ></path>
        </svg>
      </button>

      <!-- 带渐变背景的头部 -->
      <div class="modal-header">
        <div class="modal-header-bg-1"></div>
        <div class="modal-header-bg-2"></div>

        <div class="modal-header-content">
          <div class="modal-header-top">
            <div class="modal-icon-container">
              <svg
                class="modal-icon"
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
              >
                <path
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  stroke-width="2"
                  d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7"
                ></path>
              </svg>
            </div>
            <div>
              <h2 class="modal-title">Wait! Don't Leave!</h2>
              <div class="modal-badge">
                <svg
                  class="modal-badge-icon"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                >
                  <path
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    stroke-width="2"
                    d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z"
                  ></path>
                </svg>
                <span class="modal-badge-text">Limited Time Offer</span>
              </div>
            </div>
          </div>

          <p class="modal-discount">
            Get <span class="modal-discount-number">20%</span> OFF
          </p>
          <p class="modal-subtitle">
            Don't miss out on this exclusive deal before you go!
          </p>
        </div>
      </div>

      <!-- 内容区域 -->
      <div class="modal-body">
        <div class="modal-content-title">
          <h3 class="modal-content-heading">🎉 Exclusive Discount Code</h3>
          <p class="modal-content-description">
            Copy the discount code below and save 20% instantly!
          </p>

          <!-- 折扣码框 -->
          <div class="discount-code-box">
            <div class="discount-code" id="discountCodeDisplay">SAVE20</div>
            <button
              id="copyBtn"
              onclick="copyToClipboard()"
              class="copy-button"
            >
              <svg
                id="copyIcon"
                class="copy-icon"
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
              >
                <path
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  stroke-width="2"
                  d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
                ></path>
              </svg>
              <span id="copyText">Copy Code</span>
            </button>
          </div>
        </div>

        <!-- 优惠列表 -->
        <div class="benefits-list">
          <div class="benefit-item">
            <div class="benefit-dot"></div>
            <span>Valid on all products</span>
          </div>
          <div class="benefit-item">
            <div class="benefit-dot"></div>
            <span>Free shipping on orders $99+</span>
          </div>
          <div class="benefit-item">
            <div class="benefit-dot"></div>
            <span>Limited time - today only!</span>
          </div>
        </div>

        <!-- 操作按钮 -->
        <div class="action-buttons">
          <button onclick="closeModal()" class="action-button secondary">
            Maybe Later
          </button>
          <button onclick="shopNow()" class="action-button primary">
            Shop Now
          </button>
        </div>

        <!-- 倒计时 -->
        <div class="timer-section">
          <p class="timer-text">
            ⏰ Offer expires in:
            <span id="countdown" class="countdown"> 23:59:45 </span>
          </p>
        </div>
      </div>
    </div>
  </div>

  <script>
    // ========== 配置变量 ==========
    // 设置这些变量来自定义模态框行为

    // 是否在每个会话中只显示一次模态框 (true) 或每次满足条件时都显示 (false)
    const SHOW_ONCE_PER_SESSION = false;

    // 点击"立即购买"按钮时要跳转的URL
    const REDIRECT_URL = "https://shopify.now";

    // 要复制的折扣码
    const DISCOUNT_CODE = "SAVE20";

    // 退出意图敏感度(距离屏幕顶部的像素)
    const EXIT_SENSITIVITY = 50;

    // 检测到退出意图后显示模态框前的延迟(毫秒)
    const EXIT_DELAY = 100;

    // 移动端延迟(毫秒)- 在移动设备上等待多长时间后显示模态框
    const MOBILE_DELAY = 5000;

    // ========== 配置结束 ==========

    // 退出意图模态框逻辑
    class ExitIntentModal {
      constructor() {
        this.modal = document.getElementById("exitModal");
        this.hasShown = false;
        this.countdownTimer = null;

        this.init();
      }

      init() {
        // 检查此会话中是否已显示过模态框(仅当 SHOW_ONCE_PER_SESSION 为 true 时)
        if (
          SHOW_ONCE_PER_SESSION &&
          sessionStorage.getItem("exitIntentShown") === "true"
        ) {
          this.hasShown = true;
        }

        // 设置事件监听器
        if (this.isMobile()) {
          this.setupMobileTimer();
        } else {
          this.setupDesktopListeners();
        }

        // 启动倒计时
        this.startCountdown();
      }

      isMobile() {
        return (
          /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
            navigator.userAgent
          ) || window.innerWidth <= 768
        );
      }

      setupDesktopListeners() {
        document.addEventListener("mouseleave", (e) => {
          if (this.hasShown) return;

          // 检查鼠标是否向屏幕顶部移动
          if (e.clientY <= EXIT_SENSITIVITY) {
            setTimeout(() => {
              this.showModal();
            }, EXIT_DELAY);
          }
        });
      }

      setupMobileTimer() {
        if (this.hasShown) return;

        setTimeout(() => {
          this.showModal();
        }, MOBILE_DELAY);
      }

      showModal() {
        if (this.hasShown) return;

        this.modal.classList.remove("hidden");
        this.hasShown = true;

        // 仅当 SHOW_ONCE_PER_SESSION 为 true 时才设置 sessionStorage
        if (SHOW_ONCE_PER_SESSION) {
          sessionStorage.setItem("exitIntentShown", "true");
        }
      }

      closeModal() {
        this.modal.classList.add("hidden");
      }

      startCountdown() {
        // 设置初始时间为从现在开始的两小时
        const endTime = new Date().getTime() + 2 * 60 * 60 * 1000;

        this.countdownTimer = setInterval(() => {
          const now = new Date().getTime();
          const distance = endTime - now;

          if (distance > 0) {
            const hours = Math.floor(
              (distance % (1000 * 60 * 60 * 2)) / (1000 * 60 * 60)
            );
            const minutes = Math.floor(
              (distance % (1000 * 60 * 60)) / (1000 * 60)
            );
            const seconds = Math.floor((distance % (1000 * 60)) / 1000);

            const formattedTime = `${this.formatTime(
              hours
            )}:${this.formatTime(minutes)}:${this.formatTime(seconds)}`;
            document.getElementById("countdown").textContent = formattedTime;
          } else {
            // 倒计时已结束
            document.getElementById("countdown").textContent = "00:00:00";
            clearInterval(this.countdownTimer);
          }
        }, 1000);
      }

      formatTime(time) {
        return time.toString().padStart(2, "0");
      }

      reset() {
        this.hasShown = false;
        if (SHOW_ONCE_PER_SESSION) {
          sessionStorage.removeItem("exitIntentShown");
        }
        this.closeModal();
      }
    }

    // 初始化模态框
    const exitModal = new ExitIntentModal();

    // 用于按钮交互的全局函数
    function closeModal() {
      exitModal.closeModal();
    }

    function resetModal() {
      exitModal.reset();
    }

    function copyToClipboard() {
      const copyBtn = document.getElementById("copyBtn");
      const copyIcon = document.getElementById("copyIcon");
      const copyText = document.getElementById("copyText");

      // 尝试复制到剪贴板
      if (navigator.clipboard) {
        navigator.clipboard
          .writeText(DISCOUNT_CODE)
          .then(() => {
            showCopiedState();
          })
          .catch(() => {
            fallbackCopy();
          });
      } else {
        fallbackCopy();
      }

      function fallbackCopy() {
        // 旧版浏览器的备用方案
        const textArea = document.createElement("textarea");
        textArea.value = DISCOUNT_CODE;
        document.body.appendChild(textArea);
        textArea.select();
        document.execCommand("copy");
        document.body.removeChild(textArea);
        showCopiedState();
      }

      function showCopiedState() {
        // 更新按钮以显示已复制状态
        copyBtn.className = "copy-button copied";
        copyIcon.innerHTML =
          '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>';
        copyText.textContent = "Copied!";

        // 2秒后重置
        setTimeout(() => {
          copyBtn.className = "copy-button";
          copyIcon.innerHTML =
            '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>';
          copyText.textContent = "Copy Code";
        }, 2000);
      }
    }

    function shopNow() {
      // 首先复制折扣码
      copyToClipboard();

      // 等待1秒,然后跳转到配置的URL
      setTimeout(() => {
        // 先关闭模态框
        closeModal();

        // 跳转到配置的URL
        if (REDIRECT_URL && REDIRECT_URL !== "#") {
          window.location.href = REDIRECT_URL;
        } else {
          console.log("No redirect URL configured or URL is #");
        }
      }, 1000);
    }
  </script>

引用代码片段

跟随视频教程,在 theme.liquid/body 上方添加:

{% render 'exit-intent-modal' %}

三、自定义配置

主要可以自定义的选项包括:

  • 是否只展示一次(为 true 则访问其他页面之后离开不重新展示)
  • 折扣码(需替换两处,展示和复制)
  • 点击 Shop Now 之后跳转的促销链接
  • 移动端延时多少秒弹窗

总之,常用的配置都已经添加注释,对于其他文本内容,直接复制进行搜索,找到之后修改替换即可。

第 135 期 零基础 Shopify 网页修改教程 常用方法演示 优化店铺必看
面向个人卖家和运营人员的 Shopify 零基础前端教程,修改店铺网页内容必看,讲解了如何修改 Shopify 店铺中的网页元素。主要包括如何在商城编辑器中编辑和自定义页面、产品、导航栏等常见元素。同时还介绍了一些高级技巧,如使用 CSS 微调页面布局。

如果你添加到主题之后希望对样式进行微调,可以复制代码提供给 Claude 或 Gemini 结合你自己的需求进行修改。

增强版主题

更适合中国商家的 Shopify 增强版主题,额外添加五十几项功能
Shopify 增强版主题,添加几十项功能,容易上手,持续更新,支持一键开启或关闭。适用于所有 Shopify 推出的 13 款 2.0 主题,减少插件安装,降低每月的插件订阅费。Shopify 店铺用什么主题比较好?哪款主题更好用?