小程序弹窗控制

49 阅读4分钟

弹窗系统(队列模式)

一:业务弹窗

  • 创建优惠券弹窗组件 src/components/PopupCoupon.vue
  • 创建积分弹窗组件 src/components/PopupPoints.vue
  • 创建公告弹窗组件 src/components/PopupAnnouncement.vue
  • 创建活动弹窗组件 src/components/PopupActivity.vue

二、接口调用时机

2.1 何时调用后台接口

调用时机:

  • 每次页面 onShow 时调用(实时获取最新配置)

接口: GET /api/popups/configs

请求参数:

{
  platform: 'mp-weixin' | 'mp-xhs',  // 平台
  userId?: string,                    // 用户ID(可选,用于个性化)
}

返回数据:

{
  code: 0,
  data: {
    configs: PopupConfig[],
  }
}

三、数据结构定义

/**
 * 弹窗类型
 */
export enum PopupType {
  /** 优惠券 */
  COUPON = 'coupon',
  /** 积分 */
  POINTS = 'points',
  /** 公告 */
  ANNOUNCEMENT = 'announcement',
  /** 活动 */
  ACTIVITY = 'activity',
  /** 自定义 */
  CUSTOM = 'custom',
}

/**
 * 弹窗配置
 */
export interface PopupConfig {
  /** 弹窗唯一标识 */
  id: string;
  /** 弹窗类型 */
  type: PopupType;
  /** 触发页面路径列表 */
  pages: string[];
  /** 是否启用 */
  enabled: boolean;
  /** 层级 */
  zIndex?: number;
  /** 展示时长(毫秒),0表示不自动关闭 */
  showDuration?: number;
  /** 是否可手动关闭 */
  closable?: boolean;
  /** 点击遮罩是否关闭 */
  maskClosable?: boolean;
  /** 展示间隔(秒),0表示每次都展示 */
  interval?: number;
  /** 是否显示遮罩 */
  showMask?: boolean;
  /** 最大展示次数,0表示无限制 */
  maxShowCount?: number;
  /** 弹窗内容/组件 */
  content?: any;
  /** 自定义数据 */
  data?: Record<string, any>;
}

/**
 * 弹窗展示记录
 */
export interface PopupShowRecord {
  /** 弹窗ID */
  popupId: string;
  /** 上次展示时间 */
  lastShowTime: number;
  /** 已展示次数 */
  showCount: number;
}

/**
 * 弹窗队列项
 */
export interface PopupQueueItem {
  config: PopupConfig;
  resolve: () => void;
}

四、完整时序图


sequenceDiagram
    participant User as 用户
    participant App as App.vue
    participant API as 后台接口
    participant Manager as PopupManager
    participant Storage as 本地缓存
    participant Page as 页面
    participant Container as PopupContainer
    participant Component as 弹窗组件

    %% 启动阶段
    User->>App: 打开小程序
    App->>App: onLaunch()
    Note over App: 不缓存配置<br/>每次从接口获取

    %% 页面展示阶段
    User->>Page: 进入首页
    Page->>Page: onShow()
    Page->>API: GET /api/popups/configs
    API-->>Page: 返回配置列表
    Page->>Manager: register(configs)
    Page->>Manager: trigger(currentPage)

    Manager->>Storage: 获取展示记录
    Storage-->>Manager: 返回记录

    Manager->>Manager: 筛选有效弹窗
    Note over Manager: 1. 检查启用状态<br/>2. 检查页面匹配<br/>3. 检查时间范围<br/>4. 检查展示间隔<br/>

    Manager->>Manager: 加入队列

    %% 弹窗展示阶段
    loop 处理队列中的每个弹窗
        Manager->>Manager: 取出队列首个弹窗
        Manager->>Container: emit('show-popup', config)

        Container->>Component: 加载对应组件
        Component-->>Container: 组件准备就绪
        Container->>User: 显示弹窗

        Manager->>Storage: 保存展示记录

        alt 关闭弹窗
            User->>Component: 点击关闭按钮
            Component->>Container: emit('close-popup')
        end

        Container->>User: 隐藏弹窗

        Manager->>Manager: 处理下一个弹窗
    end

    Manager-->>Page: 所有弹窗展示完成

五、技术点

5.1 防重复触发

// 使用标志位防止并发
private isShowing = false

private async processQueue() {
  if (this.isShowing || this.queue.length === 0) return
  this.isShowing = true
  // ... 处理逻辑
  this.isShowing = false
}

六、使用示例

// 页面中使用
import { popupManager } from '@/utils/popupManager';
import { getPopupConfigs } from '@/services/popup';

onShow(async () => {
  // 1. 获取当前页面路径
  const pages = getCurrentPages();
  const currentPage = '/' + pages[pages.length - 1].route;

  // 2. 每次都从接口获取最新配置
  const configs = await getPopupConfigs({
    platform: 'mp-weixin',
    userId: userStore.userId,
  });

  // 3. 注册配置
  popupManager.register(configs);

  // 4. 触发弹窗检查
  popupManager.trigger(currentPage);
});

// 手动清除展示记录(测试用)
popupManager.clearRecords('coupon_spring');

七、配置示例

// 后台返回的配置示例
{
  configs: [
    {
      id: 'coupon_spring_2024',
      type: 'coupon',
      pages: ['/pages-custom/pages/index/index'],
      enabled: true,
      zIndex: 9999,
      showDuration: 0, // 不自动关闭
      closable: true,
      maskClosable: true,
      showMask: true,
      interval: 86400, // 1天展示1次
      maxShowCount: 3, // 最多展示3次
      data: {
        title: '春季优惠券',
        couponIds: ['123', '456'],
      },
    },
    {
      id: 'points_daily',
      type: 'points',
      pages: ['/pages-custom/pages/index/index'],
      enabled: true,
      interval: 0, // 每次都展示
      maxShowCount: 0, // 无限次
      showDuration: 3000, // 3秒自动关闭
      closable: false, // 不可手动关闭
      maskClosable: false,
      data: {
        message: '签到成功,获得10积分',
      },
    },
    {
      id: 'announcement_important',
      type: 'announcement',
      pages: ['*'], // 所有页面
      enabled: true,
      interval: 604800, // 7天展示1次
      maxShowCount: 1, // 只展示1次
      data: {
        title: '重要通知',
        content: '系统将于今晚维护...',
      },
    },
  ];
}

弹窗系统(轮播模式)

一、需求概述

实现一个轮播式弹窗系统,多个弹窗在一个容器内自动切换展示,用户可以通过滑动或指示器切换,也可以设置自动播放。


二、数据结构定义

/**
 * 轮播弹窗配置(继承基础配置)
 */
export interface CarouselPopupConfig extends Pick<PopupConfig, 'enabled' | 'pages'> {
  /** 弹窗图片/内容 */
  imageUrl?: string;
  /** 点击跳转链接 */
  linkUrl?: string;
  /** 弹窗类型 */
  type: PopupType;
  /** 触发页面路径列表 */
  pages:string[];
}

/**
 * 轮播容器配置
 */
export interface CarouselContainerConfig {
  /** 是否启用 */
  enabled: boolean;
  /** 展示间隔(秒),0表示每次都展示 */
  interval?: number;
  /** 弹窗列表 */
  popups: CarouselPopupConfig[];
}

/**
 * 轮播状态
 */
export interface CarouselState {
  /** 当前索引 */
  currentIndex: number;
  /** 是否正在播放 */
  isPlaying: boolean;
  /** 定时器ID */
  timerId?: number;
  /** 总数量 */
  total: number;
}

三、完整时序图

sequenceDiagram
    participant User as 用户
    participant App as App.vue
    participant API as 后台接口
    participant Manager as CarouselPopupManager
    participant Storage as 本地缓存
    participant Page as 页面
    participant Container as CarouselContainer
    participant Swiper as 轮播组件

    %% 启动阶段
    User->>App: 打开小程序
    App->>App: onLaunch()
    Note over App: 不缓存配置<br/>每次从接口获取

    %% 页面展示阶段
    User->>Page: 进入首页
    Page->>Page: onShow()
    Page->>API: GET /api/popups/carousel/configs
    API-->>Page: 返回轮播配置
    Page->>Manager: register(configs)
    Page->>Manager: trigger(currentPage)

    Manager->>Storage: 获取展示记录
    Storage-->>Manager: 返回记录

    Manager->>Manager: 筛选有效弹窗
    Note over Manager: 1. 检查启用状态<br/>2. 检查页面匹配<br/>3. 检查展示间隔<br/>4. 检查展示次数

    alt 有符合条件的弹窗组
        Manager->>Container: emit('show-carousel', config)

        Container->>Swiper: 初始化轮播组件
        Container->>Swiper: 加载所有弹窗内容
        Swiper-->>Container: 内容加载完成

        Container->>User: 显示轮播容器(带遮罩)
        Container->>User: 显示第一个弹窗

        Manager->>Storage: 保存展示记录

        loop 轮播循环展示
            Note over Container,User: 自动播放或用户手动切换
            
            alt 用户滑动
                User->>Swiper: 滑动手势
                Swiper->>User: 切换弹窗(带动画)
            else 用户点击
                User->>Container: 点击弹窗内容
                alt 有跳转链接
                    Container->>Page: 跳转到业务页面
                    Container->>Container: 关闭轮播
                end
            else 用户关闭
                User->>Container: 点击关闭/遮罩
                Container->>Container: 停止轮播
                Container->>User: 隐藏轮播(带动画)
            end
        end
    else 无符合条件的弹窗
        Manager-->>Page: 不展示
    end

四、接口调用时机

调用时机:

  • 每次页面 onShow 时调用(实时获取最新配置)

接口: GET /api/popups/carousel/configs

请求参数:

{
  platform: 'mp-weixin' | 'mp-xhs',
  userId?: string,
}

返回数据:

{
  code: 0,
  data: {
    containers: CarouselContainerConfig[]
  }
}

五、配置示例

// 后台返回的轮播配置示例

{
  containers: [
    {
      pages: ['/pages-custom/pages/index/index'],
      enabled: true,
      interval: 86400, // 1天展示1次
      popups: [
        {
          type: 'coupon',
          enabled: true,
          imageUrl: 'https://cdn.example.com/spring.jpg',
          linkUrl: '/pages-other/pages/activity/activity?id=123',
          data: {
            title: '优惠卷福利',
          },
        },
        {
          type: 'points',
          enabled: true,
          imageUrl: 'https://cdn.example.com/new-product.jpg',
          linkUrl: '/pages-custom/pages/product/detail?id=456',
          data: {
            title: '积分福利',
          },
        },
        {
          type: 'announcement',
          enabled: true,
          imageUrl: 'https://cdn.example.com/member.jpg',
          linkUrl: '/pages-other/pages/member/member',
          data: {
            title: '公告',
          },
        },
      ],
    },
  ];
}

六、使用示例

// 页面中使用
import { carouselPopupManager } from 'carouselPopupManager';
import { getCarouselPopupConfigs } from '@/services/popup';

onShow(async () => {
  // 1. 获取当前页面路径
  const pages = getCurrentPages();
  const currentPage = '/' + pages[pages.length - 1].route;

  // 2. 每次都从接口获取最新配置
  const configs = await getCarouselPopupConfigs({
    platform: 'mp-weixin',
    userId: userStore.userId,
  });

  // 3. 注册配置
  carouselPopupManager.register(configs);

  // 4. 触发轮播弹窗检查
  carouselPopupManager.trigger(currentPage);
});

// 手动清除展示记录
carouselPopupManager.clearRecords('home_carousel_2024');