npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vue-dialog-queue

v0.1.3

Published

Vue 3 dialog queue and scheduling system with scoped containers, priority, frequency control, and server push support.

Readme

vue-dialog-queue

Vue 3 弹窗队列调度器。它把本地弹窗、服务端推送弹窗、页面作用域、优先级、去重、频控和重复展示控制统一到一套 API 里。

特性

  • 支持 Vue 3 异步弹窗组件。
  • 同一时间只展示一个弹窗,关闭后自动消费下一个。
  • 支持全局弹窗和页面局部弹窗。
  • 支持 scope 页面作用域,弹窗只在匹配容器中渲染。
  • 支持服务端单条/批量推送弹窗。
  • 默认按 type + key 去重,避免重复推送造成连续弹窗。
  • 支持 append,允许同一个 type + key 多次排队。
  • 支持 weightorder 排序。
  • 支持高优先级弹窗打断低优先级弹窗。
  • 支持每日展示限制、最大展示次数、重复展示间隔。
  • 支持按用户/租户/会话隔离频控记录。
  • 支持 TypeScript 类型推导。
  • vue 是 peer dependency,不会被打进包里。

安装

npm install vue-dialog-queue
pnpm add vue-dialog-queue

核心概念

type

type 是弹窗类型,也是你触发弹窗时使用的名字。每个注册项的 type 必须唯一。

{
  type: "WELCOME",
  component: () => import("./WelcomeDialog.vue"),
}

data

data 是唯一会传给弹窗组件的参数。调度器不会读取、修改或解析它。

dialogApi.show("WELCOME", {
  title: "欢迎回来",
  source: "home",
})

弹窗组件中通过 openDialog(data) 接收。

meta

meta 只控制调度行为,不会传给弹窗组件。

dialogApi.push({
  type: "ACTIVITY",
  data: activity,
  meta: {
    key: activity.id,
    order: 1,
    weight: 5,
    maxShowCount: 1,
  },
})

scope

scope 是容器作用域。它不是自动路由识别字段,只是一个字符串。

弹窗注册时的 control.scope 必须和 DialogContainerscope 一致,弹窗才会在这个容器中渲染。

{
  type: "HOME_POPUP",
  component: () => import("./HomePopup.vue"),
  control: {
    scope: "home",
  },
}
<DialogContainer scope="home" />

没有设置 scope 的弹窗会使用默认作用域 default,由 <DialogContainer /> 承载。

快速开始

创建弹窗系统:

// dialog.ts
import { defineDialogSystem } from "vue-dialog-queue"

export const dialogSystem = defineDialogSystem([
  {
    type: "WELCOME",
    component: () => import("./dialogs/WelcomeDialog.vue"),
    weight: 10,
    control: ["daily"],
  },
  {
    type: "RECHARGE",
    component: () => import("./dialogs/RechargeDialog.vue"),
    weight: 1,
    control: ["high"],
  },
])

export const DialogContainer = dialogSystem.Container
export const dialogApi = dialogSystem.api

挂载全局容器:

<!-- App.vue -->
<template>
  <DialogContainer />
  <RouterView />
</template>

<script setup lang="ts">
import { DialogContainer } from "./dialog"
</script>

编写弹窗组件:

<!-- WelcomeDialog.vue -->
<script setup lang="ts">
import { ref } from "vue"

interface WelcomeData {
  title?: string
}

const visible = ref(false)
const payload = ref<WelcomeData | undefined>()

function openDialog(data?: WelcomeData) {
  payload.value = data
  visible.value = true
}

function closeDialog() {
  visible.value = false
}

defineExpose({
  openDialog,
  closeDialog,
})
</script>

<template>
  <div v-if="visible">
    <h3>{{ payload?.title ?? "欢迎" }}</h3>
    <button @click="closeDialog">关闭</button>
  </div>
</template>

触发弹窗:

dialogApi.show("WELCOME", { title: "欢迎回来" })

全局弹窗和局部弹窗

全局弹窗

不写 control.scope 的弹窗默认属于 default 作用域。

{
  type: "GLOBAL_NOTICE",
  component: () => import("./GlobalNotice.vue"),
}

它会渲染到默认容器:

<DialogContainer />

局部页面弹窗

注册时设置 scope

{
  type: "HOME_POPUP",
  component: () => import("./HomePopup.vue"),
  control: {
    scope: "home",
  },
}

在对应页面挂同名容器:

<!-- Home.vue -->
<template>
  <DialogContainer scope="home" />
</template>

这样 HOME_POPUP 只会在 scope="home" 的容器里渲染,不会出现在全局 <DialogContainer /> 里。

App.vue 或 Layout 中常驻局部容器

如果你把局部容器放在 App.vue 或布局组件里,它会一直挂载。默认 autoActive=true,这个 scope 会一直 active,页面限制会失效。

这种场景要关闭自动激活,并根据路由手动同步:

<DialogContainer scope="home" :auto-active="false" />
import { watch } from "vue"
import { useRoute } from "vue-router"
import { dialogSystem } from "./dialog"

const route = useRoute()

watch(
  () => route.name,
  name => {
    dialogSystem.api.setScopeActive(name === "Home", "home")
  },
  { immediate: true }
)

也可以用 path:

watch(
  () => route.path,
  path => {
    dialogSystem.api.setScopeActive(path === "/home", "home")
  },
  { immediate: true }
)

scope 可以是 path 或 name 吗?

可以,但库不会自动读取路由。scope 只是字符串。

control: { scope: "home" }
control: { scope: "Home" }
control: { scope: "/home" }

只要它和容器的 scope 完全一致即可。

outsideallowOutside

如果你设置了:

control: ["outside"]

或:

control: {
  scope: "home",
  allowOutside: true,
}

这个弹窗即使当前 scope 不活跃,也允许展示。需要严格限制页面时不要开启它。

本地触发

立即尝试展示。如果当前不能展示,会根据 scope、优先级和队列状态进入队列或 pending。

dialogApi.show("WELCOME")

传入组件参数:

dialogApi.show("RECHARGE", {
  amount: 100,
  currency: "CNY",
})

传入调度参数:

dialogApi.show(
  "RECHARGE",
  { amount: 100 },
  {
    key: "activity-1001",
    weight: 1,
    maxShowCount: 1,
  }
)

show 返回 boolean

  • true:本次已经展示。
  • false:本次未立即展示,可能被忽略、进入队列或等待容器 ready。

服务端推送

单条推送

dialogApi.push({
  type: "ACTIVITY",
  data: {
    id: 1001,
    title: "活动弹窗",
  },
  meta: {
    key: 1001,
  },
})

批量推送

dialogApi.push([
  {
    type: "ACTIVITY",
    data: { id: 1001, title: "活动 1" },
    meta: { key: 1001, order: 1 },
  },
  {
    type: "COUPON",
    data: { id: 2001, title: "优惠券" },
    meta: { key: 2001, order: 2 },
  },
])

批量推送会批处理排序,避免每条都重新排序。

同时推送多个相同弹窗

默认按 type + key 去重。

dialogApi.push([
  { type: "ACTIVITY", data: { id: 1, title: "旧数据" }, meta: { key: 1 } },
  { type: "ACTIVITY", data: { id: 1, title: "新数据" }, meta: { key: 1 } },
])

最终只保留一个 ACTIVITY + key=1,后来的数据会更新前面的队列项。

不同 key 会作为不同队列项:

dialogApi.push([
  { type: "ACTIVITY", data: { id: 1 }, meta: { key: 1 } },
  { type: "ACTIVITY", data: { id: 2 }, meta: { key: 2 } },
])

这两条会分别排队。

如果没有传 key,同 type 会使用默认 key,所以会被视为同一个弹窗:

dialogApi.push([
  { type: "ACTIVITY", data: { id: 1 } },
  { type: "ACTIVITY", data: { id: 2 } },
])

这种场景只会保留一个。服务端推送多个业务弹窗时,建议一定传 meta.key

允许相同弹窗重复排队

开启 append 后,同 type + key 也可以重复进入队列。

{
  type: "ACTIVITY",
  component: () => import("./ActivityDialog.vue"),
  control: {
    append: true,
  },
}
dialogApi.push([
  { type: "ACTIVITY", data: { id: 1 }, meta: { key: 1 } },
  { type: "ACTIVITY", data: { id: 1 }, meta: { key: 1 } },
])

这两条都会排队展示。

异步来源 source

如果弹窗来自接口拉取,可以在创建系统时配置 source

export const dialogSystem = defineDialogSystem({
  dialogs,
  source: async () => {
    const list = await apiGetPopupList()

    return list.map((item, index) => ({
      type: item.type,
      data: item.payload,
      meta: {
        key: item.id,
        order: index,
        maxShowCount: item.maxShowCount,
        weight: item.weight,
      },
    }))
  },
})

触发拉取:

await dialogApi.fetch()

WebSocket 收到通知后也可以复用:

socket.on("popup-change", () => {
  dialogApi.fetch()
})

排序规则

排序主要由三个因素决定:

  1. 高优先级弹窗优先于普通弹窗。
  2. weight 越小越先展示。
  3. weight 相同时,order 越小越先展示。

注册默认权重:

{
  type: "RECHARGE",
  component: () => import("./RechargeDialog.vue"),
  weight: 1,
}

单次触发覆盖权重:

dialogApi.push({
  type: "RECHARGE",
  data,
  meta: {
    weight: 0,
    order: 1,
  },
})

高优先级弹窗

高优先级弹窗可以打断低优先级弹窗。

{
  type: "URGENT_NOTICE",
  component: () => import("./UrgentNotice.vue"),
  weight: 0,
  control: {
    high: true,
  },
}

如果当前正在展示普通弹窗,优先级更高的高优先级弹窗会关闭当前弹窗,当前弹窗会重新回到队列中,之后继续展示。

去重、频控和重复展示

去重 key

keyiddialogId 是别名。

meta: { key: 1001 }
meta: { id: 1001 }
meta: { dialogId: 1001 }

它们都会解析为同一个调度 key。

每日限制

注册时开启每日限制:

{
  type: "WELCOME",
  component: () => import("./WelcomeDialog.vue"),
  control: ["daily"],
}

同一个 type + key + userScope 当天最多展示一次。

单次最大展示次数

服务端可以通过 maxShowCountcount 控制当天最大展示次数。

dialogApi.push({
  type: "ACTIVITY",
  data,
  meta: {
    key: data.id,
    maxShowCount: 3,
  },
})

重复展示间隔

{
  type: "ACTIVITY",
  component: () => import("./ActivityDialog.vue"),
  control: {
    repeatMs: 6 * 60 * 60 * 1000,
  },
}

同一个 type + key 两次展示之间至少间隔 6 小时。

库内部有最小保护值,默认 5 秒,可以通过 minRepeatGuardMs 调整。

用户隔离

默认频控按 guest 存储。如果你的应用有用户 id,建议配置 getUserScope

export const dialogSystem = defineDialogSystem(dialogs, {
  getUserScope: () => userStore.userId,
})

这样不同用户之间不会共用展示记录。

control 配置

简单场景可以使用字符串标记:

control: ["daily", "high", "append", "outside", "retain"]

对象写法更清晰,也支持更多配置:

control: {
  enabled: true,
  daily: true,
  high: true,
  append: false,
  repeatMs: 6 * 60 * 60 * 1000,
  scope: "home",
  allowOutside: false,
  retainOutside: true,
}

| 字段 | 说明 | | --- | --- | | enabled | 设为 false 时保留注册,但所有触发都会被忽略。 | | daily | 当天同 type + key 最多展示一次。 | | high | 是否为高优先级弹窗。 | | append | 是否允许同 type + key 重复排队。 | | repeatMs | 同 type + key 再次展示前的最小间隔。 | | scope | 渲染该弹窗的容器作用域。 | | allowOutside | scope 不活跃时是否仍允许展示。 | | retainOutside | scope 不活跃时是否保留队列和 pending 项。 |

API

api.show(type, data?, meta?, order?, count?)

立即尝试展示弹窗。

const shown = dialogApi.show("WELCOME", data, { key: "welcome" })

返回 true 表示已经展示,返回 false 表示没有立即展示。

api.open(...) / api.forceShow(...)

show 的别名。

dialogApi.open("WELCOME")
dialogApi.forceShow("WELCOME")

api.push(input)

推入一个或多个服务端弹窗项。

dialogApi.push({ type: "WELCOME", data, meta: { key: "welcome" } })
dialogApi.push([{ type: "A" }, { type: "B" }])

api.enqueue(type, data?, meta?, order?, count?)

加入队列,不强调立即展示。

dialogApi.enqueue("WELCOME", data, { key: "welcome" })

api.add(...)

enqueue 的别名。

api.fetch()

调用创建系统时配置的 source,并把返回结果推入队列。

await dialogApi.fetch()

api.showNext()

手动消费队列中的下一个可展示弹窗。

一般不需要业务手动调用。

api.showWhenReady(type, data?, options?)

等待弹窗组件 ref 可用并展示成功。

const shown = await dialogApi.showWhenReady("WELCOME", data, {
  timeout: 5000,
  meta: { key: "welcome" },
})

返回 true 表示在超时前展示成功,false 表示超时。

等待粒度是 type + key,同 type 不同 key 不会互相 resolve。

api.closeCurrent()

关闭当前弹窗,并在关闭延迟后继续展示下一个。

dialogApi.closeCurrent()

api.closeAll()

关闭所有已注册弹窗组件,并清理当前展示状态。

dialogApi.closeAll()

api.pause() / api.resume()

暂停或恢复队列消费。

dialogApi.pause()
dialogApi.resume()

暂停不会关闭当前弹窗,只是不继续展示下一个。

api.markShown(type, key?)

手动标记某个 type + key 已展示,用于频控。

dialogApi.markShown("WELCOME", "welcome")

api.reset()

关闭弹窗、清空队列、清空 pending、清空等待回调,并执行 onReset

dialogApi.reset()

api.setScopeActive(value, scope?)

手动设置某个 scope 是否 active。

dialogApi.setScopeActive(true, "home")
dialogApi.setScopeActive(false, "home")

setActive 是它的别名。

api.handleExternalSignal()

外部信号入口,内部会调用 fetch()

socket.on("popup-change", () => {
  dialogApi.handleExternalSignal()
})

api.getDialogId(data?, meta?)

解析调度 key,调试或适配时使用。

api.getDialogCount(data?, meta?)

解析最大展示次数。

api.getDialogOrder(data?, meta?)

解析队列顺序。

api.state

响应式状态,适合调试或做自定义状态面板。

dialogApi.state.queue
dialogApi.state.currentDialog
dialogApi.state.currentDialogData
dialogApi.state.currentDialogMeta
dialogApi.state.isProcessing
dialogApi.state.activeScopes
dialogApi.state.pendingDialogs
dialogApi.state.refs

TypeScript 用法

限制弹窗 type

type DialogType = "WELCOME" | "RECHARGE" | "ACTIVITY"

export const dialogSystem = defineDialogSystem<DialogType>([
  {
    type: "WELCOME",
    component: () => import("./WelcomeDialog.vue"),
  },
  {
    type: "RECHARGE",
    component: () => import("./RechargeDialog.vue"),
  },
])

dialogSystem.api.show("WELCOME")
// dialogSystem.api.show("UNKNOWN") // TS 会报错

统一 data 类型

type DialogType = "WELCOME" | "RECHARGE"

interface DialogData {
  id?: string | number
  title?: string
  payload?: unknown
}

export const dialogSystem = defineDialogSystem<DialogType, DialogData>([
  {
    type: "WELCOME",
    component: () => import("./WelcomeDialog.vue"),
  },
])

dialogSystem.api.show("WELCOME", {
  title: "欢迎",
})

常用导出类型

import type {
  DialogDefinition,
  DialogMeta,
  DialogPushItem,
  DialogQueueItem,
  DialogSystemApi,
  DialogSystemInstance,
  DialogSystemState,
  DialogControlOptions,
  DialogWaitOptions,
} from "vue-dialog-queue"

高级入口 createDialogSystem

普通项目优先使用 defineDialogSystem。如果你在做适配层、迁移旧系统或需要底层配置,可以使用 createDialogSystem

import { createDialogSystem } from "vue-dialog-queue"

const dialogSystem = createDialogSystem({
  configs: [
    {
      type: "WELCOME",
      component: () => import("./WelcomeDialog.vue"),
      priority: 10,
      mountScope: "default",
      hasDailyLimit: true,
      queueStrategy: "dedupe",
    },
  ],
  defaultMountScope: "default",
  storagePrefix: "my-app:dialog:",
  getUserScope: () => userStore.userId,
})

自定义 data 解析

默认情况下,调度器不会读取 data。如果你希望从业务数据里自动解析 key、次数或顺序,可以配置适配函数。

export const dialogSystem = defineDialogSystem(dialogs, {
  getDialogId: data => {
    return typeof data === "object" && data && "id" in data
      ? String((data as { id: string | number }).id)
      : undefined
  },
  getDialogCount: data => {
    return typeof data === "object" && data && "maxShowCount" in data
      ? Number((data as { maxShowCount: number }).maxShowCount)
      : undefined
  },
  getDialogOrder: data => {
    return typeof data === "object" && data && "order" in data
      ? Number((data as { order: number }).order)
      : undefined
  },
})

之后可以少写 meta

dialogApi.push({
  type: "ACTIVITY",
  data: {
    id: 1001,
    order: 1,
    maxShowCount: 1,
  },
})

自定义存储

频控默认使用 localStorage,失败时降级到内存存储。也可以传入自定义存储。

const memory = new Map<string, string>()

defineDialogSystem(dialogs, {
  storage: {
    get: key => memory.get(key) ?? null,
    set: (key, value) => {
      memory.set(key, value)
    },
    remove: key => {
      memory.delete(key)
    },
  },
})

常见问题

设置了 scope: "home",为什么其它页面也弹?

检查 DialogContainer scope="home" 是否放在 App.vue 或常驻布局里。只要这个容器一直挂载,home 就一直 active。

解决方式:

  • <DialogContainer scope="home" /> 放到 Home 页面组件里。
  • 或者设置 :auto-active="false",自己根据路由调用 setScopeActive

全局容器和局部容器会互相影响吗?

不会。默认全局容器是 scope="default",局部容器比如 scope="home"。注册了 control.scope="home" 的弹窗只会渲染到 home 容器。

服务端推了多个相同 type,只弹了一个?

默认按 type + key 去重。如果没有传 key,同 type 会被视为同一个默认 key。服务端多条业务弹窗请传 meta.key

想让同一个弹窗重复排队怎么办?

开启 append

control: { append: true }

弹窗组件必须实现 closeDialog 吗?

openDialog 必须实现。closeDialog 可选,但强烈建议实现。这样 api.closeCurrent()、高优先级打断、scope 失活、closeAll() 都能真正关闭组件。

能不能在弹窗组件里关闭后自动展示下一个?

可以在组件关闭按钮里调用:

dialogApi.closeCurrent()

如果组件只把自己的 visible 设为 false,调度器不知道它已经关闭,不会自动展示下一个。

为什么 unknown type 没反应?

未注册的 type 会被忽略,避免拼错的弹窗永久进入 pending 队列。

发布包体积

当前包不发布 sourcemap。

  • tarball 约 24 KB。
  • 解压约 116 KB。
  • ESM gzip 约 9 KB。

License

MIT